From aff5b8bc4afb8c12176e3b779e6596f3f0a32e13 Mon Sep 17 00:00:00 2001
From: James Page <james.page@ubuntu.com>
Date: Wed, 14 Jan 2015 09:23:53 +0000
Subject: [PATCH 01/16] [trivial] resync helpers

---
 .../charmhelpers/contrib/hahelpers/cluster.py | 38 ++++++++-----
 .../charmhelpers/contrib/openstack/context.py | 38 +++++++------
 .../charmhelpers/contrib/openstack/neutron.py | 10 +++-
 .../contrib/openstack/templates/haproxy.cfg   |  6 ++-
 hooks/charmhelpers/contrib/openstack/utils.py |  6 +++
 .../contrib/storage/linux/ceph.py             | 54 +++++++++++++++++++
 hooks/charmhelpers/core/host.py               | 11 ++--
 hooks/charmhelpers/fetch/__init__.py          |  9 +++-
 8 files changed, 136 insertions(+), 36 deletions(-)

diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py
index 52ce4b7c..912b2fe3 100644
--- a/hooks/charmhelpers/contrib/hahelpers/cluster.py
+++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py
@@ -13,6 +13,7 @@ clustering-related helpers.
 
 import subprocess
 import os
+
 from socket import gethostname as get_unit_hostname
 
 import six
@@ -28,12 +29,19 @@ from charmhelpers.core.hookenv import (
     WARNING,
     unit_get,
 )
+from charmhelpers.core.decorators import (
+    retry_on_exception,
+)
 
 
 class HAIncompleteConfig(Exception):
     pass
 
 
+class CRMResourceNotFound(Exception):
+    pass
+
+
 def is_elected_leader(resource):
     """
     Returns True if the charm executing this is the elected cluster leader.
@@ -68,24 +76,30 @@ def is_clustered():
     return False
 
 
-def is_crm_leader(resource):
+@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
+def is_crm_leader(resource, retry=False):
     """
     Returns True if the charm calling this is the elected corosync leader,
     as returned by calling the external "crm" command.
