← Back to team overview

cf-charmers team mailing list archive

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

 

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

Requested reviews:
  Cloud Foundry Charmers (cf-charmers)

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

Refactored to use refactored charm-helpers



https://codereview.appspot.com/91460046/

-- 
https://code.launchpad.net/~johnsca/charms/trusty/cf-dea/refactor/+merge/219913
Your team Cloud Foundry Charmers is requested to review the proposed merge of lp:~johnsca/charms/trusty/cf-dea/refactor into lp:~cf-charmers/charms/trusty/cf-dea/trunk.
=== modified file 'hooks/charmhelpers/contrib/cloudfoundry/contexts.py'
--- hooks/charmhelpers/contrib/cloudfoundry/contexts.py	2014-05-14 19:08:36 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/contexts.py	2014-05-16 22:52:13 +0000
@@ -59,6 +59,7 @@
         return self.data
 
 
+<<<<<<< TREE
 class StaticContext(OSContextGenerator):
     def __init__(self, data):
         self.data = data
@@ -67,6 +68,8 @@
         return self.data
 
 
+=======
+>>>>>>> MERGE-SOURCE
 class NatsContext(RelationContext):
     interface = 'nats'
     required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']

=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py	2014-05-14 19:08:36 +0000
+++ hooks/charmhelpers/core/host.py	2014-05-16 22:52:13 +0000
@@ -12,6 +12,11 @@
 import string
 import subprocess
 import hashlib
+<<<<<<< TREE
+=======
+import shutil
+from contextlib import contextmanager
+>>>>>>> MERGE-SOURCE
 
 from collections import OrderedDict
 
@@ -143,6 +148,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 +310,31 @@
     if 'link/ether' in words:
         hwaddr = words[words.index('link/ether') + 1]
     return hwaddr
+<<<<<<< TREE
+=======
+
+
+@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
+>>>>>>> MERGE-SOURCE

=== added file 'hooks/charmhelpers/core/services.py.OTHER'
--- hooks/charmhelpers/core/services.py.OTHER	1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services.py.OTHER	2014-05-16 22:52:13 +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.OTHER'
--- hooks/charmhelpers/core/templating.py.OTHER	1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/templating.py.OTHER	2014-05-16 22:52:13 +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/hooks.py'
--- hooks/hooks.py	2014-05-14 19:08:36 +0000
+++ hooks/hooks.py	2014-05-16 22:52:13 +0000
@@ -12,13 +12,13 @@
 from config import *
 
 hooks = hookenv.Hooks()
-TEMPLATE_DIR = os.path.join(hookenv.charm_dir(), 'templates')
 warden_service_templates = [('warden.yml', WARDEN_CONFIG_PATH)]
 
 fileproperties = {'owner': 'vcap'}
 services.register([
     {
         'service': 'cf-dea',
+<<<<<<< TREE
         'templates': [{
             'source': 'dea.yml',
             'target': DEA_CONFIG_PATH,
@@ -26,9 +26,25 @@
             'contexts': [contexts.NatsContext(),
                          contexts.RouterContext()]
         }]
+=======
+        'templates': [
+            {'source': 'cf-dea.conf'},
+            {'source': 'dea.yml',
+             'target': DEA_CONFIG_PATH,
+             'file_properties': fileproperties,
+             'contexts': [contexts.NatsContext(),
+                          contexts.RouterContext()]},
+            {'source': 'cf-dir-server.conf'},
+            {'source': 'cf-warden.conf'},
+            {'source': 'warden.yml',
+             'target': WARDEN_CONFIG_PATH,
+             'file_properties': {'perms': 0400}},
+        ],
+>>>>>>> MERGE-SOURCE
     },
     {
         'service': 'cf-dea-logging-agent',
+<<<<<<< TREE
         'templates': [{
             'source': 'dea_logging_agent.json',
             'target': os.path.join(DLA_CONF_DIR, 'dea_logging_agent.json'),
@@ -37,6 +53,18 @@
                          contexts.LoggregatorContext()]
         }]
     }], TEMPLATE_DIR)
+=======
+        'templates': [
+            {'source': 'cf-dea-logging-agent.conf'},
+            {'source': 'dea_logging_agent.json',
+             'target': os.path.join(DLA_CONF_DIR, 'dea_logging_agent.json'),
+             'file_properties': fileproperties,
+             'contexts': [contexts.NatsContext(),
+                          contexts.LoggregatorContext()]}
+        ],
+    },
+])
+>>>>>>> MERGE-SOURCE
 
 
 @hooks.hook("upgrade-charm")


References