← Back to team overview

cf-charmers team mailing list archive

[Merge] lp:~johnsca/charms/trusty/cf-logrouter/refactor into lp:~cf-charmers/charms/trusty/cf-logrouter/trunk

 

Cory Johns has proposed merging lp:~johnsca/charms/trusty/cf-logrouter/refactor into lp:~cf-charmers/charms/trusty/cf-logrouter/trunk.

Requested reviews:
  Cloud Foundry Charmers (cf-charmers)

For more details, see:
https://code.launchpad.net/~johnsca/charms/trusty/cf-logrouter/refactor/+merge/219916

Refactored to use refactored charm-helpers



https://codereview.appspot.com/94500044/

-- 
https://code.launchpad.net/~johnsca/charms/trusty/cf-logrouter/refactor/+merge/219916
Your team Cloud Foundry Charmers is requested to review the proposed merge of lp:~johnsca/charms/trusty/cf-logrouter/refactor into lp:~cf-charmers/charms/trusty/cf-logrouter/trunk.
=== modified file 'config.yaml'
--- config.yaml	2014-03-28 23:54:57 +0000
+++ config.yaml	2014-05-16 23:06:38 +0000
@@ -7,4 +7,23 @@
     type: string
     default: "example.net"
     description: "Router api for cloud controller endpoint"
+  source:
+    type: string
+    default: 'ppa:cf-charm/ppa'
+    description: |
+      Optional configuration to support use of additional sources such as:
+      .
+        - ppa:myteam/ppa
+        - cloud:precise-proposed/folsom
+        - http://my.archive.com/ubuntu main
+      .
+      The last option should be used in conjunction with the key configuration
+      option.
+      .
+  key:
+    type: string
+    default: '4C430C3C2828E07D'
+    description: |
+      Key ID to import to the apt keyring to support use with arbitary source
+      configuration from outside of Launchpad archives or PPA's.
 

=== removed directory 'files/upstart'
=== modified file 'hooks/charmhelpers/contrib/cloudfoundry/common.py'
--- hooks/charmhelpers/contrib/cloudfoundry/common.py	2014-05-07 16:31:47 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/common.py	2014-05-16 23:06:38 +0000
@@ -1,11 +1,3 @@
-import sys
-import os
-import pwd
-import grp
-import subprocess
-
-from contextlib import contextmanager
-from charmhelpers.core.hookenv import log, ERROR, DEBUG
 from charmhelpers.core import host
 
 from charmhelpers.fetch import (
@@ -13,59 +5,8 @@
 )
 
 
-def run(command, exit_on_error=True, quiet=False):
-    '''Run a command and return the output.'''
-    if not quiet:
-        log("Running {!r}".format(command), DEBUG)
-    p = subprocess.Popen(
-        command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
-        shell=isinstance(command, basestring))
-    p.stdin.close()
-    lines = []
-    for line in p.stdout:
-        if line:
-            if not quiet:
-                print line
-            lines.append(line)
-        elif p.poll() is not None:
-            break
-
-    p.wait()
-
-    if p.returncode == 0:
-        return '\n'.join(lines)
-
-    if p.returncode != 0 and exit_on_error:
-        log("ERROR: {}".format(p.returncode), ERROR)
-        sys.exit(p.returncode)
-
-    raise subprocess.CalledProcessError(
-        p.returncode, command, '\n'.join(lines))
-
-
-def chownr(path, owner, group):
-    uid = pwd.getpwnam(owner).pw_uid
-    gid = grp.getgrnam(group).gr_gid
-    for root, dirs, files in os.walk(path):
-        for momo in dirs:
-            os.chown(os.path.join(root, momo), uid, gid)
-            for momo in files:
-                os.chown(os.path.join(root, momo), uid, gid)
-
-
-@contextmanager
-def chdir(d):
-    cur = os.getcwd()
-    try:
-        yield os.chdir(d)
-    finally:
-        os.chdir(cur)
-
-
 def prepare_cloudfoundry_environment(config_data, packages):
-    if 'source' in config_data:
-        add_source(config_data['source'], config_data.get('key'))
-        apt_update(fatal=True)
-    if packages:
-        apt_install(packages=filter_installed_packages(packages), fatal=True)
+    add_source(config_data['source'], config_data.get('key'))
+    apt_update(fatal=True)
+    apt_install(packages=filter_installed_packages(packages), fatal=True)
     host.adduser('vcap')

=== removed file 'hooks/charmhelpers/contrib/cloudfoundry/config_helper.py'
--- hooks/charmhelpers/contrib/cloudfoundry/config_helper.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/config_helper.py	1970-01-01 00:00:00 +0000
@@ -1,11 +0,0 @@
-import jinja2
-
-TEMPLATES_DIR = 'templates'
-
-def render_template(template_name, context, template_dir=TEMPLATES_DIR):
-    templates = jinja2.Environment(
-        loader=jinja2.FileSystemLoader(template_dir))
-    template = templates.get_template(template_name)
-    return template.render(context)
-
-

