Merge from lp:~openstack-charmers/.../next

This commit is contained in:
Subbarayudu Mukkamala 2015-08-20 06:45:36 -07:00
commit 37802d3866
38 changed files with 1281 additions and 165 deletions

View File

@ -2,6 +2,7 @@ branch: lp:charm-helpers
destination: hooks/charmhelpers destination: hooks/charmhelpers
include: include:
- core - core
- cli
- fetch - fetch
- contrib.openstack|inc=* - contrib.openstack|inc=*
- contrib.hahelpers - contrib.hahelpers

View File

@ -411,3 +411,9 @@ options:
description: | description: |
A comma-separated list of nagios servicegroups. A comma-separated list of nagios servicegroups.
If left empty, the nagios_context will be used as the servicegroup If left empty, the nagios_context will be used as the servicegroup
manage-neutron-plugin-legacy-mode:
type: boolean
default: True
description: |
If True neutron-server will install neutron packages for the plugin
configured.

View File

@ -0,0 +1,191 @@
# 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 <http://www.gnu.org/licenses/>.
import inspect
import argparse
import sys
from six.moves import zip
from charmhelpers.core import unitdata
class OutputFormatter(object):
def __init__(self, outfile=sys.stdout):
self.formats = (
"raw",
"json",
"py",
"yaml",
"csv",
"tab",
)
self.outfile = outfile
def add_arguments(self, argument_parser):
formatgroup = argument_parser.add_mutually_exclusive_group()
choices = self.supported_formats
formatgroup.add_argument("--format", metavar='FMT',
help="Select output format for returned data, "
"where FMT is one of: {}".format(choices),
choices=choices, default='raw')
for fmt in self.formats:
fmtfunc = getattr(self, fmt)
formatgroup.add_argument("-{}".format(fmt[0]),
"--{}".format(fmt), action='store_const',
const=fmt, dest='format',
help=fmtfunc.__doc__)
@property
def supported_formats(self):
return self.formats
def raw(self, output):
"""Output data as raw string (default)"""
if isinstance(output, (list, tuple)):
output = '\n'.join(map(str, output))
self.outfile.write(str(output))
def py(self, output):
"""Output data as a nicely-formatted python data structure"""
import pprint
pprint.pprint(output, stream=self.outfile)
def json(self, output):
"""Output data in JSON format"""
import json
json.dump(output, self.outfile)
def yaml(self, output):
"""Output data in YAML format"""
import yaml
yaml.safe_dump(output, self.outfile)
def csv(self, output):
"""Output data as excel-compatible CSV"""
import csv
csvwriter = csv.writer(self.outfile)
csvwriter.writerows(output)
def tab(self, output):
"""Output data in excel-compatible tab-delimited format"""
import csv
csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
csvwriter.writerows(output)
def format_output(self, output, fmt='raw'):
fmtfunc = getattr(self, fmt)
fmtfunc(output)
class CommandLine(object):
argument_parser = None
subparsers = None
formatter = None
exit_code = 0
def __init__(self):
if not self.argument_parser:
self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
if not self.formatter:
self.formatter = OutputFormatter()
self.formatter.add_arguments(self.argument_parser)
if not self.subparsers:
self.subparsers = self.argument_parser.add_subparsers(help='Commands')
def subcommand(self, command_name=None):
"""
Decorate a function as a subcommand. Use its arguments as the
command-line arguments"""
def wrapper(decorated):
cmd_name = command_name or decorated.__name__
subparser = self.subparsers.add_parser(cmd_name,
description=decorated.__doc__)
for args, kwargs in describe_arguments(decorated):
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=decorated)
return decorated
return wrapper
def test_command(self, decorated):
"""
Subcommand is a boolean test function, so bool return values should be
converted to a 0/1 exit code.
"""
decorated._cli_test_command = True
return decorated
def no_output(self, decorated):
"""
Subcommand is not expected to return a value, so don't print a spurious None.
"""
decorated._cli_no_output = True
return decorated
def subcommand_builder(self, command_name, description=None):
"""
Decorate a function that builds a subcommand. Builders should accept a
single argument (the subparser instance) and return the function to be
run as the command."""
def wrapper(decorated):
subparser = self.subparsers.add_parser(command_name)
func = decorated(subparser)
subparser.set_defaults(func=func)
subparser.description = description or func.__doc__
return wrapper
def run(self):
"Run cli, processing arguments and executing subcommands."
arguments = self.argument_parser.parse_args()
argspec = inspect.getargspec(arguments.func)
vargs = []
for arg in argspec.args:
vargs.append(getattr(arguments, arg))
if argspec.varargs:
vargs.extend(getattr(arguments, argspec.varargs))
output = arguments.func(*vargs)
if getattr(arguments.func, '_cli_test_command', False):
self.exit_code = 0 if output else 1
output = ''
if getattr(arguments.func, '_cli_no_output', False):
output = ''
self.formatter.format_output(output, arguments.format)
if unitdata._KV:
unitdata._KV.flush()
cmdline = CommandLine()
def describe_arguments(func):
"""
Analyze a function's signature and return a data structure suitable for
passing in as arguments to an argparse parser's add_argument() method."""
argspec = inspect.getargspec(func)
# we should probably raise an exception somewhere if func includes **kwargs
if argspec.defaults:
positional_args = argspec.args[:-len(argspec.defaults)]
keyword_names = argspec.args[-len(argspec.defaults):]
for arg, default in zip(keyword_names, argspec.defaults):
yield ('--{}'.format(arg),), {'default': default}
else:
positional_args = argspec.args
for arg in positional_args:
yield (arg,), {}
if argspec.varargs:
yield (argspec.varargs,), {'nargs': '*'}

View File

@ -0,0 +1,36 @@
# 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 <http://www.gnu.org/licenses/>.
from . import cmdline
from charmhelpers.contrib.benchmark import Benchmark
@cmdline.subcommand(command_name='benchmark-start')
def start():
Benchmark.start()
@cmdline.subcommand(command_name='benchmark-finish')
def finish():
Benchmark.finish()
@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
def service(subparser):
subparser.add_argument("value", help="The composite score.")
subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
return Benchmark.set_composite_score

View File

@ -0,0 +1,32 @@
# 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 <http://www.gnu.org/licenses/>.
"""
This module loads sub-modules into the python runtime so they can be
discovered via the inspect module. In order to prevent flake8 from (rightfully)
telling us these are unused modules, throw a ' # noqa' at the end of each import
so that the warning is suppressed.
"""
from . import CommandLine # noqa
"""
Import the sub-modules which have decorated subcommands to register with chlp.
"""
from . import host # noqa
from . import benchmark # noqa
from . import unitdata # noqa
from . import hookenv # noqa

View File

@ -0,0 +1,23 @@
# 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 <http://www.gnu.org/licenses/>.
from . import cmdline
from charmhelpers.core import hookenv
cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
cmdline.subcommand('service-name')(hookenv.service_name)
cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)

View File

@ -0,0 +1,31 @@
# 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 <http://www.gnu.org/licenses/>.
from . import cmdline
from charmhelpers.core import host
@cmdline.subcommand()
def mounts():
"List mounts"
return host.mounts()
@cmdline.subcommand_builder('service', description="Control system services")
def service(subparser):
subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
subparser.add_argument("service_name", help="Name of the service to control")
return host.service

View File

@ -0,0 +1,39 @@
# 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 <http://www.gnu.org/licenses/>.
from . import cmdline
from charmhelpers.core import unitdata
@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
def unitdata_cmd(subparser):
nested = subparser.add_subparsers()
get_cmd = nested.add_parser('get', help='Retrieve data')
get_cmd.add_argument('key', help='Key to retrieve the value of')
get_cmd.set_defaults(action='get', value=None)
set_cmd = nested.add_parser('set', help='Store data')
set_cmd.add_argument('key', help='Key to set')
set_cmd.add_argument('value', help='Value to store')
set_cmd.set_defaults(action='set')
def _unitdata_cmd(action, key, value):
if action == 'get':
return unitdata.kv().get(key)
elif action == 'set':
unitdata.kv().set(key, value)
unitdata.kv().flush()
return ''
return _unitdata_cmd

View File

@ -44,7 +44,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
Determine if the local branch being tested is derived from its Determine if the local branch being tested is derived from its
stable or next (dev) branch, and based on this, use the corresonding stable or next (dev) branch, and based on this, use the corresonding
stable or next branches for the other_services.""" stable or next branches for the other_services."""
base_charms = ['mysql', 'mongodb'] base_charms = ['mysql', 'mongodb', 'nrpe']
if self.series in ['precise', 'trusty']: if self.series in ['precise', 'trusty']:
base_series = self.series base_series = self.series
@ -81,7 +81,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
'ceph-osd', 'ceph-radosgw'] 'ceph-osd', 'ceph-radosgw']
# Most OpenStack subordinate charms do not expose an origin option # Most OpenStack subordinate charms do not expose an origin option
# as that is controlled by the principle. # as that is controlled by the principle.
ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch'] ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
if self.openstack: if self.openstack:
for svc in services: for svc in services:

