diff --git a/nova/tests/unit/virt/hyperv/test_vmops.py b/nova/tests/unit/virt/hyperv/test_vmops.py index 33a25b7d8a6a..50b5df8a60c5 100644 --- a/nova/tests/unit/virt/hyperv/test_vmops.py +++ b/nova/tests/unit/virt/hyperv/test_vmops.py @@ -24,6 +24,7 @@ import unittest2 from nova import exception from nova import objects from nova.tests.unit import fake_instance +from nova.tests.unit.objects import test_virtual_interface from nova.tests.unit.virt.hyperv import test_base from nova.virt import hardware from nova.virt.hyperv import constants @@ -46,6 +47,9 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): FAKE_UUID = '4f54fb69-d3a2-45b7-bb9b-b6e6b3d893b3' FAKE_LOG = 'fake_log' + _WIN_VERSION_6_3 = '6.3.0' + _WIN_VERSION_10 = '10.0' + ISO9660 = 'iso9660' _FAKE_CONFIGDRIVE_PATH = 'C:/fake_instance_dir/configdrive.vhd' @@ -629,13 +633,17 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_disconnect_volumes): mock_instance = fake_instance.fake_instance_obj(self.context) self._vmops._vmutils.vm_exists.return_value = True + self._vmops._vif_driver = mock.MagicMock() self._vmops.destroy(instance=mock_instance, + network_info=[mock.sentinel.fake_vif], block_device_info=mock.sentinel.FAKE_BD_INFO) self._vmops._vmutils.vm_exists.assert_called_with( mock_instance.name) mock_power_off.assert_called_once_with(mock_instance) + self._vmops._vif_driver.unplug.assert_called_once_with( + mock_instance, mock.sentinel.fake_vif) self._vmops._vmutils.destroy_vm.assert_called_once_with( mock_instance.name) mock_disconnect_volumes.assert_called_once_with( @@ -1080,3 +1088,96 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock.sentinel.FAKE_DEST_PATH), mock.call(mock.sentinel.FAKE_DVD_PATH2, mock.sentinel.FAKE_DEST_PATH)) + + @mock.patch.object(vmops.VMOps, '_get_vm_state') + def test_check_hotplug_available_vm_disabled(self, mock_get_vm_state): + fake_vm = fake_instance.fake_instance_obj(self.context) + mock_get_vm_state.return_value = constants.HYPERV_VM_STATE_DISABLED + + result = self._vmops._check_hotplug_available(fake_vm) + + self.assertTrue(result) + mock_get_vm_state.assert_called_once_with(fake_vm.name) + self.assertFalse( + self._vmops._hostutils.check_min_windows_version.called) + self.assertFalse(self._vmops._vmutils.get_vm_generation.called) + + @mock.patch.object(vmops.VMOps, '_get_vm_state') + def _test_check_hotplug_available( + self, mock_get_vm_state, expected_result=False, + vm_gen=constants.VM_GEN_2, windows_version=_WIN_VERSION_10): + + fake_vm = fake_instance.fake_instance_obj(self.context) + mock_get_vm_state.return_value = constants.HYPERV_VM_STATE_ENABLED + self._vmops._vmutils.get_vm_generation.return_value = vm_gen + fake_check_win_vers = self._vmops._hostutils.check_min_windows_version + fake_check_win_vers.return_value = ( + windows_version == self._WIN_VERSION_10) + + result = self._vmops._check_hotplug_available(fake_vm) + + self.assertEqual(expected_result, result) + mock_get_vm_state.assert_called_once_with(fake_vm.name) + fake_check_win_vers.assert_called_once_with(10, 0) + + def test_check_if_hotplug_available(self): + self._test_check_hotplug_available(expected_result=True) + + def test_check_if_hotplug_available_gen1(self): + self._test_check_hotplug_available( + expected_result=False, vm_gen=constants.VM_GEN_1) + + def test_check_if_hotplug_available_win_6_3(self): + self._test_check_hotplug_available( + expected_result=False, windows_version=self._WIN_VERSION_6_3) + + @mock.patch.object(vmops.VMOps, '_check_hotplug_available') + def test_attach_interface(self, mock_check_hotplug_available): + mock_check_hotplug_available.return_value = True + fake_vm = fake_instance.fake_instance_obj(self.context) + fake_vif = test_virtual_interface.fake_vif + self._vmops._vif_driver = mock.MagicMock() + + self._vmops.attach_interface(fake_vm, fake_vif) + + mock_check_hotplug_available.assert_called_once_with(fake_vm) + self._vmops._vif_driver.plug.assert_called_once_with( + fake_vm, fake_vif) + self._vmops._vmutils.create_nic.assert_called_once_with( + fake_vm.name, fake_vif['id'], fake_vif['address']) + + @mock.patch.object(vmops.VMOps, '_check_hotplug_available') + def test_attach_interface_failed(self, mock_check_hotplug_available): + mock_check_hotplug_available.return_value = False + self.assertRaises(exception.InterfaceAttachFailed, + self._vmops.attach_interface, + mock.MagicMock(), mock.sentinel.fake_vif) + + @mock.patch.object(vmops.VMOps, '_check_hotplug_available') + def test_detach_interface(self, mock_check_hotplug_available): + mock_check_hotplug_available.return_value = True + fake_vm = fake_instance.fake_instance_obj(self.context) + fake_vif = test_virtual_interface.fake_vif + self._vmops._vif_driver = mock.MagicMock() + + self._vmops.detach_interface(fake_vm, fake_vif) + + mock_check_hotplug_available.assert_called_once_with(fake_vm) + self._vmops._vif_driver.unplug.assert_called_once_with( + fake_vm, fake_vif) + self._vmops._vmutils.destroy_nic.assert_called_once_with( + fake_vm.name, fake_vif['id']) + + @mock.patch.object(vmops.VMOps, '_check_hotplug_available') + def test_detach_interface_failed(self, mock_check_hotplug_available): + mock_check_hotplug_available.return_value = False + self.assertRaises(exception.InterfaceDetachFailed, + self._vmops.detach_interface, + mock.MagicMock(), mock.sentinel.fake_vif) + + @mock.patch.object(vmops.VMOps, '_check_hotplug_available') + def test_detach_interface_missing_instance(self, mock_check_hotplug): + mock_check_hotplug.side_effect = exception.NotFound + self.assertRaises(exception.InterfaceDetachFailed, + self._vmops.detach_interface, + mock.MagicMock(), mock.sentinel.fake_vif) diff --git a/nova/tests/unit/virt/hyperv/test_vmutils.py b/nova/tests/unit/virt/hyperv/test_vmutils.py index c97db4b329e8..eac39c17057d 100644 --- a/nova/tests/unit/virt/hyperv/test_vmutils.py +++ b/nova/tests/unit/virt/hyperv/test_vmutils.py @@ -396,6 +396,17 @@ class VMUtilsTestCase(test.NoDBTestCase): mock_add_virt_res.assert_called_with(mock_nic, self._FAKE_VM_PATH) + @mock.patch.object(vmutils.VMUtils, '_get_nic_data_by_name') + def test_destroy_nic(self, mock_get_nic_data_by_name): + self._lookup_vm() + fake_nic_data = mock_get_nic_data_by_name.return_value + with mock.patch.object(self._vmutils, + '_remove_virt_resource') as mock_rem_virt_res: + self._vmutils.destroy_nic(self._FAKE_VM_NAME, + mock.sentinel.FAKE_NIC_NAME) + mock_rem_virt_res.assert_called_once_with(fake_nic_data, + self._FAKE_VM_PATH) + def test_set_vm_state(self): mock_vm = self._lookup_vm() mock_vm.RequestStateChange.return_value = ( @@ -869,6 +880,10 @@ class VMUtilsTestCase(test.NoDBTestCase): self.assertEqual(watcher.return_value, listener) + def test_get_vm_generation_gen1(self): + ret = self._vmutils.get_vm_generation(mock.sentinel.FAKE_VM_NAME) + self.assertEqual(constants.VM_GEN_1, ret) + def test_stop_vm_jobs(self): mock_vm = self._lookup_vm() diff --git a/nova/tests/unit/virt/hyperv/test_vmutilsv2.py b/nova/tests/unit/virt/hyperv/test_vmutilsv2.py index 9827fa11952c..4d8b5ff65b08 100644 --- a/nova/tests/unit/virt/hyperv/test_vmutilsv2.py +++ b/nova/tests/unit/virt/hyperv/test_vmutilsv2.py @@ -275,3 +275,20 @@ class VMUtilsV2TestCase(test_vmutils.VMUtilsTestCase): ret_val = self._vmutils.get_vm_dvd_disk_paths(self._FAKE_VM_NAME) self.assertEqual(mock.sentinel.FAKE_DVD_PATH1, ret_val[0]) + + @mock.patch.object(vmutilsv2.VMUtilsV2, '_get_vm_setting_data') + def _test_get_vm_generation(self, vm_gen, mock_get_vm_setting_data): + self._lookup_vm() + vm_gen_string = "Microsoft:Hyper-V:SubType:" + str(vm_gen) + mock_vssd = mock.MagicMock(VirtualSystemSubType=vm_gen_string) + mock_get_vm_setting_data.return_value = mock_vssd + + ret = self._vmutils.get_vm_generation(mock.sentinel.FAKE_VM_NAME) + + self.assertEqual(vm_gen, ret) + + def test_get_vm_generation_gen1(self): + self._test_get_vm_generation(constants.VM_GEN_1) + + def test_get_vm_generation_gen2(self): + self._test_get_vm_generation(constants.VM_GEN_2) diff --git a/nova/virt/hyperv/driver.py b/nova/virt/hyperv/driver.py index 12128bef7f4c..26fab7d9a6d1 100644 --- a/nova/virt/hyperv/driver.py +++ b/nova/virt/hyperv/driver.py @@ -267,3 +267,9 @@ class HyperVDriver(driver.ComputeDriver): def get_console_output(self, context, instance): return self._vmops.get_console_output(instance) + + def attach_interface(self, instance, image_meta, vif): + return self._vmops.attach_interface(instance, vif) + + def detach_interface(self, instance, vif): + return self._vmops.detach_interface(instance, vif) diff --git a/nova/virt/hyperv/vmops.py b/nova/virt/hyperv/vmops.py index 72f3c5bc3c17..4a00049f7676 100644 --- a/nova/virt/hyperv/vmops.py +++ b/nova/virt/hyperv/vmops.py @@ -448,6 +448,10 @@ class VMOps(object): self._vmutils.stop_vm_jobs(instance_name) self.power_off(instance) + if network_info: + for vif in network_info: + self._vif_driver.unplug(instance, vif) + self._vmutils.destroy_vm(instance_name) self._volumeops.disconnect_volumes(block_device_info) else: @@ -717,3 +721,54 @@ class VMOps(object): vm_name, remote_server=dest_host) for path in dvd_disk_paths: self._pathutils.copyfile(path, dest_path) + + def _check_hotplug_available(self, instance): + """Check whether attaching an interface is possible for the given + instance. + + :returns: True if attaching / detaching interfaces is possible for the + given instance. + """ + vm_state = self._get_vm_state(instance.name) + if vm_state == constants.HYPERV_VM_STATE_DISABLED: + # can attach / detach interface to stopped VMs. + return True + + if not self._hostutils.check_min_windows_version(10, 0): + # TODO(claudiub): add set log level to error after string freeze. + LOG.debug("vNIC hot plugging is supported only in newer " + "versions than Windows Hyper-V / Server 2012 R2.") + return False + + if (self._vmutils.get_vm_generation(instance.name) == + constants.VM_GEN_1): + # TODO(claudiub): add set log level to error after string freeze. + LOG.debug("Cannot hot plug vNIC to a first generation VM.", + instance=instance) + return False + + return True + + def attach_interface(self, instance, vif): + if not self._check_hotplug_available(instance): + raise exception.InterfaceAttachFailed(instance_uuid=instance.uuid) + + LOG.debug('Attaching vif: %s', vif['id'], instance=instance) + self._vmutils.create_nic(instance.name, vif['id'], vif['address']) + self._vif_driver.plug(instance, vif) + + def detach_interface(self, instance, vif): + try: + if not self._check_hotplug_available(instance): + raise exception.InterfaceDetachFailed( + instance_uuid=instance.uuid) + + LOG.debug('Detaching vif: %s', vif['id'], instance=instance) + self._vif_driver.unplug(instance, vif) + self._vmutils.destroy_nic(instance.name, vif['id']) + except exception.NotFound: + # TODO(claudiub): add set log level to error after string freeze. + LOG.debug("Instance not found during detach interface. It " + "might have been destroyed beforehand.", + instance=instance) + raise exception.InterfaceDetachFailed(instance_uuid=instance.uuid) diff --git a/nova/virt/hyperv/vmutils.py b/nova/virt/hyperv/vmutils.py index b12754e75deb..962414909e40 100644 --- a/nova/virt/hyperv/vmutils.py +++ b/nova/virt/hyperv/vmutils.py @@ -494,6 +494,17 @@ class VMUtils(object): vm = self._lookup_vm_check(vm_name) self._modify_virt_resource(nic_data, vm.path_()) + def destroy_nic(self, vm_name, nic_name): + """Destroys the NIC with the given nic_name from the given VM. + + :param vm_name: The name of the VM which has the NIC to be destroyed. + :param nic_name: The NIC's ElementName. + """ + nic_data = self._get_nic_data_by_name(nic_name) + + vm = self._lookup_vm_check(vm_name) + self._remove_virt_resource(nic_data, vm.path_()) + def _get_nic_data_by_name(self, name): return self._conn.Msvm_SyntheticEthernetPortSettingData( ElementName=name)[0] @@ -841,6 +852,9 @@ class VMUtils(object): return self._enabled_states_map.get(vm_enabled_state, constants.HYPERV_VM_STATE_OTHER) + def get_vm_generation(self, vm_name): + return constants.VM_GEN_1 + def stop_vm_jobs(self, vm_name): vm = self._lookup_vm_check(vm_name) vm_jobs = vm.associators(wmi_result_class=self._CONCRETE_JOB_CLASS) diff --git a/nova/virt/hyperv/vmutilsv2.py b/nova/virt/hyperv/vmutilsv2.py index 4fa8cfa87729..b647b8fb4a1e 100644 --- a/nova/virt/hyperv/vmutilsv2.py +++ b/nova/virt/hyperv/vmutilsv2.py @@ -47,6 +47,7 @@ class VMUtilsV2(vmutils.VMUtils): _SCSI_CTRL_RES_SUB_TYPE = 'Microsoft:Hyper-V:Synthetic SCSI Controller' _SERIAL_PORT_RES_SUB_TYPE = 'Microsoft:Hyper-V:Serial Port' + _VIRTUAL_SYSTEM_SUBTYPE = 'VirtualSystemSubType' _VIRTUAL_SYSTEM_TYPE_REALIZED = 'Microsoft:Hyper-V:System:Realized' _VIRTUAL_SYSTEM_SUBTYPE_GEN2 = 'Microsoft:Hyper-V:SubType:2' @@ -335,3 +336,11 @@ class VMUtilsV2(vmutils.VMUtils): vm = self._lookup_vm_check(vm_name) vmsettings = self._get_vm_setting_data(vm) return [note for note in vmsettings.Notes if note] + + def get_vm_generation(self, vm_name): + vm = self._lookup_vm_check(vm_name) + vssd = self._get_vm_setting_data(vm) + if hasattr(vssd, self._VIRTUAL_SYSTEM_SUBTYPE): + # expected format: 'Microsoft:Hyper-V:SubType:2' + return int(vssd.VirtualSystemSubType.split(':')[-1]) + return constants.VM_GEN_1