X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FRO.git;a=blobdiff_plain;f=RO-VIM-azure%2Fosm_rovim_azure%2Fvimconn_azure.py;fp=RO-VIM-azure%2Fosm_rovim_azure%2Fvimconn_azure.py;h=0cc143fae94b29da2eb6a159a48aabbbd2f676ba;hp=0000000000000000000000000000000000000000;hb=7d782eff123e5b44d41437377ccca66ad1e8b21b;hpb=5db670b68349fd1f00a5efc8c0ccd0ef9d073dca diff --git a/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py b/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py new file mode 100755 index 00000000..0cc143fa --- /dev/null +++ b/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py @@ -0,0 +1,495 @@ +# -*- coding: utf-8 -*- + +__author__='Sergio Gonzalez' +__date__ ='$18-apr-2019 23:59:59$' + +from osm_ro import vimconn +import logging + +from os import getenv +from uuid import uuid4 + +from azure.common.credentials import ServicePrincipalCredentials +from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.network import NetworkManagementClient +from azure.mgmt.compute import ComputeManagementClient + + +class vimconnector(vimconn.vimconnector): + + def __init__(self, uuid, name, tenant_id, tenant_name, url, url_admin=None, user=None, passwd=None, log_level=None, + config={}, persistent_info={}): + + vimconn.vimconnector.__init__(self, uuid, name, tenant_id, tenant_name, url, url_admin, user, passwd, log_level, + config, persistent_info) + + # LOGGER + self.logger = logging.getLogger('openmano.vim.azure') + if log_level: + logging.basicConfig() + self.logger.setLevel(getattr(logging, log_level)) + + # CREDENTIALS + self.credentials = ServicePrincipalCredentials( + client_id=user, + secret=passwd, + tenant=(tenant_id or tenant_name) + ) + + # SUBSCRIPTION + if 'subscription_id' in config: + self.subscription_id = config.get('subscription_id') + self.logger.debug('Setting subscription '+str(self.subscription_id)) + else: + raise vimconn.vimconnException('Subscription not specified') + # REGION + if 'region_name' in config: + self.region = config.get('region_name') + else: + raise vimconn.vimconnException('Azure region_name is not specified at config') + # RESOURCE_GROUP + if 'resource_group' in config: + self.resource_group = config.get('resource_group') + else: + raise vimconn.vimconnException('Azure resource_group is not specified at config') + # VNET_NAME + if 'vnet_name' in config: + self.vnet_name = config["vnet_name"] + + # public ssh key + self.pub_key = config.get('pub_key') + + def _reload_connection(self): + """ + Sets connections to work with Azure service APIs + :return: + """ + self.logger.debug('Reloading API Connection') + try: + self.conn = ResourceManagementClient(self.credentials, self.subscription_id) + self.conn_compute = ComputeManagementClient(self.credentials, self.subscription_id) + self.conn_vnet = NetworkManagementClient(self.credentials, self.subscription_id) + self._check_or_create_resource_group() + self._check_or_create_vnet() + except Exception as e: + self.format_vimconn_exception(e) + + def _get_resource_name_from_resource_id(self, resource_id): + return str(resource_id.split('/')[-1]) + + def _get_location_from_resource_group(self, resource_group_name): + return self.conn.resource_groups.get(resource_group_name).location + + def _get_resource_group_name_from_resource_id(self, resource_id): + return str(resource_id.split('/')[4]) + + def _check_subnets_for_vm(self, net_list): + # All subnets must belong to the same resource group and vnet + if len(set(self._get_resource_group_name_from_resource_id(net['id']) + + self._get_resource_name_from_resource_id(net['id']) for net in net_list)) != 1: + raise self.format_vimconn_exception('Azure VMs can only attach to subnets in same VNET') + + def format_vimconn_exception(self, e): + """ + Params: an Exception object + :param e: + :return: Raises the proper vimconnException + """ + self.conn = None + self.conn_vnet = None + raise vimconn.vimconnConnectionException(type(e).__name__ + ': ' + str(e)) + + def _check_or_create_resource_group(self): + """ + Creates a resource group in indicated region + :return: None + """ + self.logger.debug('Creating RG {} in location {}'.format(self.resource_group, self.region)) + self.conn.resource_groups.create_or_update(self.resource_group, {'location': self.region}) + + def _check_or_create_vnet(self): + try: + vnet_params = { + 'location': self.region, + 'address_space': { + 'address_prefixes': "10.0.0.0/8" + }, + } + self.conn_vnet.virtual_networks.create_or_update(self.resource_group, self.vnet_name, vnet_params) + except Exception as e: + self.format_vimconn_exception(e) + + def new_network(self, net_name, net_type, ip_profile=None, shared=False, vlan=None): + """ + Adds a tenant network to VIM + :param net_name: name of the network + :param net_type: + :param ip_profile: is a dict containing the IP parameters of the network (Currently only IPv4 is implemented) + 'ip-version': can be one of ['IPv4','IPv6'] + 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y + 'gateway-address': (Optional) ip_schema, that is X.X.X.X + 'dns-address': (Optional) ip_schema, + 'dhcp': (Optional) dict containing + 'enabled': {'type': 'boolean'}, + 'start-address': ip_schema, first IP to grant + 'count': number of IPs to grant. + :param shared: + :param vlan: + :return: a tuple with the network identifier and created_items, or raises an exception on error + created_items can be None or a dictionary where this method can include key-values that will be passed to + the method delete_network. Can be used to store created segments, created l2gw connections, etc. + Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same + as not present. + """ + + return self._new_subnet(net_name, ip_profile) + + def _new_subnet(self, net_name, ip_profile): + """ + Adds a tenant network to VIM. It creates a new VNET with a single subnet + :param net_name: + :param ip_profile: + :return: + """ + self.logger.debug('Adding a subnet to VNET '+self.vnet_name) + self._reload_connection() + + if ip_profile is None: + # TODO get a non used vnet ip range /24 and allocate automatically + raise vimconn.vimconnException('Azure cannot create VNET with no CIDR') + + try: + vnet_params= { + 'location': self.region, + 'address_space': { + 'address_prefixes': [ip_profile['subnet_address']] + }, + 'subnets': [ + { + 'name': "{}-{}".format(net_name[:24], uuid4()), + 'address_prefix': ip_profile['subnet_address'] + } + ] + } + self.conn_vnet.virtual_networks.create_or_update(self.resource_group, self.vnet_name, vnet_params) + # TODO return a tuple (subnet-ID, None) + except Exception as e: + self.format_vimconn_exception(e) + + def _create_nic(self, subnet_id, nic_name, static_ip=None): + self._reload_connection() + + resource_group_name=self._get_resource_group_name_from_resource_id(subnet_id) + location = self._get_location_from_resource_group(resource_group_name) + + if static_ip: + async_nic_creation = self.conn_vnet.network_interfaces.create_or_update( + resource_group_name, + nic_name, + { + 'location': location, + 'ip_configurations': [{ + 'name': nic_name + 'ipconfiguration', + 'privateIPAddress': static_ip, + 'privateIPAllocationMethod': 'Static', + 'subnet': { + 'id': subnet_id + } + }] + } + ) + else: + async_nic_creation = self.conn_vnet.network_interfaces.create_or_update( + resource_group_name, + nic_name, + { + 'location': location, + 'ip_configurations': [{ + 'name': nic_name + 'ipconfiguration', + 'subnet': { + 'id': subnet_id + } + }] + } + ) + + return async_nic_creation.result() + + def get_image_list(self, filter_dict={}): + """ + The urn contains for marketplace 'publisher:offer:sku:version' + + :param filter_dict: + :return: + """ + image_list = [] + + self._reload_connection() + if filter_dict.get("name"): + params = filter_dict["name"].split(":") + if len(params) >= 3: + publisher = params[0] + offer = params[1] + sku = params[2] + version = None + if len(params) == 4: + version = params[3] + images = self.conn_compute.virtual_machine_images.list(self.region, publisher, offer, sku) + for image in images: + if version: + image_version = str(image.id).split("/")[-1] + if image_version != version: + continue + image_list.append({ + 'id': str(image.id), + 'name': self._get_resource_name_from_resource_id(image.id) + }) + return image_list + + images = self.conn_compute.virtual_machine_images.list() + + for image in images: + # TODO implement filter_dict + if filter_dict: + if filter_dict.get("id") and str(image.id) != filter_dict["id"]: + continue + if filter_dict.get("name") and \ + self._get_resource_name_from_resource_id(image.id) != filter_dict["name"]: + continue + # TODO add checksum + image_list.append({ + 'id': str(image.id), + 'name': self._get_resource_name_from_resource_id(image.id), + }) + return image_list + + def get_network_list(self, filter_dict={}): + """Obtain tenant networks of VIM + Filter_dict can be: + name: network name + id: network uuid + shared: boolean + tenant_id: tenant + admin_state_up: boolean + status: 'ACTIVE' + Returns the network list of dictionaries + """ + self.logger.debug('Getting all subnets from VIM') + try: + self._reload_connection() + vnet = self.conn_vnet.virtual_networks.get(self.config["resource_group"], self.vnet_name) + subnet_list = [] + + for subnet in vnet.subnets: + # TODO implement filter_dict + if filter_dict: + if filter_dict.get("id") and str(subnet.id) != filter_dict["id"]: + continue + if filter_dict.get("name") and \ + self._get_resource_name_from_resource_id(subnet.id) != filter_dict["name"]: + continue + + subnet_list.append({ + 'id': str(subnet.id), + 'name': self._get_resource_name_from_resource_id(subnet.id), + 'status': str(vnet.provisioning_state), # TODO Does subnet contains status??? + 'cidr_block': str(subnet.address_prefix) + } + ) + return subnet_list + except Exception as e: + self.format_vimconn_exception(e) + + def new_vminstance(self, vm_name, description, start, image_id, flavor_id, net_list, cloud_config=None, + disk_list=None, availability_zone_index=None, availability_zone_list=None): + + return self._new_vminstance(vm_name, image_id, flavor_id, net_list) + + def _new_vminstance(self, vm_name, image_id, flavor_id, net_list, cloud_config=None, disk_list=None, + availability_zone_index=None, availability_zone_list=None): + #Create NICs + self._check_subnets_for_vm(net_list) + vm_nics = [] + for idx, net in enumerate(net_list): + subnet_id=net['subnet_id'] + nic_name = vm_name + '-nic-'+str(idx) + vm_nic = self._create_nic(subnet_id, nic_name) + vm_nics.append({ 'id': str(vm_nic.id)}) + + try: + vm_parameters = { + 'location': self.region, + 'os_profile': { + 'computer_name': vm_name, # TODO if vm_name cannot be repeated add uuid4() suffix + 'admin_username': 'sergio', # TODO is it mandatory??? + 'linuxConfiguration': { + 'disablePasswordAuthentication': 'true', + 'ssh': { + 'publicKeys': [ + { + 'path': '/home/sergio/.ssh/authorized_keys', + 'keyData': self.pub_key + } + ] + } + } + + }, + 'hardware_profile': { + 'vm_size':flavor_id + }, + 'storage_profile': { + 'image_reference': image_id + }, + 'network_profile': { + 'network_interfaces': [ + vm_nics[0] + ] + } + } + creation_result = self.conn_compute.virtual_machines.create_or_update( + self.resource_group, + vm_name, + vm_parameters + ) + + run_command_parameters = { + 'command_id': 'RunShellScript', # For linux, don't change it + 'script': [ + 'date > /home/sergio/test.txt' + ] + } + poller = self.conn_compute.virtual_machines.run_command( + self.resource_group, + vm_name, + run_command_parameters + ) + # TODO return a tuple (vm-ID, None) + except Exception as e: + self.format_vimconn_exception(e) + + def get_flavor_id_from_data(self, flavor_dict): + self.logger.debug("Getting flavor id from data") + self._reload_connection() + vm_sizes_list = [vm_size.serialize() for vm_size in self.conn_compute.virtual_machine_sizes.list(self.region)] + + cpus = flavor_dict['vcpus'] + memMB = flavor_dict['ram'] + + filteredSizes = [size for size in vm_sizes_list if size['numberOfCores'] > cpus and size['memoryInMB'] > memMB] + listedFilteredSizes = sorted(filteredSizes, key=lambda k: k['numberOfCores']) + + return listedFilteredSizes[0]['name'] + + def check_vim_connectivity(self): + try: + self._reload_connection() + return True + except Exception as e: + raise vimconn.vimconnException("Connectivity issue with Azure API: {}".format(e)) + + def get_network(self, net_id): + resGroup = self._get_resource_group_name_from_resource_id(net_id) + resName = self._get_resource_name_from_resource_id(net_id) + + self._reload_connection() + vnet = self.conn_vnet.virtual_networks.get(resGroup, resName) + + return vnet + + def delete_network(self, net_id): + resGroup = self._get_resource_group_name_from_resource_id(net_id) + resName = self._get_resource_name_from_resource_id(net_id) + + self._reload_connection() + self.conn_vnet.virtual_networks.delete(resGroup, resName) + + def delete_vminstance(self, vm_id): + resGroup = self._get_resource_group_name_from_resource_id(net_id) + resName = self._get_resource_name_from_resource_id(net_id) + + self._reload_connection() + self.conn_compute.virtual_machines.delete(resGroup, resName) + + def get_vminstance(self, vm_id): + resGroup = self._get_resource_group_name_from_resource_id(net_id) + resName = self._get_resource_name_from_resource_id(net_id) + + self._reload_connection() + vm=self.conn_compute.virtual_machines.get(resGroup, resName) + + return vm + + def get_flavor(self, flavor_id): + self._reload_connection() + for vm_size in self.conn_compute.virtual_machine_sizes.list(self.region): + if vm_size.name == flavor_id : + return vm_size + + +# TODO refresh_nets_status ver estado activo +# TODO refresh_vms_status ver estado activo +# TODO get_vminstance_console for getting console + +if __name__ == "__main__": + + # Making some basic test + vim_id='azure' + vim_name='azure' + needed_test_params = { + "client_id": "AZURE_CLIENT_ID", + "secret": "AZURE_SECRET", + "tenant": "AZURE_TENANT", + "resource_group": "AZURE_RESOURCE_GROUP", + "subscription_id": "AZURE_SUBSCRIPTION_ID", + "vnet_name": "AZURE_VNET_NAME", + } + test_params = {} + + for param, env_var in needed_test_params.items(): + value = getenv(env_var) + if not value: + raise Exception("Provide a valid value for env '{}'".format(env_var)) + test_params[param] = value + + config = { + 'region_name': getenv("AZURE_REGION_NAME", 'westeurope'), + 'resource_group': getenv("AZURE_RESOURCE_GROUP"), + 'subscription_id': getenv("AZURE_SUBSCRIPTION_ID"), + 'pub_key': getenv("AZURE_PUB_KEY", None), + 'vnet_name': getenv("AZURE_VNET_NAME", 'myNetwork'), + } + + virtualMachine = { + 'name': 'sergio', + 'description': 'new VM', + 'status': 'running', + 'image': { + 'publisher': 'Canonical', + 'offer': 'UbuntuServer', + 'sku': '16.04.0-LTS', + 'version': 'latest' + }, + 'hardware_profile': { + 'vm_size': 'Standard_DS1_v2' + }, + 'networks': [ + 'sergio' + ] + } + + vnet_config = { + 'subnet_address': '10.1.2.0/24', + #'subnet_name': 'subnet-oam' + } + ########################### + + azure = vimconnector(vim_id, vim_name, tenant_id=test_params["tenant"], tenant_name=None, url=None, url_admin=None, + user=test_params["client_id"], passwd=test_params["secret"], log_level=None, config=config) + + # azure.get_flavor_id_from_data("here") + # subnets=azure.get_network_list() + # azure.new_vminstance(virtualMachine['name'], virtualMachine['description'], virtualMachine['status'], + # virtualMachine['image'], virtualMachine['hardware_profile']['vm_size'], subnets) + + azure.get_flavor("Standard_A11")