From c8c34eefea83cf69d9528e458818d2e07a3ef5b0 Mon Sep 17 00:00:00 2001 From: Drew Thorstensen Date: Tue, 5 Apr 2016 20:56:29 -0400 Subject: [PATCH] Initial LB VIF Type This is the initial change set that starts the Linux Bridge VIF Type work for PowerVM. It should support connecting to the Neutron Linux Bridge Agent for OpenStack. Change-Id: I93af62a21f6cc22685a7d1f30643475c161265f3 --- nova_powervm/tests/virt/powervm/test_vif.py | 118 ++++++++++--- nova_powervm/virt/powervm/vif.py | 182 ++++++++++++++++---- 2 files changed, 246 insertions(+), 54 deletions(-) diff --git a/nova_powervm/tests/virt/powervm/test_vif.py b/nova_powervm/tests/virt/powervm/test_vif.py index 9a077d47..634616bb 100644 --- a/nova_powervm/tests/virt/powervm/test_vif.py +++ b/nova_powervm/tests/virt/powervm/test_vif.py @@ -15,7 +15,7 @@ # under the License. import mock -import netifaces +from mock import call from nova import exception from nova import test @@ -188,6 +188,86 @@ class TestVifSeaDriver(test.TestCase): self.drv.post_live_migrate_at_source(mock.Mock()) +class TestVifLBDriver(test.TestCase): + + def setUp(self): + super(TestVifLBDriver, self).setUp() + + self.adpt = self.useFixture(pvm_fx.AdapterFx( + traits=pvm_fx.LocalPVMTraits)).adpt + self.inst = mock.MagicMock(uuid='inst_uuid') + self.drv = vif.PvmLBVifDriver(self.adpt, 'host_uuid', self.inst) + + @mock.patch('nova.network.linux_net.LinuxBridgeInterfaceDriver.' + 'ensure_bridge') + @mock.patch('nova.utils.execute') + @mock.patch('nova.network.linux_net.create_ovs_vif_port') + @mock.patch('nova_powervm.virt.powervm.vif.PvmOvsVifDriver.' + 'get_trunk_dev_name') + @mock.patch('pypowervm.tasks.cna.crt_p2p_cna') + @mock.patch('pypowervm.tasks.partition.get_this_partition') + @mock.patch('nova_powervm.virt.powervm.vm.get_pvm_uuid') + def test_plug( + self, mock_pvm_uuid, mock_mgmt_lpar, mock_p2p_cna, + mock_trunk_dev_name, mock_crt_ovs_vif_port, mock_exec, + mock_ensure_bridge): + # 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 + vif = {'network': {'bridge': 'br0'}, 'address': 'aa:bb:cc:dd:ee:ff', + 'id': 'vif_id', 'devname': 'tap_dev'} + self.drv.plug(vif, 6) + + # Validate the calls + mock_p2p_cna.assert_called_once_with( + self.adpt, 'host_uuid', 'lpar_uuid', ['mgmt_uuid'], 'OpenStackOVS', + crt_vswitch=True, mac_addr='aa:bb:cc:dd:ee:ff', dev_name='tap_dev', + slot_num=6) + mock_exec.assert_called_once_with('ip', 'link', 'set', 'tap_dev', 'up', + run_as_root=True) + mock_ensure_bridge.assert_called_once_with('br0', 'tap_dev') + + @mock.patch('nova.utils.execute') + @mock.patch('pypowervm.tasks.cna.find_trunks') + @mock.patch('nova_powervm.virt.powervm.vif.PvmLBVifDriver.' + 'get_trunk_dev_name') + @mock.patch('nova_powervm.virt.powervm.vif.PvmLBVifDriver.' + '_find_cna_for_vif') + @mock.patch('nova_powervm.virt.powervm.vm.get_cnas') + def test_unplug(self, mock_get_cnas, mock_find_cna, mock_trunk_dev_name, + mock_find_trunks, mock_exec): + # Set up the mocks + mock_cna = mock.Mock() + mock_get_cnas.return_value = [mock_cna, mock.Mock()] + mock_find_cna.return_value = mock_cna + + t1 = mock.MagicMock() + mock_find_trunks.return_value = [t1] + + mock_trunk_dev_name.return_value = 'fake_dev' + + # Call the unplug + vif = {'address': 'aa:bb:cc:dd:ee:ff', 'network': {'bridge': 'br0'}} + self.drv.unplug(vif) + + # The trunks and the cna should have been deleted + self.assertTrue(t1.delete.called) + self.assertTrue(mock_cna.delete.called) + + # Validate the execute + call_ip = call('ip', 'link', 'set', 'fake_dev', 'down', + run_as_root=True) + call_delif = call('brctl', 'delif', 'br0', 'fake_dev', + run_as_root=True) + mock_exec.assert_has_calls([call_ip, call_delif]) + + class TestVifOvsDriver(test.TestCase): def setUp(self): @@ -226,33 +306,21 @@ class TestVifOvsDriver(test.TestCase): 'br0', 'device', 'vif_id', 'aa:bb:cc:dd:ee:ff', 'inst_uuid') mock_p2p_cna.assert_called_once_with( self.adpt, 'host_uuid', 'lpar_uuid', ['mgmt_uuid'], 'OpenStackOVS', - crt_vswitch=True, mac_addr='aa:bb:cc:dd:ee:ff', slot_num=slot_num) - mock_exec.assert_called_once_with('ip', 'link', 'set', 'device', 'up', - run_as_root=True) + crt_vswitch=True, mac_addr='aa:bb:cc:dd:ee:ff', slot_num=slot_num, + dev_name='device') + mock_exec.assert_called_with('ip', 'link', 'set', 'device', 'up', + run_as_root=True) - @mock.patch('netifaces.ifaddresses') - @mock.patch('netifaces.interfaces') - def test_get_trunk_dev_name(self, mock_interfaces, mock_ifaddresses): - trunk_w = mock.Mock(mac='01234567890A') + def test_get_trunk_dev_name(self): + mock_vif = {'devname': 'tap_test', 'id': '1234567890123456'} - mock_link_addrs1 = { - netifaces.AF_LINK: [{'addr': '00:11:22:33:44:55'}, - {'addr': '00:11:22:33:44:66'}]} - mock_link_addrs2 = { - netifaces.AF_LINK: [{'addr': '00:11:22:33:44:77'}, - {'addr': '01:23:45:67:89:0a'}]} + # Test when the dev name is available + self.assertEqual('tap_test', self.drv.get_trunk_dev_name(mock_vif)) - mock_interfaces.return_value = ['a', 'b'] - mock_ifaddresses.side_effect = [mock_link_addrs1, mock_link_addrs2] - - # The mock_link_addrs2 (or interface b) should be the match - self.assertEqual('b', self.drv.get_trunk_dev_name(trunk_w)) - - # If you take out the correct adapter, make sure it fails. - mock_interfaces.return_value = ['a'] - mock_ifaddresses.side_effect = [mock_link_addrs1] - self.assertRaises(exception.VirtualInterfacePlugException, - self.drv.get_trunk_dev_name, trunk_w) + # And when it isn't. Should also cut off a few characters from the id + del mock_vif['devname'] + self.assertEqual('nic12345678901', + self.drv.get_trunk_dev_name(mock_vif)) @mock.patch('pypowervm.tasks.cna.find_trunks') @mock.patch('nova.network.linux_net.delete_ovs_vif_port') diff --git a/nova_powervm/virt/powervm/vif.py b/nova_powervm/virt/powervm/vif.py index a8260709..d604e66b 100644 --- a/nova_powervm/virt/powervm/vif.py +++ b/nova_powervm/virt/powervm/vif.py @@ -16,11 +16,11 @@ import abc import logging -import netifaces import six from nova import exception from nova.network import linux_net +from nova.network import model as network_model from nova import utils from oslo_concurrency import lockutils from oslo_config import cfg @@ -42,7 +42,8 @@ SECURE_RMC_VSWITCH = 'MGMTSWITCH' SECURE_RMC_VLAN = 4094 VIF_MAPPING = {'pvm_sea': 'nova_powervm.virt.powervm.vif.PvmSeaVifDriver', - 'ovs': 'nova_powervm.virt.powervm.vif.PvmOvsVifDriver'} + 'ovs': 'nova_powervm.virt.powervm.vif.PvmOvsVifDriver', + 'bridge': 'nova_powervm.virt.powervm.vif.PvmLBVifDriver'} CONF = cfg.CONF @@ -259,6 +260,14 @@ class PvmVifDriver(object): return cna_w def _find_cna_for_vif(self, 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 the MAC address matched, attempt the delete. if vm.norm_mac(cna_w.mac) == vif['address']: @@ -288,6 +297,13 @@ class PvmSeaVifDriver(PvmVifDriver): """The PowerVM Shared Ethernet Adapter VIF Driver.""" def plug(self, vif, slot_num): + """Plugs a virtual interface (network) into a VM. + + This method simply creates the client network adapter into the VM. + + :param vif: The virtual interface to plug into the instance. + :param slot_num: Which slot number to plug the VIF into. May be None. + """ lpar_uuid = vm.get_pvm_uuid(self.instance) # CNA's require a VLAN. If the network doesn't provide, default to 1 @@ -298,53 +314,161 @@ class PvmSeaVifDriver(PvmVifDriver): return cna_w -class PvmOvsVifDriver(PvmVifDriver): - """The Open vSwitch VIF driver for PowerVM.""" +@six.add_metaclass(abc.ABCMeta) +class PvmLioVifDriver(PvmVifDriver): + """An abstract VIF driver that uses Linux I/O to host.""" + + def get_trunk_dev_name(self, vif): + """Returns the device name for the trunk adapter. + + A given VIF in the Linux I/O model will have a trunk adapter and a + client adapter. This will return the trunk adapter's name as it + will appear on the management VM. + + :param vif: The nova network interface + :return: The device name. + """ + if 'devname' in vif: + return vif['devname'] + return ("nic" + vif['id'])[:network_model.NIC_NAME_LEN] def plug(self, vif, slot_num): + """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. + + :param vif: The virtual interface to plug into the instance. + :param slot_num: Which slot number to plug the VIF into. May be None. + """ # Create the trunk and client adapter. lpar_uuid = vm.get_pvm_uuid(self.instance) mgmt_uuid = pvm_par.get_this_partition(self.adapter).uuid - + dev_name = self.get_trunk_dev_name(vif) cna_w, trunk_wraps = pvm_cna.crt_p2p_cna( self.adapter, self.host_uuid, lpar_uuid, [mgmt_uuid], CONF.powervm.pvm_vswitch_for_ovs, crt_vswitch=True, - mac_addr=vif['address'], slot_num=slot_num) + mac_addr=vif['address'], dev_name=dev_name, slot_num=slot_num) + + utils.execute('ip', 'link', 'set', dev_name, 'up', run_as_root=True) + + return cna_w + + +class PvmLBVifDriver(PvmLioVifDriver): + """The Linux Bridge VIF driver for PowerVM.""" + + def plug(self, vif, slot_num): + """Plugs a virtual interface (network) into a VM. + + Extends the base Lio implementation. Will make sure that the bridge + supports the trunk adapter. + + :param vif: The virtual interface to plug into the instance. + :param slot_num: Which slot number to plug the VIF into. May be None. + """ + cna_w = super(PvmLBVifDriver, self).plug(vif, slot_num) + + # Similar to libvirt's vif.py plug_bridge. Need to attach the + # interface to the bridge. + linux_net.LinuxBridgeInterfaceDriver.ensure_bridge( + vif['network']['bridge'], self.get_trunk_dev_name(vif)) + + return cna_w + + def unplug(self, vif, cna_w_list=None): + """Unplugs a virtual interface (network) from a VM. + + Extends the base implementation, but before invoking it will remove + itself from the bridge it is connected to and delete the corresponding + trunk device on the mgmt partition. + + :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. + """ + # 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) + + # Find the CNA for this vif. + cna_w = self._find_cna_for_vif(cna_w_list, vif) + if not cna_w: + 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}) + return None + + # Find and delete the trunk adapters + trunks = pvm_cna.find_trunks(self.adapter, cna_w) + + for trunk in trunks: + dev_name = self.get_trunk_dev_name(vif) + utils.execute('ip', 'link', 'set', dev_name, 'down', + run_as_root=True) + utils.execute('brctl', 'delif', vif['network']['bridge'], + dev_name, run_as_root=True) + trunk.delete() + + # Now delete the client CNA + return super(PvmLBVifDriver, self).unplug(vif, cna_w_list=cna_w_list) + + +class PvmOvsVifDriver(PvmLioVifDriver): + """The Open vSwitch VIF driver for PowerVM.""" + + def plug(self, vif, slot_num): + """Plugs a virtual interface (network) into a VM. + + Extends the Lio implementation. Will make sure that the trunk device + has the appropriate metadata (ex. 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 slot_num: Which slot number to plug the VIF into. May be None. + """ + cna_w = super(PvmOvsVifDriver, self).plug(vif, slot_num) # There will only be one trunk wrap, as we have created with just the # mgmt lpar. Next step is to set the device up and connect to the OVS - dev = self.get_trunk_dev_name(trunk_wraps[0]) - utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True) - linux_net.create_ovs_vif_port(vif['network']['bridge'], dev, + dev_name = self.get_trunk_dev_name(vif) + utils.execute('ip', 'link', 'set', dev_name, 'up', run_as_root=True) + linux_net.create_ovs_vif_port(vif['network']['bridge'], dev_name, self.get_ovs_interfaceid(vif), vif['address'], self.instance.uuid) return cna_w def get_ovs_interfaceid(self, vif): + """Returns the interface id to set for a given VIF. + + When a VIF is plugged for an Open vSwitch, it needs to have the + interface ID set in the OVS metadata. This returns what the + appropriate interface id is. + + :param vif: The Nova network interface. + """ return vif.get('ovs_interfaceid') or vif['id'] - def get_trunk_dev_name(self, trunk_w): - # The mac address from the API is of format: 01234567890A - # We need it in format: 01:23:45:67:89:0a - # That means we need to add colons and lower case it - mac_addr = ":".join(trunk_w.mac[i:i + 2] - for i in range(0, len(trunk_w.mac), 2)).lower() - - # Use netifaces to find the appropriate matching interface name - # TODO(thorst) I don't like this logic. Seems gross. - ifaces = netifaces.interfaces() - for iface in ifaces: - link_addrs = netifaces.ifaddresses(iface)[netifaces.AF_LINK] - for link_addr in link_addrs: - if link_addr.get('addr') == mac_addr: - return iface - - raise exception.VirtualInterfacePlugException( - _("Unable to find appropriate Trunk Device for mac " - "%(mac_addr)s.") % {'mac_addr': mac_addr}) - def unplug(self, vif, cna_w_list=None): + """Unplugs a virtual interface (network) from a VM. + + Extends the base implementation, but before calling it will remove + the adapter from the Open vSwitch and delete the trunk. + + :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. + """ # 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,