| # -*- coding: utf-8 -*- |
| |
| __author__='Sergio Gonzalez' |
| __date__ ='$18-apr-2019 23:59:59$' |
| |
| 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") |