View File

@ -50,6 +50,8 @@ from charmhelpers.core.sysctl import create as sysctl_create
from charmhelpers.core.strutils import bool_from_string from charmhelpers.core.strutils import bool_from_string
from charmhelpers.core.host import ( from charmhelpers.core.host import (
get_bond_master,
is_phy_iface,
list_nics, list_nics,
get_nic_hwaddr, get_nic_hwaddr,
mkdir, mkdir,
@ -923,7 +925,6 @@ class NeutronContext(OSContextGenerator):
class NeutronPortContext(OSContextGenerator): class NeutronPortContext(OSContextGenerator):
NIC_PREFIXES = ['eth', 'bond']
def resolve_ports(self, ports): def resolve_ports(self, ports):
"""Resolve NICs not yet bound to bridge(s) """Resolve NICs not yet bound to bridge(s)
@ -935,7 +936,18 @@ class NeutronPortContext(OSContextGenerator):
hwaddr_to_nic = {} hwaddr_to_nic = {}
hwaddr_to_ip = {} hwaddr_to_ip = {}
for nic in list_nics(self.NIC_PREFIXES): for nic in list_nics():
# Ignore virtual interfaces (bond masters will be identified from
# their slaves)
if not is_phy_iface(nic):
continue
_nic = get_bond_master(nic)
if _nic:
log("Replacing iface '%s' with bond master '%s'" % (nic, _nic),
level=DEBUG)
nic = _nic
hwaddr = get_nic_hwaddr(nic) hwaddr = get_nic_hwaddr(nic)
hwaddr_to_nic[hwaddr] = nic hwaddr_to_nic[hwaddr] = nic
addresses = get_ipv4_addr(nic, fatal=False) addresses = get_ipv4_addr(nic, fatal=False)
@ -961,7 +973,8 @@ class NeutronPortContext(OSContextGenerator):
# trust it to be the real external network). # trust it to be the real external network).
resolved.append(entry) resolved.append(entry)
return resolved # Ensure no duplicates
return list(set(resolved))
class OSConfigFlagContext(OSContextGenerator): class OSConfigFlagContext(OSContextGenerator):
@ -1051,13 +1064,22 @@ class SubordinateConfigContext(OSContextGenerator):
:param config_file : Service's config file to query sections :param config_file : Service's config file to query sections
:param interface : Subordinate interface to inspect :param interface : Subordinate interface to inspect
""" """
self.service = service
self.config_file = config_file self.config_file = config_file
self.interface = interface if isinstance(service, list):
self.services = service
else:
self.services = [service]
if isinstance(interface, list):
self.interfaces = interface
else:
self.interfaces = [interface]
def __call__(self): def __call__(self):
ctxt = {'sections': {}} ctxt = {'sections': {}}
for rid in relation_ids(self.interface): rids = []
for interface in self.interfaces:
rids.extend(relation_ids(interface))
for rid in rids:
for unit in related_units(rid): for unit in related_units(rid):
sub_config = relation_get('subordinate_configuration', sub_config = relation_get('subordinate_configuration',
rid=rid, unit=unit) rid=rid, unit=unit)
@ -1069,13 +1091,14 @@ class SubordinateConfigContext(OSContextGenerator):
'setting from %s' % rid, level=ERROR) 'setting from %s' % rid, level=ERROR)
continue continue
if self.service not in sub_config: for service in self.services:
if service not in sub_config:
log('Found subordinate_config on %s but it contained' log('Found subordinate_config on %s but it contained'
'nothing for %s service' % (rid, self.service), 'nothing for %s service' % (rid, service),
level=INFO) level=INFO)
continue continue
sub_config = sub_config[self.service] sub_config = sub_config[service]
if self.config_file not in sub_config: if self.config_file not in sub_config:
log('Found subordinate_config on %s but it contained' log('Found subordinate_config on %s but it contained'
'nothing for %s' % (rid, self.config_file), 'nothing for %s' % (rid, self.config_file),
@ -1085,13 +1108,15 @@ class SubordinateConfigContext(OSContextGenerator):
sub_config = sub_config[self.config_file] sub_config = sub_config[self.config_file]
for k, v in six.iteritems(sub_config): for k, v in six.iteritems(sub_config):
if k == 'sections': if k == 'sections':
for section, config_dict in six.iteritems(v): for section, config_list in six.iteritems(v):
log("adding section '%s'" % (section), log("adding section '%s'" % (section),
level=DEBUG) level=DEBUG)
ctxt[k][section] = config_dict if ctxt[k].get(section):
ctxt[k][section].extend(config_list)
else:
ctxt[k][section] = config_list
else: else:
ctxt[k] = v ctxt[k] = v
log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG) log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
return ctxt return ctxt
@ -1268,15 +1293,19 @@ class DataPortContext(NeutronPortContext):
def __call__(self): def __call__(self):
ports = config('data-port') ports = config('data-port')
if ports: if ports:
# Map of {port/mac:bridge}
portmap = parse_data_port_mappings(ports) portmap = parse_data_port_mappings(ports)
ports = portmap.values() ports = portmap.keys()
# Resolve provided ports or mac addresses and filter out those
# already attached to a bridge.
resolved = self.resolve_ports(ports) resolved = self.resolve_ports(ports)
# FIXME: is this necessary?
normalized = {get_nic_hwaddr(port): port for port in resolved normalized = {get_nic_hwaddr(port): port for port in resolved
if port not in ports} if port not in ports}
normalized.update({port: port for port in resolved normalized.update({port: port for port in resolved
if port in ports}) if port in ports})
if resolved: if resolved:
return {bridge: normalized[port] for bridge, port in return {bridge: normalized[port] for port, bridge in
six.iteritems(portmap) if port in normalized.keys()} six.iteritems(portmap) if port in normalized.keys()}
return None return None

View File

@ -255,17 +255,30 @@ def network_manager():
return 'neutron' return 'neutron'
def parse_mappings(mappings): def parse_mappings(mappings, key_rvalue=False):
"""By default mappings are lvalue keyed.
If key_rvalue is True, the mapping will be reversed to allow multiple
configs for the same lvalue.
"""
parsed = {} parsed = {}
if mappings: if mappings:
mappings = mappings.split() mappings = mappings.split()
for m in mappings: for m in mappings:
p = m.partition(':') p = m.partition(':')
key = p[0].strip()
if p[1]: if key_rvalue:
parsed[key] = p[2].strip() key_index = 2
val_index = 0
# if there is no rvalue skip to next
if not p[1]:
continue
else: else:
parsed[key] = '' key_index = 0
val_index = 2
key = p[key_index].strip()
parsed[key] = p[val_index].strip()
return parsed return parsed
@ -283,25 +296,25 @@ def parse_bridge_mappings(mappings):
def parse_data_port_mappings(mappings, default_bridge='br-data'): def parse_data_port_mappings(mappings, default_bridge='br-data'):
"""Parse data port mappings. """Parse data port mappings.
Mappings must be a space-delimited list of bridge:port mappings. Mappings must be a space-delimited list of port:bridge mappings.
Returns dict of the form {bridge:port}. Returns dict of the form {port:bridge} where port may be an mac address or
interface name.
""" """
_mappings = parse_mappings(mappings)
# NOTE(dosaboy): we use rvalue for key to allow multiple values to be
# proposed for <port> since it may be a mac address which will differ
# across units this allowing first-known-good to be chosen.
_mappings = parse_mappings(mappings, key_rvalue=True)
if not _mappings or list(_mappings.values()) == ['']: if not _mappings or list(_mappings.values()) == ['']:
if not mappings: if not mappings:
return {} return {}
# For backwards-compatibility we need to support port-only provided in # For backwards-compatibility we need to support port-only provided in
# config. # config.
_mappings = {default_bridge: mappings.split()[0]} _mappings = {mappings.split()[0]: default_bridge}
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")
ports = _mappings.keys()
if len(set(ports)) != len(ports): if len(set(ports)) != len(ports):
raise Exception("It is not allowed to have the same port configured " raise Exception("It is not allowed to have the same port configured "
"on more than one bridge") "on more than one bridge")

View File

@ -29,8 +29,8 @@ from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
try: try:
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
except ImportError: except ImportError:
# python-jinja2 may not be installed yet, or we're running unittests. apt_install('python-jinja2', fatal=True)
FileSystemLoader = ChoiceLoader = Environment = exceptions = None from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
class OSConfigException(Exception): class OSConfigException(Exception):

View File

@ -24,6 +24,7 @@ import subprocess
import json import json
import os import os
import sys import sys
import re
import six import six
import yaml import yaml
@ -69,7 +70,6 @@ CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed ' DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
'restricted main multiverse universe') 'restricted main multiverse universe')
UBUNTU_OPENSTACK_RELEASE = OrderedDict([ UBUNTU_OPENSTACK_RELEASE = OrderedDict([
('oneiric', 'diablo'), ('oneiric', 'diablo'),
('precise', 'essex'), ('precise', 'essex'),
@ -118,6 +118,34 @@ SWIFT_CODENAMES = OrderedDict([
('2.3.0', 'liberty'), ('2.3.0', 'liberty'),
]) ])
# >= Liberty version->codename mapping
PACKAGE_CODENAMES = {
'nova-common': OrderedDict([
('12.0.0', 'liberty'),
]),
'neutron-common': OrderedDict([
('7.0.0', 'liberty'),
]),
'cinder-common': OrderedDict([
('7.0.0', 'liberty'),
]),
'keystone': OrderedDict([
('8.0.0', 'liberty'),
]),
'horizon-common': OrderedDict([
('8.0.0', 'liberty'),
]),
'ceilometer-common': OrderedDict([
('5.0.0', 'liberty'),
]),
'heat-common': OrderedDict([
('5.0.0', 'liberty'),
]),
'glance-common': OrderedDict([
('11.0.0', 'liberty'),
]),
}
DEFAULT_LOOPBACK_SIZE = '5G' DEFAULT_LOOPBACK_SIZE = '5G'
@ -201,7 +229,16 @@ def get_os_codename_package(package, fatal=True):
error_out(e) error_out(e)
vers = apt.upstream_version(pkg.current_ver.ver_str) vers = apt.upstream_version(pkg.current_ver.ver_str)
match = re.match('^(\d)\.(\d)\.(\d)', vers)
if match:
vers = match.group(0)
# >= Liberty independent project versions
if (package in PACKAGE_CODENAMES and
vers in PACKAGE_CODENAMES[package]):
return PACKAGE_CODENAMES[package][vers]
else:
# < Liberty co-ordinated project versions
try: try:
if 'swift' in pkg.name: if 'swift' in pkg.name:
swift_vers = vers[:5] swift_vers = vers[:5]

View File

@ -43,9 +43,10 @@ def zap_disk(block_device):
:param block_device: str: Full path of block device to clean. :param block_device: str: Full path of block device to clean.
''' '''
# https://github.com/ceph/ceph/commit/fdd7f8d83afa25c4e09aaedd90ab93f3b64a677b
# sometimes sgdisk exits non-zero; this is OK, dd will clean up # sometimes sgdisk exits non-zero; this is OK, dd will clean up
call(['sgdisk', '--zap-all', '--mbrtogpt', call(['sgdisk', '--zap-all', '--', block_device])
'--clear', block_device]) call(['sgdisk', '--clear', '--mbrtogpt', '--', block_device])
dev_end = check_output(['blockdev', '--getsz', dev_end = check_output(['blockdev', '--getsz',
block_device]).decode('UTF-8') block_device]).decode('UTF-8')
gpt_end = int(dev_end.split()[0]) - 100 gpt_end = int(dev_end.split()[0]) - 100
@ -67,4 +68,4 @@ def is_device_mounted(device):
out = check_output(['mount']).decode('UTF-8') out = check_output(['mount']).decode('UTF-8')
if is_partition: if is_partition:
return bool(re.search(device + r"\b", out)) return bool(re.search(device + r"\b", out))
return bool(re.search(device + r"[0-9]+\b", out)) return bool(re.search(device + r"[0-9]*\b", out))

View File

@ -0,0 +1,45 @@
#!/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 <http://www.gnu.org/licenses/>.
__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
import os
import subprocess
def sed(filename, before, after, flags='g'):
"""
Search and replaces the given pattern on filename.
:param filename: relative or absolute file path.
:param before: expression to be replaced (see 'man sed')
:param after: expression to replace with (see 'man sed')
:param flags: sed-compatible regex flags in example, to make
the search and replace case insensitive, specify ``flags="i"``.
The ``g`` flag is always specified regardless, so you do not
need to remember to include it when overriding this parameter.
:returns: If the sed command exit code was zero then return,
otherwise raise CalledProcessError.
"""
expression = r's/{0}/{1}/{2}'.format(before,
after, flags)
return subprocess.check_call(["sed", "-i", "-r", "-e",
expression,
os.path.expanduser(filename)])

View File

@ -21,6 +21,7 @@
# Charm Helpers Developers <juju@lists.ubuntu.com> # Charm Helpers Developers <juju@lists.ubuntu.com>
from __future__ import print_function from __future__ import print_function
import copy
from distutils.version import LooseVersion from distutils.version import LooseVersion
from functools import wraps from functools import wraps
import glob import glob
@ -73,6 +74,7 @@ def cached(func):
res = func(*args, **kwargs) res = func(*args, **kwargs)
cache[key] = res cache[key] = res
return res return res
wrapper._wrapped = func
return wrapper return wrapper
@ -172,9 +174,19 @@ def relation_type():
return os.environ.get('JUJU_RELATION', None) return os.environ.get('JUJU_RELATION', None)
def relation_id(): @cached
"""The relation ID for the current relation hook""" def relation_id(relation_name=None, service_or_unit=None):
"""The relation ID for the current or a specified relation"""
if not relation_name and not service_or_unit:
return os.environ.get('JUJU_RELATION_ID', None) return os.environ.get('JUJU_RELATION_ID', None)
elif relation_name and service_or_unit:
service_name = service_or_unit.split('/')[0]
for relid in relation_ids(relation_name):
remote_service = remote_service_name(relid)
if remote_service == service_name:
return relid
else:
raise ValueError('Must specify neither or both of relation_name and service_or_unit')
def local_unit(): def local_unit():
@ -192,9 +204,20 @@ def service_name():
return local_unit().split('/')[0] return local_unit().split('/')[0]
@cached
def remote_service_name(relid=None):
"""The remote service name for a given relation-id (or the current relation)"""
if relid is None:
unit = remote_unit()
else:
units = related_units(relid)
unit = units[0] if units else None
return unit.split('/')[0] if unit else None
def hook_name(): def hook_name():
"""The name of the currently executing hook""" """The name of the currently executing hook"""
return os.path.basename(sys.argv[0]) return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
class Config(dict): class Config(dict):
@ -263,7 +286,7 @@ class Config(dict):
self.path = path or self.path self.path = path or self.path
with open(self.path) as f: with open(self.path) as f:
self._prev_dict = json.load(f) self._prev_dict = json.load(f)
for k, v in self._prev_dict.items(): for k, v in copy.deepcopy(self._prev_dict).items():
if k not in self: if k not in self:
self[k] = v self[k] = v
@ -467,6 +490,63 @@ def relation_types():
return rel_types return rel_types
@cached
def relation_to_interface(relation_name):
"""
Given the name of a relation, return the interface that relation uses.
:returns: The interface name, or ``None``.
"""
return relation_to_role_and_interface(relation_name)[1]
@cached
def relation_to_role_and_interface(relation_name):
"""
Given the name of a relation, return the role and the name of the interface
that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
:returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
"""
_metadata = metadata()
for role in ('provides', 'requires', 'peer'):
interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
if interface:
return role, interface
return None, None
@cached
def role_and_interface_to_relations(role, interface_name):
"""
Given a role and interface name, return a list of relation names for the
current charm that use that interface under that role (where role is one
of ``provides``, ``requires``, or ``peer``).
:returns: A list of relation names.
"""
_metadata = metadata()
results = []
for relation_name, relation in _metadata.get(role, {}).items():
if relation['interface'] == interface_name:
results.append(relation_name)
return results
@cached
def interface_to_relations(interface_name):
"""
Given an interface, return a list of relation names for the current
charm that use that interface.
:returns: A list of relation names.
"""
results = []
for role in ('provides', 'requires', 'peer'):
results.extend(role_and_interface_to_relations(role, interface_name))
return results
@cached @cached
def charm_name(): def charm_name():
"""Get the name of the current charm as is specified on metadata.yaml""" """Get the name of the current charm as is specified on metadata.yaml"""
@ -643,6 +723,21 @@ def action_fail(message):
subprocess.check_call(['action-fail', message]) subprocess.check_call(['action-fail', message])
def action_name():
"""Get the name of the currently executing action."""
return os.environ.get('JUJU_ACTION_NAME')
def action_uuid():
"""Get the UUID of the currently executing action."""
return os.environ.get('JUJU_ACTION_UUID')
def action_tag():
"""Get the tag for the currently executing action."""
return os.environ.get('JUJU_ACTION_TAG')
def status_set(workload_state, message): def status_set(workload_state, message):
"""Set the workload state with a message """Set the workload state with a message

View File

@ -72,7 +72,7 @@ def service_pause(service_name, init_dir=None):
stopped = service_stop(service_name) stopped = service_stop(service_name)
# XXX: Support systemd too # XXX: Support systemd too
override_path = os.path.join( override_path = os.path.join(
init_dir, '{}.conf.override'.format(service_name)) init_dir, '{}.override'.format(service_name))
with open(override_path, 'w') as fh: with open(override_path, 'w') as fh:
fh.write("manual\n") fh.write("manual\n")
return stopped return stopped
@ -86,7 +86,7 @@ def service_resume(service_name, init_dir=None):
if init_dir is None: if init_dir is None:
init_dir = "/etc/init" init_dir = "/etc/init"
override_path = os.path.join( override_path = os.path.join(
init_dir, '{}.conf.override'.format(service_name)) init_dir, '{}.override'.format(service_name))
if os.path.exists(override_path): if os.path.exists(override_path):
os.unlink(override_path) os.unlink(override_path)
started = service_start(service_name) started = service_start(service_name)
@ -148,6 +148,16 @@ def adduser(username, password=None, shell='/bin/bash', system_user=False):
return user_info return user_info
def user_exists(username):
"""Check if a user exists"""
try:
pwd.getpwnam(username)
user_exists = True
except KeyError:
user_exists = False
return user_exists
def add_group(group_name, system_group=False): def add_group(group_name, system_group=False):
"""Add a group to the system""" """Add a group to the system"""
try: try:
@ -280,6 +290,17 @@ def mounts():
return system_mounts return system_mounts
def fstab_mount(mountpoint):
"""Mount filesystem using fstab"""
cmd_args = ['mount', mountpoint]
try:
subprocess.check_output(cmd_args)
except subprocess.CalledProcessError as e:
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
return False
return True
def file_hash(path, hash_type='md5'): def file_hash(path, hash_type='md5'):
""" """
Generate a hash checksum of the contents of 'path' or None if not found. Generate a hash checksum of the contents of 'path' or None if not found.
@ -396,25 +417,80 @@ def pwgen(length=None):
return(''.join(random_chars)) return(''.join(random_chars))
def list_nics(nic_type): def is_phy_iface(interface):
"""Returns True if interface is not virtual, otherwise False."""
if interface:
sys_net = '/sys/class/net'
if os.path.isdir(sys_net):
for iface in glob.glob(os.path.join(sys_net, '*')):
if '/virtual/' in os.path.realpath(iface):
continue
if interface == os.path.basename(iface):
return True
return False
def get_bond_master(interface):
"""Returns bond master if interface is bond slave otherwise None.
NOTE: the provided interface is expected to be physical
"""
if interface:
iface_path = '/sys/class/net/%s' % (interface)
if os.path.exists(iface_path):
if '/virtual/' in os.path.realpath(iface_path):
return None
master = os.path.join(iface_path, 'master')
if os.path.exists(master):
master = os.path.realpath(master)
# make sure it is a bond master
if os.path.exists(os.path.join(master, 'bonding')):
return os.path.basename(master)
return None
def list_nics(nic_type=None):
'''Return a list of nics of given type(s)''' '''Return a list of nics of given type(s)'''
if isinstance(nic_type, six.string_types): if isinstance(nic_type, six.string_types):
int_types = [nic_type] int_types = [nic_type]
else: else:
int_types = nic_type int_types = nic_type
interfaces = [] interfaces = []
if nic_type:
for int_type in int_types: for int_type in int_types:
cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') ip_output = subprocess.check_output(cmd).decode('UTF-8')
ip_output = ip_output.split('\n')
ip_output = (line for line in ip_output if line) ip_output = (line for line in ip_output if line)
for line in ip_output: for line in ip_output:
if line.split()[1].startswith(int_type): if line.split()[1].startswith(int_type):
matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line) matched = re.search('.*: (' + int_type +
r'[0-9]+\.[0-9]+)@.*', line)
if matched: if matched:
interface = matched.groups()[0] iface = matched.groups()[0]
else: else:
interface = line.split()[1].replace(":", "") iface = line.split()[1].replace(":", "")
interfaces.append(interface)
if iface not in interfaces:
interfaces.append(iface)
else:
cmd = ['ip', 'a']
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
ip_output = (line.strip() for line in ip_output if line)
key = re.compile('^[0-9]+:\s+(.+):')
for line in ip_output:
matched = re.search(key, line)
if matched:
iface = matched.group(1)
iface = iface.partition("@")[0]
if iface not in interfaces:
interfaces.append(iface)
return interfaces return interfaces

View File

@ -0,0 +1,62 @@
# -*- 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 <http://www.gnu.org/licenses/>.
import yaml
from charmhelpers.core import fstab
from charmhelpers.core import sysctl
from charmhelpers.core.host import (
add_group,
add_user_to_group,
fstab_mount,
mkdir,
)
def hugepage_support(user, group='hugetlb', nr_hugepages=256,
max_map_count=65536, mnt_point='/run/hugepages/kvm',
pagesize='2MB', mount=True):
"""Enable hugepages on system.
Args:
user (str) -- Username to allow access to hugepages to
group (str) -- Group name to own hugepages
nr_hugepages (int) -- Number of pages to reserve
max_map_count (int) -- Number of Virtual Memory Areas a process can own
mnt_point (str) -- Directory to mount hugepages on
pagesize (str) -- Size of hugepages
mount (bool) -- Whether to Mount hugepages
"""
group_info = add_group(group)
gid = group_info.gr_gid
add_user_to_group(user, group)
sysctl_settings = {
'vm.nr_hugepages': nr_hugepages,
'vm.max_map_count': max_map_count,
'vm.hugetlb_shm_group': gid,
}
sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
lfstab = fstab.Fstab()
fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
if fstab_entry:
lfstab.remove_entry(fstab_entry)
entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
lfstab.add_entry(entry)
if mount:
fstab_mount(mnt_point)

View File

@ -16,7 +16,9 @@
import os import os
import yaml import yaml
from charmhelpers.core import hookenv from charmhelpers.core import hookenv
from charmhelpers.core import host
from charmhelpers.core import templating from charmhelpers.core import templating
from charmhelpers.core.services.base import ManagerCallback from charmhelpers.core.services.base import ManagerCallback
@ -240,27 +242,41 @@ class TemplateCallback(ManagerCallback):
:param str source: The template source file, relative to :param str source: The template source file, relative to
`$CHARM_DIR/templates` `$CHARM_DIR/templates`
:param str target: The target to write the rendered template to :param str target: The target to write the rendered template to
:param str owner: The owner of the rendered file :param str owner: The owner of the rendered file
:param str group: The group of the rendered file :param str group: The group of the rendered file
:param int perms: The permissions of the rendered file :param int perms: The permissions of the rendered file
:param partial on_change_action: functools partial to be executed when
rendered file changes
""" """
def __init__(self, source, target, def __init__(self, source, target,
owner='root', group='root', perms=0o444): owner='root', group='root', perms=0o444,
on_change_action=None):
self.source = source self.source = source
self.target = target self.target = target
self.owner = owner self.owner = owner
self.group = group self.group = group
self.perms = perms self.perms = perms
self.on_change_action = on_change_action
def __call__(self, manager, service_name, event_name): def __call__(self, manager, service_name, event_name):
pre_checksum = ''
if self.on_change_action and os.path.isfile(self.target):
pre_checksum = host.file_hash(self.target)
service = manager.get_service(service_name) service = manager.get_service(service_name)
context = {} context = {}
for ctx in service.get('required_data', []): for ctx in service.get('required_data', []):
context.update(ctx) context.update(ctx)
templating.render(self.source, self.target, context, templating.render(self.source, self.target, context,
self.owner, self.group, self.perms) self.owner, self.group, self.perms)
if self.on_change_action:
if pre_checksum == host.file_hash(self.target):
hookenv.log(
'No change detected: {}'.format(self.target),
hookenv.DEBUG)
else:
self.on_change_action()
# Convenience aliases for templates # Convenience aliases for templates

View File

@ -152,6 +152,7 @@ associated to the hookname.
import collections import collections
import contextlib import contextlib
import datetime import datetime
import itertools
import json import json
import os import os
import pprint import pprint
@ -164,8 +165,7 @@ __author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
class Storage(object): class Storage(object):
"""Simple key value database for local unit state within charms. """Simple key value database for local unit state within charms.
Modifications are automatically committed at hook exit. That's Modifications are not persisted unless :meth:`flush` is called.
currently regardless of exit code.
To support dicts, lists, integer, floats, and booleans values To support dicts, lists, integer, floats, and booleans values
are automatically json encoded/decoded. are automatically json encoded/decoded.
@ -173,6 +173,9 @@ class Storage(object):
def __init__(self, path=None): def __init__(self, path=None):
self.db_path = path self.db_path = path
if path is None: if path is None:
if 'UNIT_STATE_DB' in os.environ:
self.db_path = os.environ['UNIT_STATE_DB']
else:
self.db_path = os.path.join( self.db_path = os.path.join(
os.environ.get('CHARM_DIR', ''), '.unit-state.db') os.environ.get('CHARM_DIR', ''), '.unit-state.db')
self.conn = sqlite3.connect('%s' % self.db_path) self.conn = sqlite3.connect('%s' % self.db_path)
@ -189,15 +192,8 @@ class Storage(object):
self.conn.close() self.conn.close()
self._closed = True self._closed = True
def _scoped_query(self, stmt, params=None):
if params is None:
params = []
return stmt, params
def get(self, key, default=None, record=False): def get(self, key, default=None, record=False):
self.cursor.execute( self.cursor.execute('select data from kv where key=?', [key])
*self._scoped_query(
'select data from kv where key=?', [key]))
result = self.cursor.fetchone() result = self.cursor.fetchone()
if not result: if not result:
return default return default
@ -206,33 +202,81 @@ class Storage(object):
return json.loads(result[0]) return json.loads(result[0])
def getrange(self, key_prefix, strip=False): def getrange(self, key_prefix, strip=False):
stmt = "select key, data from kv where key like '%s%%'" % key_prefix """
self.cursor.execute(*self._scoped_query(stmt)) Get a range of keys starting with a common prefix as a mapping of
keys to values.
:param str key_prefix: Common prefix among all keys
:param bool strip: Optionally strip the common prefix from the key
names in the returned dict
:return dict: A (possibly empty) dict of key-value mappings
"""
self.cursor.execute("select key, data from kv where key like ?",
['%s%%' % key_prefix])
result = self.cursor.fetchall() result = self.cursor.fetchall()
if not result: if not result:
return None return {}
if not strip: if not strip:
key_prefix = '' key_prefix = ''
return dict([ return dict([
(k[len(key_prefix):], json.loads(v)) for k, v in result]) (k[len(key_prefix):], json.loads(v)) for k, v in result])
def update(self, mapping, prefix=""): def update(self, mapping, prefix=""):
"""
Set the values of multiple keys at once.
:param dict mapping: Mapping of keys to values
:param str prefix: Optional prefix to apply to all keys in `mapping`
before setting
"""
for k, v in mapping.items(): for k, v in mapping.items():
self.set("%s%s" % (prefix, k), v) self.set("%s%s" % (prefix, k), v)
def unset(self, key): def unset(self, key):
"""
Remove a key from the database entirely.
"""
self.cursor.execute('delete from kv where key=?', [key]) self.cursor.execute('delete from kv where key=?', [key])
if self.revision and self.cursor.rowcount: if self.revision and self.cursor.rowcount:
self.cursor.execute( self.cursor.execute(
'insert into kv_revisions values (?, ?, ?)', 'insert into kv_revisions values (?, ?, ?)',
[key, self.revision, json.dumps('DELETED')]) [key, self.revision, json.dumps('DELETED')])
def unsetrange(self, keys=None, prefix=""):
"""
Remove a range of keys starting with a common prefix, from the database
entirely.
:param list keys: List of keys to remove.
:param str prefix: Optional prefix to apply to all keys in ``keys``
before removing.
"""
if keys is not None:
keys = ['%s%s' % (prefix, key) for key in keys]
self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
if self.revision and self.cursor.rowcount:
self.cursor.execute(
'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
else:
self.cursor.execute('delete from kv where key like ?',
['%s%%' % prefix])
if self.revision and self.cursor.rowcount:
self.cursor.execute(
'insert into kv_revisions values (?, ?, ?)',
['%s%%' % prefix, self.revision, json.dumps('DELETED')])
def set(self, key, value): def set(self, key, value):
"""
Set a value in the database.
:param str key: Key to set the value for
:param value: Any JSON-serializable value to be set
"""
serialized = json.dumps(value) serialized = json.dumps(value)
self.cursor.execute( self.cursor.execute('select data from kv where key=?', [key])
'select data from kv where key=?', [key])
exists = self.cursor.fetchone() exists = self.cursor.fetchone()
# Skip mutations to the same value # Skip mutations to the same value

View File

@ -90,6 +90,14 @@ CLOUD_ARCHIVE_POCKETS = {
'kilo/proposed': 'trusty-proposed/kilo', 'kilo/proposed': 'trusty-proposed/kilo',
'trusty-kilo/proposed': 'trusty-proposed/kilo', 'trusty-kilo/proposed': 'trusty-proposed/kilo',
'trusty-proposed/kilo': 'trusty-proposed/kilo', 'trusty-proposed/kilo': 'trusty-proposed/kilo',
# Liberty
'liberty': 'trusty-updates/liberty',
'trusty-liberty': 'trusty-updates/liberty',
'trusty-liberty/updates': 'trusty-updates/liberty',
'trusty-updates/liberty': 'trusty-updates/liberty',
'liberty/proposed': 'trusty-proposed/liberty',
'trusty-liberty/proposed': 'trusty-proposed/liberty',
'trusty-proposed/liberty': 'trusty-proposed/liberty',
} }
# The order of this list is very important. Handlers should be listed in from # The order of this list is very important. Handlers should be listed in from

View File

@ -0,0 +1 @@
neutron_api_hooks.py

View File

@ -0,0 +1 @@
neutron_api_hooks.py

View File

@ -0,0 +1 @@
neutron_api_hooks.py

View File

@ -249,3 +249,63 @@ class HAProxyContext(context.HAProxyContext):
# for haproxy.conf # for haproxy.conf
ctxt['service_ports'] = port_mapping ctxt['service_ports'] = port_mapping
return ctxt return ctxt
class NeutronApiSDNContext(context.SubordinateConfigContext):
interfaces = 'neutron-plugin-api-subordinate'
def __init__(self):
super(NeutronApiSDNContext, self).__init__(
interface='neutron-plugin-api-subordinate',
service='neutron-api',
config_file='/etc/neutron/neutron.conf')
def __call__(self):
ctxt = super(NeutronApiSDNContext, self).__call__()
defaults = {
'core-plugin': {
'templ_key': 'core_plugin',
'value': 'neutron.plugins.ml2.plugin.Ml2Plugin',
},
'neutron-plugin-config': {
'templ_key': 'neutron_plugin_config',
'value': '/etc/neutron/plugins/ml2/ml2_conf.ini',
},
'service-plugins': {
'templ_key': 'service_plugins',
'value': 'router,firewall,lbaas,vpnaas,metering',
},
'restart-trigger': {
'templ_key': 'restart_trigger',
'value': '',
},
}
for rid in relation_ids('neutron-plugin-api-subordinate'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
plugin = rdata.get('neutron-plugin')
if not plugin:
continue
ctxt['neutron_plugin'] = plugin
for key in defaults.keys():
remote_value = rdata.get(key)
ctxt_key = defaults[key]['templ_key']
if remote_value:
ctxt[ctxt_key] = remote_value
else:
ctxt[ctxt_key] = defaults[key]['value']
return ctxt
return ctxt
class NeutronApiSDNConfigFileContext(context.OSContextGenerator):
interfaces = ['neutron-plugin-api-subordinate']
def __call__(self):
for rid in relation_ids('neutron-plugin-api-subordinate'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
neutron_server_plugin_conf = rdata.get('neutron-plugin-config')
if neutron_server_plugin_conf:
return {'config': neutron_server_plugin_conf}
return {'config': '/etc/neutron/plugins/ml2/ml2_conf.ini'}

View File

@ -536,7 +536,8 @@ def zeromq_configuration_relation_joined(relid=None):
users="neutron") users="neutron")
@hooks.hook('zeromq-configuration-relation-changed') @hooks.hook('zeromq-configuration-relation-changed',
'neutron-plugin-api-subordinate-relation-changed')
@restart_on_change(restart_map(), stopstart=True) @restart_on_change(restart_map(), stopstart=True)
def zeromq_configuration_relation_changed(): def zeromq_configuration_relation_changed():
CONFIGS.write_all() CONFIGS.write_all()

View File

@ -160,12 +160,17 @@ def api_port(service):
return API_PORTS[service] return API_PORTS[service]
def manage_plugin():
return config('manage-neutron-plugin-legacy-mode')
def determine_packages(source=None): def determine_packages(source=None):
# currently all packages match service names # currently all packages match service names
packages = [] + BASE_PACKAGES packages = [] + BASE_PACKAGES
for v in resource_map().values(): for v in resource_map().values():
packages.extend(v['services']) packages.extend(v['services'])
if manage_plugin():
pkgs = neutron_plugin_attribute(config('neutron-plugin'), pkgs = neutron_plugin_attribute(config('neutron-plugin'),
'server_packages', 'server_packages',
'neutron') 'neutron')
@ -215,8 +220,9 @@ def resource_map():
else: else:
resource_map.pop(APACHE_24_CONF) resource_map.pop(APACHE_24_CONF)
# add neutron plugin requirements. nova-c-c only needs the neutron-server if manage_plugin():
# associated with configs, not the plugin agent. # add neutron plugin requirements. nova-c-c only needs the
# neutron-server associated with configs, not the plugin agent.
plugin = config('neutron-plugin') plugin = config('neutron-plugin')
conf = neutron_plugin_attribute(plugin, 'config', 'neutron') conf = neutron_plugin_attribute(plugin, 'config', 'neutron')
ctxts = (neutron_plugin_attribute(plugin, 'contexts', 'neutron') ctxts = (neutron_plugin_attribute(plugin, 'contexts', 'neutron')
@ -233,6 +239,12 @@ def resource_map():
resource_map[conf]['contexts'].append( resource_map[conf]['contexts'].append(
context.PostgresqlDBContext(database=config('database'))) context.PostgresqlDBContext(database=config('database')))
else:
resource_map[NEUTRON_CONF]['contexts'].append(
neutron_api_context.NeutronApiSDNContext()
)
resource_map[NEUTRON_DEFAULT]['contexts'] = \
[neutron_api_context.NeutronApiSDNConfigFileContext()]
return resource_map return resource_map

View File

@ -40,6 +40,9 @@ requires:
zeromq-configuration: zeromq-configuration:
interface: zeromq-configuration interface: zeromq-configuration
scope: container scope: container
neutron-plugin-api-subordinate:
interface: neutron-plugin-api-subordinate
scope: container
peers: peers:
cluster: cluster:
interface: neutron-api-ha interface: neutron-api-ha

View File

@ -27,10 +27,14 @@ bind_port = 9696
{% if core_plugin -%} {% if core_plugin -%}
core_plugin = {{ core_plugin }} core_plugin = {{ core_plugin }}
{% if service_plugins -%}
service_plugins = {{ service_plugins }}
{% else -%}
{% if neutron_plugin in ['ovs', 'ml2'] -%} {% if neutron_plugin in ['ovs', 'ml2'] -%}
service_plugins = neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,neutron.services.firewall.fwaas_plugin.FirewallPlugin,neutron.services.loadbalancer.plugin.LoadBalancerPlugin,neutron.services.vpn.plugin.VPNDriverPlugin,neutron.services.metering.metering_plugin.MeteringPlugin service_plugins = neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,neutron.services.firewall.fwaas_plugin.FirewallPlugin,neutron.services.loadbalancer.plugin.LoadBalancerPlugin,neutron.services.vpn.plugin.VPNDriverPlugin,neutron.services.metering.metering_plugin.MeteringPlugin
{% endif -%} {% endif -%}
{% endif -%} {% endif -%}
{% endif -%}
{% if neutron_security_groups -%} {% if neutron_security_groups -%}
allow_overlapping_ips = True allow_overlapping_ips = True
@ -50,6 +54,12 @@ nova_admin_password = {{ admin_password }}
nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0
{% endif -%} {% endif -%}
{% if sections and 'DEFAULT' in sections -%}
{% for key, value in sections['DEFAULT'] -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif %}
[quotas] [quotas]
quota_driver = neutron.db.quota_db.DbQuotaDriver quota_driver = neutron.db.quota_db.DbQuotaDriver
{% if neutron_security_groups -%} {% if neutron_security_groups -%}

View File

@ -54,6 +54,12 @@ nova_admin_password = {{ admin_password }}
nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0
{% endif -%} {% endif -%}
{% if sections and 'DEFAULT' in sections -%}
{% for key, value in sections['DEFAULT'] -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif %}
[quotas] [quotas]
quota_driver = neutron.db.quota_db.DbQuotaDriver quota_driver = neutron.db.quota_db.DbQuotaDriver
{% if neutron_security_groups -%} {% if neutron_security_groups -%}

View File

@ -31,10 +31,14 @@ bind_port = 9696
{% if core_plugin -%} {% if core_plugin -%}
core_plugin = {{ core_plugin }} core_plugin = {{ core_plugin }}
{% if service_plugins -%}
service_plugins = {{ service_plugins }}
{% else -%}
{% if neutron_plugin in ['ovs', 'ml2'] -%} {% if neutron_plugin in ['ovs', 'ml2'] -%}
service_plugins = router,firewall,lbaas,vpnaas,metering service_plugins = router,firewall,lbaas,vpnaas,metering
{% endif -%} {% endif -%}
{% endif -%} {% endif -%}
{% endif -%}
{% if neutron_security_groups -%} {% if neutron_security_groups -%}
allow_overlapping_ips = True allow_overlapping_ips = True
@ -52,6 +56,12 @@ nova_admin_password = {{ admin_password }}
nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0 nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0
{% endif -%} {% endif -%}
{% if sections and 'DEFAULT' in sections -%}
{% for key, value in sections['DEFAULT'] -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif %}
{% include "section-zeromq" %} {% include "section-zeromq" %}
[quotas] [quotas]

View File

@ -1,9 +0,0 @@
#!/usr/bin/python
"""Amulet tests on a basic neutron-api deployment on utopic-juno."""
from basic_deployment import NeutronAPIBasicDeployment
if __name__ == '__main__':
deployment = NeutronAPIBasicDeployment(series='utopic')
deployment.run_tests()

View File

@ -81,7 +81,7 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment):
{'name': 'rabbitmq-server'}, {'name': 'keystone'}, {'name': 'rabbitmq-server'}, {'name': 'keystone'},
{'name': 'neutron-openvswitch'}, {'name': 'neutron-openvswitch'},
{'name': 'nova-cloud-controller'}, {'name': 'nova-cloud-controller'},
{'name': 'quantum-gateway'}, {'name': 'neutron-gateway'},
{'name': 'nova-compute'}] {'name': 'nova-compute'}]
super(NeutronAPIBasicDeployment, self)._add_services(this_service, super(NeutronAPIBasicDeployment, self)._add_services(this_service,
other_services) other_services)
@ -92,7 +92,7 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment):
'neutron-api:shared-db': 'mysql:shared-db', 'neutron-api:shared-db': 'mysql:shared-db',
'neutron-api:amqp': 'rabbitmq-server:amqp', 'neutron-api:amqp': 'rabbitmq-server:amqp',
'neutron-api:neutron-api': 'nova-cloud-controller:neutron-api', 'neutron-api:neutron-api': 'nova-cloud-controller:neutron-api',
'neutron-api:neutron-plugin-api': 'quantum-gateway:' 'neutron-api:neutron-plugin-api': 'neutron-gateway:'
'neutron-plugin-api', 'neutron-plugin-api',
'neutron-api:neutron-plugin-api': 'neutron-openvswitch:' 'neutron-api:neutron-plugin-api': 'neutron-openvswitch:'
'neutron-plugin-api', 'neutron-plugin-api',
@ -171,7 +171,7 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment):
self.keystone_sentry = self.d.sentry.unit['keystone/0'] self.keystone_sentry = self.d.sentry.unit['keystone/0']
self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0'] self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']
self.nova_cc_sentry = self.d.sentry.unit['nova-cloud-controller/0'] self.nova_cc_sentry = self.d.sentry.unit['nova-cloud-controller/0']
self.quantum_gateway_sentry = self.d.sentry.unit['quantum-gateway/0'] self.neutron_gateway_sentry = self.d.sentry.unit['neutron-gateway/0']
self.neutron_api_sentry = self.d.sentry.unit['neutron-api/0'] self.neutron_api_sentry = self.d.sentry.unit['neutron-api/0']
self.nova_compute_sentry = self.d.sentry.unit['nova-compute/0'] self.nova_compute_sentry = self.d.sentry.unit['nova-compute/0']
u.log.debug('openstack release val: {}'.format( u.log.debug('openstack release val: {}'.format(
@ -212,7 +212,7 @@ class NeutronAPIBasicDeployment(OpenStackAmuletDeployment):
self.mysql_sentry: ['status mysql'], self.mysql_sentry: ['status mysql'],
self.keystone_sentry: ['status keystone'], self.keystone_sentry: ['status keystone'],
self.nova_cc_sentry: nova_cc_services, self.nova_cc_sentry: nova_cc_services,
self.quantum_gateway_sentry: neutron_services, self.neutron_gateway_sentry: neutron_services,
self.neutron_api_sentry: neutron_api_services, self.neutron_api_sentry: neutron_api_services,
} }

View File

@ -14,16 +14,22 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
import amulet
import ConfigParser
import distro_info
import io import io
import json
import logging import logging
import os import os
import re import re
import six import subprocess
import sys import sys
import time import time
import amulet
import distro_info
import six
from six.moves import configparser
if six.PY3:
from urllib import parse as urlparse
else:
import urlparse import urlparse
@ -142,19 +148,23 @@ class AmuletUtils(object):
for service_name in services_list: for service_name in services_list:
if (self.ubuntu_releases.index(release) >= systemd_switch or if (self.ubuntu_releases.index(release) >= systemd_switch or
service_name == "rabbitmq-server"): service_name in ['rabbitmq-server', 'apache2']):
# init is systemd # init is systemd (or regular sysv)
cmd = 'sudo service {} status'.format(service_name) cmd = 'sudo service {} status'.format(service_name)
output, code = sentry_unit.run(cmd)
service_running = code == 0
elif self.ubuntu_releases.index(release) < systemd_switch: elif self.ubuntu_releases.index(release) < systemd_switch:
# init is upstart # init is upstart
cmd = 'sudo status {}'.format(service_name) cmd = 'sudo status {}'.format(service_name)
output, code = sentry_unit.run(cmd) output, code = sentry_unit.run(cmd)
service_running = code == 0 and "start/running" in output
self.log.debug('{} `{}` returned ' self.log.debug('{} `{}` returned '
'{}'.format(sentry_unit.info['unit_name'], '{}'.format(sentry_unit.info['unit_name'],
cmd, code)) cmd, code))
if code != 0: if not service_running:
return "command `{}` returned {}".format(cmd, str(code)) return u"command `{}` returned {} {}".format(
cmd, output, str(code))
return None return None
def _get_config(self, unit, filename): def _get_config(self, unit, filename):
@ -164,7 +174,7 @@ class AmuletUtils(object):
# NOTE(beisner): by default, ConfigParser does not handle options # NOTE(beisner): by default, ConfigParser does not handle options
# with no value, such as the flags used in the mysql my.cnf file. # with no value, such as the flags used in the mysql my.cnf file.
# https://bugs.python.org/issue7005 # https://bugs.python.org/issue7005
config = ConfigParser.ConfigParser(allow_no_value=True) config = configparser.ConfigParser(allow_no_value=True)
config.readfp(io.StringIO(file_contents)) config.readfp(io.StringIO(file_contents))
return config return config
@ -450,15 +460,20 @@ class AmuletUtils(object):
cmd, code, output)) cmd, code, output))
return None return None
def get_process_id_list(self, sentry_unit, process_name): def get_process_id_list(self, sentry_unit, process_name,
expect_success=True):
"""Get a list of process ID(s) from a single sentry juju unit """Get a list of process ID(s) from a single sentry juju unit
for a single process name. for a single process name.
:param sentry_unit: Pointer to amulet sentry instance (juju unit) :param sentry_unit: Amulet sentry instance (juju unit)
:param process_name: Process name :param process_name: Process name
:param expect_success: If False, expect the PID to be missing,
raise if it is present.
:returns: List of process IDs :returns: List of process IDs
""" """
cmd = 'pidof {}'.format(process_name) cmd = 'pidof -x {}'.format(process_name)
if not expect_success:
cmd += " || exit 0 && exit 1"
output, code = sentry_unit.run(cmd) output, code = sentry_unit.run(cmd)
if code != 0: if code != 0:
msg = ('{} `{}` returned {} ' msg = ('{} `{}` returned {} '
@ -467,14 +482,23 @@ class AmuletUtils(object):
amulet.raise_status(amulet.FAIL, msg=msg) amulet.raise_status(amulet.FAIL, msg=msg)
return str(output).split() return str(output).split()
def get_unit_process_ids(self, unit_processes): def get_unit_process_ids(self, unit_processes, expect_success=True):
"""Construct a dict containing unit sentries, process names, and """Construct a dict containing unit sentries, process names, and
process IDs.""" process IDs.
:param unit_processes: A dictionary of Amulet sentry instance
to list of process names.
:param expect_success: if False expect the processes to not be
running, raise if they are.
:returns: Dictionary of Amulet sentry instance to dictionary
of process names to PIDs.
"""
pid_dict = {} pid_dict = {}
for sentry_unit, process_list in unit_processes.iteritems(): for sentry_unit, process_list in six.iteritems(unit_processes):
pid_dict[sentry_unit] = {} pid_dict[sentry_unit] = {}
for process in process_list: for process in process_list:
pids = self.get_process_id_list(sentry_unit, process) pids = self.get_process_id_list(
sentry_unit, process, expect_success=expect_success)
pid_dict[sentry_unit].update({process: pids}) pid_dict[sentry_unit].update({process: pids})
return pid_dict return pid_dict
@ -488,7 +512,7 @@ class AmuletUtils(object):
return ('Unit count mismatch. expected, actual: {}, ' return ('Unit count mismatch. expected, actual: {}, '
'{} '.format(len(expected), len(actual))) '{} '.format(len(expected), len(actual)))
for (e_sentry, e_proc_names) in expected.iteritems(): for (e_sentry, e_proc_names) in six.iteritems(expected):
e_sentry_name = e_sentry.info['unit_name'] e_sentry_name = e_sentry.info['unit_name']
if e_sentry in actual.keys(): if e_sentry in actual.keys():
a_proc_names = actual[e_sentry] a_proc_names = actual[e_sentry]
@ -507,11 +531,23 @@ class AmuletUtils(object):
'{}'.format(e_proc_name, a_proc_name)) '{}'.format(e_proc_name, a_proc_name))
a_pids_length = len(a_pids) a_pids_length = len(a_pids)
if e_pids_length != a_pids_length: fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
return ('PID count mismatch. {} ({}) expected, actual: '
'{}, {} ({})'.format(e_sentry_name, e_proc_name, '{}, {} ({})'.format(e_sentry_name, e_proc_name,
e_pids_length, a_pids_length, e_pids_length, a_pids_length,
a_pids)) a_pids))
# If expected is not bool, ensure PID quantities match
if not isinstance(e_pids_length, bool) and \
a_pids_length != e_pids_length:
return fail_msg
# If expected is bool True, ensure 1 or more PIDs exist
elif isinstance(e_pids_length, bool) and \
e_pids_length is True and a_pids_length < 1:
return fail_msg
# If expected is bool False, ensure 0 PIDs exist
elif isinstance(e_pids_length, bool) and \
e_pids_length is False and a_pids_length != 0:
return fail_msg
else: else:
self.log.debug('PID check OK: {} {} {}: ' self.log.debug('PID check OK: {} {} {}: '
'{}'.format(e_sentry_name, e_proc_name, '{}'.format(e_sentry_name, e_proc_name,
@ -531,3 +567,30 @@ class AmuletUtils(object):
return 'Dicts within list are not identical' return 'Dicts within list are not identical'
return None return None
def run_action(self, unit_sentry, action,
_check_output=subprocess.check_output):
"""Run the named action on a given unit sentry.
_check_output parameter is used for dependency injection.
@return action_id.
"""
unit_id = unit_sentry.info["unit_name"]
command = ["juju", "action", "do", "--format=json", unit_id, action]
self.log.info("Running command: %s\n" % " ".join(command))
output = _check_output(command, universal_newlines=True)
data = json.loads(output)
action_id = data[u'Action queued with id']
return action_id
def wait_on_action(self, action_id, _check_output=subprocess.check_output):
"""Wait for a given action, returning if it completed or not.
_check_output parameter is used for dependency injection.
"""
command = ["juju", "action", "fetch", "--format=json", "--wait=0",
action_id]
output = _check_output(command, universal_newlines=True)
data = json.loads(output)
return data.get(u"status") == "completed"

View File

@ -44,7 +44,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
Determine if the local branch being tested is derived from its Determine if the local branch being tested is derived from its
stable or next (dev) branch, and based on this, use the corresonding stable or next (dev) branch, and based on this, use the corresonding
stable or next branches for the other_services.""" stable or next branches for the other_services."""
base_charms = ['mysql', 'mongodb'] base_charms = ['mysql', 'mongodb', 'nrpe']
if self.series in ['precise', 'trusty']: if self.series in ['precise', 'trusty']:
base_series = self.series base_series = self.series
@ -81,7 +81,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
'ceph-osd', 'ceph-radosgw'] 'ceph-osd', 'ceph-radosgw']
# Most OpenStack subordinate charms do not expose an origin option # Most OpenStack subordinate charms do not expose an origin option
# as that is controlled by the principle. # as that is controlled by the principle.
ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch'] ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
if self.openstack: if self.openstack:
for svc in services: for svc in services:

View File

@ -1,3 +1,4 @@
import json
from test_utils import CharmTestCase from test_utils import CharmTestCase
from mock import patch from mock import patch
import neutron_api_context as context import neutron_api_context as context
@ -456,3 +457,135 @@ class NeutronCCContextTest(CharmTestCase):
} }
for key in expect.iterkeys(): for key in expect.iterkeys():
self.assertEquals(napi_ctxt[key], expect[key]) self.assertEquals(napi_ctxt[key], expect[key])
class NeutronApiSDNContextTest(CharmTestCase):
def setUp(self):
super(NeutronApiSDNContextTest, self).setUp(context, TO_PATCH)
self.relation_get.side_effect = self.test_relation.get
def tearDown(self):
super(NeutronApiSDNContextTest, self).tearDown()
def test_init(self):
napisdn_ctxt = context.NeutronApiSDNContext()
self.assertEquals(
napisdn_ctxt.interfaces,
['neutron-plugin-api-subordinate']
)
self.assertEquals(napisdn_ctxt.services, ['neutron-api'])
self.assertEquals(
napisdn_ctxt.config_file,
'/etc/neutron/neutron.conf'
)
@patch.object(charmhelpers.contrib.openstack.context, 'log')
@patch.object(charmhelpers.contrib.openstack.context, 'relation_get')
@patch.object(charmhelpers.contrib.openstack.context, 'related_units')
@patch.object(charmhelpers.contrib.openstack.context, 'relation_ids')
def ctxt_check(self, rel_settings, expect, _rids, _runits, _rget, _log):
self.test_relation.set(rel_settings)
_runits.return_value = ['unit1']
_rids.return_value = ['rid2']
_rget.side_effect = self.test_relation.get
self.relation_ids.return_value = ['rid2']
self.related_units.return_value = ['unit1']
napisdn_ctxt = context.NeutronApiSDNContext()()
self.assertEquals(napisdn_ctxt, expect)
def test_defaults(self):
self.ctxt_check(
{'neutron-plugin': 'ovs'},
{
'core_plugin': 'neutron.plugins.ml2.plugin.Ml2Plugin',
'neutron_plugin_config': ('/etc/neutron/plugins/ml2/'
'ml2_conf.ini'),
'service_plugins': 'router,firewall,lbaas,vpnaas,metering',
'restart_trigger': '',
'neutron_plugin': 'ovs',
'sections': {},
}
)
def test_overrides(self):
self.ctxt_check(
{
'neutron-plugin': 'ovs',
'core-plugin': 'neutron.plugins.ml2.plugin.MidoPlumODL',
'neutron-plugin-config': '/etc/neutron/plugins/fl/flump.ini',
'service-plugins': 'router,unicorn,rainbows',
'restart-trigger': 'restartnow',
},
{
'core_plugin': 'neutron.plugins.ml2.plugin.MidoPlumODL',
'neutron_plugin_config': '/etc/neutron/plugins/fl/flump.ini',
'service_plugins': 'router,unicorn,rainbows',
'restart_trigger': 'restartnow',
'neutron_plugin': 'ovs',
'sections': {},
}
)
def test_subordinateconfig(self):
principle_config = {
"neutron-api": {
"/etc/neutron/neutron.conf": {
"sections": {
'DEFAULT': [
('neutronboost', True)
],
}
}
}
}
self.ctxt_check(
{
'neutron-plugin': 'ovs',
'subordinate_configuration': json.dumps(principle_config),
},
{
'core_plugin': 'neutron.plugins.ml2.plugin.Ml2Plugin',
'neutron_plugin_config': ('/etc/neutron/plugins/ml2/'
'ml2_conf.ini'),
'service_plugins': 'router,firewall,lbaas,vpnaas,metering',
'restart_trigger': '',
'neutron_plugin': 'ovs',
'sections': {u'DEFAULT': [[u'neutronboost', True]]},
}
)
def test_empty(self):
self.ctxt_check(
{},
{'sections': {}},
)
class NeutronApiSDNConfigFileContextTest(CharmTestCase):
def setUp(self):
super(NeutronApiSDNConfigFileContextTest, self).setUp(
context, TO_PATCH)
self.relation_get.side_effect = self.test_relation.get
def tearDown(self):
super(NeutronApiSDNConfigFileContextTest, self).tearDown()
def test_configset(self):
self.test_relation.set({
'neutron-plugin-config': '/etc/neutron/superplugin.ini'
})
self.relation_ids.return_value = ['rid2']
self.related_units.return_value = ['unit1']
napisdn_ctxt = context.NeutronApiSDNConfigFileContext()()
self.assertEquals(napisdn_ctxt, {
'config': '/etc/neutron/superplugin.ini'
})
def test_default(self):
self.relation_ids.return_value = []
napisdn_ctxt = context.NeutronApiSDNConfigFileContext()()
self.assertEquals(napisdn_ctxt, {
'config': '/etc/neutron/plugins/ml2/ml2_conf.ini'
})

