diff --git a/Makefile b/Makefile index 28fad993..5026b768 100644 --- a/Makefile +++ b/Makefile @@ -3,13 +3,8 @@ PYTHON := /usr/bin/env python lint: @flake8 --exclude hooks/charmhelpers hooks -# @flake8 --exclude hooks/charmhelpers unit_tests @charm proof -# Disabling since we do not yet have unit tests -#test: -# @echo Starting tests... -# @$(PYTHON) /usr/bin/nosetests --nologcapture unit_tests - sync: @charm-helper-sync -c charm-helpers.yaml + diff --git a/charm-helpers.yaml b/charm-helpers.yaml index 82291cdd..0b4c61d9 100644 --- a/charm-helpers.yaml +++ b/charm-helpers.yaml @@ -1,7 +1,11 @@ -destination: lib/charmhelpers +destination: hooks/charmhelpers branch: lp:charm-helpers include: + - fetch - core - contrib.charmsupport - contrib.openstack - - fetch \ No newline at end of file + - contrib.storage + - contrib.peerstorage + - contrib.ssl + - contrib.hahelpers.cluster diff --git a/config.yaml b/config.yaml index 24eca573..468fd552 100644 --- a/config.yaml +++ b/config.yaml @@ -1,24 +1,42 @@ options: - ssl_enabled: - type: boolean - default: False - description: enable SSL management_plugin: type: boolean default: False description: enable the management plugin + + # SSL Configuration options + ssl: + type: string + default: "off" + description: | + Enable SSL connections on rabbitmq, valid values are 'off', 'on', 'only'. If ssl_key, + ssl_cert, ssl_ca are provided then then those values will be used. Otherwise + the service will act as its own certificate authority and pass its ca cert to clients. + For HA or clustered rabbits ssl key/cert must be provided. + ssl_enabled: + type: boolean + default: False + description: | + (DEPRECATED see 'ssl' config option.) enable SSL ssl_port: type: int default: 5671 description: SSL port ssl_key: type: string - description: private unencrypted key in PEM format (starts "-----BEGIN RSA PRIVATE KEY-----") + description: private unencrypted key in base64 PEM format (starts "-----BEGIN RSA PRIVATE KEY-----") default: "" ssl_cert: type: string - description: X.509 certificate in PEM format (starts "-----BEGIN CERTIFICATE-----") + description: X.509 certificate in base64 PEM format (starts "-----BEGIN CERTIFICATE-----") default: "" + ssl_ca: + type: string + description: | + Certificate authority cert that the cert. Optional if the ssl_cert is signed by a ca + recognized by the os. Format is base64 PEM (concatenated certs if needed). + default: "" + nagios_context: default: "juju" type: string @@ -54,6 +72,16 @@ options: description: | Default multicast port number that will be used to communicate between HA Cluster nodes. + ha-vip-only: + type: boolean + default: False + description: | + By default, without pairing with hacluster charm, rabbitmq will deploy + in active/active/active... HA. When pairied with hacluster charm, it + will deploy as active/passive. By enabling this option, pairing with + hacluster charm will keep rabbit in active/active setup, but in addition + it will deploy a VIP that can be used by services that cannot work + with mutiple AMQPs (like Glance in pre-Icehouse). rbd-size: type: string default: 5G @@ -83,20 +111,25 @@ options: description: | If True, services that support it will log to syslog instead of their normal log location. - key: - type: string + max-cluster-tries: + type: int + default: 3 description: | - Key ID to import to the apt keyring to support use with arbitary source - configuration from outside of Launchpad archives or PPA's. + Number of tries to cluster with other units before giving up and throwing + a hook error. source: type: string description: | Optional configuration to support use of additional sources such as: . - - ppa:myteam/ppa - - cloud:precise-proposed/folsom - - http://my.archive.com/ubuntu main + - ppa:myteam/ppa + - cloud:precise-proposed/folsom + - http://my.archive.com/ubuntu main . The last option should be used in conjunction with the key configuration option. - + key: + type: string + description: | + Key ID to import to the apt keyring to support use with arbitary source + configuration from outside of Launchpad archives or PPA's. diff --git a/hooks/_pythonpath.py b/hooks/_pythonpath.py deleted file mode 100644 index 5837d9cc..00000000 --- a/hooks/_pythonpath.py +++ /dev/null @@ -1,14 +0,0 @@ -import sys -import os -import os.path - -# Make sure that charmhelpers is importable, or bail out. -local_copy = os.path.join( - os.path.dirname(os.path.dirname(__file__)), "lib") -if os.path.exists(local_copy) and os.path.isdir(local_copy): - sys.path.insert(0, local_copy) -try: - import charmhelpers - _ = charmhelpers -except ImportError: - sys.exit("Could not find required 'charmhelpers' library.") diff --git a/lib/charmhelpers/__init__.py b/hooks/charmhelpers/__init__.py similarity index 100% rename from lib/charmhelpers/__init__.py rename to hooks/charmhelpers/__init__.py diff --git a/lib/charmhelpers/contrib/__init__.py b/hooks/charmhelpers/contrib/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/__init__.py rename to hooks/charmhelpers/contrib/__init__.py diff --git a/lib/charmhelpers/contrib/charmsupport/__init__.py b/hooks/charmhelpers/contrib/charmsupport/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/charmsupport/__init__.py rename to hooks/charmhelpers/contrib/charmsupport/__init__.py diff --git a/lib/charmhelpers/contrib/charmsupport/nrpe.py b/hooks/charmhelpers/contrib/charmsupport/nrpe.py similarity index 100% rename from lib/charmhelpers/contrib/charmsupport/nrpe.py rename to hooks/charmhelpers/contrib/charmsupport/nrpe.py diff --git a/lib/charmhelpers/contrib/charmsupport/volumes.py b/hooks/charmhelpers/contrib/charmsupport/volumes.py similarity index 100% rename from lib/charmhelpers/contrib/charmsupport/volumes.py rename to hooks/charmhelpers/contrib/charmsupport/volumes.py diff --git a/lib/charmhelpers/contrib/hahelpers/__init__.py b/hooks/charmhelpers/contrib/hahelpers/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/hahelpers/__init__.py rename to hooks/charmhelpers/contrib/hahelpers/__init__.py diff --git a/lib/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py similarity index 96% rename from lib/charmhelpers/contrib/hahelpers/cluster.py rename to hooks/charmhelpers/contrib/hahelpers/cluster.py index 074855f4..bf832f7d 100644 --- a/lib/charmhelpers/contrib/hahelpers/cluster.py +++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py @@ -126,17 +126,17 @@ def determine_api_port(public_port): return public_port - (i * 10) -def determine_haproxy_port(public_port): +def determine_apache_port(public_port): ''' - Description: Determine correct proxy listening port based on public IP + - existence of HTTPS reverse proxy. + Description: Determine correct apache listening port based on public IP + + state of the cluster. public_port: int: standard public port for given service returns: int: the correct listening port for the HAProxy service ''' i = 0 - if https(): + if len(peer_units()) > 0 or is_clustered(): i += 1 return public_port - (i * 10) diff --git a/lib/charmhelpers/contrib/jujugui/__init__.py b/hooks/charmhelpers/contrib/openstack/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/jujugui/__init__.py rename to hooks/charmhelpers/contrib/openstack/__init__.py diff --git a/lib/charmhelpers/contrib/openstack/alternatives.py b/hooks/charmhelpers/contrib/openstack/alternatives.py similarity index 100% rename from lib/charmhelpers/contrib/openstack/alternatives.py rename to hooks/charmhelpers/contrib/openstack/alternatives.py diff --git a/lib/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py similarity index 79% rename from lib/charmhelpers/contrib/openstack/context.py rename to hooks/charmhelpers/contrib/openstack/context.py index a2d4636b..e92b1cc8 100644 --- a/lib/charmhelpers/contrib/openstack/context.py +++ b/hooks/charmhelpers/contrib/openstack/context.py @@ -1,5 +1,6 @@ import json import os +import time from base64 import b64decode @@ -113,7 +114,8 @@ class OSContextGenerator(object): class SharedDBContext(OSContextGenerator): interfaces = ['shared-db'] - def __init__(self, database=None, user=None, relation_prefix=None): + def __init__(self, + database=None, user=None, relation_prefix=None, ssl_dir=None): ''' Allows inspecting relation for settings prefixed with relation_prefix. This is useful for parsing access for multiple databases returned via @@ -122,6 +124,7 @@ class SharedDBContext(OSContextGenerator): self.relation_prefix = relation_prefix self.database = database self.user = user + self.ssl_dir = ssl_dir def __call__(self): self.database = self.database or config('database') @@ -139,19 +142,74 @@ class SharedDBContext(OSContextGenerator): for rid in relation_ids('shared-db'): for unit in related_units(rid): - passwd = relation_get(password_setting, rid=rid, unit=unit) + rdata = relation_get(rid=rid, unit=unit) ctxt = { - 'database_host': relation_get('db_host', rid=rid, - unit=unit), + 'database_host': rdata.get('db_host'), 'database': self.database, 'database_user': self.user, - 'database_password': passwd, + 'database_password': rdata.get(password_setting), + 'database_type': 'mysql' + } + if context_complete(ctxt): + db_ssl(rdata, ctxt, self.ssl_dir) + return ctxt + return {} + + +class PostgresqlDBContext(OSContextGenerator): + interfaces = ['pgsql-db'] + + def __init__(self, database=None): + self.database = database + + def __call__(self): + self.database = self.database or config('database') + if self.database is None: + log('Could not generate postgresql_db context. ' + 'Missing required charm config options. ' + '(database name)') + raise OSContextError + ctxt = {} + + for rid in relation_ids(self.interfaces[0]): + for unit in related_units(rid): + ctxt = { + 'database_host': relation_get('host', rid=rid, unit=unit), + 'database': self.database, + 'database_user': relation_get('user', rid=rid, unit=unit), + 'database_password': relation_get('password', rid=rid, unit=unit), + 'database_type': 'postgresql', } if context_complete(ctxt): return ctxt return {} +def db_ssl(rdata, ctxt, ssl_dir): + if 'ssl_ca' in rdata and ssl_dir: + ca_path = os.path.join(ssl_dir, 'db-client.ca') + with open(ca_path, 'w') as fh: + fh.write(b64decode(rdata['ssl_ca'])) + ctxt['database_ssl_ca'] = ca_path + elif 'ssl_ca' in rdata: + log("Charm not setup for ssl support but ssl ca found") + return ctxt + if 'ssl_cert' in rdata: + cert_path = os.path.join( + ssl_dir, 'db-client.cert') + if not os.path.exists(cert_path): + log("Waiting 1m for ssl client cert validity") + time.sleep(60) + with open(cert_path, 'w') as fh: + fh.write(b64decode(rdata['ssl_cert'])) + ctxt['database_ssl_cert'] = cert_path + key_path = os.path.join(ssl_dir, 'db-client.key') + with open(key_path, 'w') as fh: + fh.write(b64decode(rdata['ssl_key'])) + ctxt['database_ssl_key'] = key_path + return ctxt + + class IdentityServiceContext(OSContextGenerator): interfaces = ['identity-service'] @@ -161,24 +219,25 @@ class IdentityServiceContext(OSContextGenerator): for rid in relation_ids('identity-service'): for unit in related_units(rid): + rdata = relation_get(rid=rid, unit=unit) ctxt = { - 'service_port': relation_get('service_port', rid=rid, - unit=unit), - 'service_host': relation_get('service_host', rid=rid, - unit=unit), - 'auth_host': relation_get('auth_host', rid=rid, unit=unit), - 'auth_port': relation_get('auth_port', rid=rid, unit=unit), - 'admin_tenant_name': relation_get('service_tenant', - rid=rid, unit=unit), - 'admin_user': relation_get('service_username', rid=rid, - unit=unit), - 'admin_password': relation_get('service_password', rid=rid, - unit=unit), - # XXX: Hard-coded http. - 'service_protocol': 'http', - 'auth_protocol': 'http', + 'service_port': rdata.get('service_port'), + 'service_host': rdata.get('service_host'), + 'auth_host': rdata.get('auth_host'), + 'auth_port': rdata.get('auth_port'), + 'admin_tenant_name': rdata.get('service_tenant'), + 'admin_user': rdata.get('service_username'), + 'admin_password': rdata.get('service_password'), + 'service_protocol': + rdata.get('service_protocol') or 'http', + 'auth_protocol': + rdata.get('auth_protocol') or 'http', } if context_complete(ctxt): + # NOTE(jamespage) this is required for >= icehouse + # so a missing value just indicates keystone needs + # upgrading + ctxt['admin_tenant_id'] = rdata.get('service_tenant_id') return ctxt return {} @@ -186,6 +245,9 @@ class IdentityServiceContext(OSContextGenerator): class AMQPContext(OSContextGenerator): interfaces = ['amqp'] + def __init__(self, ssl_dir=None): + self.ssl_dir = ssl_dir + def __call__(self): log('Generating template context for amqp') conf = config() @@ -196,9 +258,9 @@ class AMQPContext(OSContextGenerator): log('Could not generate shared_db context. ' 'Missing required charm config options: %s.' % e) raise OSContextError - ctxt = {} for rid in relation_ids('amqp'): + ha_vip_only = False for unit in related_units(rid): if relation_get('clustered', rid=rid, unit=unit): ctxt['clustered'] = True @@ -213,16 +275,36 @@ class AMQPContext(OSContextGenerator): unit=unit), 'rabbitmq_virtual_host': vhost, }) + + ssl_port = relation_get('ssl_port', rid=rid, unit=unit) + if ssl_port: + ctxt['rabbit_ssl_port'] = ssl_port + ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit) + if ssl_ca: + ctxt['rabbit_ssl_ca'] = ssl_ca + + if relation_get('ha_queues', rid=rid, unit=unit) is not None: + ctxt['rabbitmq_ha_queues'] = True + + ha_vip_only = relation_get('ha-vip-only', + rid=rid, unit=unit) is not None + if context_complete(ctxt): + if 'rabbit_ssl_ca' in ctxt: + if not self.ssl_dir: + log(("Charm not setup for ssl support " + "but ssl ca found")) + break + ca_path = os.path.join( + self.ssl_dir, 'rabbit-client-ca.pem') + with open(ca_path, 'w') as fh: + fh.write(b64decode(ctxt['rabbit_ssl_ca'])) + ctxt['rabbit_ssl_ca'] = ca_path # Sufficient information found = break out! break # Used for active/active rabbitmq >= grizzly - if ('clustered' not in ctxt or relation_get('ha-vip-only') == 'True') and \ - len(related_units(rid)) > 1: - if relation_get('ha_queues'): - ctxt['rabbitmq_ha_queues'] = relation_get('ha_queues') - else: - ctxt['rabbitmq_ha_queues'] = False + if ('clustered' not in ctxt or ha_vip_only) \ + and len(related_units(rid)) > 1: rabbitmq_hosts = [] for unit in related_units(rid): rabbitmq_hosts.append(relation_get('private-address', @@ -388,6 +470,8 @@ class ApacheSSLContext(OSContextGenerator): 'private_address': unit_get('private-address'), 'endpoints': [] } + if is_clustered(): + ctxt['private_address'] = config('vip') for api_port in self.external_ports: ext_port = determine_apache_port(api_port) int_port = determine_api_port(api_port) diff --git a/lib/charmhelpers/contrib/openstack/neutron.py b/hooks/charmhelpers/contrib/openstack/neutron.py similarity index 76% rename from lib/charmhelpers/contrib/openstack/neutron.py rename to hooks/charmhelpers/contrib/openstack/neutron.py index 8d32bd00..563e804d 100644 --- a/lib/charmhelpers/contrib/openstack/neutron.py +++ b/hooks/charmhelpers/contrib/openstack/neutron.py @@ -17,8 +17,28 @@ def headers_package(): kver = check_output(['uname', '-r']).strip() return 'linux-headers-%s' % kver +QUANTUM_CONF_DIR = '/etc/quantum' + + +def kernel_version(): + """ Retrieve the current major kernel version as a tuple e.g. (3, 13) """ + kver = check_output(['uname', '-r']).strip() + kver = kver.split('.') + return (int(kver[0]), int(kver[1])) + + +def determine_dkms_package(): + """ Determine which DKMS package should be used based on kernel version """ + # NOTE: 3.13 kernels have support for GRE and VXLAN native + if kernel_version() >= (3, 13): + return [] + else: + return ['openvswitch-datapath-dkms'] + # legacy + + def quantum_plugins(): from charmhelpers.contrib.openstack import context return { @@ -30,9 +50,10 @@ def quantum_plugins(): 'contexts': [ context.SharedDBContext(user=config('neutron-database-user'), database=config('neutron-database'), - relation_prefix='neutron')], + relation_prefix='neutron', + ssl_dir=QUANTUM_CONF_DIR)], 'services': ['quantum-plugin-openvswitch-agent'], - 'packages': [[headers_package(), 'openvswitch-datapath-dkms'], + 'packages': [[headers_package()] + determine_dkms_package(), ['quantum-plugin-openvswitch-agent']], 'server_packages': ['quantum-server', 'quantum-plugin-openvswitch'], @@ -45,7 +66,8 @@ def quantum_plugins(): 'contexts': [ context.SharedDBContext(user=config('neutron-database-user'), database=config('neutron-database'), - relation_prefix='neutron')], + relation_prefix='neutron', + ssl_dir=QUANTUM_CONF_DIR)], 'services': [], 'packages': [], 'server_packages': ['quantum-server', @@ -54,10 +76,13 @@ def quantum_plugins(): } } +NEUTRON_CONF_DIR = '/etc/neutron' + def neutron_plugins(): from charmhelpers.contrib.openstack import context - return { + release = os_release('nova-common') + plugins = { 'ovs': { 'config': '/etc/neutron/plugins/openvswitch/' 'ovs_neutron_plugin.ini', @@ -66,10 +91,11 @@ def neutron_plugins(): 'contexts': [ context.SharedDBContext(user=config('neutron-database-user'), database=config('neutron-database'), - relation_prefix='neutron')], + relation_prefix='neutron', + ssl_dir=NEUTRON_CONF_DIR)], 'services': ['neutron-plugin-openvswitch-agent'], - 'packages': [[headers_package(), 'openvswitch-datapath-dkms'], - ['quantum-plugin-openvswitch-agent']], + 'packages': [[headers_package()] + determine_dkms_package(), + ['neutron-plugin-openvswitch-agent']], 'server_packages': ['neutron-server', 'neutron-plugin-openvswitch'], 'server_services': ['neutron-server'] @@ -81,7 +107,8 @@ def neutron_plugins(): 'contexts': [ context.SharedDBContext(user=config('neutron-database-user'), database=config('neutron-database'), - relation_prefix='neutron')], + relation_prefix='neutron', + ssl_dir=NEUTRON_CONF_DIR)], 'services': [], 'packages': [], 'server_packages': ['neutron-server', @@ -89,6 +116,13 @@ def neutron_plugins(): 'server_services': ['neutron-server'] } } + # NOTE: patch in ml2 plugin for icehouse onwards + if release >= 'icehouse': + plugins['ovs']['config'] = '/etc/neutron/plugins/ml2/ml2_conf.ini' + plugins['ovs']['driver'] = 'neutron.plugins.ml2.plugin.Ml2Plugin' + plugins['ovs']['server_packages'] = ['neutron-server', + 'neutron-plugin-ml2'] + return plugins def neutron_plugin_attribute(plugin, attr, net_manager=None): diff --git a/lib/charmhelpers/contrib/openstack/templates/__init__.py b/hooks/charmhelpers/contrib/openstack/templates/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/openstack/templates/__init__.py rename to hooks/charmhelpers/contrib/openstack/templates/__init__.py diff --git a/lib/charmhelpers/contrib/openstack/templating.py b/hooks/charmhelpers/contrib/openstack/templating.py similarity index 100% rename from lib/charmhelpers/contrib/openstack/templating.py rename to hooks/charmhelpers/contrib/openstack/templating.py diff --git a/lib/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py similarity index 96% rename from lib/charmhelpers/contrib/openstack/utils.py rename to hooks/charmhelpers/contrib/openstack/utils.py index 56d04245..ac261fd7 100644 --- a/lib/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -65,6 +65,10 @@ SWIFT_CODENAMES = OrderedDict([ ('1.10.0', 'havana'), ('1.9.1', 'havana'), ('1.9.0', 'havana'), + ('1.13.1', 'icehouse'), + ('1.13.0', 'icehouse'), + ('1.12.0', 'icehouse'), + ('1.11.0', 'icehouse'), ]) DEFAULT_LOOPBACK_SIZE = '5G' @@ -420,19 +424,19 @@ def get_hostname(address, fqdn=True): Resolves hostname for given IP, or returns the input if it is already a hostname. """ - if not is_ip(address): - return address + if is_ip(address): + try: + import dns.reversename + except ImportError: + apt_install('python-dnspython') + import dns.reversename - try: - import dns.reversename - except ImportError: - apt_install('python-dnspython') - import dns.reversename - - rev = dns.reversename.from_address(address) - result = ns_query(rev) - if not result: - return None + rev = dns.reversename.from_address(address) + result = ns_query(rev) + if not result: + return None + else: + result = address if fqdn: # strip trailing . diff --git a/hooks/charmhelpers/contrib/peerstorage/__init__.py b/hooks/charmhelpers/contrib/peerstorage/__init__.py new file mode 100644 index 00000000..e0eeab5b --- /dev/null +++ b/hooks/charmhelpers/contrib/peerstorage/__init__.py @@ -0,0 +1,83 @@ +from charmhelpers.core.hookenv import ( + relation_ids, + relation_get, + local_unit, + relation_set, +) + +""" +This helper provides functions to support use of a peer relation +for basic key/value storage, with the added benefit that all storage +can be replicated across peer units, so this is really useful for +services that issue usernames/passwords to remote services. + +def shared_db_changed() + # Only the lead unit should create passwords + if not is_leader(): + return + username = relation_get('username') + key = '{}.password'.format(username) + # Attempt to retrieve any existing password for this user + password = peer_retrieve(key) + if password is None: + # New user, create password and store + password = pwgen(length=64) + peer_store(key, password) + create_access(username, password) + relation_set(password=password) + + +def cluster_changed() + # Echo any relation data other that *-address + # back onto the peer relation so all units have + # all *.password keys stored on their local relation + # for later retrieval. + peer_echo() + +""" + + +def peer_retrieve(key, relation_name='cluster'): + """ Retrieve a named key from peer relation relation_name """ + cluster_rels = relation_ids(relation_name) + if len(cluster_rels) > 0: + cluster_rid = cluster_rels[0] + return relation_get(attribute=key, rid=cluster_rid, + unit=local_unit()) + else: + raise ValueError('Unable to detect' + 'peer relation {}'.format(relation_name)) + + +def peer_store(key, value, relation_name='cluster'): + """ Store the key/value pair on the named peer relation relation_name """ + cluster_rels = relation_ids(relation_name) + if len(cluster_rels) > 0: + cluster_rid = cluster_rels[0] + relation_set(relation_id=cluster_rid, + relation_settings={key: value}) + else: + raise ValueError('Unable to detect ' + 'peer relation {}'.format(relation_name)) + + +def peer_echo(includes=None): + """Echo filtered attributes back onto the same relation for storage + + Note that this helper must only be called within a peer relation + changed hook + """ + rdata = relation_get() + echo_data = {} + if includes is None: + echo_data = rdata.copy() + for ex in ['private-address', 'public-address']: + if ex in echo_data: + echo_data.pop(ex) + else: + for attribute, value in rdata.iteritems(): + for include in includes: + if include in attribute: + echo_data[attribute] = value + if len(echo_data) > 0: + relation_set(relation_settings=echo_data) diff --git a/lib/charmhelpers/contrib/ssl/__init__.py b/hooks/charmhelpers/contrib/ssl/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/ssl/__init__.py rename to hooks/charmhelpers/contrib/ssl/__init__.py diff --git a/hooks/charmhelpers/contrib/ssl/service.py b/hooks/charmhelpers/contrib/ssl/service.py new file mode 100644 index 00000000..295f7219 --- /dev/null +++ b/hooks/charmhelpers/contrib/ssl/service.py @@ -0,0 +1,267 @@ +import logging +import os +from os.path import join as path_join +from os.path import exists +import subprocess + + +log = logging.getLogger("service_ca") + +logging.basicConfig(level=logging.DEBUG) + +STD_CERT = "standard" + +# Mysql server is fairly picky about cert creation +# and types, spec its creation separately for now. +MYSQL_CERT = "mysql" + + +class ServiceCA(object): + + default_expiry = str(365 * 2) + default_ca_expiry = str(365 * 6) + + def __init__(self, name, ca_dir, cert_type=STD_CERT): + self.name = name + self.ca_dir = ca_dir + self.cert_type = cert_type + + ############### + # Hook Helper API + @staticmethod + def get_ca(type=STD_CERT): + service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0] + ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca') + ca = ServiceCA(service_name, ca_path, type) + ca.init() + return ca + + @classmethod + def get_service_cert(cls, type=STD_CERT): + service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0] + ca = cls.get_ca() + crt, key = ca.get_or_create_cert(service_name) + return crt, key, ca.get_ca_bundle() + + ############### + + def init(self): + log.debug("initializing service ca") + if not exists(self.ca_dir): + self._init_ca_dir(self.ca_dir) + self._init_ca() + + @property + def ca_key(self): + return path_join(self.ca_dir, 'private', 'cacert.key') + + @property + def ca_cert(self): + return path_join(self.ca_dir, 'cacert.pem') + + @property + def ca_conf(self): + return path_join(self.ca_dir, 'ca.cnf') + + @property + def signing_conf(self): + return path_join(self.ca_dir, 'signing.cnf') + + def _init_ca_dir(self, ca_dir): + os.mkdir(ca_dir) + for i in ['certs', 'crl', 'newcerts', 'private']: + sd = path_join(ca_dir, i) + if not exists(sd): + os.mkdir(sd) + + if not exists(path_join(ca_dir, 'serial')): + with open(path_join(ca_dir, 'serial'), 'wb') as fh: + fh.write('02\n') + + if not exists(path_join(ca_dir, 'index.txt')): + with open(path_join(ca_dir, 'index.txt'), 'wb') as fh: + fh.write('') + + def _init_ca(self): + """Generate the root ca's cert and key. + """ + if not exists(path_join(self.ca_dir, 'ca.cnf')): + with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh: + fh.write( + CA_CONF_TEMPLATE % (self.get_conf_variables())) + + if not exists(path_join(self.ca_dir, 'signing.cnf')): + with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh: + fh.write( + SIGNING_CONF_TEMPLATE % (self.get_conf_variables())) + + if exists(self.ca_cert) or exists(self.ca_key): + raise RuntimeError("Initialized called when CA already exists") + cmd = ['openssl', 'req', '-config', self.ca_conf, + '-x509', '-nodes', '-newkey', 'rsa', + '-days', self.default_ca_expiry, + '-keyout', self.ca_key, '-out', self.ca_cert, + '-outform', 'PEM'] + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + log.debug("CA Init:\n %s", output) + + def get_conf_variables(self): + return dict( + org_name="juju", + org_unit_name="%s service" % self.name, + common_name=self.name, + ca_dir=self.ca_dir) + + def get_or_create_cert(self, common_name): + if common_name in self: + return self.get_certificate(common_name) + return self.create_certificate(common_name) + + def create_certificate(self, common_name): + if common_name in self: + return self.get_certificate(common_name) + key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name) + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) + csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name) + self._create_certificate(common_name, key_p, csr_p, crt_p) + return self.get_certificate(common_name) + + def get_certificate(self, common_name): + if not common_name in self: + raise ValueError("No certificate for %s" % common_name) + key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name) + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) + with open(crt_p) as fh: + crt = fh.read() + with open(key_p) as fh: + key = fh.read() + return crt, key + + def __contains__(self, common_name): + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) + return exists(crt_p) + + def _create_certificate(self, common_name, key_p, csr_p, crt_p): + template_vars = self.get_conf_variables() + template_vars['common_name'] = common_name + subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % ( + template_vars) + + log.debug("CA Create Cert %s", common_name) + cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048', + '-nodes', '-days', self.default_expiry, + '-keyout', key_p, '-out', csr_p, '-subj', subj] + subprocess.check_call(cmd) + cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p] + subprocess.check_call(cmd) + + log.debug("CA Sign Cert %s", common_name) + if self.cert_type == MYSQL_CERT: + cmd = ['openssl', 'x509', '-req', + '-in', csr_p, '-days', self.default_expiry, + '-CA', self.ca_cert, '-CAkey', self.ca_key, + '-set_serial', '01', '-out', crt_p] + else: + cmd = ['openssl', 'ca', '-config', self.signing_conf, + '-extensions', 'req_extensions', + '-days', self.default_expiry, '-notext', + '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch'] + log.debug("running %s", " ".join(cmd)) + subprocess.check_call(cmd) + + def get_ca_bundle(self): + with open(self.ca_cert) as fh: + return fh.read() + + +CA_CONF_TEMPLATE = """ +[ ca ] +default_ca = CA_default + +[ CA_default ] +dir = %(ca_dir)s +policy = policy_match +database = $dir/index.txt +serial = $dir/serial +certs = $dir/certs +crl_dir = $dir/crl +new_certs_dir = $dir/newcerts +certificate = $dir/cacert.pem +private_key = $dir/private/cacert.key +RANDFILE = $dir/private/.rand +default_md = default + +[ req ] +default_bits = 1024 +default_md = sha1 + +prompt = no +distinguished_name = ca_distinguished_name + +x509_extensions = ca_extensions + +[ ca_distinguished_name ] +organizationName = %(org_name)s +organizationalUnitName = %(org_unit_name)s Certificate Authority + + +[ policy_match ] +countryName = optional +stateOrProvinceName = optional +organizationName = match +organizationalUnitName = optional +commonName = supplied + +[ ca_extensions ] +basicConstraints = critical,CA:true +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always, issuer +keyUsage = cRLSign, keyCertSign +""" + + +SIGNING_CONF_TEMPLATE = """ +[ ca ] +default_ca = CA_default + +[ CA_default ] +dir = %(ca_dir)s +policy = policy_match +database = $dir/index.txt +serial = $dir/serial +certs = $dir/certs +crl_dir = $dir/crl +new_certs_dir = $dir/newcerts +certificate = $dir/cacert.pem +private_key = $dir/private/cacert.key +RANDFILE = $dir/private/.rand +default_md = default + +[ req ] +default_bits = 1024 +default_md = sha1 + +prompt = no +distinguished_name = req_distinguished_name + +x509_extensions = req_extensions + +[ req_distinguished_name ] +organizationName = %(org_name)s +organizationalUnitName = %(org_unit_name)s machine resources +commonName = %(common_name)s + +[ policy_match ] +countryName = optional +stateOrProvinceName = optional +organizationName = match +organizationalUnitName = optional +commonName = supplied + +[ req_extensions ] +basicConstraints = CA:false +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always, issuer +keyUsage = digitalSignature, keyEncipherment, keyAgreement +extendedKeyUsage = serverAuth, clientAuth +""" diff --git a/lib/charmhelpers/contrib/network/__init__.py b/hooks/charmhelpers/contrib/storage/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/network/__init__.py rename to hooks/charmhelpers/contrib/storage/__init__.py diff --git a/lib/charmhelpers/contrib/openstack/__init__.py b/hooks/charmhelpers/contrib/storage/linux/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/openstack/__init__.py rename to hooks/charmhelpers/contrib/storage/linux/__init__.py diff --git a/lib/charmhelpers/contrib/storage/linux/ceph.py b/hooks/charmhelpers/contrib/storage/linux/ceph.py similarity index 100% rename from lib/charmhelpers/contrib/storage/linux/ceph.py rename to hooks/charmhelpers/contrib/storage/linux/ceph.py diff --git a/lib/charmhelpers/contrib/storage/linux/loopback.py b/hooks/charmhelpers/contrib/storage/linux/loopback.py similarity index 100% rename from lib/charmhelpers/contrib/storage/linux/loopback.py rename to hooks/charmhelpers/contrib/storage/linux/loopback.py diff --git a/lib/charmhelpers/contrib/storage/linux/lvm.py b/hooks/charmhelpers/contrib/storage/linux/lvm.py similarity index 100% rename from lib/charmhelpers/contrib/storage/linux/lvm.py rename to hooks/charmhelpers/contrib/storage/linux/lvm.py diff --git a/lib/charmhelpers/contrib/storage/linux/utils.py b/hooks/charmhelpers/contrib/storage/linux/utils.py similarity index 85% rename from lib/charmhelpers/contrib/storage/linux/utils.py rename to hooks/charmhelpers/contrib/storage/linux/utils.py index c40218f0..5349c3ea 100644 --- a/lib/charmhelpers/contrib/storage/linux/utils.py +++ b/hooks/charmhelpers/contrib/storage/linux/utils.py @@ -22,4 +22,5 @@ def zap_disk(block_device): :param block_device: str: Full path of block device to clean. ''' - check_call(['sgdisk', '--zap-all', '--mbrtogpt', block_device]) + check_call(['sgdisk', '--zap-all', '--clear', + '--mbrtogpt', block_device]) diff --git a/lib/charmhelpers/contrib/storage/__init__.py b/hooks/charmhelpers/core/__init__.py similarity index 100% rename from lib/charmhelpers/contrib/storage/__init__.py rename to hooks/charmhelpers/core/__init__.py diff --git a/lib/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py similarity index 100% rename from lib/charmhelpers/core/hookenv.py rename to hooks/charmhelpers/core/hookenv.py diff --git a/lib/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py similarity index 100% rename from lib/charmhelpers/core/host.py rename to hooks/charmhelpers/core/host.py diff --git a/lib/charmhelpers/fetch/__init__.py b/hooks/charmhelpers/fetch/__init__.py similarity index 93% rename from lib/charmhelpers/fetch/__init__.py rename to hooks/charmhelpers/fetch/__init__.py index c05e0335..97a19912 100644 --- a/lib/charmhelpers/fetch/__init__.py +++ b/hooks/charmhelpers/fetch/__init__.py @@ -97,6 +97,29 @@ def apt_install(packages, options=None, fatal=False): subprocess.call(cmd, env=env) +def apt_upgrade(options=None, fatal=False, dist=False): + """Upgrade all packages""" + if options is None: + options = ['--option=Dpkg::Options::=--force-confold'] + + cmd = ['apt-get', '--assume-yes'] + cmd.extend(options) + if dist: + cmd.append('dist-upgrade') + else: + cmd.append('upgrade') + log("Upgrading with options: {}".format(options)) + + env = os.environ.copy() + if 'DEBIAN_FRONTEND' not in env: + env['DEBIAN_FRONTEND'] = 'noninteractive' + + if fatal: + subprocess.check_call(cmd, env=env) + else: + subprocess.call(cmd, env=env) + + def apt_update(fatal=False): """Update local apt cache""" cmd = ['apt-get', 'update'] diff --git a/lib/charmhelpers/fetch/archiveurl.py b/hooks/charmhelpers/fetch/archiveurl.py similarity index 64% rename from lib/charmhelpers/fetch/archiveurl.py rename to hooks/charmhelpers/fetch/archiveurl.py index e35b8f15..87e7071a 100644 --- a/lib/charmhelpers/fetch/archiveurl.py +++ b/hooks/charmhelpers/fetch/archiveurl.py @@ -1,5 +1,7 @@ import os import urllib2 +import urlparse + from charmhelpers.fetch import ( BaseFetchHandler, UnhandledSource @@ -24,6 +26,19 @@ class ArchiveUrlFetchHandler(BaseFetchHandler): def download(self, source, dest): # propogate all exceptions # URLError, OSError, etc + proto, netloc, path, params, query, fragment = urlparse.urlparse(source) + if proto in ('http', 'https'): + auth, barehost = urllib2.splituser(netloc) + if auth is not None: + source = urlparse.urlunparse((proto, barehost, path, params, query, fragment)) + username, password = urllib2.splitpasswd(auth) + passman = urllib2.HTTPPasswordMgrWithDefaultRealm() + # Realm is set to None in add_password to force the username and password + # to be used whatever the realm + passman.add_password(None, source, username, password) + authhandler = urllib2.HTTPBasicAuthHandler(passman) + opener = urllib2.build_opener(authhandler) + urllib2.install_opener(opener) response = urllib2.urlopen(source) try: with open(dest, 'w') as dest_file: diff --git a/lib/charmhelpers/fetch/bzrurl.py b/hooks/charmhelpers/fetch/bzrurl.py similarity index 100% rename from lib/charmhelpers/fetch/bzrurl.py rename to hooks/charmhelpers/fetch/bzrurl.py diff --git a/hooks/lib/ceph_utils.py b/hooks/lib/ceph_utils.py deleted file mode 100644 index c336ac9d..00000000 --- a/hooks/lib/ceph_utils.py +++ /dev/null @@ -1,318 +0,0 @@ -# -# Copyright 2012 Canonical Ltd. -# -# This file is sourced from lp:openstack-charm-helpers -# -# Authors: -# James Page -# Adam Gandelman -# - -import commands -import json -import subprocess -import os -import shutil -import time -import lib.utils as utils - -KEYRING = '/etc/ceph/ceph.client.%s.keyring' -KEYFILE = '/etc/ceph/ceph.client.%s.key' - -CEPH_CONF = """[global] - auth supported = %(auth)s - keyring = %(keyring)s - mon host = %(mon_hosts)s - log to syslog = %(use_syslog)s - err to syslog = %(use_syslog)s - clog to syslog = %(use_syslog)s -""" - - -def execute(cmd): - subprocess.check_call(cmd) - - -def execute_shell(cmd): - subprocess.check_call(cmd, shell=True) - - -def install(): - ceph_dir = "/etc/ceph" - if not os.path.isdir(ceph_dir): - os.mkdir(ceph_dir) - utils.install('ceph-common') - - -def rbd_exists(service, pool, rbd_img): - (rc, out) = commands.getstatusoutput('rbd list --id %s --pool %s' % - (service, pool)) - return rbd_img in out - - -def create_rbd_image(service, pool, image, sizemb): - cmd = [ - 'rbd', - 'create', - image, - '--size', - str(sizemb), - '--id', - service, - '--pool', - pool] - execute(cmd) - - -def pool_exists(service, name): - (rc, out) = commands.getstatusoutput("rados --id %s lspools" % service) - return name in out - - -def ceph_version(): - ''' Retrieve the local version of ceph ''' - if os.path.exists('/usr/bin/ceph'): - cmd = ['ceph', '-v'] - output = subprocess.check_output(cmd) - output = output.split() - if len(output) > 3: - return output[2] - else: - return None - else: - return None - - -def get_osds(service): - ''' - Return a list of all Ceph Object Storage Daemons - currently in the cluster - ''' - version = ceph_version() - if version and version >= '0.56': - cmd = ['ceph', '--id', service, 'osd', 'ls', '--format=json'] - return json.loads(subprocess.check_output(cmd)) - else: - return None - - -def create_pool(service, name, replicas=2): - ''' Create a new RADOS pool ''' - if pool_exists(service, name): - utils.juju_log('WARNING', - "Ceph pool {} already exists, " - "skipping creation".format(name)) - return - - osds = get_osds(service) - if osds: - pgnum = (len(osds) * 100 / replicas) - else: - # NOTE(james-page): Default to 200 for older ceph versions - # which don't support OSD query from cli - pgnum = 200 - - cmd = [ - 'ceph', '--id', service, - 'osd', 'pool', 'create', - name, str(pgnum) - ] - subprocess.check_call(cmd) - cmd = [ - 'ceph', '--id', service, - 'osd', 'pool', 'set', name, - 'size', str(replicas) - ] - subprocess.check_call(cmd) - - -def keyfile_path(service): - return KEYFILE % service - - -def keyring_path(service): - return KEYRING % service - - -def create_keyring(service, key): - keyring = keyring_path(service) - if os.path.exists(keyring): - utils.juju_log('INFO', 'ceph: Keyring exists at %s.' % keyring) - cmd = [ - 'ceph-authtool', - keyring, - '--create-keyring', - '--name=client.%s' % service, - '--add-key=%s' % key] - execute(cmd) - utils.juju_log('INFO', 'ceph: Created new ring at %s.' % keyring) - - -def create_key_file(service, key): - # create a file containing the key - keyfile = keyfile_path(service) - if os.path.exists(keyfile): - utils.juju_log('INFO', 'ceph: Keyfile exists at %s.' % keyfile) - fd = open(keyfile, 'w') - fd.write(key) - fd.close() - utils.juju_log('INFO', 'ceph: Created new keyfile at %s.' % keyfile) - - -def get_ceph_nodes(): - hosts = [] - for r_id in utils.relation_ids('ceph'): - for unit in utils.relation_list(r_id): - hosts.append(utils.relation_get('private-address', - unit=unit, rid=r_id)) - return hosts - - -def configure(service, key, auth, use_syslog): - create_keyring(service, key) - create_key_file(service, key) - hosts = get_ceph_nodes() - mon_hosts = ",".join(map(str, hosts)) - keyring = keyring_path(service) - with open('/etc/ceph/ceph.conf', 'w') as ceph_conf: - ceph_conf.write(CEPH_CONF % locals()) - modprobe_kernel_module('rbd') - - -def image_mapped(image_name): - (rc, out) = commands.getstatusoutput('rbd showmapped') - return image_name in out - - -def map_block_storage(service, pool, image): - cmd = [ - 'rbd', - 'map', - '%s/%s' % (pool, image), - '--user', - service, - '--secret', - keyfile_path(service)] - execute(cmd) - - -def filesystem_mounted(fs): - return subprocess.call(['grep', '-wqs', fs, '/proc/mounts']) == 0 - - -def make_filesystem(blk_device, fstype='ext4'): - count = 0 - e_noent = os.errno.ENOENT - while not os.path.exists(blk_device): - if count >= 10: - utils.juju_log('ERROR', - 'ceph: gave up waiting on block device %s' % - blk_device) - raise IOError(e_noent, os.strerror(e_noent), blk_device) - utils.juju_log('INFO', - 'ceph: waiting for block device %s to appear' % - blk_device) - count += 1 - time.sleep(1) - else: - utils.juju_log('INFO', - 'ceph: Formatting block device %s as filesystem %s.' % - (blk_device, fstype)) - execute(['mkfs', '-t', fstype, blk_device]) - - -def place_data_on_ceph(service, blk_device, data_src_dst, fstype='ext4'): - # mount block device into /mnt - cmd = ['mount', '-t', fstype, blk_device, '/mnt'] - execute(cmd) - - # copy data to /mnt - try: - copy_files(data_src_dst, '/mnt') - except: - pass - - # umount block device - cmd = ['umount', '/mnt'] - execute(cmd) - - _dir = os.stat(data_src_dst) - uid = _dir.st_uid - gid = _dir.st_gid - - # re-mount where the data should originally be - cmd = ['mount', '-t', fstype, blk_device, data_src_dst] - execute(cmd) - - # ensure original ownership of new mount. - cmd = ['chown', '-R', '%s:%s' % (uid, gid), data_src_dst] - execute(cmd) - - -# TODO: re-use -def modprobe_kernel_module(module): - utils.juju_log('INFO', 'Loading kernel module') - cmd = ['modprobe', module] - execute(cmd) - cmd = 'echo %s >> /etc/modules' % module - execute_shell(cmd) - - -def copy_files(src, dst, symlinks=False, ignore=None): - for item in os.listdir(src): - s = os.path.join(src, item) - d = os.path.join(dst, item) - if os.path.isdir(s): - shutil.copytree(s, d, symlinks, ignore) - else: - shutil.copy2(s, d) - - -def ensure_ceph_storage(service, pool, rbd_img, sizemb, mount_point, - blk_device, fstype, system_services=[], - rbd_pool_replicas=2): - """ - To be called from the current cluster leader. - Ensures given pool and RBD image exists, is mapped to a block device, - and the device is formatted and mounted at the given mount_point. - - If formatting a device for the first time, data existing at mount_point - will be migrated to the RBD device before being remounted. - - All services listed in system_services will be stopped prior to data - migration and restarted when complete. - """ - # Ensure pool, RBD image, RBD mappings are in place. - if not pool_exists(service, pool): - utils.juju_log('INFO', 'ceph: Creating new pool %s.' % pool) - create_pool(service, pool, replicas=rbd_pool_replicas) - - if not rbd_exists(service, pool, rbd_img): - utils.juju_log('INFO', 'ceph: Creating RBD image (%s).' % rbd_img) - create_rbd_image(service, pool, rbd_img, sizemb) - - if not image_mapped(rbd_img): - utils.juju_log('INFO', 'ceph: Mapping RBD Image as a Block Device.') - map_block_storage(service, pool, rbd_img) - - # make file system - # TODO: What happens if for whatever reason this is run again and - # the data is already in the rbd device and/or is mounted?? - # When it is mounted already, it will fail to make the fs - # XXX: This is really sketchy! Need to at least add an fstab entry - # otherwise this hook will blow away existing data if its executed - # after a reboot. - if not filesystem_mounted(mount_point): - make_filesystem(blk_device, fstype) - - for svc in system_services: - if utils.running(svc): - utils.juju_log('INFO', - 'Stopping services %s prior to migrating ' - 'data' % svc) - utils.stop(svc) - - place_data_on_ceph(service, blk_device, mount_point, fstype) - - for svc in system_services: - utils.start(svc) diff --git a/hooks/lib/cluster_utils.py b/hooks/lib/cluster_utils.py deleted file mode 100644 index 534acb3b..00000000 --- a/hooks/lib/cluster_utils.py +++ /dev/null @@ -1,128 +0,0 @@ -# -# Copyright 2012 Canonical Ltd. -# -# This file is sourced from lp:openstack-charm-helpers -# -# Authors: -# James Page -# Adam Gandelman -# - -from lib.utils import ( - juju_log, - relation_ids, - relation_list, - relation_get, - get_unit_hostname, - config_get) -import subprocess -import os - - -def is_clustered(): - for r_id in (relation_ids('ha') or []): - for unit in (relation_list(r_id) or []): - clustered = relation_get('clustered', - rid=r_id, - unit=unit) - if clustered: - return True - return False - - -def is_leader(resource): - cmd = [ - "crm", "resource", - "show", resource] - try: - status = subprocess.check_output(cmd) - except subprocess.CalledProcessError: - return False - else: - if get_unit_hostname() in status: - return True - else: - return False - - -def peer_units(): - peers = [] - for r_id in (relation_ids('cluster') or []): - for unit in (relation_list(r_id) or []): - peers.append(unit) - return peers - - -def oldest_peer(peers): - local_unit_no = os.getenv('JUJU_UNIT_NAME').split('/')[1] - for peer in peers: - remote_unit_no = peer.split('/')[1] - if remote_unit_no < local_unit_no: - return False - return True - - -def eligible_leader(resource): - if is_clustered(): - if not is_leader(resource): - juju_log('INFO', 'Deferring action to CRM leader.') - return False - else: - peers = peer_units() - if peers and not oldest_peer(peers): - juju_log('INFO', 'Deferring action to oldest service unit.') - return False - return True - - -def https(): - ''' - Determines whether enough data has been provided in configuration - or relation data to configure HTTPS - . - returns: boolean - ''' - if config_get('use-https') == "yes": - return True - if config_get('ssl_cert') and config_get('ssl_key'): - return True - for r_id in relation_ids('identity-service'): - for unit in relation_list(r_id): - if (relation_get('https_keystone', rid=r_id, unit=unit) and - relation_get('ssl_cert', rid=r_id, unit=unit) and - relation_get('ssl_key', rid=r_id, unit=unit) and - relation_get('ca_cert', rid=r_id, unit=unit)): - return True - return False - - -def determine_api_port(public_port): - ''' - Determine correct API server listening port based on - existence of HTTPS reverse proxy and/or haproxy. - - public_port: int: standard public port for given service - - returns: int: the correct listening port for the API service - ''' - i = 0 - if len(peer_units()) > 0 or is_clustered(): - i += 1 - if https(): - i += 1 - return public_port - (i * 10) - - -def determine_haproxy_port(public_port): - ''' - Description: Determine correct proxy listening port based on public IP + - existence of HTTPS reverse proxy. - - public_port: int: standard public port for given service - - returns: int: the correct listening port for the HAProxy service - ''' - i = 0 - if https(): - i += 1 - return public_port - (i * 10) diff --git a/hooks/lib/openstack_common.py b/hooks/lib/openstack_common.py deleted file mode 100644 index b897e5ec..00000000 --- a/hooks/lib/openstack_common.py +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/python - -# Common python helper functions used for OpenStack charms. - -import apt_pkg as apt -import subprocess -import os - -CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu" -CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA' - -ubuntu_openstack_release = { - 'oneiric': 'diablo', - 'precise': 'essex', - 'quantal': 'folsom', - 'raring': 'grizzly', -} - - -openstack_codenames = { - '2011.2': 'diablo', - '2012.1': 'essex', - '2012.2': 'folsom', - '2013.1': 'grizzly', - '2013.2': 'havana', -} - -# The ugly duckling -swift_codenames = { - '1.4.3': 'diablo', - '1.4.8': 'essex', - '1.7.4': 'folsom', - '1.7.6': 'grizzly', - '1.7.7': 'grizzly', - '1.8.0': 'grizzly', -} - - -def juju_log(msg): - subprocess.check_call(['juju-log', msg]) - - -def error_out(msg): - juju_log("FATAL ERROR: %s" % msg) - exit(1) - - -def lsb_release(): - '''Return /etc/lsb-release in a dict''' - lsb = open('/etc/lsb-release', 'r') - d = {} - for l in lsb: - k, v = l.split('=') - d[k.strip()] = v.strip() - return d - - -def get_os_codename_install_source(src): - '''Derive OpenStack release codename from a given installation source.''' - ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] - - rel = '' - if src == 'distro': - try: - rel = ubuntu_openstack_release[ubuntu_rel] - except KeyError: - e = 'Code not derive openstack release for '\ - 'this Ubuntu release: %s' % rel - error_out(e) - return rel - - if src.startswith('cloud:'): - ca_rel = src.split(':')[1] - ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0] - return ca_rel - - # Best guess match based on deb string provided - if src.startswith('deb') or src.startswith('ppa'): - for k, v in openstack_codenames.iteritems(): - if v in src: - return v - - -def get_os_codename_version(vers): - '''Determine OpenStack codename from version number.''' - try: - return openstack_codenames[vers] - except KeyError: - e = 'Could not determine OpenStack codename for version %s' % vers - error_out(e) - - -def get_os_version_codename(codename): - '''Determine OpenStack version number from codename.''' - for k, v in openstack_codenames.iteritems(): - if v == codename: - return k - e = 'Code not derive OpenStack version for '\ - 'codename: %s' % codename - error_out(e) - - -def get_os_codename_package(pkg): - '''Derive OpenStack release codename from an installed package.''' - apt.init() - cache = apt.Cache() - try: - pkg = cache[pkg] - except: - e = 'Could not determine version of installed package: %s' % pkg - error_out(e) - - vers = apt.UpstreamVersion(pkg.current_ver.ver_str) - - try: - if 'swift' in pkg.name: - vers = vers[:5] - return swift_codenames[vers] - else: - vers = vers[:6] - return openstack_codenames[vers] - except KeyError: - e = 'Could not determine OpenStack codename for version %s' % vers - error_out(e) - - -def get_os_version_package(pkg): - '''Derive OpenStack version number from an installed package.''' - codename = get_os_codename_package(pkg) - - if 'swift' in pkg: - vers_map = swift_codenames - else: - vers_map = openstack_codenames - - for version, cname in vers_map.iteritems(): - if cname == codename: - return version - e = "Could not determine OpenStack version for package: %s" % pkg - error_out(e) - - -def configure_installation_source(rel): - '''Configure apt installation source.''' - - def _import_key(keyid): - cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \ - "--recv-keys %s" % keyid - try: - subprocess.check_call(cmd.split(' ')) - except subprocess.CalledProcessError: - error_out("Error importing repo key %s" % keyid) - - if rel == 'distro': - return - elif rel[:4] == "ppa:": - src = rel - subprocess.check_call(["add-apt-repository", "-y", src]) - elif rel[:3] == "deb": - l = len(rel.split('|')) - if l == 2: - src, key = rel.split('|') - juju_log("Importing PPA key from keyserver for %s" % src) - _import_key(key) - elif l == 1: - src = rel - else: - error_out("Invalid openstack-release: %s" % rel) - - with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f: - f.write(src) - elif rel[:6] == 'cloud:': - ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] - rel = rel.split(':')[1] - u_rel = rel.split('-')[0] - ca_rel = rel.split('-')[1] - - if u_rel != ubuntu_rel: - e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\ - 'version (%s)' % (ca_rel, ubuntu_rel) - error_out(e) - - if 'staging' in ca_rel: - # staging is just a regular PPA. - os_rel = ca_rel.split('/')[0] - ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel - cmd = 'add-apt-repository -y %s' % ppa - subprocess.check_call(cmd.split(' ')) - return - - # map charm config options to actual archive pockets. - pockets = { - 'folsom': 'precise-updates/folsom', - 'folsom/updates': 'precise-updates/folsom', - 'folsom/proposed': 'precise-proposed/folsom', - 'grizzly': 'precise-updates/grizzly', - 'grizzly/updates': 'precise-updates/grizzly', - 'grizzly/proposed': 'precise-proposed/grizzly' - } - - try: - pocket = pockets[ca_rel] - except KeyError: - e = 'Invalid Cloud Archive release specified: %s' % rel - error_out(e) - - src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket) - _import_key(CLOUD_ARCHIVE_KEY_ID) - - with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f: - f.write(src) - else: - error_out("Invalid openstack-release specified: %s" % rel) - - -def save_script_rc(script_path="scripts/scriptrc", **env_vars): - """ - Write an rc file in the charm-delivered directory containing - exported environment variables provided by env_vars. Any charm scripts run - outside the juju hook environment can source this scriptrc to obtain - updated config information necessary to perform health checks or - service changes. - """ - charm_dir = os.getenv('CHARM_DIR') - juju_rc_path = "%s/%s" % (charm_dir, script_path) - with open(juju_rc_path, 'wb') as rc_script: - rc_script.write( - "#!/bin/bash\n") - [rc_script.write('export %s=%s\n' % (u, p)) - for u, p in env_vars.iteritems() if u != "script_path"] diff --git a/hooks/lib/unison.py b/hooks/lib/unison.py deleted file mode 100755 index 44657194..00000000 --- a/hooks/lib/unison.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/python -# -# Easy file synchronization among peer units using ssh + unison. -# -# From *both* peer relation -joined and -changed, add a call to -# ssh_authorized_peers() describing the peer relation and the desired -# user + group. After all peer relations have settled, all hosts should -# be able to connect to on another via key auth'd ssh as the specified user. -# -# Other hooks are then free to synchronize files and directories using -# sync_to_peers(). -# -# For a peer relation named 'cluster', for example: -# -# cluster-relation-joined: -# ... -# ssh_authorized_peers(peer_interface='cluster', -# user='juju_ssh', group='juju_ssh', -# ensure_user=True) -# ... -# -# cluster-relation-changed: -# ... -# ssh_authorized_peers(peer_interface='cluster', -# user='juju_ssh', group='juju_ssh', -# ensure_user=True) -# ... -# -# Hooks are now free to sync files as easily as: -# -# files = ['/etc/fstab', '/etc/apt.conf.d/'] -# sync_to_peers(peer_interface='cluster', -# user='juju_ssh, paths=[files]) -# -# It is assumed the charm itself has setup permissions on each unit -# such that 'juju_ssh' has read + write permissions. Also assumed -# that the calling charm takes care of leader delegation. -# -# TODO: Currently depends on the utils.py shipped with the keystone charm. -# Either copy required functionality to this library or depend on -# something more generic. - -import os -import sys -import lib.utils as utils -import subprocess -import grp -import pwd - - -def get_homedir(user): - try: - user = pwd.getpwnam(user) - return user.pw_dir - except KeyError: - utils.juju_log('INFO', - 'Could not get homedir for user %s: user exists?') - sys.exit(1) - - -def get_keypair(user): - home_dir = get_homedir(user) - ssh_dir = os.path.join(home_dir, '.ssh') - if not os.path.isdir(ssh_dir): - os.mkdir(ssh_dir) - - priv_key = os.path.join(ssh_dir, 'id_rsa') - if not os.path.isfile(priv_key): - utils.juju_log('INFO', 'Generating new ssh key for user %s.' % user) - cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048', - '-f', priv_key] - subprocess.check_call(cmd) - - pub_key = '%s.pub' % priv_key - if not os.path.isfile(pub_key): - utils.juju_log('INFO', 'Generatring missing ssh public key @ %s.' % - pub_key) - cmd = ['ssh-keygen', '-y', '-f', priv_key] - p = subprocess.check_output(cmd).strip() - with open(pub_key, 'wb') as out: - out.write(p) - subprocess.check_call(['chown', '-R', user, ssh_dir]) - return (open(priv_key, 'r').read().strip(), - open(pub_key, 'r').read().strip()) - - -def write_authorized_keys(user, keys): - home_dir = get_homedir(user) - ssh_dir = os.path.join(home_dir, '.ssh') - auth_keys = os.path.join(ssh_dir, 'authorized_keys') - utils.juju_log('INFO', 'Syncing authorized_keys @ %s.' % auth_keys) - with open(auth_keys, 'wb') as out: - for k in keys: - out.write('%s\n' % k) - - -def write_known_hosts(user, hosts): - home_dir = get_homedir(user) - ssh_dir = os.path.join(home_dir, '.ssh') - known_hosts = os.path.join(ssh_dir, 'known_hosts') - khosts = [] - for host in hosts: - cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host] - remote_key = subprocess.check_output(cmd).strip() - khosts.append(remote_key) - utils.juju_log('INFO', 'Syncing known_hosts @ %s.' % known_hosts) - with open(known_hosts, 'wb') as out: - for host in khosts: - out.write('%s\n' % host) - - -def ensure_user(user, group=None): - # need to ensure a bash shell'd user exists. - try: - pwd.getpwnam(user) - except KeyError: - utils.juju_log('INFO', 'Creating new user %s.%s.' % (user, group)) - cmd = ['adduser', '--system', '--shell', '/bin/bash', user] - if group: - try: - grp.getgrnam(group) - except KeyError: - subprocess.check_call(['addgroup', group]) - cmd += ['--ingroup', group] - subprocess.check_call(cmd) - - -def ssh_authorized_peers(peer_interface, user, group=None, - ensure_local_user=False): - """ - Main setup function, should be called from both peer -changed and -joined - hooks with the same parameters. - """ - if ensure_local_user: - ensure_user(user, group) - priv_key, pub_key = get_keypair(user) - hook = os.path.basename(sys.argv[0]) - if hook == '%s-relation-joined' % peer_interface: - utils.relation_set(ssh_pub_key=pub_key) - print 'joined' - elif hook == '%s-relation-changed' % peer_interface: - hosts = [] - keys = [] - for r_id in utils.relation_ids(peer_interface): - for unit in utils.relation_list(r_id): - settings = utils.relation_get_dict(relation_id=r_id, - remote_unit=unit) - if 'ssh_pub_key' in settings: - keys.append(settings['ssh_pub_key']) - hosts.append(settings['private-address']) - else: - utils.juju_log('INFO', - 'ssh_authorized_peers(): ssh_pub_key ' - 'missing for unit %s, skipping.' % unit) - write_authorized_keys(user, keys) - write_known_hosts(user, hosts) - authed_hosts = ':'.join(hosts) - utils.relation_set(ssh_authorized_hosts=authed_hosts) - - -def _run_as_user(user): - try: - user = pwd.getpwnam(user) - except KeyError: - utils.juju_log('INFO', 'Invalid user: %s' % user) - sys.exit(1) - uid, gid = user.pw_uid, user.pw_gid - os.environ['HOME'] = user.pw_dir - - def _inner(): - os.setgid(gid) - os.setuid(uid) - return _inner - - -def run_as_user(user, cmd): - return subprocess.check_output(cmd, preexec_fn=_run_as_user(user), cwd='/') - - -def sync_to_peers(peer_interface, user, paths=[], verbose=False): - base_cmd = ['unison', '-auto', '-batch=true', '-confirmbigdel=false', - '-fastcheck=true', '-group=false', '-owner=false', - '-prefer=newer', '-times=true'] - if not verbose: - base_cmd.append('-silent') - - hosts = [] - for r_id in (utils.relation_ids(peer_interface) or []): - for unit in utils.relation_list(r_id): - settings = utils.relation_get_dict(relation_id=r_id, - remote_unit=unit) - try: - authed_hosts = settings['ssh_authorized_hosts'].split(':') - except KeyError: - print 'unison sync_to_peers: peer has not authorized *any* '\ - 'hosts yet.' - return - - unit_hostname = utils.unit_get('private-address') - add_host = None - for authed_host in authed_hosts: - if unit_hostname == authed_host: - add_host = settings['private-address'] - if add_host: - hosts.append(settings['private-address']) - else: - print ('unison sync_to_peers: peer (%s) has not authorized ' - '*this* host yet, skipping.' % - settings['private-address']) - - for path in paths: - # removing trailing slash from directory paths, unison - # doesn't like these. - if path.endswith('/'): - path = path[:(len(path) - 1)] - for host in hosts: - try: - cmd = base_cmd + [path, 'ssh://%s@%s/%s' % (user, host, path)] - utils.juju_log('INFO', 'Syncing local path %s to %s@%s:%s' % - (path, user, host, path)) - run_as_user(user, cmd) - except: - # it may fail for permissions on some files - pass diff --git a/hooks/lib/utils.py b/hooks/lib/utils.py index 7ef30a7c..23f4e984 100644 --- a/hooks/lib/utils.py +++ b/hooks/lib/utils.py @@ -9,48 +9,26 @@ # Adam Gandelman # -import json +import grp import os -import subprocess -import socket -import sys - - -def do_hooks(hooks): - hook = os.path.basename(sys.argv[0]) - - try: - hook_func = hooks[hook] - except KeyError: - juju_log('INFO', - "This charm doesn't know how to handle '{}'.".format(hook)) - else: - hook_func() - - -def install(*pkgs): - cmd = [ - 'apt-get', - '-y', - 'install'] - for pkg in pkgs: - cmd.append(pkg) - subprocess.check_call(cmd) +import pwd +from charmhelpers.fetch import ( + apt_install +) +from charmhelpers.core.hookenv import ( + local_unit, + remote_unit, + log +) TEMPLATES_DIR = 'templates' try: import jinja2 except ImportError: - install('python-jinja2') + apt_install('python-jinja2', fatal=True) import jinja2 -try: - import dns.resolver -except ImportError: - install('python-dnspython') - import dns.resolver - def render_template(template_name, context, template_dir=TEMPLATES_DIR): templates = \ @@ -58,219 +36,45 @@ def render_template(template_name, context, template_dir=TEMPLATES_DIR): template = templates.get_template(template_name) return template.render(context) -# Protocols -TCP = 'TCP' -UDP = 'UDP' + +def is_newer(): + l_unit_no = local_unit().split('/')[1] + r_unit_no = remote_unit().split('/')[1] + return (l_unit_no > r_unit_no) -def expose(port, protocol='TCP'): - cmd = [ - 'open-port', - '{}/{}'.format(port, protocol)] - subprocess.check_call(cmd) +def chown(path, owner='root', group='root', recursive=False): + """Changes owner of given path, recursively if needed""" + if os.path.exists(path): + log('Changing ownership of path %s to %s:%s' % + (path, owner, group)) + uid = pwd.getpwnam(owner).pw_uid + gid = grp.getgrnam(group).gr_gid - -def open_port(port, protocol='TCP'): - expose(port, protocol) - - -def close_port(port, protocol='TCP'): - cmd = [ - 'close-port', - '{}/{}'.format(port, protocol)] - subprocess.check_call(cmd) - - -def juju_log(severity, message): - cmd = [ - 'juju-log', - '--log-level', severity, - message] - subprocess.check_call(cmd) - - -cache = {} - - -def cached(func): - def wrapper(*args, **kwargs): - global cache - key = str((func, args, kwargs)) - try: - return cache[key] - except KeyError: - res = func(*args, **kwargs) - cache[key] = res - return res - return wrapper - - -@cached -def relation_ids(relation): - cmd = [ - 'relation-ids', - relation] - result = str(subprocess.check_output(cmd)).split() - if result == "": - return None - else: - return result - - -@cached -def relation_list(rid): - cmd = [ - 'relation-list', - '-r', rid] - result = str(subprocess.check_output(cmd)).split() - if result == "": - return None - else: - return result - - -@cached -def relation_get(attribute, unit=None, rid=None): - cmd = [ - 'relation-get'] - if rid: - cmd.append('-r') - cmd.append(rid) - cmd.append(attribute) - if unit: - cmd.append(unit) - value = subprocess.check_output(cmd).strip() # IGNORE:E1103 - if value == "": - return None - else: - return value - - -@cached -def relation_get_dict(relation_id=None, remote_unit=None): - """Obtain all relation data as dict by way of JSON""" - cmd = [ - 'relation-get', '--format=json'] - if relation_id: - cmd.append('-r') - cmd.append(relation_id) - if remote_unit: - cmd.append('-') - cmd.append(remote_unit) - j = subprocess.check_output(cmd) - d = json.loads(j) - settings = {} - # convert unicode to strings - for k, v in d.iteritems(): - settings[str(k)] = str(v) - return settings - - -def relation_set(**kwargs): - cmd = [ - 'relation-set'] - args = [] - for k, v in kwargs.items(): - if k == 'rid': - if v: - cmd.append('-r') - cmd.append(v) + if recursive: + for root, dirs, files in os.walk(path): + for d in dirs: + os.chown(os.path.join(root, d), uid, gid) + for f in files: + os.chown(os.path.join(root, f), uid, gid) else: - args.append('{}={}'.format(k, v)) - cmd += args - subprocess.check_call(cmd) - - -@cached -def unit_get(attribute): - cmd = [ - 'unit-get', - attribute] - value = subprocess.check_output(cmd).strip() # IGNORE:E1103 - if value == "": - return None + os.chown(path, uid, gid) else: - return value + log('%s path does not exist' % path) -@cached -def config_get(attribute): - cmd = [ - 'config-get', - '--format', - 'json'] - out = subprocess.check_output(cmd).strip() # IGNORE:E1103 - cfg = json.loads(out) +def chmod(path, perms, recursive=False): + """Changes perms of given path, recursively if needed""" + if os.path.exists(path): + log('Changing perms of path %s ' % path) - try: - return cfg[attribute] - except KeyError: - return None - - -@cached -def get_unit_hostname(): - return socket.gethostname() - - -@cached -def get_host_ip(hostname=unit_get('private-address')): - try: - # Test to see if already an IPv4 address - socket.inet_aton(hostname) - return hostname - except socket.error: - answers = dns.resolver.query(hostname, 'A') - if answers: - return answers[0].address - return None - - -def _svc_control(service, action): - subprocess.check_call(['service', service, action]) - - -def restart(*services): - for service in services: - _svc_control(service, 'restart') - - -def stop(*services): - for service in services: - _svc_control(service, 'stop') - - -def start(*services): - for service in services: - _svc_control(service, 'start') - - -def reload(*services): - for service in services: - try: - _svc_control(service, 'reload') - except subprocess.CalledProcessError: - # Reload failed - either service does not support reload - # or it was not running - restart will fixup most things - _svc_control(service, 'restart') - - -def running(service): - try: - output = subprocess.check_output(['service', service, 'status']) - except subprocess.CalledProcessError: - return False - else: - if ("start/running" in output or - "is running" in output): - return True + if recursive: + for root, dirs, files in os.walk(path): + for d in dirs: + os.chmod(os.path.join(root, d), perms) + for f in files: + os.chmod(os.path.join(root, f), perms) else: - return False - - -def is_relation_made(relation, key='private-address'): - for r_id in (relation_ids(relation) or []): - for unit in (relation_list(r_id) or []): - if relation_get(key, rid=r_id, unit=unit): - return True - return False + os.chmod(path, perms) + else: + log('ERROR', '%s path does not exist' % path) diff --git a/hooks/rabbit_utils.py b/hooks/rabbit_utils.py index 4fca282b..cd5008b9 100644 --- a/hooks/rabbit_utils.py +++ b/hooks/rabbit_utils.py @@ -5,38 +5,53 @@ import re import sys import subprocess import glob -import lib.utils as utils -import lib.unison as unison -import lib.cluster_utils as cluster +from lib.utils import render_template import apt_pkg as apt -import _pythonpath -_ = _pythonpath - from charmhelpers.contrib.openstack.utils import ( get_hostname, - error_out ) -PACKAGES = ['pwgen', 'rabbitmq-server', 'python-amqplib', 'unison'] +from charmhelpers.core.hookenv import ( + config, + relation_ids, + relation_get, + related_units, + log, ERROR, + service_name +) + +from charmhelpers.core.host import pwgen, mkdir, write_file + +from charmhelpers.contrib.peerstorage import ( + peer_store, + peer_retrieve +) + +PACKAGES = ['rabbitmq-server', 'python-amqplib'] RABBITMQ_CTL = '/usr/sbin/rabbitmqctl' COOKIE_PATH = '/var/lib/rabbitmq/.erlang.cookie' ENV_CONF = '/etc/rabbitmq/rabbitmq-env.conf' RABBITMQ_CONF = '/etc/rabbitmq/rabbitmq.config' -SSH_USER = 'juju_rabbit' RABBIT_USER = 'rabbitmq' LIB_PATH = '/var/lib/rabbitmq/' +_named_passwd = '/var/lib/charm/{}/rabbit-{}.passwd' + def vhost_exists(vhost): - cmd = [RABBITMQ_CTL, 'list_vhosts'] - out = subprocess.check_output(cmd) - for line in out.split('\n')[1:]: - if line == vhost: - utils.juju_log('INFO', 'vhost (%s) already exists.' % vhost) - return True - return False + try: + cmd = [RABBITMQ_CTL, 'list_vhosts'] + out = subprocess.check_output(cmd) + for line in out.split('\n')[1:]: + if line == vhost: + log('vhost (%s) already exists.' % vhost) + return True + return False + except: + # if no vhosts, just raises an exception + return False def create_vhost(vhost): @@ -44,7 +59,7 @@ def create_vhost(vhost): return cmd = [RABBITMQ_CTL, 'add_vhost', vhost] subprocess.check_call(cmd) - utils.juju_log('INFO', 'Created new vhost (%s).' % vhost) + log('Created new vhost (%s).' % vhost) def user_exists(user): @@ -64,17 +79,17 @@ def create_user(user, password, admin=False): if not exists: cmd = [RABBITMQ_CTL, 'add_user', user, password] subprocess.check_call(cmd) - utils.juju_log('INFO', 'Created new user (%s).' % user) + log('Created new user (%s).' % user) if admin == is_admin: return if admin: cmd = [RABBITMQ_CTL, 'set_user_tags', user, 'administrator'] - utils.juju_log('INFO', 'Granting user (%s) admin access.') + log('Granting user (%s) admin access.') else: cmd = [RABBITMQ_CTL, 'set_user_tags', user] - utils.juju_log('INFO', 'Revoking user (%s) admin access.') + log('Revoking user (%s) admin access.') def grant_permissions(user, vhost): @@ -88,68 +103,84 @@ def service(action): subprocess.check_call(cmd) -def rabbit_version(): +def compare_version(base_version): apt.init() cache = apt.Cache() pkg = cache['rabbitmq-server'] if pkg.current_ver: - return apt.upstream_version(pkg.current_ver.ver_str) + return apt.version_compare( + apt.upstream_version(pkg.current_ver.ver_str), + base_version) else: - return None + return False def cluster_with(): - vers = rabbit_version() - if vers >= '3.0.1-1': + log('Clustering with new node') + if compare_version('3.0.1') >= 0: cluster_cmd = 'join_cluster' - cmd = [RABBITMQ_CTL, 'set_policy HA \'^(?!amq\.).*\' ' - '\'{"ha-mode": "all"}\''] - subprocess.check_call(cmd) else: cluster_cmd = 'cluster' out = subprocess.check_output([RABBITMQ_CTL, 'cluster_status']) - current_host = subprocess.check_output(['hostname']).strip() + log('cluster status is %s' % str(out)) + + # check if node is already clustered + total_nodes = 1 + running_nodes = [] + m = re.search("\{running_nodes,\[(.*?)\]\}", out.strip(), re.DOTALL) + if m is not None: + running_nodes = m.group(1).split(',') + running_nodes = [x.replace("'", '') for x in running_nodes] + total_nodes = len(running_nodes) + + if total_nodes > 1: + log('Node is already clustered, skipping') + return False # check all peers and try to cluster with them available_nodes = [] - first_hostname = utils.relation_get('host') - available_nodes.append(first_hostname) - - for r_id in (utils.relation_ids('cluster') or []): - for unit in (utils.relation_list(r_id) or []): - address = utils.relation_get('private_address', - rid=r_id, unit=unit) + for r_id in relation_ids('cluster'): + for unit in related_units(r_id): + address = relation_get('private-address', + rid=r_id, unit=unit) if address is not None: node = get_hostname(address, fqdn=False) - if current_host != node: - available_nodes.append(node) + available_nodes.append(node) + + if len(available_nodes) == 0: + log('No nodes available to cluster with') + return False # iterate over all the nodes, join to the first available + num_tries = 0 for node in available_nodes: - utils.juju_log('INFO', - 'Clustering with remote rabbit host (%s).' % node) - for line in out.split('\n'): - if re.search(node, line): - utils.juju_log('INFO', - 'Host already clustered with %s.' % node) - return + log('Clustering with remote rabbit host (%s).' % node) + if node in running_nodes: + log('Host already clustered with %s.' % node) + return False - try: - cmd = [RABBITMQ_CTL, 'stop_app'] + try: + cmd = [RABBITMQ_CTL, 'stop_app'] + subprocess.check_call(cmd) + cmd = [RABBITMQ_CTL, cluster_cmd, 'rabbit@%s' % node] + subprocess.check_call(cmd) + cmd = [RABBITMQ_CTL, 'start_app'] + subprocess.check_call(cmd) + log('Host clustered with %s.' % node) + if compare_version('3.0.1') >= 0: + cmd = [RABBITMQ_CTL, 'set_policy', 'HA', + '^(?!amq\.).*', '{"ha-mode": "all"}'] subprocess.check_call(cmd) - cmd = [RABBITMQ_CTL, cluster_cmd, 'rabbit@%s' % node] - subprocess.check_call(cmd) - cmd = [RABBITMQ_CTL, 'start_app'] - subprocess.check_call(cmd) - utils.juju_log('INFO', 'Host clustered with %s.' % node) - return - except: - # continue to the next node - pass + return True + except: + log('Failed to cluster with %s.' % node) + # continue to the next node + num_tries += 1 + if num_tries > config('max-cluster-tries'): + log('Max tries number exhausted, exiting', level=ERROR) + raise - # error, no nodes available for clustering - utils.juju_log('ERROR', 'No nodes available for clustering') - sys.exit(1) + return False def break_cluster(): @@ -160,12 +191,11 @@ def break_cluster(): subprocess.check_call(cmd) cmd = [RABBITMQ_CTL, 'start_app'] subprocess.check_call(cmd) - utils.juju_log('INFO', 'Cluster successfully broken.') - return + log('Cluster successfully broken.') except: # error, no nodes available for clustering - utils.juju_log('ERROR', 'Error breaking rabbit cluster') - sys.exit(1) + log('Error breaking rabbit cluster', level=ERROR) + raise def set_node_name(name): @@ -173,7 +203,7 @@ def set_node_name(name): # rabbitmq.conf.d is not present on all releases, so use or create # rabbitmq-env.conf instead. if not os.path.isfile(ENV_CONF): - utils.juju_log('INFO', '%s does not exist, creating.' % ENV_CONF) + log('%s does not exist, creating.' % ENV_CONF) with open(ENV_CONF, 'wb') as out: out.write('RABBITMQ_NODENAME=%s\n' % name) return @@ -187,8 +217,8 @@ def set_node_name(name): out.append(line) if not f: out.append('RABBITMQ_NODENAME=%s\n' % name) - utils.juju_log('INFO', 'Updating %s, RABBITMQ_NODENAME=%s' % - (ENV_CONF, name)) + log('Updating %s, RABBITMQ_NODENAME=%s' % + (ENV_CONF, name)) with open(ENV_CONF, 'wb') as conf: conf.write(''.join(out)) @@ -221,24 +251,39 @@ def disable_plugin(plugin): ssl_key_file = "/etc/rabbitmq/rabbit-server-privkey.pem" ssl_cert_file = "/etc/rabbitmq/rabbit-server-cert.pem" +ssl_ca_file = "/etc/rabbitmq/rabbit-server-ca.pem" -def enable_ssl(ssl_key, ssl_cert, ssl_port): +def enable_ssl(ssl_key, ssl_cert, ssl_port, + ssl_ca=None, ssl_only=False, ssl_client=None): uid = pwd.getpwnam("root").pw_uid gid = grp.getgrnam("rabbitmq").gr_gid - with open(ssl_key_file, 'w') as key_file: - key_file.write(ssl_key) - os.chmod(ssl_key_file, 0640) - os.chown(ssl_key_file, uid, gid) - with open(ssl_cert_file, 'w') as cert_file: - cert_file.write(ssl_cert) - os.chmod(ssl_cert_file, 0640) - os.chown(ssl_cert_file, uid, gid) + + for contents, path in ( + (ssl_key, ssl_key_file), + (ssl_cert, ssl_cert_file), + (ssl_ca, ssl_ca_file)): + if not contents: + continue + with open(path, 'w') as fh: + fh.write(contents) + os.chmod(path, 0o640) + os.chown(path, uid, gid) + + data = { + "ssl_port": ssl_port, + "ssl_cert_file": ssl_cert_file, + "ssl_key_file": ssl_key_file, + "ssl_client": ssl_client, + "ssl_ca_file": "", + "ssl_only": ssl_only} + + if ssl_ca: + data["ssl_ca_file"] = ssl_ca_file + with open(RABBITMQ_CONF, 'w') as rmq_conf: - rmq_conf.write(utils.render_template(os.path.basename(RABBITMQ_CONF), - {"ssl_port": ssl_port, - "ssl_cert_file": ssl_cert_file, - "ssl_key_file": ssl_key_file})) + rmq_conf.write(render_template( + os.path.basename(RABBITMQ_CONF), data)) def execute(cmd, die=False, echo=False): @@ -272,27 +317,53 @@ def execute(cmd, die=False, echo=False): rc = p.returncode if die and rc != 0: - error_out("ERROR: command %s return non-zero.\n" % cmd) + log("command %s return non-zero." % cmd, level=ERROR) return (stdout, stderr, rc) -def synchronize_service_credentials(): - ''' - Broadcast service credentials to peers or consume those that have been - broadcasted by peer, depending on hook context. - ''' - if not os.path.isdir(LIB_PATH): - return - peers = cluster.peer_units() - if peers and not cluster.oldest_peer(peers): - utils.juju_log('INFO', 'Deferring action to oldest service unit.') - return +def get_rabbit_password_on_disk(username, password=None): + ''' Retrieve, generate or store a rabbit password for + the provided username on disk''' + _passwd_file = _named_passwd.format(service_name(), username) + _password = None + if os.path.exists(_passwd_file): + with open(_passwd_file, 'r') as passwd: + _password = passwd.read().strip() + else: + mkdir(os.path.dirname(_passwd_file), owner=RABBIT_USER, + group=RABBIT_USER, perms=0o775) + os.chmod(os.path.dirname(_passwd_file), 0o775) + _password = password or pwgen(length=64) + write_file(_passwd_file, _password, owner=RABBIT_USER, + group=RABBIT_USER, perms=0o660) + return _password - utils.juju_log('INFO', 'Synchronizing service passwords to all peers.') + +def migrate_passwords_to_peer_relation(): + '''Migrate any passwords storage on disk to cluster peer relation''' + for f in glob.glob('/var/lib/charm/{}/*.passwd'.format(service_name())): + _key = os.path.basename(f) + with open(f, 'r') as passwd: + _value = passwd.read().strip() + try: + peer_store(_key, _value) + os.unlink(f) + except ValueError: + # NOTE cluster relation not yet ready - skip for now + pass + + +def get_rabbit_password(username, password=None): + ''' Retrieve, generate or store a rabbit password for + the provided username using peer relation cluster''' + migrate_passwords_to_peer_relation() + _key = '{}.passwd'.format(username) try: - unison.sync_to_peers(peer_interface='cluster', - paths=[LIB_PATH], user=SSH_USER, - verbose=True) - except Exception: - # to skip files without perms safely - pass + _password = peer_retrieve(_key) + if _password is None: + _password = password or pwgen(length=64) + peer_store(_key, _password) + except ValueError: + # cluster relation is not yet started, use on-disk + _password = get_rabbit_password_on_disk(username, password) + return _password diff --git a/hooks/rabbitmq_server_relations.py b/hooks/rabbitmq_server_relations.py index daf0a8a8..76bfce52 100755 --- a/hooks/rabbitmq_server_relations.py +++ b/hooks/rabbitmq_server_relations.py @@ -1,30 +1,57 @@ #!/usr/bin/python - +import base64 import os import shutil import sys import subprocess import glob - import rabbit_utils as rabbit -import lib.utils as utils -import lib.cluster_utils as cluster -import lib.ceph_utils as ceph -import lib.openstack_common as openstack -import lib.unison as unison - -import _pythonpath -_ = _pythonpath - -from charmhelpers.core import hookenv -from charmhelpers.core.host import rsync -from charmhelpers.contrib.charmsupport.nrpe import NRPE -from charmhelpers.fetch import ( - apt_update, - add_source +from lib.utils import ( + chown, chmod, + is_newer, +) +from charmhelpers.contrib.hahelpers.cluster import ( + is_clustered, + eligible_leader ) +import charmhelpers.contrib.storage.linux.ceph as ceph +from charmhelpers.contrib.openstack.utils import save_script_rc + +from charmhelpers.fetch import ( + add_source, + apt_update, + apt_install) + +from charmhelpers.core.hookenv import ( + open_port, close_port, + log, ERROR, + relation_get, + relation_set, + relation_ids, + related_units, + service_name, + local_unit, + config, + unit_get, + is_relation_made, + Hooks, + UnregisteredHookError +) +from charmhelpers.core.host import ( + rsync, service_stop, service_restart +) +from charmhelpers.contrib.charmsupport.nrpe import NRPE +from charmhelpers.contrib.ssl.service import ServiceCA + +from charmhelpers.contrib.peerstorage import ( + peer_echo, + peer_store, + peer_retrieve +) + +hooks = Hooks() SERVICE_NAME = os.getenv('JUJU_UNIT_NAME').split('/')[0] POOL_NAME = SERVICE_NAME @@ -32,26 +59,17 @@ RABBIT_DIR = '/var/lib/rabbitmq' NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins' -def ensure_unison_rabbit_permissions(): - rabbit.execute("chmod g+wrx %s" % rabbit.LIB_PATH) - rabbit.execute("chmod g+wrx %s*.passwd" % rabbit.LIB_PATH) - - +@hooks.hook('install') def install(): pre_install_hooks() # NOTE(jamespage) install actually happens in config_changed hook def configure_amqp(username, vhost): - password_file = os.path.join(RABBIT_DIR, '%s.passwd' % username) - if os.path.exists(password_file): - password = open(password_file).read().strip() - else: - cmd = ['pwgen', '64', '1'] - password = subprocess.check_output(cmd).strip() - with open(password_file, 'wb') as out: - out.write(password) + # get and update service password + password = rabbit.get_rabbit_password(username) + # update vhost rabbit.create_vhost(vhost) rabbit.create_user(username, password) rabbit.grant_permissions(username, vhost) @@ -59,27 +77,26 @@ def configure_amqp(username, vhost): return password +@hooks.hook('amqp-relation-changed') def amqp_changed(relation_id=None, remote_unit=None): - if not cluster.eligible_leader('res_rabbitmq_vip'): - msg = 'amqp_changed(): Deferring amqp_changed to eligible_leader.' - utils.juju_log('INFO', msg) + if not eligible_leader('res_rabbitmq_vip'): + log('amqp_changed(): Deferring amqp_changed' + ' to eligible_leader.') return relation_settings = {} - settings = hookenv.relation_get(rid=relation_id, unit=remote_unit) + settings = relation_get(rid=relation_id, unit=remote_unit) - singleset = set([ - 'username', - 'vhost']) + singleset = set(['username', 'vhost']) if singleset.issubset(settings): if None in [settings['username'], settings['vhost']]: - utils.juju_log('INFO', 'amqp_changed(): Relation not ready.') + log('amqp_changed(): Relation not ready.') return - relation_settings['password'] = \ - configure_amqp(username=settings['username'], - vhost=settings['vhost']) + relation_settings['password'] = configure_amqp( + username=settings['username'], + vhost=settings['vhost']) else: queues = {} for k, v in settings.iteritems(): @@ -88,211 +105,228 @@ def amqp_changed(relation_id=None, remote_unit=None): if amqp not in queues: queues[amqp] = {} queues[amqp][x] = v - relation_settings = {} for amqp in queues: if singleset.issubset(queues[amqp]): - relation_settings['_'.join([amqp, 'password'])] = \ - configure_amqp(queues[amqp]['username'], - queues[amqp]['vhost']) + relation_settings[ + '_'.join([amqp, 'password'])] = configure_amqp( + queues[amqp]['username'], + queues[amqp]['vhost']) - relation_settings['hostname'] = utils.unit_get('private-address') + relation_settings['hostname'] = unit_get('private-address') + configure_client_ssl(relation_settings) - if cluster.is_clustered(): + if is_clustered(): relation_settings['clustered'] = 'true' - if utils.is_relation_made('ha'): + if is_relation_made('ha'): # active/passive settings - relation_settings['vip'] = utils.config_get('vip') + relation_settings['vip'] = config('vip') + # or ha-vip-only to support active/active, but + # accessed via a VIP for older clients. + if config('ha-vip-only') is True: + relation_settings['ha-vip-only'] = 'true' if relation_id: relation_settings['rid'] = relation_id - utils.relation_set(**relation_settings) - # sync new creds to all peers - rabbit.synchronize_service_credentials() + # set if need HA queues or not + if rabbit.compare_version('3.0.1') < 0: + relation_settings['ha_queues'] = True + relation_set(relation_settings=relation_settings) +@hooks.hook('cluster-relation-joined') def cluster_joined(): - unison.ssh_authorized_peers(user=rabbit.SSH_USER, - group='rabbit', - peer_interface='cluster', - ensure_local_user=True) - if utils.is_relation_made('ha'): - utils.juju_log('INFO', - 'hacluster relation is present, skipping native ' - 'rabbitmq cluster config.') + if is_relation_made('ha') and \ + config('ha-vip-only') is False: + log('hacluster relation is present, skipping native ' + 'rabbitmq cluster config.') return - l_unit_no = os.getenv('JUJU_UNIT_NAME').split('/')[1] - r_unit_no = os.getenv('JUJU_REMOTE_UNIT').split('/')[1] - if l_unit_no > r_unit_no: - utils.juju_log('INFO', 'cluster_joined: Relation greater.') + + if is_newer(): + log('cluster_joined: Relation greater.') return + rabbit.COOKIE_PATH = '/var/lib/rabbitmq/.erlang.cookie' if not os.path.isfile(rabbit.COOKIE_PATH): - utils.juju_log('ERROR', 'erlang cookie missing from %s' % - rabbit.COOKIE_PATH) + log('erlang cookie missing from %s' % rabbit.COOKIE_PATH, + level=ERROR) return cookie = open(rabbit.COOKIE_PATH, 'r').read().strip() - - # add parent host to the relation - local_hostname = subprocess.check_output(['hostname']).strip() - utils.relation_set(cookie=cookie, host=local_hostname) + peer_store('cookie', cookie) +@hooks.hook('cluster-relation-changed') def cluster_changed(): - unison.ssh_authorized_peers(user=rabbit.SSH_USER, - group='rabbit', - peer_interface='cluster', - ensure_local_user=True) - rabbit.synchronize_service_credentials() - - if utils.is_relation_made('ha'): - utils.juju_log('INFO', - 'hacluster relation is present, skipping native ' - 'rabbitmq cluster config.') - return - l_unit_no = os.getenv('JUJU_UNIT_NAME').split('/')[1] - r_unit_no = os.getenv('JUJU_REMOTE_UNIT').split('/')[1] - if l_unit_no < r_unit_no: - utils.juju_log('INFO', 'cluster_joined: Relation lesser.') - return - - cookie = utils.relation_get('cookie') - if cookie is None: - utils.juju_log('INFO', - 'cluster_joined: cookie not yet set.') + rdata = relation_get() + if 'cookie' not in rdata: + log('cluster_joined: cookie not yet set.') return + # sync passwords + peer_echo() + # sync cookie + cookie = peer_retrieve('cookie') if open(rabbit.COOKIE_PATH, 'r').read().strip() == cookie: - utils.juju_log('INFO', 'Cookie already synchronized with peer.') + log('Cookie already synchronized with peer.') else: - utils.juju_log('INFO', 'Synchronizing erlang cookie from peer.') + log('Synchronizing erlang cookie from peer.') rabbit.service('stop') with open(rabbit.COOKIE_PATH, 'wb') as out: out.write(cookie) rabbit.service('start') - # cluster with other nodes - rabbit.cluster_with() - - -def cluster_departed(): - if utils.is_relation_made('ha'): - utils.juju_log('INFO', - 'hacluster relation is present, skipping native ' - 'rabbitmq cluster config.') + if is_relation_made('ha') and \ + config('ha-vip-only') is False: + log('hacluster relation is present, skipping native ' + 'rabbitmq cluster config.') return - l_unit_no = os.getenv('JUJU_UNIT_NAME').split('/')[1] - r_unit_no = os.getenv('JUJU_REMOTE_UNIT').split('/')[1] - if l_unit_no < r_unit_no: - utils.juju_log('INFO', 'cluster_joined: Relation lesser.') + + # cluster with node + if is_newer(): + if rabbit.cluster_with(): + # resync nrpe user after clustering + update_nrpe_checks() + + +@hooks.hook('cluster-relation-departed') +def cluster_departed(): + if is_relation_made('ha') and \ + config('ha-vip-only') is False: + log('hacluster relation is present, skipping native ' + 'rabbitmq cluster config.') + return + if not is_newer(): + log('cluster_joined: Relation lesser.') return rabbit.break_cluster() +@hooks.hook('ha-relation-joined') def ha_joined(): - corosync_bindiface = utils.config_get('ha-bindiface') - corosync_mcastport = utils.config_get('ha-mcastport') - vip = utils.config_get('vip') - vip_iface = utils.config_get('vip_iface') - vip_cidr = utils.config_get('vip_cidr') - rbd_name = utils.config_get('rbd-name') + corosync_bindiface = config('ha-bindiface') + corosync_mcastport = config('ha-mcastport') + vip = config('vip') + vip_iface = config('vip_iface') + vip_cidr = config('vip_cidr') + rbd_name = config('rbd-name') + vip_only = config('ha-vip-only') if None in [corosync_bindiface, corosync_mcastport, vip, vip_iface, - vip_cidr, rbd_name]: - utils.juju_log('ERROR', 'Insufficient configuration data to ' - 'configure hacluster.') + vip_cidr, rbd_name] and vip_only is False: + log('Insufficient configuration data to configure hacluster.', + level=ERROR) + sys.exit(1) + elif None in [corosync_bindiface, corosync_mcastport, vip, vip_iface, + vip_cidr] and vip_only is True: + log('Insufficient configuration data to configure VIP-only hacluster.', + level=ERROR) sys.exit(1) - if not utils.is_relation_made('ceph', 'auth'): - utils.juju_log('INFO', - 'ha_joined: No ceph relation yet, deferring.') + if not is_relation_made('ceph', 'auth') and vip_only is False: + log('ha_joined: No ceph relation yet, deferring.') return name = '%s@localhost' % SERVICE_NAME - if rabbit.get_node_name() != name: - utils.juju_log('INFO', 'Stopping rabbitmq-server.') - utils.stop('rabbitmq-server') + if rabbit.get_node_name() != name and vip_only is False: + log('Stopping rabbitmq-server.') + service_stop('rabbitmq-server') rabbit.set_node_name('%s@localhost' % SERVICE_NAME) else: - utils.juju_log('INFO', 'Node name already set to %s.' % name) + log('Node name already set to %s.' % name) relation_settings = {} relation_settings['corosync_bindiface'] = corosync_bindiface relation_settings['corosync_mcastport'] = corosync_mcastport - relation_settings['resources'] = { - 'res_rabbitmq_rbd': 'ocf:ceph:rbd', - 'res_rabbitmq_fs': 'ocf:heartbeat:Filesystem', - 'res_rabbitmq_vip': 'ocf:heartbeat:IPaddr2', - 'res_rabbitmq-server': 'lsb:rabbitmq-server', - } + if vip_only is True: + relation_settings['resources'] = { + 'res_rabbitmq_vip': 'ocf:heartbeat:IPaddr2', + } + relation_settings['resource_params'] = { + 'res_rabbitmq_vip': 'params ip="%s" cidr_netmask="%s" nic="%s"' % + (vip, vip_cidr, vip_iface), + } + else: + relation_settings['resources'] = { + 'res_rabbitmq_rbd': 'ocf:ceph:rbd', + 'res_rabbitmq_fs': 'ocf:heartbeat:Filesystem', + 'res_rabbitmq_vip': 'ocf:heartbeat:IPaddr2', + 'res_rabbitmq-server': 'lsb:rabbitmq-server', + } - relation_settings['resource_params'] = { - 'res_rabbitmq_rbd': 'params name="%s" pool="%s" user="%s" ' - 'secret="%s"' % - (rbd_name, POOL_NAME, - SERVICE_NAME, ceph.keyfile_path(SERVICE_NAME)), - 'res_rabbitmq_fs': 'params device="/dev/rbd/%s/%s" directory="%s" ' - 'fstype="ext4" op start start-delay="10s"' % - (POOL_NAME, rbd_name, RABBIT_DIR), - 'res_rabbitmq_vip': 'params ip="%s" cidr_netmask="%s" nic="%s"' % - (vip, vip_cidr, vip_iface), - 'res_rabbitmq-server': 'op start start-delay="5s" ' - 'op monitor interval="5s"', - } + relation_settings['resource_params'] = { + 'res_rabbitmq_rbd': 'params name="%s" pool="%s" user="%s" ' + 'secret="%s"' % + (rbd_name, POOL_NAME, + SERVICE_NAME, ceph._keyfile_path( + SERVICE_NAME)), + 'res_rabbitmq_fs': 'params device="/dev/rbd/%s/%s" directory="%s" ' + 'fstype="ext4" op start start-delay="10s"' % + (POOL_NAME, rbd_name, RABBIT_DIR), + 'res_rabbitmq_vip': 'params ip="%s" cidr_netmask="%s" nic="%s"' % + (vip, vip_cidr, vip_iface), + 'res_rabbitmq-server': 'op start start-delay="5s" ' + 'op monitor interval="5s"', + } - relation_settings['groups'] = { - 'grp_rabbitmq': 'res_rabbitmq_rbd res_rabbitmq_fs res_rabbitmq_vip ' - 'res_rabbitmq-server', - } + relation_settings['groups'] = { + 'grp_rabbitmq': + 'res_rabbitmq_rbd res_rabbitmq_fs res_rabbitmq_vip ' + 'res_rabbitmq-server', + } - for rel_id in utils.relation_ids('ha'): - utils.relation_set(rid=rel_id, **relation_settings) + for rel_id in relation_ids('ha'): + relation_set(relation_id=rel_id, relation_settings=relation_settings) env_vars = { 'OPENSTACK_PORT_EPMD': 4369, - 'OPENSTACK_PORT_MCASTPORT': utils.config_get('ha-mcastport'), + 'OPENSTACK_PORT_MCASTPORT': config('ha-mcastport'), } - openstack.save_script_rc(**env_vars) + save_script_rc(**env_vars) +@hooks.hook('ha-relation-changed') def ha_changed(): - if not cluster.is_clustered(): + if not is_clustered(): return - vip = utils.config_get('vip') - utils.juju_log('INFO', 'ha_changed(): We are now HA clustered. ' - 'Advertising our VIP (%s) to all AMQP clients.' % - vip) + vip = config('vip') + log('ha_changed(): We are now HA clustered. ' + 'Advertising our VIP (%s) to all AMQP clients.' % + vip) # need to re-authenticate all clients since node-name changed. - for rid in utils.relation_ids('amqp'): - for unit in utils.relation_list(rid): + for rid in relation_ids('amqp'): + for unit in related_units(rid): amqp_changed(relation_id=rid, remote_unit=unit) +@hooks.hook('ceph-relation-joined') def ceph_joined(): - utils.juju_log('INFO', 'Start Ceph Relation Joined') + log('Start Ceph Relation Joined') + #NOTE fixup + #utils.configure_source() ceph.install() - utils.juju_log('INFO', 'Finish Ceph Relation Joined') + log('Finish Ceph Relation Joined') +@hooks.hook('ceph-relation-changed') def ceph_changed(): - utils.juju_log('INFO', 'Start Ceph Relation Changed') - auth = utils.relation_get('auth') - key = utils.relation_get('key') - use_syslog = str(utils.config_get('use-syslog')).lower() + log('Start Ceph Relation Changed') + auth = relation_get('auth') + key = relation_get('key') + use_syslog = str(config('use-syslog')).lower() if None in [auth, key]: - utils.juju_log('INFO', 'Missing key or auth in relation') + log('Missing key or auth in relation') sys.exit(0) ceph.configure(service=SERVICE_NAME, key=key, auth=auth, use_syslog=use_syslog) - if cluster.eligible_leader('res_rabbitmq_vip'): - rbd_img = utils.config_get('rbd-name') - rbd_size = utils.config_get('rbd-size') + if eligible_leader('res_rabbitmq_vip'): + rbd_img = config('rbd-name') + rbd_size = config('rbd-size') sizemb = int(rbd_size.split('G')[0]) * 1024 blk_device = '/dev/rbd/%s/%s' % (POOL_NAME, rbd_img) - rbd_pool_rep_count = utils.config_get('ceph-osd-replication-count') + rbd_pool_rep_count = config('ceph-osd-replication-count') ceph.ensure_ceph_storage(service=SERVICE_NAME, pool=POOL_NAME, rbd_img=rbd_img, sizemb=sizemb, fstype='ext4', mount_point=RABBIT_DIR, @@ -300,37 +334,33 @@ def ceph_changed(): system_services=['rabbitmq-server'], rbd_pool_replicas=rbd_pool_rep_count) else: - utils.juju_log('INFO', - 'This is not the peer leader. Not configuring RBD.') - utils.juju_log('INFO', 'Stopping rabbitmq-server.') - utils.stop('rabbitmq-server') + log('This is not the peer leader. Not configuring RBD.') + log('Stopping rabbitmq-server.') + service_stop('rabbitmq-server') # If 'ha' relation has been made before the 'ceph' relation # it is important to make sure the ha-relation data is being # sent. - if utils.is_relation_made('ha'): - utils.juju_log('INFO', '*ha* relation exists. Triggering ha_joined()') + if is_relation_made('ha'): + log('*ha* relation exists. Triggering ha_joined()') ha_joined() else: - utils.juju_log('INFO', '*ha* relation does not exist.') - utils.juju_log('INFO', 'Finish Ceph Relation Changed') + log('*ha* relation does not exist.') + log('Finish Ceph Relation Changed') +@hooks.hook('nrpe-external-master-relation-changed') def update_nrpe_checks(): if os.path.isdir(NAGIOS_PLUGINS): rsync(os.path.join(os.getenv('CHARM_DIR'), 'scripts', 'check_rabbitmq.py'), os.path.join(NAGIOS_PLUGINS, 'check_rabbitmq.py')) - user = 'naigos' - vhost = 'nagios' - password_file = os.path.join(RABBIT_DIR, '%s.passwd' % user) - if os.path.exists(password_file): - password = open(password_file).read().strip() - else: - cmd = ['pwgen', '64', '1'] - password = subprocess.check_output(cmd).strip() - with open(password_file, 'wb') as out: - out.write(password) + + # create unique user and vhost for each unit + current_unit = local_unit().replace('/', '-') + user = 'nagios-%s' % current_unit + vhost = 'nagios-%s' % current_unit + password = rabbit.get_rabbit_password(user) rabbit.create_vhost(vhost) rabbit.create_user(user, password) @@ -346,63 +376,156 @@ def update_nrpe_checks(): nrpe_compat.write() +@hooks.hook('upgrade-charm') def upgrade_charm(): pre_install_hooks() + add_source(config('source'), config('key')) + apt_update(fatal=True) + # Ensure older passwd files in /var/lib/juju are moved to - # /var/lib/rabbitmq which will end up replicated if clustered. + # /var/lib/rabbitmq which will end up replicated if clustered for f in [f for f in os.listdir('/var/lib/juju') if os.path.isfile(os.path.join('/var/lib/juju', f))]: if f.endswith('.passwd'): s = os.path.join('/var/lib/juju', f) - d = os.path.join('/var/lib/rabbitmq', f) - utils.juju_log('INFO', - 'upgrade_charm: Migrating stored passwd' - ' from %s to %s.' % (s, d)) + d = os.path.join('/var/lib/charm/{}'.format(service_name()), f) + + log('upgrade_charm: Migrating stored passwd' + ' from %s to %s.' % (s, d)) shutil.move(s, d) + rabbit.migrate_passwords_to_peer_relation() + + # explicitly update buggy file name naigos.passwd + old = os.path.join('var/lib/rabbitmq', 'naigos.passwd') + if os.path.isfile(old): + new = os.path.join('var/lib/rabbitmq', 'nagios.passwd') + shutil.move(old, new) + MAN_PLUGIN = 'rabbitmq_management' +def configure_client_ssl(relation_data): + """Configure client with ssl + """ + ssl_mode, external_ca = _get_ssl_mode() + if ssl_mode == 'off': + return + relation_data['ssl_port'] = config('ssl_port') + if external_ca: + if config('ssl_ca'): + relation_data['ssl_ca'] = base64.b64encode( + config('ssl_ca')) + return + ca = ServiceCA.get_ca() + relation_data['ssl_ca'] = base64.b64encode(ca.get_ca_bundle()) + + +def _get_ssl_mode(): + ssl_mode = config('ssl') + external_ca = False + # Legacy config boolean option + ssl_on = config('ssl_enabled') + if ssl_mode == 'off' and ssl_on is False: + ssl_mode = 'off' + elif ssl_mode == 'off' and ssl_on: + ssl_mode = 'on' + ssl_key = config('ssl_key') + ssl_cert = config('ssl_cert') + if all((ssl_key, ssl_cert)): + external_ca = True + return ssl_mode, external_ca + + +def _convert_from_base64(v): + # Rabbit originally supported pem encoded key/cert in config, play + # nice on upgrades as we now expect base64 encoded key/cert/ca. + if not v: + return v + if v.startswith('-----BEGIN'): + return v + try: + return base64.b64decode(v) + except TypeError: + return v + + +def reconfigure_client_ssl(ssl_enabled=False): + ssl_config_keys = set(('ssl_key', 'ssl_cert', 'ssl_ca')) + for rid in relation_ids('amqp'): + rdata = relation_get(rid=rid, unit=os.environ['JUJU_UNIT_NAME']) + if not ssl_enabled and ssl_config_keys.intersection(rdata): + # No clean way to remove entirely, but blank them. + relation_set(relation_id=rid, ssl_key='', ssl_cert='', ssl_ca='') + elif ssl_enabled and not ssl_config_keys.intersection(rdata): + configure_client_ssl(rdata) + relation_set(relation_id=rid, **rdata) + + +def configure_rabbit_ssl(): + """ + The legacy config support adds some additional complications. + + ssl_enabled = True, ssl = off -> ssl enabled + ssl_enabled = False, ssl = on -> ssl enabled + """ + ssl_mode, external_ca = _get_ssl_mode() + + if ssl_mode == 'off': + if os.path.exists(rabbit.RABBITMQ_CONF): + os.remove(rabbit.RABBITMQ_CONF) + close_port(config('ssl_port')) + reconfigure_client_ssl() + return + ssl_key = _convert_from_base64(config('ssl_key')) + ssl_cert = _convert_from_base64(config('ssl_cert')) + ssl_ca = _convert_from_base64(config('ssl_ca')) + ssl_port = config('ssl_port') + + # If external managed certs then we need all the fields. + if (ssl_mode in ('on', 'only') and any((ssl_key, ssl_cert)) and + not all((ssl_key, ssl_cert))): + log('If ssl_key or ssl_cert are specified both are required.', + level=ERROR) + sys.exit(1) + + if not external_ca: + ssl_cert, ssl_key, ssl_ca = ServiceCA.get_service_cert() + + rabbit.enable_ssl( + ssl_key, ssl_cert, ssl_port, ssl_ca, + ssl_only=(ssl_mode == "only"), ssl_client=False) + reconfigure_client_ssl(True) + open_port(ssl_port) + + +@hooks.hook('config-changed') def config_changed(): # Add archive source if provided - add_source(utils.config_get('source'), utils.config_get('key')) + add_source(config('source'), config('key')) apt_update(fatal=True) # Install packages to ensure any changes to source # result in an upgrade if applicable. - utils.install(*rabbit.PACKAGES) + apt_install(rabbit.PACKAGES, fatal=True) - utils.expose(5672) + open_port(5672) - unison.ensure_user(user=rabbit.SSH_USER, group=rabbit.RABBIT_USER) - ensure_unison_rabbit_permissions() + chown(RABBIT_DIR, rabbit.RABBIT_USER, rabbit.RABBIT_USER) + chmod(RABBIT_DIR, 0o775) - if utils.config_get('management_plugin') is True: + if config('management_plugin') is True: rabbit.enable_plugin(MAN_PLUGIN) - utils.open_port(55672) + open_port(55672) else: rabbit.disable_plugin(MAN_PLUGIN) - utils.close_port(55672) + close_port(55672) - if utils.config_get('ssl_enabled') is True: - ssl_key = utils.config_get('ssl_key') - ssl_cert = utils.config_get('ssl_cert') - ssl_port = utils.config_get('ssl_port') - if None in [ssl_key, ssl_cert, ssl_port]: - utils.juju_log('ERROR', - 'Please provide ssl_key, ssl_cert and ssl_port' - ' config when enabling SSL support') - sys.exit(1) - else: - rabbit.enable_ssl(ssl_key, ssl_cert, ssl_port) - utils.open_port(ssl_port) - else: - if os.path.exists(rabbit.RABBITMQ_CONF): - os.remove(rabbit.RABBITMQ_CONF) - utils.close_port(utils.config_get('ssl_port')) + configure_rabbit_ssl() - if cluster.eligible_leader('res_rabbitmq_vip'): - utils.restart('rabbitmq-server') + if eligible_leader('res_rabbitmq_vip') or \ + config('ha-vip-only') is True: + service_restart('rabbitmq-server') update_nrpe_checks() @@ -412,19 +535,9 @@ def pre_install_hooks(): if os.path.isfile(f) and os.access(f, os.X_OK): subprocess.check_call(['sh', '-c', f]) -hooks = { - 'install': install, - 'amqp-relation-changed': amqp_changed, - 'cluster-relation-joined': cluster_joined, - 'cluster-relation-changed': cluster_changed, - 'cluster-relation-departed': cluster_departed, - 'ha-relation-joined': ha_joined, - 'ha-relation-changed': ha_changed, - 'ceph-relation-joined': ceph_joined, - 'ceph-relation-changed': ceph_changed, - 'upgrade-charm': upgrade_charm, - 'config-changed': config_changed, - 'nrpe-external-master-relation-changed': update_nrpe_checks -} -utils.do_hooks(hooks) +if __name__ == '__main__': + try: + hooks.execute(sys.argv) + except UnregisteredHookError as e: + log('Unknown hook {} - skipping.'.format(e)) diff --git a/hooks/start b/hooks/start new file mode 120000 index 00000000..eecaaa74 --- /dev/null +++ b/hooks/start @@ -0,0 +1 @@ +rabbitmq_server_relations.py \ No newline at end of file diff --git a/hooks/stop b/hooks/stop new file mode 120000 index 00000000..eecaaa74 --- /dev/null +++ b/hooks/stop @@ -0,0 +1 @@ +rabbitmq_server_relations.py \ No newline at end of file diff --git a/lib/charmhelpers-0.1.2.egg-info b/lib/charmhelpers-0.1.2.egg-info deleted file mode 100644 index 6d99dbe2..00000000 --- a/lib/charmhelpers-0.1.2.egg-info +++ /dev/null @@ -1,18 +0,0 @@ -Metadata-Version: 1.0 -Name: charmhelpers -Version: 0.1.2 -Summary: UNKNOWN -Home-page: https://code.launchpad.net/charm-helpers -Author: Ubuntu Developers -Author-email: ubuntu-devel-discuss@lists.ubuntu.com -License: Affero GNU Public License v3 -Description: ============ - CharmHelpers - ============ - - CharmHelpers provides an opinionated set of tools for building Juju - charms that work together. In addition to basic tasks like interact- - ing with the charm environment and the machine it runs on, it also - helps keep you build hooks and establish relations effortlessly. - -Platform: UNKNOWN diff --git a/lib/charmhelpers/cli/README.rst b/lib/charmhelpers/cli/README.rst deleted file mode 100644 index f7901c09..00000000 --- a/lib/charmhelpers/cli/README.rst +++ /dev/null @@ -1,57 +0,0 @@ -========== -Commandant -========== - ------------------------------------------------------ -Automatic command-line interfaces to Python functions ------------------------------------------------------ - -One of the benefits of ``libvirt`` is the uniformity of the interface: the C API (as well as the bindings in other languages) is a set of functions that accept parameters that are nearly identical to the command-line arguments. If you run ``virsh``, you get an interactive command prompt that supports all of the same commands that your shell scripts use as ``virsh`` subcommands. - -Command execution and stdio manipulation is the greatest common factor across all development systems in the POSIX environment. By exposing your functions as commands that manipulate streams of text, you can make life easier for all the Ruby and Erlang and Go programmers in your life. - -Goals -===== - -* Single decorator to expose a function as a command. - * now two decorators - one "automatic" and one that allows authors to manipulate the arguments for fine-grained control.(MW) -* Automatic analysis of function signature through ``inspect.getargspec()`` -* Command argument parser built automatically with ``argparse`` -* Interactive interpreter loop object made with ``Cmd`` -* Options to output structured return value data via ``pprint``, ``yaml`` or ``json`` dumps. - -Other Important Features that need writing ------------------------------------------- - -* Help and Usage documentation can be automatically generated, but it will be important to let users override this behaviour -* The decorator should allow specifying further parameters to the parser's add_argument() calls, to specify types or to make arguments behave as boolean flags, etc. - - Filename arguments are important, as good practice is for functions to accept file objects as parameters. - - choices arguments help to limit bad input before the function is called -* Some automatic behaviour could make for better defaults, once the user can override them. - - We could automatically detect arguments that default to False or True, and automatically support --no-foo for foo=True. - - We could automatically support hyphens as alternates for underscores - - Arguments defaulting to sequence types could support the ``append`` action. - - ------------------------------------------------------ -Implementing subcommands ------------------------------------------------------ - -(WIP) - -So as to avoid dependencies on the cli module, subcommands should be defined separately from their implementations. The recommmendation would be to place definitions into separate modules near the implementations which they expose. - -Some examples:: - - from charmhelpers.cli import CommandLine - from charmhelpers.payload import execd - from charmhelpers.foo import bar - - cli = CommandLine() - - cli.subcommand(execd.execd_run) - - @cli.subcommand_builder("bar", help="Bar baz qux") - def barcmd_builder(subparser): - subparser.add_argument('argument1', help="yackety") - return bar diff --git a/lib/charmhelpers/cli/__init__.py b/lib/charmhelpers/cli/__init__.py deleted file mode 100644 index def53983..00000000 --- a/lib/charmhelpers/cli/__init__.py +++ /dev/null @@ -1,147 +0,0 @@ -import inspect -import itertools -import argparse -import sys - - -class OutputFormatter(object): - def __init__(self, outfile=sys.stdout): - self.formats = ( - "raw", - "json", - "py", - "yaml", - "csv", - "tab", - ) - self.outfile = outfile - - def add_arguments(self, argument_parser): - formatgroup = argument_parser.add_mutually_exclusive_group() - choices = self.supported_formats - formatgroup.add_argument("--format", metavar='FMT', - help="Select output format for returned data, " - "where FMT is one of: {}".format(choices), - choices=choices, default='raw') - for fmt in self.formats: - fmtfunc = getattr(self, fmt) - formatgroup.add_argument("-{}".format(fmt[0]), - "--{}".format(fmt), action='store_const', - const=fmt, dest='format', - help=fmtfunc.__doc__) - - @property - def supported_formats(self): - return self.formats - - def raw(self, output): - """Output data as raw string (default)""" - self.outfile.write(str(output)) - - def py(self, output): - """Output data as a nicely-formatted python data structure""" - import pprint - pprint.pprint(output, stream=self.outfile) - - def json(self, output): - """Output data in JSON format""" - import json - json.dump(output, self.outfile) - - def yaml(self, output): - """Output data in YAML format""" - import yaml - yaml.safe_dump(output, self.outfile) - - def csv(self, output): - """Output data as excel-compatible CSV""" - import csv - csvwriter = csv.writer(self.outfile) - csvwriter.writerows(output) - - def tab(self, output): - """Output data in excel-compatible tab-delimited format""" - import csv - csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab) - csvwriter.writerows(output) - - def format_output(self, output, fmt='raw'): - fmtfunc = getattr(self, fmt) - fmtfunc(output) - - -class CommandLine(object): - argument_parser = None - subparsers = None - formatter = None - - def __init__(self): - if not self.argument_parser: - self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks') - if not self.formatter: - self.formatter = OutputFormatter() - self.formatter.add_arguments(self.argument_parser) - if not self.subparsers: - self.subparsers = self.argument_parser.add_subparsers(help='Commands') - - def subcommand(self, command_name=None): - """ - Decorate a function as a subcommand. Use its arguments as the - command-line arguments""" - def wrapper(decorated): - cmd_name = command_name or decorated.__name__ - subparser = self.subparsers.add_parser(cmd_name, - description=decorated.__doc__) - for args, kwargs in describe_arguments(decorated): - subparser.add_argument(*args, **kwargs) - subparser.set_defaults(func=decorated) - return decorated - return wrapper - - def subcommand_builder(self, command_name, description=None): - """ - Decorate a function that builds a subcommand. Builders should accept a - single argument (the subparser instance) and return the function to be - run as the command.""" - def wrapper(decorated): - subparser = self.subparsers.add_parser(command_name) - func = decorated(subparser) - subparser.set_defaults(func=func) - subparser.description = description or func.__doc__ - return wrapper - - def run(self): - "Run cli, processing arguments and executing subcommands." - arguments = self.argument_parser.parse_args() - argspec = inspect.getargspec(arguments.func) - vargs = [] - kwargs = {} - if argspec.varargs: - vargs = getattr(arguments, argspec.varargs) - for arg in argspec.args: - kwargs[arg] = getattr(arguments, arg) - self.formatter.format_output(arguments.func(*vargs, **kwargs), arguments.format) - - -cmdline = CommandLine() - - -def describe_arguments(func): - """ - Analyze a function's signature and return a data structure suitable for - passing in as arguments to an argparse parser's add_argument() method.""" - - argspec = inspect.getargspec(func) - # we should probably raise an exception somewhere if func includes **kwargs - if argspec.defaults: - positional_args = argspec.args[:-len(argspec.defaults)] - keyword_names = argspec.args[-len(argspec.defaults):] - for arg, default in itertools.izip(keyword_names, argspec.defaults): - yield ('--{}'.format(arg),), {'default': default} - else: - positional_args = argspec.args - - for arg in positional_args: - yield (arg,), {} - if argspec.varargs: - yield (argspec.varargs,), {'nargs': '*'} diff --git a/lib/charmhelpers/cli/commands.py b/lib/charmhelpers/cli/commands.py deleted file mode 100644 index 5aa79705..00000000 --- a/lib/charmhelpers/cli/commands.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import CommandLine -import host diff --git a/lib/charmhelpers/cli/host.py b/lib/charmhelpers/cli/host.py deleted file mode 100644 index bada5244..00000000 --- a/lib/charmhelpers/cli/host.py +++ /dev/null @@ -1,15 +0,0 @@ -from . import cmdline -from charmhelpers.core import host - - -@cmdline.subcommand() -def mounts(): - "List mounts" - return host.mounts() - - -@cmdline.subcommand_builder('service', description="Control system services") -def service(subparser): - subparser.add_argument("action", help="The action to perform (start, stop, etc...)") - subparser.add_argument("service_name", help="Name of the service to control") - return host.service diff --git a/lib/charmhelpers/contrib/ansible/__init__.py b/lib/charmhelpers/contrib/ansible/__init__.py deleted file mode 100644 index f72bae36..00000000 --- a/lib/charmhelpers/contrib/ansible/__init__.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright 2013 Canonical Ltd. -# -# Authors: -# Charm Helpers Developers -"""Charm Helpers ansible - declare the state of your machines. - -This helper enables you to declare your machine state, rather than -program it procedurally (and have to test each change to your procedures). -Your install hook can be as simple as: - -{{{ -import charmhelpers.contrib.ansible - - -def install(): - charmhelpers.contrib.ansible.install_ansible_support() - charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml') -}}} - -and won't need to change (nor will its tests) when you change the machine -state. - -All of your juju config and relation-data are available as template -variables within your playbooks and templates. An install playbook looks -something like: - -{{{ ---- -- hosts: localhost - user: root - - tasks: - - name: Add private repositories. - template: - src: ../templates/private-repositories.list.jinja2 - dest: /etc/apt/sources.list.d/private.list - - - name: Update the cache. - apt: update_cache=yes - - - name: Install dependencies. - apt: pkg={{ item }} - with_items: - - python-mimeparse - - python-webob - - sunburnt - - - name: Setup groups. - group: name={{ item.name }} gid={{ item.gid }} - with_items: - - { name: 'deploy_user', gid: 1800 } - - { name: 'service_user', gid: 1500 } - - ... -}}} - -Read more online about playbooks[1] and standard ansible modules[2]. - -[1] http://www.ansibleworks.com/docs/playbooks.html -[2] http://www.ansibleworks.com/docs/modules.html -""" -import os -import subprocess - -import charmhelpers.contrib.templating.contexts -import charmhelpers.core.host -import charmhelpers.core.hookenv -import charmhelpers.fetch - - -charm_dir = os.environ.get('CHARM_DIR', '') -ansible_hosts_path = '/etc/ansible/hosts' -# Ansible will automatically include any vars in the following -# file in its inventory when run locally. -ansible_vars_path = '/etc/ansible/host_vars/localhost' - - -def install_ansible_support(from_ppa=True): - """Installs the ansible package. - - By default it is installed from the PPA [1] linked from - the ansible website [2]. - - [1] https://launchpad.net/~rquillo/+archive/ansible - [2] http://www.ansibleworks.com/docs/gettingstarted.html#ubuntu-and-debian - - If from_ppa is false, you must ensure that the package is available - from a configured repository. - """ - if from_ppa: - charmhelpers.fetch.add_source('ppa:rquillo/ansible') - charmhelpers.fetch.apt_update(fatal=True) - charmhelpers.fetch.apt_install('ansible') - with open(ansible_hosts_path, 'w+') as hosts_file: - hosts_file.write('localhost ansible_connection=local') - - -def apply_playbook(playbook, tags=None): - tags = tags or [] - tags = ",".join(tags) - charmhelpers.contrib.templating.contexts.juju_state_to_yaml( - ansible_vars_path, namespace_separator='__', - allow_hyphens_in_keys=False) - call = [ - 'ansible-playbook', - '-c', - 'local', - playbook, - ] - if tags: - call.extend(['--tags', '{}'.format(tags)]) - subprocess.check_call(call) - - -class AnsibleHooks(charmhelpers.core.hookenv.Hooks): - """Run a playbook with the hook-name as the tag. - - This helper builds on the standard hookenv.Hooks helper, - but additionally runs the playbook with the hook-name specified - using --tags (ie. running all the tasks tagged with the hook-name). - - Example: - hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml') - - # All the tasks within my_machine_state.yaml tagged with 'install' - # will be run automatically after do_custom_work() - @hooks.hook() - def install(): - do_custom_work() - - # For most of your hooks, you won't need to do anything other - # than run the tagged tasks for the hook: - @hooks.hook('config-changed', 'start', 'stop') - def just_use_playbook(): - pass - - # As a convenience, you can avoid the above noop function by specifying - # the hooks which are handled by ansible-only and they'll be registered - # for you: - # hooks = AnsibleHooks( - # 'playbooks/my_machine_state.yaml', - # default_hooks=['config-changed', 'start', 'stop']) - - if __name__ == "__main__": - # execute a hook based on the name the program is called by - hooks.execute(sys.argv) - """ - - def __init__(self, playbook_path, default_hooks=None): - """Register any hooks handled by ansible.""" - super(AnsibleHooks, self).__init__() - - self.playbook_path = playbook_path - - default_hooks = default_hooks or [] - noop = lambda *args, **kwargs: None - for hook in default_hooks: - self.register(hook, noop) - - def execute(self, args): - """Execute the hook followed by the playbook using the hook as tag.""" - super(AnsibleHooks, self).execute(args) - hook_name = os.path.basename(args[0]) - charmhelpers.contrib.ansible.apply_playbook( - self.playbook_path, tags=[hook_name]) diff --git a/lib/charmhelpers/contrib/charmhelpers/IMPORT b/lib/charmhelpers/contrib/charmhelpers/IMPORT deleted file mode 100644 index d41cb041..00000000 --- a/lib/charmhelpers/contrib/charmhelpers/IMPORT +++ /dev/null @@ -1,4 +0,0 @@ -Source lp:charm-tools/trunk - -charm-tools/helpers/python/charmhelpers/__init__.py -> charmhelpers/charmhelpers/contrib/charmhelpers/__init__.py -charm-tools/helpers/python/charmhelpers/tests/test_charmhelpers.py -> charmhelpers/tests/contrib/charmhelpers/test_charmhelpers.py diff --git a/lib/charmhelpers/contrib/charmhelpers/__init__.py b/lib/charmhelpers/contrib/charmhelpers/__init__.py deleted file mode 100644 index b08f33d2..00000000 --- a/lib/charmhelpers/contrib/charmhelpers/__init__.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright 2012 Canonical Ltd. This software is licensed under the -# GNU Affero General Public License version 3 (see the file LICENSE). - -import warnings -warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning) - -"""Helper functions for writing Juju charms in Python.""" - -__metaclass__ = type -__all__ = [ - #'get_config', # core.hookenv.config() - #'log', # core.hookenv.log() - #'log_entry', # core.hookenv.log() - #'log_exit', # core.hookenv.log() - #'relation_get', # core.hookenv.relation_get() - #'relation_set', # core.hookenv.relation_set() - #'relation_ids', # core.hookenv.relation_ids() - #'relation_list', # core.hookenv.relation_units() - #'config_get', # core.hookenv.config() - #'unit_get', # core.hookenv.unit_get() - #'open_port', # core.hookenv.open_port() - #'close_port', # core.hookenv.close_port() - #'service_control', # core.host.service() - 'unit_info', # client-side, NOT IMPLEMENTED - 'wait_for_machine', # client-side, NOT IMPLEMENTED - 'wait_for_page_contents', # client-side, NOT IMPLEMENTED - 'wait_for_relation', # client-side, NOT IMPLEMENTED - 'wait_for_unit', # client-side, NOT IMPLEMENTED -] - -import operator -from shelltoolbox import ( - command, -) -import tempfile -import time -import urllib2 -import yaml - -SLEEP_AMOUNT = 0.1 -# We create a juju_status Command here because it makes testing much, -# much easier. -juju_status = lambda: command('juju')('status') - -# re-implemented as charmhelpers.fetch.configure_sources() -#def configure_source(update=False): -# source = config_get('source') -# if ((source.startswith('ppa:') or -# source.startswith('cloud:') or -# source.startswith('http:'))): -# run('add-apt-repository', source) -# if source.startswith("http:"): -# run('apt-key', 'import', config_get('key')) -# if update: -# run('apt-get', 'update') - - -# DEPRECATED: client-side only -def make_charm_config_file(charm_config): - charm_config_file = tempfile.NamedTemporaryFile() - charm_config_file.write(yaml.dump(charm_config)) - charm_config_file.flush() - # The NamedTemporaryFile instance is returned instead of just the name - # because we want to take advantage of garbage collection-triggered - # deletion of the temp file when it goes out of scope in the caller. - return charm_config_file - - -# DEPRECATED: client-side only -def unit_info(service_name, item_name, data=None, unit=None): - if data is None: - data = yaml.safe_load(juju_status()) - service = data['services'].get(service_name) - if service is None: - # XXX 2012-02-08 gmb: - # This allows us to cope with the race condition that we - # have between deploying a service and having it come up in - # `juju status`. We could probably do with cleaning it up so - # that it fails a bit more noisily after a while. - return '' - units = service['units'] - if unit is not None: - item = units[unit][item_name] - else: - # It might seem odd to sort the units here, but we do it to - # ensure that when no unit is specified, the first unit for the - # service (or at least the one with the lowest number) is the - # one whose data gets returned. - sorted_unit_names = sorted(units.keys()) - item = units[sorted_unit_names[0]][item_name] - return item - - -# DEPRECATED: client-side only -def get_machine_data(): - return yaml.safe_load(juju_status())['machines'] - - -# DEPRECATED: client-side only -def wait_for_machine(num_machines=1, timeout=300): - """Wait `timeout` seconds for `num_machines` machines to come up. - - This wait_for... function can be called by other wait_for functions - whose timeouts might be too short in situations where only a bare - Juju setup has been bootstrapped. - - :return: A tuple of (num_machines, time_taken). This is used for - testing. - """ - # You may think this is a hack, and you'd be right. The easiest way - # to tell what environment we're working in (LXC vs EC2) is to check - # the dns-name of the first machine. If it's localhost we're in LXC - # and we can just return here. - if get_machine_data()[0]['dns-name'] == 'localhost': - return 1, 0 - start_time = time.time() - while True: - # Drop the first machine, since it's the Zookeeper and that's - # not a machine that we need to wait for. This will only work - # for EC2 environments, which is why we return early above if - # we're in LXC. - machine_data = get_machine_data() - non_zookeeper_machines = [ - machine_data[key] for key in machine_data.keys()[1:]] - if len(non_zookeeper_machines) >= num_machines: - all_machines_running = True - for machine in non_zookeeper_machines: - if machine.get('instance-state') != 'running': - all_machines_running = False - break - if all_machines_running: - break - if time.time() - start_time >= timeout: - raise RuntimeError('timeout waiting for service to start') - time.sleep(SLEEP_AMOUNT) - return num_machines, time.time() - start_time - - -# DEPRECATED: client-side only -def wait_for_unit(service_name, timeout=480): - """Wait `timeout` seconds for a given service name to come up.""" - wait_for_machine(num_machines=1) - start_time = time.time() - while True: - state = unit_info(service_name, 'agent-state') - if 'error' in state or state == 'started': - break - if time.time() - start_time >= timeout: - raise RuntimeError('timeout waiting for service to start') - time.sleep(SLEEP_AMOUNT) - if state != 'started': - raise RuntimeError('unit did not start, agent-state: ' + state) - - -# DEPRECATED: client-side only -def wait_for_relation(service_name, relation_name, timeout=120): - """Wait `timeout` seconds for a given relation to come up.""" - start_time = time.time() - while True: - relation = unit_info(service_name, 'relations').get(relation_name) - if relation is not None and relation['state'] == 'up': - break - if time.time() - start_time >= timeout: - raise RuntimeError('timeout waiting for relation to be up') - time.sleep(SLEEP_AMOUNT) - - -# DEPRECATED: client-side only -def wait_for_page_contents(url, contents, timeout=120, validate=None): - if validate is None: - validate = operator.contains - start_time = time.time() - while True: - try: - stream = urllib2.urlopen(url) - except (urllib2.HTTPError, urllib2.URLError): - pass - else: - page = stream.read() - if validate(page, contents): - return page - if time.time() - start_time >= timeout: - raise RuntimeError('timeout waiting for contents of ' + url) - time.sleep(SLEEP_AMOUNT) diff --git a/lib/charmhelpers/contrib/hahelpers/apache.py b/lib/charmhelpers/contrib/hahelpers/apache.py deleted file mode 100644 index 3208a85c..00000000 --- a/lib/charmhelpers/contrib/hahelpers/apache.py +++ /dev/null @@ -1,58 +0,0 @@ -# -# Copyright 2012 Canonical Ltd. -# -# This file is sourced from lp:openstack-charm-helpers -# -# Authors: -# James Page -# Adam Gandelman -# - -import subprocess - -from charmhelpers.core.hookenv import ( - config as config_get, - relation_get, - relation_ids, - related_units as relation_list, - log, - INFO, -) - - -def get_cert(): - cert = config_get('ssl_cert') - key = config_get('ssl_key') - if not (cert and key): - log("Inspecting identity-service relations for SSL certificate.", - level=INFO) - cert = key = None - for r_id in relation_ids('identity-service'): - for unit in relation_list(r_id): - if not cert: - cert = relation_get('ssl_cert', - rid=r_id, unit=unit) - if not key: - key = relation_get('ssl_key', - rid=r_id, unit=unit) - return (cert, key) - - -def get_ca_cert(): - ca_cert = None - log("Inspecting identity-service relations for CA SSL certificate.", - level=INFO) - for r_id in relation_ids('identity-service'): - for unit in relation_list(r_id): - if not ca_cert: - ca_cert = relation_get('ca_cert', - rid=r_id, unit=unit) - return ca_cert - - -def install_ca_cert(ca_cert): - if ca_cert: - with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt', - 'w') as crt: - crt.write(ca_cert) - subprocess.check_call(['update-ca-certificates', '--fresh']) diff --git a/lib/charmhelpers/contrib/jujugui/IMPORT b/lib/charmhelpers/contrib/jujugui/IMPORT deleted file mode 100644 index 619a403c..00000000 --- a/lib/charmhelpers/contrib/jujugui/IMPORT +++ /dev/null @@ -1,4 +0,0 @@ -Source: lp:charms/juju-gui - -juju-gui/hooks/utils.py -> charm-helpers/charmhelpers/contrib/jujugui/utils.py -juju-gui/tests/test_utils.py -> charm-helpers/tests/contrib/jujugui/test_utils.py diff --git a/lib/charmhelpers/contrib/jujugui/utils.py b/lib/charmhelpers/contrib/jujugui/utils.py deleted file mode 100644 index c7d2b89b..00000000 --- a/lib/charmhelpers/contrib/jujugui/utils.py +++ /dev/null @@ -1,602 +0,0 @@ -"""Juju GUI charm utilities.""" - -__all__ = [ - 'AGENT', - 'APACHE', - 'API_PORT', - 'CURRENT_DIR', - 'HAPROXY', - 'IMPROV', - 'JUJU_DIR', - 'JUJU_GUI_DIR', - 'JUJU_GUI_SITE', - 'JUJU_PEM', - 'WEB_PORT', - 'bzr_checkout', - 'chain', - 'cmd_log', - 'fetch_api', - 'fetch_gui', - 'find_missing_packages', - 'first_path_in_dir', - 'get_api_address', - 'get_npm_cache_archive_url', - 'get_release_file_url', - 'get_staging_dependencies', - 'get_zookeeper_address', - 'legacy_juju', - 'log_hook', - 'merge', - 'parse_source', - 'prime_npm_cache', - 'render_to_file', - 'save_or_create_certificates', - 'setup_apache', - 'setup_gui', - 'start_agent', - 'start_gui', - 'start_improv', - 'write_apache_config', -] - -from contextlib import contextmanager -import errno -import json -import os -import logging -import shutil -from subprocess import CalledProcessError -import tempfile -from urlparse import urlparse - -import apt -import tempita - -from launchpadlib.launchpad import Launchpad -from shelltoolbox import ( - Serializer, - apt_get_install, - command, - environ, - install_extra_repositories, - run, - script_name, - search_file, - su, -) -from charmhelpers.core.host import ( - service_start, -) -from charmhelpers.core.hookenv import ( - log, - config, - unit_get, -) - - -AGENT = 'juju-api-agent' -APACHE = 'apache2' -IMPROV = 'juju-api-improv' -HAPROXY = 'haproxy' - -API_PORT = 8080 -WEB_PORT = 8000 - -CURRENT_DIR = os.getcwd() -JUJU_DIR = os.path.join(CURRENT_DIR, 'juju') -JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui') -JUJU_GUI_SITE = '/etc/apache2/sites-available/juju-gui' -JUJU_GUI_PORTS = '/etc/apache2/ports.conf' -JUJU_PEM = 'juju.includes-private-key.pem' -BUILD_REPOSITORIES = ('ppa:chris-lea/node.js-legacy',) -DEB_BUILD_DEPENDENCIES = ( - 'bzr', 'imagemagick', 'make', 'nodejs', 'npm', -) -DEB_STAGE_DEPENDENCIES = ( - 'zookeeper', -) - - -# Store the configuration from on invocation to the next. -config_json = Serializer('/tmp/config.json') -# Bazaar checkout command. -bzr_checkout = command('bzr', 'co', '--lightweight') -# Whether or not the charm is deployed using juju-core. -# If juju-core has been used to deploy the charm, an agent.conf file must -# be present in the charm parent directory. -legacy_juju = lambda: not os.path.exists( - os.path.join(CURRENT_DIR, '..', 'agent.conf')) - - -def _get_build_dependencies(): - """Install deb dependencies for building.""" - log('Installing build dependencies.') - cmd_log(install_extra_repositories(*BUILD_REPOSITORIES)) - cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES)) - - -def get_api_address(unit_dir): - """Return the Juju API address stored in the uniter agent.conf file.""" - import yaml # python-yaml is only installed if juju-core is used. - # XXX 2013-03-27 frankban bug=1161443: - # currently the uniter agent.conf file does not include the API - # address. For now retrieve it from the machine agent file. - base_dir = os.path.abspath(os.path.join(unit_dir, '..')) - for dirname in os.listdir(base_dir): - if dirname.startswith('machine-'): - agent_conf = os.path.join(base_dir, dirname, 'agent.conf') - break - else: - raise IOError('Juju agent configuration file not found.') - contents = yaml.load(open(agent_conf)) - return contents['apiinfo']['addrs'][0] - - -def get_staging_dependencies(): - """Install deb dependencies for the stage (improv) environment.""" - log('Installing stage dependencies.') - cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES)) - - -def first_path_in_dir(directory): - """Return the full path of the first file/dir in *directory*.""" - return os.path.join(directory, os.listdir(directory)[0]) - - -def _get_by_attr(collection, attr, value): - """Return the first item in collection having attr == value. - - Return None if the item is not found. - """ - for item in collection: - if getattr(item, attr) == value: - return item - - -def get_release_file_url(project, series_name, release_version): - """Return the URL of the release file hosted in Launchpad. - - The returned URL points to a release file for the given project, series - name and release version. - The argument *project* is a project object as returned by launchpadlib. - The arguments *series_name* and *release_version* are strings. If - *release_version* is None, the URL of the latest release will be returned. - """ - series = _get_by_attr(project.series, 'name', series_name) - if series is None: - raise ValueError('%r: series not found' % series_name) - # Releases are returned by Launchpad in reverse date order. - releases = list(series.releases) - if not releases: - raise ValueError('%r: series does not contain releases' % series_name) - if release_version is not None: - release = _get_by_attr(releases, 'version', release_version) - if release is None: - raise ValueError('%r: release not found' % release_version) - releases = [release] - for release in releases: - for file_ in release.files: - if str(file_).endswith('.tgz'): - return file_.file_link - raise ValueError('%r: file not found' % release_version) - - -def get_zookeeper_address(agent_file_path): - """Retrieve the Zookeeper address contained in the given *agent_file_path*. - - The *agent_file_path* is a path to a file containing a line similar to the - following:: - - env JUJU_ZOOKEEPER="address" - """ - line = search_file('JUJU_ZOOKEEPER', agent_file_path).strip() - return line.split('=')[1].strip('"') - - -@contextmanager -def log_hook(): - """Log when a hook starts and stops its execution. - - Also log to stdout possible CalledProcessError exceptions raised executing - the hook. - """ - script = script_name() - log(">>> Entering {}".format(script)) - try: - yield - except CalledProcessError as err: - log('Exception caught:') - log(err.output) - raise - finally: - log("<<< Exiting {}".format(script)) - - -def parse_source(source): - """Parse the ``juju-gui-source`` option. - - Return a tuple of two elements representing info on how to deploy Juju GUI. - Examples: - - ('stable', None): latest stable release; - - ('stable', '0.1.0'): stable release v0.1.0; - - ('trunk', None): latest trunk release; - - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1; - - ('branch', 'lp:juju-gui'): release is made from a branch; - - ('url', 'http://example.com/gui'): release from a downloaded file. - """ - if source.startswith('url:'): - source = source[4:] - # Support file paths, including relative paths. - if urlparse(source).scheme == '': - if not source.startswith('/'): - source = os.path.join(os.path.abspath(CURRENT_DIR), source) - source = "file://%s" % source - return 'url', source - if source in ('stable', 'trunk'): - return source, None - if source.startswith('lp:') or source.startswith('http://'): - return 'branch', source - if 'build' in source: - return 'trunk', source - return 'stable', source - - -def render_to_file(template_name, context, destination): - """Render the given *template_name* into *destination* using *context*. - - The tempita template language is used to render contents - (see http://pythonpaste.org/tempita/). - The argument *template_name* is the name or path of the template file: - it may be either a path relative to ``../config`` or an absolute path. - The argument *destination* is a file path. - The argument *context* is a dict-like object. - """ - template_path = os.path.abspath(template_name) - template = tempita.Template.from_filename(template_path) - with open(destination, 'w') as stream: - stream.write(template.substitute(context)) - - -results_log = None - - -def _setupLogging(): - global results_log - if results_log is not None: - return - cfg = config() - logging.basicConfig( - filename=cfg['command-log-file'], - level=logging.INFO, - format="%(asctime)s: %(name)s@%(levelname)s %(message)s") - results_log = logging.getLogger('juju-gui') - - -def cmd_log(results): - global results_log - if not results: - return - if results_log is None: - _setupLogging() - # Since 'results' may be multi-line output, start it on a separate line - # from the logger timestamp, etc. - results_log.info('\n' + results) - - -def start_improv(staging_env, ssl_cert_path, - config_path='/etc/init/juju-api-improv.conf'): - """Start a simulated juju environment using ``improv.py``.""" - log('Setting up staging start up script.') - context = { - 'juju_dir': JUJU_DIR, - 'keys': ssl_cert_path, - 'port': API_PORT, - 'staging_env': staging_env, - } - render_to_file('config/juju-api-improv.conf.template', context, config_path) - log('Starting the staging backend.') - with su('root'): - service_start(IMPROV) - - -def start_agent( - ssl_cert_path, config_path='/etc/init/juju-api-agent.conf', - read_only=False): - """Start the Juju agent and connect to the current environment.""" - # Retrieve the Zookeeper address from the start up script. - unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..')) - agent_file = '/etc/init/juju-{0}.conf'.format(os.path.basename(unit_dir)) - zookeeper = get_zookeeper_address(agent_file) - log('Setting up API agent start up script.') - context = { - 'juju_dir': JUJU_DIR, - 'keys': ssl_cert_path, - 'port': API_PORT, - 'zookeeper': zookeeper, - 'read_only': read_only - } - render_to_file('config/juju-api-agent.conf.template', context, config_path) - log('Starting API agent.') - with su('root'): - service_start(AGENT) - - -def start_gui( - console_enabled, login_help, readonly, in_staging, ssl_cert_path, - charmworld_url, serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg', - config_js_path=None, secure=True, sandbox=False): - """Set up and start the Juju GUI server.""" - with su('root'): - run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR) - # XXX 2013-02-05 frankban bug=1116320: - # External insecure resources are still loaded when testing in the - # debug environment. For now, switch to the production environment if - # the charm is configured to serve tests. - if in_staging and not serve_tests: - build_dirname = 'build-debug' - else: - build_dirname = 'build-prod' - build_dir = os.path.join(JUJU_GUI_DIR, build_dirname) - log('Generating the Juju GUI configuration file.') - is_legacy_juju = legacy_juju() - user, password = None, None - if (is_legacy_juju and in_staging) or sandbox: - user, password = 'admin', 'admin' - else: - user, password = None, None - - api_backend = 'python' if is_legacy_juju else 'go' - if secure: - protocol = 'wss' - else: - log('Running in insecure mode! Port 80 will serve unencrypted.') - protocol = 'ws' - - context = { - 'raw_protocol': protocol, - 'address': unit_get('public-address'), - 'console_enabled': json.dumps(console_enabled), - 'login_help': json.dumps(login_help), - 'password': json.dumps(password), - 'api_backend': json.dumps(api_backend), - 'readonly': json.dumps(readonly), - 'user': json.dumps(user), - 'protocol': json.dumps(protocol), - 'sandbox': json.dumps(sandbox), - 'charmworld_url': json.dumps(charmworld_url), - } - if config_js_path is None: - config_js_path = os.path.join( - build_dir, 'juju-ui', 'assets', 'config.js') - render_to_file('config/config.js.template', context, config_js_path) - - write_apache_config(build_dir, serve_tests) - - log('Generating haproxy configuration file.') - if is_legacy_juju: - # The PyJuju API agent is listening on localhost. - api_address = '127.0.0.1:{0}'.format(API_PORT) - else: - # Retrieve the juju-core API server address. - api_address = get_api_address(os.path.join(CURRENT_DIR, '..')) - context = { - 'api_address': api_address, - 'api_pem': JUJU_PEM, - 'legacy_juju': is_legacy_juju, - 'ssl_cert_path': ssl_cert_path, - # In PyJuju environments, use the same certificate for both HTTPS and - # WebSocket connections. In juju-core the system already has the proper - # certificate installed. - 'web_pem': JUJU_PEM, - 'web_port': WEB_PORT, - 'secure': secure - } - render_to_file('config/haproxy.cfg.template', context, haproxy_path) - log('Starting Juju GUI.') - - -def write_apache_config(build_dir, serve_tests=False): - log('Generating the apache site configuration file.') - context = { - 'port': WEB_PORT, - 'serve_tests': serve_tests, - 'server_root': build_dir, - 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''), - } - render_to_file('config/apache-ports.template', context, JUJU_GUI_PORTS) - render_to_file('config/apache-site.template', context, JUJU_GUI_SITE) - - -def get_npm_cache_archive_url(Launchpad=Launchpad): - """Figure out the URL of the most recent NPM cache archive on Launchpad.""" - launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') - project = launchpad.projects['juju-gui'] - # Find the URL of the most recently created NPM cache archive. - npm_cache_url = get_release_file_url(project, 'npm-cache', None) - return npm_cache_url - - -def prime_npm_cache(npm_cache_url): - """Download NPM cache archive and prime the NPM cache with it.""" - # Download the cache archive and then uncompress it into the NPM cache. - npm_cache_archive = os.path.join(CURRENT_DIR, 'npm-cache.tgz') - cmd_log(run('curl', '-L', '-o', npm_cache_archive, npm_cache_url)) - npm_cache_dir = os.path.expanduser('~/.npm') - # The NPM cache directory probably does not exist, so make it if not. - try: - os.mkdir(npm_cache_dir) - except OSError, e: - # If the directory already exists then ignore the error. - if e.errno != errno.EEXIST: # File exists. - raise - uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f') - cmd_log(uncompress(npm_cache_archive)) - - -def fetch_gui(juju_gui_source, logpath): - """Retrieve the Juju GUI release/branch.""" - # Retrieve a Juju GUI release. - origin, version_or_branch = parse_source(juju_gui_source) - if origin == 'branch': - # Make sure we have the dependencies necessary for us to actually make - # a build. - _get_build_dependencies() - # Create a release starting from a branch. - juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source') - log('Retrieving Juju GUI source checkout from %s.' % version_or_branch) - cmd_log(run('rm', '-rf', juju_gui_source_dir)) - cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir)) - log('Preparing a Juju GUI release.') - logdir = os.path.dirname(logpath) - fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir) - log('Output from "make distfile" sent to %s' % name) - with environ(NO_BZR='1'): - run('make', '-C', juju_gui_source_dir, 'distfile', - stdout=fd, stderr=fd) - release_tarball = first_path_in_dir( - os.path.join(juju_gui_source_dir, 'releases')) - else: - log('Retrieving Juju GUI release.') - if origin == 'url': - file_url = version_or_branch - else: - # Retrieve a release from Launchpad. - launchpad = Launchpad.login_anonymously( - 'Juju GUI charm', 'production') - project = launchpad.projects['juju-gui'] - file_url = get_release_file_url(project, origin, version_or_branch) - log('Downloading release file from %s.' % file_url) - release_tarball = os.path.join(CURRENT_DIR, 'release.tgz') - cmd_log(run('curl', '-L', '-o', release_tarball, file_url)) - return release_tarball - - -def fetch_api(juju_api_branch): - """Retrieve the Juju branch.""" - # Retrieve Juju API source checkout. - log('Retrieving Juju API source checkout.') - cmd_log(run('rm', '-rf', JUJU_DIR)) - cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR)) - - -def setup_gui(release_tarball): - """Set up Juju GUI.""" - # Uncompress the release tarball. - log('Installing Juju GUI.') - release_dir = os.path.join(CURRENT_DIR, 'release') - cmd_log(run('rm', '-rf', release_dir)) - os.mkdir(release_dir) - uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f') - cmd_log(uncompress(release_tarball)) - # Link the Juju GUI dir to the contents of the release tarball. - cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR)) - - -def setup_apache(): - """Set up apache.""" - log('Setting up apache.') - if not os.path.exists(JUJU_GUI_SITE): - cmd_log(run('touch', JUJU_GUI_SITE)) - cmd_log(run('chown', 'ubuntu:', JUJU_GUI_SITE)) - cmd_log( - run('ln', '-s', JUJU_GUI_SITE, - '/etc/apache2/sites-enabled/juju-gui')) - - if not os.path.exists(JUJU_GUI_PORTS): - cmd_log(run('touch', JUJU_GUI_PORTS)) - cmd_log(run('chown', 'ubuntu:', JUJU_GUI_PORTS)) - - with su('root'): - run('a2dissite', 'default') - run('a2ensite', 'juju-gui') - - -def save_or_create_certificates( - ssl_cert_path, ssl_cert_contents, ssl_key_contents): - """Generate the SSL certificates. - - If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them - as certificates; otherwise, generate them. - - Also create a pem file, suitable for use in the haproxy configuration, - concatenating the key and the certificate files. - """ - crt_path = os.path.join(ssl_cert_path, 'juju.crt') - key_path = os.path.join(ssl_cert_path, 'juju.key') - if not os.path.exists(ssl_cert_path): - os.makedirs(ssl_cert_path) - if ssl_cert_contents and ssl_key_contents: - # Save the provided certificates. - with open(crt_path, 'w') as cert_file: - cert_file.write(ssl_cert_contents) - with open(key_path, 'w') as key_file: - key_file.write(ssl_key_contents) - else: - # Generate certificates. - # See http://superuser.com/questions/226192/openssl-without-prompt - cmd_log(run( - 'openssl', 'req', '-new', '-newkey', 'rsa:4096', - '-days', '365', '-nodes', '-x509', '-subj', - # These are arbitrary test values for the certificate. - '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com', - '-keyout', key_path, '-out', crt_path)) - # Generate the pem file. - pem_path = os.path.join(ssl_cert_path, JUJU_PEM) - if os.path.exists(pem_path): - os.remove(pem_path) - with open(pem_path, 'w') as pem_file: - shutil.copyfileobj(open(key_path), pem_file) - shutil.copyfileobj(open(crt_path), pem_file) - - -def find_missing_packages(*packages): - """Given a list of packages, return the packages which are not installed. - """ - cache = apt.Cache() - missing = set() - for pkg_name in packages: - try: - pkg = cache[pkg_name] - except KeyError: - missing.add(pkg_name) - continue - if pkg.is_installed: - continue - missing.add(pkg_name) - return missing - - -## Backend support decorators - -def chain(name): - """Helper method to compose a set of mixin objects into a callable. - - Each method is called in the context of its mixin instance, and its - argument is the Backend instance. - """ - # Chain method calls through all implementing mixins. - def method(self): - for mixin in self.mixins: - a_callable = getattr(type(mixin), name, None) - if a_callable: - a_callable(mixin, self) - - method.__name__ = name - return method - - -def merge(name): - """Helper to merge a property from a set of strategy objects - into a unified set. - """ - # Return merged property from every providing mixin as a set. - @property - def method(self): - result = set() - for mixin in self.mixins: - segment = getattr(type(mixin), name, None) - if segment and isinstance(segment, (list, tuple, set)): - result |= set(segment) - - return result - return method diff --git a/lib/charmhelpers/contrib/network/ovs/__init__.py b/lib/charmhelpers/contrib/network/ovs/__init__.py deleted file mode 100644 index 5eba8376..00000000 --- a/lib/charmhelpers/contrib/network/ovs/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -''' Helpers for interacting with OpenvSwitch ''' -import subprocess -import os -from charmhelpers.core.hookenv import ( - log, WARNING -) -from charmhelpers.core.host import ( - service -) - - -def add_bridge(name): - ''' Add the named bridge to openvswitch ''' - log('Creating bridge {}'.format(name)) - subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name]) - - -def del_bridge(name): - ''' Delete the named bridge from openvswitch ''' - log('Deleting bridge {}'.format(name)) - subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-br", name]) - - -def add_bridge_port(name, port): - ''' Add a port to the named openvswitch bridge ''' - log('Adding port {} to bridge {}'.format(port, name)) - subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-port", - name, port]) - subprocess.check_call(["ip", "link", "set", port, "up"]) - - -def del_bridge_port(name, port): - ''' Delete a port from the named openvswitch bridge ''' - log('Deleting port {} from bridge {}'.format(port, name)) - subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-port", - name, port]) - subprocess.check_call(["ip", "link", "set", port, "down"]) - - -def set_manager(manager): - ''' Set the controller for the local openvswitch ''' - log('Setting manager for local ovs to {}'.format(manager)) - subprocess.check_call(['ovs-vsctl', 'set-manager', - 'ssl:{}'.format(manager)]) - - -CERT_PATH = '/etc/openvswitch/ovsclient-cert.pem' - - -def get_certificate(): - ''' Read openvswitch certificate from disk ''' - if os.path.exists(CERT_PATH): - log('Reading ovs certificate from {}'.format(CERT_PATH)) - with open(CERT_PATH, 'r') as cert: - full_cert = cert.read() - begin_marker = "-----BEGIN CERTIFICATE-----" - end_marker = "-----END CERTIFICATE-----" - begin_index = full_cert.find(begin_marker) - end_index = full_cert.rfind(end_marker) - if end_index == -1 or begin_index == -1: - raise RuntimeError("Certificate does not contain valid begin" - " and end markers.") - full_cert = full_cert[begin_index:(end_index + len(end_marker))] - return full_cert - else: - log('Certificate not found', level=WARNING) - return None - - -def full_restart(): - ''' Full restart and reload of openvswitch ''' - if os.path.exists('/etc/init/openvswitch-force-reload-kmod.conf'): - service('start', 'openvswitch-force-reload-kmod') - else: - service('force-reload-kmod', 'openvswitch-switch') diff --git a/lib/charmhelpers/contrib/saltstack/__init__.py b/lib/charmhelpers/contrib/saltstack/__init__.py deleted file mode 100644 index 19078f8e..00000000 --- a/lib/charmhelpers/contrib/saltstack/__init__.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Charm Helpers saltstack - declare the state of your machines. - -This helper enables you to declare your machine state, rather than -program it procedurally (and have to test each change to your procedures). -Your install hook can be as simple as: - -{{{ -from charmhelpers.contrib.saltstack import ( - install_salt_support, - update_machine_state, -) - - -def install(): - install_salt_support() - update_machine_state('machine_states/dependencies.yaml') - update_machine_state('machine_states/installed.yaml') -}}} - -and won't need to change (nor will its tests) when you change the machine -state. - -It's using a python package called salt-minion which allows various formats for -specifying resources, such as: - -{{{ -/srv/{{ basedir }}: - file.directory: - - group: ubunet - - user: ubunet - - require: - - user: ubunet - - recurse: - - user - - group - -ubunet: - group.present: - - gid: 1500 - user.present: - - uid: 1500 - - gid: 1500 - - createhome: False - - require: - - group: ubunet -}}} - -The docs for all the different state definitions are at: - http://docs.saltstack.com/ref/states/all/ - - -TODO: - * Add test helpers which will ensure that machine state definitions - are functionally (but not necessarily logically) correct (ie. getting - salt to parse all state defs. - * Add a link to a public bootstrap charm example / blogpost. - * Find a way to obviate the need to use the grains['charm_dir'] syntax - in templates. -""" -# Copyright 2013 Canonical Ltd. -# -# Authors: -# Charm Helpers Developers -import subprocess - -import charmhelpers.contrib.templating.contexts -import charmhelpers.core.host -import charmhelpers.core.hookenv - - -salt_grains_path = '/etc/salt/grains' - - -def install_salt_support(from_ppa=True): - """Installs the salt-minion helper for machine state. - - By default the salt-minion package is installed from - the saltstack PPA. If from_ppa is False you must ensure - that the salt-minion package is available in the apt cache. - """ - if from_ppa: - subprocess.check_call([ - '/usr/bin/add-apt-repository', - '--yes', - 'ppa:saltstack/salt', - ]) - subprocess.check_call(['/usr/bin/apt-get', 'update']) - # We install salt-common as salt-minion would run the salt-minion - # daemon. - charmhelpers.fetch.apt_install('salt-common') - - -def update_machine_state(state_path): - """Update the machine state using the provided state declaration.""" - charmhelpers.contrib.templating.contexts.juju_state_to_yaml( - salt_grains_path) - subprocess.check_call([ - 'salt-call', - '--local', - 'state.template', - state_path, - ]) diff --git a/lib/charmhelpers/contrib/storage/linux/__init__.py b/lib/charmhelpers/contrib/storage/linux/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/charmhelpers/contrib/templating/__init__.py b/lib/charmhelpers/contrib/templating/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/charmhelpers/contrib/templating/contexts.py b/lib/charmhelpers/contrib/templating/contexts.py deleted file mode 100644 index b117b2de..00000000 --- a/lib/charmhelpers/contrib/templating/contexts.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2013 Canonical Ltd. -# -# Authors: -# Charm Helpers Developers -"""A helper to create a yaml cache of config with namespaced relation data.""" -import os -import yaml - -import charmhelpers.core.hookenv - - -charm_dir = os.environ.get('CHARM_DIR', '') - - -def juju_state_to_yaml(yaml_path, namespace_separator=':', - allow_hyphens_in_keys=True): - """Update the juju config and state in a yaml file. - - This includes any current relation-get data, and the charm - directory. - - This function was created for the ansible and saltstack - support, as those libraries can use a yaml file to supply - context to templates, but it may be useful generally to - create and update an on-disk cache of all the config, including - previous relation data. - - By default, hyphens are allowed in keys as this is supported - by yaml, but for tools like ansible, hyphens are not valid [1]. - - [1] http://www.ansibleworks.com/docs/playbooks_variables.html#what-makes-a-valid-variable-name - """ - config = charmhelpers.core.hookenv.config() - - # Add the charm_dir which we will need to refer to charm - # file resources etc. - config['charm_dir'] = charm_dir - config['local_unit'] = charmhelpers.core.hookenv.local_unit() - - # Add any relation data prefixed with the relation type. - relation_type = charmhelpers.core.hookenv.relation_type() - if relation_type is not None: - relation_data = charmhelpers.core.hookenv.relation_get() - relation_data = dict( - ("{relation_type}{namespace_separator}{key}".format( - relation_type=relation_type.replace('-', '_'), - key=key, - namespace_separator=namespace_separator), val) - for key, val in relation_data.items()) - config.update(relation_data) - - # Don't use non-standard tags for unicode which will not - # work when salt uses yaml.load_safe. - yaml.add_representer(unicode, lambda dumper, - value: dumper.represent_scalar( - u'tag:yaml.org,2002:str', value)) - - yaml_dir = os.path.dirname(yaml_path) - if not os.path.exists(yaml_dir): - os.makedirs(yaml_dir) - - if os.path.exists(yaml_path): - with open(yaml_path, "r") as existing_vars_file: - existing_vars = yaml.load(existing_vars_file.read()) - else: - existing_vars = {} - - if not allow_hyphens_in_keys: - config = dict( - (key.replace('-', '_'), val) for key, val in config.items()) - existing_vars.update(config) - with open(yaml_path, "w+") as fp: - fp.write(yaml.dump(existing_vars)) diff --git a/lib/charmhelpers/contrib/templating/pyformat.py b/lib/charmhelpers/contrib/templating/pyformat.py deleted file mode 100644 index baaf98e7..00000000 --- a/lib/charmhelpers/contrib/templating/pyformat.py +++ /dev/null @@ -1,13 +0,0 @@ -''' -Templating using standard Python str.format() method. -''' - -from charmhelpers.core import hookenv - - -def render(template, extra={}, **kwargs): - """Return the template rendered using Python's str.format().""" - context = hookenv.execution_environment() - context.update(extra) - context.update(kwargs) - return template.format(**context) diff --git a/lib/charmhelpers/core/__init__.py b/lib/charmhelpers/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/charmhelpers/payload/__init__.py b/lib/charmhelpers/payload/__init__.py deleted file mode 100644 index fc9fbc08..00000000 --- a/lib/charmhelpers/payload/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"Tools for working with files injected into a charm just before deployment." diff --git a/lib/charmhelpers/payload/archive.py b/lib/charmhelpers/payload/archive.py deleted file mode 100644 index de03e1b7..00000000 --- a/lib/charmhelpers/payload/archive.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import tarfile -import zipfile -from charmhelpers.core import ( - host, - hookenv, -) - - -class ArchiveError(Exception): - pass - - -def get_archive_handler(archive_name): - if os.path.isfile(archive_name): - if tarfile.is_tarfile(archive_name): - return extract_tarfile - elif zipfile.is_zipfile(archive_name): - return extract_zipfile - else: - # look at the file name - for ext in ('.tar', '.tar.gz', '.tgz', 'tar.bz2', '.tbz2', '.tbz'): - if archive_name.endswith(ext): - return extract_tarfile - for ext in ('.zip', '.jar'): - if archive_name.endswith(ext): - return extract_zipfile - - -def archive_dest_default(archive_name): - archive_file = os.path.basename(archive_name) - return os.path.join(hookenv.charm_dir(), "archives", archive_file) - - -def extract(archive_name, destpath=None): - handler = get_archive_handler(archive_name) - if handler: - if not destpath: - destpath = archive_dest_default(archive_name) - if not os.path.isdir(destpath): - host.mkdir(destpath) - handler(archive_name, destpath) - return destpath - else: - raise ArchiveError("No handler for archive") - - -def extract_tarfile(archive_name, destpath): - "Unpack a tar archive, optionally compressed" - archive = tarfile.open(archive_name) - archive.extractall(destpath) - - -def extract_zipfile(archive_name, destpath): - "Unpack a zip file" - archive = zipfile.ZipFile(archive_name) - archive.extractall(destpath) diff --git a/lib/charmhelpers/payload/execd.py b/lib/charmhelpers/payload/execd.py deleted file mode 100644 index 6476a75f..00000000 --- a/lib/charmhelpers/payload/execd.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import subprocess -from charmhelpers.core import hookenv - - -def default_execd_dir(): - return os.path.join(os.environ['CHARM_DIR'], 'exec.d') - - -def execd_module_paths(execd_dir=None): - """Generate a list of full paths to modules within execd_dir.""" - if not execd_dir: - execd_dir = default_execd_dir() - - if not os.path.exists(execd_dir): - return - - for subpath in os.listdir(execd_dir): - module = os.path.join(execd_dir, subpath) - if os.path.isdir(module): - yield module - - -def execd_submodule_paths(command, execd_dir=None): - """Generate a list of full paths to the specified command within exec_dir. - """ - for module_path in execd_module_paths(execd_dir): - path = os.path.join(module_path, command) - if os.access(path, os.X_OK) and os.path.isfile(path): - yield path - - -def execd_run(command, execd_dir=None, die_on_error=False, stderr=None): - """Run command for each module within execd_dir which defines it.""" - for submodule_path in execd_submodule_paths(command, execd_dir): - try: - subprocess.check_call(submodule_path, shell=True, stderr=stderr) - except subprocess.CalledProcessError as e: - hookenv.log("Error ({}) running {}. Output: {}".format( - e.returncode, e.cmd, e.output)) - if die_on_error: - sys.exit(e.returncode) - - -def execd_preinstall(execd_dir=None): - """Run charm-pre-install for each module within execd_dir.""" - execd_run('charm-pre-install', execd_dir=execd_dir) diff --git a/revision b/revision index ee977b5e..4699eb3c 100644 --- a/revision +++ b/revision @@ -1 +1 @@ -115 +116 diff --git a/templates/rabbitmq.config b/templates/rabbitmq.config index 4c99ea99..7246ee94 100644 --- a/templates/rabbitmq.config +++ b/templates/rabbitmq.config @@ -1,10 +1,21 @@ [ {rabbit, [ +{% if ssl_only %} + {tcp_listeners, []}, +{% else %} + {tcp_listeners, [5672]}, +{% endif %} {ssl_listeners, [{{ ssl_port }}]}, {ssl_options, [ + {verify, verify_peer}, +{% if ssl_client %} + {fail_if_no_peer_cert, true}, +{% else %} + {fail_if_no_peer_cert, false}, +{% endif %}{% if ssl_ca_file %} + {cacertfile, "{{ ssl_ca_file }}"}, {% endif %} {certfile, "{{ ssl_cert_file }}"}, {keyfile, "{{ ssl_key_file }}"} - ]}, - {tcp_listeners, [5672]} + ]} ]} ]. \ No newline at end of file