=== modified file 'hooks/charmhelpers/contrib/cloudfoundry/contexts.py'
--- hooks/charmhelpers/contrib/cloudfoundry/contexts.py	2014-05-07 16:31:47 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/contexts.py	2014-05-16 23:06:38 +0000
@@ -1,54 +1,16 @@
 import os
-import yaml
-
-from charmhelpers.core import hookenv
-from charmhelpers.contrib.openstack.context import OSContextGenerator
-
-
-class RelationContext(OSContextGenerator):
-    def __call__(self):
-        if not hookenv.relation_ids(self.interface):
-            return {}
-
-        ctx = {}
-        for rid in hookenv.relation_ids(self.interface):
-            for unit in hookenv.related_units(rid):
-                reldata = hookenv.relation_get(rid=rid, unit=unit)
-                required = set(self.required_keys)
-                if set(reldata.keys()).issuperset(required):
-                    ns = ctx.setdefault(self.interface, {})
-                    for k, v in reldata.items():
-                        ns[k] = v
-                    return ctx
-
-        return {}
-
-
-class ConfigContext(OSContextGenerator):
-    def __call__(self):
-        return hookenv.config()
-
-
-class StorableContext(object):
-    
-    def store_context(self, file_name, config_data):
-        with open(file_name, 'w') as file_stream:
-            yaml.dump(config_data, file_stream)
-
-    def read_context(self, file_name):
-        with open(file_name, 'r') as file_stream:
-            data = yaml.load(file_stream)
-            if not data:
-                raise OSError("%s is empty" % file_name)
-            return data
+
+from charmhelpers.core.templating import (
+    ContextGenerator,
+    RelationContext,
+    StorableContext,
+)
 
 
 # Stores `config_data` hash into yaml file with `file_name` as a name
 # if `file_name` already exists, then it loads data from `file_name`.
-class StoredContext(OSContextGenerator, StorableContext):
-
+class StoredContext(ContextGenerator, StorableContext):
     def __init__(self, file_name, config_data):
-        self.data = config_data
         if os.path.exists(file_name):
             self.data = self.read_context(file_name)
         else:
@@ -59,14 +21,6 @@
         return self.data
 
 
-class StaticContext(OSContextGenerator):
-    def __init__(self, data):
-        self.data = data
-
-    def __call__(self):
-        return self.data
-
-
 class NatsContext(RelationContext):
     interface = 'nats'
     required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']
@@ -77,7 +31,11 @@
     required_keys = ['domain']
 
 
+class LogRouterContext(RelationContext):
+    interface = 'logrouter'
+    required_keys = ['shared-secret', 'logrouter-address']
+
+
 class LoggregatorContext(RelationContext):
     interface = 'loggregator'
     required_keys = ['shared_secret', 'loggregator_address']
-

=== removed file 'hooks/charmhelpers/contrib/cloudfoundry/install.py'
--- hooks/charmhelpers/contrib/cloudfoundry/install.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/install.py	1970-01-01 00:00:00 +0000
@@ -1,35 +0,0 @@
-import os
-import subprocess
-
-
-def install(src, dest, fileprops=None, sudo=False):
-    """Install a file from src to dest. Dest can be a complete filename
-    or a target directory. fileprops is a dict with 'owner' (username of owner)
-    and mode (octal string) as keys, the defaults are 'ubuntu' and '400'
-
-    When owner is passed or when access requires it sudo can be set to True and
-    sudo will be used to install the file.
-    """
-    if not fileprops:
-        fileprops = {}
-    mode = fileprops.get('mode', '400')
-    owner = fileprops.get('owner')
-    cmd = ['install']
-
-    if not os.path.exists(src):
-        raise OSError(src)
-
-    if not os.path.exists(dest) and not os.path.exists(os.path.dirname(dest)):
-        # create all but the last component as path
-        cmd.append('-D')
-
-    if mode:
-        cmd.extend(['-m', mode])
-
-    if owner:
-        cmd.extend(['-o', owner])
-
-    if sudo:
-        cmd.insert(0, 'sudo')
-    cmd.extend([src, dest])
-    subprocess.check_call(cmd)

