diff --git a/charm-helpers-hooks.yaml b/charm-helpers-hooks.yaml index 4765f2f3..84cc6c76 100644 --- a/charm-helpers-hooks.yaml +++ b/charm-helpers-hooks.yaml @@ -8,5 +8,6 @@ include: - contrib.network - contrib.python.packages - contrib.storage.linux + - contrib.python - payload.execd - contrib.charmsupport diff --git a/config.yaml b/config.yaml index f9a74bc9..6bf27e47 100644 --- a/config.yaml +++ b/config.yaml @@ -13,7 +13,7 @@ options: type: string default: description: | - A space-separated list of external ports to use for routing of instance + Space-delimited 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) @@ -21,8 +21,10 @@ options: type: string default: description: | - The data port will be added to br-data and will allow usage of flat or VLAN - network types with Neutron. + Space-delimited list of bridge:port mappings. Ports will be added to + their corresponding bridge. The bridges will allow usage of flat or + VLAN network types with Neutron and should match this defined in + bridge-mappings. openstack-origin: type: string default: distro @@ -120,6 +122,17 @@ options: description: | A comma-separated list of nagios servicegroups. If left empty, the nagios_context will be used as the servicegroup + bridge-mappings: + type: string + default: 'physnet1:br-data' + description: | + Space-separated list of ML2 data bridge mappings with format + :. + vlan-ranges: + type: string + default: "physnet1:1000:2000" + description: | + Space-delimited list of network provider vlan id ranges. # Network configuration options # by default all access is over 'private-address' os-data-network: @@ -160,4 +173,3 @@ options: description: | Default multicast port number that will be used to communicate between HA Cluster nodes. - diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py index 0cfeaa4c..0e0db566 100644 --- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py @@ -15,6 +15,7 @@ # along with charm-helpers. If not, see . import six +from collections import OrderedDict from charmhelpers.contrib.amulet.deployment import ( AmuletDeployment ) @@ -100,12 +101,34 @@ class OpenStackAmuletDeployment(AmuletDeployment): """ (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, - self.trusty_icehouse) = range(6) + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8) releases = { ('precise', None): self.precise_essex, ('precise', 'cloud:precise-folsom'): self.precise_folsom, ('precise', 'cloud:precise-grizzly'): self.precise_grizzly, ('precise', 'cloud:precise-havana'): self.precise_havana, ('precise', 'cloud:precise-icehouse'): self.precise_icehouse, - ('trusty', None): self.trusty_icehouse} + ('trusty', None): self.trusty_icehouse, + ('trusty', 'cloud:trusty-juno'): self.trusty_juno, + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo} return releases[(self.series, self.openstack)] + + def _get_openstack_release_string(self): + """Get openstack release string. + + Return a string representing the openstack release. + """ + releases = OrderedDict([ + ('precise', 'essex'), + ('quantal', 'folsom'), + ('raring', 'grizzly'), + ('saucy', 'havana'), + ('trusty', 'icehouse'), + ('utopic', 'juno'), + ('vivid', 'kilo'), + ]) + if self.openstack: + os_origin = self.openstack.split(':')[1] + return os_origin.split('%s-' % self.series)[1].split('/')[0] + else: + return releases[self.series] diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py index 2d9a95cd..45e65790 100644 --- a/hooks/charmhelpers/contrib/openstack/context.py +++ b/hooks/charmhelpers/contrib/openstack/context.py @@ -16,6 +16,7 @@ import json import os +import re import time from base64 import b64decode from subprocess import check_call @@ -46,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, ) @@ -64,16 +68,22 @@ 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, + get_ipv4_addr, get_ipv6_addr, get_netmask_for_address, format_ipv6_addr, is_address_in_network, + 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'] @@ -727,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() @@ -883,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. @@ -1104,3 +1163,145 @@ class SysctlContext(OSContextGenerator): sysctl_create(sysctl_dict, '/etc/sysctl.d/50-{0}.conf'.format(charm_name())) return {'sysctl': sysctl_dict} + + +class NeutronAPIContext(OSContextGenerator): + ''' + Inspects current neutron-plugin-api relation for neutron settings. Return + defaults if it is not present. + ''' + interfaces = ['neutron-plugin-api'] + + 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: + settings[nkey] = defv + return settings + + +class ExternalPortContext(NeutronPortContext): + + def __call__(self): + 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): + 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 {} diff --git a/hooks/charmhelpers/contrib/openstack/neutron.py b/hooks/charmhelpers/contrib/openstack/neutron.py index 902757fe..f8851050 100644 --- a/hooks/charmhelpers/contrib/openstack/neutron.py +++ b/hooks/charmhelpers/contrib/openstack/neutron.py @@ -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 diff --git a/hooks/charmhelpers/contrib/openstack/templates/git.upstart b/hooks/charmhelpers/contrib/openstack/templates/git.upstart new file mode 100644 index 00000000..da94ad12 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/git.upstart @@ -0,0 +1,13 @@ +description "{{ service_description }}" +author "Juju {{ service_name }} Charm " + +start on runlevel [2345] +stop on runlevel [!2345] + +respawn + +exec start-stop-daemon --start --chuid {{ user_name }} \ + --chdir {{ start_dir }} --name {{ process_name }} \ + --exec {{ executable_name }} -- \ + --config-file={{ config_file }} \ + --log-file={{ log_file }} diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken b/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken new file mode 100644 index 00000000..2a37edd5 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken @@ -0,0 +1,9 @@ +{% if auth_host -%} +[keystone_authtoken] +identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }} +auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }} +admin_tenant_name = {{ admin_tenant_name }} +admin_user = {{ admin_user }} +admin_password = {{ admin_password }} +signing_dir = {{ signing_dir }} +{% endif -%} diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo b/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo new file mode 100644 index 00000000..b444c9c9 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo @@ -0,0 +1,22 @@ +{% if rabbitmq_host or rabbitmq_hosts -%} +[oslo_messaging_rabbit] +rabbit_userid = {{ rabbitmq_user }} +rabbit_virtual_host = {{ rabbitmq_virtual_host }} +rabbit_password = {{ rabbitmq_password }} +{% if rabbitmq_hosts -%} +rabbit_hosts = {{ rabbitmq_hosts }} +{% if rabbitmq_ha_queues -%} +rabbit_ha_queues = True +rabbit_durable_queues = False +{% endif -%} +{% else -%} +rabbit_host = {{ rabbitmq_host }} +{% endif -%} +{% if rabbit_ssl_port -%} +rabbit_use_ssl = True +rabbit_port = {{ rabbit_ssl_port }} +{% if rabbit_ssl_ca -%} +kombu_ssl_ca_certs = {{ rabbit_ssl_ca }} +{% endif -%} +{% endif -%} +{% endif -%} diff --git a/hooks/charmhelpers/contrib/openstack/templates/zeromq b/hooks/charmhelpers/contrib/openstack/templates/section-zeromq similarity index 66% rename from hooks/charmhelpers/contrib/openstack/templates/zeromq rename to hooks/charmhelpers/contrib/openstack/templates/section-zeromq index 0695eef1..95f1a76c 100644 --- a/hooks/charmhelpers/contrib/openstack/templates/zeromq +++ b/hooks/charmhelpers/contrib/openstack/templates/section-zeromq @@ -3,12 +3,12 @@ rpc_backend = zmq rpc_zmq_host = {{ zmq_host }} {% if zmq_redis_address -%} -rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_redis.MatchMakerRedis +rpc_zmq_matchmaker = redis matchmaker_heartbeat_freq = 15 matchmaker_heartbeat_ttl = 30 [matchmaker_redis] host = {{ zmq_redis_address }} {% else -%} -rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_ring.MatchMakerRing +rpc_zmq_matchmaker = ring {% endif -%} {% endif -%} diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py index 4f110c63..78c5e2df 100644 --- a/hooks/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -30,6 +30,10 @@ import yaml from charmhelpers.contrib.network import ip +from charmhelpers.core import ( + unitdata, +) + from charmhelpers.core.hookenv import ( config, log as juju_log, @@ -330,6 +334,21 @@ def configure_installation_source(rel): error_out("Invalid openstack-release specified: %s" % rel) +def config_value_changed(option): + """ + Determine if config value changed since last call to this function. + """ + hook_data = unitdata.HookData() + with hook_data(): + db = unitdata.kv() + current = config(option) + saved = db.get(option) + db.set(option, current) + if saved is None: + return False + return current != saved + + def save_script_rc(script_path="scripts/scriptrc", **env_vars): """ Write an rc file in the charm-delivered directory containing @@ -469,82 +488,95 @@ def os_requires_version(ostack_release, pkg): def git_install_requested(): - """Returns true if openstack-origin-git is specified.""" - return config('openstack-origin-git') != "None" + """ + Returns true if openstack-origin-git is specified. + """ + return config('openstack-origin-git') is not None requirements_dir = None -def git_clone_and_install(file_name, core_project): - """Clone/install all OpenStack repos specified in yaml config file.""" - global requirements_dir +def git_clone_and_install(projects_yaml, core_project): + """ + Clone/install all specified OpenStack repositories. - if file_name == "None": + The expected format of projects_yaml is: + repositories: + - {name: keystone, + repository: 'git://git.openstack.org/openstack/keystone.git', + branch: 'stable/icehouse'} + - {name: requirements, + repository: 'git://git.openstack.org/openstack/requirements.git', + branch: 'stable/icehouse'} + directory: /mnt/openstack-git + + The directory key is optional. + """ + global requirements_dir + parent_dir = '/mnt/openstack-git' + + if not projects_yaml: return - yaml_file = os.path.join(charm_dir(), file_name) + projects = yaml.load(projects_yaml) + _git_validate_projects_yaml(projects, core_project) - # clone/install the requirements project first - installed = _git_clone_and_install_subset(yaml_file, - whitelist=['requirements']) - if 'requirements' not in installed: - error_out('requirements git repository must be specified') + if 'directory' in projects.keys(): + parent_dir = projects['directory'] - # clone/install all other projects except requirements and the core project - blacklist = ['requirements', core_project] - _git_clone_and_install_subset(yaml_file, blacklist=blacklist, - update_requirements=True) - - # clone/install the core project - whitelist = [core_project] - installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist, - update_requirements=True) - if core_project not in installed: - error_out('{} git repository must be specified'.format(core_project)) + for p in projects['repositories']: + repo = p['repository'] + branch = p['branch'] + if p['name'] == 'requirements': + repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, + update_requirements=False) + requirements_dir = repo_dir + else: + repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, + update_requirements=True) -def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[], - update_requirements=False): - """Clone/install subset of OpenStack repos specified in yaml config file.""" - global requirements_dir - installed = [] +def _git_validate_projects_yaml(projects, core_project): + """ + Validate the projects yaml. + """ + _git_ensure_key_exists('repositories', projects) - with open(yaml_file, 'r') as fd: - projects = yaml.load(fd) - for proj, val in projects.items(): - # The project subset is chosen based on the following 3 rules: - # 1) If project is in blacklist, we don't clone/install it, period. - # 2) If whitelist is empty, we clone/install everything else. - # 3) If whitelist is not empty, we clone/install everything in the - # whitelist. - if proj in blacklist: - continue - if whitelist and proj not in whitelist: - continue - repo = val['repository'] - branch = val['branch'] - repo_dir = _git_clone_and_install_single(repo, branch, - update_requirements) - if proj == 'requirements': - requirements_dir = repo_dir - installed.append(proj) - return installed + for project in projects['repositories']: + _git_ensure_key_exists('name', project.keys()) + _git_ensure_key_exists('repository', project.keys()) + _git_ensure_key_exists('branch', project.keys()) + + if projects['repositories'][0]['name'] != 'requirements': + error_out('{} git repo must be specified first'.format('requirements')) + + if projects['repositories'][-1]['name'] != core_project: + error_out('{} git repo must be specified last'.format(core_project)) -def _git_clone_and_install_single(repo, branch, update_requirements=False): - """Clone and install a single git repository.""" - dest_parent_dir = "/mnt/openstack-git/" - dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo)) +def _git_ensure_key_exists(key, keys): + """ + Ensure that key exists in keys. + """ + if key not in keys: + error_out('openstack-origin-git key \'{}\' is missing'.format(key)) - if not os.path.exists(dest_parent_dir): - juju_log('Host dir not mounted at {}. ' - 'Creating directory there instead.'.format(dest_parent_dir)) - os.mkdir(dest_parent_dir) + +def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements): + """ + Clone and install a single git repository. + """ + dest_dir = os.path.join(parent_dir, os.path.basename(repo)) + + if not os.path.exists(parent_dir): + juju_log('Directory already exists at {}. ' + 'No need to create directory.'.format(parent_dir)) + os.mkdir(parent_dir) if not os.path.exists(dest_dir): juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch)) - repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch) + repo_dir = install_remote(repo, dest=parent_dir, branch=branch) else: repo_dir = dest_dir @@ -561,16 +593,39 @@ def _git_clone_and_install_single(repo, branch, update_requirements=False): def _git_update_requirements(package_dir, reqs_dir): - """Update from global requirements. + """ + Update from global requirements. - Update an OpenStack git directory's requirements.txt and - test-requirements.txt from global-requirements.txt.""" + Update an OpenStack git directory's requirements.txt and + test-requirements.txt from global-requirements.txt. + """ orig_dir = os.getcwd() os.chdir(reqs_dir) - cmd = "python update.py {}".format(package_dir) + cmd = ['python', 'update.py', package_dir] try: - subprocess.check_call(cmd.split(' ')) + subprocess.check_call(cmd) except subprocess.CalledProcessError: package = os.path.basename(package_dir) error_out("Error updating {} from global-requirements.txt".format(package)) os.chdir(orig_dir) + + +def git_src_dir(projects_yaml, project): + """ + Return the directory where the specified project's source is located. + """ + parent_dir = '/mnt/openstack-git' + + if not projects_yaml: + return + + projects = yaml.load(projects_yaml) + + if 'directory' in projects.keys(): + parent_dir = projects['directory'] + + for p in projects['repositories']: + if p['name'] == project: + return os.path.join(parent_dir, os.path.basename(p['repository'])) + + return None diff --git a/hooks/charmhelpers/contrib/python/debug.py b/hooks/charmhelpers/contrib/python/debug.py new file mode 100644 index 00000000..871cd6f3 --- /dev/null +++ b/hooks/charmhelpers/contrib/python/debug.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright 2014-2015 Canonical Limited. +# +# This file is part of charm-helpers. +# +# charm-helpers is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 as +# published by the Free Software Foundation. +# +# charm-helpers is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with charm-helpers. If not, see . + +from __future__ import print_function + +import atexit +import sys + +from charmhelpers.contrib.python.rpdb import Rpdb +from charmhelpers.core.hookenv import ( + open_port, + close_port, + ERROR, + log +) + +__author__ = "Jorge Niedbalski " + +DEFAULT_ADDR = "0.0.0.0" +DEFAULT_PORT = 4444 + + +def _error(message): + log(message, level=ERROR) + + +def set_trace(addr=DEFAULT_ADDR, port=DEFAULT_PORT): + """ + Set a trace point using the remote debugger + """ + atexit.register(close_port, port) + try: + log("Starting a remote python debugger session on %s:%s" % (addr, + port)) + open_port(port) + debugger = Rpdb(addr=addr, port=port) + debugger.set_trace(sys._getframe().f_back) + except: + _error("Cannot start a remote debug session on %s:%s" % (addr, + port)) diff --git a/hooks/charmhelpers/contrib/python/rpdb.py b/hooks/charmhelpers/contrib/python/rpdb.py new file mode 100644 index 00000000..d503f88f --- /dev/null +++ b/hooks/charmhelpers/contrib/python/rpdb.py @@ -0,0 +1,58 @@ +# Copyright 2014-2015 Canonical Limited. +# +# This file is part of charm-helpers. +# +# charm-helpers is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 as +# published by the Free Software Foundation. +# +# charm-helpers is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with charm-helpers. If not, see . + +"""Remote Python Debugger (pdb wrapper).""" + +import pdb +import socket +import sys + +__author__ = "Bertrand Janin " +__version__ = "0.1.3" + + +class Rpdb(pdb.Pdb): + + def __init__(self, addr="127.0.0.1", port=4444): + """Initialize the socket and initialize pdb.""" + + # Backup stdin and stdout before replacing them by the socket handle + self.old_stdout = sys.stdout + self.old_stdin = sys.stdin + + # Open a 'reusable' socket to let the webapp reload on the same port + self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) + self.skt.bind((addr, port)) + self.skt.listen(1) + (clientsocket, address) = self.skt.accept() + handle = clientsocket.makefile('rw') + pdb.Pdb.__init__(self, completekey='tab', stdin=handle, stdout=handle) + sys.stdout = sys.stdin = handle + + def shutdown(self): + """Revert stdin and stdout, close the socket.""" + sys.stdout = self.old_stdout + sys.stdin = self.old_stdin + self.skt.close() + self.set_continue() + + def do_continue(self, arg): + """Stop all operation on ``continue``.""" + self.shutdown() + return 1 + + do_EOF = do_quit = do_exit = do_c = do_cont = do_continue diff --git a/hooks/charmhelpers/contrib/python/version.py b/hooks/charmhelpers/contrib/python/version.py new file mode 100644 index 00000000..c39fcbf7 --- /dev/null +++ b/hooks/charmhelpers/contrib/python/version.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright 2014-2015 Canonical Limited. +# +# This file is part of charm-helpers. +# +# charm-helpers is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 as +# published by the Free Software Foundation. +# +# charm-helpers is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with charm-helpers. If not, see . + +import sys + +__author__ = "Jorge Niedbalski " + + +def current_version(): + """Current system python version""" + return sys.version_info + + +def current_version_string(): + """Current system python version as string major.minor.micro""" + return "{0}.{1}.{2}".format(sys.version_info.major, + sys.version_info.minor, + sys.version_info.micro) diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index cf552b39..715dd4c5 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -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]) diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py index b771c611..830822af 100644 --- a/hooks/charmhelpers/core/host.py +++ b/hooks/charmhelpers/core/host.py @@ -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)) diff --git a/hooks/charmhelpers/core/services/helpers.py b/hooks/charmhelpers/core/services/helpers.py index 15b21664..3eb5fb44 100644 --- a/hooks/charmhelpers/core/services/helpers.py +++ b/hooks/charmhelpers/core/services/helpers.py @@ -139,7 +139,7 @@ class MysqlRelation(RelationContext): def __init__(self, *args, **kwargs): self.required_keys = ['host', 'user', 'password', 'database'] - super(HttpRelation).__init__(self, *args, **kwargs) + RelationContext.__init__(self, *args, **kwargs) class HttpRelation(RelationContext): @@ -154,7 +154,7 @@ class HttpRelation(RelationContext): def __init__(self, *args, **kwargs): self.required_keys = ['host', 'port'] - super(HttpRelation).__init__(self, *args, **kwargs) + RelationContext.__init__(self, *args, **kwargs) def provide_data(self): return { diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py index 3000134a..406a35c5 100644 --- a/hooks/charmhelpers/core/unitdata.py +++ b/hooks/charmhelpers/core/unitdata.py @@ -443,7 +443,7 @@ class HookData(object): data = hookenv.execution_environment() self.conf = conf_delta = self.kv.delta(data['conf'], 'config') self.rels = rels_delta = self.kv.delta(data['rels'], 'rels') - self.kv.set('env', data['env']) + self.kv.set('env', dict(data['env'])) self.kv.set('unit', data['unit']) self.kv.set('relid', data.get('relid')) return conf_delta, rels_delta diff --git a/hooks/quantum_contexts.py b/hooks/quantum_contexts.py index 4cea695f..82f112e7 100644 --- a/hooks/quantum_contexts.py +++ b/hooks/quantum_contexts.py @@ -2,15 +2,8 @@ import os import uuid import socket -from charmhelpers.core.host import ( - list_nics, - get_nic_hwaddr -) from charmhelpers.core.hookenv import ( config, - relation_ids, - related_units, - relation_get, unit_get, cached ) @@ -19,7 +12,7 @@ from charmhelpers.fetch import ( ) from charmhelpers.contrib.openstack.context import ( OSContextGenerator, - context_complete, + NeutronAPIContext, ) from charmhelpers.contrib.openstack.utils import ( get_os_codename_install_source @@ -27,12 +20,11 @@ from charmhelpers.contrib.openstack.utils import ( from charmhelpers.contrib.hahelpers.cluster import( eligible_leader ) -import re from charmhelpers.contrib.network.ip import ( get_address_in_network, - get_ipv4_addr, - get_ipv6_addr, - is_bridge_member, +) +from charmhelpers.contrib.openstack.neutron import ( + parse_vlan_range_mappings, ) DB_USER = "quantum" @@ -104,60 +96,10 @@ def core_plugin(): return CORE_PLUGIN[networking_name()][plugin] -def neutron_api_settings(): - ''' - Inspects current neutron-plugin-api relation for neutron settings. Return - defaults if it is not present - ''' - neutron_settings = { - 'l2_population': False, - 'overlay_network_type': 'gre', - - } - 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': rdata['l2-population'], - 'overlay_network_type': rdata['overlay-network-type'], - } - return neutron_settings - return neutron_settings - - -class NetworkServiceContext(OSContextGenerator): - interfaces = ['quantum-network-service'] - - def __call__(self): - for rid in relation_ids('quantum-network-service'): - 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 {} - - class L3AgentContext(OSContextGenerator): def __call__(self): + api_settings = NeutronAPIContext()() ctxt = {} if config('run-internal-router') == 'leader': ctxt['handle_internal_only_router'] = eligible_leader(None) @@ -170,68 +112,19 @@ class L3AgentContext(OSContextGenerator): if config('external-network-id'): ctxt['ext_net_id'] = config('external-network-id') - if config('plugin'): ctxt['plugin'] = config('plugin') + if api_settings['enable_dvr']: + ctxt['agent_mode'] = 'dvr_snat' + else: + ctxt['agent_mode'] = 'legacy' return ctxt -class NeutronPortContext(OSContextGenerator): - - 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] - 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 - - -class ExternalPortContext(NeutronPortContext): - - def __call__(self): - port = self._resolve_port('ext-port') - if port: - return {"ext_port": port} - else: - return None - - -class DataPortContext(NeutronPortContext): - - def __call__(self): - port = self._resolve_port('data-port') - if port: - return {"data_port": port} - else: - return None - - class QuantumGatewayContext(OSContextGenerator): def __call__(self): - napi_settings = neutron_api_settings() + api_settings = NeutronAPIContext()() ctxt = { 'shared_secret': get_shared_secret(), 'local_ip': @@ -242,10 +135,29 @@ class QuantumGatewayContext(OSContextGenerator): 'debug': config('debug'), 'verbose': config('verbose'), 'instance_mtu': config('instance-mtu'), - 'l2_population': napi_settings['l2_population'], + 'l2_population': api_settings['l2_population'], + 'enable_dvr': api_settings['enable_dvr'], + 'enable_l3ha': api_settings['enable_l3ha'], 'overlay_network_type': - napi_settings['overlay_network_type'], + api_settings['overlay_network_type'], } + + mappings = config('bridge-mappings') + if mappings: + ctxt['bridge_mappings'] = mappings + + vlan_ranges = config('vlan-ranges') + vlan_range_mappings = parse_vlan_range_mappings(vlan_ranges) + if vlan_range_mappings: + providers = sorted(vlan_range_mappings.keys()) + ctxt['network_providers'] = ' '.join(providers) + ctxt['vlan_ranges'] = vlan_ranges + + net_dev_mtu = api_settings['network_device_mtu'] + if net_dev_mtu: + ctxt['network_device_mtu'] = net_dev_mtu + ctxt['veth_mtu'] = net_dev_mtu + return ctxt diff --git a/hooks/quantum_hooks.py b/hooks/quantum_hooks.py index 29e5109e..61e81f7a 100755 --- a/hooks/quantum_hooks.py +++ b/hooks/quantum_hooks.py @@ -41,6 +41,7 @@ from charmhelpers.contrib.charmsupport import nrpe import sys from quantum_utils import ( + L3HA_PACKAGES, register_configs, restart_map, services, @@ -58,7 +59,8 @@ from quantum_utils import ( install_legacy_ha_files, cleanup_ovs_netns, reassign_agent_resources, - stop_neutron_ha_monitor_daemon + stop_neutron_ha_monitor_daemon, + use_l3ha, ) hooks = Hooks() @@ -197,13 +199,23 @@ def amqp_departed(): 'pgsql-db-relation-changed', 'amqp-relation-changed', 'cluster-relation-changed', - 'cluster-relation-joined', - 'neutron-plugin-api-relation-changed') + 'cluster-relation-joined') @restart_on_change(restart_map()) def db_amqp_changed(): CONFIGS.write_all() +@hooks.hook('neutron-plugin-api-relation-changed') +@restart_on_change(restart_map()) +def neutron_plugin_api_changed(): + if use_l3ha(): + apt_update() + apt_install(L3HA_PACKAGES, fatal=True) + else: + apt_purge(L3HA_PACKAGES, fatal=True) + CONFIGS.write_all() + + @hooks.hook('quantum-network-service-relation-changed') @restart_on_change(restart_map()) def nm_changed(): diff --git a/hooks/quantum_utils.py b/hooks/quantum_utils.py index e7813027..d96475b8 100644 --- a/hooks/quantum_utils.py +++ b/hooks/quantum_utils.py @@ -41,7 +41,12 @@ from charmhelpers.contrib.openstack.neutron import ( import charmhelpers.contrib.openstack.context as context from charmhelpers.contrib.openstack.context import ( - SyslogContext + SyslogContext, + NeutronAPIContext, + NetworkServiceContext, + ExternalPortContext, + PhyNICMTUContext, + DataPortContext, ) import charmhelpers.contrib.openstack.templating as templating from charmhelpers.contrib.openstack.neutron import headers_package @@ -50,11 +55,11 @@ from quantum_contexts import ( NEUTRON, QUANTUM, networking_name, QuantumGatewayContext, - NetworkServiceContext, L3AgentContext, - ExternalPortContext, - DataPortContext, - remap_plugin + remap_plugin, +) +from charmhelpers.contrib.openstack.neutron import ( + parse_bridge_mappings, ) from copy import deepcopy @@ -167,6 +172,7 @@ LEGACY_FILES_MAP = { }, } LEGACY_RES_MAP = ['res_monitor'] +L3HA_PACKAGES = ['keepalived'] def get_early_packages(): @@ -197,16 +203,28 @@ def get_packages(): packages.append('openswan') if source >= 'kilo': packages.append('python-neutron-fwaas') + packages.extend(determine_l3ha_packages()) return packages +def determine_l3ha_packages(): + if use_l3ha(): + return L3HA_PACKAGES + return [] + + def get_common_package(): if get_os_codename_package('quantum-common', fatal=False) is not None: return 'quantum-common' else: return 'neutron-common' + +def use_l3ha(): + return NeutronAPIContext()()['enable_l3ha'] + EXT_PORT_CONF = '/etc/init/ext-port.conf' +PHY_NIC_MTU_CONF = '/etc/init/os-charm-phy-nic-mtu.conf' TEMPLATES = 'templates' QUANTUM_CONF = "/etc/quantum/quantum.conf" @@ -293,7 +311,11 @@ QUANTUM_OVS_CONFIG_FILES = { }, EXT_PORT_CONF: { 'hook_contexts': [ExternalPortContext()], - 'services': [] + 'services': ['ext-port'] + }, + PHY_NIC_MTU_CONF: { + 'hook_contexts': [PhyNICMTUContext()], + 'services': ['os-charm-phy-nic-mtu'] } } QUANTUM_OVS_CONFIG_FILES.update(QUANTUM_SHARED_CONFIG_FILES) @@ -319,7 +341,7 @@ NEUTRON_OVS_CONFIG_FILES = { 'hook_contexts': [NetworkServiceContext(), L3AgentContext(), QuantumGatewayContext()], - 'services': ['neutron-l3-agent'] + 'services': ['neutron-l3-agent', 'neutron-vpn-agent'] }, NEUTRON_METERING_AGENT_CONF: { 'hook_contexts': [QuantumGatewayContext()], @@ -337,7 +359,7 @@ NEUTRON_OVS_CONFIG_FILES = { }, NEUTRON_FWAAS_CONF: { 'hook_contexts': [QuantumGatewayContext()], - 'services': ['neutron-l3-agent'] + 'services': ['neutron-l3-agent', 'neutron-vpn-agent'] }, NEUTRON_OVS_PLUGIN_CONF: { 'hook_contexts': [QuantumGatewayContext()], @@ -349,7 +371,11 @@ NEUTRON_OVS_CONFIG_FILES = { }, EXT_PORT_CONF: { 'hook_contexts': [ExternalPortContext()], - 'services': [] + 'services': ['ext-port'] + }, + PHY_NIC_MTU_CONF: { + 'hook_contexts': [PhyNICMTUContext()], + 'services': ['os-charm-phy-nic-mtu'] } } NEUTRON_OVS_CONFIG_FILES.update(NEUTRON_SHARED_CONFIG_FILES) @@ -471,7 +497,6 @@ def restart_map(): INT_BRIDGE = "br-int" EXT_BRIDGE = "br-ex" -DATA_BRIDGE = 'br-data' DHCP_AGENT = "DHCP Agent" L3_AGENT = "L3 Agent" @@ -603,11 +628,20 @@ def configure_ovs(): ext_port_ctx = ExternalPortContext()() if ext_port_ctx and ext_port_ctx['ext_port']: add_bridge_port(EXT_BRIDGE, ext_port_ctx['ext_port']) - add_bridge(DATA_BRIDGE) - data_port_ctx = DataPortContext()() - if data_port_ctx and data_port_ctx['data_port']: - add_bridge_port(DATA_BRIDGE, data_port_ctx['data_port'], - promisc=True) + + portmaps = DataPortContext()() + bridgemaps = parse_bridge_mappings(config('bridge-mappings')) + for provider, br in bridgemaps.iteritems(): + add_bridge(br) + + if not portmaps or br not in portmaps: + continue + + add_bridge_port(br, portmaps[br], promisc=True) + + # Ensure this runs so that mtu is applied to data-port interfaces if + # provided. + service_restart('os-charm-phy-nic-mtu') def copy_file(src, dst, perms=None, force=False): diff --git a/templates/ext-port.conf b/templates/ext-port.conf index 6080c30e..1d850240 100644 --- a/templates/ext-port.conf +++ b/templates/ext-port.conf @@ -5,5 +5,12 @@ start on runlevel [2345] task script - ip link set {{ ext_port }} up -end script \ No newline at end of file + EXT_PORT="{{ ext_port }}" + MTU="{{ ext_port_mtu }}" + if [ -n "$EXT_PORT" ]; then + ip link set $EXT_PORT up + if [ -n "$MTU" ]; then + ip link set $EXT_PORT mtu $MTU + fi + fi +end script diff --git a/templates/havana/ovs_neutron_plugin.ini b/templates/havana/ovs_neutron_plugin.ini index 96d0f824..e58afd65 100644 --- a/templates/havana/ovs_neutron_plugin.ini +++ b/templates/havana/ovs_neutron_plugin.ini @@ -7,3 +7,8 @@ local_ip = {{ local_ip }} tenant_network_type = gre enable_tunneling = True tunnel_id_ranges = 1:1000 + +[agent] +{% if veth_mtu -%} +veth_mtu = {{ veth_mtu }} +{% endif %} diff --git a/templates/icehouse/ml2_conf.ini b/templates/icehouse/ml2_conf.ini index b7245222..c9c04982 100644 --- a/templates/icehouse/ml2_conf.ini +++ b/templates/icehouse/ml2_conf.ini @@ -14,19 +14,22 @@ tunnel_id_ranges = 1:1000 vni_ranges = 1001:2000 [ml2_type_vlan] -network_vlan_ranges = physnet1:1000:2000 +network_vlan_ranges = {{ vlan_ranges }} [ml2_type_flat] -flat_networks = physnet1 +flat_networks = {{ network_providers }} [ovs] enable_tunneling = True local_ip = {{ local_ip }} -bridge_mappings = physnet1:br-data +bridge_mappings = {{ bridge_mappings }} [agent] tunnel_types = {{ overlay_network_type }} l2_population = {{ l2_population }} +{% if veth_mtu -%} +veth_mtu = {{ veth_mtu }} +{% endif %} [securitygroup] firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver diff --git a/templates/icehouse/neutron.conf b/templates/icehouse/neutron.conf index 6a8d98df..aa7ca65a 100644 --- a/templates/icehouse/neutron.conf +++ b/templates/icehouse/neutron.conf @@ -9,9 +9,10 @@ lock_path = /var/lock/neutron core_plugin = {{ core_plugin }} {% include "parts/rabbitmq" %} control_exchange = neutron -{% if notifications == 'True' -%} notification_driver = neutron.openstack.common.notifier.list_notifier +list_notifier_drivers = neutron.openstack.common.notifier.rabbit_notifier +{% if network_device_mtu -%} +network_device_mtu = {{ network_device_mtu }} {% endif -%} - [agent] root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf diff --git a/templates/juno/l3_agent.ini b/templates/juno/l3_agent.ini new file mode 100644 index 00000000..6fd6cd4b --- /dev/null +++ b/templates/juno/l3_agent.ini @@ -0,0 +1,25 @@ +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +############################################################################### +[DEFAULT] +interface_driver = neutron.agent.linux.interface.OVSInterfaceDriver +auth_url = {{ service_protocol }}://{{ keystone_host }}:{{ service_port }}/v2.0 +auth_region = {{ region }} +admin_tenant_name = {{ service_tenant }} +admin_user = {{ service_username }} +admin_password = {{ service_password }} +root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf +handle_internal_only_routers = {{ handle_internal_only_router }} +{% if plugin == 'n1kv' %} +l3_agent_manager = neutron.agent.l3_agent.L3NATAgentWithStateReport +external_network_bridge = br-int +ovs_use_veth = False +use_namespaces = True +{% else %} +ovs_use_veth = True +{% endif %} +{% if ext_net_id -%} +gateway_external_network_id = {{ ext_net_id }} +{% endif -%} +agent_mode = {{ agent_mode }} diff --git a/templates/juno/ml2_conf.ini b/templates/juno/ml2_conf.ini new file mode 100644 index 00000000..4c1dfa9d --- /dev/null +++ b/templates/juno/ml2_conf.ini @@ -0,0 +1,36 @@ +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +############################################################################### +[ml2] +type_drivers = gre,vxlan,vlan,flat +tenant_network_types = gre,vxlan,vlan,flat +mechanism_drivers = openvswitch,l2population + +[ml2_type_gre] +tunnel_id_ranges = 1:1000 + +[ml2_type_vxlan] +vni_ranges = 1001:2000 + +[ml2_type_vlan] +network_vlan_ranges = {{ vlan_ranges }} + +[ml2_type_flat] +flat_networks = {{ network_providers }} + +[ovs] +enable_tunneling = True +local_ip = {{ local_ip }} +bridge_mappings = {{ bridge_mappings }} + +[agent] +tunnel_types = {{ overlay_network_type }} +l2_population = {{ l2_population }} +enable_distributed_routing = {{ enable_dvr }} +{% if veth_mtu -%} +veth_mtu = {{ veth_mtu }} +{% endif %} + +[securitygroup] +firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver diff --git a/templates/juno/nova.conf b/templates/juno/nova.conf deleted file mode 100644 index 4653379a..00000000 --- a/templates/juno/nova.conf +++ /dev/null @@ -1,29 +0,0 @@ -# juno -############################################################################### -# [ WARNING ] -# Configuration file maintained by Juju. Local changes may be overwritten. -############################################################################### -[DEFAULT] -logdir=/var/log/nova -state_path=/var/lib/nova -lock_path=/var/lock/nova -root_helper=sudo nova-rootwrap /etc/nova/rootwrap.conf -verbose= {{ verbose }} -use_syslog = {{ use_syslog }} -api_paste_config=/etc/nova/api-paste.ini -enabled_apis=metadata -multi_host=True -{% include "parts/database" %} -neutron_metadata_proxy_shared_secret={{ shared_secret }} -service_neutron_metadata_proxy=True -# Access to message bus -{% include "parts/rabbitmq" %} -# Access to neutron API services -network_api_class=nova.network.neutronv2.api.API -neutron_auth_strategy=keystone -neutron_url={{ quantum_url }} -neutron_admin_tenant_name={{ service_tenant }} -neutron_admin_username={{ service_username }} -neutron_admin_password={{ service_password }} -neutron_admin_auth_url={{ service_protocol }}://{{ keystone_host }}:{{ service_port }}/v2.0 -{% include "zeromq" %} diff --git a/templates/kilo/fwaas_driver.ini b/templates/kilo/fwaas_driver.ini index 8ce9e542..b31a5008 100644 --- a/templates/kilo/fwaas_driver.ini +++ b/templates/kilo/fwaas_driver.ini @@ -1,3 +1,4 @@ +# kilo ############################################################################### # [ WARNING ] # Configuration file maintained by Juju. Local changes may be overwritten. diff --git a/templates/kilo/lbaas_agent.ini b/templates/kilo/lbaas_agent.ini index b37b7e1d..1c541b85 100644 --- a/templates/kilo/lbaas_agent.ini +++ b/templates/kilo/lbaas_agent.ini @@ -1,3 +1,8 @@ +# kilo +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +############################################################################### [DEFAULT] periodic_interval = 10 interface_driver = neutron.agent.linux.interface.OVSInterfaceDriver diff --git a/templates/juno/neutron.conf b/templates/kilo/neutron.conf similarity index 65% rename from templates/juno/neutron.conf rename to templates/kilo/neutron.conf index 49802e5c..34bafcc8 100644 --- a/templates/juno/neutron.conf +++ b/templates/kilo/neutron.conf @@ -1,3 +1,4 @@ +# kilo ############################################################################### # [ WARNING ] # Configuration file maintained by Juju. Local changes may be overwritten. @@ -5,15 +6,20 @@ [DEFAULT] verbose = {{ verbose }} debug = {{ debug }} -lock_path = /var/lock/neutron -core_plugin = {{ core_plugin }} -{% include "parts/rabbitmq" %} +core_plugin = {{ core_plugin }} control_exchange = neutron -{% if notifications == 'True' -%} notification_driver = neutron.openstack.common.notifier.list_notifier +list_notifier_drivers = neutron.openstack.common.notifier.rabbit_notifier +{% if network_device_mtu -%} +network_device_mtu = {{ network_device_mtu }} {% endif -%} -{% include "zeromq" %} +{% include "section-zeromq" %} [agent] root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf + +{% include "section-rabbitmq-oslo" %} + +[oslo_concurrency] +lock_path = /var/lock/neutron diff --git a/templates/kilo/nova.conf b/templates/kilo/nova.conf index 5b27041c..3545e36d 100644 --- a/templates/kilo/nova.conf +++ b/templates/kilo/nova.conf @@ -6,19 +6,17 @@ [DEFAULT] logdir=/var/log/nova state_path=/var/lib/nova -lock_path=/var/lock/nova root_helper=sudo nova-rootwrap /etc/nova/rootwrap.conf verbose= {{ verbose }} use_syslog = {{ use_syslog }} api_paste_config=/etc/nova/api-paste.ini enabled_apis=metadata multi_host=True -{% include "parts/database" %} -# Access to message bus -{% include "parts/rabbitmq" %} # Access to neutron API services network_api_class=nova.network.neutronv2.api.API -{% include "zeromq" %} + +{% include "section-zeromq" %} + [neutron] auth_strategy=keystone url={{ quantum_url }} @@ -28,3 +26,8 @@ admin_password={{ service_password }} admin_auth_url={{ service_protocol }}://{{ keystone_host }}:{{ service_port }}/v2.0 service_metadata_proxy=True metadata_proxy_shared_secret={{ shared_secret }} + +{% include "section-rabbitmq-oslo" %} + +[oslo_concurrency] +lock_path=/var/lock/nova diff --git a/templates/kilo/vpn_agent.ini b/templates/kilo/vpn_agent.ini index 90874fd0..95c41926 100644 --- a/templates/kilo/vpn_agent.ini +++ b/templates/kilo/vpn_agent.ini @@ -1,3 +1,4 @@ +# kilo ############################################################################### # [ WARNING ] # Configuration file maintained by Juju. Local changes may be overwritten. diff --git a/templates/kilo/zeromq b/templates/kilo/zeromq deleted file mode 100644 index 873be80e..00000000 --- a/templates/kilo/zeromq +++ /dev/null @@ -1,14 +0,0 @@ -{% if zmq_host -%} -# ZeroMQ configuration (restart-nonce: {{ zmq_nonce }}) -rpc_backend = zmq -rpc_zmq_host = {{ zmq_host }} -{% if zmq_redis_address -%} -rpc_zmq_matchmaker = oslo_messaging._drivers.matchmaker_redis.MatchMakerRedis -matchmaker_heartbeat_freq = 15 -matchmaker_heartbeat_ttl = 30 -[matchmaker_redis] -host = {{ zmq_redis_address }} -{% else -%} -rpc_zmq_matchmaker = oslo_messaging._drivers.matchmaker_ring.MatchMakerRing -{% endif -%} -{% endif -%} diff --git a/templates/os-charm-phy-nic-mtu.conf b/templates/os-charm-phy-nic-mtu.conf new file mode 100644 index 00000000..06d1967b --- /dev/null +++ b/templates/os-charm-phy-nic-mtu.conf @@ -0,0 +1,22 @@ +description "Enabling Quantum external networking port" + +start on runlevel [2345] + +task + +script + devs="{{ devs }}" + mtu="{{ mtu }}" + tmpfile=`mktemp` + echo $devs > $tmpfile + if [ -n "$mtu" ]; then + while read -r dev; do + [ -n "$dev" ] || continue + rc=0 + # Try all devices before exiting with error + ip link set $dev mtu $mtu || rc=$? + done < $tmpfile + rm $tmpfile + [ $rc = 0 ] || exit $rc + fi +end script \ No newline at end of file diff --git a/tests/charmhelpers/contrib/openstack/amulet/deployment.py b/tests/charmhelpers/contrib/openstack/amulet/deployment.py index 0cfeaa4c..0e0db566 100644 --- a/tests/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/tests/charmhelpers/contrib/openstack/amulet/deployment.py @@ -15,6 +15,7 @@ # along with charm-helpers. If not, see . import six +from collections import OrderedDict from charmhelpers.contrib.amulet.deployment import ( AmuletDeployment ) @@ -100,12 +101,34 @@ class OpenStackAmuletDeployment(AmuletDeployment): """ (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, - self.trusty_icehouse) = range(6) + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8) releases = { ('precise', None): self.precise_essex, ('precise', 'cloud:precise-folsom'): self.precise_folsom, ('precise', 'cloud:precise-grizzly'): self.precise_grizzly, ('precise', 'cloud:precise-havana'): self.precise_havana, ('precise', 'cloud:precise-icehouse'): self.precise_icehouse, - ('trusty', None): self.trusty_icehouse} + ('trusty', None): self.trusty_icehouse, + ('trusty', 'cloud:trusty-juno'): self.trusty_juno, + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo} return releases[(self.series, self.openstack)] + + def _get_openstack_release_string(self): + """Get openstack release string. + + Return a string representing the openstack release. + """ + releases = OrderedDict([ + ('precise', 'essex'), + ('quantal', 'folsom'), + ('raring', 'grizzly'), + ('saucy', 'havana'), + ('trusty', 'icehouse'), + ('utopic', 'juno'), + ('vivid', 'kilo'), + ]) + if self.openstack: + os_origin = self.openstack.split(':')[1] + return os_origin.split('%s-' % self.series)[1].split('/')[0] + else: + return releases[self.series] diff --git a/unit_tests/test_quantum_contexts.py b/unit_tests/test_quantum_contexts.py index af399609..f5513c74 100644 --- a/unit_tests/test_quantum_contexts.py +++ b/unit_tests/test_quantum_contexts.py @@ -14,16 +14,8 @@ 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', 'unit_get', ] @@ -46,138 +38,13 @@ def patch_open(): yield mock_open, mock_file -class _TestQuantumContext(CharmTestCase): +class DummyNeutronAPIContext(): - def setUp(self): - super(_TestQuantumContext, self).setUp(quantum_contexts, TO_PATCH) - self.config.side_effect = self.test_config.get + def __init__(self, return_value): + self.return_value = return_value - def test_not_related(self): - self.relation_ids.return_value = [] - self.assertEquals(self.context(), {}) - - def test_no_units(self): - self.relation_ids.return_value = [] - self.relation_ids.return_value = ['foo'] - self.related_units.return_value = [] - self.assertEquals(self.context(), {}) - - def test_no_data(self): - self.relation_ids.return_value = ['foo'] - self.related_units.return_value = ['bar'] - self.relation_get.side_effect = self.test_relation.get - self.context_complete.return_value = False - self.assertEquals(self.context(), {}) - - def test_data_multi_unit(self): - self.relation_ids.return_value = ['foo'] - self.related_units.return_value = ['bar', 'baz'] - self.context_complete.return_value = True - self.relation_get.side_effect = self.test_relation.get - self.assertEquals(self.context(), self.data_result) - - def test_data_single_unit(self): - self.relation_ids.return_value = ['foo'] - self.related_units.return_value = ['bar'] - self.context_complete.return_value = True - self.relation_get.side_effect = self.test_relation.get - self.assertEquals(self.context(), self.data_result) - - -class TestNetworkServiceContext(_TestQuantumContext): - - def setUp(self): - super(TestNetworkServiceContext, self).setUp() - self.context = quantum_contexts.NetworkServiceContext() - self.test_relation.set( - {'keystone_host': '10.5.0.1', - 'service_port': '5000', - 'auth_port': '20000', - 'service_tenant': 'tenant', - 'service_username': 'username', - 'service_password': 'password', - 'quantum_host': '10.5.0.2', - 'quantum_port': '9696', - 'quantum_url': 'http://10.5.0.2:9696/v2', - 'region': 'aregion'} - ) - self.data_result = { - 'keystone_host': '10.5.0.1', - 'service_port': '5000', - 'auth_port': '20000', - 'service_tenant': 'tenant', - 'service_username': 'username', - 'service_password': 'password', - 'quantum_host': '10.5.0.2', - 'quantum_port': '9696', - 'quantum_url': 'http://10.5.0.2:9696/v2', - 'region': 'aregion', - 'service_protocol': 'http', - 'auth_protocol': 'http', - } - - -class TestNeutronPortContext(CharmTestCase): - - def setUp(self): - super(TestNeutronPortContext, 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.assertIsNone(quantum_contexts.ExternalPortContext()()) - - def test_ext_port_eth(self): - self.config.return_value = 'eth1010' - self.assertEquals(quantum_contexts.ExternalPortContext()(), - {'ext_port': 'eth1010'}) - - def _fake_get_hwaddr(self, arg): - return self.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 = 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 = 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()(), - {'ext_port': 'eth2'}) - - def test_data_port_eth(self): - self.config.return_value = 'eth1010' - self.assertEquals(quantum_contexts.DataPortContext()(), - {'data_port': 'eth1010'}) + def __call__(self): + return self.return_value class TestL3AgentContext(CharmTestCase): @@ -187,32 +54,51 @@ class TestL3AgentContext(CharmTestCase): TO_PATCH) self.config.side_effect = self.test_config.get - def test_no_ext_netid(self): + @patch('quantum_contexts.NeutronAPIContext') + def test_no_ext_netid(self, _NeutronAPIContext): + _NeutronAPIContext.return_value = \ + DummyNeutronAPIContext(return_value={'enable_dvr': False}) self.test_config.set('run-internal-router', 'none') self.test_config.set('external-network-id', '') self.eligible_leader.return_value = False self.assertEquals(quantum_contexts.L3AgentContext()(), - {'handle_internal_only_router': False, + {'agent_mode': 'legacy', + 'handle_internal_only_router': False, 'plugin': 'ovs'}) - def test_hior_leader(self): + @patch('quantum_contexts.NeutronAPIContext') + def test_hior_leader(self, _NeutronAPIContext): + _NeutronAPIContext.return_value = \ + DummyNeutronAPIContext(return_value={'enable_dvr': False}) self.test_config.set('run-internal-router', 'leader') self.test_config.set('external-network-id', 'netid') self.eligible_leader.return_value = True self.assertEquals(quantum_contexts.L3AgentContext()(), - {'handle_internal_only_router': True, + {'agent_mode': 'legacy', + 'handle_internal_only_router': True, 'ext_net_id': 'netid', 'plugin': 'ovs'}) - def test_hior_all(self): + @patch('quantum_contexts.NeutronAPIContext') + def test_hior_all(self, _NeutronAPIContext): + _NeutronAPIContext.return_value = \ + DummyNeutronAPIContext(return_value={'enable_dvr': False}) self.test_config.set('run-internal-router', 'all') self.test_config.set('external-network-id', 'netid') self.eligible_leader.return_value = True self.assertEquals(quantum_contexts.L3AgentContext()(), - {'handle_internal_only_router': True, + {'agent_mode': 'legacy', + 'handle_internal_only_router': True, 'ext_net_id': 'netid', 'plugin': 'ovs'}) + @patch('quantum_contexts.NeutronAPIContext') + def test_dvr(self, _NeutronAPIContext): + _NeutronAPIContext.return_value = \ + DummyNeutronAPIContext(return_value={'enable_dvr': True}) + self.assertEquals(quantum_contexts.L3AgentContext()()['agent_mode'], + 'dvr_snat') + class TestQuantumGatewayContext(CharmTestCase): @@ -220,19 +106,37 @@ class TestQuantumGatewayContext(CharmTestCase): super(TestQuantumGatewayContext, self).setUp(quantum_contexts, TO_PATCH) self.config.side_effect = self.test_config.get + self.maxDiff = None + @patch('charmhelpers.contrib.openstack.context.relation_get') + @patch('charmhelpers.contrib.openstack.context.related_units') + @patch('charmhelpers.contrib.openstack.context.relation_ids') @patch.object(quantum_contexts, 'get_shared_secret') @patch.object(quantum_contexts, 'get_host_ip') - def test_all(self, _host_ip, _secret): + def test_all(self, _host_ip, _secret, _rids, _runits, _rget): + rdata = {'l2-population': 'True', + 'enable-dvr': 'True', + 'overlay-network-type': 'gre', + 'enable-l3ha': 'True', + 'network-device-mtu': 9000} self.test_config.set('plugin', 'ovs') self.test_config.set('debug', False) self.test_config.set('verbose', True) self.test_config.set('instance-mtu', 1420) + self.test_config.set('vlan-ranges', + 'physnet1:1000:2000 physnet2:2001:3000') + # Provided by neutron-api relation + _rids.return_value = ['neutron-plugin-api:0'] + _runits.return_value = ['neutron-api/0'] + _rget.side_effect = lambda *args, **kwargs: rdata self.get_os_codename_install_source.return_value = 'folsom' _host_ip.return_value = '10.5.0.1' _secret.return_value = 'testsecret' - self.assertEquals(quantum_contexts.QuantumGatewayContext()(), { + ctxt = quantum_contexts.QuantumGatewayContext()() + self.assertEquals(ctxt, { 'shared_secret': 'testsecret', + 'enable_dvr': True, + 'enable_l3ha': True, 'local_ip': '10.5.0.1', 'instance_mtu': 1420, 'core_plugin': "quantum.plugins.openvswitch.ovs_quantum_plugin." @@ -240,8 +144,13 @@ class TestQuantumGatewayContext(CharmTestCase): 'plugin': 'ovs', 'debug': False, 'verbose': True, - 'l2_population': False, + 'l2_population': True, 'overlay_network_type': 'gre', + 'bridge_mappings': 'physnet1:br-data', + 'network_providers': 'physnet1 physnet2', + 'vlan_ranges': 'physnet1:1000:2000 physnet2:2001:3000', + 'network_device_mtu': 9000, + 'veth_mtu': 9000, }) @@ -362,29 +271,3 @@ class TestMisc(CharmTestCase): self.config.return_value = 'ovs' self.assertEquals(quantum_contexts.core_plugin(), quantum_contexts.NEUTRON_ML2_PLUGIN) - - def test_neutron_api_settings(self): - self.relation_ids.return_value = ['foo'] - self.related_units.return_value = ['bar'] - self.test_relation.set({'l2-population': True, - 'overlay-network-type': 'gre', }) - self.relation_get.side_effect = self.test_relation.get - self.assertEquals(quantum_contexts.neutron_api_settings(), - {'l2_population': True, - 'overlay_network_type': 'gre'}) - - def test_neutron_api_settings2(self): - self.relation_ids.return_value = ['foo'] - self.related_units.return_value = ['bar'] - self.test_relation.set({'l2-population': True, - 'overlay-network-type': 'gre', }) - self.relation_get.side_effect = self.test_relation.get - self.assertEquals(quantum_contexts.neutron_api_settings(), - {'l2_population': True, - 'overlay_network_type': 'gre'}) - - def test_neutron_api_settings_no_apiplugin(self): - self.relation_ids.return_value = [] - self.assertEquals(quantum_contexts.neutron_api_settings(), - {'l2_population': False, - 'overlay_network_type': 'gre', }) diff --git a/unit_tests/test_quantum_hooks.py b/unit_tests/test_quantum_hooks.py index 06452f36..dc5d5d50 100644 --- a/unit_tests/test_quantum_hooks.py +++ b/unit_tests/test_quantum_hooks.py @@ -47,7 +47,8 @@ TO_PATCH = [ 'get_hacluster_config', 'remove_legacy_ha_files', 'cleanup_ovs_netns', - 'stop_neutron_ha_monitor_daemon' + 'stop_neutron_ha_monitor_daemon', + 'use_l3ha', ] @@ -248,6 +249,12 @@ class TestQuantumHooks(CharmTestCase): self.assertTrue(self.CONFIGS.write_all.called) self.install_ca_cert.assert_called_with('cert') + def test_neutron_plugin_changed(self): + self.use_l3ha.return_value = True + self._call_hook('neutron-plugin-api-relation-changed') + self.apt_install.assert_called_with(['keepalived'], fatal=True) + self.assertTrue(self.CONFIGS.write_all.called) + def test_cluster_departed_nvp(self): self.test_config.set('plugin', 'nvp') self._call_hook('cluster-relation-departed') diff --git a/unit_tests/test_quantum_utils.py b/unit_tests/test_quantum_utils.py index 8c319761..7fa472c6 100644 --- a/unit_tests/test_quantum_utils.py +++ b/unit_tests/test_quantum_utils.py @@ -36,7 +36,6 @@ TO_PATCH = [ 'service_running', 'NetworkServiceContext', 'ExternalPortContext', - 'DataPortContext', 'unit_private_ip', 'relations_of_type', 'service_stop', @@ -46,7 +45,8 @@ TO_PATCH = [ 'is_relation_made', 'lsb_release', 'mkdir', - 'copy2' + 'copy2', + 'NeutronAPIContext', ] @@ -139,7 +139,14 @@ class TestQuantumUtils(CharmTestCase): self.get_os_codename_install_source.return_value = 'kilo' self.assertTrue('python-neutron-fwaas' in quantum_utils.get_packages()) - def test_configure_ovs_starts_service_if_required(self): + def test_get_packages_l3ha(self): + self.config.return_value = 'ovs' + self.get_os_codename_install_source.return_value = 'juno' + self.assertTrue('keepalived' in quantum_utils.get_packages()) + + @patch('charmhelpers.contrib.openstack.context.config') + def test_configure_ovs_starts_service_if_required(self, mock_config): + mock_config.side_effect = self.test_config.get self.config.return_value = 'ovs' self.service_running.return_value = False quantum_utils.configure_ovs() @@ -150,14 +157,14 @@ class TestQuantumUtils(CharmTestCase): quantum_utils.configure_ovs() self.assertFalse(self.full_restart.called) - def test_configure_ovs_ovs_ext_port(self): + @patch('charmhelpers.contrib.openstack.context.config') + def test_configure_ovs_ovs_ext_port(self, mock_config): + mock_config.side_effect = self.test_config.get self.config.side_effect = self.test_config.get self.test_config.set('plugin', 'ovs') self.test_config.set('ext-port', 'eth0') self.ExternalPortContext.return_value = \ DummyExternalPortContext(return_value={'ext_port': 'eth0'}) - self.DataPortContext.return_value = \ - DummyExternalPortContext(return_value=None) quantum_utils.configure_ovs() self.add_bridge.assert_has_calls([ call('br-int'), @@ -166,22 +173,36 @@ class TestQuantumUtils(CharmTestCase): ]) self.add_bridge_port.assert_called_with('br-ex', 'eth0') - def test_configure_ovs_ovs_data_port(self): + @patch('charmhelpers.contrib.openstack.context.config') + def test_configure_ovs_ovs_data_port(self, mock_config): + mock_config.side_effect = self.test_config.get self.config.side_effect = self.test_config.get self.test_config.set('plugin', 'ovs') - self.test_config.set('data-port', 'eth0') self.ExternalPortContext.return_value = \ DummyExternalPortContext(return_value=None) - self.DataPortContext.return_value = \ - DummyExternalPortContext(return_value={'data_port': 'eth0'}) + # Test back-compatibility i.e. port but no bridge (so br-data is + # assumed) + self.test_config.set('data-port', 'eth0') quantum_utils.configure_ovs() self.add_bridge.assert_has_calls([ call('br-int'), call('br-ex'), call('br-data') ]) - self.add_bridge_port.assert_called_with('br-data', 'eth0', - promisc=True) + self.assertTrue(self.add_bridge_port.called) + + # Now test with bridge:port format + self.test_config.set('data-port', 'br-foo:eth0') + self.add_bridge.reset_mock() + self.add_bridge_port.reset_mock() + quantum_utils.configure_ovs() + self.add_bridge.assert_has_calls([ + call('br-int'), + call('br-ex'), + call('br-data') + ]) + # Not called since we have a bogus bridge in data-ports + self.assertFalse(self.add_bridge_port.called) def test_do_openstack_upgrade(self): self.config.side_effect = self.test_config.get @@ -241,6 +262,7 @@ class TestQuantumUtils(CharmTestCase): def test_restart_map_ovs(self): self.config.return_value = 'ovs' + self.get_os_codename_install_source.return_value = 'havana' ex_map = { quantum_utils.NEUTRON_CONF: ['neutron-l3-agent', 'neutron-dhcp-agent', @@ -261,12 +283,16 @@ class TestQuantumUtils(CharmTestCase): quantum_utils.NEUTRON_VPNAAS_AGENT_CONF: [ 'neutron-plugin-vpn-agent', 'neutron-vpn-agent'], - quantum_utils.NEUTRON_L3_AGENT_CONF: ['neutron-l3-agent'], + quantum_utils.NEUTRON_L3_AGENT_CONF: ['neutron-l3-agent', + 'neutron-vpn-agent'], quantum_utils.NEUTRON_DHCP_AGENT_CONF: ['neutron-dhcp-agent'], - quantum_utils.NEUTRON_FWAAS_CONF: ['neutron-l3-agent'], + quantum_utils.NEUTRON_FWAAS_CONF: ['neutron-l3-agent', + 'neutron-vpn-agent'], quantum_utils.NEUTRON_METERING_AGENT_CONF: ['neutron-metering-agent', 'neutron-plugin-metering-agent'], quantum_utils.NOVA_CONF: ['nova-api-metadata'], + quantum_utils.EXT_PORT_CONF: ['ext-port'], + quantum_utils.PHY_NIC_MTU_CONF: ['os-charm-phy-nic-mtu'], } self.assertDictEqual(quantum_utils.restart_map(), ex_map)