[gnuoy,r=tribaal] Filter MAC addresses that belong to interfaces with assigned IPs

and bridges out.
This commit is contained in:
Christopher Glass 2014-09-24 10:04:32 +02:00
commit 3a01c99a00
8 changed files with 251 additions and 57 deletions

View File

@ -12,8 +12,10 @@ options:
type: string
default:
description: |
External port to use for routing of instance
traffic to the external public network.
A space-separated list of external ports to use for routing of instance
traffic to the external public network. Valid values are either MAC
addresses (in which case only MAC addresses for interfaces without an IP
address already assigned will be used), or interfaces (eth0)
openstack-origin:
type: string
default: distro

View File

@ -1,10 +1,11 @@
import glob
import sys
from functools import partial
from charmhelpers.fetch import apt_install
from charmhelpers.core.hookenv import (
ERROR, log, config,
ERROR, log,
)
try:
@ -156,19 +157,102 @@ get_iface_for_address = partial(_get_for_address, key='iface')
get_netmask_for_address = partial(_get_for_address, key='netmask')
def get_ipv6_addr(iface="eth0"):
def format_ipv6_addr(address):
"""
IPv6 needs to be wrapped with [] in url link to parse correctly.
"""
if is_ipv6(address):
address = "[%s]" % address
else:
log("Not an valid ipv6 address: %s" % address,
level=ERROR)
address = None
return address
def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, fatal=True, exc_list=None):
"""
Return the assigned IP address for a given interface, if any, or [].
"""
# Extract nic if passed /dev/ethX
if '/' in iface:
iface = iface.split('/')[-1]
if not exc_list:
exc_list = []
try:
iface_addrs = netifaces.ifaddresses(iface)
if netifaces.AF_INET6 not in iface_addrs:
raise Exception("Interface '%s' doesn't have an ipv6 address." % iface)
inet_num = getattr(netifaces, inet_type)
except AttributeError:
raise Exception('Unknown inet type ' + str(inet_type))
addresses = netifaces.ifaddresses(iface)[netifaces.AF_INET6]
ipv6_addr = [a['addr'] for a in addresses if not a['addr'].startswith('fe80')
and config('vip') != a['addr']]
if not ipv6_addr:
raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
interfaces = netifaces.interfaces()
if inc_aliases:
ifaces = []
for _iface in interfaces:
if iface == _iface or _iface.split(':')[0] == iface:
ifaces.append(_iface)
if fatal and not ifaces:
raise Exception("Invalid interface '%s'" % iface)
ifaces.sort()
else:
if iface not in interfaces:
if fatal:
raise Exception("%s not found " % (iface))
else:
return []
else:
ifaces = [iface]
return ipv6_addr[0]
addresses = []
for netiface in ifaces:
net_info = netifaces.ifaddresses(netiface)
if inet_num in net_info:
for entry in net_info[inet_num]:
if 'addr' in entry and entry['addr'] not in exc_list:
addresses.append(entry['addr'])
if fatal and not addresses:
raise Exception("Interface '%s' doesn't have any %s addresses." % (iface, inet_type))
return addresses
except ValueError:
raise ValueError("Invalid interface '%s'" % iface)
get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
def get_ipv6_addr(iface='eth0', inc_aliases=False, fatal=True, exc_list=None):
"""
Return the assigned IPv6 address for a given interface, if any, or [].
"""
addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
inc_aliases=inc_aliases, fatal=fatal,
exc_list=exc_list)
remotly_addressable = []
for address in addresses:
if not address.startswith('fe80'):
remotly_addressable.append(address)
if fatal and not remotly_addressable:
raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
return remotly_addressable
def get_bridges(vnic_dir='/sys/devices/virtual/net'):
"""
Return a list of bridges on the system or []
"""
b_rgex = vnic_dir + '/*/bridge'
return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_rgex)]
def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
"""
Return a list of nics comprising a given bridge on the system or []
"""
brif_rgex = "%s/%s/brif/*" % (vnic_dir, bridge)
return [x.split('/')[-1] for x in glob.glob(brif_rgex)]
def is_bridge_member(nic):
"""
Check if a given nic is a member of a bridge
"""
for bridge in get_bridges():
if nic in get_bridge_nics(bridge):
return True
return False

