X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=osm_ro%2Fvimconn_openstack.py;h=518385875f0f66894ce43495b0d9910c569dfdb9;hb=8e3ce87077f8f5a6edc75733ab552780a702fc1d;hp=b280da89cc1276787af797dcd2d0a4dab3117224;hpb=721d79b1f7efe56ee3ff72f9884ae7f3db671c89;p=osm%2FRO.git diff --git a/osm_ro/vimconn_openstack.py b/osm_ro/vimconn_openstack.py index b280da89..51838587 100644 --- a/osm_ro/vimconn_openstack.py +++ b/osm_ro/vimconn_openstack.py @@ -22,20 +22,28 @@ ## ''' -osconnector implements all the methods to interact with openstack using the python-client. +osconnector implements all the methods to interact with openstack using the python-neutronclient. + +For the VNF forwarding graph, The OpenStack VIM connector calls the +networking-sfc Neutron extension methods, whose resources are mapped +to the VIM connector's SFC resources as follows: +- Classification (OSM) -> Flow Classifier (Neutron) +- Service Function Instance (OSM) -> Port Pair (Neutron) +- Service Function (OSM) -> Port Pair Group (Neutron) +- Service Function Path (OSM) -> Port Chain (Neutron) ''' -__author__="Alfonso Tierno, Gerardo Garcia, Pablo Montes, xFlow Research" -__date__ ="$22-jun-2014 11:19:29$" +__author__ = "Alfonso Tierno, Gerardo Garcia, Pablo Montes, xFlow Research, Igor D.C." +__date__ = "$22-sep-2017 23:59:59$" import vimconn import json -import yaml import logging import netaddr import time import yaml import random import re +import copy from novaclient import client as nClient, exceptions as nvExceptions from keystoneauth1.identity import v2, v3 @@ -47,12 +55,14 @@ from glanceclient import client as glClient import glanceclient.client as gl1Client import glanceclient.exc as gl1Exceptions from cinderclient import client as cClient -from httplib import HTTPException +#from httplib import HTTPException +from http.client import HTTPException from neutronclient.neutron import client as neClient from neutronclient.common import exceptions as neExceptions from requests.exceptions import ConnectionError -'''contain the openstack virtual machine status to openmano status''' + +"""contain the openstack virtual machine status to openmano status""" vmStatus2manoFormat={'ACTIVE':'ACTIVE', 'PAUSED':'PAUSED', 'SUSPENDED': 'SUSPENDED', @@ -63,9 +73,11 @@ vmStatus2manoFormat={'ACTIVE':'ACTIVE', netStatus2manoFormat={'ACTIVE':'ACTIVE','PAUSED':'PAUSED','INACTIVE':'INACTIVE','BUILD':'BUILD','ERROR':'ERROR','DELETED':'DELETED' } +supportedClassificationTypes = ['legacy_flow_classifier'] + #global var to have a timeout creating and deleting volumes volume_timeout = 60 -server_timeout = 60 +server_timeout = 300 class vimconnector(vimconn.vimconnector): def __init__(self, uuid, name, tenant_id, tenant_name, url, url_admin=None, user=None, passwd=None, @@ -92,8 +104,9 @@ class vimconnector(vimconn.vimconnector): self.insecure = self.config.get("insecure", False) if not url: - raise TypeError, 'url param can not be NoneType' + raise TypeError('url param can not be NoneType') self.persistent_info = persistent_info + self.availability_zone = persistent_info.get('availability_zone', None) self.session = persistent_info.get('session', {'reload_client': True}) self.nova = self.session.get('nova') self.neutron = self.session.get('neutron') @@ -117,7 +130,7 @@ class vimconnector(vimconn.vimconnector): self.logger = logging.getLogger('openmano.vim.vio') if log_level: - self.logger.setLevel(getattr(logging, log_level)) + self.logger.setLevel( getattr(logging, log_level)) def __getitem__(self, index): """Get individuals parameters. @@ -144,21 +157,31 @@ class vimconnector(vimconn.vimconnector): '''Called before any operation, it check if credentials has changed Throw keystoneclient.apiclient.exceptions.AuthorizationFailure ''' - #TODO control the timing and possible token timeout, but it seams that python client does this task for us :-) + #TODO control the timing and possible token timeout, but it seams that python client does this task for us :-) if self.session['reload_client']: if self.config.get('APIversion'): self.api_version3 = self.config['APIversion'] == 'v3.3' or self.config['APIversion'] == '3' else: # get from ending auth_url that end with v3 or with v2.0 - self.api_version3 = self.url.split("/")[-1] == "v3" + self.api_version3 = self.url.endswith("/v3") or self.url.endswith("/v3/") self.session['api_version3'] = self.api_version3 if self.api_version3: + if self.config.get('project_domain_id') or self.config.get('project_domain_name'): + project_domain_id_default = None + else: + project_domain_id_default = 'default' + if self.config.get('user_domain_id') or self.config.get('user_domain_name'): + user_domain_id_default = None + else: + user_domain_id_default = 'default' auth = v3.Password(auth_url=self.url, username=self.user, password=self.passwd, project_name=self.tenant_name, project_id=self.tenant_id, - project_domain_id=self.config.get('project_domain_id', 'default'), - user_domain_id=self.config.get('user_domain_id', 'default')) + project_domain_id=self.config.get('project_domain_id', project_domain_id_default), + user_domain_id=self.config.get('user_domain_id', user_domain_id_default), + project_domain_name=self.config.get('project_domain_name'), + user_domain_name=self.config.get('user_domain_name')) else: auth = v2.Password(auth_url=self.url, username=self.user, @@ -171,7 +194,16 @@ class vimconnector(vimconn.vimconnector): else: self.keystone = ksClient_v2.Client(session=sess, endpoint_type=self.endpoint_type) self.session['keystone'] = self.keystone - self.nova = self.session['nova'] = nClient.Client("2.1", session=sess, endpoint_type=self.endpoint_type) + # In order to enable microversion functionality an explicit microversion must be specified in 'config'. + # This implementation approach is due to the warning message in + # https://developer.openstack.org/api-guide/compute/microversions.html + # where it is stated that microversion backwards compatibility is not guaranteed and clients should + # always require an specific microversion. + # To be able to use 'device role tagging' functionality define 'microversion: 2.32' in datacenter config + version = self.config.get("microversion") + if not version: + version = "2.1" + self.nova = self.session['nova'] = nClient.Client(str(version), session=sess, endpoint_type=self.endpoint_type) self.neutron = self.session['neutron'] = neClient.Client('2.0', session=sess, endpoint_type=self.endpoint_type) self.cinder = self.session['cinder'] = cClient.Client(2, session=sess, endpoint_type=self.endpoint_type) if self.endpoint_type == "internalURL": @@ -185,6 +217,9 @@ class vimconnector(vimconn.vimconnector): endpoint=glance_endpoint) self.session['reload_client'] = False self.persistent_info['session'] = self.session + # add availablity zone info inside self.persistent_info + self._set_availablity_zones() + self.persistent_info['availability_zone'] = self.availability_zone def __net_os2mano(self, net_list_dict): '''Transform the net openstack format to mano format @@ -201,20 +236,127 @@ class vimconnector(vimconn.vimconnector): else: net['type']='bridge' + def __classification_os2mano(self, class_list_dict): + """Transform the openstack format (Flow Classifier) to mano format + (Classification) class_list_dict can be a list of dict or a single dict + """ + if isinstance(class_list_dict, dict): + class_list_ = [class_list_dict] + elif isinstance(class_list_dict, list): + class_list_ = class_list_dict + else: + raise TypeError( + "param class_list_dict must be a list or a dictionary") + for classification in class_list_: + id = classification.pop('id') + name = classification.pop('name') + description = classification.pop('description') + project_id = classification.pop('project_id') + tenant_id = classification.pop('tenant_id') + original_classification = copy.deepcopy(classification) + classification.clear() + classification['ctype'] = 'legacy_flow_classifier' + classification['definition'] = original_classification + classification['id'] = id + classification['name'] = name + classification['description'] = description + classification['project_id'] = project_id + classification['tenant_id'] = tenant_id + + def __sfi_os2mano(self, sfi_list_dict): + """Transform the openstack format (Port Pair) to mano format (SFI) + sfi_list_dict can be a list of dict or a single dict + """ + if isinstance(sfi_list_dict, dict): + sfi_list_ = [sfi_list_dict] + elif isinstance(sfi_list_dict, list): + sfi_list_ = sfi_list_dict + else: + raise TypeError( + "param sfi_list_dict must be a list or a dictionary") + for sfi in sfi_list_: + sfi['ingress_ports'] = [] + sfi['egress_ports'] = [] + if sfi.get('ingress'): + sfi['ingress_ports'].append(sfi['ingress']) + if sfi.get('egress'): + sfi['egress_ports'].append(sfi['egress']) + del sfi['ingress'] + del sfi['egress'] + params = sfi.get('service_function_parameters') + sfc_encap = False + if params: + correlation = params.get('correlation') + if correlation: + sfc_encap = True + sfi['sfc_encap'] = sfc_encap + del sfi['service_function_parameters'] + + def __sf_os2mano(self, sf_list_dict): + """Transform the openstack format (Port Pair Group) to mano format (SF) + sf_list_dict can be a list of dict or a single dict + """ + if isinstance(sf_list_dict, dict): + sf_list_ = [sf_list_dict] + elif isinstance(sf_list_dict, list): + sf_list_ = sf_list_dict + else: + raise TypeError( + "param sf_list_dict must be a list or a dictionary") + for sf in sf_list_: + del sf['port_pair_group_parameters'] + sf['sfis'] = sf['port_pairs'] + del sf['port_pairs'] + + def __sfp_os2mano(self, sfp_list_dict): + """Transform the openstack format (Port Chain) to mano format (SFP) + sfp_list_dict can be a list of dict or a single dict + """ + if isinstance(sfp_list_dict, dict): + sfp_list_ = [sfp_list_dict] + elif isinstance(sfp_list_dict, list): + sfp_list_ = sfp_list_dict + else: + raise TypeError( + "param sfp_list_dict must be a list or a dictionary") + for sfp in sfp_list_: + params = sfp.pop('chain_parameters') + sfc_encap = False + if params: + correlation = params.get('correlation') + if correlation: + sfc_encap = True + sfp['sfc_encap'] = sfc_encap + sfp['spi'] = sfp.pop('chain_id') + sfp['classifications'] = sfp.pop('flow_classifiers') + sfp['service_functions'] = sfp.pop('port_pair_groups') + + # placeholder for now; read TODO note below + def _validate_classification(self, type, definition): + # only legacy_flow_classifier Type is supported at this point + return True + # TODO(igordcard): this method should be an abstract method of an + # abstract Classification class to be implemented by the specific + # Types. Also, abstract vimconnector should call the validation + # method before the implemented VIM connectors are called. + def _format_exception(self, exception): '''Transform a keystone, nova, neutron exception into a vimconn exception''' if isinstance(exception, (HTTPException, gl1Exceptions.HTTPException, gl1Exceptions.CommunicationError, ConnectionError, ksExceptions.ConnectionError, neExceptions.ConnectionFailed )): - raise vimconn.vimconnConnectionException(type(exception).__name__ + ": " + str(exception)) - elif isinstance(exception, (nvExceptions.ClientException, ksExceptions.ClientException, + raise vimconn.vimconnConnectionException(type(exception).__name__ + ": " + str(exception)) + elif isinstance(exception, (nvExceptions.ClientException, ksExceptions.ClientException, neExceptions.NeutronException, nvExceptions.BadRequest)): raise vimconn.vimconnUnexpectedResponse(type(exception).__name__ + ": " + str(exception)) elif isinstance(exception, (neExceptions.NetworkNotFoundClient, nvExceptions.NotFound)): raise vimconn.vimconnNotFoundException(type(exception).__name__ + ": " + str(exception)) elif isinstance(exception, nvExceptions.Conflict): raise vimconn.vimconnConflictException(type(exception).__name__ + ": " + str(exception)) + elif isinstance(exception, vimconn.vimconnException): + raise else: # () + self.logger.error("General Exception " + str(exception), exc_info=True) raise vimconn.vimconnConnectionException(type(exception).__name__ + ": " + str(exception)) def get_tenant_list(self, filter_dict={}): @@ -306,21 +448,21 @@ class vimconnector(vimconn.vimconnector): #Fake subnet is required subnet_rand = random.randint(0, 255) ip_profile['subnet_address'] = "192.168.{}.0/24".format(subnet_rand) - if 'ip_version' not in ip_profile: + if 'ip_version' not in ip_profile: ip_profile['ip_version'] = "IPv4" - subnet={"name":net_name+"-subnet", + subnet = {"name":net_name+"-subnet", "network_id": new_net["network"]["id"], "ip_version": 4 if ip_profile['ip_version']=="IPv4" else 6, "cidr": ip_profile['subnet_address'] } - if 'gateway_address' in ip_profile: - subnet['gateway_ip'] = ip_profile['gateway_address'] + # Gateway should be set to None if not needed. Otherwise openstack assigns one by default + subnet['gateway_ip'] = ip_profile.get('gateway_address') if ip_profile.get('dns_address'): subnet['dns_nameservers'] = ip_profile['dns_address'].split(";") if 'dhcp_enabled' in ip_profile: subnet['enable_dhcp'] = False if ip_profile['dhcp_enabled']=="false" else True if 'dhcp_start_address' in ip_profile: - subnet['allocation_pools']=[] + subnet['allocation_pools'] = [] subnet['allocation_pools'].append(dict()) subnet['allocation_pools'][0]['start'] = ip_profile['dhcp_start_address'] if 'dhcp_count' in ip_profile: @@ -410,16 +552,16 @@ class vimconnector(vimconn.vimconnector): net_id: #VIM id of this network status: #Mandatory. Text with one of: # DELETED (not found at vim) - # VIM_ERROR (Cannot connect to VIM, VIM response error, ...) + # VIM_ERROR (Cannot connect to VIM, VIM response error, ...) # OTHER (Vim reported other status not understood) # ERROR (VIM indicates an ERROR status) - # ACTIVE, INACTIVE, DOWN (admin down), + # ACTIVE, INACTIVE, DOWN (admin down), # BUILD (on building process) # - error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR + error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR vim_info: #Text with plain information obtained from vim (yaml.safe_dump) - ''' + ''' net_dict={} for net_id in net_list: net = {} @@ -430,7 +572,7 @@ class vimconnector(vimconn.vimconnector): else: net["status"] = "OTHER" net["error_msg"] = "VIM status reported " + net_vim['status'] - + if net['status'] == "ACTIVE" and not net_vim['admin_state_up']: net['status'] = 'DOWN' try: @@ -501,7 +643,6 @@ class vimconnector(vimconn.vimconnector): except (nvExceptions.NotFound, nvExceptions.ClientException, ksExceptions.ClientException, ConnectionError) as e: self._format_exception(e) - def new_flavor(self, flavor_data, change_name_if_used=True): '''Adds a tenant flavor to openstack VIM if change_name_if_used is True, it will change name in case of conflict, because it is not supported name repetition @@ -608,7 +749,6 @@ class vimconnector(vimconn.vimconnector): metadata: metadata of the image Returns the image_id ''' - # ALF TODO: revise and change for the new method or session retry=0 max_retries=3 while retry= server_timeout: + raise vimconn.vimconnException('Timeout waiting for instance ' + vm_id + ' to get ' + status, + http_code=vimconn.HTTP_Request_Timeout) + + def _get_openstack_availablity_zones(self): + """ + Get from openstack availability zones available + :return: + """ + try: + openstack_availability_zone = self.nova.availability_zones.list() + openstack_availability_zone = [str(zone.zoneName) for zone in openstack_availability_zone + if zone.zoneName != 'internal'] + return openstack_availability_zone + except Exception as e: + return None + + def _set_availablity_zones(self): + """ + Set vim availablity zone + :return: + """ + + if 'availability_zone' in self.config: + vim_availability_zones = self.config.get('availability_zone') + if isinstance(vim_availability_zones, str): + self.availability_zone = [vim_availability_zones] + elif isinstance(vim_availability_zones, list): + self.availability_zone = vim_availability_zones + else: + self.availability_zone = self._get_openstack_availablity_zones() + + def _get_vm_availability_zone(self, availability_zone_index, availability_zone_list): + """ + Return thge availability zone to be used by the created VM. + :return: The VIM availability zone to be used or None + """ + if availability_zone_index is None: + if not self.config.get('availability_zone'): + return None + elif isinstance(self.config.get('availability_zone'), str): + return self.config['availability_zone'] + else: + # TODO consider using a different parameter at config for default AV and AV list match + return self.config['availability_zone'][0] + + vim_availability_zones = self.availability_zone + # check if VIM offer enough availability zones describe in the VNFD + if vim_availability_zones and len(availability_zone_list) <= len(vim_availability_zones): + # check if all the names of NFV AV match VIM AV names + match_by_index = False + for av in availability_zone_list: + if av not in vim_availability_zones: + match_by_index = True + break + if match_by_index: + return vim_availability_zones[availability_zone_index] + else: + return availability_zone_list[availability_zone_index] + else: + raise vimconn.vimconnConflictException("No enough availability zones at VIM for this deployment") - def new_vminstance(self,name,description,start,image_id,flavor_id,net_list,cloud_config=None,disk_list=None): + def new_vminstance(self, name, description, start, image_id, flavor_id, net_list, cloud_config=None, disk_list=None, + availability_zone_index=None, availability_zone_list=None): '''Adds a VM instance to VIM Params: start: indicates if VM must start or boot in pause mode. Ignored @@ -731,25 +951,48 @@ class vimconnector(vimconn.vimconnector): type: 'virtual', 'PF', 'VF', 'VFnotShared' vim_id: filled/added by this function floating_ip: True/False (or it can be None) + 'cloud_config': (optional) dictionary with: + 'key-pairs': (optional) list of strings with the public key to be inserted to the default user + 'users': (optional) list of users to be inserted, each item is a dict with: + 'name': (mandatory) user name, + 'key-pairs': (optional) list of strings with the public key to be inserted to the user + 'user-data': (optional) string is a text script to be passed directly to cloud-init + 'config-files': (optional). List of files to be transferred. Each item is a dict with: + 'dest': (mandatory) string with the destination absolute path + 'encoding': (optional, by default text). Can be one of: + 'b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64' + 'content' (mandatory): string with the content of the file + 'permissions': (optional) string with file permissions, typically octal notation '0644' + 'owner': (optional) file owner, string with the format 'owner:group' + 'boot-data-drive': boolean to indicate if user-data must be passed using a boot drive (hard disk) + 'disk_list': (optional) list with additional disks to the VM. Each item is a dict with: + 'image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted + 'size': (mandatory) string with the size of the disk in GB + availability_zone_index: Index of availability_zone_list to use for this this VM. None if not AV required + availability_zone_list: list of availability zones given by user in the VNFD descriptor. Ignore if + availability_zone_index is None #TODO ip, security groups Returns the instance identifier ''' self.logger.debug("new_vminstance input: image='%s' flavor='%s' nics='%s'",image_id, flavor_id,str(net_list)) try: + server = None metadata={} net_list_vim=[] - external_network=[] #list of external networks to be connected to instance, later on used to create floating_ip + external_network=[] # list of external networks to be connected to instance, later on used to create floating_ip + no_secured_ports = [] # List of port-is with port-security disabled self._reload_connection() - metadata_vpci = {} #For a specific neutron plugin + metadata_vpci={} # For a specific neutron plugin + block_device_mapping = None for net in net_list: if not net.get("net_id"): #skip non connected iface continue port_dict={ - "network_id": net["net_id"], - "name": net.get("name"), - "admin_state_up": True - } + "network_id": net["net_id"], + "name": net.get("name"), + "admin_state_up": True + } if net["type"]=="virtual": if "vpci" in net: metadata_vpci[ net["net_id"] ] = [[ net["vpci"], "" ]] @@ -781,15 +1024,20 @@ class vimconnector(vimconn.vimconnector): port_dict["name"]=name if net.get("mac_address"): port_dict["mac_address"]=net["mac_address"] - if net.get("port_security") == False: - port_dict["port_security_enabled"]=net["port_security"] - new_port = self.neutron.create_port({"port": port_dict }) - net["mac_adress"] = new_port["port"]["mac_address"] net["vim_id"] = new_port["port"]["id"] - net["ip"] = new_port["port"].get("fixed_ips", [{}])[0].get("ip_address") - net_list_vim.append({"port-id": new_port["port"]["id"]}) + # if try to use a network without subnetwork, it will return a emtpy list + fixed_ips = new_port["port"].get("fixed_ips") + if fixed_ips: + net["ip"] = fixed_ips[0].get("ip_address") + else: + net["ip"] = None + + port = {"port-id": new_port["port"]["id"]} + if float(self.nova.api_version.get_string()) >= 2.32: + port["tag"] = new_port["port"]["name"] + net_list_vim.append(port) if net.get('floating_ip', False): net['exit_on_floating_ip_error'] = True @@ -798,6 +1046,11 @@ class vimconnector(vimconn.vimconnector): net['exit_on_floating_ip_error'] = False external_network.append(net) + # If port security is disabled when the port has not yet been attached to the VM, then all vm traffic is dropped. + # As a workaround we wait until the VM is active and then disable the port-security + if net.get("port_security") == False: + no_secured_ports.append(new_port["port"]["id"]) + if metadata_vpci: metadata = {"pci_assignement": json.dumps(metadata_vpci)} if len(metadata["pci_assignement"]) >255: @@ -813,60 +1066,12 @@ class vimconnector(vimconn.vimconnector): if type(security_groups) is str: security_groups = ( security_groups, ) #cloud config - userdata=None - config_drive = None - if isinstance(cloud_config, dict): - if cloud_config.get("user-data"): - userdata=cloud_config["user-data"] - if cloud_config.get("boot-data-drive") != None: - config_drive = cloud_config["boot-data-drive"] - if cloud_config.get("config-files") or cloud_config.get("users") or cloud_config.get("key-pairs"): - if userdata: - raise vimconn.vimconnConflictException("Cloud-config cannot contain both 'userdata' and 'config-files'/'users'/'key-pairs'") - userdata_dict={} - #default user - if cloud_config.get("key-pairs"): - userdata_dict["ssh-authorized-keys"] = cloud_config["key-pairs"] - userdata_dict["users"] = [{"default": None, "ssh-authorized-keys": cloud_config["key-pairs"] }] - if cloud_config.get("users"): - if "users" not in userdata_dict: - userdata_dict["users"] = [ "default" ] - for user in cloud_config["users"]: - user_info = { - "name" : user["name"], - "sudo": "ALL = (ALL)NOPASSWD:ALL" - } - if "user-info" in user: - user_info["gecos"] = user["user-info"] - if user.get("key-pairs"): - user_info["ssh-authorized-keys"] = user["key-pairs"] - userdata_dict["users"].append(user_info) - - if cloud_config.get("config-files"): - userdata_dict["write_files"] = [] - for file in cloud_config["config-files"]: - file_info = { - "path" : file["dest"], - "content": file["content"] - } - if file.get("encoding"): - file_info["encoding"] = file["encoding"] - if file.get("permissions"): - file_info["permissions"] = file["permissions"] - if file.get("owner"): - file_info["owner"] = file["owner"] - userdata_dict["write_files"].append(file_info) - userdata = "#cloud-config\n" - userdata += yaml.safe_dump(userdata_dict, indent=4, default_flow_style=False) - self.logger.debug("userdata: %s", userdata) - elif isinstance(cloud_config, str): - userdata = cloud_config + config_drive, userdata = self._create_user_data(cloud_config) #Create additional volumes in case these are present in disk_list - block_device_mapping = None base_disk_index = ord('b') if disk_list != None: - block_device_mapping = dict() + block_device_mapping = {} for disk in disk_list: if 'image_id' in disk: volume = self.cinder.volumes.create(size = disk['size'],name = name + '_vd' + @@ -902,34 +1107,45 @@ class vimconnector(vimconn.vimconnector): raise vimconn.vimconnException('Timeout creating volumes for instance ' + name, http_code=vimconn.HTTP_Request_Timeout) - + # get availability Zone + vm_av_zone = self._get_vm_availability_zone(availability_zone_index, availability_zone_list) + + self.logger.debug("nova.servers.create({}, {}, {}, nics={}, meta={}, security_groups={}, " + "availability_zone={}, key_name={}, userdata={}, config_drive={}, " + "block_device_mapping={})".format(name, image_id, flavor_id, net_list_vim, metadata, + security_groups, vm_av_zone, self.config.get('keypair'), + userdata, config_drive, block_device_mapping)) server = self.nova.servers.create(name, image_id, flavor_id, nics=net_list_vim, meta=metadata, security_groups=security_groups, - availability_zone=self.config.get('availability_zone'), + availability_zone=vm_av_zone, key_name=self.config.get('keypair'), userdata=userdata, - config_drive = config_drive, - block_device_mapping = block_device_mapping + config_drive=config_drive, + block_device_mapping=block_device_mapping ) # , description=description) + + # Previously mentioned workaround to wait until the VM is active and then disable the port-security + if no_secured_ports: + self.__wait_for_vm(server.id, 'ACTIVE') + + for port_id in no_secured_ports: + try: + self.neutron.update_port(port_id, {"port": {"port_security_enabled": False, "security_groups": None} }) + + except Exception as e: + self.logger.error("It was not possible to disable port security for port {}".format(port_id)) + self.delete_vminstance(server.id) + raise + #print "DONE :-)", server pool_id = None floating_ips = self.neutron.list_floatingips().get("floatingips", ()) - for floating_network in external_network: - try: - # wait until vm is active - elapsed_time = 0 - while elapsed_time < server_timeout: - status = self.nova.servers.get(server.id).status - if status == 'ACTIVE': - break - time.sleep(1) - elapsed_time += 1 - #if we exceeded the timeout rollback - if elapsed_time >= server_timeout: - raise vimconn.vimconnException('Timeout creating instance ' + name, - http_code=vimconn.HTTP_Request_Timeout) + if external_network: + self.__wait_for_vm(server.id, 'ACTIVE') + for floating_network in external_network: + try: assigned = False while(assigned == False): if floating_ips: @@ -973,26 +1189,31 @@ class vimconnector(vimconn.vimconnector): if not floating_network['exit_on_floating_ip_error']: self.logger.warn("Cannot create floating_ip. %s", str(e)) continue - self.delete_vminstance(server.id) raise return server.id # except nvExceptions.NotFound as e: # error_value=-vimconn.HTTP_Not_Found # error_text= "vm instance %s not found" % vm_id - except (ksExceptions.ClientException, nvExceptions.ClientException, ConnectionError) as e: +# except TypeError as e: +# raise vimconn.vimconnException(type(e).__name__ + ": "+ str(e), http_code=vimconn.HTTP_Bad_Request) + + except Exception as e: # delete the volumes we just created - if block_device_mapping != None: + if block_device_mapping: for volume_id in block_device_mapping.itervalues(): self.cinder.volumes.delete(volume_id) - # delete ports we just created - for net_item in net_list_vim: - if 'port-id' in net_item: - self.neutron.delete_port(net_item['port-id']) + # Delete the VM + if server != None: + self.delete_vminstance(server.id) + else: + # delete ports we just created + for net_item in net_list_vim: + if 'port-id' in net_item: + self.neutron.delete_port(net_item['port-id']) + self._format_exception(e) - except TypeError as e: - raise vimconn.vimconnException(type(e).__name__ + ": "+ str(e), http_code=vimconn.HTTP_Bad_Request) def get_vminstance(self,vm_id): '''Returns the VM instance information from VIM''' @@ -1011,13 +1232,13 @@ class vimconnector(vimconn.vimconnector): Params: vm_id: uuid of the VM console_type, can be: - "novnc" (by default), "xvpvnc" for VNC types, + "novnc" (by default), "xvpvnc" for VNC types, "rdp-html5" for RDP types, "spice-html5" for SPICE types Returns dict with the console parameters: protocol: ssh, ftp, http, https, ... - server: usually ip address - port: the http, ssh, ... port - suffix: extra text, e.g. the http path and query string + server: usually ip address + port: the http, ssh, ... port + suffix: extra text, e.g. the http path and query string ''' self.logger.debug("Getting VM CONSOLE from VIM") try: @@ -1033,7 +1254,7 @@ class vimconnector(vimconn.vimconnector): console_dict = server.get_spice_console(console_type) else: raise vimconn.vimconnException("console type '{}' not allowed".format(console_type), http_code=vimconn.HTTP_Bad_Request) - + console_dict1 = console_dict.get("console") if console_dict1: console_url = console_dict1.get("url") @@ -1045,14 +1266,14 @@ class vimconnector(vimconn.vimconnector): if protocol_index < 0 or port_index<0 or suffix_index<0: return -vimconn.HTTP_Internal_Server_Error, "Unexpected response from VIM" console_dict={"protocol": console_url[0:protocol_index], - "server": console_url[protocol_index+2:port_index], - "port": console_url[port_index:suffix_index], - "suffix": console_url[suffix_index+1:] + "server": console_url[protocol_index+2:port_index], + "port": console_url[port_index:suffix_index], + "suffix": console_url[suffix_index+1:] } protocol_index += 2 return console_dict raise vimconn.vimconnUnexpectedResponse("Unexpected response from VIM") - + except (nvExceptions.NotFound, ksExceptions.ClientException, nvExceptions.ClientException, nvExceptions.BadRequest, ConnectionError) as e: self._format_exception(e) @@ -1108,14 +1329,14 @@ class vimconnector(vimconn.vimconnector): vm_id: #VIM id of this Virtual Machine status: #Mandatory. Text with one of: # DELETED (not found at vim) - # VIM_ERROR (Cannot connect to VIM, VIM response error, ...) + # VIM_ERROR (Cannot connect to VIM, VIM response error, ...) # OTHER (Vim reported other status not understood) # ERROR (VIM indicates an ERROR status) - # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running), + # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running), # CREATING (on building process), ERROR # ACTIVE:NoMgmtIP (Active but any of its interface has an IP address # - error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR + error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR vim_info: #Text with plain information obtained from vim (yaml.safe_dump) interfaces: - vim_info: #Text with plain information obtained from vim (yaml.safe_dump) @@ -1158,13 +1379,13 @@ class vimconnector(vimconn.vimconnector): interface["mac_address"] = port.get("mac_address") interface["vim_net_id"] = port["network_id"] interface["vim_interface_id"] = port["id"] - # check if OS-EXT-SRV-ATTR:host is there, + # check if OS-EXT-SRV-ATTR:host is there, # in case of non-admin credentials, it will be missing if vm_vim.get('OS-EXT-SRV-ATTR:host'): interface["compute_node"] = vm_vim['OS-EXT-SRV-ATTR:host'] interface["pci"] = None - # check if binding:profile is there, + # check if binding:profile is there, # in case of non-admin credentials, it will be missing if port.get('binding:profile'): if port['binding:profile'].get('pci_slot'): @@ -1202,7 +1423,7 @@ class vimconnector(vimconn.vimconnector): vm['error_msg'] = str(e) vm_dict[vm_id] = vm return vm_dict - + def action_vminstance(self, vm_id, action_dict): '''Send and action over a VM instance from VIM Returns the vm_id if the action was successfully sent to the VIM''' @@ -1211,7 +1432,7 @@ class vimconnector(vimconn.vimconnector): self._reload_connection() server = self.nova.servers.find(id=vm_id) if "start" in action_dict: - if action_dict["start"]=="rebuild": + if action_dict["start"]=="rebuild": server.rebuild() else: if server.status=="PAUSED": @@ -1253,7 +1474,7 @@ class vimconnector(vimconn.vimconnector): elif console_type == "spice-html5": console_dict = server.get_spice_console(console_type) else: - raise vimconn.vimconnException("console type '{}' not allowed".format(console_type), + raise vimconn.vimconnException("console type '{}' not allowed".format(console_type), http_code=vimconn.HTTP_Bad_Request) try: console_url = console_dict["console"]["url"] @@ -1264,14 +1485,14 @@ class vimconnector(vimconn.vimconnector): if protocol_index < 0 or port_index<0 or suffix_index<0: raise vimconn.vimconnException("Unexpected response from VIM " + str(console_dict)) console_dict2={"protocol": console_url[0:protocol_index], - "server": console_url[protocol_index+2 : port_index], - "port": int(console_url[port_index+1 : suffix_index]), - "suffix": console_url[suffix_index+1:] + "server": console_url[protocol_index+2 : port_index], + "port": int(console_url[port_index+1 : suffix_index]), + "suffix": console_url[suffix_index+1:] } - return console_dict2 + return console_dict2 except Exception as e: raise vimconn.vimconnException("Unexpected response from VIM " + str(console_dict)) - + return vm_id except (ksExceptions.ClientException, nvExceptions.ClientException, nvExceptions.NotFound, ConnectionError) as e: self._format_exception(e) @@ -1339,19 +1560,19 @@ class vimconnector(vimconn.vimconnector): "start_ID < end_ID ".format(vlanID_range)) #NOT USED FUNCTIONS - + def new_external_port(self, port_data): #TODO openstack if needed '''Adds a external port to VIM''' '''Returns the port identifier''' - return -vimconn.HTTP_Internal_Server_Error, "osconnector.new_external_port() not implemented" - + return -vimconn.HTTP_Internal_Server_Error, "osconnector.new_external_port() not implemented" + def connect_port_network(self, port_id, network_id, admin=False): #TODO openstack if needed '''Connects a external port to a network''' '''Returns status code of the VIM response''' - return -vimconn.HTTP_Internal_Server_Error, "osconnector.connect_port_network() not implemented" - + return -vimconn.HTTP_Internal_Server_Error, "osconnector.connect_port_network() not implemented" + def new_user(self, user_name, user_passwd, tenant_id=None): '''Adds a new user to openstack VIM''' '''Returns the user identifier''' @@ -1371,13 +1592,13 @@ class vimconnector(vimconn.vimconnector): #if reaching here is because an exception if self.debug: self.logger.debug("new_user " + error_text) - return error_value, error_text + return error_value, error_text def delete_user(self, user_id): '''Delete a user from openstack VIM''' '''Returns the user identifier''' if self.debug: - print "osconnector: Deleting a user from VIM" + print("osconnector: Deleting a user from VIM") try: self._reload_connection() self.keystone.users.delete(user_id) @@ -1394,14 +1615,14 @@ class vimconnector(vimconn.vimconnector): #TODO insert exception vimconn.HTTP_Unauthorized #if reaching here is because an exception if self.debug: - print "delete_tenant " + error_text + print("delete_tenant " + error_text) return error_value, error_text - + def get_hosts_info(self): '''Get the information of deployed hosts Returns the hosts content''' if self.debug: - print "osconnector: Getting Host info from VIM" + print("osconnector: Getting Host info from VIM") try: h_list=[] self._reload_connection() @@ -1418,8 +1639,8 @@ class vimconnector(vimconn.vimconnector): #TODO insert exception vimconn.HTTP_Unauthorized #if reaching here is because an exception if self.debug: - print "get_hosts_info " + error_text - return error_value, error_text + print("get_hosts_info " + error_text) + return error_value, error_text def get_hosts(self, vim_tenant): '''Get the hosts and deployed instances @@ -1447,6 +1668,297 @@ class vimconnector(vimconn.vimconnector): #TODO insert exception vimconn.HTTP_Unauthorized #if reaching here is because an exception if self.debug: - print "get_hosts " + error_text - return error_value, error_text + print("get_hosts " + error_text) + return error_value, error_text + + def new_classification(self, name, ctype, definition): + self.logger.debug( + 'Adding a new (Traffic) Classification to VIM, named %s', name) + try: + new_class = None + self._reload_connection() + if ctype not in supportedClassificationTypes: + raise vimconn.vimconnNotSupportedException( + 'OpenStack VIM connector doesn\'t support provided ' + 'Classification Type {}, supported ones are: ' + '{}'.format(ctype, supportedClassificationTypes)) + if not self._validate_classification(ctype, definition): + raise vimconn.vimconnException( + 'Incorrect Classification definition ' + 'for the type specified.') + classification_dict = definition + classification_dict['name'] = name + + new_class = self.neutron.create_flow_classifier( + {'flow_classifier': classification_dict}) + return new_class['flow_classifier']['id'] + except (neExceptions.ConnectionFailed, ksExceptions.ClientException, + neExceptions.NeutronException, ConnectionError) as e: + self.logger.error( + 'Creation of Classification failed.') + self._format_exception(e) + + def get_classification(self, class_id): + self.logger.debug(" Getting Classification %s from VIM", class_id) + filter_dict = {"id": class_id} + class_list = self.get_classification_list(filter_dict) + if len(class_list) == 0: + raise vimconn.vimconnNotFoundException( + "Classification '{}' not found".format(class_id)) + elif len(class_list) > 1: + raise vimconn.vimconnConflictException( + "Found more than one Classification with this criteria") + classification = class_list[0] + return classification + + def get_classification_list(self, filter_dict={}): + self.logger.debug("Getting Classifications from VIM filter: '%s'", + str(filter_dict)) + try: + self._reload_connection() + if self.api_version3 and "tenant_id" in filter_dict: + filter_dict['project_id'] = filter_dict.pop('tenant_id') + classification_dict = self.neutron.list_flow_classifier( + **filter_dict) + classification_list = classification_dict["flow_classifiers"] + self.__classification_os2mano(classification_list) + return classification_list + except (neExceptions.ConnectionFailed, ksExceptions.ClientException, + neExceptions.NeutronException, ConnectionError) as e: + self._format_exception(e) + + def delete_classification(self, class_id): + self.logger.debug("Deleting Classification '%s' from VIM", class_id) + try: + self._reload_connection() + self.neutron.delete_flow_classifier(class_id) + return class_id + except (neExceptions.ConnectionFailed, neExceptions.NeutronException, + ksExceptions.ClientException, neExceptions.NeutronException, + ConnectionError) as e: + self._format_exception(e) + + def new_sfi(self, name, ingress_ports, egress_ports, sfc_encap=True): + self.logger.debug( + "Adding a new Service Function Instance to VIM, named '%s'", name) + try: + new_sfi = None + self._reload_connection() + correlation = None + if sfc_encap: + # TODO(igordc): must be changed to NSH in Queens + # (MPLS is a workaround) + correlation = 'mpls' + if len(ingress_ports) != 1: + raise vimconn.vimconnNotSupportedException( + "OpenStack VIM connector can only have " + "1 ingress port per SFI") + if len(egress_ports) != 1: + raise vimconn.vimconnNotSupportedException( + "OpenStack VIM connector can only have " + "1 egress port per SFI") + sfi_dict = {'name': name, + 'ingress': ingress_ports[0], + 'egress': egress_ports[0], + 'service_function_parameters': { + 'correlation': correlation}} + new_sfi = self.neutron.create_port_pair({'port_pair': sfi_dict}) + return new_sfi['port_pair']['id'] + except (neExceptions.ConnectionFailed, ksExceptions.ClientException, + neExceptions.NeutronException, ConnectionError) as e: + if new_sfi: + try: + self.neutron.delete_port_pair_group( + new_sfi['port_pair']['id']) + except Exception: + self.logger.error( + 'Creation of Service Function Instance failed, with ' + 'subsequent deletion failure as well.') + self._format_exception(e) + + def get_sfi(self, sfi_id): + self.logger.debug( + 'Getting Service Function Instance %s from VIM', sfi_id) + filter_dict = {"id": sfi_id} + sfi_list = self.get_sfi_list(filter_dict) + if len(sfi_list) == 0: + raise vimconn.vimconnNotFoundException( + "Service Function Instance '{}' not found".format(sfi_id)) + elif len(sfi_list) > 1: + raise vimconn.vimconnConflictException( + 'Found more than one Service Function Instance ' + 'with this criteria') + sfi = sfi_list[0] + return sfi + + def get_sfi_list(self, filter_dict={}): + self.logger.debug("Getting Service Function Instances from " + "VIM filter: '%s'", str(filter_dict)) + try: + self._reload_connection() + if self.api_version3 and "tenant_id" in filter_dict: + filter_dict['project_id'] = filter_dict.pop('tenant_id') + sfi_dict = self.neutron.list_port_pair(**filter_dict) + sfi_list = sfi_dict["port_pairs"] + self.__sfi_os2mano(sfi_list) + return sfi_list + except (neExceptions.ConnectionFailed, ksExceptions.ClientException, + neExceptions.NeutronException, ConnectionError) as e: + self._format_exception(e) + def delete_sfi(self, sfi_id): + self.logger.debug("Deleting Service Function Instance '%s' " + "from VIM", sfi_id) + try: + self._reload_connection() + self.neutron.delete_port_pair(sfi_id) + return sfi_id + except (neExceptions.ConnectionFailed, neExceptions.NeutronException, + ksExceptions.ClientException, neExceptions.NeutronException, + ConnectionError) as e: + self._format_exception(e) + + def new_sf(self, name, sfis, sfc_encap=True): + self.logger.debug("Adding a new Service Function to VIM, " + "named '%s'", name) + try: + new_sf = None + self._reload_connection() + correlation = None + if sfc_encap: + # TODO(igordc): must be changed to NSH in Queens + # (MPLS is a workaround) + correlation = 'mpls' + for instance in sfis: + sfi = self.get_sfi(instance) + if sfi.get('sfc_encap') != correlation: + raise vimconn.vimconnNotSupportedException( + "OpenStack VIM connector requires all SFIs of the " + "same SF to share the same SFC Encapsulation") + sf_dict = {'name': name, + 'port_pairs': sfis} + new_sf = self.neutron.create_port_pair_group({ + 'port_pair_group': sf_dict}) + return new_sf['port_pair_group']['id'] + except (neExceptions.ConnectionFailed, ksExceptions.ClientException, + neExceptions.NeutronException, ConnectionError) as e: + if new_sf: + try: + self.neutron.delete_port_pair_group( + new_sf['port_pair_group']['id']) + except Exception: + self.logger.error( + 'Creation of Service Function failed, with ' + 'subsequent deletion failure as well.') + self._format_exception(e) + + def get_sf(self, sf_id): + self.logger.debug("Getting Service Function %s from VIM", sf_id) + filter_dict = {"id": sf_id} + sf_list = self.get_sf_list(filter_dict) + if len(sf_list) == 0: + raise vimconn.vimconnNotFoundException( + "Service Function '{}' not found".format(sf_id)) + elif len(sf_list) > 1: + raise vimconn.vimconnConflictException( + "Found more than one Service Function with this criteria") + sf = sf_list[0] + return sf + + def get_sf_list(self, filter_dict={}): + self.logger.debug("Getting Service Function from VIM filter: '%s'", + str(filter_dict)) + try: + self._reload_connection() + if self.api_version3 and "tenant_id" in filter_dict: + filter_dict['project_id'] = filter_dict.pop('tenant_id') + sf_dict = self.neutron.list_port_pair_group(**filter_dict) + sf_list = sf_dict["port_pair_groups"] + self.__sf_os2mano(sf_list) + return sf_list + except (neExceptions.ConnectionFailed, ksExceptions.ClientException, + neExceptions.NeutronException, ConnectionError) as e: + self._format_exception(e) + + def delete_sf(self, sf_id): + self.logger.debug("Deleting Service Function '%s' from VIM", sf_id) + try: + self._reload_connection() + self.neutron.delete_port_pair_group(sf_id) + return sf_id + except (neExceptions.ConnectionFailed, neExceptions.NeutronException, + ksExceptions.ClientException, neExceptions.NeutronException, + ConnectionError) as e: + self._format_exception(e) + + def new_sfp(self, name, classifications, sfs, sfc_encap=True, spi=None): + self.logger.debug("Adding a new Service Function Path to VIM, " + "named '%s'", name) + try: + new_sfp = None + self._reload_connection() + if not sfc_encap: + raise vimconn.vimconnNotSupportedException( + "OpenStack VIM connector only supports " + "SFC-Encapsulated chains") + # TODO(igordc): must be changed to NSH in Queens + # (MPLS is a workaround) + correlation = 'mpls' + sfp_dict = {'name': name, + 'flow_classifiers': classifications, + 'port_pair_groups': sfs, + 'chain_parameters': {'correlation': correlation}} + if spi: + sfp_dict['chain_id'] = spi + new_sfp = self.neutron.create_port_chain({'port_chain': sfp_dict}) + return new_sfp["port_chain"]["id"] + except (neExceptions.ConnectionFailed, ksExceptions.ClientException, + neExceptions.NeutronException, ConnectionError) as e: + if new_sfp: + try: + self.neutron.delete_port_chain(new_sfp['port_chain']['id']) + except Exception: + self.logger.error( + 'Creation of Service Function Path failed, with ' + 'subsequent deletion failure as well.') + self._format_exception(e) + + def get_sfp(self, sfp_id): + self.logger.debug(" Getting Service Function Path %s from VIM", sfp_id) + filter_dict = {"id": sfp_id} + sfp_list = self.get_sfp_list(filter_dict) + if len(sfp_list) == 0: + raise vimconn.vimconnNotFoundException( + "Service Function Path '{}' not found".format(sfp_id)) + elif len(sfp_list) > 1: + raise vimconn.vimconnConflictException( + "Found more than one Service Function Path with this criteria") + sfp = sfp_list[0] + return sfp + + def get_sfp_list(self, filter_dict={}): + self.logger.debug("Getting Service Function Paths from VIM filter: " + "'%s'", str(filter_dict)) + try: + self._reload_connection() + if self.api_version3 and "tenant_id" in filter_dict: + filter_dict['project_id'] = filter_dict.pop('tenant_id') + sfp_dict = self.neutron.list_port_chain(**filter_dict) + sfp_list = sfp_dict["port_chains"] + self.__sfp_os2mano(sfp_list) + return sfp_list + except (neExceptions.ConnectionFailed, ksExceptions.ClientException, + neExceptions.NeutronException, ConnectionError) as e: + self._format_exception(e) + + def delete_sfp(self, sfp_id): + self.logger.debug( + "Deleting Service Function Path '%s' from VIM", sfp_id) + try: + self._reload_connection() + self.neutron.delete_port_chain(sfp_id) + return sfp_id + except (neExceptions.ConnectionFailed, neExceptions.NeutronException, + ksExceptions.ClientException, neExceptions.NeutronException, + ConnectionError) as e: + self._format_exception(e)