+
+    We allow this operation to be retried to avoid the possibility of getting a
+    false negative. See LP #1396246 for more info.
     """
-    cmd = [
-        "crm", "resource",
-        "show", resource
-    ]
+    cmd = ['crm', 'resource', 'show', resource]
     try:
-        status = subprocess.check_output(cmd).decode('UTF-8')
+        status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+        if not isinstance(status, six.text_type):
+            status = six.text_type(status, "utf-8")
     except subprocess.CalledProcessError:
-        return False
-    else:
-        if get_unit_hostname() in status:
-            return True
-        else:
-            return False
+        status = None
+
+    if status and get_unit_hostname() in status:
+        return True
+
+    if status and "resource %s is NOT running" % (resource) in status:
+        raise CRMResourceNotFound("CRM resource %s not found" % (resource))
+
+    return False
 
 
 def is_leader(resource):
diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py
index eb108910..eaa89a67 100644
--- a/hooks/charmhelpers/contrib/openstack/context.py
+++ b/hooks/charmhelpers/contrib/openstack/context.py
@@ -468,21 +468,25 @@ class HAProxyContext(OSContextGenerator):
                             _unit = unit.replace('/', '-')
                             cluster_hosts[laddr]['backends'][_unit] = _laddr
 
-        # NOTE(jamespage) no split configurations found, just use
-        # private addresses
-        if not cluster_hosts:
-            netmask = get_netmask_for_address(addr)
-            cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
-                                   'backends': {l_unit: addr}}
-            for rid in relation_ids('cluster'):
-                for unit in related_units(rid):
-                    _laddr = relation_get('private-address',
-                                          rid=rid, unit=unit)
-                    if _laddr:
-                        _unit = unit.replace('/', '-')
-                        cluster_hosts[addr]['backends'][_unit] = _laddr
+        # NOTE(jamespage) add backend based on private address - this
+        # with either be the only backend or the fallback if no acls
+        # match in the frontend
+        cluster_hosts[addr] = {}
+        netmask = get_netmask_for_address(addr)
+        cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
+                               'backends': {l_unit: addr}}
+        for rid in relation_ids('cluster'):
+            for unit in related_units(rid):
+                _laddr = relation_get('private-address',
+                                      rid=rid, unit=unit)
+                if _laddr:
+                    _unit = unit.replace('/', '-')
+                    cluster_hosts[addr]['backends'][_unit] = _laddr
 
-        ctxt = {'frontends': cluster_hosts}
+        ctxt = {
+            'frontends': cluster_hosts,
+            'default_backend': addr
+        }
 
         if config('haproxy-server-timeout'):
             ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
@@ -491,6 +495,7 @@ class HAProxyContext(OSContextGenerator):
             ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
 
         if config('prefer-ipv6'):
+            ctxt['ipv6'] = True
             ctxt['local_host'] = 'ip6-localhost'
             ctxt['haproxy_host'] = '::'
             ctxt['stat_port'] = ':::8888'
@@ -662,8 +667,9 @@ class ApacheSSLContext(OSContextGenerator):
         addresses = self.get_network_addresses()
         for address, endpoint in sorted(set(addresses)):
             for api_port in self.external_ports:
-                ext_port = determine_apache_port(api_port)
-                int_port = determine_api_port(api_port)
+                ext_port = determine_apache_port(api_port,
+                                                 singlenode_mode=True)
+                int_port = determine_api_port(api_port, singlenode_mode=True)
                 portmap = (address, endpoint, int(ext_port), int(int_port))
                 ctxt['endpoints'].append(portmap)
                 ctxt['ext_ports'].append(int(ext_port))
diff --git a/hooks/charmhelpers/contrib/openstack/neutron.py b/hooks/charmhelpers/contrib/openstack/neutron.py
index 1446f637..095cc24b 100644
--- a/hooks/charmhelpers/contrib/openstack/neutron.py
+++ b/hooks/charmhelpers/contrib/openstack/neutron.py
@@ -152,9 +152,15 @@ def neutron_plugins():
                                         database=config('neutron-database'),
                                         relation_prefix='neutron',
                                         ssl_dir=NEUTRON_CONF_DIR)],
-            'services': ['calico-compute', 'bird', 'neutron-dhcp-agent'],
+            'services': ['calico-felix',
+                         'bird',
+                         'neutron-dhcp-agent',
+                         'nova-api-metadata'],
             'packages': [[headers_package()] + determine_dkms_package(),
-                         ['calico-compute', 'bird', 'neutron-dhcp-agent']],
+                         ['calico-compute',
+                          'bird',
+                          'neutron-dhcp-agent',
+                          'nova-api-metadata']],
             'server_packages': ['neutron-server', 'calico-control'],
             'server_services': ['neutron-server']
         }
diff --git a/hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg b/hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg
index 0229f9d4..ad875f16 100644
--- a/hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg
+++ b/hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg
@@ -38,11 +38,15 @@ listen stats {{ stat_port }}
 {% for service, ports in service_ports.items() -%}
 frontend tcp-in_{{ service }}
     bind *:{{ ports[0] }}
+    {% if ipv6 -%}
     bind :::{{ ports[0] }}
+    {% endif -%}
     {% for frontend in frontends -%}
     acl net_{{ frontend }} dst {{ frontends[frontend]['network'] }}
     use_backend {{ service }}_{{ frontend }} if net_{{ frontend }}
-    {% endfor %}
+    {% endfor -%}
+    default_backend {{ service }}_{{ default_backend }}
+
 {% for frontend in frontends -%}
 backend {{ service }}_{{ frontend }}
     balance leastconn
diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py
index 44179679..ddd40ce5 100644
--- a/hooks/charmhelpers/contrib/openstack/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/utils.py
@@ -53,6 +53,7 @@ UBUNTU_OPENSTACK_RELEASE = OrderedDict([
     ('saucy', 'havana'),
     ('trusty', 'icehouse'),
     ('utopic', 'juno'),
+    ('vivid', 'kilo'),
 ])
 
 
@@ -64,6 +65,7 @@ OPENSTACK_CODENAMES = OrderedDict([
     ('2013.2', 'havana'),
     ('2014.1', 'icehouse'),
     ('2014.2', 'juno'),
+    ('2015.1', 'kilo'),
 ])
 
 # The ugly duckling
@@ -84,6 +86,7 @@ SWIFT_CODENAMES = OrderedDict([
     ('2.0.0', 'juno'),
     ('2.1.0', 'juno'),
     ('2.2.0', 'juno'),
+    ('2.2.1', 'kilo'),
 ])
 
 DEFAULT_LOOPBACK_SIZE = '5G'
@@ -289,6 +292,9 @@ def configure_installation_source(rel):
             'juno': 'trusty-updates/juno',
             'juno/updates': 'trusty-updates/juno',
             'juno/proposed': 'trusty-proposed/juno',
+            'kilo': 'trusty-updates/kilo',
+            'kilo/updates': 'trusty-updates/kilo',
+            'kilo/proposed': 'trusty-proposed/kilo',
         }
 
         try:
diff --git a/hooks/charmhelpers/contrib/storage/linux/ceph.py b/hooks/charmhelpers/contrib/storage/linux/ceph.py
index d47dc228..6ebeab5c 100644
--- a/hooks/charmhelpers/contrib/storage/linux/ceph.py
+++ b/hooks/charmhelpers/contrib/storage/linux/ceph.py
@@ -157,6 +157,17 @@ def create_keyring(service, key):
     log('Created new ceph keyring at %s.' % keyring, level=DEBUG)
 
 
+def delete_keyring(service):
+    """Delete an existing Ceph keyring."""
+    keyring = _keyring_path(service)
+    if not os.path.exists(keyring):
+        log('Keyring does not exist at %s' % keyring, level=WARNING)
+        return
+
+    os.remove(keyring)
+    log('Deleted ring at %s.' % keyring, level=INFO)
+
+
 def create_key_file(service, key):
     """Create a file containing key."""
     keyfile = _keyfile_path(service)
@@ -372,3 +383,46 @@ def ceph_version():
             return None
     else:
         return None
+
+
+class CephBrokerRq(object):
+    """Ceph broker request.
+
+    Multiple operations can be added to a request and sent to the Ceph broker
+    to be executed.
+
+    Request is json-encoded for sending over the wire.
+
+    The API is versioned and defaults to version 1.
+    """
+    def __init__(self, api_version=1):
+        self.api_version = api_version
+        self.ops = []
+
+    def add_op_create_pool(self, name, replica_count=3):
+        self.ops.append({'op': 'create-pool', 'name': name,
+                         'replicas': replica_count})
+
+    @property
+    def request(self):
+        return json.dumps({'api-version': self.api_version, 'ops': self.ops})
+
+
+class CephBrokerRsp(object):
+    """Ceph broker response.
+
+    Response is json-decoded and contents provided as methods/properties.
+
+    The API is versioned and defaults to version 1.
+    """
+    def __init__(self, encoded_rsp):
+        self.api_version = None
+        self.rsp = json.loads(encoded_rsp)
+
+    @property
+    def exit_code(self):
+        return self.rsp.get('exit-code')
+
+    @property
+    def exit_msg(self):
+        return self.rsp.get('stderr')
diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py
index c6f1680a..5221120c 100644
--- a/hooks/charmhelpers/core/host.py
+++ b/hooks/charmhelpers/core/host.py
@@ -162,13 +162,16 @@ def mkdir(path, owner='root', group='root', perms=0o555, force=False):
     uid = pwd.getpwnam(owner).pw_uid
     gid = grp.getgrnam(group).gr_gid
     realpath = os.path.abspath(path)
-    if os.path.exists(realpath):
-        if force and not os.path.isdir(realpath):
+    path_exists = os.path.exists(realpath)
+    if path_exists and force:
+        if not os.path.isdir(realpath):
             log("Removing non-directory file {} prior to mkdir()".format(path))
             os.unlink(realpath)
-    else:
+            os.makedirs(realpath, perms)
+        os.chown(realpath, uid, gid)
+    elif not path_exists:
         os.makedirs(realpath, perms)
-    os.chown(realpath, uid, gid)
+        os.chown(realpath, uid, gid)
 
 
 def write_file(path, content, owner='root', group='root', perms=0o444):
diff --git a/hooks/charmhelpers/fetch/__init__.py b/hooks/charmhelpers/fetch/__init__.py
index 0a126fc3..aceadea4 100644
--- a/hooks/charmhelpers/fetch/__init__.py
+++ b/hooks/charmhelpers/fetch/__init__.py
@@ -64,9 +64,16 @@ CLOUD_ARCHIVE_POCKETS = {
     'trusty-juno/updates': 'trusty-updates/juno',
     'trusty-updates/juno': 'trusty-updates/juno',
     'juno/proposed': 'trusty-proposed/juno',
-    'juno/proposed': 'trusty-proposed/juno',
     'trusty-juno/proposed': 'trusty-proposed/juno',
     'trusty-proposed/juno': 'trusty-proposed/juno',
+    # Kilo
+    'kilo': 'trusty-updates/kilo',
+    'trusty-kilo': 'trusty-updates/kilo',
+    'trusty-kilo/updates': 'trusty-updates/kilo',
+    'trusty-updates/kilo': 'trusty-updates/kilo',
+    'kilo/proposed': 'trusty-proposed/kilo',
+    'trusty-kilo/proposed': 'trusty-proposed/kilo',
+    'trusty-proposed/kilo': 'trusty-proposed/kilo',
 }
 
 # The order of this list is very important. Handlers should be listed in from

From 564880ebd81b1b67b7a0d27aa8044aebdcfe2622 Mon Sep 17 00:00:00 2001
From: James Page <james.page@ubuntu.com>
Date: Wed, 14 Jan 2015 09:24:12 +0000
Subject: [PATCH 02/16] [trivial] Add missing file

---
 hooks/charmhelpers/core/decorators.py | 41 +++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)
 create mode 100644 hooks/charmhelpers/core/decorators.py

diff --git a/hooks/charmhelpers/core/decorators.py b/hooks/charmhelpers/core/decorators.py
new file mode 100644
index 00000000..029a4ef4
--- /dev/null
+++ b/hooks/charmhelpers/core/decorators.py
@@ -0,0 +1,41 @@
+#
+# Copyright 2014 Canonical Ltd.
+#
+# Authors:
+#  Edward Hope-Morley <opentastic@gmail.com>
+#
+
+import time
+
+from charmhelpers.core.hookenv import (
+    log,
+    INFO,
+)
+
+
+def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
+    """If the decorated function raises exception exc_type, allow num_retries
+    retry attempts before raise the exception.
+    """
+    def _retry_on_exception_inner_1(f):
+        def _retry_on_exception_inner_2(*args, **kwargs):
+            retries = num_retries
+            multiplier = 1
+            while True:
+                try:
+                    return f(*args, **kwargs)
+                except exc_type:
+                    if not retries:
+                        raise
+
+                delay = base_delay * multiplier
+                multiplier += 1
+                log("Retrying '%s' %d more times (delay=%s)" %
+                    (f.__name__, retries, delay), level=INFO)
+                retries -= 1
+                if delay:
+                    time.sleep(delay)
+
+        return _retry_on_exception_inner_2
+
+    return _retry_on_exception_inner_1

From 1ac86b2067738ee6719163cdaeb0d4f17ce7641e Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Mon, 26 Jan 2015 09:42:44 +0000
Subject: [PATCH 03/16] [gnuoy,trivial] Pre-release charmhelper sync

---
 hooks/charmhelpers/__init__.py                | 16 +++++++++
 hooks/charmhelpers/contrib/__init__.py        | 15 ++++++++
 .../contrib/hahelpers/__init__.py             | 15 ++++++++
 .../charmhelpers/contrib/hahelpers/apache.py  | 16 +++++++++
 .../charmhelpers/contrib/hahelpers/cluster.py | 22 +++++++++++-
 .../charmhelpers/contrib/network/__init__.py  | 15 ++++++++
 hooks/charmhelpers/contrib/network/ip.py      | 16 +++++++++
 .../contrib/network/ovs/__init__.py           | 16 +++++++++
 .../contrib/openstack/__init__.py             | 15 ++++++++
 .../contrib/openstack/alternatives.py         | 16 +++++++++
 .../contrib/openstack/amulet/__init__.py      | 15 ++++++++
 .../contrib/openstack/amulet/deployment.py    | 16 +++++++++
 .../contrib/openstack/amulet/utils.py         | 16 +++++++++
 .../charmhelpers/contrib/openstack/context.py | 16 +++++++++
 hooks/charmhelpers/contrib/openstack/ip.py    | 16 +++++++++
 .../charmhelpers/contrib/openstack/neutron.py | 16 +++++++++
 .../contrib/openstack/templates/__init__.py   | 16 +++++++++
 .../contrib/openstack/templating.py           | 16 +++++++++
 hooks/charmhelpers/contrib/openstack/utils.py | 16 +++++++++
 hooks/charmhelpers/contrib/python/__init__.py | 15 ++++++++
 hooks/charmhelpers/contrib/python/packages.py | 21 ++++++++++-
 .../charmhelpers/contrib/storage/__init__.py  | 15 ++++++++
 .../contrib/storage/linux/__init__.py         | 15 ++++++++
 .../contrib/storage/linux/ceph.py             | 16 +++++++++
 .../contrib/storage/linux/loopback.py         | 16 +++++++++
 .../charmhelpers/contrib/storage/linux/lvm.py | 16 +++++++++
 .../contrib/storage/linux/utils.py            | 16 +++++++++
 hooks/charmhelpers/core/__init__.py           | 15 ++++++++
 hooks/charmhelpers/core/decorators.py         | 16 +++++++++
 hooks/charmhelpers/core/fstab.py              | 16 +++++++++
 hooks/charmhelpers/core/hookenv.py            | 16 +++++++++
 hooks/charmhelpers/core/host.py               | 35 ++++++++++++++++---
 hooks/charmhelpers/core/services/__init__.py  | 16 +++++++++
 hooks/charmhelpers/core/services/base.py      | 16 +++++++++
 hooks/charmhelpers/core/services/helpers.py   | 16 +++++++++
 hooks/charmhelpers/core/sysctl.py             | 16 +++++++++
 hooks/charmhelpers/core/templating.py         | 16 +++++++++
 hooks/charmhelpers/fetch/__init__.py          | 16 +++++++++
 hooks/charmhelpers/fetch/archiveurl.py        | 16 +++++++++
 hooks/charmhelpers/fetch/bzrurl.py            | 26 +++++++++++++-
 hooks/charmhelpers/fetch/giturl.py            | 20 +++++++++++
 hooks/charmhelpers/payload/__init__.py        | 16 +++++++++
 hooks/charmhelpers/payload/execd.py           | 16 +++++++++
 43 files changed, 716 insertions(+), 7 deletions(-)

diff --git a/hooks/charmhelpers/__init__.py b/hooks/charmhelpers/__init__.py
index b46e2e23..f72e7f84 100644
--- a/hooks/charmhelpers/__init__.py
+++ b/hooks/charmhelpers/__init__.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 # Bootstrap charm-helpers, installing its dependencies if necessary using
 # only standard libraries.
 import subprocess
diff --git a/hooks/charmhelpers/contrib/__init__.py b/hooks/charmhelpers/contrib/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/contrib/__init__.py
+++ b/hooks/charmhelpers/contrib/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/contrib/hahelpers/__init__.py b/hooks/charmhelpers/contrib/hahelpers/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/contrib/hahelpers/__init__.py
+++ b/hooks/charmhelpers/contrib/hahelpers/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/contrib/hahelpers/apache.py b/hooks/charmhelpers/contrib/hahelpers/apache.py
index 6616ffff..00917195 100644
--- a/hooks/charmhelpers/contrib/hahelpers/apache.py
+++ b/hooks/charmhelpers/contrib/hahelpers/apache.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 #
 # Copyright 2012 Canonical Ltd.
 #
diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py
index 912b2fe3..9a2588b6 100644
--- a/hooks/charmhelpers/contrib/hahelpers/cluster.py
+++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 #
 # Copyright 2012 Canonical Ltd.
 #
@@ -205,19 +221,23 @@ def determine_apache_port(public_port, singlenode_mode=False):
     return public_port - (i * 10)
 
 
-def get_hacluster_config():
+def get_hacluster_config(exclude_keys=None):
     '''
     Obtains all relevant configuration from charm configuration required
     for initiating a relation to hacluster:
 
         ha-bindiface, ha-mcastport, vip
 
+    param: exclude_keys: list of setting key(s) to be excluded.
     returns: dict: A dict containing settings keyed by setting name.
     raises: HAIncompleteConfig if settings are missing.
     '''
     settings = ['ha-bindiface', 'ha-mcastport', 'vip']
     conf = {}
     for setting in settings:
+        if exclude_keys and setting in exclude_keys:
+            continue
+
         conf[setting] = config_get(setting)
     missing = []
     [missing.append(s) for s, v in six.iteritems(conf) if v is None]
diff --git a/hooks/charmhelpers/contrib/network/__init__.py b/hooks/charmhelpers/contrib/network/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/contrib/network/__init__.py
+++ b/hooks/charmhelpers/contrib/network/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/contrib/network/ip.py b/hooks/charmhelpers/contrib/network/ip.py
index 8dc83165..98b17544 100644
--- a/hooks/charmhelpers/contrib/network/ip.py
+++ b/hooks/charmhelpers/contrib/network/ip.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import glob
 import re
 import subprocess
diff --git a/hooks/charmhelpers/contrib/network/ovs/__init__.py b/hooks/charmhelpers/contrib/network/ovs/__init__.py
index 8f8a5230..77e2db7f 100644
--- a/hooks/charmhelpers/contrib/network/ovs/__init__.py
+++ b/hooks/charmhelpers/contrib/network/ovs/__init__.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 ''' Helpers for interacting with OpenvSwitch '''
 import subprocess
 import os
diff --git a/hooks/charmhelpers/contrib/openstack/__init__.py b/hooks/charmhelpers/contrib/openstack/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/contrib/openstack/__init__.py
+++ b/hooks/charmhelpers/contrib/openstack/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/contrib/openstack/alternatives.py b/hooks/charmhelpers/contrib/openstack/alternatives.py
index b413259c..ef77caf3 100644
--- a/hooks/charmhelpers/contrib/openstack/alternatives.py
+++ b/hooks/charmhelpers/contrib/openstack/alternatives.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 ''' Helper for managing alternatives for file conflict resolution '''
 
 import subprocess
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/__init__.py b/hooks/charmhelpers/contrib/openstack/amulet/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/__init__.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
index f3fee074..c50d3ec6 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import six
 from charmhelpers.contrib.amulet.deployment import (
     AmuletDeployment
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/utils.py b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
index 3e0cc61c..9c3d918a 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import logging
 import os
 import time
diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py
index eaa89a67..c7c4cd4a 100644
--- a/hooks/charmhelpers/contrib/openstack/context.py
+++ b/hooks/charmhelpers/contrib/openstack/context.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import json
 import os
 import time
diff --git a/hooks/charmhelpers/contrib/openstack/ip.py b/hooks/charmhelpers/contrib/openstack/ip.py
index f062c807..9eabed73 100644
--- a/hooks/charmhelpers/contrib/openstack/ip.py
+++ b/hooks/charmhelpers/contrib/openstack/ip.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 from charmhelpers.core.hookenv import (
     config,
     unit_get,
diff --git a/hooks/charmhelpers/contrib/openstack/neutron.py b/hooks/charmhelpers/contrib/openstack/neutron.py
index 095cc24b..902757fe 100644
--- a/hooks/charmhelpers/contrib/openstack/neutron.py
+++ b/hooks/charmhelpers/contrib/openstack/neutron.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 # Various utilies for dealing with Neutron and the renaming from Quantum.
 
 from subprocess import check_output
diff --git a/hooks/charmhelpers/contrib/openstack/templates/__init__.py b/hooks/charmhelpers/contrib/openstack/templates/__init__.py
index 0b49ad28..75876796 100644
--- a/hooks/charmhelpers/contrib/openstack/templates/__init__.py
+++ b/hooks/charmhelpers/contrib/openstack/templates/__init__.py
@@ -1,2 +1,18 @@
+# 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/>.
+
 # dummy __init__.py to fool syncer into thinking this is a syncable python
 # module
diff --git a/hooks/charmhelpers/contrib/openstack/templating.py b/hooks/charmhelpers/contrib/openstack/templating.py
index 33df0675..24cb272b 100644
--- a/hooks/charmhelpers/contrib/openstack/templating.py
+++ b/hooks/charmhelpers/contrib/openstack/templating.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 
 import six
diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py
index ddd40ce5..26259a03 100644
--- a/hooks/charmhelpers/contrib/openstack/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/utils.py
@@ -1,5 +1,21 @@
 #!/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/>.
+
 # Common python helper functions used for OpenStack charms.
 from collections import OrderedDict
 from functools import wraps
diff --git a/hooks/charmhelpers/contrib/python/__init__.py b/hooks/charmhelpers/contrib/python/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/contrib/python/__init__.py
+++ b/hooks/charmhelpers/contrib/python/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/contrib/python/packages.py b/hooks/charmhelpers/contrib/python/packages.py
index 78162b1b..d848a120 100644
--- a/hooks/charmhelpers/contrib/python/packages.py
+++ b/hooks/charmhelpers/contrib/python/packages.py
@@ -1,6 +1,22 @@
 #!/usr/bin/env python
 # coding: utf-8
 
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 __author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
 
 from charmhelpers.fetch import apt_install, apt_update
@@ -35,7 +51,7 @@ def pip_install_requirements(requirements, **options):
     pip_execute(command)
 
 
-def pip_install(package, fatal=False, **options):
+def pip_install(package, fatal=False, upgrade=False, **options):
     """Install a python package"""
     command = ["install"]
 
@@ -43,6 +59,9 @@ def pip_install(package, fatal=False, **options):
     for option in parse_options(options, available_options):
         command.append(option)
 
+    if upgrade:
+        command.append('--upgrade')
+
     if isinstance(package, list):
         command.extend(package)
     else:
diff --git a/hooks/charmhelpers/contrib/storage/__init__.py b/hooks/charmhelpers/contrib/storage/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/contrib/storage/__init__.py
+++ b/hooks/charmhelpers/contrib/storage/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/contrib/storage/linux/__init__.py b/hooks/charmhelpers/contrib/storage/linux/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/contrib/storage/linux/__init__.py
+++ b/hooks/charmhelpers/contrib/storage/linux/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/contrib/storage/linux/ceph.py b/hooks/charmhelpers/contrib/storage/linux/ceph.py
index 6ebeab5c..31ea7f9e 100644
--- a/hooks/charmhelpers/contrib/storage/linux/ceph.py
+++ b/hooks/charmhelpers/contrib/storage/linux/ceph.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 #
 # Copyright 2012 Canonical Ltd.
 #
diff --git a/hooks/charmhelpers/contrib/storage/linux/loopback.py b/hooks/charmhelpers/contrib/storage/linux/loopback.py
index a22c3d7b..c296f098 100644
--- a/hooks/charmhelpers/contrib/storage/linux/loopback.py
+++ b/hooks/charmhelpers/contrib/storage/linux/loopback.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 import re
 from subprocess import (
diff --git a/hooks/charmhelpers/contrib/storage/linux/lvm.py b/hooks/charmhelpers/contrib/storage/linux/lvm.py
index 0aa65f4f..34b5f71a 100644
--- a/hooks/charmhelpers/contrib/storage/linux/lvm.py
+++ b/hooks/charmhelpers/contrib/storage/linux/lvm.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 from subprocess import (
     CalledProcessError,
     check_call,
diff --git a/hooks/charmhelpers/contrib/storage/linux/utils.py b/hooks/charmhelpers/contrib/storage/linux/utils.py
index c6a15e14..c8373b72 100644
--- a/hooks/charmhelpers/contrib/storage/linux/utils.py
+++ b/hooks/charmhelpers/contrib/storage/linux/utils.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 import re
 from stat import S_ISBLK
diff --git a/hooks/charmhelpers/core/__init__.py b/hooks/charmhelpers/core/__init__.py
index e69de29b..d1400a02 100644
--- a/hooks/charmhelpers/core/__init__.py
+++ b/hooks/charmhelpers/core/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/hooks/charmhelpers/core/decorators.py b/hooks/charmhelpers/core/decorators.py
index 029a4ef4..bb05620b 100644
--- a/hooks/charmhelpers/core/decorators.py
+++ b/hooks/charmhelpers/core/decorators.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 #
 # Copyright 2014 Canonical Ltd.
 #
diff --git a/hooks/charmhelpers/core/fstab.py b/hooks/charmhelpers/core/fstab.py
index 0adf0db3..be7de248 100644
--- a/hooks/charmhelpers/core/fstab.py
+++ b/hooks/charmhelpers/core/fstab.py
@@ -1,6 +1,22 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
 
 import io
diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py
index 69ae4564..cf552b39 100644
--- a/hooks/charmhelpers/core/hookenv.py
+++ b/hooks/charmhelpers/core/hookenv.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 "Interactions with the Juju environment"
 # Copyright 2013 Canonical Ltd.
 #
diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py
index 5221120c..cf2cbe14 100644
--- a/hooks/charmhelpers/core/host.py
+++ b/hooks/charmhelpers/core/host.py
@@ -1,3 +1,19 @@
+# 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/>.
+
 """Tools for working with the host system"""
 # Copyright 2012 Canonical Ltd.
 #
@@ -168,10 +184,10 @@ def mkdir(path, owner='root', group='root', perms=0o555, force=False):
             log("Removing non-directory file {} prior to mkdir()".format(path))
             os.unlink(realpath)
             os.makedirs(realpath, perms)
-        os.chown(realpath, uid, gid)
     elif not path_exists:
         os.makedirs(realpath, perms)
-        os.chown(realpath, uid, gid)
+    os.chown(realpath, uid, gid)
+    os.chmod(realpath, perms)
 
 
 def write_file(path, content, owner='root', group='root', perms=0o444):
@@ -389,6 +405,9 @@ def cmp_pkgrevno(package, revno, pkgcache=None):
     *  0 => Installed revno is the same as supplied arg
     * -1 => Installed revno is less than supplied arg
 
+    This function imports apt_cache function from charmhelpers.fetch if
+    the pkgcache argument is None. Be sure to add charmhelpers.fetch if
+    you call this function, or pass an apt_pkg.Cache() instance.
     '''
     import apt_pkg
     if not pkgcache:
@@ -407,13 +426,21 @@ def chdir(d):
         os.chdir(cur)
 
 
-def chownr(path, owner, group):
+def chownr(path, owner, group, follow_links=True):
     uid = pwd.getpwnam(owner).pw_uid
     gid = grp.getgrnam(group).gr_gid
+    if follow_links:
+        chown = os.chown
+    else:
+        chown = os.lchown
 
     for root, dirs, files in os.walk(path):
         for name in dirs + files:
             full = os.path.join(root, name)
             broken_symlink = os.path.lexists(full) and not os.path.exists(full)
             if not broken_symlink:
-                os.chown(full, uid, gid)
+                chown(full, uid, gid)
+
+
+def lchownr(path, owner, group):
+    chownr(path, owner, group, follow_links=False)
diff --git a/hooks/charmhelpers/core/services/__init__.py b/hooks/charmhelpers/core/services/__init__.py
index 69dde79a..0928158b 100644
--- a/hooks/charmhelpers/core/services/__init__.py
+++ b/hooks/charmhelpers/core/services/__init__.py
@@ -1,2 +1,18 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 from .base import *  # NOQA
 from .helpers import *  # NOQA
diff --git a/hooks/charmhelpers/core/services/base.py b/hooks/charmhelpers/core/services/base.py
index 87ecb130..c5534e4c 100644
--- a/hooks/charmhelpers/core/services/base.py
+++ b/hooks/charmhelpers/core/services/base.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 import re
 import json
diff --git a/hooks/charmhelpers/core/services/helpers.py b/hooks/charmhelpers/core/services/helpers.py
index 163a7932..5e3af9da 100644
--- a/hooks/charmhelpers/core/services/helpers.py
+++ b/hooks/charmhelpers/core/services/helpers.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 import yaml
 from charmhelpers.core import hookenv
diff --git a/hooks/charmhelpers/core/sysctl.py b/hooks/charmhelpers/core/sysctl.py
index 0f299630..d642a371 100644
--- a/hooks/charmhelpers/core/sysctl.py
+++ b/hooks/charmhelpers/core/sysctl.py
@@ -1,6 +1,22 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
 
 import yaml
diff --git a/hooks/charmhelpers/core/templating.py b/hooks/charmhelpers/core/templating.py
index 569eaed6..97669092 100644
--- a/hooks/charmhelpers/core/templating.py
+++ b/hooks/charmhelpers/core/templating.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 
 from charmhelpers.core import host
diff --git a/hooks/charmhelpers/fetch/__init__.py b/hooks/charmhelpers/fetch/__init__.py
index aceadea4..792e629a 100644
--- a/hooks/charmhelpers/fetch/__init__.py
+++ b/hooks/charmhelpers/fetch/__init__.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import importlib
 from tempfile import NamedTemporaryFile
 import time
diff --git a/hooks/charmhelpers/fetch/archiveurl.py b/hooks/charmhelpers/fetch/archiveurl.py
index 8a4624b2..d25a0ddd 100644
--- a/hooks/charmhelpers/fetch/archiveurl.py
+++ b/hooks/charmhelpers/fetch/archiveurl.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 import hashlib
 import re
diff --git a/hooks/charmhelpers/fetch/bzrurl.py b/hooks/charmhelpers/fetch/bzrurl.py
index 8ef48f30..3531315a 100644
--- a/hooks/charmhelpers/fetch/bzrurl.py
+++ b/hooks/charmhelpers/fetch/bzrurl.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 from charmhelpers.fetch import (
     BaseFetchHandler,
@@ -11,10 +27,12 @@ if six.PY3:
 
 try:
     from bzrlib.branch import Branch
+    from bzrlib import bzrdir, workingtree, errors
 except ImportError:
     from charmhelpers.fetch import apt_install
     apt_install("python-bzrlib")
     from bzrlib.branch import Branch
+    from bzrlib import bzrdir, workingtree, errors
 
 
 class BzrUrlFetchHandler(BaseFetchHandler):
@@ -34,9 +52,15 @@ class BzrUrlFetchHandler(BaseFetchHandler):
         if url_parts.scheme == "lp":
             from bzrlib.plugin import load_plugins
             load_plugins()
+        try:
+            local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
+        except errors.AlreadyControlDirError:
+            local_branch = Branch.open(dest)
         try:
             remote_branch = Branch.open(source)
-            remote_branch.bzrdir.sprout(dest).open_branch()
+            remote_branch.push(local_branch)
+            tree = workingtree.WorkingTree.open(dest)
+            tree.update()
         except Exception as e:
             raise e
 
diff --git a/hooks/charmhelpers/fetch/giturl.py b/hooks/charmhelpers/fetch/giturl.py
index f3aa2821..5376786b 100644
--- a/hooks/charmhelpers/fetch/giturl.py
+++ b/hooks/charmhelpers/fetch/giturl.py
@@ -1,3 +1,19 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
 from charmhelpers.fetch import (
     BaseFetchHandler,
@@ -16,6 +32,8 @@ except ImportError:
     apt_install("python-git")
     from git import Repo
 
+from git.exc import GitCommandError
+
 
 class GitUrlFetchHandler(BaseFetchHandler):
     """Handler for git branches via generic and github URLs"""
@@ -46,6 +64,8 @@ class GitUrlFetchHandler(BaseFetchHandler):
             mkdir(dest_dir, perms=0o755)
         try:
             self.clone(source, dest_dir, branch)
+        except GitCommandError as e:
+            raise UnhandledSource(e.message)
         except OSError as e:
             raise UnhandledSource(e.strerror)
         return dest_dir
diff --git a/hooks/charmhelpers/payload/__init__.py b/hooks/charmhelpers/payload/__init__.py
index fc9fbc08..e6f42497 100644
--- a/hooks/charmhelpers/payload/__init__.py
+++ b/hooks/charmhelpers/payload/__init__.py
@@ -1 +1,17 @@
+# 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/>.
+
 "Tools for working with files injected into a charm just before deployment."
diff --git a/hooks/charmhelpers/payload/execd.py b/hooks/charmhelpers/payload/execd.py
index 6476a75f..4d4d81a6 100644
--- a/hooks/charmhelpers/payload/execd.py
+++ b/hooks/charmhelpers/payload/execd.py
@@ -1,5 +1,21 @@
 #!/usr/bin/env 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/>.
+
 import os
 import sys
 import subprocess

From db2a534dd82caf03efe596dfd2f28d0473b9bb15 Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Wed, 28 Jan 2015 09:44:49 +0000
Subject: [PATCH 04/16] Supply ssl dir for amqp setup

---
 hooks/neutron_ovs_utils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hooks/neutron_ovs_utils.py b/hooks/neutron_ovs_utils.py
index 367020f9..b5d742de 100644
--- a/hooks/neutron_ovs_utils.py
+++ b/hooks/neutron_ovs_utils.py
@@ -18,7 +18,7 @@ BASE_RESOURCE_MAP = OrderedDict([
     (NEUTRON_CONF, {
         'services': ['neutron-plugin-openvswitch-agent'],
         'contexts': [neutron_ovs_context.OVSPluginContext(),
-                     context.AMQPContext()],
+                     context.AMQPContext(ssl_dir=NEUTRON_CONF_DIR)],
     }),
     (ML2_CONF, {
         'services': ['neutron-plugin-openvswitch-agent'],

From a8e42cc1294e28de8b47acc7b1c1aa20197c78fb Mon Sep 17 00:00:00 2001
From: Edward Hope-Morley <edward.hope-morley@canonical.com>
Date: Wed, 11 Feb 2015 12:37:28 +0000
Subject: [PATCH 05/16] charm-helpers sync

---
 hooks/charmhelpers/core/host.py       |  10 +-
 hooks/charmhelpers/core/sysctl.py     |  16 +-
 hooks/charmhelpers/core/templating.py |   6 +-
 hooks/charmhelpers/core/unitdata.py   | 477 ++++++++++++++++++++++++++
 4 files changed, 496 insertions(+), 13 deletions(-)
 create mode 100644 hooks/charmhelpers/core/unitdata.py

diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py
index cf2cbe14..b771c611 100644
--- a/hooks/charmhelpers/core/host.py
+++ b/hooks/charmhelpers/core/host.py
@@ -191,11 +191,11 @@ def mkdir(path, owner='root', group='root', perms=0o555, force=False):
 
 
 def write_file(path, content, owner='root', group='root', perms=0o444):
-    """Create or overwrite a file with the contents of a string"""
+    """Create or overwrite a file with the contents of a byte string."""
     log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
     uid = pwd.getpwnam(owner).pw_uid
     gid = grp.getgrnam(group).gr_gid
-    with open(path, 'w') as target:
+    with open(path, 'wb') as target:
         os.fchown(target.fileno(), uid, gid)
         os.fchmod(target.fileno(), perms)
         target.write(content)
@@ -305,11 +305,11 @@ def restart_on_change(restart_map, stopstart=False):
     ceph_client_changed function.
     """
     def wrap(f):