=== removed file 'hooks/charmhelpers/contrib/cloudfoundry/services.py'
--- hooks/charmhelpers/contrib/cloudfoundry/services.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/services.py	1970-01-01 00:00:00 +0000
@@ -1,118 +0,0 @@
-import os
-import tempfile
-from charmhelpers.core import host
-
-from charmhelpers.contrib.cloudfoundry.install import install
-from charmhelpers.core.hookenv import log
-from jinja2 import Environment, FileSystemLoader
-
-SERVICE_CONFIG = []
-TEMPLATE_LOADER = None
-
-
-def render_template(template_name, context):
-    """Render template to a tempfile returning the name"""
-    _, fn = tempfile.mkstemp()
-    template = load_template(template_name)
-    output = template.render(context)
-    with open(fn, "w") as fp:
-        fp.write(output)
-    return fn
-
-
-def collect_contexts(context_providers):
-    ctx = {}
-    for provider in context_providers:
-        c = provider()
-        if not c:
-            return {}
-        ctx.update(c)
-    return ctx
-
-
-def load_template(name):
-    return TEMPLATE_LOADER.get_template(name)
-
-
-def configure_templates(template_dir):
-    global TEMPLATE_LOADER
-    TEMPLATE_LOADER = Environment(loader=FileSystemLoader(template_dir))
-
-
-def register(service_configs, template_dir):
-    """Register a list of service configs.
-
-    Service Configs are dicts in the following formats:
-
-        {
-            "service": <service name>,
-            "templates": [ {
-                'target': <render target of template>,
-                'source': <optional name of template in passed in template_dir>
-                'file_properties': <optional dict taking owner and octal mode>
-                'contexts': [ context generators, see contexts.py ]
-                }
-            ] }
-
-    If 'source' is not provided for a template the template_dir will
-    be consulted for ``basename(target).j2``.
-    """
-    global SERVICE_CONFIG
-    if template_dir:
-        configure_templates(template_dir)
-    SERVICE_CONFIG.extend(service_configs)
-
-
-def reset():
-    global SERVICE_CONFIG
-    SERVICE_CONFIG = []
-
-
-# def service_context(name):
-#     contexts = collect_contexts(template['contexts'])
-
-def reconfigure_service(service_name, restart=True):
-    global SERVICE_CONFIG
-    service = None
-    for service in SERVICE_CONFIG:
-        if service['service'] == service_name:
-            break
-    if not service or service['service'] != service_name:
-        raise KeyError('Service not registered: %s' % service_name)
-
-    templates = service['templates']
-    for template in templates:
-        contexts = collect_contexts(template['contexts'])
-        if contexts:
-            template_target = template['target']
-            default_template = "%s.j2" % os.path.basename(template_target)
-            template_name = template.get('source', default_template)
-            output_file = render_template(template_name, contexts)
-            file_properties = template.get('file_properties')
-            install(output_file, template_target, file_properties)
-            os.unlink(output_file)
-        else:
-            restart = False
-
-    if restart:
-        host.service_restart(service_name)
-
-
-def stop_services():
-    global SERVICE_CONFIG
-    for service in SERVICE_CONFIG:
-        if host.service_running(service['service']):
-            host.service_stop(service['service'])
-
-
-def get_service(service_name):
-    global SERVICE_CONFIG
-    for service in SERVICE_CONFIG:
-        if service_name == service['service']:
-            return service
-    return None
-
-
-def reconfigure_services(restart=True):
-    for service in SERVICE_CONFIG:
-        reconfigure_service(service['service'], restart=restart)

=== removed file 'hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py'
--- hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py	1970-01-01 00:00:00 +0000
@@ -1,14 +0,0 @@
-import os
-import glob
-from charmhelpers.core import hookenv
-from charmhelpers.core.hookenv import charm_dir
-from charmhelpers.contrib.cloudfoundry.install import install
-
-
-def install_upstart_scripts(dirname=os.path.join(hookenv.charm_dir(),
-                                                 'files/upstart'),
-                            pattern='*.conf'):
-    for script in glob.glob("%s/%s" % (dirname, pattern)):
-        filename = os.path.join(dirname, script)
-        hookenv.log('Installing upstart job:' + filename, hookenv.DEBUG)
-        install(filename, '/etc/init')

=== modified file 'hooks/charmhelpers/contrib/hahelpers/apache.py'
--- hooks/charmhelpers/contrib/hahelpers/apache.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/hahelpers/apache.py	2014-05-16 23:06:38 +0000
@@ -39,14 +39,15 @@
 
 
 def get_ca_cert():
-    ca_cert = None
-    log("Inspecting identity-service relations for CA SSL certificate.",
-        level=INFO)
-    for r_id in relation_ids('identity-service'):
-        for unit in relation_list(r_id):
-            if not ca_cert:
-                ca_cert = relation_get('ca_cert',
-                                       rid=r_id, unit=unit)
+    ca_cert = config_get('ssl_ca')
+    if ca_cert is None:
+        log("Inspecting identity-service relations for CA SSL certificate.",
+            level=INFO)
+        for r_id in relation_ids('identity-service'):
+            for unit in relation_list(r_id):
+                if ca_cert is None:
+                    ca_cert = relation_get('ca_cert',
+                                           rid=r_id, unit=unit)
     return ca_cert
 
 

=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py	2014-05-16 23:06:38 +0000
@@ -1,5 +1,6 @@
 import json
 import os
+import time
 
 from base64 import b64decode
 
@@ -113,7 +114,8 @@
 class SharedDBContext(OSContextGenerator):
     interfaces = ['shared-db']
 
