From 5888af0815c6ce58c5cdb4d2a2fcfc4c08b09a65 Mon Sep 17 00:00:00 2001 From: "Daniel P. Berrange" Date: Wed, 27 Jan 2016 15:55:55 +0000 Subject: [PATCH] import linux bridge plugin implementation While most of the vendor plugins will be in separate repositories, the os-vif library will include the Linux bridge plugin as one of the reference implementations. Change-Id: If53e1e987991a9695ff4390bb9a52d7a80c0e2ee --- setup.cfg | 5 + vif_plug_linux_bridge/__init__.py | 0 vif_plug_linux_bridge/iptables.py | 534 +++++++++++++++++++++ vif_plug_linux_bridge/linux_bridge.py | 108 +++++ vif_plug_linux_bridge/linux_net.py | 195 ++++++++ vif_plug_linux_bridge/tests/__init__.py | 0 vif_plug_linux_bridge/tests/test_plugin.py | 115 +++++ 7 files changed, 957 insertions(+) create mode 100644 vif_plug_linux_bridge/__init__.py create mode 100644 vif_plug_linux_bridge/iptables.py create mode 100644 vif_plug_linux_bridge/linux_bridge.py create mode 100644 vif_plug_linux_bridge/linux_net.py create mode 100644 vif_plug_linux_bridge/tests/__init__.py create mode 100644 vif_plug_linux_bridge/tests/test_plugin.py diff --git a/setup.cfg b/setup.cfg index f1e5c143..862e5efa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ setup-hooks = [files] packages = os_vif + vif_plug_linux_bridge [egg_info] tag_build = @@ -52,3 +53,7 @@ input_file = os_vif/locale/os-vif.pot keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg output_file = os_vif/locale/os-vif.pot + +[entry_points] +os_vif = + linux_bridge = vif_plug_linux_bridge.linux_bridge:LinuxBridgePlugin diff --git a/vif_plug_linux_bridge/__init__.py b/vif_plug_linux_bridge/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vif_plug_linux_bridge/iptables.py b/vif_plug_linux_bridge/iptables.py new file mode 100644 index 00000000..f7e14e6f --- /dev/null +++ b/vif_plug_linux_bridge/iptables.py @@ -0,0 +1,534 @@ +# Derived from nova/network/linux_net.py +# +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +# TODO(jaypipes): Replace this entire module with use of the python-iptables +# library: https://github.com/ldx/python-iptables + +import inspect +import os +import re + +from oslo_concurrency import lockutils +from oslo_concurrency import processutils + +import six + + +# NOTE(vish): Iptables supports chain names of up to 28 characters, and we +# add up to 12 characters to binary_name which is used as a prefix, +# so we limit it to 16 characters. +# (max_chain_name_length - len('-POSTROUTING') == 16) +def get_binary_name(): + """Grab the name of the binary we're running in.""" + return os.path.basename(inspect.stack()[-1][1])[:16] + +binary_name = get_binary_name() + + +class IptablesRule(object): + """An iptables rule. + + You shouldn't need to use this class directly, it's only used by + IptablesManager. + + """ + + def __init__(self, chain, rule, wrap=True, top=False): + self.chain = chain + self.rule = rule + self.wrap = wrap + self.top = top + + def __eq__(self, other): + return ((self.chain == other.chain) and + (self.rule == other.rule) and + (self.top == other.top) and + (self.wrap == other.wrap)) + + def __ne__(self, other): + return not self == other + + def __repr__(self): + if self.wrap: + chain = '%s-%s' % (binary_name, self.chain) + else: + chain = self.chain + # new rules should have a zero [packet: byte] count + return '[0:0] -A %s %s' % (chain, self.rule) + + +class IptablesTable(object): + """An iptables table.""" + + def __init__(self): + self.rules = [] + self.remove_rules = [] + self.chains = set() + self.unwrapped_chains = set() + self.remove_chains = set() + self.dirty = True + + def has_chain(self, name, wrap=True): + if wrap: + return name in self.chains + else: + return name in self.unwrapped_chains + + def add_chain(self, name, wrap=True): + """Adds a named chain to the table. + + The chain name is wrapped to be unique for the component creating + it, so different components of Nova can safely create identically + named chains without interfering with one another. + + At the moment, its wrapped name is -, + so if nova-compute creates a chain named 'OUTPUT', it'll actually + end up named 'nova-compute-OUTPUT'. + + """ + if wrap: + self.chains.add(name) + else: + self.unwrapped_chains.add(name) + self.dirty = True + + def remove_chain(self, name, wrap=True): + """Remove named chain. + + This removal "cascades". All rule in the chain are removed, as are + all rules in other chains that jump to it. + + If the chain is not found, this is merely logged. + + """ + if wrap: + chain_set = self.chains + else: + chain_set = self.unwrapped_chains + + if name not in chain_set: + return + + self.dirty = True + + # non-wrapped chains and rules need to be dealt with specially, + # so we keep a list of them to be iterated over in apply() + if not wrap: + self.remove_chains.add(name) + chain_set.remove(name) + if not wrap: + self.remove_rules += filter(lambda r: r.chain == name, self.rules) + self.rules = filter(lambda r: r.chain != name, self.rules) + + if wrap: + jump_snippet = '-j %s-%s' % (binary_name, name) + else: + jump_snippet = '-j %s' % (name,) + + if not wrap: + self.remove_rules += filter(lambda r: jump_snippet in r.rule, + self.rules) + self.rules = filter(lambda r: jump_snippet not in r.rule, self.rules) + + def add_rule(self, chain, rule, wrap=True, top=False): + """Add a rule to the table. + + This is just like what you'd feed to iptables, just without + the '-A ' bit at the start. + + However, if you need to jump to one of your wrapped chains, + prepend its name with a '$' which will ensure the wrapping + is applied correctly. + + """ + if wrap and chain not in self.chains: + raise ValueError(_('Unknown chain: %r') % chain) + + if '$' in rule: + rule = ' '.join(map(self._wrap_target_chain, rule.split(' '))) + + rule_obj = IptablesRule(chain, rule, wrap, top) + if rule_obj not in self.rules: + self.rules.append(IptablesRule(chain, rule, wrap, top)) + self.dirty = True + + def _wrap_target_chain(self, s): + if s.startswith('$'): + return '%s-%s' % (binary_name, s[1:]) + return s + + def remove_rule(self, chain, rule, wrap=True, top=False): + """Remove a rule from a chain. + + Note: The rule must be exactly identical to the one that was added. + You cannot switch arguments around like you can with the iptables + CLI tool. + + """ + try: + self.rules.remove(IptablesRule(chain, rule, wrap, top)) + if not wrap: + self.remove_rules.append(IptablesRule(chain, rule, wrap, top)) + self.dirty = True + except ValueError: + pass + + def remove_rules_regex(self, regex): + """Remove all rules matching regex.""" + if isinstance(regex, six.string_types): + regex = re.compile(regex) + num_rules = len(self.rules) + self.rules = filter(lambda r: not regex.match(str(r)), self.rules) + removed = num_rules - len(self.rules) + if removed > 0: + self.dirty = True + return removed + + def empty_chain(self, chain, wrap=True): + """Remove all rules from a chain.""" + chained_rules = [rule for rule in self.rules + if rule.chain == chain and rule.wrap == wrap] + if chained_rules: + self.dirty = True + for rule in chained_rules: + self.rules.remove(rule) + + +class IptablesManager(object): + """Wrapper for iptables. + + See IptablesTable for some usage docs + + A number of chains are set up to begin with. + + First, nova-filter-top. It's added at the top of FORWARD and OUTPUT. Its + name is not wrapped, so it's shared between the various nova workers. It's + intended for rules that need to live at the top of the FORWARD and OUTPUT + chains. It's in both the ipv4 and ipv6 set of tables. + + For ipv4 and ipv6, the built-in INPUT, OUTPUT, and FORWARD filter chains + are wrapped, meaning that the "real" INPUT chain has a rule that jumps to + the wrapped INPUT chain, etc. Additionally, there's a wrapped chain named + "local" which is jumped to from nova-filter-top. + + For ipv4, the built-in PREROUTING, OUTPUT, and POSTROUTING nat chains are + wrapped in the same was as the built-in filter chains. Additionally, + there's a snat chain that is applied after the POSTROUTING chain. + + """ + + def __init__(self, use_ipv6=False, iptables_top_regex=None, + iptables_bottom_regex=None, iptables_drop_action='DROP', + forward_bridge_interface=None): + self.use_ipv6 = use_ipv6 + self.iptables_top_regex = iptables_top_regex + self.iptables_bottom_regex = iptables_bottom_regex + self.iptables_drop_action = iptables_drop_action + self.forward_bridge_interface = forward_bridge_interface or ['all'] + self.ipv4 = {'filter': IptablesTable(), + 'nat': IptablesTable(), + 'mangle': IptablesTable()} + self.ipv6 = {'filter': IptablesTable()} + + self.iptables_apply_deferred = False + + # Add a nova-filter-top chain. It's intended to be shared + # among the various nova components. It sits at the very top + # of FORWARD and OUTPUT. + for tables in [self.ipv4, self.ipv6]: + tables['filter'].add_chain('nova-filter-top', wrap=False) + tables['filter'].add_rule('FORWARD', '-j nova-filter-top', + wrap=False, top=True) + tables['filter'].add_rule('OUTPUT', '-j nova-filter-top', + wrap=False, top=True) + + tables['filter'].add_chain('local') + tables['filter'].add_rule('nova-filter-top', '-j $local', + wrap=False) + + # Wrap the built-in chains + builtin_chains = {4: {'filter': ['INPUT', 'OUTPUT', 'FORWARD'], + 'nat': ['PREROUTING', 'OUTPUT', 'POSTROUTING'], + 'mangle': ['POSTROUTING']}, + 6: {'filter': ['INPUT', 'OUTPUT', 'FORWARD']}} + + for ip_version in builtin_chains: + if ip_version == 4: + tables = self.ipv4 + elif ip_version == 6: + tables = self.ipv6 + + for table, chains in six.iteritems(builtin_chains[ip_version]): + for chain in chains: + tables[table].add_chain(chain) + tables[table].add_rule(chain, '-j $%s' % (chain,), + wrap=False) + + # Add a nova-postrouting-bottom chain. It's intended to be shared + # among the various nova components. We set it as the last chain + # of POSTROUTING chain. + self.ipv4['nat'].add_chain('nova-postrouting-bottom', wrap=False) + self.ipv4['nat'].add_rule('POSTROUTING', '-j nova-postrouting-bottom', + wrap=False) + + # We add a snat chain to the shared nova-postrouting-bottom chain + # so that it's applied last. + self.ipv4['nat'].add_chain('snat') + self.ipv4['nat'].add_rule('nova-postrouting-bottom', '-j $snat', + wrap=False) + + # And then we add a float-snat chain and jump to first thing in + # the snat chain. + self.ipv4['nat'].add_chain('float-snat') + self.ipv4['nat'].add_rule('snat', '-j $float-snat') + + def defer_apply_on(self): + self.iptables_apply_deferred = True + + def defer_apply_off(self): + self.iptables_apply_deferred = False + self.apply() + + def dirty(self): + for table in six.itervalues(self.ipv4): + if table.dirty: + return True + if self.use_ipv6: + for table in six.itervalues(self.ipv6): + if table.dirty: + return True + return False + + def apply(self): + if self.iptables_apply_deferred: + return + if self.dirty(): + self._apply() + + @lockutils.synchronized('nova-iptables', external=True) + def _apply(self): + """Apply the current in-memory set of iptables rules. + + This will blow away any rules left over from previous runs of the + same component of Nova, and replace them with our current set of + rules. This happens atomically, thanks to iptables-restore. + + """ + s = [('iptables', self.ipv4)] + if self.use_ipv6: + s += [('ip6tables', self.ipv6)] + + for cmd, tables in s: + all_tables, _err = processutils.execute('%s-save' % (cmd,), + '-c', attempts=5, + run_as_root=True) + all_lines = all_tables.split('\n') + for table_name, table in six.iteritems(tables): + start, end = self._find_table(all_lines, table_name) + all_lines[start:end] = self._modify_rules( + all_lines[start:end], table, table_name) + table.dirty = False + processutils.execute('%s-restore' % (cmd,), '-c', + process_input='\n'.join(all_lines), + attempts=5, run_as_root=True) + + def _find_table(self, lines, table_name): + if len(lines) < 3: + # length only <2 when fake iptables + return (0, 0) + try: + start = lines.index('*%s' % table_name) - 1 + except ValueError: + # Couldn't find table_name + return (0, 0) + end = lines[start:].index('COMMIT') + start + 2 + return (start, end) + + def _modify_rules(self, current_lines, table, table_name): + unwrapped_chains = table.unwrapped_chains + chains = sorted(table.chains) + remove_chains = table.remove_chains + rules = table.rules + remove_rules = table.remove_rules + + if not current_lines: + fake_table = ['#Generated by nova', + '*' + table_name, 'COMMIT', + '#Completed by nova'] + current_lines = fake_table + + # Remove any trace of our rules + new_filter = filter(lambda line: binary_name not in line, + current_lines) + + top_rules = [] + bottom_rules = [] + + if self.iptables_top_regex: + regex = re.compile(self.iptables_top_regex) + temp_filter = filter(lambda line: regex.search(line), new_filter) + for rule_str in temp_filter: + new_filter = filter(lambda s: s.strip() != rule_str.strip(), + new_filter) + top_rules = temp_filter + + if self.iptables_bottom_regex: + regex = re.compile(self.iptables_bottom_regex) + temp_filter = filter(lambda line: regex.search(line), new_filter) + for rule_str in temp_filter: + new_filter = filter(lambda s: s.strip() != rule_str.strip(), + new_filter) + bottom_rules = temp_filter + + seen_chains = False + rules_index = 0 + for rules_index, rule in enumerate(new_filter): + if not seen_chains: + if rule.startswith(':'): + seen_chains = True + else: + if not rule.startswith(':'): + break + + if not seen_chains: + rules_index = 2 + + our_rules = top_rules + bot_rules = [] + for rule in rules: + rule_str = str(rule) + if rule.top: + # rule.top == True means we want this rule to be at the top. + # Further down, we weed out duplicates from the bottom of the + # list, so here we remove the dupes ahead of time. + + # We don't want to remove an entry if it has non-zero + # [packet:byte] counts and replace it with [0:0], so let's + # go look for a duplicate, and over-ride our table rule if + # found. + + # ignore [packet:byte] counts at beginning of line + if rule_str.startswith('['): + rule_str = rule_str.split(']', 1)[1] + dup_filter = filter(lambda s: rule_str.strip() in s.strip(), + new_filter) + + new_filter = filter(lambda s: + rule_str.strip() not in s.strip(), + new_filter) + # if no duplicates, use original rule + if dup_filter: + # grab the last entry, if there is one + dup = dup_filter[-1] + rule_str = str(dup) + else: + rule_str = str(rule) + rule_str.strip() + + our_rules += [rule_str] + else: + bot_rules += [rule_str] + + our_rules += bot_rules + + new_filter[rules_index:rules_index] = our_rules + + new_filter[rules_index:rules_index] = [':%s - [0:0]' % (name,) + for name in unwrapped_chains] + new_filter[rules_index:rules_index] = [':%s-%s - [0:0]' % + (binary_name, name,) + for name in chains] + + commit_index = new_filter.index('COMMIT') + new_filter[commit_index:commit_index] = bottom_rules + seen_lines = set() + + def _weed_out_duplicates(line): + # ignore [packet:byte] counts at beginning of lines + if line.startswith('['): + line = line.split(']', 1)[1] + line = line.strip() + if line in seen_lines: + return False + else: + seen_lines.add(line) + return True + + def _weed_out_removes(line): + # We need to find exact matches here + if line.startswith(':'): + # it's a chain, for example, ":nova-billing - [0:0]" + # strip off everything except the chain name + line = line.split(':')[1] + line = line.split('- [')[0] + line = line.strip() + for chain in remove_chains: + if chain == line: + remove_chains.remove(chain) + return False + elif line.startswith('['): + # it's a rule + # ignore [packet:byte] counts at beginning of lines + line = line.split(']', 1)[1] + line = line.strip() + for rule in remove_rules: + # ignore [packet:byte] counts at beginning of rules + rule_str = str(rule) + rule_str = rule_str.split(' ', 1)[1] + rule_str = rule_str.strip() + if rule_str == line: + remove_rules.remove(rule) + return False + + # Leave it alone + return True + + # We filter duplicates, letting the *last* occurrence take + # precedence. We also filter out anything in the "remove" + # lists. + new_filter.reverse() + new_filter = filter(_weed_out_duplicates, new_filter) + new_filter = filter(_weed_out_removes, new_filter) + new_filter.reverse() + + # flush lists, just in case we didn't find something + remove_chains.clear() + for rule in remove_rules: + remove_rules.remove(rule) + + return new_filter + + def get_gateway_rules(self, bridge): + interfaces = self.forward_bridge_interface + if 'all' in interfaces: + return [('FORWARD', '-i %s -j ACCEPT' % bridge), + ('FORWARD', '-o %s -j ACCEPT' % bridge)] + rules = [] + for iface in self.forward_bridge_interface: + if iface: + rules.append(('FORWARD', '-i %s -o %s -j ACCEPT' % (bridge, + iface))) + rules.append(('FORWARD', '-i %s -o %s -j ACCEPT' % (iface, + bridge))) + rules.append(('FORWARD', '-i %s -o %s -j ACCEPT' % (bridge, bridge))) + rules.append(('FORWARD', '-i %s -j %s' % (bridge, + self.iptables_drop_action))) + rules.append(('FORWARD', '-o %s -j %s' % (bridge, + self.iptables_drop_action))) + return rules diff --git a/vif_plug_linux_bridge/linux_bridge.py b/vif_plug_linux_bridge/linux_bridge.py new file mode 100644 index 00000000..2c77ef91 --- /dev/null +++ b/vif_plug_linux_bridge/linux_bridge.py @@ -0,0 +1,108 @@ +# Derived from nova/virt/libvirt/vif.py +# +# Copyright (C) 2011 Midokura KK +# Copyright (C) 2011 Nicira, Inc +# Copyright 2011 OpenStack Foundation +# 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. + +from os_vif import objects +from os_vif import plugin +from oslo_config import cfg + +from vif_plug_linux_bridge import iptables +from vif_plug_linux_bridge import linux_net + + +class LinuxBridgePlugin(plugin.PluginBase): + """A VIF type that uses a standard Linux bridge device.""" + + CONFIG_OPTS = ( + cfg.BoolOpt('use_ipv6', + default=False, + help='Use IPv6', + deprecated_group="DEFAULT"), + + cfg.StrOpt('iptables_top_regex', + default='', + help='Regular expression to match the iptables rule that ' + 'should always be on the top.', + deprecated_group="DEFAULT"), + cfg.StrOpt('iptables_bottom_regex', + default='', + help='Regular expression to match the iptables rule that ' + 'should always be on the bottom.', + deprecated_group="DEFAULT"), + cfg.StrOpt('iptables_drop_action', + default='DROP', + help='The table that iptables to jump to when a packet is ' + 'to be dropped.', + deprecated_group="DEFAULT"), + + cfg.MultiStrOpt('forward_bridge_interface', + default=['all'], + help='An interface that bridges can forward to. If ' + 'this is set to all then all traffic will be ' + 'forwarded. Can be specified multiple times.', + deprecated_group="DEFAULT"), + cfg.StrOpt('vlan_interface', + help='VLANs will bridge into this interface if set', + deprecated_group="DEFAULT"), + cfg.StrOpt('flat_interface', + help='FlatDhcp will bridge into this interface if set', + deprecated_group="DEFAULT"), + cfg.IntOpt('network_device_mtu', + default=1500, + help='MTU setting for network interface.', + deprecated_group="DEFAULT"), + ) + + def __init__(self, config): + super(LinuxBridgePlugin, self).__init__(config) + + ipm = iptables.IptablesManager( + use_ipv6=config.use_ipv6, + iptables_top_regex=config.iptables_top_regex, + iptables_bottom_regex=config.iptables_bottom_regex, + iptables_drop_action=config.iptables_drop_action, + forward_bridge_interface=config.forward_bridge_interface) + + linux_net.configure(ipm) + + def describe(self): + return plugin.PluginInfo( + [ + plugin.PluginVIFInfo( + objects.vif.VIFBridge, + "1.0", "1.0") + ]) + + def plug(self, vif, instance_info): + """Ensure that the bridge exists, and add VIF to it.""" + network = vif.network + bridge_name = vif.bridge_name + if not network.multi_host and network.should_provide_bridge: + if network.should_provide_vlan: + iface = self.config.vlan_interface or network.bridge_interface + mtu = self.config.network_device_mtu + linux_net.ensure_vlan_bridge(network.vlan, + bridge_name, iface, mtu=mtu) + else: + iface = self.config.flat_interface or network.bridge_interface + linux_net.ensure_bridge(bridge_name, iface) + + def unplug(self, vif, instance_info): + # Nothing required to unplug a port for a VIF using standard + # Linux bridge device... + pass diff --git a/vif_plug_linux_bridge/linux_net.py b/vif_plug_linux_bridge/linux_net.py new file mode 100644 index 00000000..70405aa6 --- /dev/null +++ b/vif_plug_linux_bridge/linux_net.py @@ -0,0 +1,195 @@ +# Derived from nova/network/linux_net.py +# +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +"""Implements vlans, bridges, and iptables rules using linux utilities.""" + +import os + +from oslo_concurrency import lockutils +from oslo_concurrency import processutils +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) +_IPTABLES_MANAGER = None + + +def device_exists(device): + """Check if ethernet device exists.""" + return os.path.exists('/sys/class/net/%s' % device) + + +def _set_device_mtu(dev, mtu): + """Set the device MTU.""" + processutils.execute('ip', 'link', 'set', dev, 'mtu', mtu, + check_exit_code=[0, 2, 254]) + + +def _ip_bridge_cmd(action, params, device): + """Build commands to add/del ips to bridges/devices.""" + cmd = ['ip', 'addr', action] + cmd.extend(params) + cmd.extend(['dev', device]) + return cmd + + +def ensure_vlan_bridge(vlan_num, bridge, bridge_interface, + net_attrs=None, mac_address=None, + mtu=None): + """Create a vlan and bridge unless they already exist.""" + interface = ensure_vlan(vlan_num, bridge_interface, mac_address, mtu=mtu) + ensure_bridge(bridge, interface, net_attrs) + return interface + + +@lockutils.synchronized('nova-lock_vlan', external=True) +def ensure_vlan(vlan_num, bridge_interface, mac_address=None, mtu=None): + """Create a vlan unless it already exists.""" + interface = 'vlan%s' % vlan_num + if not device_exists(interface): + LOG.debug('Starting VLAN interface %s', interface) + processutils.execute('ip', 'link', 'add', 'link', + bridge_interface, 'name', interface, 'type', + 'vlan', 'id', vlan_num, + check_exit_code=[0, 2, 254], + run_as_root=True) + # (danwent) the bridge will inherit this address, so we want to + # make sure it is the value set from the NetworkManager + if mac_address: + processutils.execute('ip', 'link', 'set', interface, + 'address', mac_address, + check_exit_code=[0, 2, 254], + run_as_root=True) + processutils.execute('ip', 'link', 'set', interface, 'up', + check_exit_code=[0, 2, 254], + run_as_root=True) + # NOTE(vish): set mtu every time to ensure that changes to mtu get + # propogated + _set_device_mtu(interface, mtu) + return interface + + +@lockutils.synchronized('nova-lock_bridge', external=True) +def ensure_bridge(bridge, interface, net_attrs=None, gateway=True, + filtering=True): + """Create a bridge unless it already exists. + + :param interface: the interface to create the bridge on. + :param net_attrs: dictionary with attributes used to create bridge. + :param gateway: whether or not the bridge is a gateway. + :param filtering: whether or not to create filters on the bridge. + + If net_attrs is set, it will add the net_attrs['gateway'] to the bridge + using net_attrs['broadcast'] and net_attrs['cidr']. It will also add + the ip_v6 address specified in net_attrs['cidr_v6'] if use_ipv6 is set. + + The code will attempt to move any ips that already exist on the + interface onto the bridge and reset the default gateway if necessary. + + """ + if not device_exists(bridge): + LOG.debug('Starting Bridge %s', bridge) + processutils.execute('brctl', 'addbr', bridge, + run_as_root=True) + processutils.execute('brctl', 'setfd', bridge, 0, + run_as_root=True) + # processutils.execute('brctl setageing %s 10' % bridge, + # run_as_root=True) + processutils.execute('brctl', 'stp', bridge, 'off', + run_as_root=True) + # (danwent) bridge device MAC address can't be set directly. + # instead it inherits the MAC address of the first device on the + # bridge, which will either be the vlan interface, or a + # physical NIC. + processutils.execute('ip', 'link', 'set', bridge, 'up', + run_as_root=True) + + if interface: + LOG.debug('Adding interface %(interface)s to bridge %(bridge)s', + {'interface': interface, 'bridge': bridge}) + out, err = processutils.execute('brctl', 'addif', bridge, + interface, check_exit_code=False, + run_as_root=True) + if (err and err != "device %s is already a member of a bridge; " + "can't enslave it to bridge %s.\n" % (interface, bridge)): + msg = _('Failed to add interface: %s') % err + raise Exception(msg) + + out, err = processutils.execute('ip', 'link', 'set', + interface, 'up', check_exit_code=False, + run_as_root=True) + + # NOTE(vish): This will break if there is already an ip on the + # interface, so we move any ips to the bridge + # NOTE(danms): We also need to copy routes to the bridge so as + # not to break existing connectivity on the interface + old_routes = [] + out, err = processutils.execute('ip', 'route', 'show', 'dev', + interface) + for line in out.split('\n'): + fields = line.split() + if fields and 'via' in fields: + old_routes.append(fields) + processutils.execute('ip', 'route', 'del', *fields, + run_as_root=True) + out, err = processutils.execute('ip', 'addr', 'show', 'dev', interface, + 'scope', 'global') + for line in out.split('\n'): + fields = line.split() + if fields and fields[0] == 'inet': + if fields[-2] in ('secondary', 'dynamic', ): + params = fields[1:-2] + else: + params = fields[1:-1] + processutils.execute(*_ip_bridge_cmd('del', params, + fields[-1]), + check_exit_code=[0, 2, 254], + run_as_root=True) + processutils.execute(*_ip_bridge_cmd('add', params, + bridge), + check_exit_code=[0, 2, 254], + run_as_root=True) + for fields in old_routes: + processutils.execute('ip', 'route', 'add', *fields, + run_as_root=True) + + if filtering: + # Don't forward traffic unless we were told to be a gateway + global _IPTABLES_MANAGER + ipv4_filter = _IPTABLES_MANAGER.ipv4['filter'] + if gateway: + for rule in _IPTABLES_MANAGER.get_gateway_rules(bridge): + ipv4_filter.add_rule(*rule) + else: + ipv4_filter.add_rule('FORWARD', + ('--in-interface %s -j %s' + % (bridge, + _IPTABLES_MANAGER.iptables_drop_action))) + ipv4_filter.add_rule('FORWARD', + ('--out-interface %s -j %s' + % (bridge, + _IPTABLES_MANAGER.iptables_drop_action))) + + +def configure(iptables_mgr): + """Configure the iptables manager impl. + + :param iptables_mgr: the iptables manager instance + """ + global _IPTABLES_MANAGER + _IPTABLES_MANAGER = iptables_mgr diff --git a/vif_plug_linux_bridge/tests/__init__.py b/vif_plug_linux_bridge/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vif_plug_linux_bridge/tests/test_plugin.py b/vif_plug_linux_bridge/tests/test_plugin.py new file mode 100644 index 00000000..ef539206 --- /dev/null +++ b/vif_plug_linux_bridge/tests/test_plugin.py @@ -0,0 +1,115 @@ +# 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 contextlib +import mock +import six +import testtools + +from os_vif import objects + +from vif_plug_linux_bridge import linux_bridge +from vif_plug_linux_bridge import linux_net + + +if six.PY2: + nested = contextlib.nested +else: + @contextlib.contextmanager + def nested(*contexts): + with contextlib.ExitStack() as stack: + yield [stack.enter_context(c) for c in contexts] + + +class PluginTest(testtools.TestCase): + + def __init__(self, *args, **kwargs): + super(PluginTest, self).__init__(*args, **kwargs) + + objects.register_all() + + self.instance = objects.instance_info.InstanceInfo( + name='demo', + uuid='f0000000-0000-0000-0000-000000000001') + + def test_plug_bridge(self): + network = objects.network.Network( + id='network-id-xxx-yyy-zzz', + bridge='br0') + + vif = objects.vif.VIFBridge( + id='vif-xxx-yyy-zzz', + address='ca:fe:de:ad:be:ef', + network=network, + dev_name='tap-xxx-yyy-zzz', + bridge_name="br0") + + with nested( + mock.patch.object(linux_net, 'ensure_bridge'), + mock.patch.object(linux_net, 'ensure_vlan_bridge') + ) as (mock_ensure_bridge, mock_ensure_vlan_bridge): + plugin = linux_bridge.LinuxBridgePlugin.load("linux_bridge") + plugin.plug(vif, self.instance) + + self.assertEqual(len(mock_ensure_bridge.calls), 0) + self.assertEqual(len(mock_ensure_vlan_bridge.calls), 0) + + def test_plug_bridge_create_br(self): + network = objects.network.Network( + id='network-id-xxx-yyy-zzz', + bridge='br0', + bridge_interface='eth0', + should_provide_bridge=True) + + vif = objects.vif.VIFBridge( + id='vif-xxx-yyy-zzz', + address='ca:fe:de:ad:be:ef', + network=network, + dev_name='tap-xxx-yyy-zzz', + bridge_name="br0") + + with nested( + mock.patch.object(linux_net, 'ensure_bridge'), + mock.patch.object(linux_net, 'ensure_vlan_bridge') + ) as (mock_ensure_bridge, mock_ensure_vlan_bridge): + plugin = linux_bridge.LinuxBridgePlugin.load("linux_bridge") + plugin.plug(vif, self.instance) + + mock_ensure_bridge.assert_called_with("br0", "eth0") + self.assertEqual(len(mock_ensure_vlan_bridge.calls), 0) + + def test_plug_bridge_create_br_vlan(self): + network = objects.network.Network( + id='network-id-xxx-yyy-zzz', + bridge='br0', + bridge_interface='eth0', + vlan=99, + should_provide_bridge=True, + should_provide_vlan=True) + + vif = objects.vif.VIFBridge( + id='vif-xxx-yyy-zzz', + address='ca:fe:de:ad:be:ef', + network=network, + dev_name='tap-xxx-yyy-zzz', + bridge_name="br0") + + with nested( + mock.patch.object(linux_net, 'ensure_bridge'), + mock.patch.object(linux_net, 'ensure_vlan_bridge') + ) as (mock_ensure_bridge, mock_ensure_vlan_bridge): + plugin = linux_bridge.LinuxBridgePlugin.load("linux_bridge") + plugin.plug(vif, self.instance) + + self.assertEqual(len(mock_ensure_bridge.calls), 0) + mock_ensure_vlan_bridge.assert_called_with( + "99", "br0", "eth0", mtu=1500)