[gnuoy,r=tribaal] Filter MAC addresses that belong to interfaces with assigned IPs
and bridges out.
This commit is contained in:
commit
3a01c99a00
@ -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
|
||||
|
@ -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:
|
||||
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]
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
return ipv6_addr[0]
|
||||
|
||||
except ValueError:
|
||||
raise ValueError("Invalid interface '%s'" % iface)
|
||||
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
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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()))
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user