# Copyright 2013 Cloudbase Solutions Srl # All Rights Reserved. # # 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. """ Management class for migration / resize operations. """ import os import re import nova.conf from nova import exception from nova.virt import configdrive from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import units from compute_hyperv.i18n import _ from compute_hyperv.nova import block_device_manager from compute_hyperv.nova import constants from compute_hyperv.nova import imagecache from compute_hyperv.nova import pathutils from compute_hyperv.nova import vmops from compute_hyperv.nova import volumeops LOG = logging.getLogger(__name__) CONF = nova.conf.CONF hyperv_migration_opts = [ cfg.BoolOpt('move_disks_on_cold_migration', default=True) ] CONF.register_opts(hyperv_migration_opts, 'hyperv') class MigrationOps(object): _ADMINISTRATIVE_SHARE_RE = re.compile(r'\\\\.*\\[a-zA-Z]\$\\.*') def __init__(self): self._vmutils = utilsfactory.get_vmutils() self._vhdutils = utilsfactory.get_vhdutils() self._pathutils = pathutils.PathUtils() self._volumeops = volumeops.VolumeOps() self._vmops = vmops.VMOps() self._imagecache = imagecache.ImageCache() self._block_dev_man = block_device_manager.BlockDeviceInfoManager() self._migrationutils = utilsfactory.get_migrationutils() self._metricsutils = utilsfactory.get_metricsutils() def _move_vm_files(self, instance): instance_path = self._pathutils.get_instance_dir(instance.name) revert_path = self._pathutils.get_instance_migr_revert_dir( instance_path, remove_dir=True, create_dir=True) export_path = self._pathutils.get_export_dir( instance_dir=revert_path, create_dir=True) # copy the given instance's files to a _revert folder, as backup. LOG.debug("Moving instance files to a revert path: %s", revert_path, instance=instance) self._pathutils.move_folder_files(instance_path, revert_path) self._pathutils.copy_vm_config_files(instance.name, export_path) return revert_path def _check_target_flavor(self, instance, flavor): new_root_gb = flavor.root_gb curr_root_gb = instance.flavor.root_gb if new_root_gb < curr_root_gb: raise exception.InstanceFaultRollback( exception.CannotResizeDisk( reason=_("Cannot resize the root disk to a smaller size. " "Current size: %(curr_root_gb)s GB. Requested " "size: %(new_root_gb)s GB.") % { 'curr_root_gb': curr_root_gb, 'new_root_gb': new_root_gb})) def migrate_disk_and_power_off(self, context, instance, dest, flavor, network_info, block_device_info=None, timeout=0, retry_interval=0): LOG.debug("migrate_disk_and_power_off called", instance=instance) self._check_target_flavor(instance, flavor) self._vmops.power_off(instance, timeout, retry_interval) instance_path = self._move_vm_files(instance) instance.system_metadata['backup_location'] = instance_path instance.save() self._vmops.destroy(instance, network_info, destroy_disks=True) # return the instance's path location. return instance_path def confirm_migration(self, context, migration, instance, network_info): LOG.debug("confirm_migration called", instance=instance) revert_path = instance.system_metadata['backup_location'] export_path = self._pathutils.get_export_dir(instance_dir=revert_path) self._pathutils.check_dir(export_path, remove_dir=True) self._pathutils.check_dir(revert_path, remove_dir=True) def _revert_migration_files(self, instance): revert_path = instance.system_metadata['backup_location'] instance_path = revert_path.rstrip('_revert') # the instance dir might still exist, if the destination node kept # the files on the original node. self._pathutils.check_dir(instance_path, remove_dir=True) self._pathutils.rename(revert_path, instance_path) return instance_path def _check_and_attach_config_drive(self, instance, vm_gen): if configdrive.required_by(instance): configdrive_path = self._pathutils.lookup_configdrive_path( instance.name) if configdrive_path: self._vmops.attach_config_drive(instance, configdrive_path, vm_gen) else: raise exception.ConfigDriveNotFound( instance_uuid=instance.uuid) def finish_revert_migration(self, context, instance, network_info, block_device_info=None, power_on=True): LOG.debug("finish_revert_migration called", instance=instance) instance_path = self._revert_migration_files(instance) image_meta = self._imagecache.get_image_details(context, instance) self._import_and_setup_vm(context, instance, instance_path, image_meta, block_device_info) if power_on: self._vmops.power_on(instance, network_info=network_info) def _merge_base_vhd(self, diff_vhd_path, base_vhd_path): base_vhd_copy_path = os.path.join(os.path.dirname(diff_vhd_path), os.path.basename(base_vhd_path)) try: LOG.debug('Copying base disk %(base_vhd_path)s to ' '%(base_vhd_copy_path)s', {'base_vhd_path': base_vhd_path, 'base_vhd_copy_path': base_vhd_copy_path}) self._pathutils.copyfile(base_vhd_path, base_vhd_copy_path) LOG.debug("Reconnecting copied base VHD " "%(base_vhd_copy_path)s and diff " "VHD %(diff_vhd_path)s", {'base_vhd_copy_path': base_vhd_copy_path, 'diff_vhd_path': diff_vhd_path}) self._vhdutils.reconnect_parent_vhd(diff_vhd_path, base_vhd_copy_path) LOG.debug("Merging differential disk %s into its parent.", diff_vhd_path) self._vhdutils.merge_vhd(diff_vhd_path) # Replace the differential VHD with the merged one self._pathutils.rename(base_vhd_copy_path, diff_vhd_path) except Exception: with excutils.save_and_reraise_exception(): if self._pathutils.exists(base_vhd_copy_path): self._pathutils.remove(base_vhd_copy_path) def _check_resize_vhd(self, vhd_path, vhd_info, new_size): curr_size = vhd_info['VirtualSize'] if new_size < curr_size: raise exception.CannotResizeDisk( reason=_("Cannot resize the root disk to a smaller size. " "Current size: %(curr_root_gb)s GB. Requested " "size: %(new_root_gb)s GB.") % { 'curr_root_gb': curr_size / units.Gi, 'new_root_gb': new_size / units.Gi}) elif new_size > curr_size: self._resize_vhd(vhd_path, new_size) def _resize_vhd(self, vhd_path, new_size): if vhd_path.split('.')[-1].lower() == "vhd": LOG.debug("Getting parent disk info for disk: %s", vhd_path) base_disk_path = self._vhdutils.get_vhd_parent_path(vhd_path) if base_disk_path: # A differential VHD cannot be resized. This limitation # does not apply to the VHDX format. self._merge_base_vhd(vhd_path, base_disk_path) LOG.debug("Resizing disk \"%(vhd_path)s\" to new max " "size %(new_size)s", {'vhd_path': vhd_path, 'new_size': new_size}) self._vhdutils.resize_vhd(vhd_path, new_size) def _check_base_disk(self, context, instance, diff_vhd_path, src_base_disk_path): base_vhd_path = self._imagecache.get_cached_image(context, instance) # If the location of the base host differs between source # and target hosts we need to reconnect the base disk if src_base_disk_path.lower() != base_vhd_path.lower(): LOG.debug("Reconnecting copied base VHD " "%(base_vhd_path)s and diff " "VHD %(diff_vhd_path)s", {'base_vhd_path': base_vhd_path, 'diff_vhd_path': diff_vhd_path}) self._vhdutils.reconnect_parent_vhd(diff_vhd_path, base_vhd_path) def _migrate_disks_from_source(self, migration, instance, source_inst_dir): source_inst_dir = self._pathutils.get_remote_path( migration.source_compute, source_inst_dir) source_export_path = self._pathutils.get_export_dir( instance_dir=source_inst_dir) if CONF.hyperv.move_disks_on_cold_migration: # copy the files from the source node to this node's configured # location. inst_dir = self._pathutils.get_instance_dir( instance.name, create_dir=True, remove_dir=True) elif self._ADMINISTRATIVE_SHARE_RE.match(source_inst_dir): # make sure that the source is not a remote local path. # e.g.: \\win-srv\\C$\OpenStack\Instances\.. # CSVs, local paths, and shares are fine. inst_dir = source_inst_dir.rstrip('_revert') LOG.warning( 'Host is configured not to copy disks on cold migration, but ' 'the instance will not be able to start with the remote path: ' '"%s". Only local, share, or CSV paths are acceptable.', inst_dir) inst_dir = self._pathutils.get_instance_dir( instance.name, create_dir=True, remove_dir=True) else: # make a copy on the source node's configured location. # strip the _revert from the source backup dir. inst_dir = source_inst_dir.rstrip('_revert') self._pathutils.check_dir(inst_dir, create_dir=True) export_path = self._pathutils.get_export_dir( instance_dir=inst_dir) self._pathutils.copy_folder_files(source_inst_dir, inst_dir) self._pathutils.copy_dir(source_export_path, export_path) return inst_dir def finish_migration(self, context, migration, instance, disk_info, network_info, image_meta, resize_instance=False, block_device_info=None, power_on=True): LOG.debug("finish_migration called", instance=instance) instance_dir = self._migrate_disks_from_source(migration, instance, disk_info) # NOTE(claudiub): nova compute manager only takes into account disk # flavor changes when passing to the driver resize_instance=True. # we need to take into account flavor extra_specs as well. resize_instance = ( migration.old_instance_type_id != migration.new_instance_type_id) self._import_and_setup_vm(context, instance, instance_dir, image_meta, block_device_info, resize_instance) if power_on: self._vmops.power_on(instance, network_info=network_info) def _import_and_setup_vm(self, context, instance, instance_dir, image_meta, block_device_info, resize_instance=False): vm_gen = self._vmops.get_image_vm_generation(instance.uuid, image_meta) self._import_vm(instance_dir) self._volumeops.connect_volumes(block_device_info) self._update_disk_image_paths(instance, instance_dir) self._check_and_update_disks(context, instance, vm_gen, image_meta, block_device_info, resize_instance=resize_instance) self._volumeops.fix_instance_volume_disk_paths( instance.name, block_device_info) self._vmops.update_vm_resources(instance, vm_gen, image_meta, instance_dir, resize_instance) self._migrationutils.realize_vm(instance.name) self._vmops.configure_remotefx(instance, vm_gen, resize_instance) if CONF.hyperv.enable_instance_metrics_collection: self._metricsutils.enable_vm_metrics_collection(instance.name) def _import_vm(self, instance_dir): snapshot_dir = self._pathutils.get_instance_snapshot_dir( instance_dir=instance_dir) export_dir = self._pathutils.get_export_dir(instance_dir=instance_dir) vm_config_file_path = self._pathutils.get_vm_config_file(export_dir) self._migrationutils.import_vm_definition(vm_config_file_path, snapshot_dir) # NOTE(claudiub): after the VM was imported, the VM config files are # not necessary anymore. self._pathutils.get_export_dir(instance_dir=instance_dir, remove_dir=True) def _update_disk_image_paths(self, instance, instance_path): """Checks if disk images have the correct path and updates them if not. When resizing an instance, the vm is imported on the destination node and the disk files are copied from source node. If the hosts have different instance_path config options set, the disks are migrated to the correct paths, but vm disk resources are not updated to point to the new location. """ (disk_files, volume_drives) = self._vmutils.get_vm_storage_paths( instance.name) pattern = re.compile('configdrive|eph|root') for disk_file in disk_files: disk_name = os.path.basename(disk_file) if not pattern.match(disk_name): # skip files that do not match the pattern. continue expected_disk_path = os.path.join(instance_path, disk_name) if not os.path.exists(expected_disk_path): raise exception.DiskNotFound(location=expected_disk_path) if expected_disk_path != disk_file: LOG.debug("Updating VM disk location from %(src)s to %(dest)s", {'src': disk_file, 'dest': expected_disk_path, 'instance': instance}) self._vmutils.update_vm_disk_path(disk_file, expected_disk_path, is_physical=False) def _check_and_update_disks(self, context, instance, vm_gen, image_meta, block_device_info, resize_instance=False): self._block_dev_man.validate_and_update_bdi(instance, image_meta, vm_gen, block_device_info) root_device = block_device_info['root_disk'] if root_device['type'] == constants.DISK: root_vhd_path = self._pathutils.lookup_root_vhd_path(instance.name) root_device['path'] = root_vhd_path if not root_vhd_path: base_vhd_path = self._pathutils.get_instance_dir(instance.name) raise exception.DiskNotFound(location=base_vhd_path) root_vhd_info = self._vhdutils.get_vhd_info(root_vhd_path) src_base_disk_path = root_vhd_info.get("ParentPath") if src_base_disk_path: self._check_base_disk(context, instance, root_vhd_path, src_base_disk_path) if resize_instance: new_size = instance.flavor.root_gb * units.Gi self._check_resize_vhd(root_vhd_path, root_vhd_info, new_size) ephemerals = block_device_info['ephemerals'] self._check_ephemeral_disks(instance, ephemerals, resize_instance) def _check_ephemeral_disks(self, instance, ephemerals, resize_instance=False): instance_name = instance.name new_eph_gb = instance.get('ephemeral_gb', 0) if len(ephemerals) == 1: # NOTE(claudiub): Resize only if there is one ephemeral. If there # are more than 1, resizing them can be problematic. This behaviour # also exists in the libvirt driver and it has to be addressed in # the future. ephemerals[0]['size'] = new_eph_gb elif sum(eph['size'] for eph in ephemerals) != new_eph_gb: # New ephemeral size is different from the original ephemeral size # and there are multiple ephemerals. LOG.warning("Cannot resize multiple ephemeral disks for instance.", instance=instance) for index, eph in enumerate(ephemerals): eph_name = "eph%s" % index existing_eph_path = self._pathutils.lookup_ephemeral_vhd_path( instance_name, eph_name) if not existing_eph_path: eph['format'] = self._vhdutils.get_best_supported_vhd_format() eph['path'] = self._pathutils.get_ephemeral_vhd_path( instance_name, eph['format'], eph_name) if not resize_instance: # ephemerals should have existed. raise exception.DiskNotFound(location=eph['path']) if eph['size']: # create ephemerals self._vmops.create_ephemeral_disk(instance.name, eph) self._vmops.attach_ephemerals(instance_name, [eph]) elif eph['size'] > 0: # ephemerals exist. resize them. eph['path'] = existing_eph_path eph_vhd_info = self._vhdutils.get_vhd_info(eph['path']) self._check_resize_vhd( eph['path'], eph_vhd_info, eph['size'] * units.Gi) else: # ephemeral new size is 0, remove it. self._pathutils.remove(existing_eph_path) eph['path'] = None