From 89cfe5f01b3465d359f92ca5910035d41da65654 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 9 Mar 2015 10:35:30 +0000 Subject: [PATCH 01/12] Charmsync Leadership election --- charm-helpers-hooks.yaml | 2 +- hooks/charmhelpers/contrib/hahelpers/cluster.py | 11 +++++++++-- hooks/charmhelpers/core/hookenv.py | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/charm-helpers-hooks.yaml b/charm-helpers-hooks.yaml index 2a37274e..b583ea45 100644 --- a/charm-helpers-hooks.yaml +++ b/charm-helpers-hooks.yaml @@ -1,4 +1,4 @@ -branch: lp:charm-helpers +branch: lp:~le-charmers/charm-helpers/leadership-election destination: hooks/charmhelpers include: - core diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py index 9333efc3..e9f85b2b 100644 --- a/hooks/charmhelpers/contrib/hahelpers/cluster.py +++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py @@ -44,6 +44,7 @@ from charmhelpers.core.hookenv import ( ERROR, WARNING, unit_get, + is_leader as juju_is_leader ) from charmhelpers.core.decorators import ( retry_on_exception, @@ -66,12 +67,18 @@ def is_elected_leader(resource): Returns True if the charm executing this is the elected cluster leader. It relies on two mechanisms to determine leadership: - 1. If the charm is part of a corosync cluster, call corosync to + 1. If juju is sufficiently new and leadership election is supported, + the is_leader command will be used. + 2. If the charm is part of a corosync cluster, call corosync to determine leadership. - 2. If the charm is not part of a corosync cluster, the leader is + 3. If the charm is not part of a corosync cluster, the leader is determined as being "the alive unit with the lowest unit numer". In other words, the oldest surviving unit. """ + try: + return juju_is_leader() + except NotImplementedError: + pass if is_clustered(): if not is_crm_leader(resource): log('Deferring action to CRM leader.', level=INFO) diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index cf552b39..96cfd7e1 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -566,3 +566,17 @@ class Hooks(object): def charm_dir(): """Return the root directory of the current charm""" return os.environ.get('CHARM_DIR') + + +def is_leader(): + """Does the current unit hold the juju leadership + + Uses juju to determine whether the current unit is the leader of its peers + """ + try: + leader = json.loads( + subprocess.check_output(['is-leader', '--format=json']) + ) + return (leader is True) + except ValueError: + raise NotImplementedError From f41a673ec4b4214e9f0a9009a434154bb5bef3db Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 9 Mar 2015 11:08:07 +0000 Subject: [PATCH 02/12] Charmsync Leadership election --- hooks/charmhelpers/core/hookenv.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index 96cfd7e1..7a1f65f1 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -580,3 +580,5 @@ def is_leader(): return (leader is True) except ValueError: raise NotImplementedError + except CalledProcessError: + raise NotImplementedError From 7ace28c310448e92d8e11b687733fe9bc9c1bc9f Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 9 Mar 2015 12:04:17 +0000 Subject: [PATCH 03/12] Charmsync Leadership election --- hooks/charmhelpers/core/hookenv.py | 47 ++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index 7a1f65f1..606ee68d 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -568,6 +568,20 @@ def charm_dir(): return os.environ.get('CHARM_DIR') +def translate_exc(from_exc, to_exc): + def inner_translate_exc1(f): + def inner_translate_exc2(*args, **kwargs): + try: + return f(*args, **kwargs) + except from_exc: + raise to_exc + + return inner_translate_exc2 + + return inner_translate_exc1 + + +@translate_exc(from_exc=CalledProcessError, to_exc=NotImplementedError) def is_leader(): """Does the current unit hold the juju leadership @@ -580,5 +594,34 @@ def is_leader(): return (leader is True) except ValueError: raise NotImplementedError - except CalledProcessError: - raise NotImplementedError + + +@cached +@translate_exc(from_exc=CalledProcessError, to_exc=NotImplementedError) +def leader_get(attribute=None): + """Juju leader get value(s)""" + cmd = ['leader-get', '--format=json'] + [attribute or '-'] + try: + return json.loads(subprocess.check_output(cmd).decode('UTF-8')) + except ValueError: + return None + except CalledProcessError as e: + if e.returncode == 2: + return None + + raise + + +@translate_exc(from_exc=CalledProcessError, to_exc=NotImplementedError) +def leader_set(settings=None, **kwargs): + """Juju leader set value(s)""" + cmd = ['leader-set'] + settings = settings or {} + settings.update(kwargs) + for k, v in settings.iteritems(): + if v is None: + cmd.append('{}='.format(k)) + else: + cmd.append('{}={}'.format(k, v)) + + subprocess.check_call(cmd) From fbf77194b4e42eaadd6419d7e2e6c3631376e225 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 9 Mar 2015 16:15:42 +0000 Subject: [PATCH 04/12] Replace deprecated eligible_leader with is_elected_leader --- hooks/cinder_hooks.py | 6 +++--- hooks/cinder_utils.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hooks/cinder_hooks.py b/hooks/cinder_hooks.py index 520c181b..148112e6 100755 --- a/hooks/cinder_hooks.py +++ b/hooks/cinder_hooks.py @@ -62,7 +62,7 @@ from charmhelpers.contrib.storage.linux.ceph import ( ) from charmhelpers.contrib.hahelpers.cluster import ( - eligible_leader, + is_elected_leader, get_hacluster_config, ) @@ -175,7 +175,7 @@ def db_changed(): juju_log('shared-db relation incomplete. Peer not ready?') return CONFIGS.write(CINDER_CONF) - if eligible_leader(CLUSTER_RES): + if is_elected_leader(CLUSTER_RES): # Bugs 1353135 & 1187508. Dbs can appear to be ready before the units # acl entry has been added. So, if the db supports passing a list of # permitted units then check if we're in the list. @@ -194,7 +194,7 @@ def pgsql_db_changed(): juju_log('pgsql-db relation incomplete. Peer not ready?') return CONFIGS.write(CINDER_CONF) - if eligible_leader(CLUSTER_RES): + if is_elected_leader(CLUSTER_RES): juju_log('Cluster leader, performing db sync') migrate_database() diff --git a/hooks/cinder_utils.py b/hooks/cinder_utils.py index 04de94bc..a671e31a 100644 --- a/hooks/cinder_utils.py +++ b/hooks/cinder_utils.py @@ -29,7 +29,7 @@ from charmhelpers.core.host import ( from charmhelpers.contrib.openstack.alternatives import install_alternative from charmhelpers.contrib.hahelpers.cluster import ( - eligible_leader, + is_elected_leader, ) from charmhelpers.contrib.storage.linux.utils import ( @@ -445,7 +445,7 @@ def do_openstack_upgrade(configs): # Stop/start services and migrate DB if leader [service_stop(s) for s in services()] - if eligible_leader(CLUSTER_RES): + if is_elected_leader(CLUSTER_RES): migrate_database() [service_start(s) for s in services()] From c2fcfb53bea9f5c00ca0d38503114a3ecde189cf Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 9 Mar 2015 16:35:25 +0000 Subject: [PATCH 05/12] Charmhelper leader sync --- hooks/charmhelpers/core/hookenv.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index 606ee68d..d2b70c03 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -596,13 +596,14 @@ def is_leader(): raise NotImplementedError -@cached @translate_exc(from_exc=CalledProcessError, to_exc=NotImplementedError) def leader_get(attribute=None): """Juju leader get value(s)""" cmd = ['leader-get', '--format=json'] + [attribute or '-'] try: - return json.loads(subprocess.check_output(cmd).decode('UTF-8')) + ret = json.loads(subprocess.check_output(cmd).decode('UTF-8')) + log("Juju leader-get '%s' = '%s'" % (attribute, ret), level=DEBUG) + return ret except ValueError: return None except CalledProcessError as e: @@ -615,6 +616,7 @@ def leader_get(attribute=None): @translate_exc(from_exc=CalledProcessError, to_exc=NotImplementedError) def leader_set(settings=None, **kwargs): """Juju leader set value(s)""" + log("Juju leader-set '%s'" % (settings), level=DEBUG) cmd = ['leader-set'] settings = settings or {} settings.update(kwargs) From 7f8e97d40751f403857755d0a32f5b7f4e902052 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Tue, 10 Mar 2015 09:06:37 +0000 Subject: [PATCH 06/12] Fix unit_tests --- unit_tests/test_cinder_hooks.py | 10 +++++----- unit_tests/test_cinder_utils.py | 6 +++--- unit_tests/test_cluster_hooks.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/unit_tests/test_cinder_hooks.py b/unit_tests/test_cinder_hooks.py index e3e40048..a4057908 100644 --- a/unit_tests/test_cinder_hooks.py +++ b/unit_tests/test_cinder_hooks.py @@ -64,7 +64,7 @@ TO_PATCH = [ 'openstack_upgrade_available', # charmhelpers.contrib.hahelpers.cluster_utils 'canonical_url', - 'eligible_leader', + 'is_elected_leader', 'get_hacluster_config', 'execd_preinstall', 'get_ipv6_addr', @@ -174,7 +174,7 @@ class TestChangedHooks(CharmTestCase): self.relation_get.return_value = 'cinder/1 cinder/2' self.local_unit.return_value = 'cinder/0' self.CONFIGS.complete_contexts.return_value = ['shared-db'] - self.eligible_leader.return_value = True + self.is_elected_leader.return_value = True hooks.hooks.execute(['hooks/shared-db-relation-changed']) self.assertFalse(self.migrate_database.called) @@ -188,7 +188,7 @@ class TestChangedHooks(CharmTestCase): 'It does not migrate database when not leader' self.relation_get.return_value = 'cinder/0 cinder/1' self.local_unit.return_value = 'cinder/0' - self.eligible_leader.return_value = False + self.is_elected_leader.return_value = False self.CONFIGS.complete_contexts.return_value = ['shared-db'] hooks.hooks.execute(['hooks/shared-db-relation-changed']) self.CONFIGS.write.assert_called_with('/etc/cinder/cinder.conf') @@ -196,7 +196,7 @@ class TestChangedHooks(CharmTestCase): def test_pgsql_db_changed_not_leader(self): 'It does not migrate database when not leader' - self.eligible_leader.return_value = False + self.is_elected_leader.return_value = False self.CONFIGS.complete_contexts.return_value = ['pgsql-db'] hooks.hooks.execute(['hooks/pgsql-db-relation-changed']) self.CONFIGS.write.assert_called_with('/etc/cinder/cinder.conf') @@ -444,7 +444,7 @@ class TestJoinedHooks(CharmTestCase): def test_ceph_changed_no_leadership(self): '''It does not attempt to create ceph pool if not leader''' - self.eligible_leader.return_value = False + self.is_elected_leader.return_value = False self.service_name.return_value = 'cinder' self.ensure_ceph_keyring.return_value = True hooks.hooks.execute(['hooks/ceph-relation-changed']) diff --git a/unit_tests/test_cinder_utils.py b/unit_tests/test_cinder_utils.py index eabe2d5d..54c61935 100644 --- a/unit_tests/test_cinder_utils.py +++ b/unit_tests/test_cinder_utils.py @@ -33,7 +33,7 @@ TO_PATCH = [ 'get_os_codename_package', 'get_os_codename_install_source', 'configure_installation_source', - 'eligible_leader', + 'is_elected_leader', 'templating', 'install_alternative', # fetch @@ -475,7 +475,7 @@ class TestCinderUtils(CharmTestCase): self.config.side_effect = None self.config.return_value = 'cloud:precise-havana' services.return_value = ['cinder-api', 'cinder-volume'] - self.eligible_leader.return_value = True + self.is_elected_leader.return_value = True self.get_os_codename_install_source.return_value = 'havana' configs = MagicMock() cinder_utils.do_openstack_upgrade(configs) @@ -494,7 +494,7 @@ class TestCinderUtils(CharmTestCase): self.config.side_effect = None self.config.return_value = 'cloud:precise-havana' services.return_value = ['cinder-api', 'cinder-volume'] - self.eligible_leader.return_value = False + self.is_elected_leader.return_value = False self.get_os_codename_install_source.return_value = 'havana' configs = MagicMock() cinder_utils.do_openstack_upgrade(configs) diff --git a/unit_tests/test_cluster_hooks.py b/unit_tests/test_cluster_hooks.py index 1e27b991..8aa8fbc6 100644 --- a/unit_tests/test_cluster_hooks.py +++ b/unit_tests/test_cluster_hooks.py @@ -48,7 +48,7 @@ TO_PATCH = [ # charmhelpers.contrib.openstack.openstack_utils 'configure_installation_source', # charmhelpers.contrib.hahelpers.cluster_utils - 'eligible_leader', + 'is_elected_leader', 'get_hacluster_config', # charmhelpers.contrib.network.ip 'get_iface_for_address', From 7853372db1221b7422e457091c20413e464d230a Mon Sep 17 00:00:00 2001 From: James Page Date: Tue, 10 Mar 2015 15:06:40 +0000 Subject: [PATCH 07/12] Add retry for database migrations to deal with single shot ha deployments --- hooks/cinder_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hooks/cinder_utils.py b/hooks/cinder_utils.py index a671e31a..036c779d 100644 --- a/hooks/cinder_utils.py +++ b/hooks/cinder_utils.py @@ -62,6 +62,10 @@ from charmhelpers.contrib.openstack.utils import ( get_os_codename_install_source, ) +from charmhelpers.core.decorators import ( + retry_on_exception, +) + import cinder_contexts COMMON_PACKAGES = [ @@ -400,6 +404,9 @@ def _parse_block_device(block_device): return ('/dev/{}'.format(block_device), 0) +# NOTE(jamespage): Retry deals with sync issues during one-shot HA deploys. +# mysql might be restarting or suchlike. +@retry_on_exception(5, base_delay=3, exc_type=subprocess.CalledProcessError) def migrate_database(): 'Runs cinder-manage to initialize a new database or migrate existing' cmd = ['cinder-manage', 'db', 'sync'] From d6b2d58c758bc959d7800fa76e1bf1089044d0f7 Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 12 Mar 2015 12:40:57 +0000 Subject: [PATCH 08/12] Resync helpers --- hooks/charmhelpers/contrib/hahelpers/cluster.py | 5 ++++- hooks/charmhelpers/core/hookenv.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py index e9f85b2b..ebec3790 100644 --- a/hooks/charmhelpers/contrib/hahelpers/cluster.py +++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py @@ -78,7 +78,10 @@ def is_elected_leader(resource): try: return juju_is_leader() except NotImplementedError: - pass + log('Juju leadership election feature not enabled' + ', using fallback support', + level=WARNING) + if is_clustered(): if not is_crm_leader(resource): log('Deferring action to CRM leader.', level=INFO) diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index d2b70c03..e68d4b57 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -581,7 +581,7 @@ def translate_exc(from_exc, to_exc): return inner_translate_exc1 -@translate_exc(from_exc=CalledProcessError, to_exc=NotImplementedError) +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) def is_leader(): """Does the current unit hold the juju leadership @@ -596,7 +596,7 @@ def is_leader(): raise NotImplementedError -@translate_exc(from_exc=CalledProcessError, to_exc=NotImplementedError) +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) def leader_get(attribute=None): """Juju leader get value(s)""" cmd = ['leader-get', '--format=json'] + [attribute or '-'] @@ -613,7 +613,7 @@ def leader_get(attribute=None): raise -@translate_exc(from_exc=CalledProcessError, to_exc=NotImplementedError) +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) def leader_set(settings=None, **kwargs): """Juju leader set value(s)""" log("Juju leader-set '%s'" % (settings), level=DEBUG) From db1888b566b728b4cc2221fb1a23d18d50909b4f Mon Sep 17 00:00:00 2001 From: Edward Hope-Morley Date: Thu, 26 Mar 2015 10:37:04 -0700 Subject: [PATCH 09/12] synced charm-helpers --- charm-helpers-tests.yaml | 2 +- .../charmhelpers/contrib/openstack/context.py | 205 +++++++++++++++++- .../charmhelpers/contrib/openstack/neutron.py | 70 ++++++ .../templates/section-keystone-authtoken | 9 + .../openstack/templates/section-rabbitmq-oslo | 22 ++ hooks/charmhelpers/core/hookenv.py | 37 ++++ hooks/charmhelpers/core/host.py | 6 +- hooks/charmhelpers/core/services/helpers.py | 4 +- 8 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken create mode 100644 hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo diff --git a/charm-helpers-tests.yaml b/charm-helpers-tests.yaml index 48b12f6f..cad8e7cb 100644 --- a/charm-helpers-tests.yaml +++ b/charm-helpers-tests.yaml @@ -1,4 +1,4 @@ -branch: lp:charm-helpers +branch: lp:~le-charmers/charm-helpers/leadership-election destination: tests/charmhelpers include: - contrib.amulet diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py index 2d9a95cd..45e65790 100644 --- a/hooks/charmhelpers/contrib/openstack/context.py +++ b/hooks/charmhelpers/contrib/openstack/context.py @@ -16,6 +16,7 @@ import json import os +import re import time from base64 import b64decode from subprocess import check_call @@ -46,8 +47,11 @@ from charmhelpers.core.hookenv import ( ) from charmhelpers.core.sysctl import create as sysctl_create +from charmhelpers.core.strutils import bool_from_string from charmhelpers.core.host import ( + list_nics, + get_nic_hwaddr, mkdir, write_file, ) @@ -64,16 +68,22 @@ from charmhelpers.contrib.hahelpers.apache import ( ) from charmhelpers.contrib.openstack.neutron import ( neutron_plugin_attribute, + parse_data_port_mappings, +) +from charmhelpers.contrib.openstack.ip import ( + resolve_address, + INTERNAL, ) from charmhelpers.contrib.network.ip import ( get_address_in_network, + get_ipv4_addr, get_ipv6_addr, get_netmask_for_address, format_ipv6_addr, is_address_in_network, + is_bridge_member, ) from charmhelpers.contrib.openstack.utils import get_host_ip - CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' ADDRESS_TYPES = ['admin', 'internal', 'public'] @@ -727,7 +737,14 @@ class ApacheSSLContext(OSContextGenerator): 'endpoints': [], 'ext_ports': []} - for cn in self.canonical_names(): + cns = self.canonical_names() + if cns: + for cn in cns: + self.configure_cert(cn) + else: + # Expect cert/key provided in config (currently assumed that ca + # uses ip for cn) + cn = resolve_address(endpoint_type=INTERNAL) self.configure_cert(cn) addresses = self.get_network_addresses() @@ -883,6 +900,48 @@ class NeutronContext(OSContextGenerator): return ctxt +class NeutronPortContext(OSContextGenerator): + NIC_PREFIXES = ['eth', 'bond'] + + def resolve_ports(self, ports): + """Resolve NICs not yet bound to bridge(s) + + If hwaddress provided then returns resolved hwaddress otherwise NIC. + """ + if not ports: + return None + + hwaddr_to_nic = {} + hwaddr_to_ip = {} + for nic in list_nics(self.NIC_PREFIXES): + hwaddr = get_nic_hwaddr(nic) + hwaddr_to_nic[hwaddr] = nic + addresses = get_ipv4_addr(nic, fatal=False) + addresses += get_ipv6_addr(iface=nic, fatal=False) + hwaddr_to_ip[hwaddr] = addresses + + resolved = [] + mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I) + for entry in ports: + if re.match(mac_regex, entry): + # NIC is in known NICs and does NOT hace an IP address + if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]: + # If the nic is part of a bridge then don't use it + if is_bridge_member(hwaddr_to_nic[entry]): + continue + + # Entry is a MAC address for a valid interface that doesn't + # have an IP address assigned yet. + resolved.append(hwaddr_to_nic[entry]) + else: + # If the passed entry is not a MAC address, assume it's a valid + # interface, and that the user put it there on purpose (we can + # trust it to be the real external network). + resolved.append(entry) + + return resolved + + class OSConfigFlagContext(OSContextGenerator): """Provides support for user-defined config flags. @@ -1104,3 +1163,145 @@ class SysctlContext(OSContextGenerator): sysctl_create(sysctl_dict, '/etc/sysctl.d/50-{0}.conf'.format(charm_name())) return {'sysctl': sysctl_dict} + + +class NeutronAPIContext(OSContextGenerator): + ''' + Inspects current neutron-plugin-api relation for neutron settings. Return + defaults if it is not present. + ''' + interfaces = ['neutron-plugin-api'] + + def __call__(self): + self.neutron_defaults = { + 'l2_population': { + 'rel_key': 'l2-population', + 'default': False, + }, + 'overlay_network_type': { + 'rel_key': 'overlay-network-type', + 'default': 'gre', + }, + 'neutron_security_groups': { + 'rel_key': 'neutron-security-groups', + 'default': False, + }, + 'network_device_mtu': { + 'rel_key': 'network-device-mtu', + 'default': None, + }, + 'enable_dvr': { + 'rel_key': 'enable-dvr', + 'default': False, + }, + 'enable_l3ha': { + 'rel_key': 'enable-l3ha', + 'default': False, + }, + } + ctxt = self.get_neutron_options({}) + for rid in relation_ids('neutron-plugin-api'): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + if 'l2-population' in rdata: + ctxt.update(self.get_neutron_options(rdata)) + + return ctxt + + def get_neutron_options(self, rdata): + settings = {} + for nkey in self.neutron_defaults.keys(): + defv = self.neutron_defaults[nkey]['default'] + rkey = self.neutron_defaults[nkey]['rel_key'] + if rkey in rdata.keys(): + if type(defv) is bool: + settings[nkey] = bool_from_string(rdata[rkey]) + else: + settings[nkey] = rdata[rkey] + else: + settings[nkey] = defv + return settings + + +class ExternalPortContext(NeutronPortContext): + + def __call__(self): + ctxt = {} + ports = config('ext-port') + if ports: + ports = [p.strip() for p in ports.split()] + ports = self.resolve_ports(ports) + if ports: + ctxt = {"ext_port": ports[0]} + napi_settings = NeutronAPIContext()() + mtu = napi_settings.get('network_device_mtu') + if mtu: + ctxt['ext_port_mtu'] = mtu + + return ctxt + + +class DataPortContext(NeutronPortContext): + + def __call__(self): + ports = config('data-port') + if ports: + portmap = parse_data_port_mappings(ports) + ports = portmap.values() + resolved = self.resolve_ports(ports) + normalized = {get_nic_hwaddr(port): port for port in resolved + if port not in ports} + normalized.update({port: port for port in resolved + if port in ports}) + if resolved: + return {bridge: normalized[port] for bridge, port in + six.iteritems(portmap) if port in normalized.keys()} + + return None + + +class PhyNICMTUContext(DataPortContext): + + def __call__(self): + ctxt = {} + mappings = super(PhyNICMTUContext, self).__call__() + if mappings and mappings.values(): + ports = mappings.values() + napi_settings = NeutronAPIContext()() + mtu = napi_settings.get('network_device_mtu') + if mtu: + ctxt["devs"] = '\\n'.join(ports) + ctxt['mtu'] = mtu + + return ctxt + + +class NetworkServiceContext(OSContextGenerator): + + def __init__(self, rel_name='quantum-network-service'): + self.rel_name = rel_name + self.interfaces = [rel_name] + + def __call__(self): + for rid in relation_ids(self.rel_name): + for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) + ctxt = { + 'keystone_host': rdata.get('keystone_host'), + 'service_port': rdata.get('service_port'), + 'auth_port': rdata.get('auth_port'), + 'service_tenant': rdata.get('service_tenant'), + 'service_username': rdata.get('service_username'), + 'service_password': rdata.get('service_password'), + 'quantum_host': rdata.get('quantum_host'), + 'quantum_port': rdata.get('quantum_port'), + 'quantum_url': rdata.get('quantum_url'), + 'region': rdata.get('region'), + 'service_protocol': + rdata.get('service_protocol') or 'http', + 'auth_protocol': + rdata.get('auth_protocol') or 'http', + } + if context_complete(ctxt): + return ctxt + return {} diff --git a/hooks/charmhelpers/contrib/openstack/neutron.py b/hooks/charmhelpers/contrib/openstack/neutron.py index 902757fe..f8851050 100644 --- a/hooks/charmhelpers/contrib/openstack/neutron.py +++ b/hooks/charmhelpers/contrib/openstack/neutron.py @@ -16,6 +16,7 @@ # Various utilies for dealing with Neutron and the renaming from Quantum. +import six from subprocess import check_output from charmhelpers.core.hookenv import ( @@ -237,3 +238,72 @@ def network_manager(): else: # ensure accurate naming for all releases post-H return 'neutron' + + +def parse_mappings(mappings): + parsed = {} + if mappings: + mappings = mappings.split(' ') + for m in mappings: + p = m.partition(':') + if p[1] == ':': + parsed[p[0].strip()] = p[2].strip() + + return parsed + + +def parse_bridge_mappings(mappings): + """Parse bridge mappings. + + Mappings must be a space-delimited list of provider:bridge mappings. + + Returns dict of the form {provider:bridge}. + """ + return parse_mappings(mappings) + + +def parse_data_port_mappings(mappings, default_bridge='br-data'): + """Parse data port mappings. + + Mappings must be a space-delimited list of bridge:port mappings. + + Returns dict of the form {bridge:port}. + """ + _mappings = parse_mappings(mappings) + if not _mappings: + if not mappings: + return {} + + # For backwards-compatibility we need to support port-only provided in + # config. + _mappings = {default_bridge: mappings.split(' ')[0]} + + bridges = _mappings.keys() + ports = _mappings.values() + if len(set(bridges)) != len(bridges): + raise Exception("It is not allowed to have more than one port " + "configured on the same bridge") + + if len(set(ports)) != len(ports): + raise Exception("It is not allowed to have the same port configured " + "on more than one bridge") + + return _mappings + + +def parse_vlan_range_mappings(mappings): + """Parse vlan range mappings. + + Mappings must be a space-delimited list of provider:start:end mappings. + + Returns dict of the form {provider: (start, end)}. + """ + _mappings = parse_mappings(mappings) + if not _mappings: + return {} + + mappings = {} + for p, r in six.iteritems(_mappings): + mappings[p] = tuple(r.split(':')) + + return mappings diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken b/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken new file mode 100644 index 00000000..2a37edd5 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken @@ -0,0 +1,9 @@ +{% if auth_host -%} +[keystone_authtoken] +identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }} +auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }} +admin_tenant_name = {{ admin_tenant_name }} +admin_user = {{ admin_user }} +admin_password = {{ admin_password }} +signing_dir = {{ signing_dir }} +{% endif -%} diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo b/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo new file mode 100644 index 00000000..b444c9c9 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo @@ -0,0 +1,22 @@ +{% if rabbitmq_host or rabbitmq_hosts -%} +[oslo_messaging_rabbit] +rabbit_userid = {{ rabbitmq_user }} +rabbit_virtual_host = {{ rabbitmq_virtual_host }} +rabbit_password = {{ rabbitmq_password }} +{% if rabbitmq_hosts -%} +rabbit_hosts = {{ rabbitmq_hosts }} +{% if rabbitmq_ha_queues -%} +rabbit_ha_queues = True +rabbit_durable_queues = False +{% endif -%} +{% else -%} +rabbit_host = {{ rabbitmq_host }} +{% endif -%} +{% if rabbit_ssl_port -%} +rabbit_use_ssl = True +rabbit_port = {{ rabbit_ssl_port }} +{% if rabbit_ssl_ca -%} +kombu_ssl_ca_certs = {{ rabbit_ssl_ca }} +{% endif -%} +{% endif -%} +{% endif -%} diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index e68d4b57..60820cc4 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -352,6 +352,17 @@ def relation_set(relation_id=None, relation_settings=None, **kwargs): flush(local_unit()) +def relation_clear(r_id=None): + ''' Clears any relation data already set on relation r_id ''' + settings = relation_get(rid=r_id, + unit=local_unit()) + for setting in settings: + if setting not in ['public-address', 'private-address']: + settings[setting] = None + relation_set(relation_id=r_id, + **settings) + + @cached def relation_ids(reltype=None): """A list of relation_ids""" @@ -627,3 +638,29 @@ def leader_set(settings=None, **kwargs): cmd.append('{}={}'.format(k, v)) subprocess.check_call(cmd) + + +@cached +def action_get(key=None): + """Gets the value of an action parameter, or all key/value param pairs""" + cmd = ['action-get'] + if key is not None: + cmd.append(key) + cmd.append('--format=json') + action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) + return action_data + + +def action_set(values): + """Sets the values to be returned after the action finishes""" + cmd = ['action-set'] + for k, v in list(values.items()): + cmd.append('{}={}'.format(k, v)) + subprocess.check_call(cmd) + + +def action_fail(message): + """Sets the action status to failed and sets the error message. + + The results set by action_set are preserved.""" + subprocess.check_call(['action-fail', message]) diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py index b771c611..830822af 100644 --- a/hooks/charmhelpers/core/host.py +++ b/hooks/charmhelpers/core/host.py @@ -339,12 +339,16 @@ def lsb_release(): def pwgen(length=None): """Generate a random pasword.""" if length is None: + # A random length is ok to use a weak PRNG length = random.choice(range(35, 45)) alphanumeric_chars = [ l for l in (string.ascii_letters + string.digits) if l not in 'l0QD1vAEIOUaeiou'] + # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the + # actual password + random_generator = random.SystemRandom() random_chars = [ - random.choice(alphanumeric_chars) for _ in range(length)] + random_generator.choice(alphanumeric_chars) for _ in range(length)] return(''.join(random_chars)) diff --git a/hooks/charmhelpers/core/services/helpers.py b/hooks/charmhelpers/core/services/helpers.py index 15b21664..3eb5fb44 100644 --- a/hooks/charmhelpers/core/services/helpers.py +++ b/hooks/charmhelpers/core/services/helpers.py @@ -139,7 +139,7 @@ class MysqlRelation(RelationContext): def __init__(self, *args, **kwargs): self.required_keys = ['host', 'user', 'password', 'database'] - super(HttpRelation).__init__(self, *args, **kwargs) + RelationContext.__init__(self, *args, **kwargs) class HttpRelation(RelationContext): @@ -154,7 +154,7 @@ class HttpRelation(RelationContext): def __init__(self, *args, **kwargs): self.required_keys = ['host', 'port'] - super(HttpRelation).__init__(self, *args, **kwargs) + RelationContext.__init__(self, *args, **kwargs) def provide_data(self): return { From 1beb9949ff8b66cc7e3f8dd4383439456685a252 Mon Sep 17 00:00:00 2001 From: Edward Hope-Morley Date: Wed, 1 Apr 2015 14:13:58 +0100 Subject: [PATCH 10/12] set sync location --- charm-helpers-hooks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charm-helpers-hooks.yaml b/charm-helpers-hooks.yaml index 5c6a8303..b583ea45 100644 --- a/charm-helpers-hooks.yaml +++ b/charm-helpers-hooks.yaml @@ -1,4 +1,4 @@ -branch: lp:charm-helpers +branch: lp:~le-charmers/charm-helpers/leadership-election destination: hooks/charmhelpers include: - core From 5f6c24083254e2f03400e8c6d4b25005ab401fbf Mon Sep 17 00:00:00 2001 From: Edward Hope-Morley Date: Wed, 1 Apr 2015 14:50:28 +0100 Subject: [PATCH 11/12] synced l/e ch --- .../charmhelpers/contrib/hahelpers/cluster.py | 14 +- .../contrib/openstack/amulet/deployment.py | 27 ++- .../charmhelpers/contrib/openstack/context.py | 9 +- .../contrib/openstack/templates/git.upstart | 13 ++ .../templates/{zeromq => section-zeromq} | 4 +- hooks/charmhelpers/contrib/openstack/utils.py | 179 ++++++++++++------ hooks/charmhelpers/core/hookenv.py | 72 +++++++ hooks/charmhelpers/core/unitdata.py | 2 +- .../contrib/openstack/amulet/deployment.py | 27 ++- 9 files changed, 272 insertions(+), 75 deletions(-) create mode 100644 hooks/charmhelpers/contrib/openstack/templates/git.upstart rename hooks/charmhelpers/contrib/openstack/templates/{zeromq => section-zeromq} (66%) diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py index 9333efc3..ebec3790 100644 --- a/hooks/charmhelpers/contrib/hahelpers/cluster.py +++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py @@ -44,6 +44,7 @@ from charmhelpers.core.hookenv import ( ERROR, WARNING, unit_get, + is_leader as juju_is_leader ) from charmhelpers.core.decorators import ( retry_on_exception, @@ -66,12 +67,21 @@ def is_elected_leader(resource): Returns True if the charm executing this is the elected cluster leader. It relies on two mechanisms to determine leadership: - 1. If the charm is part of a corosync cluster, call corosync to + 1. If juju is sufficiently new and leadership election is supported, + the is_leader command will be used. + 2. If the charm is part of a corosync cluster, call corosync to determine leadership. - 2. If the charm is not part of a corosync cluster, the leader is + 3. If the charm is not part of a corosync cluster, the leader is determined as being "the alive unit with the lowest unit numer". In other words, the oldest surviving unit. """ + try: + return juju_is_leader() + except NotImplementedError: + log('Juju leadership election feature not enabled' + ', using fallback support', + level=WARNING) + if is_clustered(): if not is_crm_leader(resource): log('Deferring action to CRM leader.', level=INFO) diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py index 0cfeaa4c..0e0db566 100644 --- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py @@ -15,6 +15,7 @@ # along with charm-helpers. If not, see . import six +from collections import OrderedDict from charmhelpers.contrib.amulet.deployment import ( AmuletDeployment ) @@ -100,12 +101,34 @@ class OpenStackAmuletDeployment(AmuletDeployment): """ (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, - self.trusty_icehouse) = range(6) + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8) 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} + ('trusty', None): self.trusty_icehouse, + ('trusty', 'cloud:trusty-juno'): self.trusty_juno, + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo} return releases[(self.series, self.openstack)] + + def _get_openstack_release_string(self): + """Get openstack release string. + + Return a string representing the openstack release. + """ + releases = OrderedDict([ + ('precise', 'essex'), + ('quantal', 'folsom'), + ('raring', 'grizzly'), + ('saucy', 'havana'), + ('trusty', 'icehouse'), + ('utopic', 'juno'), + ('vivid', 'kilo'), + ]) + if self.openstack: + os_origin = self.openstack.split(':')[1] + return os_origin.split('%s-' % self.series)[1].split('/')[0] + else: + return releases[self.series] diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py index 45e65790..dd51bfbb 100644 --- a/hooks/charmhelpers/contrib/openstack/context.py +++ b/hooks/charmhelpers/contrib/openstack/context.py @@ -320,14 +320,15 @@ def db_ssl(rdata, ctxt, ssl_dir): class IdentityServiceContext(OSContextGenerator): - interfaces = ['identity-service'] - def __init__(self, service=None, service_user=None): + def __init__(self, service=None, service_user=None, rel_name='identity-service'): self.service = service self.service_user = service_user + self.rel_name = rel_name + self.interfaces = [self.rel_name] def __call__(self): - log('Generating template context for identity-service', level=DEBUG) + log('Generating template context for ' + self.rel_name, level=DEBUG) ctxt = {} if self.service and self.service_user: @@ -341,7 +342,7 @@ class IdentityServiceContext(OSContextGenerator): ctxt['signing_dir'] = cachedir - for rid in relation_ids('identity-service'): + for rid in relation_ids(self.rel_name): for unit in related_units(rid): rdata = relation_get(rid=rid, unit=unit) serv_host = rdata.get('service_host') diff --git a/hooks/charmhelpers/contrib/openstack/templates/git.upstart b/hooks/charmhelpers/contrib/openstack/templates/git.upstart new file mode 100644 index 00000000..da94ad12 --- /dev/null +++ b/hooks/charmhelpers/contrib/openstack/templates/git.upstart @@ -0,0 +1,13 @@ +description "{{ service_description }}" +author "Juju {{ service_name }} Charm " + +start on runlevel [2345] +stop on runlevel [!2345] + +respawn + +exec start-stop-daemon --start --chuid {{ user_name }} \ + --chdir {{ start_dir }} --name {{ process_name }} \ + --exec {{ executable_name }} -- \ + --config-file={{ config_file }} \ + --log-file={{ log_file }} diff --git a/hooks/charmhelpers/contrib/openstack/templates/zeromq b/hooks/charmhelpers/contrib/openstack/templates/section-zeromq similarity index 66% rename from hooks/charmhelpers/contrib/openstack/templates/zeromq rename to hooks/charmhelpers/contrib/openstack/templates/section-zeromq index 0695eef1..95f1a76c 100644 --- a/hooks/charmhelpers/contrib/openstack/templates/zeromq +++ b/hooks/charmhelpers/contrib/openstack/templates/section-zeromq @@ -3,12 +3,12 @@ rpc_backend = zmq rpc_zmq_host = {{ zmq_host }} {% if zmq_redis_address -%} -rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_redis.MatchMakerRedis +rpc_zmq_matchmaker = redis matchmaker_heartbeat_freq = 15 matchmaker_heartbeat_ttl = 30 [matchmaker_redis] host = {{ zmq_redis_address }} {% else -%} -rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_ring.MatchMakerRing +rpc_zmq_matchmaker = ring {% endif -%} {% endif -%} diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py index 4f110c63..78c5e2df 100644 --- a/hooks/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -30,6 +30,10 @@ import yaml from charmhelpers.contrib.network import ip +from charmhelpers.core import ( + unitdata, +) + from charmhelpers.core.hookenv import ( config, log as juju_log, @@ -330,6 +334,21 @@ def configure_installation_source(rel): error_out("Invalid openstack-release specified: %s" % rel) +def config_value_changed(option): + """ + Determine if config value changed since last call to this function. + """ + hook_data = unitdata.HookData() + with hook_data(): + db = unitdata.kv() + current = config(option) + saved = db.get(option) + db.set(option, current) + if saved is None: + return False + return current != saved + + def save_script_rc(script_path="scripts/scriptrc", **env_vars): """ Write an rc file in the charm-delivered directory containing @@ -469,82 +488,95 @@ def os_requires_version(ostack_release, pkg): def git_install_requested(): - """Returns true if openstack-origin-git is specified.""" - return config('openstack-origin-git') != "None" + """ + Returns true if openstack-origin-git is specified. + """ + return config('openstack-origin-git') is not None requirements_dir = None -def git_clone_and_install(file_name, core_project): - """Clone/install all OpenStack repos specified in yaml config file.""" - global requirements_dir +def git_clone_and_install(projects_yaml, core_project): + """ + Clone/install all specified OpenStack repositories. - if file_name == "None": + The expected format of projects_yaml is: + repositories: + - {name: keystone, + repository: 'git://git.openstack.org/openstack/keystone.git', + branch: 'stable/icehouse'} + - {name: requirements, + repository: 'git://git.openstack.org/openstack/requirements.git', + branch: 'stable/icehouse'} + directory: /mnt/openstack-git + + The directory key is optional. + """ + global requirements_dir + parent_dir = '/mnt/openstack-git' + + if not projects_yaml: return - yaml_file = os.path.join(charm_dir(), file_name) + projects = yaml.load(projects_yaml) + _git_validate_projects_yaml(projects, core_project) - # clone/install the requirements project first - installed = _git_clone_and_install_subset(yaml_file, - whitelist=['requirements']) - if 'requirements' not in installed: - error_out('requirements git repository must be specified') + if 'directory' in projects.keys(): + parent_dir = projects['directory'] - # clone/install all other projects except requirements and the core project - blacklist = ['requirements', core_project] - _git_clone_and_install_subset(yaml_file, blacklist=blacklist, - update_requirements=True) - - # clone/install the core project - whitelist = [core_project] - installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist, - update_requirements=True) - if core_project not in installed: - error_out('{} git repository must be specified'.format(core_project)) + for p in projects['repositories']: + repo = p['repository'] + branch = p['branch'] + if p['name'] == 'requirements': + repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, + update_requirements=False) + requirements_dir = repo_dir + else: + repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, + update_requirements=True) -def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[], - update_requirements=False): - """Clone/install subset of OpenStack repos specified in yaml config file.""" - global requirements_dir - installed = [] +def _git_validate_projects_yaml(projects, core_project): + """ + Validate the projects yaml. + """ + _git_ensure_key_exists('repositories', projects) - with open(yaml_file, 'r') as fd: - projects = yaml.load(fd) - for proj, val in projects.items(): - # The project subset is chosen based on the following 3 rules: - # 1) If project is in blacklist, we don't clone/install it, period. - # 2) If whitelist is empty, we clone/install everything else. - # 3) If whitelist is not empty, we clone/install everything in the - # whitelist. - if proj in blacklist: - continue - if whitelist and proj not in whitelist: - continue - repo = val['repository'] - branch = val['branch'] - repo_dir = _git_clone_and_install_single(repo, branch, - update_requirements) - if proj == 'requirements': - requirements_dir = repo_dir - installed.append(proj) - return installed + for project in projects['repositories']: + _git_ensure_key_exists('name', project.keys()) + _git_ensure_key_exists('repository', project.keys()) + _git_ensure_key_exists('branch', project.keys()) + + if projects['repositories'][0]['name'] != 'requirements': + error_out('{} git repo must be specified first'.format('requirements')) + + if projects['repositories'][-1]['name'] != core_project: + error_out('{} git repo must be specified last'.format(core_project)) -def _git_clone_and_install_single(repo, branch, update_requirements=False): - """Clone and install a single git repository.""" - dest_parent_dir = "/mnt/openstack-git/" - dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo)) +def _git_ensure_key_exists(key, keys): + """ + Ensure that key exists in keys. + """ + if key not in keys: + error_out('openstack-origin-git key \'{}\' is missing'.format(key)) - if not os.path.exists(dest_parent_dir): - juju_log('Host dir not mounted at {}. ' - 'Creating directory there instead.'.format(dest_parent_dir)) - os.mkdir(dest_parent_dir) + +def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements): + """ + Clone and install a single git repository. + """ + dest_dir = os.path.join(parent_dir, os.path.basename(repo)) + + if not os.path.exists(parent_dir): + juju_log('Directory already exists at {}. ' + 'No need to create directory.'.format(parent_dir)) + os.mkdir(parent_dir) if not os.path.exists(dest_dir): juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch)) - repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch) + repo_dir = install_remote(repo, dest=parent_dir, branch=branch) else: repo_dir = dest_dir @@ -561,16 +593,39 @@ def _git_clone_and_install_single(repo, branch, update_requirements=False): def _git_update_requirements(package_dir, reqs_dir): - """Update from global requirements. + """ + Update from global requirements. - Update an OpenStack git directory's requirements.txt and - test-requirements.txt from global-requirements.txt.""" + Update an OpenStack git directory's requirements.txt and + test-requirements.txt from global-requirements.txt. + """ orig_dir = os.getcwd() os.chdir(reqs_dir) - cmd = "python update.py {}".format(package_dir) + cmd = ['python', 'update.py', package_dir] try: - subprocess.check_call(cmd.split(' ')) + subprocess.check_call(cmd) except subprocess.CalledProcessError: package = os.path.basename(package_dir) error_out("Error updating {} from global-requirements.txt".format(package)) os.chdir(orig_dir) + + +def git_src_dir(projects_yaml, project): + """ + Return the directory where the specified project's source is located. + """ + parent_dir = '/mnt/openstack-git' + + if not projects_yaml: + return + + projects = yaml.load(projects_yaml) + + if 'directory' in projects.keys(): + parent_dir = projects['directory'] + + for p in projects['repositories']: + if p['name'] == project: + return os.path.join(parent_dir, os.path.basename(p['repository'])) + + return None diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py index 715dd4c5..60820cc4 100644 --- a/hooks/charmhelpers/core/hookenv.py +++ b/hooks/charmhelpers/core/hookenv.py @@ -352,6 +352,17 @@ def relation_set(relation_id=None, relation_settings=None, **kwargs): flush(local_unit()) +def relation_clear(r_id=None): + ''' Clears any relation data already set on relation r_id ''' + settings = relation_get(rid=r_id, + unit=local_unit()) + for setting in settings: + if setting not in ['public-address', 'private-address']: + settings[setting] = None + relation_set(relation_id=r_id, + **settings) + + @cached def relation_ids(reltype=None): """A list of relation_ids""" @@ -568,6 +579,67 @@ def charm_dir(): return os.environ.get('CHARM_DIR') +def translate_exc(from_exc, to_exc): + def inner_translate_exc1(f): + def inner_translate_exc2(*args, **kwargs): + try: + return f(*args, **kwargs) + except from_exc: + raise to_exc + + return inner_translate_exc2 + + return inner_translate_exc1 + + +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) +def is_leader(): + """Does the current unit hold the juju leadership + + Uses juju to determine whether the current unit is the leader of its peers + """ + try: + leader = json.loads( + subprocess.check_output(['is-leader', '--format=json']) + ) + return (leader is True) + except ValueError: + raise NotImplementedError + + +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) +def leader_get(attribute=None): + """Juju leader get value(s)""" + cmd = ['leader-get', '--format=json'] + [attribute or '-'] + try: + ret = json.loads(subprocess.check_output(cmd).decode('UTF-8')) + log("Juju leader-get '%s' = '%s'" % (attribute, ret), level=DEBUG) + return ret + except ValueError: + return None + except CalledProcessError as e: + if e.returncode == 2: + return None + + raise + + +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) +def leader_set(settings=None, **kwargs): + """Juju leader set value(s)""" + log("Juju leader-set '%s'" % (settings), level=DEBUG) + cmd = ['leader-set'] + settings = settings or {} + settings.update(kwargs) + for k, v in settings.iteritems(): + if v is None: + cmd.append('{}='.format(k)) + else: + cmd.append('{}={}'.format(k, v)) + + subprocess.check_call(cmd) + + @cached def action_get(key=None): """Gets the value of an action parameter, or all key/value param pairs""" diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py index 3000134a..406a35c5 100644 --- a/hooks/charmhelpers/core/unitdata.py +++ b/hooks/charmhelpers/core/unitdata.py @@ -443,7 +443,7 @@ class HookData(object): 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('env', dict(data['env'])) self.kv.set('unit', data['unit']) self.kv.set('relid', data.get('relid')) return conf_delta, rels_delta diff --git a/tests/charmhelpers/contrib/openstack/amulet/deployment.py b/tests/charmhelpers/contrib/openstack/amulet/deployment.py index 0cfeaa4c..0e0db566 100644 --- a/tests/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/tests/charmhelpers/contrib/openstack/amulet/deployment.py @@ -15,6 +15,7 @@ # along with charm-helpers. If not, see . import six +from collections import OrderedDict from charmhelpers.contrib.amulet.deployment import ( AmuletDeployment ) @@ -100,12 +101,34 @@ class OpenStackAmuletDeployment(AmuletDeployment): """ (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, - self.trusty_icehouse) = range(6) + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8) 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} + ('trusty', None): self.trusty_icehouse, + ('trusty', 'cloud:trusty-juno'): self.trusty_juno, + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo} return releases[(self.series, self.openstack)] + + def _get_openstack_release_string(self): + """Get openstack release string. + + Return a string representing the openstack release. + """ + releases = OrderedDict([ + ('precise', 'essex'), + ('quantal', 'folsom'), + ('raring', 'grizzly'), + ('saucy', 'havana'), + ('trusty', 'icehouse'), + ('utopic', 'juno'), + ('vivid', 'kilo'), + ]) + if self.openstack: + os_origin = self.openstack.split(':')[1] + return os_origin.split('%s-' % self.series)[1].split('/')[0] + else: + return releases[self.series] From 8175f68f5f5dc5e523a124df8cd654a5a335949e Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 11 May 2015 09:19:49 +0100 Subject: [PATCH 12/12] Fix unit_tests --- unit_tests/test_cinder_hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/unit_tests/test_cinder_hooks.py b/unit_tests/test_cinder_hooks.py index 3486e842..53ca6d03 100644 --- a/unit_tests/test_cinder_hooks.py +++ b/unit_tests/test_cinder_hooks.py @@ -62,6 +62,7 @@ TO_PATCH = [ 'apt_install', 'apt_update', 'service_reload', + 'service_restart', # charmhelpers.contrib.openstack.openstack_utils 'configure_installation_source', 'openstack_upgrade_available', @@ -249,7 +250,7 @@ class TestChangedHooks(CharmTestCase): self.relation_get.return_value = None self.local_unit.return_value = 'cinder/0' self.CONFIGS.complete_contexts.return_value = ['shared-db'] - self.eligible_leader.return_value = True + self.is_elected_leader.return_value = True hooks.hooks.execute(['hooks/shared-db-relation-changed']) self.assertFalse(self.migrate_database.called) @@ -518,6 +519,7 @@ class TestJoinedHooks(CharmTestCase): call('/etc/cinder/cinder.conf')]: self.assertIn(c, self.CONFIGS.write.call_args_list) self.set_ceph_env_variables.assert_called_with(service='cinder') + self.service_restart.assert_called_with('cinder-volume') @patch("cinder_hooks.relation_get", autospec=True) def test_ceph_changed_broker_nonzero_rc(self, mock_relation_get):