-    def __init__(self, database=None, user=None, relation_prefix=None):
+    def __init__(self,
+                 database=None, user=None, relation_prefix=None, ssl_dir=None):
         '''
         Allows inspecting relation for settings prefixed with relation_prefix.
         This is useful for parsing access for multiple databases returned via
@@ -122,6 +124,7 @@
         self.relation_prefix = relation_prefix
         self.database = database
         self.user = user
+        self.ssl_dir = ssl_dir
 
     def __call__(self):
         self.database = self.database or config('database')
@@ -139,17 +142,72 @@
 
         for rid in relation_ids('shared-db'):
             for unit in related_units(rid):
-                passwd = relation_get(password_setting, rid=rid, unit=unit)
+                rdata = relation_get(rid=rid, unit=unit)
                 ctxt = {
-                    'database_host': relation_get('db_host', rid=rid,
-                                                  unit=unit),
+                    'database_host': rdata.get('db_host'),
                     'database': self.database,
                     'database_user': self.user,
-                    'database_password': passwd,
-                }
-                if context_complete(ctxt):
-                    return ctxt
-        return {}
+                    'database_password': rdata.get(password_setting),
+                    'database_type': 'mysql'
+                }
+                if context_complete(ctxt):
+                    db_ssl(rdata, ctxt, self.ssl_dir)
+                    return ctxt
+        return {}
+
+
+class PostgresqlDBContext(OSContextGenerator):
+    interfaces = ['pgsql-db']
+
+    def __init__(self, database=None):
+        self.database = database
+
+    def __call__(self):
+        self.database = self.database or config('database')
+        if self.database is None:
+            log('Could not generate postgresql_db context. '
+                'Missing required charm config options. '
+                '(database name)')
+            raise OSContextError
+        ctxt = {}
+
+        for rid in relation_ids(self.interfaces[0]):
+            for unit in related_units(rid):
+                ctxt = {
+                    'database_host': relation_get('host', rid=rid, unit=unit),
+                    'database': self.database,
+                    'database_user': relation_get('user', rid=rid, unit=unit),
+                    'database_password': relation_get('password', rid=rid, unit=unit),
+                    'database_type': 'postgresql',
+                }
+                if context_complete(ctxt):
+                    return ctxt
+        return {}
+
+
+def db_ssl(rdata, ctxt, ssl_dir):
+    if 'ssl_ca' in rdata and ssl_dir:
+        ca_path = os.path.join(ssl_dir, 'db-client.ca')
+        with open(ca_path, 'w') as fh:
+            fh.write(b64decode(rdata['ssl_ca']))
+        ctxt['database_ssl_ca'] = ca_path
+    elif 'ssl_ca' in rdata:
+        log("Charm not setup for ssl support but ssl ca found")
+        return ctxt
+    if 'ssl_cert' in rdata:
+        cert_path = os.path.join(
+            ssl_dir, 'db-client.cert')
+        if not os.path.exists(cert_path):
+            log("Waiting 1m for ssl client cert validity")
+            time.sleep(60)
+        with open(cert_path, 'w') as fh:
+            fh.write(b64decode(rdata['ssl_cert']))
+        ctxt['database_ssl_cert'] = cert_path
+        key_path = os.path.join(ssl_dir, 'db-client.key')
+        with open(key_path, 'w') as fh:
+            fh.write(b64decode(rdata['ssl_key']))
+        ctxt['database_ssl_key'] = key_path
+    return ctxt
 
 
 class IdentityServiceContext(OSContextGenerator):
@@ -161,24 +219,25 @@
 
         for rid in relation_ids('identity-service'):
             for unit in related_units(rid):
+                rdata = relation_get(rid=rid, unit=unit)
                 ctxt = {
-                    'service_port': relation_get('service_port', rid=rid,
-                                                 unit=unit),
-                    'service_host': relation_get('service_host', rid=rid,
-                                                 unit=unit),
-                    'auth_host': relation_get('auth_host', rid=rid, unit=unit),
-                    'auth_port': relation_get('auth_port', rid=rid, unit=unit),
-                    'admin_tenant_name': relation_get('service_tenant',
-                                                      rid=rid, unit=unit),
-                    'admin_user': relation_get('service_username', rid=rid,
-                                               unit=unit),
-                    'admin_password': relation_get('service_password', rid=rid,
-                                                   unit=unit),
-                    # XXX: Hard-coded http.
-                    'service_protocol': 'http',
-                    'auth_protocol': 'http',
+                    'service_port': rdata.get('service_port'),
+                    'service_host': rdata.get('service_host'),
+                    'auth_host': rdata.get('auth_host'),
+                    'auth_port': rdata.get('auth_port'),
+                    'admin_tenant_name': rdata.get('service_tenant'),
+                    'admin_user': rdata.get('service_username'),
+                    'admin_password': rdata.get('service_password'),
+                    'service_protocol':
+                    rdata.get('service_protocol') or 'http',
+                    'auth_protocol':
+                    rdata.get('auth_protocol') or 'http',
                 }
                 if context_complete(ctxt):
+                    # NOTE(jamespage) this is required for >= icehouse
+                    # so a missing value just indicates keystone needs
+                    # upgrading
+                    ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
                     return ctxt
         return {}
 
@@ -186,6 +245,9 @@
 class AMQPContext(OSContextGenerator):
     interfaces = ['amqp']
 
+    def __init__(self, ssl_dir=None):
+        self.ssl_dir = ssl_dir
+
     def __call__(self):
         log('Generating template context for amqp')
         conf = config()
@@ -196,7 +258,6 @@
             log('Could not generate shared_db context. '
                 'Missing required charm config options: %s.' % e)
             raise OSContextError
-
         ctxt = {}
         for rid in relation_ids('amqp'):
             ha_vip_only = False
@@ -214,6 +275,14 @@
                                                       unit=unit),
                     'rabbitmq_virtual_host': vhost,
                 })
+
+                ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
+                if ssl_port:
+                    ctxt['rabbit_ssl_port'] = ssl_port
+                ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
+                if ssl_ca:
+                    ctxt['rabbit_ssl_ca'] = ssl_ca
+
                 if relation_get('ha_queues', rid=rid, unit=unit) is not None:
                     ctxt['rabbitmq_ha_queues'] = True
 
@@ -221,6 +290,16 @@
                                            rid=rid, unit=unit) is not None
 
                 if context_complete(ctxt):
+                    if 'rabbit_ssl_ca' in ctxt:
+                        if not self.ssl_dir:
+                            log(("Charm not setup for ssl support "
+                                 "but ssl ca found"))
+                            break
+                        ca_path = os.path.join(
+                            self.ssl_dir, 'rabbit-client-ca.pem')
+                        with open(ca_path, 'w') as fh:
+                            fh.write(b64decode(ctxt['rabbit_ssl_ca']))
+                            ctxt['rabbit_ssl_ca'] = ca_path
                     # Sufficient information found = break out!
                     break
             # Used for active/active rabbitmq >= grizzly
@@ -391,6 +470,8 @@
             'private_address': unit_get('private-address'),
             'endpoints': []
         }
+        if is_clustered():
+            ctxt['private_address'] = config('vip')
         for api_port in self.external_ports:
             ext_port = determine_apache_port(api_port)
             int_port = determine_api_port(api_port)

=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
--- hooks/charmhelpers/contrib/openstack/neutron.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/openstack/neutron.py	2014-05-16 23:06:38 +0000
@@ -17,6 +17,8 @@
     kver = check_output(['uname', '-r']).strip()
     return 'linux-headers-%s' % kver
 
+QUANTUM_CONF_DIR = '/etc/quantum'
+
 
 def kernel_version():
     """ Retrieve the current major kernel version as a tuple e.g. (3, 13) """
@@ -35,6 +37,8 @@
 
 
 # legacy
+
+
 def quantum_plugins():
     from charmhelpers.contrib.openstack import context
     return {
@@ -46,7 +50,8 @@
             'contexts': [
                 context.SharedDBContext(user=config('neutron-database-user'),
                                         database=config('neutron-database'),
-                                        relation_prefix='neutron')],
+                                        relation_prefix='neutron',
+                                        ssl_dir=QUANTUM_CONF_DIR)],
             'services': ['quantum-plugin-openvswitch-agent'],
             'packages': [[headers_package()] + determine_dkms_package(),
                          ['quantum-plugin-openvswitch-agent']],
@@ -61,7 +66,8 @@
             'contexts': [
                 context.SharedDBContext(user=config('neutron-database-user'),
                                         database=config('neutron-database'),
-                                        relation_prefix='neutron')],
+                                        relation_prefix='neutron',
+                                        ssl_dir=QUANTUM_CONF_DIR)],
             'services': [],
             'packages': [],
             'server_packages': ['quantum-server',
@@ -70,6 +76,8 @@
         }
     }
 
+NEUTRON_CONF_DIR = '/etc/neutron'
+
 
 def neutron_plugins():
     from charmhelpers.contrib.openstack import context
@@ -83,7 +91,8 @@
             'contexts': [
                 context.SharedDBContext(user=config('neutron-database-user'),
                                         database=config('neutron-database'),
-                                        relation_prefix='neutron')],
+                                        relation_prefix='neutron',
+                                        ssl_dir=NEUTRON_CONF_DIR)],
             'services': ['neutron-plugin-openvswitch-agent'],
             'packages': [[headers_package()] + determine_dkms_package(),
                          ['neutron-plugin-openvswitch-agent']],
@@ -98,7 +107,8 @@
             'contexts': [
                 context.SharedDBContext(user=config('neutron-database-user'),
                                         database=config('neutron-database'),
-                                        relation_prefix='neutron')],
+                                        relation_prefix='neutron',
+                                        ssl_dir=NEUTRON_CONF_DIR)],
             'services': [],
             'packages': [],
             'server_packages': ['neutron-server',

=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
--- hooks/charmhelpers/contrib/openstack/utils.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/openstack/utils.py	2014-05-16 23:06:38 +0000
@@ -65,6 +65,7 @@
     ('1.10.0', 'havana'),
     ('1.9.1', 'havana'),
     ('1.9.0', 'havana'),
+    ('1.13.1', 'icehouse'),
     ('1.13.0', 'icehouse'),
     ('1.12.0', 'icehouse'),
     ('1.11.0', 'icehouse'),
@@ -400,6 +401,8 @@
         rtype = 'PTR'
     elif isinstance(address, basestring):
         rtype = 'A'
+    else:
+        return None
 
     answers = dns.resolver.query(address, rtype)
     if answers:

=== modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
--- hooks/charmhelpers/contrib/storage/linux/utils.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/contrib/storage/linux/utils.py	2014-05-16 23:06:38 +0000
@@ -2,7 +2,9 @@
 from stat import S_ISBLK
 
 from subprocess import (
-    check_call
+    check_call,
+    check_output,
+    call
 )
 
 
@@ -22,5 +24,12 @@
 
     :param block_device: str: Full path of block device to clean.
     '''
