PLUMgrid gateway initial charm

This commit is contained in:
Bilal Baqar 2015-05-19 14:07:07 -07:00
commit 2e2935412a
45 changed files with 1602 additions and 0 deletions

BIN
.coverage Normal file

Binary file not shown.

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>neutron-openvswitch</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

9
.pydevproject Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/neutron-openvswitch/hooks</path>
<path>/neutron-openvswitch/unit_tests</path>
</pydev_pathproperty>
</pydev_project>

24
Makefile Normal file
View File

@ -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

16
README.md Normal file
View File

@ -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

253
bin/charm_helpers_sync.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
# Authors:
# Adam Gandelman <adamg@ubuntu.com>
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)

12
charm-helpers-sync.yaml Normal file
View File

@ -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

5
config.yaml Normal file
View File

@ -0,0 +1,5 @@
options:
external-interface:
default: eth1
type: string
description: The interface that will provide external connectivity

9
copyright Normal file
View File

@ -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'

1
hooks/config-changed Symbolic link
View File

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

1
hooks/install Symbolic link
View File

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

View File

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

View File

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

View File

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

View File

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

93
hooks/pg_gw_context.py Normal file
View File

@ -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

47
hooks/pg_gw_hooks.py Executable file
View File

@ -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()

128
hooks/pg_gw_utils.py Normal file
View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
hooks/stop Symbolic link
View File

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

1
hooks/upgrade-charm Symbolic link
View File

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

304
icon.svg Normal file
View File

