Add Trunk Plumber module for Linux Bridge

This adds the module responsible for creating/deleting
VLAN sub-interfaces when passed a trunk object. It handles
parsing of ip link output to find existing VLAN children and
the calls to ip_lib to create/destroy sub-interfaces.

This includes unit tests as well as functional tests.

Partially-Implements: blueprint vlan-aware-vms
Change-Id: I1e3ab69aaff7bca322fa0d738ac74c3dd0dc69b4
This commit is contained in:
Kevin Benton 2016-09-09 01:07:05 -07:00
parent 06361f7daf
commit 19e4b107f0
5 changed files with 428 additions and 0 deletions

View File

@ -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))

View File

@ -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

View File

@ -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: <LOOPBACK,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,MASTER> 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: <BROADCAST,MULTICAST> 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: <BROADCAST,UP,LOWER_UP> 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: <BROADCAST,UP,LOWER_UP> 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: <BROADCAST,UP,LOWER_UP> 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: <BROADCAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST> 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: <BROADCAST,MULTICAST,M-DOWN> 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 <REORDER_HDR>
12: tap39df7d44-b2@tapa962cfc7-9d: <BROADCAST,MULTICAST,M-DOWN> 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 <REORDER_HDR>
13: tap11113d44-3f@tapa962cfc7-9d: <BROADCAST,MULTICAST,M-DOWN> 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 <REORDER_HDR>
14: tap34786ac-28: <BROADCAST,MULTICAST> 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: <BROADCAST,MULTICAST,M-DOWN> 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 <REORDER_HDR>
16: tap47198374-5b@tap34786ac-28: <BROADCAST,MULTICAST,M-DOWN> 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 <REORDER_HDR>
17: tap47198374-5c@tap34786ac-28: <BROADCAST,MULTICAST,M-DOWN> 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 <REORDER_HDR>
""" # noqa