-    check_call(['sgdisk', '--zap-all', '--clear',
-                '--mbrtogpt', block_device])
+    # sometimes sgdisk exits non-zero; this is OK, dd will clean up
+    call(['sgdisk', '--zap-all', '--mbrtogpt',
+          '--clear', block_device])
+    dev_end = check_output(['blockdev', '--getsz', block_device])
+    gpt_end = int(dev_end.split()[0]) - 100
+    check_call(['dd', 'if=/dev/zero', 'of=%s'%(block_device),
+                'bs=1M', 'count=1'])
+    check_call(['dd', 'if=/dev/zero', 'of=%s'%(block_device),
+                'bs=512', 'count=100', 'seek=%s'%(gpt_end)])

=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py	2014-03-28 23:53:46 +0000
+++ hooks/charmhelpers/core/host.py	2014-05-16 23:06:38 +0000
@@ -12,6 +12,8 @@
 import string
 import subprocess
 import hashlib
+import shutil
+from contextlib import contextmanager
 
 from collections import OrderedDict
 
@@ -143,6 +145,16 @@
         target.write(content)
 
 
+def copy_file(src, dst, owner='root', group='root', perms=0444):
+    """Create or overwrite a file with the contents of another file"""
+    log("Writing file {} {}:{} {:o} from {}".format(dst, owner, group, perms, src))
+    uid = pwd.getpwnam(owner).pw_uid
+    gid = grp.getgrnam(group).gr_gid
+    shutil.copyfile(src, dst)
+    os.chown(dst, uid, gid)
+    os.chmod(dst, perms)
+
+
 def mount(device, mountpoint, options=None, persist=False):
     """Mount a filesystem at a particular mountpoint"""
     cmd_args = ['mount']