View File

@ -70,6 +70,7 @@ SWIFT_CODENAMES = OrderedDict([
('1.13.0', 'icehouse'),
('1.12.0', 'icehouse'),
('1.11.0', 'icehouse'),
('2.0.0', 'juno'),
])
DEFAULT_LOOPBACK_SIZE = '5G'

View File

@ -156,12 +156,15 @@ def hook_name():
class Config(dict):
"""A Juju charm config dictionary that can write itself to
disk (as json) and track which values have changed since
the previous hook invocation.
"""A dictionary representation of the charm's config.yaml, with some
extra features:
Do not instantiate this object directly - instead call
``hookenv.config()``
- See which values in the dictionary have changed since the previous hook.
- For values that have changed, see what the previous value was.
- Store arbitrary data for use in a later hook.
NOTE: Do not instantiate this object directly - instead call
``hookenv.config()``, which will return an instance of :class:`Config`.
Example usage::
@ -170,8 +173,8 @@ class Config(dict):
>>> config = hookenv.config()
>>> config['foo']
'bar'
>>> # store a new key/value for later use
>>> config['mykey'] = 'myval'
>>> config.save()
>>> # user runs `juju set mycharm foo=baz`
@ -188,22 +191,34 @@ class Config(dict):
>>> # keys/values that we add are preserved across hooks
>>> config['mykey']
'myval'
>>> # don't forget to save at the end of hook!
>>> config.save()
"""
CONFIG_FILE_NAME = '.juju-persistent-config'
def __init__(self, *args, **kw):
super(Config, self).__init__(*args, **kw)
self.implicit_save = True
self._prev_dict = None
self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
if os.path.exists(self.path):
self.load_previous()
def __getitem__(self, key):
"""For regular dict lookups, check the current juju config first,
then the previous (saved) copy. This ensures that user-saved values
will be returned by a dict lookup.
"""
try:
return dict.__getitem__(self, key)
except KeyError:
return (self._prev_dict or {})[key]
def load_previous(self, path=None):
"""Load previous copy of config from disk so that current values
can be compared to previous values.
"""Load previous copy of config from disk.
In normal usage you don't need to call this method directly - it
is called automatically at object initialization.
:param path:
@ -218,8 +233,8 @@ class Config(dict):
self._prev_dict = json.load(f)
def changed(self, key):
"""Return true if the value for this key has changed since
the last save.
"""Return True if the current value for this key is different from
the previous value.
"""
if self._prev_dict is None:
@ -228,7 +243,7 @@ class Config(dict):
def previous(self, key):
"""Return previous value for this key, or None if there
is no "previous" value.
is no previous value.
"""
if self._prev_dict:
@ -238,7 +253,13 @@ class Config(dict):
def save(self):
"""Save this config to disk.
Preserves items in _prev_dict that do not exist in self.
If the charm is using the :mod:`Services Framework <services.base>`
or :meth:'@hook <Hooks.hook>' decorator, this
is called automatically at the end of successful hook execution.
Otherwise, it should be called directly by user code.
To disable automatic saves, set ``implicit_save=False`` on this
instance.
"""
if self._prev_dict:
@ -478,6 +499,9 @@ class Hooks(object):
hook_name = os.path.basename(args[0])
if hook_name in self._hooks:
self._hooks[hook_name]()
cfg = config()
if cfg.implicit_save:
cfg.save()
else:
raise UnregisteredHookError(hook_name)

View File