-        def wrapped_f(*args):
+        def wrapped_f(*args, **kwargs):
             checksums = {}
             for path in restart_map:
                 checksums[path] = file_hash(path)
-            f(*args)
+            f(*args, **kwargs)
             restarts = []
             for path in restart_map:
                 if checksums[path] != file_hash(path):
@@ -361,7 +361,7 @@ def list_nics(nic_type):
         ip_output = (line for line in ip_output if line)
         for line in ip_output:
             if line.split()[1].startswith(int_type):
-                matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
+                matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
                 if matched:
                     interface = matched.groups()[0]
                 else:
diff --git a/hooks/charmhelpers/core/sysctl.py b/hooks/charmhelpers/core/sysctl.py
index d642a371..8e1b9eeb 100644
--- a/hooks/charmhelpers/core/sysctl.py
+++ b/hooks/charmhelpers/core/sysctl.py
@@ -26,25 +26,31 @@ from subprocess import check_call
 from charmhelpers.core.hookenv import (
     log,
     DEBUG,
+    ERROR,
 )
 
 
 def create(sysctl_dict, sysctl_file):
     """Creates a sysctl.conf file from a YAML associative array
 
-    :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
-    :type sysctl_dict: dict
+    :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
+    :type sysctl_dict: str
     :param sysctl_file: path to the sysctl file to be saved
     :type sysctl_file: str or unicode
     :returns: None
     """
-    sysctl_dict = yaml.load(sysctl_dict)
+    try:
+        sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
+    except yaml.YAMLError:
+        log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
+            level=ERROR)
+        return
 
     with open(sysctl_file, "w") as fd:
-        for key, value in sysctl_dict.items():
+        for key, value in sysctl_dict_parsed.items():
             fd.write("{}={}\n".format(key, value))
 
-    log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
+    log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
         level=DEBUG)
 
     check_call(["sysctl", "-p", sysctl_file])