@@ -295,3 +307,28 @@
     if 'link/ether' in words:
         hwaddr = words[words.index('link/ether') + 1]
     return hwaddr
+
+
+@contextmanager
+def chdir(d):
+    cur = os.getcwd()
+    try:
+        yield os.chdir(d)
+    finally:
+        os.chdir(cur)
+
+
+def chownr(path, owner, group, fatal=False):
+    uid = pwd.getpwnam(owner).pw_uid
+    gid = grp.getgrnam(group).gr_gid
+
+    def _raise(e):
+        raise e
+
+    for root, dirs, files in os.walk(path, onerror=_raise if fatal else None):
+        for name in dirs + files:
+            try:
+                os.chown(os.path.join(root, name), uid, gid)
+            except Exception:
+                if fatal:
+                    raise

=== added file 'hooks/charmhelpers/core/services.py'
--- hooks/charmhelpers/core/services.py	1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services.py	2014-05-16 23:06:38 +0000
@@ -0,0 +1,84 @@
+from charmhelpers.core import templating
+from charmhelpers.core import host
+
+
+SERVICES = {}
+
+
+def register(services, templates_dir=None):
+    """
+    Register a list of service configs.
+
+    Service Configs are dicts in the following formats:
+
+        {
+            "service": <service name>,
+            "templates": [ {
+                'target': <render target of template>,
+                'source': <optional name of template in passed in templates_dir>
+                'file_properties': <optional dict taking owner and octal mode>
+                'contexts': [ context generators, see contexts.py ]
+                }
+            ] }
+
+    Either `source` or `target` must be provided.
+
+    If 'source' is not provided for a template the templates_dir will
+    be consulted for ``basename(target).j2``.
+
+    If `target` is not provided, it will be assumed to be
+    ``/etc/init/<service name>.conf``.
+    """
+    for service in services:
+        service.setdefault('templates_dir', templates_dir)
+        SERVICES[service['service']] = service
+
+
+def reconfigure_services(restart=True):
+    """
+    Update all files for all services and optionally restart them, if ready.
+    """
+    for service_name in SERVICES.keys():
+        reconfigure_service(service_name, restart=restart)
+
+
+def reconfigure_service(service_name, restart=True):
+    """
+    Update all files for a single service and optionally restart it, if ready.
+    """
+    service = SERVICES.get(service_name)
+    if not service or service['service'] != service_name:
+        raise KeyError('Service not registered: %s' % service_name)
+
+    manager_type = service.get('type', UpstartService)
+    manager_type(service).reconfigure(restart)
+
+
+def stop_services():
+    for service_name in SERVICES.keys():
+        if host.service_running(service_name):
+            host.service_stop(service_name)
+
+
+class ServiceTypeManager(object):
+    def __init__(self, service_definition):
+        self.service_name = service_definition['service']
+        self.templates = service_definition['templates']
+        self.templates_dir = service_definition['templates_dir']
+
+    def reconfigure(self, restart=True):
+        raise NotImplementedError()
+
+
+class UpstartService(ServiceTypeManager):
+    def __init__(self, service_definition):
+        super(UpstartService, self).__init__(service_definition)
+        for tmpl in self.templates:
+            if 'target' not in tmpl:
+                tmpl['target'] = '/etc/init/%s.conf' % self.service_name
+
+    def reconfigure(self, restart):
+        complete = templating.render(self.templates, self.templates_dir)
+
+        if restart and complete:
+            host.service_restart(self.service_name)

