diff --git a/neutron/services/trunk/drivers/linuxbridge/agent/__init__.py b/neutron/services/trunk/drivers/linuxbridge/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/trunk/drivers/linuxbridge/agent/trunk_plumber.py b/neutron/services/trunk/drivers/linuxbridge/agent/trunk_plumber.py new file mode 100644 index 00000000000..cb7e10af53e --- /dev/null +++ b/neutron/services/trunk/drivers/linuxbridge/agent/trunk_plumber.py @@ -0,0 +1,176 @@ +# +# 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 re + +from oslo_concurrency import lockutils +from oslo_log import log as logging +from oslo_utils import excutils + +from neutron._i18n import _LW +from neutron.agent.linux import ip_lib +from neutron.common import utils +from neutron.plugins.ml2.drivers.linuxbridge.agent.common import utils as lutil + +LOG = logging.getLogger(__name__) + + +class Plumber(object): + """Object responsible for VLAN interface CRUD. + + This handles the creation/deletion/listing of VLAN interfaces for + a trunk within a namespace. + """ + + def __init__(self, namespace=None): + self.namespace = namespace + + def trunk_on_host(self, trunk): + """Returns true if trunk device is present else False.""" + trunk_dev = self._trunk_device_name(trunk) + return ip_lib.device_exists(trunk_dev, namespace=self.namespace) + + def ensure_trunk_subports(self, trunk): + """Idempotent wiring for a trunk's subports. + + Given a trunk object, delete any vlan subinterfaces belonging to a + trunk that aren't on the object. Create any which are on the object + which do not exist. + """ + trunk_dev = self._trunk_device_name(trunk) + with self._trunk_lock(trunk_dev): + # lock scoped to trunk device so two diffs don't interleave + expected = self._get_subport_devs_and_vlans(trunk.sub_ports) + existing = self._get_vlan_children(trunk_dev) + to_delete = existing - expected + to_create = expected - existing + for devname, vlan_id in to_delete: + LOG.debug("Deleting subport %(name)s with vlan tag %(tag)s", + dict(name=devname, tag=vlan_id)) + self._safe_delete_device(devname) + for devname, vlan_id in to_create: + LOG.debug("Creating subport %(name)s with vlan tag %(tag)s", + dict(name=devname, tag=vlan_id)) + self._create_vlan_subint(trunk_dev, devname, vlan_id) + + def delete_trunk_subports(self, trunk): + return self.delete_subports_by_port_id(trunk.port_id) + + def delete_subports_by_port_id(self, port_id): + device = self._get_tap_device_name(port_id) + if not ip_lib.device_exists(device, namespace=self.namespace): + LOG.debug("Device %s not present on this host", device) + return + with self._trunk_lock(device): + for subname, vlan_id in self._get_vlan_children(device): + LOG.debug("Deleting subport %(name)s with vlan tag %(tag)s", + dict(name=subname, tag=vlan_id)) + self._safe_delete_device(subname) + + def set_port_mac(self, port_id, mac_address): + """Sets mac address of physical device for port_id to mac_address.""" + dev_name = self._get_tap_device_name(port_id) + ipd = ip_lib.IPDevice(dev_name, namespace=self.namespace) + try: + if mac_address == ipd.link.address: + return False + LOG.debug("Changing MAC from %(old)s to %(new)s for device " + "%(dev)s", dict(old=ipd.link.address, new=mac_address, + dev=dev_name)) + ipd.link.set_down() + ipd.link.set_address(mac_address) + ipd.link.set_up() + except Exception: + with excutils.save_and_reraise_exception() as ectx: + ectx.reraise = ip_lib.IPDevice( + dev_name, namespace=self.namespace).exists() + return True + + def _trunk_lock(self, trunk_dev): + lock_name = 'trunk-%s' % trunk_dev + return lockutils.lock(lock_name, utils.SYNCHRONIZED_PREFIX) + + def _create_vlan_subint(self, trunk_name, devname, vlan_id): + ip_wrap = ip_lib.IPWrapper(namespace=self.namespace) + try: + dev = ip_wrap.add_vlan(devname, trunk_name, vlan_id) + dev.disable_ipv6() + except Exception: + with excutils.save_and_reraise_exception() as ectx: + ectx.reraise = ip_lib.IPDevice( + devname, namespace=self.namespace).exists() + + def _safe_delete_device(self, devname): + dev = ip_lib.IPDevice(devname, namespace=self.namespace) + try: + dev.link.set_down() + dev.link.delete() + except Exception: + with excutils.save_and_reraise_exception() as ectx: + ectx.reraise = dev.exists() + + def _trunk_device_name(self, trunk): + return self._get_tap_device_name(trunk.port_id) + + def _get_subport_devs_and_vlans(self, subports): + return {(self._get_tap_device_name(s.port_id), + s.segmentation_id) + for s in subports} + + def _get_tap_device_name(self, devname): + return lutil.get_tap_device_name(devname) + + def _get_vlan_children(self, dev): + """Return set of (devname, vlan_id) tuples for children of device.""" + # TODO(kevinbenton): move into ip-lib after privsep stuff settles + ip_wrapper = ip_lib.IPWrapper(namespace=self.namespace) + output = ip_wrapper.netns.execute(["ip", "-d", "link", "list"], + check_exit_code=True) + return {(i.devname, i.vlan_tag) + for i in _iter_output_by_interface(output) + if i.parent_devname == dev} + + +def _iter_output_by_interface(output): + interface = [] + for line in output.splitlines(): + if not line.startswith(' '): + # no space indicates new interface info + interface_str = ' '.join(interface) + if interface_str.strip(): + yield _InterfaceInfo(interface_str) + interface = [] + interface.append(line) + if interface: + yield _InterfaceInfo(' '.join(interface)) + + +class _InterfaceInfo(object): + def __init__(self, line): + try: + name_section = line.split(': ')[1] + except IndexError: + name_section = None + LOG.warning(_LW("Bad interface line: %s"), line) + if not name_section or '@' not in name_section: + self.devname = name_section + self.parent_devname = self.vlan_tag = None + else: + self.devname, self.parent_devname = name_section.split('@') + m = re.match(r'.*802\.1Q id (\d+).*', line) + self.vlan_tag = int(m.group(1)) if m else None + if self.vlan_tag is None: + LOG.warning(_LW("Failed to parse VLAN from: %s"), line) + + def __repr__(self): + return ('_InterfaceInfo(devname=%s, parent=%s, vlan=%s)' % + (self.devname, self.parent_devname, self.vlan_tag)) diff --git a/neutron/tests/functional/agent/test_l2_lb_agent.py b/neutron/tests/functional/agent/test_l2_lb_agent.py index 74981ade2ac..bde56bad8d3 100644 --- a/neutron/tests/functional/agent/test_l2_lb_agent.py +++ b/neutron/tests/functional/agent/test_l2_lb_agent.py @@ -14,10 +14,15 @@ import mock from oslo_config import cfg +from oslo_utils import uuidutils import testtools +from neutron.agent.linux import ip_lib +from neutron.common import utils +from neutron.objects import trunk from neutron.plugins.ml2.drivers.linuxbridge.agent import \ linuxbridge_neutron_agent +from neutron.services.trunk.drivers.linuxbridge.agent import trunk_plumber from neutron.tests.functional.agent.linux import test_ip_lib lba = linuxbridge_neutron_agent @@ -54,3 +59,100 @@ class LinuxBridgeAgentTests(test_ip_lib.IpLibTestFramework): self.generate_device_details()._replace(namespace=None, name='br-eth1')) lba.LinuxBridgeManager(mappings, {}) + + def test_set_port_mac(self): + attr = self.generate_device_details() + self.manage_device(attr) + plumber = trunk_plumber.Plumber(namespace=attr.namespace) + # force it to return name of above + plumber._get_tap_device_name = lambda x: attr.name + new_mac = utils.get_random_mac('fa:16:3e:00:00:00'.split(':')) + self.assertTrue(plumber.set_port_mac('port_id', new_mac)) + self.assertFalse(plumber.set_port_mac('port_id', new_mac)) + new_mac = utils.get_random_mac('fa:16:3e:00:00:00'.split(':')) + self.assertTrue(plumber.set_port_mac('port_id', new_mac)) + self.assertFalse(plumber.set_port_mac('port_id', new_mac)) + + def test_vlan_subinterfaces(self): + attr = self.generate_device_details() + device = self.manage_device(attr) + devname = device.name + plumber = trunk_plumber.Plumber(namespace=attr.namespace) + for i in range(20): + subname = 'vtest-%s' % i + plumber._create_vlan_subint(devname, subname, i) + # ensure no addresses were assigned (e.g. ipv6) + vlan_int = ip_lib.IPDevice(subname, namespace=attr.namespace) + self.assertFalse(vlan_int.addr.list()) + children = plumber._get_vlan_children(devname) + expected = {('vtest-%s' % i, i) for i in range(20)} + self.assertEqual(expected, children) + + # delete one + plumber._safe_delete_device('vtest-19') + children = plumber._get_vlan_children(devname) + expected = {('vtest-%s' % i, i) for i in range(19)} + self.assertEqual(expected, children) + # ensure they are removed by parent removal + self._safe_delete_device(device) + self.assertFalse(plumber._get_vlan_children(devname)) + + def test_vlan_QinQ_subinterfaces(self): + # the trunk model does not support this right now, but this is to + # the plumber on the agent side doesn't explode in their presense + # in case an operator does something fancy or we have a race where + # a trunk's parent port is converted to a subport while the agent + # is offline. + attr = self.generate_device_details() + device = self.manage_device(attr) + devname = device.name + plumber = trunk_plumber.Plumber(namespace=attr.namespace) + for i in range(20): + plumber._create_vlan_subint(devname, 'vtest-%s' % i, i) + plumber._create_vlan_subint('vtest-%s' % i, 'qinq-%s' % i, 2) + top_level = {('vtest-%s' % i, i) for i in range(20)} + for i in range(20): + # as we iterate, we delete a vlan from each dev and ensure it + # didn't break the top-level vlans + self.assertEqual({('qinq-%s' % i, 2)}, + plumber._get_vlan_children('vtest-%s' % i)) + plumber._safe_delete_device('qinq-%s' % i) + self.assertEqual(set(), plumber._get_vlan_children('vtest-%i' % i)) + self.assertEqual(top_level, plumber._get_vlan_children(devname)) + + def test_ensure_trunk_subports(self): + attr = self.generate_device_details() + device = self.manage_device(attr) + devname = device.name + plumber = trunk_plumber.Plumber(namespace=attr.namespace) + plumber._trunk_device_name = lambda x: devname + trunk_obj = self._gen_trunk() + plumber.ensure_trunk_subports(trunk_obj) + # ensure no mutation the second time + with mock.patch.object(plumber, '_safe_delete_device', + side_effect=RuntimeError()): + plumber.ensure_trunk_subports(trunk_obj) + + while trunk_obj.sub_ports: + # drain down the sub-ports and make sure it keeps + # them equal + trunk_obj.sub_ports.pop() + plumber.ensure_trunk_subports(trunk_obj) + expected = {(plumber._get_tap_device_name(sp.port_id), + sp.segmentation_id) + for sp in trunk_obj.sub_ports} + wired = plumber._get_vlan_children(devname) + self.assertEqual(expected, wired) + + def _gen_trunk(self): + trunk_obj = trunk.Trunk(id=uuidutils.generate_uuid(), + port_id=uuidutils.generate_uuid(), + tenant_id=uuidutils.generate_uuid()) + subports = [trunk.SubPort(id=uuidutils.generate_uuid(), + port_id=uuidutils.generate_uuid(), + segmentation_type='vlan', + trunk_id=trunk_obj.id, + segmentation_id=i) + for i in range(20, 40)] + trunk_obj.sub_ports = subports + return trunk_obj diff --git a/neutron/tests/unit/services/trunk/drivers/linuxbridge/agent/__init__.py b/neutron/tests/unit/services/trunk/drivers/linuxbridge/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/services/trunk/drivers/linuxbridge/agent/test_trunk_plumber.py b/neutron/tests/unit/services/trunk/drivers/linuxbridge/agent/test_trunk_plumber.py new file mode 100644 index 00000000000..b9bdc5bc260 --- /dev/null +++ b/neutron/tests/unit/services/trunk/drivers/linuxbridge/agent/test_trunk_plumber.py @@ -0,0 +1,150 @@ +# +# 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_utils import uuidutils + +from neutron.objects import trunk +from neutron.services.trunk.drivers.linuxbridge.agent import trunk_plumber +from neutron.tests import base + + +class PlumberTestCase(base.BaseTestCase): + def setUp(self): + self.plumber = trunk_plumber.Plumber() + self.get_tap_device_name = mock.patch.object( + self.plumber, '_get_tap_device_name', + return_value='devname').start() + self.trunk = trunk.Trunk() + self.trunk.port_id = uuidutils.generate_uuid() + self.trunk.sub_ports = [] + self.device_exists = mock.patch.object(trunk_plumber.ip_lib, + 'device_exists').start() + self.device_exists.return_value = True + ipwrap = mock.patch.object(trunk_plumber.ip_lib, 'IPWrapper').start() + ipwrap.return_value.netns.execute.return_value = IP_LINK_OUTPUT + super(PlumberTestCase, self).setUp() + + def test_trunk_on_host(self): + self.assertTrue(self.plumber.trunk_on_host(self.trunk)) + self.device_exists.return_value = False + self.assertFalse(self.plumber.trunk_on_host(self.trunk)) + + def test_ensure_trunk_subports(self): + trunk_vals = set([('dev2', 23), ('dev3', 44), ('dev4', 45)]) + existing_vals = set([('dev1', 21), ('dev2', 23), ('dev3', 45)]) + mock.patch.object(self.plumber, '_get_subport_devs_and_vlans', + return_value=trunk_vals).start() + mock.patch.object(self.plumber, '_get_vlan_children', + return_value=existing_vals).start() + delete = mock.patch.object(self.plumber, '_safe_delete_device').start() + create = mock.patch.object(self.plumber, '_create_vlan_subint').start() + self.plumber.ensure_trunk_subports(self.trunk) + # dev1 is gone and dev3 changed vlans + delete.assert_has_calls([mock.call('dev3'), mock.call('dev1')], + any_order=True) + create.assert_has_calls([mock.call('devname', 'dev4', 45), + mock.call('devname', 'dev3', 44)], + any_order=True) + + def test_delete_trunk_subports(self): + existing_vals = set([('dev1', 21), ('dev2', 23), ('dev3', 45)]) + mock.patch.object(self.plumber, '_get_vlan_children', + return_value=existing_vals).start() + delete = mock.patch.object(self.plumber, '_safe_delete_device').start() + self.plumber.delete_trunk_subports(self.trunk) + delete.assert_has_calls([mock.call('dev3'), mock.call('dev2'), + mock.call('dev1')], + any_order=True) + + def test_set_port_mac(self): + ipd = mock.patch.object(trunk_plumber.ip_lib, 'IPDevice').start() + ipdi = ipd.return_value + self.plumber.set_port_mac('port_id', mac_address='44') + ipdi.link.set_address.assert_called_once_with('44') + ipdi.exists.return_value = False + ipdi.link.set_address.side_effect = ValueError() + # exception suppressed since it no longer 'exists' + self.plumber.set_port_mac('port_id', mac_address='44') + + def test__get_vlan_children(self): + expected = [('tap47198374-5a', 777), + ('tap47198374-5b', 2), + ('tap47198374-5c', 3)] + self.assertEqual(set(expected), + self.plumber._get_vlan_children('tap34786ac-28')) + expected = [('tap39df7d39-c5', 99), + ('tap39df7d44-b2', 904), + ('tap11113d44-3f', 777)] + self.assertEqual(set(expected), + self.plumber._get_vlan_children('tapa962cfc7-9d')) + # vlan sub-interface and non-trunk shouldn't have children + self.assertEqual(set(), + self.plumber._get_vlan_children('tap47198374-5c')) + self.assertEqual(set(), + self.plumber._get_vlan_children('br-int')) + + def test__iter_output_by_interface(self): + iterator = trunk_plumber._iter_output_by_interface(IP_LINK_OUTPUT) + names = [i.devname for i in iterator] + expected = ['lo', 'eth0', 'bond0', 'ovs-system', 'br-ex', + 'testb9cfb5d7', 'br-int', 'br-tun', 'tapa962cfc7-9d', + 'tap39df7d39-c5', 'tap39df7d44-b2', 'tap11113d44-3f', + 'tap34786ac-28', 'tap47198374-5a', 'tap47198374-5b', + 'tap47198374-5c'] + self.assertEqual(expected, names) + +IP_LINK_OUTPUT = """ +1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 +2: eth0: mtu 1500 qdisc pfifo_fast state UP mode DEFA + link/ether 00:0c:29:10:68:04 brd ff:ff:ff:ff:ff:ff promiscuity 0 +3: bond0: mtu 1500 qdisc noop state DOWN mode DEFAULT grou + link/ether 5e:dc:86:6f:b7:19 brd ff:ff:ff:ff:ff:ff promiscuity 0 + bond +4: ovs-system: mtu 1500 qdisc noop state DOWN mode DEFAULT group + link/ether 5a:95:a1:b9:42:25 brd ff:ff:ff:ff:ff:ff promiscuity 1 +5: br-ex: mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT gro + link/ether be:cc:4f:f7:28:48 brd ff:ff:ff:ff:ff:ff promiscuity 1 +6: testb9cfb5d7: mtu 1500 qdisc noqueue state UNKNOWN mode DEFA + link/ether 82:90:49:84:32:47 brd ff:ff:ff:ff:ff:ff promiscuity 1 +7: br-int: mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT gr + link/ether 5a:5e:7d:02:7c:4d brd ff:ff:ff:ff:ff:ff promiscuity 1 +8: br-tun: mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT gr + link/ether 76:d8:a5:16:d7:4a brd ff:ff:ff:ff:ff:ff promiscuity 1 +10: tapa962cfc7-9d: mtu 1500 qdisc noop state DOWN mode DEFAULT g + link/ether 9a:31:1d:cc:b3:86 brd ff:ff:ff:ff:ff:ff promiscuity 0 + tun +11: tap39df7d39-c5@tapa962cfc7-9d: mtu 1500 qdisc noop sta + link/ether 9a:31:1d:cc:b3:86 brd ff:ff:ff:ff:ff:ff promiscuity 0 + vlan protocol 802.1Q id 99 +12: tap39df7d44-b2@tapa962cfc7-9d: mtu 1500 qdisc noop sta + link/ether 9a:31:1d:cc:b3:86 brd ff:ff:ff:ff:ff:ff promiscuity 0 + vlan protocol 802.1Q id 904 +13: tap11113d44-3f@tapa962cfc7-9d: mtu 1500 qdisc noop sta + link/ether 9a:31:1d:cc:b3:86 brd ff:ff:ff:ff:ff:ff promiscuity 0 + vlan protocol 802.1Q id 777 +14: tap34786ac-28: mtu 1500 qdisc noop state DOWN mode DEFAULT gr + link/ether f6:07:9f:11:4c:dc brd ff:ff:ff:ff:ff:ff promiscuity 0 + tun +15: tap47198374-5a@tap34786ac-28: mtu 1500 qdisc noop stat + link/ether f6:07:9f:11:4c:dc brd ff:ff:ff:ff:ff:ff promiscuity 0 + vlan protocol 802.1Q id 777 +16: tap47198374-5b@tap34786ac-28: mtu 1500 qdisc noop stat + link/ether f6:07:9f:11:4c:dc brd ff:ff:ff:ff:ff:ff promiscuity 0 + vlan protocol 802.1Q id 2 +17: tap47198374-5c@tap34786ac-28: mtu 1500 qdisc noop stat + link/ether f6:07:9f:11:4c:dc brd ff:ff:ff:ff:ff:ff promiscuity 0 + vlan protocol 802.1Q id 3 +""" # noqa