summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Ames <david.ames@canonical.com>2017-02-14 11:47:53 -0800
committerDavid Ames <david.ames@canonical.com>2017-02-14 14:41:07 -0800
commit239435b26ed6e7fe1feb478dcb96a47a74cd2a65 (patch)
tree3a86fb4cfa846ab3c74bf5b3466cea6839db618f
parentc614311aa244e05b1bec6183bfcb368fc48267b7 (diff)
Pre-release charm-helpers sync 17.02
Get each charm up to date with lp:charm-helpers for release testing. Change-Id: I227cdb94fd77d76cbd4071c9126ee3743b6a4f47
Notes
Notes (review): Verified+1: Canonical CI <uosci-testing-bot@ubuntu.com> Code-Review+2: Liam Young <liam.young@canonical.com> Workflow+1: Liam Young <liam.young@canonical.com> Verified+2: Jenkins Submitted-by: Jenkins Submitted-at: Wed, 15 Feb 2017 09:09:25 +0000 Reviewed-on: https://review.openstack.org/433886 Project: openstack/charm-cinder-backup Branch: refs/heads/master
-rw-r--r--charm-helpers-tests.yaml1
-rw-r--r--hooks/charmhelpers/contrib/network/ip.py6
-rw-r--r--hooks/charmhelpers/contrib/openstack/amulet/utils.py174
-rw-r--r--hooks/charmhelpers/contrib/openstack/context.py74
-rw-r--r--hooks/charmhelpers/contrib/openstack/templates/memcached.conf53
-rw-r--r--hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken-mitaka3
-rw-r--r--hooks/charmhelpers/contrib/openstack/templates/wsgi-openstack-api.conf100
-rw-r--r--hooks/charmhelpers/contrib/openstack/utils.py70
-rw-r--r--hooks/charmhelpers/contrib/storage/linux/ceph.py16
-rw-r--r--hooks/charmhelpers/core/hookenv.py45
-rw-r--r--hooks/charmhelpers/core/host.py227
-rw-r--r--hooks/charmhelpers/osplatform.py6
-rw-r--r--tests/charmhelpers/contrib/amulet/utils.py3
-rw-r--r--tests/charmhelpers/contrib/openstack/amulet/utils.py174
-rw-r--r--tests/charmhelpers/core/__init__.py13
-rw-r--r--tests/charmhelpers/core/decorators.py55
-rw-r--r--tests/charmhelpers/core/files.py43
-rw-r--r--tests/charmhelpers/core/fstab.py132
-rw-r--r--tests/charmhelpers/core/hookenv.py1068
-rw-r--r--tests/charmhelpers/core/host.py918
-rw-r--r--tests/charmhelpers/core/host_factory/__init__.py0
-rw-r--r--tests/charmhelpers/core/host_factory/centos.py56
-rw-r--r--tests/charmhelpers/core/host_factory/ubuntu.py56
-rw-r--r--tests/charmhelpers/core/hugepage.py69
-rw-r--r--tests/charmhelpers/core/kernel.py72
-rw-r--r--tests/charmhelpers/core/kernel_factory/__init__.py0
-rw-r--r--tests/charmhelpers/core/kernel_factory/centos.py17
-rw-r--r--tests/charmhelpers/core/kernel_factory/ubuntu.py13
-rw-r--r--tests/charmhelpers/core/services/__init__.py16
-rw-r--r--tests/charmhelpers/core/services/base.py351
-rw-r--r--tests/charmhelpers/core/services/helpers.py290
-rw-r--r--tests/charmhelpers/core/strutils.py70
-rw-r--r--tests/charmhelpers/core/sysctl.py54
-rw-r--r--tests/charmhelpers/core/templating.py84
-rw-r--r--tests/charmhelpers/core/unitdata.py518
35 files changed, 4773 insertions, 74 deletions
diff --git a/charm-helpers-tests.yaml b/charm-helpers-tests.yaml
index 48b12f6..e506325 100644
--- a/charm-helpers-tests.yaml
+++ b/charm-helpers-tests.yaml
@@ -3,3 +3,4 @@ destination: tests/charmhelpers
3include: 3include:
4 - contrib.amulet 4 - contrib.amulet
5 - contrib.openstack.amulet 5 - contrib.openstack.amulet
6 - core
diff --git a/hooks/charmhelpers/contrib/network/ip.py b/hooks/charmhelpers/contrib/network/ip.py
index 2d2026e..e141fc1 100644
--- a/hooks/charmhelpers/contrib/network/ip.py
+++ b/hooks/charmhelpers/contrib/network/ip.py
@@ -424,7 +424,11 @@ def ns_query(address):
424 else: 424 else:
425 return None 425 return None
426 426
427 answers = dns.resolver.query(address, rtype) 427 try:
428 answers = dns.resolver.query(address, rtype)
429 except dns.resolver.NXDOMAIN:
430 return None
431
428 if answers: 432 if answers:
429 return str(answers[0]) 433 return str(answers[0])
430 return None 434 return None
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/utils.py b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
index 6a0ba83..401c032 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
@@ -20,6 +20,7 @@ import re
20import six 20import six
21import time 21import time
22import urllib 22import urllib
23import urlparse
23 24
24import cinderclient.v1.client as cinder_client 25import cinderclient.v1.client as cinder_client
25import glanceclient.v1.client as glance_client 26import glanceclient.v1.client as glance_client
@@ -37,6 +38,7 @@ import swiftclient
37from charmhelpers.contrib.amulet.utils import ( 38from charmhelpers.contrib.amulet.utils import (
38 AmuletUtils 39 AmuletUtils
39) 40)
41from charmhelpers.core.decorators import retry_on_exception
40 42
41DEBUG = logging.DEBUG 43DEBUG = logging.DEBUG
42ERROR = logging.ERROR 44ERROR = logging.ERROR
@@ -303,6 +305,46 @@ class OpenStackAmuletUtils(AmuletUtils):
303 self.log.debug('Checking if tenant exists ({})...'.format(tenant)) 305 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
304 return tenant in [t.name for t in keystone.tenants.list()] 306 return tenant in [t.name for t in keystone.tenants.list()]
305 307
308 @retry_on_exception(5, base_delay=10)
309 def keystone_wait_for_propagation(self, sentry_relation_pairs,
310 api_version):
311 """Iterate over list of sentry and relation tuples and verify that
312 api_version has the expected value.
313
314 :param sentry_relation_pairs: list of sentry, relation name tuples used
315 for monitoring propagation of relation
316 data
317 :param api_version: api_version to expect in relation data
318 :returns: None if successful. Raise on error.
319 """
320 for (sentry, relation_name) in sentry_relation_pairs:
321 rel = sentry.relation('identity-service',
322 relation_name)
323 self.log.debug('keystone relation data: {}'.format(rel))
324 if rel['api_version'] != str(api_version):
325 raise Exception("api_version not propagated through relation"
326 " data yet ('{}' != '{}')."
327 "".format(rel['api_version'], api_version))
328
329 def keystone_configure_api_version(self, sentry_relation_pairs, deployment,
330 api_version):
331 """Configure preferred-api-version of keystone in deployment and
332 monitor provided list of relation objects for propagation
333 before returning to caller.
334
335 :param sentry_relation_pairs: list of sentry, relation tuples used for
336 monitoring propagation of relation data
337 :param deployment: deployment to configure
338 :param api_version: value preferred-api-version will be set to
339 :returns: None if successful. Raise on error.
340 """
341 self.log.debug("Setting keystone preferred-api-version: '{}'"
342 "".format(api_version))
343
344 config = {'preferred-api-version': api_version}
345 deployment.d.configure('keystone', config)
346 self.keystone_wait_for_propagation(sentry_relation_pairs, api_version)
347
306 def authenticate_cinder_admin(self, keystone_sentry, username, 348 def authenticate_cinder_admin(self, keystone_sentry, username,
307 password, tenant): 349 password, tenant):
308 """Authenticates admin user with cinder.""" 350 """Authenticates admin user with cinder."""
@@ -311,6 +353,37 @@ class OpenStackAmuletUtils(AmuletUtils):
311 ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8')) 353 ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8'))
312 return cinder_client.Client(username, password, tenant, ept) 354 return cinder_client.Client(username, password, tenant, ept)
313 355
356 def authenticate_keystone(self, keystone_ip, username, password,
357 api_version=False, admin_port=False,
358 user_domain_name=None, domain_name=None,
359 project_domain_name=None, project_name=None):
360 """Authenticate with Keystone"""
361 self.log.debug('Authenticating with keystone...')
362 port = 5000
363 if admin_port:
364 port = 35357
365 base_ep = "http://{}:{}".format(keystone_ip.strip().decode('utf-8'),
366 port)
367 if not api_version or api_version == 2:
368 ep = base_ep + "/v2.0"
369 return keystone_client.Client(username=username, password=password,
370 tenant_name=project_name,
371 auth_url=ep)
372 else:
373 ep = base_ep + "/v3"
374 auth = keystone_id_v3.Password(
375 user_domain_name=user_domain_name,
376 username=username,
377 password=password,
378 domain_name=domain_name,
379 project_domain_name=project_domain_name,
380 project_name=project_name,
381 auth_url=ep
382 )
383 return keystone_client_v3.Client(
384 session=keystone_session.Session(auth=auth)
385 )
386
314 def authenticate_keystone_admin(self, keystone_sentry, user, password, 387 def authenticate_keystone_admin(self, keystone_sentry, user, password,
315 tenant=None, api_version=None, 388 tenant=None, api_version=None,
316 keystone_ip=None): 389 keystone_ip=None):
@@ -319,30 +392,28 @@ class OpenStackAmuletUtils(AmuletUtils):
319 if not keystone_ip: 392 if not keystone_ip:
320 keystone_ip = keystone_sentry.info['public-address'] 393 keystone_ip = keystone_sentry.info['public-address']
321 394
322 base_ep = "http://{}:35357".format(keystone_ip.strip().decode('utf-8')) 395 user_domain_name = None
323 if not api_version or api_version == 2: 396 domain_name = None
324 ep = base_ep + "/v2.0" 397 if api_version == 3:
325 return keystone_client.Client(username=user, password=password, 398 user_domain_name = 'admin_domain'
326 tenant_name=tenant, auth_url=ep) 399 domain_name = user_domain_name
327 else: 400
328 ep = base_ep + "/v3" 401 return self.authenticate_keystone(keystone_ip, user, password,
329 auth = keystone_id_v3.Password( 402 project_name=tenant,
330 user_domain_name='admin_domain', 403 api_version=api_version,
331 username=user, 404 user_domain_name=user_domain_name,
332 password=password, 405 domain_name=domain_name,
333 domain_name='admin_domain', 406 admin_port=True)
334 auth_url=ep,
335 )
336 sess = keystone_session.Session(auth=auth)
337 return keystone_client_v3.Client(session=sess)
338 407
339 def authenticate_keystone_user(self, keystone, user, password, tenant): 408 def authenticate_keystone_user(self, keystone, user, password, tenant):
340 """Authenticates a regular user with the keystone public endpoint.""" 409 """Authenticates a regular user with the keystone public endpoint."""
341 self.log.debug('Authenticating keystone user ({})...'.format(user)) 410 self.log.debug('Authenticating keystone user ({})...'.format(user))
342 ep = keystone.service_catalog.url_for(service_type='identity', 411 ep = keystone.service_catalog.url_for(service_type='identity',
343 endpoint_type='publicURL') 412 endpoint_type='publicURL')
344 return keystone_client.Client(username=user, password=password, 413 keystone_ip = urlparse.urlparse(ep).hostname
345 tenant_name=tenant, auth_url=ep) 414
415 return self.authenticate_keystone(keystone_ip, user, password,
416 project_name=tenant)
346 417
347 def authenticate_glance_admin(self, keystone): 418 def authenticate_glance_admin(self, keystone):
348 """Authenticates admin user with glance.""" 419 """Authenticates admin user with glance."""
@@ -1133,3 +1204,70 @@ class OpenStackAmuletUtils(AmuletUtils):
1133 else: 1204 else:
1134 msg = 'No message retrieved.' 1205 msg = 'No message retrieved.'
1135 amulet.raise_status(amulet.FAIL, msg) 1206 amulet.raise_status(amulet.FAIL, msg)
1207
1208 def validate_memcache(self, sentry_unit, conf, os_release,
1209 earliest_release=5, section='keystone_authtoken',
1210 check_kvs=None):
1211 """Check Memcache is running and is configured to be used
1212
1213 Example call from Amulet test:
1214
1215 def test_110_memcache(self):
1216 u.validate_memcache(self.neutron_api_sentry,
1217 '/etc/neutron/neutron.conf',
1218 self._get_openstack_release())
1219
1220 :param sentry_unit: sentry unit
1221 :param conf: OpenStack config file to check memcache settings
1222 :param os_release: Current OpenStack release int code
1223 :param earliest_release: Earliest Openstack release to check int code
1224 :param section: OpenStack config file section to check
1225 :param check_kvs: Dict of settings to check in config file
1226 :returns: None
1227 """
1228 if os_release < earliest_release:
1229 self.log.debug('Skipping memcache checks for deployment. {} <'
1230 'mitaka'.format(os_release))
1231 return
1232 _kvs = check_kvs or {'memcached_servers': 'inet6:[::1]:11211'}
1233 self.log.debug('Checking memcached is running')
1234 ret = self.validate_services_by_name({sentry_unit: ['memcached']})
1235 if ret:
1236 amulet.raise_status(amulet.FAIL, msg='Memcache running check'
1237 'failed {}'.format(ret))
1238 else:
1239 self.log.debug('OK')
1240 self.log.debug('Checking memcache url is configured in {}'.format(
1241 conf))
1242 if self.validate_config_data(sentry_unit, conf, section, _kvs):
1243 message = "Memcache config error in: {}".format(conf)
1244 amulet.raise_status(amulet.FAIL, msg=message)
1245 else:
1246 self.log.debug('OK')
1247 self.log.debug('Checking memcache configuration in '
1248 '/etc/memcached.conf')
1249 contents = self.file_contents_safe(sentry_unit, '/etc/memcached.conf',
1250 fatal=True)
1251 ubuntu_release, _ = self.run_cmd_unit(sentry_unit, 'lsb_release -cs')
1252 if ubuntu_release <= 'trusty':
1253 memcache_listen_addr = 'ip6-localhost'
1254 else:
1255 memcache_listen_addr = '::1'
1256 expected = {
1257 '-p': '11211',
1258 '-l': memcache_listen_addr}
1259 found = []
1260 for key, value in expected.items():
1261 for line in contents.split('\n'):
1262 if line.startswith(key):
1263 self.log.debug('Checking {} is set to {}'.format(
1264 key,
1265 value))
1266 assert value == line.split()[-1]
1267 self.log.debug(line.split()[-1])
1268 found.append(key)
1269 if sorted(found) == sorted(expected.keys()):
1270 self.log.debug('OK')
1271 else:
1272 message = "Memcache config error in: /etc/memcached.conf"
1273 amulet.raise_status(amulet.FAIL, msg=message)
diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py
index d5b3a33..4231633 100644
--- a/hooks/charmhelpers/contrib/openstack/context.py
+++ b/hooks/charmhelpers/contrib/openstack/context.py
@@ -14,6 +14,7 @@
14 14
15import glob 15import glob
16import json 16import json
17import math
17import os 18import os
18import re 19import re
19import time 20import time
@@ -90,6 +91,9 @@ from charmhelpers.contrib.network.ip import (
90from charmhelpers.contrib.openstack.utils import ( 91from charmhelpers.contrib.openstack.utils import (
91 config_flags_parser, 92 config_flags_parser,
92 get_host_ip, 93 get_host_ip,
94 git_determine_usr_bin,
95 git_determine_python_path,
96 enable_memcache,
93) 97)
94from charmhelpers.core.unitdata import kv 98from charmhelpers.core.unitdata import kv
95 99
@@ -1207,6 +1211,43 @@ class WorkerConfigContext(OSContextGenerator):
1207 return ctxt 1211 return ctxt
1208 1212
1209 1213
1214class WSGIWorkerConfigContext(WorkerConfigContext):
1215
1216 def __init__(self, name=None, script=None, admin_script=None,
1217 public_script=None, process_weight=1.00,
1218 admin_process_weight=0.75, public_process_weight=0.25):
1219 self.service_name = name
1220 self.user = name
1221 self.group = name
1222 self.script = script
1223 self.admin_script = admin_script
1224 self.public_script = public_script
1225 self.process_weight = process_weight
1226 self.admin_process_weight = admin_process_weight
1227 self.public_process_weight = public_process_weight
1228
1229 def __call__(self):
1230 multiplier = config('worker-multiplier') or 1
1231 total_processes = self.num_cpus * multiplier
1232 ctxt = {
1233 "service_name": self.service_name,
1234 "user": self.user,
1235 "group": self.group,
1236 "script": self.script,
1237 "admin_script": self.admin_script,
1238 "public_script": self.public_script,
1239 "processes": int(math.ceil(self.process_weight * total_processes)),
1240 "admin_processes": int(math.ceil(self.admin_process_weight *
1241 total_processes)),
1242 "public_processes": int(math.ceil(self.public_process_weight *
1243 total_processes)),
1244 "threads": 1,
1245 "usr_bin": git_determine_usr_bin(),
1246 "python_path": git_determine_python_path(),
1247 }
1248 return ctxt
1249
1250
1210class ZeroMQContext(OSContextGenerator): 1251class ZeroMQContext(OSContextGenerator):
1211 interfaces = ['zeromq-configuration'] 1252 interfaces = ['zeromq-configuration']
1212 1253
@@ -1512,3 +1553,36 @@ class AppArmorContext(OSContextGenerator):
1512 "".format(self.ctxt['aa_profile'], 1553 "".format(self.ctxt['aa_profile'],
1513 self.ctxt['aa_profile_mode'])) 1554 self.ctxt['aa_profile_mode']))
1514 raise e 1555 raise e
1556
1557
1558class MemcacheContext(OSContextGenerator):
1559 """Memcache context
1560
1561 This context provides options for configuring a local memcache client and
1562 server
1563 """
1564
1565 def __init__(self, package=None):
1566 """
1567 @param package: Package to examine to extrapolate OpenStack release.
1568 Used when charms have no openstack-origin config
1569 option (ie subordinates)
1570 """
1571 self.package = package
1572
1573 def __call__(self):
1574 ctxt = {}
1575 ctxt['use_memcache'] = enable_memcache(package=self.package)
1576 if ctxt['use_memcache']:
1577 # Trusty version of memcached does not support ::1 as a listen
1578 # address so use host file entry instead
1579 if lsb_release()['DISTRIB_CODENAME'].lower() > 'trusty':
1580 ctxt['memcache_server'] = '::1'
1581 else:
1582 ctxt['memcache_server'] = 'ip6-localhost'
1583 ctxt['memcache_server_formatted'] = '[::1]'
1584 ctxt['memcache_port'] = '11211'
1585 ctxt['memcache_url'] = 'inet6:{}:{}'.format(
1586 ctxt['memcache_server_formatted'],
1587 ctxt['memcache_port'])
1588 return ctxt
diff --git a/hooks/charmhelpers/contrib/openstack/templates/memcached.conf b/hooks/charmhelpers/contrib/openstack/templates/memcached.conf
new file mode 100644
index 0000000..26cb037
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/templates/memcached.conf
@@ -0,0 +1,53 @@
1###############################################################################
2# [ WARNING ]
3# memcached configuration file maintained by Juju
4# local changes may be overwritten.
5###############################################################################
6
7# memcached default config file
8# 2003 - Jay Bonci <jaybonci@debian.org>
9# This configuration file is read by the start-memcached script provided as
10# part of the Debian GNU/Linux distribution.
11
12# Run memcached as a daemon. This command is implied, and is not needed for the
13# daemon to run. See the README.Debian that comes with this package for more
14# information.
15-d
16
17# Log memcached's output to /var/log/memcached
18logfile /var/log/memcached.log
19
20# Be verbose
21# -v
22
23# Be even more verbose (print client commands as well)
24# -vv
25
26# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default
27# Note that the daemon will grow to this size, but does not start out holding this much
28# memory
29-m 64
30
31# Default connection port is 11211
32-p {{ memcache_port }}
33
34# Run the daemon as root. The start-memcached will default to running as root if no
35# -u command is present in this config file
36-u memcache
37
38# Specify which IP address to listen on. The default is to listen on all IP addresses
39# This parameter is one of the only security measures that memcached has, so make sure
40# it's listening on a firewalled interface.
41-l {{ memcache_server }}
42
43# Limit the number of simultaneous incoming connections. The daemon default is 1024
44# -c 1024
45
46# Lock down all paged memory. Consult with the README and homepage before you do this
47# -k
48
49# Return error when memory is exhausted (rather than removing items)
50# -M
51
52# Maximize core file limit
53# -r
diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken-mitaka b/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken-mitaka
index 7c6f0c3..8e6889e 100644
--- a/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken-mitaka
+++ b/hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken-mitaka
@@ -14,4 +14,7 @@ project_name = {{ admin_tenant_name }}
14username = {{ admin_user }} 14username = {{ admin_user }}
15password = {{ admin_password }} 15password = {{ admin_password }}
16signing_dir = {{ signing_dir }} 16signing_dir = {{ signing_dir }}
17{% if use_memcache == true %}
18memcached_servers = {{ memcache_url }}
19{% endif -%}
17{% endif -%} 20{% endif -%}
diff --git a/hooks/charmhelpers/contrib/openstack/templates/wsgi-openstack-api.conf b/hooks/charmhelpers/contrib/openstack/templates/wsgi-openstack-api.conf
new file mode 100644
index 0000000..315b2a3
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/templates/wsgi-openstack-api.conf
@@ -0,0 +1,100 @@
1# Configuration file maintained by Juju. Local changes may be overwritten.
2
3{% if port -%}
4Listen {{ port }}
5{% endif -%}
6
7{% if admin_port -%}
8Listen {{ admin_port }}
9{% endif -%}
10
11{% if public_port -%}
12Listen {{ public_port }}
13{% endif -%}
14
15{% if port -%}
16<VirtualHost *:{{ port }}>
17 WSGIDaemonProcess {{ service_name }} processes={{ processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \
18{% if python_path -%}
19 python-path={{ python_path }} \
20{% endif -%}
21 display-name=%{GROUP}
22 WSGIProcessGroup {{ service_name }}
23 WSGIScriptAlias / {{ script }}
24 WSGIApplicationGroup %{GLOBAL}
25 WSGIPassAuthorization On
26 <IfVersion >= 2.4>
27 ErrorLogFormat "%{cu}t %M"
28 </IfVersion>
29 ErrorLog /var/log/apache2/{{ service_name }}_error.log
30 CustomLog /var/log/apache2/{{ service_name }}_access.log combined
31
32 <Directory {{ usr_bin }}>
33 <IfVersion >= 2.4>
34 Require all granted
35 </IfVersion>
36 <IfVersion < 2.4>
37 Order allow,deny
38 Allow from all
39 </IfVersion>
40 </Directory>
41</VirtualHost>
42{% endif -%}
43
44{% if admin_port -%}
45<VirtualHost *:{{ admin_port }}>
46 WSGIDaemonProcess {{ service_name }}-admin processes={{ admin_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \
47{% if python_path -%}
48 python-path={{ python_path }} \
49{% endif -%}
50 display-name=%{GROUP}
51 WSGIProcessGroup {{ service_name }}-admin
52 WSGIScriptAlias / {{ admin_script }}
53 WSGIApplicationGroup %{GLOBAL}
54 WSGIPassAuthorization On
55 <IfVersion >= 2.4>
56 ErrorLogFormat "%{cu}t %M"
57 </IfVersion>
58 ErrorLog /var/log/apache2/{{ service_name }}_error.log
59 CustomLog /var/log/apache2/{{ service_name }}_access.log combined
60
61 <Directory {{ usr_bin }}>
62 <IfVersion >= 2.4>
63 Require all granted
64 </IfVersion>
65 <IfVersion < 2.4>
66 Order allow,deny
67 Allow from all
68 </IfVersion>
69 </Directory>
70</VirtualHost>
71{% endif -%}
72
73{% if public_port -%}
74<VirtualHost *:{{ public_port }}>
75 WSGIDaemonProcess {{ service_name }}-public processes={{ public_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \
76{% if python_path -%}
77 python-path={{ python_path }} \
78{% endif -%}
79 display-name=%{GROUP}
80 WSGIProcessGroup {{ service_name }}-public
81 WSGIScriptAlias / {{ public_script }}
82 WSGIApplicationGroup %{GLOBAL}
83 WSGIPassAuthorization On
84 <IfVersion >= 2.4>
85 ErrorLogFormat "%{cu}t %M"
86 </IfVersion>
87 ErrorLog /var/log/apache2/{{ service_name }}_error.log
88 CustomLog /var/log/apache2/{{ service_name }}_access.log combined
89
90 <Directory {{ usr_bin }}>
91 <IfVersion >= 2.4>
92 Require all granted
93 </IfVersion>
94 <IfVersion < 2.4>
95 Order allow,deny
96 Allow from all
97 </IfVersion>
98 </Directory>
99</VirtualHost>
100{% endif -%}
diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py
index 6d544e7..80219d6 100644
--- a/hooks/charmhelpers/contrib/openstack/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/utils.py
@@ -153,7 +153,7 @@ SWIFT_CODENAMES = OrderedDict([
153 ('newton', 153 ('newton',
154 ['2.8.0', '2.9.0', '2.10.0']), 154 ['2.8.0', '2.9.0', '2.10.0']),
155 ('ocata', 155 ('ocata',
156 ['2.11.0']), 156 ['2.11.0', '2.12.0']),
157]) 157])
158 158
159# >= Liberty version->codename mapping 159# >= Liberty version->codename mapping
@@ -549,9 +549,9 @@ def configure_installation_source(rel):
549 'newton': 'xenial-updates/newton', 549 'newton': 'xenial-updates/newton',
550 'newton/updates': 'xenial-updates/newton', 550 'newton/updates': 'xenial-updates/newton',
551 'newton/proposed': 'xenial-proposed/newton', 551 'newton/proposed': 'xenial-proposed/newton',
552 'zesty': 'zesty-updates/ocata', 552 'ocata': 'xenial-updates/ocata',
553 'zesty/updates': 'xenial-updates/ocata', 553 'ocata/updates': 'xenial-updates/ocata',
554 'zesty/proposed': 'xenial-proposed/ocata', 554 'ocata/proposed': 'xenial-proposed/ocata',
555 } 555 }
556 556
557 try: 557 try:
@@ -1119,6 +1119,35 @@ def git_generate_systemd_init_files(templates_dir):
1119 shutil.copyfile(service_source, service_dest) 1119 shutil.copyfile(service_source, service_dest)
1120 1120
1121 1121
1122def git_determine_usr_bin():
1123 """Return the /usr/bin path for Apache2 config.
1124
1125 The /usr/bin path will be located in the virtualenv if the charm
1126 is configured to deploy from source.
1127 """
1128 if git_install_requested():
1129 projects_yaml = config('openstack-origin-git')
1130 projects_yaml = git_default_repos(projects_yaml)
1131 return os.path.join(git_pip_venv_dir(projects_yaml), 'bin')
1132 else:
1133 return '/usr/bin'
1134
1135
1136def git_determine_python_path():
1137 """Return the python-path for Apache2 config.
1138
1139 Returns 'None' unless the charm is configured to deploy from source,
1140 in which case the path of the virtualenv's site-packages is returned.
1141 """
1142 if git_install_requested():
1143 projects_yaml = config('openstack-origin-git')
1144 projects_yaml = git_default_repos(projects_yaml)
1145 return os.path.join(git_pip_venv_dir(projects_yaml),
1146 'lib/python2.7/site-packages')
1147 else:
1148 return None
1149
1150
1122def os_workload_status(configs, required_interfaces, charm_func=None): 1151def os_workload_status(configs, required_interfaces, charm_func=None):
1123 """ 1152 """
1124 Decorator to set workload status based on complete contexts 1153 Decorator to set workload status based on complete contexts
@@ -1925,3 +1954,36 @@ def os_application_version_set(package):
1925 application_version_set(os_release(package)) 1954 application_version_set(os_release(package))
1926 else: 1955 else:
1927 application_version_set(application_version) 1956 application_version_set(application_version)
1957
1958
1959def enable_memcache(source=None, release=None, package=None):
1960 """Determine if memcache should be enabled on the local unit
1961
1962 @param release: release of OpenStack currently deployed
1963 @param package: package to derive OpenStack version deployed
1964 @returns boolean Whether memcache should be enabled
1965 """
1966 _release = None
1967 if release:
1968 _release = release
1969 else:
1970 _release = os_release(package, base='icehouse')
1971 if not _release:
1972 _release = get_os_codename_install_source(source)
1973
1974 # TODO: this should be changed to a numeric comparison using a known list
1975 # of releases and comparing by index.
1976 return _release >= 'mitaka'
1977
1978
1979def token_cache_pkgs(source=None, release=None):
1980 """Determine additional packages needed for token caching
1981
1982 @param source: source string for charm
1983 @param release: release of OpenStack currently deployed
1984 @returns List of package to enable token caching
1985 """
1986 packages = []
1987 if enable_memcache(source=source, release=release):
1988 packages.extend(['memcached', 'python-memcache'])
1989 return packages
diff --git a/hooks/charmhelpers/contrib/storage/linux/ceph.py b/hooks/charmhelpers/contrib/storage/linux/ceph.py
index edb536c..ae7f3f9 100644
--- a/hooks/charmhelpers/contrib/storage/linux/ceph.py
+++ b/hooks/charmhelpers/contrib/storage/linux/ceph.py
@@ -40,6 +40,7 @@ from subprocess import (
40) 40)
41from charmhelpers.core.hookenv import ( 41from charmhelpers.core.hookenv import (
42 config, 42 config,
43 service_name,
43 local_unit, 44 local_unit,
44 relation_get, 45 relation_get,
45 relation_ids, 46 relation_ids,
@@ -1043,8 +1044,18 @@ class CephBrokerRq(object):
1043 self.request_id = str(uuid.uuid1()) 1044 self.request_id = str(uuid.uuid1())
1044 self.ops = [] 1045 self.ops = []
1045 1046
1047 def add_op_request_access_to_group(self, name, namespace=None,
1048 permission=None, key_name=None):
1049 """
1050 Adds the requested permissions to the current service's Ceph key,
1051 allowing the key to access only the specified pools
1052 """
1053 self.ops.append({'op': 'add-permissions-to-key', 'group': name,
1054 'namespace': namespace, 'name': key_name or service_name(),
1055 'group-permission': permission})
1056
1046 def add_op_create_pool(self, name, replica_count=3, pg_num=None, 1057 def add_op_create_pool(self, name, replica_count=3, pg_num=None,
1047 weight=None): 1058 weight=None, group=None, namespace=None):
1048 """Adds an operation to create a pool. 1059 """Adds an operation to create a pool.
1049 1060
1050 @param pg_num setting: optional setting. If not provided, this value 1061 @param pg_num setting: optional setting. If not provided, this value
@@ -1058,7 +1069,8 @@ class CephBrokerRq(object):
1058 1069
1059 self.ops.append({'op': 'create-pool', 'name': name, 1070 self.ops.append({'op': 'create-pool', 'name': name,
1060 'replicas': replica_count, 'pg_num': pg_num, 1071 'replicas': replica_count, 'pg_num': pg_num,
1061 'weight': weight}) 1072 'weight': weight, 'group': group,
1073 'group-namespace': namespace})
1062 1074
1063 def set_ops(self, ops): 1075 def set_ops(self, ops):
1064 """Set request ops to provided value. 1076 """Set request ops to provided value.
diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py
index 94fc996..e44e22b 100644
--- a/hooks/charmhelpers/core/hookenv.py
+++ b/hooks/charmhelpers/core/hookenv.py
@@ -616,6 +616,20 @@ def close_port(port, protocol="TCP"):
616 subprocess.check_call(_args) 616 subprocess.check_call(_args)
617 617
618 618
619def open_ports(start, end, protocol="TCP"):
620 """Opens a range of service network ports"""
621 _args = ['open-port']
622 _args.append('{}-{}/{}'.format(start, end, protocol))
623 subprocess.check_call(_args)
624
625
626def close_ports(start, end, protocol="TCP"):
627 """Close a range of service network ports"""
628 _args = ['close-port']
629 _args.append('{}-{}/{}'.format(start, end, protocol))
630 subprocess.check_call(_args)
631
632
619@cached 633@cached
620def unit_get(attribute): 634def unit_get(attribute):
621 """Get the unit ID for the remote unit""" 635 """Get the unit ID for the remote unit"""
@@ -1021,3 +1035,34 @@ def network_get_primary_address(binding):
1021 ''' 1035 '''
1022 cmd = ['network-get', '--primary-address', binding] 1036 cmd = ['network-get', '--primary-address', binding]
1023 return subprocess.check_output(cmd).decode('UTF-8').strip() 1037 return subprocess.check_output(cmd).decode('UTF-8').strip()
1038
1039
1040def add_metric(*args, **kwargs):
1041 """Add metric values. Values may be expressed with keyword arguments. For
1042 metric names containing dashes, these may be expressed as one or more
1043 'key=value' positional arguments. May only be called from the collect-metrics
1044 hook."""
1045 _args = ['add-metric']
1046 _kvpairs = []
1047 _kvpairs.extend(args)
1048 _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1049 _args.extend(sorted(_kvpairs))
1050 try:
1051 subprocess.check_call(_args)
1052 return
1053 except EnvironmentError as e:
1054 if e.errno != errno.ENOENT:
1055 raise
1056 log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1057 log(log_message, level='INFO')
1058
1059
1060def meter_status():
1061 """Get the meter status, if running in the meter-status-changed hook."""
1062 return os.environ.get('JUJU_METER_STATUS')
1063
1064
1065def meter_info():
1066 """Get the meter status information, if running in the meter-status-changed
1067 hook."""
1068 return os.environ.get('JUJU_METER_INFO')
diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py
index 04cadb3..edbb72f 100644
--- a/hooks/charmhelpers/core/host.py
+++ b/hooks/charmhelpers/core/host.py
@@ -54,38 +54,138 @@ elif __platform__ == "centos":
54 cmp_pkgrevno, 54 cmp_pkgrevno,
55 ) # flake8: noqa -- ignore F401 for this import 55 ) # flake8: noqa -- ignore F401 for this import
56 56
57UPDATEDB_PATH = '/etc/updatedb.conf'
58
59def service_start(service_name, **kwargs):
60 """Start a system service.
61
62 The specified service name is managed via the system level init system.
63 Some init systems (e.g. upstart) require that additional arguments be
64 provided in order to directly control service instances whereas other init
65 systems allow for addressing instances of a service directly by name (e.g.
66 systemd).
67
68 The kwargs allow for the additional parameters to be passed to underlying
69 init systems for those systems which require/allow for them. For example,
70 the ceph-osd upstart script requires the id parameter to be passed along
71 in order to identify which running daemon should be reloaded. The follow-
72 ing example stops the ceph-osd service for instance id=4:
73
74 service_stop('ceph-osd', id=4)
75
76 :param service_name: the name of the service to stop
77 :param **kwargs: additional parameters to pass to the init system when
78 managing services. These will be passed as key=value
79 parameters to the init system's commandline. kwargs
80 are ignored for systemd enabled systems.
81 """
82 return service('start', service_name, **kwargs)
83
84
85def service_stop(service_name, **kwargs):
86 """Stop a system service.
87
88 The specified service name is managed via the system level init system.
89 Some init systems (e.g. upstart) require that additional arguments be
90 provided in order to directly control service instances whereas other init
91 systems allow for addressing instances of a service directly by name (e.g.
92 systemd).
57 93
58def service_start(service_name): 94 The kwargs allow for the additional parameters to be passed to underlying
59 """Start a system service""" 95 init systems for those systems which require/allow for them. For example,
60 return service('start', service_name) 96 the ceph-osd upstart script requires the id parameter to be passed along
97 in order to identify which running daemon should be reloaded. The follow-
98 ing example stops the ceph-osd service for instance id=4:
99
100 service_stop('ceph-osd', id=4)
101
102 :param service_name: the name of the service to stop
103 :param **kwargs: additional parameters to pass to the init system when
104 managing services. These will be passed as key=value
105 parameters to the init system's commandline. kwargs
106 are ignored for systemd enabled systems.
107 """
108 return service('stop', service_name, **kwargs)
61 109
62 110
63def service_stop(service_name): 111def service_restart(service_name, **kwargs):
64 """Stop a system service""" 112 """Restart a system service.
65 return service('stop', service_name)
66 113
114 The specified service name is managed via the system level init system.
115 Some init systems (e.g. upstart) require that additional arguments be
116 provided in order to directly control service instances whereas other init
117 systems allow for addressing instances of a service directly by name (e.g.
118 systemd).
67 119
68def service_restart(service_name): 120 The kwargs allow for the additional parameters to be passed to underlying
69 """Restart a system service""" 121 init systems for those systems which require/allow for them. For example,
122 the ceph-osd upstart script requires the id parameter to be passed along
123 in order to identify which running daemon should be restarted. The follow-
124 ing example restarts the ceph-osd service for instance id=4:
125
126 service_restart('ceph-osd', id=4)
127
128 :param service_name: the name of the service to restart
129 :param **kwargs: additional parameters to pass to the init system when
130 managing services. These will be passed as key=value
131 parameters to the init system's commandline. kwargs
132 are ignored for init systems not allowing additional
133 parameters via the commandline (systemd).
134 """
70 return service('restart', service_name) 135 return service('restart', service_name)
71 136
72 137
73def service_reload(service_name, restart_on_failure=False): 138def service_reload(service_name, restart_on_failure=False, **kwargs):
74 """Reload a system service, optionally falling back to restart if 139 """Reload a system service, optionally falling back to restart if
75 reload fails""" 140 reload fails.
76 service_result = service('reload', service_name) 141
142 The specified service name is managed via the system level init system.
143 Some init systems (e.g. upstart) require that additional arguments be
144 provided in order to directly control service instances whereas other init
145 systems allow for addressing instances of a service directly by name (e.g.
146 systemd).
147
148 The kwargs allow for the additional parameters to be passed to underlying
149 init systems for those systems which require/allow for them. For example,
150 the ceph-osd upstart script requires the id parameter to be passed along
151 in order to identify which running daemon should be reloaded. The follow-
152 ing example restarts the ceph-osd service for instance id=4:
153
154 service_reload('ceph-osd', id=4)
155
156 :param service_name: the name of the service to reload
157 :param restart_on_failure: boolean indicating whether to fallback to a
158 restart if the reload fails.
159 :param **kwargs: additional parameters to pass to the init system when
160 managing services. These will be passed as key=value
161 parameters to the init system's commandline. kwargs
162 are ignored for init systems not allowing additional
163 parameters via the commandline (systemd).
164 """
165 service_result = service('reload', service_name, **kwargs)
77 if not service_result and restart_on_failure: 166 if not service_result and restart_on_failure:
78 service_result = service('restart', service_name) 167 service_result = service('restart', service_name, **kwargs)
79 return service_result 168 return service_result
80 169
81 170
82def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"): 171def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
172 **kwargs):
83 """Pause a system service. 173 """Pause a system service.
84 174
85 Stop it, and prevent it from starting again at boot.""" 175 Stop it, and prevent it from starting again at boot.
176
177 :param service_name: the name of the service to pause
178 :param init_dir: path to the upstart init directory
179 :param initd_dir: path to the sysv init directory
180 :param **kwargs: additional parameters to pass to the init system when
181 managing services. These will be passed as key=value
182 parameters to the init system's commandline. kwargs
183 are ignored for init systems which do not support
184 key=value arguments via the commandline.
185 """
86 stopped = True 186 stopped = True
87 if service_running(service_name): 187 if service_running(service_name, **kwargs):
88 stopped = service_stop(service_name) 188 stopped = service_stop(service_name, **kwargs)
89 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) 189 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
90 sysv_file = os.path.join(initd_dir, service_name) 190 sysv_file = os.path.join(initd_dir, service_name)
91 if init_is_systemd(): 191 if init_is_systemd():
@@ -106,10 +206,19 @@ def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
106 206
107 207
108def service_resume(service_name, init_dir="/etc/init", 208def service_resume(service_name, init_dir="/etc/init",
109 initd_dir="/etc/init.d"): 209 initd_dir="/etc/init.d", **kwargs):
110 """Resume a system service. 210 """Resume a system service.
111 211
112 Reenable starting again at boot. Start the service""" 212 Reenable starting again at boot. Start the service.
213
214 :param service_name: the name of the service to resume
215 :param init_dir: the path to the init dir
216 :param initd dir: the path to the initd dir
217 :param **kwargs: additional parameters to pass to the init system when
218 managing services. These will be passed as key=value
219 parameters to the init system's commandline. kwargs
220 are ignored for systemd enabled systems.
221 """
113 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) 222 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
114 sysv_file = os.path.join(initd_dir, service_name) 223 sysv_file = os.path.join(initd_dir, service_name)
115 if init_is_systemd(): 224 if init_is_systemd():
@@ -126,19 +235,28 @@ def service_resume(service_name, init_dir="/etc/init",
126 "Unable to detect {0} as SystemD, Upstart {1} or" 235 "Unable to detect {0} as SystemD, Upstart {1} or"
127 " SysV {2}".format( 236 " SysV {2}".format(
128 service_name, upstart_file, sysv_file)) 237 service_name, upstart_file, sysv_file))
238 started = service_running(service_name, **kwargs)
129 239
130 started = service_running(service_name)
131 if not started: 240 if not started:
132 started = service_start(service_name) 241 started = service_start(service_name, **kwargs)
133 return started 242 return started
134 243
135 244
136def service(action, service_name): 245def service(action, service_name, **kwargs):
137 """Control a system service""" 246 """Control a system service.
247
248 :param action: the action to take on the service
249 :param service_name: the name of the service to perform th action on
250 :param **kwargs: additional params to be passed to the service command in
251 the form of key=value.
252 """
138 if init_is_systemd(): 253 if init_is_systemd():
139 cmd = ['systemctl', action, service_name] 254 cmd = ['systemctl', action, service_name]
140 else: 255 else:
141 cmd = ['service', service_name, action] 256 cmd = ['service', service_name, action]
257 for key, value in six.iteritems(kwargs):
258 parameter = '%s=%s' % (key, value)
259 cmd.append(parameter)
142 return subprocess.call(cmd) == 0 260 return subprocess.call(cmd) == 0
143 261
144 262
@@ -146,15 +264,26 @@ _UPSTART_CONF = "/etc/init/{}.conf"
146_INIT_D_CONF = "/etc/init.d/{}" 264_INIT_D_CONF = "/etc/init.d/{}"
147 265
148 266
149def service_running(service_name): 267def service_running(service_name, **kwargs):
150 """Determine whether a system service is running""" 268 """Determine whether a system service is running.
269
270 :param service_name: the name of the service
271 :param **kwargs: additional args to pass to the service command. This is
272 used to pass additional key=value arguments to the
273 service command line for managing specific instance
274 units (e.g. service ceph-osd status id=2). The kwargs
275 are ignored in systemd services.
276 """
151 if init_is_systemd(): 277 if init_is_systemd():
152 return service('is-active', service_name) 278 return service('is-active', service_name)
153 else: 279 else:
154 if os.path.exists(_UPSTART_CONF.format(service_name)): 280 if os.path.exists(_UPSTART_CONF.format(service_name)):
155 try: 281 try:
156 output = subprocess.check_output( 282 cmd = ['status', service_name]
157 ['status', service_name], 283 for key, value in six.iteritems(kwargs):
284 parameter = '%s=%s' % (key, value)
285 cmd.append(parameter)
286 output = subprocess.check_output(cmd,
158 stderr=subprocess.STDOUT).decode('UTF-8') 287 stderr=subprocess.STDOUT).decode('UTF-8')
159 except subprocess.CalledProcessError: 288 except subprocess.CalledProcessError:
160 return False 289 return False
@@ -306,15 +435,17 @@ def add_user_to_group(username, group):
306 subprocess.check_call(cmd) 435 subprocess.check_call(cmd)
307 436
308 437
309def rsync(from_path, to_path, flags='-r', options=None): 438def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
310 """Replicate the contents of a path""" 439 """Replicate the contents of a path"""
311 options = options or ['--delete', '--executability'] 440 options = options or ['--delete', '--executability']
312 cmd = ['/usr/bin/rsync', flags] 441 cmd = ['/usr/bin/rsync', flags]
442 if timeout:
443 cmd = ['timeout', str(timeout)] + cmd
313 cmd.extend(options) 444 cmd.extend(options)
314 cmd.append(from_path) 445 cmd.append(from_path)
315 cmd.append(to_path) 446 cmd.append(to_path)
316 log(" ".join(cmd)) 447 log(" ".join(cmd))
317 return subprocess.check_output(cmd).decode('UTF-8').strip() 448 return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
318 449
319 450
320def symlink(source, destination): 451def symlink(source, destination):
@@ -684,7 +815,7 @@ def chownr(path, owner, group, follow_links=True, chowntopdir=False):
684 :param str path: The string path to start changing ownership. 815 :param str path: The string path to start changing ownership.
685 :param str owner: The owner string to use when looking up the uid. 816 :param str owner: The owner string to use when looking up the uid.
686 :param str group: The group string to use when looking up the gid. 817 :param str group: The group string to use when looking up the gid.
687 :param bool follow_links: Also Chown links if True 818 :param bool follow_links: Also follow and chown links if True
688 :param bool chowntopdir: Also chown path itself if True 819 :param bool chowntopdir: Also chown path itself if True
689 """ 820 """
690 uid = pwd.getpwnam(owner).pw_uid 821 uid = pwd.getpwnam(owner).pw_uid
@@ -698,7 +829,7 @@ def chownr(path, owner, group, follow_links=True, chowntopdir=False):
698 broken_symlink = os.path.lexists(path) and not os.path.exists(path) 829 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
699 if not broken_symlink: 830 if not broken_symlink:
700 chown(path, uid, gid) 831 chown(path, uid, gid)
701 for root, dirs, files in os.walk(path): 832 for root, dirs, files in os.walk(path, followlinks=follow_links):
702 for name in dirs + files: 833 for name in dirs + files:
703 full = os.path.join(root, name) 834 full = os.path.join(root, name)
704 broken_symlink = os.path.lexists(full) and not os.path.exists(full) 835 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
@@ -718,6 +849,20 @@ def lchownr(path, owner, group):
718 chownr(path, owner, group, follow_links=False) 849 chownr(path, owner, group, follow_links=False)
719 850
720 851
852def owner(path):
853 """Returns a tuple containing the username & groupname owning the path.
854
855 :param str path: the string path to retrieve the ownership
856 :return tuple(str, str): A (username, groupname) tuple containing the
857 name of the user and group owning the path.
858 :raises OSError: if the specified path does not exist
859 """
860 stat = os.stat(path)
861 username = pwd.getpwuid(stat.st_uid)[0]
862 groupname = grp.getgrgid(stat.st_gid)[0]
863 return username, groupname
864
865
721def get_total_ram(): 866def get_total_ram():
722 """The total amount of system RAM in bytes. 867 """The total amount of system RAM in bytes.
723 868
@@ -749,3 +894,25 @@ def is_container():
749 else: 894 else:
750 # Detect using upstart container file marker 895 # Detect using upstart container file marker
751 return os.path.exists(UPSTART_CONTAINER_TYPE) 896 return os.path.exists(UPSTART_CONTAINER_TYPE)
897
898
899def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
900 with open(updatedb_path, 'r+') as f_id:
901 updatedb_text = f_id.read()
902 output = updatedb(updatedb_text, path)
903 f_id.seek(0)
904 f_id.write(output)
905 f_id.truncate()
906
907
908def updatedb(updatedb_text, new_path):
909 lines = [line for line in updatedb_text.split("\n")]
910 for i, line in enumerate(lines):
911 if line.startswith("PRUNEPATHS="):
912 paths_line = line.split("=")[1].replace('"', '')
913 paths = paths_line.split(" ")
914 if new_path not in paths:
915 paths.append(new_path)
916 lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
917 output = "\n".join(lines)
918 return output
diff --git a/hooks/charmhelpers/osplatform.py b/hooks/charmhelpers/osplatform.py
index ea490bb..d9a4d5c 100644
--- a/hooks/charmhelpers/osplatform.py
+++ b/hooks/charmhelpers/osplatform.py
@@ -8,12 +8,18 @@ def get_platform():
8 will be returned (which is the name of the module). 8 will be returned (which is the name of the module).
9 This string is used to decide which platform module should be imported. 9 This string is used to decide which platform module should be imported.
10 """ 10 """
11 # linux_distribution is deprecated and will be removed in Python 3.7
12 # Warings *not* disabled, as we certainly need to fix this.
11 tuple_platform = platform.linux_distribution() 13 tuple_platform = platform.linux_distribution()
12 current_platform = tuple_platform[0] 14 current_platform = tuple_platform[0]
13 if "Ubuntu" in current_platform: 15 if "Ubuntu" in current_platform:
14 return "ubuntu" 16 return "ubuntu"
15 elif "CentOS" in current_platform: 17 elif "CentOS" in current_platform:
16 return "centos" 18 return "centos"
19 elif "debian" in current_platform:
20 # Stock Python does not detect Ubuntu and instead returns debian.
21 # Or at least it does in some build environments like Travis CI
22 return "ubuntu"
17 else: 23 else:
18 raise RuntimeError("This module is not supported on {}." 24 raise RuntimeError("This module is not supported on {}."
19 .format(current_platform)) 25 .format(current_platform))
diff --git a/tests/charmhelpers/contrib/amulet/utils.py b/tests/charmhelpers/contrib/amulet/utils.py
index 8e13ab1..f9e4c3a 100644
--- a/tests/charmhelpers/contrib/amulet/utils.py
+++ b/tests/charmhelpers/contrib/amulet/utils.py
@@ -148,7 +148,8 @@ class AmuletUtils(object):
148 148
149 for service_name in services_list: 149 for service_name in services_list:
150 if (self.ubuntu_releases.index(release) >= systemd_switch or 150 if (self.ubuntu_releases.index(release) >= systemd_switch or
151 service_name in ['rabbitmq-server', 'apache2']): 151 service_name in ['rabbitmq-server', 'apache2',
152 'memcached']):
152 # init is systemd (or regular sysv) 153 # init is systemd (or regular sysv)
153 cmd = 'sudo service {} status'.format(service_name) 154 cmd = 'sudo service {} status'.format(service_name)
154 output, code = sentry_unit.run(cmd) 155 output, code = sentry_unit.run(cmd)
diff --git a/tests/charmhelpers/contrib/openstack/amulet/utils.py b/tests/charmhelpers/contrib/openstack/amulet/utils.py
index 6a0ba83..401c032 100644
--- a/tests/charmhelpers/contrib/openstack/amulet/utils.py
+++ b/tests/charmhelpers/contrib/openstack/amulet/utils.py
@@ -20,6 +20,7 @@ import re
20import six 20import six
21import time 21import time
22import urllib 22import urllib
23import urlparse
23 24
24import cinderclient.v1.client as cinder_client 25import cinderclient.v1.client as cinder_client
25import glanceclient.v1.client as glance_client 26import glanceclient.v1.client as glance_client
@@ -37,6 +38,7 @@ import swiftclient
37from charmhelpers.contrib.amulet.utils import ( 38from charmhelpers.contrib.amulet.utils import (
38 AmuletUtils 39 AmuletUtils
39) 40)
41from charmhelpers.core.decorators import retry_on_exception
40 42
41DEBUG = logging.DEBUG 43DEBUG = logging.DEBUG
42ERROR = logging.ERROR 44ERROR = logging.ERROR
@@ -303,6 +305,46 @@ class OpenStackAmuletUtils(AmuletUtils):
303 self.log.debug('Checking if tenant exists ({})...'.format(tenant)) 305 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
304 return tenant in [t.name for t in keystone.tenants.list()] 306 return tenant in [t.name for t in keystone.tenants.list()]
305 307
308 @retry_on_exception(5, base_delay=10)
309 def keystone_wait_for_propagation(self, sentry_relation_pairs,
310 api_version):
311 """Iterate over list of sentry and relation tuples and verify that
312 api_version has the expected value.
313
314 :param sentry_relation_pairs: list of sentry, relation name tuples used
315 for monitoring propagation of relation
316 data
317 :param api_version: api_version to expect in relation data
318 :returns: None if successful. Raise on error.
319 """
320 for (sentry, relation_name) in sentry_relation_pairs:
321 rel = sentry.relation('identity-service',
322 relation_name)
323 self.log.debug('keystone relation data: {}'.format(rel))
324 if rel['api_version'] != str(api_version):
325 raise Exception("api_version not propagated through relation"
326 " data yet ('{}' != '{}')."
327 "".format(rel['api_version'], api_version))
328
329 def keystone_configure_api_version(self, sentry_relation_pairs, deployment,
330 api_version):
331 """Configure preferred-api-version of keystone in deployment and
332 monitor provided list of relation objects for propagation
333 before returning to caller.
334
335 :param sentry_relation_pairs: list of sentry, relation tuples used for
336 monitoring propagation of relation data
337 :param deployment: deployment to configure
338 :param api_version: value preferred-api-version will be set to
339 :returns: None if successful. Raise on error.
340 """
341 self.log.debug("Setting keystone preferred-api-version: '{}'"
342 "".format(api_version))
343
344 config = {'preferred-api-version': api_version}
345 deployment.d.configure('keystone', config)
346 self.keystone_wait_for_propagation(sentry_relation_pairs, api_version)
347
306 def authenticate_cinder_admin(self, keystone_sentry, username, 348 def authenticate_cinder_admin(self, keystone_sentry, username,
307 password, tenant): 349 password, tenant):
308 """Authenticates admin user with cinder.""" 350 """Authenticates admin user with cinder."""
@@ -311,6 +353,37 @@ class OpenStackAmuletUtils(AmuletUtils):
311 ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8')) 353 ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8'))
312 return cinder_client.Client(username, password, tenant, ept) 354 return cinder_client.Client(username, password, tenant, ept)
313 355
356 def authenticate_keystone(self, keystone_ip, username, password,
357 api_version=False, admin_port=False,
358 user_domain_name=None, domain_name=None,
359 project_domain_name=None, project_name=None):
360 """Authenticate with Keystone"""
361 self.log.debug('Authenticating with keystone...')
362 port = 5000
363 if admin_port:
364 port = 35357
365 base_ep = "http://{}:{}".format(keystone_ip.strip().decode('utf-8'),
366 port)
367 if not api_version or api_version == 2:
368 ep = base_ep + "/v2.0"
369 return keystone_client.Client(username=username, password=password,
370 tenant_name=project_name,
371 auth_url=ep)
372 else:
373 ep = base_ep + "/v3"
374 auth = keystone_id_v3.Password(
375 user_domain_name=user_domain_name,
376 username=username,
377 password=password,
378 domain_name=domain_name,
379 project_domain_name=project_domain_name,
380 project_name=project_name,
381 auth_url=ep
382 )
383 return keystone_client_v3.Client(
384 session=keystone_session.Session(auth=auth)
385 )
386
314 def authenticate_keystone_admin(self, keystone_sentry, user, password, 387 def authenticate_keystone_admin(self, keystone_sentry, user, password,
315 tenant=None, api_version=None, 388 tenant=None, api_version=None,
316 keystone_ip=None): 389 keystone_ip=None):
@@ -319,30 +392,28 @@ class OpenStackAmuletUtils(AmuletUtils):
319 if not keystone_ip: 392 if not keystone_ip:
320 keystone_ip = keystone_sentry.info['public-address'] 393 keystone_ip = keystone_sentry.info['public-address']
321 394
322 base_ep = "http://{}:35357".format(keystone_ip.strip().decode('utf-8')) 395 user_domain_name = None
323 if not api_version or api_version == 2: 396 domain_name = None
324 ep = base_ep + "/v2.0" 397 if api_version == 3:
325 return keystone_client.Client(username=user, password=password, 398 user_domain_name = 'admin_domain'
326 tenant_name=tenant, auth_url=ep) 399 domain_name = user_domain_name
327 else: 400
328 ep = base_ep + "/v3" 401 return self.authenticate_keystone(keystone_ip, user, password,
329 auth = keystone_id_v3.Password( 402 project_name=tenant,
330 user_domain_name='admin_domain', 403 api_version=api_version,
331 username=user, 404 user_domain_name=user_domain_name,
332 password=password, 405 domain_name=domain_name,
333 domain_name='admin_domain', 406 admin_port=True)
334 auth_url=ep,
335 )
336 sess = keystone_session.Session(auth=auth)
337 return keystone_client_v3.Client(session=sess)
338 407
339 def authenticate_keystone_user(self, keystone, user, password, tenant): 408 def authenticate_keystone_user(self, keystone, user, password, tenant):
340 """Authenticates a regular user with the keystone public endpoint.""" 409 """Authenticates a regular user with the keystone public endpoint."""
341 self.log.debug('Authenticating keystone user ({})...'.format(user)) 410 self.log.debug('Authenticating keystone user ({})...'.format(user))
342 ep = keystone.service_catalog.url_for(service_type='identity', 411 ep = keystone.service_catalog.url_for(service_type='identity',
343 endpoint_type='publicURL') 412 endpoint_type='publicURL')
344 return keystone_client.Client(username=user, password=password, 413 keystone_ip = urlparse.urlparse(ep).hostname
345 tenant_name=tenant, auth_url=ep) 414
415 return self.authenticate_keystone(keystone_ip, user, password,
416 project_name=tenant)
346 417
347 def authenticate_glance_admin(self, keystone): 418 def authenticate_glance_admin(self, keystone):
348 """Authenticates admin user with glance.""" 419 """Authenticates admin user with glance."""
@@ -1133,3 +1204,70 @@ class OpenStackAmuletUtils(AmuletUtils):
1133 else: 1204 else:
1134 msg = 'No message retrieved.' 1205 msg = 'No message retrieved.'
1135 amulet.raise_status(amulet.FAIL, msg) 1206 amulet.raise_status(amulet.FAIL, msg)
1207
1208 def validate_memcache(self, sentry_unit, conf, os_release,
1209 earliest_release=5, section='keystone_authtoken',
1210 check_kvs=None):
1211 """Check Memcache is running and is configured to be used
1212
1213 Example call from Amulet test:
1214
1215 def test_110_memcache(self):
1216 u.validate_memcache(self.neutron_api_sentry,
1217 '/etc/neutron/neutron.conf',
1218 self._get_openstack_release())
1219
1220 :param sentry_unit: sentry unit
1221 :param conf: OpenStack config file to check memcache settings
1222 :param os_release: Current OpenStack release int code
1223 :param earliest_release: Earliest Openstack release to check int code
1224 :param section: OpenStack config file section to check
1225 :param check_kvs: Dict of settings to check in config file
1226 :returns: None
1227 """
1228 if os_release < earliest_release:
1229 self.log.debug('Skipping memcache checks for deployment. {} <'
1230 'mitaka'.format(os_release))
1231 return
1232 _kvs = check_kvs or {'memcached_servers': 'inet6:[::1]:11211'}
1233 self.log.debug('Checking memcached is running')
1234 ret = self.validate_services_by_name({sentry_unit: ['memcached']})
1235 if ret:
1236 amulet.raise_status(amulet.FAIL, msg='Memcache running check'
1237 'failed {}'.format(ret))
1238 else:
1239 self.log.debug('OK')
1240 self.log.debug('Checking memcache url is configured in {}'.format(
1241 conf))
1242 if self.validate_config_data(sentry_unit, conf, section, _kvs):
1243 message = "Memcache config error in: {}".format(conf)
1244 amulet.raise_status(amulet.FAIL, msg=message)
1245 else:
1246 self.log.debug('OK')
1247 self.log.debug('Checking memcache configuration in '
1248 '/etc/memcached.conf')
1249 contents = self.file_contents_safe(sentry_unit, '/etc/memcached.conf',
1250 fatal=True)
1251 ubuntu_release, _ = self.run_cmd_unit(sentry_unit, 'lsb_release -cs')
1252 if ubuntu_release <= 'trusty':
1253 memcache_listen_addr = 'ip6-localhost'
1254 else:
1255 memcache_listen_addr = '::1'
1256 expected = {
1257 '-p': '11211',
1258 '-l': memcache_listen_addr}
1259 found = []
1260 for key, value in expected.items():
1261 for line in contents.split('\n'):
1262 if line.startswith(key):
1263 self.log.debug('Checking {} is set to {}'.format(
1264 key,
1265 value))
1266 assert value == line.split()[-1]
1267 self.log.debug(line.split()[-1])
1268 found.append(key)
1269 if sorted(found) == sorted(expected.keys()):
1270 self.log.debug('OK')
1271 else:
1272 message = "Memcache config error in: /etc/memcached.conf"
1273 amulet.raise_status(amulet.FAIL, msg=message)
diff --git a/tests/charmhelpers/core/__init__.py b/tests/charmhelpers/core/__init__.py
new file mode 100644
index 0000000..d7567b8
--- /dev/null
+++ b/tests/charmhelpers/core/__init__.py
@@ -0,0 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
diff --git a/tests/charmhelpers/core/decorators.py b/tests/charmhelpers/core/decorators.py
new file mode 100644
index 0000000..6ad41ee
--- /dev/null
+++ b/tests/charmhelpers/core/decorators.py
@@ -0,0 +1,55 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15#
16# Copyright 2014 Canonical Ltd.
17#
18# Authors:
19# Edward Hope-Morley <opentastic@gmail.com>
20#
21
22import time
23
24from charmhelpers.core.hookenv import (
25 log,
26 INFO,
27)
28
29
30def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
31 """If the decorated function raises exception exc_type, allow num_retries
32 retry attempts before raise the exception.
33 """
34 def _retry_on_exception_inner_1(f):
35 def _retry_on_exception_inner_2(*args, **kwargs):
36 retries = num_retries
37 multiplier = 1
38 while True:
39 try:
40 return f(*args, **kwargs)
41 except exc_type:
42 if not retries:
43 raise
44
45 delay = base_delay * multiplier
46 multiplier += 1
47 log("Retrying '%s' %d more times (delay=%s)" %
48 (f.__name__, retries, delay), level=INFO)
49 retries -= 1
50 if delay:
51 time.sleep(delay)
52
53 return _retry_on_exception_inner_2
54
55 return _retry_on_exception_inner_1
diff --git a/tests/charmhelpers/core/files.py b/tests/charmhelpers/core/files.py
new file mode 100644
index 0000000..fdd82b7
--- /dev/null
+++ b/tests/charmhelpers/core/files.py
@@ -0,0 +1,43 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
19
20import os
21import subprocess
22
23
24def sed(filename, before, after, flags='g'):
25 """
26 Search and replaces the given pattern on filename.
27
28 :param filename: relative or absolute file path.
29 :param before: expression to be replaced (see 'man sed')
30 :param after: expression to replace with (see 'man sed')
31 :param flags: sed-compatible regex flags in example, to make
32 the search and replace case insensitive, specify ``flags="i"``.
33 The ``g`` flag is always specified regardless, so you do not
34 need to remember to include it when overriding this parameter.
35 :returns: If the sed command exit code was zero then return,
36 otherwise raise CalledProcessError.
37 """
38 expression = r's/{0}/{1}/{2}'.format(before,
39 after, flags)
40
41 return subprocess.check_call(["sed", "-i", "-r", "-e",
42 expression,
43 os.path.expanduser(filename)])
diff --git a/tests/charmhelpers/core/fstab.py b/tests/charmhelpers/core/fstab.py
new file mode 100644
index 0000000..d9fa915
--- /dev/null
+++ b/tests/charmhelpers/core/fstab.py
@@ -0,0 +1,132 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import io
19import os
20
21__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
22
23
24class Fstab(io.FileIO):
25 """This class extends file in order to implement a file reader/writer
26 for file `/etc/fstab`
27 """
28
29 class Entry(object):
30 """Entry class represents a non-comment line on the `/etc/fstab` file
31 """
32 def __init__(self, device, mountpoint, filesystem,
33 options, d=0, p=0):
34 self.device = device
35 self.mountpoint = mountpoint
36 self.filesystem = filesystem
37
38 if not options:
39 options = "defaults"
40
41 self.options = options
42 self.d = int(d)
43 self.p = int(p)
44
45 def __eq__(self, o):
46 return str(self) == str(o)
47
48 def __str__(self):
49 return "{} {} {} {} {} {}".format(self.device,
50 self.mountpoint,
51 self.filesystem,
52 self.options,
53 self.d,
54 self.p)
55
56 DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
57
58 def __init__(self, path=None):
59 if path:
60 self._path = path
61 else:
62 self._path = self.DEFAULT_PATH
63 super(Fstab, self).__init__(self._path, 'rb+')
64
65 def _hydrate_entry(self, line):
66 # NOTE: use split with no arguments to split on any
67 # whitespace including tabs
68 return Fstab.Entry(*filter(
69 lambda x: x not in ('', None),
70 line.strip("\n").split()))
71
72 @property
73 def entries(self):
74 self.seek(0)
75 for line in self.readlines():
76 line = line.decode('us-ascii')
77 try:
78 if line.strip() and not line.strip().startswith("#"):
79 yield self._hydrate_entry(line)
80 except ValueError:
81 pass
82
83 def get_entry_by_attr(self, attr, value):
84 for entry in self.entries:
85 e_attr = getattr(entry, attr)
86 if e_attr == value:
87 return entry
88 return None
89
90 def add_entry(self, entry):
91 if self.get_entry_by_attr('device', entry.device):
92 return False
93
94 self.write((str(entry) + '\n').encode('us-ascii'))
95 self.truncate()
96 return entry
97
98 def remove_entry(self, entry):
99 self.seek(0)
100
101 lines = [l.decode('us-ascii') for l in self.readlines()]
102
103 found = False
104 for index, line in enumerate(lines):
105 if line.strip() and not line.strip().startswith("#"):
106 if self._hydrate_entry(line) == entry:
107 found = True
108 break
109
110 if not found:
111 return False
112
113 lines.remove(line)
114
115 self.seek(0)
116 self.write(''.join(lines).encode('us-ascii'))
117 self.truncate()
118 return True
119
120 @classmethod
121 def remove_by_mountpoint(cls, mountpoint, path=None):
122 fstab = cls(path=path)
123 entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
124 if entry:
125 return fstab.remove_entry(entry)
126 return False
127
128 @classmethod
129 def add(cls, device, mountpoint, filesystem, options=None, path=None):
130 return cls(path=path).add_entry(Fstab.Entry(device,
131 mountpoint, filesystem,
132 options=options))
diff --git a/tests/charmhelpers/core/hookenv.py b/tests/charmhelpers/core/hookenv.py
new file mode 100644
index 0000000..e44e22b
--- /dev/null
+++ b/tests/charmhelpers/core/hookenv.py
@@ -0,0 +1,1068 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"Interactions with the Juju environment"
16# Copyright 2013 Canonical Ltd.
17#
18# Authors:
19# Charm Helpers Developers <juju@lists.ubuntu.com>
20
21from __future__ import print_function
22import copy
23from distutils.version import LooseVersion
24from functools import wraps
25import glob
26import os
27import json
28import yaml
29import subprocess
30import sys
31import errno
32import tempfile
33from subprocess import CalledProcessError
34
35import six
36if not six.PY3:
37 from UserDict import UserDict
38else:
39 from collections import UserDict
40
41CRITICAL = "CRITICAL"
42ERROR = "ERROR"
43WARNING = "WARNING"
44INFO = "INFO"
45DEBUG = "DEBUG"
46MARKER = object()
47
48cache = {}
49
50
51def cached(func):
52 """Cache return values for multiple executions of func + args
53
54 For example::
55
56 @cached
57 def unit_get(attribute):
58 pass
59
60 unit_get('test')
61
62 will cache the result of unit_get + 'test' for future calls.
63 """
64 @wraps(func)
65 def wrapper(*args, **kwargs):
66 global cache
67 key = str((func, args, kwargs))
68 try:
69 return cache[key]
70 except KeyError:
71 pass # Drop out of the exception handler scope.
72 res = func(*args, **kwargs)
73 cache[key] = res
74 return res
75 wrapper._wrapped = func
76 return wrapper
77
78
79def flush(key):
80 """Flushes any entries from function cache where the
81 key is found in the function+args """
82 flush_list = []
83 for item in cache:
84 if key in item:
85 flush_list.append(item)
86 for item in flush_list:
87 del cache[item]
88
89
90def log(message, level=None):
91 """Write a message to the juju log"""
92 command = ['juju-log']
93 if level:
94 command += ['-l', level]
95 if not isinstance(message, six.string_types):
96 message = repr(message)
97 command += [message]
98 # Missing juju-log should not cause failures in unit tests
99 # Send log output to stderr
100 try:
101 subprocess.call(command)
102 except OSError as e:
103 if e.errno == errno.ENOENT:
104 if level:
105 message = "{}: {}".format(level, message)
106 message = "juju-log: {}".format(message)
107 print(message, file=sys.stderr)
108 else:
109 raise
110
111
112class Serializable(UserDict):
113 """Wrapper, an object that can be serialized to yaml or json"""
114
115 def __init__(self, obj):
116 # wrap the object
117 UserDict.__init__(self)
118 self.data = obj
119
120 def __getattr__(self, attr):
121 # See if this object has attribute.
122 if attr in ("json", "yaml", "data"):
123 return self.__dict__[attr]
124 # Check for attribute in wrapped object.
125 got = getattr(self.data, attr, MARKER)
126 if got is not MARKER:
127 return got
128 # Proxy to the wrapped object via dict interface.
129 try:
130 return self.data[attr]
131 except KeyError:
132 raise AttributeError(attr)
133
134 def __getstate__(self):
135 # Pickle as a standard dictionary.
136 return self.data
137
138 def __setstate__(self, state):
139 # Unpickle into our wrapper.
140 self.data = state
141
142 def json(self):
143 """Serialize the object to json"""
144 return json.dumps(self.data)
145
146 def yaml(self):
147 """Serialize the object to yaml"""
148 return yaml.dump(self.data)
149
150
151def execution_environment():
152 """A convenient bundling of the current execution context"""
153 context = {}
154 context['conf'] = config()
155 if relation_id():
156 context['reltype'] = relation_type()
157 context['relid'] = relation_id()
158 context['rel'] = relation_get()
159 context['unit'] = local_unit()
160 context['rels'] = relations()
161 context['env'] = os.environ
162 return context
163
164
165def in_relation_hook():
166 """Determine whether we're running in a relation hook"""
167 return 'JUJU_RELATION' in os.environ
168
169
170def relation_type():
171 """The scope for the current relation hook"""
172 return os.environ.get('JUJU_RELATION', None)
173
174
175@cached
176def relation_id(relation_name=None, service_or_unit=None):
177 """The relation ID for the current or a specified relation"""
178 if not relation_name and not service_or_unit:
179 return os.environ.get('JUJU_RELATION_ID', None)
180 elif relation_name and service_or_unit:
181 service_name = service_or_unit.split('/')[0]
182 for relid in relation_ids(relation_name):
183 remote_service = remote_service_name(relid)
184 if remote_service == service_name:
185 return relid
186 else:
187 raise ValueError('Must specify neither or both of relation_name and service_or_unit')
188
189
190def local_unit():
191 """Local unit ID"""
192 return os.environ['JUJU_UNIT_NAME']
193
194
195def remote_unit():
196 """The remote unit for the current relation hook"""
197 return os.environ.get('JUJU_REMOTE_UNIT', None)
198
199
200def service_name():
201 """The name service group this unit belongs to"""
202 return local_unit().split('/')[0]
203
204
205@cached
206def remote_service_name(relid=None):
207 """The remote service name for a given relation-id (or the current relation)"""
208 if relid is None:
209 unit = remote_unit()
210 else:
211 units = related_units(relid)
212 unit = units[0] if units else None
213 return unit.split('/')[0] if unit else None
214
215
216def hook_name():
217 """The name of the currently executing hook"""
218 return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
219
220
221class Config(dict):
222 """A dictionary representation of the charm's config.yaml, with some
223 extra features:
224
225 - See which values in the dictionary have changed since the previous hook.
226 - For values that have changed, see what the previous value was.
227 - Store arbitrary data for use in a later hook.
228
229 NOTE: Do not instantiate this object directly - instead call
230 ``hookenv.config()``, which will return an instance of :class:`Config`.
231
232 Example usage::
233
234 >>> # inside a hook
235 >>> from charmhelpers.core import hookenv
236 >>> config = hookenv.config()
237 >>> config['foo']
238 'bar'
239 >>> # store a new key/value for later use
240 >>> config['mykey'] = 'myval'
241
242
243 >>> # user runs `juju set mycharm foo=baz`
244 >>> # now we're inside subsequent config-changed hook
245 >>> config = hookenv.config()
246 >>> config['foo']
247 'baz'
248 >>> # test to see if this val has changed since last hook
249 >>> config.changed('foo')
250 True
251 >>> # what was the previous value?
252 >>> config.previous('foo')
253 'bar'
254 >>> # keys/values that we add are preserved across hooks
255 >>> config['mykey']
256 'myval'
257
258 """
259 CONFIG_FILE_NAME = '.juju-persistent-config'
260
261 def __init__(self, *args, **kw):
262 super(Config, self).__init__(*args, **kw)
263 self.implicit_save = True
264 self._prev_dict = None
265 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
266 if os.path.exists(self.path):
267 self.load_previous()
268 atexit(self._implicit_save)
269
270 def load_previous(self, path=None):
271 """Load previous copy of config from disk.
272
273 In normal usage you don't need to call this method directly - it
274 is called automatically at object initialization.
275
276 :param path:
277
278 File path from which to load the previous config. If `None`,
279 config is loaded from the default location. If `path` is
280 specified, subsequent `save()` calls will write to the same
281 path.
282
283 """
284 self.path = path or self.path
285 with open(self.path) as f:
286 self._prev_dict = json.load(f)
287 for k, v in copy.deepcopy(self._prev_dict).items():
288 if k not in self:
289 self[k] = v
290
291 def changed(self, key):
292 """Return True if the current value for this key is different from
293 the previous value.
294
295 """
296 if self._prev_dict is None:
297 return True
298 return self.previous(key) != self.get(key)
299
300 def previous(self, key):
301 """Return previous value for this key, or None if there
302 is no previous value.
303
304 """
305 if self._prev_dict:
306 return self._prev_dict.get(key)
307 return None
308
309 def save(self):
310 """Save this config to disk.
311
312 If the charm is using the :mod:`Services Framework <services.base>`
313 or :meth:'@hook <Hooks.hook>' decorator, this
314 is called automatically at the end of successful hook execution.
315 Otherwise, it should be called directly by user code.
316
317 To disable automatic saves, set ``implicit_save=False`` on this
318 instance.
319
320 """
321 with open(self.path, 'w') as f:
322 json.dump(self, f)
323
324 def _implicit_save(self):
325 if self.implicit_save:
326 self.save()
327
328
329@cached
330def config(scope=None):
331 """Juju charm configuration"""
332 config_cmd_line = ['config-get']
333 if scope is not None:
334 config_cmd_line.append(scope)
335 else:
336 config_cmd_line.append('--all')
337 config_cmd_line.append('--format=json')
338 try:
339 config_data = json.loads(
340 subprocess.check_output(config_cmd_line).decode('UTF-8'))
341 if scope is not None:
342 return config_data
343 return Config(config_data)
344 except ValueError:
345 return None
346
347
348@cached
349def relation_get(attribute=None, unit=None, rid=None):
350 """Get relation information"""
351 _args = ['relation-get', '--format=json']
352 if rid:
353 _args.append('-r')
354 _args.append(rid)
355 _args.append(attribute or '-')
356 if unit:
357 _args.append(unit)
358 try:
359 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
360 except ValueError:
361 return None
362 except CalledProcessError as e:
363 if e.returncode == 2:
364 return None
365 raise
366
367
368def relation_set(relation_id=None, relation_settings=None, **kwargs):
369 """Set relation information for the current unit"""
370 relation_settings = relation_settings if relation_settings else {}
371 relation_cmd_line = ['relation-set']
372 accepts_file = "--file" in subprocess.check_output(
373 relation_cmd_line + ["--help"], universal_newlines=True)
374 if relation_id is not None:
375 relation_cmd_line.extend(('-r', relation_id))
376 settings = relation_settings.copy()
377 settings.update(kwargs)
378 for key, value in settings.items():
379 # Force value to be a string: it always should, but some call
380 # sites pass in things like dicts or numbers.
381 if value is not None:
382 settings[key] = "{}".format(value)
383 if accepts_file:
384 # --file was introduced in Juju 1.23.2. Use it by default if
385 # available, since otherwise we'll break if the relation data is
386 # too big. Ideally we should tell relation-set to read the data from
387 # stdin, but that feature is broken in 1.23.2: Bug #1454678.
388 with tempfile.NamedTemporaryFile(delete=False) as settings_file:
389 settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
390 subprocess.check_call(
391 relation_cmd_line + ["--file", settings_file.name])
392 os.remove(settings_file.name)
393 else:
394 for key, value in settings.items():
395 if value is None:
396 relation_cmd_line.append('{}='.format(key))
397 else:
398 relation_cmd_line.append('{}={}'.format(key, value))
399 subprocess.check_call(relation_cmd_line)
400 # Flush cache of any relation-gets for local unit
401 flush(local_unit())
402
403
404def relation_clear(r_id=None):
405 ''' Clears any relation data already set on relation r_id '''
406 settings = relation_get(rid=r_id,
407 unit=local_unit())
408 for setting in settings:
409 if setting not in ['public-address', 'private-address']:
410 settings[setting] = None
411 relation_set(relation_id=r_id,
412 **settings)
413
414
415@cached
416def relation_ids(reltype=None):
417 """A list of relation_ids"""
418 reltype = reltype or relation_type()
419 relid_cmd_line = ['relation-ids', '--format=json']
420 if reltype is not None:
421 relid_cmd_line.append(reltype)
422 return json.loads(
423 subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
424 return []
425
426
427@cached
428def related_units(relid=None):
429 """A list of related units"""
430 relid = relid or relation_id()
431 units_cmd_line = ['relation-list', '--format=json']
432 if relid is not None:
433 units_cmd_line.extend(('-r', relid))
434 return json.loads(
435 subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
436
437
438@cached
439def relation_for_unit(unit=None, rid=None):
440 """Get the json represenation of a unit's relation"""
441 unit = unit or remote_unit()
442 relation = relation_get(unit=unit, rid=rid)
443 for key in relation:
444 if key.endswith('-list'):
445 relation[key] = relation[key].split()
446 relation['__unit__'] = unit
447 return relation
448
449
450@cached
451def relations_for_id(relid=None):
452 """Get relations of a specific relation ID"""
453 relation_data = []
454 relid = relid or relation_ids()
455 for unit in related_units(relid):
456 unit_data = relation_for_unit(unit, relid)
457 unit_data['__relid__'] = relid
458 relation_data.append(unit_data)
459 return relation_data
460
461
462@cached
463def relations_of_type(reltype=None):
464 """Get relations of a specific type"""
465 relation_data = []
466 reltype = reltype or relation_type()
467 for relid in relation_ids(reltype):
468 for relation in relations_for_id(relid):
469 relation['__relid__'] = relid
470 relation_data.append(relation)
471 return relation_data
472
473
474@cached
475def metadata():
476 """Get the current charm metadata.yaml contents as a python object"""
477 with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
478 return yaml.safe_load(md)
479
480
481@cached
482def relation_types():
483 """Get a list of relation types supported by this charm"""
484 rel_types = []
485 md = metadata()
486 for key in ('provides', 'requires', 'peers'):
487 section = md.get(key)
488 if section:
489 rel_types.extend(section.keys())
490 return rel_types
491
492
493@cached
494def peer_relation_id():
495 '''Get the peers relation id if a peers relation has been joined, else None.'''
496 md = metadata()
497 section = md.get('peers')
498 if section:
499 for key in section:
500 relids = relation_ids(key)
501 if relids:
502 return relids[0]
503 return None
504
505
506@cached
507def relation_to_interface(relation_name):
508 """
509 Given the name of a relation, return the interface that relation uses.
510
511 :returns: The interface name, or ``None``.
512 """
513 return relation_to_role_and_interface(relation_name)[1]
514
515
516@cached
517def relation_to_role_and_interface(relation_name):
518 """
519 Given the name of a relation, return the role and the name of the interface
520 that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
521
522 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
523 """
524 _metadata = metadata()
525 for role in ('provides', 'requires', 'peers'):
526 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
527 if interface:
528 return role, interface
529 return None, None
530
531
532@cached
533def role_and_interface_to_relations(role, interface_name):
534 """
535 Given a role and interface name, return a list of relation names for the
536 current charm that use that interface under that role (where role is one
537 of ``provides``, ``requires``, or ``peers``).
538
539 :returns: A list of relation names.
540 """
541 _metadata = metadata()
542 results = []
543 for relation_name, relation in _metadata.get(role, {}).items():
544 if relation['interface'] == interface_name:
545 results.append(relation_name)
546 return results
547
548
549@cached
550def interface_to_relations(interface_name):
551 """
552 Given an interface, return a list of relation names for the current
553 charm that use that interface.
554
555 :returns: A list of relation names.
556 """
557 results = []
558 for role in ('provides', 'requires', 'peers'):
559 results.extend(role_and_interface_to_relations(role, interface_name))
560 return results
561
562
563@cached
564def charm_name():
565 """Get the name of the current charm as is specified on metadata.yaml"""
566 return metadata().get('name')
567
568
569@cached
570def relations():
571 """Get a nested dictionary of relation data for all related units"""
572 rels = {}
573 for reltype in relation_types():
574 relids = {}
575 for relid in relation_ids(reltype):
576 units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
577 for unit in related_units(relid):
578 reldata = relation_get(unit=unit, rid=relid)
579 units[unit] = reldata
580 relids[relid] = units
581 rels[reltype] = relids
582 return rels
583
584
585@cached
586def is_relation_made(relation, keys='private-address'):
587 '''
588 Determine whether a relation is established by checking for
589 presence of key(s). If a list of keys is provided, they
590 must all be present for the relation to be identified as made
591 '''
592 if isinstance(keys, str):
593 keys = [keys]
594 for r_id in relation_ids(relation):
595 for unit in related_units(r_id):
596 context = {}
597 for k in keys:
598 context[k] = relation_get(k, rid=r_id,
599 unit=unit)
600 if None not in context.values():
601 return True
602 return False
603
604
605def open_port(port, protocol="TCP"):
606 """Open a service network port"""
607 _args = ['open-port']
608 _args.append('{}/{}'.format(port, protocol))
609 subprocess.check_call(_args)
610
611
612def close_port(port, protocol="TCP"):
613 """Close a service network port"""
614 _args = ['close-port']
615 _args.append('{}/{}'.format(port, protocol))
616 subprocess.check_call(_args)
617
618
619def open_ports(start, end, protocol="TCP"):
620 """Opens a range of service network ports"""
621 _args = ['open-port']
622 _args.append('{}-{}/{}'.format(start, end, protocol))
623 subprocess.check_call(_args)
624
625
626def close_ports(start, end, protocol="TCP"):
627 """Close a range of service network ports"""
628 _args = ['close-port']
629 _args.append('{}-{}/{}'.format(start, end, protocol))
630 subprocess.check_call(_args)
631
632
633@cached
634def unit_get(attribute):
635 """Get the unit ID for the remote unit"""
636 _args = ['unit-get', '--format=json', attribute]
637 try:
638 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
639 except ValueError:
640 return None
641
642
643def unit_public_ip():
644 """Get this unit's public IP address"""
645 return unit_get('public-address')
646
647
648def unit_private_ip():
649 """Get this unit's private IP address"""
650 return unit_get('private-address')
651
652
653@cached
654def storage_get(attribute=None, storage_id=None):
655 """Get storage attributes"""
656 _args = ['storage-get', '--format=json']
657 if storage_id:
658 _args.extend(('-s', storage_id))
659 if attribute:
660 _args.append(attribute)
661 try:
662 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
663 except ValueError:
664 return None
665
666
667@cached
668def storage_list(storage_name=None):
669 """List the storage IDs for the unit"""
670 _args = ['storage-list', '--format=json']
671 if storage_name:
672 _args.append(storage_name)
673 try:
674 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
675 except ValueError:
676 return None
677 except OSError as e:
678 import errno
679 if e.errno == errno.ENOENT:
680 # storage-list does not exist
681 return []
682 raise
683
684
685class UnregisteredHookError(Exception):
686 """Raised when an undefined hook is called"""
687 pass
688
689
690class Hooks(object):
691 """A convenient handler for hook functions.
692
693 Example::
694
695 hooks = Hooks()
696
697 # register a hook, taking its name from the function name
698 @hooks.hook()
699 def install():
700 pass # your code here
701
702 # register a hook, providing a custom hook name
703 @hooks.hook("config-changed")
704 def config_changed():
705 pass # your code here
706
707 if __name__ == "__main__":
708 # execute a hook based on the name the program is called by
709 hooks.execute(sys.argv)
710 """
711
712 def __init__(self, config_save=None):
713 super(Hooks, self).__init__()
714 self._hooks = {}
715
716 # For unknown reasons, we allow the Hooks constructor to override
717 # config().implicit_save.
718 if config_save is not None:
719 config().implicit_save = config_save
720
721 def register(self, name, function):
722 """Register a hook"""
723 self._hooks[name] = function
724
725 def execute(self, args):
726 """Execute a registered hook based on args[0]"""
727 _run_atstart()
728 hook_name = os.path.basename(args[0])
729 if hook_name in self._hooks:
730 try:
731 self._hooks[hook_name]()
732 except SystemExit as x:
733 if x.code is None or x.code == 0:
734 _run_atexit()
735 raise
736 _run_atexit()
737 else:
738 raise UnregisteredHookError(hook_name)
739
740 def hook(self, *hook_names):
741 """Decorator, registering them as hooks"""
742 def wrapper(decorated):
743 for hook_name in hook_names:
744 self.register(hook_name, decorated)
745 else:
746 self.register(decorated.__name__, decorated)
747 if '_' in decorated.__name__:
748 self.register(
749 decorated.__name__.replace('_', '-'), decorated)
750 return decorated
751 return wrapper
752
753
754def charm_dir():
755 """Return the root directory of the current charm"""
756 return os.environ.get('CHARM_DIR')
757
758
759@cached
760def action_get(key=None):
761 """Gets the value of an action parameter, or all key/value param pairs"""
762 cmd = ['action-get']
763 if key is not None:
764 cmd.append(key)
765 cmd.append('--format=json')
766 action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
767 return action_data
768
769
770def action_set(values):
771 """Sets the values to be returned after the action finishes"""
772 cmd = ['action-set']
773 for k, v in list(values.items()):
774 cmd.append('{}={}'.format(k, v))
775 subprocess.check_call(cmd)
776
777
778def action_fail(message):
779 """Sets the action status to failed and sets the error message.
780
781 The results set by action_set are preserved."""
782 subprocess.check_call(['action-fail', message])
783
784
785def action_name():
786 """Get the name of the currently executing action."""
787 return os.environ.get('JUJU_ACTION_NAME')
788
789
790def action_uuid():
791 """Get the UUID of the currently executing action."""
792 return os.environ.get('JUJU_ACTION_UUID')
793
794
795def action_tag():
796 """Get the tag for the currently executing action."""
797 return os.environ.get('JUJU_ACTION_TAG')
798
799
800def status_set(workload_state, message):
801 """Set the workload state with a message
802
803 Use status-set to set the workload state with a message which is visible
804 to the user via juju status. If the status-set command is not found then
805 assume this is juju < 1.23 and juju-log the message unstead.
806
807 workload_state -- valid juju workload state.
808 message -- status update message
809 """
810 valid_states = ['maintenance', 'blocked', 'waiting', 'active']
811 if workload_state not in valid_states:
812 raise ValueError(
813 '{!r} is not a valid workload state'.format(workload_state)
814 )
815 cmd = ['status-set', workload_state, message]
816 try:
817 ret = subprocess.call(cmd)
818 if ret == 0:
819 return
820 except OSError as e:
821 if e.errno != errno.ENOENT:
822 raise
823 log_message = 'status-set failed: {} {}'.format(workload_state,
824 message)
825 log(log_message, level='INFO')
826
827
828def status_get():
829 """Retrieve the previously set juju workload state and message
830
831 If the status-get command is not found then assume this is juju < 1.23 and
832 return 'unknown', ""
833
834 """
835 cmd = ['status-get', "--format=json", "--include-data"]
836 try:
837 raw_status = subprocess.check_output(cmd)
838 except OSError as e:
839 if e.errno == errno.ENOENT:
840 return ('unknown', "")
841 else:
842 raise
843 else:
844 status = json.loads(raw_status.decode("UTF-8"))
845 return (status["status"], status["message"])
846
847
848def translate_exc(from_exc, to_exc):
849 def inner_translate_exc1(f):
850 @wraps(f)
851 def inner_translate_exc2(*args, **kwargs):
852 try:
853 return f(*args, **kwargs)
854 except from_exc:
855 raise to_exc
856
857 return inner_translate_exc2
858
859 return inner_translate_exc1
860
861
862def application_version_set(version):
863 """Charm authors may trigger this command from any hook to output what
864 version of the application is running. This could be a package version,
865 for instance postgres version 9.5. It could also be a build number or
866 version control revision identifier, for instance git sha 6fb7ba68. """
867
868 cmd = ['application-version-set']
869 cmd.append(version)
870 try:
871 subprocess.check_call(cmd)
872 except OSError:
873 log("Application Version: {}".format(version))
874
875
876@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
877def is_leader():
878 """Does the current unit hold the juju leadership
879
880 Uses juju to determine whether the current unit is the leader of its peers
881 """
882 cmd = ['is-leader', '--format=json']
883 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
884
885
886@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
887def leader_get(attribute=None):
888 """Juju leader get value(s)"""
889 cmd = ['leader-get', '--format=json'] + [attribute or '-']
890 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
891
892
893@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
894def leader_set(settings=None, **kwargs):
895 """Juju leader set value(s)"""
896 # Don't log secrets.
897 # log("Juju leader-set '%s'" % (settings), level=DEBUG)
898 cmd = ['leader-set']
899 settings = settings or {}
900 settings.update(kwargs)
901 for k, v in settings.items():
902 if v is None:
903 cmd.append('{}='.format(k))
904 else:
905 cmd.append('{}={}'.format(k, v))
906 subprocess.check_call(cmd)
907
908
909@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
910def payload_register(ptype, klass, pid):
911 """ is used while a hook is running to let Juju know that a
912 payload has been started."""
913 cmd = ['payload-register']
914 for x in [ptype, klass, pid]:
915 cmd.append(x)
916 subprocess.check_call(cmd)
917
918
919@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
920def payload_unregister(klass, pid):
921 """ is used while a hook is running to let Juju know
922 that a payload has been manually stopped. The <class> and <id> provided
923 must match a payload that has been previously registered with juju using
924 payload-register."""
925 cmd = ['payload-unregister']
926 for x in [klass, pid]:
927 cmd.append(x)
928 subprocess.check_call(cmd)
929
930
931@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
932def payload_status_set(klass, pid, status):
933 """is used to update the current status of a registered payload.
934 The <class> and <id> provided must match a payload that has been previously
935 registered with juju using payload-register. The <status> must be one of the
936 follow: starting, started, stopping, stopped"""
937 cmd = ['payload-status-set']
938 for x in [klass, pid, status]:
939 cmd.append(x)
940 subprocess.check_call(cmd)
941
942
943@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
944def resource_get(name):
945 """used to fetch the resource path of the given name.
946
947 <name> must match a name of defined resource in metadata.yaml
948
949 returns either a path or False if resource not available
950 """
951 if not name:
952 return False
953
954 cmd = ['resource-get', name]
955 try:
956 return subprocess.check_output(cmd).decode('UTF-8')
957 except subprocess.CalledProcessError:
958 return False
959
960
961@cached
962def juju_version():
963 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
964 # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
965 jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
966 return subprocess.check_output([jujud, 'version'],
967 universal_newlines=True).strip()
968
969
970@cached
971def has_juju_version(minimum_version):
972 """Return True if the Juju version is at least the provided version"""
973 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
974
975
976_atexit = []
977_atstart = []
978
979
980def atstart(callback, *args, **kwargs):
981 '''Schedule a callback to run before the main hook.
982
983 Callbacks are run in the order they were added.
984
985 This is useful for modules and classes to perform initialization
986 and inject behavior. In particular:
987
988 - Run common code before all of your hooks, such as logging
989 the hook name or interesting relation data.
990 - Defer object or module initialization that requires a hook
991 context until we know there actually is a hook context,
992 making testing easier.
993 - Rather than requiring charm authors to include boilerplate to
994 invoke your helper's behavior, have it run automatically if
995 your object is instantiated or module imported.
996
997 This is not at all useful after your hook framework as been launched.
998 '''
999 global _atstart
1000 _atstart.append((callback, args, kwargs))
1001
1002
1003def atexit(callback, *args, **kwargs):
1004 '''Schedule a callback to run on successful hook completion.
1005
1006 Callbacks are run in the reverse order that they were added.'''
1007 _atexit.append((callback, args, kwargs))
1008
1009
1010def _run_atstart():
1011 '''Hook frameworks must invoke this before running the main hook body.'''
1012 global _atstart
1013 for callback, args, kwargs in _atstart:
1014 callback(*args, **kwargs)
1015 del _atstart[:]
1016
1017
1018def _run_atexit():
1019 '''Hook frameworks must invoke this after the main hook body has
1020 successfully completed. Do not invoke it if the hook fails.'''
1021 global _atexit
1022 for callback, args, kwargs in reversed(_atexit):
1023 callback(*args, **kwargs)
1024 del _atexit[:]
1025
1026
1027@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1028def network_get_primary_address(binding):
1029 '''
1030 Retrieve the primary network address for a named binding
1031
1032 :param binding: string. The name of a relation of extra-binding
1033 :return: string. The primary IP address for the named binding
1034 :raise: NotImplementedError if run on Juju < 2.0
1035 '''
1036 cmd = ['network-get', '--primary-address', binding]
1037 return subprocess.check_output(cmd).decode('UTF-8').strip()
1038
1039
1040def add_metric(*args, **kwargs):
1041 """Add metric values. Values may be expressed with keyword arguments. For
1042 metric names containing dashes, these may be expressed as one or more
1043 'key=value' positional arguments. May only be called from the collect-metrics
1044 hook."""
1045 _args = ['add-metric']
1046 _kvpairs = []
1047 _kvpairs.extend(args)
1048 _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1049 _args.extend(sorted(_kvpairs))
1050 try:
1051 subprocess.check_call(_args)
1052 return
1053 except EnvironmentError as e:
1054 if e.errno != errno.ENOENT:
1055 raise
1056 log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1057 log(log_message, level='INFO')
1058
1059
1060def meter_status():
1061 """Get the meter status, if running in the meter-status-changed hook."""
1062 return os.environ.get('JUJU_METER_STATUS')
1063
1064
1065def meter_info():
1066 """Get the meter status information, if running in the meter-status-changed
1067 hook."""
1068 return os.environ.get('JUJU_METER_INFO')
diff --git a/tests/charmhelpers/core/host.py b/tests/charmhelpers/core/host.py
new file mode 100644
index 0000000..edbb72f
--- /dev/null
+++ b/tests/charmhelpers/core/host.py
@@ -0,0 +1,918 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Tools for working with the host system"""
16# Copyright 2012 Canonical Ltd.
17#
18# Authors:
19# Nick Moffitt <nick.moffitt@canonical.com>
20# Matthew Wedgwood <matthew.wedgwood@canonical.com>
21
22import os
23import re
24import pwd
25import glob
26import grp
27import random
28import string
29import subprocess
30import hashlib
31import functools
32import itertools
33import six
34
35from contextlib import contextmanager
36from collections import OrderedDict
37from .hookenv import log
38from .fstab import Fstab
39from charmhelpers.osplatform import get_platform
40
41__platform__ = get_platform()
42if __platform__ == "ubuntu":
43 from charmhelpers.core.host_factory.ubuntu import (
44 service_available,
45 add_new_group,
46 lsb_release,
47 cmp_pkgrevno,
48 ) # flake8: noqa -- ignore F401 for this import
49elif __platform__ == "centos":
50 from charmhelpers.core.host_factory.centos import (
51 service_available,
52 add_new_group,
53 lsb_release,
54 cmp_pkgrevno,
55 ) # flake8: noqa -- ignore F401 for this import
56
57UPDATEDB_PATH = '/etc/updatedb.conf'
58
59def service_start(service_name, **kwargs):
60 """Start a system service.
61
62 The specified service name is managed via the system level init system.
63 Some init systems (e.g. upstart) require that additional arguments be
64 provided in order to directly control service instances whereas other init
65 systems allow for addressing instances of a service directly by name (e.g.
66 systemd).
67
68 The kwargs allow for the additional parameters to be passed to underlying
69 init systems for those systems which require/allow for them. For example,
70 the ceph-osd upstart script requires the id parameter to be passed along
71 in order to identify which running daemon should be reloaded. The follow-
72 ing example stops the ceph-osd service for instance id=4:
73
74 service_stop('ceph-osd', id=4)
75
76 :param service_name: the name of the service to stop
77 :param **kwargs: additional parameters to pass to the init system when
78 managing services. These will be passed as key=value
79 parameters to the init system's commandline. kwargs
80 are ignored for systemd enabled systems.
81 """
82 return service('start', service_name, **kwargs)
83
84
85def service_stop(service_name, **kwargs):
86 """Stop a system service.
87
88 The specified service name is managed via the system level init system.
89 Some init systems (e.g. upstart) require that additional arguments be
90 provided in order to directly control service instances whereas other init
91 systems allow for addressing instances of a service directly by name (e.g.
92 systemd).
93
94 The kwargs allow for the additional parameters to be passed to underlying
95 init systems for those systems which require/allow for them. For example,
96 the ceph-osd upstart script requires the id parameter to be passed along
97 in order to identify which running daemon should be reloaded. The follow-
98 ing example stops the ceph-osd service for instance id=4:
99
100 service_stop('ceph-osd', id=4)
101
102 :param service_name: the name of the service to stop
103 :param **kwargs: additional parameters to pass to the init system when
104 managing services. These will be passed as key=value
105 parameters to the init system's commandline. kwargs
106 are ignored for systemd enabled systems.
107 """
108 return service('stop', service_name, **kwargs)
109
110
111def service_restart(service_name, **kwargs):
112 """Restart a system service.
113
114 The specified service name is managed via the system level init system.
115 Some init systems (e.g. upstart) require that additional arguments be
116 provided in order to directly control service instances whereas other init
117 systems allow for addressing instances of a service directly by name (e.g.
118 systemd).
119
120 The kwargs allow for the additional parameters to be passed to underlying
121 init systems for those systems which require/allow for them. For example,
122 the ceph-osd upstart script requires the id parameter to be passed along
123 in order to identify which running daemon should be restarted. The follow-
124 ing example restarts the ceph-osd service for instance id=4:
125
126 service_restart('ceph-osd', id=4)
127
128 :param service_name: the name of the service to restart
129 :param **kwargs: additional parameters to pass to the init system when
130 managing services. These will be passed as key=value
131 parameters to the init system's commandline. kwargs
132 are ignored for init systems not allowing additional
133 parameters via the commandline (systemd).
134 """
135 return service('restart', service_name)
136
137
138def service_reload(service_name, restart_on_failure=False, **kwargs):
139 """Reload a system service, optionally falling back to restart if
140 reload fails.
141
142 The specified service name is managed via the system level init system.
143 Some init systems (e.g. upstart) require that additional arguments be
144 provided in order to directly control service instances whereas other init
145 systems allow for addressing instances of a service directly by name (e.g.
146 systemd).
147
148 The kwargs allow for the additional parameters to be passed to underlying
149 init systems for those systems which require/allow for them. For example,
150 the ceph-osd upstart script requires the id parameter to be passed along
151 in order to identify which running daemon should be reloaded. The follow-
152 ing example restarts the ceph-osd service for instance id=4:
153
154 service_reload('ceph-osd', id=4)
155
156 :param service_name: the name of the service to reload
157 :param restart_on_failure: boolean indicating whether to fallback to a
158 restart if the reload fails.
159 :param **kwargs: additional parameters to pass to the init system when
160 managing services. These will be passed as key=value
161 parameters to the init system's commandline. kwargs
162 are ignored for init systems not allowing additional
163 parameters via the commandline (systemd).
164 """
165 service_result = service('reload', service_name, **kwargs)
166 if not service_result and restart_on_failure:
167 service_result = service('restart', service_name, **kwargs)
168 return service_result
169
170
171def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
172 **kwargs):
173 """Pause a system service.
174
175 Stop it, and prevent it from starting again at boot.
176
177 :param service_name: the name of the service to pause
178 :param init_dir: path to the upstart init directory
179 :param initd_dir: path to the sysv init directory
180 :param **kwargs: additional parameters to pass to the init system when
181 managing services. These will be passed as key=value
182 parameters to the init system's commandline. kwargs
183 are ignored for init systems which do not support
184 key=value arguments via the commandline.
185 """
186 stopped = True
187 if service_running(service_name, **kwargs):
188 stopped = service_stop(service_name, **kwargs)
189 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
190 sysv_file = os.path.join(initd_dir, service_name)
191 if init_is_systemd():
192 service('disable', service_name)
193 elif os.path.exists(upstart_file):
194 override_path = os.path.join(
195 init_dir, '{}.override'.format(service_name))
196 with open(override_path, 'w') as fh:
197 fh.write("manual\n")
198 elif os.path.exists(sysv_file):
199 subprocess.check_call(["update-rc.d", service_name, "disable"])
200 else:
201 raise ValueError(
202 "Unable to detect {0} as SystemD, Upstart {1} or"
203 " SysV {2}".format(
204 service_name, upstart_file, sysv_file))
205 return stopped
206
207
208def service_resume(service_name, init_dir="/etc/init",
209 initd_dir="/etc/init.d", **kwargs):
210 """Resume a system service.
211
212 Reenable starting again at boot. Start the service.
213
214 :param service_name: the name of the service to resume
215 :param init_dir: the path to the init dir
216 :param initd dir: the path to the initd dir
217 :param **kwargs: additional parameters to pass to the init system when
218 managing services. These will be passed as key=value
219 parameters to the init system's commandline. kwargs
220 are ignored for systemd enabled systems.
221 """
222 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
223 sysv_file = os.path.join(initd_dir, service_name)
224 if init_is_systemd():
225 service('enable', service_name)
226 elif os.path.exists(upstart_file):
227 override_path = os.path.join(
228 init_dir, '{}.override'.format(service_name))
229 if os.path.exists(override_path):
230 os.unlink(override_path)
231 elif os.path.exists(sysv_file):
232 subprocess.check_call(["update-rc.d", service_name, "enable"])
233 else:
234 raise ValueError(
235 "Unable to detect {0} as SystemD, Upstart {1} or"
236 " SysV {2}".format(
237 service_name, upstart_file, sysv_file))
238 started = service_running(service_name, **kwargs)
239
240 if not started:
241 started = service_start(service_name, **kwargs)
242 return started
243
244
245def service(action, service_name, **kwargs):
246 """Control a system service.
247
248 :param action: the action to take on the service
249 :param service_name: the name of the service to perform th action on
250 :param **kwargs: additional params to be passed to the service command in
251 the form of key=value.
252 """
253 if init_is_systemd():
254 cmd = ['systemctl', action, service_name]
255 else:
256 cmd = ['service', service_name, action]
257 for key, value in six.iteritems(kwargs):
258 parameter = '%s=%s' % (key, value)
259 cmd.append(parameter)
260 return subprocess.call(cmd) == 0
261
262
263_UPSTART_CONF = "/etc/init/{}.conf"
264_INIT_D_CONF = "/etc/init.d/{}"
265
266
267def service_running(service_name, **kwargs):
268 """Determine whether a system service is running.
269
270 :param service_name: the name of the service
271 :param **kwargs: additional args to pass to the service command. This is
272 used to pass additional key=value arguments to the
273 service command line for managing specific instance
274 units (e.g. service ceph-osd status id=2). The kwargs
275 are ignored in systemd services.
276 """
277 if init_is_systemd():
278 return service('is-active', service_name)
279 else:
280 if os.path.exists(_UPSTART_CONF.format(service_name)):
281 try:
282 cmd = ['status', service_name]
283 for key, value in six.iteritems(kwargs):
284 parameter = '%s=%s' % (key, value)
285 cmd.append(parameter)
286 output = subprocess.check_output(cmd,
287 stderr=subprocess.STDOUT).decode('UTF-8')
288 except subprocess.CalledProcessError:
289 return False
290 else:
291 # This works for upstart scripts where the 'service' command
292 # returns a consistent string to represent running
293 # 'start/running'
294 if ("start/running" in output or
295 "is running" in output or
296 "up and running" in output):
297 return True
298 elif os.path.exists(_INIT_D_CONF.format(service_name)):
299 # Check System V scripts init script return codes
300 return service('status', service_name)
301 return False
302
303
304SYSTEMD_SYSTEM = '/run/systemd/system'
305
306
307def init_is_systemd():
308 """Return True if the host system uses systemd, False otherwise."""
309 return os.path.isdir(SYSTEMD_SYSTEM)
310
311
312def adduser(username, password=None, shell='/bin/bash',
313 system_user=False, primary_group=None,
314 secondary_groups=None, uid=None, home_dir=None):
315 """Add a user to the system.
316
317 Will log but otherwise succeed if the user already exists.
318
319 :param str username: Username to create
320 :param str password: Password for user; if ``None``, create a system user
321 :param str shell: The default shell for the user
322 :param bool system_user: Whether to create a login or system user
323 :param str primary_group: Primary group for user; defaults to username
324 :param list secondary_groups: Optional list of additional groups
325 :param int uid: UID for user being created
326 :param str home_dir: Home directory for user
327
328 :returns: The password database entry struct, as returned by `pwd.getpwnam`
329 """
330 try:
331 user_info = pwd.getpwnam(username)
332 log('user {0} already exists!'.format(username))
333 if uid:
334 user_info = pwd.getpwuid(int(uid))
335 log('user with uid {0} already exists!'.format(uid))
336 except KeyError:
337 log('creating user {0}'.format(username))
338 cmd = ['useradd']
339 if uid:
340 cmd.extend(['--uid', str(uid)])
341 if home_dir:
342 cmd.extend(['--home', str(home_dir)])
343 if system_user or password is None:
344 cmd.append('--system')
345 else:
346 cmd.extend([
347 '--create-home',
348 '--shell', shell,
349 '--password', password,
350 ])
351 if not primary_group:
352 try:
353 grp.getgrnam(username)
354 primary_group = username # avoid "group exists" error
355 except KeyError:
356 pass
357 if primary_group:
358 cmd.extend(['-g', primary_group])
359 if secondary_groups:
360 cmd.extend(['-G', ','.join(secondary_groups)])
361 cmd.append(username)
362 subprocess.check_call(cmd)
363 user_info = pwd.getpwnam(username)
364 return user_info
365
366
367def user_exists(username):
368 """Check if a user exists"""
369 try:
370 pwd.getpwnam(username)
371 user_exists = True
372 except KeyError:
373 user_exists = False
374 return user_exists
375
376
377def uid_exists(uid):
378 """Check if a uid exists"""
379 try:
380 pwd.getpwuid(uid)
381 uid_exists = True
382 except KeyError:
383 uid_exists = False
384 return uid_exists
385
386
387def group_exists(groupname):
388 """Check if a group exists"""
389 try:
390 grp.getgrnam(groupname)
391 group_exists = True
392 except KeyError:
393 group_exists = False
394 return group_exists
395
396
397def gid_exists(gid):
398 """Check if a gid exists"""
399 try:
400 grp.getgrgid(gid)
401 gid_exists = True
402 except KeyError:
403 gid_exists = False
404 return gid_exists
405
406
407def add_group(group_name, system_group=False, gid=None):
408 """Add a group to the system
409
410 Will log but otherwise succeed if the group already exists.
411
412 :param str group_name: group to create
413 :param bool system_group: Create system group
414 :param int gid: GID for user being created
415
416 :returns: The password database entry struct, as returned by `grp.getgrnam`
417 """
418 try:
419 group_info = grp.getgrnam(group_name)
420 log('group {0} already exists!'.format(group_name))
421 if gid:
422 group_info = grp.getgrgid(gid)
423 log('group with gid {0} already exists!'.format(gid))
424 except KeyError:
425 log('creating group {0}'.format(group_name))
426 add_new_group(group_name, system_group, gid)
427 group_info = grp.getgrnam(group_name)
428 return group_info
429
430
431def add_user_to_group(username, group):
432 """Add a user to a group"""
433 cmd = ['gpasswd', '-a', username, group]
434 log("Adding user {} to group {}".format(username, group))
435 subprocess.check_call(cmd)
436
437
438def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
439 """Replicate the contents of a path"""
440 options = options or ['--delete', '--executability']
441 cmd = ['/usr/bin/rsync', flags]
442 if timeout:
443 cmd = ['timeout', str(timeout)] + cmd
444 cmd.extend(options)
445 cmd.append(from_path)
446 cmd.append(to_path)
447 log(" ".join(cmd))
448 return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
449
450
451def symlink(source, destination):
452 """Create a symbolic link"""
453 log("Symlinking {} as {}".format(source, destination))
454 cmd = [
455 'ln',
456 '-sf',
457 source,
458 destination,
459 ]
460 subprocess.check_call(cmd)
461
462
463def mkdir(path, owner='root', group='root', perms=0o555, force=False):
464 """Create a directory"""
465 log("Making dir {} {}:{} {:o}".format(path, owner, group,
466 perms))
467 uid = pwd.getpwnam(owner).pw_uid
468 gid = grp.getgrnam(group).gr_gid
469 realpath = os.path.abspath(path)
470 path_exists = os.path.exists(realpath)
471 if path_exists and force:
472 if not os.path.isdir(realpath):
473 log("Removing non-directory file {} prior to mkdir()".format(path))
474 os.unlink(realpath)
475 os.makedirs(realpath, perms)
476 elif not path_exists:
477 os.makedirs(realpath, perms)
478 os.chown(realpath, uid, gid)
479 os.chmod(realpath, perms)
480
481
482def write_file(path, content, owner='root', group='root', perms=0o444):
483 """Create or overwrite a file with the contents of a byte string."""
484 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
485 uid = pwd.getpwnam(owner).pw_uid
486 gid = grp.getgrnam(group).gr_gid
487 with open(path, 'wb') as target:
488 os.fchown(target.fileno(), uid, gid)
489 os.fchmod(target.fileno(), perms)
490 target.write(content)
491
492
493def fstab_remove(mp):
494 """Remove the given mountpoint entry from /etc/fstab"""
495 return Fstab.remove_by_mountpoint(mp)
496
497
498def fstab_add(dev, mp, fs, options=None):
499 """Adds the given device entry to the /etc/fstab file"""
500 return Fstab.add(dev, mp, fs, options=options)
501
502
503def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
504 """Mount a filesystem at a particular mountpoint"""
505 cmd_args = ['mount']
506 if options is not None:
507 cmd_args.extend(['-o', options])
508 cmd_args.extend([device, mountpoint])
509 try:
510 subprocess.check_output(cmd_args)
511 except subprocess.CalledProcessError as e:
512 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
513 return False
514
515 if persist:
516 return fstab_add(device, mountpoint, filesystem, options=options)
517 return True
518
519
520def umount(mountpoint, persist=False):
521 """Unmount a filesystem"""
522 cmd_args = ['umount', mountpoint]
523 try:
524 subprocess.check_output(cmd_args)
525 except subprocess.CalledProcessError as e:
526 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
527 return False
528
529 if persist:
530 return fstab_remove(mountpoint)
531 return True
532
533
534def mounts():
535 """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
536 with open('/proc/mounts') as f:
537 # [['/mount/point','/dev/path'],[...]]
538 system_mounts = [m[1::-1] for m in [l.strip().split()
539 for l in f.readlines()]]
540 return system_mounts
541
542
543def fstab_mount(mountpoint):
544 """Mount filesystem using fstab"""
545 cmd_args = ['mount', mountpoint]
546 try:
547 subprocess.check_output(cmd_args)
548 except subprocess.CalledProcessError as e:
549 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
550 return False
551 return True
552
553
554def file_hash(path, hash_type='md5'):
555 """Generate a hash checksum of the contents of 'path' or None if not found.
556
557 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
558 such as md5, sha1, sha256, sha512, etc.
559 """
560 if os.path.exists(path):
561 h = getattr(hashlib, hash_type)()
562 with open(path, 'rb') as source:
563 h.update(source.read())
564 return h.hexdigest()
565 else:
566 return None
567
568
569def path_hash(path):
570 """Generate a hash checksum of all files matching 'path'. Standard
571 wildcards like '*' and '?' are supported, see documentation for the 'glob'
572 module for more information.
573
574 :return: dict: A { filename: hash } dictionary for all matched files.
575 Empty if none found.
576 """
577 return {
578 filename: file_hash(filename)
579 for filename in glob.iglob(path)
580 }
581
582
583def check_hash(path, checksum, hash_type='md5'):
584 """Validate a file using a cryptographic checksum.
585
586 :param str checksum: Value of the checksum used to validate the file.
587 :param str hash_type: Hash algorithm used to generate `checksum`.
588 Can be any hash alrgorithm supported by :mod:`hashlib`,
589 such as md5, sha1, sha256, sha512, etc.
590 :raises ChecksumError: If the file fails the checksum
591
592 """
593 actual_checksum = file_hash(path, hash_type)
594 if checksum != actual_checksum:
595 raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
596
597
598class ChecksumError(ValueError):
599 """A class derived from Value error to indicate the checksum failed."""
600 pass
601
602
603def restart_on_change(restart_map, stopstart=False, restart_functions=None):
604 """Restart services based on configuration files changing
605
606 This function is used a decorator, for example::
607
608 @restart_on_change({
609 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
610 '/etc/apache/sites-enabled/*': [ 'apache2' ]
611 })
612 def config_changed():
613 pass # your code here
614
615 In this example, the cinder-api and cinder-volume services
616 would be restarted if /etc/ceph/ceph.conf is changed by the
617 ceph_client_changed function. The apache2 service would be
618 restarted if any file matching the pattern got changed, created
619 or removed. Standard wildcards are supported, see documentation
620 for the 'glob' module for more information.
621
622 @param restart_map: {path_file_name: [service_name, ...]
623 @param stopstart: DEFAULT false; whether to stop, start OR restart
624 @param restart_functions: nonstandard functions to use to restart services
625 {svc: func, ...}
626 @returns result from decorated function
627 """
628 def wrap(f):
629 @functools.wraps(f)
630 def wrapped_f(*args, **kwargs):
631 return restart_on_change_helper(
632 (lambda: f(*args, **kwargs)), restart_map, stopstart,
633 restart_functions)
634 return wrapped_f
635 return wrap
636
637
638def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
639 restart_functions=None):
640 """Helper function to perform the restart_on_change function.
641
642 This is provided for decorators to restart services if files described
643 in the restart_map have changed after an invocation of lambda_f().
644
645 @param lambda_f: function to call.
646 @param restart_map: {file: [service, ...]}
647 @param stopstart: whether to stop, start or restart a service
648 @param restart_functions: nonstandard functions to use to restart services
649 {svc: func, ...}
650 @returns result of lambda_f()
651 """
652 if restart_functions is None:
653 restart_functions = {}
654 checksums = {path: path_hash(path) for path in restart_map}
655 r = lambda_f()
656 # create a list of lists of the services to restart
657 restarts = [restart_map[path]
658 for path in restart_map
659 if path_hash(path) != checksums[path]]
660 # create a flat list of ordered services without duplicates from lists
661 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
662 if services_list:
663 actions = ('stop', 'start') if stopstart else ('restart',)
664 for service_name in services_list:
665 if service_name in restart_functions:
666 restart_functions[service_name](service_name)
667 else:
668 for action in actions:
669 service(action, service_name)
670 return r
671
672
673def pwgen(length=None):
674 """Generate a random pasword."""
675 if length is None:
676 # A random length is ok to use a weak PRNG
677 length = random.choice(range(35, 45))
678 alphanumeric_chars = [
679 l for l in (string.ascii_letters + string.digits)
680 if l not in 'l0QD1vAEIOUaeiou']
681 # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
682 # actual password
683 random_generator = random.SystemRandom()
684 random_chars = [
685 random_generator.choice(alphanumeric_chars) for _ in range(length)]
686 return(''.join(random_chars))
687
688
689def is_phy_iface(interface):
690 """Returns True if interface is not virtual, otherwise False."""
691 if interface:
692 sys_net = '/sys/class/net'
693 if os.path.isdir(sys_net):
694 for iface in glob.glob(os.path.join(sys_net, '*')):
695 if '/virtual/' in os.path.realpath(iface):
696 continue
697
698 if interface == os.path.basename(iface):
699 return True
700
701 return False
702
703
704def get_bond_master(interface):
705 """Returns bond master if interface is bond slave otherwise None.
706
707 NOTE: the provided interface is expected to be physical
708 """
709 if interface:
710 iface_path = '/sys/class/net/%s' % (interface)
711 if os.path.exists(iface_path):
712 if '/virtual/' in os.path.realpath(iface_path):
713 return None
714
715 master = os.path.join(iface_path, 'master')
716 if os.path.exists(master):
717 master = os.path.realpath(master)
718 # make sure it is a bond master