diff --git a/config.yaml b/config.yaml index 4ed6d7f1..91db1a8a 100644 --- a/config.yaml +++ b/config.yaml @@ -160,6 +160,13 @@ options: order for this charm to function correctly, the privacy extension must be disabled and a non-temporary address must be configured/available on your network interface. + network-device-mtu: + type: int + default: + description: | + The MTU size for the interfaces managed by neutron. If unset or set to + 0, no value will be applied. + # Storage configuration options libvirt-image-backend: default: type: string @@ -182,9 +189,18 @@ options: rbd pool has been created, changing this value will not have any effect (although it can be changed in ceph by manually configuring your ceph cluster). + # Other configuration options sysctl: type: string default: description: | YAML formatted associative array of sysctl values, e.g.: '{ kernel.pid_max : 4194303 }' + manage-neutron-plugin-legacy-mode: + type: boolean + default: True + description: | + If True nova-compute will install neutron packages for the plugin + stipulated by nova-cloud-controller. The option is only available for + backward compatibility for deployments which do not use the neutron-api + charm. Please do not enable this on new deployments. 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..960770f2 --- /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 + +__author__ = "Jorge Niedbalski " + +import atexit +import sys + +from charmhelpers.contrib.python.rpdb import Rpdb +from charmhelpers.core.hookenv import ( + open_port, + close_port, + ERROR, + log +) + +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..80bf66aa --- /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).""" + +__author__ = "Bertrand Janin " +__version__ = "0.1.3" + +import pdb +import socket +import sys + + +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..6dc11296 --- /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 . + +__author__ = "Jorge Niedbalski " + +import sys + + +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/nova_compute_context.py b/hooks/nova_compute_context.py index 0049437a..5281cddd 100644 --- a/hooks/nova_compute_context.py +++ b/hooks/nova_compute_context.py @@ -1,5 +1,5 @@ import uuid - +import platform from charmhelpers.contrib.openstack import context from charmhelpers.core.host import service_running, service_start from charmhelpers.fetch import apt_install, filter_installed_packages @@ -104,6 +104,9 @@ class NovaComputeLibvirtContext(context.OSContextGenerator): 'listen_tls': 0, } + # get the processor architecture to use in the nova.conf template + ctxt['arch'] = platform.machine() + # enable tcp listening if configured for live migration. if config('enable-live-migration'): ctxt['libvirtd_opts'] += ' -l' @@ -125,6 +128,16 @@ class NovaComputeLibvirtContext(context.OSContextGenerator): return ctxt +class NovaComputeLibvirtOverrideContext(context.OSContextGenerator): + """Provides overrides to the libvirt-bin service""" + interfaces = [] + + def __call__(self): + ctxt = {} + ctxt['overrides'] = "limit nofile 65535 65535" + return ctxt + + class NovaComputeVirtContext(context.OSContextGenerator): interfaces = [] @@ -341,6 +354,10 @@ class CloudComputeContext(context.OSContextGenerator): ctxt['network_manager'] = self.network_manager ctxt['network_manager_config'] = net_manager + net_dev_mtu = config('network-device-mtu') + if net_dev_mtu: + ctxt['network_device_mtu'] = net_dev_mtu + vol_service = self.volume_context() if vol_service: ctxt['volume_service'] = vol_service @@ -386,6 +403,19 @@ class InstanceConsoleContext(context.OSContextGenerator): return ctxt +class MetadataServiceContext(context.OSContextGenerator): + + def __call__(self): + ctxt = {} + for rid in relation_ids('neutron-plugin'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + if 'metadata-shared-secret' in rdata: + ctxt['metadata_shared_secret'] = \ + rdata['metadata-shared-secret'] + return ctxt + + class NeutronComputeContext(context.NeutronContext): interfaces = [] diff --git a/hooks/nova_compute_hooks.py b/hooks/nova_compute_hooks.py index f3364589..9d99acf2 100755 --- a/hooks/nova_compute_hooks.py +++ b/hooks/nova_compute_hooks.py @@ -17,10 +17,12 @@ from charmhelpers.core.hookenv import ( ) from charmhelpers.core.host import ( restart_on_change, + service_restart, ) from charmhelpers.fetch import ( apt_install, + apt_purge, apt_update, filter_installed_packages, ) @@ -46,7 +48,6 @@ from nova_compute_utils import ( initialize_ssh_keys, migration_enabled, network_manager, - neutron_plugin, do_openstack_upgrade, public_ssh_key, restart_map, @@ -59,6 +60,7 @@ from nova_compute_utils import ( fix_path_ownership, get_topics, assert_charm_supports_ipv6, + manage_ovs, ) from charmhelpers.contrib.network.ip import ( @@ -142,10 +144,10 @@ def amqp_changed(): return CONFIGS.write(NOVA_CONF) # No need to write NEUTRON_CONF if neutron-plugin is managing it - if not relation_ids('neutron-plugin'): - if network_manager() == 'quantum' and neutron_plugin() == 'ovs': + if manage_ovs(): + if network_manager() == 'quantum': CONFIGS.write(QUANTUM_CONF) - if network_manager() == 'neutron' and neutron_plugin() == 'ovs': + if network_manager() == 'neutron': CONFIGS.write(NEUTRON_CONF) @@ -239,6 +241,8 @@ def compute_changed(): @restart_on_change(restart_map()) def ceph_joined(): apt_install(filter_installed_packages(['ceph-common']), fatal=True) + # Bug 1427660 + service_restart('libvirt-bin') @hooks.hook('ceph-relation-changed') @@ -342,6 +346,18 @@ def update_nrpe_config(): nrpe_setup.write() +@hooks.hook('neutron-plugin-relation-changed') +@restart_on_change(restart_map()) +def neutron_plugin_changed(): + settings = relation_get() + if 'metadata-shared-secret' in settings: + apt_update() + apt_install('nova-api-metadata', fatal=True) + else: + apt_purge('nova-api-metadata', fatal=True) + CONFIGS.write(NOVA_CONF) + + def main(): try: hooks.execute(sys.argv) diff --git a/hooks/nova_compute_utils.py b/hooks/nova_compute_utils.py index 24f0873c..b3dbeb51 100644 --- a/hooks/nova_compute_utils.py +++ b/hooks/nova_compute_utils.py @@ -39,7 +39,9 @@ from charmhelpers.contrib.openstack.utils import ( from nova_compute_context import ( CloudComputeContext, + MetadataServiceContext, NovaComputeLibvirtContext, + NovaComputeLibvirtOverrideContext, NovaComputeCephContext, NeutronComputeContext, InstanceConsoleContext, @@ -62,6 +64,7 @@ NOVA_CONF_DIR = "/etc/nova" QEMU_CONF = '/etc/libvirt/qemu.conf' LIBVIRTD_CONF = '/etc/libvirt/libvirtd.conf' LIBVIRT_BIN = '/etc/default/libvirt-bin' +LIBVIRT_BIN_OVERRIDES = '/etc/init/libvirt-bin.override' NOVA_CONF = '%s/nova.conf' % NOVA_CONF_DIR BASE_RESOURCE_MAP = { @@ -77,6 +80,10 @@ BASE_RESOURCE_MAP = { 'services': ['libvirt-bin'], 'contexts': [NovaComputeLibvirtContext()], }, + LIBVIRT_BIN_OVERRIDES: { + 'services': ['libvirt-bin'], + 'contexts': [NovaComputeLibvirtOverrideContext()], + }, NOVA_CONF: { 'services': ['nova-compute'], 'contexts': [context.AMQPContext(ssl_dir=NOVA_CONF_DIR), @@ -96,6 +103,7 @@ BASE_RESOURCE_MAP = { InstanceConsoleContext(), context.ZeroMQContext(), context.NotificationDriverContext(), + MetadataServiceContext(), HostIPContext()], }, } @@ -118,7 +126,7 @@ QUANTUM_RESOURCES = { 'contexts': [NeutronComputeContext(), context.AMQPContext(ssl_dir=QUANTUM_CONF_DIR), context.SyslogContext()], - } + }, } NEUTRON_CONF_DIR = "/etc/neutron" @@ -130,7 +138,7 @@ NEUTRON_RESOURCES = { 'contexts': [NeutronComputeContext(), context.AMQPContext(ssl_dir=NEUTRON_CONF_DIR), context.SyslogContext()], - } + }, } @@ -175,7 +183,9 @@ def resource_map(): # depending on the plugin used. # NOTE(james-page): only required for ovs plugin right now if net_manager in ['neutron', 'quantum']: - if not relation_ids('neutron-plugin') and plugin == 'ovs': + # This stanza supports the legacy case of ovs supported within + # compute charm code (now moved to neutron-openvswitch subordinate) + if manage_ovs(): if net_manager == 'quantum': nm_rsc = QUANTUM_RESOURCES if net_manager == 'neutron': @@ -203,6 +213,8 @@ def resource_map(): } resource_map.update(CEPH_RESOURCES) + if enable_nova_metadata(): + resource_map[NOVA_CONF]['services'].append('nova-api-metadata') return resource_map @@ -256,7 +268,8 @@ def determine_packages(): if (net_manager in ['flatmanager', 'flatdhcpmanager'] and config('multi-host').lower() == 'yes'): packages.extend(['nova-api', 'nova-network']) - elif net_manager in ['quantum', 'neutron']: + elif (net_manager in ['quantum', 'neutron'] + and neutron_plugin_legacy_mode()): plugin = neutron_plugin() pkg_lists = neutron_plugin_attribute(plugin, 'packages', net_manager) for pkg_list in pkg_lists: @@ -271,6 +284,8 @@ def determine_packages(): except KeyError: log('Unsupported virt-type configured: %s' % virt_type) raise + if enable_nova_metadata(): + packages.append('nova-api-metadata') return packages @@ -492,3 +507,21 @@ def assert_charm_supports_ipv6(): if lsb_release()['DISTRIB_CODENAME'].lower() < "trusty": raise Exception("IPv6 is not supported in the charms for Ubuntu " "versions less than Trusty 14.04") + + +def enable_nova_metadata(): + ctxt = MetadataServiceContext()() + return 'metadata_shared_secret' in ctxt + + +def neutron_plugin_legacy_mode(): + # If a charm is attatched to the neutron-plugin relation then its managing + # neutron + if relation_ids('neutron-plugin'): + return False + else: + return config('manage-neutron-plugin-legacy-mode') + + +def manage_ovs(): + return neutron_plugin_legacy_mode() and neutron_plugin() == 'ovs' diff --git a/templates/havana/nova.conf b/templates/havana/nova.conf index 20aa1ec0..e88b92c5 100644 --- a/templates/havana/nova.conf +++ b/templates/havana/nova.conf @@ -13,7 +13,14 @@ logdir=/var/log/nova state_path=/var/lib/nova lock_path=/var/lock/nova force_dhcp_release=True + +{% if arch == 'aarch64' -%} +libvirt_use_virtio_for_bridges=False +libvirt_disk_prefix=vd +{% else -%} libvirt_use_virtio_for_bridges=True +{% endif -%} + verbose=True use_syslog = {{ use_syslog }} ec2_private_dns_show_ip=True @@ -64,6 +71,11 @@ security_group_api = neutron firewall_driver = nova.virt.firewall.NoopFirewallDriver {% endif -%} +{% if neutron_plugin and neutron_plugin == 'Calico' -%} +security_group_api = neutron +firewall_driver = nova.virt.firewall.NoopFirewallDriver +{% endif -%} + {% if network_manager_config -%} {% for key, value in network_manager_config.iteritems() -%} {{ key }} = {{ value }} @@ -76,6 +88,10 @@ network_api_class = nova.network.neutronv2.api.API network_manager = nova.network.manager.FlatDHCPManager {% endif -%} +{% if network_device_mtu -%} +network_device_mtu = {{ network_device_mtu }} +{% endif -%} + {% if volume_service -%} volume_api_class = nova.volume.cinder.API {% endif -%} diff --git a/templates/juno/nova.conf b/templates/juno/nova.conf index 1b7cbcf9..65fb19f9 100644 --- a/templates/juno/nova.conf +++ b/templates/juno/nova.conf @@ -13,7 +13,14 @@ logdir=/var/log/nova state_path=/var/lib/nova lock_path=/var/lock/nova force_dhcp_release=True + +{% if arch == 'aarch64' -%} +libvirt_use_virtio_for_bridges=False +libvirt_disk_prefix=vd +{% else -%} libvirt_use_virtio_for_bridges=True +{% endif -%} + verbose=True use_syslog = {{ use_syslog }} ec2_private_dns_show_ip=True @@ -31,6 +38,11 @@ my_ip = {{ host_ip }} glance_api_servers = {{ glance_api_servers }} {% endif -%} +{% if metadata_shared_secret -%} +neutron_metadata_proxy_shared_secret = {{ metadata_shared_secret }} +service_neutron_metadata_proxy=True +{% endif -%} + {% if console_vnc_type -%} vnc_enabled = True novnc_enabled = True @@ -64,6 +76,11 @@ security_group_api = neutron firewall_driver = nova.virt.firewall.NoopFirewallDriver {% endif -%} +{% if neutron_plugin and neutron_plugin == 'Calico' -%} +security_group_api = neutron +firewall_driver = nova.virt.firewall.NoopFirewallDriver +{% endif -%} + {% if network_manager_config -%} {% for key, value in network_manager_config.iteritems() -%} {{ key }} = {{ value }} @@ -96,8 +113,6 @@ instances_path = {{ instances_path }} {% endfor -%} {% endif -%} -{% include "zeromq" %} - {% if console_access_protocol == 'spice' -%} [spice] agent_enabled = True diff --git a/templates/kilo/nova.conf b/templates/kilo/nova.conf index 61245ed1..385106cc 100644 --- a/templates/kilo/nova.conf +++ b/templates/kilo/nova.conf @@ -11,7 +11,6 @@ dhcpbridge_flagfile=/etc/nova/nova.conf dhcpbridge=/usr/bin/nova-dhcpbridge logdir=/var/log/nova state_path=/var/lib/nova -lock_path=/var/lock/nova force_dhcp_release=True verbose=True use_syslog = {{ use_syslog }} @@ -22,9 +21,15 @@ auth_strategy=keystone compute_driver=libvirt.LibvirtDriver my_ip = {{ host_ip }} -{% include "parts/database" %} +{% if arch == 'aarch64' -%} +libvirt_use_virtio_for_bridges=False +libvirt_disk_prefix=vd +{% endif -%} -{% include "parts/rabbitmq" %} +{% if metadata_shared_secret -%} +neutron_metadata_proxy_shared_secret = {{ metadata_shared_secret }} +service_neutron_metadata_proxy=True +{% endif -%} {% if console_vnc_type -%} vnc_enabled = True @@ -59,6 +64,11 @@ security_group_api = neutron firewall_driver = nova.virt.firewall.NoopFirewallDriver {% endif -%} +{% if neutron_plugin and neutron_plugin == 'Calico' -%} +security_group_api = neutron +firewall_driver = nova.virt.firewall.NoopFirewallDriver +{% endif -%} + {% if network_manager != 'neutron' and network_manager_config -%} {% for key, value in network_manager_config.iteritems() -%} {{ key }} = {{ value }} @@ -91,7 +101,7 @@ instances_path = {{ instances_path }} {% endfor -%} {% endif -%} -{% include "zeromq" %} +{% include "section-zeromq" %} {% if network_manager == 'neutron' and network_manager_config -%} [neutron] @@ -140,3 +150,10 @@ live_migration_uri = {{ live_migration_uri }} {% if disk_cachemodes -%} disk_cachemodes = {{ disk_cachemodes }} {% endif -%} + +{% include "parts/section-database" %} + +{% include "section-rabbitmq-oslo" %} + +[oslo_concurrency] +lock_path=/var/lock/nova 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/libvirt-bin.override b/templates/libvirt-bin.override new file mode 100644 index 00000000..d29009c5 --- /dev/null +++ b/templates/libvirt-bin.override @@ -0,0 +1,3 @@ +{% if overrides -%} +{{ overrides }} +{% endif -%} diff --git a/templates/parts/section-database b/templates/parts/section-database new file mode 100644 index 00000000..ae4a5ba0 --- /dev/null +++ b/templates/parts/section-database @@ -0,0 +1,4 @@ +{% if database_host -%} +[database] +connection = {{ database_type }}://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}{% if database_ssl_ca %}?ssl_ca={{ database_ssl_ca }}{% if database_ssl_cert %}&ssl_cert={{ database_ssl_cert }}&ssl_key={{ database_ssl_key }}{% endif %}{% endif %} +{% endif -%} diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py index 776da875..aeb2fafe 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -409,7 +409,7 @@ class NovaBasicDeployment(OpenStackAmuletDeployment): found = True if instance.status != 'ACTIVE': msg = "cirros instance is not active" - amulet.raise_status(amulet.FAIL, msg=message) + amulet.raise_status(amulet.FAIL, msg=msg) if not found: message = "nova cirros instance does not exist" 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_nova_compute_contexts.py b/unit_tests/test_nova_compute_contexts.py index 956d28b2..e5987de3 100644 --- a/unit_tests/test_nova_compute_contexts.py +++ b/unit_tests/test_nova_compute_contexts.py @@ -1,3 +1,5 @@ +import platform + from mock import patch from test_utils import CharmTestCase @@ -179,6 +181,7 @@ class NovaComputeContextTests(CharmTestCase): self.assertEqual( {'libvirtd_opts': '-d', + 'arch': platform.machine(), 'listen_tls': 0, 'host_uuid': 'e46e530d-18ae-4a67-9ff0-e6e2ba7c60a7'}, libvirt()) @@ -190,6 +193,7 @@ class NovaComputeContextTests(CharmTestCase): self.assertEqual( {'libvirtd_opts': '-d -l', + 'arch': platform.machine(), 'listen_tls': 0, 'host_uuid': 'e46e530d-18ae-4a67-9ff0-e6e2ba7c60a7'}, libvirt()) @@ -201,9 +205,10 @@ class NovaComputeContextTests(CharmTestCase): self.assertEqual( {'libvirtd_opts': '-d', + 'disk_cachemodes': 'file=unsafe,block=none', + 'arch': platform.machine(), 'listen_tls': 0, - 'host_uuid': 'e46e530d-18ae-4a67-9ff0-e6e2ba7c60a7', - 'disk_cachemodes': 'file=unsafe,block=none'}, libvirt()) + 'host_uuid': 'e46e530d-18ae-4a67-9ff0-e6e2ba7c60a7'}, libvirt()) @patch.object(context.NeutronComputeContext, 'network_manager') @patch.object(context.NeutronComputeContext, 'plugin') @@ -228,3 +233,11 @@ class NovaComputeContextTests(CharmTestCase): self.assertEquals( {'host_ip': '172.24.0.79'}, host_ip()) self.unit_get.assert_called_with('private-address') + + def test_metadata_service_ctxt(self): + self.relation_ids.return_value = 'neutron-plugin:0' + self.related_units.return_value = 'neutron-openvswitch/0' + self.test_relation.set({'metadata-shared-secret': 'shared_secret'}) + metadatactxt = context.MetadataServiceContext() + self.assertEqual(metadatactxt(), {'metadata_shared_secret': + 'shared_secret'}) diff --git a/unit_tests/test_nova_compute_hooks.py b/unit_tests/test_nova_compute_hooks.py index 7fac3a1b..2d171789 100644 --- a/unit_tests/test_nova_compute_hooks.py +++ b/unit_tests/test_nova_compute_hooks.py @@ -27,6 +27,7 @@ TO_PATCH = [ 'apt_update', 'filter_installed_packages', 'restart_on_change', + 'service_restart', # charmhelpers.contrib.openstack.utils 'configure_installation_source', 'openstack_upgrade_available', @@ -41,7 +42,7 @@ TO_PATCH = [ 'migration_enabled', 'do_openstack_upgrade', 'network_manager', - 'neutron_plugin', + 'manage_ovs', 'public_ssh_key', 'register_configs', 'disable_shell', @@ -169,7 +170,6 @@ class NovaComputeRelationsTests(CharmTestCase): configs.write = MagicMock() if quantum: self.network_manager.return_value = 'quantum' - self.neutron_plugin.return_value = 'ovs' hooks.amqp_changed() @patch.object(hooks, 'CONFIGS') @@ -188,6 +188,7 @@ class NovaComputeRelationsTests(CharmTestCase): @patch.object(hooks, 'CONFIGS') def test_amqp_changed_with_data_and_quantum_api(self, configs): + self.manage_ovs.return_value = False self.relation_ids.return_value = ['neutron-plugin:0'] self._amqp_test(configs, quantum=True) self.assertEquals([call('/etc/nova/nova.conf')], @@ -348,6 +349,7 @@ class NovaComputeRelationsTests(CharmTestCase): def test_ceph_joined(self): hooks.ceph_joined() self.apt_install.assert_called_with(['ceph-common'], fatal=True) + self.service_restart.assert_called_with('libvirt-bin') @patch.object(hooks, 'CONFIGS') def test_ceph_changed_missing_relation_data(self, configs): @@ -384,3 +386,12 @@ class NovaComputeRelationsTests(CharmTestCase): call('/etc/nova/nova.conf'), ] self.assertEquals(ex, configs.write.call_args_list) + + @patch.object(hooks, 'CONFIGS') + def test_neutron_plugin_changed(self, configs): + self.relation_get.return_value = {'metadata-shared-secret': + 'sharedsecret'} + hooks.neutron_plugin_changed() + self.assertTrue(self.apt_update.called) + self.apt_install.assert_called_with('nova-api-metadata', fatal=True) + configs.write.assert_called_with('/etc/nova/nova.conf') diff --git a/unit_tests/test_nova_compute_utils.py b/unit_tests/test_nova_compute_utils.py index bdc26b76..6e9c180b 100644 --- a/unit_tests/test_nova_compute_utils.py +++ b/unit_tests/test_nova_compute_utils.py @@ -24,7 +24,8 @@ TO_PATCH = [ 'relation_ids', 'relation_get', 'mkdir', - 'install_alternative' + 'install_alternative', + 'MetadataServiceContext', ] OVS_PKGS = [ @@ -41,8 +42,10 @@ class NovaComputeUtilsTests(CharmTestCase): super(NovaComputeUtilsTests, self).setUp(utils, TO_PATCH) self.config.side_effect = self.test_config.get + @patch.object(utils, 'enable_nova_metadata') @patch.object(utils, 'network_manager') - def test_determine_packages_nova_network(self, net_man): + def test_determine_packages_nova_network(self, net_man, en_meta): + en_meta.return_value = False net_man.return_value = 'flatdhcpmanager' self.relation_ids.return_value = [] result = utils.determine_packages() @@ -53,9 +56,11 @@ class NovaComputeUtilsTests(CharmTestCase): ] self.assertEquals(ex, result) + @patch.object(utils, 'enable_nova_metadata') @patch.object(utils, 'neutron_plugin') @patch.object(utils, 'network_manager') - def test_determine_packages_quantum(self, net_man, n_plugin): + def test_determine_packages_quantum(self, net_man, n_plugin, en_meta): + en_meta.return_value = False self.neutron_plugin_attribute.return_value = OVS_PKGS net_man.return_value = 'quantum' n_plugin.return_value = 'ovs' @@ -64,9 +69,30 @@ class NovaComputeUtilsTests(CharmTestCase): ex = utils.BASE_PACKAGES + OVS_PKGS_FLAT + ['nova-compute-kvm'] self.assertEquals(ex, result) + @patch.object(utils, 'neutron_plugin_legacy_mode') + @patch.object(utils, 'enable_nova_metadata') @patch.object(utils, 'neutron_plugin') @patch.object(utils, 'network_manager') - def test_determine_packages_quantum_ceph(self, net_man, n_plugin): + def test_determine_packages_quantum_legacy_off(self, net_man, n_plugin, + en_meta, leg_mode): + en_meta.return_value = False + leg_mode.return_value = False + self.neutron_plugin_attribute.return_value = OVS_PKGS + net_man.return_value = 'quantum' + n_plugin.return_value = 'ovs' + self.relation_ids.return_value = [] + result = utils.determine_packages() + ex = utils.BASE_PACKAGES + ['nova-compute-kvm'] + self.assertEquals(ex, result) + + @patch.object(utils, 'neutron_plugin_legacy_mode') + @patch.object(utils, 'enable_nova_metadata') + @patch.object(utils, 'neutron_plugin') + @patch.object(utils, 'network_manager') + def test_determine_packages_quantum_ceph(self, net_man, n_plugin, en_meta, + leg_mode): + en_meta.return_value = False + leg_mode.return_value = True self.neutron_plugin_attribute.return_value = OVS_PKGS net_man.return_value = 'quantum' n_plugin.return_value = 'ovs' @@ -76,6 +102,18 @@ class NovaComputeUtilsTests(CharmTestCase): ['ceph-common', 'nova-compute-kvm']) self.assertEquals(ex, result) + @patch.object(utils, 'enable_nova_metadata') + @patch.object(utils, 'neutron_plugin') + @patch.object(utils, 'network_manager') + def test_determine_packages_metadata(self, net_man, n_plugin, en_meta): + en_meta.return_value = True + self.neutron_plugin_attribute.return_value = OVS_PKGS + net_man.return_value = 'bob' + n_plugin.return_value = 'ovs' + self.relation_ids.return_value = [] + result = utils.determine_packages() + self.assertTrue('nova-api-metadata' in result) + @patch.object(utils, 'network_manager') def test_resource_map_nova_network_no_multihost(self, net_man): self.skipTest('skipped until contexts are properly mocked') @@ -172,6 +210,17 @@ class NovaComputeUtilsTests(CharmTestCase): result = utils.resource_map() self.assertTrue('/etc/neutron/neutron.conf' not in result) + @patch.object(utils, 'enable_nova_metadata') + @patch.object(utils, 'neutron_plugin') + @patch.object(utils, 'network_manager') + def test_resource_map_metadata(self, net_man, _plugin, _metadata): + _metadata.return_value = True + net_man.return_value = 'bob' + _plugin.return_value = 'ovs' + self.relation_ids.return_value = [] + result = utils.resource_map()['/etc/nova/nova.conf']['services'] + self.assertTrue('nova-api-metadata' in result) + def fake_user(self, username='foo'): user = MagicMock() user.pw_dir = '/home/' + username @@ -375,3 +424,54 @@ class NovaComputeUtilsTests(CharmTestCase): 'secret-set-value', '--secret', compute_context.CEPH_SECRET_UUID, '--base64', key]) + + def test_enable_nova_metadata(self): + class DummyContext(): + + def __init__(self, return_value): + self.return_value = return_value + + def __call__(self): + return self.return_value + + self.MetadataServiceContext.return_value = \ + DummyContext(return_value={'metadata_shared_secret': + 'sharedsecret'}) + self.assertEqual(utils.enable_nova_metadata(), True) + + def test_neutron_plugin_legacy_mode_plugin(self): + self.relation_ids.return_value = ['neutron-plugin:0'] + self.assertFalse(utils.neutron_plugin_legacy_mode()) + + def test_neutron_plugin_legacy_mode_legacy_off(self): + self.relation_ids.return_value = [] + self.test_config.set('manage-neutron-plugin-legacy-mode', False) + self.assertFalse(utils.neutron_plugin_legacy_mode()) + + def test_neutron_plugin_legacy_mode_legacy_on(self): + self.relation_ids.return_value = [] + self.test_config.set('manage-neutron-plugin-legacy-mode', True) + self.assertTrue(utils.neutron_plugin_legacy_mode()) + + @patch.object(utils, 'neutron_plugin_legacy_mode') + def test_manage_ovs_legacy_mode_legacy_off(self, + _neutron_plugin_legacy_mode): + _neutron_plugin_legacy_mode.return_value = False + self.assertFalse(utils.manage_ovs()) + + @patch.object(utils, 'neutron_plugin') + @patch.object(utils, 'neutron_plugin_legacy_mode') + def test_manage_ovs_legacy_mode_legacy_on(self, + _neutron_plugin_legacy_mode, + _neutron_plugin): + _neutron_plugin_legacy_mode.return_value = True + _neutron_plugin.return_value = 'ovs' + self.assertTrue(utils.manage_ovs()) + + @patch.object(utils, 'neutron_plugin') + @patch.object(utils, 'neutron_plugin_legacy_mode') + def test_manage_ovs_legacy_mode_not_ovs(self, _neutron_plugin_legacy_mode, + _neutron_plugin): + _neutron_plugin_legacy_mode.return_value = True + _neutron_plugin.return_value = 'bobvs' + self.assertFalse(utils.manage_ovs())