From dbeb79504c8b706ba5fa18144f4e7a0d23586d5d Mon Sep 17 00:00:00 2001 From: Gary Kotton Date: Thu, 23 Aug 2012 04:57:26 -0400 Subject: [PATCH] Ensure network connectivity for linuxbridge flat network. Fixes bug 1040447 In the event that an IP address and gateway are defined on an interface that is used in a flat network, then the interface details will be copied to the bridge when it is created. If the network is deleted then the IP addresses are returned to the interface. Change-Id: Ic860d7635c137f49e5aaaa25dc9727ac60227f1a --- quantum/agent/linux/ip_lib.py | 53 ++++++++++++- .../agent/linuxbridge_quantum_agent.py | 58 ++++++++++++-- quantum/tests/unit/test_linux_ip_lib.py | 78 +++++++++++++++++-- 3 files changed, 176 insertions(+), 13 deletions(-) diff --git a/quantum/agent/linux/ip_lib.py b/quantum/agent/linux/ip_lib.py index 9d548b9d9..c89faf4c9 100644 --- a/quantum/agent/linux/ip_lib.py +++ b/quantum/agent/linux/ip_lib.py @@ -110,6 +110,7 @@ class IPDevice(SubProcessBase): self.name = name self.link = IpLinkCommand(self) self.addr = IpAddrCommand(self) + self.route = IpRouteCommand(self) def __eq__(self, other): return (other is not None and self.name == other.name @@ -227,7 +228,10 @@ class IpAddrCommand(IpDeviceCommandBase): def flush(self): self._as_root('flush', self.name) - def list(self, scope=None, to=None, filters=[]): + def list(self, scope=None, to=None, filters=None): + if filters is None: + filters = [] + retval = [] if scope: @@ -243,17 +247,64 @@ class IpAddrCommand(IpDeviceCommandBase): if parts[0] == 'inet6': version = 6 scope = parts[3] + broadcast = '::' else: version = 4 + broadcast = parts[3] scope = parts[5] retval.append(dict(cidr=parts[1], + broadcast=broadcast, scope=scope, ip_version=version, dynamic=('dynamic' == parts[-1]))) return retval +class IpRouteCommand(IpDeviceCommandBase): + COMMAND = 'route' + + def add_gateway(self, gateway, metric=None): + args = ['add', 'default', 'via', gateway] + if metric: + args += ['metric', metric] + args += ['dev', self.name] + self._as_root(*args) + + def delete_gateway(self, gateway): + self._as_root('del', + 'default', + 'via', + gateway, + 'dev', + self.name) + + def get_gateway(self, scope=None, filters=None): + if filters is None: + filters = [] + + retval = None + + if scope: + filters += ['scope', scope] + + route_list_lines = self._run('list', 'dev', self.name, + *filters).split('\n') + default_route_line = next((x.strip() for x in + route_list_lines if + x.strip().startswith('default')), None) + if default_route_line: + gateway_index = 2 + parts = default_route_line.split() + retval = dict(gateway=parts[gateway_index]) + metric_index = 4 + parts_has_metric = (len(parts) > metric_index) + if parts_has_metric: + retval.update(metric=int(parts[metric_index])) + + return retval + + class IpNetnsCommand(IpCommandBase): COMMAND = 'netns' diff --git a/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py b/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py index 78f09e449..06778cce2 100755 --- a/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py +++ b/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py @@ -34,6 +34,7 @@ import eventlet import pyudev from sqlalchemy.ext.sqlsoup import SqlSoup +from quantum.agent.linux import ip_lib from quantum.agent.linux import utils from quantum.agent import rpc as agent_rpc from quantum.common import config as logging_config @@ -65,6 +66,7 @@ class LinuxBridge: def __init__(self, interface_mappings, root_helper): self.interface_mappings = interface_mappings self.root_helper = root_helper + self.ip = ip_lib.IPWrapper(self.root_helper) def device_exists(self, device): """Check if ethernet device exists.""" @@ -175,10 +177,19 @@ class LinuxBridge: self.ensure_bridge(bridge_name, interface) return interface + def get_interface_details(self, interface): + device = self.ip.device(interface) + ips = device.addr.list(scope='global') + + # Update default gateway if necessary + gateway = device.route.get_gateway(scope='global') + return ips, gateway + def ensure_flat_bridge(self, network_id, physical_interface): """Create a non-vlan bridge unless it already exists.""" bridge_name = self.get_bridge_name(network_id) - self.ensure_bridge(bridge_name, physical_interface) + ips, gateway = self.get_interface_details(physical_interface) + self.ensure_bridge(bridge_name, physical_interface, ips, gateway) return physical_interface def ensure_vlan(self, physical_interface, vlan_id): @@ -198,7 +209,33 @@ class LinuxBridge: LOG.debug("Done creating subinterface %s" % interface) return interface - def ensure_bridge(self, bridge_name, interface): + def update_interface_ip_details(self, destination, source, ips, + gateway): + if ips or gateway: + dst_device = self.ip.device(destination) + src_device = self.ip.device(source) + + # Append IP's to bridge if necessary + for ip in ips: + dst_device.addr.add(ip_version=ip['ip_version'], + cidr=ip['cidr'], + broadcast=ip['broadcast']) + + if gateway: + # Ensure that the gateway can be updated by changing the metric + metric = 100 + if 'metric' in gateway: + metric = gateway['metric'] - 1 + dst_device.route.add_gateway(gateway=gateway['gateway'], + metric=metric) + src_device.route.delete_gateway(gateway=gateway['gateway']) + + # Remove IP's from interface + for ip in ips: + src_device.addr.delete(ip_version=ip['ip_version'], + cidr=ip['cidr']) + + def ensure_bridge(self, bridge_name, interface, ips=None, gateway=None): """ Create a bridge unless it already exists. """ @@ -220,6 +257,9 @@ class LinuxBridge: LOG.debug("Done starting bridge %s for subinterface %s" % (bridge_name, interface)) + # Update IP info if necessary + self.update_interface_ip_details(bridge_name, interface, ips, gateway) + # Check if the interface is part of the bridge if not self.interface_exists_on_bridge(bridge_name, interface): try: @@ -296,8 +336,16 @@ class LinuxBridge: for interface in interfaces_on_bridge: self.remove_interface(bridge_name, interface) for physical_interface in self.interface_mappings.itervalues(): - if interface.startswith(physical_interface): - self.delete_vlan(interface) + if physical_interface == interface: + # This is a flat network => return IP's from bridge to + # interface + ips, gateway = self.get_interface_details(bridge_name) + self.update_interface_ip_details(interface, + bridge_name, + ips, gateway) + else: + if interface.startswith(physical_interface): + self.delete_vlan(interface) LOG.debug("Deleting bridge %s" % bridge_name) if utils.execute(['ip', 'link', 'set', bridge_name, 'down'], @@ -353,8 +401,8 @@ class LinuxBridgeRpcCallbacks(): LOG.debug("network_delete received") network_id = kwargs.get('network_id') bridge_name = self.linux_br.get_bridge_name(network_id) - # (TODO) garyk delete the bridge interface LOG.debug("Delete %s", bridge_name) + self.linux_br.delete_vlan_bridge(bridge_name) def port_update(self, context, **kwargs): LOG.debug("port_update received") diff --git a/quantum/tests/unit/test_linux_ip_lib.py b/quantum/tests/unit/test_linux_ip_lib.py index cb432e7f0..51a9d13ca 100644 --- a/quantum/tests/unit/test_linux_ip_lib.py +++ b/quantum/tests/unit/test_linux_ip_lib.py @@ -55,6 +55,23 @@ ADDR_SAMPLE = (""" valid_lft forever preferred_lft forever """) +GATEWAY_SAMPLE1 = (""" +default via 10.35.19.254 metric 100 +10.35.16.0/22 proto kernel scope link src 10.35.17.97 +""") + +GATEWAY_SAMPLE2 = (""" +default via 10.35.19.254 metric 100 +""") + +GATEWAY_SAMPLE3 = (""" +10.35.16.0/22 proto kernel scope link src 10.35.17.97 +""") + +GATEWAY_SAMPLE4 = (""" +default via 10.35.19.254 +""") + class TestSubProcessBase(unittest.TestCase): def setUp(self): @@ -383,17 +400,23 @@ class TestIpAddrCommand(TestIPCmdBase): def test_list(self): expected = [ dict(ip_version=4, scope='global', - dynamic=False, cidr='172.16.77.240/24'), + dynamic=False, cidr='172.16.77.240/24', + broadcast='172.16.77.255'), dict(ip_version=6, scope='global', - dynamic=True, cidr='2001:470:9:1224:5595:dd51:6ba2:e788/64'), + dynamic=True, cidr='2001:470:9:1224:5595:dd51:6ba2:e788/64', + broadcast='::'), dict(ip_version=6, scope='global', - dynamic=True, cidr='2001:470:9:1224:fd91:272:581e:3a32/64'), + dynamic=True, cidr='2001:470:9:1224:fd91:272:581e:3a32/64', + broadcast='::'), dict(ip_version=6, scope='global', - dynamic=True, cidr='2001:470:9:1224:4508:b885:5fb:740b/64'), + dynamic=True, cidr='2001:470:9:1224:4508:b885:5fb:740b/64', + broadcast='::'), dict(ip_version=6, scope='global', - dynamic=True, cidr='2001:470:9:1224:dfcc:aaff:feb9:76ce/64'), + dynamic=True, cidr='2001:470:9:1224:dfcc:aaff:feb9:76ce/64', + broadcast='::'), dict(ip_version=6, scope='link', - dynamic=False, cidr='fe80::dfcc:aaff:feb9:76ce/64')] + dynamic=False, cidr='fe80::dfcc:aaff:feb9:76ce/64', + broadcast='::')] self.parent._run = mock.Mock(return_value=ADDR_SAMPLE) self.assertEquals(self.addr_cmd.list(), expected) @@ -402,7 +425,8 @@ class TestIpAddrCommand(TestIPCmdBase): def test_list_filtered(self): expected = [ dict(ip_version=4, scope='global', - dynamic=False, cidr='172.16.77.240/24')] + dynamic=False, cidr='172.16.77.240/24', + broadcast='172.16.77.255')] output = '\n'.join(ADDR_SAMPLE.split('\n')[0:4]) self.parent._run.return_value = output @@ -411,6 +435,46 @@ class TestIpAddrCommand(TestIPCmdBase): self._assert_call([], ('show', 'tap0', 'permanent', 'scope', 'global')) +class TestIpRouteCommand(TestIPCmdBase): + def setUp(self): + super(TestIpRouteCommand, self).setUp() + self.parent.name = 'eth0' + self.command = 'route' + self.route_cmd = ip_lib.IpRouteCommand(self.parent) + + def test_add_gateway(self): + gateway = '192.168.45.100' + metric = 100 + self.route_cmd.add_gateway(gateway, metric) + self._assert_sudo([], + ('add', 'default', 'via', gateway, + 'metric', metric, + 'dev', self.parent.name)) + + def test_del_gateway(self): + gateway = '192.168.45.100' + self.route_cmd.delete_gateway(gateway) + self._assert_sudo([], + ('del', 'default', 'via', gateway, + 'dev', self.parent.name)) + + def test_get_gateway(self): + test_cases = [{'sample': GATEWAY_SAMPLE1, + 'expected': {'gateway':'10.35.19.254', + 'metric': 100}}, + {'sample': GATEWAY_SAMPLE2, + 'expected': {'gateway':'10.35.19.254', + 'metric': 100}}, + {'sample': GATEWAY_SAMPLE3, + 'expected': None}, + {'sample': GATEWAY_SAMPLE4, + 'expected': {'gateway': '10.35.19.254'}}] + for test_case in test_cases: + self.parent._run = mock.Mock(return_value=test_case['sample']) + self.assertEquals(self.route_cmd.get_gateway(), + test_case['expected']) + + class TestIpNetnsCommand(TestIPCmdBase): def setUp(self): super(TestIpNetnsCommand, self).setUp() -- 2.45.2