diff --git a/hooks/charmhelpers/core/templating.py b/hooks/charmhelpers/core/templating.py
index 97669092..45319998 100644
--- a/hooks/charmhelpers/core/templating.py
+++ b/hooks/charmhelpers/core/templating.py
@@ -21,7 +21,7 @@ from charmhelpers.core import hookenv
 
 
 def render(source, target, context, owner='root', group='root',
-           perms=0o444, templates_dir=None):
+           perms=0o444, templates_dir=None, encoding='UTF-8'):
     """
     Render a template.
 
@@ -64,5 +64,5 @@ def render(source, target, context, owner='root', group='root',
                     level=hookenv.ERROR)
         raise e
     content = template.render(context)
-    host.mkdir(os.path.dirname(target), owner, group)
-    host.write_file(target, content, owner, group, perms)
+    host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
+    host.write_file(target, content.encode(encoding), owner, group, perms)
diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py
new file mode 100644
index 00000000..01329ab7
--- /dev/null
+++ b/hooks/charmhelpers/core/unitdata.py
@@ -0,0 +1,477 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+# Authors:
+#  Kapil Thangavelu <kapil.foss@gmail.com>
+#
+"""
+Intro
+-----
+
+A simple way to store state in units. This provides a key value
+storage with support for versioned, transactional operation,
+and can calculate deltas from previous values to simplify unit logic
+when processing changes.
+
+
+Hook Integration
+----------------
+
+There are several extant frameworks for hook execution, including
+
+ - charmhelpers.core.hookenv.Hooks
+ - charmhelpers.core.services.ServiceManager
+
+The storage classes are framework agnostic, one simple integration is
+via the HookData contextmanager. It will record the current hook
+execution environment (including relation data, config data, etc.),
+setup a transaction and allow easy access to the changes from
+previously seen values. One consequence of the integration is the
+reservation of particular keys ('rels', 'unit', 'env', 'config',
+'charm_revisions') for their respective values.
+
+Here's a fully worked integration example using hookenv.Hooks::
+
+       from charmhelper.core import hookenv, unitdata
+
+       hook_data = unitdata.HookData()
+       db = unitdata.kv()
+       hooks = hookenv.Hooks()
+
+       @hooks.hook
+       def config_changed():
+           # Print all changes to configuration from previously seen
+           # values.
+           for changed, (prev, cur) in hook_data.conf.items():
+               print('config changed', changed,
+                     'previous value', prev,
+                     'current value',  cur)
+
+           # Get some unit specific bookeeping
+           if not db.get('pkg_key'):
+               key = urllib.urlopen('https://example.com/pkg_key').read()
+               db.set('pkg_key', key)
+
+           # Directly access all charm config as a mapping.
+           conf = db.getrange('config', True)
+
+           # Directly access all relation data as a mapping
+           rels = db.getrange('rels', True)
+
+       if __name__ == '__main__':
+           with hook_data():
+               hook.execute()
+
+
+A more basic integration is via the hook_scope context manager which simply
+manages transaction scope (and records hook name, and timestamp)::
+
+  >>> from unitdata import kv
+  >>> db = kv()
+  >>> with db.hook_scope('install'):
+  ...    # do work, in transactional scope.
+  ...    db.set('x', 1)
+  >>> db.get('x')
+  1
+
+
+Usage
+-----
+
+Values are automatically json de/serialized to preserve basic typing
+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
+
+Individual values can be manipulated via get/set::
+
+   >>> kv.set('y', True)
+   >>> kv.get('y')
+   True
+
+   # We can set complex values (dicts, lists) as a single key.
+   >>> kv.set('config', {'a': 1, 'b': True'})
+
+   # Also supports returning dictionaries as a record which
+   # provides attribute access.
+   >>> config = kv.get('config', record=True)
+   >>> config.b
+   True
+
+
+Groups of keys can be manipulated with update/getrange::
+
+   >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
+   >>> kv.getrange('gui.', strip=True)
+   {'z': 1, 'y': 2}
+
+When updating values, its very helpful to understand which values
+have actually changed and how have they changed. The storage
+provides a delta method to provide for this::
+
+   >>> data = {'debug': True, 'option': 2}
+   >>> delta = kv.delta(data, 'config.')
+   >>> delta.debug.previous
+   None
+   >>> delta.debug.current
+   True
+   >>> delta
+   {'debug': (None, True), 'option': (None, 2)}
+
+Note the delta method does not persist the actual change, it needs to
+be explicitly saved via 'update' method::
+
+   >>> kv.update(data, 'config.')
+
+Values modified in the context of a hook scope retain historical values
+associated to the hookname.
+
+   >>> with db.hook_scope('config-changed'):
+   ...      db.set('x', 42)
+   >>> db.gethistory('x')
+   [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
+    (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
+
+"""
+
+import collections
+import contextlib
+import datetime
+import json
+import os
+import pprint
+import sqlite3
+import sys
+
+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
+
+
+class Storage(object):
+    """Simple key value database for local unit state within charms.
+
+    Modifications are automatically committed at hook exit. That's
+    currently regardless of exit code.
+
+    To support dicts, lists, integer, floats, and booleans values
+    are automatically json encoded/decoded.
+    """
+    def __init__(self, path=None):
+        self.db_path = path
+        if path is None:
+            self.db_path = os.path.join(
+                os.environ.get('CHARM_DIR', ''), '.unit-state.db')
+        self.conn = sqlite3.connect('%s' % self.db_path)
+        self.cursor = self.conn.cursor()
+        self.revision = None
+        self._closed = False
+        self._init()
+
+    def close(self):
+        if self._closed:
+            return
+        self.flush(False)
+        self.cursor.close()
+        self.conn.close()
+        self._closed = True
+
+    def _scoped_query(self, stmt, params=None):
+        if params is None:
+            params = []
+        return stmt, params
+
+    def get(self, key, default=None, record=False):
+        self.cursor.execute(
+            *self._scoped_query(
+                'select data from kv where key=?', [key]))
+        result = self.cursor.fetchone()
+        if not result:
+            return default
+        if record:
+            return Record(json.loads(result[0]))
+        return json.loads(result[0])
+
+    def getrange(self, key_prefix, strip=False):
+        stmt = "select key, data from kv where key like '%s%%'" % key_prefix
+        self.cursor.execute(*self._scoped_query(stmt))
+        result = self.cursor.fetchall()
+
+        if not result:
+            return None
+        if not strip:
+            key_prefix = ''
+        return dict([
+            (k[len(key_prefix):], json.loads(v)) for k, v in result])
+
+    def update(self, mapping, prefix=""):
+        for k, v in mapping.items():
+            self.set("%s%s" % (prefix, k), v)
+
+    def unset(self, key):
+        self.cursor.execute('delete from kv where key=?', [key])
+        if self.revision and self.cursor.rowcount:
+            self.cursor.execute(
+                'insert into kv_revisions values (?, ?, ?)',
+                [key, self.revision, json.dumps('DELETED')])
+
+    def set(self, key, value):
+        serialized = json.dumps(value)
+
+        self.cursor.execute(
+            'select data from kv where key=?', [key])
+        exists = self.cursor.fetchone()
+
+        # Skip mutations to the same value
+        if exists:
+            if exists[0] == serialized:
+                return value
+
+        if not exists:
+            self.cursor.execute(
+                'insert into kv (key, data) values (?, ?)',
+                (key, serialized))
+        else:
+            self.cursor.execute('''
+            update kv
+            set data = ?
+            where key = ?''', [serialized, key])
+
+        # Save
+        if not self.revision:
+            return value
+
+        self.cursor.execute(
+            'select 1 from kv_revisions where key=? and revision=?',
+            [key, self.revision])
+        exists = self.cursor.fetchone()
+
+        if not exists:
+            self.cursor.execute(
+                '''insert into kv_revisions (
+                revision, key, data) values (?, ?, ?)''',
+                (self.revision, key, serialized))
+        else:
+            self.cursor.execute(
+                '''
+                update kv_revisions
+                set data = ?
+                where key = ?
+                and   revision = ?''',
+                [serialized, key, self.revision])
+
+        return value
+
+    def delta(self, mapping, prefix):
+        """
+        return a delta containing values that have changed.
+        """
+        previous = self.getrange(prefix, strip=True)
+        if not previous:
+            pk = set()
+        else:
+            pk = set(previous.keys())
+        ck = set(mapping.keys())
+        delta = DeltaSet()
+
+        # added
+        for k in ck.difference(pk):
+            delta[k] = Delta(None, mapping[k])
+
+        # removed
+        for k in pk.difference(ck):
+            delta[k] = Delta(previous[k], None)
+
+        # changed
+        for k in pk.intersection(ck):
+            c = mapping[k]
+            p = previous[k]
+            if c != p:
+                delta[k] = Delta(p, c)
+
+        return delta
+
+    @contextlib.contextmanager
+    def hook_scope(self, name=""):
+        """Scope all future interactions to the current hook execution
+        revision."""
+        assert not self.revision
+        self.cursor.execute(
+            'insert into hooks (hook, date) values (?, ?)',
+            (name or sys.argv[0],
+             datetime.datetime.utcnow().isoformat()))
+        self.revision = self.cursor.lastrowid
+        try:
+            yield self.revision
+            self.revision = None
+        except:
+            self.flush(False)
+            self.revision = None
+            raise
+        else:
+            self.flush()
+
+    def flush(self, save=True):
+        if save:
+            self.conn.commit()
+        elif self._closed:
+            return
+        else:
+            self.conn.rollback()
+
+    def _init(self):
+        self.cursor.execute('''
+            create table if not exists kv (
+               key text,
+               data text,
+               primary key (key)
+               )''')
+        self.cursor.execute('''
+            create table if not exists kv_revisions (
+               key text,
+               revision integer,
+               data text,
+               primary key (key, revision)
+               )''')
+        self.cursor.execute('''
+            create table if not exists hooks (
+               version integer primary key autoincrement,
+               hook text,
+               date text
+               )''')
+        self.conn.commit()
+
+    def gethistory(self, key, deserialize=False):
+        self.cursor.execute(
+            '''
+            select kv.revision, kv.key, kv.data, h.hook, h.date
+            from kv_revisions kv,
+                 hooks h
+            where kv.key=?
+             and kv.revision = h.version
+            ''', [key])
+        if deserialize is False:
+            return self.cursor.fetchall()
+        return map(_parse_history, self.cursor.fetchall())
+
+    def debug(self, fh=sys.stderr):
+        self.cursor.execute('select * from kv')
+        pprint.pprint(self.cursor.fetchall(), stream=fh)
+        self.cursor.execute('select * from kv_revisions')
+        pprint.pprint(self.cursor.fetchall(), stream=fh)
+
+
+def _parse_history(d):
+    return (d[0], d[1], json.loads(d[2]), d[3],
+            datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
+
+
+class HookData(object):
+    """Simple integration for existing hook exec frameworks.
+
+    Records all unit information, and stores deltas for processing
+    by the hook.
+
+    Sample::
+
+       from charmhelper.core import hookenv, unitdata
+
+       changes = unitdata.HookData()
+       db = unitdata.kv()
+       hooks = hookenv.Hooks()
+
+       @hooks.hook
+       def config_changed():
+           # View all changes to configuration
+           for changed, (prev, cur) in changes.conf.items():
+               print('config changed', changed,
+                     'previous value', prev,
+                     'current value',  cur)
+
+           # Get some unit specific bookeeping
+           if not db.get('pkg_key'):
+               key = urllib.urlopen('https://example.com/pkg_key').read()
+               db.set('pkg_key', key)
+
+       if __name__ == '__main__':
+           with changes():
+               hook.execute()
+
+    """
+    def __init__(self):
+        self.kv = kv()
+        self.conf = None
+        self.rels = None
+
+    @contextlib.contextmanager
+    def __call__(self):
+        from charmhelpers.core import hookenv
+        hook_name = hookenv.hook_name()
+
+        with self.kv.hook_scope(hook_name):
+            self._record_charm_version(hookenv.charm_dir())
+            delta_config, delta_relation = self._record_hook(hookenv)
+            yield self.kv, delta_config, delta_relation
+
+    def _record_charm_version(self, charm_dir):
+        # Record revisions.. charm revisions are meaningless
+        # to charm authors as they don't control the revision.
+        # so logic dependnent on revision is not particularly
+        # useful, however it is useful for debugging analysis.
+        charm_rev = open(
+            os.path.join(charm_dir, 'revision')).read().strip()
+        charm_rev = charm_rev or '0'
+        revs = self.kv.get('charm_revisions', [])
+        if not charm_rev in revs:
+            revs.append(charm_rev.strip() or '0')
+            self.kv.set('charm_revisions', revs)
+
+    def _record_hook(self, hookenv):
+        data = hookenv.execution_environment()
+        self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
+        self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
+        self.kv.set('env', data['env'])
+        self.kv.set('unit', data['unit'])
+        self.kv.set('relid', data.get('relid'))
+        return conf_delta, rels_delta
+
+
+class Record(dict):
+
+    __slots__ = ()
+
+    def __getattr__(self, k):
+        if k in self:
+            return self[k]
+        raise AttributeError(k)
+
+
+class DeltaSet(Record):
+
+    __slots__ = ()
+
+
+Delta = collections.namedtuple('Delta', ['previous', 'current'])
+
+
+_KV = None
+
+
+def kv():
+    global _KV
+    if _KV is None:
+        _KV = Storage()
+    return _KV

From 6f33e03ab86de19a75bae2444e97f2d0d0cd9d4e Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Thu, 12 Feb 2015 16:27:40 +0000
Subject: [PATCH 06/16] _neutron_api_settings should return booleans for things
 which are supposed to be booleans rather than strings. this avoids confusion
 in the template

---
 hooks/neutron_ovs_context.py           | 11 ++++++++---
 unit_tests/test_neutron_ovs_context.py |  8 ++++----
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/hooks/neutron_ovs_context.py b/hooks/neutron_ovs_context.py
index 7dbf5211..e4aa2587 100644
--- a/hooks/neutron_ovs_context.py
+++ b/hooks/neutron_ovs_context.py
@@ -11,13 +11,18 @@ from charmhelpers.core.host import service_running, service_start
 from charmhelpers.contrib.network.ovs import add_bridge, add_bridge_port
 from charmhelpers.contrib.openstack.utils import get_host_ip
 from charmhelpers.contrib.network.ip import get_address_in_network
-
+import ast
 import re
 
 OVS_BRIDGE = 'br-int'
 DATA_BRIDGE = 'br-data'
 
 
+def to_boolean(option):
+    if option is None:
+        return False
+    return ast.literal_eval(option)
+
 def _neutron_api_settings():
     '''
     Inspects current neutron-plugin relation
@@ -33,9 +38,9 @@ def _neutron_api_settings():
             if 'l2-population' not in rdata:
                 continue
             neutron_settings = {
-                'l2_population': rdata['l2-population'],
-                'neutron_security_groups': rdata['neutron-security-groups'],
+                'l2_population': to_boolean(rdata['l2-population']),
                 'overlay_network_type': rdata['overlay-network-type'],
+                'neutron_security_groups': to_boolean(rdata['neutron-security-groups']),
             }
             # Override with configuration if set to true
             if config('disable-security-groups'):
diff --git a/unit_tests/test_neutron_ovs_context.py b/unit_tests/test_neutron_ovs_context.py
index 3e2ee906..ec51d54a 100644
--- a/unit_tests/test_neutron_ovs_context.py
+++ b/unit_tests/test_neutron_ovs_context.py
@@ -88,8 +88,8 @@ class OVSPluginContextTest(CharmTestCase):
         _is_clus.return_value = False
         self.related_units.return_value = ['unit1']
         self.relation_ids.return_value = ['rid2']
-        self.test_relation.set({'neutron-security-groups': True,
-                                'l2-population': True,
+        self.test_relation.set({'neutron-security-groups': 'True',
+                                'l2-population': 'True',
                                 'overlay-network-type': 'gre',
                                 })
         self.get_host_ip.return_value = '127.0.0.15'
@@ -141,8 +141,8 @@ class OVSPluginContextTest(CharmTestCase):
         self.test_config.set('disable-security-groups', True)
         self.related_units.return_value = ['unit1']
         self.relation_ids.return_value = ['rid2']
-        self.test_relation.set({'neutron-security-groups': True,
-                                'l2-population': True,
+        self.test_relation.set({'neutron-security-groups': 'True',
+                                'l2-population': 'True',
                                 'overlay-network-type': 'gre',
                                 })
         self.get_host_ip.return_value = '127.0.0.15'

From 6995409ebca882f949231f1ba54e260f383c60ca Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Thu, 12 Feb 2015 21:06:19 +0000
Subject: [PATCH 07/16] Fix lint

---
 hooks/neutron_ovs_context.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/hooks/neutron_ovs_context.py b/hooks/neutron_ovs_context.py
index e4aa2587..cf3d25aa 100644
--- a/hooks/neutron_ovs_context.py
+++ b/hooks/neutron_ovs_context.py
@@ -23,6 +23,7 @@ def to_boolean(option):
         return False
     return ast.literal_eval(option)
 
+
 def _neutron_api_settings():
     '''
     Inspects current neutron-plugin relation
@@ -40,7 +41,8 @@ def _neutron_api_settings():
             neutron_settings = {
                 'l2_population': to_boolean(rdata['l2-population']),
                 'overlay_network_type': rdata['overlay-network-type'],
-                'neutron_security_groups': to_boolean(rdata['neutron-security-groups']),
+                'neutron_security_groups':
+                    to_boolean(rdata['neutron-security-groups']),
             }
             # Override with configuration if set to true
             if config('disable-security-groups'):

From 1986e44d627f1b330a0057cdb6f6e562985117e4 Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Fri, 13 Feb 2015 09:15:36 +0000
Subject: [PATCH 08/16] Try to fix lint error that osci is seeing although I
 cannot reproduce locally

---
 hooks/neutron_ovs_context.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/hooks/neutron_ovs_context.py b/hooks/neutron_ovs_context.py
index cf3d25aa..fb5f89c1 100644
--- a/hooks/neutron_ovs_context.py
+++ b/hooks/neutron_ovs_context.py
@@ -41,8 +41,9 @@ def _neutron_api_settings():
             neutron_settings = {
                 'l2_population': to_boolean(rdata['l2-population']),
                 'overlay_network_type': rdata['overlay-network-type'],
-                'neutron_security_groups':
-                    to_boolean(rdata['neutron-security-groups']),
+                'neutron_security_groups': to_boolean(
+                    rdata['neutron-security-groups']
+                ),
             }
             # Override with configuration if set to true
             if config('disable-security-groups'):

From 5a6e1fb15914b073a7a3a7590c01b8d95ea232b6 Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Fri, 13 Feb 2015 12:02:13 +0000
Subject: [PATCH 09/16] Add amulet tests

---
 Makefile                                      |  13 +-
 charm-helpers-tests.yaml                      |   5 +
 hooks/charmhelpers/contrib/python/packages.py |   4 +-
 hooks/charmhelpers/core/fstab.py              |   4 +-
 hooks/charmhelpers/core/sysctl.py             |   4 +-
 hooks/charmhelpers/core/unitdata.py           |   2 +-
 hooks/charmhelpers/fetch/archiveurl.py        |  20 +-
 hooks/charmhelpers/fetch/giturl.py            |   2 +-
 tests/00-setup                                |  11 +
 tests/14-basic-precise-icehouse               |  11 +
 tests/15-basic-trusty-icehouse                |   9 +
 tests/16-basic-trusty-juno                    |  11 +
 tests/README                                  |  53 ++++
 tests/basic_deployment.py                     | 209 +++++++++++++
 tests/charmhelpers/__init__.py                |  38 +++
 tests/charmhelpers/contrib/__init__.py        |  15 +
 tests/charmhelpers/contrib/amulet/__init__.py |  15 +
 .../charmhelpers/contrib/amulet/deployment.py |  93 ++++++
 tests/charmhelpers/contrib/amulet/utils.py    | 194 ++++++++++++
 .../contrib/openstack/__init__.py             |  15 +
 .../contrib/openstack/amulet/__init__.py      |  15 +
 .../contrib/openstack/amulet/deployment.py    | 108 +++++++
 .../contrib/openstack/amulet/utils.py         | 294 ++++++++++++++++++
 23 files changed, 1125 insertions(+), 20 deletions(-)
 create mode 100644 charm-helpers-tests.yaml
 create mode 100755 tests/00-setup
 create mode 100755 tests/14-basic-precise-icehouse
 create mode 100755 tests/15-basic-trusty-icehouse
 create mode 100755 tests/16-basic-trusty-juno
 create mode 100644 tests/README
 create mode 100644 tests/basic_deployment.py
 create mode 100644 tests/charmhelpers/__init__.py
 create mode 100644 tests/charmhelpers/contrib/__init__.py
 create mode 100644 tests/charmhelpers/contrib/amulet/__init__.py
 create mode 100644 tests/charmhelpers/contrib/amulet/deployment.py
 create mode 100644 tests/charmhelpers/contrib/amulet/utils.py
 create mode 100644 tests/charmhelpers/contrib/openstack/__init__.py
 create mode 100644 tests/charmhelpers/contrib/openstack/amulet/__init__.py
 create mode 100644 tests/charmhelpers/contrib/openstack/amulet/deployment.py
 create mode 100644 tests/charmhelpers/contrib/openstack/amulet/utils.py

diff --git a/Makefile b/Makefile
index 5b02fbfa..db4e2c25 100644
--- a/Makefile
+++ b/Makefile
@@ -2,8 +2,7 @@
 PYTHON := /usr/bin/env python
 
 lint:
-	@flake8 --exclude hooks/charmhelpers hooks
-	@flake8 --exclude hooks/charmhelpers unit_tests
+	@flake8 --exclude hooks/charmhelpers hooks unit_tests tests
 	@charm proof
 
 unit_test:
@@ -17,6 +16,16 @@ bin/charm_helpers_sync.py:
 
 sync: bin/charm_helpers_sync.py
 	@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-sync.yaml
+	@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
+
+test:
+	@echo Starting Amulet tests...
+	# coreycb note: The -v should only be temporary until Amulet sends
+	# raise_status() messages to stderr:
+	#   https://bugs.launchpad.net/amulet/+bug/1320357
+	@juju test -v -p AMULET_HTTP_PROXY --timeout 900 \
+        00-setup 14-basic-precise-icehouse 15-basic-trusty-icehouse \
+	16-basic-trusty-juno
 
 publish: lint unit_test
 	bzr push lp:charms/neutron-openvswitch
diff --git a/charm-helpers-tests.yaml b/charm-helpers-tests.yaml
new file mode 100644
index 00000000..48b12f6f
--- /dev/null
+++ b/charm-helpers-tests.yaml
@@ -0,0 +1,5 @@
+branch: lp:charm-helpers
+destination: tests/charmhelpers
+include:
+    - contrib.amulet
+    - contrib.openstack.amulet
diff --git a/hooks/charmhelpers/contrib/python/packages.py b/hooks/charmhelpers/contrib/python/packages.py
index d848a120..8659516b 100644
--- a/hooks/charmhelpers/contrib/python/packages.py
+++ b/hooks/charmhelpers/contrib/python/packages.py
@@ -17,8 +17,6 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
-
 from charmhelpers.fetch import apt_install, apt_update
 from charmhelpers.core.hookenv import log
 
@@ -29,6 +27,8 @@ except ImportError:
     apt_install('python-pip')
     from pip import main as pip_execute
 
+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
+
 
 def parse_options(given, available):
     """Given a set of options, check if available"""
diff --git a/hooks/charmhelpers/core/fstab.py b/hooks/charmhelpers/core/fstab.py
index be7de248..9cdcc886 100644
--- a/hooks/charmhelpers/core/fstab.py
+++ b/hooks/charmhelpers/core/fstab.py
@@ -17,11 +17,11 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
-
 import io
 import os
 
+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
+
 
 class Fstab(io.FileIO):
     """This class extends file in order to implement a file reader/writer
diff --git a/hooks/charmhelpers/core/sysctl.py b/hooks/charmhelpers/core/sysctl.py
index 8e1b9eeb..21cc8ab2 100644
--- a/hooks/charmhelpers/core/sysctl.py
+++ b/hooks/charmhelpers/core/sysctl.py
@@ -17,8 +17,6 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
-
 import yaml
 
 from subprocess import check_call
@@ -29,6 +27,8 @@ from charmhelpers.core.hookenv import (
     ERROR,
 )
 
+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
+
 
 def create(sysctl_dict, sysctl_file):
     """Creates a sysctl.conf file from a YAML associative array
diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py
index 01329ab7..3000134a 100644
--- a/hooks/charmhelpers/core/unitdata.py
+++ b/hooks/charmhelpers/core/unitdata.py
@@ -435,7 +435,7 @@ class HookData(object):
             os.path.join(charm_dir, 'revision')).read().strip()
         charm_rev = charm_rev or '0'
         revs = self.kv.get('charm_revisions', [])
-        if not charm_rev in revs:
+        if charm_rev not in revs:
             revs.append(charm_rev.strip() or '0')
             self.kv.set('charm_revisions', revs)
 
diff --git a/hooks/charmhelpers/fetch/archiveurl.py b/hooks/charmhelpers/fetch/archiveurl.py
index d25a0ddd..8dfce505 100644
--- a/hooks/charmhelpers/fetch/archiveurl.py
+++ b/hooks/charmhelpers/fetch/archiveurl.py
@@ -18,6 +18,16 @@ import os
 import hashlib
 import re
 
+from charmhelpers.fetch import (
+    BaseFetchHandler,
+    UnhandledSource
+)
+from charmhelpers.payload.archive import (
+    get_archive_handler,
+    extract,
+)
+from charmhelpers.core.host import mkdir, check_hash
+
 import six
 if six.PY3:
     from urllib.request import (
@@ -35,16 +45,6 @@ else:
     )
     from urlparse import urlparse, urlunparse, parse_qs
 
-from charmhelpers.fetch import (
-    BaseFetchHandler,
-    UnhandledSource
-)
-from charmhelpers.payload.archive import (
-    get_archive_handler,
-    extract,
-)
-from charmhelpers.core.host import mkdir, check_hash
-
 
 def splituser(host):
     '''urllib.splituser(), but six's support of this seems broken'''
diff --git a/hooks/charmhelpers/fetch/giturl.py b/hooks/charmhelpers/fetch/giturl.py
index 5376786b..93aae87b 100644
--- a/hooks/charmhelpers/fetch/giturl.py
+++ b/hooks/charmhelpers/fetch/giturl.py
@@ -32,7 +32,7 @@ except ImportError:
     apt_install("python-git")
     from git import Repo
 
-from git.exc import GitCommandError
+from git.exc import GitCommandError  # noqa E402
 
 
 class GitUrlFetchHandler(BaseFetchHandler):
diff --git a/tests/00-setup b/tests/00-setup
new file mode 100755
index 00000000..06cfdb07
--- /dev/null
+++ b/tests/00-setup
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+set -ex
+
+sudo add-apt-repository --yes ppa:juju/stable
+sudo apt-get update --yes
+sudo apt-get install --yes python-amulet \
+                           python-neutronclient \
+                           python-keystoneclient \
+                           python-novaclient \
+                           python-glanceclient
diff --git a/tests/14-basic-precise-icehouse b/tests/14-basic-precise-icehouse
new file mode 100755
index 00000000..8b9ce35a
--- /dev/null
+++ b/tests/14-basic-precise-icehouse
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+# NeutronOVSBasicDeployment
+"""Amulet tests on a basic neutron-openvswitch deployment on precise-icehouse."""
+
+from basic_deployment import NeutronOVSBasicDeployment
+
+if __name__ == '__main__':
+    deployment = NeutronOVSBasicDeployment(series='precise',
+                                           openstack='cloud:precise-icehouse',
+                                           source='cloud:precise-updates/icehouse')
+    deployment.run_tests()
diff --git a/tests/15-basic-trusty-icehouse b/tests/15-basic-trusty-icehouse
new file mode 100755
index 00000000..67f2191c
--- /dev/null
+++ b/tests/15-basic-trusty-icehouse
@@ -0,0 +1,9 @@
+#!/usr/bin/python
+
+"""Amulet tests on a basic neutron-openvswitch deployment on trusty-icehouse."""
+
+from basic_deployment import NeutronOVSBasicDeployment
+
+if __name__ == '__main__':
+    deployment = NeutronOVSBasicDeployment(series='trusty')
+    deployment.run_tests()
diff --git a/tests/16-basic-trusty-juno b/tests/16-basic-trusty-juno
new file mode 100755
index 00000000..dd6ba7b6
--- /dev/null
+++ b/tests/16-basic-trusty-juno
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+
+"""Amulet tests on a basic neutron-openvswitch deployment on trusty-juno."""
+
+from basic_deployment import NeutronOVSBasicDeployment
+
+if __name__ == '__main__':
+    deployment = NeutronOVSBasicDeployment(series='trusty',
+                                           openstack='cloud:trusty-juno',
+                                           source='cloud:trusty-updates/juno')
+    deployment.run_tests()
diff --git a/tests/README b/tests/README
new file mode 100644
index 00000000..9c3bdbcf
--- /dev/null
+++ b/tests/README
@@ -0,0 +1,53 @@
+This directory provides Amulet tests that focus on verification of
+neutron-openvswitch deployments.
+
+In order to run tests, you'll need charm-tools installed (in addition to
+juju, of course):
+    sudo add-apt-repository ppa:juju/stable
+    sudo apt-get update
+    sudo apt-get install charm-tools
+
+If you use a web proxy server to access the web, you'll need to set the
+AMULET_HTTP_PROXY environment variable to the http URL of the proxy server.
+
+The following examples demonstrate different ways that tests can be executed.
+All examples are run from the charm's root directory.
+
+  * To run all tests (starting with 00-setup):
+
+      make test
+
+  * To run a specific test module (or modules):
+
+      juju test -v -p AMULET_HTTP_PROXY 15-basic-trusty-icehouse
+
+  * To run a specific test module (or modules), and keep the environment
+    deployed after a failure:
+
+      juju test --set-e -v -p AMULET_HTTP_PROXY 15-basic-trusty-icehouse
+
+  * To re-run a test module against an already deployed environment (one
+    that was deployed by a previous call to 'juju test --set-e'):
+
+      ./tests/15-basic-trusty-icehouse
+
+For debugging and test development purposes, all code should be idempotent.
+In other words, the code should have the ability to be re-run without changing
+the results beyond the initial run.  This enables editing and re-running of a
+test module against an already deployed environment, as described above.
+
+Manual debugging tips:
+
+  * Set the following env vars before using the OpenStack CLI as admin:
+      export OS_AUTH_URL=http://`juju-deployer -f keystone 2>&1 | tail -n 1`:5000/v2.0
+      export OS_TENANT_NAME=admin
+      export OS_USERNAME=admin
+      export OS_PASSWORD=openstack
+      export OS_REGION_NAME=RegionOne
+
+  * Set the following env vars before using the OpenStack CLI as demoUser:
+      export OS_AUTH_URL=http://`juju-deployer -f keystone 2>&1 | tail -n 1`:5000/v2.0
+      export OS_TENANT_NAME=demoTenant
+      export OS_USERNAME=demoUser
+      export OS_PASSWORD=password
+      export OS_REGION_NAME=RegionOne
diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py
new file mode 100644
index 00000000..6e96bdc3
--- /dev/null
+++ b/tests/basic_deployment.py
@@ -0,0 +1,209 @@
+#!/usr/bin/python
+
+import amulet
+import time
+
+from charmhelpers.contrib.openstack.amulet.deployment import (
+    OpenStackAmuletDeployment
+)
+
+from charmhelpers.contrib.openstack.amulet.utils import (
+    OpenStackAmuletUtils,
+    DEBUG,
+    ERROR
+)
+
+# Use DEBUG to turn on debug logging
+u = OpenStackAmuletUtils(ERROR)
+
+# XXX Tests inspecting relation data from the perspective of the
+# neutron-openvswitch are missing because amulet sentries aren't created for
+# subordinates Bug#1421388
+
+
+class NeutronOVSBasicDeployment(OpenStackAmuletDeployment):
+    """Amulet tests on a basic quantum-gateway deployment."""
+
+    def __init__(self, series, openstack=None, source=None, stable=False):
+        """Deploy the entire test environment."""
+        super(NeutronOVSBasicDeployment, self).__init__(series, openstack,
+                                                        source, stable)
+        self._add_services()
+        self._add_relations()
+        self._configure_services()
+        self._deploy()
+        self._initialize_tests()
+
+    def _add_services(self):
+        """Add services
+
+           Add the services that we're testing, where quantum-gateway is local,
+           and the rest of the service are from lp branches that are
+           compatible with the local charm (e.g. stable or next).
+           """
+        this_service = {'name': 'neutron-openvswitch'}
+        other_services = [{'name': 'nova-compute'},
+                          {'name': 'rabbitmq-server'},
+                          {'name': 'neutron-api'}]
+        super(NeutronOVSBasicDeployment, self)._add_services(this_service,
+                                                             other_services)
+
+    def _add_relations(self):
+        """Add all of the relations for the services."""
+        relations = {
+            'neutron-openvswitch:amqp': 'rabbitmq-server:amqp',
+            'neutron-openvswitch:neutron-plugin':
+            'nova-compute:neutron-plugin',
+            'neutron-openvswitch:neutron-plugin-api':
+            'neutron-api:neutron-plugin-api',
+        }
+        super(NeutronOVSBasicDeployment, self)._add_relations(relations)
+
+    def _configure_services(self):
+        """Configure all of the services."""
+        configs = {}
+        super(NeutronOVSBasicDeployment, self)._configure_services(configs)
+
+    def _initialize_tests(self):
+        """Perform final initialization before tests get run."""
+        # Access the sentries for inspecting service units
+        self.compute_sentry = self.d.sentry.unit['nova-compute/0']
+        self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']
+        self.neutron_api_sentry = self.d.sentry.unit['neutron-api/0']
+
+    def test_services(self):
+        """Verify the expected services are running on the corresponding
+           service units."""
+
+        commands = {
+            self.compute_sentry: ['status nova-compute'],
+            self.rabbitmq_sentry: ['service rabbitmq-server status'],
+            self.neutron_api_sentry: ['status neutron-server'],
+        }
+
+        ret = u.validate_services(commands)
+        if ret:
+            amulet.raise_status(amulet.FAIL, msg=ret)
+
+    def test_rabbitmq_amqp_relation(self):
+        """Verify data in rabbitmq-server/neutron-openvswitch amqp relation"""
+        unit = self.rabbitmq_sentry
+        relation = ['amqp', 'neutron-openvswitch:amqp']
+        expected = {
+            'private-address': u.valid_ip,
+            'password': u.not_null,
+            'hostname': u.valid_ip
+        }
+
+        ret = u.validate_relation_data(unit, relation, expected)
+        if ret:
+            message = u.relation_error('rabbitmq amqp', ret)
+            amulet.raise_status(amulet.FAIL, msg=message)
+
+    def test_nova_compute_relation(self):
+        """Verify the nova-compute to neutron-openvswitch relation data"""
+        unit = self.compute_sentry
+        relation = ['neutron-plugin', 'neutron-openvswitch:neutron-plugin']
+        expected = {
+            'private-address': u.valid_ip,
+        }
+
+        ret = u.validate_relation_data(unit, relation, expected)
+        if ret:
+            message = u.relation_error('nova-compute neutron-plugin', ret)
+            amulet.raise_status(amulet.FAIL, msg=message)
+
+    def test_neutron_api_relation(self):
+        """Verify the neutron-api to neutron-openvswitch relation data"""
+        unit = self.neutron_api_sentry
+        relation = ['neutron-plugin-api',
+                    'neutron-openvswitch:neutron-plugin-api']
+        expected = {
+            'private-address': u.valid_ip,
+        }
+
+        ret = u.validate_relation_data(unit, relation, expected)
+        if ret:
+            message = u.relation_error('neutron-api neutron-plugin-api', ret)
+            amulet.raise_status(amulet.FAIL, msg=message)
+
+    def process_ret(self, ret=None, message=None):
+        if ret:
+            amulet.raise_status(amulet.FAIL, msg=message)
+
+    def check_ml2_setting_propagation(self, service, charm_key,
+                                      config_file_key, vpair,
+                                      section):
+        unit = self.compute_sentry
+        conf = "/etc/neutron/plugins/ml2/ml2_conf.ini"
+        for value in vpair:
+            self.d.configure(service, {charm_key: value})
+            time.sleep(30)
+            ret = u.validate_config_data(unit, conf, section,
+                                         {config_file_key: value})
+            msg = "Propagation error, expected %s=%s" % (config_file_key,
+                                                         value)
+            self.process_ret(ret=ret, message=msg)
+
+    def test_l2pop_propagation(self):
+        """Verify that neutron-api l2pop setting propagates to neutron-ovs"""
+        self.check_ml2_setting_propagation('neutron-api',
+                                           'l2-population',
+                                           'l2_population',
+                                           ['False', 'True'],
+                                           'agent')
+
+    def test_nettype_propagation(self):
+        """Verify that neutron-api nettype setting propagates to neutron-ovs"""
+        self.check_ml2_setting_propagation('neutron-api',
+                                           'overlay-network-type',
+                                           'tunnel_types',
+                                           ['vxlan', 'gre'],
+                                           'agent')
+
+#    def test_secgroup_propagation(self):
+#        """Verify that neutron-api secgroup propagates to neutron-ovs"""
+#        self.check_ml2_setting_propagation('neutron-api',
+#                                           'neutron-security-groups',
+#                                           'enable_security_group',
+#                                           ['False', 'True'],
+#                                           'securitygroup')
+
+    def test_secgroup_propagation_local_override(self):
+        """Verify disable-security-groups overrides what neutron-api says"""
+        unit = self.compute_sentry
+        conf = "/etc/neutron/plugins/ml2/ml2_conf.ini"
+        self.d.configure('neutron-api', {'neutron-security-groups': 'True'})
+        self.d.configure('neutron-openvswitch',
+                         {'disable-security-groups': 'True'})
+        time.sleep(30)
+        ret = u.validate_config_data(unit, conf, 'securitygroup',
+                                     {'enable_security_group': 'False'})
+        msg = "Propagation error, expected %s=%s" % ('enable_security_group',
+                                                     'False')
+        self.process_ret(ret=ret, message=msg)
+        self.d.configure('neutron-openvswitch',
+                         {'disable-security-groups': 'False'})
+        self.d.configure('neutron-api', {'neutron-security-groups': 'True'})
+        time.sleep(30)
+        ret = u.validate_config_data(unit, conf, 'securitygroup',
+                                     {'enable_security_group': 'True'})
+
+    def test_z_restart_on_config_change(self):
+        """Verify that the specified services are restarted when the config
+           is changed.
+
+           Note(coreycb): The method name with the _z_ is a little odd
+           but it forces the test to run last.  It just makes things
+           easier because restarting services requires re-authorization.
+           """
+        conf = '/etc/neutron/neutron.conf'
+        self.d.configure('neutron-openvswitch', {'use-syslog': 'True'})
+        if not u.service_restarted(self.compute_sentry,
+                                   'neutron-openvswitch-agent', conf,
+                                   pgrep_full=True, sleep_time=20):
+            self.d.configure('neutron-openvswitch', {'use-syslog': 'False'})
+            msg = ('service neutron-openvswitch-agent did not restart after '
+                   'config change')
+            amulet.raise_status(amulet.FAIL, msg=msg)
+        self.d.configure('neutron-openvswitch', {'use-syslog': 'False'})
diff --git a/tests/charmhelpers/__init__.py b/tests/charmhelpers/__init__.py
new file mode 100644
index 00000000..f72e7f84
--- /dev/null
+++ b/tests/charmhelpers/__init__.py
@@ -0,0 +1,38 @@
+# 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/>.
+
+# Bootstrap charm-helpers, installing its dependencies if necessary using
+# only standard libraries.
+import subprocess
+import sys
+
+try:
+    import six  # flake8: noqa
+except ImportError:
+    if sys.version_info.major == 2:
+        subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
+    else:
+        subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
+    import six  # flake8: noqa
+
+try:
+    import yaml  # flake8: noqa
+except ImportError:
+    if sys.version_info.major == 2:
+        subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
+    else:
+        subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
+    import yaml  # flake8: noqa
diff --git a/tests/charmhelpers/contrib/__init__.py b/tests/charmhelpers/contrib/__init__.py
new file mode 100644
index 00000000..d1400a02
--- /dev/null
+++ b/tests/charmhelpers/contrib/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/tests/charmhelpers/contrib/amulet/__init__.py b/tests/charmhelpers/contrib/amulet/__init__.py
new file mode 100644
index 00000000..d1400a02
--- /dev/null
+++ b/tests/charmhelpers/contrib/amulet/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/tests/charmhelpers/contrib/amulet/deployment.py b/tests/charmhelpers/contrib/amulet/deployment.py
new file mode 100644
index 00000000..367d6b47
--- /dev/null
+++ b/tests/charmhelpers/contrib/amulet/deployment.py
@@ -0,0 +1,93 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
+import amulet
+import os
+import six
+
+
+class AmuletDeployment(object):
+    """Amulet deployment.
+
+       This class provides generic Amulet deployment and test runner
+       methods.
+       """
+
+    def __init__(self, series=None):
+        """Initialize the deployment environment."""
+        self.series = None
+
+        if series:
+            self.series = series
+            self.d = amulet.Deployment(series=self.series)
+        else:
+            self.d = amulet.Deployment()
+
+    def _add_services(self, this_service, other_services):
+        """Add services.
+
+           Add services to the deployment where this_service is the local charm
+           that we're testing and other_services are the other services that
+           are being used in the local amulet tests.
+           """
+        if this_service['name'] != os.path.basename(os.getcwd()):
+            s = this_service['name']
+            msg = "The charm's root directory name needs to be {}".format(s)
+            amulet.raise_status(amulet.FAIL, msg=msg)
+
+        if 'units' not in this_service:
+            this_service['units'] = 1
+
+        self.d.add(this_service['name'], units=this_service['units'])
+
+        for svc in other_services:
+            if 'location' in svc:
+                branch_location = svc['location']
+            elif self.series:
+                branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
+            else:
+                branch_location = None
+
+            if 'units' not in svc:
+                svc['units'] = 1
+
+            self.d.add(svc['name'], charm=branch_location, units=svc['units'])
+
+    def _add_relations(self, relations):
+        """Add all of the relations for the services."""
+        for k, v in six.iteritems(relations):
+            self.d.relate(k, v)
+
+    def _configure_services(self, configs):
+        """Configure all of the services."""
+        for service, config in six.iteritems(configs):
+            self.d.configure(service, config)
+
+    def _deploy(self):
+        """Deploy environment and wait for all hooks to finish executing."""
+        try:
+            self.d.setup(timeout=900)
+            self.d.sentry.wait(timeout=900)
+        except amulet.helpers.TimeoutError:
+            amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
+        except Exception:
+            raise
+
+    def run_tests(self):
+        """Run all of the methods that are prefixed with 'test_'."""
+        for test in dir(self):
+            if test.startswith('test_'):
+                getattr(self, test)()
diff --git a/tests/charmhelpers/contrib/amulet/utils.py b/tests/charmhelpers/contrib/amulet/utils.py
new file mode 100644
index 00000000..3464b873
--- /dev/null
+++ b/tests/charmhelpers/contrib/amulet/utils.py
@@ -0,0 +1,194 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
+import ConfigParser
+import io
+import logging
+import re
+import sys
+import time
+
+import six
+
+
+class AmuletUtils(object):
+    """Amulet utilities.
+
+       This class provides common utility functions that are used by Amulet
+       tests.
+       """
+
+    def __init__(self, log_level=logging.ERROR):
+        self.log = self.get_logger(level=log_level)
+
+    def get_logger(self, name="amulet-logger", level=logging.DEBUG):
+        """Get a logger object that will log to stdout."""
+        log = logging
+        logger = log.getLogger(name)
+        fmt = log.Formatter("%(asctime)s %(funcName)s "
+                            "%(levelname)s: %(message)s")
+
+        handler = log.StreamHandler(stream=sys.stdout)
+        handler.setLevel(level)
+        handler.setFormatter(fmt)
+
+        logger.addHandler(handler)
+        logger.setLevel(level)
+
+        return logger
+
+    def valid_ip(self, ip):
+        if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
+            return True
+        else:
+            return False
+
+    def valid_url(self, url):
+        p = re.compile(
+            r'^(?:http|ftp)s?://'
+            r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # noqa
+            r'localhost|'
+            r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
+            r'(?::\d+)?'
+            r'(?:/?|[/?]\S+)$',
+            re.IGNORECASE)
+        if p.match(url):
+            return True
+        else:
+            return False
+
+    def validate_services(self, commands):
+        """Validate services.
+
+           Verify the specified services are running on the corresponding
+           service units.
+           """
+        for k, v in six.iteritems(commands):
+            for cmd in v:
+                output, code = k.run(cmd)
+                if code != 0:
+                    return "command `{}` returned {}".format(cmd, str(code))
+        return None
+
+    def _get_config(self, unit, filename):
+        """Get a ConfigParser object for parsing a unit's config file."""
+        file_contents = unit.file_contents(filename)
+        config = ConfigParser.ConfigParser()
+        config.readfp(io.StringIO(file_contents))
+        return config
+
+    def validate_config_data(self, sentry_unit, config_file, section,
+                             expected):
+        """Validate config file data.
+
+           Verify that the specified section of the config file contains
+           the expected option key:value pairs.
+           """
+        config = self._get_config(sentry_unit, config_file)
+
+        if section != 'DEFAULT' and not config.has_section(section):
+            return "section [{}] does not exist".format(section)
+
+        for k in expected.keys():
+            if not config.has_option(section, k):
+                return "section [{}] is missing option {}".format(section, k)
+            if config.get(section, k) != expected[k]:
+                return "section [{}] {}:{} != expected {}:{}".format(
+                       section, k, config.get(section, k), k, expected[k])
+        return None
+
+    def _validate_dict_data(self, expected, actual):
+        """Validate dictionary data.
+
+           Compare expected dictionary data vs actual dictionary data.
+           The values in the 'expected' dictionary can be strings, bools, ints,
+           longs, or can be a function that evaluate a variable and returns a
+           bool.
+           """
+        for k, v in six.iteritems(expected):
+            if k in actual:
+                if (isinstance(v, six.string_types) or
+                        isinstance(v, bool) or
+                        isinstance(v, six.integer_types)):
+                    if v != actual[k]:
+                        return "{}:{}".format(k, actual[k])
+                elif not v(actual[k]):
+                    return "{}:{}".format(k, actual[k])
+            else:
+                return "key '{}' does not exist".format(k)
+        return None
+
+    def validate_relation_data(self, sentry_unit, relation, expected):
+        """Validate actual relation data based on expected relation data."""
+        actual = sentry_unit.relation(relation[0], relation[1])
+        self.log.debug('actual: {}'.format(repr(actual)))
+        return self._validate_dict_data(expected, actual)
+
+    def _validate_list_data(self, expected, actual):
+        """Compare expected list vs actual list data."""
+        for e in expected:
+            if e not in actual:
+                return "expected item {} not found in actual list".format(e)
+        return None
+
+    def not_null(self, string):
+        if string is not None:
+            return True
+        else:
+            return False
+
+    def _get_file_mtime(self, sentry_unit, filename):
+        """Get last modification time of file."""
+        return sentry_unit.file_stat(filename)['mtime']
+
+    def _get_dir_mtime(self, sentry_unit, directory):
+        """Get last modification time of directory."""
+        return sentry_unit.directory_stat(directory)['mtime']
+
+    def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
+        """Get process' start time.
+
+           Determine start time of the process based on the last modification
+           time of the /proc/pid directory. If pgrep_full is True, the process
+           name is matched against the full command line.
+           """
+        if pgrep_full:
+            cmd = 'pgrep -o -f {}'.format(service)
+        else:
+            cmd = 'pgrep -o {}'.format(service)
+        proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
+        return self._get_dir_mtime(sentry_unit, proc_dir)
+
+    def service_restarted(self, sentry_unit, service, filename,
+                          pgrep_full=False, sleep_time=20):
+        """Check if service was restarted.
+
+           Compare a service's start time vs a file's last modification time
+           (such as a config file for that service) to determine if the service
+           has been restarted.
+           """
+        time.sleep(sleep_time)
+        if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
+                self._get_file_mtime(sentry_unit, filename)):
+            return True
+        else:
+            return False
+
+    def relation_error(self, name, data):
+        return 'unexpected relation data in {} - {}'.format(name, data)
+
+    def endpoint_error(self, name, data):
+        return 'unexpected endpoint data in {} - {}'.format(name, data)
diff --git a/tests/charmhelpers/contrib/openstack/__init__.py b/tests/charmhelpers/contrib/openstack/__init__.py
new file mode 100644
index 00000000..d1400a02
--- /dev/null
+++ b/tests/charmhelpers/contrib/openstack/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/tests/charmhelpers/contrib/openstack/amulet/__init__.py b/tests/charmhelpers/contrib/openstack/amulet/__init__.py
new file mode 100644
index 00000000..d1400a02
--- /dev/null
+++ b/tests/charmhelpers/contrib/openstack/amulet/__init__.py
@@ -0,0 +1,15 @@
+# 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/>.
diff --git a/tests/charmhelpers/contrib/openstack/amulet/deployment.py b/tests/charmhelpers/contrib/openstack/amulet/deployment.py
new file mode 100644
index 00000000..c50d3ec6
--- /dev/null
+++ b/tests/charmhelpers/contrib/openstack/amulet/deployment.py
@@ -0,0 +1,108 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
+import six
+from charmhelpers.contrib.amulet.deployment import (
+    AmuletDeployment
+)
+
+
+class OpenStackAmuletDeployment(AmuletDeployment):
+    """OpenStack amulet deployment.
+
+       This class inherits from AmuletDeployment and has additional support
+       that is specifically for use by OpenStack charms.
+       """
+
+    def __init__(self, series=None, openstack=None, source=None, stable=True):
+        """Initialize the deployment environment."""
+        super(OpenStackAmuletDeployment, self).__init__(series)
+        self.openstack = openstack
+        self.source = source
+        self.stable = stable
+        # Note(coreycb): this needs to be changed when new next branches come
+        # out.
+        self.current_next = "trusty"
+
+    def _determine_branch_locations(self, other_services):
+        """Determine the branch locations for the other services.
+
+           Determine if the local branch being tested is derived from its
+           stable or next (dev) branch, and based on this, use the corresonding
+           stable or next branches for the other_services."""
+        base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
+
+        if self.stable:
+            for svc in other_services:
+                temp = 'lp:charms/{}'
+                svc['location'] = temp.format(svc['name'])
+        else:
+            for svc in other_services:
+                if svc['name'] in base_charms:
+                    temp = 'lp:charms/{}'
+                    svc['location'] = temp.format(svc['name'])
+                else:
+                    temp = 'lp:~openstack-charmers/charms/{}/{}/next'
+                    svc['location'] = temp.format(self.current_next,
+                                                  svc['name'])
+        return other_services
+
+    def _add_services(self, this_service, other_services):
+        """Add services to the deployment and set openstack-origin/source."""
+        other_services = self._determine_branch_locations(other_services)
+
+        super(OpenStackAmuletDeployment, self)._add_services(this_service,
+                                                             other_services)
+
+        services = other_services
+        services.append(this_service)
+        use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
+                      'ceph-osd', 'ceph-radosgw']
+
+        if self.openstack:
+            for svc in services:
+                if svc['name'] not in use_source:
+                    config = {'openstack-origin': self.openstack}
+                    self.d.configure(svc['name'], config)
+
+        if self.source:
+            for svc in services:
+                if svc['name'] in use_source:
+                    config = {'source': self.source}
+                    self.d.configure(svc['name'], config)
+
+    def _configure_services(self, configs):
+        """Configure all of the services."""
+        for service, config in six.iteritems(configs):
+            self.d.configure(service, config)
+
+    def _get_openstack_release(self):
+        """Get openstack release.
+
+           Return an integer representing the enum value of the openstack
+           release.
+           """
+        (self.precise_essex, self.precise_folsom, self.precise_grizzly,
+         self.precise_havana, self.precise_icehouse,
+         self.trusty_icehouse) = range(6)
+        releases = {
+            ('precise', None): self.precise_essex,
+            ('precise', 'cloud:precise-folsom'): self.precise_folsom,
+            ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
+            ('precise', 'cloud:precise-havana'): self.precise_havana,
+            ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
+            ('trusty', None): self.trusty_icehouse}
+        return releases[(self.series, self.openstack)]
diff --git a/tests/charmhelpers/contrib/openstack/amulet/utils.py b/tests/charmhelpers/contrib/openstack/amulet/utils.py
new file mode 100644
index 00000000..9c3d918a
--- /dev/null
+++ b/tests/charmhelpers/contrib/openstack/amulet/utils.py
@@ -0,0 +1,294 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import os
+import time
+import urllib
+
+import glanceclient.v1.client as glance_client
+import keystoneclient.v2_0 as keystone_client
+import novaclient.v1_1.client as nova_client
+
+import six
+
+from charmhelpers.contrib.amulet.utils import (
+    AmuletUtils
+)
+
+DEBUG = logging.DEBUG
+ERROR = logging.ERROR
+
+
+class OpenStackAmuletUtils(AmuletUtils):
+    """OpenStack amulet utilities.
+
+       This class inherits from AmuletUtils and has additional support
+       that is specifically for use by OpenStack charms.
+       """
+
+    def __init__(self, log_level=ERROR):
+        """Initialize the deployment environment."""
+        super(OpenStackAmuletUtils, self).__init__(log_level)
+
+    def validate_endpoint_data(self, endpoints, admin_port, internal_port,
+                               public_port, expected):
+        """Validate endpoint data.
+
+           Validate actual endpoint data vs expected endpoint data. The ports
+           are used to find the matching endpoint.
+           """
+        found = False
+        for ep in endpoints:
+            self.log.debug('endpoint: {}'.format(repr(ep)))
+            if (admin_port in ep.adminurl and
+                    internal_port in ep.internalurl and
+                    public_port in ep.publicurl):
+                found = True
+                actual = {'id': ep.id,
+                          'region': ep.region,
+                          'adminurl': ep.adminurl,
+                          'internalurl': ep.internalurl,
+                          'publicurl': ep.publicurl,
+                          'service_id': ep.service_id}
+                ret = self._validate_dict_data(expected, actual)
+                if ret:
+                    return 'unexpected endpoint data - {}'.format(ret)
+
+        if not found:
+            return 'endpoint not found'
+
+    def validate_svc_catalog_endpoint_data(self, expected, actual):
+        """Validate service catalog endpoint data.
+
+           Validate a list of actual service catalog endpoints vs a list of
+           expected service catalog endpoints.
+           """
+        self.log.debug('actual: {}'.format(repr(actual)))
+        for k, v in six.iteritems(expected):
+            if k in actual:
+                ret = self._validate_dict_data(expected[k][0], actual[k][0])
+                if ret:
+                    return self.endpoint_error(k, ret)
+            else:
+                return "endpoint {} does not exist".format(k)
+        return ret
+
+    def validate_tenant_data(self, expected, actual):
+        """Validate tenant data.
+
+           Validate a list of actual tenant data vs list of expected tenant
+           data.
+           """
+        self.log.debug('actual: {}'.format(repr(actual)))
+        for e in expected:
+            found = False
+            for act in actual:
+                a = {'enabled': act.enabled, 'description': act.description,
+                     'name': act.name, 'id': act.id}
+                if e['name'] == a['name']:
+                    found = True
+                    ret = self._validate_dict_data(e, a)
+                    if ret:
+                        return "unexpected tenant data - {}".format(ret)
+            if not found:
+                return "tenant {} does not exist".format(e['name'])
+        return ret
+
+    def validate_role_data(self, expected, actual):
+        """Validate role data.
+
+           Validate a list of actual role data vs a list of expected role
+           data.
+           """
+        self.log.debug('actual: {}'.format(repr(actual)))
+        for e in expected:
+            found = False
+            for act in actual:
+                a = {'name': act.name, 'id': act.id}
+                if e['name'] == a['name']:
+                    found = True
+                    ret = self._validate_dict_data(e, a)
+                    if ret:
+                        return "unexpected role data - {}".format(ret)
+            if not found:
+                return "role {} does not exist".format(e['name'])
+        return ret
+
+    def validate_user_data(self, expected, actual):
+        """Validate user data.
+
+           Validate a list of actual user data vs a list of expected user
+           data.
+           """
+        self.log.debug('actual: {}'.format(repr(actual)))
+        for e in expected:
+            found = False
+            for act in actual:
+                a = {'enabled': act.enabled, 'name': act.name,
+                     'email': act.email, 'tenantId': act.tenantId,
+                     'id': act.id}
+                if e['name'] == a['name']:
+                    found = True
+                    ret = self._validate_dict_data(e, a)
+                    if ret:
+                        return "unexpected user data - {}".format(ret)
+            if not found:
+                return "user {} does not exist".format(e['name'])
+        return ret
+
+    def validate_flavor_data(self, expected, actual):
+        """Validate flavor data.
+
+           Validate a list of actual flavors vs a list of expected flavors.
+           """
+        self.log.debug('actual: {}'.format(repr(actual)))
+        act = [a.name for a in actual]
+        return self._validate_list_data(expected, act)
+
+    def tenant_exists(self, keystone, tenant):
+        """Return True if tenant exists."""
+        return tenant in [t.name for t in keystone.tenants.list()]
+
+    def authenticate_keystone_admin(self, keystone_sentry, user, password,
+                                    tenant):
+        """Authenticates admin user with the keystone admin endpoint."""
+        unit = keystone_sentry
+        service_ip = unit.relation('shared-db',
+                                   'mysql:shared-db')['private-address']
+        ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
+        return keystone_client.Client(username=user, password=password,
+                                      tenant_name=tenant, auth_url=ep)
+
+    def authenticate_keystone_user(self, keystone, user, password, tenant):
+        """Authenticates a regular user with the keystone public endpoint."""
+        ep = keystone.service_catalog.url_for(service_type='identity',
+                                              endpoint_type='publicURL')
+        return keystone_client.Client(username=user, password=password,
+                                      tenant_name=tenant, auth_url=ep)
+
+    def authenticate_glance_admin(self, keystone):
+        """Authenticates admin user with glance."""
+        ep = keystone.service_catalog.url_for(service_type='image',
+                                              endpoint_type='adminURL')
+        return glance_client.Client(ep, token=keystone.auth_token)
+
+    def authenticate_nova_user(self, keystone, user, password, tenant):
+        """Authenticates a regular user with nova-api."""
+        ep = keystone.service_catalog.url_for(service_type='identity',
+                                              endpoint_type='publicURL')
+        return nova_client.Client(username=user, api_key=password,
+                                  project_id=tenant, auth_url=ep)
+
+    def create_cirros_image(self, glance, image_name):
+        """Download the latest cirros image and upload it to glance."""
+        http_proxy = os.getenv('AMULET_HTTP_PROXY')
+        self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
+        if http_proxy:
+            proxies = {'http': http_proxy}
+            opener = urllib.FancyURLopener(proxies)
+        else:
+            opener = urllib.FancyURLopener()
+
+        f = opener.open("http://download.cirros-cloud.net/version/released")
+        version = f.read().strip()
+        cirros_img = "cirros-{}-x86_64-disk.img".format(version)
+        local_path = os.path.join('tests', cirros_img)
+
+        if not os.path.exists(local_path):
+            cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
+                                                  version, cirros_img)
+            opener.retrieve(cirros_url, local_path)
+        f.close()
+
+        with open(local_path) as f:
+            image = glance.images.create(name=image_name, is_public=True,
+                                         disk_format='qcow2',
+                                         container_format='bare', data=f)
+        count = 1
+        status = image.status
+        while status != 'active' and count < 10:
+            time.sleep(3)
+            image = glance.images.get(image.id)
+            status = image.status
+            self.log.debug('image status: {}'.format(status))
+            count += 1
+
+        if status != 'active':
+            self.log.error('image creation timed out')
+            return None
+
+        return image
+
+    def delete_image(self, glance, image):
+        """Delete the specified image."""
+        num_before = len(list(glance.images.list()))
+        glance.images.delete(image)
+
+        count = 1
+        num_after = len(list(glance.images.list()))
+        while num_after != (num_before - 1) and count < 10:
+            time.sleep(3)
+            num_after = len(list(glance.images.list()))
+            self.log.debug('number of images: {}'.format(num_after))
+            count += 1
+
+        if num_after != (num_before - 1):
+            self.log.error('image deletion timed out')
+            return False
+
+        return True
+
+    def create_instance(self, nova, image_name, instance_name, flavor):
+        """Create the specified instance."""
+        image = nova.images.find(name=image_name)
+        flavor = nova.flavors.find(name=flavor)
+        instance = nova.servers.create(name=instance_name, image=image,
+                                       flavor=flavor)
+
+        count = 1
+        status = instance.status
+        while status != 'ACTIVE' and count < 60:
+            time.sleep(3)
+            instance = nova.servers.get(instance.id)
+            status = instance.status
+            self.log.debug('instance status: {}'.format(status))
+            count += 1
+
+        if status != 'ACTIVE':
+            self.log.error('instance creation timed out')
+            return None
+
+        return instance
+
+    def delete_instance(self, nova, instance):
+        """Delete the specified instance."""
+        num_before = len(list(nova.servers.list()))
+        nova.servers.delete(instance)
+
+        count = 1
+        num_after = len(list(nova.servers.list()))
+        while num_after != (num_before - 1) and count < 10:
+            time.sleep(3)
+            num_after = len(list(nova.servers.list()))
+            self.log.debug('number of instances: {}'.format(num_after))
+            count += 1
+
+        if num_after != (num_before - 1):
+            self.log.error('instance deletion timed out')
+            return False
+
+        return True

From 283974d867311dd79b72e2d6a02a271ff1d01c4d Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Mon, 16 Feb 2015 11:16:11 +0000
Subject: [PATCH 11/16] Use charmhelper bool_from_string rather than local
 to_boolean

---
 .../contrib/openstack/amulet/deployment.py    |  7 +++-
 hooks/charmhelpers/contrib/python/packages.py |  4 +-
 hooks/charmhelpers/core/fstab.py              |  4 +-
 hooks/charmhelpers/core/strutils.py           | 38 +++++++++++++++++++
 hooks/charmhelpers/core/sysctl.py             |  4 +-
 hooks/charmhelpers/core/unitdata.py           |  2 +-
 hooks/charmhelpers/fetch/archiveurl.py        | 20 +++++-----
 hooks/charmhelpers/fetch/giturl.py            |  2 +-
 hooks/neutron_ovs_context.py                  | 12 ++----
 9 files changed, 64 insertions(+), 29 deletions(-)
 create mode 100644 hooks/charmhelpers/core/strutils.py

diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
index c50d3ec6..0cfeaa4c 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
@@ -71,16 +71,19 @@ class OpenStackAmuletDeployment(AmuletDeployment):
         services.append(this_service)
         use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
                       'ceph-osd', 'ceph-radosgw']
