Prep for merging next in

This commit is contained in:
Liam Young 2015-03-25 08:37:29 +00:00
parent 8534a357af
commit b54c200370
10 changed files with 433 additions and 199 deletions

View File

@ -1,4 +1,4 @@
branch: lp:~gnuoy/charm-helpers/neutron-contexts
branch: lp:~gnuoy/charm-helpers/neutron-shuffle
destination: hooks/charmhelpers
include:
- core

View File

@ -17,13 +17,16 @@
import glob
import re
import subprocess
import six
import socket
from functools import partial
from charmhelpers.core.hookenv import unit_get
from charmhelpers.fetch import apt_install
from charmhelpers.core.hookenv import (
log
log,
WARNING,
)
try:
@ -365,3 +368,83 @@ def is_bridge_member(nic):
return True
return False
def is_ip(address):
"""
Returns True if address is a valid IP address.
"""
try:
# Test to see if already an IPv4 address
socket.inet_aton(address)
return True
except socket.error:
return False
def ns_query(address):
try:
import dns.resolver
except ImportError:
apt_install('python-dnspython')
import dns.resolver
if isinstance(address, dns.name.Name):
rtype = 'PTR'
elif isinstance(address, six.string_types):
rtype = 'A'
else:
return None
answers = dns.resolver.query(address, rtype)
if answers:
return str(answers[0])
return None
def get_host_ip(hostname, fallback=None):
"""
Resolves the IP for a given hostname, or returns
the input if it is already an IP.
"""
if is_ip(hostname):
return hostname
ip_addr = ns_query(hostname)
if not ip_addr:
try:
ip_addr = socket.gethostbyname(hostname)
except:
log("Failed to resolve hostname '%s'" % (hostname),
level=WARNING)
return fallback
return ip_addr
def get_hostname(address, fqdn=True):
"""
Resolves hostname for given IP, or returns the input
if it is already a hostname.
"""
if is_ip(address):
try:
import dns.reversename
except ImportError:
apt_install("python-dnspython")
import dns.reversename
rev = dns.reversename.from_address(address)
result = ns_query(rev)
if not result:
return None
else:
result = address
if fqdn:
# strip trailing .
if result.endswith('.'):
return result[:-1]
else:
return result
else:
return result.split('.')[0]

View File

