# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # Copyright (c) 2012 VMware, Inc. # Copyright (c) 2011 Citrix Systems, Inc. # Copyright 2011 OpenStack Foundation # # 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. """ A connection to the VMware vCenter platform. """ import os import re import os_resource_classes as orc from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import units from oslo_utils import versionutils as v_utils from oslo_vmware import api from oslo_vmware import exceptions as vexc from oslo_vmware import pbm from oslo_vmware import vim from oslo_vmware import vim_util from nova.compute import power_state from nova.compute import task_states from nova.compute import utils as compute_utils import nova.conf from nova import exception from nova.i18n import _ from nova import objects import nova.privsep.path from nova.virt import driver from nova.virt.vmwareapi import constants from nova.virt.vmwareapi import ds_util from nova.virt.vmwareapi import error_util from nova.virt.vmwareapi import host from nova.virt.vmwareapi import vim_util as nova_vim_util from nova.virt.vmwareapi import vm_util from nova.virt.vmwareapi import vmops from nova.virt.vmwareapi import volumeops LOG = logging.getLogger(__name__) CONF = nova.conf.CONF TIME_BETWEEN_API_CALL_RETRIES = 1.0 MAX_CONSOLE_BYTES = 100 * units.Ki class VMwareVCDriver(driver.ComputeDriver): """The VC host connection object.""" capabilities = { "has_imagecache": True, "supports_evacuate": False, "supports_migrate_to_same_host": True, "supports_attach_interface": True, "supports_multiattach": False, "supports_trusted_certs": False, "supports_pcpus": False, "supports_accelerators": False, # Image type support flags "supports_image_type_aki": False, "supports_image_type_ami": False, "supports_image_type_ari": False, "supports_image_type_iso": True, "supports_image_type_qcow2": False, "supports_image_type_raw": False, "supports_image_type_vdi": False, "supports_image_type_vhd": False, "supports_image_type_vhdx": False, "supports_image_type_vmdk": True, "supports_image_type_ploop": False, } # Legacy nodename is of the form: () # e.g. domain-26(TestCluster) # We assume consists of alphanumeric, _ and -. # We assume cluster name is everything between the first ( and the last ). # We pull out for re-use. LEGACY_NODENAME = re.compile(r'([\w-]+)\(.+\)') # The vCenter driver includes API that acts on ESX hosts or groups # of ESX hosts in clusters or non-cluster logical-groupings. # # vCenter is not a hypervisor itself, it works with multiple # hypervisor host machines and their guests. This fact can # subtly alter how vSphere and OpenStack interoperate. def __init__(self, virtapi, scheme="https"): super(VMwareVCDriver, self).__init__(virtapi) if (CONF.vmware.host_ip is None or CONF.vmware.host_username is None or CONF.vmware.host_password is None): raise Exception(_("Must specify host_ip, host_username and " "host_password to use vmwareapi.VMwareVCDriver")) if CONF.vnc.keymap: LOG.warning('The option "[vnc] keymap" has been deprecated in ' 'favor of the VMWare-specific "[vmware] vnc_keymap" ' 'option. Please update nova.conf to address this ' 'change') self._datastore_regex = None if CONF.vmware.datastore_regex: try: self._datastore_regex = re.compile(CONF.vmware.datastore_regex) except re.error: raise exception.InvalidInput(reason= _("Invalid Regular Expression %s") % CONF.vmware.datastore_regex) self._session = VMwareAPISession(scheme=scheme) self._check_min_version() # Update the PBM location if necessary if CONF.vmware.pbm_enabled: self._update_pbm_location() self._validate_configuration() self._cluster_name = CONF.vmware.cluster_name self._cluster_ref = vm_util.get_cluster_ref_by_name(self._session, self._cluster_name) if self._cluster_ref is None: raise exception.NotFound(_("The specified cluster '%s' was not " "found in vCenter") % self._cluster_name) self._vcenter_uuid = self._get_vcenter_uuid() self._nodename = self._create_nodename(self._cluster_ref.value) self._volumeops = volumeops.VMwareVolumeOps(self._session, self._cluster_ref) self._vmops = vmops.VMwareVMOps(self._session, virtapi, self._volumeops, self._cluster_ref, datastore_regex=self._datastore_regex) self._vc_state = host.VCState(self._session, self._nodename, self._cluster_ref, self._datastore_regex) # Register the OpenStack extension self._register_openstack_extension() def _check_min_version(self): min_version = v_utils.convert_version_to_int(constants.MIN_VC_VERSION) next_min_ver = v_utils.convert_version_to_int( constants.NEXT_MIN_VC_VERSION) vc_version = vim_util.get_vc_version(self._session) LOG.info("VMware vCenter version: %s", vc_version) if v_utils.convert_version_to_int(vc_version) < min_version: raise exception.NovaException( _('Detected vCenter version %(version)s. Nova requires VMware ' 'vCenter version %(min_version)s or greater.') % { 'version': vc_version, 'min_version': constants.MIN_VC_VERSION}) elif v_utils.convert_version_to_int(vc_version) < next_min_ver: LOG.warning('Running Nova with a VMware vCenter version less ' 'than %(version)s is deprecated. The required ' 'minimum version of vCenter will be raised to ' '%(version)s in the 16.0.0 release.', {'version': constants.NEXT_MIN_VC_VERSION}) @property def need_legacy_block_device_info(self): return False def _update_pbm_location(self): if CONF.vmware.pbm_wsdl_location: pbm_wsdl_loc = CONF.vmware.pbm_wsdl_location else: version = vim_util.get_vc_version(self._session) pbm_wsdl_loc = pbm.get_pbm_wsdl_location(version) self._session.pbm_wsdl_loc_set(pbm_wsdl_loc) def _validate_configuration(self): if CONF.vmware.pbm_enabled: if not CONF.vmware.pbm_default_policy: raise error_util.PbmDefaultPolicyUnspecified() if not pbm.get_profile_id_by_name( self._session, CONF.vmware.pbm_default_policy): raise error_util.PbmDefaultPolicyDoesNotExist() if CONF.vmware.datastore_regex: LOG.warning("datastore_regex is ignored when PBM is enabled") self._datastore_regex = None def init_host(self, host): LOG.warning('The vmwareapi driver is deprecated and may be removed in ' 'a future release. The driver is not tested by the ' 'OpenStack project nor does it have clear maintainer(s) ' 'and thus its quality can not be ensured. If you are ' 'using the driver in production please let us know in ' 'freenode IRC and/or the openstack-discuss mailing list.') vim = self._session.vim if vim is None: self._session._create_session() def cleanup_host(self, host): self._session.logout() def _register_openstack_extension(self): # Register an 'OpenStack' extension in vCenter os_extension = self._session._call_method(vim_util, 'find_extension', constants.EXTENSION_KEY) if os_extension is None: try: self._session._call_method(vim_util, 'register_extension', constants.EXTENSION_KEY, constants.EXTENSION_TYPE_INSTANCE) LOG.info('Registered extension %s with vCenter', constants.EXTENSION_KEY) except vexc.VimFaultException as e: with excutils.save_and_reraise_exception() as ctx: if 'InvalidArgument' in e.fault_list: LOG.debug('Extension %s already exists.', constants.EXTENSION_KEY) ctx.reraise = False else: LOG.debug('Extension %s already exists.', constants.EXTENSION_KEY) def cleanup(self, context, instance, network_info, block_device_info=None, destroy_disks=True, migrate_data=None, destroy_vifs=True): """Cleanup after instance being destroyed by Hypervisor.""" pass def resume_state_on_host_boot(self, context, instance, network_info, block_device_info=None): """resume guest state when a host is booted.""" # Check if the instance is running already and avoid doing # anything if it is. state = vm_util.get_vm_state(self._session, instance) ignored_states = [power_state.RUNNING, power_state.SUSPENDED] if state in ignored_states: return # Instance is not up and could be in an unknown state. # Be as absolute as possible about getting it back into # a known and running state. self.reboot(context, instance, network_info, 'hard', block_device_info) def list_instance_uuids(self): """List VM instance UUIDs.""" return self._vmops.list_instances() def list_instances(self): """List VM instances from the single compute node.""" return self._vmops.list_instances() def migrate_disk_and_power_off(self, context, instance, dest, flavor, network_info, block_device_info=None, timeout=0, retry_interval=0): """Transfers the disk of a running instance in multiple phases, turning off the instance before the end. """ # TODO(PhilDay): Add support for timeout (clean shutdown) return self._vmops.migrate_disk_and_power_off(context, instance, dest, flavor) def confirm_migration(self, context, migration, instance, network_info): """Confirms a resize, destroying the source VM.""" self._vmops.confirm_migration(migration, instance, network_info) def finish_revert_migration(self, context, instance, network_info, migration, block_device_info=None, power_on=True): """Finish reverting a resize, powering back on the instance.""" self._vmops.finish_revert_migration(context, instance, network_info, block_device_info, power_on) def finish_migration(self, context, migration, instance, disk_info, network_info, image_meta, resize_instance, allocations, block_device_info=None, power_on=True): """Completes a resize, turning on the migrated instance.""" self._vmops.finish_migration(context, migration, instance, disk_info, network_info, image_meta, resize_instance, block_device_info, power_on) def pre_live_migration(self, context, instance, block_device_info, network_info, disk_info, migrate_data): return migrate_data def post_live_migration_at_source(self, context, instance, network_info): pass def post_live_migration_at_destination(self, context, instance, network_info, block_migration=False, block_device_info=None): pass def cleanup_live_migration_destination_check(self, context, dest_check_data): pass def live_migration(self, context, instance, dest, post_method, recover_method, block_migration=False, migrate_data=None): """Live migration of an instance to another host.""" self._vmops.live_migration(context, instance, dest, post_method, recover_method, block_migration, migrate_data) def check_can_live_migrate_source(self, context, instance, dest_check_data, block_device_info=None): cluster_name = dest_check_data.cluster_name cluster_ref = vm_util.get_cluster_ref_by_name(self._session, cluster_name) if cluster_ref is None: msg = (_("Cannot find destination cluster %s for live migration") % cluster_name) raise exception.MigrationPreCheckError(reason=msg) res_pool_ref = vm_util.get_res_pool_ref(self._session, cluster_ref) if res_pool_ref is None: msg = _("Cannot find destination resource pool for live migration") raise exception.MigrationPreCheckError(reason=msg) return dest_check_data def check_can_live_migrate_destination(self, context, instance, src_compute_info, dst_compute_info, block_migration=False, disk_over_commit=False): # the information that we need for the destination compute node # is the name of its cluster and datastore regex data = objects.VMwareLiveMigrateData() data.cluster_name = CONF.vmware.cluster_name data.datastore_regex = CONF.vmware.datastore_regex return data def rollback_live_migration_at_destination(self, context, instance, network_info, block_device_info, destroy_disks=True, migrate_data=None): """Clean up destination node after a failed live migration.""" self.destroy(context, instance, network_info, block_device_info) def get_instance_disk_info(self, instance, block_device_info=None): pass def get_vnc_console(self, context, instance): """Return link to instance's VNC console using vCenter logic.""" # vCenter does not actually run the VNC service # itself. You must talk to the VNC host underneath vCenter. return self._vmops.get_vnc_console(instance) def get_mks_console(self, context, instance): return self._vmops.get_mks_console(instance) def get_console_output(self, context, instance): if not CONF.vmware.serial_log_dir: LOG.error("The 'serial_log_dir' config option is not set!") return fname = instance.uuid.replace('-', '') path = os.path.join(CONF.vmware.serial_log_dir, fname) if not os.path.exists(path): LOG.warning('The console log is missing. Check your VSPC ' 'configuration', instance=instance) return b"" read_log_data, remaining = nova.privsep.path.last_bytes( path, MAX_CONSOLE_BYTES) return read_log_data def _get_vcenter_uuid(self): """Retrieves the vCenter UUID.""" about = self._session._call_method(nova_vim_util, 'get_about_info') return about.instanceUuid def _create_nodename(self, mo_id): """Return a nodename which uniquely describes a cluster. The name will be of the form: . e.g. domain-26.9d51f082-58a4-4449-beed-6fd205a5726b """ return '%s.%s' % (mo_id, self._vcenter_uuid) def _get_available_resources(self, host_stats): return {'vcpus': host_stats['vcpus'], 'memory_mb': host_stats['host_memory_total'], 'local_gb': host_stats['disk_total'], 'vcpus_used': 0, 'memory_mb_used': host_stats['host_memory_total'] - host_stats['host_memory_free'], 'local_gb_used': host_stats['disk_used'], 'hypervisor_type': host_stats['hypervisor_type'], 'hypervisor_version': host_stats['hypervisor_version'], 'hypervisor_hostname': host_stats['hypervisor_hostname'], # The VMWare driver manages multiple hosts, so there are # likely many different CPU models in use. As such it is # impossible to provide any meaningful info on the CPU # model of the "host" 'cpu_info': None, 'supported_instances': host_stats['supported_instances'], 'numa_topology': None, } def get_available_resource(self, nodename): """Retrieve resource info. This method is called when nova-compute launches, and as part of a periodic task. :returns: dictionary describing resources """ host_stats = self._vc_state.get_host_stats(refresh=True) stats_dict = self._get_available_resources(host_stats) return stats_dict def get_available_nodes(self, refresh=False): """Returns nodenames of all nodes managed by the compute service. This driver supports only one compute node. """ return [self._nodename] def update_provider_tree(self, provider_tree, nodename, allocations=None): """Update a ProviderTree object with current resource provider, inventory information and CPU traits. :param nova.compute.provider_tree.ProviderTree provider_tree: A nova.compute.provider_tree.ProviderTree object representing all the providers in the tree associated with the compute node, and any sharing providers (those with the ``MISC_SHARES_VIA_AGGREGATE`` trait) associated via aggregate with any of those providers (but not *their* tree- or aggregate-associated providers), as currently known by placement. :param nodename: String name of the compute node (i.e. ComputeNode.hypervisor_hostname) for which the caller is requesting updated provider information. :param allocations: Dict of allocation data of the form: { $CONSUMER_UUID: { # The shape of each "allocations" dict below is identical # to the return from GET /allocations/{consumer_uuid} "allocations": { $RP_UUID: { "generation": $RP_GEN, "resources": { $RESOURCE_CLASS: $AMOUNT, ... }, }, ... }, "project_id": $PROJ_ID, "user_id": $USER_ID, "consumer_generation": $CONSUMER_GEN, }, ... } If None, and the method determines that any inventory needs to be moved (from one provider to another and/or to a different resource class), the ReshapeNeeded exception must be raised. Otherwise, this dict must be edited in place to indicate the desired final state of allocations. :raises ReshapeNeeded: If allocations is None and any inventory needs to be moved from one provider to another and/or to a different resource class. At this time the VMware driver does not reshape. :raises: ReshapeFailed if the requested tree reshape fails for whatever reason. """ # NOTE(cdent): This is a side-effecty method, we are changing the # the provider tree in place (on purpose). inv = provider_tree.data(nodename).inventory ratios = self._get_allocation_ratios(inv) stats = vm_util.get_stats_from_cluster(self._session, self._cluster_ref) datastores = ds_util.get_available_datastores(self._session, self._cluster_ref, self._datastore_regex) total_disk_capacity = sum([ds.capacity for ds in datastores]) max_free_space = max([ds.freespace for ds in datastores]) reserved_disk_gb = compute_utils.convert_mb_to_ceil_gb( CONF.reserved_host_disk_mb) result = { orc.VCPU: { 'total': stats['cpu']['vcpus'], 'reserved': CONF.reserved_host_cpus, 'min_unit': 1, 'max_unit': stats['cpu']['max_vcpus_per_host'], 'step_size': 1, 'allocation_ratio': ratios[orc.VCPU], }, orc.MEMORY_MB: { 'total': stats['mem']['total'], 'reserved': CONF.reserved_host_memory_mb, 'min_unit': 1, 'max_unit': stats['mem']['max_mem_mb_per_host'], 'step_size': 1, 'allocation_ratio': ratios[orc.MEMORY_MB], }, } # If a sharing DISK_GB provider exists in the provider tree, then our # storage is shared, and we should not report the DISK_GB inventory in # the compute node provider. # TODO(cdent): We don't do this yet, in part because of the issues # in bug #1784020, but also because we can represent all datastores # as shared providers and should do once update_provider_tree is # working well. if provider_tree.has_sharing_provider(orc.DISK_GB): LOG.debug('Ignoring sharing provider - see bug #1784020') result[orc.DISK_GB] = { 'total': total_disk_capacity // units.Gi, 'reserved': reserved_disk_gb, 'min_unit': 1, 'max_unit': max_free_space // units.Gi, 'step_size': 1, 'allocation_ratio': ratios[orc.DISK_GB], } provider_tree.update_inventory(nodename, result) # TODO(cdent): Here is where additional functionality would be added. # In the libvirt driver this is where nested GPUs are reported and # where cpu traits are added. In the vmware world, this is where we # would add nested providers representing tenant VDC and similar. def spawn(self, context, instance, image_meta, injected_files, admin_password, allocations, network_info=None, block_device_info=None, power_on=True, accel_info=None): """Create VM instance.""" self._vmops.spawn(context, instance, image_meta, injected_files, admin_password, network_info, block_device_info) def attach_volume(self, context, connection_info, instance, mountpoint, disk_bus=None, device_type=None, encryption=None): """Attach volume storage to VM instance.""" return self._volumeops.attach_volume(connection_info, instance) def detach_volume(self, context, connection_info, instance, mountpoint, encryption=None): """Detach volume storage to VM instance.""" # NOTE(claudiub): if context parameter is to be used in the future, # the _detach_instance_volumes method will have to be updated as well. return self._volumeops.detach_volume(connection_info, instance) def get_volume_connector(self, instance): """Return volume connector information.""" return self._volumeops.get_volume_connector(instance) def get_host_ip_addr(self): """Returns the IP address of the vCenter host.""" return CONF.vmware.host_ip def snapshot(self, context, instance, image_id, update_task_state): """Create snapshot from a running VM instance.""" self._vmops.snapshot(context, instance, image_id, update_task_state) def reboot(self, context, instance, network_info, reboot_type, block_device_info=None, bad_volumes_callback=None, accel_info=None): """Reboot VM instance.""" self._vmops.reboot(instance, network_info, reboot_type) def _detach_instance_volumes(self, instance, block_device_info): # We need to detach attached volumes block_device_mapping = driver.block_device_info_get_mapping( block_device_info) if block_device_mapping: # Certain disk types, for example 'IDE' do not support hot # plugging. Hence we need to power off the instance and update # the instance state. self._vmops.power_off(instance) for disk in block_device_mapping: connection_info = disk['connection_info'] try: # NOTE(claudiub): Passing None as the context, as it is # not currently used. self.detach_volume(None, connection_info, instance, disk.get('device_name')) except exception.DiskNotFound: LOG.warning('The volume %s does not exist!', disk.get('device_name'), instance=instance) except Exception as e: with excutils.save_and_reraise_exception(): LOG.error("Failed to detach %(device_name)s. " "Exception: %(exc)s", {'device_name': disk.get('device_name'), 'exc': e}, instance=instance) def destroy(self, context, instance, network_info, block_device_info=None, destroy_disks=True): """Destroy VM instance.""" # Destroy gets triggered when Resource Claim in resource_tracker # is not successful. When resource claim is not successful, # node is not set in instance. Perform destroy only if node is set if not instance.node: return # A resize uses the same instance on the VC. We do not delete that # VM in the event of a revert if instance.task_state == task_states.RESIZE_REVERTING: return # We need to detach attached volumes if block_device_info is not None: try: self._detach_instance_volumes(instance, block_device_info) except vexc.ManagedObjectNotFoundException: LOG.warning('Instance does not exists. Proceeding to ' 'delete instance properties on datastore', instance=instance) self._vmops.destroy(instance, destroy_disks) def pause(self, instance): """Pause VM instance.""" self._vmops.pause(instance) def unpause(self, instance): """Unpause paused VM instance.""" self._vmops.unpause(instance) def suspend(self, context, instance): """Suspend the specified instance.""" self._vmops.suspend(instance) def resume(self, context, instance, network_info, block_device_info=None): """Resume the suspended VM instance.""" self._vmops.resume(instance) def rescue(self, context, instance, network_info, image_meta, rescue_password, block_device_info): """Rescue the specified instance.""" self._vmops.rescue(context, instance, network_info, image_meta) def unrescue(self, instance, network_info): """Unrescue the specified instance.""" self._vmops.unrescue(instance) def power_off(self, instance, timeout=0, retry_interval=0): """Power off the specified instance.""" self._vmops.power_off(instance, timeout, retry_interval) def power_on(self, context, instance, network_info, block_device_info=None, accel_info=None): """Power on the specified instance.""" self._vmops.power_on(instance) def poll_rebooting_instances(self, timeout, instances): """Poll for rebooting instances.""" self._vmops.poll_rebooting_instances(timeout, instances) def get_info(self, instance, use_cache=True): """Return info about the VM instance.""" return self._vmops.get_info(instance) def get_diagnostics(self, instance): """Return data about VM diagnostics.""" return self._vmops.get_diagnostics(instance) def get_instance_diagnostics(self, instance): """Return data about VM diagnostics.""" return self._vmops.get_instance_diagnostics(instance) def host_power_action(self, action): """Host operations not supported by VC driver. This needs to override the ESX driver implementation. """ raise NotImplementedError() def host_maintenance_mode(self, host, mode): """Host operations not supported by VC driver. This needs to override the ESX driver implementation. """ raise NotImplementedError() def set_host_enabled(self, enabled): """Host operations not supported by VC driver. This needs to override the ESX driver implementation. """ raise NotImplementedError() def get_host_uptime(self): """Host uptime operation not supported by VC driver.""" msg = _("Multiple hosts may be managed by the VMWare " "vCenter driver; therefore we do not return " "uptime for just one host.") raise NotImplementedError(msg) def inject_network_info(self, instance, nw_info): """inject network info for specified instance.""" self._vmops.inject_network_info(instance, nw_info) def manage_image_cache(self, context, all_instances): """Manage the local cache of images.""" self._vmops.manage_image_cache(context, all_instances) def instance_exists(self, instance): """Efficient override of base instance_exists method.""" return self._vmops.instance_exists(instance) def attach_interface(self, context, instance, image_meta, vif): """Attach an interface to the instance.""" self._vmops.attach_interface(context, instance, image_meta, vif) def detach_interface(self, context, instance, vif): """Detach an interface from the instance.""" self._vmops.detach_interface(context, instance, vif) class VMwareAPISession(api.VMwareAPISession): """Sets up a session with the VC/ESX host and handles all the calls made to the host. """ def __init__(self, host_ip=CONF.vmware.host_ip, host_port=CONF.vmware.host_port, username=CONF.vmware.host_username, password=CONF.vmware.host_password, retry_count=CONF.vmware.api_retry_count, scheme="https", cacert=CONF.vmware.ca_file, insecure=CONF.vmware.insecure, pool_size=CONF.vmware.connection_pool_size): super(VMwareAPISession, self).__init__( host=host_ip, port=host_port, server_username=username, server_password=password, api_retry_count=retry_count, task_poll_interval=CONF.vmware.task_poll_interval, scheme=scheme, create_session=True, cacert=cacert, insecure=insecure, pool_size=pool_size) def _is_vim_object(self, module): """Check if the module is a VIM Object instance.""" return isinstance(module, vim.Vim) def _call_method(self, module, method, *args, **kwargs): """Calls a method within the module specified with args provided. """ if not self._is_vim_object(module): return self.invoke_api(module, method, self.vim, *args, **kwargs) else: return self.invoke_api(module, method, *args, **kwargs) def _wait_for_task(self, task_ref): """Return a Deferred that will give the result of the given task. The task is polled until it completes. """ return self.wait_for_task(task_ref)