From 2e2935412afb8c6e815a04a6e6dfca7844ea0aae Mon Sep 17 00:00:00 2001 From: Bilal Baqar Date: Tue, 19 May 2015 14:07:07 -0700 Subject: [PATCH] PLUMgrid gateway initial charm --- .coverage | Bin 0 -> 7655 bytes .project | 17 ++ .pydevproject | 9 + Makefile | 24 ++ README.md | 16 ++ bin/charm_helpers_sync.py | 253 +++++++++++++++++ charm-helpers-sync.yaml | 12 + config.yaml | 5 + copyright | 9 + hooks/config-changed | 1 + hooks/install | 1 + hooks/neutron-plugin-api-relation-broken | 1 + hooks/neutron-plugin-api-relation-changed | 1 + hooks/neutron-plugin-api-relation-departed | 1 + hooks/neutron-plugin-api-relation-joined | 1 + hooks/pg_gw_context.py | 93 +++++++ hooks/pg_gw_hooks.py | 47 ++++ hooks/pg_gw_utils.py | 128 +++++++++ hooks/plumgrid-plugin-relation-broken | 1 + hooks/plumgrid-plugin-relation-changed | 1 + hooks/plumgrid-plugin-relation-departed | 1 + hooks/plumgrid-plugin-relation-joined | 1 + hooks/plumgrid-relation-broken | 1 + hooks/plumgrid-relation-changed | 1 + hooks/plumgrid-relation-departed | 1 + hooks/plumgrid-relation-joined | 1 + hooks/stop | 1 + hooks/upgrade-charm | 1 + icon.svg | 304 +++++++++++++++++++++ metadata.yaml | 24 ++ setup.cfg | 5 + templates/icehouse/hostname | 2 + templates/icehouse/hosts | 10 + templates/icehouse/ifcs.conf | 6 + templates/icehouse/network.filters | 94 +++++++ templates/icehouse/plumgrid.conf | 11 + templates/parts/rabbitmq | 21 ++ tests/00-setup | 5 + tests/14-juno | 39 +++ tests/files/plumgrid-gateway.yaml | 98 +++++++ unit_tests/__init__.py | 4 + unit_tests/test_pg_gw_context.py | 82 ++++++ unit_tests/test_pg_gw_hooks.py | 68 +++++ unit_tests/test_pg_gw_utils.py | 79 ++++++ unit_tests/test_utils.py | 121 ++++++++ 45 files changed, 1602 insertions(+) create mode 100644 .coverage create mode 100644 .project create mode 100644 .pydevproject create mode 100644 Makefile create mode 100644 README.md create mode 100644 bin/charm_helpers_sync.py create mode 100644 charm-helpers-sync.yaml create mode 100644 config.yaml create mode 100644 copyright create mode 120000 hooks/config-changed create mode 120000 hooks/install create mode 120000 hooks/neutron-plugin-api-relation-broken create mode 120000 hooks/neutron-plugin-api-relation-changed create mode 120000 hooks/neutron-plugin-api-relation-departed create mode 120000 hooks/neutron-plugin-api-relation-joined create mode 100644 hooks/pg_gw_context.py create mode 100755 hooks/pg_gw_hooks.py create mode 100644 hooks/pg_gw_utils.py create mode 120000 hooks/plumgrid-plugin-relation-broken create mode 120000 hooks/plumgrid-plugin-relation-changed create mode 120000 hooks/plumgrid-plugin-relation-departed create mode 120000 hooks/plumgrid-plugin-relation-joined create mode 120000 hooks/plumgrid-relation-broken create mode 120000 hooks/plumgrid-relation-changed create mode 120000 hooks/plumgrid-relation-departed create mode 120000 hooks/plumgrid-relation-joined create mode 120000 hooks/stop create mode 120000 hooks/upgrade-charm create mode 100644 icon.svg create mode 100644 metadata.yaml create mode 100644 setup.cfg create mode 100644 templates/icehouse/hostname create mode 100644 templates/icehouse/hosts create mode 100644 templates/icehouse/ifcs.conf create mode 100644 templates/icehouse/network.filters create mode 100644 templates/icehouse/plumgrid.conf create mode 100644 templates/parts/rabbitmq create mode 100755 tests/00-setup create mode 100755 tests/14-juno create mode 100644 tests/files/plumgrid-gateway.yaml create mode 100644 unit_tests/__init__.py create mode 100644 unit_tests/test_pg_gw_context.py create mode 100644 unit_tests/test_pg_gw_hooks.py create mode 100644 unit_tests/test_pg_gw_utils.py create mode 100644 unit_tests/test_utils.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..122a4c4975e9456421db3ec565d710469912857d GIT binary patch literal 7655 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 literal 0 HcmV?d00001 diff --git a/.project b/.project new file mode 100644 index 0000000..a146609 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + neutron-openvswitch + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..d6fbb94 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,9 @@ + + +python 2.7 +Default + +/neutron-openvswitch/hooks +/neutron-openvswitch/unit_tests + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..06a02f1 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +#!/usr/bin/make +PYTHON := /usr/bin/env python + +lint: + @flake8 --exclude hooks/charmhelpers hooks + @flake8 --exclude hooks/charmhelpers unit_tests + @charm proof + +unit_test: + @echo Starting tests... + @$(PYTHON) /usr/bin/nosetests --nologcapture unit_tests + +bin/charm_helpers_sync.py: + @mkdir -p bin + @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \ + > bin/charm_helpers_sync.py + +sync: bin/charm_helpers_sync.py + @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-sync.yaml + patch -p0 < ~/profsvcs/canonical/charms/charmhelpers.patch + +publish: lint unit_test + bzr push lp:charms/plumgrid-gateway + bzr push lp:charms/trusty/plumgrid-gateway diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca83cb5 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Overview + +This charm provides the PLUMgrid Gateway configuration for a node. + + +# Usage + +To deploy (partial deployment of linked charms only): + + juju deploy neutron-api + juju deploy neutron-iovisor + juju deploy plumgrid-director + juju deploy plumgrid-gateway + juju add-relation plumgrid-gateway neutron-iovisor + juju add-relation plumgrid-gateway plumgrid-director + diff --git a/bin/charm_helpers_sync.py b/bin/charm_helpers_sync.py new file mode 100644 index 0000000..f67fdb9 --- /dev/null +++ b/bin/charm_helpers_sync.py @@ -0,0 +1,253 @@ +#!/usr/bin/python + +# 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 . + +# Authors: +# Adam Gandelman + +import logging +import optparse +import os +import subprocess +import shutil +import sys +import tempfile +import yaml +from fnmatch import fnmatch + +import six + +CHARM_HELPERS_BRANCH = 'lp:charm-helpers' + + +def parse_config(conf_file): + if not os.path.isfile(conf_file): + logging.error('Invalid config file: %s.' % conf_file) + return False + return yaml.load(open(conf_file).read()) + + +def clone_helpers(work_dir, branch): + dest = os.path.join(work_dir, 'charm-helpers') + logging.info('Checking out %s to %s.' % (branch, dest)) + cmd = ['bzr', 'checkout', '--lightweight', branch, dest] + subprocess.check_call(cmd) + return dest + + +def _module_path(module): + return os.path.join(*module.split('.')) + + +def _src_path(src, module): + return os.path.join(src, 'charmhelpers', _module_path(module)) + + +def _dest_path(dest, module): + return os.path.join(dest, _module_path(module)) + + +def _is_pyfile(path): + return os.path.isfile(path + '.py') + + +def ensure_init(path): + ''' + ensure directories leading up to path are importable, omitting + parent directory, eg path='/hooks/helpers/foo'/: + hooks/ + hooks/helpers/__init__.py + hooks/helpers/foo/__init__.py + ''' + for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])): + _i = os.path.join(d, '__init__.py') + if not os.path.exists(_i): + logging.info('Adding missing __init__.py: %s' % _i) + open(_i, 'wb').close() + + +def sync_pyfile(src, dest): + src = src + '.py' + src_dir = os.path.dirname(src) + logging.info('Syncing pyfile: %s -> %s.' % (src, dest)) + if not os.path.exists(dest): + os.makedirs(dest) + shutil.copy(src, dest) + if os.path.isfile(os.path.join(src_dir, '__init__.py')): + shutil.copy(os.path.join(src_dir, '__init__.py'), + dest) + ensure_init(dest) + + +def get_filter(opts=None): + opts = opts or [] + if 'inc=*' in opts: + # do not filter any files, include everything + return None + + def _filter(dir, ls): + incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt] + _filter = [] + for f in ls: + _f = os.path.join(dir, f) + + if not os.path.isdir(_f) and not _f.endswith('.py') and incs: + if True not in [fnmatch(_f, inc) for inc in incs]: + logging.debug('Not syncing %s, does not match include ' + 'filters (%s)' % (_f, incs)) + _filter.append(f) + else: + logging.debug('Including file, which matches include ' + 'filters (%s): %s' % (incs, _f)) + elif (os.path.isfile(_f) and not _f.endswith('.py')): + logging.debug('Not syncing file: %s' % f) + _filter.append(f) + elif (os.path.isdir(_f) and not + os.path.isfile(os.path.join(_f, '__init__.py'))): + logging.debug('Not syncing directory: %s' % f) + _filter.append(f) + return _filter + return _filter + + +def sync_directory(src, dest, opts=None): + if os.path.exists(dest): + logging.debug('Removing existing directory: %s' % dest) + shutil.rmtree(dest) + logging.info('Syncing directory: %s -> %s.' % (src, dest)) + + shutil.copytree(src, dest, ignore=get_filter(opts)) + ensure_init(dest) + + +def sync(src, dest, module, opts=None): + + # Sync charmhelpers/__init__.py for bootstrap code. + sync_pyfile(_src_path(src, '__init__'), dest) + + # Sync other __init__.py files in the path leading to module. + m = [] + steps = module.split('.')[:-1] + while steps: + m.append(steps.pop(0)) + init = '.'.join(m + ['__init__']) + sync_pyfile(_src_path(src, init), + os.path.dirname(_dest_path(dest, init))) + + # Sync the module, or maybe a .py file. + if os.path.isdir(_src_path(src, module)): + sync_directory(_src_path(src, module), _dest_path(dest, module), opts) + elif _is_pyfile(_src_path(src, module)): + sync_pyfile(_src_path(src, module), + os.path.dirname(_dest_path(dest, module))) + else: + logging.warn('Could not sync: %s. Neither a pyfile or directory, ' + 'does it even exist?' % module) + + +def parse_sync_options(options): + if not options: + return [] + return options.split(',') + + +def extract_options(inc, global_options=None): + global_options = global_options or [] + if global_options and isinstance(global_options, six.string_types): + global_options = [global_options] + if '|' not in inc: + return (inc, global_options) + inc, opts = inc.split('|') + return (inc, parse_sync_options(opts) + global_options) + + +def sync_helpers(include, src, dest, options=None): + if not os.path.isdir(dest): + os.makedirs(dest) + + global_options = parse_sync_options(options) + + for inc in include: + if isinstance(inc, str): + inc, opts = extract_options(inc, global_options) + sync(src, dest, inc, opts) + elif isinstance(inc, dict): + # could also do nested dicts here. + for k, v in six.iteritems(inc): + if isinstance(v, list): + for m in v: + inc, opts = extract_options(m, global_options) + sync(src, dest, '%s.%s' % (k, inc), opts) + +if __name__ == '__main__': + parser = optparse.OptionParser() + parser.add_option('-c', '--config', action='store', dest='config', + default=None, help='helper config file') + parser.add_option('-D', '--debug', action='store_true', dest='debug', + default=False, help='debug') + parser.add_option('-b', '--branch', action='store', dest='branch', + help='charm-helpers bzr branch (overrides config)') + parser.add_option('-d', '--destination', action='store', dest='dest_dir', + help='sync destination dir (overrides config)') + (opts, args) = parser.parse_args() + + if opts.debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + if opts.config: + logging.info('Loading charm helper config from %s.' % opts.config) + config = parse_config(opts.config) + if not config: + logging.error('Could not parse config from %s.' % opts.config) + sys.exit(1) + else: + config = {} + + if 'branch' not in config: + config['branch'] = CHARM_HELPERS_BRANCH + if opts.branch: + config['branch'] = opts.branch + if opts.dest_dir: + config['destination'] = opts.dest_dir + + if 'destination' not in config: + logging.error('No destination dir. specified as option or config.') + sys.exit(1) + + if 'include' not in config: + if not args: + logging.error('No modules to sync specified as option or config.') + sys.exit(1) + config['include'] = [] + [config['include'].append(a) for a in args] + + sync_options = None + if 'options' in config: + sync_options = config['options'] + tmpd = tempfile.mkdtemp() + try: + checkout = clone_helpers(tmpd, config['branch']) + sync_helpers(config['include'], checkout, config['destination'], + options=sync_options) + except Exception as e: + logging.error("Could not sync: %s" % e) + raise e + finally: + logging.debug('Cleaning up %s' % tmpd) + shutil.rmtree(tmpd) diff --git a/charm-helpers-sync.yaml b/charm-helpers-sync.yaml new file mode 100644 index 0000000..9b5e79e --- /dev/null +++ b/charm-helpers-sync.yaml @@ -0,0 +1,12 @@ +branch: lp:charm-helpers +destination: hooks/charmhelpers +include: + - core + - fetch + - contrib.openstack|inc=* + - contrib.hahelpers + - contrib.network.ovs + - contrib.storage.linux + - payload.execd + - contrib.network.ip + - contrib.python.packages diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..ae581a2 --- /dev/null +++ b/config.yaml @@ -0,0 +1,5 @@ +options: + external-interface: + default: eth1 + type: string + description: The interface that will provide external connectivity diff --git a/copyright b/copyright new file mode 100644 index 0000000..d44f24c --- /dev/null +++ b/copyright @@ -0,0 +1,9 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0 + +Files: * +Copyright: 2012, Canonical Ltd. +License: GPL-3 + +License: GPL-3 + On Debian GNU/Linux system you can find the complete text of the + GPL-3 license in '/usr/share/common-licenses/GPL-3' diff --git a/hooks/config-changed b/hooks/config-changed new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/config-changed @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/install b/hooks/install new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/install @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/neutron-plugin-api-relation-broken b/hooks/neutron-plugin-api-relation-broken new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/neutron-plugin-api-relation-broken @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/neutron-plugin-api-relation-changed b/hooks/neutron-plugin-api-relation-changed new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/neutron-plugin-api-relation-changed @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/neutron-plugin-api-relation-departed b/hooks/neutron-plugin-api-relation-departed new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/neutron-plugin-api-relation-departed @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/neutron-plugin-api-relation-joined b/hooks/neutron-plugin-api-relation-joined new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/neutron-plugin-api-relation-joined @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/pg_gw_context.py b/hooks/pg_gw_context.py new file mode 100644 index 0000000..b42a90e --- /dev/null +++ b/hooks/pg_gw_context.py @@ -0,0 +1,93 @@ +from charmhelpers.core.hookenv import ( + relation_ids, + related_units, + relation_get, + config, +) +from charmhelpers.contrib.openstack import context + +from socket import gethostname as get_unit_hostname + +''' +#This function will be used to get information from neutron-api +def _neutron_api_settings(): + neutron_settings = { + 'neutron_security_groups': False, + 'l2_population': True, + 'overlay_network_type': 'gre', + } + for rid in relation_ids('neutron-plugin-api'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + if 'l2-population' not in rdata: + continue + neutron_settings = { + 'l2_population': rdata['l2-population'], + 'neutron_security_groups': rdata['neutron-security-groups'], + 'overlay_network_type': rdata['overlay-network-type'], + } + # Override with configuration if set to true + if config('disable-security-groups'): + neutron_settings['neutron_security_groups'] = False + return neutron_settings + return neutron_settings +''' + + +def _pg_dir_settings(): + ''' + Inspects current neutron-plugin relation + ''' + pg_settings = { + 'pg_dir_ip': '192.168.100.201', + } + for rid in relation_ids('plumgrid'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + pg_settings = { + 'pg_dir_ip': rdata['private-address'], + } + return pg_settings + + +class PGGwContext(context.NeutronContext): + interfaces = [] + + @property + def plugin(self): + return 'plumgrid' + + @property + def network_manager(self): + return 'neutron' + + def _save_flag_file(self): + pass + + #@property + #def neutron_security_groups(self): + # neutron_api_settings = _neutron_api_settings() + # return neutron_api_settings['neutron_security_groups'] + + def pg_ctxt(self): + #Generated Config for all Plumgrid templates inside + #the templates folder + pg_ctxt = super(PGGwContext, self).pg_ctxt() + if not pg_ctxt: + return {} + + conf = config() + pg_dir_settings = _pg_dir_settings() + pg_ctxt['local_ip'] = pg_dir_settings['pg_dir_ip'] + + #neutron_api_settings = _neutron_api_settings() + #TODO: Either get this value from the director or neutron-api charm + unit_hostname = get_unit_hostname() + pg_ctxt['pg_hostname'] = unit_hostname + pg_ctxt['interface'] = "juju-br0" + pg_ctxt['label'] = unit_hostname + pg_ctxt['fabric_mode'] = 'host' + + pg_ctxt['ext_interface'] = conf['external-interface'] + + return pg_ctxt diff --git a/hooks/pg_gw_hooks.py b/hooks/pg_gw_hooks.py new file mode 100755 index 0000000..821c54c --- /dev/null +++ b/hooks/pg_gw_hooks.py @@ -0,0 +1,47 @@ +#!/usr/bin/python + +import sys + +from charmhelpers.core.hookenv import ( + Hooks, + UnregisteredHookError, + log, +) + +from pg_gw_utils import ( + register_configs, + ensure_files, + restart_pg, + stop_pg, +) + +hooks = Hooks() +CONFIGS = register_configs() + + +@hooks.hook() +def install(): + ensure_files() + + +@hooks.hook('plumgrid-plugin-relation-joined') +def plumgrid_dir(): + ensure_files() + CONFIGS.write_all() + restart_pg() + + +@hooks.hook('stop') +def stop(): + stop_pg() + + +def main(): + try: + hooks.execute(sys.argv) + except UnregisteredHookError as e: + log('Unknown hook {} - skipping.'.format(e)) + + +if __name__ == '__main__': + main() diff --git a/hooks/pg_gw_utils.py b/hooks/pg_gw_utils.py new file mode 100644 index 0000000..3668cc0 --- /dev/null +++ b/hooks/pg_gw_utils.py @@ -0,0 +1,128 @@ +from charmhelpers.contrib.openstack.neutron import neutron_plugin_attribute +from copy import deepcopy +from charmhelpers.core.hookenv import log +from charmhelpers.core.host import ( + write_file, +) +from charmhelpers.contrib.openstack import templating +from collections import OrderedDict +from charmhelpers.contrib.openstack.utils import ( + os_release, +) +import pg_gw_context +import subprocess +import time + +#Dont need these right now +NOVA_CONF_DIR = "/etc/nova" +NEUTRON_CONF_DIR = "/etc/neutron" +NEUTRON_CONF = '%s/neutron.conf' % NEUTRON_CONF_DIR +NEUTRON_DEFAULT = '/etc/default/neutron-server' + +#Puppet Files +P_PGKA_CONF = '/opt/pg/etc/puppet/modules/sal/templates/keepalived.conf.erb' +P_PG_CONF = '/opt/pg/etc/puppet/modules/plumgrid/templates/plumgrid.conf.erb' +P_PGDEF_CONF = '/opt/pg/etc/puppet/modules/sal/templates/default.conf.erb' + +#Plumgrid Files +PGKA_CONF = '/var/lib/libvirt/filesystems/plumgrid/etc/keepalived/keepalived.conf' +PG_CONF = '/var/lib/libvirt/filesystems/plumgrid/opt/pg/etc/plumgrid.conf' +PGDEF_CONF = '/var/lib/libvirt/filesystems/plumgrid/opt/pg/sal/nginx/conf.d/default.conf' +PGHN_CONF = '/var/lib/libvirt/filesystems/plumgrid-data/conf/etc/hostname' +PGHS_CONF = '/var/lib/libvirt/filesystems/plumgrid-data/conf/etc/hosts' +PGIFCS_CONF = '/var/lib/libvirt/filesystems/plumgrid-data/conf/pg/ifcs.conf' +IFCTL_CONF = '/var/run/plumgrid/lxc/ifc_list_gateway' +IFCTL_P_CONF = '/var/lib/libvirt/filesystems/plumgrid/var/run/plumgrid/lxc/ifc_list_gateway' + +#EDGE SPECIFIC +SUDOERS_CONF = '/etc/sudoers.d/ifc_ctl_sudoers' +FILTERS_CONF_DIR = '/etc/nova/rootwrap.d' +FILTERS_CONF = '%s/network.filters' % FILTERS_CONF_DIR + +BASE_RESOURCE_MAP = OrderedDict([ + (PG_CONF, { + 'services': ['plumgrid'], + 'contexts': [pg_gw_context.PGGwContext()], + }), + (PGHN_CONF, { + 'services': ['plumgrid'], + 'contexts': [pg_gw_context.PGGwContext()], + }), + (PGHS_CONF, { + 'services': ['plumgrid'], + 'contexts': [pg_gw_context.PGGwContext()], + }), + (PGIFCS_CONF, { + 'services': [], + 'contexts': [pg_gw_context.PGGwContext()], + }), + (FILTERS_CONF, { + 'services': [], + 'contexts': [pg_gw_context.PGGwContext()], + }), +]) + +TEMPLATES = 'templates/' + + +def determine_packages(): + return neutron_plugin_attribute('plumgrid', 'packages', 'neutron') + + +def register_configs(release=None): + release = release or os_release('neutron-common', base='icehouse') + configs = templating.OSConfigRenderer(templates_dir=TEMPLATES, + openstack_release=release) + for cfg, rscs in resource_map().iteritems(): + configs.register(cfg, rscs['contexts']) + return configs + + +def resource_map(): + ''' + Dynamically generate a map of resources that will be managed for a single + hook execution. + ''' + resource_map = deepcopy(BASE_RESOURCE_MAP) + return resource_map + + +def restart_map(): + ''' + Constructs a restart map based on charm config settings and relation + state. + ''' + return {k: v['services'] for k, v in resource_map().iteritems()} + + +def ensure_files(): + _exec_cmd(cmd=['cp', '--remove-destination', '-f', P_PG_CONF, PG_CONF]) + write_file(SUDOERS_CONF, "\nnova ALL=(root) NOPASSWD: /opt/pg/bin/ifc_ctl_pp *\n", owner='root', group='root', perms=0o644) + _exec_cmd(cmd=['mkdir', '-p', FILTERS_CONF_DIR]) + _exec_cmd(cmd=['touch', FILTERS_CONF]) + + +def restart_pg(): + _exec_cmd(cmd=['virsh', '-c', 'lxc:', 'destroy', 'plumgrid'], error_msg='ERROR Destroying PLUMgrid') + _exec_cmd(cmd=['rm', IFCTL_CONF, IFCTL_P_CONF], error_msg='ERROR Removing ifc_ctl_gateway file') + _exec_cmd(cmd=['iptables', '-F']) + _exec_cmd(cmd=['virsh', '-c', 'lxc:', 'start', 'plumgrid'], error_msg='ERROR Starting PLUMgrid') + time.sleep(5) + _exec_cmd(cmd=['service', 'plumgrid', 'start'], error_msg='ERROR starting PLUMgrid service') + time.sleep(5) + + +def stop_pg(): + _exec_cmd(cmd=['virsh', '-c', 'lxc:', 'destroy', 'plumgrid'], error_msg='ERROR Destroying PLUMgrid') + time.sleep(2) + _exec_cmd(cmd=['rm', IFCTL_CONF, IFCTL_P_CONF], error_msg='ERROR Removing ifc_ctl_gateway file') + + +def _exec_cmd(cmd=None, error_msg='Command exited with ERRORs'): + if cmd is None: + log("NO command") + else: + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError, e: + log(error_msg) diff --git a/hooks/plumgrid-plugin-relation-broken b/hooks/plumgrid-plugin-relation-broken new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/plumgrid-plugin-relation-broken @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/plumgrid-plugin-relation-changed b/hooks/plumgrid-plugin-relation-changed new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/plumgrid-plugin-relation-changed @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/plumgrid-plugin-relation-departed b/hooks/plumgrid-plugin-relation-departed new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/plumgrid-plugin-relation-departed @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/plumgrid-plugin-relation-joined b/hooks/plumgrid-plugin-relation-joined new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/plumgrid-plugin-relation-joined @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/plumgrid-relation-broken b/hooks/plumgrid-relation-broken new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/plumgrid-relation-broken @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/plumgrid-relation-changed b/hooks/plumgrid-relation-changed new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/plumgrid-relation-changed @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/plumgrid-relation-departed b/hooks/plumgrid-relation-departed new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/plumgrid-relation-departed @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/plumgrid-relation-joined b/hooks/plumgrid-relation-joined new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/plumgrid-relation-joined @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/stop b/hooks/stop new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/stop @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/hooks/upgrade-charm b/hooks/upgrade-charm new file mode 120000 index 0000000..3aec9ba --- /dev/null +++ b/hooks/upgrade-charm @@ -0,0 +1 @@ +pg_gw_hooks.py \ No newline at end of file diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..44e925a --- /dev/null +++ b/icon.svg @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/metadata.yaml b/metadata.yaml new file mode 100644 index 0000000..ceed1b4 --- /dev/null +++ b/metadata.yaml @@ -0,0 +1,24 @@ +name: plumgrid-gateway +subordinate: false +maintainer: Bilal Baqar +summary: "OpenStack Neutron OpenvSwitch Agent" +description: | + Neutron is a virtual network service for Openstack, and a part of + Netstack. Just like OpenStack Nova provides an API to dynamically + request and configure virtual servers, Neutron provides an API to + dynamically request and configure virtual networks. These networks + connect "interfaces" from other OpenStack services (e.g., virtual NICs + from Nova VMs). The Neutron API supports extensions to provide + advanced network capabilities (e.g., QoS, ACLs, network monitoring, + etc.) + . + This charm provides the Plumgrid Gateway +tags: + - openstack +requires: + plumgrid-plugin: + interface: plumgrid-plugin + plumgrid: + interface: plumgrid + neutron-plugin-api: + interface: neutron-plugin-api diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..72c20b2 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[nosetests] +verbosity=1 +with-coverage=1 +cover-erase=1 +cover-package=hooks diff --git a/templates/icehouse/hostname b/templates/icehouse/hostname new file mode 100644 index 0000000..1712a7b --- /dev/null +++ b/templates/icehouse/hostname @@ -0,0 +1,2 @@ +{{ pg_hostname }} + diff --git a/templates/icehouse/hosts b/templates/icehouse/hosts new file mode 100644 index 0000000..99e3be5 --- /dev/null +++ b/templates/icehouse/hosts @@ -0,0 +1,10 @@ +127.0.0.1 localhost +127.0.1.1 {{ pg_hostname }} + +# The following lines are desirable for IPv6 capable hosts +::1 ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + diff --git a/templates/icehouse/ifcs.conf b/templates/icehouse/ifcs.conf new file mode 100644 index 0000000..2c5794e --- /dev/null +++ b/templates/icehouse/ifcs.conf @@ -0,0 +1,6 @@ +{{ interface }} = fabric_core host +{% if ext_interface -%} +{{ ext_interface }} = access_phys + +{% endif -%} + diff --git a/templates/icehouse/network.filters b/templates/icehouse/network.filters new file mode 100644 index 0000000..568e8d4 --- /dev/null +++ b/templates/icehouse/network.filters @@ -0,0 +1,94 @@ +# nova-rootwrap command filters for network nodes +# This file should be owned by (and only-writeable by) the root user + +[Filters] +# nova/virt/libvirt/vif.py: 'ip', 'tuntap', 'add', dev, 'mode', 'tap' +# nova/virt/libvirt/vif.py: 'ip', 'link', 'set', dev, 'up' +# nova/virt/libvirt/vif.py: 'ip', 'link', 'delete', dev +# nova/network/linux_net.py: 'ip', 'addr', 'add', str(floating_ip)+'/32'i.. +# nova/network/linux_net.py: 'ip', 'addr', 'del', str(floating_ip)+'/32'.. +# nova/network/linux_net.py: 'ip', 'addr', 'add', '169.254.169.254/32',.. +# nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', dev, 'scope',.. +# nova/network/linux_net.py: 'ip', 'addr', 'del/add', ip_params, dev) +# nova/network/linux_net.py: 'ip', 'addr', 'del', params, fields[-1] +# nova/network/linux_net.py: 'ip', 'addr', 'add', params, bridge +# nova/network/linux_net.py: 'ip', '-f', 'inet6', 'addr', 'change', .. +# nova/network/linux_net.py: 'ip', 'link', 'set', 'dev', dev, 'promisc',.. +# nova/network/linux_net.py: 'ip', 'link', 'add', 'link', bridge_if ... +# nova/network/linux_net.py: 'ip', 'link', 'set', interface, address,.. +# nova/network/linux_net.py: 'ip', 'link', 'set', interface, 'up' +# nova/network/linux_net.py: 'ip', 'link', 'set', bridge, 'up' +# nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', interface, .. +# nova/network/linux_net.py: 'ip', 'link', 'set', dev, address, .. +# nova/network/linux_net.py: 'ip', 'link', 'set', dev, 'up' +# nova/network/linux_net.py: 'ip', 'route', 'add', .. +# nova/network/linux_net.py: 'ip', 'route', 'del', . +# nova/network/linux_net.py: 'ip', 'route', 'show', 'dev', dev +ip: CommandFilter, ip, root + +# nova/virt/libvirt/vif.py: 'ovs-vsctl', ... +# nova/virt/libvirt/vif.py: 'ovs-vsctl', 'del-port', ... +# nova/network/linux_net.py: 'ovs-vsctl', .... +ovs-vsctl: CommandFilter, ovs-vsctl, root + +# nova/network/linux_net.py: 'ovs-ofctl', .... +ovs-ofctl: CommandFilter, ovs-ofctl, root + +# nova/virt/libvirt/vif.py: 'ivs-ctl', ... +# nova/virt/libvirt/vif.py: 'ivs-ctl', 'del-port', ... +# nova/network/linux_net.py: 'ivs-ctl', .... +ivs-ctl: CommandFilter, ivs-ctl, root + +# nova/virt/libvirt/vif.py: 'ifc_ctl', ... +ifc_ctl: CommandFilter, /opt/pg/bin/ifc_ctl, root + +# nova/virt/libvirt/vif.py: 'ebrctl', ... +ebrctl: CommandFilter, ebrctl, root + +# nova/virt/libvirt/vif.py: 'mm-ctl', ... +mm-ctl: CommandFilter, mm-ctl, root + +# nova/network/linux_net.py: 'ebtables', '-D' ... +# nova/network/linux_net.py: 'ebtables', '-I' ... +ebtables: CommandFilter, ebtables, root +ebtables_usr: CommandFilter, ebtables, root + +# nova/network/linux_net.py: 'ip[6]tables-save' % (cmd, '-t', ... +iptables-save: CommandFilter, iptables-save, root +ip6tables-save: CommandFilter, ip6tables-save, root + +# nova/network/linux_net.py: 'ip[6]tables-restore' % (cmd,) +iptables-restore: CommandFilter, iptables-restore, root +ip6tables-restore: CommandFilter, ip6tables-restore, root + +# nova/network/linux_net.py: 'arping', '-U', floating_ip, '-A', '-I', ... +# nova/network/linux_net.py: 'arping', '-U', network_ref['dhcp_server'],.. +arping: CommandFilter, arping, root + +# nova/network/linux_net.py: 'dhcp_release', dev, address, mac_address +dhcp_release: CommandFilter, dhcp_release, root + +# nova/network/linux_net.py: 'kill', '-9', pid +# nova/network/linux_net.py: 'kill', '-HUP', pid +kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP + +# nova/network/linux_net.py: 'kill', pid +kill_radvd: KillFilter, root, /usr/sbin/radvd + +# nova/network/linux_net.py: dnsmasq call +dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq + +# nova/network/linux_net.py: 'radvd', '-C', '%s' % _ra_file(dev, 'conf'.. +radvd: CommandFilter, radvd, root + +# nova/network/linux_net.py: 'brctl', 'addbr', bridge +# nova/network/linux_net.py: 'brctl', 'setfd', bridge, 0 +# nova/network/linux_net.py: 'brctl', 'stp', bridge, 'off' +# nova/network/linux_net.py: 'brctl', 'addif', bridge, interface +brctl: CommandFilter, brctl, root + +# nova/network/linux_net.py: 'sysctl', .... +sysctl: CommandFilter, sysctl, root + +# nova/network/linux_net.py: 'conntrack' +conntrack: CommandFilter, conntrack, root diff --git a/templates/icehouse/plumgrid.conf b/templates/icehouse/plumgrid.conf new file mode 100644 index 0000000..52a9dc8 --- /dev/null +++ b/templates/icehouse/plumgrid.conf @@ -0,0 +1,11 @@ +plumgrid_ip={{ local_ip }} +plumgrid_port=8001 +mgmt_dev={{ interface }} +label={{ label}} +plumgrid_rsync_port=2222 +plumgrid_rest_addr=0.0.0.0:9180 +fabric_mode={{ fabric_mode }} +start_plumgrid_iovisor=yes +start_plumgrid=`/opt/pg/scripts/pg_is_director.sh $plumgrid_ip` +location= + diff --git a/templates/parts/rabbitmq b/templates/parts/rabbitmq new file mode 100644 index 0000000..7aabc51 --- /dev/null +++ b/templates/parts/rabbitmq @@ -0,0 +1,21 @@ +{% if rabbitmq_host or rabbitmq_hosts -%} +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/tests/00-setup b/tests/00-setup new file mode 100755 index 0000000..858fe13 --- /dev/null +++ b/tests/00-setup @@ -0,0 +1,5 @@ +#!/bin/bash + +sudo add-apt-repository ppa:juju/stable -y +sudo apt-get update +sudo apt-get install amulet python3-requests juju-deployer -y diff --git a/tests/14-juno b/tests/14-juno new file mode 100755 index 0000000..f984cd9 --- /dev/null +++ b/tests/14-juno @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +import amulet +import requests +import unittest + +class TestDeployment(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.deployment = amulet.Deployment(series='trusty') + cls.deployment.load_bundle_file(bundle_file='files/plumgrid-gateway.yaml', deployment_name='test') + try: + cls.deployment.setup(timeout=2000) + cls.deployment.sentry.wait() + except amulet.helpers.TimeoutError: + amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time") + except: + raise + + def test_plumgrid_gateway_external_interface(self): + external_interface = self.deployment.services['plumgrid-gateway']['options']['external-interface'] + if not external_interface: + amulet.raise_status(amulet.FAIL, msg='plumgrid external-interface parameter was not found.') + output, code = self.deployment.sentry['plumgrid-gateway/0'].run("ethtool {}".format(external_interface)) + if code != 0: + amulet.raise_status(amulet.FAIL, msg='external interface not found on the host') + + def test_plumgrid_gateway_started(self): + agent_state = self.deployment.sentry['plumgrid-gateway/0'].info['agent-state'] + if agent_state != 'started': + amulet.raise_status(amulet.FAIL, msg='plumgrid gateway is not in a started state') + + def test_plumgrid_gateway_relation(self): + relation = self.deployment.sentry['plumgrid-gateway/0'].relation('plumgrid-plugin', 'neutron-iovisor:plumgrid-plugin') + if not relation['private-address']: + amulet.raise_status(amulet.FAIL, msg='private address was not set in the plumgrid gateway relation') + +if __name__ == '__main__': + unittest.main() diff --git a/tests/files/plumgrid-gateway.yaml b/tests/files/plumgrid-gateway.yaml new file mode 100644 index 0000000..4bb13bd --- /dev/null +++ b/tests/files/plumgrid-gateway.yaml @@ -0,0 +1,98 @@ +test: + series: 'trusty' + relations: + - - neutron-api + - neutron-iovisor + - - neutron-iovisor + - plumgrid-gateway + - - nova-cloud-controller + - nova-compute + - - glance + - nova-compute + - - nova-compute + - rabbitmq-server + - - mysql + - nova-compute + - - cinder + - nova-cloud-controller + - - nova-cloud-controller + - rabbitmq-server + - - glance + - nova-cloud-controller + - - keystone + - nova-cloud-controller + - - mysql + - nova-cloud-controller + - - neutron-api + - nova-cloud-controller + services: + cinder: + charm: cs:trusty/cinder + num_units: 1 + options: + openstack-origin: cloud:trusty-juno + to: 'lxc:0' + glance: + charm: cs:trusty/glance + num_units: 1 + options: + openstack-origin: cloud:trusty-juno + to: 'lxc:0' + keystone: + charm: cs:trusty/keystone + num_units: 1 + options: + admin-password: plumgrid + openstack-origin: cloud:trusty-juno + to: 'lxc:0' + mysql: + charm: cs:trusty/mysql + num_units: 1 + to: 'lxc:0' + neutron-api: + charm: cs:~juliann/trusty/neutron-api + num_units: 1 + options: + install_keys: 'null' + install_sources: "deb http://10.22.24.200/debs ./" + neutron-plugin: "plumgrid" + neutron-security-groups: "true" + openstack-origin: "cloud:trusty-juno" + plumgrid-password: "plumgrid" + plumgrid-username: "plumgrid" + plumgrid-virtual-ip: "192.168.100.250" + to: 'lxc:0' + neutron-iovisor: + charm: cs:~juliann/trusty/neutron-iovisor + num_units: 1 + options: + install_keys: 'null' + install_sources: "deb http://10.22.24.200/debs ./" + to: 'nova-compute' + plumgrid-gateway: + charm: cs:~juliann/trusty/plumgrid-gateway + num_units: 1 + options: + external-interface: 'eth1' + to: 'nova-compute' + nova-cloud-controller: + charm: cs:trusty/nova-cloud-controller + num_units: 1 + options: + console-access-protocol: novnc + network-manager: Neutron + openstack-origin: cloud:trusty-juno + quantum-security-groups: 'yes' + to: 'lxc:0' + nova-compute: + charm: cs:~juliann/trusty/nova-compute + num_units: 1 + options: + enable-live-migration: true + enable-resize: true + migration-auth-type: ssh + openstack-origin: cloud:trusty-juno + rabbitmq-server: + charm: cs:trusty/rabbitmq-server + num_units: 1 + to: 'lxc:0' diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 0000000..43aa361 --- /dev/null +++ b/unit_tests/__init__.py @@ -0,0 +1,4 @@ +import sys + +sys.path.append('actions/') +sys.path.append('hooks/') diff --git a/unit_tests/test_pg_gw_context.py b/unit_tests/test_pg_gw_context.py new file mode 100644 index 0000000..40b4b4a --- /dev/null +++ b/unit_tests/test_pg_gw_context.py @@ -0,0 +1,82 @@ +from test_utils import CharmTestCase +from mock import patch +import pg_gw_context as context +import charmhelpers + +TO_PATCH = [ + #'_pg_dir_settings', + 'config', + 'get_unit_hostname', +] + + +def fake_context(settings): + def outer(): + def inner(): + return settings + return inner + return outer + + +class PGGwContextTest(CharmTestCase): + + def setUp(self): + super(PGGwContextTest, self).setUp(context, TO_PATCH) + self.config.side_effect = self.test_config.get + self.test_config.set('external-interface', 'eth1') + + def tearDown(self): + super(PGGwContextTest, self).tearDown() + + @patch.object(context.PGGwContext, '_ensure_packages') + @patch.object(charmhelpers.contrib.openstack.context, 'https') + @patch.object(charmhelpers.contrib.openstack.context, 'is_clustered') + @patch.object(charmhelpers.contrib.openstack.context, 'config') + @patch.object(charmhelpers.contrib.openstack.context, 'unit_private_ip') + @patch.object(charmhelpers.contrib.openstack.context, 'unit_get') + @patch.object(charmhelpers.contrib.openstack.context, 'config_flags_parser') + @patch.object(context.PGGwContext, '_save_flag_file') + @patch.object(context, '_pg_dir_settings') + @patch.object(charmhelpers.contrib.openstack.context, 'neutron_plugin_attribute') + def test_neutroncc_context_api_rel(self, _npa, _pg_dir_settings, _save_flag_file, + _config_flag, _unit_get, _unit_priv_ip, _config, + _is_clus, _https, _ens_pkgs): + def mock_npa(plugin, section, manager): + if section == "driver": + return "neutron.randomdriver" + if section == "config": + return "neutron.randomconfig" + config = {'external-interface': "eth1"} + + def mock_config(key=None): + if key: + return config.get(key) + + return config + + self.maxDiff = None + self.config.side_effect = mock_config + _npa.side_effect = mock_npa + _unit_get.return_value = '192.168.100.201' + _unit_priv_ip.return_value = '192.168.100.201' + self.get_unit_hostname.return_value = 'node0' + _is_clus.return_value = False + _config_flag.return_value = False + _pg_dir_settings.return_value = {'pg_dir_ip': '192.168.100.201'} + napi_ctxt = context.PGGwContext() + expect = { + 'ext_interface': "eth1", + 'config': 'neutron.randomconfig', + 'core_plugin': 'neutron.randomdriver', + 'local_ip': '192.168.100.201', + 'network_manager': 'neutron', + 'neutron_plugin': 'plumgrid', + 'neutron_security_groups': None, + 'neutron_url': 'https://192.168.100.201:9696', + 'pg_hostname': 'node0', + 'interface': 'juju-br0', + 'label': 'node0', + 'fabric_mode': 'host', + 'neutron_alchemy_flags': False, + } + self.assertEquals(expect, napi_ctxt()) diff --git a/unit_tests/test_pg_gw_hooks.py b/unit_tests/test_pg_gw_hooks.py new file mode 100644 index 0000000..d4cbcf5 --- /dev/null +++ b/unit_tests/test_pg_gw_hooks.py @@ -0,0 +1,68 @@ +from mock import MagicMock, patch + +from test_utils import CharmTestCase + +with patch('charmhelpers.core.hookenv.config') as config: + config.return_value = 'neutron' + import pg_gw_utils as utils + +_reg = utils.register_configs +_map = utils.restart_map + +utils.register_configs = MagicMock() +utils.restart_map = MagicMock() + +import pg_gw_hooks as hooks + +utils.register_configs = _reg +utils.restart_map = _map + +TO_PATCH = [ + #'apt_update', + #'apt_install', + #'apt_purge', + #'config', + 'CONFIGS', + #'determine_packages', + #'determine_dvr_packages', + #'get_shared_secret', + #'git_install', + 'log', + #'relation_ids', + #'relation_set', + #'configure_ovs', + #'use_dvr', + 'ensure_files', + 'stop_pg', + 'restart_pg', +] +NEUTRON_CONF_DIR = "/etc/neutron" + +NEUTRON_CONF = '%s/neutron.conf' % NEUTRON_CONF_DIR + + +class PGGwHooksTests(CharmTestCase): + + def setUp(self): + super(PGGwHooksTests, self).setUp(hooks, TO_PATCH) + + #self.config.side_effect = self.test_config.get + hooks.hooks._config_save = False + + def _call_hook(self, hookname): + hooks.hooks.execute([ + 'hooks/{}'.format(hookname)]) + + def test_install_hook(self): + self._call_hook('install') + self.ensure_files.assert_called_with() + + def test_plumgrid_edge_joined(self): + self._call_hook('plumgrid-plugin-relation-joined') + self.ensure_files.assert_called_with() + self.CONFIGS.write_all.assert_called_with() + self.restart_pg.assert_called_with() + + def test_stop(self): + self._call_hook('stop') + self.stop_pg.assert_called_with() diff --git a/unit_tests/test_pg_gw_utils.py b/unit_tests/test_pg_gw_utils.py new file mode 100644 index 0000000..a9e9cbc --- /dev/null +++ b/unit_tests/test_pg_gw_utils.py @@ -0,0 +1,79 @@ +from mock import MagicMock +from collections import OrderedDict +import charmhelpers.contrib.openstack.templating as templating + +templating.OSConfigRenderer = MagicMock() + +import pg_gw_utils as nutils + +from test_utils import ( + CharmTestCase, +) +import charmhelpers.core.hookenv as hookenv + + +TO_PATCH = [ + 'os_release', + 'neutron_plugin_attribute', +] + + +class DummyContext(): + + def __init__(self, return_value): + self.return_value = return_value + + def __call__(self): + return self.return_value + + +class TestPGGwUtils(CharmTestCase): + + def setUp(self): + super(TestPGGwUtils, self).setUp(nutils, TO_PATCH) + #self.config.side_effect = self.test_config.get + + def tearDown(self): + # Reset cached cache + hookenv.cache = {} + + def test_register_configs(self): + class _mock_OSConfigRenderer(): + def __init__(self, templates_dir=None, openstack_release=None): + self.configs = [] + self.ctxts = [] + + def register(self, config, ctxt): + self.configs.append(config) + self.ctxts.append(ctxt) + + self.os_release.return_value = 'trusty' + templating.OSConfigRenderer.side_effect = _mock_OSConfigRenderer + _regconfs = nutils.register_configs() + confs = ['/var/lib/libvirt/filesystems/plumgrid/opt/pg/etc/plumgrid.conf', + '/var/lib/libvirt/filesystems/plumgrid-data/conf/etc/hostname', + '/var/lib/libvirt/filesystems/plumgrid-data/conf/etc/hosts', + '/var/lib/libvirt/filesystems/plumgrid-data/conf/pg/ifcs.conf', + '/etc/nova/rootwrap.d/network.filters'] + self.assertItemsEqual(_regconfs.configs, confs) + + def test_resource_map(self): + _map = nutils.resource_map() + svcs = ['plumgrid'] + confs = [nutils.PG_CONF] + [self.assertIn(q_conf, _map.keys()) for q_conf in confs] + self.assertEqual(_map[nutils.PG_CONF]['services'], svcs) + + def test_restart_map(self): + _restart_map = nutils.restart_map() + expect = OrderedDict([ + (nutils.PG_CONF, ['plumgrid']), + (nutils.PGHN_CONF, ['plumgrid']), + (nutils.PGHS_CONF, ['plumgrid']), + (nutils.PGIFCS_CONF, []), + (nutils.FILTERS_CONF, []), + ]) + self.assertEqual(expect, _restart_map) + for item in _restart_map: + self.assertTrue(item in _restart_map) + self.assertTrue(expect[item] == _restart_map[item]) diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py new file mode 100644 index 0000000..86ee0f7 --- /dev/null +++ b/unit_tests/test_utils.py @@ -0,0 +1,121 @@ +import logging +import unittest +import os +import yaml + +from contextlib import contextmanager +from mock import patch, MagicMock + + +def load_config(): + ''' + Walk backwords from __file__ looking for config.yaml, load and return the + 'options' section' + ''' + config = None + f = __file__ + while config is None: + d = os.path.dirname(f) + if os.path.isfile(os.path.join(d, 'config.yaml')): + config = os.path.join(d, 'config.yaml') + break + f = d + + if not config: + logging.error('Could not find config.yaml in any parent directory ' + 'of %s. ' % file) + raise Exception + + return yaml.safe_load(open(config).read())['options'] + + +def get_default_config(): + ''' + Load default charm config from config.yaml return as a dict. + If no default is set in config.yaml, its value is None. + ''' + default_config = {} + config = load_config() + for k, v in config.iteritems(): + if 'default' in v: + default_config[k] = v['default'] + else: + default_config[k] = None + return default_config + + +class CharmTestCase(unittest.TestCase): + + def setUp(self, obj, patches): + super(CharmTestCase, self).setUp() + self.patches = patches + self.obj = obj + self.test_config = TestConfig() + self.test_relation = TestRelation() + self.patch_all() + + def patch(self, method): + _m = patch.object(self.obj, method) + mock = _m.start() + self.addCleanup(_m.stop) + return mock + + def patch_all(self): + for method in self.patches: + setattr(self, method, self.patch(method)) + + +class TestConfig(object): + + def __init__(self): + self.config = get_default_config() + + def get(self, attr=None): + if not attr: + return self.get_all() + try: + return self.config[attr] + except KeyError: + return None + + def get_all(self): + return self.config + + def set(self, attr, value): + if attr not in self.config: + raise KeyError + self.config[attr] = value + + +class TestRelation(object): + + def __init__(self, relation_data={}): + self.relation_data = relation_data + + def set(self, relation_data): + self.relation_data = relation_data + + def get(self, attribute=None, unit=None, rid=None): + if attribute is None: + return self.relation_data + elif attribute in self.relation_data: + return self.relation_data[attribute] + return None + + +@contextmanager +def patch_open(): + '''Patch open() to allow mocking both open() itself and the file that is + yielded. + + Yields the mock for "open" and "file", respectively.''' + mock_open = MagicMock(spec=open) + mock_file = MagicMock(spec=file) + + @contextmanager + def stub_open(*args, **kwargs): + mock_open(*args, **kwargs) + yield mock_file + + with patch('__builtin__.open', stub_open): + yield mock_open, mock_file