@ -16,17 +16,14 @@
import json
import os
import re
import time
from base64 import b64decode
from subprocess import check_call
import re
import six
import yaml
from charmhelpers.core.host import (
list_nics,
get_nic_hwaddr
)
from charmhelpers.fetch import (
apt_install,
filter_installed_packages,
@ -50,8 +47,11 @@ from charmhelpers.core.hookenv import (
)
from charmhelpers.core.sysctl import create as sysctl_create
from charmhelpers.core.strutils import bool_from_string
from charmhelpers.core.host import (
list_nics,
get_nic_hwaddr,
mkdir,
write_file,
)
@ -68,6 +68,11 @@ from charmhelpers.contrib.hahelpers.apache import (
)
from charmhelpers.contrib.openstack.neutron import (
neutron_plugin_attribute,
parse_data_port_mappings,
)
from charmhelpers.contrib.openstack.ip import (
resolve_address,
INTERNAL,
)
from charmhelpers.contrib.network.ip import (
get_address_in_network,
@ -79,7 +84,6 @@ from charmhelpers.contrib.network.ip import (
is_bridge_member,
)
from charmhelpers.contrib.openstack.utils import get_host_ip
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
ADDRESS_TYPES = ['admin', 'internal', 'public']
@ -111,9 +115,41 @@ def context_complete(ctxt):
def config_flags_parser(config_flags):
"""Parses config flags string into dict.
This parsing method supports a few different formats for the config
flag values to be parsed:
1. A string in the simple format of key=value pairs, with the possibility
of specifying multiple key value pairs within the same string. For
example, a string in the format of 'key1=value1, key2=value2' will
return a dict of:
{'key1': 'value1',
'key2': 'value2'}.
2. A string in the above format, but supporting a comma-delimited list
of values for the same key. For example, a string in the format of
'key1=value1, key2=value3,value4,value5' will return a dict of:
{'key1', 'value1',
'key2', 'value2,value3,value4'}
3. A string containing a colon character (:) prior to an equal
character (=) will be treated as yaml and parsed as such. This can be
used to specify more complex key value pairs. For example,
a string in the format of 'key1: subkey1=value1, subkey2=value2' will
return a dict of:
{'key1', 'subkey1=value1, subkey2=value2'}
The provided config_flags string may be a list of comma-separated values
which themselves may be comma-separated list of values.
"""
# If we find a colon before an equals sign then treat it as yaml.
# Note: limit it to finding the colon first since this indicates assignment
# for inline yaml.
colon = config_flags.find(':')
equals = config_flags.find('=')
if colon > 0:
if colon < equals or equals < 0:
return yaml.safe_load(config_flags)
if config_flags.find('==') >= 0:
log("config_flags is not in expected format (key=value)", level=ERROR)
raise OSContextError
@ -198,7 +234,7 @@ class SharedDBContext(OSContextGenerator):
unit=local_unit())
if set_hostname != access_hostname:
relation_set(relation_settings={hostname_key: access_hostname})
return ctxt # Defer any further hook execution for now....
return None # Defer any further hook execution for now....
password_setting = 'password'
if self.relation_prefix:
@ -701,7 +737,14 @@ class ApacheSSLContext(OSContextGenerator):
'endpoints': [],
'ext_ports': []}
for cn in self.canonical_names():
cns = self.canonical_names()
if cns:
for cn in cns:
self.configure_cert(cn)
else:
# Expect cert/key provided in config (currently assumed that ca
# uses ip for cn)
cn = resolve_address(endpoint_type=INTERNAL)
self.configure_cert(cn)
addresses = self.get_network_addresses()
@ -857,6 +900,48 @@ class NeutronContext(OSContextGenerator):
return ctxt
class NeutronPortContext(OSContextGenerator):
NIC_PREFIXES = ['eth', 'bond']
def resolve_ports(self, ports):
"""Resolve NICs not yet bound to bridge(s)
If hwaddress provided then returns resolved hwaddress otherwise NIC.
"""
if not ports:
return None
hwaddr_to_nic = {}
hwaddr_to_ip = {}
for nic in list_nics(self.NIC_PREFIXES):
hwaddr = get_nic_hwaddr(nic)
hwaddr_to_nic[hwaddr] = nic
addresses = get_ipv4_addr(nic, fatal=False)
addresses += get_ipv6_addr(iface=nic, fatal=False)
hwaddr_to_ip[hwaddr] = addresses
resolved = []
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
for entry in ports:
if re.match(mac_regex, entry):
# NIC is in known NICs and does NOT hace an IP address
if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
# 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.
resolved.append(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).
resolved.append(entry)
return resolved
class OSConfigFlagContext(OSContextGenerator):
"""Provides support for user-defined config flags.
@ -1080,53 +1165,143 @@ class SysctlContext(OSContextGenerator):
return {'sysctl': sysctl_dict}
class NeutronPortContext(OSContextGenerator):
class NeutronAPIContext(OSContextGenerator):
'''
Inspects current neutron-plugin-api relation for neutron settings. Return
defaults if it is not present.
'''
interfaces = ['neutron-plugin-api']
def _resolve_port(self, config_key):
if not config(config_key):
return None
hwaddr_to_nic = {}
hwaddr_to_ip = {}
for nic in list_nics(['eth', 'bond']):
hwaddr = get_nic_hwaddr(nic)
hwaddr_to_nic[hwaddr] = nic
addresses = get_ipv4_addr(nic, fatal=False) + \
get_ipv6_addr(iface=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(config_key).split():
entry = entry.strip()
if re.match(mac_regex, 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 hwaddr_to_nic[entry]
def __call__(self):
self.neutron_defaults = {
'l2_population': {
'rel_key': 'l2-population',
'default': False,
},
'overlay_network_type': {
'rel_key': 'overlay-network-type',
'default': 'gre',
},
'neutron_security_groups': {
'rel_key': 'neutron-security-groups',
'default': False,
},
'network_device_mtu': {
'rel_key': 'network-device-mtu',
'default': None,
},
'enable_dvr': {
'rel_key': 'enable-dvr',
'default': False,
},
'enable_l3ha': {
'rel_key': 'enable-l3ha',
'default': False,
},
}
ctxt = self.get_neutron_options({})
for rid in relation_ids('neutron-plugin-api'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
if 'l2-population' in rdata:
ctxt.update(self.get_neutron_options(rdata))
return ctxt
def get_neutron_options(self, rdata):
settings = {}
for nkey in self.neutron_defaults.keys():
defv = self.neutron_defaults[nkey]['default']
rkey = self.neutron_defaults[nkey]['rel_key']
if rkey in rdata.keys():
if type(defv) is bool:
settings[nkey] = bool_from_string(rdata[rkey])
else:
settings[nkey] = rdata[rkey]
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 entry
return None
settings[nkey] = defv
return settings
class ExternalPortContext(NeutronPortContext):
def __call__(self):
port = self._resolve_port('ext-port')
if port:
return {"ext_port": port}
else:
return None
ctxt = {}
ports = config('ext-port')
if ports:
ports = [p.strip() for p in ports.split()]
ports = self.resolve_ports(ports)
if ports:
ctxt = {"ext_port": ports[0]}
napi_settings = NeutronAPIContext()()
mtu = napi_settings.get('network_device_mtu')
if mtu:
ctxt['ext_port_mtu'] = mtu
return ctxt
class DataPortContext(NeutronPortContext):
def __call__(self):
port = self._resolve_port('data-port')
if port:
return {"data_port": port}
else:
return None
ports = config('data-port')
if ports:
portmap = parse_data_port_mappings(ports)
ports = portmap.values()
resolved = self.resolve_ports(ports)
normalized = {get_nic_hwaddr(port): port for port in resolved
if port not in ports}
normalized.update({port: port for port in resolved
if port in ports})
if resolved:
return {bridge: normalized[port] for bridge, port in
six.iteritems(portmap) if port in normalized.keys()}
return None
class PhyNICMTUContext(DataPortContext):
def __call__(self):
ctxt = {}
mappings = super(PhyNICMTUContext, self).__call__()
if mappings and mappings.values():
ports = mappings.values()
napi_settings = NeutronAPIContext()()
mtu = napi_settings.get('network_device_mtu')
if mtu:
ctxt["devs"] = '\\n'.join(ports)
ctxt['mtu'] = mtu
return ctxt
class NetworkServiceContext(OSContextGenerator):
def __init__(self, rel_name='quantum-network-service'):
self.rel_name = rel_name
self.interfaces = [rel_name]
def __call__(self):
for rid in relation_ids(self.rel_name):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
ctxt = {
'keystone_host': rdata.get('keystone_host'),
'service_port': rdata.get('service_port'),
'auth_port': rdata.get('auth_port'),
'service_tenant': rdata.get('service_tenant'),
'service_username': rdata.get('service_username'),
'service_password': rdata.get('service_password'),
'quantum_host': rdata.get('quantum_host'),
'quantum_port': rdata.get('quantum_port'),
'quantum_url': rdata.get('quantum_url'),
'region': rdata.get('region'),
'service_protocol':
rdata.get('service_protocol') or 'http',
'auth_protocol':
rdata.get('auth_protocol') or 'http',
}
if context_complete(ctxt):
return ctxt
return {}

View File

@ -16,6 +16,7 @@
# Various utilies for dealing with Neutron and the renaming from Quantum.
import six
from subprocess import check_output
from charmhelpers.core.hookenv import (
@ -237,3 +238,72 @@ def network_manager():
else:
# ensure accurate naming for all releases post-H
return 'neutron'
def parse_mappings(mappings):
parsed = {}
if mappings:
mappings = mappings.split(' ')
for m in mappings:
p = m.partition(':')
if p[1] == ':':
parsed[p[0].strip()] = p[2].strip()
return parsed
def parse_bridge_mappings(mappings):
"""Parse bridge mappings.
Mappings must be a space-delimited list of provider:bridge mappings.
Returns dict of the form {provider:bridge}.
"""
return parse_mappings(mappings)
def parse_data_port_mappings(mappings, default_bridge='br-data'):
"""Parse data port mappings.
Mappings must be a space-delimited list of bridge:port mappings.
Returns dict of the form {bridge:port}.
"""
_mappings = parse_mappings(mappings)
if not _mappings:
if not mappings:
return {}
# For backwards-compatibility we need to support port-only provided in
# config.
_mappings = {default_bridge: mappings.split(' ')[0]}
bridges = _mappings.keys()
ports = _mappings.values()
if len(set(bridges)) != len(bridges):
raise Exception("It is not allowed to have more than one port "
"configured on the same bridge")
if len(set(ports)) != len(ports):
raise Exception("It is not allowed to have the same port configured "
"on more than one bridge")
return _mappings
def parse_vlan_range_mappings(mappings):
"""Parse vlan range mappings.
Mappings must be a space-delimited list of provider:start:end mappings.
Returns dict of the form {provider: (start, end)}.
"""
_mappings = parse_mappings(mappings)
if not _mappings:
return {}
mappings = {}
for p, r in six.iteritems(_mappings):
mappings[p] = tuple(r.split(':'))
return mappings

View File

@ -23,12 +23,13 @@ from functools import wraps
import subprocess
import json
import os
import socket
import sys
import six
import yaml
from charmhelpers.contrib.network import ip
from charmhelpers.core.hookenv import (
config,
log as juju_log,
@ -421,77 +422,10 @@ def clean_storage(block_device):
else:
zap_disk(block_device)
def is_ip(address):
"""
Returns True if address is a valid IP address.
"""
try:
# Test to see if already an IPv4 address
socket.inet_aton(address)
return True
except socket.error:
return False
def ns_query(address):
try:
import dns.resolver
except ImportError:
apt_install('python-dnspython')
import dns.resolver
if isinstance(address, dns.name.Name):
rtype = 'PTR'
elif isinstance(address, six.string_types):
rtype = 'A'
else:
return None
answers = dns.resolver.query(address, rtype)
if answers:
return str(answers[0])
return None
def get_host_ip(hostname):
"""
Resolves the IP for a given hostname, or returns
the input if it is already an IP.
"""
if is_ip(hostname):
return hostname
return ns_query(hostname)
def get_hostname(address, fqdn=True):
"""
Resolves hostname for given IP, or returns the input
if it is already a hostname.
"""
if is_ip(address):
try:
import dns.reversename
except ImportError:
apt_install('python-dnspython')
import dns.reversename
rev = dns.reversename.from_address(address)
result = ns_query(rev)
if not result:
return None
else:
result = address
if fqdn:
# strip trailing .
if result.endswith('.'):
return result[:-1]
else:
return result
else:
return result.split('.')[0]
is_ip = ip.is_ip
ns_query = ip.ns_query
get_host_ip = ip.get_host_ip
get_hostname = ip.get_hostname
def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):

View File

@ -566,3 +566,29 @@ class Hooks(object):
def charm_dir():
"""Return the root directory of the current charm"""
return os.environ.get('CHARM_DIR')
@cached
def action_get(key=None):
"""Gets the value of an action parameter, or all key/value param pairs"""
cmd = ['action-get']
if key is not None:
cmd.append(key)
cmd.append('--format=json')
action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
return action_data
def action_set(values):
"""Sets the values to be returned after the action finishes"""
cmd = ['action-set']
for k, v in list(values.items()):
cmd.append('{}={}'.format(k, v))
subprocess.check_call(cmd)
def action_fail(message):
"""Sets the action status to failed and sets the error message.
The results set by action_set are preserved."""
subprocess.check_call(['action-fail', message])

View File

@ -339,12 +339,16 @@ def lsb_release():
def pwgen(length=None):
"""Generate a random pasword."""
if length is None:
# A random length is ok to use a weak PRNG
length = random.choice(range(35, 45))
alphanumeric_chars = [
l for l in (string.ascii_letters + string.digits)
if l not in 'l0QD1vAEIOUaeiou']
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
# actual password
random_generator = random.SystemRandom()
random_chars = [
random.choice(alphanumeric_chars) for _ in range(length)]
random_generator.choice(alphanumeric_chars) for _ in range(length)]
return(''.join(random_chars))

View File

@ -45,12 +45,14 @@ class RelationContext(dict):
"""
name = None
interface = None
required_keys = []
def __init__(self, name=None, additional_required_keys=None):
if not hasattr(self, 'required_keys'):
self.required_keys = []
if name is not None:
self.name = name
if additional_required_keys is not None:
if additional_required_keys:
self.required_keys.extend(additional_required_keys)
self.get_data()
@ -134,7 +136,10 @@ class MysqlRelation(RelationContext):
"""
name = 'db'
interface = 'mysql'
required_keys = ['host', 'user', 'password', 'database']
def __init__(self, *args, **kwargs):
self.required_keys = ['host', 'user', 'password', 'database']
RelationContext.__init__(self, *args, **kwargs)
class HttpRelation(RelationContext):
@ -146,7 +151,10 @@ class HttpRelation(RelationContext):
"""
name = 'website'
interface = 'http'
required_keys = ['host', 'port']
def __init__(self, *args, **kwargs):
self.required_keys = ['host', 'port']
RelationContext.__init__(self, *args, **kwargs)
def provide_data(self):
return {

View File

@ -1,9 +1,6 @@
import os
import uuid
from charmhelpers.core.hookenv import (
relation_ids,
related_units,
relation_get,
config,
unit_get,
)
@ -12,14 +9,13 @@ from charmhelpers.contrib.network.ip import (
)
from charmhelpers.contrib.openstack.ip import resolve_address
from charmhelpers.core.host import list_nics, get_nic_hwaddr
from charmhelpers.core.strutils import bool_from_string
from charmhelpers.contrib.openstack import context
from charmhelpers.core.host import service_running, service_start
from charmhelpers.contrib.network.ovs import add_bridge, add_bridge_port
from charmhelpers.contrib.openstack.utils import get_host_ip
from charmhelpers.contrib.openstack.context import (
OSContextGenerator,
context_complete,
NeutronAPIContext,
)
import re
@ -28,47 +24,6 @@ OVS_BRIDGE = 'br-int'
DATA_BRIDGE = 'br-data'
def _neutron_api_settings():
'''
Inspects current neutron-plugin relation
'''
neutron_settings = {
'neutron_security_groups': False,
'l2_population': True,
'overlay_network_type': 'gre',
'enable_dvr': False,
}
for rid in relation_ids('neutron-plugin-api'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
if 'l2-population' not in rdata:
continue
neutron_settings = {
'l2_population': bool_from_string(rdata['l2-population']),
'overlay_network_type': rdata['overlay-network-type'],
'neutron_security_groups': bool_from_string(
rdata['neutron-security-groups']
),
}
if 'enable-dvr' in rdata:
neutron_settings['enable_dvr'] = bool_from_string(
rdata['enable-dvr']
)
# Override with configuration if set to true
if config('disable-security-groups'):
neutron_settings['neutron_security_groups'] = False
return neutron_settings
return neutron_settings
def use_dvr():
api_settings = _neutron_api_settings()
if 'enable_dvr' in api_settings:
return api_settings['enable_dvr']
else:
return False
class OVSPluginContext(context.NeutronContext):
interfaces = []
@ -82,7 +37,7 @@ class OVSPluginContext(context.NeutronContext):
@property
def neutron_security_groups(self):
neutron_api_settings = _neutron_api_settings()
neutron_api_settings = NeutronAPIContext()()
return neutron_api_settings['neutron_security_groups']
def get_data_port(self):
@ -125,10 +80,10 @@ class OVSPluginContext(context.NeutronContext):
ovs_ctxt['local_ip'] = \
get_address_in_network(config('os-data-network'),
get_host_ip(unit_get('private-address')))
neutron_api_settings = _neutron_api_settings()
neutron_api_settings = NeutronAPIContext()()
ovs_ctxt['neutron_security_groups'] = self.neutron_security_groups
ovs_ctxt['l2_population'] = neutron_api_settings['l2_population']
ovs_ctxt['distributed_routing'] = use_dvr()
ovs_ctxt['distributed_routing'] = neutron_api_settings['enable_dvr']
ovs_ctxt['overlay_network_type'] = \
neutron_api_settings['overlay_network_type']
# TODO: We need to sort out the syslog and debug/verbose options as a
@ -142,7 +97,7 @@ class OVSPluginContext(context.NeutronContext):
class L3AgentContext(OSContextGenerator):
def __call__(self):
neutron_api_settings = _neutron_api_settings()
neutron_api_settings = NeutronAPIContext()()
ctxt = {}
if neutron_api_settings['enable_dvr']:
ctxt['agent_mode'] = 'dvr'
@ -151,27 +106,6 @@ class L3AgentContext(OSContextGenerator):
return ctxt
class NetworkServiceContext(OSContextGenerator):
interfaces = ['neutron-network-service']
def __call__(self):
for rid in relation_ids('neutron-network-service'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
ctxt = {
'service_protocol':
rdata.get('service_protocol') or 'http',
'keystone_host': rdata.get('keystone_host'),
'service_port': rdata.get('service_port'),
'region': rdata.get('region'),
'service_tenant': rdata.get('service_tenant'),
'service_username': rdata.get('service_username'),
'service_password': rdata.get('service_password'),
}
if context_complete(ctxt):
return ctxt
SHARED_SECRET = "/etc/neutron/secret.txt"
@ -190,7 +124,7 @@ def get_shared_secret():
class DVRSharedSecretContext(OSContextGenerator):
def __call__(self):
if use_dvr():
if NeutronAPIContext()()['enable_dvr']:
ctxt = {
'shared_secret': get_shared_secret(),
'local_ip': resolve_address(),

View File

@ -50,7 +50,7 @@ DVR_RESOURCE_MAP = OrderedDict([
(NEUTRON_METADATA_AGENT_CONF, {
'services': ['neutron-metadata-agent'],
'contexts': [neutron_ovs_context.DVRSharedSecretContext(),
neutron_ovs_context.NetworkServiceContext()],
context.NetworkServiceContext()],
}),
])
TEMPLATES = 'templates/'
@ -118,4 +118,4 @@ def get_shared_secret():
def use_dvr():
return neutron_ovs_context.use_dvr()
return context.NeutronAPIContext()()['enable_dvr']