@ -0,0 +1,304 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96"
height="96"
id="svg6517"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="Gateway1.svg">
<defs
id="defs6519">
<linearGradient
id="Background">
<stop
id="stop4178"
offset="0"
style="stop-color:#b8b8b8;stop-opacity:1" />
<stop
id="stop4180"
offset="1"
style="stop-color:#c9c9c9;stop-opacity:1" />
</linearGradient>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Inner Shadow"
id="filter1121">
<feFlood
flood-opacity="0.59999999999999998"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1123" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="out"
result="composite1"
id="feComposite1125" />
<feGaussianBlur
in="composite1"
stdDeviation="1"
result="blur"
id="feGaussianBlur1127" />
<feOffset
dx="0"
dy="2"
result="offset"
id="feOffset1129" />
<feComposite
in="offset"
in2="SourceGraphic"
operator="atop"
result="composite2"
id="feComposite1131" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter950">
<feFlood
flood-opacity="0.25"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood952" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite954" />
<feGaussianBlur
in="composite1"
stdDeviation="1"
result="blur"
id="feGaussianBlur956" />
<feOffset
dx="0"
dy="1"
result="offset"
id="feOffset958" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite960" />
</filter>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath873">
<g
transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
id="g875"
inkscape:label="Layer 1"
style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
<path
style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z"
id="path877"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssssss" />
</g>
</clipPath>
<filter
inkscape:collect="always"
id="filter891"
inkscape:label="Badge Shadow">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.71999962"
id="feGaussianBlur893" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.0745362"
inkscape:cx="57.131043"
inkscape:cy="49.018169"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
showborder="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:showpageshadow="false">
<inkscape:grid
type="xygrid"
id="grid821" />
<sodipodi:guide
orientation="1,0"
position="16,48"
id="guide823" />
<sodipodi:guide
orientation="0,1"
position="64,80"
id="guide825" />
<sodipodi:guide
orientation="1,0"
position="80,40"
id="guide827" />
<sodipodi:guide
orientation="0,1"
position="64,16"
id="guide829" />
</sodipodi:namedview>
<metadata
id="metadata6522">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="BACKGROUND"
inkscape:groupmode="layer"
id="layer1"
transform="translate(268,-635.29076)"
style="display:inline">
<path
style="fill:#029bd6;fill-opacity:0.90980393;stroke:none;display:inline;filter:url(#filter1121)"
d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z"
id="path6455"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssssss" />
<g
transform="matrix(1.2554482,0,0,1.2247945,-280.29427,621.82638)"
id="g3">
<g
id="g5">
<g
id="g7">
<polygon
style="fill:#ffffff"
points="72.643,41.599 72.643,24.878 56.501,25.459 60.839,29.796 49.781,40.854 56.813,47.891 67.872,36.83 "
id="polygon9" />
<polygon
style="fill:#ffffff"
points="40.048,74.195 23.327,74.195 23.908,58.055 28.246,62.393 39.299,51.34 46.332,58.372 35.279,69.426 "
id="polygon11" />
</g>
<g
id="g13">
<polygon
style="fill:#ffffff"
points="39.721,25.136 23.005,25.136 23.581,41.277 27.918,36.938 38.977,47.997 46.009,40.964 34.951,29.906 "
id="polygon15" />
<polygon
style="fill:#ffffff"
points="72.965,58.383 72.965,75.1 56.824,74.521 61.162,70.186 50.104,59.127 57.137,52.094 68.195,63.152 "
id="polygon17" />
</g>
</g>
<circle
style="fill:none;stroke:#ffffff;stroke-width:3;stroke-miterlimit:10"
stroke-miterlimit="10"
cx="47.985001"
cy="49.990002"
r="10.165"
id="circle19" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="PLACE YOUR PICTOGRAM HERE"
style="display:inline" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="BADGE"
style="display:none"
sodipodi:insensitive="true">
<g
style="display:inline"
transform="translate(-340.00001,-581)"
id="g4394"
clip-path="none">
<g
id="g855">
<g
inkscape:groupmode="maskhelper"
id="g870"
clip-path="url(#clipPath873)"
style="opacity:0.6;filter:url(#filter891)">
<path
transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
d="m 264,552.36218 a 12,12 0 0 1 -12,12 12,12 0 0 1 -12,-12 12,12 0 0 1 12,-12 12,12 0 0 1 12,12 z"
sodipodi:ry="12"
sodipodi:rx="12"
sodipodi:cy="552.36218"
sodipodi:cx="252"
id="path844"
style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
sodipodi:type="arc" />
</g>
<g
id="g862">
<path
sodipodi:type="arc"
style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4398"
sodipodi:cx="252"
sodipodi:cy="552.36218"
sodipodi:rx="12"
sodipodi:ry="12"
d="m 264,552.36218 a 12,12 0 0 1 -12,12 12,12 0 0 1 -12,-12 12,12 0 0 1 12,-12 12,12 0 0 1 12,12 z"
transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
<path
transform="matrix(1.25,0,0,1.25,33,-100.45273)"
d="m 264,552.36218 a 12,12 0 0 1 -12,12 12,12 0 0 1 -12,-12 12,12 0 0 1 12,-12 12,12 0 0 1 12,12 z"
sodipodi:ry="12"
sodipodi:rx="12"
sodipodi:cy="552.36218"
sodipodi:cx="252"
id="path4400"
style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
sodipodi:type="arc" />
<path
sodipodi:type="star"
style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4459"
sodipodi:sides="5"
sodipodi:cx="666.19574"
sodipodi:cy="589.50385"
sodipodi:r1="7.2431178"
sodipodi:r2="4.3458705"
sodipodi:arg1="1.0471976"
sodipodi:arg2="1.6755161"
inkscape:flatsided="false"
inkscape:rounded="0.1"
inkscape:randomized="0"
d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 -0.18379,0.41279 0.0427,4.27917 -0.34859,4.5051 z"
transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

24
metadata.yaml Normal file
View File

@ -0,0 +1,24 @@
name: plumgrid-gateway
subordinate: false
maintainer: Bilal Baqar <bbaqar@plumgrid.com>
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

5
setup.cfg Normal file
View File

@ -0,0 +1,5 @@
[nosetests]
verbosity=1
with-coverage=1
cover-erase=1
cover-package=hooks

View File

@ -0,0 +1,2 @@
{{ pg_hostname }}

10
templates/icehouse/hosts Normal file
View File

@ -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

View File

@ -0,0 +1,6 @@
{{ interface }} = fabric_core host
{% if ext_interface -%}
{{ ext_interface }} = access_phys
{% endif -%}

View File

@ -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

View File

@ -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=

21
templates/parts/rabbitmq Normal file
View File

@ -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 -%}

5
tests/00-setup Executable file
View File

@ -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

39
tests/14-juno Executable file
View File

@ -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()

View File

@ -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'

4
unit_tests/__init__.py Normal file
View File

@ -0,0 +1,4 @@
import sys
sys.path.append('actions/')
sys.path.append('hooks/')

View File

@ -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())

View File

@ -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()

View File

@ -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])

121
unit_tests/test_utils.py Normal file
View File

@ -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