From 9433b9b6fbf0ab4c49db273457eefcd3354c8fd7 Mon Sep 17 00:00:00 2001 From: Claudiu Belu Date: Wed, 5 Aug 2015 20:18:42 +0300 Subject: [PATCH] Adds Nova utils classes as is Adds Utils classes in oslo_windows.utils. Adds utilsfactory module in oslo_windows. Adds Utils classes unit tests in oslo_windows.tests.utils. Adds utilsfactory module unit tests in oslo_windows.tests. --- oslo_windows/tests/__init__.py | 0 oslo_windows/tests/test_utilsfactory.py | 57 ++ oslo_windows/tests/utils/__init__.py | 0 .../tests/utils/test_basevolumeutils.py | 188 ++++ oslo_windows/tests/utils/test_hostutils.py | 141 +++ oslo_windows/tests/utils/test_hostutilsv2.py | 30 + oslo_windows/tests/utils/test_ioutils.py | 61 ++ .../tests/utils/test_livemigrationutils.py | 274 ++++++ oslo_windows/tests/utils/test_networkutils.py | 82 ++ .../tests/utils/test_networkutilsv2.py | 45 + oslo_windows/tests/utils/test_pathutils.py | 170 ++++ .../tests/utils/test_rdpconsoleutils.py | 28 + .../tests/utils/test_rdpconsoleutilsv2.py | 37 + oslo_windows/tests/utils/test_vhdutils.py | 288 ++++++ oslo_windows/tests/utils/test_vhdutilsv2.py | 244 +++++ oslo_windows/tests/utils/test_vmutils.py | 850 ++++++++++++++++++ oslo_windows/tests/utils/test_vmutilsv2.py | 265 ++++++ oslo_windows/tests/utils/test_volumeutils.py | 164 ++++ .../tests/utils/test_volumeutilsv2.py | 164 ++++ oslo_windows/utils/__init__.py | 0 oslo_windows/utils/basevolumeutils.py | 149 +++ oslo_windows/utils/constants.py | 102 +++ oslo_windows/utils/hostutils.py | 120 +++ oslo_windows/utils/hostutilsv2.py | 34 + oslo_windows/utils/ioutils.py | 73 ++ oslo_windows/utils/livemigrationutils.py | 252 ++++++ oslo_windows/utils/networkutils.py | 68 ++ oslo_windows/utils/networkutilsv2.py | 63 ++ oslo_windows/utils/pathutils.py | 271 ++++++ oslo_windows/utils/rdpconsoleutils.py | 21 + oslo_windows/utils/rdpconsoleutilsv2.py | 31 + oslo_windows/utils/vhdutils.py | 212 +++++ oslo_windows/utils/vhdutilsv2.py | 247 +++++ oslo_windows/utils/vmutils.py | 822 +++++++++++++++++ oslo_windows/utils/vmutilsv2.py | 328 +++++++ oslo_windows/utils/volumeutils.py | 122 +++ oslo_windows/utils/volumeutilsv2.py | 132 +++ oslo_windows/utilsfactory.py | 110 +++ 38 files changed, 6245 insertions(+) create mode 100644 oslo_windows/tests/__init__.py create mode 100644 oslo_windows/tests/test_utilsfactory.py create mode 100644 oslo_windows/tests/utils/__init__.py create mode 100644 oslo_windows/tests/utils/test_basevolumeutils.py create mode 100644 oslo_windows/tests/utils/test_hostutils.py create mode 100644 oslo_windows/tests/utils/test_hostutilsv2.py create mode 100644 oslo_windows/tests/utils/test_ioutils.py create mode 100644 oslo_windows/tests/utils/test_livemigrationutils.py create mode 100644 oslo_windows/tests/utils/test_networkutils.py create mode 100644 oslo_windows/tests/utils/test_networkutilsv2.py create mode 100644 oslo_windows/tests/utils/test_pathutils.py create mode 100644 oslo_windows/tests/utils/test_rdpconsoleutils.py create mode 100644 oslo_windows/tests/utils/test_rdpconsoleutilsv2.py create mode 100644 oslo_windows/tests/utils/test_vhdutils.py create mode 100644 oslo_windows/tests/utils/test_vhdutilsv2.py create mode 100644 oslo_windows/tests/utils/test_vmutils.py create mode 100644 oslo_windows/tests/utils/test_vmutilsv2.py create mode 100644 oslo_windows/tests/utils/test_volumeutils.py create mode 100644 oslo_windows/tests/utils/test_volumeutilsv2.py create mode 100644 oslo_windows/utils/__init__.py create mode 100644 oslo_windows/utils/basevolumeutils.py create mode 100644 oslo_windows/utils/constants.py create mode 100644 oslo_windows/utils/hostutils.py create mode 100644 oslo_windows/utils/hostutilsv2.py create mode 100644 oslo_windows/utils/ioutils.py create mode 100644 oslo_windows/utils/livemigrationutils.py create mode 100644 oslo_windows/utils/networkutils.py create mode 100644 oslo_windows/utils/networkutilsv2.py create mode 100644 oslo_windows/utils/pathutils.py create mode 100644 oslo_windows/utils/rdpconsoleutils.py create mode 100644 oslo_windows/utils/rdpconsoleutilsv2.py create mode 100644 oslo_windows/utils/vhdutils.py create mode 100644 oslo_windows/utils/vhdutilsv2.py create mode 100644 oslo_windows/utils/vmutils.py create mode 100644 oslo_windows/utils/vmutilsv2.py create mode 100644 oslo_windows/utils/volumeutils.py create mode 100644 oslo_windows/utils/volumeutilsv2.py create mode 100644 oslo_windows/utilsfactory.py diff --git a/oslo_windows/tests/__init__.py b/oslo_windows/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oslo_windows/tests/test_utilsfactory.py b/oslo_windows/tests/test_utilsfactory.py new file mode 100644 index 00000000..e656a057 --- /dev/null +++ b/oslo_windows/tests/test_utilsfactory.py @@ -0,0 +1,57 @@ +# Copyright 2014 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. + +""" +Unit tests for the Hyper-V utils factory. +""" + +import mock +from oslo_config import cfg + +from nova import test +from nova.virt.hyperv import hostutils +from nova.virt.hyperv import utilsfactory +from nova.virt.hyperv import vmutils +from nova.virt.hyperv import vmutilsv2 + +CONF = cfg.CONF + + +class TestHyperVUtilsFactory(test.NoDBTestCase): + def test_get_vmutils_force_v1_and_min_version(self): + self._test_returned_class(None, True, True) + + def test_get_vmutils_v2(self): + self._test_returned_class(vmutilsv2.VMUtilsV2, False, True) + + def test_get_vmutils_v2_r2(self): + self._test_returned_class(vmutils.VMUtils, False, False) + + def test_get_vmutils_force_v1_and_not_min_version(self): + self._test_returned_class(vmutils.VMUtils, True, False) + + def _test_returned_class(self, expected_class, force_v1, os_supports_v2): + CONF.set_override('force_hyperv_utils_v1', force_v1, 'hyperv') + with mock.patch.object( + hostutils.HostUtils, + 'check_min_windows_version') as mock_check_min_windows_version: + mock_check_min_windows_version.return_value = os_supports_v2 + + if os_supports_v2 and force_v1: + self.assertRaises(vmutils.HyperVException, + utilsfactory.get_vmutils) + else: + actual_class = type(utilsfactory.get_vmutils()) + self.assertEqual(actual_class, expected_class) diff --git a/oslo_windows/tests/utils/__init__.py b/oslo_windows/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oslo_windows/tests/utils/test_basevolumeutils.py b/oslo_windows/tests/utils/test_basevolumeutils.py new file mode 100644 index 00000000..75c24afa --- /dev/null +++ b/oslo_windows/tests/utils/test_basevolumeutils.py @@ -0,0 +1,188 @@ +# Copyright 2014 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. + +import mock + +from nova import test +from nova.virt.hyperv import basevolumeutils + + +def _exception_thrower(): + raise Exception("Testing exception handling.") + + +class BaseVolumeUtilsTestCase(test.NoDBTestCase): + """Unit tests for the Hyper-V BaseVolumeUtils class.""" + + _FAKE_COMPUTER_NAME = "fake_computer_name" + _FAKE_DOMAIN_NAME = "fake_domain_name" + _FAKE_INITIATOR_NAME = "fake_initiator_name" + _FAKE_INITIATOR_IQN_NAME = "iqn.1991-05.com.microsoft:fake_computer_name" + _FAKE_DISK_PATH = 'fake_path DeviceID="123\\\\2"' + _FAKE_MOUNT_DEVICE = '/dev/fake/mount' + _FAKE_DEVICE_NAME = '/dev/fake/path' + _FAKE_SWAP = {'device_name': _FAKE_DISK_PATH} + + def setUp(self): + self._volutils = basevolumeutils.BaseVolumeUtils() + self._volutils._conn_wmi = mock.MagicMock() + self._volutils._conn_cimv2 = mock.MagicMock() + + super(BaseVolumeUtilsTestCase, self).setUp() + + def test_get_iscsi_initiator_ok(self): + self._check_get_iscsi_initiator( + mock.MagicMock(return_value=mock.sentinel.FAKE_KEY), + self._FAKE_INITIATOR_NAME) + + def test_get_iscsi_initiator_exception(self): + initiator_name = "%(iqn)s.%(domain)s" % { + 'iqn': self._FAKE_INITIATOR_IQN_NAME, + 'domain': self._FAKE_DOMAIN_NAME + } + + self._check_get_iscsi_initiator(_exception_thrower, initiator_name) + + def _check_get_iscsi_initiator(self, winreg_method, expected): + mock_computer = mock.MagicMock() + mock_computer.name = self._FAKE_COMPUTER_NAME + mock_computer.Domain = self._FAKE_DOMAIN_NAME + self._volutils._conn_cimv2.Win32_ComputerSystem.return_value = [ + mock_computer] + + with mock.patch.object(basevolumeutils, + '_winreg', create=True) as mock_winreg: + mock_winreg.OpenKey = winreg_method + mock_winreg.QueryValueEx = mock.MagicMock(return_value=[expected]) + + initiator_name = self._volutils.get_iscsi_initiator() + self.assertEqual(expected, initiator_name) + + @mock.patch.object(basevolumeutils, 'driver') + def test_volume_in_mapping(self, mock_driver): + mock_driver.block_device_info_get_mapping.return_value = [ + {'mount_device': self._FAKE_MOUNT_DEVICE}] + mock_driver.block_device_info_get_swap = mock.MagicMock( + return_value=self._FAKE_SWAP) + mock_driver.block_device_info_get_ephemerals = mock.MagicMock( + return_value=[{'device_name': self._FAKE_DEVICE_NAME}]) + + mock_driver.swap_is_usable = mock.MagicMock(return_value=True) + + self.assertTrue(self._volutils.volume_in_mapping( + self._FAKE_MOUNT_DEVICE, mock.sentinel.FAKE_BLOCK_DEVICE_INFO)) + + def test_get_drive_number_from_disk_path(self): + fake_disk_path = ( + '\\\\WIN-I5BTVHOIFGK\\root\\virtualization\\v2:Msvm_DiskDrive.' + 'CreationClassName="Msvm_DiskDrive",DeviceID="Microsoft:353B3BE8-' + '310C-4cf4-839E-4E1B14616136\\\\1",SystemCreationClassName=' + '"Msvm_ComputerSystem",SystemName="WIN-I5BTVHOIFGK"') + expected_disk_number = 1 + + ret_val = self._volutils._get_drive_number_from_disk_path( + fake_disk_path) + + self.assertEqual(expected_disk_number, ret_val) + + def test_get_drive_number_not_found(self): + fake_disk_path = 'fake_disk_path' + + ret_val = self._volutils._get_drive_number_from_disk_path( + fake_disk_path) + + self.assertFalse(ret_val) + + @mock.patch.object(basevolumeutils.BaseVolumeUtils, + "_get_drive_number_from_disk_path") + def test_get_session_id_from_mounted_disk(self, mock_get_session_id): + mock_get_session_id.return_value = mock.sentinel.FAKE_DEVICE_NUMBER + mock_initiator_session = self._create_initiator_session() + mock_ses_class = self._volutils._conn_wmi.MSiSCSIInitiator_SessionClass + mock_ses_class.return_value = [mock_initiator_session] + + session_id = self._volutils.get_session_id_from_mounted_disk( + self._FAKE_DISK_PATH) + + self.assertEqual(mock.sentinel.FAKE_SESSION_ID, session_id) + + def test_get_devices_for_target(self): + init_session = self._create_initiator_session() + mock_ses_class = self._volutils._conn_wmi.MSiSCSIInitiator_SessionClass + mock_ses_class.return_value = [init_session] + devices = self._volutils._get_devices_for_target( + mock.sentinel.FAKE_IQN) + + self.assertEqual(init_session.Devices, devices) + + def test_get_devices_for_target_not_found(self): + mock_ses_class = self._volutils._conn_wmi.MSiSCSIInitiator_SessionClass + mock_ses_class.return_value = [] + devices = self._volutils._get_devices_for_target( + mock.sentinel.FAKE_IQN) + + self.assertEqual(0, len(devices)) + + @mock.patch.object(basevolumeutils.BaseVolumeUtils, + '_get_devices_for_target') + def test_get_device_number_for_target(self, fake_get_devices): + init_session = self._create_initiator_session() + fake_get_devices.return_value = init_session.Devices + mock_ses_class = self._volutils._conn_wmi.MSiSCSIInitiator_SessionClass + mock_ses_class.return_value = [init_session] + device_number = self._volutils.get_device_number_for_target( + mock.sentinel.FAKE_IQN, mock.sentinel.FAKE_LUN) + + self.assertEqual(mock.sentinel.FAKE_DEVICE_NUMBER, device_number) + + @mock.patch.object(basevolumeutils.BaseVolumeUtils, + '_get_devices_for_target') + def test_get_target_lun_count(self, fake_get_devices): + init_session = self._create_initiator_session() + # Only disk devices are being counted. + disk_device = mock.Mock(DeviceType=self._volutils._FILE_DEVICE_DISK) + init_session.Devices.append(disk_device) + fake_get_devices.return_value = init_session.Devices + + lun_count = self._volutils.get_target_lun_count( + mock.sentinel.FAKE_IQN) + + self.assertEqual(1, lun_count) + + @mock.patch.object(basevolumeutils.BaseVolumeUtils, + "_get_drive_number_from_disk_path") + def test_get_target_from_disk_path(self, mock_get_session_id): + mock_get_session_id.return_value = mock.sentinel.FAKE_DEVICE_NUMBER + init_sess = self._create_initiator_session() + mock_ses_class = self._volutils._conn_wmi.MSiSCSIInitiator_SessionClass + mock_ses_class.return_value = [init_sess] + + (target_name, scsi_lun) = self._volutils.get_target_from_disk_path( + self._FAKE_DISK_PATH) + + self.assertEqual(mock.sentinel.FAKE_TARGET_NAME, target_name) + self.assertEqual(mock.sentinel.FAKE_LUN, scsi_lun) + + def _create_initiator_session(self): + device = mock.MagicMock() + device.ScsiLun = mock.sentinel.FAKE_LUN + device.DeviceNumber = mock.sentinel.FAKE_DEVICE_NUMBER + device.TargetName = mock.sentinel.FAKE_TARGET_NAME + init_session = mock.MagicMock() + init_session.Devices = [device] + init_session.SessionId = mock.sentinel.FAKE_SESSION_ID + + return init_session diff --git a/oslo_windows/tests/utils/test_hostutils.py b/oslo_windows/tests/utils/test_hostutils.py new file mode 100644 index 00000000..0d6b641c --- /dev/null +++ b/oslo_windows/tests/utils/test_hostutils.py @@ -0,0 +1,141 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from nova import test +from nova.virt.hyperv import constants +from nova.virt.hyperv import hostutils + + +class FakeCPUSpec(object): + """Fake CPU Spec for unit tests.""" + + Architecture = mock.sentinel.cpu_arch + Name = mock.sentinel.cpu_name + Manufacturer = mock.sentinel.cpu_man + NumberOfCores = mock.sentinel.cpu_cores + NumberOfLogicalProcessors = mock.sentinel.cpu_procs + + +class HostUtilsTestCase(test.NoDBTestCase): + """Unit tests for the Hyper-V hostutils class.""" + + _FAKE_MEMORY_TOTAL = 1024 + _FAKE_MEMORY_FREE = 512 + _FAKE_DISK_SIZE = 1024 + _FAKE_DISK_FREE = 512 + _FAKE_VERSION_GOOD = '6.2.0' + _FAKE_VERSION_BAD = '6.1.9' + + def setUp(self): + self._hostutils = hostutils.HostUtils() + self._hostutils._conn_cimv2 = mock.MagicMock() + + super(HostUtilsTestCase, self).setUp() + + @mock.patch('nova.virt.hyperv.hostutils.ctypes') + def test_get_host_tick_count64(self, mock_ctypes): + tick_count64 = "100" + mock_ctypes.windll.kernel32.GetTickCount64.return_value = tick_count64 + response = self._hostutils.get_host_tick_count64() + self.assertEqual(tick_count64, response) + + def test_get_cpus_info(self): + cpu = mock.MagicMock(spec=FakeCPUSpec) + self._hostutils._conn_cimv2.query.return_value = [cpu] + cpu_list = self._hostutils.get_cpus_info() + self.assertEqual([cpu._mock_children], cpu_list) + + def test_get_memory_info(self): + memory = mock.MagicMock() + type(memory).TotalVisibleMemorySize = mock.PropertyMock( + return_value=self._FAKE_MEMORY_TOTAL) + type(memory).FreePhysicalMemory = mock.PropertyMock( + return_value=self._FAKE_MEMORY_FREE) + + self._hostutils._conn_cimv2.query.return_value = [memory] + total_memory, free_memory = self._hostutils.get_memory_info() + + self.assertEqual(self._FAKE_MEMORY_TOTAL, total_memory) + self.assertEqual(self._FAKE_MEMORY_FREE, free_memory) + + def test_get_volume_info(self): + disk = mock.MagicMock() + type(disk).Size = mock.PropertyMock(return_value=self._FAKE_DISK_SIZE) + type(disk).FreeSpace = mock.PropertyMock( + return_value=self._FAKE_DISK_FREE) + + self._hostutils._conn_cimv2.query.return_value = [disk] + (total_memory, free_memory) = self._hostutils.get_volume_info( + mock.sentinel.FAKE_DRIVE) + + self.assertEqual(self._FAKE_DISK_SIZE, total_memory) + self.assertEqual(self._FAKE_DISK_FREE, free_memory) + + def test_check_min_windows_version_true(self): + self._test_check_min_windows_version(self._FAKE_VERSION_GOOD, True) + + def test_check_min_windows_version_false(self): + self._test_check_min_windows_version(self._FAKE_VERSION_BAD, False) + + def _test_check_min_windows_version(self, version, expected): + os = mock.MagicMock() + os.Version = version + self._hostutils._conn_cimv2.Win32_OperatingSystem.return_value = [os] + self.assertEqual(expected, + self._hostutils.check_min_windows_version(6, 2)) + + def _test_host_power_action(self, action): + fake_win32 = mock.MagicMock() + fake_win32.Win32Shutdown = mock.MagicMock() + + self._hostutils._conn_cimv2.Win32_OperatingSystem.return_value = [ + fake_win32] + + if action == constants.HOST_POWER_ACTION_SHUTDOWN: + self._hostutils.host_power_action(action) + fake_win32.Win32Shutdown.assert_called_with( + self._hostutils._HOST_FORCED_SHUTDOWN) + elif action == constants.HOST_POWER_ACTION_REBOOT: + self._hostutils.host_power_action(action) + fake_win32.Win32Shutdown.assert_called_with( + self._hostutils._HOST_FORCED_REBOOT) + else: + self.assertRaises(NotImplementedError, + self._hostutils.host_power_action, action) + + def test_host_shutdown(self): + self._test_host_power_action(constants.HOST_POWER_ACTION_SHUTDOWN) + + def test_host_reboot(self): + self._test_host_power_action(constants.HOST_POWER_ACTION_REBOOT) + + def test_host_startup(self): + self._test_host_power_action(constants.HOST_POWER_ACTION_STARTUP) + + def test_get_supported_vm_types_2012_r2(self): + with mock.patch.object(self._hostutils, + 'check_min_windows_version') as mock_check_win: + mock_check_win.return_value = True + result = self._hostutils.get_supported_vm_types() + self.assertEqual([constants.IMAGE_PROP_VM_GEN_1, + constants.IMAGE_PROP_VM_GEN_2], result) + + def test_get_supported_vm_types(self): + with mock.patch.object(self._hostutils, + 'check_min_windows_version') as mock_check_win: + mock_check_win.return_value = False + result = self._hostutils.get_supported_vm_types() + self.assertEqual([constants.IMAGE_PROP_VM_GEN_1], result) diff --git a/oslo_windows/tests/utils/test_hostutilsv2.py b/oslo_windows/tests/utils/test_hostutilsv2.py new file mode 100644 index 00000000..b2e426bf --- /dev/null +++ b/oslo_windows/tests/utils/test_hostutilsv2.py @@ -0,0 +1,30 @@ +# Copyright 2015 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. + +import mock + +from nova import test +from nova.virt.hyperv import hostutilsv2 + + +class HostUtilsV2TestCase(test.NoDBTestCase): + """Unit tests for the Hyper-V hostutilsv2 class.""" + + def setUp(self): + self._hostutils = hostutilsv2.HostUtilsV2() + self._hostutils._conn_cimv2 = mock.MagicMock() + self._hostutils._conn_virt = mock.MagicMock() + + super(HostUtilsV2TestCase, self).setUp() diff --git a/oslo_windows/tests/utils/test_ioutils.py b/oslo_windows/tests/utils/test_ioutils.py new file mode 100644 index 00000000..b412ed43 --- /dev/null +++ b/oslo_windows/tests/utils/test_ioutils.py @@ -0,0 +1,61 @@ +# Copyright 2014 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.import mock + +import mock + +import os + +from nova import test +from nova.virt.hyperv import ioutils + + +class IOThreadTestCase(test.NoDBTestCase): + _FAKE_SRC = r'fake_source_file' + _FAKE_DEST = r'fake_dest_file' + _FAKE_MAX_BYTES = 1 + + def setUp(self): + self._iothread = ioutils.IOThread( + self._FAKE_SRC, self._FAKE_DEST, self._FAKE_MAX_BYTES) + super(IOThreadTestCase, self).setUp() + + @mock.patch('__builtin__.open') + @mock.patch('os.rename') + @mock.patch('os.path.exists') + @mock.patch('os.remove') + def test_copy(self, fake_remove, fake_exists, fake_rename, fake_open): + fake_data = 'a' + fake_src = mock.Mock() + fake_dest = mock.Mock() + + fake_src.read.return_value = fake_data + fake_dest.tell.return_value = 0 + fake_exists.return_value = True + + mock_context_manager = mock.MagicMock() + fake_open.return_value = mock_context_manager + mock_context_manager.__enter__.side_effect = [fake_src, fake_dest] + self._iothread._stopped.isSet = mock.Mock(side_effect=[False, True]) + + self._iothread._copy() + + fake_dest.seek.assert_called_once_with(0, os.SEEK_END) + fake_dest.write.assert_called_once_with(fake_data) + fake_dest.close.assert_called_once_with() + fake_rename.assert_called_once_with( + self._iothread._dest, self._iothread._dest_archive) + fake_remove.assert_called_once_with( + self._iothread._dest_archive) + self.assertEqual(3, fake_open.call_count) diff --git a/oslo_windows/tests/utils/test_livemigrationutils.py b/oslo_windows/tests/utils/test_livemigrationutils.py new file mode 100644 index 00000000..f3c6f19b --- /dev/null +++ b/oslo_windows/tests/utils/test_livemigrationutils.py @@ -0,0 +1,274 @@ +# Copyright 2014 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. + +import mock + +from nova import test +from nova.virt.hyperv import livemigrationutils + + +class LiveMigrationUtilsTestCase(test.NoDBTestCase): + """Unit tests for the Hyper-V LiveMigrationUtils class.""" + + _FAKE_RET_VAL = 0 + + _RESOURCE_TYPE_VHD = 31 + _RESOURCE_TYPE_DISK = 17 + _RESOURCE_SUB_TYPE_VHD = 'Microsoft:Hyper-V:Virtual Hard Disk' + _RESOURCE_SUB_TYPE_DISK = 'Microsoft:Hyper-V:Physical Disk Drive' + + def setUp(self): + self.liveutils = livemigrationutils.LiveMigrationUtils() + self.liveutils._vmutils = mock.MagicMock() + self.liveutils._volutils = mock.MagicMock() + + self._conn = mock.MagicMock() + self.liveutils._get_conn_v2 = mock.MagicMock(return_value=self._conn) + + super(LiveMigrationUtilsTestCase, self).setUp() + + def test_check_live_migration_config(self): + mock_migr_svc = self._conn.Msvm_VirtualSystemMigrationService()[0] + + vsmssd = mock.MagicMock() + vsmssd.EnableVirtualSystemMigration = True + mock_migr_svc.associators.return_value = [vsmssd] + mock_migr_svc.MigrationServiceListenerIPAdressList.return_value = [ + mock.sentinel.FAKE_HOST] + + self.liveutils.check_live_migration_config() + self.assertTrue(mock_migr_svc.associators.called) + + @mock.patch.object(livemigrationutils.LiveMigrationUtils, + '_destroy_planned_vm') + def test_check_existing_planned_vm_found(self, mock_destroy_planned_vm): + mock_vm = mock.MagicMock() + mock_v2 = mock.MagicMock() + mock_v2.Msvm_PlannedComputerSystem.return_value = [mock_vm] + self.liveutils._check_existing_planned_vm(mock_v2, mock_vm) + + mock_destroy_planned_vm.assert_called_once_with(mock_v2, mock_vm) + + @mock.patch.object(livemigrationutils.LiveMigrationUtils, + '_destroy_planned_vm') + def test_check_existing_planned_vm_none(self, mock_destroy_planned_vm): + mock_v2 = mock.MagicMock() + mock_v2.Msvm_PlannedComputerSystem.return_value = [] + self.liveutils._check_existing_planned_vm(mock_v2, mock.MagicMock()) + + self.assertFalse(mock_destroy_planned_vm.called) + + def test_create_remote_planned_vm(self): + mock_vsmsd = self._conn.query()[0] + mock_vm = mock.MagicMock() + mock_v2 = mock.MagicMock() + mock_v2.Msvm_PlannedComputerSystem.return_value = [mock_vm] + + migr_svc = self._conn.Msvm_VirtualSystemMigrationService()[0] + migr_svc.MigrateVirtualSystemToHost.return_value = ( + self._FAKE_RET_VAL, mock.sentinel.FAKE_JOB_PATH) + + resulted_vm = self.liveutils._create_remote_planned_vm( + self._conn, mock_v2, mock_vm, [mock.sentinel.FAKE_REMOTE_IP_ADDR], + mock.sentinel.FAKE_HOST) + + self.assertEqual(mock_vm, resulted_vm) + + migr_svc.MigrateVirtualSystemToHost.assert_called_once_with( + ComputerSystem=mock_vm.path_.return_value, + DestinationHost=mock.sentinel.FAKE_HOST, + MigrationSettingData=mock_vsmsd.GetText_.return_value) + + def test_get_physical_disk_paths(self): + ide_path = {mock.sentinel.IDE_PATH: mock.sentinel.IDE_HOST_RESOURCE} + scsi_path = {mock.sentinel.SCSI_PATH: mock.sentinel.SCSI_HOST_RESOURCE} + ide_ctrl = self.liveutils._vmutils.get_vm_ide_controller.return_value + scsi_ctrl = self.liveutils._vmutils.get_vm_scsi_controller.return_value + mock_get_controller_paths = ( + self.liveutils._vmutils.get_controller_volume_paths) + + mock_get_controller_paths.side_effect = [ide_path, scsi_path] + + result = self.liveutils._get_physical_disk_paths(mock.sentinel.VM_NAME) + + expected = dict(ide_path) + expected.update(scsi_path) + self.assertDictContainsSubset(expected, result) + calls = [mock.call(ide_ctrl), mock.call(scsi_ctrl)] + mock_get_controller_paths.assert_has_calls(calls) + + def test_get_physical_disk_paths_no_ide(self): + scsi_path = {mock.sentinel.SCSI_PATH: mock.sentinel.SCSI_HOST_RESOURCE} + scsi_ctrl = self.liveutils._vmutils.get_vm_scsi_controller.return_value + mock_get_controller_paths = ( + self.liveutils._vmutils.get_controller_volume_paths) + + self.liveutils._vmutils.get_vm_ide_controller.return_value = None + mock_get_controller_paths.return_value = scsi_path + + result = self.liveutils._get_physical_disk_paths(mock.sentinel.VM_NAME) + + self.assertEqual(scsi_path, result) + mock_get_controller_paths.assert_called_once_with(scsi_ctrl) + + @mock.patch.object(livemigrationutils.volumeutilsv2, 'VolumeUtilsV2') + def test_get_remote_disk_data(self, mock_vol_utils_class): + mock_vol_utils_remote = mock_vol_utils_class.return_value + mock_vm_utils = mock.MagicMock() + disk_paths = { + mock.sentinel.FAKE_RASD_PATH: mock.sentinel.FAKE_DISK_PATH} + self.liveutils._volutils.get_target_from_disk_path.return_value = ( + mock.sentinel.FAKE_IQN, mock.sentinel.FAKE_LUN) + mock_vol_utils_remote.get_device_number_for_target.return_value = ( + mock.sentinel.FAKE_DEV_NUM) + mock_vm_utils.get_mounted_disk_by_drive_number.return_value = ( + mock.sentinel.FAKE_DISK_PATH) + + disk_paths = self.liveutils._get_remote_disk_data( + mock_vm_utils, disk_paths, mock.sentinel.FAKE_HOST) + + self.liveutils._volutils.get_target_from_disk_path.assert_called_with( + mock.sentinel.FAKE_DISK_PATH) + mock_vol_utils_remote.get_device_number_for_target.assert_called_with( + mock.sentinel.FAKE_IQN, mock.sentinel.FAKE_LUN) + mock_vm_utils.get_mounted_disk_by_drive_number.assert_called_once_with( + mock.sentinel.FAKE_DEV_NUM) + + self.assertEqual( + {mock.sentinel.FAKE_RASD_PATH: mock.sentinel.FAKE_DISK_PATH}, + disk_paths) + + def test_update_planned_vm_disk_resources(self): + mock_vm_utils = mock.MagicMock() + + self._prepare_vm_mocks(self._RESOURCE_TYPE_DISK, + self._RESOURCE_SUB_TYPE_DISK) + mock_vm = self._conn.Msvm_ComputerSystem.return_value[0] + sasd = mock_vm.associators()[0].associators()[0] + + mock_vsmsvc = self._conn.Msvm_VirtualSystemManagementService()[0] + + self.liveutils._update_planned_vm_disk_resources( + mock_vm_utils, self._conn, mock_vm, mock.sentinel.FAKE_VM_NAME, + {sasd.path.return_value.RelPath: mock.sentinel.FAKE_RASD_PATH}) + + mock_vsmsvc.ModifyResourceSettings.assert_called_once_with( + ResourceSettings=[sasd.GetText_.return_value]) + + def test_get_vhd_setting_data(self): + self._prepare_vm_mocks(self._RESOURCE_TYPE_VHD, + self._RESOURCE_SUB_TYPE_VHD) + mock_vm = self._conn.Msvm_ComputerSystem.return_value[0] + mock_sasd = mock_vm.associators()[0].associators()[0] + + vhd_sds = self.liveutils._get_vhd_setting_data(mock_vm) + self.assertEqual([mock_sasd.GetText_.return_value], vhd_sds) + + def test_live_migrate_vm_helper(self): + mock_conn_local = mock.MagicMock() + mock_vm = mock.MagicMock() + mock_vsmsd = mock_conn_local.query()[0] + + mock_vsmsvc = mock_conn_local.Msvm_VirtualSystemMigrationService()[0] + mock_vsmsvc.MigrateVirtualSystemToHost.return_value = ( + self._FAKE_RET_VAL, mock.sentinel.FAKE_JOB_PATH) + + self.liveutils._live_migrate_vm( + mock_conn_local, mock_vm, None, + [mock.sentinel.FAKE_REMOTE_IP_ADDR], + mock.sentinel.FAKE_RASD_PATH, mock.sentinel.FAKE_HOST) + + mock_vsmsvc.MigrateVirtualSystemToHost.assert_called_once_with( + ComputerSystem=mock_vm.path_.return_value, + DestinationHost=mock.sentinel.FAKE_HOST, + MigrationSettingData=mock_vsmsd.GetText_.return_value, + NewResourceSettingData=mock.sentinel.FAKE_RASD_PATH) + + @mock.patch.object(livemigrationutils, 'vmutilsv2') + def test_live_migrate_vm(self, mock_vm_utils): + mock_vm_utils_remote = mock_vm_utils.VMUtilsV2.return_value + mock_vm = self._get_vm() + + mock_migr_svc = self._conn.Msvm_VirtualSystemMigrationService()[0] + mock_migr_svc.MigrationServiceListenerIPAddressList = [ + mock.sentinel.FAKE_REMOTE_IP_ADDR] + + # patches, call and assertions. + with mock.patch.multiple( + self.liveutils, + _destroy_planned_vm=mock.DEFAULT, + _get_physical_disk_paths=mock.DEFAULT, + _get_remote_disk_data=mock.DEFAULT, + _create_remote_planned_vm=mock.DEFAULT, + _update_planned_vm_disk_resources=mock.DEFAULT, + _get_vhd_setting_data=mock.DEFAULT, + _live_migrate_vm=mock.DEFAULT): + + disk_paths = { + mock.sentinel.FAKE_IDE_PATH: mock.sentinel.FAKE_SASD_RESOURCE} + self.liveutils._get_physical_disk_paths.return_value = disk_paths + + mock_disk_paths = [mock.sentinel.FAKE_DISK_PATH] + self.liveutils._get_remote_disk_data.return_value = ( + mock_disk_paths) + + self.liveutils._create_remote_planned_vm.return_value = mock_vm + + self.liveutils.live_migrate_vm(mock.sentinel.FAKE_VM_NAME, + mock.sentinel.FAKE_HOST) + + self.liveutils._get_remote_disk_data.assert_called_once_with( + mock_vm_utils_remote, disk_paths, mock.sentinel.FAKE_HOST) + + self.liveutils._create_remote_planned_vm.assert_called_once_with( + self._conn, self._conn, mock_vm, + [mock.sentinel.FAKE_REMOTE_IP_ADDR], mock.sentinel.FAKE_HOST) + + mocked_method = self.liveutils._update_planned_vm_disk_resources + mocked_method.assert_called_once_with( + mock_vm_utils_remote, self._conn, mock_vm, + mock.sentinel.FAKE_VM_NAME, mock_disk_paths) + + self.liveutils._live_migrate_vm.assert_called_once_with( + self._conn, mock_vm, mock_vm, + [mock.sentinel.FAKE_REMOTE_IP_ADDR], + self.liveutils._get_vhd_setting_data.return_value, + mock.sentinel.FAKE_HOST) + + def _prepare_vm_mocks(self, resource_type, resource_sub_type): + mock_vm_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + vm = self._get_vm() + self._conn.Msvm_PlannedComputerSystem.return_value = [vm] + mock_vm_svc.DestroySystem.return_value = (mock.sentinel.FAKE_JOB_PATH, + self._FAKE_RET_VAL) + mock_vm_svc.ModifyResourceSettings.return_value = ( + None, mock.sentinel.FAKE_JOB_PATH, self._FAKE_RET_VAL) + + sasd = mock.MagicMock() + other_sasd = mock.MagicMock() + sasd.ResourceType = resource_type + sasd.ResourceSubType = resource_sub_type + sasd.HostResource = [mock.sentinel.FAKE_SASD_RESOURCE] + sasd.path.return_value.RelPath = mock.sentinel.FAKE_DISK_PATH + + vm_settings = mock.MagicMock() + vm.associators.return_value = [vm_settings] + vm_settings.associators.return_value = [sasd, other_sasd] + + def _get_vm(self): + mock_vm = mock.MagicMock() + self._conn.Msvm_ComputerSystem.return_value = [mock_vm] + mock_vm.path_.return_value = mock.sentinel.FAKE_VM_PATH + return mock_vm diff --git a/oslo_windows/tests/utils/test_networkutils.py b/oslo_windows/tests/utils/test_networkutils.py new file mode 100644 index 00000000..281df298 --- /dev/null +++ b/oslo_windows/tests/utils/test_networkutils.py @@ -0,0 +1,82 @@ +# Copyright 2014 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from nova import test +from nova.virt.hyperv import networkutils +from nova.virt.hyperv import vmutils + + +class NetworkUtilsTestCase(test.NoDBTestCase): + """Unit tests for the Hyper-V NetworkUtils class.""" + + _FAKE_PORT = {'Name': mock.sentinel.FAKE_PORT_NAME} + _FAKE_RET_VALUE = 0 + + _MSVM_VIRTUAL_SWITCH = 'Msvm_VirtualSwitch' + + def setUp(self): + self._networkutils = networkutils.NetworkUtils() + self._networkutils._conn = mock.MagicMock() + + super(NetworkUtilsTestCase, self).setUp() + + def test_get_external_vswitch(self): + mock_vswitch = mock.MagicMock() + mock_vswitch.path_.return_value = mock.sentinel.FAKE_VSWITCH_PATH + getattr(self._networkutils._conn, + self._MSVM_VIRTUAL_SWITCH).return_value = [mock_vswitch] + + switch_path = self._networkutils.get_external_vswitch( + mock.sentinel.FAKE_VSWITCH_NAME) + + self.assertEqual(mock.sentinel.FAKE_VSWITCH_PATH, switch_path) + + def test_get_external_vswitch_not_found(self): + self._networkutils._conn.Msvm_VirtualEthernetSwitch.return_value = [] + + self.assertRaises(vmutils.HyperVException, + self._networkutils.get_external_vswitch, + mock.sentinel.FAKE_VSWITCH_NAME) + + def test_get_external_vswitch_no_name(self): + mock_vswitch = mock.MagicMock() + mock_vswitch.path_.return_value = mock.sentinel.FAKE_VSWITCH_PATH + + mock_ext_port = self._networkutils._conn.Msvm_ExternalEthernetPort()[0] + self._prepare_external_port(mock_vswitch, mock_ext_port) + + switch_path = self._networkutils.get_external_vswitch(None) + self.assertEqual(mock.sentinel.FAKE_VSWITCH_PATH, switch_path) + + def _prepare_external_port(self, mock_vswitch, mock_ext_port): + mock_lep = mock_ext_port.associators()[0] + mock_lep.associators.return_value = [mock_vswitch] + + def test_create_vswitch_port(self): + svc = self._networkutils._conn.Msvm_VirtualSwitchManagementService()[0] + svc.CreateSwitchPort.return_value = ( + self._FAKE_PORT, self._FAKE_RET_VALUE) + + port = self._networkutils.create_vswitch_port( + mock.sentinel.FAKE_VSWITCH_PATH, mock.sentinel.FAKE_PORT_NAME) + + svc.CreateSwitchPort.assert_called_once_with( + Name=mock.ANY, FriendlyName=mock.sentinel.FAKE_PORT_NAME, + ScopeOfResidence="", VirtualSwitch=mock.sentinel.FAKE_VSWITCH_PATH) + self.assertEqual(self._FAKE_PORT, port) + + def test_vswitch_port_needed(self): + self.assertTrue(self._networkutils.vswitch_port_needed()) diff --git a/oslo_windows/tests/utils/test_networkutilsv2.py b/oslo_windows/tests/utils/test_networkutilsv2.py new file mode 100644 index 00000000..1038e886 --- /dev/null +++ b/oslo_windows/tests/utils/test_networkutilsv2.py @@ -0,0 +1,45 @@ +# Copyright 2013 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from nova.tests.unit.virt.hyperv import test_networkutils +from nova.virt.hyperv import networkutilsv2 + + +class NetworkUtilsV2TestCase(test_networkutils.NetworkUtilsTestCase): + """Unit tests for the Hyper-V NetworkUtilsV2 class.""" + + _MSVM_VIRTUAL_SWITCH = 'Msvm_VirtualEthernetSwitch' + + def setUp(self): + super(NetworkUtilsV2TestCase, self).setUp() + self._networkutils = networkutilsv2.NetworkUtilsV2() + self._networkutils._conn = mock.MagicMock() + + def _prepare_external_port(self, mock_vswitch, mock_ext_port): + mock_lep = mock_ext_port.associators()[0] + mock_lep1 = mock_lep.associators()[0] + mock_esw = mock_lep1.associators()[0] + mock_esw.associators.return_value = [mock_vswitch] + + def test_create_vswitch_port(self): + self.assertRaises( + NotImplementedError, + self._networkutils.create_vswitch_port, + mock.sentinel.FAKE_VSWITCH_PATH, + mock.sentinel.FAKE_PORT_NAME) + + def test_vswitch_port_needed(self): + self.assertFalse(self._networkutils.vswitch_port_needed()) diff --git a/oslo_windows/tests/utils/test_pathutils.py b/oslo_windows/tests/utils/test_pathutils.py new file mode 100644 index 00000000..c0f17d6a --- /dev/null +++ b/oslo_windows/tests/utils/test_pathutils.py @@ -0,0 +1,170 @@ +# Copyright 2014 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +import mock + +from nova.tests.unit.virt.hyperv import test_base +from nova.virt.hyperv import constants +from nova.virt.hyperv import pathutils +from nova.virt.hyperv import vmutils + + +class PathUtilsTestCase(test_base.HyperVBaseTestCase): + """Unit tests for the Hyper-V PathUtils class.""" + + def setUp(self): + super(PathUtilsTestCase, self).setUp() + self.fake_instance_dir = os.path.join('C:', 'fake_instance_dir') + self.fake_instance_name = 'fake_instance_name' + + self._pathutils = pathutils.PathUtils() + + @mock.patch.object(pathutils.PathUtils, 'rename') + @mock.patch.object(os.path, 'isfile') + @mock.patch.object(os, 'listdir') + def test_move_folder_files(self, mock_listdir, mock_isfile, mock_rename): + src_dir = 'src' + dest_dir = 'dest' + fname = 'tmp_file.txt' + subdir = 'tmp_folder' + src_fname = os.path.join(src_dir, fname) + dest_fname = os.path.join(dest_dir, fname) + + # making sure src_subdir is not moved. + mock_listdir.return_value = [fname, subdir] + mock_isfile.side_effect = [True, False] + + self._pathutils.move_folder_files(src_dir, dest_dir) + mock_rename.assert_called_once_with(src_fname, dest_fname) + + def _mock_lookup_configdrive_path(self, ext): + self._pathutils.get_instance_dir = mock.MagicMock( + return_value=self.fake_instance_dir) + + def mock_exists(*args, **kwargs): + path = args[0] + return True if path[(path.rfind('.') + 1):] == ext else False + self._pathutils.exists = mock_exists + configdrive_path = self._pathutils.lookup_configdrive_path( + self.fake_instance_name) + return configdrive_path + + def test_lookup_configdrive_path(self): + for format_ext in constants.DISK_FORMAT_MAP: + configdrive_path = self._mock_lookup_configdrive_path(format_ext) + fake_path = os.path.join(self.fake_instance_dir, + 'configdrive.' + format_ext) + self.assertEqual(configdrive_path, fake_path) + + def test_lookup_configdrive_path_non_exist(self): + self._pathutils.get_instance_dir = mock.MagicMock( + return_value=self.fake_instance_dir) + self._pathutils.exists = mock.MagicMock(return_value=False) + configdrive_path = self._pathutils.lookup_configdrive_path( + self.fake_instance_name) + self.assertIsNone(configdrive_path) + + @mock.patch.object(pathutils.PathUtils, 'unmount_smb_share') + @mock.patch('os.path.exists') + def _test_check_smb_mapping(self, mock_exists, mock_unmount_smb_share, + existing_mappings=True, share_available=False): + mock_exists.return_value = share_available + + fake_mappings = ( + [mock.sentinel.smb_mapping] if existing_mappings else []) + + self._pathutils._smb_conn.Msft_SmbMapping.return_value = ( + fake_mappings) + + ret_val = self._pathutils.check_smb_mapping( + mock.sentinel.share_path) + + self.assertEqual(existing_mappings and share_available, ret_val) + if existing_mappings and not share_available: + mock_unmount_smb_share.assert_called_once_with( + mock.sentinel.share_path, force=True) + + def test_check_mapping(self): + self._test_check_smb_mapping() + + def test_remake_unavailable_mapping(self): + self._test_check_smb_mapping(existing_mappings=True, + share_available=False) + + def test_available_mapping(self): + self._test_check_smb_mapping(existing_mappings=True, + share_available=True) + + def test_mount_smb_share(self): + fake_create = self._pathutils._smb_conn.Msft_SmbMapping.Create + self._pathutils.mount_smb_share(mock.sentinel.share_path, + mock.sentinel.username, + mock.sentinel.password) + fake_create.assert_called_once_with( + RemotePath=mock.sentinel.share_path, + UserName=mock.sentinel.username, + Password=mock.sentinel.password) + + def _test_unmount_smb_share(self, force=False): + fake_mapping = mock.Mock() + smb_mapping_class = self._pathutils._smb_conn.Msft_SmbMapping + smb_mapping_class.return_value = [fake_mapping] + + self._pathutils.unmount_smb_share(mock.sentinel.share_path, + force) + + smb_mapping_class.assert_called_once_with( + RemotePath=mock.sentinel.share_path) + fake_mapping.Remove.assert_called_once_with(Force=force) + + def test_soft_unmount_smb_share(self): + self._test_unmount_smb_share() + + def test_force_unmount_smb_share(self): + self._test_unmount_smb_share(force=True) + + @mock.patch('shutil.rmtree') + def test_rmtree(self, mock_rmtree): + class WindowsError(Exception): + def __init__(self, winerror=None): + self.winerror = winerror + + mock_rmtree.side_effect = [WindowsError( + pathutils.ERROR_DIR_IS_NOT_EMPTY), True] + fake_windows_error = WindowsError + with mock.patch('__builtin__.WindowsError', + fake_windows_error, create=True): + self._pathutils.rmtree(mock.sentinel.FAKE_PATH) + + mock_rmtree.assert_has_calls([mock.call(mock.sentinel.FAKE_PATH), + mock.call(mock.sentinel.FAKE_PATH)]) + + @mock.patch('os.path.join') + def test_get_instances_sub_dir(self, fake_path_join): + + class WindowsError(Exception): + def __init__(self, winerror=None): + self.winerror = winerror + + fake_dir_name = "fake_dir_name" + fake_windows_error = WindowsError + self._pathutils._check_create_dir = mock.MagicMock( + side_effect=WindowsError(pathutils.ERROR_INVALID_NAME)) + with mock.patch('__builtin__.WindowsError', + fake_windows_error, create=True): + self.assertRaises(vmutils.HyperVException, + self._pathutils._get_instances_sub_dir, + fake_dir_name) diff --git a/oslo_windows/tests/utils/test_rdpconsoleutils.py b/oslo_windows/tests/utils/test_rdpconsoleutils.py new file mode 100644 index 00000000..98d4484b --- /dev/null +++ b/oslo_windows/tests/utils/test_rdpconsoleutils.py @@ -0,0 +1,28 @@ +# Copyright 2013 Cloudbase Solutions Srl +# +# 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. + +from nova import test +from nova.virt.hyperv import rdpconsoleutils + + +class RDPConsoleUtilsTestCase(test.NoDBTestCase): + def setUp(self): + self._rdpconsoleutils = rdpconsoleutils.RDPConsoleUtils() + super(RDPConsoleUtilsTestCase, self).setUp() + + def test_get_rdp_console_port(self): + listener_port = self._rdpconsoleutils.get_rdp_console_port() + + self.assertEqual(self._rdpconsoleutils._DEFAULT_HYPERV_RDP_PORT, + listener_port) diff --git a/oslo_windows/tests/utils/test_rdpconsoleutilsv2.py b/oslo_windows/tests/utils/test_rdpconsoleutilsv2.py new file mode 100644 index 00000000..bcdfaf92 --- /dev/null +++ b/oslo_windows/tests/utils/test_rdpconsoleutilsv2.py @@ -0,0 +1,37 @@ +# Copyright 2013 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from nova import test +from nova.virt.hyperv import rdpconsoleutilsv2 + + +class RDPConsoleUtilsV2TestCase(test.NoDBTestCase): + _FAKE_RDP_PORT = 1000 + + def setUp(self): + self._rdpconsoleutils = rdpconsoleutilsv2.RDPConsoleUtilsV2() + self._rdpconsoleutils._conn = mock.MagicMock() + + super(RDPConsoleUtilsV2TestCase, self).setUp() + + def test_get_rdp_console_port(self): + conn = self._rdpconsoleutils._conn + mock_rdp_setting_data = conn.Msvm_TerminalServiceSettingData()[0] + mock_rdp_setting_data.ListenerPort = self._FAKE_RDP_PORT + + listener_port = self._rdpconsoleutils.get_rdp_console_port() + + self.assertEqual(self._FAKE_RDP_PORT, listener_port) diff --git a/oslo_windows/tests/utils/test_vhdutils.py b/oslo_windows/tests/utils/test_vhdutils.py new file mode 100644 index 00000000..659dd5ef --- /dev/null +++ b/oslo_windows/tests/utils/test_vhdutils.py @@ -0,0 +1,288 @@ +# Copyright 2013 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo_utils import units + +from nova import test +from nova.virt.hyperv import constants +from nova.virt.hyperv import vhdutils +from nova.virt.hyperv import vmutils + + +class VHDUtilsBaseTestCase(test.NoDBTestCase): + "Base Class unit test classes of Hyper-V VHD Utils classes." + + _FAKE_VHD_PATH = "C:\\fake_path.vhdx" + _FAKE_PARENT_PATH = "C:\\fake_parent_path.vhdx" + _FAKE_FORMAT = 3 + _FAKE_TYPE = 3 + _FAKE_MAX_INTERNAL_SIZE = units.Gi + _FAKE_DYNAMIC_BLK_SIZE = 2097152 + _FAKE_BAD_TYPE = 5 + + _FAKE_JOB_PATH = 'fake_job_path' + _FAKE_RET_VAL = 0 + _FAKE_VHD_INFO_XML = ( + """ + +33554432 + + +Virtual Hard Disk Setting Data + + +Setting Data for a Virtual Hard Disk. + + +fake_path.vhdx + + +%(format)s + + +52794B89-AC06-4349-AC57-486CAAD52F69 + + +4096 + + +%(max_internal_size)s + + +%(parent_path)s + + +%(path)s + + +4096 + + +%(type)s + +""" % {'path': _FAKE_VHD_PATH, + 'parent_path': _FAKE_PARENT_PATH, + 'format': _FAKE_FORMAT, + 'max_internal_size': _FAKE_MAX_INTERNAL_SIZE, + 'type': _FAKE_TYPE}) + + +class VHDUtilsTestCase(VHDUtilsBaseTestCase): + """Unit tests for the Hyper-V VHDUtils class.""" + + def setUp(self): + super(VHDUtilsTestCase, self).setUp() + self._vhdutils = vhdutils.VHDUtils() + self._vhdutils._conn = mock.MagicMock() + self._vhdutils._vmutils = mock.MagicMock() + + self._fake_vhd_info = { + 'ParentPath': self._FAKE_PARENT_PATH, + 'MaxInternalSize': self._FAKE_MAX_INTERNAL_SIZE, + 'Type': self._FAKE_TYPE} + + def test_validate_vhd(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.ValidateVirtualHardDisk.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vhdutils.validate_vhd(self._FAKE_VHD_PATH) + mock_img_svc.ValidateVirtualHardDisk.assert_called_once_with( + Path=self._FAKE_VHD_PATH) + + def test_get_vhd_info(self): + self._mock_get_vhd_info() + vhd_info = self._vhdutils.get_vhd_info(self._FAKE_VHD_PATH) + self.assertEqual(self._fake_vhd_info, vhd_info) + + def _mock_get_vhd_info(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.GetVirtualHardDiskInfo.return_value = ( + self._FAKE_VHD_INFO_XML, self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + def test_create_dynamic_vhd(self): + self._vhdutils.get_vhd_info = mock.MagicMock( + return_value={'Format': self._FAKE_FORMAT}) + + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.CreateDynamicVirtualHardDisk.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vhdutils.create_dynamic_vhd(self._FAKE_VHD_PATH, + self._FAKE_MAX_INTERNAL_SIZE, + constants.DISK_FORMAT_VHD) + + mock_img_svc.CreateDynamicVirtualHardDisk.assert_called_once_with( + Path=self._FAKE_VHD_PATH, + MaxInternalSize=self._FAKE_MAX_INTERNAL_SIZE) + self._vhdutils._vmutils.check_ret_val.assert_called_once_with( + self._FAKE_RET_VAL, self._FAKE_JOB_PATH) + + def test_reconnect_parent_vhd(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.ReconnectParentVirtualHardDisk.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vhdutils.reconnect_parent_vhd(self._FAKE_VHD_PATH, + self._FAKE_PARENT_PATH) + mock_img_svc.ReconnectParentVirtualHardDisk.assert_called_once_with( + ChildPath=self._FAKE_VHD_PATH, + ParentPath=self._FAKE_PARENT_PATH, + Force=True) + self._vhdutils._vmutils.check_ret_val.assert_called_once_with( + self._FAKE_RET_VAL, self._FAKE_JOB_PATH) + + def test_merge_vhd(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.MergeVirtualHardDisk.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vhdutils.merge_vhd(self._FAKE_VHD_PATH, self._FAKE_VHD_PATH) + + mock_img_svc.MergeVirtualHardDisk.assert_called_once_with( + SourcePath=self._FAKE_VHD_PATH, + DestinationPath=self._FAKE_VHD_PATH) + self._vhdutils._vmutils.check_ret_val.assert_called_once_with( + self._FAKE_RET_VAL, self._FAKE_JOB_PATH) + + def test_resize_vhd(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.ExpandVirtualHardDisk.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vhdutils.get_internal_vhd_size_by_file_size = mock.MagicMock( + return_value=self._FAKE_MAX_INTERNAL_SIZE) + + self._vhdutils.resize_vhd(self._FAKE_VHD_PATH, + self._FAKE_MAX_INTERNAL_SIZE) + + mock_img_svc.ExpandVirtualHardDisk.assert_called_once_with( + Path=self._FAKE_VHD_PATH, + MaxInternalSize=self._FAKE_MAX_INTERNAL_SIZE) + self._vhdutils._vmutils.check_ret_val.assert_called_once_with( + self._FAKE_RET_VAL, self._FAKE_JOB_PATH) + + def _mocked_get_internal_vhd_size(self, root_vhd_size, vhd_type): + mock_get_vhd_info = mock.MagicMock(return_value={'Type': vhd_type}) + mock_get_blk_size = mock.MagicMock( + return_value=self._FAKE_DYNAMIC_BLK_SIZE) + with mock.patch.multiple(self._vhdutils, + get_vhd_info=mock_get_vhd_info, + _get_vhd_dynamic_blk_size=mock_get_blk_size): + + return self._vhdutils.get_internal_vhd_size_by_file_size( + None, root_vhd_size) + + def test_create_differencing_vhd(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.CreateDifferencingVirtualHardDisk.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vhdutils.create_differencing_vhd(self._FAKE_VHD_PATH, + self._FAKE_PARENT_PATH) + + mock_img_svc.CreateDifferencingVirtualHardDisk.assert_called_once_with( + Path=self._FAKE_VHD_PATH, + ParentPath=self._FAKE_PARENT_PATH) + + def test_get_internal_vhd_size_by_file_size_fixed(self): + root_vhd_size = 1 * 1024 ** 3 + real_size = self._mocked_get_internal_vhd_size( + root_vhd_size, constants.VHD_TYPE_FIXED) + + expected_vhd_size = 1 * 1024 ** 3 - 512 + self.assertEqual(expected_vhd_size, real_size) + + def test_get_internal_vhd_size_by_file_size_dynamic(self): + root_vhd_size = 20 * 1024 ** 3 + real_size = self._mocked_get_internal_vhd_size( + root_vhd_size, constants.VHD_TYPE_DYNAMIC) + + expected_vhd_size = 20 * 1024 ** 3 - 43008 + self.assertEqual(expected_vhd_size, real_size) + + def test_get_internal_vhd_size_by_file_size_differencing(self): + # For differencing images, the internal size of the parent vhd + # is returned + vhdutil = vhdutils.VHDUtils() + root_vhd_size = 20 * 1024 ** 3 + vhdutil.get_vhd_info = mock.MagicMock() + vhdutil.get_vhd_parent_path = mock.MagicMock() + vhdutil.get_vhd_parent_path.return_value = self._FAKE_VHD_PATH + vhdutil.get_vhd_info.side_effect = [ + {'Type': 4}, {'Type': constants.VHD_TYPE_DYNAMIC}] + + vhdutil._get_vhd_dynamic_blk_size = mock.MagicMock() + vhdutil._get_vhd_dynamic_blk_size.return_value = 2097152 + + real_size = vhdutil.get_internal_vhd_size_by_file_size(None, + root_vhd_size) + expected_vhd_size = 20 * 1024 ** 3 - 43008 + self.assertEqual(expected_vhd_size, real_size) + + def test_get_vhd_format_vhdx(self): + with mock.patch('nova.virt.hyperv.vhdutils.open', + mock.mock_open(read_data=vhdutils.VHDX_SIGNATURE), + create=True): + + format = self._vhdutils.get_vhd_format(self._FAKE_VHD_PATH) + + self.assertEqual(constants.DISK_FORMAT_VHDX, format) + + def test_get_vhd_format_vhd(self): + with mock.patch('nova.virt.hyperv.vhdutils.open', + mock.mock_open(), + create=True) as mock_open: + f = mock_open.return_value + f.tell.return_value = 1024 + readdata = ['notthesig', vhdutils.VHD_SIGNATURE] + + def read(*args): + for content in readdata: + yield content + + f.read.side_effect = read() + + format = self._vhdutils.get_vhd_format(self._FAKE_VHD_PATH) + + self.assertEqual(constants.DISK_FORMAT_VHD, format) + + def test_get_vhd_format_invalid_format(self): + with mock.patch('nova.virt.hyperv.vhdutils.open', + mock.mock_open(read_data='invalid'), + create=True) as mock_open: + f = mock_open.return_value + f.tell.return_value = 1024 + + self.assertRaises(vmutils.HyperVException, + self._vhdutils.get_vhd_format, + self._FAKE_VHD_PATH) + + def test_get_vhd_format_zero_length_file(self): + with mock.patch('nova.virt.hyperv.vhdutils.open', + mock.mock_open(read_data=''), + create=True) as mock_open: + f = mock_open.return_value + f.tell.return_value = 0 + + self.assertRaises(vmutils.HyperVException, + self._vhdutils.get_vhd_format, + self._FAKE_VHD_PATH) + + f.seek.assert_called_once_with(0, 2) + + def test_get_supported_vhd_format(self): + fmt = self._vhdutils.get_best_supported_vhd_format() + self.assertEqual(constants.DISK_FORMAT_VHD, fmt) diff --git a/oslo_windows/tests/utils/test_vhdutilsv2.py b/oslo_windows/tests/utils/test_vhdutilsv2.py new file mode 100644 index 00000000..765d1ddb --- /dev/null +++ b/oslo_windows/tests/utils/test_vhdutilsv2.py @@ -0,0 +1,244 @@ +# Copyright 2013 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from nova.tests.unit.virt.hyperv import test_vhdutils +from nova.virt.hyperv import constants +from nova.virt.hyperv import vhdutilsv2 +from nova.virt.hyperv import vmutils + + +class VHDUtilsV2TestCase(test_vhdutils.VHDUtilsBaseTestCase): + """Unit tests for the Hyper-V VHDUtilsV2 class.""" + + _FAKE_BLOCK_SIZE = 33554432 + _FAKE_LOG_SIZE = 1048576 + _FAKE_LOGICAL_SECTOR_SIZE = 4096 + _FAKE_METADATA_SIZE = 1048576 + _FAKE_PHYSICAL_SECTOR_SIZE = 4096 + + def setUp(self): + super(VHDUtilsV2TestCase, self).setUp() + self._vhdutils = vhdutilsv2.VHDUtilsV2() + self._vhdutils._conn = mock.MagicMock() + self._vhdutils._vmutils = mock.MagicMock() + + self._fake_file_handle = mock.MagicMock() + + self._fake_vhd_info = { + 'Path': self._FAKE_VHD_PATH, + 'ParentPath': self._FAKE_PARENT_PATH, + 'Format': self._FAKE_FORMAT, + 'MaxInternalSize': self._FAKE_MAX_INTERNAL_SIZE, + 'Type': self._FAKE_TYPE, + 'BlockSize': self._FAKE_BLOCK_SIZE, + 'LogicalSectorSize': self._FAKE_LOGICAL_SECTOR_SIZE, + 'PhysicalSectorSize': self._FAKE_PHYSICAL_SECTOR_SIZE} + + def _mock_get_vhd_info(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.GetVirtualHardDiskSettingData.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL, self._FAKE_VHD_INFO_XML) + + def test_get_vhd_info(self): + self._mock_get_vhd_info() + vhd_info = self._vhdutils.get_vhd_info(self._FAKE_VHD_PATH) + + self.assertEqual(self._FAKE_VHD_PATH, vhd_info['Path']) + self.assertEqual(self._FAKE_PARENT_PATH, vhd_info['ParentPath']) + self.assertEqual(self._FAKE_FORMAT, vhd_info['Format']) + self.assertEqual(self._FAKE_MAX_INTERNAL_SIZE, + vhd_info['MaxInternalSize']) + self.assertEqual(self._FAKE_TYPE, vhd_info['Type']) + + def test_get_vhd_info_no_parent(self): + fake_vhd_xml_no_parent = self._FAKE_VHD_INFO_XML.replace( + self._FAKE_PARENT_PATH, "") + + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.GetVirtualHardDiskSettingData.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL, fake_vhd_xml_no_parent) + + vhd_info = self._vhdutils.get_vhd_info(self._FAKE_VHD_PATH) + + self.assertEqual(self._FAKE_VHD_PATH, vhd_info['Path']) + self.assertIsNone(vhd_info['ParentPath']) + self.assertEqual(self._FAKE_FORMAT, vhd_info['Format']) + self.assertEqual(self._FAKE_MAX_INTERNAL_SIZE, + vhd_info['MaxInternalSize']) + self.assertEqual(self._FAKE_TYPE, vhd_info['Type']) + + def test_create_dynamic_vhd(self): + self._vhdutils.get_vhd_info = mock.MagicMock( + return_value={'Format': self._FAKE_FORMAT}) + + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.CreateVirtualHardDisk.return_value = (self._FAKE_JOB_PATH, + self._FAKE_RET_VAL) + + self._vhdutils.create_dynamic_vhd(self._FAKE_VHD_PATH, + self._FAKE_MAX_INTERNAL_SIZE, + constants.DISK_FORMAT_VHDX) + + self.assertTrue(mock_img_svc.CreateVirtualHardDisk.called) + + def test_create_differencing_vhd(self): + self._vhdutils.get_vhd_info = mock.MagicMock( + return_value={'ParentPath': self._FAKE_PARENT_PATH, + 'Format': self._FAKE_FORMAT}) + + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.CreateVirtualHardDisk.return_value = (self._FAKE_JOB_PATH, + self._FAKE_RET_VAL) + + self._vhdutils.create_differencing_vhd(self._FAKE_VHD_PATH, + self._FAKE_PARENT_PATH) + + self.assertTrue(mock_img_svc.CreateVirtualHardDisk.called) + + def test_reconnect_parent_vhd(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + fake_new_parent_path = 'fake_new_parent_path' + + self._vhdutils._get_vhd_info_xml = mock.MagicMock( + return_value=self._FAKE_VHD_INFO_XML) + + mock_img_svc.SetVirtualHardDiskSettingData.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vhdutils.reconnect_parent_vhd(self._FAKE_VHD_PATH, + fake_new_parent_path) + + expected_virt_disk_data = self._FAKE_VHD_INFO_XML.replace( + self._FAKE_PARENT_PATH, fake_new_parent_path) + mock_img_svc.SetVirtualHardDiskSettingData.assert_called_once_with( + VirtualDiskSettingData=expected_virt_disk_data) + + def test_reconnect_parent_vhd_exception(self): + # Test that reconnect_parent_vhd raises an exception if the + # vhd info XML does not contain the ParentPath property. + fake_vhd_info_xml = self._FAKE_VHD_INFO_XML.replace('ParentPath', + 'FakeParentPath') + self._vhdutils._get_vhd_info_xml = mock.Mock( + return_value=fake_vhd_info_xml) + + self.assertRaises(vmutils.HyperVException, + self._vhdutils.reconnect_parent_vhd, + self._FAKE_VHD_PATH, + mock.sentinel.new_parent_path) + + def test_resize_vhd(self): + mock_img_svc = self._vhdutils._conn.Msvm_ImageManagementService()[0] + mock_img_svc.ResizeVirtualHardDisk.return_value = (self._FAKE_JOB_PATH, + self._FAKE_RET_VAL) + self._vhdutils.get_internal_vhd_size_by_file_size = mock.MagicMock( + return_value=self._FAKE_MAX_INTERNAL_SIZE) + + self._vhdutils.resize_vhd(self._FAKE_VHD_PATH, + self._FAKE_MAX_INTERNAL_SIZE) + + mock_img_svc.ResizeVirtualHardDisk.assert_called_once_with( + Path=self._FAKE_VHD_PATH, + MaxInternalSize=self._FAKE_MAX_INTERNAL_SIZE) + + self.mock_get = self._vhdutils.get_internal_vhd_size_by_file_size + self.mock_get.assert_called_once_with(self._FAKE_VHD_PATH, + self._FAKE_MAX_INTERNAL_SIZE) + + def _test_get_vhdx_internal_size(self, vhd_type): + self._vhdutils.get_vhd_info = mock.MagicMock() + self._vhdutils.get_vhd_parent_path = mock.Mock( + return_value=self._FAKE_PARENT_PATH) + + if vhd_type == 4: + self._vhdutils.get_vhd_info.side_effect = [ + {'Type': vhd_type}, self._fake_vhd_info] + else: + self._vhdutils.get_vhd_info.return_value = self._fake_vhd_info + + @mock.patch('nova.virt.hyperv.vhdutils.VHDUtils.get_vhd_format') + def test_get_vhdx_internal_size(self, mock_get_vhd_format): + mock_get_vhd_format.return_value = constants.DISK_FORMAT_VHDX + self._mock_get_vhd_info() + self._vhdutils._get_vhdx_log_size = mock.MagicMock( + return_value=self._FAKE_LOG_SIZE) + self._vhdutils._get_vhdx_metadata_size_and_offset = mock.MagicMock( + return_value=(self._FAKE_METADATA_SIZE, 1024)) + self._vhdutils._get_vhdx_block_size = mock.MagicMock( + return_value=self._FAKE_BLOCK_SIZE) + + file_mock = mock.MagicMock() + with mock.patch('__builtin__.open', file_mock): + internal_size = ( + self._vhdutils.get_internal_vhd_size_by_file_size( + self._FAKE_VHD_PATH, self._FAKE_MAX_INTERNAL_SIZE)) + + self.assertEqual(self._FAKE_MAX_INTERNAL_SIZE - self._FAKE_BLOCK_SIZE, + internal_size) + + def test_get_vhdx_internal_size_dynamic(self): + self._test_get_vhdx_internal_size(3) + + def test_get_vhdx_internal_size_differencing(self): + self._test_get_vhdx_internal_size(4) + + def test_get_vhdx_current_header(self): + VHDX_HEADER_OFFSETS = [64 * 1024, 128 * 1024] + fake_sequence_numbers = ['\x01\x00\x00\x00\x00\x00\x00\x00', + '\x02\x00\x00\x00\x00\x00\x00\x00'] + self._fake_file_handle.read = mock.MagicMock( + side_effect=fake_sequence_numbers) + + offset = self._vhdutils._get_vhdx_current_header_offset( + self._fake_file_handle) + self.assertEqual(offset, VHDX_HEADER_OFFSETS[1]) + + def test_get_vhdx_metadata_size(self): + fake_metadata_offset = '\x01\x00\x00\x00\x00\x00\x00\x00' + fake_metadata_size = '\x01\x00\x00\x00' + self._fake_file_handle.read = mock.MagicMock( + side_effect=[fake_metadata_offset, fake_metadata_size]) + + metadata_size, metadata_offset = ( + self._vhdutils._get_vhdx_metadata_size_and_offset( + self._fake_file_handle)) + self.assertEqual(metadata_size, 1) + self.assertEqual(metadata_offset, 1) + + def test_get_block_size(self): + self._vhdutils._get_vhdx_metadata_size_and_offset = mock.MagicMock( + return_value=(self._FAKE_METADATA_SIZE, 1024)) + fake_block_size = '\x01\x00\x00\x00' + self._fake_file_handle.read = mock.MagicMock( + return_value=fake_block_size) + + block_size = self._vhdutils._get_vhdx_block_size( + self._fake_file_handle) + self.assertEqual(block_size, 1) + + def test_get_log_size(self): + fake_current_header_offset = 64 * 1024 + self._vhdutils._get_vhdx_current_header_offset = mock.MagicMock( + return_value=fake_current_header_offset) + fake_log_size = '\x01\x00\x00\x00' + self._fake_file_handle.read = mock.MagicMock( + return_value=fake_log_size) + + log_size = self._vhdutils._get_vhdx_log_size(self._fake_file_handle) + self.assertEqual(log_size, 1) + + def test_get_supported_vhd_format(self): + fmt = self._vhdutils.get_best_supported_vhd_format() + self.assertEqual(constants.DISK_FORMAT_VHDX, fmt) diff --git a/oslo_windows/tests/utils/test_vmutils.py b/oslo_windows/tests/utils/test_vmutils.py new file mode 100644 index 00000000..745f9b28 --- /dev/null +++ b/oslo_windows/tests/utils/test_vmutils.py @@ -0,0 +1,850 @@ +# Copyright 2014 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. + +import mock + +from six.moves import range + +from nova import exception +from nova import test +from nova.virt.hyperv import constants +from nova.virt.hyperv import vmutils + + +class VMUtilsTestCase(test.NoDBTestCase): + """Unit tests for the Hyper-V VMUtils class.""" + + _FAKE_VM_NAME = 'fake_vm' + _FAKE_MEMORY_MB = 2 + _FAKE_VCPUS_NUM = 4 + _FAKE_JOB_PATH = 'fake_job_path' + _FAKE_RET_VAL = 0 + _FAKE_RET_VAL_BAD = -1 + _FAKE_PATH = "fake_path" + _FAKE_CTRL_PATH = 'fake_ctrl_path' + _FAKE_CTRL_ADDR = 0 + _FAKE_DRIVE_ADDR = 0 + _FAKE_MOUNTED_DISK_PATH = 'fake_mounted_disk_path' + _FAKE_VM_PATH = "fake_vm_path" + _FAKE_VHD_PATH = "fake_vhd_path" + _FAKE_DVD_PATH = "fake_dvd_path" + _FAKE_VOLUME_DRIVE_PATH = "fake_volume_drive_path" + _FAKE_VM_UUID = "04e79212-39bc-4065-933c-50f6d48a57f6" + _FAKE_INSTANCE = {"name": _FAKE_VM_NAME, + "uuid": _FAKE_VM_UUID} + _FAKE_SNAPSHOT_PATH = "fake_snapshot_path" + _FAKE_RES_DATA = "fake_res_data" + _FAKE_HOST_RESOURCE = "fake_host_resource" + _FAKE_CLASS = "FakeClass" + _FAKE_RES_PATH = "fake_res_path" + _FAKE_RES_NAME = 'fake_res_name' + _FAKE_ADDRESS = "fake_address" + _FAKE_JOB_STATUS_DONE = 7 + _FAKE_JOB_STATUS_BAD = -1 + _FAKE_JOB_DESCRIPTION = "fake_job_description" + _FAKE_ERROR = "fake_error" + _FAKE_ELAPSED_TIME = 0 + _CONCRETE_JOB = "Msvm_ConcreteJob" + _FAKE_DYNAMIC_MEMORY_RATIO = 1.0 + + _FAKE_SUMMARY_INFO = {'NumberOfProcessors': 4, + 'EnabledState': 2, + 'MemoryUsage': 2, + 'UpTime': 1} + + _DEFINE_SYSTEM = 'DefineVirtualSystem' + _DESTROY_SYSTEM = 'DestroyVirtualSystem' + _DESTROY_SNAPSHOT = 'RemoveVirtualSystemSnapshot' + _ADD_RESOURCE = 'AddVirtualSystemResources' + _REMOVE_RESOURCE = 'RemoveVirtualSystemResources' + _SETTING_TYPE = 'SettingType' + _VM_GEN = constants.VM_GEN_1 + + _VIRTUAL_SYSTEM_TYPE_REALIZED = 3 + + def setUp(self): + self._vmutils = vmutils.VMUtils() + self._vmutils._conn = mock.MagicMock() + + super(VMUtilsTestCase, self).setUp() + + def test_enable_vm_metrics_collection(self): + self.assertRaises(NotImplementedError, + self._vmutils.enable_vm_metrics_collection, + self._FAKE_VM_NAME) + + def test_get_vm_summary_info(self): + self._lookup_vm() + mock_svc = self._vmutils._conn.Msvm_VirtualSystemManagementService()[0] + + mock_summary = mock.MagicMock() + mock_svc.GetSummaryInformation.return_value = (self._FAKE_RET_VAL, + [mock_summary]) + + for (key, val) in self._FAKE_SUMMARY_INFO.items(): + setattr(mock_summary, key, val) + + summary = self._vmutils.get_vm_summary_info(self._FAKE_VM_NAME) + self.assertEqual(self._FAKE_SUMMARY_INFO, summary) + + def _lookup_vm(self): + mock_vm = mock.MagicMock() + self._vmutils._lookup_vm_check = mock.MagicMock( + return_value=mock_vm) + mock_vm.path_.return_value = self._FAKE_VM_PATH + return mock_vm + + def test_lookup_vm_ok(self): + mock_vm = mock.MagicMock() + self._vmutils._conn.Msvm_ComputerSystem.return_value = [mock_vm] + vm = self._vmutils._lookup_vm_check(self._FAKE_VM_NAME) + self.assertEqual(mock_vm, vm) + + def test_lookup_vm_multiple(self): + mockvm = mock.MagicMock() + self._vmutils._conn.Msvm_ComputerSystem.return_value = [mockvm, mockvm] + self.assertRaises(vmutils.HyperVException, + self._vmutils._lookup_vm_check, + self._FAKE_VM_NAME) + + def test_lookup_vm_none(self): + self._vmutils._conn.Msvm_ComputerSystem.return_value = [] + self.assertRaises(exception.NotFound, + self._vmutils._lookup_vm_check, + self._FAKE_VM_NAME) + + def test_set_vm_memory_static(self): + self._test_set_vm_memory_dynamic(1.0) + + def test_set_vm_memory_dynamic(self): + self._test_set_vm_memory_dynamic(2.0) + + def _test_set_vm_memory_dynamic(self, dynamic_memory_ratio): + mock_vm = self._lookup_vm() + + mock_s = self._vmutils._conn.Msvm_VirtualSystemSettingData()[0] + mock_s.SystemType = 3 + + mock_vmsetting = mock.MagicMock() + mock_vmsetting.associators.return_value = [mock_s] + + self._vmutils._modify_virt_resource = mock.MagicMock() + + self._vmutils._set_vm_memory(mock_vm, mock_vmsetting, + self._FAKE_MEMORY_MB, + dynamic_memory_ratio) + + self._vmutils._modify_virt_resource.assert_called_with( + mock_s, self._FAKE_VM_PATH) + + if dynamic_memory_ratio > 1: + self.assertTrue(mock_s.DynamicMemoryEnabled) + else: + self.assertFalse(mock_s.DynamicMemoryEnabled) + + def test_soft_shutdown_vm(self): + mock_vm = self._lookup_vm() + mock_shutdown = mock.MagicMock() + mock_shutdown.InitiateShutdown.return_value = (self._FAKE_RET_VAL, ) + mock_vm.associators.return_value = [mock_shutdown] + + with mock.patch.object(self._vmutils, 'check_ret_val') as mock_check: + self._vmutils.soft_shutdown_vm(self._FAKE_VM_NAME) + + mock_shutdown.InitiateShutdown.assert_called_once_with( + Force=False, Reason=mock.ANY) + mock_check.assert_called_once_with(self._FAKE_RET_VAL, None) + + def test_soft_shutdown_vm_no_component(self): + mock_vm = self._lookup_vm() + mock_vm.associators.return_value = [] + + with mock.patch.object(self._vmutils, 'check_ret_val') as mock_check: + self._vmutils.soft_shutdown_vm(self._FAKE_VM_NAME) + self.assertFalse(mock_check.called) + + @mock.patch('nova.virt.hyperv.vmutils.VMUtils._get_vm_disks') + def test_get_vm_storage_paths(self, mock_get_vm_disks): + self._lookup_vm() + mock_rasds = self._create_mock_disks() + mock_get_vm_disks.return_value = ([mock_rasds[0]], [mock_rasds[1]]) + + storage = self._vmutils.get_vm_storage_paths(self._FAKE_VM_NAME) + (disk_files, volume_drives) = storage + + self.assertEqual([self._FAKE_VHD_PATH], disk_files) + self.assertEqual([self._FAKE_VOLUME_DRIVE_PATH], volume_drives) + + def test_get_vm_disks(self): + mock_vm = self._lookup_vm() + mock_vmsettings = [mock.MagicMock()] + mock_vm.associators.return_value = mock_vmsettings + + mock_rasds = self._create_mock_disks() + mock_vmsettings[0].associators.return_value = mock_rasds + + (disks, volumes) = self._vmutils._get_vm_disks(mock_vm) + + mock_vm.associators.assert_called_with( + wmi_result_class=self._vmutils._VIRTUAL_SYSTEM_SETTING_DATA_CLASS) + mock_vmsettings[0].associators.assert_called_with( + wmi_result_class=self._vmutils._RESOURCE_ALLOC_SETTING_DATA_CLASS) + self.assertEqual([mock_rasds[0]], disks) + self.assertEqual([mock_rasds[1]], volumes) + + def _create_mock_disks(self): + mock_rasd1 = mock.MagicMock() + mock_rasd1.ResourceSubType = self._vmutils._HARD_DISK_RES_SUB_TYPE + mock_rasd1.HostResource = [self._FAKE_VHD_PATH] + mock_rasd1.Connection = [self._FAKE_VHD_PATH] + mock_rasd1.Parent = self._FAKE_CTRL_PATH + mock_rasd1.Address = self._FAKE_ADDRESS + mock_rasd1.HostResource = [self._FAKE_VHD_PATH] + + mock_rasd2 = mock.MagicMock() + mock_rasd2.ResourceSubType = self._vmutils._PHYS_DISK_RES_SUB_TYPE + mock_rasd2.HostResource = [self._FAKE_VOLUME_DRIVE_PATH] + + return [mock_rasd1, mock_rasd2] + + @mock.patch.object(vmutils.VMUtils, '_set_vm_vcpus') + @mock.patch.object(vmutils.VMUtils, '_set_vm_memory') + @mock.patch.object(vmutils.VMUtils, '_get_wmi_obj') + def test_create_vm(self, mock_get_wmi_obj, mock_set_mem, mock_set_vcpus): + mock_svc = self._vmutils._conn.Msvm_VirtualSystemManagementService()[0] + getattr(mock_svc, self._DEFINE_SYSTEM).return_value = ( + None, self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + mock_vm = mock_get_wmi_obj.return_value + self._vmutils._conn.Msvm_ComputerSystem.return_value = [mock_vm] + + mock_s = mock.MagicMock() + setattr(mock_s, + self._SETTING_TYPE, + self._VIRTUAL_SYSTEM_TYPE_REALIZED) + mock_vm.associators.return_value = [mock_s] + + self._vmutils.create_vm(self._FAKE_VM_NAME, self._FAKE_MEMORY_MB, + self._FAKE_VCPUS_NUM, False, + self._FAKE_DYNAMIC_MEMORY_RATIO, + self._VM_GEN, + mock.sentinel.instance_path) + + self.assertTrue(getattr(mock_svc, self._DEFINE_SYSTEM).called) + mock_set_mem.assert_called_with(mock_vm, mock_s, self._FAKE_MEMORY_MB, + self._FAKE_DYNAMIC_MEMORY_RATIO) + + mock_set_vcpus.assert_called_with(mock_vm, mock_s, + self._FAKE_VCPUS_NUM, + False) + + def test_get_vm_scsi_controller(self): + self._prepare_get_vm_controller(self._vmutils._SCSI_CTRL_RES_SUB_TYPE) + path = self._vmutils.get_vm_scsi_controller(self._FAKE_VM_NAME) + self.assertEqual(self._FAKE_RES_PATH, path) + + @mock.patch("nova.virt.hyperv.vmutils.VMUtils.get_attached_disks") + def test_get_free_controller_slot(self, mock_get_attached_disks): + mock_disk = mock.MagicMock() + mock_disk.AddressOnParent = 3 + mock_get_attached_disks.return_value = [mock_disk] + + response = self._vmutils.get_free_controller_slot( + self._FAKE_CTRL_PATH) + + mock_get_attached_disks.assert_called_once_with( + self._FAKE_CTRL_PATH) + + self.assertEqual(response, 0) + + def test_get_free_controller_slot_exception(self): + fake_drive = mock.MagicMock() + type(fake_drive).AddressOnParent = mock.PropertyMock( + side_effect=range(constants.SCSI_CONTROLLER_SLOTS_NUMBER)) + + with mock.patch.object(self._vmutils, + 'get_attached_disks') as fake_get_attached_disks: + fake_get_attached_disks.return_value = ( + [fake_drive] * constants.SCSI_CONTROLLER_SLOTS_NUMBER) + self.assertRaises(vmutils.HyperVException, + self._vmutils.get_free_controller_slot, + mock.sentinel.scsi_controller_path) + + def test_get_vm_ide_controller(self): + self._prepare_get_vm_controller(self._vmutils._IDE_CTRL_RES_SUB_TYPE) + path = self._vmutils.get_vm_ide_controller(self._FAKE_VM_NAME, + self._FAKE_ADDRESS) + self.assertEqual(self._FAKE_RES_PATH, path) + + def test_get_vm_ide_controller_none(self): + self._prepare_get_vm_controller(self._vmutils._IDE_CTRL_RES_SUB_TYPE) + path = self._vmutils.get_vm_ide_controller( + mock.sentinel.FAKE_VM_NAME, mock.sentinel.FAKE_NOT_FOUND_ADDR) + self.assertNotEqual(self._FAKE_RES_PATH, path) + + def _prepare_get_vm_controller(self, resource_sub_type): + mock_vm = self._lookup_vm() + mock_vm_settings = mock.MagicMock() + mock_rasds = mock.MagicMock() + mock_rasds.path_.return_value = self._FAKE_RES_PATH + mock_rasds.ResourceSubType = resource_sub_type + mock_rasds.Address = self._FAKE_ADDRESS + mock_vm_settings.associators.return_value = [mock_rasds] + mock_vm.associators.return_value = [mock_vm_settings] + + def _prepare_resources(self, mock_path, mock_subtype, mock_vm_settings): + mock_rasds = mock_vm_settings.associators.return_value[0] + mock_rasds.path_.return_value = mock_path + mock_rasds.ResourceSubType = mock_subtype + return mock_rasds + + @mock.patch("nova.virt.hyperv.vmutils.VMUtils.get_free_controller_slot") + @mock.patch("nova.virt.hyperv.vmutils.VMUtils._get_vm_scsi_controller") + def test_attach_scsi_drive(self, mock_get_vm_scsi_controller, + mock_get_free_controller_slot): + mock_vm = self._lookup_vm() + mock_get_vm_scsi_controller.return_value = self._FAKE_CTRL_PATH + mock_get_free_controller_slot.return_value = self._FAKE_DRIVE_ADDR + + with mock.patch.object(self._vmutils, + 'attach_drive') as mock_attach_drive: + self._vmutils.attach_scsi_drive(mock_vm, self._FAKE_PATH, + constants.DISK) + + mock_get_vm_scsi_controller.assert_called_once_with(mock_vm) + mock_get_free_controller_slot.assert_called_once_with( + self._FAKE_CTRL_PATH) + mock_attach_drive.assert_called_once_with( + mock_vm, self._FAKE_PATH, self._FAKE_CTRL_PATH, + self._FAKE_DRIVE_ADDR, constants.DISK) + + @mock.patch.object(vmutils.VMUtils, '_get_new_resource_setting_data') + @mock.patch.object(vmutils.VMUtils, '_get_vm_ide_controller') + def test_attach_ide_drive(self, mock_get_ide_ctrl, mock_get_new_rsd): + mock_vm = self._lookup_vm() + mock_rsd = mock_get_new_rsd.return_value + + with mock.patch.object(self._vmutils, + '_add_virt_resource') as mock_add_virt_res: + self._vmutils.attach_ide_drive(self._FAKE_VM_NAME, + self._FAKE_CTRL_PATH, + self._FAKE_CTRL_ADDR, + self._FAKE_DRIVE_ADDR) + + mock_add_virt_res.assert_called_with(mock_rsd, + mock_vm.path_.return_value) + + mock_get_ide_ctrl.assert_called_with(mock_vm, self._FAKE_CTRL_ADDR) + self.assertTrue(mock_get_new_rsd.called) + + @mock.patch.object(vmutils.VMUtils, '_get_new_resource_setting_data') + def test_create_scsi_controller(self, mock_get_new_rsd): + mock_vm = self._lookup_vm() + with mock.patch.object(self._vmutils, + '_add_virt_resource') as mock_add_virt_res: + self._vmutils.create_scsi_controller(self._FAKE_VM_NAME) + + mock_add_virt_res.assert_called_with(mock_get_new_rsd.return_value, + mock_vm.path_.return_value) + + @mock.patch.object(vmutils.VMUtils, '_get_new_resource_setting_data') + def test_attach_volume_to_controller(self, mock_get_new_rsd): + mock_vm = self._lookup_vm() + with mock.patch.object(self._vmutils, + '_add_virt_resource') as mock_add_virt_res: + self._vmutils.attach_volume_to_controller( + self._FAKE_VM_NAME, self._FAKE_CTRL_PATH, self._FAKE_CTRL_ADDR, + self._FAKE_MOUNTED_DISK_PATH) + + mock_add_virt_res.assert_called_with(mock_get_new_rsd.return_value, + mock_vm.path_.return_value) + + @mock.patch.object(vmutils.VMUtils, '_modify_virt_resource') + @mock.patch.object(vmutils.VMUtils, '_get_nic_data_by_name') + def test_set_nic_connection(self, mock_get_nic_conn, mock_modify_virt_res): + self._lookup_vm() + mock_nic = mock_get_nic_conn.return_value + self._vmutils.set_nic_connection(self._FAKE_VM_NAME, None, None) + + mock_modify_virt_res.assert_called_with(mock_nic, self._FAKE_VM_PATH) + + @mock.patch.object(vmutils.VMUtils, '_get_new_setting_data') + def test_create_nic(self, mock_get_new_virt_res): + self._lookup_vm() + mock_nic = mock_get_new_virt_res.return_value + + with mock.patch.object(self._vmutils, + '_add_virt_resource') as mock_add_virt_res: + self._vmutils.create_nic( + self._FAKE_VM_NAME, self._FAKE_RES_NAME, self._FAKE_ADDRESS) + + mock_add_virt_res.assert_called_with(mock_nic, self._FAKE_VM_PATH) + + def test_set_vm_state(self): + mock_vm = self._lookup_vm() + mock_vm.RequestStateChange.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vmutils.set_vm_state(self._FAKE_VM_NAME, + constants.HYPERV_VM_STATE_ENABLED) + mock_vm.RequestStateChange.assert_called_with( + constants.HYPERV_VM_STATE_ENABLED) + + def test_destroy_vm(self): + self._lookup_vm() + + mock_svc = self._vmutils._conn.Msvm_VirtualSystemManagementService()[0] + getattr(mock_svc, self._DESTROY_SYSTEM).return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vmutils.destroy_vm(self._FAKE_VM_NAME) + + getattr(mock_svc, self._DESTROY_SYSTEM).assert_called_with( + self._FAKE_VM_PATH) + + @mock.patch.object(vmutils.VMUtils, '_wait_for_job') + def test_check_ret_val_ok(self, mock_wait_for_job): + self._vmutils.check_ret_val(constants.WMI_JOB_STATUS_STARTED, + self._FAKE_JOB_PATH) + mock_wait_for_job.assert_called_once_with(self._FAKE_JOB_PATH) + + def test_check_ret_val_exception(self): + self.assertRaises(vmutils.HyperVException, + self._vmutils.check_ret_val, + self._FAKE_RET_VAL_BAD, + self._FAKE_JOB_PATH) + + def test_wait_for_job_done(self): + mockjob = self._prepare_wait_for_job(constants.WMI_JOB_STATE_COMPLETED) + job = self._vmutils._wait_for_job(self._FAKE_JOB_PATH) + self.assertEqual(mockjob, job) + + def test_wait_for_job_exception_concrete_job(self): + mock_job = self._prepare_wait_for_job() + mock_job.path.return_value.Class = self._CONCRETE_JOB + self.assertRaises(vmutils.HyperVException, + self._vmutils._wait_for_job, + self._FAKE_JOB_PATH) + + def test_wait_for_job_exception_with_error(self): + mock_job = self._prepare_wait_for_job() + mock_job.GetError.return_value = (self._FAKE_ERROR, self._FAKE_RET_VAL) + self.assertRaises(vmutils.HyperVException, + self._vmutils._wait_for_job, + self._FAKE_JOB_PATH) + + def test_wait_for_job_exception_no_error(self): + mock_job = self._prepare_wait_for_job() + mock_job.GetError.return_value = (None, None) + self.assertRaises(vmutils.HyperVException, + self._vmutils._wait_for_job, + self._FAKE_JOB_PATH) + + def _prepare_wait_for_job(self, state=_FAKE_JOB_STATUS_BAD): + mock_job = mock.MagicMock() + mock_job.JobState = state + mock_job.Description = self._FAKE_JOB_DESCRIPTION + mock_job.ElapsedTime = self._FAKE_ELAPSED_TIME + + self._vmutils._get_wmi_obj = mock.MagicMock(return_value=mock_job) + return mock_job + + def test_add_virt_resource(self): + mock_svc = self._vmutils._conn.Msvm_VirtualSystemManagementService()[0] + getattr(mock_svc, self._ADD_RESOURCE).return_value = ( + self._FAKE_JOB_PATH, mock.MagicMock(), self._FAKE_RET_VAL) + mock_res_setting_data = mock.MagicMock() + mock_res_setting_data.GetText_.return_value = self._FAKE_RES_DATA + + self._vmutils._add_virt_resource(mock_res_setting_data, + self._FAKE_VM_PATH) + self._assert_add_resources(mock_svc) + + def test_modify_virt_resource(self): + mock_svc = self._vmutils._conn.Msvm_VirtualSystemManagementService()[0] + mock_svc.ModifyVirtualSystemResources.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + mock_res_setting_data = mock.MagicMock() + mock_res_setting_data.GetText_.return_value = self._FAKE_RES_DATA + + self._vmutils._modify_virt_resource(mock_res_setting_data, + self._FAKE_VM_PATH) + + mock_svc.ModifyVirtualSystemResources.assert_called_with( + ResourceSettingData=[self._FAKE_RES_DATA], + ComputerSystem=self._FAKE_VM_PATH) + + def test_remove_virt_resource(self): + mock_svc = self._vmutils._conn.Msvm_VirtualSystemManagementService()[0] + getattr(mock_svc, self._REMOVE_RESOURCE).return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + mock_res_setting_data = mock.MagicMock() + mock_res_setting_data.path_.return_value = self._FAKE_RES_PATH + + self._vmutils._remove_virt_resource(mock_res_setting_data, + self._FAKE_VM_PATH) + self._assert_remove_resources(mock_svc) + + def test_set_disk_host_resource(self): + self._lookup_vm() + mock_rasds = self._create_mock_disks() + + self._vmutils._get_vm_disks = mock.MagicMock( + return_value=([mock_rasds[0]], [mock_rasds[1]])) + self._vmutils._modify_virt_resource = mock.MagicMock() + self._vmutils._get_disk_resource_address = mock.MagicMock( + return_value=self._FAKE_ADDRESS) + + self._vmutils.set_disk_host_resource( + self._FAKE_VM_NAME, + self._FAKE_CTRL_PATH, + self._FAKE_ADDRESS, + mock.sentinel.fake_new_mounted_disk_path) + self._vmutils._get_disk_resource_address.assert_called_with( + mock_rasds[0]) + self._vmutils._modify_virt_resource.assert_called_with( + mock_rasds[0], self._FAKE_VM_PATH) + self.assertEqual( + mock.sentinel.fake_new_mounted_disk_path, + mock_rasds[0].HostResource[0]) + + @mock.patch.object(vmutils, 'wmi', create=True) + @mock.patch.object(vmutils.VMUtils, 'check_ret_val') + def test_take_vm_snapshot(self, mock_check_ret_val, mock_wmi): + self._lookup_vm() + + mock_svc = self._get_snapshot_service() + mock_svc.CreateVirtualSystemSnapshot.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL, mock.MagicMock()) + + self._vmutils.take_vm_snapshot(self._FAKE_VM_NAME) + + mock_svc.CreateVirtualSystemSnapshot.assert_called_with( + self._FAKE_VM_PATH) + + mock_check_ret_val.assert_called_once_with(self._FAKE_RET_VAL, + self._FAKE_JOB_PATH) + + def test_remove_vm_snapshot(self): + mock_svc = self._get_snapshot_service() + getattr(mock_svc, self._DESTROY_SNAPSHOT).return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self._vmutils.remove_vm_snapshot(self._FAKE_SNAPSHOT_PATH) + getattr(mock_svc, self._DESTROY_SNAPSHOT).assert_called_with( + self._FAKE_SNAPSHOT_PATH) + + def test_detach_vm_disk(self): + self._lookup_vm() + mock_disk = self._prepare_mock_disk() + + with mock.patch.object(self._vmutils, + '_remove_virt_resource') as mock_rm_virt_res: + self._vmutils.detach_vm_disk(self._FAKE_VM_NAME, + self._FAKE_HOST_RESOURCE) + + mock_rm_virt_res.assert_called_with(mock_disk, self._FAKE_VM_PATH) + + def test_get_mounted_disk_resource_from_path(self): + mock_disk_1 = mock.MagicMock() + mock_disk_2 = mock.MagicMock() + mock_disk_2.HostResource = [self._FAKE_MOUNTED_DISK_PATH] + self._vmutils._conn.query.return_value = [mock_disk_1, mock_disk_2] + + physical_disk = self._vmutils._get_mounted_disk_resource_from_path( + self._FAKE_MOUNTED_DISK_PATH, True) + + self.assertEqual(mock_disk_2, physical_disk) + + def test_get_controller_volume_paths(self): + self._prepare_mock_disk() + mock_disks = {self._FAKE_RES_PATH: self._FAKE_HOST_RESOURCE} + disks = self._vmutils.get_controller_volume_paths(self._FAKE_RES_PATH) + self.assertEqual(mock_disks, disks) + + def _prepare_mock_disk(self): + mock_disk = mock.MagicMock() + mock_disk.HostResource = [self._FAKE_HOST_RESOURCE] + mock_disk.path.return_value.RelPath = self._FAKE_RES_PATH + mock_disk.ResourceSubType = self._vmutils._HARD_DISK_RES_SUB_TYPE + self._vmutils._conn.query.return_value = [mock_disk] + + return mock_disk + + def _get_snapshot_service(self): + return self._vmutils._conn.Msvm_VirtualSystemManagementService()[0] + + def _assert_add_resources(self, mock_svc): + getattr(mock_svc, self._ADD_RESOURCE).assert_called_with( + [self._FAKE_RES_DATA], self._FAKE_VM_PATH) + + def _assert_remove_resources(self, mock_svc): + getattr(mock_svc, self._REMOVE_RESOURCE).assert_called_with( + [self._FAKE_RES_PATH], self._FAKE_VM_PATH) + + def test_get_active_instances(self): + fake_vm = mock.MagicMock() + + type(fake_vm).ElementName = mock.PropertyMock( + side_effect=['active_vm', 'inactive_vm']) + type(fake_vm).EnabledState = mock.PropertyMock( + side_effect=[constants.HYPERV_VM_STATE_ENABLED, + constants.HYPERV_VM_STATE_DISABLED]) + self._vmutils.list_instances = mock.MagicMock( + return_value=[mock.sentinel.fake_vm_name] * 2) + self._vmutils._lookup_vm = mock.MagicMock(side_effect=[fake_vm] * 2) + active_instances = self._vmutils.get_active_instances() + + self.assertEqual(['active_vm'], active_instances) + + def _test_get_vm_serial_port_connection(self, new_connection=None): + old_serial_connection = 'old_serial_connection' + + mock_vm = self._lookup_vm() + mock_vmsettings = [mock.MagicMock()] + mock_vm.associators.return_value = mock_vmsettings + + fake_serial_port = mock.MagicMock() + + fake_serial_port.ResourceSubType = ( + self._vmutils._SERIAL_PORT_RES_SUB_TYPE) + fake_serial_port.Connection = [old_serial_connection] + mock_rasds = [fake_serial_port] + mock_vmsettings[0].associators.return_value = mock_rasds + self._vmutils._modify_virt_resource = mock.MagicMock() + fake_modify = self._vmutils._modify_virt_resource + + ret_val = self._vmutils.get_vm_serial_port_connection( + self._FAKE_VM_NAME, update_connection=new_connection) + + if new_connection: + self.assertEqual(new_connection, ret_val) + fake_modify.assert_called_once_with(fake_serial_port, + mock_vm.path_()) + else: + self.assertEqual(old_serial_connection, ret_val) + + def test_set_vm_serial_port_connection(self): + self._test_get_vm_serial_port_connection('new_serial_connection') + + def test_get_vm_serial_port_connection(self): + self._test_get_vm_serial_port_connection() + + def test_list_instance_notes(self): + vs = mock.MagicMock() + attrs = {'ElementName': 'fake_name', + 'Notes': '4f54fb69-d3a2-45b7-bb9b-b6e6b3d893b3'} + vs.configure_mock(**attrs) + vs2 = mock.MagicMock(ElementName='fake_name2', Notes=None) + self._vmutils._conn.Msvm_VirtualSystemSettingData.return_value = [vs, + vs2] + response = self._vmutils.list_instance_notes() + + self.assertEqual([(attrs['ElementName'], [attrs['Notes']])], response) + self._vmutils._conn.Msvm_VirtualSystemSettingData.assert_called_with( + ['ElementName', 'Notes'], + SettingType=self._vmutils._VIRTUAL_SYSTEM_CURRENT_SETTINGS) + + @mock.patch('nova.virt.hyperv.vmutils.VMUtils.check_ret_val') + def test_modify_virtual_system(self, mock_check_ret_val): + mock_vs_man_svc = mock.MagicMock() + mock_vmsetting = mock.MagicMock() + fake_path = 'fake path' + fake_job_path = 'fake job path' + fake_ret_val = 'fake return value' + + mock_vs_man_svc.ModifyVirtualSystem.return_value = (0, fake_job_path, + fake_ret_val) + + self._vmutils._modify_virtual_system(vs_man_svc=mock_vs_man_svc, + vm_path=fake_path, + vmsetting=mock_vmsetting) + + mock_vs_man_svc.ModifyVirtualSystem.assert_called_once_with( + ComputerSystem=fake_path, + SystemSettingData=mock_vmsetting.GetText_(1)) + mock_check_ret_val.assert_called_once_with(fake_ret_val, fake_job_path) + + @mock.patch('nova.virt.hyperv.vmutils.VMUtils.check_ret_val') + @mock.patch('nova.virt.hyperv.vmutils.VMUtils._get_wmi_obj') + @mock.patch('nova.virt.hyperv.vmutils.VMUtils._modify_virtual_system') + @mock.patch('nova.virt.hyperv.vmutils.VMUtils._get_vm_setting_data') + def test_create_vm_obj(self, mock_get_vm_setting_data, + mock_modify_virtual_system, + mock_get_wmi_obj, mock_check_ret_val): + mock_vs_man_svc = mock.MagicMock() + mock_vs_gs_data = mock.MagicMock() + fake_vm_path = 'fake vm path' + fake_job_path = 'fake job path' + fake_ret_val = 'fake return value' + _conn = self._vmutils._conn.Msvm_VirtualSystemGlobalSettingData + + _conn.new.return_value = mock_vs_gs_data + mock_vs_man_svc.DefineVirtualSystem.return_value = (fake_vm_path, + fake_job_path, + fake_ret_val) + + response = self._vmutils._create_vm_obj( + vs_man_svc=mock_vs_man_svc, + vm_name='fake vm', vm_gen='fake vm gen', + notes='fake notes', dynamic_memory_ratio=1.0, + instance_path=mock.sentinel.instance_path) + + _conn.new.assert_called_once_with() + self.assertEqual(mock_vs_gs_data.ElementName, 'fake vm') + mock_vs_man_svc.DefineVirtualSystem.assert_called_once_with( + [], None, mock_vs_gs_data.GetText_(1)) + mock_check_ret_val.assert_called_once_with(fake_ret_val, fake_job_path) + self.assertEqual(mock.sentinel.instance_path, + mock_vs_gs_data.ExternalDataRoot) + self.assertEqual(mock.sentinel.instance_path, + mock_vs_gs_data.SnapshotDataRoot) + + mock_get_wmi_obj.assert_called_with(fake_vm_path) + mock_get_vm_setting_data.assert_called_once_with(mock_get_wmi_obj()) + mock_modify_virtual_system.assert_called_once_with( + mock_vs_man_svc, fake_vm_path, mock_get_vm_setting_data()) + + self.assertEqual(mock_get_vm_setting_data().Notes, + '\n'.join('fake notes')) + self.assertEqual(response, mock_get_wmi_obj()) + + def test_list_instances(self): + vs = mock.MagicMock() + attrs = {'ElementName': 'fake_name'} + vs.configure_mock(**attrs) + self._vmutils._conn.Msvm_VirtualSystemSettingData.return_value = [vs] + response = self._vmutils.list_instances() + + self.assertEqual([(attrs['ElementName'])], response) + self._vmutils._conn.Msvm_VirtualSystemSettingData.assert_called_with( + ['ElementName'], + SettingType=self._vmutils._VIRTUAL_SYSTEM_CURRENT_SETTINGS) + + @mock.patch.object(vmutils.VMUtils, "_clone_wmi_obj") + def _test_check_clone_wmi_obj(self, mock_clone_wmi_obj, clone_objects): + mock_obj = mock.MagicMock() + self._vmutils._clone_wmi_objs = clone_objects + + response = self._vmutils._check_clone_wmi_obj(class_name="fakeClass", + obj=mock_obj) + if not clone_objects: + self.assertEqual(mock_obj, response) + else: + mock_clone_wmi_obj.assert_called_once_with("fakeClass", mock_obj) + self.assertEqual(mock_clone_wmi_obj.return_value, response) + + def test_check_clone_wmi_obj_true(self): + self._test_check_clone_wmi_obj(clone_objects=True) + + def test_check_clone_wmi_obj_false(self): + self._test_check_clone_wmi_obj(clone_objects=False) + + def test_clone_wmi_obj(self): + mock_obj = mock.MagicMock() + mock_value = mock.MagicMock() + mock_value.Value = mock.sentinel.fake_value + mock_obj._properties = [mock.sentinel.property] + mock_obj.Properties_.Item.return_value = mock_value + + response = self._vmutils._clone_wmi_obj( + class_name="FakeClass", obj=mock_obj) + + compare = self._vmutils._conn.FakeClass.new() + self.assertEqual(mock.sentinel.fake_value, + compare.Properties_.Item().Value) + self.assertEqual(compare, response) + + def test_get_attached_disks(self): + mock_scsi_ctrl_path = mock.MagicMock() + expected_query = ("SELECT * FROM %(class_name)s " + "WHERE (ResourceSubType='%(res_sub_type)s' OR " + "ResourceSubType='%(res_sub_type_virt)s')" + " AND Parent='%(parent)s'" % + {"class_name": + self._vmutils._RESOURCE_ALLOC_SETTING_DATA_CLASS, + "res_sub_type": + self._vmutils._PHYS_DISK_RES_SUB_TYPE, + "res_sub_type_virt": + self._vmutils._DISK_DRIVE_RES_SUB_TYPE, + "parent": + mock_scsi_ctrl_path.replace("'", "''")}) + expected_disks = self._vmutils._conn.query.return_value + + ret_disks = self._vmutils.get_attached_disks(mock_scsi_ctrl_path) + + self._vmutils._conn.query.assert_called_once_with(expected_query) + self.assertEqual(expected_disks, ret_disks) + + def _get_fake_instance_notes(self): + return self._FAKE_VM_UUID + + def test_instance_notes(self): + self._lookup_vm() + mock_vm_settings = mock.Mock() + mock_vm_settings.Notes = self._get_fake_instance_notes() + self._vmutils._get_vm_setting_data = mock.Mock( + return_value=mock_vm_settings) + + notes = self._vmutils._get_instance_notes(mock.sentinel.vm_name) + + self.assertEqual(notes[0], self._FAKE_VM_UUID) + + def test_get_event_wql_query(self): + cls = self._vmutils._COMPUTER_SYSTEM_CLASS + field = self._vmutils._VM_ENABLED_STATE_PROP + timeframe = 10 + filtered_states = [constants.HYPERV_VM_STATE_ENABLED, + constants.HYPERV_VM_STATE_DISABLED] + + expected_checks = ' OR '.join( + ["TargetInstance.%s = '%s'" % (field, state) + for state in filtered_states]) + expected_query = ( + "SELECT %(field)s, TargetInstance " + "FROM __InstanceModificationEvent " + "WITHIN %(timeframe)s " + "WHERE TargetInstance ISA '%(class)s' " + "AND TargetInstance.%(field)s != " + "PreviousInstance.%(field)s " + "AND (%(checks)s)" % + {'class': cls, + 'field': field, + 'timeframe': timeframe, + 'checks': expected_checks}) + + query = self._vmutils._get_event_wql_query( + cls=cls, field=field, timeframe=timeframe, + filtered_states=filtered_states) + self.assertEqual(expected_query, query) + + def test_get_vm_power_state_change_listener(self): + with mock.patch.object(self._vmutils, + '_get_event_wql_query') as mock_get_query: + listener = self._vmutils.get_vm_power_state_change_listener( + mock.sentinel.timeframe, + mock.sentinel.filtered_states) + + mock_get_query.assert_called_once_with( + cls=self._vmutils._COMPUTER_SYSTEM_CLASS, + field=self._vmutils._VM_ENABLED_STATE_PROP, + timeframe=mock.sentinel.timeframe, + filtered_states=mock.sentinel.filtered_states) + watcher = self._vmutils._conn.Msvm_ComputerSystem.watch_for + watcher.assert_called_once_with( + raw_wql=mock_get_query.return_value, + fields=[self._vmutils._VM_ENABLED_STATE_PROP]) + + self.assertEqual(watcher.return_value, listener) diff --git a/oslo_windows/tests/utils/test_vmutilsv2.py b/oslo_windows/tests/utils/test_vmutilsv2.py new file mode 100644 index 00000000..0dbb2c94 --- /dev/null +++ b/oslo_windows/tests/utils/test_vmutilsv2.py @@ -0,0 +1,265 @@ +# Copyright 2014 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from nova.tests.unit.virt.hyperv import test_vmutils +from nova.virt.hyperv import constants +from nova.virt.hyperv import vmutilsv2 + + +class VMUtilsV2TestCase(test_vmutils.VMUtilsTestCase): + """Unit tests for the Hyper-V VMUtilsV2 class.""" + + _DEFINE_SYSTEM = 'DefineSystem' + _DESTROY_SYSTEM = 'DestroySystem' + _DESTROY_SNAPSHOT = 'DestroySnapshot' + + _ADD_RESOURCE = 'AddResourceSettings' + _REMOVE_RESOURCE = 'RemoveResourceSettings' + _SETTING_TYPE = 'VirtualSystemType' + _VM_GEN = constants.VM_GEN_2 + + _VIRTUAL_SYSTEM_TYPE_REALIZED = 'Microsoft:Hyper-V:System:Realized' + + def setUp(self): + super(VMUtilsV2TestCase, self).setUp() + self._vmutils = vmutilsv2.VMUtilsV2() + self._vmutils._conn = mock.MagicMock() + + def test_create_vm(self): + super(VMUtilsV2TestCase, self).test_create_vm() + mock_vssd = self._vmutils._conn.Msvm_VirtualSystemSettingData.new() + self.assertEqual(self._vmutils._VIRTUAL_SYSTEM_SUBTYPE_GEN2, + mock_vssd.VirtualSystemSubType) + self.assertFalse(mock_vssd.SecureBootEnabled) + + def test_modify_virt_resource(self): + mock_svc = self._vmutils._conn.Msvm_VirtualSystemManagementService()[0] + mock_svc.ModifyResourceSettings.return_value = (self._FAKE_JOB_PATH, + mock.MagicMock(), + self._FAKE_RET_VAL) + mock_res_setting_data = mock.MagicMock() + mock_res_setting_data.GetText_.return_value = self._FAKE_RES_DATA + + self._vmutils._modify_virt_resource(mock_res_setting_data, + self._FAKE_VM_PATH) + + mock_svc.ModifyResourceSettings.assert_called_with( + ResourceSettings=[self._FAKE_RES_DATA]) + + @mock.patch.object(vmutilsv2, 'wmi', create=True) + @mock.patch.object(vmutilsv2.VMUtilsV2, 'check_ret_val') + def test_take_vm_snapshot(self, mock_check_ret_val, mock_wmi): + self._lookup_vm() + + mock_svc = self._get_snapshot_service() + mock_svc.CreateSnapshot.return_value = (self._FAKE_JOB_PATH, + mock.MagicMock(), + self._FAKE_RET_VAL) + + self._vmutils.take_vm_snapshot(self._FAKE_VM_NAME) + + mock_svc.CreateSnapshot.assert_called_with( + AffectedSystem=self._FAKE_VM_PATH, + SnapshotType=self._vmutils._SNAPSHOT_FULL) + + mock_check_ret_val.assert_called_once_with(self._FAKE_RET_VAL, + self._FAKE_JOB_PATH) + + @mock.patch.object(vmutilsv2.VMUtilsV2, '_add_virt_resource') + @mock.patch.object(vmutilsv2.VMUtilsV2, '_get_new_setting_data') + @mock.patch.object(vmutilsv2.VMUtilsV2, '_get_nic_data_by_name') + def test_set_nic_connection(self, mock_get_nic_data, mock_get_new_sd, + mock_add_virt_res): + self._lookup_vm() + fake_eth_port = mock_get_new_sd.return_value + + self._vmutils.set_nic_connection(self._FAKE_VM_NAME, None, None) + mock_add_virt_res.assert_called_with(fake_eth_port, self._FAKE_VM_PATH) + + @mock.patch('nova.virt.hyperv.vmutils.VMUtils._get_vm_disks') + def test_enable_vm_metrics_collection(self, mock_get_vm_disks): + self._lookup_vm() + mock_svc = self._vmutils._conn.Msvm_MetricService()[0] + + metric_def = mock.MagicMock() + mock_disk = mock.MagicMock() + mock_disk.path_.return_value = self._FAKE_RES_PATH + mock_get_vm_disks.return_value = ([mock_disk], [mock_disk]) + + fake_metric_def_paths = ['fake_0', 'fake_0', None] + fake_metric_resource_paths = [self._FAKE_VM_PATH, + self._FAKE_VM_PATH, + self._FAKE_RES_PATH] + + metric_def.path_.side_effect = fake_metric_def_paths + self._vmutils._conn.CIM_BaseMetricDefinition.return_value = [ + metric_def] + + self._vmutils.enable_vm_metrics_collection(self._FAKE_VM_NAME) + + calls = [mock.call(Name=def_name) + for def_name in [self._vmutils._METRIC_AGGR_CPU_AVG, + self._vmutils._METRIC_AGGR_MEMORY_AVG]] + self._vmutils._conn.CIM_BaseMetricDefinition.assert_has_calls(calls) + + calls = [] + for i in range(len(fake_metric_def_paths)): + calls.append(mock.call( + Subject=fake_metric_resource_paths[i], + Definition=fake_metric_def_paths[i], + MetricCollectionEnabled=self._vmutils._METRIC_ENABLED)) + + mock_svc.ControlMetrics.assert_has_calls(calls, any_order=True) + + def _get_snapshot_service(self): + return self._vmutils._conn.Msvm_VirtualSystemSnapshotService()[0] + + def _assert_add_resources(self, mock_svc): + getattr(mock_svc, self._ADD_RESOURCE).assert_called_with( + self._FAKE_VM_PATH, [self._FAKE_RES_DATA]) + + def _assert_remove_resources(self, mock_svc): + getattr(mock_svc, self._REMOVE_RESOURCE).assert_called_with( + [self._FAKE_RES_PATH]) + + def test_list_instance_notes(self): + vs = mock.MagicMock() + attrs = {'ElementName': 'fake_name', + 'Notes': ['4f54fb69-d3a2-45b7-bb9b-b6e6b3d893b3']} + vs.configure_mock(**attrs) + vs2 = mock.MagicMock(ElementName='fake_name2', Notes=None) + self._vmutils._conn.Msvm_VirtualSystemSettingData.return_value = [vs, + vs2] + response = self._vmutils.list_instance_notes() + + self.assertEqual([(attrs['ElementName'], attrs['Notes'])], response) + self._vmutils._conn.Msvm_VirtualSystemSettingData.assert_called_with( + ['ElementName', 'Notes'], + VirtualSystemType=self._vmutils._VIRTUAL_SYSTEM_TYPE_REALIZED) + + def _get_fake_instance_notes(self): + return [self._FAKE_VM_UUID] + + @mock.patch('nova.virt.hyperv.vmutilsv2.VMUtilsV2.check_ret_val') + @mock.patch('nova.virt.hyperv.vmutilsv2.VMUtilsV2._get_wmi_obj') + def _test_create_vm_obj(self, mock_get_wmi_obj, mock_check_ret_val, + vm_path, dynamic_memory_ratio=1.0): + mock_vs_man_svc = mock.MagicMock() + mock_vs_data = mock.MagicMock() + mock_job = mock.MagicMock() + fake_job_path = 'fake job path' + fake_ret_val = 'fake return value' + fake_vm_name = 'fake_vm_name' + _conn = self._vmutils._conn.Msvm_VirtualSystemSettingData + + mock_check_ret_val.return_value = mock_job + _conn.new.return_value = mock_vs_data + mock_vs_man_svc.DefineSystem.return_value = (fake_job_path, + vm_path, + fake_ret_val) + mock_job.associators.return_value = ['fake vm path'] + + response = self._vmutils._create_vm_obj( + vs_man_svc=mock_vs_man_svc, + vm_name=fake_vm_name, + vm_gen='fake vm gen', + notes='fake notes', + dynamic_memory_ratio=dynamic_memory_ratio, + instance_path=mock.sentinel.instance_path) + + if not vm_path: + mock_job.associators.assert_called_once_with( + self._vmutils._AFFECTED_JOB_ELEMENT_CLASS) + + _conn.new.assert_called_once_with() + self.assertEqual(mock_vs_data.ElementName, fake_vm_name) + mock_vs_man_svc.DefineSystem.assert_called_once_with( + ResourceSettings=[], ReferenceConfiguration=None, + SystemSettings=mock_vs_data.GetText_(1)) + mock_check_ret_val.assert_called_once_with(fake_ret_val, fake_job_path) + + if dynamic_memory_ratio > 1: + self.assertFalse(mock_vs_data.VirtualNumaEnabled) + + mock_get_wmi_obj.assert_called_with('fake vm path') + + self.assertEqual(mock_vs_data.Notes, 'fake notes') + self.assertEqual(mock.sentinel.instance_path, + mock_vs_data.ConfigurationDataRoot) + self.assertEqual(mock.sentinel.instance_path, mock_vs_data.LogDataRoot) + self.assertEqual(mock.sentinel.instance_path, + mock_vs_data.SnapshotDataRoot) + self.assertEqual(mock.sentinel.instance_path, + mock_vs_data.SuspendDataRoot) + self.assertEqual(mock.sentinel.instance_path, + mock_vs_data.SwapFileDataRoot) + self.assertEqual(response, mock_get_wmi_obj()) + + def test_create_vm_obj(self): + self._test_create_vm_obj(vm_path='fake vm path') + + def test_create_vm_obj_no_vm_path(self): + self._test_create_vm_obj(vm_path=None) + + def test_create_vm_obj_dynamic_memory(self): + self._test_create_vm_obj(vm_path=None, dynamic_memory_ratio=1.1) + + def test_list_instances(self): + vs = mock.MagicMock() + attrs = {'ElementName': 'fake_name'} + vs.configure_mock(**attrs) + self._vmutils._conn.Msvm_VirtualSystemSettingData.return_value = [vs] + response = self._vmutils.list_instances() + + self.assertEqual([(attrs['ElementName'])], response) + self._vmutils._conn.Msvm_VirtualSystemSettingData.assert_called_with( + ['ElementName'], + VirtualSystemType=self._vmutils._VIRTUAL_SYSTEM_TYPE_REALIZED) + + def test_get_attached_disks(self): + mock_scsi_ctrl_path = mock.MagicMock() + expected_query = ("SELECT * FROM %(class_name)s " + "WHERE (ResourceSubType='%(res_sub_type)s' OR " + "ResourceSubType='%(res_sub_type_virt)s' OR " + "ResourceSubType='%(res_sub_type_dvd)s') AND " + "Parent = '%(parent)s'" % + {"class_name": + self._vmutils._RESOURCE_ALLOC_SETTING_DATA_CLASS, + "res_sub_type": + self._vmutils._PHYS_DISK_RES_SUB_TYPE, + "res_sub_type_virt": + self._vmutils._DISK_DRIVE_RES_SUB_TYPE, + "res_sub_type_dvd": + self._vmutils._DVD_DRIVE_RES_SUB_TYPE, + "parent": mock_scsi_ctrl_path.replace("'", "''")}) + expected_disks = self._vmutils._conn.query.return_value + + ret_disks = self._vmutils.get_attached_disks(mock_scsi_ctrl_path) + + self._vmutils._conn.query.assert_called_once_with(expected_query) + self.assertEqual(expected_disks, ret_disks) + + def test_get_vm_dvd_disk_paths(self): + mock_vm = self._lookup_vm() + mock_sasd1 = mock.MagicMock( + ResourceSubType=self._vmutils._DVD_DISK_RES_SUB_TYPE, + HostResource=[mock.sentinel.FAKE_DVD_PATH1]) + mock_settings = mock.MagicMock() + mock_settings.associators.return_value = [mock_sasd1] + mock_vm.associators.return_value = [mock_settings] + + ret_val = self._vmutils.get_vm_dvd_disk_paths(self._FAKE_VM_NAME) + self.assertEqual(mock.sentinel.FAKE_DVD_PATH1, ret_val[0]) diff --git a/oslo_windows/tests/utils/test_volumeutils.py b/oslo_windows/tests/utils/test_volumeutils.py new file mode 100644 index 00000000..0a3f661c --- /dev/null +++ b/oslo_windows/tests/utils/test_volumeutils.py @@ -0,0 +1,164 @@ +# Copyright 2014 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. + +import mock +from oslo_config import cfg + +from nova.tests.unit.virt.hyperv import test_basevolumeutils +from nova.virt.hyperv import vmutils +from nova.virt.hyperv import volumeutils + +CONF = cfg.CONF +CONF.import_opt('volume_attach_retry_count', 'nova.virt.hyperv.volumeops', + 'hyperv') + + +class VolumeUtilsTestCase(test_basevolumeutils.BaseVolumeUtilsTestCase): + """Unit tests for the Hyper-V VolumeUtils class.""" + + _FAKE_PORTAL_ADDR = '10.1.1.1' + _FAKE_PORTAL_PORT = '3260' + _FAKE_LUN = 0 + _FAKE_TARGET = 'iqn.2010-10.org.openstack:fake_target' + + _FAKE_STDOUT_VALUE = 'The operation completed successfully' + + def setUp(self): + super(VolumeUtilsTestCase, self).setUp() + self._volutils = volumeutils.VolumeUtils() + self._volutils._conn_wmi = mock.MagicMock() + self._volutils._conn_cimv2 = mock.MagicMock() + self.flags(volume_attach_retry_count=4, group='hyperv') + self.flags(volume_attach_retry_interval=0, group='hyperv') + + def _test_login_target_portal(self, portal_connected): + fake_portal = '%s:%s' % (self._FAKE_PORTAL_ADDR, + self._FAKE_PORTAL_PORT) + + self._volutils.execute = mock.MagicMock() + if portal_connected: + exec_output = 'Address and Socket: %s %s' % ( + self._FAKE_PORTAL_ADDR, self._FAKE_PORTAL_PORT) + else: + exec_output = '' + + self._volutils.execute.return_value = exec_output + + self._volutils._login_target_portal(fake_portal) + + call_list = self._volutils.execute.call_args_list + all_call_args = [arg for call in call_list for arg in call[0]] + + if portal_connected: + self.assertIn('RefreshTargetPortal', all_call_args) + else: + self.assertIn('AddTargetPortal', all_call_args) + + def test_login_connected_portal(self): + self._test_login_target_portal(True) + + def test_login_new_portal(self): + self._test_login_target_portal(False) + + def _test_login_target(self, target_connected=False, raise_exception=False, + use_chap=False): + fake_portal = '%s:%s' % (self._FAKE_PORTAL_ADDR, + self._FAKE_PORTAL_PORT) + self._volutils.execute = mock.MagicMock() + self._volutils._login_target_portal = mock.MagicMock() + + if use_chap: + username, password = (mock.sentinel.username, + mock.sentinel.password) + else: + username, password = None, None + + if target_connected: + self._volutils.execute.return_value = self._FAKE_TARGET + elif raise_exception: + self._volutils.execute.return_value = '' + else: + self._volutils.execute.side_effect = ( + ['', '', '', self._FAKE_TARGET, '']) + + if raise_exception: + self.assertRaises(vmutils.HyperVException, + self._volutils.login_storage_target, + self._FAKE_LUN, self._FAKE_TARGET, + fake_portal, username, password) + else: + self._volutils.login_storage_target(self._FAKE_LUN, + self._FAKE_TARGET, + fake_portal, + username, password) + + if target_connected: + call_list = self._volutils.execute.call_args_list + all_call_args = [arg for call in call_list for arg in call[0]] + self.assertNotIn('qlogintarget', all_call_args) + else: + self._volutils.execute.assert_any_call( + 'iscsicli.exe', 'qlogintarget', + self._FAKE_TARGET, username, password) + + def test_login_connected_target(self): + self._test_login_target(target_connected=True) + + def test_login_disconncted_target(self): + self._test_login_target() + + def test_login_target_exception(self): + self._test_login_target(raise_exception=True) + + def test_login_target_using_chap(self): + self._test_login_target(use_chap=True) + + def _test_execute_wrapper(self, raise_exception): + fake_cmd = ('iscsicli.exe', 'ListTargetPortals') + + if raise_exception: + output = 'fake error' + else: + output = 'The operation completed successfully' + + with mock.patch('nova.utils.execute') as fake_execute: + fake_execute.return_value = (output, None) + + if raise_exception: + self.assertRaises(vmutils.HyperVException, + self._volutils.execute, + *fake_cmd) + else: + ret_val = self._volutils.execute(*fake_cmd) + self.assertEqual(output, ret_val) + + def test_execute_raise_exception(self): + self._test_execute_wrapper(True) + + def test_execute_exception(self): + self._test_execute_wrapper(False) + + @mock.patch.object(volumeutils, 'utils') + def test_logout_storage_target(self, mock_utils): + mock_utils.execute.return_value = (self._FAKE_STDOUT_VALUE, + mock.sentinel.FAKE_STDERR_VALUE) + session = mock.MagicMock() + session.SessionId = mock.sentinel.FAKE_SESSION_ID + self._volutils._conn_wmi.query.return_value = [session] + + self._volutils.logout_storage_target(mock.sentinel.FAKE_IQN) + mock_utils.execute.assert_called_once_with( + 'iscsicli.exe', 'logouttarget', mock.sentinel.FAKE_SESSION_ID) diff --git a/oslo_windows/tests/utils/test_volumeutilsv2.py b/oslo_windows/tests/utils/test_volumeutilsv2.py new file mode 100644 index 00000000..5c73f0ce --- /dev/null +++ b/oslo_windows/tests/utils/test_volumeutilsv2.py @@ -0,0 +1,164 @@ +# Copyright 2014 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo_config import cfg + +from nova import test +from nova.virt.hyperv import vmutils +from nova.virt.hyperv import volumeutilsv2 + +CONF = cfg.CONF +CONF.import_opt('volume_attach_retry_count', 'nova.virt.hyperv.volumeops', + 'hyperv') + + +class VolumeUtilsV2TestCase(test.NoDBTestCase): + """Unit tests for the Hyper-V VolumeUtilsV2 class.""" + + _FAKE_PORTAL_ADDR = '10.1.1.1' + _FAKE_PORTAL_PORT = '3260' + _FAKE_LUN = 0 + _FAKE_TARGET = 'iqn.2010-10.org.openstack:fake_target' + + def setUp(self): + super(VolumeUtilsV2TestCase, self).setUp() + self._volutilsv2 = volumeutilsv2.VolumeUtilsV2() + self._volutilsv2._conn_storage = mock.MagicMock() + self._volutilsv2._conn_wmi = mock.MagicMock() + self.flags(volume_attach_retry_count=4, group='hyperv') + self.flags(volume_attach_retry_interval=0, group='hyperv') + + def _test_login_target_portal(self, portal_connected): + fake_portal = '%s:%s' % (self._FAKE_PORTAL_ADDR, + self._FAKE_PORTAL_PORT) + fake_portal_object = mock.MagicMock() + _query = self._volutilsv2._conn_storage.query + self._volutilsv2._conn_storage.MSFT_iSCSITargetPortal = ( + fake_portal_object) + + if portal_connected: + _query.return_value = [fake_portal_object] + else: + _query.return_value = None + + self._volutilsv2._login_target_portal(fake_portal) + + if portal_connected: + fake_portal_object.Update.assert_called_once_with() + else: + fake_portal_object.New.assert_called_once_with( + TargetPortalAddress=self._FAKE_PORTAL_ADDR, + TargetPortalPortNumber=self._FAKE_PORTAL_PORT) + + def test_login_connected_portal(self): + self._test_login_target_portal(True) + + def test_login_new_portal(self): + self._test_login_target_portal(False) + + def _test_login_target(self, target_connected=False, raise_exception=False, + use_chap=False): + fake_portal = '%s:%s' % (self._FAKE_PORTAL_ADDR, + self._FAKE_PORTAL_PORT) + + fake_target_object = mock.MagicMock() + + if target_connected: + fake_target_object.IsConnected = True + elif not raise_exception: + type(fake_target_object).IsConnected = mock.PropertyMock( + side_effect=[False, True]) + else: + fake_target_object.IsConnected = False + + _query = self._volutilsv2._conn_storage.query + _query.return_value = [fake_target_object] + + self._volutilsv2._conn_storage.MSFT_iSCSITarget = ( + fake_target_object) + + if use_chap: + username, password = (mock.sentinel.username, + mock.sentinel.password) + auth = { + 'AuthenticationType': self._volutilsv2._CHAP_AUTH_TYPE, + 'ChapUsername': username, + 'ChapSecret': password, + } + else: + username, password = None, None + auth = {} + + if raise_exception: + self.assertRaises(vmutils.HyperVException, + self._volutilsv2.login_storage_target, + self._FAKE_LUN, self._FAKE_TARGET, fake_portal) + else: + self._volutilsv2.login_storage_target(self._FAKE_LUN, + self._FAKE_TARGET, + fake_portal, + username, password) + + if target_connected: + fake_target_object.Update.assert_called_with() + else: + fake_target_object.Connect.assert_called_once_with( + IsPersistent=True, NodeAddress=self._FAKE_TARGET, **auth) + + def test_login_connected_target(self): + self._test_login_target(target_connected=True) + + def test_login_disconncted_target(self): + self._test_login_target() + + def test_login_target_exception(self): + self._test_login_target(raise_exception=True) + + def test_login_target_using_chap(self): + self._test_login_target(use_chap=True) + + def test_logout_storage_target(self): + mock_msft_target = self._volutilsv2._conn_storage.MSFT_iSCSITarget + mock_msft_session = self._volutilsv2._conn_storage.MSFT_iSCSISession + + mock_target = mock.MagicMock() + mock_target.IsConnected = True + mock_msft_target.return_value = [mock_target] + + mock_session = mock.MagicMock() + mock_session.IsPersistent = True + mock_msft_session.return_value = [mock_session] + + self._volutilsv2.logout_storage_target(self._FAKE_TARGET) + + mock_msft_target.assert_called_once_with(NodeAddress=self._FAKE_TARGET) + mock_msft_session.assert_called_once_with( + TargetNodeAddress=self._FAKE_TARGET) + + mock_session.Unregister.assert_called_once_with() + mock_target.Disconnect.assert_called_once_with() + + @mock.patch.object(volumeutilsv2.VolumeUtilsV2, 'logout_storage_target') + def test_execute_log_out(self, mock_logout_target): + sess_class = self._volutilsv2._conn_wmi.MSiSCSIInitiator_SessionClass + + mock_session = mock.MagicMock() + sess_class.return_value = [mock_session] + + self._volutilsv2.execute_log_out(mock.sentinel.FAKE_SESSION_ID) + + sess_class.assert_called_once_with( + SessionId=mock.sentinel.FAKE_SESSION_ID) + mock_logout_target.assert_called_once_with(mock_session.TargetName) diff --git a/oslo_windows/utils/__init__.py b/oslo_windows/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oslo_windows/utils/basevolumeutils.py b/oslo_windows/utils/basevolumeutils.py new file mode 100644 index 00000000..5fcb4fac --- /dev/null +++ b/oslo_windows/utils/basevolumeutils.py @@ -0,0 +1,149 @@ +# +# Copyright 2012 Pedro Navarro Perez +# 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. + +""" +Helper methods for operations related to the management of volumes, +and storage repositories +""" + +import abc +import re +import sys + +if sys.platform == 'win32': + import _winreg + import wmi + +from oslo_log import log as logging + +from nova import block_device +from nova.i18n import _LI +from nova.virt import driver + +LOG = logging.getLogger(__name__) + + +class BaseVolumeUtils(object): + _FILE_DEVICE_DISK = 7 + + def __init__(self, host='.'): + if sys.platform == 'win32': + self._conn_wmi = wmi.WMI(moniker='//%s/root/wmi' % host) + self._conn_cimv2 = wmi.WMI(moniker='//%s/root/cimv2' % host) + self._drive_number_regex = re.compile(r'DeviceID=\"[^,]*\\(\d+)\"') + + @abc.abstractmethod + def login_storage_target(self, target_lun, target_iqn, target_portal): + pass + + @abc.abstractmethod + def logout_storage_target(self, target_iqn): + pass + + @abc.abstractmethod + def execute_log_out(self, session_id): + pass + + def get_iscsi_initiator(self): + """Get iscsi initiator name for this machine.""" + + computer_system = self._conn_cimv2.Win32_ComputerSystem()[0] + hostname = computer_system.name + keypath = ("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\" + "iSCSI\\Discovery") + try: + key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, keypath, 0, + _winreg.KEY_ALL_ACCESS) + temp = _winreg.QueryValueEx(key, 'DefaultInitiatorName') + initiator_name = str(temp[0]) + _winreg.CloseKey(key) + except Exception: + LOG.info(_LI("The ISCSI initiator name can't be found. " + "Choosing the default one")) + initiator_name = "iqn.1991-05.com.microsoft:" + hostname.lower() + if computer_system.PartofDomain: + initiator_name += '.' + computer_system.Domain.lower() + return initiator_name + + def volume_in_mapping(self, mount_device, block_device_info): + block_device_list = [block_device.strip_dev(vol['mount_device']) + for vol in + driver.block_device_info_get_mapping( + block_device_info)] + swap = driver.block_device_info_get_swap(block_device_info) + if driver.swap_is_usable(swap): + block_device_list.append( + block_device.strip_dev(swap['device_name'])) + block_device_list += [block_device.strip_dev( + ephemeral['device_name']) + for ephemeral in + driver.block_device_info_get_ephemerals(block_device_info)] + + LOG.debug("block_device_list %s", block_device_list) + return block_device.strip_dev(mount_device) in block_device_list + + def _get_drive_number_from_disk_path(self, disk_path): + drive_number = self._drive_number_regex.findall(disk_path) + if drive_number: + return int(drive_number[0]) + + def get_session_id_from_mounted_disk(self, physical_drive_path): + drive_number = self._get_drive_number_from_disk_path( + physical_drive_path) + if not drive_number: + return None + + initiator_sessions = self._conn_wmi.MSiSCSIInitiator_SessionClass() + for initiator_session in initiator_sessions: + devices = initiator_session.Devices + for device in devices: + device_number = device.DeviceNumber + if device_number == drive_number: + return initiator_session.SessionId + + def _get_devices_for_target(self, target_iqn): + initiator_sessions = self._conn_wmi.MSiSCSIInitiator_SessionClass( + TargetName=target_iqn) + if not initiator_sessions: + return [] + + return initiator_sessions[0].Devices + + def get_device_number_for_target(self, target_iqn, target_lun): + devices = self._get_devices_for_target(target_iqn) + + for device in devices: + if device.ScsiLun == target_lun: + return device.DeviceNumber + + def get_target_lun_count(self, target_iqn): + devices = self._get_devices_for_target(target_iqn) + disk_devices = [device for device in devices + if device.DeviceType == self._FILE_DEVICE_DISK] + return len(disk_devices) + + def get_target_from_disk_path(self, disk_path): + initiator_sessions = self._conn_wmi.MSiSCSIInitiator_SessionClass() + drive_number = self._get_drive_number_from_disk_path(disk_path) + if not drive_number: + return None + + for initiator_session in initiator_sessions: + devices = initiator_session.Devices + for device in devices: + if device.DeviceNumber == drive_number: + return (device.TargetName, device.ScsiLun) diff --git a/oslo_windows/utils/constants.py b/oslo_windows/utils/constants.py new file mode 100644 index 00000000..5d2fd6b6 --- /dev/null +++ b/oslo_windows/utils/constants.py @@ -0,0 +1,102 @@ +# Copyright 2012 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. + +""" +Constants used in ops classes +""" + +from nova.compute import arch +from nova.compute import power_state + +HYPERV_VM_STATE_OTHER = 1 +HYPERV_VM_STATE_ENABLED = 2 +HYPERV_VM_STATE_DISABLED = 3 +HYPERV_VM_STATE_SHUTTING_DOWN = 4 +HYPERV_VM_STATE_REBOOT = 10 +HYPERV_VM_STATE_PAUSED = 32768 +HYPERV_VM_STATE_SUSPENDED = 32769 + +HYPERV_POWER_STATE = { + HYPERV_VM_STATE_DISABLED: power_state.SHUTDOWN, + HYPERV_VM_STATE_SHUTTING_DOWN: power_state.SHUTDOWN, + HYPERV_VM_STATE_ENABLED: power_state.RUNNING, + HYPERV_VM_STATE_PAUSED: power_state.PAUSED, + HYPERV_VM_STATE_SUSPENDED: power_state.SUSPENDED +} + +WMI_WIN32_PROCESSOR_ARCHITECTURE = { + 0: arch.I686, + 1: arch.MIPS, + 2: arch.ALPHA, + 3: arch.PPC, + 5: arch.ARMV7, + 6: arch.IA64, + 9: arch.X86_64, +} + +PROCESSOR_FEATURE = { + 7: '3dnow', + 3: 'mmx', + 12: 'nx', + 9: 'pae', + 8: 'rdtsc', + 20: 'slat', + 13: 'sse3', + 21: 'vmx', + 6: 'sse', + 10: 'sse2', + 17: 'xsave', +} + +WMI_JOB_STATUS_STARTED = 4096 +WMI_JOB_STATE_RUNNING = 4 +WMI_JOB_STATE_COMPLETED = 7 + +VM_SUMMARY_NUM_PROCS = 4 +VM_SUMMARY_ENABLED_STATE = 100 +VM_SUMMARY_MEMORY_USAGE = 103 +VM_SUMMARY_UPTIME = 105 + +CTRL_TYPE_IDE = "IDE" +CTRL_TYPE_SCSI = "SCSI" + +DISK = "VHD" +DISK_FORMAT = DISK +DVD = "DVD" +DVD_FORMAT = "ISO" + +DISK_FORMAT_MAP = { + DISK_FORMAT.lower(): DISK, + DVD_FORMAT.lower(): DVD +} + +DISK_FORMAT_VHD = "VHD" +DISK_FORMAT_VHDX = "VHDX" + +VHD_TYPE_FIXED = 2 +VHD_TYPE_DYNAMIC = 3 + +SCSI_CONTROLLER_SLOTS_NUMBER = 64 + +HOST_POWER_ACTION_SHUTDOWN = "shutdown" +HOST_POWER_ACTION_REBOOT = "reboot" +HOST_POWER_ACTION_STARTUP = "startup" + +IMAGE_PROP_VM_GEN = "hw_machine_type" +IMAGE_PROP_VM_GEN_1 = "hyperv-gen1" +IMAGE_PROP_VM_GEN_2 = "hyperv-gen2" + +VM_GEN_1 = 1 +VM_GEN_2 = 2 diff --git a/oslo_windows/utils/hostutils.py b/oslo_windows/utils/hostutils.py new file mode 100644 index 00000000..7f073e6a --- /dev/null +++ b/oslo_windows/utils/hostutils.py @@ -0,0 +1,120 @@ +# 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. + +import ctypes +import socket +import sys + +if sys.platform == 'win32': + import wmi + +from nova.i18n import _ +from nova.virt.hyperv import constants + + +class HostUtils(object): + + _HOST_FORCED_REBOOT = 6 + _HOST_FORCED_SHUTDOWN = 12 + _DEFAULT_VM_GENERATION = constants.IMAGE_PROP_VM_GEN_1 + + def __init__(self): + if sys.platform == 'win32': + self._conn_cimv2 = wmi.WMI(privileges=["Shutdown"]) + self._init_wmi_virt_conn() + + def _init_wmi_virt_conn(self): + self._conn_virt = None + + def get_cpus_info(self): + cpus = self._conn_cimv2.query("SELECT * FROM Win32_Processor " + "WHERE ProcessorType = 3") + cpus_list = [] + for cpu in cpus: + cpu_info = {'Architecture': cpu.Architecture, + 'Name': cpu.Name, + 'Manufacturer': cpu.Manufacturer, + 'NumberOfCores': cpu.NumberOfCores, + 'NumberOfLogicalProcessors': + cpu.NumberOfLogicalProcessors} + cpus_list.append(cpu_info) + return cpus_list + + def is_cpu_feature_present(self, feature_key): + return ctypes.windll.kernel32.IsProcessorFeaturePresent(feature_key) + + def get_memory_info(self): + """Returns a tuple with total visible memory and free physical memory + expressed in kB. + """ + mem_info = self._conn_cimv2.query("SELECT TotalVisibleMemorySize, " + "FreePhysicalMemory " + "FROM win32_operatingsystem")[0] + return (long(mem_info.TotalVisibleMemorySize), + long(mem_info.FreePhysicalMemory)) + + def get_volume_info(self, drive): + """Returns a tuple with total size and free space + expressed in bytes. + """ + logical_disk = self._conn_cimv2.query("SELECT Size, FreeSpace " + "FROM win32_logicaldisk " + "WHERE DeviceID='%s'" + % drive)[0] + return (long(logical_disk.Size), long(logical_disk.FreeSpace)) + + def check_min_windows_version(self, major, minor, build=0): + version_str = self.get_windows_version() + return map(int, version_str.split('.')) >= [major, minor, build] + + 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] + + def get_host_tick_count64(self): + return ctypes.windll.kernel32.GetTickCount64() + + def host_power_action(self, action): + win32_os = self._conn_cimv2.Win32_OperatingSystem()[0] + + if action == constants.HOST_POWER_ACTION_SHUTDOWN: + win32_os.Win32Shutdown(self._HOST_FORCED_SHUTDOWN) + elif action == constants.HOST_POWER_ACTION_REBOOT: + win32_os.Win32Shutdown(self._HOST_FORCED_REBOOT) + else: + raise NotImplementedError( + _("Host %(action)s is not supported by the Hyper-V driver") % + {"action": action}) + + def get_supported_vm_types(self): + """Get the supported Hyper-V VM generations. + Hyper-V Generation 2 VMs are supported in Windows 8.1, + Windows Server / Hyper-V Server 2012 R2 or newer. + + :returns: array of supported VM generations (ex. ['hyperv-gen1']) + """ + if self.check_min_windows_version(6, 3): + return [constants.IMAGE_PROP_VM_GEN_1, + constants.IMAGE_PROP_VM_GEN_2] + else: + return [constants.IMAGE_PROP_VM_GEN_1] + + def get_default_vm_generation(self): + return self._DEFAULT_VM_GENERATION diff --git a/oslo_windows/utils/hostutilsv2.py b/oslo_windows/utils/hostutilsv2.py new file mode 100644 index 00000000..7bed7a57 --- /dev/null +++ b/oslo_windows/utils/hostutilsv2.py @@ -0,0 +1,34 @@ +# Copyright 2015 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. + +import sys + +if sys.platform == 'win32': + import wmi + +from nova.virt.hyperv import hostutils + + +class HostUtilsV2(hostutils.HostUtils): + + FEATURE_RDS_VIRTUALIZATION = 322 + + def __init__(self): + super(HostUtilsV2, self).__init__() + self._init_wmi_virt_conn() + + def _init_wmi_virt_conn(self): + if sys.platform == 'win32': + self._conn_virt = wmi.WMI(moniker='//./root/virtualization/v2') diff --git a/oslo_windows/utils/ioutils.py b/oslo_windows/utils/ioutils.py new file mode 100644 index 00000000..e28deb84 --- /dev/null +++ b/oslo_windows/utils/ioutils.py @@ -0,0 +1,73 @@ +# Copyright 2014 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. + +import errno +import os + +from eventlet import patcher +from oslo_log import log as logging + +from nova.i18n import _LE + +LOG = logging.getLogger(__name__) + +native_threading = patcher.original('threading') + + +class IOThread(native_threading.Thread): + def __init__(self, src, dest, max_bytes): + super(IOThread, self).__init__() + self.setDaemon(True) + self._src = src + self._dest = dest + self._dest_archive = dest + '.1' + self._max_bytes = max_bytes + self._stopped = native_threading.Event() + + def run(self): + try: + self._copy() + except IOError as err: + self._stopped.set() + # Invalid argument error means that the vm console pipe was closed, + # probably the vm was stopped. The worker can stop it's execution. + if err.errno != errno.EINVAL: + LOG.error(_LE("Error writing vm console log file from " + "serial console pipe. Error: %s") % err) + + def _copy(self): + with open(self._src, 'rb') as src: + with open(self._dest, 'ab', 0) as dest: + dest.seek(0, os.SEEK_END) + log_size = dest.tell() + while (not self._stopped.isSet()): + # Read one byte at a time to avoid blocking. + data = src.read(1) + dest.write(data) + log_size += len(data) + if (log_size >= self._max_bytes): + dest.close() + if os.path.exists(self._dest_archive): + os.remove(self._dest_archive) + os.rename(self._dest, self._dest_archive) + dest = open(self._dest, 'ab', 0) + log_size = 0 + + def join(self): + self._stopped.set() + super(IOThread, self).join() + + def is_active(self): + return not self._stopped.isSet() diff --git a/oslo_windows/utils/livemigrationutils.py b/oslo_windows/utils/livemigrationutils.py new file mode 100644 index 00000000..71f406a8 --- /dev/null +++ b/oslo_windows/utils/livemigrationutils.py @@ -0,0 +1,252 @@ +# 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. + +import sys + +if sys.platform == 'win32': + import wmi + +from oslo_log import log as logging + +from nova import exception +from nova.i18n import _, _LE +from nova.virt.hyperv import vmutils +from nova.virt.hyperv import vmutilsv2 +from nova.virt.hyperv import volumeutilsv2 + +LOG = logging.getLogger(__name__) + + +class LiveMigrationUtils(object): + + def __init__(self): + self._vmutils = vmutilsv2.VMUtilsV2() + self._volutils = volumeutilsv2.VolumeUtilsV2() + + def _get_conn_v2(self, host='localhost'): + try: + return wmi.WMI(moniker='//%s/root/virtualization/v2' % host) + except wmi.x_wmi as ex: + LOG.exception(_LE('Get version 2 connection error')) + if ex.com_error.hresult == -2147217394: + msg = (_('Live migration is not supported on target host "%s"') + % host) + elif ex.com_error.hresult == -2147023174: + msg = (_('Target live migration host "%s" is unreachable') + % host) + else: + msg = _('Live migration failed: %s') % ex.message + raise vmutils.HyperVException(msg) + + def check_live_migration_config(self): + conn_v2 = self._get_conn_v2() + migration_svc = conn_v2.Msvm_VirtualSystemMigrationService()[0] + vsmssds = migration_svc.associators( + wmi_association_class='Msvm_ElementSettingData', + wmi_result_class='Msvm_VirtualSystemMigrationServiceSettingData') + vsmssd = vsmssds[0] + if not vsmssd.EnableVirtualSystemMigration: + raise vmutils.HyperVException( + _('Live migration is not enabled on this host')) + if not migration_svc.MigrationServiceListenerIPAddressList: + raise vmutils.HyperVException( + _('Live migration networks are not configured on this host')) + + def _get_vm(self, conn_v2, vm_name): + vms = conn_v2.Msvm_ComputerSystem(ElementName=vm_name) + n = len(vms) + if not n: + raise exception.NotFound(_('VM not found: %s') % vm_name) + elif n > 1: + raise vmutils.HyperVException(_('Duplicate VM name found: %s') + % vm_name) + return vms[0] + + def _destroy_planned_vm(self, conn_v2_remote, planned_vm): + LOG.debug("Destroying existing remote planned VM: %s", + planned_vm.ElementName) + vs_man_svc = conn_v2_remote.Msvm_VirtualSystemManagementService()[0] + (job_path, ret_val) = vs_man_svc.DestroySystem(planned_vm.path_()) + self._vmutils.check_ret_val(ret_val, job_path) + + def _check_existing_planned_vm(self, conn_v2_remote, vm): + # Make sure that there's not yet a remote planned VM on the target + # host for this VM + planned_vms = conn_v2_remote.Msvm_PlannedComputerSystem(Name=vm.Name) + if planned_vms: + self._destroy_planned_vm(conn_v2_remote, planned_vms[0]) + + def _create_remote_planned_vm(self, conn_v2_local, conn_v2_remote, + vm, rmt_ip_addr_list, dest_host): + # Staged + vsmsd = conn_v2_local.query("select * from " + "Msvm_VirtualSystemMigrationSettingData " + "where MigrationType = 32770")[0] + vsmsd.DestinationIPAddressList = rmt_ip_addr_list + migration_setting_data = vsmsd.GetText_(1) + + LOG.debug("Creating remote planned VM for VM: %s", + vm.ElementName) + migr_svc = conn_v2_local.Msvm_VirtualSystemMigrationService()[0] + (job_path, ret_val) = migr_svc.MigrateVirtualSystemToHost( + ComputerSystem=vm.path_(), + DestinationHost=dest_host, + MigrationSettingData=migration_setting_data) + self._vmutils.check_ret_val(ret_val, job_path) + + return conn_v2_remote.Msvm_PlannedComputerSystem(Name=vm.Name)[0] + + def _get_physical_disk_paths(self, vm_name): + ide_ctrl_path = self._vmutils.get_vm_ide_controller(vm_name, 0) + if ide_ctrl_path: + ide_paths = self._vmutils.get_controller_volume_paths( + ide_ctrl_path) + else: + ide_paths = {} + + scsi_ctrl_path = self._vmutils.get_vm_scsi_controller(vm_name) + scsi_paths = self._vmutils.get_controller_volume_paths(scsi_ctrl_path) + + return dict(ide_paths.items() + scsi_paths.items()) + + def _get_remote_disk_data(self, vmutils_remote, disk_paths, dest_host): + volutils_remote = volumeutilsv2.VolumeUtilsV2(dest_host) + + disk_paths_remote = {} + for (rasd_rel_path, disk_path) in disk_paths.items(): + target = self._volutils.get_target_from_disk_path(disk_path) + if target: + (target_iqn, target_lun) = target + + dev_num = volutils_remote.get_device_number_for_target( + target_iqn, target_lun) + disk_path_remote = ( + vmutils_remote.get_mounted_disk_by_drive_number(dev_num)) + + disk_paths_remote[rasd_rel_path] = disk_path_remote + else: + LOG.debug("Could not retrieve iSCSI target " + "from disk path: %s", disk_path) + + return disk_paths_remote + + def _update_planned_vm_disk_resources(self, vmutils_remote, conn_v2_remote, + planned_vm, vm_name, + disk_paths_remote): + vm_settings = planned_vm.associators( + wmi_association_class='Msvm_SettingsDefineState', + wmi_result_class='Msvm_VirtualSystemSettingData')[0] + + updated_resource_setting_data = [] + sasds = vm_settings.associators( + wmi_association_class='Msvm_VirtualSystemSettingDataComponent') + for sasd in sasds: + if (sasd.ResourceType == 17 and sasd.ResourceSubType == + "Microsoft:Hyper-V:Physical Disk Drive" and + sasd.HostResource): + # Replace the local disk target with the correct remote one + old_disk_path = sasd.HostResource[0] + new_disk_path = disk_paths_remote.pop(sasd.path().RelPath) + + LOG.debug("Replacing host resource " + "%(old_disk_path)s with " + "%(new_disk_path)s on planned VM %(vm_name)s", + {'old_disk_path': old_disk_path, + 'new_disk_path': new_disk_path, + 'vm_name': vm_name}) + sasd.HostResource = [new_disk_path] + updated_resource_setting_data.append(sasd.GetText_(1)) + + LOG.debug("Updating remote planned VM disk paths for VM: %s", + vm_name) + vsmsvc = conn_v2_remote.Msvm_VirtualSystemManagementService()[0] + (res_settings, job_path, ret_val) = vsmsvc.ModifyResourceSettings( + ResourceSettings=updated_resource_setting_data) + vmutils_remote.check_ret_val(ret_val, job_path) + + def _get_vhd_setting_data(self, vm): + vm_settings = vm.associators( + wmi_association_class='Msvm_SettingsDefineState', + wmi_result_class='Msvm_VirtualSystemSettingData')[0] + + new_resource_setting_data = [] + sasds = vm_settings.associators( + wmi_association_class='Msvm_VirtualSystemSettingDataComponent', + wmi_result_class='Msvm_StorageAllocationSettingData') + for sasd in sasds: + if (sasd.ResourceType == 31 and sasd.ResourceSubType == + "Microsoft:Hyper-V:Virtual Hard Disk"): + new_resource_setting_data.append(sasd.GetText_(1)) + return new_resource_setting_data + + def _live_migrate_vm(self, conn_v2_local, vm, planned_vm, rmt_ip_addr_list, + new_resource_setting_data, dest_host): + # VirtualSystemAndStorage + vsmsd = conn_v2_local.query("select * from " + "Msvm_VirtualSystemMigrationSettingData " + "where MigrationType = 32771")[0] + vsmsd.DestinationIPAddressList = rmt_ip_addr_list + if planned_vm: + vsmsd.DestinationPlannedVirtualSystemId = planned_vm.Name + migration_setting_data = vsmsd.GetText_(1) + + migr_svc = conn_v2_local.Msvm_VirtualSystemMigrationService()[0] + + LOG.debug("Starting live migration for VM: %s", vm.ElementName) + (job_path, ret_val) = migr_svc.MigrateVirtualSystemToHost( + ComputerSystem=vm.path_(), + DestinationHost=dest_host, + MigrationSettingData=migration_setting_data, + NewResourceSettingData=new_resource_setting_data) + self._vmutils.check_ret_val(ret_val, job_path) + + def _get_remote_ip_address_list(self, conn_v2_remote, dest_host): + LOG.debug("Getting live migration networks for remote host: %s", + dest_host) + migr_svc_rmt = conn_v2_remote.Msvm_VirtualSystemMigrationService()[0] + return migr_svc_rmt.MigrationServiceListenerIPAddressList + + def live_migrate_vm(self, vm_name, dest_host): + self.check_live_migration_config() + + conn_v2_local = self._get_conn_v2() + conn_v2_remote = self._get_conn_v2(dest_host) + + vm = self._get_vm(conn_v2_local, vm_name) + self._check_existing_planned_vm(conn_v2_remote, vm) + + rmt_ip_addr_list = self._get_remote_ip_address_list(conn_v2_remote, + dest_host) + + planned_vm = None + disk_paths = self._get_physical_disk_paths(vm_name) + if disk_paths: + vmutils_remote = vmutilsv2.VMUtilsV2(dest_host) + disk_paths_remote = self._get_remote_disk_data(vmutils_remote, + disk_paths, + dest_host) + + planned_vm = self._create_remote_planned_vm(conn_v2_local, + conn_v2_remote, + vm, rmt_ip_addr_list, + dest_host) + + self._update_planned_vm_disk_resources(vmutils_remote, + conn_v2_remote, planned_vm, + vm_name, disk_paths_remote) + + new_resource_setting_data = self._get_vhd_setting_data(vm) + self._live_migrate_vm(conn_v2_local, vm, planned_vm, rmt_ip_addr_list, + new_resource_setting_data, dest_host) diff --git a/oslo_windows/utils/networkutils.py b/oslo_windows/utils/networkutils.py new file mode 100644 index 00000000..07ad4891 --- /dev/null +++ b/oslo_windows/utils/networkutils.py @@ -0,0 +1,68 @@ +# 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. + +""" +Utility class for network related operations. +""" + +import sys +import uuid + +if sys.platform == 'win32': + import wmi + +from nova.i18n import _ +from nova.virt.hyperv import vmutils + + +class NetworkUtils(object): + def __init__(self): + if sys.platform == 'win32': + self._conn = wmi.WMI(moniker='//./root/virtualization') + + def get_external_vswitch(self, vswitch_name): + if vswitch_name: + vswitches = self._conn.Msvm_VirtualSwitch(ElementName=vswitch_name) + else: + # Find the vswitch that is connected to the first physical nic. + ext_port = self._conn.Msvm_ExternalEthernetPort(IsBound='TRUE')[0] + port = ext_port.associators(wmi_result_class='Msvm_SwitchPort')[0] + vswitches = port.associators(wmi_result_class='Msvm_VirtualSwitch') + + if not len(vswitches): + raise vmutils.HyperVException(_('vswitch "%s" not found') + % vswitch_name) + return vswitches[0].path_() + + def create_vswitch_port(self, vswitch_path, port_name): + switch_svc = self._conn.Msvm_VirtualSwitchManagementService()[0] + # Create a port on the vswitch. + (new_port, ret_val) = switch_svc.CreateSwitchPort( + Name=str(uuid.uuid4()), + FriendlyName=port_name, + ScopeOfResidence="", + VirtualSwitch=vswitch_path) + if ret_val != 0: + raise vmutils.HyperVException(_("Failed to create vswitch port " + "%(port_name)s on switch " + "%(vswitch_path)s") % + {'port_name': port_name, + 'vswitch_path': vswitch_path}) + return new_port + + def vswitch_port_needed(self): + # NOTE(alexpilotti): In WMI V2 the vswitch_path is set in the VM + # setting data without the need for a vswitch port. + return True diff --git a/oslo_windows/utils/networkutilsv2.py b/oslo_windows/utils/networkutilsv2.py new file mode 100644 index 00000000..558f7c44 --- /dev/null +++ b/oslo_windows/utils/networkutilsv2.py @@ -0,0 +1,63 @@ +# 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. + +""" +Utility class for network related operations. +Based on the "root/virtualization/v2" namespace available starting with +Hyper-V Server / Windows Server 2012. +""" + +import sys + +if sys.platform == 'win32': + import wmi + +from nova.i18n import _ +from nova.virt.hyperv import networkutils +from nova.virt.hyperv import vmutils + + +class NetworkUtilsV2(networkutils.NetworkUtils): + def __init__(self): + if sys.platform == 'win32': + self._conn = wmi.WMI(moniker='//./root/virtualization/v2') + + def get_external_vswitch(self, vswitch_name): + if vswitch_name: + vswitches = self._conn.Msvm_VirtualEthernetSwitch( + ElementName=vswitch_name) + if not len(vswitches): + raise vmutils.HyperVException(_('vswitch "%s" not found') + % vswitch_name) + else: + # Find the vswitch that is connected to the first physical nic. + ext_port = self._conn.Msvm_ExternalEthernetPort(IsBound='TRUE')[0] + lep = ext_port.associators(wmi_result_class='Msvm_LANEndpoint')[0] + lep1 = lep.associators(wmi_result_class='Msvm_LANEndpoint')[0] + esw = lep1.associators( + wmi_result_class='Msvm_EthernetSwitchPort')[0] + vswitches = esw.associators( + wmi_result_class='Msvm_VirtualEthernetSwitch') + + if not len(vswitches): + raise vmutils.HyperVException(_('No external vswitch found')) + + return vswitches[0].path_() + + def create_vswitch_port(self, vswitch_path, port_name): + raise NotImplementedError() + + def vswitch_port_needed(self): + return False diff --git a/oslo_windows/utils/pathutils.py b/oslo_windows/utils/pathutils.py new file mode 100644 index 00000000..657717bb --- /dev/null +++ b/oslo_windows/utils/pathutils.py @@ -0,0 +1,271 @@ +# 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. + +import os +import shutil +import sys +import time + +if sys.platform == 'win32': + import wmi + +from oslo_config import cfg +from oslo_log import log as logging + +from nova.i18n import _ +from nova import utils +from nova.virt.hyperv import constants +from nova.virt.hyperv import vmutils + +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') + +ERROR_INVALID_NAME = 123 +ERROR_DIR_IS_NOT_EMPTY = 145 + + +class PathUtils(object): + def __init__(self): + self._smb_conn = wmi.WMI(moniker=r"root\Microsoft\Windows\SMB") + + def open(self, path, mode): + """Wrapper on __builtin__.open used to simplify unit testing.""" + import __builtin__ + return __builtin__.open(path, mode) + + 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): + self.copy(src, dest) + + def copy(self, src, dest): + # With large files this is 2x-3x faster than shutil.copy(src, dest), + # especially when copying to a UNC target. + # shutil.copyfileobj(...) with a proper buffer is better than + # shutil.copy(...) but still 20% slower than a shell copy. + # It can be replaced with Win32 API calls to avoid the process + # spawning overhead. + LOG.debug('Copying file from %s to %s', src, dest) + output, ret = utils.execute('cmd.exe', '/C', 'copy', '/Y', src, dest) + if ret: + raise IOError(_('The file copy from %(src)s to %(dest)s failed') + % {'src': src, 'dest': dest}) + + def move_folder_files(self, src_dir, dest_dir): + """Moves the files of the given src_dir to dest_dir. + It will ignore any nested folders. + + :param src_dir: Given folder from which to move files. + :param dest_dir: Folder to which to move files. + """ + + for fname in os.listdir(src_dir): + src = os.path.join(src_dir, fname) + # ignore subdirs. + if os.path.isfile(src): + self.rename(src, os.path.join(dest_dir, fname)) + + def rmtree(self, path): + # This will be removed once support for Windows Server 2008R2 is + # stopped + for i in range(5): + try: + shutil.rmtree(path) + return + except WindowsError as e: + if e.winerror == ERROR_DIR_IS_NOT_EMPTY: + time.sleep(1) + else: + raise e + + def get_instances_dir(self, remote_server=None): + local_instance_path = os.path.normpath(CONF.instances_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' % + {'remote_server': remote_server, 'path': path}) + 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) + try: + if remove_dir: + self._check_remove_dir(path) + if create_dir: + self._check_create_dir(path) + return path + except WindowsError as ex: + if ex.winerror == ERROR_INVALID_NAME: + raise vmutils.HyperVException(_( + "Cannot access \"%(instances_path)s\", make sure the " + "path exists and that you have the proper permissions. " + "In particular Nova-Compute must not be executed with the " + "builtin SYSTEM account or other accounts unable to " + "authenticate on a remote host.") % + {'instances_path': instances_path}) + raise + + 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 _lookup_vhd_path(self, instance_name, vhd_path_func): + vhd_path = None + for format_ext in ['vhd', 'vhdx']: + test_path = vhd_path_func(instance_name, format_ext) + if self.exists(test_path): + vhd_path = test_path + break + return vhd_path + + def lookup_root_vhd_path(self, instance_name): + return self._lookup_vhd_path(instance_name, self.get_root_vhd_path) + + def lookup_configdrive_path(self, instance_name): + configdrive_path = None + for format_ext in constants.DISK_FORMAT_MAP: + test_path = self.get_configdrive_path(instance_name, format_ext) + if self.exists(test_path): + configdrive_path = test_path + break + return configdrive_path + + def lookup_ephemeral_vhd_path(self, instance_name): + return self._lookup_vhd_path(instance_name, + self.get_ephemeral_vhd_path) + + def get_root_vhd_path(self, instance_name, format_ext): + instance_path = self.get_instance_dir(instance_name) + return os.path.join(instance_path, 'root.' + format_ext.lower()) + + def get_configdrive_path(self, instance_name, format_ext, + remote_server=None): + instance_path = self.get_instance_dir(instance_name, remote_server) + return os.path.join(instance_path, 'configdrive.' + format_ext.lower()) + + def get_ephemeral_vhd_path(self, instance_name, format_ext): + instance_path = self.get_instance_dir(instance_name) + return os.path.join(instance_path, 'ephemeral.' + format_ext.lower()) + + 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) + + def get_vm_console_log_paths(self, vm_name, remote_server=None): + instance_dir = self.get_instance_dir(vm_name, + remote_server) + console_log_path = os.path.join(instance_dir, 'console.log') + return console_log_path, console_log_path + '.1' + + def check_smb_mapping(self, smbfs_share): + mappings = self._smb_conn.Msft_SmbMapping(RemotePath=smbfs_share) + + if not mappings: + return False + + if os.path.exists(smbfs_share): + LOG.debug('Share already mounted: %s', smbfs_share) + return True + else: + LOG.debug('Share exists but is unavailable: %s ', smbfs_share) + self.unmount_smb_share(smbfs_share, force=True) + return False + + def mount_smb_share(self, smbfs_share, username=None, password=None): + try: + LOG.debug('Mounting share: %s', smbfs_share) + self._smb_conn.Msft_SmbMapping.Create(RemotePath=smbfs_share, + UserName=username, + Password=password) + except wmi.x_wmi as exc: + err_msg = (_( + 'Unable to mount SMBFS share: %(smbfs_share)s ' + 'WMI exception: %(wmi_exc)s'), {'smbfs_share': smbfs_share, + 'wmi_exc': exc}) + raise vmutils.HyperVException(err_msg) + + def unmount_smb_share(self, smbfs_share, force=False): + mappings = self._smb_conn.Msft_SmbMapping(RemotePath=smbfs_share) + if not mappings: + LOG.debug('Share %s is not mounted. Skipping unmount.', + smbfs_share) + + for mapping in mappings: + # Due to a bug in the WMI module, getting the output of + # methods returning None will raise an AttributeError + try: + mapping.Remove(Force=force) + except AttributeError: + pass + except wmi.x_wmi: + # If this fails, a 'Generic Failure' exception is raised. + # This happens even if we unforcefully unmount an in-use + # share, for which reason we'll simply ignore it in this + # case. + if force: + raise vmutils.HyperVException( + _("Could not unmount share: %s"), smbfs_share) diff --git a/oslo_windows/utils/rdpconsoleutils.py b/oslo_windows/utils/rdpconsoleutils.py new file mode 100644 index 00000000..c897ea8c --- /dev/null +++ b/oslo_windows/utils/rdpconsoleutils.py @@ -0,0 +1,21 @@ +# 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. + + +class RDPConsoleUtils(object): + _DEFAULT_HYPERV_RDP_PORT = 2179 + + def get_rdp_console_port(self): + return self._DEFAULT_HYPERV_RDP_PORT diff --git a/oslo_windows/utils/rdpconsoleutilsv2.py b/oslo_windows/utils/rdpconsoleutilsv2.py new file mode 100644 index 00000000..4c173e22 --- /dev/null +++ b/oslo_windows/utils/rdpconsoleutilsv2.py @@ -0,0 +1,31 @@ +# 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. + +import sys + +from nova.virt.hyperv import rdpconsoleutils + +if sys.platform == 'win32': + import wmi + + +class RDPConsoleUtilsV2(rdpconsoleutils.RDPConsoleUtils): + def __init__(self): + if sys.platform == 'win32': + self._conn = wmi.WMI(moniker='//./root/virtualization/v2') + + def get_rdp_console_port(self): + rdp_setting_data = self._conn.Msvm_TerminalServiceSettingData()[0] + return rdp_setting_data.ListenerPort diff --git a/oslo_windows/utils/vhdutils.py b/oslo_windows/utils/vhdutils.py new file mode 100644 index 00000000..e5ac1311 --- /dev/null +++ b/oslo_windows/utils/vhdutils.py @@ -0,0 +1,212 @@ +# 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. + +""" +Utility class for VHD related operations. + +Official VHD format specs can be retrieved at: +http://technet.microsoft.com/en-us/library/bb676673.aspx +See "Download the Specifications Without Registering" + +Official VHDX format specs can be retrieved at: +http://www.microsoft.com/en-us/download/details.aspx?id=34750 +""" +import struct +import sys + +if sys.platform == 'win32': + import wmi + +from xml.etree import ElementTree + +from nova.i18n import _ +from nova.virt.hyperv import constants +from nova.virt.hyperv import vmutils + + +VHD_HEADER_SIZE_FIX = 512 +VHD_BAT_ENTRY_SIZE = 4 +VHD_DYNAMIC_DISK_HEADER_SIZE = 1024 +VHD_HEADER_SIZE_DYNAMIC = 512 +VHD_FOOTER_SIZE_DYNAMIC = 512 +VHD_BLK_SIZE_OFFSET = 544 + +VHD_SIGNATURE = 'conectix' +VHDX_SIGNATURE = 'vhdxfile' + + +class VHDUtils(object): + + def __init__(self): + self._vmutils = vmutils.VMUtils() + if sys.platform == 'win32': + self._conn = wmi.WMI(moniker='//./root/virtualization') + + def validate_vhd(self, vhd_path): + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + + (job_path, ret_val) = image_man_svc.ValidateVirtualHardDisk( + Path=vhd_path) + self._vmutils.check_ret_val(ret_val, job_path) + + def create_dynamic_vhd(self, path, max_internal_size, format): + if format != constants.DISK_FORMAT_VHD: + raise vmutils.HyperVException(_("Unsupported disk format: %s") % + format) + + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + + (job_path, ret_val) = image_man_svc.CreateDynamicVirtualHardDisk( + Path=path, MaxInternalSize=max_internal_size) + self._vmutils.check_ret_val(ret_val, job_path) + + def create_differencing_vhd(self, path, parent_path): + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + + (job_path, ret_val) = image_man_svc.CreateDifferencingVirtualHardDisk( + Path=path, ParentPath=parent_path) + self._vmutils.check_ret_val(ret_val, job_path) + + def reconnect_parent_vhd(self, child_vhd_path, parent_vhd_path): + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + + (job_path, ret_val) = image_man_svc.ReconnectParentVirtualHardDisk( + ChildPath=child_vhd_path, + ParentPath=parent_vhd_path, + Force=True) + self._vmutils.check_ret_val(ret_val, job_path) + + def merge_vhd(self, src_vhd_path, dest_vhd_path): + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + + (job_path, ret_val) = image_man_svc.MergeVirtualHardDisk( + SourcePath=src_vhd_path, + DestinationPath=dest_vhd_path) + self._vmutils.check_ret_val(ret_val, job_path) + + def _get_resize_method(self): + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + return image_man_svc.ExpandVirtualHardDisk + + def resize_vhd(self, vhd_path, new_max_size, is_file_max_size=True): + if is_file_max_size: + new_internal_max_size = self.get_internal_vhd_size_by_file_size( + vhd_path, new_max_size) + else: + new_internal_max_size = new_max_size + + resize = self._get_resize_method() + + (job_path, ret_val) = resize( + Path=vhd_path, MaxInternalSize=new_internal_max_size) + self._vmutils.check_ret_val(ret_val, job_path) + + def get_internal_vhd_size_by_file_size(self, vhd_path, new_vhd_file_size): + """Fixed VHD size = Data Block size + 512 bytes + | Dynamic_VHD_size = Dynamic Disk Header + | + Copy of hard disk footer + | + Hard Disk Footer + | + Data Block + | + BAT + | Dynamic Disk header fields + | Copy of hard disk footer (512 bytes) + | Dynamic Disk Header (1024 bytes) + | BAT (Block Allocation table) + | Data Block 1 + | Data Block 2 + | Data Block n + | Hard Disk Footer (512 bytes) + | Default block size is 2M + | BAT entry size is 4byte + """ + base_vhd_info = self.get_vhd_info(vhd_path) + vhd_type = base_vhd_info['Type'] + + if vhd_type == constants.VHD_TYPE_FIXED: + vhd_header_size = VHD_HEADER_SIZE_FIX + return new_vhd_file_size - vhd_header_size + elif vhd_type == constants.VHD_TYPE_DYNAMIC: + bs = self._get_vhd_dynamic_blk_size(vhd_path) + bes = VHD_BAT_ENTRY_SIZE + ddhs = VHD_DYNAMIC_DISK_HEADER_SIZE + hs = VHD_HEADER_SIZE_DYNAMIC + fs = VHD_FOOTER_SIZE_DYNAMIC + + max_internal_size = (new_vhd_file_size - + (hs + ddhs + fs)) * bs / (bes + bs) + return max_internal_size + else: + vhd_parent = self.get_vhd_parent_path(vhd_path) + return self.get_internal_vhd_size_by_file_size(vhd_parent, + new_vhd_file_size) + + def _get_vhd_dynamic_blk_size(self, vhd_path): + blk_size_offset = VHD_BLK_SIZE_OFFSET + try: + with open(vhd_path, "rb") as f: + f.seek(blk_size_offset) + version = f.read(4) + except IOError: + raise vmutils.HyperVException(_("Unable to obtain block size from" + " VHD %(vhd_path)s") % + {"vhd_path": vhd_path}) + return struct.unpack('>i', version)[0] + + 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, + job_path, + ret_val) = image_man_svc.GetVirtualHardDiskInfo(vhd_path) + self._vmutils.check_ret_val(ret_val, job_path) + + vhd_info_dict = {} + + et = ElementTree.fromstring(vhd_info) + for item in et.findall("PROPERTY"): + 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 + + def get_vhd_format(self, path): + with open(path, 'rb') as f: + # Read header + if f.read(8) == VHDX_SIGNATURE: + return constants.DISK_FORMAT_VHDX + + # Read footer + f.seek(0, 2) + file_size = f.tell() + if file_size >= 512: + f.seek(-512, 2) + if f.read(8) == VHD_SIGNATURE: + return constants.DISK_FORMAT_VHD + + raise vmutils.HyperVException(_('Unsupported virtual disk format')) + + def get_best_supported_vhd_format(self): + return constants.DISK_FORMAT_VHD diff --git a/oslo_windows/utils/vhdutilsv2.py b/oslo_windows/utils/vhdutilsv2.py new file mode 100644 index 00000000..07f267d4 --- /dev/null +++ b/oslo_windows/utils/vhdutilsv2.py @@ -0,0 +1,247 @@ +# 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. + +""" +Utility class for VHD related operations. +Based on the "root/virtualization/v2" namespace available starting with +Hyper-V Server / Windows Server 2012. +""" +import struct +import sys + +if sys.platform == 'win32': + import wmi + +from xml.etree import ElementTree + +from oslo_utils import units + +from nova.i18n import _ +from nova.virt.hyperv import constants +from nova.virt.hyperv import vhdutils +from nova.virt.hyperv import vmutils +from nova.virt.hyperv import vmutilsv2 + + +VHDX_BAT_ENTRY_SIZE = 8 +VHDX_HEADER_OFFSETS = [64 * units.Ki, 128 * units.Ki] +VHDX_HEADER_SECTION_SIZE = units.Mi +VHDX_LOG_LENGTH_OFFSET = 68 +VHDX_METADATA_SIZE_OFFSET = 64 +VHDX_REGION_TABLE_OFFSET = 192 * units.Ki +VHDX_BS_METADATA_ENTRY_OFFSET = 48 + + +class VHDUtilsV2(vhdutils.VHDUtils): + + _VHD_TYPE_DYNAMIC = 3 + _VHD_TYPE_DIFFERENCING = 4 + + _vhd_format_map = { + constants.DISK_FORMAT_VHD: 2, + constants.DISK_FORMAT_VHDX: 3, + } + + def __init__(self): + self._vmutils = vmutilsv2.VMUtilsV2() + if sys.platform == 'win32': + self._conn = wmi.WMI(moniker='//./root/virtualization/v2') + + def create_dynamic_vhd(self, path, max_internal_size, format): + vhd_format = self._vhd_format_map.get(format) + if not vhd_format: + raise vmutils.HyperVException(_("Unsupported disk format: %s") % + format) + + self._create_vhd(self._VHD_TYPE_DYNAMIC, vhd_format, path, + max_internal_size=max_internal_size) + + def create_differencing_vhd(self, path, parent_path): + # Although this method can take a size argument in case of VHDX + # images, avoid it as the underlying Win32 is currently not + # resizing the disk properly. This can be reconsidered once the + # Win32 issue is fixed. + parent_vhd_info = self.get_vhd_info(parent_path) + self._create_vhd(self._VHD_TYPE_DIFFERENCING, + parent_vhd_info["Format"], + path, parent_path=parent_path) + + def _create_vhd(self, vhd_type, format, path, max_internal_size=None, + parent_path=None): + vhd_info = self._conn.Msvm_VirtualHardDiskSettingData.new() + + vhd_info.Type = vhd_type + vhd_info.Format = format + vhd_info.Path = path + vhd_info.ParentPath = parent_path + + if max_internal_size: + vhd_info.MaxInternalSize = max_internal_size + + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + (job_path, ret_val) = image_man_svc.CreateVirtualHardDisk( + VirtualDiskSettingData=vhd_info.GetText_(1)) + self._vmutils.check_ret_val(ret_val, job_path) + + def reconnect_parent_vhd(self, child_vhd_path, parent_vhd_path): + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + vhd_info_xml = self._get_vhd_info_xml(image_man_svc, child_vhd_path) + + et = ElementTree.fromstring(vhd_info_xml) + item = et.find(".//PROPERTY[@NAME='ParentPath']/VALUE") + if item is not None: + item.text = parent_vhd_path + else: + msg = (_("Failed to reconnect image %(child_vhd_path)s to " + "parent %(parent_vhd_path)s. The child image has no " + "parent path property.") % + {'child_vhd_path': child_vhd_path, + 'parent_vhd_path': parent_vhd_path}) + raise vmutils.HyperVException(msg) + + vhd_info_xml = ElementTree.tostring(et) + + (job_path, ret_val) = image_man_svc.SetVirtualHardDiskSettingData( + VirtualDiskSettingData=vhd_info_xml) + + self._vmutils.check_ret_val(ret_val, job_path) + + def _get_resize_method(self): + image_man_svc = self._conn.Msvm_ImageManagementService()[0] + return image_man_svc.ResizeVirtualHardDisk + + def get_internal_vhd_size_by_file_size(self, vhd_path, + new_vhd_file_size): + """Get internal size of a VHD according to new VHD file size. + + VHDX Size = Header (1MB) + Log + Metadata Region + BAT + Payload Blocks + + The chunk size is the maximum number of bytes described by a SB + block. + + Chunk size = 2^{23} * LogicalSectorSize + + :param str vhd_path: VHD file path + :param new_vhd_file_size: Size of the new VHD file. + :return: Internal VHD size according to new VHD file size. + """ + vhd_format = self.get_vhd_format(vhd_path) + if vhd_format == constants.DISK_FORMAT_VHD: + return super(VHDUtilsV2, + self).get_internal_vhd_size_by_file_size( + vhd_path, new_vhd_file_size) + else: + vhd_info = self.get_vhd_info(vhd_path) + vhd_type = vhd_info['Type'] + if vhd_type == self._VHD_TYPE_DIFFERENCING: + vhd_parent = self.get_vhd_parent_path(vhd_path) + return self.get_internal_vhd_size_by_file_size(vhd_parent, + new_vhd_file_size) + else: + try: + with open(vhd_path, 'rb') as f: + hs = VHDX_HEADER_SECTION_SIZE + bes = VHDX_BAT_ENTRY_SIZE + + lss = vhd_info['LogicalSectorSize'] + bs = self._get_vhdx_block_size(f) + ls = self._get_vhdx_log_size(f) + ms = self._get_vhdx_metadata_size_and_offset(f)[0] + + chunk_ratio = (1 << 23) * lss / bs + size = new_vhd_file_size + + max_internal_size = (bs * chunk_ratio * (size - hs - + ls - ms - bes - bes / chunk_ratio) / (bs * + chunk_ratio + bes * chunk_ratio + bes)) + + return max_internal_size - (max_internal_size % bs) + + except IOError as ex: + raise vmutils.HyperVException(_("Unable to obtain " + "internal size from VHDX: " + "%(vhd_path)s. Exception: " + "%(ex)s") % + {"vhd_path": vhd_path, + "ex": ex}) + + def _get_vhdx_current_header_offset(self, vhdx_file): + sequence_numbers = [] + for offset in VHDX_HEADER_OFFSETS: + vhdx_file.seek(offset + 8) + sequence_numbers.append(struct.unpack(' 1: + raise HyperVException(_('Duplicate VM name found: %s') % vm_name) + else: + return vms[0] + + def vm_exists(self, vm_name): + return self._lookup_vm(vm_name) is not None + + def get_vm_id(self, vm_name): + vm = self._lookup_vm_check(vm_name) + return vm.Name + + def _get_vm_setting_data(self, vm): + vmsettings = vm.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS) + # Avoid snapshots + return [s for s in vmsettings if s.SettingType == 3][0] + + def _set_vm_memory(self, vm, vmsetting, memory_mb, dynamic_memory_ratio): + mem_settings = vmsetting.associators( + wmi_result_class=self._MEMORY_SETTING_DATA_CLASS)[0] + + max_mem = long(memory_mb) + mem_settings.Limit = max_mem + + if dynamic_memory_ratio > 1: + mem_settings.DynamicMemoryEnabled = True + # Must be a multiple of 2 + reserved_mem = min( + long(max_mem / dynamic_memory_ratio) >> 1 << 1, + max_mem) + else: + mem_settings.DynamicMemoryEnabled = False + reserved_mem = max_mem + + mem_settings.Reservation = reserved_mem + # Start with the minimum memory + mem_settings.VirtualQuantity = reserved_mem + + self._modify_virt_resource(mem_settings, vm.path_()) + + def _set_vm_vcpus(self, vm, vmsetting, vcpus_num, limit_cpu_features): + procsetting = vmsetting.associators( + wmi_result_class=self._PROCESSOR_SETTING_DATA_CLASS)[0] + vcpus = long(vcpus_num) + procsetting.VirtualQuantity = vcpus + procsetting.Reservation = vcpus + procsetting.Limit = 100000 # static assignment to 100% + procsetting.LimitProcessorFeatures = limit_cpu_features + + self._modify_virt_resource(procsetting, vm.path_()) + + def update_vm(self, vm_name, memory_mb, vcpus_num, limit_cpu_features, + dynamic_memory_ratio): + vm = self._lookup_vm_check(vm_name) + vmsetting = self._get_vm_setting_data(vm) + self._set_vm_memory(vm, vmsetting, memory_mb, dynamic_memory_ratio) + self._set_vm_vcpus(vm, vmsetting, vcpus_num, limit_cpu_features) + + def check_admin_permissions(self): + if not self._conn.Msvm_VirtualSystemManagementService(): + msg = _("The Windows account running nova-compute on this Hyper-V" + " host doesn't have the required permissions to create or" + " operate the virtual machine.") + raise HyperVAuthorizationException(msg) + + def create_vm(self, vm_name, memory_mb, vcpus_num, limit_cpu_features, + dynamic_memory_ratio, vm_gen, instance_path, notes=None): + """Creates a VM.""" + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + + LOG.debug('Creating VM %s', vm_name) + vm = self._create_vm_obj(vs_man_svc, vm_name, vm_gen, notes, + dynamic_memory_ratio, instance_path) + + vmsetting = self._get_vm_setting_data(vm) + + LOG.debug('Setting memory for vm %s', vm_name) + self._set_vm_memory(vm, vmsetting, memory_mb, dynamic_memory_ratio) + + LOG.debug('Set vCPUs for vm %s', vm_name) + self._set_vm_vcpus(vm, vmsetting, vcpus_num, limit_cpu_features) + + def _create_vm_obj(self, vs_man_svc, vm_name, vm_gen, notes, + dynamic_memory_ratio, instance_path): + vs_gs_data = self._conn.Msvm_VirtualSystemGlobalSettingData.new() + vs_gs_data.ElementName = vm_name + # Don't start automatically on host boot + vs_gs_data.AutomaticStartupAction = self._AUTOMATIC_STARTUP_ACTION_NONE + vs_gs_data.ExternalDataRoot = instance_path + vs_gs_data.SnapshotDataRoot = instance_path + + (vm_path, + job_path, + ret_val) = vs_man_svc.DefineVirtualSystem([], None, + vs_gs_data.GetText_(1)) + self.check_ret_val(ret_val, job_path) + + vm = self._get_wmi_obj(vm_path) + + if notes: + vmsetting = self._get_vm_setting_data(vm) + vmsetting.Notes = '\n'.join(notes) + self._modify_virtual_system(vs_man_svc, vm_path, vmsetting) + + return self._get_wmi_obj(vm_path) + + def _modify_virtual_system(self, vs_man_svc, vm_path, vmsetting): + (job_path, ret_val) = vs_man_svc.ModifyVirtualSystem( + ComputerSystem=vm_path, + SystemSettingData=vmsetting.GetText_(1))[1:] + self.check_ret_val(ret_val, job_path) + + def get_vm_scsi_controller(self, vm_name): + vm = self._lookup_vm_check(vm_name) + return self._get_vm_scsi_controller(vm) + + def _get_vm_scsi_controller(self, vm): + vmsettings = vm.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS) + rasds = vmsettings[0].associators( + wmi_result_class=self._RESOURCE_ALLOC_SETTING_DATA_CLASS) + res = [r for r in rasds + if r.ResourceSubType == self._SCSI_CTRL_RES_SUB_TYPE][0] + return res.path_() + + def _get_vm_ide_controller(self, vm, ctrller_addr): + vmsettings = vm.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS) + rasds = vmsettings[0].associators( + wmi_result_class=self._RESOURCE_ALLOC_SETTING_DATA_CLASS) + ide_ctrls = [r for r in rasds + if r.ResourceSubType == self._IDE_CTRL_RES_SUB_TYPE + and r.Address == str(ctrller_addr)] + + return ide_ctrls[0].path_() if ide_ctrls else None + + def get_vm_ide_controller(self, vm_name, ctrller_addr): + vm = self._lookup_vm_check(vm_name) + return self._get_vm_ide_controller(vm, ctrller_addr) + + def get_attached_disks(self, scsi_controller_path): + volumes = self._conn.query( + self._get_attached_disks_query_string(scsi_controller_path)) + return volumes + + def _get_attached_disks_query_string(self, scsi_controller_path): + return ("SELECT * FROM %(class_name)s WHERE (" + "ResourceSubType='%(res_sub_type)s' OR " + "ResourceSubType='%(res_sub_type_virt)s') AND " + "Parent='%(parent)s'" % { + 'class_name': self._RESOURCE_ALLOC_SETTING_DATA_CLASS, + 'res_sub_type': self._PHYS_DISK_RES_SUB_TYPE, + 'res_sub_type_virt': self._DISK_DRIVE_RES_SUB_TYPE, + 'parent': scsi_controller_path.replace("'", "''")}) + + def _get_new_setting_data(self, class_name): + obj = self._conn.query("SELECT * FROM %s WHERE InstanceID " + "LIKE '%%\\Default'" % class_name)[0] + return self._check_clone_wmi_obj(class_name, obj) + + def _get_new_resource_setting_data(self, resource_sub_type, + class_name=None): + if class_name is None: + class_name = self._RESOURCE_ALLOC_SETTING_DATA_CLASS + obj = self._conn.query("SELECT * FROM %(class_name)s " + "WHERE ResourceSubType = " + "'%(res_sub_type)s' AND " + "InstanceID LIKE '%%\\Default'" % + {"class_name": class_name, + "res_sub_type": resource_sub_type})[0] + return self._check_clone_wmi_obj(class_name, obj) + + def _check_clone_wmi_obj(self, class_name, obj): + if self._clone_wmi_objs: + return self._clone_wmi_obj(class_name, obj) + else: + return obj + + def _clone_wmi_obj(self, class_name, obj): + wmi_class = getattr(self._conn, class_name) + new_obj = wmi_class.new() + # Copy the properties from the original. + for prop in obj._properties: + value = obj.Properties_.Item(prop).Value + new_obj.Properties_.Item(prop).Value = value + return new_obj + + def attach_scsi_drive(self, vm_name, path, drive_type=constants.DISK): + vm = self._lookup_vm_check(vm_name) + ctrller_path = self._get_vm_scsi_controller(vm) + drive_addr = self.get_free_controller_slot(ctrller_path) + self.attach_drive(vm_name, path, ctrller_path, drive_addr, drive_type) + + def attach_ide_drive(self, vm_name, path, ctrller_addr, drive_addr, + drive_type=constants.DISK): + vm = self._lookup_vm_check(vm_name) + ctrller_path = self._get_vm_ide_controller(vm, ctrller_addr) + self.attach_drive(vm_name, path, ctrller_path, drive_addr, drive_type) + + def attach_drive(self, vm_name, path, ctrller_path, drive_addr, + drive_type=constants.DISK): + """Create a drive and attach it to the vm.""" + + vm = self._lookup_vm_check(vm_name) + + if drive_type == constants.DISK: + res_sub_type = self._DISK_DRIVE_RES_SUB_TYPE + elif drive_type == constants.DVD: + res_sub_type = self._DVD_DRIVE_RES_SUB_TYPE + + drive = self._get_new_resource_setting_data(res_sub_type) + + # Set the ctrller as parent. + drive.Parent = ctrller_path + drive.Address = drive_addr + # Add the cloned disk drive object to the vm. + new_resources = self._add_virt_resource(drive, vm.path_()) + drive_path = new_resources[0] + + if drive_type == constants.DISK: + res_sub_type = self._HARD_DISK_RES_SUB_TYPE + elif drive_type == constants.DVD: + res_sub_type = self._DVD_DISK_RES_SUB_TYPE + + res = self._get_new_resource_setting_data(res_sub_type) + # Set the new drive as the parent. + res.Parent = drive_path + res.Connection = [path] + + # Add the new vhd object as a virtual hard disk to the vm. + self._add_virt_resource(res, vm.path_()) + + def create_scsi_controller(self, vm_name): + """Create an iscsi controller ready to mount volumes.""" + + vm = self._lookup_vm_check(vm_name) + scsicontrl = self._get_new_resource_setting_data( + self._SCSI_CTRL_RES_SUB_TYPE) + + scsicontrl.VirtualSystemIdentifiers = ['{' + str(uuid.uuid4()) + '}'] + self._add_virt_resource(scsicontrl, vm.path_()) + + def attach_volume_to_controller(self, vm_name, controller_path, address, + mounted_disk_path): + """Attach a volume to a controller.""" + + vm = self._lookup_vm_check(vm_name) + + diskdrive = self._get_new_resource_setting_data( + self._PHYS_DISK_RES_SUB_TYPE) + + diskdrive.Address = address + diskdrive.Parent = controller_path + diskdrive.HostResource = [mounted_disk_path] + self._add_virt_resource(diskdrive, vm.path_()) + + def _get_disk_resource_address(self, disk_resource): + return disk_resource.Address + + def set_disk_host_resource(self, vm_name, controller_path, address, + mounted_disk_path): + disk_found = False + vm = self._lookup_vm_check(vm_name) + (disk_resources, volume_resources) = self._get_vm_disks(vm) + for disk_resource in disk_resources + volume_resources: + if (disk_resource.Parent == controller_path and + self._get_disk_resource_address(disk_resource) == + str(address)): + if (disk_resource.HostResource and + disk_resource.HostResource[0] != mounted_disk_path): + LOG.debug('Updating disk host resource "%(old)s" to ' + '"%(new)s"' % + {'old': disk_resource.HostResource[0], + 'new': mounted_disk_path}) + disk_resource.HostResource = [mounted_disk_path] + self._modify_virt_resource(disk_resource, vm.path_()) + disk_found = True + break + if not disk_found: + LOG.warning(_LW('Disk not found on controller ' + '"%(controller_path)s" with ' + 'address "%(address)s"'), + {'controller_path': controller_path, + 'address': address}) + + def set_nic_connection(self, vm_name, nic_name, vswitch_conn_data): + nic_data = self._get_nic_data_by_name(nic_name) + nic_data.Connection = [vswitch_conn_data] + + vm = self._lookup_vm_check(vm_name) + self._modify_virt_resource(nic_data, vm.path_()) + + def _get_nic_data_by_name(self, name): + return self._conn.Msvm_SyntheticEthernetPortSettingData( + ElementName=name)[0] + + def create_nic(self, vm_name, nic_name, mac_address): + """Create a (synthetic) nic and attach it to the vm.""" + # Create a new nic + new_nic_data = self._get_new_setting_data( + self._SYNTHETIC_ETHERNET_PORT_SETTING_DATA_CLASS) + + # Configure the nic + new_nic_data.ElementName = nic_name + new_nic_data.Address = mac_address.replace(':', '') + new_nic_data.StaticMacAddress = 'True' + new_nic_data.VirtualSystemIdentifiers = ['{' + str(uuid.uuid4()) + '}'] + + # Add the new nic to the vm + vm = self._lookup_vm_check(vm_name) + + self._add_virt_resource(new_nic_data, vm.path_()) + + def soft_shutdown_vm(self, vm_name): + vm = self._lookup_vm_check(vm_name) + shutdown_component = vm.associators( + wmi_result_class=self._SHUTDOWN_COMPONENT) + + if not shutdown_component: + # If no shutdown_component is found, it means the VM is already + # in a shutdown state. + return + + reason = 'Soft shutdown requested by OpenStack Nova.' + (ret_val, ) = shutdown_component[0].InitiateShutdown(Force=False, + Reason=reason) + self.check_ret_val(ret_val, None) + + def set_vm_state(self, vm_name, req_state): + """Set the desired state of the VM.""" + vm = self._lookup_vm_check(vm_name) + (job_path, + ret_val) = vm.RequestStateChange(self._vm_power_states_map[req_state]) + # Invalid state for current operation (32775) typically means that + # the VM is already in the state requested + self.check_ret_val(ret_val, job_path, [0, 32775]) + LOG.debug("Successfully changed vm state of %(vm_name)s " + "to %(req_state)s", + {'vm_name': vm_name, 'req_state': req_state}) + + def _get_disk_resource_disk_path(self, disk_resource): + return disk_resource.Connection + + def get_vm_storage_paths(self, vm_name): + vm = self._lookup_vm_check(vm_name) + (disk_resources, volume_resources) = self._get_vm_disks(vm) + + volume_drives = [] + for volume_resource in volume_resources: + drive_path = volume_resource.HostResource[0] + volume_drives.append(drive_path) + + disk_files = [] + for disk_resource in disk_resources: + disk_files.extend( + [c for c in self._get_disk_resource_disk_path(disk_resource)]) + + return (disk_files, volume_drives) + + def _get_vm_disks(self, vm): + vmsettings = vm.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS) + rasds = vmsettings[0].associators( + wmi_result_class=self._STORAGE_ALLOC_SETTING_DATA_CLASS) + disk_resources = [r for r in rasds if + r.ResourceSubType in + [self._HARD_DISK_RES_SUB_TYPE, + self._DVD_DISK_RES_SUB_TYPE]] + + if (self._RESOURCE_ALLOC_SETTING_DATA_CLASS != + self._STORAGE_ALLOC_SETTING_DATA_CLASS): + rasds = vmsettings[0].associators( + wmi_result_class=self._RESOURCE_ALLOC_SETTING_DATA_CLASS) + + volume_resources = [r for r in rasds if + r.ResourceSubType == self._PHYS_DISK_RES_SUB_TYPE] + + return (disk_resources, volume_resources) + + 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) + + def check_ret_val(self, ret_val, job_path, success_values=[0]): + if ret_val == constants.WMI_JOB_STATUS_STARTED: + return self._wait_for_job(job_path) + elif ret_val not in success_values: + raise HyperVException(_('Operation failed with return value: %s') + % ret_val) + + def _wait_for_job(self, job_path): + """Poll WMI job state and wait for completion.""" + job = self._get_wmi_obj(job_path) + + while job.JobState == constants.WMI_JOB_STATE_RUNNING: + time.sleep(0.1) + job = self._get_wmi_obj(job_path) + if job.JobState != constants.WMI_JOB_STATE_COMPLETED: + job_state = job.JobState + if job.path().Class == "Msvm_ConcreteJob": + err_sum_desc = job.ErrorSummaryDescription + err_desc = job.ErrorDescription + err_code = job.ErrorCode + raise HyperVException(_("WMI job failed with status " + "%(job_state)d. Error details: " + "%(err_sum_desc)s - %(err_desc)s - " + "Error code: %(err_code)d") % + {'job_state': job_state, + 'err_sum_desc': err_sum_desc, + 'err_desc': err_desc, + 'err_code': err_code}) + else: + (error, ret_val) = job.GetError() + if not ret_val and error: + raise HyperVException(_("WMI job failed with status " + "%(job_state)d. Error details: " + "%(error)s") % + {'job_state': job_state, + 'error': error}) + else: + raise HyperVException(_("WMI job failed with status " + "%d. No error " + "description available") % + job_state) + desc = job.Description + elap = job.ElapsedTime + LOG.debug("WMI job succeeded: %(desc)s, Elapsed=%(elap)s", + {'desc': desc, 'elap': elap}) + return job + + def _get_wmi_obj(self, path): + return wmi.WMI(moniker=path.replace('\\', '/')) + + def _add_virt_resource(self, res_setting_data, vm_path): + """Adds a new resource to the VM.""" + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + res_xml = [res_setting_data.GetText_(1)] + (job_path, + new_resources, + ret_val) = vs_man_svc.AddVirtualSystemResources(res_xml, vm_path) + self.check_ret_val(ret_val, job_path) + return new_resources + + def _modify_virt_resource(self, res_setting_data, vm_path): + """Updates a VM resource.""" + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + (job_path, ret_val) = vs_man_svc.ModifyVirtualSystemResources( + ResourceSettingData=[res_setting_data.GetText_(1)], + ComputerSystem=vm_path) + self.check_ret_val(ret_val, job_path) + + def _remove_virt_resource(self, res_setting_data, vm_path): + """Removes a VM resource.""" + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + res_path = [res_setting_data.path_()] + (job_path, ret_val) = vs_man_svc.RemoveVirtualSystemResources(res_path, + vm_path) + self.check_ret_val(ret_val, job_path) + + def take_vm_snapshot(self, vm_name): + vm = self._lookup_vm_check(vm_name) + + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + + (job_path, ret_val, + snp_setting_data) = vs_man_svc.CreateVirtualSystemSnapshot(vm.path_()) + self.check_ret_val(ret_val, job_path) + + job_wmi_path = job_path.replace('\\', '/') + job = wmi.WMI(moniker=job_wmi_path) + snp_setting_data = job.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS)[0] + return snp_setting_data.path_() + + def remove_vm_snapshot(self, snapshot_path): + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + + (job_path, ret_val) = vs_man_svc.RemoveVirtualSystemSnapshot( + snapshot_path) + self.check_ret_val(ret_val, job_path) + + def detach_vm_disk(self, vm_name, disk_path, is_physical=True): + vm = self._lookup_vm_check(vm_name) + disk_resource = self._get_mounted_disk_resource_from_path(disk_path, + is_physical) + + if disk_resource: + parent = self._conn.query("SELECT * FROM " + "Msvm_ResourceAllocationSettingData " + "WHERE __PATH = '%s'" % + disk_resource.Parent)[0] + + self._remove_virt_resource(disk_resource, vm.path_()) + if not is_physical: + self._remove_virt_resource(parent, vm.path_()) + + def _get_mounted_disk_resource_from_path(self, disk_path, is_physical): + if is_physical: + class_name = self._RESOURCE_ALLOC_SETTING_DATA_CLASS + res_sub_type = self._PHYS_DISK_RES_SUB_TYPE + else: + class_name = self._STORAGE_ALLOC_SETTING_DATA_CLASS + res_sub_type = self._HARD_DISK_RES_SUB_TYPE + + disk_resources = self._conn.query("SELECT * FROM %(class_name)s " + "WHERE ResourceSubType = " + "'%(res_sub_type)s'" % + {"class_name": class_name, + "res_sub_type": res_sub_type}) + + for disk_resource in disk_resources: + if disk_resource.HostResource: + if disk_resource.HostResource[0].lower() == disk_path.lower(): + return disk_resource + + def get_mounted_disk_by_drive_number(self, device_number): + mounted_disks = self._conn.query("SELECT * FROM Msvm_DiskDrive " + "WHERE DriveNumber=" + + str(device_number)) + if len(mounted_disks): + return mounted_disks[0].path_() + + def get_controller_volume_paths(self, controller_path): + disks = self._conn.query("SELECT * FROM %(class_name)s " + "WHERE ResourceSubType = '%(res_sub_type)s' " + "AND Parent='%(parent)s'" % + {"class_name": + self._RESOURCE_ALLOC_SETTING_DATA_CLASS, + "res_sub_type": + self._PHYS_DISK_RES_SUB_TYPE, + "parent": + controller_path}) + disk_data = {} + for disk in disks: + if disk.HostResource: + disk_data[disk.path().RelPath] = disk.HostResource[0] + return disk_data + + def get_free_controller_slot(self, scsi_controller_path): + attached_disks = self.get_attached_disks(scsi_controller_path) + used_slots = [int(disk.AddressOnParent) for disk in attached_disks] + + for slot in range(constants.SCSI_CONTROLLER_SLOTS_NUMBER): + if slot not in used_slots: + return slot + raise HyperVException(_("Exceeded the maximum number of slots")) + + def enable_vm_metrics_collection(self, vm_name): + raise NotImplementedError(_("Metrics collection is not supported on " + "this version of Hyper-V")) + + def get_vm_serial_port_connection(self, vm_name, update_connection=None): + vm = self._lookup_vm_check(vm_name) + + vmsettings = vm.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS) + rasds = vmsettings[0].associators( + wmi_result_class=self._RESOURCE_ALLOC_SETTING_DATA_CLASS) + serial_port = ( + [r for r in rasds if + r.ResourceSubType == self._SERIAL_PORT_RES_SUB_TYPE][0]) + + if update_connection: + serial_port.Connection = [update_connection] + self._modify_virt_resource(serial_port, vm.path_()) + + if len(serial_port.Connection) > 0: + return serial_port.Connection[0] + + def get_active_instances(self): + """Return the names of all the active instances known to Hyper-V.""" + vm_names = self.list_instances() + vms = [self._lookup_vm(vm_name) for vm_name in vm_names] + active_vm_names = [v.ElementName for v in vms + if v.EnabledState == constants.HYPERV_VM_STATE_ENABLED] + + return active_vm_names + + def get_vm_power_state_change_listener(self, timeframe, filtered_states): + field = self._VM_ENABLED_STATE_PROP + query = self._get_event_wql_query(cls=self._COMPUTER_SYSTEM_CLASS, + field=field, + timeframe=timeframe, + filtered_states=filtered_states) + return self._conn.Msvm_ComputerSystem.watch_for(raw_wql=query, + fields=[field]) + + def _get_event_wql_query(self, cls, field, + timeframe, filtered_states=None): + """Return a WQL query used for polling WMI events. + + :param cls: the WMI class polled for events + :param field: the field checked + :param timeframe: check for events that occurred in + the specified timeframe + :param filtered_states: only catch events triggered when a WMI + object transitioned into one of those + states. + """ + query = ("SELECT %(field)s, TargetInstance " + "FROM __InstanceModificationEvent " + "WITHIN %(timeframe)s " + "WHERE TargetInstance ISA '%(class)s' " + "AND TargetInstance.%(field)s != " + "PreviousInstance.%(field)s" % + {'class': cls, + 'field': field, + 'timeframe': timeframe}) + if filtered_states: + checks = ["TargetInstance.%s = '%s'" % (field, state) + for state in filtered_states] + query += " AND (%s)" % " OR ".join(checks) + return query + + def _get_instance_notes(self, vm_name): + vm = self._lookup_vm_check(vm_name) + vmsettings = self._get_vm_setting_data(vm) + return [note for note in vmsettings.Notes.split('\n') if note] + + def get_instance_uuid(self, vm_name): + instance_notes = self._get_instance_notes(vm_name) + if instance_notes and uuidutils.is_uuid_like(instance_notes[0]): + return instance_notes[0] + + def get_vm_power_state(self, vm_enabled_state): + return self._enabled_states_map.get(vm_enabled_state, + constants.HYPERV_VM_STATE_OTHER) diff --git a/oslo_windows/utils/vmutilsv2.py b/oslo_windows/utils/vmutilsv2.py new file mode 100644 index 00000000..a78564e4 --- /dev/null +++ b/oslo_windows/utils/vmutilsv2.py @@ -0,0 +1,328 @@ +# 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. +""" +Utility class for VM related operations. +Based on the "root/virtualization/v2" namespace available starting with +Hyper-V Server / Windows Server 2012. +""" + +import sys +import uuid + +if sys.platform == 'win32': + import wmi + +from oslo_config import cfg +from oslo_log import log as logging + +from nova.virt.hyperv import constants +from nova.virt.hyperv import vmutils + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class VMUtilsV2(vmutils.VMUtils): + + _PHYS_DISK_RES_SUB_TYPE = 'Microsoft:Hyper-V:Physical Disk Drive' + _DISK_DRIVE_RES_SUB_TYPE = 'Microsoft:Hyper-V:Synthetic Disk Drive' + _DVD_DRIVE_RES_SUB_TYPE = 'Microsoft:Hyper-V:Synthetic DVD Drive' + _SCSI_RES_SUBTYPE = 'Microsoft:Hyper-V:Synthetic SCSI Controller' + _HARD_DISK_RES_SUB_TYPE = 'Microsoft:Hyper-V:Virtual Hard Disk' + _DVD_DISK_RES_SUB_TYPE = 'Microsoft:Hyper-V:Virtual CD/DVD Disk' + _IDE_CTRL_RES_SUB_TYPE = 'Microsoft:Hyper-V:Emulated IDE Controller' + _SCSI_CTRL_RES_SUB_TYPE = 'Microsoft:Hyper-V:Synthetic SCSI Controller' + _SERIAL_PORT_RES_SUB_TYPE = 'Microsoft:Hyper-V:Serial Port' + + _VIRTUAL_SYSTEM_TYPE_REALIZED = 'Microsoft:Hyper-V:System:Realized' + _VIRTUAL_SYSTEM_SUBTYPE_GEN2 = 'Microsoft:Hyper-V:SubType:2' + + _SNAPSHOT_FULL = 2 + + _METRIC_AGGR_CPU_AVG = 'Aggregated Average CPU Utilization' + _METRIC_AGGR_MEMORY_AVG = 'Aggregated Average Memory Utilization' + _METRIC_ENABLED = 2 + + _STORAGE_ALLOC_SETTING_DATA_CLASS = 'Msvm_StorageAllocationSettingData' + _ETHERNET_PORT_ALLOCATION_SETTING_DATA_CLASS = \ + 'Msvm_EthernetPortAllocationSettingData' + + _AUTOMATIC_STARTUP_ACTION_NONE = 2 + + _vm_power_states_map = {constants.HYPERV_VM_STATE_ENABLED: 2, + constants.HYPERV_VM_STATE_DISABLED: 3, + constants.HYPERV_VM_STATE_SHUTTING_DOWN: 4, + constants.HYPERV_VM_STATE_REBOOT: 11, + constants.HYPERV_VM_STATE_PAUSED: 9, + constants.HYPERV_VM_STATE_SUSPENDED: 6} + + def __init__(self, host='.'): + super(VMUtilsV2, self).__init__(host) + + def _init_hyperv_wmi_conn(self, host): + self._conn = wmi.WMI(moniker='//%s/root/virtualization/v2' % host) + + def list_instance_notes(self): + instance_notes = [] + + for vs in self._conn.Msvm_VirtualSystemSettingData( + ['ElementName', 'Notes'], + VirtualSystemType=self._VIRTUAL_SYSTEM_TYPE_REALIZED): + if vs.Notes is not None: + instance_notes.append( + (vs.ElementName, [v for v in vs.Notes if v])) + + return instance_notes + + def list_instances(self): + """Return the names of all the instances known to Hyper-V.""" + return [v.ElementName for v in + self._conn.Msvm_VirtualSystemSettingData( + ['ElementName'], + VirtualSystemType=self._VIRTUAL_SYSTEM_TYPE_REALIZED)] + + def _create_vm_obj(self, vs_man_svc, vm_name, vm_gen, notes, + dynamic_memory_ratio, instance_path): + vs_data = self._conn.Msvm_VirtualSystemSettingData.new() + vs_data.ElementName = vm_name + vs_data.Notes = notes + # Don't start automatically on host boot + vs_data.AutomaticStartupAction = self._AUTOMATIC_STARTUP_ACTION_NONE + + # vNUMA and dynamic memory are mutually exclusive + if dynamic_memory_ratio > 1: + vs_data.VirtualNumaEnabled = False + + if vm_gen == constants.VM_GEN_2: + vs_data.VirtualSystemSubType = self._VIRTUAL_SYSTEM_SUBTYPE_GEN2 + vs_data.SecureBootEnabled = False + + # Created VMs must have their *DataRoot paths in the same location as + # the instances' path. + vs_data.ConfigurationDataRoot = instance_path + vs_data.LogDataRoot = instance_path + vs_data.SnapshotDataRoot = instance_path + vs_data.SuspendDataRoot = instance_path + vs_data.SwapFileDataRoot = instance_path + + (job_path, + vm_path, + ret_val) = vs_man_svc.DefineSystem(ResourceSettings=[], + ReferenceConfiguration=None, + SystemSettings=vs_data.GetText_(1)) + job = self.check_ret_val(ret_val, job_path) + if not vm_path and job: + vm_path = job.associators(self._AFFECTED_JOB_ELEMENT_CLASS)[0] + return self._get_wmi_obj(vm_path) + + def _get_vm_setting_data(self, vm): + vmsettings = vm.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS) + # Avoid snapshots + return [s for s in vmsettings if + s.VirtualSystemType == self._VIRTUAL_SYSTEM_TYPE_REALIZED][0] + + def _get_attached_disks_query_string(self, scsi_controller_path): + # DVD Drives can be attached to SCSI as well, if the VM Generation is 2 + return ("SELECT * FROM Msvm_ResourceAllocationSettingData WHERE (" + "ResourceSubType='%(res_sub_type)s' OR " + "ResourceSubType='%(res_sub_type_virt)s' OR " + "ResourceSubType='%(res_sub_type_dvd)s') AND " + "Parent = '%(parent)s'" % { + 'res_sub_type': self._PHYS_DISK_RES_SUB_TYPE, + 'res_sub_type_virt': self._DISK_DRIVE_RES_SUB_TYPE, + 'res_sub_type_dvd': self._DVD_DRIVE_RES_SUB_TYPE, + 'parent': scsi_controller_path.replace("'", "''")}) + + def attach_drive(self, vm_name, path, ctrller_path, drive_addr, + drive_type=constants.DISK): + """Create a drive and attach it to the vm.""" + + vm = self._lookup_vm_check(vm_name) + + if drive_type == constants.DISK: + res_sub_type = self._DISK_DRIVE_RES_SUB_TYPE + elif drive_type == constants.DVD: + res_sub_type = self._DVD_DRIVE_RES_SUB_TYPE + + drive = self._get_new_resource_setting_data(res_sub_type) + + # Set the ctrller as parent. + drive.Parent = ctrller_path + drive.Address = drive_addr + drive.AddressOnParent = drive_addr + # Add the cloned disk drive object to the vm. + new_resources = self._add_virt_resource(drive, vm.path_()) + drive_path = new_resources[0] + + if drive_type == constants.DISK: + res_sub_type = self._HARD_DISK_RES_SUB_TYPE + elif drive_type == constants.DVD: + res_sub_type = self._DVD_DISK_RES_SUB_TYPE + + res = self._get_new_resource_setting_data( + res_sub_type, self._STORAGE_ALLOC_SETTING_DATA_CLASS) + + res.Parent = drive_path + res.HostResource = [path] + + self._add_virt_resource(res, vm.path_()) + + def attach_volume_to_controller(self, vm_name, controller_path, address, + mounted_disk_path): + """Attach a volume to a controller.""" + + vm = self._lookup_vm_check(vm_name) + + diskdrive = self._get_new_resource_setting_data( + self._PHYS_DISK_RES_SUB_TYPE) + + diskdrive.AddressOnParent = address + diskdrive.Parent = controller_path + diskdrive.HostResource = [mounted_disk_path] + + self._add_virt_resource(diskdrive, vm.path_()) + + def _get_disk_resource_address(self, disk_resource): + return disk_resource.AddressOnParent + + def create_scsi_controller(self, vm_name): + """Create an iscsi controller ready to mount volumes.""" + scsicontrl = self._get_new_resource_setting_data( + self._SCSI_RES_SUBTYPE) + + scsicontrl.VirtualSystemIdentifiers = ['{' + str(uuid.uuid4()) + '}'] + + vm = self._lookup_vm_check(vm_name) + self._add_virt_resource(scsicontrl, vm.path_()) + + def _get_disk_resource_disk_path(self, disk_resource): + return disk_resource.HostResource + + def destroy_vm(self, vm_name): + vm = self._lookup_vm_check(vm_name) + + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + # Remove the VM. It does not destroy any associated virtual disk. + (job_path, ret_val) = vs_man_svc.DestroySystem(vm.path_()) + self.check_ret_val(ret_val, job_path) + + def _add_virt_resource(self, res_setting_data, vm_path): + """Adds a new resource to the VM.""" + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + res_xml = [res_setting_data.GetText_(1)] + (job_path, + new_resources, + ret_val) = vs_man_svc.AddResourceSettings(vm_path, res_xml) + self.check_ret_val(ret_val, job_path) + return new_resources + + def _modify_virt_resource(self, res_setting_data, vm_path): + """Updates a VM resource.""" + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + (job_path, + out_res_setting_data, + ret_val) = vs_man_svc.ModifyResourceSettings( + ResourceSettings=[res_setting_data.GetText_(1)]) + self.check_ret_val(ret_val, job_path) + + def _remove_virt_resource(self, res_setting_data, vm_path): + """Removes a VM resource.""" + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + res_path = [res_setting_data.path_()] + (job_path, ret_val) = vs_man_svc.RemoveResourceSettings(res_path) + self.check_ret_val(ret_val, job_path) + + def get_vm_state(self, vm_name): + settings = self.get_vm_summary_info(vm_name) + return settings['EnabledState'] + + def take_vm_snapshot(self, vm_name): + vm = self._lookup_vm_check(vm_name) + vs_snap_svc = self._conn.Msvm_VirtualSystemSnapshotService()[0] + + (job_path, snp_setting_data, ret_val) = vs_snap_svc.CreateSnapshot( + AffectedSystem=vm.path_(), + SnapshotType=self._SNAPSHOT_FULL) + self.check_ret_val(ret_val, job_path) + + job_wmi_path = job_path.replace('\\', '/') + job = wmi.WMI(moniker=job_wmi_path) + snp_setting_data = job.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS)[0] + + return snp_setting_data.path_() + + def remove_vm_snapshot(self, snapshot_path): + vs_snap_svc = self._conn.Msvm_VirtualSystemSnapshotService()[0] + (job_path, ret_val) = vs_snap_svc.DestroySnapshot(snapshot_path) + self.check_ret_val(ret_val, job_path) + + def set_nic_connection(self, vm_name, nic_name, vswitch_conn_data): + nic_data = self._get_nic_data_by_name(nic_name) + + eth_port_data = self._get_new_setting_data( + self._ETHERNET_PORT_ALLOCATION_SETTING_DATA_CLASS) + + eth_port_data.HostResource = [vswitch_conn_data] + eth_port_data.Parent = nic_data.path_() + + vm = self._lookup_vm_check(vm_name) + self._add_virt_resource(eth_port_data, vm.path_()) + + def enable_vm_metrics_collection(self, vm_name): + metric_names = [self._METRIC_AGGR_CPU_AVG, + self._METRIC_AGGR_MEMORY_AVG] + + vm = self._lookup_vm_check(vm_name) + metric_svc = self._conn.Msvm_MetricService()[0] + (disks, volumes) = self._get_vm_disks(vm) + filtered_disks = [d for d in disks if + d.ResourceSubType is not self._DVD_DISK_RES_SUB_TYPE] + + # enable metrics for disk. + for disk in filtered_disks: + self._enable_metrics(metric_svc, disk) + + for metric_name in metric_names: + metric_def = self._conn.CIM_BaseMetricDefinition(Name=metric_name) + if not metric_def: + LOG.debug("Metric not found: %s", metric_name) + else: + self._enable_metrics(metric_svc, vm, metric_def[0].path_()) + + def _enable_metrics(self, metric_svc, element, definition_path=None): + metric_svc.ControlMetrics( + Subject=element.path_(), + Definition=definition_path, + MetricCollectionEnabled=self._METRIC_ENABLED) + + def get_vm_dvd_disk_paths(self, vm_name): + vm = self._lookup_vm_check(vm_name) + + settings = vm.associators( + wmi_result_class=self._VIRTUAL_SYSTEM_SETTING_DATA_CLASS)[0] + sasds = settings.associators( + wmi_result_class=self._STORAGE_ALLOC_SETTING_DATA_CLASS) + + dvd_paths = [sasd.HostResource[0] for sasd in sasds + if sasd.ResourceSubType == self._DVD_DISK_RES_SUB_TYPE] + + return dvd_paths + + def _get_instance_notes(self, vm_name): + vm = self._lookup_vm_check(vm_name) + vmsettings = self._get_vm_setting_data(vm) + return [note for note in vmsettings.Notes if note] diff --git a/oslo_windows/utils/volumeutils.py b/oslo_windows/utils/volumeutils.py new file mode 100644 index 00000000..fa5f2ff9 --- /dev/null +++ b/oslo_windows/utils/volumeutils.py @@ -0,0 +1,122 @@ +# Copyright 2012 Pedro Navarro Perez +# 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. + +""" +Helper methods for operations related to the management of volumes, +and storage repositories + +Official Microsoft iSCSI Initiator and iSCSI command line interface +documentation can be retrieved at: +http://www.microsoft.com/en-us/download/details.aspx?id=34750 +""" + +import re +import time + +from oslo_config import cfg +from oslo_log import log as logging +from six.moves import range + +from nova.i18n import _ +from nova import utils +from nova.virt.hyperv import basevolumeutils +from nova.virt.hyperv import vmutils + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +class VolumeUtils(basevolumeutils.BaseVolumeUtils): + + def __init__(self): + super(VolumeUtils, self).__init__() + + def execute(self, *args, **kwargs): + stdout_value, stderr_value = utils.execute(*args, **kwargs) + if stdout_value.find('The operation completed successfully') == -1: + raise vmutils.HyperVException(_('An error has occurred when ' + 'calling the iscsi initiator: %s') + % stdout_value) + return stdout_value + + def _login_target_portal(self, target_portal): + (target_address, + target_port) = utils.parse_server_string(target_portal) + + output = self.execute('iscsicli.exe', 'ListTargetPortals') + pattern = r'Address and Socket *: (.*)' + portals = [addr.split() for addr in re.findall(pattern, output)] + LOG.debug("Ensuring connection to portal: %s" % target_portal) + if [target_address, str(target_port)] in portals: + self.execute('iscsicli.exe', 'RefreshTargetPortal', + target_address, target_port) + else: + # Adding target portal to iscsi initiator. Sending targets + self.execute('iscsicli.exe', 'AddTargetPortal', + target_address, target_port, + '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', + '*', '*') + + def login_storage_target(self, target_lun, target_iqn, target_portal, + auth_username=None, auth_password=None): + """Ensure that the target is logged in.""" + + self._login_target_portal(target_portal) + # Listing targets + self.execute('iscsicli.exe', 'ListTargets') + + retry_count = CONF.hyperv.volume_attach_retry_count + + # If the target is not connected, at least two iterations are needed: + # one for performing the login and another one for checking if the + # target was logged in successfully. + if retry_count < 2: + retry_count = 2 + + for attempt in range(retry_count): + try: + session_info = self.execute('iscsicli.exe', 'SessionList') + if session_info.find(target_iqn) == -1: + # Sending login + self.execute('iscsicli.exe', 'qlogintarget', target_iqn, + auth_username, auth_password) + else: + return + except vmutils.HyperVException as exc: + LOG.debug("Attempt %(attempt)d to connect to target " + "%(target_iqn)s failed. Retrying. " + "Exceptipn: %(exc)s ", + {'target_iqn': target_iqn, + 'exc': exc, + 'attempt': attempt}) + time.sleep(CONF.hyperv.volume_attach_retry_interval) + + raise vmutils.HyperVException(_('Failed to login target %s') % + target_iqn) + + def logout_storage_target(self, target_iqn): + """Logs out storage target through its session id.""" + + sessions = self._conn_wmi.query("SELECT * FROM " + "MSiSCSIInitiator_SessionClass " + "WHERE TargetName='%s'" % target_iqn) + for session in sessions: + self.execute_log_out(session.SessionId) + + def execute_log_out(self, session_id): + """Executes log out of the session described by its session ID.""" + self.execute('iscsicli.exe', 'logouttarget', session_id) diff --git a/oslo_windows/utils/volumeutilsv2.py b/oslo_windows/utils/volumeutilsv2.py new file mode 100644 index 00000000..0a29e01d --- /dev/null +++ b/oslo_windows/utils/volumeutilsv2.py @@ -0,0 +1,132 @@ +# Copyright 2012 Pedro Navarro Perez +# 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. + +""" +Helper methods for operations related to the management of volumes +and storage repositories on Windows Server 2012 and above +""" +import sys +import time + +if sys.platform == 'win32': + import wmi + +from oslo_config import cfg +from oslo_log import log as logging +from six.moves import range + +from nova.i18n import _ +from nova import utils +from nova.virt.hyperv import basevolumeutils +from nova.virt.hyperv import vmutils + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class VolumeUtilsV2(basevolumeutils.BaseVolumeUtils): + _CHAP_AUTH_TYPE = 'ONEWAYCHAP' + + def __init__(self, host='.'): + super(VolumeUtilsV2, self).__init__(host) + + storage_namespace = '//%s/root/microsoft/windows/storage' % host + if sys.platform == 'win32': + self._conn_storage = wmi.WMI(moniker=storage_namespace) + + def _login_target_portal(self, target_portal): + (target_address, + target_port) = utils.parse_server_string(target_portal) + + # Checking if the portal is already connected. + portal = self._conn_storage.query("SELECT * FROM " + "MSFT_iSCSITargetPortal " + "WHERE TargetPortalAddress='%s' " + "AND TargetPortalPortNumber='%s'" + % (target_address, target_port)) + if portal: + portal[0].Update() + else: + # Adding target portal to iscsi initiator. Sending targets + portal = self._conn_storage.MSFT_iSCSITargetPortal + portal.New(TargetPortalAddress=target_address, + TargetPortalPortNumber=target_port) + + def login_storage_target(self, target_lun, target_iqn, target_portal, + auth_username=None, auth_password=None): + """Ensure that the target is logged in.""" + + self._login_target_portal(target_portal) + + retry_count = CONF.hyperv.volume_attach_retry_count + + # If the target is not connected, at least two iterations are needed: + # one for performing the login and another one for checking if the + # target was logged in successfully. + if retry_count < 2: + retry_count = 2 + + for attempt in range(retry_count): + target = self._conn_storage.query("SELECT * FROM MSFT_iSCSITarget " + "WHERE NodeAddress='%s' " % + target_iqn) + + if target and target[0].IsConnected: + if attempt == 0: + # The target was already connected but an update may be + # required + target[0].Update() + return + try: + target = self._conn_storage.MSFT_iSCSITarget + auth = {} + if auth_username and auth_password: + auth['AuthenticationType'] = self._CHAP_AUTH_TYPE + auth['ChapUsername'] = auth_username + auth['ChapSecret'] = auth_password + target.Connect(NodeAddress=target_iqn, + IsPersistent=True, **auth) + time.sleep(CONF.hyperv.volume_attach_retry_interval) + except wmi.x_wmi as exc: + LOG.debug("Attempt %(attempt)d to connect to target " + "%(target_iqn)s failed. Retrying. " + "WMI exception: %(exc)s " % + {'target_iqn': target_iqn, + 'exc': exc, + 'attempt': attempt}) + raise vmutils.HyperVException(_('Failed to login target %s') % + target_iqn) + + def logout_storage_target(self, target_iqn): + """Logs out storage target through its session id.""" + targets = self._conn_storage.MSFT_iSCSITarget(NodeAddress=target_iqn) + if targets: + target = targets[0] + if target.IsConnected: + sessions = self._conn_storage.MSFT_iSCSISession( + TargetNodeAddress=target_iqn) + + for session in sessions: + if session.IsPersistent: + session.Unregister() + + target.Disconnect() + + def execute_log_out(self, session_id): + sessions = self._conn_wmi.MSiSCSIInitiator_SessionClass( + SessionId=session_id) + if sessions: + self.logout_storage_target(sessions[0].TargetName) diff --git a/oslo_windows/utilsfactory.py b/oslo_windows/utilsfactory.py new file mode 100644 index 00000000..453993bd --- /dev/null +++ b/oslo_windows/utilsfactory.py @@ -0,0 +1,110 @@ +# 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. + +from oslo_config import cfg +from oslo_log import log as logging + +from nova.i18n import _ +from nova.virt.hyperv import hostutils +from nova.virt.hyperv import hostutilsv2 +from nova.virt.hyperv import livemigrationutils +from nova.virt.hyperv import networkutils +from nova.virt.hyperv import networkutilsv2 +from nova.virt.hyperv import pathutils +from nova.virt.hyperv import rdpconsoleutils +from nova.virt.hyperv import rdpconsoleutilsv2 +from nova.virt.hyperv import vhdutils +from nova.virt.hyperv import vhdutilsv2 +from nova.virt.hyperv import vmutils +from nova.virt.hyperv import vmutilsv2 +from nova.virt.hyperv import volumeutils +from nova.virt.hyperv import volumeutilsv2 + +hyper_opts = [ + cfg.BoolOpt('force_hyperv_utils_v1', + default=False, + help='Force V1 WMI utility classes'), + cfg.BoolOpt('force_volumeutils_v1', + default=False, + help='Force V1 volume utility class'), +] + +CONF = cfg.CONF +CONF.register_opts(hyper_opts, 'hyperv') + +LOG = logging.getLogger(__name__) + +utils = hostutils.HostUtils() + + +def _get_class(v1_class, v2_class, force_v1_flag): + # V2 classes are supported starting from Hyper-V Server 2012 and + # Windows Server 2012 (kernel version 6.2) + if not force_v1_flag and utils.check_min_windows_version(6, 2): + cls = v2_class + else: + cls = v1_class + LOG.debug("Loading class: %(module_name)s.%(class_name)s", + {'module_name': cls.__module__, 'class_name': cls.__name__}) + return cls + + +def _get_virt_utils_class(v1_class, v2_class): + # The "root/virtualization" WMI namespace is no longer supported on + # Windows Server / Hyper-V Server 2012 R2 / Windows 8.1 + # (kernel version 6.3) or above. + if (CONF.hyperv.force_hyperv_utils_v1 and + utils.check_min_windows_version(6, 3)): + raise vmutils.HyperVException( + _('The "force_hyperv_utils_v1" option cannot be set to "True" ' + 'on Windows Server / Hyper-V Server 2012 R2 or above as the WMI ' + '"root/virtualization" namespace is no longer supported.')) + return _get_class(v1_class, v2_class, CONF.hyperv.force_hyperv_utils_v1) + + +def get_vmutils(host='.'): + return _get_virt_utils_class(vmutils.VMUtils, vmutilsv2.VMUtilsV2)(host) + + +def get_vhdutils(): + return _get_virt_utils_class(vhdutils.VHDUtils, vhdutilsv2.VHDUtilsV2)() + + +def get_networkutils(): + return _get_virt_utils_class(networkutils.NetworkUtils, + networkutilsv2.NetworkUtilsV2)() + + +def get_hostutils(): + return _get_virt_utils_class(hostutils.HostUtils, + hostutilsv2.HostUtilsV2)() + + +def get_pathutils(): + return pathutils.PathUtils() + + +def get_volumeutils(): + return _get_class(volumeutils.VolumeUtils, volumeutilsv2.VolumeUtilsV2, + CONF.hyperv.force_volumeutils_v1)() + + +def get_livemigrationutils(): + return livemigrationutils.LiveMigrationUtils() + + +def get_rdpconsoleutils(): + return _get_virt_utils_class(rdpconsoleutils.RDPConsoleUtils, + rdpconsoleutilsv2.RDPConsoleUtilsV2)()