@ -118,6 +118,9 @@ class ServiceManager(object):
else:
self.provide_data()
self.reconfigure_services()
cfg = hookenv.config()
if cfg.implicit_save:
cfg.save()
def provide_data(self):
"""

View File

@ -1,6 +1,8 @@
import os
import urllib2
from urllib import urlretrieve
import urlparse
import hashlib
from charmhelpers.fetch import (
BaseFetchHandler,
@ -12,7 +14,17 @@ from charmhelpers.payload.archive import (
)
from charmhelpers.core.host import mkdir
"""
This class is a plugin for charmhelpers.fetch.install_remote.
It grabs, validates and installs remote archives fetched over "http", "https", "ftp" or "file" protocols. The contents of the archive are installed in $CHARM_DIR/fetched/.
Example usage:
install_remote("https://example.com/some/archive.tar.gz")
# Installs the contents of archive.tar.gz in $CHARM_DIR/fetched/.
See charmhelpers.fetch.archiveurl.get_archivehandler for supported archive types.
"""
class ArchiveUrlFetchHandler(BaseFetchHandler):
"""Handler for archives via generic URLs"""
def can_handle(self, source):
@ -61,3 +73,31 @@ class ArchiveUrlFetchHandler(BaseFetchHandler):
except OSError as e:
raise UnhandledSource(e.strerror)
return extract(dld_file)
# Mandatory file validation via Sha1 or MD5 hashing.
def download_and_validate(self, url, hashsum, validate="sha1"):
if validate == 'sha1' and len(hashsum) != 40:
raise ValueError("HashSum must be = 40 characters when using sha1"
" validation")
if validate == 'md5' and len(hashsum) != 32:
raise ValueError("HashSum must be = 32 characters when using md5"
" validation")
tempfile, headers = urlretrieve(url)
self.validate_file(tempfile, hashsum, validate)
return tempfile
# Predicate method that returns status of hash matching expected hash.
def validate_file(self, source, hashsum, vmethod='sha1'):
if vmethod != 'sha1' and vmethod != 'md5':
raise ValueError("Validation Method not supported")
if vmethod == 'md5':
m = hashlib.md5()
if vmethod == 'sha1':
m = hashlib.sha1()
with open(source) as f:
for line in f:
m.update(line)
if hashsum != m.hexdigest():
msg = "Hash Mismatch on {} expected {} got {}"
raise ValueError(msg.format(source, hashsum, m.hexdigest()))

View File

@ -28,7 +28,12 @@ from charmhelpers.contrib.hahelpers.cluster import(
eligible_leader
)
import re
from charmhelpers.contrib.network.ip import get_address_in_network
from charmhelpers.contrib.network.ip import (
get_address_in_network,
get_ipv4_addr,
get_ipv6_addr,
is_bridge_member,
)
DB_USER = "quantum"
QUANTUM_DB = "quantum"
@ -146,16 +151,29 @@ class ExternalPortContext(OSContextGenerator):
def __call__(self):
if not config('ext-port'):
return None
hwaddrs = {}
hwaddr_to_nic = {}
hwaddr_to_ip = {}
for nic in list_nics(['eth', 'bond']):
hwaddrs[get_nic_hwaddr(nic)] = nic
hwaddr = get_nic_hwaddr(nic)
hwaddr_to_nic[hwaddr] = nic
addresses = get_ipv4_addr(nic, fatal=False) + \
get_ipv6_addr(nic, fatal=False)
hwaddr_to_ip[hwaddr] = addresses
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
for entry in config('ext-port').split():
entry = entry.strip()
if re.match(mac_regex, entry):
if entry in hwaddrs:
return {"ext_port": hwaddrs[entry]}
if entry in hwaddr_to_nic and len(hwaddr_to_ip[entry]) == 0:
# If the nic is part of a bridge then don't use it
if is_bridge_member(hwaddr_to_nic[entry]):
continue
# Entry is a MAC address for a valid interface that doesn't
# have an IP address assigned yet.
return {"ext_port": hwaddr_to_nic[entry]}
else:
# If the passed entry is not a MAC address, assume it's a valid
# interface, and that the user put it there on purpose (we can
# trust it to be the real external network).
return {"ext_port": entry}
return None

View File

@ -12,17 +12,19 @@ from test_utils import (
)
TO_PATCH = [
'apt_install',
'config',
'context_complete',
'eligible_leader',
'get_ipv4_addr',
'get_ipv6_addr',
'get_nic_hwaddr',
'get_os_codename_install_source',
'list_nics',
'relation_get',
'relation_ids',
'related_units',
'context_complete',
'unit_get',
'apt_install',
'get_os_codename_install_source',
'eligible_leader',
'list_nics',
'get_nic_hwaddr',
]
@ -120,37 +122,57 @@ class TestExternalPortContext(CharmTestCase):
def setUp(self):
super(TestExternalPortContext, self).setUp(quantum_contexts,
TO_PATCH)
self.machine_macs = {
'eth0': 'fe:c5:ce:8e:2b:00',
'eth1': 'fe:c5:ce:8e:2b:01',
'eth2': 'fe:c5:ce:8e:2b:02',
'eth3': 'fe:c5:ce:8e:2b:03',
}
self.machine_nics = {
'eth0': ['192.168.0.1'],
'eth1': ['192.168.0.2'],
'eth2': [],
'eth3': [],
}
self.absent_macs = "aa:a5:ae:ae:ab:a4 "
def test_no_ext_port(self):
self.config.return_value = None
self.assertEquals(quantum_contexts.ExternalPortContext()(),
None)
self.assertIsNone(quantum_contexts.ExternalPortContext()())
def test_ext_port_eth(self):
self.config.return_value = 'eth1010'
self.assertEquals(quantum_contexts.ExternalPortContext()(),
{'ext_port': 'eth1010'})
def test_ext_port_mac(self):
machine_macs = {
'eth0': 'fe:c5:ce:8e:2b:00',
'eth1': 'fe:c5:ce:8e:2b:01',
'eth2': 'fe:c5:ce:8e:2b:02',
'eth3': 'fe:c5:ce:8e:2b:03',
}
absent_macs = "aa:a5:ae:ae:ab:a4 "
config_macs = absent_macs + " " + machine_macs['eth2']
def _fake_get_hwaddr(self, arg):
return self.machine_macs[arg]
def get_hwaddr(arg):
return machine_macs[arg]
def _fake_get_ipv4(self, arg, fatal=False):
return self.machine_nics[arg]
def test_ext_port_mac(self):
config_macs = self.absent_macs + " " + self.machine_macs['eth2']
self.get_ipv4_addr.side_effect = self._fake_get_ipv4
self.get_ipv6_addr.return_value = []
self.config.return_value = config_macs
self.list_nics.return_value = machine_macs.keys()
self.get_nic_hwaddr.side_effect = get_hwaddr
self.list_nics.return_value = self.machine_macs.keys()
self.get_nic_hwaddr.side_effect = self._fake_get_hwaddr
self.assertEquals(quantum_contexts.ExternalPortContext()(),
{'ext_port': 'eth2'})
self.config.return_value = absent_macs
self.config.return_value = self.absent_macs
self.assertIsNone(quantum_contexts.ExternalPortContext()())
def test_ext_port_mac_one_used_nic(self):
config_macs = self.machine_macs['eth1'] + " " + \
self.machine_macs['eth2']
self.get_ipv4_addr.side_effect = self._fake_get_ipv4
self.get_ipv6_addr.return_value = []
self.config.return_value = config_macs
self.list_nics.return_value = self.machine_macs.keys()
self.get_nic_hwaddr.side_effect = self._fake_get_hwaddr
self.assertEquals(quantum_contexts.ExternalPortContext()(),
None)
{'ext_port': 'eth2'})
class TestL3AgentContext(CharmTestCase):