View File

@ -81,7 +81,7 @@ def _mock_nuage_npa(plugin, attr, net_manager=None):
'services': [], 'services': [],
'packages': [], 'packages': [],
'server_packages': ['neutron-server', 'server_packages': ['neutron-server',
'python-neutron-plugin-nuage'], 'neutron-plugin-nuage'],
'server_services': ['neutron-server'] 'server_services': ['neutron-server']
}, },
} }

View File

@ -65,7 +65,7 @@ def _mock_npa(plugin, attr, net_manager=None):
'services': [], 'services': [],
'packages': [], 'packages': [],
'server_packages': ['neutron-server', 'server_packages': ['neutron-server',
'python-neutron-plugin-nuage'], 'neutron-plugin-nuage'],
'server_services': ['neutron-server'] 'server_services': ['neutron-server']
}, },
} }
@ -111,8 +111,8 @@ class TestNeutronAPIUtils(CharmTestCase):
self.get_os_codename_install_source.return_value = 'juno' self.get_os_codename_install_source.return_value = 'juno'
pkg_list = nutils.determine_packages() pkg_list = nutils.determine_packages()
expect = deepcopy(nutils.BASE_PACKAGES) expect = deepcopy(nutils.BASE_PACKAGES)
expect.extend(['neutron-server', 'python-neutron-plugin-nuage', expect.extend(['neutron-server', 'neutron-plugin-nuage',
'python-nuagenetlib']) 'python-nuagenetlib', 'nuage-neutron'])
self.assertItemsEqual(pkg_list, expect) self.assertItemsEqual(pkg_list, expect)
@patch.object(nutils, 'git_install_requested') @patch.object(nutils, 'git_install_requested')
@ -125,28 +125,57 @@ class TestNeutronAPIUtils(CharmTestCase):
expect.extend(nutils.KILO_PACKAGES) expect.extend(nutils.KILO_PACKAGES)
self.assertItemsEqual(pkg_list, expect) self.assertItemsEqual(pkg_list, expect)
@patch.object(nutils, 'git_install_requested')
def test_determine_packages_noplugin(self, git_requested):
git_requested.return_value = False
self.test_config.set('manage-neutron-plugin-legacy-mode', False)
pkg_list = nutils.determine_packages()
expect = deepcopy(nutils.BASE_PACKAGES)
expect.extend(['neutron-server'])
self.assertItemsEqual(pkg_list, expect)
def test_determine_ports(self): def test_determine_ports(self):
port_list = nutils.determine_ports() port_list = nutils.determine_ports()
self.assertItemsEqual(port_list, [9696]) self.assertItemsEqual(port_list, [9696])
@patch.object(nutils, 'manage_plugin')
@patch('os.path.exists') @patch('os.path.exists')
def test_resource_map(self, _path_exists): def test_resource_map(self, _path_exists, _manage_plugin):
_path_exists.return_value = False _path_exists.return_value = False
_manage_plugin.return_value = True
_map = nutils.resource_map() _map = nutils.resource_map()
confs = [nutils.NEUTRON_CONF, nutils.NEUTRON_DEFAULT, confs = [nutils.NEUTRON_CONF, nutils.NEUTRON_DEFAULT,
nutils.APACHE_CONF] nutils.APACHE_CONF]
[self.assertIn(q_conf, _map.keys()) for q_conf in confs] [self.assertIn(q_conf, _map.keys()) for q_conf in confs]
self.assertTrue(nutils.APACHE_24_CONF not in _map.keys()) self.assertTrue(nutils.APACHE_24_CONF not in _map.keys())
@patch.object(nutils, 'manage_plugin')
@patch('os.path.exists') @patch('os.path.exists')
def test_resource_map_apache24(self, _path_exists): def test_resource_map_apache24(self, _path_exists, _manage_plugin):
_path_exists.return_value = True _path_exists.return_value = True
_manage_plugin.return_value = True
_map = nutils.resource_map() _map = nutils.resource_map()
confs = [nutils.NEUTRON_CONF, nutils.NEUTRON_DEFAULT, confs = [nutils.NEUTRON_CONF, nutils.NEUTRON_DEFAULT,
nutils.APACHE_24_CONF] nutils.APACHE_24_CONF]
[self.assertIn(q_conf, _map.keys()) for q_conf in confs] [self.assertIn(q_conf, _map.keys()) for q_conf in confs]
self.assertTrue(nutils.APACHE_CONF not in _map.keys()) self.assertTrue(nutils.APACHE_CONF not in _map.keys())
@patch.object(nutils, 'manage_plugin')
@patch('os.path.exists')
def test_resource_map_noplugin(self, _path_exists, _manage_plugin):
_path_exists.return_value = True
_manage_plugin.return_value = False
_map = nutils.resource_map()
found_sdn_ctxt = False
found_sdnconfig_ctxt = False
for ctxt in _map[nutils.NEUTRON_CONF]['contexts']:
if isinstance(ctxt, ncontext.NeutronApiSDNContext):
found_sdn_ctxt = True
for ctxt in _map[nutils.NEUTRON_DEFAULT]['contexts']:
if isinstance(ctxt, ncontext.NeutronApiSDNConfigFileContext):
found_sdnconfig_ctxt = True
self.assertTrue(found_sdn_ctxt and found_sdnconfig_ctxt)
@patch('os.path.exists') @patch('os.path.exists')
def test_restart_map(self, mock_path_exists): def test_restart_map(self, mock_path_exists):
mock_path_exists.return_value = False mock_path_exists.return_value = False
@ -541,3 +570,13 @@ class TestNeutronAPIUtils(CharmTestCase):
'upgrade', 'upgrade',
'head'] 'head']
self.subprocess.check_output.assert_called_with(cmd) self.subprocess.check_output.assert_called_with(cmd)
def test_manage_plugin_true(self):
self.test_config.set('manage-neutron-plugin-legacy-mode', True)
manage = nutils.manage_plugin()
self.assertTrue(manage)
def test_manage_plugin_false(self):
self.test_config.set('manage-neutron-plugin-legacy-mode', False)
manage = nutils.manage_plugin()
self.assertFalse(manage)