diff options
Diffstat (limited to 'nova')
| -rw-r--r-- | nova/tests/hyperv/fake.py | 52 | ||||
| -rw-r--r-- | nova/tests/test_hypervapi.py | 290 | ||||
| -rwxr-xr-x | nova/virt/hyperv/driver.py | 31 | ||||
| -rw-r--r-- | nova/virt/hyperv/hostops.py | 15 | ||||
| -rw-r--r-- | nova/virt/hyperv/hostutils.py | 7 | ||||
| -rw-r--r-- | nova/virt/hyperv/imagecache.py | 60 | ||||
| -rw-r--r-- | nova/virt/hyperv/livemigrationops.py | 19 | ||||
| -rw-r--r-- | nova/virt/hyperv/livemigrationutils.py | 2 | ||||
| -rw-r--r-- | nova/virt/hyperv/migrationops.py | 233 | ||||
| -rw-r--r-- | nova/virt/hyperv/pathutils.py | 113 | ||||
| -rw-r--r-- | nova/virt/hyperv/snapshotops.py | 20 | ||||
| -rw-r--r-- | nova/virt/hyperv/vhdutils.py | 29 | ||||
| -rw-r--r-- | nova/virt/hyperv/vmops.py | 188 | ||||
| -rw-r--r-- | nova/virt/hyperv/vmutils.py | 39 | ||||
| -rw-r--r-- | nova/virt/hyperv/volumeops.py | 74 |
15 files changed, 870 insertions, 302 deletions
diff --git a/nova/tests/hyperv/fake.py b/nova/tests/hyperv/fake.py index 9890a5462..e0e5a6bbe 100644 --- a/nova/tests/hyperv/fake.py +++ b/nova/tests/hyperv/fake.py @@ -23,24 +23,50 @@ class PathUtils(object): def open(self, path, mode): return io.BytesIO(b'fake content') - def get_instances_path(self): - return 'C:\\FakePath\\' + def exists(self, path): + return False + + def makedirs(self, path): + pass + + def remove(self, path): + pass + + def rename(self, src, dest): + pass + + def copyfile(self, src, dest): + pass + + def copy(self, src, dest): + pass + + def rmtree(self, path): + pass + + def get_instances_dir(self, remote_server=None): + return 'C:\\FakeInstancesPath\\' + + def get_instance_migr_revert_dir(self, instance_name, create_dir=False, + remove_dir=False): + return os.path.join(self.get_instances_dir(), instance_name, '_revert') - def get_instance_path(self, instance_name): - return os.path.join(self.get_instances_path(), instance_name) + def get_instance_dir(self, instance_name, remote_server=None, + create_dir=True, remove_dir=False): + return os.path.join(self.get_instances_dir(remote_server), + instance_name) def get_vhd_path(self, instance_name): - instance_path = self.get_instance_path(instance_name) - return os.path.join(instance_path, instance_name + ".vhd") + instance_path = self.get_instance_dir(instance_name) + return os.path.join(instance_path, 'root.vhd') - def get_base_vhd_path(self, image_name): - base_dir = os.path.join(self.get_instances_path(), '_base') - return os.path.join(base_dir, image_name + ".vhd") + def get_base_vhd_dir(self): + return os.path.join(self.get_instances_dir(), '_base') - def make_export_path(self, instance_name): - export_folder = os.path.join(self.get_instances_path(), "export", - instance_name) - return export_folder + def get_export_dir(self, instance_name): + export_dir = os.path.join(self.get_instances_dir(), 'export', + instance_name) + return export_dir def vhd_exists(self, path): return False diff --git a/nova/tests/test_hypervapi.py b/nova/tests/test_hypervapi.py index 0c2f90a4d..025d3a454 100644 --- a/nova/tests/test_hypervapi.py +++ b/nova/tests/test_hypervapi.py @@ -80,6 +80,7 @@ class HyperVAPITestCase(test.TestCase): self._instance_ide_disks = [] self._instance_ide_dvds = [] self._instance_volume_disks = [] + self._test_vm_name = None self._setup_stubs() @@ -116,6 +117,14 @@ class HyperVAPITestCase(test.TestCase): self.stubs.Set(pathutils, 'PathUtils', fake.PathUtils) self._mox.StubOutWithMock(fake.PathUtils, 'open') + self._mox.StubOutWithMock(fake.PathUtils, 'copyfile') + self._mox.StubOutWithMock(fake.PathUtils, 'rmtree') + self._mox.StubOutWithMock(fake.PathUtils, 'copy') + self._mox.StubOutWithMock(fake.PathUtils, 'remove') + self._mox.StubOutWithMock(fake.PathUtils, 'rename') + self._mox.StubOutWithMock(fake.PathUtils, 'makedirs') + self._mox.StubOutWithMock(fake.PathUtils, + 'get_instance_migr_revert_dir') self._mox.StubOutWithMock(vmutils.VMUtils, 'vm_exists') self._mox.StubOutWithMock(vmutils.VMUtils, 'create_vm') @@ -137,11 +146,13 @@ class HyperVAPITestCase(test.TestCase): self._mox.StubOutWithMock(vmutils.VMUtils, 'get_mounted_disk_by_drive_number') self._mox.StubOutWithMock(vmutils.VMUtils, 'detach_vm_disk') + self._mox.StubOutWithMock(vmutils.VMUtils, 'get_vm_storage_paths') self._mox.StubOutWithMock(vhdutils.VHDUtils, 'create_differencing_vhd') self._mox.StubOutWithMock(vhdutils.VHDUtils, 'reconnect_parent_vhd') self._mox.StubOutWithMock(vhdutils.VHDUtils, 'merge_vhd') self._mox.StubOutWithMock(vhdutils.VHDUtils, 'get_vhd_parent_path') + self._mox.StubOutWithMock(vhdutils.VHDUtils, 'get_vhd_info') self._mox.StubOutWithMock(hostutils.HostUtils, 'get_cpus_info') self._mox.StubOutWithMock(hostutils.HostUtils, @@ -149,6 +160,7 @@ class HyperVAPITestCase(test.TestCase): self._mox.StubOutWithMock(hostutils.HostUtils, 'get_memory_info') self._mox.StubOutWithMock(hostutils.HostUtils, 'get_volume_info') self._mox.StubOutWithMock(hostutils.HostUtils, 'get_windows_version') + self._mox.StubOutWithMock(hostutils.HostUtils, 'get_local_ips') self._mox.StubOutWithMock(networkutils.NetworkUtils, 'get_external_vswitch') @@ -181,11 +193,6 @@ class HyperVAPITestCase(test.TestCase): self._mox.StubOutWithMock(volumeutilsv2.VolumeUtilsV2, 'execute_log_out') - self._mox.StubOutWithMock(shutil, 'copyfile') - self._mox.StubOutWithMock(shutil, 'rmtree') - - self._mox.StubOutWithMock(os, 'remove') - self._mox.StubOutClassWithMocks(instance_metadata, 'InstanceMetadata') self._mox.StubOutWithMock(instance_metadata.InstanceMetadata, 'metadata_for_config_drive') @@ -332,7 +339,7 @@ class HyperVAPITestCase(test.TestCase): mox.IsA(str), mox.IsA(str), attempts=1) - os.remove(mox.IsA(str)) + fake.PathUtils.remove(mox.IsA(str)) m = vmutils.VMUtils.attach_ide_drive(mox.IsA(str), mox.IsA(str), @@ -490,15 +497,22 @@ class HyperVAPITestCase(test.TestCase): None) self._mox.VerifyAll() - def test_destroy(self): - self._instance_data = self._get_instance_data() - + def _setup_destroy_mocks(self): m = vmutils.VMUtils.vm_exists(mox.Func(self._check_instance_name)) m.AndReturn(True) - m = vmutils.VMUtils.destroy_vm(mox.Func(self._check_instance_name), - True) - m.AndReturn([]) + func = mox.Func(self._check_instance_name) + vmutils.VMUtils.set_vm_state(func, constants.HYPERV_VM_STATE_DISABLED) + + m = vmutils.VMUtils.get_vm_storage_paths(func) + m.AndReturn(([], [])) + + vmutils.VMUtils.destroy_vm(func) + + def test_destroy(self): + self._instance_data = self._get_instance_data() + + self._setup_destroy_mocks() self._mox.ReplayAll() self._conn.destroy(self._instance_data) @@ -562,7 +576,9 @@ class HyperVAPITestCase(test.TestCase): if cow: m = basevolumeutils.BaseVolumeUtils.volume_in_mapping(mox.IsA(str), None) - m.AndReturn([]) + m.AndReturn(False) + + vhdutils.VHDUtils.get_vhd_info(mox.Func(self._check_img_path)) self._mox.ReplayAll() self._conn.pre_live_migration(self._context, instance_data, @@ -617,7 +633,7 @@ class HyperVAPITestCase(test.TestCase): def copy_dest_disk_path(src, dest): self._fake_dest_disk_path = dest - m = shutil.copyfile(mox.IsA(str), mox.IsA(str)) + m = fake.PathUtils.copyfile(mox.IsA(str), mox.IsA(str)) m.WithSideEffects(copy_dest_disk_path) self._fake_dest_base_disk_path = None @@ -625,7 +641,7 @@ class HyperVAPITestCase(test.TestCase): def copy_dest_base_disk_path(src, dest): self._fake_dest_base_disk_path = dest - m = shutil.copyfile(fake_parent_vhd_path, mox.IsA(str)) + m = fake.PathUtils.copyfile(fake_parent_vhd_path, mox.IsA(str)) m.WithSideEffects(copy_dest_base_disk_path) def check_dest_disk_path(path): @@ -647,7 +663,7 @@ class HyperVAPITestCase(test.TestCase): func = mox.Func(check_snapshot_path) vmutils.VMUtils.remove_vm_snapshot(func) - shutil.rmtree(mox.IsA(str)) + fake.PathUtils.rmtree(mox.IsA(str)) m = fake.PathUtils.open(func2, 'rb') m.AndReturn(io.BytesIO(b'fake content')) @@ -702,65 +718,70 @@ class HyperVAPITestCase(test.TestCase): mounted_disk_path): self._instance_volume_disks.append(mounted_disk_path) - def _setup_spawn_instance_mocks(self, cow, setup_vif_mocks_func=None, - with_exception=False, - block_device_info=None): - self._test_vm_name = None - - def set_vm_name(vm_name): - self._test_vm_name = vm_name - - def check_vm_name(vm_name): - return vm_name == self._test_vm_name - - m = vmutils.VMUtils.vm_exists(mox.IsA(str)) - m.WithSideEffects(set_vm_name).AndReturn(False) - - if not block_device_info: - m = basevolumeutils.BaseVolumeUtils.volume_in_mapping(mox.IsA(str), - None) - m.AndReturn([]) - else: - m = basevolumeutils.BaseVolumeUtils.volume_in_mapping( - mox.IsA(str), block_device_info) - m.AndReturn(True) - - if cow: - def check_path(parent_path): - return parent_path == self._fetched_image - - vhdutils.VHDUtils.create_differencing_vhd(mox.IsA(str), - mox.Func(check_path)) + def _check_img_path(self, image_path): + return image_path == self._fetched_image - vmutils.VMUtils.create_vm(mox.Func(check_vm_name), mox.IsA(int), + def _setup_create_instance_mocks(self, setup_vif_mocks_func=None, + boot_from_volume=False): + vmutils.VMUtils.create_vm(mox.Func(self._check_vm_name), mox.IsA(int), mox.IsA(int), mox.IsA(bool)) - if not block_device_info: - m = vmutils.VMUtils.attach_ide_drive(mox.Func(check_vm_name), + if not boot_from_volume: + m = vmutils.VMUtils.attach_ide_drive(mox.Func(self._check_vm_name), mox.IsA(str), mox.IsA(int), mox.IsA(int), mox.IsA(str)) m.WithSideEffects(self._add_ide_disk).InAnyOrder() - m = vmutils.VMUtils.create_scsi_controller(mox.Func(check_vm_name)) + func = mox.Func(self._check_vm_name) + m = vmutils.VMUtils.create_scsi_controller(func) m.InAnyOrder() - vmutils.VMUtils.create_nic(mox.Func(check_vm_name), mox.IsA(str), + vmutils.VMUtils.create_nic(mox.Func(self._check_vm_name), mox.IsA(str), mox.IsA(str)).InAnyOrder() if setup_vif_mocks_func: setup_vif_mocks_func() + def _set_vm_name(self, vm_name): + self._test_vm_name = vm_name + + def _check_vm_name(self, vm_name): + return vm_name == self._test_vm_name + + def _setup_spawn_instance_mocks(self, cow, setup_vif_mocks_func=None, + with_exception=False, + block_device_info=None, + boot_from_volume=False): + m = vmutils.VMUtils.vm_exists(mox.IsA(str)) + m.WithSideEffects(self._set_vm_name).AndReturn(False) + + m = basevolumeutils.BaseVolumeUtils.volume_in_mapping( + mox.IsA(str), block_device_info) + m.AndReturn(boot_from_volume) + + if not boot_from_volume: + vhdutils.VHDUtils.get_vhd_info(mox.Func(self._check_img_path)) + + if cow: + vhdutils.VHDUtils.create_differencing_vhd( + mox.IsA(str), mox.Func(self._check_img_path)) + else: + fake.PathUtils.copyfile(mox.IsA(str), mox.IsA(str)) + + self._setup_create_instance_mocks(setup_vif_mocks_func, + boot_from_volume) + # TODO(alexpilotti) Based on where the exception is thrown # some of the above mock calls need to be skipped if with_exception: - m = vmutils.VMUtils.vm_exists(mox.Func(check_vm_name)) + m = vmutils.VMUtils.vm_exists(mox.Func(self._check_vm_name)) m.AndReturn(True) - vmutils.VMUtils.destroy_vm(mox.Func(check_vm_name), True) + vmutils.VMUtils.destroy_vm(mox.Func(self._check_vm_name)) else: - vmutils.VMUtils.set_vm_state(mox.Func(check_vm_name), + vmutils.VMUtils.set_vm_state(mox.Func(self._check_vm_name), constants.HYPERV_VM_STATE_ENABLED) def _test_spawn_instance(self, cow=True, @@ -772,14 +793,14 @@ class HyperVAPITestCase(test.TestCase): with_exception) self._mox.ReplayAll() - self._spawn_instance(cow, ) + self._spawn_instance(cow) self._mox.VerifyAll() self.assertEquals(len(self._instance_ide_disks), expected_ide_disks) self.assertEquals(len(self._instance_ide_dvds), expected_ide_dvds) - if not cow: - self.assertEquals(self._fetched_image, self._instance_ide_disks[0]) + vhd_path = pathutils.PathUtils().get_vhd_path(self._test_vm_name) + self.assertEquals(vhd_path, self._instance_ide_disks[0]) def test_attach_volume(self): instance_data = self._get_instance_data() @@ -897,10 +918,165 @@ class HyperVAPITestCase(test.TestCase): m.WithSideEffects(self._add_volume_disk) self._setup_spawn_instance_mocks(cow=False, - block_device_info=block_device_info) + block_device_info=block_device_info, + boot_from_volume=True) self._mox.ReplayAll() self._spawn_instance(False, block_device_info) self._mox.VerifyAll() self.assertEquals(len(self._instance_volume_disks), 1) + + def _setup_test_migrate_disk_and_power_off_mocks(self, same_host=False, + with_exception=False): + self._instance_data = self._get_instance_data() + instance = db.instance_create(self._context, self._instance_data) + network_info = fake_network.fake_get_instance_nw_info( + self.stubs, spectacular=True) + + fake_local_ip = '10.0.0.1' + if same_host: + fake_dest_ip = fake_local_ip + else: + fake_dest_ip = '10.0.0.2' + + fake_root_vhd_path = 'C:\\FakePath\\root.vhd' + fake_revert_path = ('C:\\FakeInstancesPath\\%s\\_revert' % + instance['name']) + + func = mox.Func(self._check_instance_name) + vmutils.VMUtils.set_vm_state(func, constants.HYPERV_VM_STATE_DISABLED) + + m = vmutils.VMUtils.get_vm_storage_paths(func) + m.AndReturn(([fake_root_vhd_path], [])) + + m = hostutils.HostUtils.get_local_ips() + m.AndReturn([fake_local_ip]) + + m = pathutils.PathUtils.get_instance_migr_revert_dir(instance['name'], + remove_dir=True) + m.AndReturn(fake_revert_path) + + if same_host: + fake.PathUtils.makedirs(mox.IsA(str)) + + m = fake.PathUtils.copy(fake_root_vhd_path, mox.IsA(str)) + if with_exception: + m.AndRaise(shutil.Error('Simulated copy error')) + else: + fake.PathUtils.rename(mox.IsA(str), mox.IsA(str)) + if same_host: + fake.PathUtils.rename(mox.IsA(str), mox.IsA(str)) + + self._setup_destroy_mocks() + + return (instance, fake_dest_ip, network_info) + + def test_migrate_disk_and_power_off(self): + (instance, + fake_dest_ip, + network_info) = self._setup_test_migrate_disk_and_power_off_mocks() + + self._mox.ReplayAll() + self._conn.migrate_disk_and_power_off(self._context, instance, + fake_dest_ip, None, + network_info) + self._mox.VerifyAll() + + def test_migrate_disk_and_power_off_same_host(self): + args = self._setup_test_migrate_disk_and_power_off_mocks( + same_host=True) + (instance, fake_dest_ip, network_info) = args + + self._mox.ReplayAll() + self._conn.migrate_disk_and_power_off(self._context, instance, + fake_dest_ip, None, + network_info) + self._mox.VerifyAll() + + def test_migrate_disk_and_power_off_exception(self): + args = self._setup_test_migrate_disk_and_power_off_mocks( + with_exception=True) + (instance, fake_dest_ip, network_info) = args + + self._mox.ReplayAll() + self.assertRaises(shutil.Error, self._conn.migrate_disk_and_power_off, + self._context, instance, fake_dest_ip, None, + network_info) + self._mox.VerifyAll() + + def test_finish_migration(self): + self._instance_data = self._get_instance_data() + instance = db.instance_create(self._context, self._instance_data) + network_info = fake_network.fake_get_instance_nw_info( + self.stubs, spectacular=True) + + m = basevolumeutils.BaseVolumeUtils.volume_in_mapping(mox.IsA(str), + None) + m.AndReturn(False) + + self._mox.StubOutWithMock(fake.PathUtils, 'exists') + m = fake.PathUtils.exists(mox.IsA(str)) + m.AndReturn(True) + + fake_parent_vhd_path = (os.path.join('FakeParentPath', '%s.vhd' % + instance["image_ref"])) + + m = vhdutils.VHDUtils.get_vhd_info(mox.IsA(str)) + m.AndReturn({'ParentPath': fake_parent_vhd_path, + 'MaxInternalSize': 1}) + + m = fake.PathUtils.exists(mox.IsA(str)) + m.AndReturn(True) + + vhdutils.VHDUtils.reconnect_parent_vhd(mox.IsA(str), mox.IsA(str)) + + self._set_vm_name(instance['name']) + self._setup_create_instance_mocks(None, False) + + vmutils.VMUtils.set_vm_state(mox.Func(self._check_instance_name), + constants.HYPERV_VM_STATE_ENABLED) + + self._mox.ReplayAll() + self._conn.finish_migration(self._context, None, instance, "", + network_info, None, False, None) + self._mox.VerifyAll() + + def test_confirm_migration(self): + self._instance_data = self._get_instance_data() + instance = db.instance_create(self._context, self._instance_data) + network_info = fake_network.fake_get_instance_nw_info( + self.stubs, spectacular=True) + + pathutils.PathUtils.get_instance_migr_revert_dir(instance['name'], + remove_dir=True) + self._mox.ReplayAll() + self._conn.confirm_migration(None, instance, network_info) + self._mox.VerifyAll() + + def test_finish_revert_migration(self): + self._instance_data = self._get_instance_data() + instance = db.instance_create(self._context, self._instance_data) + network_info = fake_network.fake_get_instance_nw_info( + self.stubs, spectacular=True) + + fake_revert_path = ('C:\\FakeInstancesPath\\%s\\_revert' % + instance['name']) + + m = basevolumeutils.BaseVolumeUtils.volume_in_mapping(mox.IsA(str), + None) + m.AndReturn(False) + + m = pathutils.PathUtils.get_instance_migr_revert_dir(instance['name']) + m.AndReturn(fake_revert_path) + fake.PathUtils.rename(fake_revert_path, mox.IsA(str)) + + self._set_vm_name(instance['name']) + self._setup_create_instance_mocks(None, False) + + vmutils.VMUtils.set_vm_state(mox.Func(self._check_instance_name), + constants.HYPERV_VM_STATE_ENABLED) + + self._mox.ReplayAll() + self._conn.finish_revert_migration(instance, network_info, None) + self._mox.VerifyAll() diff --git a/nova/virt/hyperv/driver.py b/nova/virt/hyperv/driver.py index 4af3b8b05..aac47deef 100755 --- a/nova/virt/hyperv/driver.py +++ b/nova/virt/hyperv/driver.py @@ -23,6 +23,7 @@ from nova.openstack.common import log as logging from nova.virt import driver from nova.virt.hyperv import hostops from nova.virt.hyperv import livemigrationops +from nova.virt.hyperv import migrationops from nova.virt.hyperv import snapshotops from nova.virt.hyperv import vmops from nova.virt.hyperv import volumeops @@ -36,10 +37,10 @@ class HyperVDriver(driver.ComputeDriver): self._hostops = hostops.HostOps() self._volumeops = volumeops.VolumeOps() - self._vmops = vmops.VMOps(self._volumeops) + self._vmops = vmops.VMOps() self._snapshotops = snapshotops.SnapshotOps() - self._livemigrationops = livemigrationops.LiveMigrationOps( - self._volumeops) + self._livemigrationops = livemigrationops.LiveMigrationOps() + self._migrationops = migrationops.MigrationOps() def init_host(self, host): pass @@ -146,7 +147,7 @@ class HyperVDriver(driver.ComputeDriver): LOG.debug(_("plug_vifs called"), instance=instance) def unplug_vifs(self, instance, network_info): - LOG.debug(_("plug_vifs called"), instance=instance) + LOG.debug(_("unplug_vifs called"), instance=instance) def ensure_filtering_rules_for_instance(self, instance_ref, network_info): LOG.debug(_("ensure_filtering_rules_for_instance called"), @@ -155,17 +156,33 @@ class HyperVDriver(driver.ComputeDriver): def unfilter_instance(self, instance, network_info): LOG.debug(_("unfilter_instance called"), instance=instance) + def migrate_disk_and_power_off(self, context, instance, dest, + instance_type, network_info, + block_device_info=None): + return self._migrationops.migrate_disk_and_power_off(context, + instance, dest, + instance_type, + network_info, + block_device_info) + def confirm_migration(self, migration, instance, network_info): - LOG.debug(_("confirm_migration called"), instance=instance) + self._migrationops.confirm_migration(migration, instance, network_info) def finish_revert_migration(self, instance, network_info, block_device_info=None): - LOG.debug(_("finish_revert_migration called"), instance=instance) + self._migrationops.finish_revert_migration(instance, network_info, + block_device_info) def finish_migration(self, context, migration, instance, disk_info, network_info, image_meta, resize_instance=False, block_device_info=None): - LOG.debug(_("finish_migration called"), instance=instance) + self._migrationops.finish_migration(context, migration, instance, + disk_info, network_info, + image_meta, resize_instance, + block_device_info) + + def get_host_ip_addr(self): + return self._hostops.get_host_ip_addr() def get_console_output(self, instance): LOG.debug(_("get_console_output called"), instance=instance) diff --git a/nova/virt/hyperv/hostops.py b/nova/virt/hyperv/hostops.py index 5a22b60de..b3b38aab9 100644 --- a/nova/virt/hyperv/hostops.py +++ b/nova/virt/hyperv/hostops.py @@ -21,12 +21,15 @@ Management class for host operations. import os import platform +from nova.openstack.common import cfg from nova.openstack.common import jsonutils from nova.openstack.common import log as logging from nova.virt.hyperv import constants from nova.virt.hyperv import hostutils from nova.virt.hyperv import pathutils +CONF = cfg.CONF +CONF.import_opt('my_ip', 'nova.netconf') LOG = logging.getLogger(__name__) @@ -73,7 +76,7 @@ class HostOps(object): return (total_mem_mb, free_mem_mb, total_mem_mb - free_mem_mb) def _get_local_hdd_info_gb(self): - (drive, _) = os.path.splitdrive(self._pathutils.get_instances_path()) + drive = os.path.splitdrive(self._pathutils.get_instances_dir())[0] (size, free_space) = self._hostutils.get_volume_info(drive) total_gb = size / (1024 ** 3) @@ -152,7 +155,7 @@ class HostOps(object): def get_host_stats(self, refresh=False): """Return the current state of the host. If 'refresh' is True, run the update first.""" - LOG.info(_("get_host_stats called")) + LOG.debug(_("get_host_stats called")) if refresh or not self._stats: self._update_stats() @@ -161,3 +164,11 @@ class HostOps(object): def host_power_action(self, host, action): """Reboots, shuts down or powers up the host.""" pass + + def get_host_ip_addr(self): + host_ip = CONF.my_ip + if not host_ip: + # Return the first available address + host_ip = self._hostutils.get_local_ips()[0] + LOG.debug(_("Host IP address is: %s"), host_ip) + return host_ip diff --git a/nova/virt/hyperv/hostutils.py b/nova/virt/hyperv/hostutils.py index 71f3bc5b2..d28ce75a5 100644 --- a/nova/virt/hyperv/hostutils.py +++ b/nova/virt/hyperv/hostutils.py @@ -16,6 +16,7 @@ # under the License. import ctypes +import socket import sys if sys.platform == 'win32': @@ -72,3 +73,9 @@ class HostUtils(object): def get_windows_version(self): return self._conn_cimv2.Win32_OperatingSystem()[0].Version + + def get_local_ips(self): + addr_info = socket.getaddrinfo(socket.gethostname(), None, 0, 0, 0) + # Returns IPv4 and IPv6 addresses, ordered by protocol family + addr_info.sort() + return [a[4][0] for a in addr_info] diff --git a/nova/virt/hyperv/imagecache.py b/nova/virt/hyperv/imagecache.py new file mode 100644 index 000000000..93ea32b25 --- /dev/null +++ b/nova/virt/hyperv/imagecache.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. +""" +Image caching and management. +""" +import os + +from nova.openstack.common import lockutils +from nova.openstack.common import log as logging +from nova.virt.hyperv import pathutils +from nova.virt.hyperv import vhdutils +from nova.virt.hyperv import vmutils +from nova.virt import images + +LOG = logging.getLogger(__name__) + + +class ImageCache(object): + def __init__(self): + self._pathutils = pathutils.PathUtils() + self._vhdutils = vhdutils.VHDUtils() + + def _validate_vhd_image(self, vhd_path): + try: + self._vhdutils.get_vhd_info(vhd_path) + except Exception as ex: + LOG.exception(ex) + raise vmutils.HyperVException(_('The image is not a valid VHD: %s') + % vhd_path) + + def get_cached_image(self, context, instance): + image_id = instance['image_ref'] + + base_vhd_dir = self._pathutils.get_base_vhd_dir() + vhd_path = os.path.join(base_vhd_dir, image_id + ".vhd") + + @lockutils.synchronized(vhd_path, 'nova-') + def fetch_image_if_not_existing(): + if not self._pathutils.exists(vhd_path): + images.fetch(context, image_id, vhd_path, + instance['user_id'], + instance['project_id']) + self._validate_vhd_image(vhd_path) + + fetch_image_if_not_existing() + return vhd_path diff --git a/nova/virt/hyperv/livemigrationops.py b/nova/virt/hyperv/livemigrationops.py index 8ee3005f1..108413b0b 100644 --- a/nova/virt/hyperv/livemigrationops.py +++ b/nova/virt/hyperv/livemigrationops.py @@ -18,15 +18,14 @@ """ Management class for live migration VM operations. """ -import os - from nova.openstack.common import cfg from nova.openstack.common import excutils from nova.openstack.common import log as logging +from nova.virt.hyperv import imagecache from nova.virt.hyperv import livemigrationutils from nova.virt.hyperv import pathutils from nova.virt.hyperv import vmutils -from nova.virt import images +from nova.virt.hyperv import volumeops LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -34,12 +33,13 @@ CONF.import_opt('use_cow_images', 'nova.virt.driver') class LiveMigrationOps(object): - def __init__(self, volumeops): + def __init__(self): self._pathutils = pathutils.PathUtils() self._vmutils = vmutils.VMUtils() self._livemigrutils = livemigrationutils.LiveMigrationUtils() - self._volumeops = volumeops + self._volumeops = volumeops.VolumeOps() + self._imagecache = imagecache.ImageCache() def live_migration(self, context, instance_ref, dest, post_method, recover_method, block_migration=False, @@ -65,15 +65,10 @@ class LiveMigrationOps(object): self._livemigrutils.check_live_migration_config() if CONF.use_cow_images: - ebs_root = self._volumeops.volume_in_mapping( - self._volumeops.get_default_root_device(), + ebs_root = self._volumeops.ebs_root_in_block_devices( block_device_info) if not ebs_root: - base_vhd_path = self._pathutils.get_base_vhd_path( - instance["image_ref"]) - if not os.path.exists(base_vhd_path): - images.fetch(context, instance["image_ref"], base_vhd_path, - instance["user_id"], instance["project_id"]) + self._imagecache.get_cached_image(context, instance) def post_live_migration_at_destination(self, ctxt, instance_ref, network_info, block_migration): diff --git a/nova/virt/hyperv/livemigrationutils.py b/nova/virt/hyperv/livemigrationutils.py index 6af4f0fa5..d039a5016 100644 --- a/nova/virt/hyperv/livemigrationutils.py +++ b/nova/virt/hyperv/livemigrationutils.py @@ -41,7 +41,7 @@ class LiveMigrationUtils(object): % host) elif ex.com_error.hresult == -2147023174: msg = (_('Target live migration host "%s" is unreachable') - % host) + % host) else: msg = _('Live migration failed: %s') % ex.message raise vmutils.HyperVException(msg) diff --git a/nova/virt/hyperv/migrationops.py b/nova/virt/hyperv/migrationops.py new file mode 100644 index 000000000..8d5b5e90c --- /dev/null +++ b/nova/virt/hyperv/migrationops.py @@ -0,0 +1,233 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 + +from nova.openstack.common import excutils +from nova.openstack.common import log as logging +from nova.virt.hyperv import hostutils +from nova.virt.hyperv import imagecache +from nova.virt.hyperv import pathutils +from nova.virt.hyperv import vhdutils +from nova.virt.hyperv import vmops +from nova.virt.hyperv import vmutils +from nova.virt.hyperv import volumeops + +LOG = logging.getLogger(__name__) + + +class MigrationOps(object): + def __init__(self): + self._hostutils = hostutils.HostUtils() + self._vmutils = vmutils.VMUtils() + self._vhdutils = vhdutils.VHDUtils() + self._pathutils = pathutils.PathUtils() + self._volumeops = volumeops.VolumeOps() + self._vmops = vmops.VMOps() + self._imagecache = imagecache.ImageCache() + + def _migrate_disk_files(self, instance_name, disk_files, dest): + same_host = False + if dest in self._hostutils.get_local_ips(): + same_host = True + LOG.debug(_("Migration target is the source host")) + else: + LOG.debug(_("Migration target host: %s") % dest) + + instance_path = self._pathutils.get_instance_dir(instance_name) + revert_path = self._pathutils.get_instance_migr_revert_dir( + instance_name, remove_dir=True) + dest_path = None + + try: + if same_host: + # Since source and target are the same, we copy the files to + # a temporary location before moving them into place + dest_path = '%s_tmp' % instance_path + if self._pathutils.exists(dest_path): + self._pathutils.rmtree(dest_path) + self._pathutils.makedirs(dest_path) + else: + dest_path = self._pathutils.get_instance_dir( + instance_name, dest, remove_dir=True) + for disk_file in disk_files: + # Skip the config drive as the instance is already configured + if os.path.basename(disk_file).lower() != 'configdrive.vhd': + LOG.debug(_('Copying disk "%(disk_file)s" to ' + '"%(dest_path)s"') % locals()) + self._pathutils.copy(disk_file, dest_path) + + self._pathutils.rename(instance_path, revert_path) + + if same_host: + self._pathutils.rename(dest_path, instance_path) + except Exception: + with excutils.save_and_reraise_exception(): + self._cleanup_failed_disk_migration(instance_path, revert_path, + dest_path) + + def _cleanup_failed_disk_migration(self, instance_path, + revert_path, dest_path): + try: + if dest_path and self._pathutils.exists(dest_path): + self._pathutils.rmtree(dest_path) + if self._pathutils.exists(revert_path): + self._pathutils.rename(revert_path, instance_path) + except Exception as ex: + # Log and ignore this exception + LOG.exception(ex) + LOG.error(_("Cannot cleanup migration files")) + + def migrate_disk_and_power_off(self, context, instance, dest, + instance_type, network_info, + block_device_info=None): + LOG.debug(_("migrate_disk_and_power_off called"), instance=instance) + + self._vmops.power_off(instance) + + instance_name = instance["name"] + + (disk_files, + volume_drives) = self._vmutils.get_vm_storage_paths(instance_name) + + if disk_files: + self._migrate_disk_files(instance_name, disk_files, dest) + + self._vmops.destroy(instance, destroy_disks=False) + + # disk_info is not used + return "" + + def confirm_migration(self, migration, instance, network_info): + LOG.debug(_("confirm_migration called"), instance=instance) + + self._pathutils.get_instance_migr_revert_dir(instance['name'], + remove_dir=True) + + def _revert_migration_files(self, instance_name): + instance_path = self._pathutils.get_instance_dir( + instance_name, create_dir=False, remove_dir=True) + + revert_path = self._pathutils.get_instance_migr_revert_dir( + instance_name) + self._pathutils.rename(revert_path, instance_path) + + def finish_revert_migration(self, instance, network_info, + block_device_info=None): + LOG.debug(_("finish_revert_migration called"), instance=instance) + + instance_name = instance['name'] + self._revert_migration_files(instance_name) + + if self._volumeops.ebs_root_in_block_devices(block_device_info): + boot_vhd_path = None + else: + boot_vhd_path = self._pathutils.get_vhd_path(instance_name) + self._vmops.create_instance(instance, network_info, block_device_info, + boot_vhd_path) + + self._vmops.power_on(instance) + + 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'), locals()) + 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"), locals()) + self._vhdutils.reconnect_parent_vhd(diff_vhd_path, + base_vhd_copy_path) + + LOG.debug(_("Merging base disk %(base_vhd_copy_path)s and " + "diff disk %(diff_vhd_path)s"), locals()) + self._vhdutils.merge_vhd(diff_vhd_path, base_vhd_copy_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 _resize_vhd(self, vhd_path, new_size): + LOG.debug(_("Getting 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 + self._merge_base_vhd(vhd_path, base_disk_path) + LOG.debug(_("Resizing disk \"%(vhd_path)s\" to new max " + "size %(new_size)s"), locals()) + self._vhdutils.resize_vhd(vhd_path, new_size) + + def _check_base_disk(self, context, instance, diff_vhd_path, + src_base_disk_path): + base_disk_file_name = os.path.basename(src_base_disk_path) + if os.path.splitext(base_disk_file_name)[0] != instance["image_ref"]: + raise vmutils.HyperVException(_("Unexpected base VHD 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"), locals()) + self._vhdutils.reconnect_parent_vhd(diff_vhd_path, + base_vhd_path) + + def finish_migration(self, context, migration, instance, disk_info, + network_info, image_meta, resize_instance=False, + block_device_info=None): + LOG.debug(_("finish_migration called"), instance=instance) + + instance_name = instance['name'] + + if self._volumeops.ebs_root_in_block_devices(block_device_info): + boot_vhd_path = None + else: + boot_vhd_path = self._pathutils.get_vhd_path(instance_name) + if not self._pathutils.exists(boot_vhd_path): + raise vmutils.HyperVException(_("Cannot find boot VHD " + "file: %s") % boot_vhd_path) + + vhd_info = self._vhdutils.get_vhd_info(boot_vhd_path) + src_base_disk_path = vhd_info.get("ParentPath") + if src_base_disk_path: + self._check_base_disk(context, instance, boot_vhd_path, + src_base_disk_path) + + if resize_instance: + curr_size = vhd_info['MaxInternalSize'] + new_size = instance['root_gb'] * 1024 * 1024 * 1024 + if new_size < curr_size: + raise vmutils.HyperVException(_("Cannot resize a VHD to a " + "smaller size")) + elif new_size > curr_size: + self._resize_vhd(boot_vhd_path, new_size) + + self._vmops.create_instance(instance, network_info, block_device_info, + boot_vhd_path) + self._vmops.power_on(instance) diff --git a/nova/virt/hyperv/pathutils.py b/nova/virt/hyperv/pathutils.py index 7bc2e7ac2..05cfffaac 100644 --- a/nova/virt/hyperv/pathutils.py +++ b/nova/virt/hyperv/pathutils.py @@ -23,7 +23,18 @@ from nova.openstack.common import log as logging LOG = logging.getLogger(__name__) +hyperv_opts = [ + cfg.StrOpt('instances_path_share', + default="", + help='The name of a Windows share name mapped to the ' + '"instances_path" dir and used by the resize feature ' + 'to copy files to the target host. If left blank, an ' + 'administrative share will be used, looking for the same ' + '"instances_path" used locally'), +] + CONF = cfg.CONF +CONF.register_opts(hyperv_opts, 'HYPERV') CONF.import_opt('instances_path', 'nova.compute.manager') @@ -33,35 +44,79 @@ class PathUtils(object): import __builtin__ return __builtin__.open(path, mode) - def get_instances_path(self): - return os.path.normpath(CONF.instances_path) + def exists(self, path): + return os.path.exists(path) + + def makedirs(self, path): + os.makedirs(path) + + def remove(self, path): + os.remove(path) + + def rename(self, src, dest): + os.rename(src, dest) + + def copyfile(self, src, dest): + shutil.copyfile(src, dest) + + def copy(self, src, dest): + shutil.copy(src, dest) + + def rmtree(self, path): + shutil.rmtree(path) + + def get_instances_dir(self, remote_server=None): + local_instance_path = os.path.normpath(CONF.instances_path) - def get_instance_path(self, instance_name): - instance_path = os.path.join(self.get_instances_path(), instance_name) - if not os.path.exists(instance_path): - LOG.debug(_('Creating folder %s '), instance_path) - os.makedirs(instance_path) - return instance_path + if remote_server: + if CONF.HYPERV.instances_path_share: + path = CONF.HYPERV.instances_path_share + else: + # Use an administrative share + path = local_instance_path.replace(':', '$') + return '\\\\%(remote_server)s\\%(path)s' % locals() + else: + return local_instance_path + + def _check_create_dir(self, path): + if not self.exists(path): + LOG.debug(_('Creating directory: %s') % path) + self.makedirs(path) + + def _check_remove_dir(self, path): + if self.exists(path): + LOG.debug(_('Removing directory: %s') % path) + self.rmtree(path) + + def _get_instances_sub_dir(self, dir_name, remote_server=None, + create_dir=True, remove_dir=False): + instances_path = self.get_instances_dir(remote_server) + path = os.path.join(instances_path, dir_name) + if remove_dir: + self._check_remove_dir(path) + if create_dir: + self._check_create_dir(path) + return path + + def get_instance_migr_revert_dir(self, instance_name, create_dir=False, + remove_dir=False): + dir_name = '%s_revert' % instance_name + return self._get_instances_sub_dir(dir_name, None, create_dir, + remove_dir) + + def get_instance_dir(self, instance_name, remote_server=None, + create_dir=True, remove_dir=False): + return self._get_instances_sub_dir(instance_name, remote_server, + create_dir, remove_dir) def get_vhd_path(self, instance_name): - instance_path = self.get_instance_path(instance_name) - return os.path.join(instance_path, instance_name + ".vhd") - - def get_base_vhd_path(self, image_name): - base_dir = os.path.join(self.get_instances_path(), '_base') - if not os.path.exists(base_dir): - os.makedirs(base_dir) - return os.path.join(base_dir, image_name + ".vhd") - - def make_export_path(self, instance_name): - export_folder = os.path.join(self.get_instances_path(), "export", - instance_name) - if os.path.isdir(export_folder): - LOG.debug(_('Removing existing folder %s '), export_folder) - shutil.rmtree(export_folder) - LOG.debug(_('Creating folder %s '), export_folder) - os.makedirs(export_folder) - return export_folder - - def vhd_exists(self, path): - return os.path.exists(path) + instance_path = self.get_instance_dir(instance_name) + return os.path.join(instance_path, 'root.vhd') + + def get_base_vhd_dir(self): + return self._get_instances_sub_dir('_base') + + def get_export_dir(self, instance_name): + dir_name = os.path.join('export', instance_name) + return self._get_instances_sub_dir(dir_name, create_dir=True, + remove_dir=True) diff --git a/nova/virt/hyperv/snapshotops.py b/nova/virt/hyperv/snapshotops.py index c43f59b70..ab7c96943 100644 --- a/nova/virt/hyperv/snapshotops.py +++ b/nova/virt/hyperv/snapshotops.py @@ -19,7 +19,6 @@ Management class for VM snapshot operations. """ import os -import shutil from nova.compute import task_states from nova.image import glance @@ -57,7 +56,7 @@ class SnapshotOps(object): snapshot_path = self._vmutils.take_vm_snapshot(instance_name) update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) - export_folder = None + export_dir = None try: src_vhd_path = self._pathutils.get_vhd_path(instance_name) @@ -66,23 +65,24 @@ class SnapshotOps(object): src_base_disk_path = self._vhdutils.get_vhd_parent_path( src_vhd_path) - export_folder = self._pathutils.make_export_path(instance_name) + export_dir = self._pathutils.get_export_dir(instance_name) - dest_vhd_path = os.path.join(export_folder, os.path.basename( + dest_vhd_path = os.path.join(export_dir, os.path.basename( src_vhd_path)) LOG.debug(_('Copying VHD %(src_vhd_path)s to %(dest_vhd_path)s'), locals()) - shutil.copyfile(src_vhd_path, dest_vhd_path) + self._pathutils.copyfile(src_vhd_path, dest_vhd_path) image_vhd_path = None if not src_base_disk_path: image_vhd_path = dest_vhd_path else: basename = os.path.basename(src_base_disk_path) - dest_base_disk_path = os.path.join(export_folder, basename) + dest_base_disk_path = os.path.join(export_dir, basename) LOG.debug(_('Copying base disk %(src_vhd_path)s to ' '%(dest_base_disk_path)s'), locals()) - shutil.copyfile(src_base_disk_path, dest_base_disk_path) + self._pathutils.copyfile(src_base_disk_path, + dest_base_disk_path) LOG.debug(_("Reconnecting copied base VHD " "%(dest_base_disk_path)s and diff " @@ -111,6 +111,6 @@ class SnapshotOps(object): LOG.exception(ex) LOG.warning(_('Failed to remove snapshot for VM %s') % instance_name) - if export_folder: - LOG.debug(_('Removing folder %s '), export_folder) - shutil.rmtree(export_folder) + if export_dir: + LOG.debug(_('Removing directory: %s'), export_dir) + self._pathutils.rmtree(export_dir) diff --git a/nova/virt/hyperv/vhdutils.py b/nova/virt/hyperv/vhdutils.py index 21c4b4a6d..1e529807d 100644 --- a/nova/virt/hyperv/vhdutils.py +++ b/nova/virt/hyperv/vhdutils.py @@ -55,7 +55,17 @@ class VHDUtils(object): DestinationPath=dest_vhd_path) self._vmutils.check_ret_val(ret_val, job_path) + def resize_vhd(self, vhd_path, new_max_size): + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + + (job_path, ret_val) = image_man_svc.ExpandVirtualHardDisk( + Path=vhd_path, MaxInternalSize=new_max_size) + self._vmutils.check_ret_val(ret_val, job_path) + def get_vhd_parent_path(self, vhd_path): + return self.get_vhd_info(vhd_path).get("ParentPath") + + def get_vhd_info(self, vhd_path): image_man_svc = self._conn.Msvm_ImageManagementService()[0] (vhd_info, @@ -63,10 +73,19 @@ class VHDUtils(object): ret_val) = image_man_svc.GetVirtualHardDiskInfo(vhd_path) self._vmutils.check_ret_val(ret_val, job_path) - base_disk_path = None + vhd_info_dict = {} + et = ElementTree.fromstring(vhd_info) for item in et.findall("PROPERTY"): - if item.attrib["NAME"] == "ParentPath": - base_disk_path = item.find("VALUE").text - break - return base_disk_path + name = item.attrib["NAME"] + value_text = item.find("VALUE").text + if name == "ParentPath": + vhd_info_dict[name] = value_text + elif name in ["FileSize", "MaxInternalSize"]: + vhd_info_dict[name] = long(value_text) + elif name in ["InSavedState", "InUse"]: + vhd_info_dict[name] = bool(value_text) + elif name == "Type": + vhd_info_dict[name] = int(value_text) + + return vhd_info_dict diff --git a/nova/virt/hyperv/vmops.py b/nova/virt/hyperv/vmops.py index 58c1fc66a..c8acc0fa1 100644 --- a/nova/virt/hyperv/vmops.py +++ b/nova/virt/hyperv/vmops.py @@ -24,16 +24,18 @@ import os from nova.api.metadata import base as instance_metadata from nova import exception from nova.openstack.common import cfg +from nova.openstack.common import excutils from nova.openstack.common import importutils -from nova.openstack.common import lockutils from nova.openstack.common import log as logging from nova import utils from nova.virt import configdrive from nova.virt.hyperv import constants +from nova.virt.hyperv import hostutils +from nova.virt.hyperv import imagecache from nova.virt.hyperv import pathutils from nova.virt.hyperv import vhdutils from nova.virt.hyperv import vmutils -from nova.virt import images +from nova.virt.hyperv import volumeops LOG = logging.getLogger(__name__) @@ -69,11 +71,13 @@ class VMOps(object): 'nova.virt.hyperv.vif.HyperVNovaNetworkVIFDriver', } - def __init__(self, volumeops): + def __init__(self): + self._hostutils = hostutils.HostUtils() self._vmutils = vmutils.VMUtils() self._vhdutils = vhdutils.VHDUtils() self._pathutils = pathutils.PathUtils() - self._volumeops = volumeops + self._volumeops = volumeops.VolumeOps() + self._imagecache = imagecache.ImageCache() self._vif_driver = None self._load_vif_driver_class() @@ -106,71 +110,77 @@ class VMOps(object): 'num_cpu': info['NumberOfProcessors'], 'cpu_time': info['UpTime']} + def _create_boot_vhd(self, context, instance): + base_vhd_path = self._imagecache.get_cached_image(context, instance) + boot_vhd_path = self._pathutils.get_vhd_path(instance['name']) + + if CONF.use_cow_images: + LOG.debug(_("Creating differencing VHD. Parent: " + "%(base_vhd_path)s, Target: %(boot_vhd_path)s") + % locals()) + self._vhdutils.create_differencing_vhd(boot_vhd_path, + base_vhd_path) + else: + self._pathutils.copyfile(base_vhd_path, boot_vhd_path) + return boot_vhd_path + def spawn(self, context, instance, image_meta, injected_files, admin_password, network_info, block_device_info=None): """Create a new VM and start it.""" + LOG.info(_("Spawning new instance"), instance=instance) instance_name = instance['name'] if self._vmutils.vm_exists(instance_name): raise exception.InstanceExists(name=instance_name) - ebs_root = self._volumeops.volume_in_mapping( - self._volumeops.get_default_root_device(), - block_device_info) - - #If is not a boot from volume spawn - if not (ebs_root): - #Fetch the file, assume it is a VHD file. - vhdfile = self._pathutils.get_vhd_path(instance_name) - try: - self._cache_image(fn=self._fetch_image, - context=context, - target=vhdfile, - fname=instance['image_ref'], - image_id=instance['image_ref'], - user=instance['user_id'], - project=instance['project_id'], - cow=CONF.use_cow_images) - except Exception as exn: - LOG.exception(_('cache image failed: %s'), exn) - raise + if self._volumeops.ebs_root_in_block_devices(block_device_info): + boot_vhd_path = None + else: + boot_vhd_path = self._create_boot_vhd(context, instance) try: - self._vmutils.create_vm(instance_name, - instance['memory_mb'], - instance['vcpus'], - CONF.limit_cpu_features) - - if not ebs_root: - self._vmutils.attach_ide_drive(instance_name, - vhdfile, - 0, - 0, - constants.IDE_DISK) - else: - self._volumeops.attach_boot_volume(block_device_info, - instance_name) - - self._vmutils.create_scsi_controller(instance_name) - - for vif in network_info: - LOG.debug(_('Creating nic for instance: %s'), instance_name) - self._vmutils.create_nic(instance_name, - vif['id'], - vif['address']) - self._vif_driver.plug(instance, vif) + self.create_instance(instance, network_info, block_device_info, + boot_vhd_path) if configdrive.required_by(instance): self._create_config_drive(instance, injected_files, admin_password) - self._set_vm_state(instance_name, - constants.HYPERV_VM_STATE_ENABLED) + self.power_on(instance) except Exception as ex: LOG.exception(ex) self.destroy(instance) raise vmutils.HyperVException(_('Spawn instance failed')) + def create_instance(self, instance, network_info, + block_device_info, boot_vhd_path): + instance_name = instance['name'] + + self._vmutils.create_vm(instance_name, + instance['memory_mb'], + instance['vcpus'], + CONF.limit_cpu_features) + + if boot_vhd_path: + self._vmutils.attach_ide_drive(instance_name, + boot_vhd_path, + 0, + 0, + constants.IDE_DISK) + + self._vmutils.create_scsi_controller(instance_name) + + self._volumeops.attach_volumes(block_device_info, + instance_name, + boot_vhd_path is None) + + for vif in network_info: + LOG.debug(_('Creating nic for instance: %s'), instance_name) + self._vmutils.create_nic(instance_name, + vif['id'], + vif['address']) + self._vif_driver.plug(instance, vif) + def _create_config_drive(self, instance, injected_files, admin_password): if CONF.config_drive_format != 'iso9660': vmutils.HyperVException(_('Invalid config_drive_format "%s"') % @@ -186,7 +196,7 @@ class VMOps(object): content=injected_files, extra_md=extra_md) - instance_path = self._pathutils.get_instance_path( + instance_path = self._pathutils.get_instance_dir( instance['name']) configdrive_path_iso = os.path.join(instance_path, 'configdrive.iso') LOG.info(_('Creating config drive at %(path)s'), @@ -196,9 +206,9 @@ class VMOps(object): try: cdb.make_drive(configdrive_path_iso) except exception.ProcessExecutionError, e: - LOG.error(_('Creating config drive failed with error: %s'), - e, instance=instance) - raise + with excutils.save_and_reraise_exception(): + LOG.error(_('Creating config drive failed with error: %s'), + e, instance=instance) if not CONF.config_drive_cdrom: drive_type = constants.IDE_DISK @@ -213,7 +223,7 @@ class VMOps(object): configdrive_path_iso, configdrive_path, attempts=1) - os.remove(configdrive_path_iso) + self._pathutils.remove(configdrive_path_iso) else: drive_type = constants.IDE_DVD configdrive_path = configdrive_path_iso @@ -221,19 +231,35 @@ class VMOps(object): self._vmutils.attach_ide_drive(instance['name'], configdrive_path, 1, 0, drive_type) + def _disconnect_volumes(self, volume_drives): + for volume_drive in volume_drives: + self._volumeops.disconnect_volume(volume_drive) + + def _delete_disk_files(self, instance_name): + self._pathutils.get_instance_dir(instance_name, + create_dir=False, + remove_dir=True) + def destroy(self, instance, network_info=None, cleanup=True, destroy_disks=True): instance_name = instance['name'] - LOG.debug(_("Got request to destroy instance: %s"), instance_name) + LOG.info(_("Got request to destroy instance: %s"), instance_name) try: if self._vmutils.vm_exists(instance_name): - volumes_drives_list = self._vmutils.destroy_vm(instance_name, - destroy_disks) - #Disconnect volumes - for volume_drive in volumes_drives_list: - self._volumeops.disconnect_volume(volume_drive) + + #Stop the VM first. + self.power_off(instance) + + storage = self._vmutils.get_vm_storage_paths(instance_name) + (disk_files, volume_drives) = storage + + self._vmutils.destroy_vm(instance_name) + self._disconnect_volumes(volume_drives) else: LOG.debug(_("Instance not found: %s"), instance_name) + + if destroy_disks: + self._delete_disk_files(instance_name) except Exception as ex: LOG.exception(ex) raise vmutils.HyperVException(_('Failed to destroy instance: %s') % @@ -292,45 +318,3 @@ class VMOps(object): msg = _("Failed to change vm state of %(vm_name)s" " to %(req_state)s") % locals() raise vmutils.HyperVException(msg) - - def _fetch_image(self, target, context, image_id, user, project, - *args, **kwargs): - images.fetch(context, image_id, target, user, project) - - def _cache_image(self, fn, target, fname, cow=False, size=None, - *args, **kwargs): - """Wrapper for a method that creates and caches an image. - - This wrapper will save the image into a common store and create a - copy for use by the hypervisor. - - The underlying method should specify a kwarg of target representing - where the image will be saved. - - fname is used as the filename of the base image. The filename needs - to be unique to a given image. - - If cow is True, it will make a CoW image instead of a copy. - """ - @lockutils.synchronized(fname, 'nova-') - def call_if_not_exists(path, fn, *args, **kwargs): - if not os.path.exists(path): - fn(target=path, *args, **kwargs) - - if not self._pathutils.vhd_exists(target): - LOG.debug(_("Use CoW image: %s"), cow) - if cow: - parent_path = self._pathutils.get_base_vhd_path(fname) - call_if_not_exists(parent_path, fn, *args, **kwargs) - - LOG.debug(_("Creating differencing VHD. Parent: " - "%(parent_path)s, Target: %(target)s") % locals()) - try: - self._vhdutils.create_differencing_vhd(target, parent_path) - except Exception as ex: - LOG.exception(ex) - raise vmutils.HyperVException( - _('Failed to create a differencing disk from ' - '%(parent_path)s to %(target)s') % locals()) - else: - call_if_not_exists(target, fn, *args, **kwargs) diff --git a/nova/virt/hyperv/vmutils.py b/nova/virt/hyperv/vmutils.py index 876153902..d80144b65 100644 --- a/nova/virt/hyperv/vmutils.py +++ b/nova/virt/hyperv/vmutils.py @@ -135,6 +135,12 @@ class VMUtils(object): self._modify_virt_resource(procsetting, vm.path_()) + def update_vm(self, vm_name, memory_mb, vcpus_num, limit_cpu_features): + vm = self._lookup_vm_check(vm_name) + vmsetting = self._get_vm_setting_data(vm) + self._set_vm_memory(vm, vmsetting, memory_mb) + self._set_vm_vcpus(vm, vmsetting, vcpus_num, limit_cpu_features) + def create_vm(self, vm_name, memory_mb, vcpus_num, limit_cpu_features): """Creates a VM.""" vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] @@ -318,14 +324,9 @@ class VMUtils(object): LOG.debug(_("Successfully changed vm state of %(vm_name)s" " to %(req_state)s") % locals()) - def destroy_vm(self, vm_name, destroy_disks=True): - """Destroy the VM. Also destroy the associated VHD disk files.""" - + def get_vm_storage_paths(self, vm_name): vm = self._lookup_vm_check(vm_name) - #Stop the VM first. - self.set_vm_state(vm_name, constants.HYPERV_VM_STATE_DISABLED) - vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] vmsettings = vm.associators( wmi_result_class='Msvm_VirtualSystemSettingData') @@ -338,35 +339,25 @@ class VMUtils(object): if r.ResourceSubType == 'Microsoft Physical Disk Drive'] - #Collect volumes information before destroying the VM. - volumes_drives_list = [] + volume_drives = [] for volume_resource in volume_resources: drive_path = volume_resource.HostResource[0] - #Appending the Msvm_Disk path - volumes_drives_list.append(drive_path) + volume_drives.append(drive_path) - #Collect disk file information before destroying the VM. disk_files = [] for disk_resource in disk_resources: disk_files.extend([c for c in disk_resource.Connection]) + return (disk_files, volume_drives) + + def destroy_vm(self, vm_name): + vm = self._lookup_vm_check(vm_name) + + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] #Remove the VM. Does not destroy disks. (job_path, ret_val) = vs_man_svc.DestroyVirtualSystem(vm.path_()) self.check_ret_val(ret_val, job_path) - if destroy_disks: - #Delete associated vhd disk files. - for disk in disk_files: - LOG.debug(_("Deleting disk file: %(disk)s") % locals()) - self._delete_file(disk) - - return volumes_drives_list - - def _delete_file(self, path): - f = self._conn_cimv2.query("Select * from CIM_DataFile where " - "Name = '%s'" % path.replace("'", "''"))[0] - f.Delete() - def check_ret_val(self, ret_val, job_path, success_values=[0]): if ret_val == constants.WMI_JOB_STATUS_STARTED: self._wait_for_job(job_path) diff --git a/nova/virt/hyperv/volumeops.py b/nova/virt/hyperv/volumeops.py index a7e56b739..c665cbeb7 100644 --- a/nova/virt/hyperv/volumeops.py +++ b/nova/virt/hyperv/volumeops.py @@ -68,42 +68,25 @@ class VolumeOps(object): else: return volumeutils.VolumeUtils() - def attach_boot_volume(self, block_device_info, vm_name): - """Attach the boot volume to the IDE controller.""" - - LOG.debug(_("block device info: %s"), block_device_info) - ebs_root = driver.block_device_info_get_mapping( - block_device_info)[0] - - connection_info = ebs_root['connection_info'] - data = connection_info['data'] - target_lun = data['target_lun'] - target_iqn = data['target_iqn'] - target_portal = data['target_portal'] - self._volutils.login_storage_target(target_lun, target_iqn, - target_portal) - try: - #Getting the mounted disk - mounted_disk_path = self._get_mounted_disk_from_lun(target_iqn, - target_lun) - #Find the IDE controller for the vm. - ctrller_path = self._vmutils.get_vm_ide_controller(vm_name, 0) - #Attaching to the same slot as the VHD disk file - self._vmutils.attach_volume_to_controller(vm_name, - ctrller_path, 0, - mounted_disk_path) - except Exception as exn: - LOG.exception(_('Attach boot from volume failed: %s'), exn) - self._volutils.logout_storage_target(target_iqn) - raise vmutils.HyperVException( - _('Unable to attach boot volume to instance %s') % vm_name) - - def volume_in_mapping(self, mount_device, block_device_info): - return self._volutils.volume_in_mapping(mount_device, + def ebs_root_in_block_devices(self, block_device_info): + return self._volutils.volume_in_mapping(self._default_root_device, block_device_info) - def attach_volume(self, connection_info, instance_name): - """Attach a volume to the SCSI controller.""" + def attach_volumes(self, block_device_info, instance_name, ebs_root): + mapping = driver.block_device_info_get_mapping(block_device_info) + + if ebs_root: + self.attach_volume(mapping[0]['connection_info'], + instance_name, True) + mapping = mapping[1:] + for vol in mapping: + self.attach_volume(vol['connection_info'], instance_name) + + def attach_volume(self, connection_info, instance_name, ebs_root=False): + """ + Attach a volume to the SCSI controller or to the IDE controller if + ebs_root is True + """ LOG.debug(_("Attach_volume: %(connection_info)s to %(instance_name)s") % locals()) data = connection_info['data'] @@ -116,10 +99,19 @@ class VolumeOps(object): #Getting the mounted disk mounted_disk_path = self._get_mounted_disk_from_lun(target_iqn, target_lun) - #Find the SCSI controller for the vm - ctrller_path = self._vmutils.get_vm_iscsi_controller(instance_name) - slot = self._get_free_controller_slot(ctrller_path) + if ebs_root: + #Find the IDE controller for the vm. + ctrller_path = self._vmutils.get_vm_ide_controller( + instance_name, 0) + #Attaching to the first slot + slot = 0 + else: + #Find the SCSI controller for the vm + ctrller_path = self._vmutils.get_vm_iscsi_controller( + instance_name) + slot = self._get_free_controller_slot(ctrller_path) + self._vmutils.attach_volume_to_controller(instance_name, ctrller_path, slot, @@ -134,6 +126,11 @@ class VolumeOps(object): #Slots starts from 0, so the lenght of the disks gives us the free slot return self._vmutils.get_attached_disks_count(scsi_controller_path) + def detach_volumes(self, block_device_info, instance_name): + mapping = driver.block_device_info_get_mapping(block_device_info) + for vol in mapping: + self.detach_volume(vol['connection_info'], instance_name) + def detach_volume(self, connection_info, instance_name): """Dettach a volume to the SCSI controller.""" LOG.debug(_("Detach_volume: %(connection_info)s " @@ -192,6 +189,3 @@ class VolumeOps(object): physical_drive_path) #Logging out the target self._volutils.execute_log_out(session_id) - - def get_default_root_device(self): - return self._default_root_device |
