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
This commit is contained in:
parent
72d5dfbf48
commit
5888af0815
@ -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
|
||||
|
0
vif_plug_linux_bridge/__init__.py
Normal file
0
vif_plug_linux_bridge/__init__.py
Normal file
534
vif_plug_linux_bridge/iptables.py
Normal file
534
vif_plug_linux_bridge/iptables.py
Normal file
@ -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 <binary name>-<chain name>,
|
||||
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 <chain name>' 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
|
108
vif_plug_linux_bridge/linux_bridge.py
Normal file
108
vif_plug_linux_bridge/linux_bridge.py
Normal file
@ -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
|
195
vif_plug_linux_bridge/linux_net.py
Normal file
195
vif_plug_linux_bridge/linux_net.py
Normal file
@ -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
|
0
vif_plug_linux_bridge/tests/__init__.py
Normal file
0
vif_plug_linux_bridge/tests/__init__.py
Normal file
115
vif_plug_linux_bridge/tests/test_plugin.py
Normal file
115
vif_plug_linux_bridge/tests/test_plugin.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user