+        # Openstack subordinate charms do not expose an origin option as that
+        # is controlled by the principle
+        ignore = ['neutron-openvswitch']
 
         if self.openstack:
             for svc in services:
-                if svc['name'] not in use_source:
+                if svc['name'] not in use_source + ignore:
                     config = {'openstack-origin': self.openstack}
                     self.d.configure(svc['name'], config)
 
         if self.source:
             for svc in services:
-                if svc['name'] in use_source:
+                if svc['name'] in use_source and svc['name'] not in ignore:
                     config = {'source': self.source}
                     self.d.configure(svc['name'], config)
 
diff --git a/hooks/charmhelpers/contrib/python/packages.py b/hooks/charmhelpers/contrib/python/packages.py
index d848a120..8659516b 100644
--- a/hooks/charmhelpers/contrib/python/packages.py
+++ b/hooks/charmhelpers/contrib/python/packages.py
@@ -17,8 +17,6 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
-
 from charmhelpers.fetch import apt_install, apt_update
 from charmhelpers.core.hookenv import log
 
@@ -29,6 +27,8 @@ except ImportError:
     apt_install('python-pip')
     from pip import main as pip_execute
 
+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
+
 
 def parse_options(given, available):
     """Given a set of options, check if available"""
diff --git a/hooks/charmhelpers/core/fstab.py b/hooks/charmhelpers/core/fstab.py
index be7de248..9cdcc886 100644
--- a/hooks/charmhelpers/core/fstab.py
+++ b/hooks/charmhelpers/core/fstab.py
@@ -17,11 +17,11 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
-
 import io
 import os
 