=== added file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py	1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/templating.py	2014-05-16 23:06:38 +0000
@@ -0,0 +1,159 @@
+import os
+import yaml
+
+from charmhelpers.core import host
+from charmhelpers.core import hookenv
+
+LOADER = None
+
+
+class ContextGenerator(object):
+    """
+    Base interface for template context container generators.
+
+    A template context is a dictionary that contains data needed to populate
+    the template.  The generator instance should produce the context when
+    called (without arguments) by collecting information from juju (config-get,
+    relation-get, etc), the system, or whatever other sources are appropriate.
+
+    A context generator should only return any values if it has enough information
+    to provide all of its values.  Any context that is missing data is considered
+    incomplete and will cause that template to not render until it has all of its
+    necessary data.
+
+    The template may receive several contexts, which will be merged together,
+    so care should be taken in the key names.
+    """
+    def __call__(self):
+        raise NotImplementedError
+
+
+class StorableContext(object):
+    """
+    A mixin for persisting a context to disk.
+    """
+    def store_context(self, file_name, config_data):
+        with open(file_name, 'w') as file_stream:
+            yaml.dump(config_data, file_stream)
+
+    def read_context(self, file_name):
+        with open(file_name, 'r') as file_stream:
+            data = yaml.load(file_stream)
+            if not data:
+                raise OSError("%s is empty" % file_name)
+            return data
+
+
+class ConfigContext(ContextGenerator):
+    """
+    A context generator that generates a context containing all of the
+    juju config values.
+    """
+    def __call__(self):
+        return hookenv.config()
+
+
+class RelationContext(ContextGenerator):
+    """
+    Base class for a context generator that gets relation data from juju.
+
+    Subclasses must provide `interface`, which is the interface type of interest,
+    and `required_keys`, which is the set of keys required for the relation to
+    be considered complete.  The first relation for the interface that is complete
+    will be used to populate the data for template.
+
+    The generated context will be namespaced under the interface type, to prevent
+    potential naming conflicts.
+    """
+    interface = None
+    required_keys = []
+
+    def __call__(self):
+        if not hookenv.relation_ids(self.interface):
+            return {}
+
+        ctx = {}
+        for rid in hookenv.relation_ids(self.interface):
+            for unit in hookenv.related_units(rid):
+                reldata = hookenv.relation_get(rid=rid, unit=unit)
+                required = set(self.required_keys)
+                if set(reldata.keys()).issuperset(required):
+                    ns = ctx.setdefault(self.interface, {})
+                    for k, v in reldata.items():
+                        ns[k] = v
+                    return ctx
+
+        return {}
+
+
+class StaticContext(ContextGenerator):
+    def __init__(self, data):
+        self.data = data
+
+    def __call__(self):
+        return self.data
+
+
+def _collect_contexts(context_providers):
+    """
+    Helper function to collect and merge contexts from a list of providers.
+
+    If any of the contexts are incomplete (i.e., they return an empty dict),
+    the template is considered incomplete and will not render.
+    """
+    ctx = {}
+    for provider in context_providers:
+        c = provider()
+        if not c:
+            return False
+        ctx.update(c)
+    return ctx
+
+
+def render(template_definitions, templates_dir=None):
+    """
+    Render one or more templates, given a list of template definitions.
+
+    The template definitions should be dicts with the keys: `source`, `target`,
+    `file_properties`, and `contexts`.
+
+    The `source` path, if not absolute, is relative to the `templates_dir`
+    given when the rendered was created.  If `source` is not provided
+    for a template the `template_dir` will be consulted for
+    ``basename(target).j2``.
+
+    The `target` path should be absolute.
+
+    The `file_properties` should be a dict optionally containing
+    `owner`, `group`, or `perms` options, to be passed to `write_file`.
+
+    The `contexts` should be a list containing zero or more ContextGenerators.
+
+    The `template_dir` defaults to `$CHARM_DIR/templates`
+
+    Returns True if all of the templates were "complete" (i.e., the context
+    generators were able to collect the information needed to render the
+    template) and were rendered.
+    """
+    from jinja2 import FileSystemLoader, Environment, exceptions
+    all_complete = True
+    if templates_dir is None:
+        templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
+    loader = Environment(loader=FileSystemLoader(templates_dir))
+    for tmpl in template_definitions:
+        ctx = _collect_contexts(tmpl.get('contexts', []))
+        if ctx is False:
+            all_complete = False
+            continue
+        try:
+            source = tmpl.get('source', os.path.basename(tmpl['target'])+'.j2')
+            template = loader.get_template(source)
+        except exceptions.TemplateNotFound as e:
+            hookenv.log('Could not load template %s from %s.' %
+                        (tmpl['source'], templates_dir),
+                        level=hookenv.ERROR)
+            raise e
+        content = template.render(ctx)
+        host.mkdir(os.path.dirname(tmpl['target']))
+        host.write_file(tmpl['target'], content, **tmpl.get('file_properties', {}))
+    return all_complete

