From a31d6b02fc4f9f89f5fa28b650f3f1864ffe01e9 Mon Sep 17 00:00:00 2001 From: Qing Wu Wang Date: Thu, 19 Jan 2017 03:39:16 -0600 Subject: [PATCH] PowerVM driver: ovs vif This change set adds Open vSwitch VIF support for the PowerVM virt driver. Change-Id: If23aeb890c4365014a9f1262647611162f981f12 Partially-Implements: blueprint powervm-nova-it-compute-driver --- doc/source/user/support-matrix.ini | 10 +- .../unit/virt/powervm/tasks/test_network.py | 306 ++++++++++++++++++ .../unit/virt/powervm/tasks/test_storage.py | 10 +- nova/tests/unit/virt/powervm/test_driver.py | 29 +- nova/tests/unit/virt/powervm/test_media.py | 32 ++ nova/tests/unit/virt/powervm/test_vif.py | 225 +++++++++++++ nova/tests/unit/virt/powervm/test_vm.py | 28 ++ nova/virt/powervm/driver.py | 20 +- nova/virt/powervm/media.py | 44 ++- nova/virt/powervm/tasks/network.py | 259 +++++++++++++++ nova/virt/powervm/tasks/storage.py | 12 +- nova/virt/powervm/tasks/vm.py | 2 +- nova/virt/powervm/vif.py | 247 ++++++++++++++ nova/virt/powervm/vm.py | 33 ++ 14 files changed, 1231 insertions(+), 26 deletions(-) create mode 100644 nova/tests/unit/virt/powervm/tasks/test_network.py create mode 100644 nova/tests/unit/virt/powervm/test_vif.py create mode 100644 nova/virt/powervm/tasks/network.py create mode 100644 nova/virt/powervm/vif.py diff --git a/doc/source/user/support-matrix.ini b/doc/source/user/support-matrix.ini index 3bd6f3783238..3377c8d17a71 100644 --- a/doc/source/user/support-matrix.ini +++ b/doc/source/user/support-matrix.ini @@ -1180,7 +1180,7 @@ driver-impl-hyperv=missing driver-impl-ironic=missing driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=complete -driver-impl-powervm=missing +driver-impl-powervm=complete [networking.routing] title=Network routing @@ -1199,7 +1199,7 @@ driver-impl-hyperv=missing driver-impl-ironic=complete driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=complete -driver-impl-powervm=missing +driver-impl-powervm=complete [networking.securitygroups] title=Network security groups @@ -1225,7 +1225,7 @@ driver-impl-hyperv=missing driver-impl-ironic=missing driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=complete -driver-impl-powervm=missing +driver-impl-powervm=complete [networking.topology.flat] title=Flat networking @@ -1247,7 +1247,7 @@ driver-impl-hyperv=complete driver-impl-ironic=complete driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=complete -driver-impl-powervm=missing +driver-impl-powervm=complete [networking.topology.vlan] title=VLAN networking @@ -1268,7 +1268,7 @@ driver-impl-hyperv=missing driver-impl-ironic=missing driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=complete -driver-impl-powervm=missing +driver-impl-powervm=complete [operation.uefi-boot] title=uefi boot diff --git a/nova/tests/unit/virt/powervm/tasks/test_network.py b/nova/tests/unit/virt/powervm/tasks/test_network.py new file mode 100644 index 000000000000..08bceffca7fb --- /dev/null +++ b/nova/tests/unit/virt/powervm/tasks/test_network.py @@ -0,0 +1,306 @@ +# Copyright 2015, 2017 IBM Corp. +# +# 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 copy + +import eventlet +import mock +from pypowervm.wrappers import network as pvm_net + +from nova import exception +from nova import test +from nova.tests.unit.virt import powervm +from nova.virt.powervm.tasks import network as tf_net + + +def cna(mac): + """Builds a mock Client Network Adapter for unit tests.""" + return mock.MagicMock(mac=mac, vswitch_uri='fake_href') + + +class TestNetwork(test.NoDBTestCase): + def setUp(self): + super(TestNetwork, self).setUp() + self.flags(host='host1') + self.apt = mock.Mock() + + self.mock_lpar_wrap = mock.MagicMock() + self.mock_lpar_wrap.can_modify_io.return_value = True, None + + @mock.patch('nova.virt.powervm.vm.get_instance_wrapper') + @mock.patch('nova.virt.powervm.vif.unplug') + @mock.patch('nova.virt.powervm.vm.get_cnas') + def test_unplug_vifs(self, mock_vm_get, mock_unplug, mock_get_wrap): + """Tests that a delete of the vif can be done.""" + inst = powervm.TEST_INSTANCE + + # Mock up the CNA responses. + cnas = [cna('AABBCCDDEEFF'), cna('AABBCCDDEE11'), cna('AABBCCDDEE22')] + mock_vm_get.return_value = cnas + + # Mock up the network info. This also validates that they will be + # sanitized to upper case. + net_info = [ + {'address': 'aa:bb:cc:dd:ee:ff'}, {'address': 'aa:bb:cc:dd:ee:22'}, + {'address': 'aa:bb:cc:dd:ee:33'} + ] + + # Mock out the instance wrapper + mock_get_wrap.return_value = self.mock_lpar_wrap + + # Mock out the vif driver + def validate_unplug(adapter, instance, vif, cna_w_list=None): + self.assertEqual(adapter, self.apt) + self.assertEqual(instance, inst) + self.assertIn(vif, net_info) + self.assertEqual(cna_w_list, cnas) + + mock_unplug.side_effect = validate_unplug + + # Run method + p_vifs = tf_net.UnplugVifs(self.apt, inst, net_info) + p_vifs.execute() + + # Make sure the unplug was invoked, so that we know that the validation + # code was called + self.assertEqual(3, mock_unplug.call_count) + + @mock.patch('nova.virt.powervm.vm.get_instance_wrapper') + def test_unplug_vifs_invalid_state(self, mock_get_wrap): + """Tests that the delete raises an exception if bad VM state.""" + inst = powervm.TEST_INSTANCE + + # Mock out the instance wrapper + mock_get_wrap.return_value = self.mock_lpar_wrap + + # Mock that the state is incorrect + self.mock_lpar_wrap.can_modify_io.return_value = False, 'bad' + + # Run method + p_vifs = tf_net.UnplugVifs(self.apt, inst, mock.Mock()) + self.assertRaises(exception.VirtualInterfaceUnplugException, + p_vifs.execute) + + @mock.patch('nova.virt.powervm.vif.plug') + @mock.patch('nova.virt.powervm.vm.get_cnas') + def test_plug_vifs_rmc(self, mock_cna_get, mock_plug): + """Tests that a crt vif can be done with secure RMC.""" + inst = powervm.TEST_INSTANCE + + # Mock up the CNA response. One should already exist, the other + # should not. + pre_cnas = [cna('AABBCCDDEEFF'), cna('AABBCCDDEE11')] + mock_cna_get.return_value = copy.deepcopy(pre_cnas) + + # Mock up the network info. This also validates that they will be + # sanitized to upper case. + net_info = [ + {'address': 'aa:bb:cc:dd:ee:ff', 'vnic_type': 'normal'}, + {'address': 'aa:bb:cc:dd:ee:22', 'vnic_type': 'normal'}, + ] + + # First run the CNA update, then the CNA create. + mock_new_cna = mock.Mock(spec=pvm_net.CNA) + mock_plug.side_effect = ['upd_cna', mock_new_cna] + + # Run method + p_vifs = tf_net.PlugVifs(mock.MagicMock(), self.apt, inst, net_info) + + all_cnas = p_vifs.execute(self.mock_lpar_wrap) + + # new vif should be created twice. + mock_plug.assert_any_call(self.apt, inst, net_info[0], new_vif=False) + mock_plug.assert_any_call(self.apt, inst, net_info[1], new_vif=True) + + # The Task provides the list of original CNAs plus only CNAs that were + # created. + self.assertEqual(pre_cnas + [mock_new_cna], all_cnas) + + @mock.patch('nova.virt.powervm.vif.plug') + @mock.patch('nova.virt.powervm.vm.get_cnas') + def test_plug_vifs_rmc_no_create(self, mock_vm_get, mock_plug): + """Verifies if no creates are needed, none are done.""" + inst = powervm.TEST_INSTANCE + + # Mock up the CNA response. Both should already exist. + mock_vm_get.return_value = [cna('AABBCCDDEEFF'), cna('AABBCCDDEE11')] + + # Mock up the network info. This also validates that they will be + # sanitized to upper case. This also validates that we don't call + # get_vnics if no nets have vnic_type 'direct'. + net_info = [ + {'address': 'aa:bb:cc:dd:ee:ff', 'vnic_type': 'normal'}, + {'address': 'aa:bb:cc:dd:ee:11', 'vnic_type': 'normal'} + ] + + # Run method + p_vifs = tf_net.PlugVifs(mock.MagicMock(), self.apt, inst, net_info) + p_vifs.execute(self.mock_lpar_wrap) + + # The create should have been called with new_vif as False. + mock_plug.assert_any_call(self.apt, inst, net_info[0], new_vif=False) + mock_plug.assert_any_call(self.apt, inst, net_info[1], new_vif=False) + + @mock.patch('nova.virt.powervm.vif.plug') + @mock.patch('nova.virt.powervm.vm.get_cnas') + def test_plug_vifs_invalid_state(self, mock_vm_get, mock_plug): + """Tests that a crt_vif fails when the LPAR state is bad.""" + inst = powervm.TEST_INSTANCE + + # Mock up the CNA response. Only doing one for simplicity + mock_vm_get.return_value = [] + net_info = [{'address': 'aa:bb:cc:dd:ee:ff', 'vnic_type': 'normal'}] + + # Mock that the state is incorrect + self.mock_lpar_wrap.can_modify_io.return_value = False, 'bad' + + # Run method + p_vifs = tf_net.PlugVifs(mock.MagicMock(), self.apt, inst, net_info) + self.assertRaises(exception.VirtualInterfaceCreateException, + p_vifs.execute, self.mock_lpar_wrap) + + # The create should not have been invoked + self.assertEqual(0, mock_plug.call_count) + + @mock.patch('nova.virt.powervm.vif.plug') + @mock.patch('nova.virt.powervm.vm.get_cnas') + def test_plug_vifs_timeout(self, mock_vm_get, mock_plug): + """Tests that crt vif failure via loss of neutron callback.""" + inst = powervm.TEST_INSTANCE + + # Mock up the CNA response. Only doing one for simplicity + mock_vm_get.return_value = [cna('AABBCCDDEE11')] + + # Mock up the network info. + net_info = [{'address': 'aa:bb:cc:dd:ee:ff', 'vnic_type': 'normal'}] + + # Ensure that an exception is raised by a timeout. + mock_plug.side_effect = eventlet.timeout.Timeout() + + # Run method + p_vifs = tf_net.PlugVifs(mock.MagicMock(), self.apt, inst, net_info) + self.assertRaises(exception.VirtualInterfaceCreateException, + p_vifs.execute, self.mock_lpar_wrap) + + # The create should have only been called once. + self.assertEqual(1, mock_plug.call_count) + + @mock.patch('nova.virt.powervm.vif.unplug') + @mock.patch('nova.virt.powervm.vif.plug') + @mock.patch('nova.virt.powervm.vm.get_cnas') + def test_plug_vifs_revert(self, mock_vm_get, mock_plug, mock_unplug): + """Tests that the revert flow works properly.""" + inst = powervm.TEST_INSTANCE + + # Fake CNA list. The one pre-existing VIF should *not* get reverted. + cna_list = [cna('AABBCCDDEEFF'), cna('FFEEDDCCBBAA')] + mock_vm_get.return_value = cna_list + + # Mock up the network info. Three roll backs. + net_info = [ + {'address': 'aa:bb:cc:dd:ee:ff', 'vnic_type': 'normal'}, + {'address': 'aa:bb:cc:dd:ee:22', 'vnic_type': 'normal'}, + {'address': 'aa:bb:cc:dd:ee:33', 'vnic_type': 'normal'} + ] + + # Make sure we test raising an exception + mock_unplug.side_effect = [exception.NovaException(), None] + + # Run method + p_vifs = tf_net.PlugVifs(mock.MagicMock(), self.apt, inst, net_info) + p_vifs.execute(self.mock_lpar_wrap) + p_vifs.revert(self.mock_lpar_wrap, mock.Mock(), mock.Mock()) + + # The unplug should be called twice. The exception shouldn't stop the + # second call. + self.assertEqual(2, mock_unplug.call_count) + + # Make sure each call is invoked correctly. The first plug was not a + # new vif, so it should not be reverted. + c2 = mock.call(self.apt, inst, net_info[1], cna_w_list=cna_list) + c3 = mock.call(self.apt, inst, net_info[2], cna_w_list=cna_list) + mock_unplug.assert_has_calls([c2, c3]) + + @mock.patch('pypowervm.tasks.cna.crt_cna') + @mock.patch('pypowervm.wrappers.network.VSwitch.search') + @mock.patch('nova.virt.powervm.vif.plug') + @mock.patch('nova.virt.powervm.vm.get_cnas') + def test_plug_mgmt_vif(self, mock_vm_get, mock_plug, mock_vs_search, + mock_crt_cna): + """Tests that a mgmt vif can be created.""" + inst = powervm.TEST_INSTANCE + + # Mock up the rmc vswitch + vswitch_w = mock.MagicMock() + vswitch_w.href = 'fake_mgmt_uri' + mock_vs_search.return_value = [vswitch_w] + + # Run method such that it triggers a fresh CNA search + p_vifs = tf_net.PlugMgmtVif(self.apt, inst) + p_vifs.execute(None) + + # With the default get_cnas mock (which returns a Mock()), we think we + # found an existing management CNA. + mock_crt_cna.assert_not_called() + mock_vm_get.assert_called_once_with( + self.apt, inst, vswitch_uri='fake_mgmt_uri') + + # Now mock get_cnas to return no hits + mock_vm_get.reset_mock() + mock_vm_get.return_value = [] + p_vifs.execute(None) + + # Get was called; and since it didn't have the mgmt CNA, so was plug. + self.assertEqual(1, mock_crt_cna.call_count) + mock_vm_get.assert_called_once_with( + self.apt, inst, vswitch_uri='fake_mgmt_uri') + + # Now pass CNAs, but not the mgmt vif, "from PlugVifs" + cnas = [mock.Mock(vswitch_uri='uri1'), mock.Mock(vswitch_uri='uri2')] + mock_crt_cna.reset_mock() + mock_vm_get.reset_mock() + p_vifs.execute(cnas) + + # Get wasn't called, since the CNAs were passed "from PlugVifs"; but + # since the mgmt vif wasn't included, plug was called. + mock_vm_get.assert_not_called() + mock_crt_cna.assert_called() + + # Finally, pass CNAs including the mgmt. + cnas.append(mock.Mock(vswitch_uri='fake_mgmt_uri')) + mock_crt_cna.reset_mock() + p_vifs.execute(cnas) + + # Neither get nor plug was called. + mock_vm_get.assert_not_called() + mock_crt_cna.assert_not_called() + + def test_get_vif_events(self): + # Set up common mocks. + inst = powervm.TEST_INSTANCE + net_info = [mock.MagicMock(), mock.MagicMock()] + net_info[0]['id'] = 'a' + net_info[0].get.return_value = False + net_info[1]['id'] = 'b' + net_info[1].get.return_value = True + + # Set up the runner. + p_vifs = tf_net.PlugVifs(mock.MagicMock(), self.apt, inst, net_info) + p_vifs.crt_network_infos = net_info + resp = p_vifs._get_vif_events() + + # Only one should be returned since only one was active. + self.assertEqual(1, len(resp)) diff --git a/nova/tests/unit/virt/powervm/tasks/test_storage.py b/nova/tests/unit/virt/powervm/tasks/test_storage.py index cf4205a46919..1ff78aa3e2de 100644 --- a/nova/tests/unit/virt/powervm/tasks/test_storage.py +++ b/nova/tests/unit/virt/powervm/tasks/test_storage.py @@ -38,14 +38,14 @@ class TestStorage(test.NoDBTestCase): task = tf_stg.CreateAndConnectCfgDrive( self.adapter, self.instance, 'injected_files', 'network_info', 'stg_ftsk', admin_pass='admin_pass') - task.execute() + task.execute('mgmt_cna') self.mock_cfg_drv.assert_called_once_with(self.adapter) self.mock_mb.create_cfg_drv_vopt.assert_called_once_with( self.instance, 'injected_files', 'network_info', 'stg_ftsk', - admin_pass='admin_pass') + admin_pass='admin_pass', mgmt_cna='mgmt_cna') # Normal revert - task.revert('result', 'flow_failures') + task.revert('mgmt_cna', 'result', 'flow_failures') self.mock_mb.dlt_vopt.assert_called_once_with(self.instance, 'stg_ftsk') @@ -53,14 +53,14 @@ class TestStorage(test.NoDBTestCase): # Revert when dlt_vopt fails self.mock_mb.dlt_vopt.side_effect = pvm_exc.Error('fake-exc') - task.revert('result', 'flow_failures') + task.revert('mgmt_cna', 'result', 'flow_failures') self.mock_mb.dlt_vopt.assert_called_once() self.mock_mb.reset_mock() # Revert when media builder not created task.mb = None - task.revert('result', 'flow_failures') + task.revert('mgmt_cna', 'result', 'flow_failures') self.mock_mb.assert_not_called() def test_delete_vopt(self): diff --git a/nova/tests/unit/virt/powervm/test_driver.py b/nova/tests/unit/virt/powervm/test_driver.py index 4ff03fa3f415..c97f6c1a9a9f 100644 --- a/nova/tests/unit/virt/powervm/test_driver.py +++ b/nova/tests/unit/virt/powervm/test_driver.py @@ -111,6 +111,8 @@ class TestPowerVMDriver(test.NoDBTestCase): mock_bhrfm.assert_called_once_with('sys') self.assertEqual('sys', self.drv.host_wrapper) + @mock.patch('nova.virt.powervm.tasks.network.PlugMgmtVif.execute') + @mock.patch('nova.virt.powervm.tasks.network.PlugVifs.execute') @mock.patch('nova.virt.powervm.media.ConfigDrivePowerVM') @mock.patch('nova.virt.configdrive.required_by') @mock.patch('nova.virt.powervm.vm.create_lpar') @@ -119,10 +121,11 @@ class TestPowerVMDriver(test.NoDBTestCase): @mock.patch('pypowervm.tasks.storage.add_lpar_storage_scrub_tasks', autospec=True) def test_spawn_ops(self, mock_scrub, mock_bldftsk, mock_crt_lpar, - mock_cdrb, mock_cfg_drv): + mock_cdrb, mock_cfg_drv, mock_plug_vifs, + mock_plug_mgmt_vif): """Validates the 'typical' spawn flow of the spawn of an instance. """ mock_cdrb.return_value = True - self.drv.host_wrapper = mock.Mock(uuid='host_uuid') + self.drv.host_wrapper = mock.Mock() self.drv.disk_dvr = mock.create_autospec(ssp.SSPDiskAdapter, instance=True) mock_ftsk = pvm_tx.FeedTask('fake', [mock.Mock(spec=pvm_vios.VIOS)]) @@ -133,6 +136,8 @@ class TestPowerVMDriver(test.NoDBTestCase): self.adp, self.drv.host_wrapper, self.inst) mock_bldftsk.assert_called_once_with( self.adp, xag={pvm_const.XAG.VIO_SMAP, pvm_const.XAG.VIO_FMAP}) + self.assertTrue(mock_plug_vifs.called) + self.assertTrue(mock_plug_mgmt_vif.called) mock_scrub.assert_called_once_with( [mock_crt_lpar.return_value.id], mock_ftsk, lpars_exist=True) self.drv.disk_dvr.create_disk_from_image.assert_called_once_with( @@ -142,7 +147,8 @@ class TestPowerVMDriver(test.NoDBTestCase): mock_ftsk) mock_cfg_drv.assert_called_once_with(self.adp) mock_cfg_drv.return_value.create_cfg_drv_vopt.assert_called_once_with( - self.inst, 'files', 'netinfo', mock_ftsk, admin_pass='password') + self.inst, 'files', 'netinfo', mock_ftsk, admin_pass='password', + mgmt_cna=mock.ANY) self.pwron.assert_called_once_with(self.adp, self.inst) mock_cfg_drv.reset_mock() @@ -153,17 +159,19 @@ class TestPowerVMDriver(test.NoDBTestCase): 'allocs') mock_cfg_drv.assert_not_called() + @mock.patch('nova.virt.powervm.tasks.network.UnplugVifs.execute') @mock.patch('nova.virt.powervm.vm.delete_lpar') @mock.patch('nova.virt.powervm.media.ConfigDrivePowerVM') @mock.patch('nova.virt.configdrive.required_by') @mock.patch('pypowervm.tasks.partition.build_active_vio_feed_task', autospec=True) def test_destroy(self, mock_bldftsk, mock_cdrb, mock_cfgdrv, - mock_dlt_lpar): + mock_dlt_lpar, mock_unplug): """Validates PowerVM destroy.""" - self.drv.host_wrapper = mock.Mock(uuid='host_uuid') + self.drv.host_wrapper = mock.Mock() self.drv.disk_dvr = mock.create_autospec(ssp.SSPDiskAdapter, instance=True) + mock_ftsk = pvm_tx.FeedTask('fake', [mock.Mock(spec=pvm_vios.VIOS)]) mock_bldftsk.return_value = mock_ftsk @@ -174,6 +182,7 @@ class TestPowerVMDriver(test.NoDBTestCase): self.adp, self.inst, force_immediate=True) mock_bldftsk.assert_called_once_with( self.adp, xag=[pvm_const.XAG.VIO_SMAP]) + mock_unplug.assert_called_once() mock_cdrb.assert_called_once_with(self.inst) mock_cfgdrv.assert_called_once_with(self.adp) mock_cfgdrv.return_value.dlt_vopt.assert_called_once_with( @@ -186,6 +195,7 @@ class TestPowerVMDriver(test.NoDBTestCase): self.pwroff.reset_mock() mock_bldftsk.reset_mock() + mock_unplug.reset_mock() mock_cdrb.reset_mock() mock_cfgdrv.reset_mock() self.drv.disk_dvr.detach_disk.reset_mock() @@ -204,6 +214,7 @@ class TestPowerVMDriver(test.NoDBTestCase): self.adp, self.inst, force_immediate=False) mock_bldftsk.assert_called_once_with( self.adp, xag=[pvm_const.XAG.VIO_SMAP]) + mock_unplug.assert_called_once() mock_cdrb.assert_called_once_with(self.inst) mock_cfgdrv.assert_not_called() mock_cfgdrv.return_value.dlt_vopt.assert_not_called() @@ -214,6 +225,7 @@ class TestPowerVMDriver(test.NoDBTestCase): self.pwroff.reset_mock() mock_bldftsk.reset_mock() + mock_unplug.reset_mock() mock_cdrb.reset_mock() mock_cfgdrv.reset_mock() self.drv.disk_dvr.detach_disk.reset_mock() @@ -228,11 +240,13 @@ class TestPowerVMDriver(test.NoDBTestCase): self.pwroff.assert_called_once_with( self.adp, self.inst, force_immediate=False) self.drv.disk_dvr.detach_disk.assert_not_called() + mock_unplug.assert_not_called() self.drv.disk_dvr.delete_disks.assert_not_called() mock_dlt_lpar.assert_not_called() self.pwroff.reset_mock() self.pwroff.side_effect = None + mock_unplug.reset_mock() # Convertible (PowerVM) exception mock_dlt_lpar.side_effect = pvm_exc.TimeoutError("Timed out") @@ -243,6 +257,7 @@ class TestPowerVMDriver(test.NoDBTestCase): # Everything got called self.pwroff.assert_called_once_with( self.adp, self.inst, force_immediate=True) + mock_unplug.assert_called_once() self.drv.disk_dvr.detach_disk.assert_called_once_with(self.inst) self.drv.disk_dvr.delete_disks.assert_called_once_with( self.drv.disk_dvr.detach_disk.return_value) @@ -304,3 +319,7 @@ class TestPowerVMDriver(test.NoDBTestCase): mock_vterm.side_effect = pvm_exc.HttpError(mock.Mock(status=404)) self.assertRaises(exception.InstanceNotFound, self.drv.get_vnc_console, mock.ANY, self.inst) + + def test_deallocate_networks_on_reschedule(self): + candeallocate = self.drv.deallocate_networks_on_reschedule(mock.Mock()) + self.assertTrue(candeallocate) diff --git a/nova/tests/unit/virt/powervm/test_media.py b/nova/tests/unit/virt/powervm/test_media.py index 5d01305df9ae..ba1890d95b80 100644 --- a/nova/tests/unit/virt/powervm/test_media.py +++ b/nova/tests/unit/virt/powervm/test_media.py @@ -21,6 +21,7 @@ import mock from pypowervm.tasks import scsi_mapper as tsk_map from pypowervm.tests import test_fixtures as pvm_fx from pypowervm.utils import transaction as pvm_tx +from pypowervm.wrappers import network as pvm_net from pypowervm.wrappers import storage as pvm_stg from pypowervm.wrappers import virtual_io_server as pvm_vios import six @@ -187,3 +188,34 @@ class TestConfigDrivePowerVM(test.NoDBTestCase): cfg_dr.dlt_vopt('inst', ftsk) mock_functask.assert_called_once() ftsk.add_post_execute.assert_called_once_with('functor_task') + + def test_mgmt_cna_to_vif(self): + mock_cna = mock.Mock(spec=pvm_net.CNA, mac="FAD4433ED120") + + # Run + cfg_dr_builder = m.ConfigDrivePowerVM(self.apt) + vif = cfg_dr_builder._mgmt_cna_to_vif(mock_cna) + + # Validate + self.assertEqual(vif.get('address'), "fa:d4:43:3e:d1:20") + self.assertEqual(vif.get('id'), 'mgmt_vif') + self.assertIsNotNone(vif.get('network')) + self.assertEqual(1, len(vif.get('network').get('subnets'))) + subnet = vif.get('network').get('subnets')[0] + self.assertEqual(6, subnet.get('version')) + self.assertEqual('fe80::/64', subnet.get('cidr')) + ip = subnet.get('ips')[0] + self.assertEqual('fe80::f8d4:43ff:fe3e:d120', ip.get('address')) + + def test_mac_to_link_local(self): + mac = 'fa:d4:43:3e:d1:20' + self.assertEqual('fe80::f8d4:43ff:fe3e:d120', + m.ConfigDrivePowerVM._mac_to_link_local(mac)) + + mac = '00:00:00:00:00:00' + self.assertEqual('fe80::0200:00ff:fe00:0000', + m.ConfigDrivePowerVM._mac_to_link_local(mac)) + + mac = 'ff:ff:ff:ff:ff:ff' + self.assertEqual('fe80::fdff:ffff:feff:ffff', + m.ConfigDrivePowerVM._mac_to_link_local(mac)) diff --git a/nova/tests/unit/virt/powervm/test_vif.py b/nova/tests/unit/virt/powervm/test_vif.py new file mode 100644 index 000000000000..1033bae661f9 --- /dev/null +++ b/nova/tests/unit/virt/powervm/test_vif.py @@ -0,0 +1,225 @@ +# Copyright 2017 IBM Corp. +# +# 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 pypowervm import exceptions as pvm_ex +from pypowervm.wrappers import network as pvm_net + +from nova import exception +from nova.network import model +from nova import test +from nova.virt.powervm import vif + +CONF = cfg.CONF + + +def cna(mac): + """Builds a mock Client Network Adapter for unit tests.""" + return mock.Mock(spec=pvm_net.CNA, mac=mac, vswitch_uri='fake_href') + + +class TestVifFunctions(test.NoDBTestCase): + + def setUp(self): + super(TestVifFunctions, self).setUp() + + self.adpt = mock.Mock() + + @mock.patch('nova.virt.powervm.vif.PvmOvsVifDriver') + def test_build_vif_driver(self, mock_driver): + # Valid vif type + driver = vif._build_vif_driver(self.adpt, 'instance', {'type': 'ovs'}) + self.assertEqual(mock_driver.return_value, driver) + + mock_driver.reset_mock() + + # Fail if no vif type + self.assertRaises(exception.VirtualInterfacePlugException, + vif._build_vif_driver, self.adpt, 'instance', + {'type': None}) + mock_driver.assert_not_called() + + # Fail if invalid vif type + self.assertRaises(exception.VirtualInterfacePlugException, + vif._build_vif_driver, self.adpt, 'instance', + {'type': 'bad_type'}) + mock_driver.assert_not_called() + + @mock.patch('nova.virt.powervm.vif._build_vif_driver') + def test_plug(self, mock_bld_drv): + """Test the top-level plug method.""" + mock_vif = {'address': 'MAC', 'type': 'pvm_sea'} + + # 1) With new_vif=True (default) + vnet = vif.plug(self.adpt, 'instance', mock_vif) + + mock_bld_drv.assert_called_once_with(self.adpt, 'instance', mock_vif) + mock_bld_drv.return_value.plug.assert_called_once_with(mock_vif, + new_vif=True) + self.assertEqual(mock_bld_drv.return_value.plug.return_value, vnet) + + # Clean up + mock_bld_drv.reset_mock() + mock_bld_drv.return_value.plug.reset_mock() + + # 2) Plug returns None (which it should IRL whenever new_vif=False). + mock_bld_drv.return_value.plug.return_value = None + vnet = vif.plug(self.adpt, 'instance', mock_vif, new_vif=False) + + mock_bld_drv.assert_called_once_with(self.adpt, 'instance', mock_vif) + mock_bld_drv.return_value.plug.assert_called_once_with(mock_vif, + new_vif=False) + self.assertIsNone(vnet) + + @mock.patch('nova.virt.powervm.vif._build_vif_driver') + def test_plug_raises(self, mock_vif_drv): + """HttpError is converted to VirtualInterfacePlugException.""" + vif_drv = mock.Mock(plug=mock.Mock(side_effect=pvm_ex.HttpError( + resp=mock.Mock()))) + mock_vif_drv.return_value = vif_drv + mock_vif = {'address': 'vifaddr'} + self.assertRaises(exception.VirtualInterfacePlugException, + vif.plug, 'adap', 'inst', mock_vif, + new_vif='new_vif') + mock_vif_drv.assert_called_once_with('adap', 'inst', mock_vif) + vif_drv.plug.assert_called_once_with(mock_vif, new_vif='new_vif') + + @mock.patch('nova.virt.powervm.vif._build_vif_driver') + def test_unplug(self, mock_bld_drv): + """Test the top-level unplug method.""" + mock_vif = {'address': 'MAC', 'type': 'pvm_sea'} + + # 1) With default cna_w_list + mock_bld_drv.return_value.unplug.return_value = 'vnet_w' + vif.unplug(self.adpt, 'instance', mock_vif) + mock_bld_drv.assert_called_once_with(self.adpt, 'instance', mock_vif) + mock_bld_drv.return_value.unplug.assert_called_once_with( + mock_vif, cna_w_list=None) + + # Clean up + mock_bld_drv.reset_mock() + mock_bld_drv.return_value.unplug.reset_mock() + + # 2) With specified cna_w_list + mock_bld_drv.return_value.unplug.return_value = None + vif.unplug(self.adpt, 'instance', mock_vif, cna_w_list='cnalist') + mock_bld_drv.assert_called_once_with(self.adpt, 'instance', mock_vif) + mock_bld_drv.return_value.unplug.assert_called_once_with( + mock_vif, cna_w_list='cnalist') + + @mock.patch('nova.virt.powervm.vif._build_vif_driver') + def test_unplug_raises(self, mock_vif_drv): + """HttpError is converted to VirtualInterfacePlugException.""" + vif_drv = mock.Mock(unplug=mock.Mock(side_effect=pvm_ex.HttpError( + resp=mock.Mock()))) + mock_vif_drv.return_value = vif_drv + mock_vif = {'address': 'vifaddr'} + self.assertRaises(exception.VirtualInterfaceUnplugException, + vif.unplug, 'adap', 'inst', mock_vif, + cna_w_list='cna_w_list') + mock_vif_drv.assert_called_once_with('adap', 'inst', mock_vif) + vif_drv.unplug.assert_called_once_with( + mock_vif, cna_w_list='cna_w_list') + + +class TestVifOvsDriver(test.NoDBTestCase): + + def setUp(self): + super(TestVifOvsDriver, self).setUp() + + self.adpt = mock.Mock() + self.inst = mock.MagicMock(uuid='inst_uuid') + self.drv = vif.PvmOvsVifDriver(self.adpt, self.inst) + + @mock.patch('pypowervm.tasks.cna.crt_p2p_cna', autospec=True) + @mock.patch('pypowervm.tasks.partition.get_this_partition', autospec=True) + @mock.patch('nova.virt.powervm.vm.get_pvm_uuid') + def test_plug(self, mock_pvm_uuid, mock_mgmt_lpar, mock_p2p_cna,): + # Mock the data + mock_pvm_uuid.return_value = 'lpar_uuid' + mock_mgmt_lpar.return_value = mock.Mock(uuid='mgmt_uuid') + # mock_trunk_dev_name.return_value = 'device' + + cna_w, trunk_wraps = mock.MagicMock(), [mock.MagicMock()] + mock_p2p_cna.return_value = cna_w, trunk_wraps + + # Run the plug + network_model = model.Model({'bridge': 'br0', 'meta': {'mtu': 1450}}) + mock_vif = model.VIF(address='aa:bb:cc:dd:ee:ff', id='vif_id', + network=network_model, devname='device') + self.drv.plug(mock_vif) + + # Validate the calls + ovs_ext_ids = ('iface-id=vif_id,iface-status=active,' + 'attached-mac=aa:bb:cc:dd:ee:ff,vm-uuid=inst_uuid') + mock_p2p_cna.assert_called_once_with( + self.adpt, None, 'lpar_uuid', ['mgmt_uuid'], + 'NovaLinkVEABridge', configured_mtu=1450, crt_vswitch=True, + mac_addr='aa:bb:cc:dd:ee:ff', dev_name='device', ovs_bridge='br0', + ovs_ext_ids=ovs_ext_ids) + + @mock.patch('pypowervm.tasks.partition.get_this_partition', autospec=True) + @mock.patch('nova.virt.powervm.vm.get_pvm_uuid') + @mock.patch('nova.virt.powervm.vm.get_cnas') + @mock.patch('pypowervm.tasks.cna.find_trunks', autospec=True) + def test_plug_existing_vif(self, mock_find_trunks, mock_get_cnas, + mock_pvm_uuid, mock_mgmt_lpar): + # Mock the data + t1, t2 = mock.MagicMock(), mock.MagicMock() + mock_find_trunks.return_value = [t1, t2] + + mock_cna = mock.Mock(mac='aa:bb:cc:dd:ee:ff') + mock_get_cnas.return_value = [mock_cna] + + mock_pvm_uuid.return_value = 'lpar_uuid' + + mock_mgmt_lpar.return_value = mock.Mock(uuid='mgmt_uuid') + + self.inst = mock.MagicMock(uuid='c2e7ff9f-b9b6-46fa-8716-93bbb795b8b4') + self.drv = vif.PvmOvsVifDriver(self.adpt, self.inst) + + # Run the plug + network_model = model.Model({'bridge': 'br0', 'meta': {'mtu': 1500}}) + mock_vif = model.VIF(address='aa:bb:cc:dd:ee:ff', id='vif_id', + network=network_model, devname='devname') + resp = self.drv.plug(mock_vif, new_vif=False) + + self.assertIsNone(resp) + + # Validate if trunk.update got invoked for all trunks of CNA of vif + self.assertTrue(t1.update.called) + self.assertTrue(t2.update.called) + + @mock.patch('pypowervm.tasks.cna.find_trunks') + @mock.patch('nova.virt.powervm.vm.get_cnas') + def test_unplug(self, mock_get_cnas, mock_find_trunks): + # Set up the mocks + mock_cna = mock.Mock(mac='aa:bb:cc:dd:ee:ff') + mock_get_cnas.return_value = [mock_cna] + + t1, t2 = mock.MagicMock(), mock.MagicMock() + mock_find_trunks.return_value = [t1, t2] + + # Call the unplug + mock_vif = {'address': 'aa:bb:cc:dd:ee:ff', + 'network': {'bridge': 'br-int'}} + self.drv.unplug(mock_vif) + + # The trunks and the cna should have been deleted + self.assertTrue(t1.delete.called) + self.assertTrue(t2.delete.called) + self.assertTrue(mock_cna.delete.called) diff --git a/nova/tests/unit/virt/powervm/test_vm.py b/nova/tests/unit/virt/powervm/test_vm.py index 5d7552132296..73f55f54ced8 100644 --- a/nova/tests/unit/virt/powervm/test_vm.py +++ b/nova/tests/unit/virt/powervm/test_vm.py @@ -536,3 +536,31 @@ class TestVM(test.NoDBTestCase): self.apt.read.side_effect = pvm_exc.Error("message", response=resp) self.assertRaises(pvm_exc.Error, vm.get_vm_qp, self.apt, 'lpar_uuid', log_errors=False) + + @mock.patch('nova.virt.powervm.vm.get_pvm_uuid') + @mock.patch('pypowervm.wrappers.network.CNA.search') + @mock.patch('pypowervm.wrappers.network.CNA.get') + def test_get_cnas(self, mock_get, mock_search, mock_uuid): + # No kwargs: get + self.assertEqual(mock_get.return_value, vm.get_cnas(self.apt, 'inst')) + mock_uuid.assert_called_once_with('inst') + mock_get.assert_called_once_with(self.apt, parent_type=pvm_lpar.LPAR, + parent_uuid=mock_uuid.return_value) + mock_search.assert_not_called() + # With kwargs: search + mock_get.reset_mock() + mock_uuid.reset_mock() + self.assertEqual(mock_search.return_value, vm.get_cnas( + self.apt, 'inst', one=2, three=4)) + mock_uuid.assert_called_once_with('inst') + mock_search.assert_called_once_with( + self.apt, parent_type=pvm_lpar.LPAR, + parent_uuid=mock_uuid.return_value, one=2, three=4) + mock_get.assert_not_called() + + def test_norm_mac(self): + EXPECTED = "12:34:56:78:90:ab" + self.assertEqual(EXPECTED, vm.norm_mac("12:34:56:78:90:ab")) + self.assertEqual(EXPECTED, vm.norm_mac("1234567890ab")) + self.assertEqual(EXPECTED, vm.norm_mac("12:34:56:78:90:AB")) + self.assertEqual(EXPECTED, vm.norm_mac("1234567890AB")) diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py index 6e58d875bf56..b8e833a985b4 100644 --- a/nova/virt/powervm/driver.py +++ b/nova/virt/powervm/driver.py @@ -36,6 +36,7 @@ from nova.virt import driver from nova.virt.powervm.disk import ssp from nova.virt.powervm import host as pvm_host from nova.virt.powervm.tasks import base as tf_base +from nova.virt.powervm.tasks import network as tf_net from nova.virt.powervm.tasks import storage as tf_stg from nova.virt.powervm.tasks import vm as tf_vm from nova.virt.powervm import vm @@ -179,7 +180,11 @@ class PowerVMDriver(driver.ComputeDriver): flow_spawn.add(tf_vm.Create( self.adapter, self.host_wrapper, instance, stg_ftsk)) - # TODO(thorst, efried) Plug the VIFs + # Create a flow for the IO + flow_spawn.add(tf_net.PlugVifs( + self.virtapi, self.adapter, instance, network_info)) + flow_spawn.add(tf_net.PlugMgmtVif( + self.adapter, instance)) # Create the boot image. flow_spawn.add(tf_stg.CreateDiskForImg( @@ -232,13 +237,16 @@ class PowerVMDriver(driver.ComputeDriver): # hard shutdown. flow.add(tf_vm.PowerOff(self.adapter, instance, force_immediate=destroy_disks)) - # TODO(thorst, efried) Add unplug vifs task # The FeedTask accumulates storage disconnection tasks to be run in # parallel. stg_ftsk = pvm_par.build_active_vio_feed_task( self.adapter, xag=[pvm_const.XAG.VIO_SMAP]) + # Call the unplug VIFs task. While CNAs get removed from the LPAR + # directly on the destroy, this clears up the I/O Host side. + flow.add(tf_net.UnplugVifs(self.adapter, instance, network_info)) + # Add the disconnect/deletion of the vOpt to the transaction # manager. if configdrive.required_by(instance): @@ -349,3 +357,11 @@ class PowerVMDriver(driver.ComputeDriver): if e.response.status == 404: sare.reraise = False raise exc.InstanceNotFound(instance_id=instance.uuid) + + def deallocate_networks_on_reschedule(self, instance): + """Does the driver want networks deallocated on reschedule? + + :param instance: the instance object. + :returns: Boolean value. If True deallocate networks on reschedule. + """ + return True diff --git a/nova/virt/powervm/media.py b/nova/virt/powervm/media.py index 24458811c643..494e1d7a0fde 100644 --- a/nova/virt/powervm/media.py +++ b/nova/virt/powervm/media.py @@ -30,6 +30,7 @@ import retrying from taskflow import task from nova.api.metadata import base as instance_metadata +from nova.network import model as network_model from nova.virt import configdrive from nova.virt.powervm import vm @@ -134,9 +135,8 @@ class ConfigDrivePowerVM(object): LOG.exception("Config drive ISO could not be built", instance=instance) - # TODO(esberglu) Add mgmt_cna when networking is introduced def create_cfg_drv_vopt(self, instance, injected_files, network_info, - stg_ftsk, admin_pass=None): + stg_ftsk, admin_pass=None, mgmt_cna=None): """Create the config drive virtual optical and attach to VM. :param instance: The VM instance from OpenStack. @@ -145,7 +145,14 @@ class ConfigDrivePowerVM(object): :param network_info: The network_info from the nova spawn method. :param stg_ftsk: FeedTask to defer storage connectivity operations. :param admin_pass: (Optional) password to inject for the VM. + :param mgmt_cna: (Optional) The management (RMC) CNA wrapper. """ + # If there is a management client network adapter, then we should + # convert that to a VIF and add it to the network info + if mgmt_cna is not None: + network_info = copy.deepcopy(network_info) + network_info.append(self._mgmt_cna_to_vif(mgmt_cna)) + iso_path, file_name = self._create_cfg_dr_iso( instance, injected_files, network_info, admin_pass=admin_pass) @@ -169,6 +176,39 @@ class ConfigDrivePowerVM(object): # Add the subtask to create the mapping when the FeedTask runs stg_ftsk.wrapper_tasks[self.vios_uuid].add_functor_subtask(add_func) + def _mgmt_cna_to_vif(self, cna): + """Converts the mgmt CNA to VIF format for network injection.""" + mac = vm.norm_mac(cna.mac) + ipv6_link_local = self._mac_to_link_local(mac) + + subnet = network_model.Subnet( + version=6, cidr=_LLA_SUBNET, + ips=[network_model.FixedIP(address=ipv6_link_local)]) + network = network_model.Network(id='mgmt', subnets=[subnet], + injected='yes') + return network_model.VIF(id='mgmt_vif', address=mac, + network=network) + + @staticmethod + def _mac_to_link_local(mac): + # Convert the address to IPv6. The first step is to separate out the + # mac address + splits = mac.split(':') + + # Create EUI-64 id per RFC 4291 Appendix A + splits.insert(3, 'ff') + splits.insert(4, 'fe') + + # Create modified EUI-64 id via bit flip per RFC 4291 Appendix A + splits[0] = "%.2x" % (int(splits[0], 16) ^ 0b00000010) + + # Convert to the IPv6 link local format. The prefix is fe80::. Join + # the hexes together at every other digit. + ll = ['fe80:'] + ll.extend([splits[x] + splits[x + 1] + for x in range(0, len(splits), 2)]) + return ':'.join(ll) + def dlt_vopt(self, instance, stg_ftsk): """Deletes the virtual optical and scsi mappings for a VM. diff --git a/nova/virt/powervm/tasks/network.py b/nova/virt/powervm/tasks/network.py new file mode 100644 index 000000000000..12f303010973 --- /dev/null +++ b/nova/virt/powervm/tasks/network.py @@ -0,0 +1,259 @@ +# Copyright 2015, 2017 IBM Corp. +# +# 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 eventlet +from oslo_log import log as logging +from pypowervm.tasks import cna as pvm_cna +from pypowervm.wrappers import managed_system as pvm_ms +from pypowervm.wrappers import network as pvm_net +from taskflow import task + +from nova import conf as cfg +from nova import exception +from nova.virt.powervm import vif +from nova.virt.powervm import vm + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + +SECURE_RMC_VSWITCH = 'MGMTSWITCH' +SECURE_RMC_VLAN = 4094 + + +class PlugVifs(task.Task): + + """The task to plug the Virtual Network Interfaces to a VM.""" + + def __init__(self, virt_api, adapter, instance, network_infos): + """Create the task. + + Provides 'vm_cnas' - the list of the Virtual Machine's Client Network + Adapters as they stand after all VIFs are plugged. May be None, in + which case the Task requiring 'vm_cnas' should discover them afresh. + + :param virt_api: The VirtAPI for the operation. + :param adapter: The pypowervm adapter. + :param instance: The nova instance. + :param network_infos: The network information containing the nova + VIFs to create. + """ + self.virt_api = virt_api + self.adapter = adapter + self.instance = instance + self.network_infos = network_infos or [] + self.crt_network_infos, self.update_network_infos = [], [] + # Cache of CNAs that is filled on initial _vif_exists() call. + self.cnas = None + + super(PlugVifs, self).__init__( + 'plug_vifs', provides='vm_cnas', requires=['lpar_wrap']) + + def _vif_exists(self, network_info): + """Does the instance have a CNA for a given net? + + :param network_info: A network information dict. This method expects + it to contain key 'address' (MAC address). + :return: True if a CNA with the network_info's MAC address exists on + the instance. False otherwise. + """ + if self.cnas is None: + self.cnas = vm.get_cnas(self.adapter, self.instance) + vifs = self.cnas + + return network_info['address'] in [vm.norm_mac(v.mac) for v in vifs] + + def execute(self, lpar_wrap): + # Check to see if the LPAR is OK to add VIFs to. + modifiable, reason = lpar_wrap.can_modify_io() + if not modifiable: + LOG.error("Unable to create VIF(s) for instance in the system's " + "current state. The reason from the system is: %s", + reason, instance=self.instance) + raise exception.VirtualInterfaceCreateException() + + # We will have two types of network infos. One is for newly created + # vifs. The others are those that exist, but should be re-'treated' + for network_info in self.network_infos: + if self._vif_exists(network_info): + self.update_network_infos.append(network_info) + else: + self.crt_network_infos.append(network_info) + + # If there are no vifs to create or update, then just exit immediately. + if not self.crt_network_infos and not self.update_network_infos: + return [] + + # For existing VIFs that we just need to update, run the plug but do + # not wait for the neutron event as that likely won't be sent (it was + # already done). + for network_info in self.update_network_infos: + LOG.info("Updating VIF with mac %s for instance.", + network_info['address'], instance=self.instance) + vif.plug(self.adapter, self.instance, network_info, new_vif=False) + + # For the new VIFs, run the creates (and wait for the events back) + try: + with self.virt_api.wait_for_instance_event( + self.instance, self._get_vif_events(), + deadline=CONF.vif_plugging_timeout, + error_callback=self._vif_callback_failed): + for network_info in self.crt_network_infos: + LOG.info('Creating VIF with mac %s for instance.', + network_info['address'], instance=self.instance) + new_vif = vif.plug( + self.adapter, self.instance, network_info, + new_vif=True) + if self.cnas is not None: + self.cnas.append(new_vif) + except eventlet.timeout.Timeout: + LOG.error('Error waiting for VIF to be created for instance.', + instance=self.instance) + raise exception.VirtualInterfaceCreateException() + + return self.cnas + + def _vif_callback_failed(self, event_name, instance): + LOG.error('VIF Plug failure for callback on event %s for instance.', + event_name, instance=self.instance) + if CONF.vif_plugging_is_fatal: + raise exception.VirtualInterfaceCreateException() + + def _get_vif_events(self): + """Returns the VIF events that need to be received for a VIF plug. + + In order for a VIF plug to be successful, certain events should be + received from other components within the OpenStack ecosystem. This + method returns the events neutron needs for a given deploy. + """ + # See libvirt's driver.py -> _get_neutron_events method for + # more information. + if CONF.vif_plugging_is_fatal and CONF.vif_plugging_timeout: + return [('network-vif-plugged', network_info['id']) + for network_info in self.crt_network_infos + if not network_info.get('active', True)] + + def revert(self, lpar_wrap, result, flow_failures): + if not self.network_infos: + return + + LOG.warning('VIF creation being rolled back for instance.', + instance=self.instance) + + # Get the current adapters on the system + cna_w_list = vm.get_cnas(self.adapter, self.instance) + for network_info in self.crt_network_infos: + try: + vif.unplug(self.adapter, self.instance, network_info, + cna_w_list=cna_w_list) + except Exception: + LOG.exception("An exception occurred during an unplug in the " + "vif rollback. Ignoring.", + instance=self.instance) + + +class UnplugVifs(task.Task): + + """The task to unplug Virtual Network Interfaces from a VM.""" + + def __init__(self, adapter, instance, network_infos): + """Create the task. + + :param adapter: The pypowervm adapter. + :param instance: The nova instance. + :param network_infos: The network information containing the nova + VIFs to create. + """ + self.adapter = adapter + self.instance = instance + self.network_infos = network_infos or [] + + super(UnplugVifs, self).__init__('unplug_vifs') + + def execute(self): + # If the LPAR is not in an OK state for deleting, then throw an + # error up front. + lpar_wrap = vm.get_instance_wrapper(self.adapter, self.instance) + modifiable, reason = lpar_wrap.can_modify_io() + if not modifiable: + LOG.error("Unable to remove VIFs from instance in the system's " + "current state. The reason reported by the system is: " + "%s", reason, instance=self.instance) + raise exception.VirtualInterfaceUnplugException(reason=reason) + + # Get all the current Client Network Adapters (CNA) on the VM itself. + cna_w_list = vm.get_cnas(self.adapter, self.instance) + + # Walk through the VIFs and delete the corresponding CNA on the VM. + for network_info in self.network_infos: + vif.unplug(self.adapter, self.instance, network_info, + cna_w_list=cna_w_list) + + +class PlugMgmtVif(task.Task): + + """The task to plug the Management VIF into a VM.""" + + def __init__(self, adapter, instance): + """Create the task. + + Requires 'vm_cnas' from PlugVifs. If None, this Task will retrieve the + VM's list of CNAs. + + Provides the mgmt_cna. This may be None if no management device was + created. This is the CNA of the mgmt vif for the VM. + + :param adapter: The pypowervm adapter. + :param instance: The nova instance. + """ + self.adapter = adapter + self.instance = instance + + super(PlugMgmtVif, self).__init__( + 'plug_mgmt_vif', provides='mgmt_cna', requires=['vm_cnas']) + + def execute(self, vm_cnas): + LOG.info('Plugging the Management Network Interface to instance.', + instance=self.instance) + # Determine if we need to create the secure RMC VIF. This should only + # be needed if there is not a VIF on the secure RMC vSwitch + vswitch = None + vswitches = pvm_net.VSwitch.search( + self.adapter, parent_type=pvm_ms.System.schema_type, + parent_uuid=self.adapter.sys_uuid, name=SECURE_RMC_VSWITCH) + if len(vswitches) == 1: + vswitch = vswitches[0] + + if vswitch is None: + LOG.warning('No management VIF created for instance due to lack ' + 'of Management Virtual Switch', instance=self.instance) + return None + + # This next check verifies that there are no existing NICs on the + # vSwitch, so that the VM does not end up with multiple RMC VIFs. + if vm_cnas is None: + has_mgmt_vif = vm.get_cnas(self.adapter, self.instance, + vswitch_uri=vswitch.href) + else: + has_mgmt_vif = vswitch.href in [cna.vswitch_uri for cna in vm_cnas] + + if has_mgmt_vif: + LOG.debug('Management VIF already created for instance', + instance=self.instance) + return None + + lpar_uuid = vm.get_pvm_uuid(self.instance) + return pvm_cna.crt_cna(self.adapter, None, lpar_uuid, SECURE_RMC_VLAN, + vswitch=SECURE_RMC_VSWITCH, crt_vswitch=True) diff --git a/nova/virt/powervm/tasks/storage.py b/nova/virt/powervm/tasks/storage.py index 52dc756b1aaf..50d14af1cf42 100644 --- a/nova/virt/powervm/tasks/storage.py +++ b/nova/virt/powervm/tasks/storage.py @@ -146,8 +146,7 @@ class CreateAndConnectCfgDrive(task.Task): network_info, stg_ftsk, admin_pass=None): """Create the Task that creates and connects the config drive. - Provides the 'cfg_drv_vscsi_map' which is an element to later map - the vscsi drive. + Requires the 'mgmt_cna' :param adapter: The adapter for the pypowervm API :param instance: The nova instance @@ -158,7 +157,8 @@ class CreateAndConnectCfgDrive(task.Task): :param admin_pass (Optional, Default None): Password to inject for the VM. """ - super(CreateAndConnectCfgDrive, self).__init__(instance, 'cfg_drive') + super(CreateAndConnectCfgDrive, self).__init__( + instance, 'cfg_drive', requires=['mgmt_cna']) self.adapter = adapter self.instance = instance self.injected_files = injected_files @@ -167,13 +167,13 @@ class CreateAndConnectCfgDrive(task.Task): self.ad_pass = admin_pass self.mb = None - def execute(self): + def execute(self, mgmt_cna): self.mb = media.ConfigDrivePowerVM(self.adapter) self.mb.create_cfg_drv_vopt(self.instance, self.injected_files, self.network_info, self.stg_ftsk, - admin_pass=self.ad_pass) + admin_pass=self.ad_pass, mgmt_cna=mgmt_cna) - def revert(self, result, flow_failures): + def revert(self, mgmt_cna, result, flow_failures): # No media builder, nothing to do if self.mb is None: return diff --git a/nova/virt/powervm/tasks/vm.py b/nova/virt/powervm/tasks/vm.py index 93a1fda829a3..773b199656f2 100644 --- a/nova/virt/powervm/tasks/vm.py +++ b/nova/virt/powervm/tasks/vm.py @@ -48,7 +48,7 @@ class Create(task.Task): :param instance: The nova instance. :param stg_ftsk: FeedTask to defer storage connectivity operations. """ - super(Create, self).__init__('crt_vm') + super(Create, self).__init__('crt_vm', provides='lpar_wrap') self.instance = instance self.adapter = adapter self.host_wrapper = host_wrapper diff --git a/nova/virt/powervm/vif.py b/nova/virt/powervm/vif.py new file mode 100644 index 000000000000..11a6f6b71121 --- /dev/null +++ b/nova/virt/powervm/vif.py @@ -0,0 +1,247 @@ +# Copyright 2016, 2017 IBM Corp. +# +# 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 +from oslo_utils import importutils +from pypowervm import exceptions as pvm_ex +from pypowervm.tasks import cna as pvm_cna +from pypowervm.tasks import partition as pvm_par +import six + +from nova import exception +from nova.network import model as network_model +from nova.virt.powervm import vm + +LOG = log.getLogger(__name__) + +NOVALINK_VSWITCH = 'NovaLinkVEABridge' + +VIF_TYPE_PVM_OVS = 'ovs' +VIF_MAPPING = {VIF_TYPE_PVM_OVS: + 'nova.virt.powervm.vif.PvmOvsVifDriver'} + +CONF = cfg.CONF + + +def _build_vif_driver(adapter, instance, vif): + """Returns the appropriate VIF Driver for the given VIF. + :param adapter: The pypowervm adapter API interface. + :param instance: The nova instance. + :param vif: The virtual interface. + :return: The appropriate PvmVifDriver for the VIF. + """ + if vif.get('type') is None: + LOG.exception("Failed to build vif driver. Missing vif type.", + instance=instance) + raise exception.VirtualInterfacePlugException() + + # Check the type to the implementations + if VIF_MAPPING.get(vif['type']): + return importutils.import_object( + VIF_MAPPING.get(vif['type']), adapter, instance) + + # No matching implementation, raise error. + LOG.exception("Failed to build vif driver. Invalid vif type provided.", + instance=instance) + raise exception.VirtualInterfacePlugException() + + +def plug(adapter, instance, vif, new_vif=True): + """Plugs a virtual interface (network) into a VM. + + :param adapter: The pypowervm adapter. + :param instance: The nova instance object. + :param vif: The virtual interface to plug into the instance. + :param new_vif: (Optional, Default: True) If set, indicates that it is + a brand new VIF. If False, it indicates that the VIF + is already on the client but should be treated on the + bridge. + :return: The wrapper (CNA) representing the plugged virtual network. None + if the vnet was not created. + """ + vif_drv = _build_vif_driver(adapter, instance, vif) + + try: + return vif_drv.plug(vif, new_vif=new_vif) + except pvm_ex.HttpError: + LOG.exception('VIF plug failed for instance.', instance=instance) + raise exception.VirtualInterfacePlugException() + # Other exceptions are (hopefully) custom VirtualInterfacePlugException + # generated lower in the call stack. + + +def unplug(adapter, instance, vif, cna_w_list=None): + """Unplugs a virtual interface (network) from a VM. + + :param adapter: The pypowervm adapter. + :param instance: The nova instance object. + :param vif: The virtual interface to plug into the instance. + :param cna_w_list: (Optional, Default: None) The list of Client Network + Adapters from pypowervm. Providing this input + allows for an improvement in operation speed. + """ + vif_drv = _build_vif_driver(adapter, instance, vif) + try: + vif_drv.unplug(vif, cna_w_list=cna_w_list) + except pvm_ex.HttpError as he: + LOG.exception('VIF unplug failed for instance', instance=instance) + raise exception.VirtualInterfaceUnplugException(reason=he.args[0]) + + +class PvmOvsVifDriver(object): + """The Open vSwitch VIF driver for PowerVM.""" + + def __init__(self, adapter, instance): + """Initializes a VIF Driver. + + :param adapter: The pypowervm adapter API interface. + :param instance: The nova instance that the vif action will be run + against. + """ + self.adapter = adapter + self.instance = instance + + def plug(self, vif, new_vif=True): + """Plugs a virtual interface (network) into a VM. + + Creates a 'peer to peer' connection between the Management partition + hosting the Linux I/O and the client VM. There will be one trunk + adapter for a given client adapter. + + The device will be 'up' on the mgmt partition. + + Will make sure that the trunk device has the appropriate metadata (e.g. + port id) set on it so that the Open vSwitch agent picks it up properly. + + :param vif: The virtual interface to plug into the instance. + :param new_vif: (Optional, Default: True) If set, indicates that it is + a brand new VIF. If False, it indicates that the VIF + is already on the client but should be treated on the + bridge. + :return: The new vif that was created. Only returned if new_vif is + set to True. Otherwise None is expected. + """ + + # Create the trunk and client adapter. + lpar_uuid = vm.get_pvm_uuid(self.instance) + mgmt_uuid = pvm_par.get_this_partition(self.adapter).uuid + + mtu = vif['network'].get_meta('mtu') + if 'devname' in vif: + dev_name = vif['devname'] + else: + dev_name = ("nic" + vif['id'])[:network_model.NIC_NAME_LEN] + + meta_attrs = ','.join([ + 'iface-id=%s' % (vif.get('ovs_interfaceid') or vif['id']), + 'iface-status=active', + 'attached-mac=%s' % vif['address'], + 'vm-uuid=%s' % self.instance.uuid]) + + if new_vif: + return pvm_cna.crt_p2p_cna( + self.adapter, None, lpar_uuid, [mgmt_uuid], NOVALINK_VSWITCH, + crt_vswitch=True, mac_addr=vif['address'], dev_name=dev_name, + ovs_bridge=vif['network']['bridge'], + ovs_ext_ids=meta_attrs, configured_mtu=mtu)[0] + else: + # Bug : https://bugs.launchpad.net/nova-powervm/+bug/1731548 + # When a host is rebooted, something is discarding tap devices for + # VMs deployed with OVS vif. To prevent VMs losing network + # connectivity, this is fixed by recreating the tap devices during + # init of the nova compute service, which will call vif plug with + # new_vif==False. + + # Find the CNA for this vif. + # TODO(esberglu) improve performance by caching VIOS wrapper(s) and + # CNA lists (in case >1 vif per VM). + cna_w_list = vm.get_cnas(self.adapter, self.instance) + cna_w = self._find_cna_for_vif(cna_w_list, vif) + if not cna_w: + LOG.warning('Unable to plug VIF with mac %s for instance. The ' + 'VIF was not found on the instance.', + vif['address'], instance=self.instance) + return None + + # Find the corresponding trunk adapter + trunks = pvm_cna.find_trunks(self.adapter, cna_w) + for trunk in trunks: + # Set MTU, OVS external ids, and OVS bridge metadata + trunk.configured_mtu = mtu + trunk.ovs_ext_ids = meta_attrs + trunk.ovs_bridge = vif['network']['bridge'] + # Updating the trunk adapter will cause NovaLink to reassociate + # the tap device. + trunk.update() + + def unplug(self, vif, cna_w_list=None): + """Unplugs a virtual interface (network) from a VM. + + This will remove the adapter from the Open vSwitch and delete the + trunk adapters. + + :param vif: The virtual interface to plug into the instance. + :param cna_w_list: (Optional, Default: None) The list of Client Network + Adapters from pypowervm. Providing this input + allows for an improvement in operation speed. + :return cna_w: The deleted Client Network Adapter or None if the CNA + is not found. + """ + # Need to find the adapters if they were not provided + if not cna_w_list: + cna_w_list = vm.get_cnas(self.adapter, self.instance) + + # Find the CNA for this vif. + cna_w = self._find_cna_for_vif(cna_w_list, vif) + + if not cna_w: + LOG.warning('Unable to unplug VIF with mac %s for instance. The ' + 'VIF was not found on the instance.', vif['address'], + instance=self.instance) + return None + + # Find and delete the trunk adapters + trunks = pvm_cna.find_trunks(self.adapter, cna_w) + for trunk in trunks: + trunk.delete() + + # Now delete the client CNA + LOG.info('Deleting VIF with mac %s for instance.', vif['address'], + instance=self.instance) + try: + cna_w.delete() + except Exception as e: + LOG.error('Unable to unplug VIF with mac %s for instance.', + vif['address'], instance=self.instance) + raise exception.VirtualInterfaceUnplugException( + reason=six.text_type(e)) + return cna_w + + @staticmethod + def _find_cna_for_vif(cna_w_list, vif): + """Finds the PowerVM CNA for a given Nova VIF. + + :param cna_w_list: The list of Client Network Adapter wrappers from + pypowervm. + :param vif: The Nova Virtual Interface (virtual network interface). + :return: The CNA that corresponds to the VIF. None if one is not + part of the cna_w_list. + """ + for cna_w in cna_w_list: + if vm.norm_mac(cna_w.mac) == vif['address']: + return cna_w + return None diff --git a/nova/virt/powervm/vm.py b/nova/virt/powervm/vm.py index 0111c6fe4184..eb52cfff9d66 100644 --- a/nova/virt/powervm/vm.py +++ b/nova/virt/powervm/vm.py @@ -30,6 +30,7 @@ from pypowervm.utils import uuid as pvm_uuid from pypowervm.utils import validation as pvm_vldn from pypowervm.wrappers import base_partition as pvm_bp from pypowervm.wrappers import logical_partition as pvm_lpar +from pypowervm.wrappers import network as pvm_net from pypowervm.wrappers import shared_proc_pool as pvm_spp import six @@ -73,6 +74,23 @@ _POWERVM_TO_NOVA_STATE = { pvm_bp.LPARState.ERROR: power_state.CRASHED} +def get_cnas(adapter, instance, **search): + """Returns the (possibly filtered) current CNAs on the instance. + + The Client Network Adapters are the Ethernet adapters for a VM. + + :param adapter: The pypowervm adapter. + :param instance: The nova instance. + :param search: Keyword arguments for CNA.search. If omitted, all CNAs are + returned. + :return The CNA wrappers that represent the ClientNetworkAdapters on the VM + """ + meth = pvm_net.CNA.search if search else pvm_net.CNA.get + + return meth(adapter, parent_type=pvm_lpar.LPAR, + parent_uuid=get_pvm_uuid(instance), **search) + + def get_lpar_names(adp): """Get a list of the LPAR names. @@ -326,6 +344,21 @@ def get_vm_info(adapter, instance): return hardware.InstanceInfo(nova_state) +def norm_mac(mac): + """Normalizes a MAC address from pypowervm format to OpenStack. + + That means that the format will be converted to lower case and will + have colons added. + + :param mac: A pypowervm mac address. Ex. 1234567890AB + :return: A mac that matches the standard neutron format. + Ex. 12:34:56:78:90:ab + """ + # Need the replacement if the mac is already normalized. + mac = mac.lower().replace(':', '') + return ':'.join(mac[i:i + 2] for i in range(0, len(mac), 2)) + + class VMBuilder(object): """Converts a Nova Instance/Flavor into a pypowervm LPARBuilder.""" _PVM_PROC_COMPAT = 'powervm:processor_compatibility'