summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorAlessandro Pilotti <ap@pilotti.it>2013-02-11 23:43:28 +0200
committerAlessandro Pilotti <ap@pilotti.it>2013-02-12 01:43:08 +0200
commit98838cc59cffca3df893084eb7da87f6ac51de2f (patch)
tree5bbc7644ffbf15de786b83597735a63127c48349 /nova
parent20424b987946ee56e39f88aed7fddd35c54d7207 (diff)
downloadnova-98838cc59cffca3df893084eb7da87f6ac51de2f.tar.gz
nova-98838cc59cffca3df893084eb7da87f6ac51de2f.tar.xz
nova-98838cc59cffca3df893084eb7da87f6ac51de2f.zip
Implements resize / cold migration on Hyper-V
Blueprint: hyper-v-compute-resize Resize / cold migration is implemented by copying the local disks to a remote SMB share, identified by the configuration option HYPERV.instances_path_share or, if empty, by an administrative share with a remote path corresponding to the configuration option instances_path. The source instance directory is renamed by adding a suffix "_revert" and preserved until the migration is confirmed or reverted. In the former case the directory will be deleted and in the latter renamed to the original name. The VM corresponding to the instance is deleted on the source host and recreated on the target. Any mapped volume is disconnected on the source and reattached to the new VM on the target host. In case of resize operations, the local VHD file is resized according to the new flavor limits. Due to VHD limitations, an attempt to resize a disk to a smaller size will result in an exception. In case of differencing disks (CoW), should the base disk be missing in the target host's cache, it will be downloaded and reconnected to the copied differencing disk. Same host migrations are supported by using a temporary directory with suffix "_tmp" during disk file copy. Unit tests have been added for the new features accordingly. Change-Id: Ieee2afff8061d2ab73a2252b7d2499178d0515fd
Diffstat (limited to 'nova')
-rw-r--r--nova/tests/hyperv/fake.py52
-rw-r--r--nova/tests/test_hypervapi.py290
-rwxr-xr-xnova/virt/hyperv/driver.py31
-rw-r--r--nova/virt/hyperv/hostops.py15
-rw-r--r--nova/virt/hyperv/hostutils.py7
-rw-r--r--nova/virt/hyperv/imagecache.py60
-rw-r--r--nova/virt/hyperv/livemigrationops.py19
-rw-r--r--nova/virt/hyperv/livemigrationutils.py2
-rw-r--r--nova/virt/hyperv/migrationops.py233
-rw-r--r--nova/virt/hyperv/pathutils.py113
-rw-r--r--nova/virt/hyperv/snapshotops.py20
-rw-r--r--nova/virt/hyperv/vhdutils.py29
-rw-r--r--nova/virt/hyperv/vmops.py188
-rw-r--r--nova/virt/hyperv/vmutils.py39
-rw-r--r--nova/virt/hyperv/volumeops.py74
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