+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
+
 
 class Fstab(io.FileIO):
     """This class extends file in order to implement a file reader/writer
diff --git a/hooks/charmhelpers/core/strutils.py b/hooks/charmhelpers/core/strutils.py
new file mode 100644
index 00000000..668753ba
--- /dev/null
+++ b/hooks/charmhelpers/core/strutils.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2014-2015 Canonical Limited.
+#
+# This file is part of charm-helpers.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
+
+
+def bool_from_string(value):
+    """Interpret string value as boolean.
+
+    Returns True if value translates to True otherwise False.
+    """
+    if isinstance(value, str):
+        value = value.lower()
+    else:
+        msg = "Unable to interpret non-string value '%s' as boolean" % (value)
+        raise ValueError(msg)
+
+    if value in ['y', 'yes', 'true', 't']:
+        return True
+    elif value in ['n', 'no', 'false', 'f']:
+        return False
+
+    msg = "Unable to interpret string value '%s' as boolean" % (value)
+    raise ValueError(msg)
diff --git a/hooks/charmhelpers/core/sysctl.py b/hooks/charmhelpers/core/sysctl.py
index 8e1b9eeb..21cc8ab2 100644
--- a/hooks/charmhelpers/core/sysctl.py
+++ b/hooks/charmhelpers/core/sysctl.py
@@ -17,8 +17,6 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
-
 import yaml
 
 from subprocess import check_call
@@ -29,6 +27,8 @@ from charmhelpers.core.hookenv import (
     ERROR,
 )
 
+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
+
 
 def create(sysctl_dict, sysctl_file):
     """Creates a sysctl.conf file from a YAML associative array
diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py
index 01329ab7..3000134a 100644
--- a/hooks/charmhelpers/core/unitdata.py
+++ b/hooks/charmhelpers/core/unitdata.py
@@ -435,7 +435,7 @@ class HookData(object):
             os.path.join(charm_dir, 'revision')).read().strip()
         charm_rev = charm_rev or '0'
         revs = self.kv.get('charm_revisions', [])
-        if not charm_rev in revs:
+        if charm_rev not in revs:
             revs.append(charm_rev.strip() or '0')
             self.kv.set('charm_revisions', revs)
 
diff --git a/hooks/charmhelpers/fetch/archiveurl.py b/hooks/charmhelpers/fetch/archiveurl.py
index d25a0ddd..8dfce505 100644
--- a/hooks/charmhelpers/fetch/archiveurl.py
+++ b/hooks/charmhelpers/fetch/archiveurl.py
@@ -18,6 +18,16 @@ import os
 import hashlib
 import re
 
+from charmhelpers.fetch import (
+    BaseFetchHandler,
+    UnhandledSource
+)
+from charmhelpers.payload.archive import (
+    get_archive_handler,
+    extract,
+)
+from charmhelpers.core.host import mkdir, check_hash
+
 import six
 if six.PY3:
     from urllib.request import (
@@ -35,16 +45,6 @@ else:
     )
     from urlparse import urlparse, urlunparse, parse_qs
 
-from charmhelpers.fetch import (
-    BaseFetchHandler,
-    UnhandledSource
-)
-from charmhelpers.payload.archive import (
-    get_archive_handler,
-    extract,
-)
-from charmhelpers.core.host import mkdir, check_hash
-
 
 def splituser(host):
     '''urllib.splituser(), but six's support of this seems broken'''
diff --git a/hooks/charmhelpers/fetch/giturl.py b/hooks/charmhelpers/fetch/giturl.py
index 5376786b..93aae87b 100644
--- a/hooks/charmhelpers/fetch/giturl.py
+++ b/hooks/charmhelpers/fetch/giturl.py
@@ -32,7 +32,7 @@ except ImportError:
     apt_install("python-git")
     from git import Repo
 
-from git.exc import GitCommandError
+from git.exc import GitCommandError  # noqa E402
 
 
 class GitUrlFetchHandler(BaseFetchHandler):
diff --git a/hooks/neutron_ovs_context.py b/hooks/neutron_ovs_context.py
index fb5f89c1..61e5a679 100644
--- a/hooks/neutron_ovs_context.py
+++ b/hooks/neutron_ovs_context.py
@@ -6,24 +6,18 @@ from charmhelpers.core.hookenv import (
     unit_get,
 )
 from charmhelpers.core.host import list_nics, get_nic_hwaddr
+from charmhelpers.core.strutils import bool_from_string
 from charmhelpers.contrib.openstack import context
 from charmhelpers.core.host import service_running, service_start
 from charmhelpers.contrib.network.ovs import add_bridge, add_bridge_port
 from charmhelpers.contrib.openstack.utils import get_host_ip
 from charmhelpers.contrib.network.ip import get_address_in_network
-import ast
 import re
 
 OVS_BRIDGE = 'br-int'
 DATA_BRIDGE = 'br-data'
 
 
