From 01a452be03041c348ca854bba6db86fda91a4047 Mon Sep 17 00:00:00 2001 From: Drew Thorstensen Date: Wed, 3 Feb 2016 16:01:45 -0500 Subject: [PATCH] Add vif driver framework This change set adds a new vif driver interface (as well as SEA implementation) for the nova-powervm driver. This is important as the platform looks to new VIF types (ex. qbg or ovs). Change-Id: Ieb250d539e4adad8977d3a2716c2f1cf341c4137 --- .../tests/virt/powervm/tasks/test_network.py | 81 ++++--- .../tests/virt/powervm/test_driver.py | 14 +- nova_powervm/tests/virt/powervm/test_vif.py | 144 ++++++++++++ nova_powervm/tests/virt/powervm/test_vm.py | 28 +-- nova_powervm/virt/powervm/tasks/network.py | 78 +++---- nova_powervm/virt/powervm/vif.py | 208 ++++++++++++++++++ nova_powervm/virt/powervm/vm.py | 52 ----- 7 files changed, 433 insertions(+), 172 deletions(-) create mode 100644 nova_powervm/tests/virt/powervm/test_vif.py create mode 100644 nova_powervm/virt/powervm/vif.py diff --git a/nova_powervm/tests/virt/powervm/tasks/test_network.py b/nova_powervm/tests/virt/powervm/tasks/test_network.py index f8ea2be7..b3ed5d4a 100644 --- a/nova_powervm/tests/virt/powervm/tasks/test_network.py +++ b/nova_powervm/tests/virt/powervm/tasks/test_network.py @@ -1,4 +1,4 @@ -# Copyright 2015 IBM Corp. +# Copyright 2015, 2016 IBM Corp. # # All Rights Reserved. # @@ -43,13 +43,13 @@ class TestNetwork(test.TestCase): self.mock_lpar_wrap = mock.MagicMock() self.mock_lpar_wrap.can_modify_io.return_value = True, None + @mock.patch('nova_powervm.virt.powervm.vif.unplug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') - def test_unplug_vifs(self, mock_vm_get): + def test_unplug_vifs(self, mock_vm_get, mock_unplug): """Tests that a delete of the vif can be done.""" inst = objects.Instance(**powervm.TEST_INSTANCE) - # Mock up the CNA response. One should already exist, the other - # should not. + # Mock up the CNA responses. cnas = [cna('AABBCCDDEEFF'), cna('AABBCCDDEE11'), cna('AABBCCDDEE22')] mock_vm_get.return_value = cnas @@ -60,15 +60,24 @@ class TestNetwork(test.TestCase): {'address': 'aa:bb:cc:dd:ee:33'} ] + # Mock out the vif driver + def validate_unplug(adapter, host_uuid, instance, vif, + cna_w_list=None): + self.assertEqual(adapter, self.apt) + self.assertEqual('host_uuid', host_uuid) + 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, 'host_uuid') p_vifs.execute(self.mock_lpar_wrap) - # The delete should have only been called once. The second CNA didn't - # have a matching mac...so it should be skipped. - self.assertEqual(1, cnas[0].delete.call_count) - self.assertEqual(0, cnas[1].delete.call_count) - self.assertEqual(1, cnas[2].delete.call_count) + # Make sure the unplug was invoked, so that we know that the validation + # code was called + self.assertEqual(3, mock_unplug.call_count) def test_unplug_vifs_invalid_state(self): """Tests that the delete raises an exception if bad VM state.""" @@ -82,9 +91,9 @@ class TestNetwork(test.TestCase): self.assertRaises(tf_net.VirtualInterfaceUnplugException, p_vifs.execute, self.mock_lpar_wrap) - @mock.patch('nova_powervm.virt.powervm.vm.crt_vif') + @mock.patch('nova_powervm.virt.powervm.vif.plug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') - def test_plug_vifs_rmc(self, mock_vm_get, mock_vm_crt): + def test_plug_vifs_rmc(self, mock_vm_get, mock_plug): """Tests that a crt vif can be done with secure RMC.""" inst = objects.Instance(**powervm.TEST_INSTANCE) @@ -105,11 +114,11 @@ class TestNetwork(test.TestCase): p_vifs.execute(self.mock_lpar_wrap) # The create should have only been called once. - self.assertEqual(2, mock_vm_crt.call_count) + self.assertEqual(2, mock_plug.call_count) - @mock.patch('nova_powervm.virt.powervm.vm.crt_vif') + @mock.patch('nova_powervm.virt.powervm.vif.plug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') - def test_plug_vifs_rmc_no_create(self, mock_vm_get, mock_vm_crt): + def test_plug_vifs_rmc_no_create(self, mock_vm_get, mock_plug): """Verifies if no creates are needed, none are done.""" inst = objects.Instance(**powervm.TEST_INSTANCE) @@ -129,16 +138,16 @@ class TestNetwork(test.TestCase): # The create should not have been called. The response should have # been empty. - self.assertEqual(0, mock_vm_crt.call_count) + self.assertEqual(0, mock_plug.call_count) self.assertEqual([], resp) # State check shouldn't have even been invoked as no creates were # required self.assertEqual(0, self.mock_lpar_wrap.can_modify_io.call_count) - @mock.patch('nova_powervm.virt.powervm.vm.crt_vif') + @mock.patch('nova_powervm.virt.powervm.vif.plug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') - def test_plug_vifs_invalid_state(self, mock_vm_get, mock_vm_crt): + 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 = objects.Instance(**powervm.TEST_INSTANCE) @@ -156,11 +165,11 @@ class TestNetwork(test.TestCase): p_vifs.execute, self.mock_lpar_wrap) # The create should not have been invoked - self.assertEqual(0, mock_vm_crt.call_count) + self.assertEqual(0, mock_plug.call_count) - @mock.patch('nova_powervm.virt.powervm.vm.crt_vif') + @mock.patch('nova_powervm.virt.powervm.vif.plug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') - def test_plug_vifs_timeout(self, mock_vm_get, mock_vm_crt): + def test_plug_vifs_timeout(self, mock_vm_get, mock_plug): """Tests that crt vif failure via loss of neutron callback.""" inst = objects.Instance(**powervm.TEST_INSTANCE) @@ -171,7 +180,7 @@ class TestNetwork(test.TestCase): net_info = [{'address': 'aa:bb:cc:dd:ee:ff'}] # Ensure that an exception is raised by a timeout. - mock_vm_crt.side_effect = eventlet.timeout.Timeout() + mock_plug.side_effect = eventlet.timeout.Timeout() # Run method p_vifs = tf_net.PlugVifs(mock.MagicMock(), self.apt, inst, net_info, @@ -180,13 +189,14 @@ class TestNetwork(test.TestCase): p_vifs.execute, self.mock_lpar_wrap) # The create should have only been called once. - self.assertEqual(1, mock_vm_crt.call_count) + self.assertEqual(1, mock_plug.call_count) - @mock.patch('nova_powervm.virt.powervm.vm.crt_vif') + @mock.patch('nova_powervm.virt.powervm.vif.plug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') - def test_plug_vifs_diff_host(self, mock_vm_get, mock_vm_crt): + def test_plug_vifs_diff_host(self, mock_vm_get, mock_plug): """Tests that crt vif handles bad inst.host value.""" inst = powervm.TEST_INST1 + # Set this up as a different host from the inst.host self.flags(host='host2') @@ -203,20 +213,21 @@ class TestNetwork(test.TestCase): p_vifs.execute(self.mock_lpar_wrap) # The create should have only been called once. - self.assertEqual(1, mock_vm_crt.call_count) + self.assertEqual(1, mock_plug.call_count) # Should have called save to save the new host and then changed it back self.assertEqual(2, mock_inst_save.call_count) self.assertEqual('host1', inst.host) - @mock.patch('nova_powervm.virt.powervm.vm.crt_vif') + @mock.patch('nova_powervm.virt.powervm.vif.plug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') - def test_plug_vifs_diff_host_except(self, mock_vm_get, mock_vm_crt): + def test_plug_vifs_diff_host_except(self, mock_vm_get, mock_plug): """Tests that crt vif handles bad inst.host value. This test ensures that if we get a timeout exception we still reset the inst.host value back to the original value """ inst = powervm.TEST_INST1 + # Set this up as a different host from the inst.host self.flags(host='host2') @@ -227,7 +238,7 @@ class TestNetwork(test.TestCase): net_info = [{'address': 'aa:bb:cc:dd:ee:ff'}] # Ensure that an exception is raised by a timeout. - mock_vm_crt.side_effect = eventlet.timeout.Timeout() + mock_plug.side_effect = eventlet.timeout.Timeout() # Run method p_vifs = tf_net.PlugVifs(mock.MagicMock(), self.apt, inst, net_info, @@ -237,17 +248,17 @@ class TestNetwork(test.TestCase): p_vifs.execute, self.mock_lpar_wrap) # The create should have only been called once. - self.assertEqual(1, mock_vm_crt.call_count) + self.assertEqual(1, mock_plug.call_count) # Should have called save to save the new host and then changed it back self.assertEqual(2, mock_inst_save.call_count) self.assertEqual('host1', inst.host) - @mock.patch('nova_powervm.virt.powervm.vm.crt_secure_rmc_vif') - @mock.patch('nova_powervm.virt.powervm.vm.get_secure_rmc_vswitch') - @mock.patch('nova_powervm.virt.powervm.vm.crt_vif') + @mock.patch('nova_powervm.virt.powervm.vif.plug_secure_rmc_vif') + @mock.patch('nova_powervm.virt.powervm.vif.get_secure_rmc_vswitch') + @mock.patch('nova_powervm.virt.powervm.vif.plug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') - def test_plug_mgmt_vif(self, mock_vm_get, mock_vm_crt, - mock_get_rmc_vswitch, mock_crt_rmc_vif): + def test_plug_mgmt_vif(self, mock_vm_get, mock_plug, + mock_get_rmc_vswitch, mock_plug_rmc_vif): """Tests that a mgmt vif can be created.""" inst = objects.Instance(**powervm.TEST_INSTANCE) @@ -261,7 +272,7 @@ class TestNetwork(test.TestCase): p_vifs.execute([]) # The create should have only been called once. - self.assertEqual(1, mock_crt_rmc_vif.call_count) + self.assertEqual(1, mock_plug_rmc_vif.call_count) @mock.patch('nova.utils.is_neutron') def test_get_vif_events(self, mock_is_neutron): diff --git a/nova_powervm/tests/virt/powervm/test_driver.py b/nova_powervm/tests/virt/powervm/test_driver.py index ef16988e..197a9d04 100644 --- a/nova_powervm/tests/virt/powervm/test_driver.py +++ b/nova_powervm/tests/virt/powervm/test_driver.py @@ -1187,13 +1187,13 @@ class TestPowerVMDriver(test.TestCase): self.assertIsNotNone(value) @mock.patch('pypowervm.wrappers.logical_partition.LPAR.can_modify_io') - @mock.patch('nova_powervm.virt.powervm.vm.crt_secure_rmc_vif') - @mock.patch('nova_powervm.virt.powervm.vm.get_secure_rmc_vswitch') - @mock.patch('nova_powervm.virt.powervm.vm.crt_vif') + @mock.patch('nova_powervm.virt.powervm.vif.plug_secure_rmc_vif') + @mock.patch('nova_powervm.virt.powervm.vif.get_secure_rmc_vswitch') + @mock.patch('nova_powervm.virt.powervm.vif.plug') @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') def test_plug_vifs( - self, mock_vm_get, mock_vm_crt, mock_get_rmc_vswitch, mock_crt_rmc_vif, - mock_can_modify_io): + self, mock_vm_get, mock_plug_vif, mock_get_rmc_vswitch, + mock_plug_rmc_vif, mock_can_modify_io): # Mock up the CNA response cnas = [mock.MagicMock(), mock.MagicMock()] cnas[0].mac = 'AABBCCDDEEFF' @@ -1220,8 +1220,8 @@ class TestPowerVMDriver(test.TestCase): # The create should have only been called once. The other was already # existing. - self.assertEqual(1, mock_vm_crt.call_count) - self.assertEqual(0, mock_crt_rmc_vif.call_count) + self.assertEqual(1, mock_plug_vif.call_count) + self.assertEqual(0, mock_plug_rmc_vif.call_count) @mock.patch('nova_powervm.virt.powervm.tasks.vm.Get') def test_plug_vif_failures(self, mock_vm): diff --git a/nova_powervm/tests/virt/powervm/test_vif.py b/nova_powervm/tests/virt/powervm/test_vif.py new file mode 100644 index 00000000..71ca4302 --- /dev/null +++ b/nova_powervm/tests/virt/powervm/test_vif.py @@ -0,0 +1,144 @@ +# Copyright 2016 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 nova import exception +from nova import test +from pypowervm.tests import test_fixtures as pvm_fx +from pypowervm.wrappers import managed_system as pvm_ms +from pypowervm.wrappers import network as pvm_net + +from nova_powervm.virt.powervm import vif + + +def cna(mac): + """Builds a mock Client Network Adapter for unit tests.""" + nic = mock.MagicMock() + nic.mac = mac + nic.vswitch_uri = 'fake_href' + return nic + + +class TestVifFunctions(test.TestCase): + + def setUp(self): + super(TestVifFunctions, self).setUp() + + self.adpt = self.useFixture(pvm_fx.AdapterFx( + traits=pvm_fx.LocalPVMTraits)).adpt + + @mock.patch('pypowervm.wrappers.network.VSwitch.search') + def test_get_secure_rmc_vswitch(self, mock_search): + # Test no data coming back gets none + mock_search.return_value = [] + resp = vif.get_secure_rmc_vswitch(self.adpt, 'host_uuid') + self.assertIsNone(resp) + + # Mock that a couple vswitches get returned, but only the correct + # MGMT Switch gets returned + mock_vs = mock.MagicMock() + mock_vs.name = 'MGMTSWITCH' + mock_search.return_value = [mock_vs] + self.assertEqual(mock_vs, + vif.get_secure_rmc_vswitch(self.adpt, 'host_uuid')) + mock_search.assert_called_with( + self.adpt, parent_type=pvm_ms.System.schema_type, + parent_uuid='host_uuid', name=vif.SECURE_RMC_VSWITCH) + + @mock.patch('pypowervm.tasks.cna.crt_cna') + @mock.patch('nova_powervm.virt.powervm.vm.get_pvm_uuid') + def test_plug_secure_rmc_vif(self, mock_pvm_uuid, mock_crt): + mock_pvm_uuid.return_value = 'lpar_uuid' + vif.plug_secure_rmc_vif(self.adpt, 'instance', 'host_uuid') + mock_crt.assert_called_once_with( + self.adpt, 'host_uuid', 'lpar_uuid', 4094, vswitch='MGMTSWITCH', + crt_vswitch=True) + + def test_build_vif_driver(self): + # Test the Shared Ethernet Adapter type VIF + mock_inst = mock.MagicMock() + mock_inst.name = 'instance' + self.assertIsInstance( + vif._build_vif_driver(self.adpt, 'host_uuid', mock_inst, + {'type': 'pvm_sea'}), + vif.PvmSeaVifDriver) + + # Test raises exception for no type + self.assertRaises(exception.VirtualInterfacePlugException, + vif._build_vif_driver, self.adpt, 'host_uuid', + mock_inst, {}) + + # Test an invalid vif type + self.assertRaises(exception.VirtualInterfacePlugException, + vif._build_vif_driver, self.adpt, 'host_uuid', + mock_inst, {'type': 'ovs'}) + + +class TestVifSeaDriver(test.TestCase): + + def setUp(self): + super(TestVifSeaDriver, self).setUp() + + self.adpt = self.useFixture(pvm_fx.AdapterFx( + traits=pvm_fx.LocalPVMTraits)).adpt + self.inst = mock.MagicMock() + self.drv = vif.PvmSeaVifDriver(self.adpt, 'host_uuid', self.inst) + + @mock.patch('nova_powervm.virt.powervm.vm.get_pvm_uuid') + @mock.patch('pypowervm.tasks.cna.crt_cna') + def test_plug(self, mock_crt_cna, mock_pvm_uuid): + """Tests that a VIF can be created.""" + + # Set up the mocks + fake_vif = {'network': {'meta': {'vlan': 5}}, + 'address': 'aabbccddeeff'} + + def validate_crt(adpt, host_uuid, lpar_uuid, vlan, mac_addr=None): + self.assertEqual('host_uuid', host_uuid) + self.assertEqual(5, vlan) + self.assertEqual('aabbccddeeff', mac_addr) + return pvm_net.CNA.bld(self.adpt, 5, host_uuid) + mock_crt_cna.side_effect = validate_crt + + # Invoke + resp = self.drv.plug(fake_vif) + + # Validate (along with validate method above) + self.assertEqual(1, mock_crt_cna.call_count) + self.assertIsNotNone(resp) + self.assertIsInstance(resp, pvm_net.CNA) + + @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') + def test_unplug_vifs(self, mock_vm_get): + """Tests that a delete of the vif can be done.""" + # Mock up the CNA response. Two should already exist, the other + # should not. + cnas = [cna('AABBCCDDEEFF'), cna('AABBCCDDEE11'), cna('AABBCCDDEE22')] + mock_vm_get.return_value = cnas + + # Run method. The AABBCCDDEE11 wont' be unplugged (wasn't invoked + # below) and the last unplug will also just no-op because its not on + # the VM. + self.drv.unplug({'address': 'aa:bb:cc:dd:ee:ff'}) + self.drv.unplug({'address': 'aa:bb:cc:dd:ee:22'}) + self.drv.unplug({'address': 'aa:bb:cc:dd:ee:33'}) + + # The delete should have only been called once. The second CNA didn't + # have a matching mac...so it should be skipped. + self.assertEqual(1, cnas[0].delete.call_count) + self.assertEqual(0, cnas[1].delete.call_count) + self.assertEqual(1, cnas[2].delete.call_count) diff --git a/nova_powervm/tests/virt/powervm/test_vm.py b/nova_powervm/tests/virt/powervm/test_vm.py index b0ce31d1..6306469e 100644 --- a/nova_powervm/tests/virt/powervm/test_vm.py +++ b/nova_powervm/tests/virt/powervm/test_vm.py @@ -1,4 +1,4 @@ -# Copyright 2014, 2015, 2016 IBM Corp. +# Copyright 2014, 2016 IBM Corp. # # All Rights Reserved. # @@ -13,7 +13,6 @@ # 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 logging import mock @@ -29,7 +28,6 @@ from pypowervm.tests import test_fixtures as pvm_fx from pypowervm.tests.test_utils import pvmhttp 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 nova_powervm.tests.virt import powervm from nova_powervm.virt.powervm import vm @@ -418,30 +416,6 @@ class TestVM(test.TestCase): vm.power_off, None, None, 'host_uuid', mock.Mock(state=pvm_bp.LPARState.RUNNING)) - @mock.patch('nova_powervm.virt.powervm.vm.get_pvm_uuid') - @mock.patch('pypowervm.tasks.cna.crt_cna') - def test_crt_vif(self, mock_crt_cna, mock_pvm_uuid): - """Tests that a VIF can be created.""" - - # Set up the mocks - fake_vif = {'network': {'meta': {'vlan': 5}}, - 'address': 'aabbccddeeff'} - - def validate_of_crt(*kargs, **kwargs): - self.assertEqual('fake_host', kargs[1]) - self.assertEqual(5, kargs[3]) - self.assertEqual('aabbccddeeff', kwargs['mac_addr']) - return pvm_net.CNA.bld(self.apt, 5, 'fake_host') - mock_crt_cna.side_effect = validate_of_crt - - # Invoke - resp = vm.crt_vif(self.apt, mock.MagicMock(), 'fake_host', fake_vif) - - # Validate (along with validate method above) - self.assertEqual(1, mock_crt_cna.call_count) - self.assertIsNotNone(resp) - self.assertIsInstance(resp, pvm_net.CNA) - @mock.patch('nova_powervm.virt.powervm.vm.get_pvm_uuid') @mock.patch('nova_powervm.virt.powervm.vm.get_vm_qp') def test_instance_exists(self, mock_getvmqp, mock_getuuid): diff --git a/nova_powervm/virt/powervm/tasks/network.py b/nova_powervm/virt/powervm/tasks/network.py index e7bfba64..76628acc 100644 --- a/nova_powervm/virt/powervm/tasks/network.py +++ b/nova_powervm/virt/powervm/tasks/network.py @@ -27,6 +27,7 @@ from nova_powervm.virt.powervm.i18n import _ from nova_powervm.virt.powervm.i18n import _LE from nova_powervm.virt.powervm.i18n import _LI from nova_powervm.virt.powervm.i18n import _LW +from nova_powervm.virt.powervm import vif from nova_powervm.virt.powervm import vm LOG = logging.getLogger(__name__) @@ -43,18 +44,18 @@ class VirtualInterfaceUnplugException(exception.NovaException): class UnplugVifs(task.Task): """The task to unplug Virtual Network Interfaces from a VM.""" - def __init__(self, adapter, instance, network_info, host_uuid): + def __init__(self, adapter, instance, network_infos, host_uuid): """Create the task. :param adapter: The pypowervm adapter. :param instance: The nova instance. - :param network_info: The network information containing the nova - VIFs to create. + :param network_infos: The network information containing the nova + VIFs to create. :param host_uuid: The host system's PowerVM UUID. """ self.adapter = adapter self.instance = instance - self.network_info = network_info + self.network_infos = network_infos self.host_uuid = host_uuid super(UnplugVifs, self).__init__(name='unplug_vifs', @@ -79,42 +80,17 @@ class UnplugVifs(task.Task): cna_w_list = vm.get_cnas(self.adapter, self.instance, self.host_uuid) # Walk through the VIFs and delete the corresponding CNA on the VM. - for vif in self.network_info: - for cna_w in cna_w_list: - # If the MAC address matched, attempt the delete. - if vm.norm_mac(cna_w.mac) == vif['address']: - LOG.info(_LI('Deleting VIF with mac %(mac)s for instance ' - '%(inst)s.'), {'mac': vif['address'], - 'inst': self.instance.name}, - instance=self.instance) - try: - cna_w.delete() - except Exception as e: - LOG.error(_LE('Unable to unplug VIF with mac %(mac)s ' - 'for instance %(inst)s.'), - {'mac': vif['address'], - 'inst': self.instance.name}, - instance=self.instance) - LOG.error(e) - raise VirtualInterfaceUnplugException() + for network_info in self.network_infos: + vif.unplug(self.adapter, self.host_uuid, self.instance, + network_info, cna_w_list=cna_w_list) - # Break from the loop as we had a successful unplug. - # This prevents from going to 'else' loop. - break - else: - LOG.warning(_LW('Unable to unplug VIF with mac %(mac)s for ' - 'instance %(inst)s. The VIF was not found on ' - 'the instance.'), - {'mac': vif['address'], - 'inst': self.instance.name}, - instance=self.instance) return cna_w_list class PlugVifs(task.Task): """The task to plug the Virtual Network Interfaces to a VM.""" - def __init__(self, virt_api, adapter, instance, network_info, host_uuid): + def __init__(self, virt_api, adapter, instance, network_infos, host_uuid): """Create the task. Provides the 'vm_cnas' - the Virtual Machine's Client Network Adapters. @@ -122,14 +98,14 @@ class PlugVifs(task.Task): :param virt_api: The VirtAPI for the operation. :param adapter: The pypowervm adapter. :param instance: The nova instance. - :param network_info: The network information containing the nova - VIFs to create. + :param network_infos: The network information containing the nova + VIFs to create. :param host_uuid: The host system's PowerVM UUID. """ self.virt_api = virt_api self.adapter = adapter self.instance = instance - self.network_info = network_info + self.network_infos = network_infos self.host_uuid = host_uuid super(PlugVifs, self).__init__(name='plug_vifs', provides='vm_cnas', @@ -143,16 +119,16 @@ class PlugVifs(task.Task): cna_w_list = vm.get_cnas(self.adapter, self.instance, self.host_uuid) # Trim the VIFs down to the ones that haven't yet been created. - crt_vifs = [] - for vif in self.network_info: + crt_network_infos = [] + for network_info in self.network_infos: for cna_w in cna_w_list: - if vm.norm_mac(cna_w.mac) == vif['address']: + if vm.norm_mac(cna_w.mac) == network_info['address']: break else: - crt_vifs.append(vif) + crt_network_infos.append(network_info) # If there are no vifs to create, then just exit immediately. - if len(crt_vifs) == 0: + if len(crt_network_infos) == 0: return [] # Check to see if the LPAR is OK to add VIFs to. @@ -188,14 +164,14 @@ class PlugVifs(task.Task): self.instance, self._get_vif_events(), deadline=CONF.vif_plugging_timeout, error_callback=self._vif_callback_failed): - for vif in crt_vifs: + for network_info in crt_network_infos: LOG.info(_LI('Creating VIF with mac %(mac)s for instance ' '%(sys)s'), - {'mac': vif['address'], + {'mac': network_info['address'], 'sys': self.instance.name}, instance=self.instance) - vm.crt_vif(self.adapter, self.instance, self.host_uuid, - vif) + vif.plug(self.adapter, self.host_uuid, self.instance, + network_info) except eventlet.timeout.Timeout: LOG.error(_LE('Error waiting for VIF to be created for instance ' '%(sys)s'), {'sys': self.instance.name}, @@ -231,9 +207,9 @@ class PlugVifs(task.Task): # more information. if (utils.is_neutron() and CONF.vif_plugging_is_fatal and CONF.vif_plugging_timeout): - return [('network-vif-plugged', vif['id']) - for vif in self.network_info - if not vif.get('active', True)] + return [('network-vif-plugged', network_info['id']) + for network_info in self.network_infos + if not network_info.get('active', True)] else: return [] @@ -265,7 +241,7 @@ class PlugMgmtVif(task.Task): '%s'), self.instance.name, 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_w = vm.get_secure_rmc_vswitch(self.adapter, self.host_uuid) + vswitch_w = vif.get_secure_rmc_vswitch(self.adapter, self.host_uuid) if vswitch_w is None: LOG.debug('No management VIF created for instance %s due to ' 'lack of Management Virtual Switch', @@ -281,5 +257,5 @@ class PlugMgmtVif(task.Task): return None # Return the created management CNA - return vm.crt_secure_rmc_vif(self.adapter, self.instance, - self.host_uuid) + return vif.plug_secure_rmc_vif(self.adapter, self.instance, + self.host_uuid) diff --git a/nova_powervm/virt/powervm/vif.py b/nova_powervm/virt/powervm/vif.py new file mode 100644 index 00000000..d9482984 --- /dev/null +++ b/nova_powervm/virt/powervm/vif.py @@ -0,0 +1,208 @@ +# Copyright 2016 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 abc +import logging +import six + +from nova import exception +from oslo_utils import importutils +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 nova_powervm.virt.powervm.i18n import _ +from nova_powervm.virt.powervm.i18n import _LE +from nova_powervm.virt.powervm.i18n import _LI +from nova_powervm.virt.powervm.i18n import _LW +from nova_powervm.virt.powervm import vm + +LOG = logging.getLogger(__name__) + +SECURE_RMC_VSWITCH = 'MGMTSWITCH' +SECURE_RMC_VLAN = 4094 + +VIF_MAPPING = {'pvm_sea': 'nova_powervm.virt.powervm.vif.PvmSeaVifDriver'} + + +class VirtualInterfaceUnplugException(exception.NovaException): + """Indicates that a VIF unplug failed.""" + # TODO(thorst) symmetrical to the exception in base Nova. Evaluate + # moving to Nova core. + msg_fmt = _("Virtual interface unplug failed") + + +def _build_vif_driver(adapter, host_uuid, instance, vif): + """Returns the appropriate VIF Driver for the given VIF. + + :param adapter: The pypowervm adapter API interface. + :param host_uuid: The host system UUID. + :param instance: The nova instance. + :param vif: The virtual interface to from Nova. + :return: The appropriate PvmVifDriver for the VIF. + """ + if vif.get('type') is None: + raise exception.VirtualInterfacePlugException( + _("vif_type parameter must be present " + "for this vif_driver implementation")) + + # Check the type to the implementations + if VIF_MAPPING.get(vif['type']): + return importutils.import_object( + VIF_MAPPING.get(vif['type']), adapter, host_uuid, instance) + + # No matching implementation, raise error. + raise exception.VirtualInterfacePlugException( + _("Unable to find appropriate PowerVM VIF Driver for VIF type " + "%(vif_type)s on instance %(instance)s") % + {'vif_type': vif['type'], 'instance': instance.name}) + + +def plug(adapter, host_uuid, instance, vif): + """Plugs a virtual interface (network) into a VM. + + :param adapter: The pypowervm adapter. + :param host_uuid: The host UUID for the PowerVM API. + :param instance: The nova instance object. + :param vif: The virtual interface to plug into the instance. + """ + vif_drv = _build_vif_driver(adapter, host_uuid, instance, vif) + vif_drv.plug(vif) + + +def unplug(adapter, host_uuid, instance, vif, cna_w_list=None): + """Unplugs a virtual interface (network) from a VM. + + :param adapter: The pypowervm adapter. + :param host_uuid: The host UUID for the PowerVM API. + :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, host_uuid, instance, vif) + vif_drv.unplug(vif, cna_w_list=cna_w_list) + + +def get_secure_rmc_vswitch(adapter, host_uuid): + """Returns the vSwitch that is used for secure RMC. + + :param adapter: The pypowervm adapter API interface. + :param host_uuid: The host system UUID. + :return: The wrapper for the secure RMC vSwitch. If it does not exist + on the system, None is returned. + """ + vswitches = pvm_net.VSwitch.search( + adapter, parent_type=pvm_ms.System.schema_type, + parent_uuid=host_uuid, name=SECURE_RMC_VSWITCH) + if len(vswitches) == 1: + return vswitches[0] + return None + + +def plug_secure_rmc_vif(adapter, instance, host_uuid): + """Creates the Secure RMC Network Adapter on the VM. + + :param adapter: The pypowervm adapter API interface. + :param instance: The nova instance to create the VIF against. + :param host_uuid: The host system UUID. + :return: The created network adapter wrapper. + """ + lpar_uuid = vm.get_pvm_uuid(instance) + return pvm_cna.crt_cna(adapter, host_uuid, lpar_uuid, SECURE_RMC_VLAN, + vswitch=SECURE_RMC_VSWITCH, crt_vswitch=True) + + +@six.add_metaclass(abc.ABCMeta) +class PvmVifDriver(object): + """Represents an abstract class for a PowerVM Vif Driver. + + A VIF Driver understands a given virtual interface type (network). It + understands how to plug and unplug a given VIF for a virtual machine. + """ + + def __init__(self, adapter, host_uuid, instance): + """Initializes a VIF Driver. + + :param adapter: The pypowervm adapter API interface. + :param host_uuid: The host system UUID. + :param instance: The nova instance that the vif action will be run + against. + """ + self.adapter = adapter + self.host_uuid = host_uuid + self.instance = instance + + @abc.abstractmethod + def plug(self, vif): + """Plugs a virtual interface (network) into a VM. + + :param vif: The virtual interface to plug into the instance. + """ + pass + + def unplug(self, vif, cna_w_list=None): + """Unplugs a virtual interface (network) from a VM. + + :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. + """ + # This is a default implementation that most implementations will + # require. + + # 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, + self.host_uuid) + + for cna_w in cna_w_list: + # If the MAC address matched, attempt the delete. + if vm.norm_mac(cna_w.mac) == vif['address']: + LOG.info(_LI('Deleting VIF with mac %(mac)s for instance ' + '%(inst)s.'), + {'mac': vif['address'], 'inst': self.instance.name}) + try: + cna_w.delete() + except Exception as e: + LOG.error(_LE('Unable to unplug VIF with mac %(mac)s ' + 'for instance %(inst)s.'), + {'mac': vif['address'], + 'inst': self.instance.name}) + LOG.exception(e) + raise VirtualInterfaceUnplugException() + + # Break from the loop as we had a successful unplug. + # This prevents from going to 'else' loop. + break + else: + LOG.warning(_LW('Unable to unplug VIF with mac %(mac)s for ' + 'instance %(inst)s. The VIF was not found on ' + 'the instance.'), + {'mac': vif['address'], 'inst': self.instance.name}) + + +class PvmSeaVifDriver(PvmVifDriver): + """The PowerVM Shared Ethernet Adapter VIF Driver.""" + + def plug(self, vif): + lpar_uuid = vm.get_pvm_uuid(self.instance) + # CNA's require a VLAN. If the network doesn't provide, default to 1 + vlan = vif['network']['meta'].get('vlan', 1) + return pvm_cna.crt_cna(self.adapter, self.host_uuid, lpar_uuid, vlan, + mac_addr=vif['address']) diff --git a/nova_powervm/virt/powervm/vm.py b/nova_powervm/virt/powervm/vm.py index 1c15cd71..e409c869 100644 --- a/nova_powervm/virt/powervm/vm.py +++ b/nova_powervm/virt/powervm/vm.py @@ -26,7 +26,6 @@ from nova.virt import event from nova.virt import hardware from pypowervm import exceptions as pvm_exc from pypowervm.helpers import log_helper as pvm_log -from pypowervm.tasks import cna from pypowervm.tasks import ibmi from pypowervm.tasks import power from pypowervm.tasks import vterm @@ -95,11 +94,6 @@ POWERVM_STOPABLE_STATE = (pvm_bp.LPARState.RUNNING, pvm_bp.LPARState.STARTING, pvm_bp.LPARState.OPEN_FIRMWARE, pvm_bp.LPARState.ERROR, pvm_bp.LPARState.RESUMING) -# Attributes for secure RMC -# TODO(thorst) The name of the secure RMC vswitch will change. -SECURE_RMC_VSWITCH = 'MGMTSWITCH' -SECURE_RMC_VLAN = 4094 - def translate_event(pvm_state, pwr_state): """Translate the PowerVM state and see if it has changed. @@ -708,52 +702,6 @@ def get_cnas(adapter, instance, host_uuid): return pvm_net.CNA.wrap(cna_resp) -def crt_vif(adapter, instance, host_uuid, vif): - """Will create a Client Network Adapter on the system. - - :param adapter: The pypowervm adapter API interface. - :param instance: The nova instance to create the VIF against. - :param host_uuid: The host system UUID. - :param vif: The nova VIF that describes the ethernet interface. - :return: The created network adapter wrapper. - """ - lpar_uuid = get_pvm_uuid(instance) - # CNA's require a VLAN. If the network doesn't provide, default to 1 - vlan = vif['network']['meta'].get('vlan', 1) - return cna.crt_cna(adapter, host_uuid, lpar_uuid, vlan, - mac_addr=vif['address']) - - -def crt_secure_rmc_vif(adapter, instance, host_uuid): - """Creates the Secure RMC Network Adapter on the VM. - - :param adapter: The pypowervm adapter API interface. - :param instance: The nova instance to create the VIF against. - :param host_uuid: The host system UUID. - :return: The created network adapter wrapper. - """ - lpar_uuid = get_pvm_uuid(instance) - return cna.crt_cna(adapter, host_uuid, lpar_uuid, SECURE_RMC_VLAN, - vswitch=SECURE_RMC_VSWITCH, crt_vswitch=True) - - -def get_secure_rmc_vswitch(adapter, host_uuid): - """Returns the vSwitch that is used for secure RMC. - - :param adapter: The pypowervm adapter API interface. - :param host_uuid: The host system UUID. - :return: The wrapper for the secure RMC vSwitch. If it does not exist - on the system, None is returned. - """ - resp = adapter.read(pvm_ms.System.schema_type, root_id=host_uuid, - child_type=pvm_net.VSwitch.schema_type) - vswitches = pvm_net.VSwitch.wrap(resp) - for vswitch in vswitches: - if vswitch.name == SECURE_RMC_VSWITCH: - return vswitch - return None - - def norm_mac(mac): """Normalizes a MAC address from pypowervm format to OpenStack.