=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py	2014-04-25 12:49:34 +0000
+++ hooks/charmhelpers/fetch/__init__.py	2014-05-16 23:06:38 +0000
@@ -184,7 +184,7 @@
             apt.write(PROPOSED_POCKET.format(release))
     if key:
         subprocess.check_call(['apt-key', 'adv', '--keyserver',
-                               'keyserver.ubuntu.com', '--recv',
+                               'hkp://keyserver.ubuntu.com:80', '--recv',
                                key])
 
 

=== modified file 'hooks/hooks.py'
--- hooks/hooks.py	2014-05-08 11:54:43 +0000
+++ hooks/hooks.py	2014-05-16 23:06:38 +0000
@@ -6,11 +6,11 @@
 from charmhelpers.core.hookenv import log
 
 from charmhelpers.contrib.cloudfoundry import contexts
-from charmhelpers.contrib.cloudfoundry import services
-from config import *
+from charmhelpers.core import services
+from charmhelpers.core import templating
+import config as lg_config
 
 from charmhelpers.core import hookenv
-from charmhelpers.core.hookenv import log
 
 hooks = hookenv.Hooks()
 
@@ -18,19 +18,20 @@
 fileproperties = {'owner': 'vcap'}
 services.register([
     {
-        'service': LOGROUTER_JOB_NAME,
-        'templates': [{
-            'source': 'trafficcontroller.json',
-            'target': LOGROUTER_CONFIG_PATH,
-            'file_properties': fileproperties,
-            'contexts': [
-                contexts.StaticContext({'service_name': service_name}),
-                contexts.RouterContext(),
-                contexts.LoggregatorContext(),
-                contexts.NatsContext()
-            ]
-        }]
-    }], os.path.join(hookenv.charm_dir(), 'templates'))
+        'service': lg_config.LOGROUTER_JOB_NAME,
+        'templates': [
+            {'source': 'trafficcontroller.conf'},
+            {'source': 'trafficcontroller.json',
+             'target': lg_config.LOGROUTER_CONFIG_PATH,
+             'file_properties': fileproperties,
+             'contexts': [
+                 templating.StaticContext({'service_name': service_name}),
+                 contexts.RouterContext(),
+                 contexts.LoggregatorContext(),
+                 contexts.NatsContext()
+             ]},
+        ]
+    }])
 
 
 @hooks.hook()

=== modified file 'hooks/install' (properties changed: -x to +x)
--- hooks/install	2014-05-08 11:47:31 +0000
+++ hooks/install	2014-05-16 23:06:38 +0000
@@ -2,15 +2,11 @@
 # vim: et ai ts=4 sw=4:
 import os
 import subprocess
-from config import *
+import config as lg_config
 from charmhelpers.core import hookenv, host
-from charmhelpers.contrib.cloudfoundry.upstart_helper import (
-    install_upstart_scripts
-)
 from charmhelpers.contrib.cloudfoundry.common import (
     prepare_cloudfoundry_environment
 )
-from charmhelpers.contrib.cloudfoundry.install import install
 
 LOGSVC = "trafficcontroller"
 
@@ -19,9 +15,11 @@
 
 
 def install_charm():
-    prepare_cloudfoundry_environment(hookenv.config(), LOGROUTER_PACKAGES)
-    install_upstart_scripts()
-    dirs = [CF_DIR, LOGROUTER_DIR, LOGROUTER_CONFIG_DIR, '/var/log/vcap']
+    prepare_cloudfoundry_environment(hookenv.config(), lg_config.LOGROUTER_PACKAGES)
+    dirs = [lg_config.CF_DIR,
+            lg_config.LOGROUTER_DIR,
+            lg_config.LOGROUTER_CONFIG_DIR,
+            '/var/log/vcap']
 
     for item in dirs:
         host.mkdir(item, owner='vcap', group='vcap', perms=0775)
@@ -30,10 +28,9 @@
     subprocess.check_call(
         ["gunzip", "-k", "trafficcontroller.gz"],
         cwd=files_folder)
-
-    install(os.path.join(files_folder, 'trafficcontroller'),
-            "/usr/bin/trafficcontroller",
-            fileprops={'mode': '0755', 'owner': 'vcap'})
+    host.copy_file(os.path.join(files_folder, 'trafficcontroller'),
+                   "/usr/bin/trafficcontroller",
+                   perms=0755, owner='vcap')
 
 
 if __name__ == '__main__':

=== renamed file 'files/upstart/trafficcontroller.conf' => 'templates/trafficcontroller.conf'

References