From ed525cc403d98e26d4c88fdc00d476ada1eb9ced Mon Sep 17 00:00:00 2001 From: esberglu Date: Wed, 21 Feb 2018 17:33:14 -0600 Subject: [PATCH] PowerVM Driver: Network interface attach/detach This adds the ability to hotplug network interfaces for the powervm virt driver. Blueprint: powervm-network-hotplug Change-Id: I78b94c9731c35e3291d46b9bf9f5554e21c2429e --- doc/source/user/support-matrix.ini | 4 +- nova/tests/unit/virt/powervm/tasks/test_vm.py | 13 +++- nova/tests/unit/virt/powervm/test_driver.py | 62 ++++++++++++++++++- nova/virt/powervm/driver.py | 58 ++++++++++++++++- nova/virt/powervm/tasks/vm.py | 22 ++++++- ...vm-hotplug-interface-e54c84ebc039b18c.yaml | 5 ++ 6 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/powervm-hotplug-interface-e54c84ebc039b18c.yaml diff --git a/doc/source/user/support-matrix.ini b/doc/source/user/support-matrix.ini index f452a41688da..b2aa0fe20ca2 100644 --- a/doc/source/user/support-matrix.ini +++ b/doc/source/user/support-matrix.ini @@ -208,7 +208,7 @@ driver-notes-hyperv=Works without issue if instance is off. When driver-impl-ironic=complete driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=complete -driver-impl-powervm=missing +driver-impl-powervm=complete [operation.attach-tagged-interface] title=Attach tagged virtual network interface to instance @@ -252,7 +252,7 @@ driver-notes-hyperv=Works without issue if instance is off. When driver-impl-ironic=complete driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=complete -driver-impl-powervm=missing +driver-impl-powervm=complete [operation.maintenance-mode] title=Set the host in a maintenance mode diff --git a/nova/tests/unit/virt/powervm/tasks/test_vm.py b/nova/tests/unit/virt/powervm/tasks/test_vm.py index 2a5988b723a7..fc68646acf77 100644 --- a/nova/tests/unit/virt/powervm/tasks/test_vm.py +++ b/nova/tests/unit/virt/powervm/tasks/test_vm.py @@ -1,4 +1,4 @@ -# Copyright 2015, 2017 IBM Corp. +# Copyright 2015, 2018 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 @@ -28,6 +28,17 @@ class TestVMTasks(test.NoDBTestCase): self.apt = mock.Mock() self.instance = mock.Mock() + @mock.patch('nova.virt.powervm.vm.get_instance_wrapper', autospec=True) + def test_get(self, mock_get_wrap): + get = tf_vm.Get(self.apt, self.instance) + get.execute() + mock_get_wrap.assert_called_once_with(self.apt, self.instance) + + # Validate args on taskflow.task.Task instantiation + with mock.patch('taskflow.task.Task.__init__') as tf: + tf_vm.Get(self.apt, self.instance) + tf.assert_called_once_with(name='get_vm', provides='lpar_wrap') + @mock.patch('pypowervm.tasks.storage.add_lpar_storage_scrub_tasks', autospec=True) @mock.patch('nova.virt.powervm.vm.create_lpar') diff --git a/nova/tests/unit/virt/powervm/test_driver.py b/nova/tests/unit/virt/powervm/test_driver.py index 960de398d194..ed3c85e00f62 100644 --- a/nova/tests/unit/virt/powervm/test_driver.py +++ b/nova/tests/unit/virt/powervm/test_driver.py @@ -22,6 +22,7 @@ from pypowervm.helpers import log_helper as pvm_hlp_log from pypowervm.helpers import vios_busy as pvm_hlp_vbusy from pypowervm.utils import transaction as pvm_tx from pypowervm.wrappers import virtual_io_server as pvm_vios +import six from nova import exception from nova import test @@ -61,7 +62,7 @@ class TestPowerVMDriver(test.NoDBTestCase): self.assertFalse(self.drv.capabilities['supports_recreate']) self.assertFalse( self.drv.capabilities['supports_migrate_to_same_host']) - self.assertFalse(self.drv.capabilities['supports_attach_interface']) + self.assertTrue(self.drv.capabilities['supports_attach_interface']) self.assertFalse(self.drv.capabilities['supports_device_tagging']) self.assertFalse( self.drv.capabilities['supports_tagged_attach_interface']) @@ -316,6 +317,65 @@ class TestPowerVMDriver(test.NoDBTestCase): self.drv.reboot('context', inst, 'network_info', 'HARD') mock_reboot.assert_called_once_with(self.adp, inst, True) + @mock.patch('nova.virt.powervm.driver.PowerVMDriver.plug_vifs') + def test_attach_interface(self, mock_plug_vifs): + self.drv.attach_interface('context', 'inst', 'image_meta', 'vif') + mock_plug_vifs.assert_called_once_with('inst', ['vif']) + + @mock.patch('nova.virt.powervm.driver.PowerVMDriver.unplug_vifs') + def test_detach_interface(self, mock_unplug_vifs): + self.drv.detach_interface('context', 'inst', 'vif') + mock_unplug_vifs.assert_called_once_with('inst', ['vif']) + + @mock.patch('nova.virt.powervm.tasks.vm.Get', autospec=True) + @mock.patch('nova.virt.powervm.tasks.base.run', autospec=True) + @mock.patch('nova.virt.powervm.tasks.network.PlugVifs', autospec=True) + @mock.patch('taskflow.patterns.linear_flow.Flow', autospec=True) + def test_plug_vifs(self, mock_tf, mock_plug_vifs, mock_tf_run, mock_get): + # Successful plug + mock_inst = mock.Mock() + self.drv.plug_vifs(mock_inst, 'net_info') + mock_get.assert_called_once_with(self.adp, mock_inst) + mock_plug_vifs.assert_called_once_with( + self.drv.virtapi, self.adp, mock_inst, 'net_info') + add_calls = [mock.call(mock_get.return_value), + mock.call(mock_plug_vifs.return_value)] + mock_tf.return_value.add.assert_has_calls(add_calls) + mock_tf_run.assert_called_once_with( + mock_tf.return_value, instance=mock_inst) + + # InstanceNotFound and generic exception both raise + mock_tf_run.side_effect = exception.InstanceNotFound('id') + exc = self.assertRaises(exception.VirtualInterfacePlugException, + self.drv.plug_vifs, mock_inst, 'net_info') + self.assertIn('instance', six.text_type(exc)) + mock_tf_run.side_effect = Exception + exc = self.assertRaises(exception.VirtualInterfacePlugException, + self.drv.plug_vifs, mock_inst, 'net_info') + self.assertIn('unexpected', six.text_type(exc)) + + @mock.patch('nova.virt.powervm.tasks.base.run', autospec=True) + @mock.patch('nova.virt.powervm.tasks.network.UnplugVifs', autospec=True) + @mock.patch('taskflow.patterns.linear_flow.Flow', autospec=True) + def test_unplug_vifs(self, mock_tf, mock_unplug_vifs, mock_tf_run): + # Successful unplug + mock_inst = mock.Mock() + self.drv.unplug_vifs(mock_inst, 'net_info') + mock_unplug_vifs.assert_called_once_with(self.adp, mock_inst, + 'net_info') + mock_tf.return_value.add.assert_called_once_with( + mock_unplug_vifs.return_value) + mock_tf_run.assert_called_once_with(mock_tf.return_value, mock_inst) + + # InstanceNotFound should pass + mock_tf_run.side_effect = exception.InstanceNotFound(instance_id='1') + self.drv.unplug_vifs(mock_inst, 'net_info') + + # Raise InterfaceDetachFailed otherwise + mock_tf_run.side_effect = Exception + self.assertRaises(exception.InterfaceDetachFailed, + self.drv.unplug_vifs, mock_inst, 'net_info') + @mock.patch('pypowervm.tasks.vterm.open_remotable_vnc_vterm', autospec=True) @mock.patch('nova.virt.powervm.vm.get_pvm_uuid', diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py index ceb0f0841cff..f2b40061ba8e 100644 --- a/nova/virt/powervm/driver.py +++ b/nova/virt/powervm/driver.py @@ -30,6 +30,7 @@ from taskflow.patterns import linear_flow as tf_lf from nova import conf as cfg from nova.console import type as console_type from nova import exception as exc +from nova.i18n import _ from nova import image from nova.virt import configdrive from nova.virt import driver @@ -58,7 +59,7 @@ class PowerVMDriver(driver.ComputeDriver): 'has_imagecache': False, 'supports_recreate': False, 'supports_migrate_to_same_host': False, - 'supports_attach_interface': False, + 'supports_attach_interface': True, 'supports_device_tagging': False, 'supports_tagged_attach_interface': False, 'supports_tagged_attach_volume': False, @@ -341,6 +342,61 @@ class PowerVMDriver(driver.ComputeDriver): # pypowervm exceptions are sufficient to indicate real failure. # Otherwise, pypowervm thinks the instance is up. + def attach_interface(self, context, instance, image_meta, vif): + """Attach an interface to the instance.""" + self.plug_vifs(instance, [vif]) + + def detach_interface(self, context, instance, vif): + """Detach an interface from the instance.""" + self.unplug_vifs(instance, [vif]) + + def plug_vifs(self, instance, network_info): + """Plug VIFs into networks.""" + self._log_operation('plug_vifs', instance) + + # Define the flow + flow = tf_lf.Flow("plug_vifs") + + # Get the LPAR Wrapper + flow.add(tf_vm.Get(self.adapter, instance)) + + # Run the attach + flow.add(tf_net.PlugVifs(self.virtapi, self.adapter, instance, + network_info)) + + # Run the flow + try: + tf_base.run(flow, instance=instance) + except exc.InstanceNotFound: + raise exc.VirtualInterfacePlugException( + _("Plug vif failed because instance %s was not found.") + % instance.name) + except Exception: + LOG.exception("PowerVM error plugging vifs.", instance=instance) + raise exc.VirtualInterfacePlugException( + _("Plug vif failed because of an unexpected error.")) + + def unplug_vifs(self, instance, network_info): + """Unplug VIFs from networks.""" + self._log_operation('unplug_vifs', instance) + + # Define the flow + flow = tf_lf.Flow("unplug_vifs") + + # Run the detach + flow.add(tf_net.UnplugVifs(self.adapter, instance, network_info)) + + # Run the flow + try: + tf_base.run(flow, instance=instance) + except exc.InstanceNotFound: + LOG.warning('VM was not found during unplug operation as it is ' + 'already possibly deleted.', instance=instance) + except Exception: + LOG.exception("PowerVM error trying to unplug vifs.", + instance=instance) + raise exc.InterfaceDetachFailed(instance_uuid=instance.uuid) + def get_vnc_console(self, context, instance): """Get connection info for a vnc console. diff --git a/nova/virt/powervm/tasks/vm.py b/nova/virt/powervm/tasks/vm.py index 58f33012c31b..59cd1a2377bb 100644 --- a/nova/virt/powervm/tasks/vm.py +++ b/nova/virt/powervm/tasks/vm.py @@ -1,4 +1,4 @@ -# Copyright 2015, 2017 IBM Corp. +# Copyright 2015, 2018 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 @@ -24,6 +24,26 @@ from nova.virt.powervm import vm LOG = logging.getLogger(__name__) +class Get(task.Task): + + """The task for getting a VM entry.""" + + def __init__(self, adapter, instance): + """Creates the Task for getting a VM entry. + + Provides the 'lpar_wrap' for other tasks. + + :param adapter: The adapter for the pypowervm API + :param instance: The nova instance. + """ + super(Get, self).__init__(name='get_vm', provides='lpar_wrap') + self.adapter = adapter + self.instance = instance + + def execute(self): + return vm.get_instance_wrapper(self.adapter, self.instance) + + class Create(task.Task): """The task for creating a VM.""" diff --git a/releasenotes/notes/powervm-hotplug-interface-e54c84ebc039b18c.yaml b/releasenotes/notes/powervm-hotplug-interface-e54c84ebc039b18c.yaml new file mode 100644 index 000000000000..c88e113bf082 --- /dev/null +++ b/releasenotes/notes/powervm-hotplug-interface-e54c84ebc039b18c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The PowerVM driver now supports hot plugging/unplugging of network + interfaces.