Merge lp:~1chb1n/charms/trusty/rabbitmq-server/amulet-refactor-1509b into lp:~openstack-charmers-archive/charms/trusty/rabbitmq-server/next

Proposed by Ryan Beisner
Status: Merged
Merged at revision: 112
Proposed branch: lp:~1chb1n/charms/trusty/rabbitmq-server/amulet-refactor-1509b
Merge into: lp:~openstack-charmers-archive/charms/trusty/rabbitmq-server/next
Diff against target: 7025 lines (+2723/-3919)
46 files modified
Makefile (+5/-4)
charm-helpers-tests.yaml (+2/-3)
hooks/rabbit_utils.py (+6/-1)
hooks/rabbitmq_server_relations.py (+30/-10)
metadata.yaml (+4/-1)
tests/00-setup (+16/-0)
tests/014-basic-precise-icehouse (+11/-0)
tests/015-basic-trusty-icehouse (+9/-0)
tests/016-basic-trusty-juno (+11/-0)
tests/017-basic-trusty-kilo (+11/-0)
tests/019-basic-vivid-kilo (+9/-0)
tests/020-basic-trusty-liberty (+11/-0)
tests/021-basic-wily-liberty (+9/-0)
tests/basic_deployment.py (+492/-0)
tests/charmhelpers/__init__.py (+0/-38)
tests/charmhelpers/cli/__init__.py (+0/-191)
tests/charmhelpers/cli/benchmark.py (+0/-36)
tests/charmhelpers/cli/commands.py (+0/-32)
tests/charmhelpers/cli/hookenv.py (+0/-23)
tests/charmhelpers/cli/host.py (+0/-31)
tests/charmhelpers/cli/unitdata.py (+0/-39)
tests/charmhelpers/contrib/__init__.py (+0/-15)
tests/charmhelpers/contrib/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+93/-0)
tests/charmhelpers/contrib/amulet/utils.py (+778/-0)
tests/charmhelpers/contrib/openstack/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+198/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+963/-0)
tests/charmhelpers/contrib/ssl/__init__.py (+0/-94)
tests/charmhelpers/contrib/ssl/service.py (+0/-279)
tests/charmhelpers/core/__init__.py (+0/-15)
tests/charmhelpers/core/decorators.py (+0/-57)
tests/charmhelpers/core/files.py (+0/-45)
tests/charmhelpers/core/fstab.py (+0/-134)
tests/charmhelpers/core/hookenv.py (+0/-898)
tests/charmhelpers/core/host.py (+0/-570)
tests/charmhelpers/core/hugepage.py (+0/-62)
tests/charmhelpers/core/services/__init__.py (+0/-18)
tests/charmhelpers/core/services/base.py (+0/-353)
tests/charmhelpers/core/services/helpers.py (+0/-283)
tests/charmhelpers/core/strutils.py (+0/-42)
tests/charmhelpers/core/sysctl.py (+0/-56)
tests/charmhelpers/core/templating.py (+0/-68)
tests/charmhelpers/core/unitdata.py (+0/-521)
tests/tests.yaml (+20/-0)
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/rabbitmq-server/amulet-refactor-1509b
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+270102@code.launchpad.net

Description of the change

Refactor amulet tests, deprecate old tests

Sync tests/charmhelpers from https://code.launchpad.net/~1chb1n/charm-helpers/amulet-rmq-helpers

Pull in cluster fixes from lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

Provides regression testing against recurrence of:
https://bugs.launchpad.net/charms/+source/rabbitmq-server/+bug/1486177

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #8626 rabbitmq-server-next for 1chb1n mp270102
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/8626/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #9328 rabbitmq-server-next for 1chb1n mp270102
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/9328/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6238 rabbitmq-server-next for 1chb1n mp270102
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 124
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/12270304/
Build: http://10.245.162.77:8080/job/charm_amulet_test/6238/

Revision history for this message
Ryan Beisner (1chb1n) wrote :

Ignore amulet fail #6238. Undercloud woes.

112. By Ryan Beisner

sync tests/charmhelpers from lp:~1chb1n/charm-helpers/amulet-rmq-helpers/

113. By Ryan Beisner

pull in proposed cluster race fix from lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #9390 rabbitmq-server-next for 1chb1n mp270102
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/9390/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #8685 rabbitmq-server-next for 1chb1n mp270102
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/8685/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6256 rabbitmq-server-next for 1chb1n mp270102
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6256/

Revision history for this message
Ryan Beisner (1chb1n) wrote :

Re-running multiple iterations of the amulet test at this rev to re-confirm LE resolution...

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6259 rabbitmq-server-next for 1chb1n mp270102
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/12279813/
Build: http://10.245.162.77:8080/job/charm_amulet_test/6259/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6258 rabbitmq-server-next for 1chb1n mp270102
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/12279853/
Build: http://10.245.162.77:8080/job/charm_amulet_test/6258/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6260 rabbitmq-server-next for 1chb1n mp270102
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6260/

114. By Ryan Beisner

pull in changes from proposed cluster race fix lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #8832 rabbitmq-server-next for 1chb1n mp270102
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/8832/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #9594 rabbitmq-server-next for 1chb1n mp270102
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/9594/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6317 rabbitmq-server-next for 1chb1n mp270102
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6317/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6322 rabbitmq-server-next for 1chb1n mp270102
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6322/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6321 rabbitmq-server-next for 1chb1n mp270102
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6321/

Revision history for this message
Liam Young (gnuoy) wrote :

