From 2def976ad76b263811a686b8cf7c7e9de494a611 Mon Sep 17 00:00:00 2001 From: Sanket Date: Tue, 22 Aug 2017 15:05:02 +0530 Subject: [PATCH] Add Azure support for Nova Change-Id: I05315b83ab28b948caadeadf9be9ecb5fd2c9e5e Implements: blueprint azure-support --- nova/virt/azure/__init__.py | 16 + nova/virt/azure/config.py | 50 ++ nova/virt/azure/constants.py | 35 ++ nova/virt/azure/create-nova-flavors.py | 112 ++++ nova/virt/azure/driver.py | 748 +++++++++++++++++++++++++ nova/virt/azure/utils.py | 133 +++++ 6 files changed, 1094 insertions(+) create mode 100644 nova/virt/azure/__init__.py create mode 100644 nova/virt/azure/config.py create mode 100644 nova/virt/azure/constants.py create mode 100644 nova/virt/azure/create-nova-flavors.py create mode 100644 nova/virt/azure/driver.py create mode 100644 nova/virt/azure/utils.py diff --git a/nova/virt/azure/__init__.py b/nova/virt/azure/__init__.py new file mode 100644 index 0000000..d568566 --- /dev/null +++ b/nova/virt/azure/__init__.py @@ -0,0 +1,16 @@ +""" +Copyright (c) 2017 Platform9 Systems Inc. +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the +License for the specific language governing permissions and limitations +under the License. +""" + +from nova.virt.azure import driver + +AzureDriver = driver.AzureDriver diff --git a/nova/virt/azure/config.py b/nova/virt/azure/config.py new file mode 100644 index 0000000..dd71e59 --- /dev/null +++ b/nova/virt/azure/config.py @@ -0,0 +1,50 @@ +""" +Copyright 2017 Platform9 Systems Inc.(http://www.platform9.com) +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +""" +import nova.conf +from oslo_config import cfg + +azure_group = cfg.OptGroup( + name='azure', title='Options to connect to Azure cloud') + +azure_opts = [ + cfg.StrOpt('tenant_id', help='Tenant id of Azure account'), + cfg.StrOpt('client_id', help='Azure client id'), + cfg.StrOpt('client_secret', help='Azure Client secret', secret=True), + cfg.StrOpt('subscription_id', help='Azure subscription id'), + cfg.StrOpt('region', help='Azure region'), + cfg.StrOpt('resource_group', help="Azure resource group"), + cfg.StrOpt( + 'vm_admin_username', + default='azureuser', + help=('Specifies the name of the administrator', + 'account in virtual machine')), + cfg.IntOpt('vnc_port', default=5900, help='VNC starting port'), + # 500 VCPUs + cfg.IntOpt( + 'max_vcpus', default=500, help='Max number of vCPUs that can be used'), + # 1000 GB RAM + cfg.IntOpt( + 'max_memory_mb', + default=1024000, + help='Max memory MB that can be used'), + # 1 TB Storage + cfg.IntOpt( + 'max_disk_gb', default=1024, help='Max storage in GB that can be used') +] + +CONF = nova.conf.CONF +CONF.register_group(azure_group) +CONF.register_opts(azure_opts, group=azure_group) + +nova_conf = CONF +azure_conf = CONF.azure diff --git a/nova/virt/azure/constants.py b/nova/virt/azure/constants.py new file mode 100644 index 0000000..03e22af --- /dev/null +++ b/nova/virt/azure/constants.py @@ -0,0 +1,35 @@ +""" +Copyright (c) 2017 Platform9 Systems Inc. +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the +License for the specific language governing permissions and limitations +under the License. +""" + +from nova.compute import power_state + +OMNI_STATE_MAP = { + "PowerState/starting": power_state.NOSTATE, + "PowerState/running": power_state.RUNNING, + "PowerState/stopping": power_state.NOSTATE, + "PowerState/deallocating": power_state.NOSTATE, + "PowerState/deallocated": power_state.SHUTDOWN, + "PowerState/stopped": power_state.SHUTDOWN +} + +PROVISION_STATES = { + 'ProvisioningState/succeeded': '', + 'ProvisioningState/failed': '', + 'ProvisioningState/updating': '', + 'ProvisioningState/creating': '', + 'ProvisioningState/deleting': '' + +} + +OMNI_ID = 'azure_id' +OMNI_NAME = 'azure_name' diff --git a/nova/virt/azure/create-nova-flavors.py b/nova/virt/azure/create-nova-flavors.py new file mode 100644 index 0000000..a320120 --- /dev/null +++ b/nova/virt/azure/create-nova-flavors.py @@ -0,0 +1,112 @@ +""" +Copyright (c) 2017 Platform9 Systems Inc. (http://www.platform9.com) +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import os +import sys +import utils as azure_utils + +from keystoneauth1 import loading +from keystoneauth1 import session +from novaclient import client as nova_client + + +def abort(message): + sys.exit(message) + + +def get_env_param(env_name): + if env_name in os.environ: + return os.environ[env_name] + abort("%s environment variable not set." % env_name) + + +def get_keystone_session(vendor_data): + username = vendor_data['username'] + password = vendor_data['password'] + project_name = vendor_data['tenant_name'] + auth_url = vendor_data['auth_url'] + + loader = loading.get_plugin_loader('password') + auth = loader.load_from_options(auth_url=auth_url, + project_name=project_name, + username=username, password=password) + sess = session.Session(auth=auth) + return sess + + +def get_nova_client(vendor_data): + NOVA_VERSION = '2' + client = nova_client.Client(NOVA_VERSION, + session=get_keystone_session(vendor_data)) + return client + + +class NovaOperator(object): + def __init__(self): + auth_url = get_env_param('OS_AUTH_URL') + project_name = os.environ.get('OS_PROJECT_NAME') + tenant_name = os.environ.get('OS_TENANT_NAME') + username = get_env_param('OS_USERNAME') + password = get_env_param('OS_PASSWORD') + if not project_name: + if not tenant_name: + raise Exception("Either OS_PROJECT_NAME or OS_TENANT_NAME is " + "required.") + project_name = tenant_name + self.vendor_data = { + 'username': username, + 'password': password, + 'auth_url': auth_url, + 'tenant_name': project_name + } + self.nova_client = get_nova_client(self.vendor_data) + + def register_flavor(self, name, memory_mb=0, vcpus=0): + self.nova_client.flavors.create(name, memory_mb, vcpus, 0) + print("Registered flavor %s" % name) + + +class FlavorProvider(object): + def __init__(self): + self.nova_operator = NovaOperator() + + def get_flavor_objs(self): + raise NotImplementedError() + + def register_flavors(self): + for flavor_info in self.get_flavor_objs(): + self.nova_operator.register_flavor(flavor_info.name, + flavor_info.memory_in_mb, + flavor_info.number_of_cores) + + +class AzureFlavors(FlavorProvider): + def __init__(self): + super(AzureFlavors, self).__init__() + tenant_id = get_env_param('AZURE_TENANT_ID') + client_id = get_env_param('AZURE_CLIENT_ID') + client_secret = get_env_param('AZURE_CLIENT_SECRET') + subscription_id = get_env_param('AZURE_SUBSCRIPTION_ID') + self.region = get_env_param('AZURE_REGION') + self.compute_client = azure_utils.get_compute_client( + tenant_id, client_id, client_secret, subscription_id) + + def get_flavor_objs(self): + vm_sizes = self.compute_client.virtual_machine_sizes + for i in vm_sizes.list(location=self.region): + yield i + + +if __name__ == '__main__': + az_flavors = AzureFlavors() + az_flavors.register_flavors() diff --git a/nova/virt/azure/driver.py b/nova/virt/azure/driver.py new file mode 100644 index 0000000..5567108 --- /dev/null +++ b/nova/virt/azure/driver.py @@ -0,0 +1,748 @@ +""" +Copyright (c) 2017 Platform9 Systems Inc. +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the +License for the specific language governing permissions and limitations +under the License. +""" + +import hashlib +import uuid + +from oslo_log import log as logging + +from nova import exception +from nova.virt.azure.config import azure_conf as drv_conf +from nova.virt.azure.config import nova_conf +from nova.virt.azure import constants +from nova.virt.azure import utils +from nova.virt import driver +from nova.virt import hardware +LOG = logging.getLogger(__name__) + +OMNI_NAME = constants.OMNI_NAME + + +class AzureDriver(driver.ComputeDriver): + capabilities = { + "has_imagecache": True, + "supports_recreate": True, + } + + def __init__(self, virtapi, read_only=False): + super(AzureDriver, self).__init__(virtapi) + self.name = 'Azure' + self.version = '1.0' + self.host_status_base = { + 'vcpus': drv_conf.max_vcpus, + 'memory_mb': drv_conf.max_memory_mb, + 'local_gb': drv_conf.max_disk_gb, + 'vcpus_used': 0, + 'memory_mb_used': 0, + 'local_gb_used': 0, + 'hypervisor_type': self.name, + 'hypervisor_version': self.version, + 'hypervisor_hostname': nova_conf.host, + 'cpu_info': {}, + 'disk_available_least': drv_conf.max_disk_gb, + } + self._mounts = {} + self._interfaces = {} + self._uuid_to_omni_instance = {} + self._drv_nodes = None + self.flavor_info = {} + + def init_host(self, host): + """Initialize anything that is necessary for the driver to function""" + if self._drv_nodes is None: + self.set_nodes([nova_conf.host]) + args = (drv_conf.tenant_id, drv_conf.client_id, drv_conf.client_secret, + drv_conf.subscription_id) + + self.compute_client = utils.get_compute_client(*args) + self.resource_client = utils.get_resource_client(*args) + self.network_client = utils.get_network_client(*args) + self.flavor_info.update( + utils.get_vm_sizes(self.compute_client, drv_conf.region)) + LOG.info("%s driver init with %s project, %s region" % + (self.name, drv_conf.tenant_id, drv_conf.region)) + + def set_nodes(self, nodes): + """Sets Driver's node list. + It has effect on the following methods: + get_available_nodes() + get_available_resource + get_host_stats() + + """ + self._drv_nodes = nodes + + def _get_uuid_from_omni_id(self, omni_id): + m = hashlib.md5() + m.update(omni_id) + return str(uuid.UUID(bytes=m.digest(), version=4)) + + def _get_omni_name_from_instance(self, instance): + if OMNI_NAME in instance.metadata and instance.metadata[OMNI_NAME]: + return instance.metadata[OMNI_NAME] + elif instance.uuid in self._uuid_to_omni_instance: + return self._uuid_to_omni_instance[instance.uuid].name + # if none of the conditions are met we cannot map OpenStack UUID to + # Azure + raise exception.InstanceNotFound( + 'Instance %s not found' % instance.uuid) + + def list_instances(self): + """ + Return the names of all the instances known to the virtualization + layer, as a list. + """ + instances = utils.list_instances(self.compute_client, + drv_conf.resource_group) + + self._uuid_to_omni_instance.clear() + instance_names = [] + for instance in instances: + openstack_id = None + if instance.tags and 'openstack_id' in instance.tags: + openstack_id = instance.tags['openstack_id'] + if openstack_id is None: + openstack_id = self._get_uuid_from_omni_id(instance.name) + self._uuid_to_omni_instance[openstack_id] = instance + instance_names.append(instance.name) + return instance_names + + def plug_vifs(self, instance, network_info): + """Plug VIFs into networks.""" + raise NotImplementedError() + + def unplug_vifs(self, instance, network_info): + """Unplug VIFs from networks.""" + raise NotImplementedError() + + def _get_hardware_profile(self, flavor): + return {'vm_size': flavor.name} + + def _get_network_profile(self, network_info): + if not network_info: + raise exception.BuildAbortException('Network info missing') + network_profile = {'network_interfaces': []} + for net_info in network_info: + nic_name = 'nic-' + net_info['id'] + nic = utils.get_nic(self.network_client, drv_conf.resource_group, + nic_name) + network_profile['network_interfaces'].append({'id': nic.id}) + return network_profile + + def _get_storage_profile(self, instance): + # Pick up os disk name same as instance name for instance cleanup + disk_name = self._azure_instance_name(instance) + img_link = instance.system_metadata['image_azure_link'] + return { + 'os_disk': { + 'name': disk_name, + 'caching': 'None', + 'create_option': 'fromImage', + }, + 'image_reference': { + 'id': img_link, + } + } + + def _get_os_profile(self, instance, admin_password): + user = drv_conf.vm_admin_username + os_profile = { + 'computer_name': instance.hostname, + 'admin_username': drv_conf.vm_admin_username, + } + try: + img_link = instance.system_metadata['image_azure_link'] + img_name = img_link.strip('/').split('/')[-1] + image = utils.get_image(self.compute_client, + drv_conf.resource_group, img_name) + os_type = image.storage_profile.os_disk.os_type.name + except Exception as e: + LOG.exception( + "Error occurred while finding os type for image: %s" % e) + os_type = None + + key_data = instance.key_data + if key_data is None or os_type != 'linux': + os_profile['admin_password'] = admin_password + else: + ssh_key = { + 'path': '/home/{0}/.ssh/authorized_keys'.format(user), + 'key_data': key_data + } + os_profile['linux_configuration'] = { + 'ssh': { + 'public_keys': [ + ssh_key, + ] + } + } + return os_profile + + def _prepare_vm_params(self, instance, network_info, admin_password): + os_profile = self._get_os_profile(instance, admin_password) + hardware_profile = self._get_hardware_profile(instance.flavor) + storage_profile = self._get_storage_profile(instance) + network_profile = self._get_network_profile(network_info) + vm_profile = { + 'location': drv_conf.region, + 'os_profile': os_profile, + 'hardware_profile': hardware_profile, + 'storage_profile': storage_profile, + 'network_profile': network_profile + } + return vm_profile + + def _azure_instance_name(self, instance): + return 'inst-' + instance.uuid + + def spawn(self, + context, + instance, + image_meta, + injected_files, + admin_password, + network_info=None, + block_device_info=None): + """Create a new instance/VM/domain on the virtualization platform. + Once this successfully completes, the instance should be + running (power_state.RUNNING). If this fails, any partial instance + should be completely cleaned up, and the virtualization platform should + be in the state that it was before this call began. + + :param context: security context + :param instance: nova.objects.instance.Instance + This function should use the data there to guide + the creation of the new instance. + :param image_meta: image object returned by nova.image.glance that + defines the image from which to boot this instance + :param injected_files: User files to inject into instance. + :param admin_password: set in instance. + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param block_device_info: Information about block devices to be + attached to the instance. + """ + vm_params = self._prepare_vm_params(instance, network_info, + admin_password) + name = self._azure_instance_name(instance) + LOG.info("Spawning vm %s with params %s" % (name, vm_params)) + utils.create_or_update_instance( + self.compute_client, drv_conf.resource_group, name, vm_params) + tags = { + 'location': drv_conf.region, + 'tags': { + 'openstack_id': instance.uuid, + 'openstack_project_id': context.project_id, + 'openstack_user_id': context.user_id + } + } + utils.create_or_update_instance(self.compute_client, + drv_conf.resource_group, name, tags) + az_instance = utils.get_instance(self.compute_client, + drv_conf.resource_group, name) + self._uuid_to_omni_instance[instance.uuid] = az_instance + instance.metadata.update({ + OMNI_NAME: name, + constants.OMNI_ID: az_instance.id + }) + + def snapshot(self, context, instance, image_id, update_task_state): + """Snapshot an image of the specified instance + + :param context: security context + :param instance: nova.objects.instance.Instance + :param image_id: Reference to a pre-created image holding the snapshot. + + """ + raise NotImplementedError() + + def reboot(self, + context, + instance, + network_info, + reboot_type, + block_device_info=None, + bad_volumes_callback=None): + """Reboot the specified instance. After this is called successfully, + the instance's state goes back to power_state.RUNNING. The + virtualization platform should ensure that the reboot action has + completed successfully even in cases in which the underlying domain/vm + completed successfully even in cases in which the underlying domain/vm + is paused or halted/stopped. + + :param instance: nova.objects.instance.Instance + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param reboot_type: Either a HARD or SOFT reboot + :param block_device_info: Info pertaining to attached volumes + :param bad_volumes_callback: Function to handle any bad volumes + encountered + """ + azure_name = self._get_omni_name_from_instance(instance) + utils.restart_instance(self.compute_client, drv_conf.resource_group, + azure_name) + + @staticmethod + def get_host_ip_addr(): + """Retrieves the IP address of the host""" + return nova_conf.my_ip + + def set_admin_password(self, instance, new_pass): + """Set root password on specified instance""" + raise NotImplementedError() + + def inject_file(self, instance, b64_path, b64_contents): + raise NotImplementedError() + + def resume_state_on_host_boot(self, + context, + instance, + network_info, + block_device_info=None): + raise NotImplementedError() + + def rescue(self, context, instance, network_info, image_meta, + rescue_password): + raise NotImplementedError() + + def unrescue(self, instance, network_info): + raise NotImplementedError() + + def poll_rebooting_instances(self, timeout, instances): + raise NotImplementedError() + + def migrate_disk_and_power_off(self, + context, + instance, + dest, + instance_type, + network_info, + block_device_info=None): + raise NotImplementedError() + + def finish_revert_migration(self, + context, + instance, + network_info, + block_device_info=None, + power_on=True): + raise NotImplementedError() + + def post_live_migration_at_destination(self, + context, + instance, + network_info, + block_migration=False, + block_device_info=None): + raise NotImplementedError() + + def power_off(self, instance, timeout=0, retry_interval=0): + """Power off the specified instance. + + :param instance: nova.objects.instance.Instance + :param timeout: time to wait for GuestOS to shutdown + :param retry_interval: How often to signal guest while + waiting for it to shutdown + """ + azure_name = self._get_omni_name_from_instance(instance) + utils.stop_instance(self.compute_client, drv_conf.resource_group, + azure_name) + + def power_on(self, context, instance, network_info, block_device_info): + """Power on the specified instance.""" + azure_name = self._get_omni_name_from_instance(instance) + utils.start_instance(self.compute_client, drv_conf.resource_group, + azure_name) + + def soft_delete(self, instance): + """Deleting the specified instance""" + self.destroy(instance) + + def restore(self, instance): + raise NotImplementedError() + + def pause(self, instance): + """ + Azure doesn't support pause and cannot save system state and hence + we've implemented the closest functionality which is to poweroff the + instance. + + :param instance: nova.objects.instance.Instance + """ + self.power_off(instance) + + def unpause(self, instance): + """ + Since Azure doesn't support pause and cannot save system state, we + had implemented the closest functionality which is to poweroff the + instance. and powering on such an instance in this method. + + :param instance: nova.objects.instance.Instance + """ + self.power_on( + context=None, + instance=instance, + network_info=None, + block_device_info=None) + + def suspend(self, context, instance): + """ + Azure doesn't support suspend and cannot save system state and hence + Azure doesn't support suspend and cannot save system state and hence + we've implemented the closest functionality which is to poweroff the + instance. + + :param instance: nova.objects.instance.Instance + """ + LOG.info("Suspending instance %s" % instance.uuid) + self.power_off(instance) + + def resume(self, context, instance, network_info, block_device_info=None): + """ + Since Azure doesn't support resume and we cannot save system state, + Since Azure doesn't support resume and we cannot save system state, + we've implemented the closest functionality which is to power on the + instance. + + :param instance: nova.objects.instance.Instance + """ + LOG.info("Resuming instance %s" % instance.uuid) + self.power_on(context, instance, network_info, block_device_info) + + def destroy(self, + context, + instance, + network_info, + block_device_info=None, + destroy_disks=True, + migrate_data=None): + """Destroy the specified instance from the Hypervisor. + If the instance is not found (for example if networking failed), this + function should still succeed. It's probably a good idea to log a + warning in that case. + + :param context: security context + :param instance: Instance object as returned by DB layer. + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param block_device_info: Information about block devices that should + be detached from the instance. + :param destroy_disks: Indicates if disks should be destroyed + :param migrate_data: implementation specific params + """ + LOG.info("Destroying instance %s" % instance.uuid) + try: + azure_name = self._get_omni_name_from_instance(instance) + except exception.InstanceNotFound: + LOG.error( + "Unable to find Azure mapping for instance %s" % instance.uuid) + return + try: + utils.delete_instance(self.compute_client, drv_conf.resource_group, + azure_name) + except utils.CloudError: + LOG.error( + "Instance %s not found in Azure, removing from openstack." % + (instance.uuid, )) + # Delete disk handles exception if disk not found + utils.delete_disk(self.compute_client, drv_conf.resource_group, + azure_name) + LOG.info("Destroy complete %s" % instance.uuid) + + def attach_volume(self, + context, + connection_info, + instance, + mountpoint, + disk_bus=None, + device_type=None, + encryption=None): + """Attach the disk to the instance at mountpoint using info.""" + raise NotImplementedError() + + def detach_volume(self, + connection_info, + instance, + mountpoint, + encryption=None): + """Detach the disk attached to the instance.""" + raise NotImplementedError() + + def swap_volume(self, old_connection_info, new_connection_info, instance, + mountpoint, resize_to): + """Replace the disk attached to the instance.""" + raise NotImplementedError() + + def attach_interface(self, instance, image_meta, vif): + raise NotImplementedError() + + def detach_interface(self, instance, vif): + raise NotImplementedError() + + def _get_power_state(self, azure_instance): + statuses = azure_instance.instance_view.statuses + state = constants.power_state.NOSTATE + for i in statuses: + if hasattr(i, 'code') and 'PowerState' in i.code: + state = constants.OMNI_STATE_MAP[i.code] + return state + + def get_info(self, instance): + """Get the current status of an instance, by name (not ID!) + + :param instance: nova.objects.instance.Instance object + Returns a dict containing: + :state: the running state, one of the power_state codes + :max_mem: (int) the maximum memory in KBytes allowed + :mem: (int) the memory in KBytes used by the domain + :num_cpu: (int) the number of virtual CPUs for the domain + :cpu_time: (int) the CPU time used in nanoseconds + """ + + azure_name = self._get_omni_name_from_instance(instance) + azure_instance = utils.get_instance( + self.compute_client, drv_conf.resource_group, azure_name) + state = self._get_power_state(azure_instance) + flavor = self.flavor_info[instance.flavor.name] + memory = flavor.memory_in_mb * 1024 + cpus = flavor.number_of_cores + return hardware.InstanceInfo( + state=state, + max_mem_kb=memory, + mem_kb=memory, + num_cpu=cpus, + cpu_time_ns=0, + id=instance.id) + + def allow_key(self, key): + DIAGNOSTIC_KEYS_TO_FILTER = ['group', 'block_device_mapping'] + if key in DIAGNOSTIC_KEYS_TO_FILTER: + return False + return True + + def get_diagnostics(self, instance): + """Return data about VM diagnostics.""" + # Fake diagnostics + return { + 'cpu0_time': 17300000000, + 'memory': 524288, + 'vda_errors': -1, + 'vda_read': 262144, + 'vda_read_req': 112, + 'vda_write': 5778432, + 'vda_write_req': 488, + 'vnet1_rx': 2070139, + 'vnet1_rx_drop': 0, + 'vnet1_rx_errors': 0, + 'vnet1_rx_packets': 26701, + 'vnet1_tx': 140208, + 'vnet1_tx_drop': 0, + 'vnet1_tx_errors': 0, + 'vnet1_tx_packets': 662, + } + + def get_all_bw_counters(self, instances): + """Return bandwidth usage counters for each interface on each + running VM. + """ + + bw = [] + return bw + + def get_all_volume_usage(self, context, compute_host_bdms): + """Return usage info for volumes attached to vms on a given host.""" + volusage = [] + return volusage + + def block_stats(self, instance_name, disk_id): + return [0L, 0L, 0L, 0L, None] + + def interface_stats(self, instance_name, iface_id): + return [0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L] + + def get_vnc_console(self, context, instance): + raise NotImplementedError() + + def get_spice_console(self, instance): + """Simple Protocol for Independent Computing Environments""" + raise NotImplementedError() + + def get_console_pool_info(self, console_type): + raise NotImplementedError() + + def refresh_provider_fw_rules(self): + raise NotImplementedError() + + def get_available_resource(self, nodename): + """Retrieve resource information. Updates compute manager resource info + on ComputeNode table. This method is called when nova-compute launches + and as part of a periodic task that records results in the DB. Without + real hypervisor, pretend we have lots of disk and ram. + + :param nodename: node which the caller want to get resources from + a driver that manages only one node can safely ignore this + :returns: Dictionary describing resources + """ + if nodename not in self._drv_nodes: + return {} + supported_tuple = ('IA64', 'kvm', 'hvm') + return { + 'vcpus': drv_conf.max_vcpus, + 'memory_mb': drv_conf.max_memory_mb, + 'local_gb': drv_conf.max_disk_gb, + 'vcpus_used': 0, + 'memory_mb_used': 0, + 'local_gb_used': 0, + 'hypervisor_type': self.name, + 'hypervisor_version': '1', + 'hypervisor_hostname': nodename, + 'disk_available_least': 0, + 'cpu_info': '?', + 'numa_topology': None, + 'supported_instances': [supported_tuple] + } + + def ensure_filtering_rules_for_instance(self, instance_ref, network_info): + return + + def get_instance_disk_info(self, instance_name): + return + + def live_migration(self, + context, + instance_ref, + dest, + post_method, + recover_method, + block_migration=False, + migrate_data=None): + post_method(context, instance_ref, dest, block_migration, migrate_data) + return + + def check_can_live_migrate_destination_cleanup(self, ctxt, + dest_check_data): + return + + def check_can_live_migrate_destination(self, + ctxt, + instance_ref, + src_compute_info, + dst_compute_info, + block_migration=False, + disk_over_commit=False): + return {} + + def check_can_live_migrate_source(self, ctxt, instance_ref, + dest_check_data): + return + + def finish_migration(self, + context, + migration, + instance, + disk_info, + network_info, + image_meta, + resize_instance, + block_device_info=None, + power_on=True): + """Completes a resize + + :param migration: the migrate/resize information + :param instance: nova.objects.instance.Instance being migrated/resized + :param power_on: is True the instance should be powered on + """ + raise NotImplementedError() + + def confirm_migration(self, migration, instance, network_info): + """Confirms a resize, destroying the source VM. + + :param instance: nova.objects.instance.Instance + """ + raise NotImplementedError() + + def pre_live_migration(self, + context, + instance_ref, + block_device_info, + network_info, + disk, + migrate_data=None): + return + + def unfilter_instance(self, instance_ref, network_info): + return + + def get_host_stats(self, refresh=False): + """Return Azure Host Status of name, ram, disk, network.""" + stats = [] + for nodename in self._drv_nodes: + host_status = self.host_status_base.copy() + host_status['hypervisor_hostname'] = nodename + host_status['host_hostname'] = nodename + host_status['host_name_label'] = nodename + host_status['hypervisor_type'] = self.name + host_status['vcpus'] = drv_conf.max_vcpus + host_status['memory_mb'] = drv_conf.max_memory_mb + host_status['local_gb'] = drv_conf.max_disk_gb + stats.append(host_status) + if len(stats) == 0: + raise exception.NovaException("Azure Driver has no node") + elif len(stats) == 1: + return stats[0] + else: + return stats + + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + return action + + def host_maintenance_mode(self, host, mode): + """Start/Stop host maintenance window. On start, it triggers + guest VMs evacuation. + """ + if not mode: + return 'off_maintenance' + return 'on_maintenance' + + def set_host_enabled(self, host, enabled): + """Sets the specified host's ability to accept new instances.""" + if enabled: + return 'enabled' + return 'disabled' + + def get_disk_available_least(self): + raise NotImplementedError() + + def add_to_aggregate(self, context, aggregate, host, **kwargs): + raise NotImplementedError() + + def remove_from_aggregate(self, context, aggregate, host, **kwargs): + raise NotImplementedError() + + def get_volume_connector(self, instance): + return { + 'ip': '127.0.0.1', + 'initiator': self.name, + 'host': '%shost' % self.name + } + + def get_available_nodes(self, refresh=False): + return self._drv_nodes + + def instance_on_disk(self, instance): + return False + + def list_instance_uuids(self, node=None, template_uuids=None, force=False): + self.list_instances() + return self._uuid_to_omni_instance.keys() diff --git a/nova/virt/azure/utils.py b/nova/virt/azure/utils.py new file mode 100644 index 0000000..cfdc1f2 --- /dev/null +++ b/nova/virt/azure/utils.py @@ -0,0 +1,133 @@ +""" +Copyright (c) 2017 Platform9 Systems Inc. +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the +License for the specific language governing permissions and limitations +under the License. +""" + +from functools import partial + +from azure.common.credentials import ServicePrincipalCredentials +from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.network import NetworkManagementClient +from azure.mgmt.resource import ResourceManagementClient +from msrestazure.azure_exceptions import CloudError +from oslo_log import log as logging + +from nova import exception + +LOG = logging.getLogger(__name__) + + +def get_credentials(tenant_id, client_id, client_secret): + credentials = ServicePrincipalCredentials( + client_id=client_id, secret=client_secret, tenant=tenant_id) + return credentials + + +def _get_client(tenant_id, client_id, client_secret, subscription_id, + cls=None): + """Returns Azure compute resource object for interacting with Azure API + + :param tenant_id: string, tenant_id from azure account + :param client_id: string, client_id (application id) + :param client_secret: string, secret key of application + :param subscription_id: string, unique identification id of account + :return: :class:`Resource ` object + """ + credentials = get_credentials(tenant_id, client_id, client_secret) + client = cls(credentials, subscription_id) + return client + + +get_compute_client = partial(_get_client, cls=ComputeManagementClient) +get_resource_client = partial(_get_client, cls=ResourceManagementClient) +get_network_client = partial(_get_client, cls=NetworkManagementClient) + + +def azure_handle_exception(fn): + def wrapper(*args, **kwargs): + try: + return fn(*args, **kwargs) + except Exception as e: + LOG.exception("Exception occurred in Azure operation: %s" % + (e.message)) + + return wrapper + + +def _perform_and_wait(operation, args=(), kwargs={}, timeout=300): + operation(*args, **kwargs).wait(timeout=timeout) + + +def get_vm_sizes(compute, region): + vmsize_dict = {} + for i in compute.virtual_machine_sizes.list(location=region): + vmsize_dict[i.name] = i + return vmsize_dict + + +def list_instances(compute, resource_group): + """Returns list of Azure instance resources for specified resource_group + + :param compute: Azure object using ComputeManagementClient + :param resource_group: string, name of Azure resource group + :return: list of Azure VMs in resource_group + :rtype: list + """ + return compute.virtual_machines.list(resource_group) + + +def get_instance(compute, resource_group, instance_name): + """Get Azure instance information + + :param compute: Azure object using ComputeManagementClient + :param resource_group: string, name of Azure resource group + :param instance_name: string, name of Azure instance + """ + return compute.virtual_machines.get( + resource_group, instance_name, expand='instanceView') + + +def get_nic(network, resource_group, name): + try: + return network.network_interfaces.get(resource_group, name) + except CloudError: + raise exception.PortNotFound(port_id=name) + + +def create_or_update_instance(compute, resource_group, name, body): + _perform_and_wait(compute.virtual_machines.create_or_update, + (resource_group, name, body)) + + +def delete_instance(compute, resource_group, name): + _perform_and_wait(compute.virtual_machines.delete, (resource_group, name)) + + +def restart_instance(compute, resource_group, name): + _perform_and_wait(compute.virtual_machines.restart, (resource_group, name)) + + +def start_instance(compute, resource_group, name): + _perform_and_wait(compute.virtual_machines.start, (resource_group, name)) + + +def stop_instance(compute, resource_group, name): + _perform_and_wait(compute.virtual_machines.power_off, (resource_group, + name)) + + +def get_image(compute, resource_group, name): + return compute.images.get(resource_group, name) + + +@azure_handle_exception +def delete_disk(compute, resource_group, name): + return compute.disks.delete(resource_group, name)