-def to_boolean(option):
-    if option is None:
-        return False
-    return ast.literal_eval(option)
-
-
 def _neutron_api_settings():
     '''
     Inspects current neutron-plugin relation
@@ -39,9 +33,9 @@ def _neutron_api_settings():
             if 'l2-population' not in rdata:
                 continue
             neutron_settings = {
-                'l2_population': to_boolean(rdata['l2-population']),
+                'l2_population': bool_from_string(rdata['l2-population']),
                 'overlay_network_type': rdata['overlay-network-type'],
-                'neutron_security_groups': to_boolean(
+                'neutron_security_groups': bool_from_string(
                     rdata['neutron-security-groups']
                 ),
             }

From 00b08d6e4707b4cfcafef62011ec939ee0fdf3b5 Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Mon, 16 Feb 2015 12:04:50 +0000
Subject: [PATCH 12/16] [gnuoy,trivial] charmhelpers sync

---
 hooks/charmhelpers/core/strutils.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/hooks/charmhelpers/core/strutils.py b/hooks/charmhelpers/core/strutils.py
index 668753ba..efc4402e 100644
--- a/hooks/charmhelpers/core/strutils.py
+++ b/hooks/charmhelpers/core/strutils.py
@@ -17,18 +17,22 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
+import six
+
 
 def bool_from_string(value):
     """Interpret string value as boolean.
 
     Returns True if value translates to True otherwise False.
     """
-    if isinstance(value, str):
-        value = value.lower()
+    if isinstance(value, six.string_types):
+        value = six.text_type(value)
     else:
         msg = "Unable to interpret non-string value '%s' as boolean" % (value)
         raise ValueError(msg)
 
+    value = value.strip().lower()
+
     if value in ['y', 'yes', 'true', 't']:
         return True
     elif value in ['n', 'no', 'false', 'f']:

From e3712301afcc6291df772a31375c841c59ee5083 Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Mon, 16 Feb 2015 12:07:08 +0000
Subject: [PATCH 13/16] Sync charmhelpers

---
 hooks/charmhelpers/core/strutils.py                       | 8 ++++++--
 tests/basic_deployment.py                                 | 8 --------
 tests/charmhelpers/contrib/openstack/amulet/deployment.py | 7 +++++--
 3 files changed, 11 insertions(+), 12 deletions(-)

diff --git a/hooks/charmhelpers/core/strutils.py b/hooks/charmhelpers/core/strutils.py
index 668753ba..efc4402e 100644
--- a/hooks/charmhelpers/core/strutils.py
+++ b/hooks/charmhelpers/core/strutils.py
@@ -17,18 +17,22 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
+import six
+
 
 def bool_from_string(value):
     """Interpret string value as boolean.
 
     Returns True if value translates to True otherwise False.
     """
-    if isinstance(value, str):
-        value = value.lower()
+    if isinstance(value, six.string_types):
+        value = six.text_type(value)
     else:
         msg = "Unable to interpret non-string value '%s' as boolean" % (value)
         raise ValueError(msg)
 
+    value = value.strip().lower()
+
     if value in ['y', 'yes', 'true', 't']:
         return True
     elif value in ['n', 'no', 'false', 'f']:
diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py
index 6e96bdc3..31dd3599 100644
--- a/tests/basic_deployment.py
+++ b/tests/basic_deployment.py
@@ -161,14 +161,6 @@ class NeutronOVSBasicDeployment(OpenStackAmuletDeployment):
                                            ['vxlan', 'gre'],
                                            'agent')
 
-#    def test_secgroup_propagation(self):
-#        """Verify that neutron-api secgroup propagates to neutron-ovs"""
-#        self.check_ml2_setting_propagation('neutron-api',
-#                                           'neutron-security-groups',
-#                                           'enable_security_group',
-#                                           ['False', 'True'],
-#                                           'securitygroup')
-
     def test_secgroup_propagation_local_override(self):
         """Verify disable-security-groups overrides what neutron-api says"""
         unit = self.compute_sentry
diff --git a/tests/charmhelpers/contrib/openstack/amulet/deployment.py b/tests/charmhelpers/contrib/openstack/amulet/deployment.py
index c50d3ec6..0cfeaa4c 100644
--- a/tests/charmhelpers/contrib/openstack/amulet/deployment.py
+++ b/tests/charmhelpers/contrib/openstack/amulet/deployment.py
@@ -71,16 +71,19 @@ class OpenStackAmuletDeployment(AmuletDeployment):
         services.append(this_service)
         use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
                       'ceph-osd', 'ceph-radosgw']
+        # Openstack subordinate charms do not expose an origin option as that
+        # is controlled by the principle
+        ignore = ['neutron-openvswitch']
 
         if self.openstack:
             for svc in services:
-                if svc['name'] not in use_source:
+                if svc['name'] not in use_source + ignore:
                     config = {'openstack-origin': self.openstack}
                     self.d.configure(svc['name'], config)
 
         if self.source:
             for svc in services:
-                if svc['name'] in use_source:
+                if svc['name'] in use_source and svc['name'] not in ignore:
                     config = {'source': self.source}
                     self.d.configure(svc['name'], config)
 

From 4a1e65a8ba9b44fecd86b9bf88f7b22ff0848289 Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Mon, 16 Feb 2015 16:05:10 +0000
Subject: [PATCH 14/16] Increase time before running service_restarted check to
 avoid /proc/<pid> errors

---
 tests/basic_deployment.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py
index 31dd3599..b6dc8e7e 100644
--- a/tests/basic_deployment.py
+++ b/tests/basic_deployment.py
@@ -193,7 +193,7 @@ class NeutronOVSBasicDeployment(OpenStackAmuletDeployment):
         self.d.configure('neutron-openvswitch', {'use-syslog': 'True'})
         if not u.service_restarted(self.compute_sentry,
                                    'neutron-openvswitch-agent', conf,
-                                   pgrep_full=True, sleep_time=20):
+                                   pgrep_full=True, sleep_time=60):
             self.d.configure('neutron-openvswitch', {'use-syslog': 'False'})
             msg = ('service neutron-openvswitch-agent did not restart after '
                    'config change')

From 7b90df8ea09c67ea091481212589929a975a3ebf Mon Sep 17 00:00:00 2001
From: Liam Young <liam.young@canonical.com>
Date: Tue, 17 Feb 2015 08:55:58 +0000
Subject: [PATCH 15/16] Disable lint checks for unused DEBUG

---
 tests/basic_deployment.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py
index b6dc8e7e..80597c73 100644
--- a/tests/basic_deployment.py
+++ b/tests/basic_deployment.py
@@ -9,7 +9,7 @@ from charmhelpers.contrib.openstack.amulet.deployment import (
 
 from charmhelpers.contrib.openstack.amulet.utils import (
     OpenStackAmuletUtils,
-    DEBUG,
+    DEBUG, # flake8: noqa
     ERROR
 )
 

From 6c5ac84c4afeaedc072ac82c3fea5e3b162c2e53 Mon Sep 17 00:00:00 2001
From: Edward Hope-Morley <edward.hope-morley@canonical.com>
Date: Tue, 24 Feb 2015 11:06:13 +0000
Subject: [PATCH 16/16] [trivial] charmhelpers sync

---
 .../charmhelpers/contrib/hahelpers/cluster.py |   6 +-
 .../contrib/openstack/files/__init__.py       |  18 +++
 .../contrib/openstack/files/check_haproxy.sh  |  32 +++++
 .../files/check_haproxy_queue_depth.sh        |  30 +++++
 hooks/charmhelpers/contrib/openstack/ip.py    |  37 ++++++
 hooks/charmhelpers/contrib/openstack/utils.py |   1 +
 hooks/charmhelpers/core/fstab.py              |   4 +-
 tests/charmhelpers/contrib/amulet/utils.py    | 124 +++++++++++++++++-
 8 files changed, 247 insertions(+), 5 deletions(-)
 create mode 100644 hooks/charmhelpers/contrib/openstack/files/__init__.py
 create mode 100755 hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh
 create mode 100755 hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh

diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py
index 9a2588b6..9333efc3 100644
--- a/hooks/charmhelpers/contrib/hahelpers/cluster.py
+++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py
@@ -48,6 +48,9 @@ from charmhelpers.core.hookenv import (
 from charmhelpers.core.decorators import (
     retry_on_exception,
 )
+from charmhelpers.core.strutils import (
+    bool_from_string,
+)
 
 
 class HAIncompleteConfig(Exception):
@@ -164,7 +167,8 @@ def https():
     .
     returns: boolean
     '''
-    if config_get('use-https') == "yes":
+    use_https = config_get('use-https')
+    if use_https and bool_from_string(use_https):
         return True
     if config_get('ssl_cert') and config_get('ssl_key'):
         return True
diff --git a/hooks/charmhelpers/contrib/openstack/files/__init__.py b/hooks/charmhelpers/contrib/openstack/files/__init__.py
new file mode 100644
index 00000000..75876796
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/files/__init__.py
@@ -0,0 +1,18 @@
+# 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/>.
+
+# dummy __init__.py to fool syncer into thinking this is a syncable python
+# module
diff --git a/hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh b/hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh
new file mode 100755
index 00000000..eb8527f5
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+#--------------------------------------------
+# This file is managed by Juju
+#--------------------------------------------
+#
+# Copyright 2009,2012 Canonical Ltd.
+# Author: Tom Haddon
+
+CRITICAL=0
+NOTACTIVE=''
+LOGFILE=/var/log/nagios/check_haproxy.log
+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
+
+for appserver in $(grep '    server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
+do
+    output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 --regex="class=\"(active|backup)(2|3).*${appserver}" -e ' 200 OK')
+    if [ $? != 0 ]; then
+        date >> $LOGFILE
+        echo $output >> $LOGFILE
+        /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
+        CRITICAL=1
+        NOTACTIVE="${NOTACTIVE} $appserver"
+    fi
+done
+
+if [ $CRITICAL = 1 ]; then
+    echo "CRITICAL:${NOTACTIVE}"
+    exit 2
+fi
+
+echo "OK: All haproxy instances looking good"
+exit 0
diff --git a/hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh b/hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh
new file mode 100755
index 00000000..3ebb5329
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+#--------------------------------------------
+# This file is managed by Juju
+#--------------------------------------------
+#                                       
+# Copyright 2009,2012 Canonical Ltd.
+# Author: Tom Haddon
+
+# These should be config options at some stage
+CURRQthrsh=0
+MAXQthrsh=100
+
+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
+
+HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
+
+for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
+do
+    CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
+    MAXQ=$(echo "$HAPROXYSTATS"  | grep $BACKEND | grep BACKEND | cut -d , -f 4)
+
+    if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
+        echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
+        exit 2
+    fi
+done
+
+echo "OK: All haproxy queue depths looking good"
+exit 0
+
diff --git a/hooks/charmhelpers/contrib/openstack/ip.py b/hooks/charmhelpers/contrib/openstack/ip.py
index 9eabed73..29bbddcb 100644
--- a/hooks/charmhelpers/contrib/openstack/ip.py
+++ b/hooks/charmhelpers/contrib/openstack/ip.py
@@ -26,6 +26,8 @@ from charmhelpers.contrib.network.ip import (
 )
 from charmhelpers.contrib.hahelpers.cluster import is_clustered
 
+from functools import partial
+
 PUBLIC = 'public'
 INTERNAL = 'int'
 ADMIN = 'admin'
@@ -107,3 +109,38 @@ def resolve_address(endpoint_type=PUBLIC):
                          "clustered=%s)" % (net_type, clustered))
 
     return resolved_address
+
+
+def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
+                 override=None):
+    """Returns the correct endpoint URL to advertise to Keystone.
+
+    This method provides the correct endpoint URL which should be advertised to
+    the keystone charm for endpoint creation. This method allows for the url to
+    be overridden to force a keystone endpoint to have specific URL for any of
+    the defined scopes (admin, internal, public).
+
+    :param configs: OSTemplateRenderer config templating object to inspect
+                    for a complete https context.
+    :param url_template: str format string for creating the url template. Only
+                         two values will be passed - the scheme+hostname
+                        returned by the canonical_url and the port.
+    :param endpoint_type: str endpoint type to resolve.
+    :param override: str the name of the config option which overrides the
+                     endpoint URL defined by the charm itself. None will
+                     disable any overrides (default).
+    """
+    if override:
+        # Return any user-defined overrides for the keystone endpoint URL.
+        user_value = config(override)
+        if user_value:
+            return user_value.strip()
+
+    return url_template % (canonical_url(configs, endpoint_type), port)
+
+
+public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
+
+internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
+
+admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py
index 26259a03..af2b3596 100644
--- a/hooks/charmhelpers/contrib/openstack/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/utils.py
@@ -103,6 +103,7 @@ SWIFT_CODENAMES = OrderedDict([
     ('2.1.0', 'juno'),
     ('2.2.0', 'juno'),
     ('2.2.1', 'kilo'),
+    ('2.2.2', 'kilo'),
 ])
 
 DEFAULT_LOOPBACK_SIZE = '5G'
diff --git a/hooks/charmhelpers/core/fstab.py b/hooks/charmhelpers/core/fstab.py
index 9cdcc886..3056fbac 100644
--- a/hooks/charmhelpers/core/fstab.py
+++ b/hooks/charmhelpers/core/fstab.py
@@ -77,7 +77,7 @@ class Fstab(io.FileIO):
         for line in self.readlines():
             line = line.decode('us-ascii')
             try:
-                if line.strip() and not line.startswith("#"):
+                if line.strip() and not line.strip().startswith("#"):
                     yield self._hydrate_entry(line)
             except ValueError:
                 pass
@@ -104,7 +104,7 @@ class Fstab(io.FileIO):
 
         found = False
         for index, line in enumerate(lines):
-            if not line.startswith("#"):
+            if line.strip() and not line.strip().startswith("#"):
                 if self._hydrate_entry(line) == entry:
                     found = True
                     break
diff --git a/tests/charmhelpers/contrib/amulet/utils.py b/tests/charmhelpers/contrib/amulet/utils.py
index 3464b873..65219d33 100644
--- a/tests/charmhelpers/contrib/amulet/utils.py
+++ b/tests/charmhelpers/contrib/amulet/utils.py
@@ -169,8 +169,13 @@ class AmuletUtils(object):
             cmd = 'pgrep -o -f {}'.format(service)
         else:
             cmd = 'pgrep -o {}'.format(service)
-        proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
-        return self._get_dir_mtime(sentry_unit, proc_dir)
+        cmd = cmd + '  | grep  -v pgrep || exit 0'
+        cmd_out = sentry_unit.run(cmd)
+        self.log.debug('CMDout: ' + str(cmd_out))
+        if cmd_out[0]:
+            self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
+            proc_dir = '/proc/{}'.format(cmd_out[0].strip())
+            return self._get_dir_mtime(sentry_unit, proc_dir)
 
     def service_restarted(self, sentry_unit, service, filename,
                           pgrep_full=False, sleep_time=20):
@@ -187,6 +192,121 @@ class AmuletUtils(object):
         else:
             return False
 
+    def service_restarted_since(self, sentry_unit, mtime, service,
+                                pgrep_full=False, sleep_time=20,
+                                retry_count=2):
+        """Check if service was been started after a given time.
+
+        Args:
+          sentry_unit (sentry): The sentry unit to check for the service on
+          mtime (float): The epoch time to check against
+          service (string): service name to look for in process table
+          pgrep_full (boolean): Use full command line search mode with pgrep
+          sleep_time (int): Seconds to sleep before looking for process
+          retry_count (int): If service is not found, how many times to retry
+
+        Returns:
+          bool: True if service found and its start time it newer than mtime,
+                False if service is older than mtime or if service was
+                not found.
+        """
+        self.log.debug('Checking %s restarted since %s' % (service, mtime))
+        time.sleep(sleep_time)
+        proc_start_time = self._get_proc_start_time(sentry_unit, service,
+                                                    pgrep_full)
+        while retry_count > 0 and not proc_start_time:
+            self.log.debug('No pid file found for service %s, will retry %i '
+                           'more times' % (service, retry_count))
+            time.sleep(30)
+            proc_start_time = self._get_proc_start_time(sentry_unit, service,
+                                                        pgrep_full)
+            retry_count = retry_count - 1
+
+        if not proc_start_time:
+            self.log.warn('No proc start time found, assuming service did '
+                          'not start')
+            return False
+        if proc_start_time >= mtime:
+            self.log.debug('proc start time is newer than provided mtime'
+                           '(%s >= %s)' % (proc_start_time, mtime))
+            return True
+        else:
+            self.log.warn('proc start time (%s) is older than provided mtime '
+                          '(%s), service did not restart' % (proc_start_time,
+                                                             mtime))
+            return False
+
+    def config_updated_since(self, sentry_unit, filename, mtime,
+                             sleep_time=20):
+        """Check if file was modified after a given time.
+
+        Args:
+          sentry_unit (sentry): The sentry unit to check the file mtime on
+          filename (string): The file to check mtime of
+          mtime (float): The epoch time to check against
+          sleep_time (int): Seconds to sleep before looking for process
+
+        Returns:
+          bool: True if file was modified more recently than mtime, False if
+                file was modified before mtime,
+        """
+        self.log.debug('Checking %s updated since %s' % (filename, mtime))
+        time.sleep(sleep_time)
+        file_mtime = self._get_file_mtime(sentry_unit, filename)
+        if file_mtime >= mtime:
+            self.log.debug('File mtime is newer than provided mtime '
+                           '(%s >= %s)' % (file_mtime, mtime))
+            return True
+        else:
+            self.log.warn('File mtime %s is older than provided mtime %s'
+                          % (file_mtime, mtime))
+            return False
+
+    def validate_service_config_changed(self, sentry_unit, mtime, service,
+                                        filename, pgrep_full=False,
+                                        sleep_time=20, retry_count=2):
+        """Check service and file were updated after mtime
+
+        Args:
+          sentry_unit (sentry): The sentry unit to check for the service on
+          mtime (float): The epoch time to check against
+          service (string): service name to look for in process table
+          filename (string): The file to check mtime of
+          pgrep_full (boolean): Use full command line search mode with pgrep
+          sleep_time (int): Seconds to sleep before looking for process
+          retry_count (int): If service is not found, how many times to retry
+
+        Typical Usage:
+            u = OpenStackAmuletUtils(ERROR)
+            ...
+            mtime = u.get_sentry_time(self.cinder_sentry)
+            self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
+            if not u.validate_service_config_changed(self.cinder_sentry,
+                                                     mtime,
+                                                     'cinder-api',
+                                                     '/etc/cinder/cinder.conf')
+                amulet.raise_status(amulet.FAIL, msg='update failed')
+        Returns:
+          bool: True if both service and file where updated/restarted after
+                mtime, False if service is older than mtime or if service was
+                not found or if filename was modified before mtime.
+        """
+        self.log.debug('Checking %s restarted since %s' % (service, mtime))
+        time.sleep(sleep_time)
+        service_restart = self.service_restarted_since(sentry_unit, mtime,
+                                                       service,
+                                                       pgrep_full=pgrep_full,
+                                                       sleep_time=0,
+                                                       retry_count=retry_count)
+        config_update = self.config_updated_since(sentry_unit, filename, mtime,
+                                                  sleep_time=0)
+        return service_restart and config_update
+
+    def get_sentry_time(self, sentry_unit):
+        """Return current epoch time on a sentry"""
+        cmd = "date +'%s'"
+        return float(sentry_unit.run(cmd)[0])
+
     def relation_error(self, name, data):
         return 'unexpected relation data in {} - {}'.format(name, data)