Thanks for cleaning this up and getting it working, looks really good. Approved

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2015-04-20 11:13:39 +0000
+++ Makefile 2015-09-08 14:01:41 +0000
@@ -29,14 +29,15 @@
29 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml29 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml
30 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml30 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
3131
32publish: lint32publish: lint test
33 bzr push lp:charms/rabbitmq-server33 bzr push lp:charms/rabbitmq-server
34 bzr push lp:charms/trusty/rabbitmq-server34 bzr push lp:charms/trusty/rabbitmq-server
3535
36unit_test: .venv36test: .venv
37 @echo Starting tests...37 @echo Starting tests...
38 env CHARM_DIR=$(CHARM_DIR) $(TEST_PREFIX) .venv/bin/nosetests unit_tests/38 env CHARM_DIR=$(CHARM_DIR) $(TEST_PREFIX) .venv/bin/nosetests \
39 --nologcapture --with-coverage unit_tests/
3940
40functional_test:41functional_test:
41 @echo Starting amulet tests...42 @echo Starting amulet tests...
42 @juju test -v -p AMULET_HTTP_PROXY,OS_USERNAME,OS_TENANT_NAME,OS_REGION_NAME,OS_PASSWORD,OS_AUTH_URL --timeout 90043 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
4344
=== modified file 'charm-helpers-tests.yaml'
--- charm-helpers-tests.yaml 2015-07-31 22:00:49 +0000
+++ charm-helpers-tests.yaml 2015-09-08 14:01:41 +0000
@@ -1,6 +1,5 @@
1destination: tests/charmhelpers1destination: tests/charmhelpers
2branch: lp:charm-helpers2branch: lp:charm-helpers
3include:3include:
4 - core4 - contrib.amulet
5 - cli5 - contrib.openstack.amulet
6 - contrib.ssl
76
=== modified file 'hooks/rabbit_utils.py'
--- hooks/rabbit_utils.py 2015-09-01 04:53:33 +0000
+++ hooks/rabbit_utils.py 2015-09-08 14:01:41 +0000
@@ -299,7 +299,12 @@
299 cmd = [RABBITMQ_CTL, 'stop_app']299 cmd = [RABBITMQ_CTL, 'stop_app']
300 subprocess.check_call(cmd)300 subprocess.check_call(cmd)
301 cmd = [RABBITMQ_CTL, cluster_cmd, 'rabbit@%s' % node]301 cmd = [RABBITMQ_CTL, cluster_cmd, 'rabbit@%s' % node]
302 subprocess.check_call(cmd)302 try:
303 subprocess.check_output(cmd, stderr=subprocess.STDOUT)
304 except subprocess.CalledProcessError as e:
305 if not e.returncode == 2 or \
306 "{ok,already_member}" not in e.output:
307 raise e
303 cmd = [RABBITMQ_CTL, 'start_app']308 cmd = [RABBITMQ_CTL, 'start_app']
304 subprocess.check_call(cmd)309 subprocess.check_call(cmd)
305 log('Host clustered with %s.' % node)310 log('Host clustered with %s.' % node)
306311
=== modified file 'hooks/rabbitmq_server_relations.py'
--- hooks/rabbitmq_server_relations.py 2015-09-01 04:53:33 +0000
+++ hooks/rabbitmq_server_relations.py 2015-09-08 14:01:41 +0000
@@ -81,6 +81,7 @@
81 peer_store,81 peer_store,
82 peer_store_and_set,82 peer_store_and_set,
83 peer_retrieve_by_prefix,83 peer_retrieve_by_prefix,
84 leader_get,
84)85)
8586
86from charmhelpers.contrib.network.ip import get_address_in_network87from charmhelpers.contrib.network.ip import get_address_in_network
@@ -304,31 +305,35 @@
304 return305 return
305306
306 if is_elected_leader('res_rabbitmq_vip'):307 if is_elected_leader('res_rabbitmq_vip'):
308 log('Leader peer_storing cookie', level=INFO)
307 cookie = open(rabbit.COOKIE_PATH, 'r').read().strip()309 cookie = open(rabbit.COOKIE_PATH, 'r').read().strip()
308 peer_store('cookie', cookie)310 peer_store('cookie', cookie)
309311
310312
311@hooks.hook('cluster-relation-changed')313@hooks.hook('cluster-relation-changed')
312def cluster_changed():314def cluster_changed():
315 # Future travelers beware ordering is significant
316 rdata = relation_get()
317 # sync passwords
318 blacklist = ['hostname', 'private-address', 'public-address']
319 whitelist = [a for a in rdata.keys() if a not in blacklist]
320 peer_echo(includes=whitelist)
321
313 cookie = peer_retrieve('cookie')322 cookie = peer_retrieve('cookie')
314 if not cookie:323 if not cookie:
315 log('cluster_joined: cookie not yet set.', level=INFO)324 log('cluster_joined: cookie not yet set.', level=INFO)
316 return325 return
317326
318 rdata = relation_get()
319 if config('prefer-ipv6') and rdata.get('hostname'):327 if config('prefer-ipv6') and rdata.get('hostname'):
320 private_address = rdata['private-address']328 private_address = rdata.get('private-address')
321 hostname = rdata['hostname']329 hostname = rdata.get('hostname')
322 if hostname:330 if hostname:
323 rabbit.update_hosts_file({private_address: hostname})331 rabbit.update_hosts_file({private_address: hostname})
324332
325 # sync passwords
326 blacklist = ['hostname', 'private-address', 'public-address']
327 whitelist = [a for a in rdata.keys() if a not in blacklist]
328 peer_echo(includes=whitelist)
329
330 if not is_sufficient_peers():333 if not is_sufficient_peers():
331 # Stop rabbit until leader has finished configuring334 # Stop rabbit until leader has finished configuring
335 log('Not enough peers, stopping until leader is configured',
336 level=INFO)
332 service_stop('rabbitmq-server')337 service_stop('rabbitmq-server')
333 return338 return
334339
@@ -358,9 +363,12 @@
358 amqp_changed(relation_id=rid, remote_unit=unit)363 amqp_changed(relation_id=rid, remote_unit=unit)
359364
360365
361def update_cookie():366def update_cookie(leaders_cookie=None):
362 # sync cookie367 # sync cookie
363 cookie = peer_retrieve('cookie')368 if leaders_cookie:
369 cookie = leaders_cookie
370 else:
371 cookie = peer_retrieve('cookie')
364 cookie_local = None372 cookie_local = None
365 with open(rabbit.COOKIE_PATH, 'r') as f:373 with open(rabbit.COOKIE_PATH, 'r') as f:
366 cookie_local = f.read().strip()374 cookie_local = f.read().strip()
@@ -763,6 +771,18 @@
763771
764@hooks.hook('leader-settings-changed')772@hooks.hook('leader-settings-changed')
765def leader_settings_changed():773def leader_settings_changed():
774 # Get cookie from leader, update cookie locally and
775 # force cluster-relation-changed hooks to run on peers
776 cookie = leader_get(attribute='cookie')
777 if cookie:
778 update_cookie(leaders_cookie=cookie)
779 # Force cluster-relation-changed hooks to run on peers
780 # This will precipitate peer clustering
781 # Without this a chicken and egg scenario prevails when
782 # using LE and peerstorage
783 for rid in relation_ids('cluster'):
784 relation_set(relation_id=rid, relation_settings={'cookie': cookie})
785
766 # If leader has changed and access credentials, ripple these786 # If leader has changed and access credentials, ripple these
767 # out from all units787 # out from all units
768 for rid in relation_ids('amqp'):788 for rid in relation_ids('amqp'):
769789
=== modified file 'metadata.yaml'
--- metadata.yaml 2013-11-15 19:15:16 +0000
+++ metadata.yaml 2015-09-08 14:01:41 +0000
@@ -5,7 +5,10 @@
5 RabbitMQ is an implementation of AMQP, the emerging standard for high5 RabbitMQ is an implementation of AMQP, the emerging standard for high
6 performance enterprise messaging. The RabbitMQ server is a robust and6 performance enterprise messaging. The RabbitMQ server is a robust and
7 scalable implementation of an AMQP broker.7 scalable implementation of an AMQP broker.
8categories: ["misc"]8tags:
9 - openstack
10 - amqp
11 - misc
9provides:12provides:
10 amqp:13 amqp:
11 interface: rabbitmq14 interface: rabbitmq
1215
=== added file 'tests/00-setup'
--- tests/00-setup 1970-01-01 00:00:00 +0000
+++ tests/00-setup 2015-09-08 14:01:41 +0000
@@ -0,0 +1,16 @@
1#!/bin/bash
2
3set -ex
4
5sudo add-apt-repository --yes ppa:juju/stable
6sudo apt-get update --yes
7sudo apt-get install --yes python-amulet \
8 python-cinderclient \
9 python-distro-info \
10 python-glanceclient \
11 python-heatclient \
12 python-keystoneclient \
13 python-neutronclient \
14 python-novaclient \
15 python-pika \
16 python-swiftclient
017
=== added file 'tests/014-basic-precise-icehouse'
--- tests/014-basic-precise-icehouse 1970-01-01 00:00:00 +0000
+++ tests/014-basic-precise-icehouse 2015-09-08 14:01:41 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic rabbitmq-server deployment on precise-icehouse."""
4
5from basic_deployment import RmqBasicDeployment
6
7if __name__ == '__main__':
8 deployment = RmqBasicDeployment(series='precise',
9 openstack='cloud:precise-icehouse',
10 source='cloud:precise-updates/icehouse')
11 deployment.run_tests()
012
=== added file 'tests/015-basic-trusty-icehouse'
--- tests/015-basic-trusty-icehouse 1970-01-01 00:00:00 +0000
+++ tests/015-basic-trusty-icehouse 2015-09-08 14:01:41 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic rabbitmq-server deployment on trusty-icehouse."""
4
5from basic_deployment import RmqBasicDeployment
6
7if __name__ == '__main__':
8 deployment = RmqBasicDeployment(series='trusty')
9 deployment.run_tests()
010
=== added file 'tests/016-basic-trusty-juno'
--- tests/016-basic-trusty-juno 1970-01-01 00:00:00 +0000
+++ tests/016-basic-trusty-juno 2015-09-08 14:01:41 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic rabbitmq-server deployment on trusty-juno."""
4
5from basic_deployment import RmqBasicDeployment
6
7if __name__ == '__main__':
8 deployment = RmqBasicDeployment(series='trusty',
9 openstack='cloud:trusty-juno',
10 source='cloud:trusty-updates/juno')
11 deployment.run_tests()
012
=== added file 'tests/017-basic-trusty-kilo'
--- tests/017-basic-trusty-kilo 1970-01-01 00:00:00 +0000
+++ tests/017-basic-trusty-kilo 2015-09-08 14:01:41 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic rabbitmq-server deployment on trusty-kilo."""
4
5from basic_deployment import RmqBasicDeployment
6
7if __name__ == '__main__':
8 deployment = RmqBasicDeployment(series='trusty',
9 openstack='cloud:trusty-kilo',
10 source='cloud:trusty-updates/kilo')
11 deployment.run_tests()
012
=== added file 'tests/019-basic-vivid-kilo'
--- tests/019-basic-vivid-kilo 1970-01-01 00:00:00 +0000
+++ tests/019-basic-vivid-kilo 2015-09-08 14:01:41 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic rabbitmq-server deployment on vivid-kilo."""
4
5from basic_deployment import RmqBasicDeployment
6
7if __name__ == '__main__':
8 deployment = RmqBasicDeployment(series='vivid')
9 deployment.run_tests()
010
=== added file 'tests/020-basic-trusty-liberty'
--- tests/020-basic-trusty-liberty 1970-01-01 00:00:00 +0000
+++ tests/020-basic-trusty-liberty 2015-09-08 14:01:41 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic rabbitmq-server deployment on trusty-liberty."""
4
5from basic_deployment import RmqBasicDeployment
6
7if __name__ == '__main__':
8 deployment = RmqBasicDeployment(series='trusty',
9 openstack='cloud:trusty-liberty',
10 source='cloud:trusty-updates/liberty')
11 deployment.run_tests()
012
=== added file 'tests/021-basic-wily-liberty'
--- tests/021-basic-wily-liberty 1970-01-01 00:00:00 +0000
+++ tests/021-basic-wily-liberty 2015-09-08 14:01:41 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic rabbitmq-server deployment on wily-liberty."""
4
5from basic_deployment import RmqBasicDeployment
6
7if __name__ == '__main__':
8 deployment = RmqBasicDeployment(series='wily')
9 deployment.run_tests()
010
=== added file 'tests/basic_deployment.py'
--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
+++ tests/basic_deployment.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,492 @@
1#!/usr/bin/python
2"""
3Basic 3-node rabbitmq-server native cluster + nrpe functional tests
4
5Cinder is present to exercise and inspect amqp relation functionality.
6
7Each individual test is idempotent, in that it creates/deletes
8a rmq test user, enables or disables ssl as needed.
9
10Test order is not required, however tests are numbered to keep
11relevant tests grouped together in run order.
12"""
13
14import amulet
15import time
16
17from charmhelpers.contrib.openstack.amulet.deployment import (
18 OpenStackAmuletDeployment
19)
20
21from charmhelpers.contrib.openstack.amulet.utils import (
22 OpenStackAmuletUtils,
23 DEBUG,
24 # ERROR
25)
26
27# Use DEBUG to turn on debug logging
28u = OpenStackAmuletUtils(DEBUG)
29
30
31class RmqBasicDeployment(OpenStackAmuletDeployment):
32 """Amulet tests on a basic rabbitmq cluster deployment. Verify
33 relations, service status, users and endpoint service catalog."""
34
35 def __init__(self, series=None, openstack=None, source=None, stable=False):
36 """Deploy the entire test environment."""
37 super(RmqBasicDeployment, self).__init__(series, openstack, source,
38 stable)
39 self._add_services()
40 self._add_relations()
41 self._configure_services()
42 self._deploy()
43 self._initialize_tests()
44
45 def _add_services(self):
46 """Add services
47
48 Add the services that we're testing, where rmq is local,
49 and the rest of the service are from lp branches that are
50 compatible with the local charm (e.g. stable or next).
51 """
52 this_service = {
53 'name': 'rabbitmq-server',
54 'units': 3
55 }
56 other_services = [{'name': 'cinder'},
57 {'name': 'nrpe'}]
58
59 super(RmqBasicDeployment, self)._add_services(this_service,
60 other_services)
61
62 def _add_relations(self):
63 """Add relations for the services."""
64 relations = {'cinder:amqp': 'rabbitmq-server:amqp',
65 'nrpe:nrpe-external-master':
66 'rabbitmq-server:nrpe-external-master'}
67
68 super(RmqBasicDeployment, self)._add_relations(relations)
69
70 def _configure_services(self):
71 """Configure all of the services."""
72 rmq_config = {
73 'min-cluster-size': '3',
74 'max-cluster-tries': '6',
75 'ssl': 'off',
76 'management_plugin': 'False',
77 'stats_cron_schedule': '*/1 * * * *'
78 }
79 cinder_config = {}
80 configs = {'rabbitmq-server': rmq_config,
81 'cinder': cinder_config}
82 super(RmqBasicDeployment, self)._configure_services(configs)
83
84 def _initialize_tests(self):
85 """Perform final initialization before tests get run."""
86 # Access the sentries for inspecting service units
87 self.rmq0_sentry = self.d.sentry.unit['rabbitmq-server/0']
88 self.rmq1_sentry = self.d.sentry.unit['rabbitmq-server/1']
89 self.rmq2_sentry = self.d.sentry.unit['rabbitmq-server/2']
90 self.cinder_sentry = self.d.sentry.unit['cinder/0']
91 self.nrpe_sentry = self.d.sentry.unit['nrpe/0']
92 u.log.debug('openstack release val: {}'.format(
93 self._get_openstack_release()))
94 u.log.debug('openstack release str: {}'.format(
95 self._get_openstack_release_string()))
96
97 # Let things settle a bit before moving forward
98 time.sleep(30)
99
100 def _get_rmq_sentry_units(self):
101 """Local helper specific to this 3-node rmq series of tests."""
102 return [self.rmq0_sentry,
103 self.rmq1_sentry,
104 self.rmq2_sentry]
105
106 def _test_rmq_amqp_messages_all_units(self, sentry_units,
107 ssl=False, port=None):
108 """Reusable test to send amqp messages to every listed rmq unit
109 and check every listed rmq unit for messages.
110
111 :param sentry_units: list of sentry units
112 :returns: None if successful. Raise on error.
113 """
114
115 # Add test user if it does not already exist
116 u.add_rmq_test_user(sentry_units)
117
118 # Handle ssl
119 if ssl:
120 u.configure_rmq_ssl_on(sentry_units, self.d, port=port)
121 else:
122 u.configure_rmq_ssl_off(sentry_units, self.d)
123
124 # Publish and get amqp messages in all possible unit combinations.
125 # Qty of checks == (qty of units) ^ 2
126 amqp_msg_counter = 1
127 host_names = u.get_unit_hostnames(sentry_units)
128
129 for dest_unit in sentry_units:
130 dest_unit_name = dest_unit.info['unit_name']
131 dest_unit_host = dest_unit.info['public-address']
132 dest_unit_host_name = host_names[dest_unit_name]
133
134 for check_unit in sentry_units:
135 check_unit_name = check_unit.info['unit_name']
136 check_unit_host = check_unit.info['public-address']
137 check_unit_host_name = host_names[check_unit_name]
138
139 amqp_msg_stamp = u.get_uuid_epoch_stamp()
140 amqp_msg = ('Message {}@{} {}'.format(amqp_msg_counter,
141 dest_unit_host,
142 amqp_msg_stamp)).upper()
143 # Publish amqp message
144 u.log.debug('Publish message to: {} '
145 '({} {})'.format(dest_unit_host,
146 dest_unit_name,
147 dest_unit_host_name))
148
149 u.publish_amqp_message_by_unit(dest_unit,
150 amqp_msg, ssl=ssl,
151 port=port)
152
153 # Wait a bit before checking for message
154 time.sleep(2)
155
156 # Get amqp message
157 u.log.debug('Get message from: {} '
158 '({} {})'.format(check_unit_host,
159 check_unit_name,
160 check_unit_host_name))
161
162 amqp_msg_rcvd = u.get_amqp_message_by_unit(check_unit,
163 ssl=ssl,
164 port=port)
165
166 # Validate amqp message content
167 if amqp_msg == amqp_msg_rcvd:
168 u.log.debug('Message {} received '
169 'OK.'.format(amqp_msg_counter))
170 else:
171 u.log.error('Expected: {}'.format(amqp_msg))
172 u.log.error('Actual: {}'.format(amqp_msg_rcvd))
173 msg = 'Message {} mismatch.'.format(amqp_msg_counter)
174 amulet.raise_status(amulet.FAIL, msg)
175
176 amqp_msg_counter += 1
177
178 # Delete the test user
179 u.delete_rmq_test_user(sentry_units)
180
181 def test_100_rmq_processes(self):
182 """Verify that the expected service processes are running
183 on each rabbitmq-server unit."""
184
185 # Beam and epmd sometimes briefly have more than one PID,
186 # True checks for at least 1.
187 rmq_processes = {
188 'beam': True,
189 'epmd': True,
190 }
191
192 # Units with process names and PID quantities expected
193 expected_processes = {
194 self.rmq0_sentry: rmq_processes,
195 self.rmq1_sentry: rmq_processes,
196 self.rmq2_sentry: rmq_processes
197 }
198
199 actual_pids = u.get_unit_process_ids(expected_processes)
200 ret = u.validate_unit_process_ids(expected_processes, actual_pids)
201 if ret:
202 amulet.raise_status(amulet.FAIL, msg=ret)
203
204 u.log.info('OK\n')
205
206 def test_102_services(self):
207 """Verify that the expected services are running on the
208 corresponding service units."""
209 services = {
210 self.rmq0_sentry: ['rabbitmq-server'],
211 self.rmq1_sentry: ['rabbitmq-server'],
212 self.rmq2_sentry: ['rabbitmq-server'],
213 self.cinder_sentry: ['cinder-api',
214 'cinder-scheduler',
215 'cinder-volume'],
216 }
217 ret = u.validate_services_by_name(services)
218 if ret:
219 amulet.raise_status(amulet.FAIL, msg=ret)
220
221 u.log.info('OK\n')
222
223 def test_200_rmq_cinder_amqp_relation(self):
224 """Verify the rabbitmq-server:cinder amqp relation data"""
225 u.log.debug('Checking rmq:cinder amqp relation data...')
226 unit = self.rmq0_sentry
227 relation = ['amqp', 'cinder:amqp']
228 expected = {
229 'private-address': u.valid_ip,
230 'password': u.not_null,
231 'hostname': u.valid_ip
232 }
233 ret = u.validate_relation_data(unit, relation, expected)
234 if ret:
235 msg = u.relation_error('amqp cinder', ret)
236 amulet.raise_status(amulet.FAIL, msg=msg)
237
238 u.log.info('OK\n')
239
240 def test_201_cinder_rmq_amqp_relation(self):
241 """Verify the cinder:rabbitmq-server amqp relation data"""
242 u.log.debug('Checking cinder:rmq amqp relation data...')
243 unit = self.cinder_sentry
244 relation = ['amqp', 'rabbitmq-server:amqp']
245 expected = {
246 'private-address': u.valid_ip,
247 'vhost': 'openstack',
248 'username': u.not_null
249 }
250 ret = u.validate_relation_data(unit, relation, expected)
251 if ret:
252 msg = u.relation_error('cinder amqp', ret)
253 amulet.raise_status(amulet.FAIL, msg=msg)
254
255 u.log.info('OK\n')
256
257 def test_202_rmq_nrpe_ext_master_relation(self):
258 """Verify rabbitmq-server:nrpe nrpe-external-master relation data"""
259 u.log.debug('Checking rmq:nrpe external master relation data...')
260 unit = self.rmq0_sentry
261 relation = ['nrpe-external-master',
262 'nrpe:nrpe-external-master']
263
264 mon_sub = ('monitors:\n remote:\n nrpe:\n rabbitmq: '
265 '{command: check_rabbitmq}\n rabbitmq_queue: '
266 '{command: check_rabbitmq_queue}\n')
267
268 expected = {
269 'private-address': u.valid_ip,
270 'monitors': mon_sub
271 }
272
273 ret = u.validate_relation_data(unit, relation, expected)
274 if ret:
275 msg = u.relation_error('amqp nrpe', ret)
276 amulet.raise_status(amulet.FAIL, msg=msg)
277
278 u.log.info('OK\n')
279
280 def test_203_nrpe_rmq_ext_master_relation(self):
281 """Verify nrpe:rabbitmq-server nrpe-external-master relation data"""
282 u.log.debug('Checking nrpe:rmq external master relation data...')
283 unit = self.nrpe_sentry
284 relation = ['nrpe-external-master',
285 'rabbitmq-server:nrpe-external-master']
286
287 expected = {
288 'private-address': u.valid_ip
289 }
290
291 ret = u.validate_relation_data(unit, relation, expected)
292 if ret:
293 msg = u.relation_error('nrpe amqp', ret)
294 amulet.raise_status(amulet.FAIL, msg=msg)
295
296 u.log.info('OK\n')
297
298 def test_300_rmq_config(self):
299 """Verify the data in the rabbitmq conf file."""
300 conf = '/etc/rabbitmq/rabbitmq-env.conf'
301 sentry_units = self._get_rmq_sentry_units()
302 for unit in sentry_units:
303 host_name = unit.file_contents('/etc/hostname').strip()
304 u.log.debug('Checking rabbitmq config file data on '
305 '{} ({})...'.format(unit.info['unit_name'],
306 host_name))
307 expected = {
308 'RABBITMQ_NODENAME': 'rabbit@{}'.format(host_name)
309 }
310
311 file_contents = unit.file_contents(conf)
312 u.validate_sectionless_conf(file_contents, expected)
313
314 u.log.info('OK\n')
315
316 def test_400_rmq_cluster_running_nodes(self):
317 """Verify that cluster status from each rmq juju unit shows
318 every cluster node as a running member in that cluster."""
319 u.log.debug('Checking that all units are in cluster_status '
320 'running nodes...')
321
322 sentry_units = self._get_rmq_sentry_units()
323
324 ret = u.validate_rmq_cluster_running_nodes(sentry_units)
325 if ret:
326 amulet.raise_status(amulet.FAIL, msg=ret)
327
328 u.log.info('OK\n')
329
330 def test_402_rmq_connect_with_ssl_off(self):
331 """Verify successful non-ssl amqp connection to all units when
332 charm config option for ssl is set False."""
333 u.log.debug('Confirming that non-ssl connection succeeds when '
334 'ssl config is off...')
335 sentry_units = self._get_rmq_sentry_units()
336 u.add_rmq_test_user(sentry_units)
337 u.configure_rmq_ssl_off(sentry_units, self.d)
338
339 # Check amqp connection for all units, expect connections to succeed
340 for unit in sentry_units:
341 connection = u.connect_amqp_by_unit(unit, ssl=False, fatal=False)
342 connection.close()
343
344 u.delete_rmq_test_user(sentry_units)
345 u.log.info('OK\n')
346
347 def test_404_rmq_ssl_connect_with_ssl_off(self):
348 """Verify unsuccessful ssl amqp connection to all units when
349 charm config option for ssl is set False."""
350 u.log.debug('Confirming that ssl connection fails when ssl '
351 'config is off...')
352 sentry_units = self._get_rmq_sentry_units()
353 u.add_rmq_test_user(sentry_units)
354 u.configure_rmq_ssl_off(sentry_units, self.d)
355
356 # Check ssl amqp connection for all units, expect connections to fail
357 for unit in sentry_units:
358 connection = u.connect_amqp_by_unit(unit, ssl=True,
359 port=5971, fatal=False)
360 if connection:
361 connection.close()
362 msg = 'SSL connection unexpectedly succeeded with ssl=off'
363 amulet.raise_status(amulet.FAIL, msg)
364
365 u.delete_rmq_test_user(sentry_units)
366 u.log.info('OK - Confirmed that ssl connection attempt fails '
367 'when ssl config is off.')
368
369 def test_406_rmq_amqp_messages_all_units_ssl_off(self):
370 """Send amqp messages to every rmq unit and check every rmq unit
371 for messages. Standard amqp tcp port, no ssl."""
372 u.log.debug('Checking amqp message publish/get on all units '
373 '(ssl off)...')
374
375 sentry_units = self._get_rmq_sentry_units()
376 self._test_rmq_amqp_messages_all_units(sentry_units, ssl=False)
377 u.log.info('OK\n')
378
379 def test_408_rmq_amqp_messages_all_units_ssl_on(self):
380 """Send amqp messages with ssl enabled, to every rmq unit and
381 check every rmq unit for messages. Standard ssl tcp port."""
382 u.log.debug('Checking amqp message publish/get on all units '
383 '(ssl on)...')
384
385 sentry_units = self._get_rmq_sentry_units()
386 self._test_rmq_amqp_messages_all_units(sentry_units,
387 ssl=True, port=5671)
388 u.log.info('OK\n')
389
390 def test_410_rmq_amqp_messages_all_units_ssl_alt_port(self):
391 """Send amqp messages with ssl on, to every rmq unit and check
392 every rmq unit for messages. Custom ssl tcp port."""
393 u.log.debug('Checking amqp message publish/get on all units '
394 '(ssl on)...')
395
396 sentry_units = self._get_rmq_sentry_units()
397 self._test_rmq_amqp_messages_all_units(sentry_units,
398 ssl=True, port=5999)
399 u.log.info('OK\n')
400
401 def test_412_rmq_management_plugin(self):
402 """Enable and check management plugin."""
403 u.log.debug('Checking tcp socket connect to management plugin '
404 'port on all rmq units...')
405
406 sentry_units = self._get_rmq_sentry_units()
407 mgmt_port = 15672
408
409 # Enable management plugin
410 u.log.debug('Enabling management_plugin charm config option...')
411 config = {'management_plugin': 'True'}
412 self.d.configure('rabbitmq-server', config)
413
414 # Check tcp connect to management plugin port
415 max_wait = 120
416 tries = 0
417 ret = u.port_knock_units(sentry_units, mgmt_port)
418 while ret and tries < (max_wait / 12):
419 time.sleep(12)
420 u.log.debug('Attempt {}: {}'.format(tries, ret))
421 ret = u.port_knock_units(sentry_units, mgmt_port)
422 tries += 1
423
424 if ret:
425 amulet.raise_status(amulet.FAIL, ret)
426 else:
427 u.log.debug('Connect to all units (OK)\n')
428
429 # Disable management plugin
430 u.log.debug('Disabling management_plugin charm config option...')
431 config = {'management_plugin': 'False'}
432 self.d.configure('rabbitmq-server', config)
433
434 # Negative check - tcp connect to management plugin port
435 u.log.info('Expect tcp connect fail since charm config '
436 'option is disabled.')
437 tries = 0
438 ret = u.port_knock_units(sentry_units, mgmt_port, expect_success=False)
439 while ret and tries < (max_wait / 12):
440 time.sleep(12)
441 u.log.debug('Attempt {}: {}'.format(tries, ret))
442 ret = u.port_knock_units(sentry_units, mgmt_port,
443 expect_success=False)
444 tries += 1
445
446 if ret:
447 amulet.raise_status(amulet.FAIL, ret)
448 else:
449 u.log.info('Confirm mgmt port closed on all units (OK)\n')
450
451 def test_414_rmq_nrpe_monitors(self):
452 """Check rabbimq-server nrpe monitor basic functionality."""
453 sentry_units = self._get_rmq_sentry_units()
454 host_names = u.get_unit_hostnames(sentry_units)
455
456 # check_rabbitmq monitor
457 u.log.debug('Checking nrpe check_rabbitmq on units...')
458 cmds = ['egrep -oh /usr/local.* /etc/nagios/nrpe.d/'
459 'check_rabbitmq.cfg']
460 ret = u.check_commands_on_units(cmds, sentry_units)
461 if ret:
462 amulet.raise_status(amulet.FAIL, msg=ret)
463
464 u.log.debug('Sleeping 70s for 1m cron job to run...')
465 time.sleep(70)
466
467 # check_rabbitmq_queue monitor
468 u.log.debug('Checking nrpe check_rabbitmq_queue on units...')
469 cmds = ['egrep -oh /usr/local.* /etc/nagios/nrpe.d/'
470 'check_rabbitmq_queue.cfg']
471 ret = u.check_commands_on_units(cmds, sentry_units)
472 if ret:
473 amulet.raise_status(amulet.FAIL, msg=ret)
474
475 # check dat file existence
476 u.log.debug('Checking nrpe dat file existence on units...')
477 for sentry_unit in sentry_units:
478 unit_name = sentry_unit.info['unit_name']
479 unit_host_name = host_names[unit_name]
480
481 cmds = [
482 'stat /var/lib/rabbitmq/data/{}_general_stats.dat'.format(
483 unit_host_name),
484 'stat /var/lib/rabbitmq/data/{}_queue_stats.dat'.format(
485 unit_host_name)
486 ]
487
488 ret = u.check_commands_on_units(cmds, [sentry_unit])
489 if ret:
490 amulet.raise_status(amulet.FAIL, msg=ret)
491
492 u.log.info('OK\n')
0493
=== added file 'tests/charmhelpers/__init__.py'
--- tests/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/__init__.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,38 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17# Bootstrap charm-helpers, installing its dependencies if necessary using
18# only standard libraries.
19import subprocess
20import sys
21
22try:
23 import six # flake8: noqa
24except ImportError:
25 if sys.version_info.major == 2:
26 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
27 else:
28 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
29 import six # flake8: noqa
30
31try:
32 import yaml # flake8: noqa
33except ImportError:
34 if sys.version_info.major == 2:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
36 else:
37 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
38 import yaml # flake8: noqa
039
=== removed file 'tests/charmhelpers/__init__.py'
--- tests/charmhelpers/__init__.py 2015-04-13 22:11:34 +0000
+++ tests/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,38 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17# Bootstrap charm-helpers, installing its dependencies if necessary using
18# only standard libraries.
19import subprocess
20import sys
21
22try:
23 import six # flake8: noqa
24except ImportError:
25 if sys.version_info.major == 2:
26 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
27 else:
28 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
29 import six # flake8: noqa
30
31try:
32 import yaml # flake8: noqa
33except ImportError:
34 if sys.version_info.major == 2:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
36 else:
37 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
38 import yaml # flake8: noqa
390
=== removed directory 'tests/charmhelpers/cli'
=== removed file 'tests/charmhelpers/cli/__init__.py'
--- tests/charmhelpers/cli/__init__.py 2015-08-19 13:49:53 +0000
+++ tests/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,191 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import inspect
18import argparse
19import sys
20
21from six.moves import zip
22
23from charmhelpers.core import unitdata
24
25
26class OutputFormatter(object):
27 def __init__(self, outfile=sys.stdout):
28 self.formats = (
29 "raw",
30 "json",
31 "py",
32 "yaml",
33 "csv",
34 "tab",
35 )
36 self.outfile = outfile
37
38 def add_arguments(self, argument_parser):
39 formatgroup = argument_parser.add_mutually_exclusive_group()
40 choices = self.supported_formats
41 formatgroup.add_argument("--format", metavar='FMT',
42 help="Select output format for returned data, "
43 "where FMT is one of: {}".format(choices),
44 choices=choices, default='raw')
45 for fmt in self.formats:
46 fmtfunc = getattr(self, fmt)
47 formatgroup.add_argument("-{}".format(fmt[0]),
48 "--{}".format(fmt), action='store_const',
49 const=fmt, dest='format',
50 help=fmtfunc.__doc__)
51
52 @property
53 def supported_formats(self):
54 return self.formats
55
56 def raw(self, output):
57 """Output data as raw string (default)"""
58 if isinstance(output, (list, tuple)):
59 output = '\n'.join(map(str, output))
60 self.outfile.write(str(output))
61
62 def py(self, output):
63 """Output data as a nicely-formatted python data structure"""
64 import pprint
65 pprint.pprint(output, stream=self.outfile)
66
67 def json(self, output):
68 """Output data in JSON format"""
69 import json
70 json.dump(output, self.outfile)
71
72 def yaml(self, output):
73 """Output data in YAML format"""
74 import yaml
75 yaml.safe_dump(output, self.outfile)
76
77 def csv(self, output):
78 """Output data as excel-compatible CSV"""
79 import csv
80 csvwriter = csv.writer(self.outfile)
81 csvwriter.writerows(output)
82
83 def tab(self, output):
84 """Output data in excel-compatible tab-delimited format"""
85 import csv
86 csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
87 csvwriter.writerows(output)
88
89 def format_output(self, output, fmt='raw'):
90 fmtfunc = getattr(self, fmt)
91 fmtfunc(output)
92
93
94class CommandLine(object):
95 argument_parser = None
96 subparsers = None
97 formatter = None
98 exit_code = 0
99
100 def __init__(self):
101 if not self.argument_parser:
102 self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
103 if not self.formatter:
104 self.formatter = OutputFormatter()
105 self.formatter.add_arguments(self.argument_parser)
106 if not self.subparsers:
107 self.subparsers = self.argument_parser.add_subparsers(help='Commands')
108
109 def subcommand(self, command_name=None):
110 """
111 Decorate a function as a subcommand. Use its arguments as the
112 command-line arguments"""
113 def wrapper(decorated):
114 cmd_name = command_name or decorated.__name__
115 subparser = self.subparsers.add_parser(cmd_name,
116 description=decorated.__doc__)
117 for args, kwargs in describe_arguments(decorated):
118 subparser.add_argument(*args, **kwargs)
119 subparser.set_defaults(func=decorated)
120 return decorated
121 return wrapper
122
123 def test_command(self, decorated):
124 """
125 Subcommand is a boolean test function, so bool return values should be
126 converted to a 0/1 exit code.
127 """
128 decorated._cli_test_command = True
129 return decorated
130
131 def no_output(self, decorated):
132 """
133 Subcommand is not expected to return a value, so don't print a spurious None.
134 """
135 decorated._cli_no_output = True
136 return decorated
137
138 def subcommand_builder(self, command_name, description=None):
139 """
140 Decorate a function that builds a subcommand. Builders should accept a
141 single argument (the subparser instance) and return the function to be
142 run as the command."""
143 def wrapper(decorated):
144 subparser = self.subparsers.add_parser(command_name)
145 func = decorated(subparser)
146 subparser.set_defaults(func=func)
147 subparser.description = description or func.__doc__
148 return wrapper
149
150 def run(self):
151 "Run cli, processing arguments and executing subcommands."
152 arguments = self.argument_parser.parse_args()
153 argspec = inspect.getargspec(arguments.func)
154 vargs = []
155 for arg in argspec.args:
156 vargs.append(getattr(arguments, arg))
157 if argspec.varargs:
158 vargs.extend(getattr(arguments, argspec.varargs))
159 output = arguments.func(*vargs)
160 if getattr(arguments.func, '_cli_test_command', False):
161 self.exit_code = 0 if output else 1
162 output = ''
163 if getattr(arguments.func, '_cli_no_output', False):
164 output = ''
165 self.formatter.format_output(output, arguments.format)
166 if unitdata._KV:
167 unitdata._KV.flush()
168
169
170cmdline = CommandLine()
171
172
173def describe_arguments(func):
174 """
175 Analyze a function's signature and return a data structure suitable for
176 passing in as arguments to an argparse parser's add_argument() method."""
177
178 argspec = inspect.getargspec(func)
179 # we should probably raise an exception somewhere if func includes **kwargs
180 if argspec.defaults:
181 positional_args = argspec.args[:-len(argspec.defaults)]
182 keyword_names = argspec.args[-len(argspec.defaults):]
183 for arg, default in zip(keyword_names, argspec.defaults):
184 yield ('--{}'.format(arg),), {'default': default}
185 else:
186 positional_args = argspec.args
187
188 for arg in positional_args:
189 yield (arg,), {}
190 if argspec.varargs:
191 yield (argspec.varargs,), {'nargs': '*'}
1920
=== removed file 'tests/charmhelpers/cli/benchmark.py'
--- tests/charmhelpers/cli/benchmark.py 2015-07-31 22:00:49 +0000
+++ tests/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
@@ -1,36 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from . import cmdline
18from charmhelpers.contrib.benchmark import Benchmark
19
20
21@cmdline.subcommand(command_name='benchmark-start')
22def start():
23 Benchmark.start()
24
25
26@cmdline.subcommand(command_name='benchmark-finish')
27def finish():
28 Benchmark.finish()
29
30
31@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
32def service(subparser):
33 subparser.add_argument("value", help="The composite score.")
34 subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
35 subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
36 return Benchmark.set_composite_score
370
=== removed file 'tests/charmhelpers/cli/commands.py'
--- tests/charmhelpers/cli/commands.py 2015-08-19 13:49:53 +0000
+++ tests/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
@@ -1,32 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""
18This module loads sub-modules into the python runtime so they can be
19discovered via the inspect module. In order to prevent flake8 from (rightfully)
20telling us these are unused modules, throw a ' # noqa' at the end of each import
21so that the warning is suppressed.
22"""
23
24from . import CommandLine # noqa
25
26"""
27Import the sub-modules which have decorated subcommands to register with chlp.
28"""
29from . import host # noqa
30from . import benchmark # noqa
31from . import unitdata # noqa
32from . import hookenv # noqa
330
=== removed file 'tests/charmhelpers/cli/hookenv.py'
--- tests/charmhelpers/cli/hookenv.py 2015-08-19 13:49:53 +0000
+++ tests/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
@@ -1,23 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from . import cmdline
18from charmhelpers.core import hookenv
19
20
21cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
22cmdline.subcommand('service-name')(hookenv.service_name)
23cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
240
=== removed file 'tests/charmhelpers/cli/host.py'
--- tests/charmhelpers/cli/host.py 2015-07-31 22:00:49 +0000
+++ tests/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from . import cmdline
18from charmhelpers.core import host
19
20
21@cmdline.subcommand()
22def mounts():
23 "List mounts"
24 return host.mounts()
25
26
27@cmdline.subcommand_builder('service', description="Control system services")
28def service(subparser):
29 subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
30 subparser.add_argument("service_name", help="Name of the service to control")
31 return host.service
320
=== removed file 'tests/charmhelpers/cli/unitdata.py'
--- tests/charmhelpers/cli/unitdata.py 2015-07-31 22:00:49 +0000
+++ tests/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
@@ -1,39 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from . import cmdline
18from charmhelpers.core import unitdata
19
20
21@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
22def unitdata_cmd(subparser):
23 nested = subparser.add_subparsers()
24 get_cmd = nested.add_parser('get', help='Retrieve data')
25 get_cmd.add_argument('key', help='Key to retrieve the value of')
26 get_cmd.set_defaults(action='get', value=None)
27 set_cmd = nested.add_parser('set', help='Store data')
28 set_cmd.add_argument('key', help='Key to set')
29 set_cmd.add_argument('value', help='Value to store')
30 set_cmd.set_defaults(action='set')
31
32 def _unitdata_cmd(action, key, value):
33 if action == 'get':
34 return unitdata.kv().get(key)
35 elif action == 'set':
36 unitdata.kv().set(key, value)
37 unitdata.kv().flush()
38 return ''
39 return _unitdata_cmd
400
=== added directory 'tests/charmhelpers/contrib'
=== removed directory 'tests/charmhelpers/contrib'
=== added file 'tests/charmhelpers/contrib/__init__.py'
--- tests/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/__init__.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== removed file 'tests/charmhelpers/contrib/__init__.py'
--- tests/charmhelpers/contrib/__init__.py 2015-04-13 22:11:34 +0000
+++ tests/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,15 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
160
=== added directory 'tests/charmhelpers/contrib/amulet'
=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
--- tests/charmhelpers/contrib/amulet/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/amulet/__init__.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,93 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import amulet
18import os
19import six
20
21
22class AmuletDeployment(object):
23 """Amulet deployment.
24
25 This class provides generic Amulet deployment and test runner
26 methods.
27 """
28
29 def __init__(self, series=None):
30 """Initialize the deployment environment."""
31 self.series = None
32
33 if series:
34 self.series = series
35 self.d = amulet.Deployment(series=self.series)
36 else:
37 self.d = amulet.Deployment()
38
39 def _add_services(self, this_service, other_services):
40 """Add services.
41
42 Add services to the deployment where this_service is the local charm
43 that we're testing and other_services are the other services that
44 are being used in the local amulet tests.
45 """
46 if this_service['name'] != os.path.basename(os.getcwd()):
47 s = this_service['name']
48 msg = "The charm's root directory name needs to be {}".format(s)
49 amulet.raise_status(amulet.FAIL, msg=msg)
50
51 if 'units' not in this_service:
52 this_service['units'] = 1
53
54 self.d.add(this_service['name'], units=this_service['units'])
55
56 for svc in other_services:
57 if 'location' in svc:
58 branch_location = svc['location']
59 elif self.series:
60 branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
61 else:
62 branch_location = None
63
64 if 'units' not in svc:
65 svc['units'] = 1
66
67 self.d.add(svc['name'], charm=branch_location, units=svc['units'])
68
69 def _add_relations(self, relations):
70 """Add all of the relations for the services."""
71 for k, v in six.iteritems(relations):
72 self.d.relate(k, v)
73
74 def _configure_services(self, configs):
75 """Configure all of the services."""
76 for service, config in six.iteritems(configs):
77 self.d.configure(service, config)
78
79 def _deploy(self):
80 """Deploy environment and wait for all hooks to finish executing."""
81 try:
82 self.d.setup(timeout=900)
83 self.d.sentry.wait(timeout=900)
84 except amulet.helpers.TimeoutError:
85 amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
86 except Exception:
87 raise
88
89 def run_tests(self):
90 """Run all of the methods that are prefixed with 'test_'."""
91 for test in dir(self):
92 if test.startswith('test_'):
93 getattr(self, test)()
094
=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,778 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import io
18import json
19import logging
20import os
21import re
22import socket
23import subprocess
24import sys
25import time
26import uuid
27
28import amulet
29import distro_info
30import six
31from six.moves import configparser
32if six.PY3:
33 from urllib import parse as urlparse
34else:
35 import urlparse
36
37
38class AmuletUtils(object):
39 """Amulet utilities.
40
41 This class provides common utility functions that are used by Amulet
42 tests.
43 """
44
45 def __init__(self, log_level=logging.ERROR):
46 self.log = self.get_logger(level=log_level)
47 self.ubuntu_releases = self.get_ubuntu_releases()
48
49 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
50 """Get a logger object that will log to stdout."""
51 log = logging
52 logger = log.getLogger(name)
53 fmt = log.Formatter("%(asctime)s %(funcName)s "
54 "%(levelname)s: %(message)s")
55
56 handler = log.StreamHandler(stream=sys.stdout)
57 handler.setLevel(level)
58 handler.setFormatter(fmt)
59
60 logger.addHandler(handler)
61 logger.setLevel(level)
62
63 return logger
64
65 def valid_ip(self, ip):
66 if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
67 return True
68 else:
69 return False
70
71 def valid_url(self, url):
72 p = re.compile(
73 r'^(?:http|ftp)s?://'
74 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
75 r'localhost|'
76 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
77 r'(?::\d+)?'
78 r'(?:/?|[/?]\S+)$',
79 re.IGNORECASE)
80 if p.match(url):
81 return True
82 else:
83 return False
84
85 def get_ubuntu_release_from_sentry(self, sentry_unit):
86 """Get Ubuntu release codename from sentry unit.
87
88 :param sentry_unit: amulet sentry/service unit pointer
89 :returns: list of strings - release codename, failure message
90 """
91 msg = None
92 cmd = 'lsb_release -cs'
93 release, code = sentry_unit.run(cmd)
94 if code == 0:
95 self.log.debug('{} lsb_release: {}'.format(
96 sentry_unit.info['unit_name'], release))
97 else:
98 msg = ('{} `{}` returned {} '
99 '{}'.format(sentry_unit.info['unit_name'],
100 cmd, release, code))
101 if release not in self.ubuntu_releases:
102 msg = ("Release ({}) not found in Ubuntu releases "
103 "({})".format(release, self.ubuntu_releases))
104 return release, msg
105
106 def validate_services(self, commands):
107 """Validate that lists of commands succeed on service units. Can be
108 used to verify system services are running on the corresponding
109 service units.
110
111 :param commands: dict with sentry keys and arbitrary command list vals
112 :returns: None if successful, Failure string message otherwise
113 """
114 self.log.debug('Checking status of system services...')
115
116 # /!\ DEPRECATION WARNING (beisner):
117 # New and existing tests should be rewritten to use
118 # validate_services_by_name() as it is aware of init systems.
119 self.log.warn('DEPRECATION WARNING: use '
120 'validate_services_by_name instead of validate_services '
121 'due to init system differences.')
122
123 for k, v in six.iteritems(commands):
124 for cmd in v:
125 output, code = k.run(cmd)
126 self.log.debug('{} `{}` returned '
127 '{}'.format(k.info['unit_name'],
128 cmd, code))
129 if code != 0:
130 return "command `{}` returned {}".format(cmd, str(code))
131 return None
132
133 def validate_services_by_name(self, sentry_services):
134 """Validate system service status by service name, automatically
135 detecting init system based on Ubuntu release codename.
136
137 :param sentry_services: dict with sentry keys and svc list values
138 :returns: None if successful, Failure string message otherwise
139 """
140 self.log.debug('Checking status of system services...')
141
142 # Point at which systemd became a thing
143 systemd_switch = self.ubuntu_releases.index('vivid')
144
145 for sentry_unit, services_list in six.iteritems(sentry_services):
146 # Get lsb_release codename from unit
147 release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
148 if ret:
149 return ret
150
151 for service_name in services_list:
152 if (self.ubuntu_releases.index(release) >= systemd_switch or
153 service_name in ['rabbitmq-server', 'apache2']):
154 # init is systemd (or regular sysv)
155 cmd = 'sudo service {} status'.format(service_name)
156 output, code = sentry_unit.run(cmd)
157 service_running = code == 0
158 elif self.ubuntu_releases.index(release) < systemd_switch:
159 # init is upstart
160 cmd = 'sudo status {}'.format(service_name)
161 output, code = sentry_unit.run(cmd)
162 service_running = code == 0 and "start/running" in output
163
164 self.log.debug('{} `{}` returned '
165 '{}'.format(sentry_unit.info['unit_name'],
166 cmd, code))
167 if not service_running:
168 return u"command `{}` returned {} {}".format(
169 cmd, output, str(code))
170 return None
171
172 def _get_config(self, unit, filename):
173 """Get a ConfigParser object for parsing a unit's config file."""
174 file_contents = unit.file_contents(filename)
175
176 # NOTE(beisner): by default, ConfigParser does not handle options
177 # with no value, such as the flags used in the mysql my.cnf file.
178 # https://bugs.python.org/issue7005
179 config = configparser.ConfigParser(allow_no_value=True)
180 config.readfp(io.StringIO(file_contents))
181 return config
182
183 def validate_config_data(self, sentry_unit, config_file, section,
184 expected):
185 """Validate config file data.
186
187 Verify that the specified section of the config file contains
188 the expected option key:value pairs.
189
190 Compare expected dictionary data vs actual dictionary data.
191 The values in the 'expected' dictionary can be strings, bools, ints,
192 longs, or can be a function that evaluates a variable and returns a
193 bool.
194 """
195 self.log.debug('Validating config file data ({} in {} on {})'
196 '...'.format(section, config_file,
197 sentry_unit.info['unit_name']))
198 config = self._get_config(sentry_unit, config_file)
199
200 if section != 'DEFAULT' and not config.has_section(section):
201 return "section [{}] does not exist".format(section)
202
203 for k in expected.keys():
204 if not config.has_option(section, k):
205 return "section [{}] is missing option {}".format(section, k)
206
207 actual = config.get(section, k)
208 v = expected[k]
209 if (isinstance(v, six.string_types) or
210 isinstance(v, bool) or
211 isinstance(v, six.integer_types)):
212 # handle explicit values
213 if actual != v:
214 return "section [{}] {}:{} != expected {}:{}".format(
215 section, k, actual, k, expected[k])
216 # handle function pointers, such as not_null or valid_ip
217 elif not v(actual):
218 return "section [{}] {}:{} != expected {}:{}".format(
219 section, k, actual, k, expected[k])
220 return None
221
222 def _validate_dict_data(self, expected, actual):
223 """Validate dictionary data.
224
225 Compare expected dictionary data vs actual dictionary data.
226 The values in the 'expected' dictionary can be strings, bools, ints,
227 longs, or can be a function that evaluates a variable and returns a
228 bool.
229 """
230 self.log.debug('actual: {}'.format(repr(actual)))
231 self.log.debug('expected: {}'.format(repr(expected)))
232
233 for k, v in six.iteritems(expected):
234 if k in actual:
235 if (isinstance(v, six.string_types) or
236 isinstance(v, bool) or
237 isinstance(v, six.integer_types)):
238 # handle explicit values
239 if v != actual[k]:
240 return "{}:{}".format(k, actual[k])
241 # handle function pointers, such as not_null or valid_ip
242 elif not v(actual[k]):
243 return "{}:{}".format(k, actual[k])
244 else:
245 return "key '{}' does not exist".format(k)
246 return None
247
248 def validate_relation_data(self, sentry_unit, relation, expected):
249 """Validate actual relation data based on expected relation data."""
250 actual = sentry_unit.relation(relation[0], relation[1])
251 return self._validate_dict_data(expected, actual)
252
253 def _validate_list_data(self, expected, actual):
254 """Compare expected list vs actual list data."""
255 for e in expected:
256 if e not in actual:
257 return "expected item {} not found in actual list".format(e)
258 return None
259
260 def not_null(self, string):
261 if string is not None:
262 return True
263 else:
264 return False
265
266 def _get_file_mtime(self, sentry_unit, filename):
267 """Get last modification time of file."""
268 return sentry_unit.file_stat(filename)['mtime']
269
270 def _get_dir_mtime(self, sentry_unit, directory):
271 """Get last modification time of directory."""
272 return sentry_unit.directory_stat(directory)['mtime']
273
274 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
275 """Get start time of a process based on the last modification time
276 of the /proc/pid directory.
277
278 :sentry_unit: The sentry unit to check for the service on
279 :service: service name to look for in process table
280 :pgrep_full: [Deprecated] Use full command line search mode with pgrep
281 :returns: epoch time of service process start
282 :param commands: list of bash commands
283 :param sentry_units: list of sentry unit pointers
284 :returns: None if successful; Failure message otherwise
285 """
286 if pgrep_full is not None:
287 # /!\ DEPRECATION WARNING (beisner):
288 # No longer implemented, as pidof is now used instead of pgrep.
289 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
290 self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
291 'longer implemented re: lp 1474030.')
292
293 pid_list = self.get_process_id_list(sentry_unit, service)
294 pid = pid_list[0]
295 proc_dir = '/proc/{}'.format(pid)
296 self.log.debug('Pid for {} on {}: {}'.format(
297 service, sentry_unit.info['unit_name'], pid))
298
299 return self._get_dir_mtime(sentry_unit, proc_dir)
300
301 def service_restarted(self, sentry_unit, service, filename,
302 pgrep_full=None, sleep_time=20):
303 """Check if service was restarted.
304
305 Compare a service's start time vs a file's last modification time
306 (such as a config file for that service) to determine if the service
307 has been restarted.
308 """
309 # /!\ DEPRECATION WARNING (beisner):
310 # This method is prone to races in that no before-time is known.
311 # Use validate_service_config_changed instead.
312
313 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
314 # used instead of pgrep. pgrep_full is still passed through to ensure
315 # deprecation WARNS. lp1474030
316 self.log.warn('DEPRECATION WARNING: use '
317 'validate_service_config_changed instead of '
318 'service_restarted due to known races.')
319
320 time.sleep(sleep_time)
321 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
322 self._get_file_mtime(sentry_unit, filename)):
323 return True
324 else:
325 return False
326
327 def service_restarted_since(self, sentry_unit, mtime, service,
328 pgrep_full=None, sleep_time=20,
329 retry_count=2, retry_sleep_time=30):
330 """Check if service was been started after a given time.
331
332 Args:
333 sentry_unit (sentry): The sentry unit to check for the service on
334 mtime (float): The epoch time to check against
335 service (string): service name to look for in process table
336 pgrep_full: [Deprecated] Use full command line search mode with pgrep
337 sleep_time (int): Seconds to sleep before looking for process
338 retry_count (int): If service is not found, how many times to retry
339
340 Returns:
341 bool: True if service found and its start time it newer than mtime,
342 False if service is older than mtime or if service was
343 not found.
344 """
345 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
346 # used instead of pgrep. pgrep_full is still passed through to ensure
347 # deprecation WARNS. lp1474030
348
349 unit_name = sentry_unit.info['unit_name']
350 self.log.debug('Checking that %s service restarted since %s on '
351 '%s' % (service, mtime, unit_name))
352 time.sleep(sleep_time)
353 proc_start_time = None
354 tries = 0
355 while tries <= retry_count and not proc_start_time:
356 try:
357 proc_start_time = self._get_proc_start_time(sentry_unit,
358 service,
359 pgrep_full)
360 self.log.debug('Attempt {} to get {} proc start time on {} '
361 'OK'.format(tries, service, unit_name))
362 except IOError:
363 # NOTE(beisner) - race avoidance, proc may not exist yet.
364 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
365 self.log.debug('Attempt {} to get {} proc start time on {} '
366 'failed'.format(tries, service, unit_name))
367 time.sleep(retry_sleep_time)
368 tries += 1
369
370 if not proc_start_time:
371 self.log.warn('No proc start time found, assuming service did '
372 'not start')
373 return False
374 if proc_start_time >= mtime:
375 self.log.debug('Proc start time is newer than provided mtime'
376 '(%s >= %s) on %s (OK)' % (proc_start_time,
377 mtime, unit_name))
378 return True
379 else:
380 self.log.warn('Proc start time (%s) is older than provided mtime '
381 '(%s) on %s, service did not '
382 'restart' % (proc_start_time, mtime, unit_name))
383 return False
384
385 def config_updated_since(self, sentry_unit, filename, mtime,
386 sleep_time=20):
387 """Check if file was modified after a given time.
388
389 Args:
390 sentry_unit (sentry): The sentry unit to check the file mtime on
391 filename (string): The file to check mtime of
392 mtime (float): The epoch time to check against
393 sleep_time (int): Seconds to sleep before looking for process
394
395 Returns:
396 bool: True if file was modified more recently than mtime, False if
397 file was modified before mtime,
398 """
399 self.log.debug('Checking %s updated since %s' % (filename, mtime))
400 time.sleep(sleep_time)
401 file_mtime = self._get_file_mtime(sentry_unit, filename)
402 if file_mtime >= mtime:
403 self.log.debug('File mtime is newer than provided mtime '
404 '(%s >= %s)' % (file_mtime, mtime))
405 return True
406 else:
407 self.log.warn('File mtime %s is older than provided mtime %s'
408 % (file_mtime, mtime))
409 return False
410
411 def validate_service_config_changed(self, sentry_unit, mtime, service,
412 filename, pgrep_full=None,
413 sleep_time=20, retry_count=2,
414 retry_sleep_time=30):
415 """Check service and file were updated after mtime
416
417 Args:
418 sentry_unit (sentry): The sentry unit to check for the service on
419 mtime (float): The epoch time to check against
420 service (string): service name to look for in process table
421 filename (string): The file to check mtime of
422 pgrep_full: [Deprecated] Use full command line search mode with pgrep
423 sleep_time (int): Initial sleep in seconds to pass to test helpers
424 retry_count (int): If service is not found, how many times to retry
425 retry_sleep_time (int): Time in seconds to wait between retries
426
427 Typical Usage:
428 u = OpenStackAmuletUtils(ERROR)
429 ...
430 mtime = u.get_sentry_time(self.cinder_sentry)
431 self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
432 if not u.validate_service_config_changed(self.cinder_sentry,
433 mtime,
434 'cinder-api',
435 '/etc/cinder/cinder.conf')
436 amulet.raise_status(amulet.FAIL, msg='update failed')
437 Returns:
438 bool: True if both service and file where updated/restarted after
439 mtime, False if service is older than mtime or if service was
440 not found or if filename was modified before mtime.
441 """
442
443 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
444 # used instead of pgrep. pgrep_full is still passed through to ensure
445 # deprecation WARNS. lp1474030
446
447 service_restart = self.service_restarted_since(
448 sentry_unit, mtime,
449 service,
450 pgrep_full=pgrep_full,
451 sleep_time=sleep_time,
452 retry_count=retry_count,
453 retry_sleep_time=retry_sleep_time)
454
455 config_update = self.config_updated_since(
456 sentry_unit,
457 filename,
458 mtime,
459 sleep_time=0)
460
461 return service_restart and config_update
462
463 def get_sentry_time(self, sentry_unit):
464 """Return current epoch time on a sentry"""
465 cmd = "date +'%s'"
466 return float(sentry_unit.run(cmd)[0])
467
468 def relation_error(self, name, data):
469 return 'unexpected relation data in {} - {}'.format(name, data)
470
471 def endpoint_error(self, name, data):
472 return 'unexpected endpoint data in {} - {}'.format(name, data)
473
474 def get_ubuntu_releases(self):
475 """Return a list of all Ubuntu releases in order of release."""
476 _d = distro_info.UbuntuDistroInfo()
477 _release_list = _d.all
478 return _release_list
479
480 def file_to_url(self, file_rel_path):
481 """Convert a relative file path to a file URL."""
482 _abs_path = os.path.abspath(file_rel_path)
483 return urlparse.urlparse(_abs_path, scheme='file').geturl()
484
485 def check_commands_on_units(self, commands, sentry_units):
486 """Check that all commands in a list exit zero on all
487 sentry units in a list.
488
489 :param commands: list of bash commands
490 :param sentry_units: list of sentry unit pointers
491 :returns: None if successful; Failure message otherwise
492 """
493 self.log.debug('Checking exit codes for {} commands on {} '
494 'sentry units...'.format(len(commands),
495 len(sentry_units)))
496 for sentry_unit in sentry_units:
497 for cmd in commands:
498 output, code = sentry_unit.run(cmd)
499 if code == 0:
500 self.log.debug('{} `{}` returned {} '
501 '(OK)'.format(sentry_unit.info['unit_name'],
502 cmd, code))
503 else:
504 return ('{} `{}` returned {} '
505 '{}'.format(sentry_unit.info['unit_name'],
506 cmd, code, output))
507 return None
508
509 def get_process_id_list(self, sentry_unit, process_name,
510 expect_success=True):
511 """Get a list of process ID(s) from a single sentry juju unit
512 for a single process name.
513
514 :param sentry_unit: Amulet sentry instance (juju unit)
515 :param process_name: Process name
516 :param expect_success: If False, expect the PID to be missing,
517 raise if it is present.
518 :returns: List of process IDs
519 """
520 cmd = 'pidof -x {}'.format(process_name)
521 if not expect_success:
522 cmd += " || exit 0 && exit 1"
523 output, code = sentry_unit.run(cmd)
524 if code != 0:
525 msg = ('{} `{}` returned {} '
526 '{}'.format(sentry_unit.info['unit_name'],
527 cmd, code, output))
528 amulet.raise_status(amulet.FAIL, msg=msg)
529 return str(output).split()
530
531 def get_unit_process_ids(self, unit_processes, expect_success=True):
532 """Construct a dict containing unit sentries, process names, and
533 process IDs.
534
535 :param unit_processes: A dictionary of Amulet sentry instance
536 to list of process names.
537 :param expect_success: if False expect the processes to not be
538 running, raise if they are.
539 :returns: Dictionary of Amulet sentry instance to dictionary
540 of process names to PIDs.
541 """
542 pid_dict = {}
543 for sentry_unit, process_list in six.iteritems(unit_processes):
544 pid_dict[sentry_unit] = {}
545 for process in process_list:
546 pids = self.get_process_id_list(
547 sentry_unit, process, expect_success=expect_success)
548 pid_dict[sentry_unit].update({process: pids})
549 return pid_dict
550
551 def validate_unit_process_ids(self, expected, actual):
552 """Validate process id quantities for services on units."""
553 self.log.debug('Checking units for running processes...')
554 self.log.debug('Expected PIDs: {}'.format(expected))
555 self.log.debug('Actual PIDs: {}'.format(actual))
556
557 if len(actual) != len(expected):
558 return ('Unit count mismatch. expected, actual: {}, '
559 '{} '.format(len(expected), len(actual)))
560
561 for (e_sentry, e_proc_names) in six.iteritems(expected):
562 e_sentry_name = e_sentry.info['unit_name']
563 if e_sentry in actual.keys():
564 a_proc_names = actual[e_sentry]
565 else:
566 return ('Expected sentry ({}) not found in actual dict data.'
567 '{}'.format(e_sentry_name, e_sentry))
568
569 if len(e_proc_names.keys()) != len(a_proc_names.keys()):
570 return ('Process name count mismatch. expected, actual: {}, '
571 '{}'.format(len(expected), len(actual)))
572
573 for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
574 zip(e_proc_names.items(), a_proc_names.items()):
575 if e_proc_name != a_proc_name:
576 return ('Process name mismatch. expected, actual: {}, '
577 '{}'.format(e_proc_name, a_proc_name))
578
579 a_pids_length = len(a_pids)
580 fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
581 '{}, {} ({})'.format(e_sentry_name, e_proc_name,
582 e_pids_length, a_pids_length,
583 a_pids))
584
585 # If expected is not bool, ensure PID quantities match
586 if not isinstance(e_pids_length, bool) and \
587 a_pids_length != e_pids_length:
588 return fail_msg
589 # If expected is bool True, ensure 1 or more PIDs exist
590 elif isinstance(e_pids_length, bool) and \
591 e_pids_length is True and a_pids_length < 1:
592 return fail_msg
593 # If expected is bool False, ensure 0 PIDs exist
594 elif isinstance(e_pids_length, bool) and \
595 e_pids_length is False and a_pids_length != 0:
596 return fail_msg
597 else:
598 self.log.debug('PID check OK: {} {} {}: '
599 '{}'.format(e_sentry_name, e_proc_name,
600 e_pids_length, a_pids))
601 return None
602
603 def validate_list_of_identical_dicts(self, list_of_dicts):
604 """Check that all dicts within a list are identical."""
605 hashes = []
606 for _dict in list_of_dicts:
607 hashes.append(hash(frozenset(_dict.items())))
608
609 self.log.debug('Hashes: {}'.format(hashes))
610 if len(set(hashes)) == 1:
611 self.log.debug('Dicts within list are identical')
612 else:
613 return 'Dicts within list are not identical'
614
615 return None
616
617 def validate_sectionless_conf(self, file_contents, expected):
618 """A crude conf parser. Useful to inspect configuration files which
619 do not have section headers (as would be necessary in order to use
620 the configparser). Such as openstack-dashboard or rabbitmq confs."""
621 for line in file_contents.split('\n'):
622 if '=' in line:
623 args = line.split('=')
624 if len(args) <= 1:
625 continue
626 key = args[0].strip()
627 value = args[1].strip()
628 if key in expected.keys():
629 if expected[key] != value:
630 msg = ('Config mismatch. Expected, actual: {}, '
631 '{}'.format(expected[key], value))
632 amulet.raise_status(amulet.FAIL, msg=msg)
633
634 def get_unit_hostnames(self, units):
635 """Return a dict of juju unit names to hostnames."""
636 host_names = {}
637 for unit in units:
638 host_names[unit.info['unit_name']] = \
639 str(unit.file_contents('/etc/hostname').strip())
640 self.log.debug('Unit host names: {}'.format(host_names))
641 return host_names
642
643 def run_cmd_unit(self, sentry_unit, cmd):
644 """Run a command on a unit, return the output and exit code."""
645 output, code = sentry_unit.run(cmd)
646 if code == 0:
647 self.log.debug('{} `{}` command returned {} '
648 '(OK)'.format(sentry_unit.info['unit_name'],
649 cmd, code))
650 else:
651 msg = ('{} `{}` command returned {} '
652 '{}'.format(sentry_unit.info['unit_name'],
653 cmd, code, output))
654 amulet.raise_status(amulet.FAIL, msg=msg)
655 return str(output), code
656
657 def file_exists_on_unit(self, sentry_unit, file_name):
658 """Check if a file exists on a unit."""
659 try:
660 sentry_unit.file_stat(file_name)
661 return True
662 except IOError:
663 return False
664 except Exception as e:
665 msg = 'Error checking file {}: {}'.format(file_name, e)
666 amulet.raise_status(amulet.FAIL, msg=msg)
667
668 def file_contents_safe(self, sentry_unit, file_name,
669 max_wait=60, fatal=False):
670 """Get file contents from a sentry unit. Wrap amulet file_contents
671 with retry logic to address races where a file checks as existing,
672 but no longer exists by the time file_contents is called.
673 Return None if file not found. Optionally raise if fatal is True."""
674 unit_name = sentry_unit.info['unit_name']
675 file_contents = False
676 tries = 0
677 while not file_contents and tries < (max_wait / 4):
678 try:
679 file_contents = sentry_unit.file_contents(file_name)
680 except IOError:
681 self.log.debug('Attempt {} to open file {} from {} '
682 'failed'.format(tries, file_name,
683 unit_name))
684 time.sleep(4)
685 tries += 1
686
687 if file_contents:
688 return file_contents
689 elif not fatal:
690 return None
691 elif fatal:
692 msg = 'Failed to get file contents from unit.'
693 amulet.raise_status(amulet.FAIL, msg)
694
695 def port_knock_tcp(self, host="localhost", port=22, timeout=15):
696 """Open a TCP socket to check for a listening sevice on a host.
697
698 :param host: host name or IP address, default to localhost
699 :param port: TCP port number, default to 22
700 :param timeout: Connect timeout, default to 15 seconds
701 :returns: True if successful, False if connect failed
702 """
703
704 # Resolve host name if possible
705 try:
706 connect_host = socket.gethostbyname(host)
707 host_human = "{} ({})".format(connect_host, host)
708 except socket.error as e:
709 self.log.warn('Unable to resolve address: '
710 '{} ({}) Trying anyway!'.format(host, e))
711 connect_host = host
712 host_human = connect_host
713
714 # Attempt socket connection
715 try:
716 knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
717 knock.settimeout(timeout)
718 knock.connect((connect_host, port))
719 knock.close()
720 self.log.debug('Socket connect OK for host '
721 '{} on port {}.'.format(host_human, port))
722 return True
723 except socket.error as e:
724 self.log.debug('Socket connect FAIL for'
725 ' {} port {} ({})'.format(host_human, port, e))
726 return False
727
728 def port_knock_units(self, sentry_units, port=22,
729 timeout=15, expect_success=True):
730 """Open a TCP socket to check for a listening sevice on each
731 listed juju unit.
732
733 :param sentry_units: list of sentry unit pointers
734 :param port: TCP port number, default to 22
735 :param timeout: Connect timeout, default to 15 seconds
736 :expect_success: True by default, set False to invert logic
737 :returns: None if successful, Failure message otherwise
738 """
739 for unit in sentry_units:
740 host = unit.info['public-address']
741 connected = self.port_knock_tcp(host, port, timeout)
742 if not connected and expect_success:
743 return 'Socket connect failed.'
744 elif connected and not expect_success:
745 return 'Socket connected unexpectedly.'
746
747 def get_uuid_epoch_stamp(self):
748 """Returns a stamp string based on uuid4 and epoch time. Useful in
749 generating test messages which need to be unique-ish."""
750 return '[{}-{}]'.format(uuid.uuid4(), time.time())
751
752# amulet juju action helpers:
753 def run_action(self, unit_sentry, action,
754 _check_output=subprocess.check_output):
755 """Run the named action on a given unit sentry.
756
757 _check_output parameter is used for dependency injection.
758
759 @return action_id.
760 """
761 unit_id = unit_sentry.info["unit_name"]
762 command = ["juju", "action", "do", "--format=json", unit_id, action]
763 self.log.info("Running command: %s\n" % " ".join(command))
764 output = _check_output(command, universal_newlines=True)
765 data = json.loads(output)
766 action_id = data[u'Action queued with id']
767 return action_id
768
769 def wait_on_action(self, action_id, _check_output=subprocess.check_output):
770 """Wait for a given action, returning if it completed or not.
771
772 _check_output parameter is used for dependency injection.
773 """
774 command = ["juju", "action", "fetch", "--format=json", "--wait=0",
775 action_id]
776 output = _check_output(command, universal_newlines=True)
777 data = json.loads(output)
778 return data.get(u"status") == "completed"
0779
=== added directory 'tests/charmhelpers/contrib/openstack'
=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
--- tests/charmhelpers/contrib/openstack/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/__init__.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
--- tests/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/__init__.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,198 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import six
18from collections import OrderedDict
19from charmhelpers.contrib.amulet.deployment import (
20 AmuletDeployment
21)
22
23
24class OpenStackAmuletDeployment(AmuletDeployment):
25 """OpenStack amulet deployment.
26
27 This class inherits from AmuletDeployment and has additional support
28 that is specifically for use by OpenStack charms.
29 """
30
31 def __init__(self, series=None, openstack=None, source=None, stable=True):
32 """Initialize the deployment environment."""
33 super(OpenStackAmuletDeployment, self).__init__(series)
34 self.openstack = openstack
35 self.source = source
36 self.stable = stable
37 # Note(coreycb): this needs to be changed when new next branches come
38 # out.
39 self.current_next = "trusty"
40
41 def _determine_branch_locations(self, other_services):
42 """Determine the branch locations for the other services.
43
44 Determine if the local branch being tested is derived from its
45 stable or next (dev) branch, and based on this, use the corresonding
46 stable or next branches for the other_services."""
47
48 # Charms outside the lp:~openstack-charmers namespace
49 base_charms = ['mysql', 'mongodb', 'nrpe']
50
51 # Force these charms to current series even when using an older series.
52 # ie. Use trusty/nrpe even when series is precise, as the P charm
53 # does not possess the necessary external master config and hooks.
54 force_series_current = ['nrpe']
55
56 if self.series in ['precise', 'trusty']:
57 base_series = self.series
58 else:
59 base_series = self.current_next
60
61 if self.stable:
62 for svc in other_services:
63 if svc['name'] in force_series_current:
64 base_series = self.current_next
65
66 temp = 'lp:charms/{}/{}'
67 svc['location'] = temp.format(base_series,
68 svc['name'])
69 else:
70 for svc in other_services:
71 if svc['name'] in force_series_current:
72 base_series = self.current_next
73
74 if svc['name'] in base_charms:
75 temp = 'lp:charms/{}/{}'
76 svc['location'] = temp.format(base_series,
77 svc['name'])
78 else:
79 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
80 svc['location'] = temp.format(self.current_next,
81 svc['name'])
82 return other_services
83
84 def _add_services(self, this_service, other_services):
85 """Add services to the deployment and set openstack-origin/source."""
86 other_services = self._determine_branch_locations(other_services)
87
88 super(OpenStackAmuletDeployment, self)._add_services(this_service,
89 other_services)
90
91 services = other_services
92 services.append(this_service)
93
94 # Charms which should use the source config option
95 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
96 'ceph-osd', 'ceph-radosgw']
97
98 # Charms which can not use openstack-origin, ie. many subordinates
99 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
100
101 if self.openstack:
102 for svc in services:
103 if svc['name'] not in use_source + no_origin:
104 config = {'openstack-origin': self.openstack}
105 self.d.configure(svc['name'], config)
106
107 if self.source:
108 for svc in services:
109 if svc['name'] in use_source and svc['name'] not in no_origin:
110 config = {'source': self.source}
111 self.d.configure(svc['name'], config)
112
113 def _configure_services(self, configs):
114 """Configure all of the services."""
115 for service, config in six.iteritems(configs):
116 self.d.configure(service, config)
117
118 def _get_openstack_release(self):
119 """Get openstack release.
120
121 Return an integer representing the enum value of the openstack
122 release.
123 """
124 # Must be ordered by OpenStack release (not by Ubuntu release):
125 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
126 self.precise_havana, self.precise_icehouse,
127 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
128 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
129 self.wily_liberty) = range(12)
130
131 releases = {
132 ('precise', None): self.precise_essex,
133 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
134 ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
135 ('precise', 'cloud:precise-havana'): self.precise_havana,
136 ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
137 ('trusty', None): self.trusty_icehouse,
138 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
139 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
140 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
141 ('utopic', None): self.utopic_juno,
142 ('vivid', None): self.vivid_kilo,
143 ('wily', None): self.wily_liberty}
144 return releases[(self.series, self.openstack)]
145
146 def _get_openstack_release_string(self):
147 """Get openstack release string.
148
149 Return a string representing the openstack release.
150 """
151 releases = OrderedDict([
152 ('precise', 'essex'),
153 ('quantal', 'folsom'),
154 ('raring', 'grizzly'),
155 ('saucy', 'havana'),
156 ('trusty', 'icehouse'),
157 ('utopic', 'juno'),
158 ('vivid', 'kilo'),
159 ('wily', 'liberty'),
160 ])
161 if self.openstack:
162 os_origin = self.openstack.split(':')[1]
163 return os_origin.split('%s-' % self.series)[1].split('/')[0]
164 else:
165 return releases[self.series]
166
167 def get_ceph_expected_pools(self, radosgw=False):
168 """Return a list of expected ceph pools in a ceph + cinder + glance
169 test scenario, based on OpenStack release and whether ceph radosgw
170 is flagged as present or not."""
171
172 if self._get_openstack_release() >= self.trusty_kilo:
173 # Kilo or later
174 pools = [
175 'rbd',
176 'cinder',
177 'glance'
178 ]
179 else:
180 # Juno or earlier
181 pools = [
182 'data',
183 'metadata',
184 'rbd',
185 'cinder',
186 'glance'
187 ]
188
189 if radosgw:
190 pools.extend([
191 '.rgw.root',
192 '.rgw.control',
193 '.rgw',
194 '.rgw.gc',
195 '.users.uid'
196 ])
197
198 return pools
0199
=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-08 14:01:41 +0000
@@ -0,0 +1,963 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import amulet
18import json
19import logging
20import os
21import six
22import time
23import urllib
24
25import cinderclient.v1.client as cinder_client
26import glanceclient.v1.client as glance_client
27import heatclient.v1.client as heat_client
28import keystoneclient.v2_0 as keystone_client
29import novaclient.v1_1.client as nova_client
30import pika
31import swiftclient
32
33from charmhelpers.contrib.amulet.utils import (
34 AmuletUtils
35)
36
37DEBUG = logging.DEBUG
38ERROR = logging.ERROR
39
40
41class OpenStackAmuletUtils(AmuletUtils):
42 """OpenStack amulet utilities.
43
44 This class inherits from AmuletUtils and has additional support
45 that is specifically for use by OpenStack charm tests.
46 """
47
48 def __init__(self, log_level=ERROR):
49 """Initialize the deployment environment."""
50 super(OpenStackAmuletUtils, self).__init__(log_level)
51
52 def validate_endpoint_data(self, endpoints, admin_port, internal_port,
53 public_port, expected):
54 """Validate endpoint data.
55
56 Validate actual endpoint data vs expected endpoint data. The ports
57 are used to find the matching endpoint.
58 """
59 self.log.debug('Validating endpoint data...')
60 self.log.debug('actual: {}'.format(repr(endpoints)))
61 found = False
62 for ep in endpoints:
63 self.log.debug('endpoint: {}'.format(repr(ep)))
64 if (admin_port in ep.adminurl and
65 internal_port in ep.internalurl and
66 public_port in ep.publicurl):
67 found = True
68 actual = {'id': ep.id,
69 'region': ep.region,
70 'adminurl': ep.adminurl,
71 'internalurl': ep.internalurl,
72 'publicurl': ep.publicurl,
73 'service_id': ep.service_id}
74 ret = self._validate_dict_data(expected, actual)
75 if ret:
76 return 'unexpected endpoint data - {}'.format(ret)
77
78 if not found:
79 return 'endpoint not found'
80
81 def validate_svc_catalog_endpoint_data(self, expected, actual):
82 """Validate service catalog endpoint data.
83
84 Validate a list of actual service catalog endpoints vs a list of
85 expected service catalog endpoints.
86 """
87 self.log.debug('Validating service catalog endpoint data...')
88 self.log.debug('actual: {}'.format(repr(actual)))
89 for k, v in six.iteritems(expected):
90 if k in actual:
91 ret = self._validate_dict_data(expected[k][0], actual[k][0])
92 if ret:
93 return self.endpoint_error(k, ret)
94 else:
95 return "endpoint {} does not exist".format(k)
96 return ret
97
98 def validate_tenant_data(self, expected, actual):
99 """Validate tenant data.
100
101 Validate a list of actual tenant data vs list of expected tenant
102 data.
103 """
104 self.log.debug('Validating tenant data...')
105 self.log.debug('actual: {}'.format(repr(actual)))
106 for e in expected:
107 found = False
108 for act in actual:
109 a = {'enabled': act.enabled, 'description': act.description,
110 'name': act.name, 'id': act.id}
111 if e['name'] == a['name']:
112 found = True
113 ret = self._validate_dict_data(e, a)
114 if ret:
115 return "unexpected tenant data - {}".format(ret)
116 if not found:
117 return "tenant {} does not exist".format(e['name'])
118 return ret
119
120 def validate_role_data(self, expected, actual):
121 """Validate role data.
122
123 Validate a list of actual role data vs a list of expected role
124 data.
125 """
126 self.log.debug('Validating role data...')
127 self.log.debug('actual: {}'.format(repr(actual)))
128 for e in expected:
129 found = False
130 for act in actual:
131 a = {'name': act.name, 'id': act.id}
132 if e['name'] == a['name']:
133 found = True
134 ret = self._validate_dict_data(e, a)
135 if ret:
136 return "unexpected role data - {}".format(ret)
137 if not found:
138 return "role {} does not exist".format(e['name'])
139 return ret
140
141 def validate_user_data(self, expected, actual):
142 """Validate user data.
143
144 Validate a list of actual user data vs a list of expected user
145 data.
146 """
147 self.log.debug('Validating user data...')
148 self.log.debug('actual: {}'.format(repr(actual)))
149 for e in expected:
150 found = False
151 for act in actual:
152 a = {'enabled': act.enabled, 'name': act.name,
153 'email': act.email, 'tenantId': act.tenantId,
154 'id': act.id}
155 if e['name'] == a['name']:
156 found = True
157 ret = self._validate_dict_data(e, a)
158 if ret:
159 return "unexpected user data - {}".format(ret)
160 if not found:
161 return "user {} does not exist".format(e['name'])
162 return ret
163
164 def validate_flavor_data(self, expected, actual):
165 """Validate flavor data.
166
167 Validate a list of actual flavors vs a list of expected flavors.
168 """
169 self.log.debug('Validating flavor data...')
170 self.log.debug('actual: {}'.format(repr(actual)))
171 act = [a.name for a in actual]
172 return self._validate_list_data(expected, act)
173
174 def tenant_exists(self, keystone, tenant):
175 """Return True if tenant exists."""
176 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
177 return tenant in [t.name for t in keystone.tenants.list()]
178
179 def authenticate_cinder_admin(self, keystone_sentry, username,
180 password, tenant):
181 """Authenticates admin user with cinder."""
182 # NOTE(beisner): cinder python client doesn't accept tokens.
183 service_ip = \
184 keystone_sentry.relation('shared-db',
185 'mysql:shared-db')['private-address']
186 ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
187 return cinder_client.Client(username, password, tenant, ept)
188
189 def authenticate_keystone_admin(self, keystone_sentry, user, password,
190 tenant):
191 """Authenticates admin user with the keystone admin endpoint."""
192 self.log.debug('Authenticating keystone admin...')
193 unit = keystone_sentry
194 service_ip = unit.relation('shared-db',
195 'mysql:shared-db')['private-address']
196 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
197 return keystone_client.Client(username=user, password=password,
198 tenant_name=tenant, auth_url=ep)
199
200 def authenticate_keystone_user(self, keystone, user, password, tenant):
201 """Authenticates a regular user with the keystone public endpoint."""
202 self.log.debug('Authenticating keystone user ({})...'.format(user))
203 ep = keystone.service_catalog.url_for(service_type='identity',
204 endpoint_type='publicURL')
205 return keystone_client.Client(username=user, password=password,
206 tenant_name=tenant, auth_url=ep)
207
208 def authenticate_glance_admin(self, keystone):
209 """Authenticates admin user with glance."""
210 self.log.debug('Authenticating glance admin...')
211 ep = keystone.service_catalog.url_for(service_type='image',
212 endpoint_type='adminURL')
213 return glance_client.Client(ep, token=keystone.auth_token)
214
215 def authenticate_heat_admin(self, keystone):
216 """Authenticates the admin user with heat."""
217 self.log.debug('Authenticating heat admin...')
218 ep = keystone.service_catalog.url_for(service_type='orchestration',
219 endpoint_type='publicURL')
220 return heat_client.Client(endpoint=ep, token=keystone.auth_token)
221
222 def authenticate_nova_user(self, keystone, user, password, tenant):
223 """Authenticates a regular user with nova-api."""
224 self.log.debug('Authenticating nova user ({})...'.format(user))
225 ep = keystone.service_catalog.url_for(service_type='identity',
226 endpoint_type='publicURL')
227 return nova_client.Client(username=user, api_key=password,
228 project_id=tenant, auth_url=ep)
229
230 def authenticate_swift_user(self, keystone, user, password, tenant):
231 """Authenticates a regular user with swift api."""
232 self.log.debug('Authenticating swift user ({})...'.format(user))
233 ep = keystone.service_catalog.url_for(service_type='identity',
234 endpoint_type='publicURL')
235 return swiftclient.Connection(authurl=ep,
236 user=user,
237 key=password,
238 tenant_name=tenant,
239 auth_version='2.0')
240
241 def create_cirros_image(self, glance, image_name):
242 """Download the latest cirros image and upload it to glance,
243 validate and return a resource pointer.
244
245 :param glance: pointer to authenticated glance connection
246 :param image_name: display name for new image
247 :returns: glance image pointer
248 """
249 self.log.debug('Creating glance cirros image '
250 '({})...'.format(image_name))
251
252 # Download cirros image
253 http_proxy = os.getenv('AMULET_HTTP_PROXY')
254 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
255 if http_proxy:
256 proxies = {'http': http_proxy}
257 opener = urllib.FancyURLopener(proxies)
258 else:
259 opener = urllib.FancyURLopener()
260
261 f = opener.open('http://download.cirros-cloud.net/version/released')
262 version = f.read().strip()
263 cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
264 local_path = os.path.join('tests', cirros_img)
265
266 if not os.path.exists(local_path):
267 cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
268 version, cirros_img)
269 opener.retrieve(cirros_url, local_path)
270 f.close()
271
272 # Create glance image
273 with open(local_path) as f:
274 image = glance.images.create(name=image_name, is_public=True,
275 disk_format='qcow2',
276 container_format='bare', data=f)
277
278 # Wait for image to reach active status
279 img_id = image.id
280 ret = self.resource_reaches_status(glance.images, img_id,
281 expected_stat='active',
282 msg='Image status wait')
283 if not ret:
284 msg = 'Glance image failed to reach expected state.'
285 amulet.raise_status(amulet.FAIL, msg=msg)
286
287 # Re-validate new image
288 self.log.debug('Validating image attributes...')
289 val_img_name = glance.images.get(img_id).name
290 val_img_stat = glance.images.get(img_id).status
291 val_img_pub = glance.images.get(img_id).is_public
292 val_img_cfmt = glance.images.get(img_id).container_format
293 val_img_dfmt = glance.images.get(img_id).disk_format
294 msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
295 'container fmt:{} disk fmt:{}'.format(
296 val_img_name, val_img_pub, img_id,
297 val_img_stat, val_img_cfmt, val_img_dfmt))
298
299 if val_img_name == image_name and val_img_stat == 'active' \
300 and val_img_pub is True and val_img_cfmt == 'bare' \
301 and val_img_dfmt == 'qcow2':
302 self.log.debug(msg_attr)
303 else:
304 msg = ('Volume validation failed, {}'.format(msg_attr))
305 amulet.raise_status(amulet.FAIL, msg=msg)
306
307 return image
308
309 def delete_image(self, glance, image):
310 """Delete the specified image."""
311
312 # /!\ DEPRECATION WARNING
313 self.log.warn('/!\\ DEPRECATION WARNING: use '
314 'delete_resource instead of delete_image.')
315 self.log.debug('Deleting glance image ({})...'.format(image))
316 return self.delete_resource(glance.images, image, msg='glance image')
317
318 def create_instance(self, nova, image_name, instance_name, flavor):
319 """Create the specified instance."""
320 self.log.debug('Creating instance '
321 '({}|{}|{})'.format(instance_name, image_name, flavor))
322 image = nova.images.find(name=image_name)
323 flavor = nova.flavors.find(name=flavor)
324 instance = nova.servers.create(name=instance_name, image=image,
325 flavor=flavor)
326
327 count = 1
328 status = instance.status
329 while status != 'ACTIVE' and count < 60:
330 time.sleep(3)
331 instance = nova.servers.get(instance.id)
332 status = instance.status
333 self.log.debug('instance status: {}'.format(status))
334 count += 1
335
336 if status != 'ACTIVE':
337 self.log.error('instance creation timed out')
338 return None
339
340 return instance
341
342 def delete_instance(self, nova, instance):
343 """Delete the specified instance."""
344
345 # /!\ DEPRECATION WARNING
346 self.log.warn('/!\\ DEPRECATION WARNING: use '
347 'delete_resource instead of delete_instance.')
348 self.log.debug('Deleting instance ({})...'.format(instance))
349 return self.delete_resource(nova.servers, instance,
350 msg='nova instance')
351
352 def create_or_get_keypair(self, nova, keypair_name="testkey"):
353 """Create a new keypair, or return pointer if it already exists."""
354 try:
355 _keypair = nova.keypairs.get(keypair_name)
356 self.log.debug('Keypair ({}) already exists, '
357 'using it.'.format(keypair_name))
358 return _keypair
359 except:
360 self.log.debug('Keypair ({}) does not exist, '
361 'creating it.'.format(keypair_name))
362
363 _keypair = nova.keypairs.create(name=keypair_name)
364 return _keypair
365
366 def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
367 img_id=None, src_vol_id=None, snap_id=None):
368 """Create cinder volume, optionally from a glance image, OR
369 optionally as a clone of an existing volume, OR optionally
370 from a snapshot. Wait for the new volume status to reach
371 the expected status, validate and return a resource pointer.
372
373 :param vol_name: cinder volume display name
374 :param vol_size: size in gigabytes
375 :param img_id: optional glance image id
376 :param src_vol_id: optional source volume id to clone
377 :param snap_id: optional snapshot id to use
378 :returns: cinder volume pointer
379 """
380 # Handle parameter input and avoid impossible combinations
381 if img_id and not src_vol_id and not snap_id:
382 # Create volume from image
383 self.log.debug('Creating cinder volume from glance image...')
384 bootable = 'true'
385 elif src_vol_id and not img_id and not snap_id:
386 # Clone an existing volume
387 self.log.debug('Cloning cinder volume...')
388 bootable = cinder.volumes.get(src_vol_id).bootable
389 elif snap_id and not src_vol_id and not img_id:
390 # Create volume from snapshot
391 self.log.debug('Creating cinder volume from snapshot...')
392 snap = cinder.volume_snapshots.find(id=snap_id)
393 vol_size = snap.size
394 snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
395 bootable = cinder.volumes.get(snap_vol_id).bootable
396 elif not img_id and not src_vol_id and not snap_id:
397 # Create volume
398 self.log.debug('Creating cinder volume...')
399 bootable = 'false'
400 else:
401 # Impossible combination of parameters
402 msg = ('Invalid method use - name:{} size:{} img_id:{} '
403 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
404 img_id, src_vol_id,
405 snap_id))
406 amulet.raise_status(amulet.FAIL, msg=msg)
407
408 # Create new volume
409 try:
410 vol_new = cinder.volumes.create(display_name=vol_name,
411 imageRef=img_id,
412 size=vol_size,
413 source_volid=src_vol_id,
414 snapshot_id=snap_id)
415 vol_id = vol_new.id
416 except Exception as e:
417 msg = 'Failed to create volume: {}'.format(e)
418 amulet.raise_status(amulet.FAIL, msg=msg)
419
420 # Wait for volume to reach available status
421 ret = self.resource_reaches_status(cinder.volumes, vol_id,
422 expected_stat="available",
423 msg="Volume status wait")
424 if not ret:
425 msg = 'Cinder volume failed to reach expected state.'
426 amulet.raise_status(amulet.FAIL, msg=msg)
427
428 # Re-validate new volume
429 self.log.debug('Validating volume attributes...')
430 val_vol_name = cinder.volumes.get(vol_id).display_name
431 val_vol_boot = cinder.volumes.get(vol_id).bootable
432 val_vol_stat = cinder.volumes.get(vol_id).status
433 val_vol_size = cinder.volumes.get(vol_id).size
434 msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
435 '{} size:{}'.format(val_vol_name, vol_id,
436 val_vol_stat, val_vol_boot,
437 val_vol_size))
438
439 if val_vol_boot == bootable and val_vol_stat == 'available' \
440 and val_vol_name == vol_name and val_vol_size == vol_size:
441 self.log.debug(msg_attr)
442 else:
443 msg = ('Volume validation failed, {}'.format(msg_attr))
444 amulet.raise_status(amulet.FAIL, msg=msg)
445
446 return vol_new
447
448 def delete_resource(self, resource, resource_id,
449 msg="resource", max_wait=120):
450 """Delete one openstack resource, such as one instance, keypair,
451 image, volume, stack, etc., and confirm deletion within max wait time.
452
453 :param resource: pointer to os resource type, ex:glance_client.images
454 :param resource_id: unique name or id for the openstack resource
455 :param msg: text to identify purpose in logging
456 :param max_wait: maximum wait time in seconds
457 :returns: True if successful, otherwise False
458 """
459 self.log.debug('Deleting OpenStack resource '
460 '{} ({})'.format(resource_id, msg))
461 num_before = len(list(resource.list()))
462 resource.delete(resource_id)
463
464 tries = 0
465 num_after = len(list(resource.list()))
466 while num_after != (num_before - 1) and tries < (max_wait / 4):
467 self.log.debug('{} delete check: '
468 '{} [{}:{}] {}'.format(msg, tries,
469 num_before,
470 num_after,
471 resource_id))
472 time.sleep(4)
473 num_after = len(list(resource.list()))
474 tries += 1
475
476 self.log.debug('{}: expected, actual count = {}, '
477 '{}'.format(msg, num_before - 1, num_after))
478
479 if num_after == (num_before - 1):
480 return True
481 else:
482 self.log.error('{} delete timed out'.format(msg))
483 return False
484
485 def resource_reaches_status(self, resource, resource_id,
486 expected_stat='available',
487 msg='resource', max_wait=120):
488 """Wait for an openstack resources status to reach an
489 expected status within a specified time. Useful to confirm that
490 nova instances, cinder vols, snapshots, glance images, heat stacks
491 and other resources eventually reach the expected status.
492
493 :param resource: pointer to os resource type, ex: heat_client.stacks
494 :param resource_id: unique id for the openstack resource
495 :param expected_stat: status to expect resource to reach
496 :param msg: text to identify purpose in logging
497 :param max_wait: maximum wait time in seconds
498 :returns: True if successful, False if status is not reached
499 """
500
501 tries = 0
502 resource_stat = resource.get(resource_id).status
503 while resource_stat != expected_stat and tries < (max_wait / 4):
504 self.log.debug('{} status check: '
505 '{} [{}:{}] {}'.format(msg, tries,
506 resource_stat,
507 expected_stat,
508 resource_id))
509 time.sleep(4)
510 resource_stat = resource.get(resource_id).status
511 tries += 1
512
513 self.log.debug('{}: expected, actual status = {}, '
514 '{}'.format(msg, resource_stat, expected_stat))
515
516 if resource_stat == expected_stat:
517 return True
518 else:
519 self.log.debug('{} never reached expected status: '
520 '{}'.format(resource_id, expected_stat))
521 return False
522
523 def get_ceph_osd_id_cmd(self, index):
524 """Produce a shell command that will return a ceph-osd id."""
525 return ("`initctl list | grep 'ceph-osd ' | "
526 "awk 'NR=={} {{ print $2 }}' | "
527 "grep -o '[0-9]*'`".format(index + 1))
528
529 def get_ceph_pools(self, sentry_unit):
530 """Return a dict of ceph pools from a single ceph unit, with
531 pool name as keys, pool id as vals."""
532 pools = {}
533 cmd = 'sudo ceph osd lspools'
534 output, code = sentry_unit.run(cmd)
535 if code != 0:
536 msg = ('{} `{}` returned {} '
537 '{}'.format(sentry_unit.info['unit_name'],
538 cmd, code, output))
539 amulet.raise_status(amulet.FAIL, msg=msg)
540
541 # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
542 for pool in str(output).split(','):
543 pool_id_name = pool.split(' ')
544 if len(pool_id_name) == 2:
545 pool_id = pool_id_name[0]
546 pool_name = pool_id_name[1]
547 pools[pool_name] = int(pool_id)
548
549 self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
550 pools))
551 return pools
552
553 def get_ceph_df(self, sentry_unit):
554 """Return dict of ceph df json output, including ceph pool state.
555
556 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
557 :returns: Dict of ceph df output
558 """
559 cmd = 'sudo ceph df --format=json'
560 output, code = sentry_unit.run(cmd)
561 if code != 0:
562 msg = ('{} `{}` returned {} '
563 '{}'.format(sentry_unit.info['unit_name'],
564 cmd, code, output))
565 amulet.raise_status(amulet.FAIL, msg=msg)
566 return json.loads(output)
567
568 def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
569 """Take a sample of attributes of a ceph pool, returning ceph
570 pool name, object count and disk space used for the specified
571 pool ID number.
572
573 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
574 :param pool_id: Ceph pool ID
575 :returns: List of pool name, object count, kb disk space used
576 """
577 df = self.get_ceph_df(sentry_unit)
578 pool_name = df['pools'][pool_id]['name']
579 obj_count = df['pools'][pool_id]['stats']['objects']
580 kb_used = df['pools'][pool_id]['stats']['kb_used']
581 self.log.debug('Ceph {} pool (ID {}): {} objects, '
582 '{} kb used'.format(pool_name, pool_id,
583 obj_count, kb_used))
584 return pool_name, obj_count, kb_used
585
586 def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
587 """Validate ceph pool samples taken over time, such as pool
588 object counts or pool kb used, before adding, after adding, and
589 after deleting items which affect those pool attributes. The
590 2nd element is expected to be greater than the 1st; 3rd is expected
591 to be less than the 2nd.
592
593 :param samples: List containing 3 data samples
594 :param sample_type: String for logging and usage context
595 :returns: None if successful, Failure message otherwise
596 """
597 original, created, deleted = range(3)
598 if samples[created] <= samples[original] or \
599 samples[deleted] >= samples[created]:
600 return ('Ceph {} samples ({}) '
601 'unexpected.'.format(sample_type, samples))
602 else:
603 self.log.debug('Ceph {} samples (OK): '
604 '{}'.format(sample_type, samples))
605 return None
606
607# rabbitmq/amqp specific helpers:
608 def add_rmq_test_user(self, sentry_units,
609 username="testuser1", password="changeme"):
610 """Add a test user via the first rmq juju unit, check connection as
611 the new user against all sentry units.
612
613 :param sentry_units: list of sentry unit pointers
614 :param username: amqp user name, default to testuser1
615 :param password: amqp user password
616 :returns: None if successful. Raise on error.
617 """
618 self.log.debug('Adding rmq user ({})...'.format(username))
619
620 # Check that user does not already exist
621 cmd_user_list = 'rabbitmqctl list_users'
622 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
623 if username in output:
624 self.log.warning('User ({}) already exists, returning '
625 'gracefully.'.format(username))
626 return
627
628 perms = '".*" ".*" ".*"'
629 cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
630 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
631
632 # Add user via first unit
633 for cmd in cmds:
634 output, _ = self.run_cmd_unit(sentry_units[0], cmd)
635
636 # Check connection against the other sentry_units
637 self.log.debug('Checking user connect against units...')
638 for sentry_unit in sentry_units:
639 connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
640 username=username,
641 password=password)
642 connection.close()
643
644 def delete_rmq_test_user(self, sentry_units, username="testuser1"):
645 """Delete a rabbitmq user via the first rmq juju unit.
646
647 :param sentry_units: list of sentry unit pointers
648 :param username: amqp user name, default to testuser1
649 :param password: amqp user password
650 :returns: None if successful or no such user.
651 """
652 self.log.debug('Deleting rmq user ({})...'.format(username))
653
654 # Check that the user exists
655 cmd_user_list = 'rabbitmqctl list_users'
656 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
657
658 if username not in output:
659 self.log.warning('User ({}) does not exist, returning '
660 'gracefully.'.format(username))
661 return
662
663 # Delete the user
664 cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
665 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
666
667 def get_rmq_cluster_status(self, sentry_unit):
668 """Execute rabbitmq cluster status command on a unit and return
669 the full output.
670
671 :param unit: sentry unit
672 :returns: String containing console output of cluster status command
673 """
674 cmd = 'rabbitmqctl cluster_status'
675 output, _ = self.run_cmd_unit(sentry_unit, cmd)
676 self.log.debug('{} cluster_status:\n{}'.format(
677 sentry_unit.info['unit_name'], output))
678 return str(output)
679
680 def get_rmq_cluster_running_nodes(self, sentry_unit):
681 """Parse rabbitmqctl cluster_status output string, return list of
682 running rabbitmq cluster nodes.
683
684 :param unit: sentry unit
685 :returns: List containing node names of running nodes
686 """
687 # NOTE(beisner): rabbitmqctl cluster_status output is not
688 # json-parsable, do string chop foo, then json.loads that.
689 str_stat = self.get_rmq_cluster_status(sentry_unit)
690 if 'running_nodes' in str_stat:
691 pos_start = str_stat.find("{running_nodes,") + 15
692 pos_end = str_stat.find("]},", pos_start) + 1
693 str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
694 run_nodes = json.loads(str_run_nodes)
695 return run_nodes
696 else:
697 return []
698
699 def validate_rmq_cluster_running_nodes(self, sentry_units):
700 """Check that all rmq unit hostnames are represented in the
701 cluster_status output of all units.
702
703 :param host_names: dict of juju unit names to host names
704 :param units: list of sentry unit pointers (all rmq units)
705 :returns: None if successful, otherwise return error message
706 """
707 host_names = self.get_unit_hostnames(sentry_units)
708 errors = []
709
710 # Query every unit for cluster_status running nodes
711 for query_unit in sentry_units:
712 query_unit_name = query_unit.info['unit_name']
713 running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
714
715 # Confirm that every unit is represented in the queried unit's
716 # cluster_status running nodes output.
717 for validate_unit in sentry_units:
718 val_host_name = host_names[validate_unit.info['unit_name']]
719 val_node_name = 'rabbit@{}'.format(val_host_name)
720
721 if val_node_name not in running_nodes:
722 errors.append('Cluster member check failed on {}: {} not '
723 'in {}\n'.format(query_unit_name,
724 val_node_name,
725 running_nodes))
726 if errors:
727 return ''.join(errors)
728
729 def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
730 """Check a single juju rmq unit for ssl and port in the config file."""
731 host = sentry_unit.info['public-address']
732 unit_name = sentry_unit.info['unit_name']
733
734 conf_file = '/etc/rabbitmq/rabbitmq.config'
735 conf_contents = str(self.file_contents_safe(sentry_unit,
736 conf_file, max_wait=16))
737 # Checks
738 conf_ssl = 'ssl' in conf_contents
739 conf_port = str(port) in conf_contents
740
741 # Port explicitly checked in config
742 if port and conf_port and conf_ssl:
743 self.log.debug('SSL is enabled @{}:{} '
744 '({})'.format(host, port, unit_name))
745 return True
746 elif port and not conf_port and conf_ssl:
747 self.log.debug('SSL is enabled @{} but not on port {} '
748 '({})'.format(host, port, unit_name))
749 return False
750 # Port not checked (useful when checking that ssl is disabled)
751 elif not port and conf_ssl:
752 self.log.debug('SSL is enabled @{}:{} '
753 '({})'.format(host, port, unit_name))
754 return True
755 elif not port and not conf_ssl:
756 self.log.debug('SSL not enabled @{}:{} '
757 '({})'.format(host, port, unit_name))
758 return False
759 else:
760 msg = ('Unknown condition when checking SSL status @{}:{} '
761 '({})'.format(host, port, unit_name))
762 amulet.raise_status(amulet.FAIL, msg)
763
764 def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
765 """Check that ssl is enabled on rmq juju sentry units.
766
767 :param sentry_units: list of all rmq sentry units
768 :param port: optional ssl port override to validate
769 :returns: None if successful, otherwise return error message
770 """
771 for sentry_unit in sentry_units:
772 if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
773 return ('Unexpected condition: ssl is disabled on unit '
774 '({})'.format(sentry_unit.info['unit_name']))
775 return None
776
777 def validate_rmq_ssl_disabled_units(self, sentry_units):
778 """Check that ssl is enabled on listed rmq juju sentry units.
779
780 :param sentry_units: list of all rmq sentry units
781 :returns: True if successful. Raise on error.
782 """
783 for sentry_unit in sentry_units:
784 if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
785 return ('Unexpected condition: ssl is enabled on unit '
786 '({})'.format(sentry_unit.info['unit_name']))
787 return None
788
789 def configure_rmq_ssl_on(self, sentry_units, deployment,
790 port=None, max_wait=60):
791 """Turn ssl charm config option on, with optional non-default
792 ssl port specification. Confirm that it is enabled on every
793 unit.
794
795 :param sentry_units: list of sentry units
796 :param deployment: amulet deployment object pointer
797 :param port: amqp port, use defaults if None
798 :param max_wait: maximum time to wait in seconds to confirm
799 :returns: None if successful. Raise on error.
800 """
801 self.log.debug('Setting ssl charm config option: on')
802
803 # Enable RMQ SSL
804 config = {'ssl': 'on'}
805 if port:
806 config['ssl_port'] = port
807
808 deployment.configure('rabbitmq-server', config)
809
810 # Confirm
811 tries = 0
812 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
813 while ret and tries < (max_wait / 4):
814 time.sleep(4)
815 self.log.debug('Attempt {}: {}'.format(tries, ret))
816 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
817 tries += 1
818
819 if ret:
820 amulet.raise_status(amulet.FAIL, ret)
821
822 def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
823 """Turn ssl charm config option off, confirm that it is disabled
824 on every unit.
825
826 :param sentry_units: list of sentry units
827 :param deployment: amulet deployment object pointer
828 :param max_wait: maximum time to wait in seconds to confirm
829 :returns: None if successful. Raise on error.
830 """
831 self.log.debug('Setting ssl charm config option: off')
832
833 # Disable RMQ SSL
834 config = {'ssl': 'off'}
835 deployment.configure('rabbitmq-server', config)
836
837 # Confirm
838 tries = 0
839 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
840 while ret and tries < (max_wait / 4):
841 time.sleep(4)
842 self.log.debug('Attempt {}: {}'.format(tries, ret))
843 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
844 tries += 1
845
846 if ret:
847 amulet.raise_status(amulet.FAIL, ret)
848
849 def connect_amqp_by_unit(self, sentry_unit, ssl=False,
850 port=None, fatal=True,
851 username="testuser1", password="changeme"):
852 """Establish and return a pika amqp connection to the rabbitmq service
853 running on a rmq juju unit.
854
855 :param sentry_unit: sentry unit pointer
856 :param ssl: boolean, default to False
857 :param port: amqp port, use defaults if None
858 :param fatal: boolean, default to True (raises on connect error)
859 :param username: amqp user name, default to testuser1
860 :param password: amqp user password
861 :returns: pika amqp connection pointer or None if failed and non-fatal
862 """
863 host = sentry_unit.info['public-address']
864 unit_name = sentry_unit.info['unit_name']
865
866 # Default port logic if port is not specified
867 if ssl and not port:
868 port = 5671
869 elif not ssl and not port:
870 port = 5672
871
872 self.log.debug('Connecting to amqp on {}:{} ({}) as '
873 '{}...'.format(host, port, unit_name, username))
874
875 try:
876 credentials = pika.PlainCredentials(username, password)
877 parameters = pika.ConnectionParameters(host=host, port=port,
878 credentials=credentials,
879 ssl=ssl,
880 connection_attempts=3,
881 retry_delay=5,
882 socket_timeout=1)
883 connection = pika.BlockingConnection(parameters)
884 assert connection.server_properties['product'] == 'RabbitMQ'
885 self.log.debug('Connect OK')
886 return connection
887 except Exception as e:
888 msg = ('amqp connection failed to {}:{} as '
889 '{} ({})'.format(host, port, username, str(e)))
890 if fatal:
891 amulet.raise_status(amulet.FAIL, msg)
892 else:
893 self.log.warn(msg)
894 return None
895
896 def publish_amqp_message_by_unit(self, sentry_unit, message,
897 queue="test", ssl=False,
898 username="testuser1",
899 password="changeme",
900 port=None):
901 """Publish an amqp message to a rmq juju unit.
902
903 :param sentry_unit: sentry unit pointer
904 :param message: amqp message string
905 :param queue: message queue, default to test
906 :param username: amqp user name, default to testuser1
907 :param password: amqp user password
908 :param ssl: boolean, default to False
909 :param port: amqp port, use defaults if None
910 :returns: None. Raises exception if publish failed.
911 """
912 self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
913 message))
914 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
915 port=port,
916 username=username,
917 password=password)
918
919 # NOTE(beisner): extra debug here re: pika hang potential:
920 # https://github.com/pika/pika/issues/297
921 # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
922 self.log.debug('Defining channel...')
923 channel = connection.channel()
924 self.log.debug('Declaring queue...')
925 channel.queue_declare(queue=queue, auto_delete=False, durable=True)
926 self.log.debug('Publishing message...')
927 channel.basic_publish(exchange='', routing_key=queue, body=message)
928 self.log.debug('Closing channel...')
929 channel.close()
930 self.log.debug('Closing connection...')
931 connection.close()
932
933 def get_amqp_message_by_unit(self, sentry_unit, queue="test",
934 username="testuser1",
935 password="changeme",
936 ssl=False, port=None):
937 """Get an amqp message from a rmq juju unit.
938
939 :param sentry_unit: sentry unit pointer
940 :param queue: message queue, default to test
941 :param username: amqp user name, default to testuser1
942 :param password: amqp user password
943 :param ssl: boolean, default to False
944 :param port: amqp port, use defaults if None
945 :returns: amqp message body as string. Raise if get fails.
946 """
947 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
948 port=port,
949 username=username,
950 password=password)
951 channel = connection.channel()
952 method_frame, _, body = channel.basic_get(queue)
953
954 if method_frame:
955 self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
956 body))
957 channel.basic_ack(method_frame.delivery_tag)
958 channel.close()
959 connection.close()
960 return body
961 else:
962 msg = 'No message retrieved.'
963 amulet.raise_status(amulet.FAIL, msg)
0964
=== removed directory 'tests/charmhelpers/contrib/ssl'
=== removed file 'tests/charmhelpers/contrib/ssl/__init__.py'
--- tests/charmhelpers/contrib/ssl/__init__.py 2015-04-13 22:11:34 +0000
+++ tests/charmhelpers/contrib/ssl/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,94 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import subprocess
18from charmhelpers.core import hookenv
19
20
21def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=None, cn=None):
22 """Generate selfsigned SSL keypair
23
24 You must provide one of the 3 optional arguments:
25 config, subject or cn
26 If more than one is provided the leftmost will be used
27
28 Arguments:
29 keyfile -- (required) full path to the keyfile to be created
30 certfile -- (required) full path to the certfile to be created
31 keysize -- (optional) SSL key length
32 config -- (optional) openssl configuration file
33 subject -- (optional) dictionary with SSL subject variables
34 cn -- (optional) cerfificate common name
35
36 Required keys in subject dict:
37 cn -- Common name (eq. FQDN)
38
39 Optional keys in subject dict
40 country -- Country Name (2 letter code)
41 state -- State or Province Name (full name)
42 locality -- Locality Name (eg, city)
43 organization -- Organization Name (eg, company)
44 organizational_unit -- Organizational Unit Name (eg, section)
45 email -- Email Address
46 """
47
48 cmd = []
49 if config:
50 cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
51 "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
52 "-keyout", keyfile,
53 "-out", certfile, "-config", config]
54 elif subject:
55 ssl_subject = ""
56 if "country" in subject:
57 ssl_subject = ssl_subject + "/C={}".format(subject["country"])
58 if "state" in subject:
59 ssl_subject = ssl_subject + "/ST={}".format(subject["state"])
60 if "locality" in subject:
61 ssl_subject = ssl_subject + "/L={}".format(subject["locality"])
62 if "organization" in subject:
63 ssl_subject = ssl_subject + "/O={}".format(subject["organization"])
64 if "organizational_unit" in subject:
65 ssl_subject = ssl_subject + "/OU={}".format(subject["organizational_unit"])
66 if "cn" in subject:
67 ssl_subject = ssl_subject + "/CN={}".format(subject["cn"])
68 else:
69 hookenv.log("When using \"subject\" argument you must "
70 "provide \"cn\" field at very least")
71 return False
72 if "email" in subject:
73 ssl_subject = ssl_subject + "/emailAddress={}".format(subject["email"])
74
75 cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
76 "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
77 "-keyout", keyfile,
78 "-out", certfile, "-subj", ssl_subject]
79 elif cn:
80 cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
81 "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
82 "-keyout", keyfile,
83 "-out", certfile, "-subj", "/CN={}".format(cn)]
84
85 if not cmd:
86 hookenv.log("No config, subject or cn provided,"
87 "unable to generate self signed SSL certificates")
88 return False
89 try:
90 subprocess.check_call(cmd)
91 return True
92 except Exception as e:
93 print("Execution of openssl command failed:\n{}".format(e))
94 return False
950
=== removed file 'tests/charmhelpers/contrib/ssl/service.py'
--- tests/charmhelpers/contrib/ssl/service.py 2015-04-16 21:35:24 +0000
+++ tests/charmhelpers/contrib/ssl/service.py 1970-01-01 00:00:00 +0000
@@ -1,279 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18from os.path import join as path_join
19from os.path import exists
20import subprocess
21
22from charmhelpers.core.hookenv import log, DEBUG
23
24STD_CERT = "standard"
25
26# Mysql server is fairly picky about cert creation
27# and types, spec its creation separately for now.
28MYSQL_CERT = "mysql"
29
30
31class ServiceCA(object):
32
33 default_expiry = str(365 * 2)
34 default_ca_expiry = str(365 * 6)
35
36 def __init__(self, name, ca_dir, cert_type=STD_CERT):
37 self.name = name
38 self.ca_dir = ca_dir
39 self.cert_type = cert_type
40
41 ###############
42 # Hook Helper API
43 @staticmethod
44 def get_ca(type=STD_CERT):
45 service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
46 ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
47 ca = ServiceCA(service_name, ca_path, type)
48 ca.init()
49 return ca
50
51 @classmethod
52 def get_service_cert(cls, type=STD_CERT):
53 service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
54 ca = cls.get_ca()
55 crt, key = ca.get_or_create_cert(service_name)
56 return crt, key, ca.get_ca_bundle()
57
58 ###############
59
60 def init(self):
61 log("initializing service ca", level=DEBUG)
62 if not exists(self.ca_dir):
63 self._init_ca_dir(self.ca_dir)
64 self._init_ca()
65
66 @property
67 def ca_key(self):
68 return path_join(self.ca_dir, 'private', 'cacert.key')
69
70 @property
71 def ca_cert(self):
72 return path_join(self.ca_dir, 'cacert.pem')
73
74 @property
75 def ca_conf(self):
76 return path_join(self.ca_dir, 'ca.cnf')
77
78 @property
79 def signing_conf(self):
80 return path_join(self.ca_dir, 'signing.cnf')
81
82 def _init_ca_dir(self, ca_dir):
83 os.mkdir(ca_dir)
84 for i in ['certs', 'crl', 'newcerts', 'private']:
85 sd = path_join(ca_dir, i)
86 if not exists(sd):
87 os.mkdir(sd)
88
89 if not exists(path_join(ca_dir, 'serial')):
90 with open(path_join(ca_dir, 'serial'), 'w') as fh:
91 fh.write('02\n')
92
93 if not exists(path_join(ca_dir, 'index.txt')):
94 with open(path_join(ca_dir, 'index.txt'), 'w') as fh:
95 fh.write('')
96
97 def _init_ca(self):
98 """Generate the root ca's cert and key.
99 """
100 if not exists(path_join(self.ca_dir, 'ca.cnf')):
101 with open(path_join(self.ca_dir, 'ca.cnf'), 'w') as fh:
102 fh.write(
103 CA_CONF_TEMPLATE % (self.get_conf_variables()))
104
105 if not exists(path_join(self.ca_dir, 'signing.cnf')):
106 with open(path_join(self.ca_dir, 'signing.cnf'), 'w') as fh:
107 fh.write(
108 SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
109
110 if exists(self.ca_cert) or exists(self.ca_key):
111 raise RuntimeError("Initialized called when CA already exists")
112 cmd = ['openssl', 'req', '-config', self.ca_conf,
113 '-x509', '-nodes', '-newkey', 'rsa',
114 '-days', self.default_ca_expiry,
115 '-keyout', self.ca_key, '-out', self.ca_cert,
116 '-outform', 'PEM']
117 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
118 log("CA Init:\n %s" % output, level=DEBUG)
119
120 def get_conf_variables(self):
121 return dict(
122 org_name="juju",
123 org_unit_name="%s service" % self.name,
124 common_name=self.name,
125 ca_dir=self.ca_dir)
126
127 def get_or_create_cert(self, common_name):
128 if common_name in self:
129 return self.get_certificate(common_name)
130 return self.create_certificate(common_name)
131
132 def create_certificate(self, common_name):
133 if common_name in self:
134 return self.get_certificate(common_name)
135 key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
136 crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
137 csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
138 self._create_certificate(common_name, key_p, csr_p, crt_p)
139 return self.get_certificate(common_name)
140
141 def get_certificate(self, common_name):
142 if common_name not in self:
143 raise ValueError("No certificate for %s" % common_name)
144 key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
145 crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
146 with open(crt_p) as fh:
147 crt = fh.read()
148 with open(key_p) as fh:
149 key = fh.read()
150 return crt, key
151
152 def __contains__(self, common_name):
153 crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
154 return exists(crt_p)
155
156 def _create_certificate(self, common_name, key_p, csr_p, crt_p):
157 template_vars = self.get_conf_variables()
158 template_vars['common_name'] = common_name
159 subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
160 template_vars)
161
162 log("CA Create Cert %s" % common_name, level=DEBUG)
163 cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
164 '-nodes', '-days', self.default_expiry,
165 '-keyout', key_p, '-out', csr_p, '-subj', subj]
166 subprocess.check_call(cmd, stderr=subprocess.PIPE)
167 cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
168 subprocess.check_call(cmd, stderr=subprocess.PIPE)
169
170 log("CA Sign Cert %s" % common_name, level=DEBUG)
171 if self.cert_type == MYSQL_CERT:
172 cmd = ['openssl', 'x509', '-req',
173 '-in', csr_p, '-days', self.default_expiry,
174 '-CA', self.ca_cert, '-CAkey', self.ca_key,
175 '-set_serial', '01', '-out', crt_p]
176 else:
177 cmd = ['openssl', 'ca', '-config', self.signing_conf,
178 '-extensions', 'req_extensions',
179 '-days', self.default_expiry, '-notext',
180 '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
181 log("running %s" % " ".join(cmd), level=DEBUG)
182 subprocess.check_call(cmd, stderr=subprocess.PIPE)
183
184 def get_ca_bundle(self):
185 with open(self.ca_cert) as fh:
186 return fh.read()
187
188
189CA_CONF_TEMPLATE = """
190[ ca ]
191default_ca = CA_default
192
193[ CA_default ]
194dir = %(ca_dir)s
195policy = policy_match
196database = $dir/index.txt
197serial = $dir/serial
198certs = $dir/certs
199crl_dir = $dir/crl
200new_certs_dir = $dir/newcerts
201certificate = $dir/cacert.pem
202private_key = $dir/private/cacert.key
203RANDFILE = $dir/private/.rand
204default_md = default
205
206[ req ]
207default_bits = 1024
208default_md = sha1
209
210prompt = no
211distinguished_name = ca_distinguished_name
212
213x509_extensions = ca_extensions
214
215[ ca_distinguished_name ]
216organizationName = %(org_name)s
217organizationalUnitName = %(org_unit_name)s Certificate Authority
218
219
220[ policy_match ]
221countryName = optional
222stateOrProvinceName = optional
223organizationName = match
224organizationalUnitName = optional
225commonName = supplied
226
227[ ca_extensions ]
228basicConstraints = critical,CA:true
229subjectKeyIdentifier = hash
230authorityKeyIdentifier = keyid:always, issuer
231keyUsage = cRLSign, keyCertSign
232"""
233
234
235SIGNING_CONF_TEMPLATE = """
236[ ca ]
237default_ca = CA_default
238
239[ CA_default ]
240dir = %(ca_dir)s
241policy = policy_match
242database = $dir/index.txt
243serial = $dir/serial
244certs = $dir/certs
245crl_dir = $dir/crl
246new_certs_dir = $dir/newcerts
247certificate = $dir/cacert.pem
248private_key = $dir/private/cacert.key
249RANDFILE = $dir/private/.rand
250default_md = default
251
252[ req ]
253default_bits = 1024
254default_md = sha1
255
256prompt = no
257distinguished_name = req_distinguished_name
258
259x509_extensions = req_extensions
260
261[ req_distinguished_name ]
262organizationName = %(org_name)s
263organizationalUnitName = %(org_unit_name)s machine resources
264commonName = %(common_name)s
265
266[ policy_match ]
267countryName = optional
268stateOrProvinceName = optional
269organizationName = match
270organizationalUnitName = optional
271commonName = supplied
272
273[ req_extensions ]
274basicConstraints = CA:false
275subjectKeyIdentifier = hash
276authorityKeyIdentifier = keyid:always, issuer
277keyUsage = digitalSignature, keyEncipherment, keyAgreement
278extendedKeyUsage = serverAuth, clientAuth
279"""
2800
=== removed directory 'tests/charmhelpers/core'
=== removed file 'tests/charmhelpers/core/__init__.py'
--- tests/charmhelpers/core/__init__.py 2015-04-13 22:11:34 +0000
+++ tests/charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,15 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
160
=== removed file 'tests/charmhelpers/core/decorators.py'
--- tests/charmhelpers/core/decorators.py 2015-04-13 22:11:34 +0000
+++ tests/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
@@ -1,57 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17#
18# Copyright 2014 Canonical Ltd.
19#
20# Authors:
21# Edward Hope-Morley <opentastic@gmail.com>
22#
23
24import time
25
26from charmhelpers.core.hookenv import (
27 log,
28 INFO,
29)
30
31
32def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
33 """If the decorated function raises exception exc_type, allow num_retries
34 retry attempts before raise the exception.
35 """
36 def _retry_on_exception_inner_1(f):
37 def _retry_on_exception_inner_2(*args, **kwargs):
38 retries = num_retries
39 multiplier = 1
40 while True:
41 try:
42 return f(*args, **kwargs)
43 except exc_type:
44 if not retries:
45 raise
46
47 delay = base_delay * multiplier
48 multiplier += 1
49 log("Retrying '%s' %d more times (delay=%s)" %
50 (f.__name__, retries, delay), level=INFO)
51 retries -= 1
52 if delay:
53 time.sleep(delay)
54
55 return _retry_on_exception_inner_2
56
57 return _retry_on_exception_inner_1
580
=== removed file 'tests/charmhelpers/core/files.py'
--- tests/charmhelpers/core/files.py 2015-07-29 10:48:05 +0000
+++ tests/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
21
22import os
23import subprocess
24
25
26def sed(filename, before, after, flags='g'):
27 """
28 Search and replaces the given pattern on filename.
29
30 :param filename: relative or absolute file path.
31 :param before: expression to be replaced (see 'man sed')
32 :param after: expression to replace with (see 'man sed')
33 :param flags: sed-compatible regex flags in example, to make
34 the search and replace case insensitive, specify ``flags="i"``.
35 The ``g`` flag is always specified regardless, so you do not
36 need to remember to include it when overriding this parameter.
37 :returns: If the sed command exit code was zero then return,
38 otherwise raise CalledProcessError.
39 """
40 expression = r's/{0}/{1}/{2}'.format(before,
41 after, flags)
42
43 return subprocess.check_call(["sed", "-i", "-r", "-e",
44 expression,
45 os.path.expanduser(filename)])
460
=== removed file 'tests/charmhelpers/core/fstab.py'
--- tests/charmhelpers/core/fstab.py 2015-04-13 22:11:34 +0000
+++ tests/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
@@ -1,134 +0,0 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20import io
21import os
22
23__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
24
25
26class Fstab(io.FileIO):
27 """This class extends file in order to implement a file reader/writer
28 for file `/etc/fstab`
29 """
30
31 class Entry(object):
32 """Entry class represents a non-comment line on the `/etc/fstab` file
33 """
34 def __init__(self, device, mountpoint, filesystem,
35 options, d=0, p=0):
36 self.device = device
37 self.mountpoint = mountpoint
38 self.filesystem = filesystem
39
40 if not options:
41 options = "defaults"
42
43 self.options = options
44 self.d = int(d)
45 self.p = int(p)
46
47 def __eq__(self, o):
48 return str(self) == str(o)
49
50 def __str__(self):
51 return "{} {} {} {} {} {}".format(self.device,
52 self.mountpoint,
53 self.filesystem,
54 self.options,
55 self.d,
56 self.p)
57
58 DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
59
60 def __init__(self, path=None):
61 if path:
62 self._path = path
63 else:
64 self._path = self.DEFAULT_PATH
65 super(Fstab, self).__init__(self._path, 'rb+')
66
67 def _hydrate_entry(self, line):
68 # NOTE: use split with no arguments to split on any
69 # whitespace including tabs
70 return Fstab.Entry(*filter(
71 lambda x: x not in ('', None),
72 line.strip("\n").split()))
73
74 @property
75 def entries(self):
76 self.seek(0)
77 for line in self.readlines():
78 line = line.decode('us-ascii')
79 try:
80 if line.strip() and not line.strip().startswith("#"):
81 yield self._hydrate_entry(line)
82 except ValueError:
83 pass
84
85 def get_entry_by_attr(self, attr, value):
86 for entry in self.entries:
87 e_attr = getattr(entry, attr)
88 if e_attr == value:
89 return entry
90 return None
91
92 def add_entry(self, entry):
93 if self.get_entry_by_attr('device', entry.device):
94 return False
95
96 self.write((str(entry) + '\n').encode('us-ascii'))
97 self.truncate()
98 return entry
99
100 def remove_entry(self, entry):
101 self.seek(0)
102
103 lines = [l.decode('us-ascii') for l in self.readlines()]
104
105 found = False
106 for index, line in enumerate(lines):
107 if line.strip() and not line.strip().startswith("#"):
108 if self._hydrate_entry(line) == entry:
109 found = True
110 break
111
112 if not found:
113 return False
114
115 lines.remove(line)
116
117 self.seek(0)
118 self.write(''.join(lines).encode('us-ascii'))
119 self.truncate()
120 return True
121
122 @classmethod
123 def remove_by_mountpoint(cls, mountpoint, path=None):
124 fstab = cls(path=path)
125 entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
126 if entry:
127 return fstab.remove_entry(entry)
128 return False
129
130 @classmethod
131 def add(cls, device, mountpoint, filesystem, options=None, path=None):
132 return cls(path=path).add_entry(Fstab.Entry(device,
133 mountpoint, filesystem,
134 options=options))
1350
=== removed file 'tests/charmhelpers/core/hookenv.py'
--- tests/charmhelpers/core/hookenv.py 2015-09-03 09:41:42 +0000
+++ tests/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
@@ -1,898 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"Interactions with the Juju environment"
18# Copyright 2013 Canonical Ltd.
19#
20# Authors:
21# Charm Helpers Developers <juju@lists.ubuntu.com>
22
23from __future__ import print_function
24import copy
25from distutils.version import LooseVersion
26from functools import wraps
27import glob
28import os
29import json
30import yaml
31import subprocess
32import sys
33import errno
34import tempfile
35from subprocess import CalledProcessError
36
37import six
38if not six.PY3:
39 from UserDict import UserDict
40else:
41 from collections import UserDict
42
43CRITICAL = "CRITICAL"
44ERROR = "ERROR"
45WARNING = "WARNING"
46INFO = "INFO"
47DEBUG = "DEBUG"
48MARKER = object()
49
50cache = {}
51
52
53def cached(func):
54 """Cache return values for multiple executions of func + args
55
56 For example::
57
58 @cached
59 def unit_get(attribute):
60 pass
61
62 unit_get('test')
63
64 will cache the result of unit_get + 'test' for future calls.
65 """
66 @wraps(func)
67 def wrapper(*args, **kwargs):
68 global cache
69 key = str((func, args, kwargs))
70 try:
71 return cache[key]
72 except KeyError:
73 pass # Drop out of the exception handler scope.
74 res = func(*args, **kwargs)
75 cache[key] = res
76 return res
77 wrapper._wrapped = func
78 return wrapper
79
80
81def flush(key):
82 """Flushes any entries from function cache where the
83 key is found in the function+args """
84 flush_list = []
85 for item in cache:
86 if key in item:
87 flush_list.append(item)
88 for item in flush_list:
89 del cache[item]
90
91
92def log(message, level=None):
93 """Write a message to the juju log"""
94 command = ['juju-log']
95 if level:
96 command += ['-l', level]
97 if not isinstance(message, six.string_types):
98 message = repr(message)
99 command += [message]
100 # Missing juju-log should not cause failures in unit tests
101 # Send log output to stderr
102 try:
103 subprocess.call(command)
104 except OSError as e:
105 if e.errno == errno.ENOENT:
106 if level:
107 message = "{}: {}".format(level, message)
108 message = "juju-log: {}".format(message)
109 print(message, file=sys.stderr)
110 else:
111 raise
112
113
114class Serializable(UserDict):
115 """Wrapper, an object that can be serialized to yaml or json"""
116
117 def __init__(self, obj):
118 # wrap the object
119 UserDict.__init__(self)
120 self.data = obj
121
122 def __getattr__(self, attr):
123 # See if this object has attribute.
124 if attr in ("json", "yaml", "data"):
125 return self.__dict__[attr]
126 # Check for attribute in wrapped object.
127 got = getattr(self.data, attr, MARKER)
128 if got is not MARKER:
129 return got
130 # Proxy to the wrapped object via dict interface.
131 try:
132 return self.data[attr]
133 except KeyError:
134 raise AttributeError(attr)
135
136 def __getstate__(self):
137 # Pickle as a standard dictionary.
138 return self.data
139
140 def __setstate__(self, state):
141 # Unpickle into our wrapper.
142 self.data = state
143
144 def json(self):
145 """Serialize the object to json"""
146 return json.dumps(self.data)
147
148 def yaml(self):
149 """Serialize the object to yaml"""
150 return yaml.dump(self.data)
151
152
153def execution_environment():
154 """A convenient bundling of the current execution context"""
155 context = {}
156 context['conf'] = config()
157 if relation_id():
158 context['reltype'] = relation_type()
159 context['relid'] = relation_id()
160 context['rel'] = relation_get()
161 context['unit'] = local_unit()
162 context['rels'] = relations()
163 context['env'] = os.environ
164 return context
165
166
167def in_relation_hook():
168 """Determine whether we're running in a relation hook"""
169 return 'JUJU_RELATION' in os.environ
170
171
172def relation_type():
173 """The scope for the current relation hook"""
174 return os.environ.get('JUJU_RELATION', None)
175
176
177@cached
178def relation_id(relation_name=None, service_or_unit=None):
179 """The relation ID for the current or a specified relation"""
180 if not relation_name and not service_or_unit:
181 return os.environ.get('JUJU_RELATION_ID', None)
182 elif relation_name and service_or_unit:
183 service_name = service_or_unit.split('/')[0]
184 for relid in relation_ids(relation_name):
185 remote_service = remote_service_name(relid)
186 if remote_service == service_name:
187 return relid
188 else:
189 raise ValueError('Must specify neither or both of relation_name and service_or_unit')
190
191
192def local_unit():
193 """Local unit ID"""
194 return os.environ['JUJU_UNIT_NAME']
195
196
197def remote_unit():
198 """The remote unit for the current relation hook"""
199 return os.environ.get('JUJU_REMOTE_UNIT', None)
200
201
202def service_name():
203 """The name service group this unit belongs to"""
204 return local_unit().split('/')[0]
205
206
207@cached
208def remote_service_name(relid=None):
209 """The remote service name for a given relation-id (or the current relation)"""
210 if relid is None:
211 unit = remote_unit()
212 else:
213 units = related_units(relid)
214 unit = units[0] if units else None
215 return unit.split('/')[0] if unit else None
216
217
218def hook_name():
219 """The name of the currently executing hook"""
220 return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
221
222
223class Config(dict):
224 """A dictionary representation of the charm's config.yaml, with some
225 extra features:
226
227 - See which values in the dictionary have changed since the previous hook.
228 - For values that have changed, see what the previous value was.
229 - Store arbitrary data for use in a later hook.
230
231 NOTE: Do not instantiate this object directly - instead call
232 ``hookenv.config()``, which will return an instance of :class:`Config`.
233
234 Example usage::
235
236 >>> # inside a hook
237 >>> from charmhelpers.core import hookenv
238 >>> config = hookenv.config()
239 >>> config['foo']
240 'bar'
241 >>> # store a new key/value for later use
242 >>> config['mykey'] = 'myval'
243
244
245 >>> # user runs `juju set mycharm foo=baz`
246 >>> # now we're inside subsequent config-changed hook
247 >>> config = hookenv.config()
248 >>> config['foo']
249 'baz'
250 >>> # test to see if this val has changed since last hook
251 >>> config.changed('foo')
252 True
253 >>> # what was the previous value?
254 >>> config.previous('foo')
255 'bar'
256 >>> # keys/values that we add are preserved across hooks
257 >>> config['mykey']
258 'myval'
259
260 """
261 CONFIG_FILE_NAME = '.juju-persistent-config'
262
263 def __init__(self, *args, **kw):
264 super(Config, self).__init__(*args, **kw)
265 self.implicit_save = True
266 self._prev_dict = None
267 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
268 if os.path.exists(self.path):
269 self.load_previous()
270 atexit(self._implicit_save)
271
272 def load_previous(self, path=None):
273 """Load previous copy of config from disk.
274
275 In normal usage you don't need to call this method directly - it
276 is called automatically at object initialization.
277
278 :param path:
279
280 File path from which to load the previous config. If `None`,
281 config is loaded from the default location. If `path` is
282 specified, subsequent `save()` calls will write to the same
283 path.
284
285 """
286 self.path = path or self.path
287 with open(self.path) as f:
288 self._prev_dict = json.load(f)
289 for k, v in copy.deepcopy(self._prev_dict).items():
290 if k not in self:
291 self[k] = v
292
293 def changed(self, key):
294 """Return True if the current value for this key is different from
295 the previous value.
296
297 """
298 if self._prev_dict is None:
299 return True
300 return self.previous(key) != self.get(key)
301
302 def previous(self, key):
303 """Return previous value for this key, or None if there
304 is no previous value.
305
306 """
307 if self._prev_dict:
308 return self._prev_dict.get(key)
309 return None
310
311 def save(self):
312 """Save this config to disk.
313
314 If the charm is using the :mod:`Services Framework <services.base>`
315 or :meth:'@hook <Hooks.hook>' decorator, this
316 is called automatically at the end of successful hook execution.
317 Otherwise, it should be called directly by user code.
318
319 To disable automatic saves, set ``implicit_save=False`` on this
320 instance.
321
322 """
323 with open(self.path, 'w') as f:
324 json.dump(self, f)
325
326 def _implicit_save(self):
327 if self.implicit_save:
328 self.save()
329
330
331@cached
332def config(scope=None):
333 """Juju charm configuration"""
334 config_cmd_line = ['config-get']
335 if scope is not None:
336 config_cmd_line.append(scope)
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 relation_to_interface(relation_name):
495 """
496 Given the name of a relation, return the interface that relation uses.
497
498 :returns: The interface name, or ``None``.
499 """
500 return relation_to_role_and_interface(relation_name)[1]
501
502
503@cached
504def relation_to_role_and_interface(relation_name):
505 """
506 Given the name of a relation, return the role and the name of the interface
507 that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
508
509 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
510 """
511 _metadata = metadata()
512 for role in ('provides', 'requires', 'peer'):
513 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
514 if interface:
515 return role, interface
516 return None, None
517
518
519@cached
520def role_and_interface_to_relations(role, interface_name):
521 """
522 Given a role and interface name, return a list of relation names for the
523 current charm that use that interface under that role (where role is one
524 of ``provides``, ``requires``, or ``peer``).
525
526 :returns: A list of relation names.
527 """
528 _metadata = metadata()
529 results = []
530 for relation_name, relation in _metadata.get(role, {}).items():
531 if relation['interface'] == interface_name:
532 results.append(relation_name)
533 return results
534
535
536@cached
537def interface_to_relations(interface_name):
538 """
539 Given an interface, return a list of relation names for the current
540 charm that use that interface.
541
542 :returns: A list of relation names.
543 """
544 results = []
545 for role in ('provides', 'requires', 'peer'):
546 results.extend(role_and_interface_to_relations(role, interface_name))
547 return results
548
549
550@cached
551def charm_name():
552 """Get the name of the current charm as is specified on metadata.yaml"""
553 return metadata().get('name')
554
555
556@cached
557def relations():
558 """Get a nested dictionary of relation data for all related units"""
559 rels = {}
560 for reltype in relation_types():
561 relids = {}
562 for relid in relation_ids(reltype):
563 units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
564 for unit in related_units(relid):
565 reldata = relation_get(unit=unit, rid=relid)
566 units[unit] = reldata
567 relids[relid] = units
568 rels[reltype] = relids
569 return rels
570
571
572@cached
573def is_relation_made(relation, keys='private-address'):
574 '''
575 Determine whether a relation is established by checking for
576 presence of key(s). If a list of keys is provided, they
577 must all be present for the relation to be identified as made
578 '''
579 if isinstance(keys, str):
580 keys = [keys]
581 for r_id in relation_ids(relation):
582 for unit in related_units(r_id):
583 context = {}
584 for k in keys:
585 context[k] = relation_get(k, rid=r_id,
586 unit=unit)
587 if None not in context.values():
588 return True
589 return False
590
591
592def open_port(port, protocol="TCP"):
593 """Open a service network port"""
594 _args = ['open-port']
595 _args.append('{}/{}'.format(port, protocol))
596 subprocess.check_call(_args)
597
598
599def close_port(port, protocol="TCP"):
600 """Close a service network port"""
601 _args = ['close-port']
602 _args.append('{}/{}'.format(port, protocol))
603 subprocess.check_call(_args)
604
605
606@cached
607def unit_get(attribute):
608 """Get the unit ID for the remote unit"""
609 _args = ['unit-get', '--format=json', attribute]
610 try:
611 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
612 except ValueError:
613 return None
614
615
616def unit_public_ip():
617 """Get this unit's public IP address"""
618 return unit_get('public-address')
619
620
621def unit_private_ip():
622 """Get this unit's private IP address"""
623 return unit_get('private-address')
624
625
626class UnregisteredHookError(Exception):
627 """Raised when an undefined hook is called"""
628 pass
629
630
631class Hooks(object):
632 """A convenient handler for hook functions.
633
634 Example::
635
636 hooks = Hooks()
637
638 # register a hook, taking its name from the function name
639 @hooks.hook()
640 def install():
641 pass # your code here
642
643 # register a hook, providing a custom hook name
644 @hooks.hook("config-changed")
645 def config_changed():
646 pass # your code here
647
648 if __name__ == "__main__":
649 # execute a hook based on the name the program is called by
650 hooks.execute(sys.argv)
651 """
652
653 def __init__(self, config_save=None):
654 super(Hooks, self).__init__()
655 self._hooks = {}
656
657 # For unknown reasons, we allow the Hooks constructor to override
658 # config().implicit_save.
659 if config_save is not None:
660 config().implicit_save = config_save
661
662 def register(self, name, function):
663 """Register a hook"""
664 self._hooks[name] = function
665
666 def execute(self, args):
667 """Execute a registered hook based on args[0]"""
668 _run_atstart()
669 hook_name = os.path.basename(args[0])
670 if hook_name in self._hooks:
671 try:
672 self._hooks[hook_name]()
673 except SystemExit as x:
674 if x.code is None or x.code == 0:
675 _run_atexit()
676 raise
677 _run_atexit()
678 else:
679 raise UnregisteredHookError(hook_name)
680
681 def hook(self, *hook_names):
682 """Decorator, registering them as hooks"""
683 def wrapper(decorated):
684 for hook_name in hook_names:
685 self.register(hook_name, decorated)
686 else:
687 self.register(decorated.__name__, decorated)
688 if '_' in decorated.__name__:
689 self.register(
690 decorated.__name__.replace('_', '-'), decorated)
691 return decorated
692 return wrapper
693
694
695def charm_dir():
696 """Return the root directory of the current charm"""
697 return os.environ.get('CHARM_DIR')
698
699
700@cached
701def action_get(key=None):
702 """Gets the value of an action parameter, or all key/value param pairs"""
703 cmd = ['action-get']
704 if key is not None:
705 cmd.append(key)
706 cmd.append('--format=json')
707 action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
708 return action_data
709
710
711def action_set(values):
712 """Sets the values to be returned after the action finishes"""
713 cmd = ['action-set']
714 for k, v in list(values.items()):
715 cmd.append('{}={}'.format(k, v))
716 subprocess.check_call(cmd)
717
718
719def action_fail(message):
720 """Sets the action status to failed and sets the error message.
721
722 The results set by action_set are preserved."""
723 subprocess.check_call(['action-fail', message])
724
725
726def action_name():
727 """Get the name of the currently executing action."""
728 return os.environ.get('JUJU_ACTION_NAME')
729
730
731def action_uuid():
732 """Get the UUID of the currently executing action."""
733 return os.environ.get('JUJU_ACTION_UUID')
734
735
736def action_tag():
737 """Get the tag for the currently executing action."""
738 return os.environ.get('JUJU_ACTION_TAG')
739
740
741def status_set(workload_state, message):
742 """Set the workload state with a message
743
744 Use status-set to set the workload state with a message which is visible
745 to the user via juju status. If the status-set command is not found then
746 assume this is juju < 1.23 and juju-log the message unstead.
747
748 workload_state -- valid juju workload state.
749 message -- status update message
750 """
751 valid_states = ['maintenance', 'blocked', 'waiting', 'active']
752 if workload_state not in valid_states:
753 raise ValueError(
754 '{!r} is not a valid workload state'.format(workload_state)
755 )
756 cmd = ['status-set', workload_state, message]
757 try:
758 ret = subprocess.call(cmd)
759 if ret == 0:
760 return
761 except OSError as e:
762 if e.errno != errno.ENOENT:
763 raise
764 log_message = 'status-set failed: {} {}'.format(workload_state,
765 message)
766 log(log_message, level='INFO')
767
768
769def status_get():
770 """Retrieve the previously set juju workload state and message
771
772 If the status-get command is not found then assume this is juju < 1.23 and
773 return 'unknown', ""
774
775 """
776 cmd = ['status-get', "--format=json", "--include-data"]
777 try:
778 raw_status = subprocess.check_output(cmd)
779 except OSError as e:
780 if e.errno == errno.ENOENT:
781 return ('unknown', "")
782 else:
783 raise
784 else:
785 status = json.loads(raw_status.decode("UTF-8"))
786 return (status["status"], status["message"])
787
788
789def translate_exc(from_exc, to_exc):
790 def inner_translate_exc1(f):
791 def inner_translate_exc2(*args, **kwargs):
792 try:
793 return f(*args, **kwargs)
794 except from_exc:
795 raise to_exc
796
797 return inner_translate_exc2
798
799 return inner_translate_exc1
800
801
802@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
803def is_leader():
804 """Does the current unit hold the juju leadership
805
806 Uses juju to determine whether the current unit is the leader of its peers
807 """
808 cmd = ['is-leader', '--format=json']
809 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
810
811
812@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
813def leader_get(attribute=None):
814 """Juju leader get value(s)"""
815 cmd = ['leader-get', '--format=json'] + [attribute or '-']
816 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
817
818
819@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
820def leader_set(settings=None, **kwargs):
821 """Juju leader set value(s)"""
822 # Don't log secrets.
823 # log("Juju leader-set '%s'" % (settings), level=DEBUG)
824 cmd = ['leader-set']
825 settings = settings or {}
826 settings.update(kwargs)
827 for k, v in settings.items():
828 if v is None:
829 cmd.append('{}='.format(k))
830 else:
831 cmd.append('{}={}'.format(k, v))
832 subprocess.check_call(cmd)
833
834
835@cached
836def juju_version():
837 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
838 # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
839 jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
840 return subprocess.check_output([jujud, 'version'],
841 universal_newlines=True).strip()
842
843
844@cached
845def has_juju_version(minimum_version):
846 """Return True if the Juju version is at least the provided version"""
847 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
848
849
850_atexit = []
851_atstart = []
852
853
854def atstart(callback, *args, **kwargs):
855 '''Schedule a callback to run before the main hook.
856
857 Callbacks are run in the order they were added.
858
859 This is useful for modules and classes to perform initialization
860 and inject behavior. In particular:
861
862 - Run common code before all of your hooks, such as logging
863 the hook name or interesting relation data.
864 - Defer object or module initialization that requires a hook
865 context until we know there actually is a hook context,
866 making testing easier.
867 - Rather than requiring charm authors to include boilerplate to
868 invoke your helper's behavior, have it run automatically if
869 your object is instantiated or module imported.
870
871 This is not at all useful after your hook framework as been launched.
872 '''
873 global _atstart
874 _atstart.append((callback, args, kwargs))
875
876
877def atexit(callback, *args, **kwargs):
878 '''Schedule a callback to run on successful hook completion.
879
880 Callbacks are run in the reverse order that they were added.'''
881 _atexit.append((callback, args, kwargs))
882
883
884def _run_atstart():
885 '''Hook frameworks must invoke this before running the main hook body.'''
886 global _atstart
887 for callback, args, kwargs in _atstart:
888 callback(*args, **kwargs)
889 del _atstart[:]
890
891
892def _run_atexit():
893 '''Hook frameworks must invoke this after the main hook body has
894 successfully completed. Do not invoke it if the hook fails.'''
895 global _atexit
896 for callback, args, kwargs in reversed(_atexit):
897 callback(*args, **kwargs)
898 del _atexit[:]
8990
=== removed file 'tests/charmhelpers/core/host.py'
--- tests/charmhelpers/core/host.py 2015-08-19 13:49:53 +0000
+++ tests/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
@@ -1,570 +0,0 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tools for working with the host system"""
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# Nick Moffitt <nick.moffitt@canonical.com>
22# Matthew Wedgwood <matthew.wedgwood@canonical.com>
23
24import os
25import re
26import pwd
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches