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

Proposed by Ryan Beisner
Status: Superseded
Proposed branch: lp:~1chb1n/charms/trusty/rabbitmq-server/amulet-refactor-1508
Merge into: lp:~openstack-charmers-archive/charms/trusty/rabbitmq-server/next
Diff against target: 6948 lines (+2700/-3906) (has conflicts)
45 files modified
Makefile (+5/-4)
charm-helpers-tests.yaml (+2/-3)
hooks/rabbitmq_server_relations.py (+18/-0)
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 (+771/-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 (+965/-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/-896)
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)
Text conflict in hooks/rabbitmq_server_relations.py
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/rabbitmq-server/amulet-refactor-1508
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
Review via email: mp+268811@code.launchpad.net

This proposal has been superseded by a proposal from 2015-09-01.

Description of the change

Refactor amulet tests, deprecate old tests, pull in cluster race fix and collect_rabbitmq_stats script fix, sync charm-helpers.

Cluster race bug:
https://bugs.launchpad.net/charms/+source/rabbitmq-server/+bug/1486177

PID file bug for Vivid and later:
https://bugs.launchpad.net/charms/+source/rabbitmq-server/+bug/1485722

This charm merge proposal is dependent on charm-helpers mp @:
https://code.launchpad.net/~1chb1n/charm-helpers/amulet-rmq-helpers

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

charm_lint_check #8491 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

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

charm_unit_test #7882 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_amulet_test #5952 rabbitmq-server-next for 1chb1n mp268811
    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/12146373/
Build: http://10.245.162.77:8080/job/charm_amulet_test/5952/

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

In amulet test 5952, all combos except Vivid-Kilo pass all tests. All combos, including V-K cluster ok. The V-K fail is a packaging issue (bug 1485722).

110. By Ryan Beisner

update makefile

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

charm_lint_check #8541 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

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

charm_amulet_test #5953 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
ERROR subprocess encountered error code 1
ERROR:root:Make target returned non-zero.

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

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

charm_unit_test #7930 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_amulet_test #5958 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-08-22 06:21:18,640 run_cmd_unit DEBUG: rabbitmq-server/2 `bash -c "$(egrep -oh /usr/local.* /etc/nagios/nrpe.d/check_rabbitmq.cfg)"` command returned 0 (OK)
ERROR:root:Make target returned non-zero.

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

111. By Ryan Beisner

update amulet nrpe check

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

charm_unit_test #8039 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_lint_check #8706 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

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

charm_amulet_test #6029 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-08-25 20:07:58,933 publish_amqp_message_by_unit DEBUG: Publishing message to test queue:
ERROR:root:Make target returned non-zero.

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

112. By Ryan Beisner

revert prev rev change to nrpe test; cron job is expected to have already run check_rabbitmq

113. By Ryan Beisner

fix collect script for vivid (cron job)

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

charm_unit_test #8089 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_lint_check #8758 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

114. By Ryan Beisner

add nrpe dat file existence check

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

charm_lint_check #8763 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

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

charm_unit_test #8094 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_amulet_test #6041 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

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

charm_lint_check #8814 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

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

charm_unit_test #8142 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_amulet_test #6043 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

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

charm_amulet_test #6044 rabbitmq-server-next for 1chb1n mp268811
    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/12203258/
Build: http://10.245.162.77:8080/job/charm_amulet_test/6044/

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

charm_amulet_test #6047 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

115. By Ryan Beisner

rebase, fix landed for bug 1485722 (vivid pid file)

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

charm_lint_check #8817 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

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

charm_unit_test #8145 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_amulet_test #6049 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-08-27 04:27:53,775 _test_rmq_amqp_messages_all_units DEBUG: Publish message to: 172.17.107.154 (rabbitmq-server/1 juju-osci-sv07-machine-3)
ERROR:root:Make target returned non-zero.

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

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

charm_amulet_test #6052 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
MESSAGE 7@172.17.117.126 [62A5E9AD-3A96-4865-B303-DC431ECF9D43-1440653092.55]
ERROR:root:Make target returned non-zero.

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

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

charm_lint_check #8828 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

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

charm_unit_test #8156 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_amulet_test #6062 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-08-27 16:27:34,911 connect_amqp_by_unit DEBUG: Connecting to amqp on 172.17.105.60:5999 (rabbitmq-server/0) as testuser1...
ERROR:root:Make target returned non-zero.

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

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

charm_unit_test #8251 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_lint_check #8932 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

117. By Ryan Beisner

 - close connections and channels when done;
 - update debug feedback;
 - fix math on mgmt plugin check.

118. By Ryan Beisner

Re-sync tests/charmhelpers re: updated fix for service_restarted_since bug 1474030

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

charm_unit_test #8409 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_lint_check #9100 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

119. By Ryan Beisner

re-merge lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

120. By Ryan Beisner

update comments

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

charm_lint_check #9152 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

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

charm_unit_test #8458 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

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

charm_amulet_test #6163 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

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

charm_amulet_test #6164 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

121. By Ryan Beisner

re-merge lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

122. By Ryan Beisner

resync tests/charmhelpers for lint fixes

123. By Ryan Beisner

resync tests/charmhelpers for review changes

124. By Ryan Beisner

resync tests/charmhelpers

125. By Ryan Beisner

revert hooks, pull from rmq/next

Unmerged revisions

125. By Ryan Beisner

revert hooks, pull from rmq/next

124. By Ryan Beisner

resync tests/charmhelpers

123. By Ryan Beisner

resync tests/charmhelpers for review changes

122. By Ryan Beisner

resync tests/charmhelpers for lint fixes

121. By Ryan Beisner

re-merge lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

120. By Ryan Beisner

update comments

119. By Ryan Beisner

re-merge lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

118. By Ryan Beisner

Re-sync tests/charmhelpers re: updated fix for service_restarted_since bug 1474030

117. By Ryan Beisner

 - close connections and channels when done;
 - update debug feedback;
 - fix math on mgmt plugin check.

116. By Ryan Beisner

re-merge lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2015-04-20 11:13:39 +0000
3+++ Makefile 2015-09-01 02:46:36 +0000
4@@ -29,14 +29,15 @@
5 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml
6 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
7
8-publish: lint
9+publish: lint test
10 bzr push lp:charms/rabbitmq-server
11 bzr push lp:charms/trusty/rabbitmq-server
12
13-unit_test: .venv
14+test: .venv
15 @echo Starting tests...
16- env CHARM_DIR=$(CHARM_DIR) $(TEST_PREFIX) .venv/bin/nosetests unit_tests/
17+ env CHARM_DIR=$(CHARM_DIR) $(TEST_PREFIX) .venv/bin/nosetests \
18+ --nologcapture --with-coverage unit_tests/
19
20 functional_test:
21 @echo Starting amulet tests...
22- @juju test -v -p AMULET_HTTP_PROXY,OS_USERNAME,OS_TENANT_NAME,OS_REGION_NAME,OS_PASSWORD,OS_AUTH_URL --timeout 900
23+ @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
24
25=== modified file 'charm-helpers-tests.yaml'
26--- charm-helpers-tests.yaml 2015-07-31 22:00:49 +0000
27+++ charm-helpers-tests.yaml 2015-09-01 02:46:36 +0000
28@@ -1,6 +1,5 @@
29 destination: tests/charmhelpers
30 branch: lp:charm-helpers
31 include:
32- - core
33- - cli
34- - contrib.ssl
35+ - contrib.amulet
36+ - contrib.openstack.amulet
37
38=== modified file 'hooks/rabbitmq_server_relations.py'
39--- hooks/rabbitmq_server_relations.py 2015-09-01 00:54:12 +0000
40+++ hooks/rabbitmq_server_relations.py 2015-09-01 02:46:36 +0000
41@@ -326,6 +326,7 @@
42 log('cluster_joined: cookie not yet set.', level=INFO)
43 return
44
45+<<<<<<< TREE
46 rdata = relation_get()
47 hostname = rdata.get('hostname', None)
48 private_address = rdata.get('private-address', None)
49@@ -343,6 +344,23 @@
50
51 if hostname and private_address:
52 rabbit.update_hosts_file({private_address: hostname})
53+=======
54+ # If called from leader-settings-changed we are not in a hook env
55+ if not in_relation_hook():
56+ rid = relation_ids('cluster')[0]
57+ for unit in related_units(rid):
58+ rdata = relation_get(rid=rid, unit=unit)
59+ if rdata:
60+ break;
61+ else:
62+ rdata = relation_get()
63+
64+ if config('prefer-ipv6') and rdata.get('hostname'):
65+ private_address = rdata['private-address']
66+ hostname = rdata['hostname']
67+ if hostname:
68+ rabbit.update_hosts_file({private_address: hostname})
69+>>>>>>> MERGE-SOURCE
70
71 # sync passwords
72 blacklist = ['hostname', 'private-address', 'public-address']
73
74=== modified file 'metadata.yaml'
75--- metadata.yaml 2013-11-15 19:15:16 +0000
76+++ metadata.yaml 2015-09-01 02:46:36 +0000
77@@ -5,7 +5,10 @@
78 RabbitMQ is an implementation of AMQP, the emerging standard for high
79 performance enterprise messaging. The RabbitMQ server is a robust and
80 scalable implementation of an AMQP broker.
81-categories: ["misc"]
82+tags:
83+ - openstack
84+ - amqp
85+ - misc
86 provides:
87 amqp:
88 interface: rabbitmq
89
90=== added file 'tests/00-setup'
91--- tests/00-setup 1970-01-01 00:00:00 +0000
92+++ tests/00-setup 2015-09-01 02:46:36 +0000
93@@ -0,0 +1,16 @@
94+#!/bin/bash
95+
96+set -ex
97+
98+sudo add-apt-repository --yes ppa:juju/stable
99+sudo apt-get update --yes
100+sudo apt-get install --yes python-amulet \
101+ python-cinderclient \
102+ python-distro-info \
103+ python-glanceclient \
104+ python-heatclient \
105+ python-keystoneclient \
106+ python-neutronclient \
107+ python-novaclient \
108+ python-pika \
109+ python-swiftclient
110
111=== added file 'tests/014-basic-precise-icehouse'
112--- tests/014-basic-precise-icehouse 1970-01-01 00:00:00 +0000
113+++ tests/014-basic-precise-icehouse 2015-09-01 02:46:36 +0000
114@@ -0,0 +1,11 @@
115+#!/usr/bin/python
116+
117+"""Amulet tests on a basic rabbitmq-server deployment on precise-icehouse."""
118+
119+from basic_deployment import RmqBasicDeployment
120+
121+if __name__ == '__main__':
122+ deployment = RmqBasicDeployment(series='precise',
123+ openstack='cloud:precise-icehouse',
124+ source='cloud:precise-updates/icehouse')
125+ deployment.run_tests()
126
127=== added file 'tests/015-basic-trusty-icehouse'
128--- tests/015-basic-trusty-icehouse 1970-01-01 00:00:00 +0000
129+++ tests/015-basic-trusty-icehouse 2015-09-01 02:46:36 +0000
130@@ -0,0 +1,9 @@
131+#!/usr/bin/python
132+
133+"""Amulet tests on a basic rabbitmq-server deployment on trusty-icehouse."""
134+
135+from basic_deployment import RmqBasicDeployment
136+
137+if __name__ == '__main__':
138+ deployment = RmqBasicDeployment(series='trusty')
139+ deployment.run_tests()
140
141=== added file 'tests/016-basic-trusty-juno'
142--- tests/016-basic-trusty-juno 1970-01-01 00:00:00 +0000
143+++ tests/016-basic-trusty-juno 2015-09-01 02:46:36 +0000
144@@ -0,0 +1,11 @@
145+#!/usr/bin/python
146+
147+"""Amulet tests on a basic rabbitmq-server deployment on trusty-juno."""
148+
149+from basic_deployment import RmqBasicDeployment
150+
151+if __name__ == '__main__':
152+ deployment = RmqBasicDeployment(series='trusty',
153+ openstack='cloud:trusty-juno',
154+ source='cloud:trusty-updates/juno')
155+ deployment.run_tests()
156
157=== added file 'tests/017-basic-trusty-kilo'
158--- tests/017-basic-trusty-kilo 1970-01-01 00:00:00 +0000
159+++ tests/017-basic-trusty-kilo 2015-09-01 02:46:36 +0000
160@@ -0,0 +1,11 @@
161+#!/usr/bin/python
162+
163+"""Amulet tests on a basic rabbitmq-server deployment on trusty-kilo."""
164+
165+from basic_deployment import RmqBasicDeployment
166+
167+if __name__ == '__main__':
168+ deployment = RmqBasicDeployment(series='trusty',
169+ openstack='cloud:trusty-kilo',
170+ source='cloud:trusty-updates/kilo')
171+ deployment.run_tests()
172
173=== added file 'tests/019-basic-vivid-kilo'
174--- tests/019-basic-vivid-kilo 1970-01-01 00:00:00 +0000
175+++ tests/019-basic-vivid-kilo 2015-09-01 02:46:36 +0000
176@@ -0,0 +1,9 @@
177+#!/usr/bin/python
178+
179+"""Amulet tests on a basic rabbitmq-server deployment on vivid-kilo."""
180+
181+from basic_deployment import RmqBasicDeployment
182+
183+if __name__ == '__main__':
184+ deployment = RmqBasicDeployment(series='vivid')
185+ deployment.run_tests()
186
187=== added file 'tests/020-basic-trusty-liberty'
188--- tests/020-basic-trusty-liberty 1970-01-01 00:00:00 +0000
189+++ tests/020-basic-trusty-liberty 2015-09-01 02:46:36 +0000
190@@ -0,0 +1,11 @@
191+#!/usr/bin/python
192+
193+"""Amulet tests on a basic rabbitmq-server deployment on trusty-liberty."""
194+
195+from basic_deployment import RmqBasicDeployment
196+
197+if __name__ == '__main__':
198+ deployment = RmqBasicDeployment(series='trusty',
199+ openstack='cloud:trusty-liberty',
200+ source='cloud:trusty-updates/liberty')
201+ deployment.run_tests()
202
203=== added file 'tests/021-basic-wily-liberty'
204--- tests/021-basic-wily-liberty 1970-01-01 00:00:00 +0000
205+++ tests/021-basic-wily-liberty 2015-09-01 02:46:36 +0000
206@@ -0,0 +1,9 @@
207+#!/usr/bin/python
208+
209+"""Amulet tests on a basic rabbitmq-server deployment on wily-liberty."""
210+
211+from basic_deployment import RmqBasicDeployment
212+
213+if __name__ == '__main__':
214+ deployment = RmqBasicDeployment(series='wily')
215+ deployment.run_tests()
216
217=== added file 'tests/basic_deployment.py'
218--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
219+++ tests/basic_deployment.py 2015-09-01 02:46:36 +0000
220@@ -0,0 +1,492 @@
221+#!/usr/bin/python
222+"""
223+Basic 3-node rabbitmq-server native cluster + nrpe functional tests
224+
225+Cinder is present to exercise and inspect amqp relation functionality.
226+
227+Each individual test is idempotent, in that it creates/deletes
228+a rmq test user, enables or disables ssl as needed.
229+
230+Test order is not required, however tests are numbered to keep
231+relevant tests grouped together in run order.
232+"""
233+
234+import amulet
235+import time
236+
237+from charmhelpers.contrib.openstack.amulet.deployment import (
238+ OpenStackAmuletDeployment
239+)
240+
241+from charmhelpers.contrib.openstack.amulet.utils import (
242+ OpenStackAmuletUtils,
243+ DEBUG,
244+ # ERROR
245+)
246+
247+# Use DEBUG to turn on debug logging
248+u = OpenStackAmuletUtils(DEBUG)
249+
250+
251+class RmqBasicDeployment(OpenStackAmuletDeployment):
252+ """Amulet tests on a basic rabbitmq cluster deployment. Verify
253+ relations, service status, users and endpoint service catalog."""
254+
255+ def __init__(self, series=None, openstack=None, source=None, stable=False):
256+ """Deploy the entire test environment."""
257+ super(RmqBasicDeployment, self).__init__(series, openstack, source,
258+ stable)
259+ self._add_services()
260+ self._add_relations()
261+ self._configure_services()
262+ self._deploy()
263+ self._initialize_tests()
264+
265+ def _add_services(self):
266+ """Add services
267+
268+ Add the services that we're testing, where rmq is local,
269+ and the rest of the service are from lp branches that are
270+ compatible with the local charm (e.g. stable or next).
271+ """
272+ this_service = {
273+ 'name': 'rabbitmq-server',
274+ 'units': 3
275+ }
276+ other_services = [{'name': 'cinder'},
277+ {'name': 'nrpe'}]
278+
279+ super(RmqBasicDeployment, self)._add_services(this_service,
280+ other_services)
281+
282+ def _add_relations(self):
283+ """Add relations for the services."""
284+ relations = {'cinder:amqp': 'rabbitmq-server:amqp',
285+ 'nrpe:nrpe-external-master':
286+ 'rabbitmq-server:nrpe-external-master'}
287+
288+ super(RmqBasicDeployment, self)._add_relations(relations)
289+
290+ def _configure_services(self):
291+ """Configure all of the services."""
292+ rmq_config = {
293+ 'min-cluster-size': '3',
294+ 'max-cluster-tries': '6',
295+ 'ssl': 'off',
296+ 'management_plugin': 'False',
297+ 'stats_cron_schedule': '*/1 * * * *'
298+ }
299+ cinder_config = {}
300+ configs = {'rabbitmq-server': rmq_config,
301+ 'cinder': cinder_config}
302+ super(RmqBasicDeployment, self)._configure_services(configs)
303+
304+ def _initialize_tests(self):
305+ """Perform final initialization before tests get run."""
306+ # Access the sentries for inspecting service units
307+ self.rmq0_sentry = self.d.sentry.unit['rabbitmq-server/0']
308+ self.rmq1_sentry = self.d.sentry.unit['rabbitmq-server/1']
309+ self.rmq2_sentry = self.d.sentry.unit['rabbitmq-server/2']
310+ self.cinder_sentry = self.d.sentry.unit['cinder/0']
311+ self.nrpe_sentry = self.d.sentry.unit['nrpe/0']
312+ u.log.debug('openstack release val: {}'.format(
313+ self._get_openstack_release()))
314+ u.log.debug('openstack release str: {}'.format(
315+ self._get_openstack_release_string()))
316+
317+ # Let things settle a bit before moving forward
318+ time.sleep(30)
319+
320+ def _get_rmq_sentry_units(self):
321+ """Local helper specific to this 3-node rmq series of tests."""
322+ return [self.rmq0_sentry,
323+ self.rmq1_sentry,
324+ self.rmq2_sentry]
325+
326+ def _test_rmq_amqp_messages_all_units(self, sentry_units,
327+ ssl=False, port=None):
328+ """Reusable test to send amqp messages to every listed rmq unit
329+ and check every listed rmq unit for messages.
330+
331+ :param sentry_units: list of sentry units
332+ :returns: None if successful. Raise on error.
333+ """
334+
335+ # Add test user if it does not already exist
336+ u.add_rmq_test_user(sentry_units)
337+
338+ # Handle ssl
339+ if ssl:
340+ u.configure_rmq_ssl_on(sentry_units, self.d, port=port)
341+ else:
342+ u.configure_rmq_ssl_off(sentry_units, self.d)
343+
344+ # Publish and get amqp messages in all possible unit combinations.
345+ # Qty of checks == (qty of units) ^ 2
346+ amqp_msg_counter = 1
347+ host_names = u.get_unit_hostnames(sentry_units)
348+
349+ for dest_unit in sentry_units:
350+ dest_unit_name = dest_unit.info['unit_name']
351+ dest_unit_host = dest_unit.info['public-address']
352+ dest_unit_host_name = host_names[dest_unit_name]
353+
354+ for check_unit in sentry_units:
355+ check_unit_name = check_unit.info['unit_name']
356+ check_unit_host = check_unit.info['public-address']
357+ check_unit_host_name = host_names[check_unit_name]
358+
359+ amqp_msg_stamp = u.get_uuid_epoch_stamp()
360+ amqp_msg = ('Message {}@{} {}'.format(amqp_msg_counter,
361+ dest_unit_host,
362+ amqp_msg_stamp)).upper()
363+ # Publish amqp message
364+ u.log.debug('Publish message to: {} '
365+ '({} {})'.format(dest_unit_host,
366+ dest_unit_name,
367+ dest_unit_host_name))
368+
369+ u.publish_amqp_message_by_unit(dest_unit,
370+ amqp_msg, ssl=ssl,
371+ port=port)
372+
373+ # Wait a bit before checking for message
374+ time.sleep(2)
375+
376+ # Get amqp message
377+ u.log.debug('Get message from: {} '
378+ '({} {})'.format(check_unit_host,
379+ check_unit_name,
380+ check_unit_host_name))
381+
382+ amqp_msg_rcvd = u.get_amqp_message_by_unit(check_unit,
383+ ssl=ssl,
384+ port=port)
385+
386+ # Validate amqp message content
387+ if amqp_msg == amqp_msg_rcvd:
388+ u.log.debug('Message {} received '
389+ 'OK.'.format(amqp_msg_counter))
390+ else:
391+ u.log.error('Expected: {}'.format(amqp_msg))
392+ u.log.error('Actual: {}'.format(amqp_msg_rcvd))
393+ msg = 'Message {} mismatch.'.format(amqp_msg_counter)
394+ amulet.raise_status(amulet.FAIL, msg)
395+
396+ amqp_msg_counter += 1
397+
398+ # Delete the test user
399+ u.delete_rmq_test_user(sentry_units)
400+
401+ def test_100_rmq_processes(self):
402+ """Verify that the expected service processes are running
403+ on each rabbitmq-server unit."""
404+
405+ # Beam and epmd sometimes briefly have more than one PID,
406+ # True checks for at least 1.
407+ rmq_processes = {
408+ 'beam': True,
409+ 'epmd': True,
410+ }
411+
412+ # Units with process names and PID quantities expected
413+ expected_processes = {
414+ self.rmq0_sentry: rmq_processes,
415+ self.rmq1_sentry: rmq_processes,
416+ self.rmq2_sentry: rmq_processes
417+ }
418+
419+ actual_pids = u.get_unit_process_ids(expected_processes)
420+ ret = u.validate_unit_process_ids(expected_processes, actual_pids)
421+ if ret:
422+ amulet.raise_status(amulet.FAIL, msg=ret)
423+
424+ u.log.info('OK\n')
425+
426+ def test_102_services(self):
427+ """Verify that the expected services are running on the
428+ corresponding service units."""
429+ services = {
430+ self.rmq0_sentry: ['rabbitmq-server'],
431+ self.rmq1_sentry: ['rabbitmq-server'],
432+ self.rmq2_sentry: ['rabbitmq-server'],
433+ self.cinder_sentry: ['cinder-api',
434+ 'cinder-scheduler',
435+ 'cinder-volume'],
436+ }
437+ ret = u.validate_services_by_name(services)
438+ if ret:
439+ amulet.raise_status(amulet.FAIL, msg=ret)
440+
441+ u.log.info('OK\n')
442+
443+ def test_200_rmq_cinder_amqp_relation(self):
444+ """Verify the rabbitmq-server:cinder amqp relation data"""
445+ u.log.debug('Checking rmq:cinder amqp relation data...')
446+ unit = self.rmq0_sentry
447+ relation = ['amqp', 'cinder:amqp']
448+ expected = {
449+ 'private-address': u.valid_ip,
450+ 'password': u.not_null,
451+ 'hostname': u.valid_ip
452+ }
453+ ret = u.validate_relation_data(unit, relation, expected)
454+ if ret:
455+ msg = u.relation_error('amqp cinder', ret)
456+ amulet.raise_status(amulet.FAIL, msg=msg)
457+
458+ u.log.info('OK\n')
459+
460+ def test_201_cinder_rmq_amqp_relation(self):
461+ """Verify the cinder:rabbitmq-server amqp relation data"""
462+ u.log.debug('Checking cinder:rmq amqp relation data...')
463+ unit = self.cinder_sentry
464+ relation = ['amqp', 'rabbitmq-server:amqp']
465+ expected = {
466+ 'private-address': u.valid_ip,
467+ 'vhost': 'openstack',
468+ 'username': u.not_null
469+ }
470+ ret = u.validate_relation_data(unit, relation, expected)
471+ if ret:
472+ msg = u.relation_error('cinder amqp', ret)
473+ amulet.raise_status(amulet.FAIL, msg=msg)
474+
475+ u.log.info('OK\n')
476+
477+ def test_202_rmq_nrpe_ext_master_relation(self):
478+ """Verify rabbitmq-server:nrpe nrpe-external-master relation data"""
479+ u.log.debug('Checking rmq:nrpe external master relation data...')
480+ unit = self.rmq0_sentry
481+ relation = ['nrpe-external-master',
482+ 'nrpe:nrpe-external-master']
483+
484+ mon_sub = ('monitors:\n remote:\n nrpe:\n rabbitmq: '
485+ '{command: check_rabbitmq}\n rabbitmq_queue: '
486+ '{command: check_rabbitmq_queue}\n')
487+
488+ expected = {
489+ 'private-address': u.valid_ip,
490+ 'monitors': mon_sub
491+ }
492+
493+ ret = u.validate_relation_data(unit, relation, expected)
494+ if ret:
495+ msg = u.relation_error('amqp nrpe', ret)
496+ amulet.raise_status(amulet.FAIL, msg=msg)
497+
498+ u.log.info('OK\n')
499+
500+ def test_203_nrpe_rmq_ext_master_relation(self):
501+ """Verify nrpe:rabbitmq-server nrpe-external-master relation data"""
502+ u.log.debug('Checking nrpe:rmq external master relation data...')
503+ unit = self.nrpe_sentry
504+ relation = ['nrpe-external-master',
505+ 'rabbitmq-server:nrpe-external-master']
506+
507+ expected = {
508+ 'private-address': u.valid_ip
509+ }
510+
511+ ret = u.validate_relation_data(unit, relation, expected)
512+ if ret:
513+ msg = u.relation_error('nrpe amqp', ret)
514+ amulet.raise_status(amulet.FAIL, msg=msg)
515+
516+ u.log.info('OK\n')
517+
518+ def test_300_rmq_config(self):
519+ """Verify the data in the rabbitmq conf file."""
520+ conf = '/etc/rabbitmq/rabbitmq-env.conf'
521+ sentry_units = self._get_rmq_sentry_units()
522+ for unit in sentry_units:
523+ host_name = unit.file_contents('/etc/hostname').strip()
524+ u.log.debug('Checking rabbitmq config file data on '
525+ '{} ({})...'.format(unit.info['unit_name'],
526+ host_name))
527+ expected = {
528+ 'RABBITMQ_NODENAME': 'rabbit@{}'.format(host_name)
529+ }
530+
531+ file_contents = unit.file_contents(conf)
532+ u.validate_sectionless_conf(file_contents, expected)
533+
534+ u.log.info('OK\n')
535+
536+ def test_400_rmq_cluster_running_nodes(self):
537+ """Verify that cluster status from each rmq juju unit shows
538+ every cluster node as a running member in that cluster."""
539+ u.log.debug('Checking that all units are in cluster_status '
540+ 'running nodes...')
541+
542+ sentry_units = self._get_rmq_sentry_units()
543+
544+ ret = u.validate_rmq_cluster_running_nodes(sentry_units)
545+ if ret:
546+ amulet.raise_status(amulet.FAIL, msg=ret)
547+
548+ u.log.info('OK\n')
549+
550+ def test_402_rmq_connect_with_ssl_off(self):
551+ """Verify successful non-ssl amqp connection to all units when
552+ charm config option for ssl is set False."""
553+ u.log.debug('Confirming that non-ssl connection succeeds when '
554+ 'ssl config is off...')
555+ sentry_units = self._get_rmq_sentry_units()
556+ u.add_rmq_test_user(sentry_units)
557+ u.configure_rmq_ssl_off(sentry_units, self.d)
558+
559+ # Check amqp connection for all units, expect connections to succeed
560+ for unit in sentry_units:
561+ connection = u.connect_amqp_by_unit(unit, ssl=False, fatal=False)
562+ connection.close()
563+
564+ u.delete_rmq_test_user(sentry_units)
565+ u.log.info('OK\n')
566+
567+ def test_404_rmq_ssl_connect_with_ssl_off(self):
568+ """Verify unsuccessful ssl amqp connection to all units when
569+ charm config option for ssl is set False."""
570+ u.log.debug('Confirming that ssl connection fails when ssl '
571+ 'config is off...')
572+ sentry_units = self._get_rmq_sentry_units()
573+ u.add_rmq_test_user(sentry_units)
574+ u.configure_rmq_ssl_off(sentry_units, self.d)
575+
576+ # Check ssl amqp connection for all units, expect connections to fail
577+ for unit in sentry_units:
578+ connection = u.connect_amqp_by_unit(unit, ssl=True,
579+ port=5971, fatal=False)
580+ if connection:
581+ connection.close()
582+ msg = 'SSL connection unexpectedly succeeded with ssl=off'
583+ amulet.raise_status(amulet.FAIL, msg)
584+
585+ u.delete_rmq_test_user(sentry_units)
586+ u.log.info('OK - Confirmed that ssl connection attempt fails '
587+ 'when ssl config is off.')
588+
589+ def test_406_rmq_amqp_messages_all_units_ssl_off(self):
590+ """Send amqp messages to every rmq unit and check every rmq unit
591+ for messages. Standard amqp tcp port, no ssl."""
592+ u.log.debug('Checking amqp message publish/get on all units '
593+ '(ssl off)...')
594+
595+ sentry_units = self._get_rmq_sentry_units()
596+ self._test_rmq_amqp_messages_all_units(sentry_units, ssl=False)
597+ u.log.info('OK\n')
598+
599+ def test_408_rmq_amqp_messages_all_units_ssl_on(self):
600+ """Send amqp messages with ssl enabled, to every rmq unit and
601+ check every rmq unit for messages. Standard ssl tcp port."""
602+ u.log.debug('Checking amqp message publish/get on all units '
603+ '(ssl on)...')
604+
605+ sentry_units = self._get_rmq_sentry_units()
606+ self._test_rmq_amqp_messages_all_units(sentry_units,
607+ ssl=True, port=5671)
608+ u.log.info('OK\n')
609+
610+ def test_410_rmq_amqp_messages_all_units_ssl_alt_port(self):
611+ """Send amqp messages with ssl on, to every rmq unit and check
612+ every rmq unit for messages. Custom ssl tcp port."""
613+ u.log.debug('Checking amqp message publish/get on all units '
614+ '(ssl on)...')
615+
616+ sentry_units = self._get_rmq_sentry_units()
617+ self._test_rmq_amqp_messages_all_units(sentry_units,
618+ ssl=True, port=5999)
619+ u.log.info('OK\n')
620+
621+ def test_412_rmq_management_plugin(self):
622+ """Enable and check management plugin."""
623+ u.log.debug('Checking tcp socket connect to management plugin '
624+ 'port on all rmq units...')
625+
626+ sentry_units = self._get_rmq_sentry_units()
627+ mgmt_port = 15672
628+
629+ # Enable management plugin
630+ u.log.debug('Enabling management_plugin charm config option...')
631+ config = {'management_plugin': 'True'}
632+ self.d.configure('rabbitmq-server', config)
633+
634+ # Check tcp connect to management plugin port
635+ max_wait = 120
636+ tries = 0
637+ ret = u.port_knock_units(sentry_units, mgmt_port)
638+ while ret and tries < (max_wait / 12):
639+ time.sleep(12)
640+ u.log.debug('Attempt {}: {}'.format(tries, ret))
641+ ret = u.port_knock_units(sentry_units, mgmt_port)
642+ tries += 1
643+
644+ if ret:
645+ amulet.raise_status(amulet.FAIL, ret)
646+ else:
647+ u.log.debug('Connect to all units (OK)\n')
648+
649+ # Disable management plugin
650+ u.log.debug('Disabling management_plugin charm config option...')
651+ config = {'management_plugin': 'False'}
652+ self.d.configure('rabbitmq-server', config)
653+
654+ # Negative check - tcp connect to management plugin port
655+ u.log.info('Expect tcp connect fail since charm config '
656+ 'option is disabled.')
657+ tries = 0
658+ ret = u.port_knock_units(sentry_units, mgmt_port, expect_success=False)
659+ while ret and tries < (max_wait / 12):
660+ time.sleep(12)
661+ u.log.debug('Attempt {}: {}'.format(tries, ret))
662+ ret = u.port_knock_units(sentry_units, mgmt_port,
663+ expect_success=False)
664+ tries += 1
665+
666+ if ret:
667+ amulet.raise_status(amulet.FAIL, ret)
668+ else:
669+ u.log.info('Confirm mgmt port closed on all units (OK)\n')
670+
671+ def test_414_rmq_nrpe_monitors(self):
672+ """Check rabbimq-server nrpe monitor basic functionality."""
673+ sentry_units = self._get_rmq_sentry_units()
674+ host_names = u.get_unit_hostnames(sentry_units)
675+
676+ # check_rabbitmq monitor
677+ u.log.debug('Checking nrpe check_rabbitmq on units...')
678+ cmds = ['egrep -oh /usr/local.* /etc/nagios/nrpe.d/'
679+ 'check_rabbitmq.cfg']
680+ ret = u.check_commands_on_units(cmds, sentry_units)
681+ if ret:
682+ amulet.raise_status(amulet.FAIL, msg=ret)
683+
684+ u.log.debug('Sleeping 70s for 1m cron job to run...')
685+ time.sleep(70)
686+
687+ # check_rabbitmq_queue monitor
688+ u.log.debug('Checking nrpe check_rabbitmq_queue on units...')
689+ cmds = ['egrep -oh /usr/local.* /etc/nagios/nrpe.d/'
690+ 'check_rabbitmq_queue.cfg']
691+ ret = u.check_commands_on_units(cmds, sentry_units)
692+ if ret:
693+ amulet.raise_status(amulet.FAIL, msg=ret)
694+
695+ # check dat file existence
696+ u.log.debug('Checking nrpe dat file existence on units...')
697+ for sentry_unit in sentry_units:
698+ unit_name = sentry_unit.info['unit_name']
699+ unit_host_name = host_names[unit_name]
700+
701+ cmds = [
702+ 'stat /var/lib/rabbitmq/data/{}_general_stats.dat'.format(
703+ unit_host_name),
704+ 'stat /var/lib/rabbitmq/data/{}_queue_stats.dat'.format(
705+ unit_host_name)
706+ ]
707+
708+ ret = u.check_commands_on_units(cmds, [sentry_unit])
709+ if ret:
710+ amulet.raise_status(amulet.FAIL, msg=ret)
711+
712+ u.log.info('OK\n')
713
714=== added directory 'tests/charmhelpers'
715=== removed directory 'tests/charmhelpers'
716=== added file 'tests/charmhelpers/__init__.py'
717--- tests/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
718+++ tests/charmhelpers/__init__.py 2015-09-01 02:46:36 +0000
719@@ -0,0 +1,38 @@
720+# Copyright 2014-2015 Canonical Limited.
721+#
722+# This file is part of charm-helpers.
723+#
724+# charm-helpers is free software: you can redistribute it and/or modify
725+# it under the terms of the GNU Lesser General Public License version 3 as
726+# published by the Free Software Foundation.
727+#
728+# charm-helpers is distributed in the hope that it will be useful,
729+# but WITHOUT ANY WARRANTY; without even the implied warranty of
730+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
731+# GNU Lesser General Public License for more details.
732+#
733+# You should have received a copy of the GNU Lesser General Public License
734+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
735+
736+# Bootstrap charm-helpers, installing its dependencies if necessary using
737+# only standard libraries.
738+import subprocess
739+import sys
740+
741+try:
742+ import six # flake8: noqa
743+except ImportError:
744+ if sys.version_info.major == 2:
745+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
746+ else:
747+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
748+ import six # flake8: noqa
749+
750+try:
751+ import yaml # flake8: noqa
752+except ImportError:
753+ if sys.version_info.major == 2:
754+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
755+ else:
756+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
757+ import yaml # flake8: noqa
758
759=== removed file 'tests/charmhelpers/__init__.py'
760--- tests/charmhelpers/__init__.py 2015-04-13 22:11:34 +0000
761+++ tests/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
762@@ -1,38 +0,0 @@
763-# Copyright 2014-2015 Canonical Limited.
764-#
765-# This file is part of charm-helpers.
766-#
767-# charm-helpers is free software: you can redistribute it and/or modify
768-# it under the terms of the GNU Lesser General Public License version 3 as
769-# published by the Free Software Foundation.
770-#
771-# charm-helpers is distributed in the hope that it will be useful,
772-# but WITHOUT ANY WARRANTY; without even the implied warranty of
773-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
774-# GNU Lesser General Public License for more details.
775-#
776-# You should have received a copy of the GNU Lesser General Public License
777-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
778-
779-# Bootstrap charm-helpers, installing its dependencies if necessary using
780-# only standard libraries.
781-import subprocess
782-import sys
783-
784-try:
785- import six # flake8: noqa
786-except ImportError:
787- if sys.version_info.major == 2:
788- subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
789- else:
790- subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
791- import six # flake8: noqa
792-
793-try:
794- import yaml # flake8: noqa
795-except ImportError:
796- if sys.version_info.major == 2:
797- subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
798- else:
799- subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
800- import yaml # flake8: noqa
801
802=== removed directory 'tests/charmhelpers/cli'
803=== removed file 'tests/charmhelpers/cli/__init__.py'
804--- tests/charmhelpers/cli/__init__.py 2015-08-19 13:49:53 +0000
805+++ tests/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
806@@ -1,191 +0,0 @@
807-# Copyright 2014-2015 Canonical Limited.
808-#
809-# This file is part of charm-helpers.
810-#
811-# charm-helpers is free software: you can redistribute it and/or modify
812-# it under the terms of the GNU Lesser General Public License version 3 as
813-# published by the Free Software Foundation.
814-#
815-# charm-helpers is distributed in the hope that it will be useful,
816-# but WITHOUT ANY WARRANTY; without even the implied warranty of
817-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
818-# GNU Lesser General Public License for more details.
819-#
820-# You should have received a copy of the GNU Lesser General Public License
821-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
822-
823-import inspect
824-import argparse
825-import sys
826-
827-from six.moves import zip
828-
829-from charmhelpers.core import unitdata
830-
831-
832-class OutputFormatter(object):
833- def __init__(self, outfile=sys.stdout):
834- self.formats = (
835- "raw",
836- "json",
837- "py",
838- "yaml",
839- "csv",
840- "tab",
841- )
842- self.outfile = outfile
843-
844- def add_arguments(self, argument_parser):
845- formatgroup = argument_parser.add_mutually_exclusive_group()
846- choices = self.supported_formats
847- formatgroup.add_argument("--format", metavar='FMT',
848- help="Select output format for returned data, "
849- "where FMT is one of: {}".format(choices),
850- choices=choices, default='raw')
851- for fmt in self.formats:
852- fmtfunc = getattr(self, fmt)
853- formatgroup.add_argument("-{}".format(fmt[0]),
854- "--{}".format(fmt), action='store_const',
855- const=fmt, dest='format',
856- help=fmtfunc.__doc__)
857-
858- @property
859- def supported_formats(self):
860- return self.formats
861-
862- def raw(self, output):
863- """Output data as raw string (default)"""
864- if isinstance(output, (list, tuple)):
865- output = '\n'.join(map(str, output))
866- self.outfile.write(str(output))
867-
868- def py(self, output):
869- """Output data as a nicely-formatted python data structure"""
870- import pprint
871- pprint.pprint(output, stream=self.outfile)
872-
873- def json(self, output):
874- """Output data in JSON format"""
875- import json
876- json.dump(output, self.outfile)
877-
878- def yaml(self, output):
879- """Output data in YAML format"""
880- import yaml
881- yaml.safe_dump(output, self.outfile)
882-
883- def csv(self, output):
884- """Output data as excel-compatible CSV"""
885- import csv
886- csvwriter = csv.writer(self.outfile)
887- csvwriter.writerows(output)
888-
889- def tab(self, output):
890- """Output data in excel-compatible tab-delimited format"""
891- import csv
892- csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
893- csvwriter.writerows(output)
894-
895- def format_output(self, output, fmt='raw'):
896- fmtfunc = getattr(self, fmt)
897- fmtfunc(output)
898-
899-
900-class CommandLine(object):
901- argument_parser = None
902- subparsers = None
903- formatter = None
904- exit_code = 0
905-
906- def __init__(self):
907- if not self.argument_parser:
908- self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
909- if not self.formatter:
910- self.formatter = OutputFormatter()
911- self.formatter.add_arguments(self.argument_parser)
912- if not self.subparsers:
913- self.subparsers = self.argument_parser.add_subparsers(help='Commands')
914-
915- def subcommand(self, command_name=None):
916- """
917- Decorate a function as a subcommand. Use its arguments as the
918- command-line arguments"""
919- def wrapper(decorated):
920- cmd_name = command_name or decorated.__name__
921- subparser = self.subparsers.add_parser(cmd_name,
922- description=decorated.__doc__)
923- for args, kwargs in describe_arguments(decorated):
924- subparser.add_argument(*args, **kwargs)
925- subparser.set_defaults(func=decorated)
926- return decorated
927- return wrapper
928-
929- def test_command(self, decorated):
930- """
931- Subcommand is a boolean test function, so bool return values should be
932- converted to a 0/1 exit code.
933- """
934- decorated._cli_test_command = True
935- return decorated
936-
937- def no_output(self, decorated):
938- """
939- Subcommand is not expected to return a value, so don't print a spurious None.
940- """
941- decorated._cli_no_output = True
942- return decorated
943-
944- def subcommand_builder(self, command_name, description=None):
945- """
946- Decorate a function that builds a subcommand. Builders should accept a
947- single argument (the subparser instance) and return the function to be
948- run as the command."""
949- def wrapper(decorated):
950- subparser = self.subparsers.add_parser(command_name)
951- func = decorated(subparser)
952- subparser.set_defaults(func=func)
953- subparser.description = description or func.__doc__
954- return wrapper
955-
956- def run(self):
957- "Run cli, processing arguments and executing subcommands."
958- arguments = self.argument_parser.parse_args()
959- argspec = inspect.getargspec(arguments.func)
960- vargs = []
961- for arg in argspec.args:
962- vargs.append(getattr(arguments, arg))
963- if argspec.varargs:
964- vargs.extend(getattr(arguments, argspec.varargs))
965- output = arguments.func(*vargs)
966- if getattr(arguments.func, '_cli_test_command', False):
967- self.exit_code = 0 if output else 1
968- output = ''
969- if getattr(arguments.func, '_cli_no_output', False):
970- output = ''
971- self.formatter.format_output(output, arguments.format)
972- if unitdata._KV:
973- unitdata._KV.flush()
974-
975-
976-cmdline = CommandLine()
977-
978-
979-def describe_arguments(func):
980- """
981- Analyze a function's signature and return a data structure suitable for
982- passing in as arguments to an argparse parser's add_argument() method."""
983-
984- argspec = inspect.getargspec(func)
985- # we should probably raise an exception somewhere if func includes **kwargs
986- if argspec.defaults:
987- positional_args = argspec.args[:-len(argspec.defaults)]
988- keyword_names = argspec.args[-len(argspec.defaults):]
989- for arg, default in zip(keyword_names, argspec.defaults):
990- yield ('--{}'.format(arg),), {'default': default}
991- else:
992- positional_args = argspec.args
993-
994- for arg in positional_args:
995- yield (arg,), {}
996- if argspec.varargs:
997- yield (argspec.varargs,), {'nargs': '*'}
998
999=== removed file 'tests/charmhelpers/cli/benchmark.py'
1000--- tests/charmhelpers/cli/benchmark.py 2015-07-31 22:00:49 +0000
1001+++ tests/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
1002@@ -1,36 +0,0 @@
1003-# Copyright 2014-2015 Canonical Limited.
1004-#
1005-# This file is part of charm-helpers.
1006-#
1007-# charm-helpers is free software: you can redistribute it and/or modify
1008-# it under the terms of the GNU Lesser General Public License version 3 as
1009-# published by the Free Software Foundation.
1010-#
1011-# charm-helpers is distributed in the hope that it will be useful,
1012-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1013-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1014-# GNU Lesser General Public License for more details.
1015-#
1016-# You should have received a copy of the GNU Lesser General Public License
1017-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1018-
1019-from . import cmdline
1020-from charmhelpers.contrib.benchmark import Benchmark
1021-
1022-
1023-@cmdline.subcommand(command_name='benchmark-start')
1024-def start():
1025- Benchmark.start()
1026-
1027-
1028-@cmdline.subcommand(command_name='benchmark-finish')
1029-def finish():
1030- Benchmark.finish()
1031-
1032-
1033-@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
1034-def service(subparser):
1035- subparser.add_argument("value", help="The composite score.")
1036- subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
1037- subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
1038- return Benchmark.set_composite_score
1039
1040=== removed file 'tests/charmhelpers/cli/commands.py'
1041--- tests/charmhelpers/cli/commands.py 2015-08-19 13:49:53 +0000
1042+++ tests/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
1043@@ -1,32 +0,0 @@
1044-# Copyright 2014-2015 Canonical Limited.
1045-#
1046-# This file is part of charm-helpers.
1047-#
1048-# charm-helpers is free software: you can redistribute it and/or modify
1049-# it under the terms of the GNU Lesser General Public License version 3 as
1050-# published by the Free Software Foundation.
1051-#
1052-# charm-helpers is distributed in the hope that it will be useful,
1053-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1054-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1055-# GNU Lesser General Public License for more details.
1056-#
1057-# You should have received a copy of the GNU Lesser General Public License
1058-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1059-
1060-"""
1061-This module loads sub-modules into the python runtime so they can be
1062-discovered via the inspect module. In order to prevent flake8 from (rightfully)
1063-telling us these are unused modules, throw a ' # noqa' at the end of each import
1064-so that the warning is suppressed.
1065-"""
1066-
1067-from . import CommandLine # noqa
1068-
1069-"""
1070-Import the sub-modules which have decorated subcommands to register with chlp.
1071-"""
1072-from . import host # noqa
1073-from . import benchmark # noqa
1074-from . import unitdata # noqa
1075-from . import hookenv # noqa
1076
1077=== removed file 'tests/charmhelpers/cli/hookenv.py'
1078--- tests/charmhelpers/cli/hookenv.py 2015-08-19 13:49:53 +0000
1079+++ tests/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
1080@@ -1,23 +0,0 @@
1081-# Copyright 2014-2015 Canonical Limited.
1082-#
1083-# This file is part of charm-helpers.
1084-#
1085-# charm-helpers is free software: you can redistribute it and/or modify
1086-# it under the terms of the GNU Lesser General Public License version 3 as
1087-# published by the Free Software Foundation.
1088-#
1089-# charm-helpers is distributed in the hope that it will be useful,
1090-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1091-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1092-# GNU Lesser General Public License for more details.
1093-#
1094-# You should have received a copy of the GNU Lesser General Public License
1095-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1096-
1097-from . import cmdline
1098-from charmhelpers.core import hookenv
1099-
1100-
1101-cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
1102-cmdline.subcommand('service-name')(hookenv.service_name)
1103-cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
1104
1105=== removed file 'tests/charmhelpers/cli/host.py'
1106--- tests/charmhelpers/cli/host.py 2015-07-31 22:00:49 +0000
1107+++ tests/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
1108@@ -1,31 +0,0 @@
1109-# Copyright 2014-2015 Canonical Limited.
1110-#
1111-# This file is part of charm-helpers.
1112-#
1113-# charm-helpers is free software: you can redistribute it and/or modify
1114-# it under the terms of the GNU Lesser General Public License version 3 as
1115-# published by the Free Software Foundation.
1116-#
1117-# charm-helpers is distributed in the hope that it will be useful,
1118-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1119-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1120-# GNU Lesser General Public License for more details.
1121-#
1122-# You should have received a copy of the GNU Lesser General Public License
1123-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1124-
1125-from . import cmdline
1126-from charmhelpers.core import host
1127-
1128-
1129-@cmdline.subcommand()
1130-def mounts():
1131- "List mounts"
1132- return host.mounts()
1133-
1134-
1135-@cmdline.subcommand_builder('service', description="Control system services")
1136-def service(subparser):
1137- subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
1138- subparser.add_argument("service_name", help="Name of the service to control")
1139- return host.service
1140
1141=== removed file 'tests/charmhelpers/cli/unitdata.py'
1142--- tests/charmhelpers/cli/unitdata.py 2015-07-31 22:00:49 +0000
1143+++ tests/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
1144@@ -1,39 +0,0 @@
1145-# Copyright 2014-2015 Canonical Limited.
1146-#
1147-# This file is part of charm-helpers.
1148-#
1149-# charm-helpers is free software: you can redistribute it and/or modify
1150-# it under the terms of the GNU Lesser General Public License version 3 as
1151-# published by the Free Software Foundation.
1152-#
1153-# charm-helpers is distributed in the hope that it will be useful,
1154-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1155-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1156-# GNU Lesser General Public License for more details.
1157-#
1158-# You should have received a copy of the GNU Lesser General Public License
1159-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1160-
1161-from . import cmdline
1162-from charmhelpers.core import unitdata
1163-
1164-
1165-@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
1166-def unitdata_cmd(subparser):
1167- nested = subparser.add_subparsers()
1168- get_cmd = nested.add_parser('get', help='Retrieve data')
1169- get_cmd.add_argument('key', help='Key to retrieve the value of')
1170- get_cmd.set_defaults(action='get', value=None)
1171- set_cmd = nested.add_parser('set', help='Store data')
1172- set_cmd.add_argument('key', help='Key to set')
1173- set_cmd.add_argument('value', help='Value to store')
1174- set_cmd.set_defaults(action='set')
1175-
1176- def _unitdata_cmd(action, key, value):
1177- if action == 'get':
1178- return unitdata.kv().get(key)
1179- elif action == 'set':
1180- unitdata.kv().set(key, value)
1181- unitdata.kv().flush()
1182- return ''
1183- return _unitdata_cmd
1184
1185=== added directory 'tests/charmhelpers/contrib'
1186=== removed directory 'tests/charmhelpers/contrib'
1187=== added file 'tests/charmhelpers/contrib/__init__.py'
1188--- tests/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
1189+++ tests/charmhelpers/contrib/__init__.py 2015-09-01 02:46:36 +0000
1190@@ -0,0 +1,15 @@
1191+# Copyright 2014-2015 Canonical Limited.
1192+#
1193+# This file is part of charm-helpers.
1194+#
1195+# charm-helpers is free software: you can redistribute it and/or modify
1196+# it under the terms of the GNU Lesser General Public License version 3 as
1197+# published by the Free Software Foundation.
1198+#
1199+# charm-helpers is distributed in the hope that it will be useful,
1200+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1201+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1202+# GNU Lesser General Public License for more details.
1203+#
1204+# You should have received a copy of the GNU Lesser General Public License
1205+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1206
1207=== removed file 'tests/charmhelpers/contrib/__init__.py'
1208--- tests/charmhelpers/contrib/__init__.py 2015-04-13 22:11:34 +0000
1209+++ tests/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
1210@@ -1,15 +0,0 @@
1211-# Copyright 2014-2015 Canonical Limited.
1212-#
1213-# This file is part of charm-helpers.
1214-#
1215-# charm-helpers is free software: you can redistribute it and/or modify
1216-# it under the terms of the GNU Lesser General Public License version 3 as
1217-# published by the Free Software Foundation.
1218-#
1219-# charm-helpers is distributed in the hope that it will be useful,
1220-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1221-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1222-# GNU Lesser General Public License for more details.
1223-#
1224-# You should have received a copy of the GNU Lesser General Public License
1225-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1226
1227=== added directory 'tests/charmhelpers/contrib/amulet'
1228=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
1229--- tests/charmhelpers/contrib/amulet/__init__.py 1970-01-01 00:00:00 +0000
1230+++ tests/charmhelpers/contrib/amulet/__init__.py 2015-09-01 02:46:36 +0000
1231@@ -0,0 +1,15 @@
1232+# Copyright 2014-2015 Canonical Limited.
1233+#
1234+# This file is part of charm-helpers.
1235+#
1236+# charm-helpers is free software: you can redistribute it and/or modify
1237+# it under the terms of the GNU Lesser General Public License version 3 as
1238+# published by the Free Software Foundation.
1239+#
1240+# charm-helpers is distributed in the hope that it will be useful,
1241+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1242+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1243+# GNU Lesser General Public License for more details.
1244+#
1245+# You should have received a copy of the GNU Lesser General Public License
1246+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1247
1248=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
1249--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
1250+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-09-01 02:46:36 +0000
1251@@ -0,0 +1,93 @@
1252+# Copyright 2014-2015 Canonical Limited.
1253+#
1254+# This file is part of charm-helpers.
1255+#
1256+# charm-helpers is free software: you can redistribute it and/or modify
1257+# it under the terms of the GNU Lesser General Public License version 3 as
1258+# published by the Free Software Foundation.
1259+#
1260+# charm-helpers is distributed in the hope that it will be useful,
1261+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1262+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1263+# GNU Lesser General Public License for more details.
1264+#
1265+# You should have received a copy of the GNU Lesser General Public License
1266+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1267+
1268+import amulet
1269+import os
1270+import six
1271+
1272+
1273+class AmuletDeployment(object):
1274+ """Amulet deployment.
1275+
1276+ This class provides generic Amulet deployment and test runner
1277+ methods.
1278+ """
1279+
1280+ def __init__(self, series=None):
1281+ """Initialize the deployment environment."""
1282+ self.series = None
1283+
1284+ if series:
1285+ self.series = series
1286+ self.d = amulet.Deployment(series=self.series)
1287+ else:
1288+ self.d = amulet.Deployment()
1289+
1290+ def _add_services(self, this_service, other_services):
1291+ """Add services.
1292+
1293+ Add services to the deployment where this_service is the local charm
1294+ that we're testing and other_services are the other services that
1295+ are being used in the local amulet tests.
1296+ """
1297+ if this_service['name'] != os.path.basename(os.getcwd()):
1298+ s = this_service['name']
1299+ msg = "The charm's root directory name needs to be {}".format(s)
1300+ amulet.raise_status(amulet.FAIL, msg=msg)
1301+
1302+ if 'units' not in this_service:
1303+ this_service['units'] = 1
1304+
1305+ self.d.add(this_service['name'], units=this_service['units'])
1306+
1307+ for svc in other_services:
1308+ if 'location' in svc:
1309+ branch_location = svc['location']
1310+ elif self.series:
1311+ branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
1312+ else:
1313+ branch_location = None
1314+
1315+ if 'units' not in svc:
1316+ svc['units'] = 1
1317+
1318+ self.d.add(svc['name'], charm=branch_location, units=svc['units'])
1319+
1320+ def _add_relations(self, relations):
1321+ """Add all of the relations for the services."""
1322+ for k, v in six.iteritems(relations):
1323+ self.d.relate(k, v)
1324+
1325+ def _configure_services(self, configs):
1326+ """Configure all of the services."""
1327+ for service, config in six.iteritems(configs):
1328+ self.d.configure(service, config)
1329+
1330+ def _deploy(self):
1331+ """Deploy environment and wait for all hooks to finish executing."""
1332+ try:
1333+ self.d.setup(timeout=900)
1334+ self.d.sentry.wait(timeout=900)
1335+ except amulet.helpers.TimeoutError:
1336+ amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
1337+ except Exception:
1338+ raise
1339+
1340+ def run_tests(self):
1341+ """Run all of the methods that are prefixed with 'test_'."""
1342+ for test in dir(self):
1343+ if test.startswith('test_'):
1344+ getattr(self, test)()
1345
1346=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
1347--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
1348+++ tests/charmhelpers/contrib/amulet/utils.py 2015-09-01 02:46:36 +0000
1349@@ -0,0 +1,771 @@
1350+# Copyright 2014-2015 Canonical Limited.
1351+#
1352+# This file is part of charm-helpers.
1353+#
1354+# charm-helpers is free software: you can redistribute it and/or modify
1355+# it under the terms of the GNU Lesser General Public License version 3 as
1356+# published by the Free Software Foundation.
1357+#
1358+# charm-helpers is distributed in the hope that it will be useful,
1359+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1360+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1361+# GNU Lesser General Public License for more details.
1362+#
1363+# You should have received a copy of the GNU Lesser General Public License
1364+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1365+
1366+import io
1367+import json
1368+import logging
1369+import os
1370+import re
1371+import socket
1372+import subprocess
1373+import sys
1374+import time
1375+import uuid
1376+
1377+import amulet
1378+import distro_info
1379+import six
1380+from six.moves import configparser
1381+if six.PY3:
1382+ from urllib import parse as urlparse
1383+else:
1384+ import urlparse
1385+
1386+
1387+class AmuletUtils(object):
1388+ """Amulet utilities.
1389+
1390+ This class provides common utility functions that are used by Amulet
1391+ tests.
1392+ """
1393+
1394+ def __init__(self, log_level=logging.ERROR):
1395+ self.log = self.get_logger(level=log_level)
1396+ self.ubuntu_releases = self.get_ubuntu_releases()
1397+
1398+ def get_logger(self, name="amulet-logger", level=logging.DEBUG):
1399+ """Get a logger object that will log to stdout."""
1400+ log = logging
1401+ logger = log.getLogger(name)
1402+ fmt = log.Formatter("%(asctime)s %(funcName)s "
1403+ "%(levelname)s: %(message)s")
1404+
1405+ handler = log.StreamHandler(stream=sys.stdout)
1406+ handler.setLevel(level)
1407+ handler.setFormatter(fmt)
1408+
1409+ logger.addHandler(handler)
1410+ logger.setLevel(level)
1411+
1412+ return logger
1413+
1414+ def valid_ip(self, ip):
1415+ if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
1416+ return True
1417+ else:
1418+ return False
1419+
1420+ def valid_url(self, url):
1421+ p = re.compile(
1422+ r'^(?:http|ftp)s?://'
1423+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
1424+ r'localhost|'
1425+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
1426+ r'(?::\d+)?'
1427+ r'(?:/?|[/?]\S+)$',
1428+ re.IGNORECASE)
1429+ if p.match(url):
1430+ return True
1431+ else:
1432+ return False
1433+
1434+ def get_ubuntu_release_from_sentry(self, sentry_unit):
1435+ """Get Ubuntu release codename from sentry unit.
1436+
1437+ :param sentry_unit: amulet sentry/service unit pointer
1438+ :returns: list of strings - release codename, failure message
1439+ """
1440+ msg = None
1441+ cmd = 'lsb_release -cs'
1442+ release, code = sentry_unit.run(cmd)
1443+ if code == 0:
1444+ self.log.debug('{} lsb_release: {}'.format(
1445+ sentry_unit.info['unit_name'], release))
1446+ else:
1447+ msg = ('{} `{}` returned {} '
1448+ '{}'.format(sentry_unit.info['unit_name'],
1449+ cmd, release, code))
1450+ if release not in self.ubuntu_releases:
1451+ msg = ("Release ({}) not found in Ubuntu releases "
1452+ "({})".format(release, self.ubuntu_releases))
1453+ return release, msg
1454+
1455+ def validate_services(self, commands):
1456+ """Validate that lists of commands succeed on service units. Can be
1457+ used to verify system services are running on the corresponding
1458+ service units.
1459+
1460+ :param commands: dict with sentry keys and arbitrary command list vals
1461+ :returns: None if successful, Failure string message otherwise
1462+ """
1463+ self.log.debug('Checking status of system services...')
1464+
1465+ # /!\ DEPRECATION WARNING (beisner):
1466+ # New and existing tests should be rewritten to use
1467+ # validate_services_by_name() as it is aware of init systems.
1468+ self.log.warn('/!\\ DEPRECATION WARNING: use '
1469+ 'validate_services_by_name instead of validate_services '
1470+ 'due to init system differences.')
1471+
1472+ for k, v in six.iteritems(commands):
1473+ for cmd in v:
1474+ output, code = k.run(cmd)
1475+ self.log.debug('{} `{}` returned '
1476+ '{}'.format(k.info['unit_name'],
1477+ cmd, code))
1478+ if code != 0:
1479+ return "command `{}` returned {}".format(cmd, str(code))
1480+ return None
1481+
1482+ def validate_services_by_name(self, sentry_services):
1483+ """Validate system service status by service name, automatically
1484+ detecting init system based on Ubuntu release codename.
1485+
1486+ :param sentry_services: dict with sentry keys and svc list values
1487+ :returns: None if successful, Failure string message otherwise
1488+ """
1489+ self.log.debug('Checking status of system services...')
1490+
1491+ # Point at which systemd became a thing
1492+ systemd_switch = self.ubuntu_releases.index('vivid')
1493+
1494+ for sentry_unit, services_list in six.iteritems(sentry_services):
1495+ # Get lsb_release codename from unit
1496+ release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
1497+ if ret:
1498+ return ret
1499+
1500+ for service_name in services_list:
1501+ if (self.ubuntu_releases.index(release) >= systemd_switch or
1502+ service_name in ['rabbitmq-server', 'apache2']):
1503+ # init is systemd (or regular sysv)
1504+ cmd = 'sudo service {} status'.format(service_name)
1505+ output, code = sentry_unit.run(cmd)
1506+ service_running = code == 0
1507+ elif self.ubuntu_releases.index(release) < systemd_switch:
1508+ # init is upstart
1509+ cmd = 'sudo status {}'.format(service_name)
1510+ output, code = sentry_unit.run(cmd)
1511+ service_running = code == 0 and "start/running" in output
1512+
1513+ self.log.debug('{} `{}` returned '
1514+ '{}'.format(sentry_unit.info['unit_name'],
1515+ cmd, code))
1516+ if not service_running:
1517+ return u"command `{}` returned {} {}".format(
1518+ cmd, output, str(code))
1519+ return None
1520+
1521+ def _get_config(self, unit, filename):
1522+ """Get a ConfigParser object for parsing a unit's config file."""
1523+ file_contents = unit.file_contents(filename)
1524+
1525+ # NOTE(beisner): by default, ConfigParser does not handle options
1526+ # with no value, such as the flags used in the mysql my.cnf file.
1527+ # https://bugs.python.org/issue7005
1528+ config = configparser.ConfigParser(allow_no_value=True)
1529+ config.readfp(io.StringIO(file_contents))
1530+ return config
1531+
1532+ def validate_config_data(self, sentry_unit, config_file, section,
1533+ expected):
1534+ """Validate config file data.
1535+
1536+ Verify that the specified section of the config file contains
1537+ the expected option key:value pairs.
1538+
1539+ Compare expected dictionary data vs actual dictionary data.
1540+ The values in the 'expected' dictionary can be strings, bools, ints,
1541+ longs, or can be a function that evaluates a variable and returns a
1542+ bool.
1543+ """
1544+ self.log.debug('Validating config file data ({} in {} on {})'
1545+ '...'.format(section, config_file,
1546+ sentry_unit.info['unit_name']))
1547+ config = self._get_config(sentry_unit, config_file)
1548+
1549+ if section != 'DEFAULT' and not config.has_section(section):
1550+ return "section [{}] does not exist".format(section)
1551+
1552+ for k in expected.keys():
1553+ if not config.has_option(section, k):
1554+ return "section [{}] is missing option {}".format(section, k)
1555+
1556+ actual = config.get(section, k)
1557+ v = expected[k]
1558+ if (isinstance(v, six.string_types) or
1559+ isinstance(v, bool) or
1560+ isinstance(v, six.integer_types)):
1561+ # handle explicit values
1562+ if actual != v:
1563+ return "section [{}] {}:{} != expected {}:{}".format(
1564+ section, k, actual, k, expected[k])
1565+ # handle function pointers, such as not_null or valid_ip
1566+ elif not v(actual):
1567+ return "section [{}] {}:{} != expected {}:{}".format(
1568+ section, k, actual, k, expected[k])
1569+ return None
1570+
1571+ def _validate_dict_data(self, expected, actual):
1572+ """Validate dictionary data.
1573+
1574+ Compare expected dictionary data vs actual dictionary data.
1575+ The values in the 'expected' dictionary can be strings, bools, ints,
1576+ longs, or can be a function that evaluates a variable and returns a
1577+ bool.
1578+ """
1579+ self.log.debug('actual: {}'.format(repr(actual)))
1580+ self.log.debug('expected: {}'.format(repr(expected)))
1581+
1582+ for k, v in six.iteritems(expected):
1583+ if k in actual:
1584+ if (isinstance(v, six.string_types) or
1585+ isinstance(v, bool) or
1586+ isinstance(v, six.integer_types)):
1587+ # handle explicit values
1588+ if v != actual[k]:
1589+ return "{}:{}".format(k, actual[k])
1590+ # handle function pointers, such as not_null or valid_ip
1591+ elif not v(actual[k]):
1592+ return "{}:{}".format(k, actual[k])
1593+ else:
1594+ return "key '{}' does not exist".format(k)
1595+ return None
1596+
1597+ def validate_relation_data(self, sentry_unit, relation, expected):
1598+ """Validate actual relation data based on expected relation data."""
1599+ actual = sentry_unit.relation(relation[0], relation[1])
1600+ return self._validate_dict_data(expected, actual)
1601+
1602+ def _validate_list_data(self, expected, actual):
1603+ """Compare expected list vs actual list data."""
1604+ for e in expected:
1605+ if e not in actual:
1606+ return "expected item {} not found in actual list".format(e)
1607+ return None
1608+
1609+ def not_null(self, string):
1610+ if string is not None:
1611+ return True
1612+ else:
1613+ return False
1614+
1615+ def _get_file_mtime(self, sentry_unit, filename):
1616+ """Get last modification time of file."""
1617+ return sentry_unit.file_stat(filename)['mtime']
1618+
1619+ def _get_dir_mtime(self, sentry_unit, directory):
1620+ """Get last modification time of directory."""
1621+ return sentry_unit.directory_stat(directory)['mtime']
1622+
1623+ def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
1624+ """Get process' start time.
1625+
1626+ Determine start time of the process based on the last modification
1627+ time of the /proc/pid directory. If pgrep_full is True, the process
1628+ name is matched against the full command line.
1629+ """
1630+ if pgrep_full:
1631+ cmd = 'pgrep -o -f {}'.format(service)
1632+ else:
1633+ cmd = 'pgrep -o {}'.format(service)
1634+ cmd = cmd + ' | grep -v pgrep || exit 0'
1635+ cmd_out = sentry_unit.run(cmd)
1636+ self.log.debug('CMDout: ' + str(cmd_out))
1637+ if cmd_out[0]:
1638+ self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
1639+ proc_dir = '/proc/{}'.format(cmd_out[0].strip())
1640+ return self._get_dir_mtime(sentry_unit, proc_dir)
1641+
1642+ def service_restarted(self, sentry_unit, service, filename,
1643+ pgrep_full=None, sleep_time=20):
1644+ """Check if service was restarted.
1645+
1646+ Compare a service's start time vs a file's last modification time
1647+ (such as a config file for that service) to determine if the service
1648+ has been restarted.
1649+ """
1650+ # /!\ DEPRECATION WARNING (beisner):
1651+ # This is prone to races in that no before-time is known.
1652+ # Use validate_service_config_changed instead.
1653+
1654+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1655+ # used instead of pgrep. pgrep_full is still passed through to ensure
1656+ # deprecation WARNS. lp1474030
1657+
1658+ self.log.warn('/!\\ DEPRECATION WARNING: use '
1659+ 'validate_service_config_changed instead of '
1660+ 'service_restarted due to known races.')
1661+
1662+ time.sleep(sleep_time)
1663+ if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
1664+ self._get_file_mtime(sentry_unit, filename)):
1665+ return True
1666+ else:
1667+ return False
1668+
1669+ def service_restarted_since(self, sentry_unit, mtime, service,
1670+ pgrep_full=None, sleep_time=20,
1671+ retry_count=2, retry_sleep_time=30):
1672+ """Check if service was been started after a given time.
1673+
1674+ Args:
1675+ sentry_unit (sentry): The sentry unit to check for the service on
1676+ mtime (float): The epoch time to check against
1677+ service (string): service name to look for in process table
1678+ pgrep_full: No longer implemented, passed for WARNs
1679+ sleep_time (int): Seconds to sleep before looking for process
1680+ retry_count (int): If service is not found, how many times to retry
1681+
1682+ Returns:
1683+ bool: True if service found and its start time it newer than mtime,
1684+ False if service is older than mtime or if service was
1685+ not found.
1686+ """
1687+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1688+ # used instead of pgrep. pgrep_full is still passed through to ensure
1689+ # deprecation WARNS. lp1474030
1690+
1691+ unit_name = sentry_unit.info['unit_name']
1692+ self.log.debug('Checking that %s service restarted since %s on '
1693+ '%s' % (service, mtime, unit_name))
1694+ time.sleep(sleep_time)
1695+ proc_start_time = None
1696+ tries = 0
1697+ while tries <= retry_count and not proc_start_time:
1698+ try:
1699+ proc_start_time = self._get_proc_start_time(sentry_unit,
1700+ service,
1701+ pgrep_full)
1702+ self.log.debug('Attempt {} to get {} proc start time on {} '
1703+ 'OK'.format(tries, service, unit_name))
1704+ except IOError:
1705+ # NOTE(beisner) - race avoidance, proc may not exist yet.
1706+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1707+ self.log.debug('Attempt {} to get {} proc start time on {} '
1708+ 'failed'.format(tries, service, unit_name))
1709+ time.sleep(retry_sleep_time)
1710+ tries += 1
1711+
1712+ if not proc_start_time:
1713+ self.log.warn('No proc start time found, assuming service did '
1714+ 'not start')
1715+ return False
1716+ if proc_start_time >= mtime:
1717+ self.log.debug('Proc start time is newer than provided mtime'
1718+ '(%s >= %s) on %s (OK)' % (proc_start_time,
1719+ mtime, unit_name))
1720+ return True
1721+ else:
1722+ self.log.warn('Proc start time (%s) is older than provided mtime '
1723+ '(%s) on %s, service did not '
1724+ 'restart' % (proc_start_time, mtime, unit_name))
1725+ return False
1726+
1727+ def config_updated_since(self, sentry_unit, filename, mtime,
1728+ sleep_time=20):
1729+ """Check if file was modified after a given time.
1730+
1731+ Args:
1732+ sentry_unit (sentry): The sentry unit to check the file mtime on
1733+ filename (string): The file to check mtime of
1734+ mtime (float): The epoch time to check against
1735+ sleep_time (int): Seconds to sleep before looking for process
1736+
1737+ Returns:
1738+ bool: True if file was modified more recently than mtime, False if
1739+ file was modified before mtime,
1740+ """
1741+ self.log.debug('Checking %s updated since %s' % (filename, mtime))
1742+ time.sleep(sleep_time)
1743+ file_mtime = self._get_file_mtime(sentry_unit, filename)
1744+ if file_mtime >= mtime:
1745+ self.log.debug('File mtime is newer than provided mtime '
1746+ '(%s >= %s)' % (file_mtime, mtime))
1747+ return True
1748+ else:
1749+ self.log.warn('File mtime %s is older than provided mtime %s'
1750+ % (file_mtime, mtime))
1751+ return False
1752+
1753+ def validate_service_config_changed(self, sentry_unit, mtime, service,
1754+ filename, pgrep_full=None,
1755+ sleep_time=20, retry_count=2,
1756+ retry_sleep_time=30):
1757+ """Check service and file were updated after mtime
1758+
1759+ Args:
1760+ sentry_unit (sentry): The sentry unit to check for the service on
1761+ mtime (float): The epoch time to check against
1762+ service (string): service name to look for in process table
1763+ filename (string): The file to check mtime of
1764+ pgrep_full: No longer implemented, passed for WARNs
1765+ sleep_time (int): Initial sleep in seconds to pass to test helpers
1766+ retry_count (int): If service is not found, how many times to retry
1767+ retry_sleep_time (int): Time in seconds to wait between retries
1768+
1769+ Typical Usage:
1770+ u = OpenStackAmuletUtils(ERROR)
1771+ ...
1772+ mtime = u.get_sentry_time(self.cinder_sentry)
1773+ self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
1774+ if not u.validate_service_config_changed(self.cinder_sentry,
1775+ mtime,
1776+ 'cinder-api',
1777+ '/etc/cinder/cinder.conf')
1778+ amulet.raise_status(amulet.FAIL, msg='update failed')
1779+ Returns:
1780+ bool: True if both service and file where updated/restarted after
1781+ mtime, False if service is older than mtime or if service was
1782+ not found or if filename was modified before mtime.
1783+ """
1784+
1785+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1786+ # used instead of pgrep. pgrep_full is still passed through to ensure
1787+ # deprecation WARNS. lp1474030
1788+
1789+ service_restart = self.service_restarted_since(
1790+ sentry_unit, mtime,
1791+ service,
1792+ pgrep_full=pgrep_full,
1793+ sleep_time=sleep_time,
1794+ retry_count=retry_count,
1795+ retry_sleep_time=retry_sleep_time)
1796+
1797+ config_update = self.config_updated_since(
1798+ sentry_unit,
1799+ filename,
1800+ mtime,
1801+ sleep_time=0)
1802+
1803+ return service_restart and config_update
1804+
1805+ def get_sentry_time(self, sentry_unit):
1806+ """Return current epoch time on a sentry"""
1807+ cmd = "date +'%s'"
1808+ return float(sentry_unit.run(cmd)[0])
1809+
1810+ def relation_error(self, name, data):
1811+ return 'unexpected relation data in {} - {}'.format(name, data)
1812+
1813+ def endpoint_error(self, name, data):
1814+ return 'unexpected endpoint data in {} - {}'.format(name, data)
1815+
1816+ def get_ubuntu_releases(self):
1817+ """Return a list of all Ubuntu releases in order of release."""
1818+ _d = distro_info.UbuntuDistroInfo()
1819+ _release_list = _d.all
1820+ return _release_list
1821+
1822+ def file_to_url(self, file_rel_path):
1823+ """Convert a relative file path to a file URL."""
1824+ _abs_path = os.path.abspath(file_rel_path)
1825+ return urlparse.urlparse(_abs_path, scheme='file').geturl()
1826+
1827+ def check_commands_on_units(self, commands, sentry_units):
1828+ """Check that all commands in a list exit zero on all
1829+ sentry units in a list.
1830+
1831+ :param commands: list of bash commands
1832+ :param sentry_units: list of sentry unit pointers
1833+ :returns: None if successful; Failure message otherwise
1834+ """
1835+ self.log.debug('Checking exit codes for {} commands on {} '
1836+ 'sentry units...'.format(len(commands),
1837+ len(sentry_units)))
1838+ for sentry_unit in sentry_units:
1839+ for cmd in commands:
1840+ output, code = sentry_unit.run(cmd)
1841+ if code == 0:
1842+ self.log.debug('{} `{}` returned {} '
1843+ '(OK)'.format(sentry_unit.info['unit_name'],
1844+ cmd, code))
1845+ else:
1846+ return ('{} `{}` returned {} '
1847+ '{}'.format(sentry_unit.info['unit_name'],
1848+ cmd, code, output))
1849+ return None
1850+
1851+ def get_process_id_list(self, sentry_unit, process_name,
1852+ expect_success=True):
1853+ """Get a list of process ID(s) from a single sentry juju unit
1854+ for a single process name.
1855+
1856+ :param sentry_unit: Amulet sentry instance (juju unit)
1857+ :param process_name: Process name
1858+ :param expect_success: If False, expect the PID to be missing,
1859+ raise if it is present.
1860+ :returns: List of process IDs
1861+ """
1862+ cmd = 'pidof -x {}'.format(process_name)
1863+ if not expect_success:
1864+ cmd += " || exit 0 && exit 1"
1865+ output, code = sentry_unit.run(cmd)
1866+ if code != 0:
1867+ msg = ('{} `{}` returned {} '
1868+ '{}'.format(sentry_unit.info['unit_name'],
1869+ cmd, code, output))
1870+ amulet.raise_status(amulet.FAIL, msg=msg)
1871+ return str(output).split()
1872+
1873+ def get_unit_process_ids(self, unit_processes, expect_success=True):
1874+ """Construct a dict containing unit sentries, process names, and
1875+ process IDs.
1876+
1877+ :param unit_processes: A dictionary of Amulet sentry instance
1878+ to list of process names.
1879+ :param expect_success: if False expect the processes to not be
1880+ running, raise if they are.
1881+ :returns: Dictionary of Amulet sentry instance to dictionary
1882+ of process names to PIDs.
1883+ """
1884+ pid_dict = {}
1885+ for sentry_unit, process_list in six.iteritems(unit_processes):
1886+ pid_dict[sentry_unit] = {}
1887+ for process in process_list:
1888+ pids = self.get_process_id_list(
1889+ sentry_unit, process, expect_success=expect_success)
1890+ pid_dict[sentry_unit].update({process: pids})
1891+ return pid_dict
1892+
1893+ def validate_unit_process_ids(self, expected, actual):
1894+ """Validate process id quantities for services on units."""
1895+ self.log.debug('Checking units for running processes...')
1896+ self.log.debug('Expected PIDs: {}'.format(expected))
1897+ self.log.debug('Actual PIDs: {}'.format(actual))
1898+
1899+ if len(actual) != len(expected):
1900+ return ('Unit count mismatch. expected, actual: {}, '
1901+ '{} '.format(len(expected), len(actual)))
1902+
1903+ for (e_sentry, e_proc_names) in six.iteritems(expected):
1904+ e_sentry_name = e_sentry.info['unit_name']
1905+ if e_sentry in actual.keys():
1906+ a_proc_names = actual[e_sentry]
1907+ else:
1908+ return ('Expected sentry ({}) not found in actual dict data.'
1909+ '{}'.format(e_sentry_name, e_sentry))
1910+
1911+ if len(e_proc_names.keys()) != len(a_proc_names.keys()):
1912+ return ('Process name count mismatch. expected, actual: {}, '
1913+ '{}'.format(len(expected), len(actual)))
1914+
1915+ for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
1916+ zip(e_proc_names.items(), a_proc_names.items()):
1917+ if e_proc_name != a_proc_name:
1918+ return ('Process name mismatch. expected, actual: {}, '
1919+ '{}'.format(e_proc_name, a_proc_name))
1920+
1921+ a_pids_length = len(a_pids)
1922+ fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
1923+ '{}, {} ({})'.format(e_sentry_name, e_proc_name,
1924+ e_pids_length, a_pids_length,
1925+ a_pids))
1926+
1927+ # If expected is not bool, ensure PID quantities match
1928+ if not isinstance(e_pids_length, bool) and \
1929+ a_pids_length != e_pids_length:
1930+ return fail_msg
1931+ # If expected is bool True, ensure 1 or more PIDs exist
1932+ elif isinstance(e_pids_length, bool) and \
1933+ e_pids_length is True and a_pids_length < 1:
1934+ return fail_msg
1935+ # If expected is bool False, ensure 0 PIDs exist
1936+ elif isinstance(e_pids_length, bool) and \
1937+ e_pids_length is False and a_pids_length != 0:
1938+ return fail_msg
1939+ else:
1940+ self.log.debug('PID check OK: {} {} {}: '
1941+ '{}'.format(e_sentry_name, e_proc_name,
1942+ e_pids_length, a_pids))
1943+ return None
1944+
1945+ def validate_list_of_identical_dicts(self, list_of_dicts):
1946+ """Check that all dicts within a list are identical."""
1947+ hashes = []
1948+ for _dict in list_of_dicts:
1949+ hashes.append(hash(frozenset(_dict.items())))
1950+
1951+ self.log.debug('Hashes: {}'.format(hashes))
1952+ if len(set(hashes)) == 1:
1953+ self.log.debug('Dicts within list are identical')
1954+ else:
1955+ return 'Dicts within list are not identical'
1956+
1957+ return None
1958+
1959+ def validate_sectionless_conf(self, file_contents, expected):
1960+ """A crude conf parser. Useful to inspect configuration files which
1961+ do not have section headers (as would be necessary in order to use
1962+ the configparser). Such as openstack-dashboard or rabbitmq confs."""
1963+ for line in file_contents.split('\n'):
1964+ if '=' in line:
1965+ args = line.split('=')
1966+ if len(args) <= 1:
1967+ continue
1968+ key = args[0].strip()
1969+ value = args[1].strip()
1970+ if key in expected.keys():
1971+ if expected[key] != value:
1972+ msg = ('Config mismatch. Expected, actual: {}, '
1973+ '{}'.format(expected[key], value))
1974+ amulet.raise_status(amulet.FAIL, msg=msg)
1975+
1976+ def get_unit_hostnames(self, units):
1977+ """Return a dict of juju unit names to hostnames."""
1978+ host_names = {}
1979+ for unit in units:
1980+ host_names[unit.info['unit_name']] = \
1981+ str(unit.file_contents('/etc/hostname').strip())
1982+ self.log.debug('Unit host names: {}'.format(host_names))
1983+ return host_names
1984+
1985+ def run_cmd_unit(self, sentry_unit, cmd):
1986+ """Run a command on a unit, return the output and exit code."""
1987+ output, code = sentry_unit.run(cmd)
1988+ if code == 0:
1989+ self.log.debug('{} `{}` command returned {} '
1990+ '(OK)'.format(sentry_unit.info['unit_name'],
1991+ cmd, code))
1992+ else:
1993+ msg = ('{} `{}` command returned {} '
1994+ '{}'.format(sentry_unit.info['unit_name'],
1995+ cmd, code, output))
1996+ amulet.raise_status(amulet.FAIL, msg=msg)
1997+ return str(output), code
1998+
1999+ def file_exists_on_unit(self, sentry_unit, file_name):
2000+ """Check if a file exists on a unit."""
2001+ try:
2002+ sentry_unit.file_stat(file_name)
2003+ return True
2004+ except IOError:
2005+ return False
2006+ except Exception as e:
2007+ msg = 'Error checking file {}: {}'.format(file_name, e)
2008+ amulet.raise_status(amulet.FAIL, msg=msg)
2009+
2010+ def file_contents_safe(self, sentry_unit, file_name,
2011+ max_wait=60, fatal=False):
2012+ """Get file contents from a sentry unit. Wrap amulet file_contents
2013+ with retry logic to address races where a file checks as existing,
2014+ but no longer exists by the time file_contents is called.
2015+ Return None if file not found. Optionally raise if fatal is True."""
2016+ unit_name = sentry_unit.info['unit_name']
2017+ file_contents = False
2018+ tries = 0
2019+ while not file_contents and tries < (max_wait / 4):
2020+ try:
2021+ file_contents = sentry_unit.file_contents(file_name)
2022+ except IOError:
2023+ self.log.debug('Attempt {} to open file {} from {} '
2024+ 'failed'.format(tries, file_name,
2025+ unit_name))
2026+ time.sleep(4)
2027+ tries += 1
2028+
2029+ if file_contents:
2030+ return file_contents
2031+ elif not fatal:
2032+ return None
2033+ elif fatal:
2034+ msg = 'Failed to get file contents from unit.'
2035+ amulet.raise_status(amulet.FAIL, msg)
2036+
2037+ def port_knock_tcp(self, host="localhost", port=22, timeout=15):
2038+ """Open a TCP socket to check for a listening sevice on a host.
2039+
2040+ :param host: host name or IP address, default to localhost
2041+ :param port: TCP port number, default to 22
2042+ :param timeout: Connect timeout, default to 15 seconds
2043+ :returns: True if successful, False if connect failed
2044+ """
2045+
2046+ # Resolve host name if possible
2047+ try:
2048+ connect_host = socket.gethostbyname(host)
2049+ host_human = "{} ({})".format(connect_host, host)
2050+ except socket.error, e:
2051+ self.log.warn('Unable to resolve address: '
2052+ '{} ({}) Trying anyway!'.format(host, e))
2053+ connect_host = host
2054+ host_human = connect_host
2055+
2056+ # Attempt socket connection
2057+ try:
2058+ knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2059+ knock.settimeout(timeout)
2060+ knock.connect((connect_host, port))
2061+ knock.close()
2062+ self.log.debug('Socket connect OK for host '
2063+ '{} on port {}.'.format(host_human, port))
2064+ return True
2065+ except socket.error, e:
2066+ self.log.debug('Socket connect FAIL for'
2067+ ' {} port {} ({})'.format(host_human, port, e))
2068+ return False
2069+
2070+ def port_knock_units(self, sentry_units, port=22,
2071+ timeout=15, expect_success=True):
2072+ """Open a TCP socket to check for a listening sevice on each
2073+ listed juju unit.
2074+
2075+ :param sentry_units: list of sentry unit pointers
2076+ :param port: TCP port number, default to 22
2077+ :param timeout: Connect timeout, default to 15 seconds
2078+ :expect_success: True by default, set False to invert logic
2079+ :returns: None if successful, Failure message otherwise
2080+ """
2081+ for unit in sentry_units:
2082+ host = unit.info['public-address']
2083+ connected = self.port_knock_tcp(host, port, timeout)
2084+ if not connected and expect_success:
2085+ return 'Socket connect failed.'
2086+ elif connected and not expect_success:
2087+ return 'Socket connected unexpectedly.'
2088+
2089+ def get_uuid_epoch_stamp(self):
2090+ """Returns a stamp string based on uuid4 and epoch time. Useful in
2091+ generating test messages which need to be unique-ish."""
2092+ return '[{}-{}]'.format(uuid.uuid4(), time.time())
2093+
2094+# amulet juju action helpers:
2095+ def run_action(self, unit_sentry, action,
2096+ _check_output=subprocess.check_output):
2097+ """Run the named action on a given unit sentry.
2098+
2099+ _check_output parameter is used for dependency injection.
2100+
2101+ @return action_id.
2102+ """
2103+ unit_id = unit_sentry.info["unit_name"]
2104+ command = ["juju", "action", "do", "--format=json", unit_id, action]
2105+ self.log.info("Running command: %s\n" % " ".join(command))
2106+ output = _check_output(command, universal_newlines=True)
2107+ data = json.loads(output)
2108+ action_id = data[u'Action queued with id']
2109+ return action_id
2110+
2111+ def wait_on_action(self, action_id, _check_output=subprocess.check_output):
2112+ """Wait for a given action, returning if it completed or not.
2113+
2114+ _check_output parameter is used for dependency injection.
2115+ """
2116+ command = ["juju", "action", "fetch", "--format=json", "--wait=0",
2117+ action_id]
2118+ output = _check_output(command, universal_newlines=True)
2119+ data = json.loads(output)
2120+ return data.get(u"status") == "completed"
2121
2122=== added directory 'tests/charmhelpers/contrib/openstack'
2123=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
2124--- tests/charmhelpers/contrib/openstack/__init__.py 1970-01-01 00:00:00 +0000
2125+++ tests/charmhelpers/contrib/openstack/__init__.py 2015-09-01 02:46:36 +0000
2126@@ -0,0 +1,15 @@
2127+# Copyright 2014-2015 Canonical Limited.
2128+#
2129+# This file is part of charm-helpers.
2130+#
2131+# charm-helpers is free software: you can redistribute it and/or modify
2132+# it under the terms of the GNU Lesser General Public License version 3 as
2133+# published by the Free Software Foundation.
2134+#
2135+# charm-helpers is distributed in the hope that it will be useful,
2136+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2137+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2138+# GNU Lesser General Public License for more details.
2139+#
2140+# You should have received a copy of the GNU Lesser General Public License
2141+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2142
2143=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
2144=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
2145--- tests/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
2146+++ tests/charmhelpers/contrib/openstack/amulet/__init__.py 2015-09-01 02:46:36 +0000
2147@@ -0,0 +1,15 @@
2148+# Copyright 2014-2015 Canonical Limited.
2149+#
2150+# This file is part of charm-helpers.
2151+#
2152+# charm-helpers is free software: you can redistribute it and/or modify
2153+# it under the terms of the GNU Lesser General Public License version 3 as
2154+# published by the Free Software Foundation.
2155+#
2156+# charm-helpers is distributed in the hope that it will be useful,
2157+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2158+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2159+# GNU Lesser General Public License for more details.
2160+#
2161+# You should have received a copy of the GNU Lesser General Public License
2162+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2163
2164=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
2165--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
2166+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-01 02:46:36 +0000
2167@@ -0,0 +1,198 @@
2168+# Copyright 2014-2015 Canonical Limited.
2169+#
2170+# This file is part of charm-helpers.
2171+#
2172+# charm-helpers is free software: you can redistribute it and/or modify
2173+# it under the terms of the GNU Lesser General Public License version 3 as
2174+# published by the Free Software Foundation.
2175+#
2176+# charm-helpers is distributed in the hope that it will be useful,
2177+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2178+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2179+# GNU Lesser General Public License for more details.
2180+#
2181+# You should have received a copy of the GNU Lesser General Public License
2182+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2183+
2184+import six
2185+from collections import OrderedDict
2186+from charmhelpers.contrib.amulet.deployment import (
2187+ AmuletDeployment
2188+)
2189+
2190+
2191+class OpenStackAmuletDeployment(AmuletDeployment):
2192+ """OpenStack amulet deployment.
2193+
2194+ This class inherits from AmuletDeployment and has additional support
2195+ that is specifically for use by OpenStack charms.
2196+ """
2197+
2198+ def __init__(self, series=None, openstack=None, source=None, stable=True):
2199+ """Initialize the deployment environment."""
2200+ super(OpenStackAmuletDeployment, self).__init__(series)
2201+ self.openstack = openstack
2202+ self.source = source
2203+ self.stable = stable
2204+ # Note(coreycb): this needs to be changed when new next branches come
2205+ # out.
2206+ self.current_next = "trusty"
2207+
2208+ def _determine_branch_locations(self, other_services):
2209+ """Determine the branch locations for the other services.
2210+
2211+ Determine if the local branch being tested is derived from its
2212+ stable or next (dev) branch, and based on this, use the corresonding
2213+ stable or next branches for the other_services."""
2214+
2215+ # Charms outside the lp:~openstack-charmers namespace
2216+ base_charms = ['mysql', 'mongodb', 'nrpe']
2217+
2218+ # Force these charms to current series even when using an older series.
2219+ # ie. Use trusty/nrpe even when series is precise, as the P charm
2220+ # does not possess the necessary external master config and hooks.
2221+ force_series_current = ['nrpe']
2222+
2223+ if self.series in ['precise', 'trusty']:
2224+ base_series = self.series
2225+ else:
2226+ base_series = self.current_next
2227+
2228+ if self.stable:
2229+ for svc in other_services:
2230+ if svc['name'] in force_series_current:
2231+ base_series = self.current_next
2232+
2233+ temp = 'lp:charms/{}/{}'
2234+ svc['location'] = temp.format(base_series,
2235+ svc['name'])
2236+ else:
2237+ for svc in other_services:
2238+ if svc['name'] in force_series_current:
2239+ base_series = self.current_next
2240+
2241+ if svc['name'] in base_charms:
2242+ temp = 'lp:charms/{}/{}'
2243+ svc['location'] = temp.format(base_series,
2244+ svc['name'])
2245+ else:
2246+ temp = 'lp:~openstack-charmers/charms/{}/{}/next'
2247+ svc['location'] = temp.format(self.current_next,
2248+ svc['name'])
2249+ return other_services
2250+
2251+ def _add_services(self, this_service, other_services):
2252+ """Add services to the deployment and set openstack-origin/source."""
2253+ other_services = self._determine_branch_locations(other_services)
2254+
2255+ super(OpenStackAmuletDeployment, self)._add_services(this_service,
2256+ other_services)
2257+
2258+ services = other_services
2259+ services.append(this_service)
2260+
2261+ # Charms which should use the source config option
2262+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
2263+ 'ceph-osd', 'ceph-radosgw']
2264+
2265+ # Charms which can not use openstack-origin, ie. many subordinates
2266+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
2267+
2268+ if self.openstack:
2269+ for svc in services:
2270+ if svc['name'] not in use_source + no_origin:
2271+ config = {'openstack-origin': self.openstack}
2272+ self.d.configure(svc['name'], config)
2273+
2274+ if self.source:
2275+ for svc in services:
2276+ if svc['name'] in use_source and svc['name'] not in no_origin:
2277+ config = {'source': self.source}
2278+ self.d.configure(svc['name'], config)
2279+
2280+ def _configure_services(self, configs):
2281+ """Configure all of the services."""
2282+ for service, config in six.iteritems(configs):
2283+ self.d.configure(service, config)
2284+
2285+ def _get_openstack_release(self):
2286+ """Get openstack release.
2287+
2288+ Return an integer representing the enum value of the openstack
2289+ release.
2290+ """
2291+ # Must be ordered by OpenStack release (not by Ubuntu release):
2292+ (self.precise_essex, self.precise_folsom, self.precise_grizzly,
2293+ self.precise_havana, self.precise_icehouse,
2294+ self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
2295+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
2296+ self.wily_liberty) = range(12)
2297+
2298+ releases = {
2299+ ('precise', None): self.precise_essex,
2300+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
2301+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
2302+ ('precise', 'cloud:precise-havana'): self.precise_havana,
2303+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
2304+ ('trusty', None): self.trusty_icehouse,
2305+ ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
2306+ ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
2307+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
2308+ ('utopic', None): self.utopic_juno,
2309+ ('vivid', None): self.vivid_kilo,
2310+ ('wily', None): self.wily_liberty}
2311+ return releases[(self.series, self.openstack)]
2312+
2313+ def _get_openstack_release_string(self):
2314+ """Get openstack release string.
2315+
2316+ Return a string representing the openstack release.
2317+ """
2318+ releases = OrderedDict([
2319+ ('precise', 'essex'),
2320+ ('quantal', 'folsom'),
2321+ ('raring', 'grizzly'),
2322+ ('saucy', 'havana'),
2323+ ('trusty', 'icehouse'),
2324+ ('utopic', 'juno'),
2325+ ('vivid', 'kilo'),
2326+ ('wily', 'liberty'),
2327+ ])
2328+ if self.openstack:
2329+ os_origin = self.openstack.split(':')[1]
2330+ return os_origin.split('%s-' % self.series)[1].split('/')[0]
2331+ else:
2332+ return releases[self.series]
2333+
2334+ def get_ceph_expected_pools(self, radosgw=False):
2335+ """Return a list of expected ceph pools in a ceph + cinder + glance
2336+ test scenario, based on OpenStack release and whether ceph radosgw
2337+ is flagged as present or not."""
2338+
2339+ if self._get_openstack_release() >= self.trusty_kilo:
2340+ # Kilo or later
2341+ pools = [
2342+ 'rbd',
2343+ 'cinder',
2344+ 'glance'
2345+ ]
2346+ else:
2347+ # Juno or earlier
2348+ pools = [
2349+ 'data',
2350+ 'metadata',
2351+ 'rbd',
2352+ 'cinder',
2353+ 'glance'
2354+ ]
2355+
2356+ if radosgw:
2357+ pools.extend([
2358+ '.rgw.root',
2359+ '.rgw.control',
2360+ '.rgw',
2361+ '.rgw.gc',
2362+ '.users.uid'
2363+ ])
2364+
2365+ return pools
2366
2367=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
2368--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
2369+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-01 02:46:36 +0000
2370@@ -0,0 +1,965 @@
2371+# Copyright 2014-2015 Canonical Limited.
2372+#
2373+# This file is part of charm-helpers.
2374+#
2375+# charm-helpers is free software: you can redistribute it and/or modify
2376+# it under the terms of the GNU Lesser General Public License version 3 as
2377+# published by the Free Software Foundation.
2378+#
2379+# charm-helpers is distributed in the hope that it will be useful,
2380+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2381+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2382+# GNU Lesser General Public License for more details.
2383+#
2384+# You should have received a copy of the GNU Lesser General Public License
2385+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2386+
2387+import amulet
2388+import json
2389+import logging
2390+import os
2391+import six
2392+import time
2393+import urllib
2394+
2395+import cinderclient.v1.client as cinder_client
2396+import glanceclient.v1.client as glance_client
2397+import heatclient.v1.client as heat_client
2398+import keystoneclient.v2_0 as keystone_client
2399+import novaclient.v1_1.client as nova_client
2400+import pika
2401+import swiftclient
2402+
2403+from charmhelpers.contrib.amulet.utils import (
2404+ AmuletUtils
2405+)
2406+
2407+DEBUG = logging.DEBUG
2408+ERROR = logging.ERROR
2409+
2410+
2411+class OpenStackAmuletUtils(AmuletUtils):
2412+ """OpenStack amulet utilities.
2413+
2414+ This class inherits from AmuletUtils and has additional support
2415+ that is specifically for use by OpenStack charm tests.
2416+ """
2417+
2418+ def __init__(self, log_level=ERROR):
2419+ """Initialize the deployment environment."""
2420+ super(OpenStackAmuletUtils, self).__init__(log_level)
2421+
2422+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
2423+ public_port, expected):
2424+ """Validate endpoint data.
2425+
2426+ Validate actual endpoint data vs expected endpoint data. The ports
2427+ are used to find the matching endpoint.
2428+ """
2429+ self.log.debug('Validating endpoint data...')
2430+ self.log.debug('actual: {}'.format(repr(endpoints)))
2431+ found = False
2432+ for ep in endpoints:
2433+ self.log.debug('endpoint: {}'.format(repr(ep)))
2434+ if (admin_port in ep.adminurl and
2435+ internal_port in ep.internalurl and
2436+ public_port in ep.publicurl):
2437+ found = True
2438+ actual = {'id': ep.id,
2439+ 'region': ep.region,
2440+ 'adminurl': ep.adminurl,
2441+ 'internalurl': ep.internalurl,
2442+ 'publicurl': ep.publicurl,
2443+ 'service_id': ep.service_id}
2444+ ret = self._validate_dict_data(expected, actual)
2445+ if ret:
2446+ return 'unexpected endpoint data - {}'.format(ret)
2447+
2448+ if not found:
2449+ return 'endpoint not found'
2450+
2451+ def validate_svc_catalog_endpoint_data(self, expected, actual):
2452+ """Validate service catalog endpoint data.
2453+
2454+ Validate a list of actual service catalog endpoints vs a list of
2455+ expected service catalog endpoints.
2456+ """
2457+ self.log.debug('Validating service catalog endpoint data...')
2458+ self.log.debug('actual: {}'.format(repr(actual)))
2459+ for k, v in six.iteritems(expected):
2460+ if k in actual:
2461+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
2462+ if ret:
2463+ return self.endpoint_error(k, ret)
2464+ else:
2465+ return "endpoint {} does not exist".format(k)
2466+ return ret
2467+
2468+ def validate_tenant_data(self, expected, actual):
2469+ """Validate tenant data.
2470+
2471+ Validate a list of actual tenant data vs list of expected tenant
2472+ data.
2473+ """
2474+ self.log.debug('Validating tenant data...')
2475+ self.log.debug('actual: {}'.format(repr(actual)))
2476+ for e in expected:
2477+ found = False
2478+ for act in actual:
2479+ a = {'enabled': act.enabled, 'description': act.description,
2480+ 'name': act.name, 'id': act.id}
2481+ if e['name'] == a['name']:
2482+ found = True
2483+ ret = self._validate_dict_data(e, a)
2484+ if ret:
2485+ return "unexpected tenant data - {}".format(ret)
2486+ if not found:
2487+ return "tenant {} does not exist".format(e['name'])
2488+ return ret
2489+
2490+ def validate_role_data(self, expected, actual):
2491+ """Validate role data.
2492+
2493+ Validate a list of actual role data vs a list of expected role
2494+ data.
2495+ """
2496+ self.log.debug('Validating role data...')
2497+ self.log.debug('actual: {}'.format(repr(actual)))
2498+ for e in expected:
2499+ found = False
2500+ for act in actual:
2501+ a = {'name': act.name, 'id': act.id}
2502+ if e['name'] == a['name']:
2503+ found = True
2504+ ret = self._validate_dict_data(e, a)
2505+ if ret:
2506+ return "unexpected role data - {}".format(ret)
2507+ if not found:
2508+ return "role {} does not exist".format(e['name'])
2509+ return ret
2510+
2511+ def validate_user_data(self, expected, actual):
2512+ """Validate user data.
2513+
2514+ Validate a list of actual user data vs a list of expected user
2515+ data.
2516+ """
2517+ self.log.debug('Validating user data...')
2518+ self.log.debug('actual: {}'.format(repr(actual)))
2519+ for e in expected:
2520+ found = False
2521+ for act in actual:
2522+ a = {'enabled': act.enabled, 'name': act.name,
2523+ 'email': act.email, 'tenantId': act.tenantId,
2524+ 'id': act.id}
2525+ if e['name'] == a['name']:
2526+ found = True
2527+ ret = self._validate_dict_data(e, a)
2528+ if ret:
2529+ return "unexpected user data - {}".format(ret)
2530+ if not found:
2531+ return "user {} does not exist".format(e['name'])
2532+ return ret
2533+
2534+ def validate_flavor_data(self, expected, actual):
2535+ """Validate flavor data.
2536+
2537+ Validate a list of actual flavors vs a list of expected flavors.
2538+ """
2539+ self.log.debug('Validating flavor data...')
2540+ self.log.debug('actual: {}'.format(repr(actual)))
2541+ act = [a.name for a in actual]
2542+ return self._validate_list_data(expected, act)
2543+
2544+ def tenant_exists(self, keystone, tenant):
2545+ """Return True if tenant exists."""
2546+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
2547+ return tenant in [t.name for t in keystone.tenants.list()]
2548+
2549+ def authenticate_cinder_admin(self, keystone_sentry, username,
2550+ password, tenant):
2551+ """Authenticates admin user with cinder."""
2552+ # NOTE(beisner): cinder python client doesn't accept tokens.
2553+ service_ip = \
2554+ keystone_sentry.relation('shared-db',
2555+ 'mysql:shared-db')['private-address']
2556+ ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
2557+ return cinder_client.Client(username, password, tenant, ept)
2558+
2559+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
2560+ tenant):
2561+ """Authenticates admin user with the keystone admin endpoint."""
2562+ self.log.debug('Authenticating keystone admin...')
2563+ unit = keystone_sentry
2564+ service_ip = unit.relation('shared-db',
2565+ 'mysql:shared-db')['private-address']
2566+ ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
2567+ return keystone_client.Client(username=user, password=password,
2568+ tenant_name=tenant, auth_url=ep)
2569+
2570+ def authenticate_keystone_user(self, keystone, user, password, tenant):
2571+ """Authenticates a regular user with the keystone public endpoint."""
2572+ self.log.debug('Authenticating keystone user ({})...'.format(user))
2573+ ep = keystone.service_catalog.url_for(service_type='identity',
2574+ endpoint_type='publicURL')
2575+ return keystone_client.Client(username=user, password=password,
2576+ tenant_name=tenant, auth_url=ep)
2577+
2578+ def authenticate_glance_admin(self, keystone):
2579+ """Authenticates admin user with glance."""
2580+ self.log.debug('Authenticating glance admin...')
2581+ ep = keystone.service_catalog.url_for(service_type='image',
2582+ endpoint_type='adminURL')
2583+ return glance_client.Client(ep, token=keystone.auth_token)
2584+
2585+ def authenticate_heat_admin(self, keystone):
2586+ """Authenticates the admin user with heat."""
2587+ self.log.debug('Authenticating heat admin...')
2588+ ep = keystone.service_catalog.url_for(service_type='orchestration',
2589+ endpoint_type='publicURL')
2590+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
2591+
2592+ def authenticate_nova_user(self, keystone, user, password, tenant):
2593+ """Authenticates a regular user with nova-api."""
2594+ self.log.debug('Authenticating nova user ({})...'.format(user))
2595+ ep = keystone.service_catalog.url_for(service_type='identity',
2596+ endpoint_type='publicURL')
2597+ return nova_client.Client(username=user, api_key=password,
2598+ project_id=tenant, auth_url=ep)
2599+
2600+ def authenticate_swift_user(self, keystone, user, password, tenant):
2601+ """Authenticates a regular user with swift api."""
2602+ self.log.debug('Authenticating swift user ({})...'.format(user))
2603+ ep = keystone.service_catalog.url_for(service_type='identity',
2604+ endpoint_type='publicURL')
2605+ return swiftclient.Connection(authurl=ep,
2606+ user=user,
2607+ key=password,
2608+ tenant_name=tenant,
2609+ auth_version='2.0')
2610+
2611+ def create_cirros_image(self, glance, image_name):
2612+ """Download the latest cirros image and upload it to glance,
2613+ validate and return a resource pointer.
2614+
2615+ :param glance: pointer to authenticated glance connection
2616+ :param image_name: display name for new image
2617+ :returns: glance image pointer
2618+ """
2619+ self.log.debug('Creating glance cirros image '
2620+ '({})...'.format(image_name))
2621+
2622+ # Download cirros image
2623+ http_proxy = os.getenv('AMULET_HTTP_PROXY')
2624+ self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
2625+ if http_proxy:
2626+ proxies = {'http': http_proxy}
2627+ opener = urllib.FancyURLopener(proxies)
2628+ else:
2629+ opener = urllib.FancyURLopener()
2630+
2631+ f = opener.open('http://download.cirros-cloud.net/version/released')
2632+ version = f.read().strip()
2633+ cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
2634+ local_path = os.path.join('tests', cirros_img)
2635+
2636+ if not os.path.exists(local_path):
2637+ cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
2638+ version, cirros_img)
2639+ opener.retrieve(cirros_url, local_path)
2640+ f.close()
2641+
2642+ # Create glance image
2643+ with open(local_path) as f:
2644+ image = glance.images.create(name=image_name, is_public=True,
2645+ disk_format='qcow2',
2646+ container_format='bare', data=f)
2647+
2648+ # Wait for image to reach active status
2649+ img_id = image.id
2650+ ret = self.resource_reaches_status(glance.images, img_id,
2651+ expected_stat='active',
2652+ msg='Image status wait')
2653+ if not ret:
2654+ msg = 'Glance image failed to reach expected state.'
2655+ amulet.raise_status(amulet.FAIL, msg=msg)
2656+
2657+ # Re-validate new image
2658+ self.log.debug('Validating image attributes...')
2659+ val_img_name = glance.images.get(img_id).name
2660+ val_img_stat = glance.images.get(img_id).status
2661+ val_img_pub = glance.images.get(img_id).is_public
2662+ val_img_cfmt = glance.images.get(img_id).container_format
2663+ val_img_dfmt = glance.images.get(img_id).disk_format
2664+ msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
2665+ 'container fmt:{} disk fmt:{}'.format(
2666+ val_img_name, val_img_pub, img_id,
2667+ val_img_stat, val_img_cfmt, val_img_dfmt))
2668+
2669+ if val_img_name == image_name and val_img_stat == 'active' \
2670+ and val_img_pub is True and val_img_cfmt == 'bare' \
2671+ and val_img_dfmt == 'qcow2':
2672+ self.log.debug(msg_attr)
2673+ else:
2674+ msg = ('Volume validation failed, {}'.format(msg_attr))
2675+ amulet.raise_status(amulet.FAIL, msg=msg)
2676+
2677+ return image
2678+
2679+ def delete_image(self, glance, image):
2680+ """Delete the specified image."""
2681+
2682+ # /!\ DEPRECATION WARNING
2683+ self.log.warn('/!\\ DEPRECATION WARNING: use '
2684+ 'delete_resource instead of delete_image.')
2685+ self.log.debug('Deleting glance image ({})...'.format(image))
2686+ return self.delete_resource(glance.images, image, msg='glance image')
2687+
2688+ def create_instance(self, nova, image_name, instance_name, flavor):
2689+ """Create the specified instance."""
2690+ self.log.debug('Creating instance '
2691+ '({}|{}|{})'.format(instance_name, image_name, flavor))
2692+ image = nova.images.find(name=image_name)
2693+ flavor = nova.flavors.find(name=flavor)
2694+ instance = nova.servers.create(name=instance_name, image=image,
2695+ flavor=flavor)
2696+
2697+ count = 1
2698+ status = instance.status
2699+ while status != 'ACTIVE' and count < 60:
2700+ time.sleep(3)
2701+ instance = nova.servers.get(instance.id)
2702+ status = instance.status
2703+ self.log.debug('instance status: {}'.format(status))
2704+ count += 1
2705+
2706+ if status != 'ACTIVE':
2707+ self.log.error('instance creation timed out')
2708+ return None
2709+
2710+ return instance
2711+
2712+ def delete_instance(self, nova, instance):
2713+ """Delete the specified instance."""
2714+
2715+ # /!\ DEPRECATION WARNING
2716+ self.log.warn('/!\\ DEPRECATION WARNING: use '
2717+ 'delete_resource instead of delete_instance.')
2718+ self.log.debug('Deleting instance ({})...'.format(instance))
2719+ return self.delete_resource(nova.servers, instance,
2720+ msg='nova instance')
2721+
2722+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
2723+ """Create a new keypair, or return pointer if it already exists."""
2724+ try:
2725+ _keypair = nova.keypairs.get(keypair_name)
2726+ self.log.debug('Keypair ({}) already exists, '
2727+ 'using it.'.format(keypair_name))
2728+ return _keypair
2729+ except:
2730+ self.log.debug('Keypair ({}) does not exist, '
2731+ 'creating it.'.format(keypair_name))
2732+
2733+ _keypair = nova.keypairs.create(name=keypair_name)
2734+ return _keypair
2735+
2736+ def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
2737+ img_id=None, src_vol_id=None, snap_id=None):
2738+ """Create cinder volume, optionally from a glance image, OR
2739+ optionally as a clone of an existing volume, OR optionally
2740+ from a snapshot. Wait for the new volume status to reach
2741+ the expected status, validate and return a resource pointer.
2742+
2743+ :param vol_name: cinder volume display name
2744+ :param vol_size: size in gigabytes
2745+ :param img_id: optional glance image id
2746+ :param src_vol_id: optional source volume id to clone
2747+ :param snap_id: optional snapshot id to use
2748+ :returns: cinder volume pointer
2749+ """
2750+ # Handle parameter input and avoid impossible combinations
2751+ if img_id and not src_vol_id and not snap_id:
2752+ # Create volume from image
2753+ self.log.debug('Creating cinder volume from glance image...')
2754+ bootable = 'true'
2755+ elif src_vol_id and not img_id and not snap_id:
2756+ # Clone an existing volume
2757+ self.log.debug('Cloning cinder volume...')
2758+ bootable = cinder.volumes.get(src_vol_id).bootable
2759+ elif snap_id and not src_vol_id and not img_id:
2760+ # Create volume from snapshot
2761+ self.log.debug('Creating cinder volume from snapshot...')
2762+ snap = cinder.volume_snapshots.find(id=snap_id)
2763+ vol_size = snap.size
2764+ snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
2765+ bootable = cinder.volumes.get(snap_vol_id).bootable
2766+ elif not img_id and not src_vol_id and not snap_id:
2767+ # Create volume
2768+ self.log.debug('Creating cinder volume...')
2769+ bootable = 'false'
2770+ else:
2771+ # Impossible combination of parameters
2772+ msg = ('Invalid method use - name:{} size:{} img_id:{} '
2773+ 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
2774+ img_id, src_vol_id,
2775+ snap_id))
2776+ amulet.raise_status(amulet.FAIL, msg=msg)
2777+
2778+ # Create new volume
2779+ try:
2780+ vol_new = cinder.volumes.create(display_name=vol_name,
2781+ imageRef=img_id,
2782+ size=vol_size,
2783+ source_volid=src_vol_id,
2784+ snapshot_id=snap_id)
2785+ vol_id = vol_new.id
2786+ except Exception as e:
2787+ msg = 'Failed to create volume: {}'.format(e)
2788+ amulet.raise_status(amulet.FAIL, msg=msg)
2789+
2790+ # Wait for volume to reach available status
2791+ ret = self.resource_reaches_status(cinder.volumes, vol_id,
2792+ expected_stat="available",
2793+ msg="Volume status wait")
2794+ if not ret:
2795+ msg = 'Cinder volume failed to reach expected state.'
2796+ amulet.raise_status(amulet.FAIL, msg=msg)
2797+
2798+ # Re-validate new volume
2799+ self.log.debug('Validating volume attributes...')
2800+ val_vol_name = cinder.volumes.get(vol_id).display_name
2801+ val_vol_boot = cinder.volumes.get(vol_id).bootable
2802+ val_vol_stat = cinder.volumes.get(vol_id).status
2803+ val_vol_size = cinder.volumes.get(vol_id).size
2804+ msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
2805+ '{} size:{}'.format(val_vol_name, vol_id,
2806+ val_vol_stat, val_vol_boot,
2807+ val_vol_size))
2808+
2809+ if val_vol_boot == bootable and val_vol_stat == 'available' \
2810+ and val_vol_name == vol_name and val_vol_size == vol_size:
2811+ self.log.debug(msg_attr)
2812+ else:
2813+ msg = ('Volume validation failed, {}'.format(msg_attr))
2814+ amulet.raise_status(amulet.FAIL, msg=msg)
2815+
2816+ return vol_new
2817+
2818+ def delete_resource(self, resource, resource_id,
2819+ msg="resource", max_wait=120):
2820+ """Delete one openstack resource, such as one instance, keypair,
2821+ image, volume, stack, etc., and confirm deletion within max wait time.
2822+
2823+ :param resource: pointer to os resource type, ex:glance_client.images
2824+ :param resource_id: unique name or id for the openstack resource
2825+ :param msg: text to identify purpose in logging
2826+ :param max_wait: maximum wait time in seconds
2827+ :returns: True if successful, otherwise False
2828+ """
2829+ self.log.debug('Deleting OpenStack resource '
2830+ '{} ({})'.format(resource_id, msg))
2831+ num_before = len(list(resource.list()))
2832+ resource.delete(resource_id)
2833+
2834+ tries = 0
2835+ num_after = len(list(resource.list()))
2836+ while num_after != (num_before - 1) and tries < (max_wait / 4):
2837+ self.log.debug('{} delete check: '
2838+ '{} [{}:{}] {}'.format(msg, tries,
2839+ num_before,
2840+ num_after,
2841+ resource_id))
2842+ time.sleep(4)
2843+ num_after = len(list(resource.list()))
2844+ tries += 1
2845+
2846+ self.log.debug('{}: expected, actual count = {}, '
2847+ '{}'.format(msg, num_before - 1, num_after))
2848+
2849+ if num_after == (num_before - 1):
2850+ return True
2851+ else:
2852+ self.log.error('{} delete timed out'.format(msg))
2853+ return False
2854+
2855+ def resource_reaches_status(self, resource, resource_id,
2856+ expected_stat='available',
2857+ msg='resource', max_wait=120):
2858+ """Wait for an openstack resources status to reach an
2859+ expected status within a specified time. Useful to confirm that
2860+ nova instances, cinder vols, snapshots, glance images, heat stacks
2861+ and other resources eventually reach the expected status.
2862+
2863+ :param resource: pointer to os resource type, ex: heat_client.stacks
2864+ :param resource_id: unique id for the openstack resource
2865+ :param expected_stat: status to expect resource to reach
2866+ :param msg: text to identify purpose in logging
2867+ :param max_wait: maximum wait time in seconds
2868+ :returns: True if successful, False if status is not reached
2869+ """
2870+
2871+ tries = 0
2872+ resource_stat = resource.get(resource_id).status
2873+ while resource_stat != expected_stat and tries < (max_wait / 4):
2874+ self.log.debug('{} status check: '
2875+ '{} [{}:{}] {}'.format(msg, tries,
2876+ resource_stat,
2877+ expected_stat,
2878+ resource_id))
2879+ time.sleep(4)
2880+ resource_stat = resource.get(resource_id).status
2881+ tries += 1
2882+
2883+ self.log.debug('{}: expected, actual status = {}, '
2884+ '{}'.format(msg, resource_stat, expected_stat))
2885+
2886+ if resource_stat == expected_stat:
2887+ return True
2888+ else:
2889+ self.log.debug('{} never reached expected status: '
2890+ '{}'.format(resource_id, expected_stat))
2891+ return False
2892+
2893+ def get_ceph_osd_id_cmd(self, index):
2894+ """Produce a shell command that will return a ceph-osd id."""
2895+ return ("`initctl list | grep 'ceph-osd ' | "
2896+ "awk 'NR=={} {{ print $2 }}' | "
2897+ "grep -o '[0-9]*'`".format(index + 1))
2898+
2899+ def get_ceph_pools(self, sentry_unit):
2900+ """Return a dict of ceph pools from a single ceph unit, with
2901+ pool name as keys, pool id as vals."""
2902+ pools = {}
2903+ cmd = 'sudo ceph osd lspools'
2904+ output, code = sentry_unit.run(cmd)
2905+ if code != 0:
2906+ msg = ('{} `{}` returned {} '
2907+ '{}'.format(sentry_unit.info['unit_name'],
2908+ cmd, code, output))
2909+ amulet.raise_status(amulet.FAIL, msg=msg)
2910+
2911+ # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
2912+ for pool in str(output).split(','):
2913+ pool_id_name = pool.split(' ')
2914+ if len(pool_id_name) == 2:
2915+ pool_id = pool_id_name[0]
2916+ pool_name = pool_id_name[1]
2917+ pools[pool_name] = int(pool_id)
2918+
2919+ self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
2920+ pools))
2921+ return pools
2922+
2923+ def get_ceph_df(self, sentry_unit):
2924+ """Return dict of ceph df json output, including ceph pool state.
2925+
2926+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
2927+ :returns: Dict of ceph df output
2928+ """
2929+ cmd = 'sudo ceph df --format=json'
2930+ output, code = sentry_unit.run(cmd)
2931+ if code != 0:
2932+ msg = ('{} `{}` returned {} '
2933+ '{}'.format(sentry_unit.info['unit_name'],
2934+ cmd, code, output))
2935+ amulet.raise_status(amulet.FAIL, msg=msg)
2936+ return json.loads(output)
2937+
2938+ def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
2939+ """Take a sample of attributes of a ceph pool, returning ceph
2940+ pool name, object count and disk space used for the specified
2941+ pool ID number.
2942+
2943+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
2944+ :param pool_id: Ceph pool ID
2945+ :returns: List of pool name, object count, kb disk space used
2946+ """
2947+ df = self.get_ceph_df(sentry_unit)
2948+ pool_name = df['pools'][pool_id]['name']
2949+ obj_count = df['pools'][pool_id]['stats']['objects']
2950+ kb_used = df['pools'][pool_id]['stats']['kb_used']
2951+ self.log.debug('Ceph {} pool (ID {}): {} objects, '
2952+ '{} kb used'.format(pool_name, pool_id,
2953+ obj_count, kb_used))
2954+ return pool_name, obj_count, kb_used
2955+
2956+ def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
2957+ """Validate ceph pool samples taken over time, such as pool
2958+ object counts or pool kb used, before adding, after adding, and
2959+ after deleting items which affect those pool attributes. The
2960+ 2nd element is expected to be greater than the 1st; 3rd is expected
2961+ to be less than the 2nd.
2962+
2963+ :param samples: List containing 3 data samples
2964+ :param sample_type: String for logging and usage context
2965+ :returns: None if successful, Failure message otherwise
2966+ """
2967+ original, created, deleted = range(3)
2968+ if samples[created] <= samples[original] or \
2969+ samples[deleted] >= samples[created]:
2970+ return ('Ceph {} samples ({}) '
2971+ 'unexpected.'.format(sample_type, samples))
2972+ else:
2973+ self.log.debug('Ceph {} samples (OK): '
2974+ '{}'.format(sample_type, samples))
2975+ return None
2976+
2977+# rabbitmq/amdqp specific helpers:
2978+ def add_rmq_test_user(self, sentry_units,
2979+ username="testuser1", password="changeme"):
2980+ """Add a test user via the first rmq juju unit, check connection as
2981+ the new user against all sentry units.
2982+
2983+ :param sentry_units: list of sentry unit pointers
2984+ :param username: amqp user name, default to testuser1
2985+ :param password: amqp user password
2986+ :returns: None if successful. Raise on error.
2987+ """
2988+ self.log.debug('Adding rmq user ({})...'.format(username))
2989+
2990+ # Check that user does not already exist
2991+ cmd_user_list = 'rabbitmqctl list_users'
2992+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
2993+ if username in output:
2994+ self.log.warning('User ({}) already exists, returning '
2995+ 'gracefully.'.format(username))
2996+ return
2997+
2998+ perms = '".*" ".*" ".*"'
2999+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
3000+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
3001+
3002+ # Add user via first unit
3003+ for cmd in cmds:
3004+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
3005+
3006+ # Check connection against the other sentry_units
3007+ self.log.debug('Checking user connect against units...')
3008+ for sentry_unit in sentry_units:
3009+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
3010+ fatal=False,
3011+ username=username,
3012+ password=password)
3013+ connection.close()
3014+
3015+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
3016+ """Delete a rabbitmq user via the first rmq juju unit.
3017+
3018+ :param sentry_units: list of sentry unit pointers
3019+ :param username: amqp user name, default to testuser1
3020+ :param password: amqp user password
3021+ :returns: None if successful or no such user.
3022+ """
3023+ self.log.debug('Deleting rmq user ({})...'.format(username))
3024+
3025+ # Check that user does not already exist
3026+ cmd_user_list = 'rabbitmqctl list_users'
3027+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
3028+
3029+ if username not in output:
3030+ self.log.warning('User ({}) does not exist, returning '
3031+ 'gracefully.'.format(username))
3032+ return
3033+
3034+ # Delete the user
3035+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
3036+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
3037+
3038+ def get_rmq_cluster_status(self, sentry_unit):
3039+ """Execute rabbitmq cluster status command on a unit and return
3040+ the full output.
3041+
3042+ :param unit: sentry unit
3043+ :returns: String containing console output of cluster status command
3044+ """
3045+ cmd = 'rabbitmqctl cluster_status'
3046+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
3047+ self.log.debug('{} cluster_status:\n{}'.format(
3048+ sentry_unit.info['unit_name'], output))
3049+ return str(output)
3050+
3051+ def get_rmq_cluster_running_nodes(self, sentry_unit):
3052+ """Parse rabbitmqctl cluster_status output string, return list of
3053+ running rabbitmq cluster nodes.
3054+
3055+ :param unit: sentry unit
3056+ :returns: List containing node names of running nodes
3057+ """
3058+ # NOTE(beisner): rabbitmqctl cluster_status output is not
3059+ # json-parsable, do string chop foo, then json.loads that.
3060+ str_stat = self.get_rmq_cluster_status(sentry_unit)
3061+ if 'running_nodes' in str_stat:
3062+ pos_start = str_stat.find("{running_nodes,") + 15
3063+ pos_end = str_stat.find("]},", pos_start) + 1
3064+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
3065+ run_nodes = json.loads(str_run_nodes)
3066+ return run_nodes
3067+ else:
3068+ return []
3069+
3070+ def validate_rmq_cluster_running_nodes(self, sentry_units):
3071+ """Check that all rmq unit hostnames are represented in the
3072+ cluster_status output of all units.
3073+
3074+ :param host_names: dict of juju unit names to host names
3075+ :param units: list of sentry unit pointers (all rmq units)
3076+ :returns: None if successful, otherwise return error message
3077+ """
3078+ host_names = self.get_unit_hostnames(sentry_units)
3079+ errors = []
3080+
3081+ # Query every unit for cluster_status running nodes
3082+ for query_unit in sentry_units:
3083+ query_unit_name = query_unit.info['unit_name']
3084+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
3085+
3086+ # Confirm that every unit is represented in the queried unit's
3087+ # cluster_status running nodes output.
3088+ for validate_unit in sentry_units:
3089+ val_host_name = host_names[validate_unit.info['unit_name']]
3090+ val_node_name = 'rabbit@{}'.format(val_host_name)
3091+
3092+ if val_node_name not in running_nodes:
3093+ errors.append('Cluster member check failed on {}: {} not '
3094+ 'in {}\n'.format(query_unit_name,
3095+ val_node_name,
3096+ running_nodes))
3097+ if errors:
3098+ return ''.join(errors)
3099+
3100+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
3101+ """Check a single juju rmq unit for ssl and port in the config file."""
3102+ host = sentry_unit.info['public-address']
3103+ unit_name = sentry_unit.info['unit_name']
3104+
3105+ conf_file = '/etc/rabbitmq/rabbitmq.config'
3106+ conf_contents = str(self.file_contents_safe(sentry_unit,
3107+ conf_file, max_wait=16))
3108+ # Checks
3109+ conf_ssl = 'ssl' in conf_contents
3110+ conf_port = str(port) in conf_contents
3111+
3112+ # Port explicitly checked in config
3113+ if port and conf_port and conf_ssl:
3114+ self.log.debug('SSL is enabled @{}:{} '
3115+ '({})'.format(host, port, unit_name))
3116+ return True
3117+ elif port and not conf_port and conf_ssl:
3118+ self.log.debug('SSL is enabled @{} but not on port {} '
3119+ '({})'.format(host, port, unit_name))
3120+ return False
3121+ # Port not checked (useful when checking that ssl is disabled)
3122+ elif not port and conf_ssl:
3123+ self.log.debug('SSL is enabled @{}:{} '
3124+ '({})'.format(host, port, unit_name))
3125+ return True
3126+ elif not port and not conf_ssl:
3127+ self.log.debug('SSL not enabled @{}:{} '
3128+ '({})'.format(host, port, unit_name))
3129+ return False
3130+ else:
3131+ msg = ('Unknown condition when checking SSL status @{}:{} '
3132+ '({})'.format(host, port, unit_name))
3133+ amulet.raise_status(amulet.FAIL, msg)
3134+
3135+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
3136+ """Check that ssl is enabled on rmq juju sentry units.
3137+
3138+ :param sentry_units: list of all rmq sentry units
3139+ :param port: optional ssl port override to validate
3140+ :returns: None if successful, otherwise return error message
3141+ """
3142+ for sentry_unit in sentry_units:
3143+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
3144+ return ('Unexpected condition: ssl is disabled on unit '
3145+ '({})'.format(sentry_unit.info['unit_name']))
3146+ return None
3147+
3148+ def validate_rmq_ssl_disabled_units(self, sentry_units):
3149+ """Check that ssl is enabled on listed rmq juju sentry units.
3150+
3151+ :param sentry_units: list of all rmq sentry units
3152+ :returns: True if successful. Raise on error.
3153+ """
3154+ for sentry_unit in sentry_units:
3155+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
3156+ return ('Unexpected condition: ssl is enabled on unit '
3157+ '({})'.format(sentry_unit.info['unit_name']))
3158+ return None
3159+
3160+ def configure_rmq_ssl_on(self, sentry_units, deployment,
3161+ port=None, max_wait=60):
3162+ """Turn ssl charm config option on, with optional non-default
3163+ ssl port specification. Confirm that it is enabled on every
3164+ unit.
3165+
3166+ :param sentry_units: list of sentry units
3167+ :param deployment: amulet deployment object pointer
3168+ :param port: amqp port, use defaults if None
3169+ :param max_wait: maximum time to wait in seconds to confirm
3170+ :returns: None if successful. Raise on error.
3171+ """
3172+ self.log.debug('Setting ssl charm config option: on')
3173+
3174+ # Enable RMQ SSL
3175+ config = {'ssl': 'on'}
3176+ if port:
3177+ config['ssl_port'] = port
3178+
3179+ deployment.configure('rabbitmq-server', config)
3180+
3181+ # Confirm
3182+ tries = 0
3183+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
3184+ while ret and tries < (max_wait / 4):
3185+ time.sleep(4)
3186+ self.log.debug('Attempt {}: {}'.format(tries, ret))
3187+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
3188+ tries += 1
3189+
3190+ if ret:
3191+ amulet.raise_status(amulet.FAIL, ret)
3192+
3193+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
3194+ """Turn ssl charm config option off, confirm that it is disabled
3195+ on every unit.
3196+
3197+ :param sentry_units: list of sentry units
3198+ :param deployment: amulet deployment object pointer
3199+ :param max_wait: maximum time to wait in seconds to confirm
3200+ :returns: None if successful. Raise on error.
3201+ """
3202+ self.log.debug('Setting ssl charm config option: off')
3203+
3204+ # Disable RMQ SSL
3205+ config = {'ssl': 'off'}
3206+ deployment.configure('rabbitmq-server', config)
3207+
3208+ # Confirm
3209+ tries = 0
3210+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
3211+ while ret and tries < (max_wait / 4):
3212+ time.sleep(4)
3213+ self.log.debug('Attempt {}: {}'.format(tries, ret))
3214+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
3215+ tries += 1
3216+
3217+ if ret:
3218+ amulet.raise_status(amulet.FAIL, ret)
3219+
3220+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
3221+ port=None, fatal=True,
3222+ username="testuser1", password="changeme"):
3223+ """Establish and return a pika amqp connection to the rabbitmq service
3224+ running on a rmq juju unit.
3225+
3226+ :param sentry_unit: sentry unit pointer
3227+ :param ssl: boolean, default to False
3228+ :param port: amqp port, use defaults if None
3229+ :param fatal: boolean, default to True (raises on connect error)
3230+ :param username: amqp user name, default to testuser1
3231+ :param password: amqp user password
3232+ :returns: pika amqp connection pointer or None if failed and non-fatal
3233+ """
3234+ host = sentry_unit.info['public-address']
3235+ unit_name = sentry_unit.info['unit_name']
3236+
3237+ # Default port logic if port is not specified
3238+ if ssl and not port:
3239+ port = 5671
3240+ elif not ssl and not port:
3241+ port = 5672
3242+
3243+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
3244+ '{}...'.format(host, port, unit_name, username))
3245+
3246+ try:
3247+ credentials = pika.PlainCredentials(username, password)
3248+ parameters = pika.ConnectionParameters(host=host, port=port,
3249+ credentials=credentials,
3250+ ssl=ssl,
3251+ #heartbeat_interval=0,
3252+ connection_attempts=3,
3253+ retry_delay=5,
3254+ socket_timeout=1)
3255+ connection = pika.BlockingConnection(parameters)
3256+ assert connection.server_properties['product'] == 'RabbitMQ'
3257+ self.log.debug('Connect OK')
3258+ return connection
3259+ except Exception as e:
3260+ msg = ('amqp connection failed to {}:{} as '
3261+ '{} ({})'.format(host, port, username, str(e)))
3262+ if fatal:
3263+ amulet.raise_status(amulet.FAIL, msg)
3264+ else:
3265+ self.log.warn(msg)
3266+ return None
3267+
3268+ def publish_amqp_message_by_unit(self, sentry_unit, message,
3269+ queue="test", ssl=False,
3270+ username="testuser1",
3271+ password="changeme",
3272+ port=None):
3273+ """Publish an amqp message to a rmq juju unit.
3274+
3275+ :param sentry_unit: sentry unit pointer
3276+ :param message: amqp message string
3277+ :param queue: message queue, default to test
3278+ :param username: amqp user name, default to testuser1
3279+ :param password: amqp user password
3280+ :param ssl: boolean, default to False
3281+ :param port: amqp port, use defaults if None
3282+ :returns: None. Raises exception if publish failed.
3283+ """
3284+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
3285+ message))
3286+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
3287+ port=port,
3288+ username=username,
3289+ password=password)
3290+
3291+ # NOTE(beisner): extra debug here re: pika hang potential:
3292+ # https://github.com/pika/pika/issues/297
3293+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
3294+ self.log.debug('Defining channel...')
3295+ channel = connection.channel()
3296+ self.log.debug('Declaring queue...')
3297+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
3298+ self.log.debug('Publishing message...')
3299+ channel.basic_publish(exchange='', routing_key=queue, body=message)
3300+ self.log.debug('Closing channel...')
3301+ channel.close()
3302+ self.log.debug('Closing connection...')
3303+ connection.close()
3304+
3305+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
3306+ username="testuser1",
3307+ password="changeme",
3308+ ssl=False, port=None):
3309+ """Get an amqp message from a rmq juju unit.
3310+
3311+ :param sentry_unit: sentry unit pointer
3312+ :param queue: message queue, default to test
3313+ :param username: amqp user name, default to testuser1
3314+ :param password: amqp user password
3315+ :param ssl: boolean, default to False
3316+ :param port: amqp port, use defaults if None
3317+ :returns: amqp message body as string. Raise if get fails.
3318+ """
3319+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
3320+ port=port,
3321+ username=username,
3322+ password=password)
3323+ channel = connection.channel()
3324+ method_frame, _, body = channel.basic_get(queue)
3325+
3326+ if method_frame:
3327+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
3328+ body))
3329+ channel.basic_ack(method_frame.delivery_tag)
3330+ channel.close()
3331+ connection.close()
3332+ return body
3333+ else:
3334+ msg = 'No message retrieved.'
3335+ amulet.raise_status(amulet.FAIL, msg)
3336
3337=== removed directory 'tests/charmhelpers/contrib/ssl'
3338=== removed file 'tests/charmhelpers/contrib/ssl/__init__.py'
3339--- tests/charmhelpers/contrib/ssl/__init__.py 2015-04-13 22:11:34 +0000
3340+++ tests/charmhelpers/contrib/ssl/__init__.py 1970-01-01 00:00:00 +0000
3341@@ -1,94 +0,0 @@
3342-# Copyright 2014-2015 Canonical Limited.
3343-#
3344-# This file is part of charm-helpers.
3345-#
3346-# charm-helpers is free software: you can redistribute it and/or modify
3347-# it under the terms of the GNU Lesser General Public License version 3 as
3348-# published by the Free Software Foundation.
3349-#
3350-# charm-helpers is distributed in the hope that it will be useful,
3351-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3352-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3353-# GNU Lesser General Public License for more details.
3354-#
3355-# You should have received a copy of the GNU Lesser General Public License
3356-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3357-
3358-import subprocess
3359-from charmhelpers.core import hookenv
3360-
3361-
3362-def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=None, cn=None):
3363- """Generate selfsigned SSL keypair
3364-
3365- You must provide one of the 3 optional arguments:
3366- config, subject or cn
3367- If more than one is provided the leftmost will be used
3368-
3369- Arguments:
3370- keyfile -- (required) full path to the keyfile to be created
3371- certfile -- (required) full path to the certfile to be created
3372- keysize -- (optional) SSL key length
3373- config -- (optional) openssl configuration file
3374- subject -- (optional) dictionary with SSL subject variables
3375- cn -- (optional) cerfificate common name
3376-
3377- Required keys in subject dict:
3378- cn -- Common name (eq. FQDN)
3379-
3380- Optional keys in subject dict
3381- country -- Country Name (2 letter code)
3382- state -- State or Province Name (full name)
3383- locality -- Locality Name (eg, city)
3384- organization -- Organization Name (eg, company)
3385- organizational_unit -- Organizational Unit Name (eg, section)
3386- email -- Email Address
3387- """
3388-
3389- cmd = []
3390- if config:
3391- cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
3392- "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
3393- "-keyout", keyfile,
3394- "-out", certfile, "-config", config]
3395- elif subject:
3396- ssl_subject = ""
3397- if "country" in subject:
3398- ssl_subject = ssl_subject + "/C={}".format(subject["country"])
3399- if "state" in subject:
3400- ssl_subject = ssl_subject + "/ST={}".format(subject["state"])
3401- if "locality" in subject:
3402- ssl_subject = ssl_subject + "/L={}".format(subject["locality"])
3403- if "organization" in subject:
3404- ssl_subject = ssl_subject + "/O={}".format(subject["organization"])
3405- if "organizational_unit" in subject:
3406- ssl_subject = ssl_subject + "/OU={}".format(subject["organizational_unit"])
3407- if "cn" in subject:
3408- ssl_subject = ssl_subject + "/CN={}".format(subject["cn"])
3409- else:
3410- hookenv.log("When using \"subject\" argument you must "
3411- "provide \"cn\" field at very least")
3412- return False
3413- if "email" in subject:
3414- ssl_subject = ssl_subject + "/emailAddress={}".format(subject["email"])
3415-
3416- cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
3417- "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
3418- "-keyout", keyfile,
3419- "-out", certfile, "-subj", ssl_subject]
3420- elif cn:
3421- cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
3422- "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
3423- "-keyout", keyfile,
3424- "-out", certfile, "-subj", "/CN={}".format(cn)]
3425-
3426- if not cmd:
3427- hookenv.log("No config, subject or cn provided,"
3428- "unable to generate self signed SSL certificates")
3429- return False
3430- try:
3431- subprocess.check_call(cmd)
3432- return True
3433- except Exception as e:
3434- print("Execution of openssl command failed:\n{}".format(e))
3435- return False
3436
3437=== removed file 'tests/charmhelpers/contrib/ssl/service.py'
3438--- tests/charmhelpers/contrib/ssl/service.py 2015-04-16 21:35:24 +0000
3439+++ tests/charmhelpers/contrib/ssl/service.py 1970-01-01 00:00:00 +0000
3440@@ -1,279 +0,0 @@
3441-# Copyright 2014-2015 Canonical Limited.
3442-#
3443-# This file is part of charm-helpers.
3444-#
3445-# charm-helpers is free software: you can redistribute it and/or modify
3446-# it under the terms of the GNU Lesser General Public License version 3 as
3447-# published by the Free Software Foundation.
3448-#
3449-# charm-helpers is distributed in the hope that it will be useful,
3450-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3451-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3452-# GNU Lesser General Public License for more details.
3453-#
3454-# You should have received a copy of the GNU Lesser General Public License
3455-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3456-
3457-import os
3458-from os.path import join as path_join
3459-from os.path import exists
3460-import subprocess
3461-
3462-from charmhelpers.core.hookenv import log, DEBUG
3463-
3464-STD_CERT = "standard"
3465-
3466-# Mysql server is fairly picky about cert creation
3467-# and types, spec its creation separately for now.
3468-MYSQL_CERT = "mysql"
3469-
3470-
3471-class ServiceCA(object):
3472-
3473- default_expiry = str(365 * 2)
3474- default_ca_expiry = str(365 * 6)
3475-
3476- def __init__(self, name, ca_dir, cert_type=STD_CERT):
3477- self.name = name
3478- self.ca_dir = ca_dir
3479- self.cert_type = cert_type
3480-
3481- ###############
3482- # Hook Helper API
3483- @staticmethod
3484- def get_ca(type=STD_CERT):
3485- service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
3486- ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
3487- ca = ServiceCA(service_name, ca_path, type)
3488- ca.init()
3489- return ca
3490-
3491- @classmethod
3492- def get_service_cert(cls, type=STD_CERT):
3493- service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
3494- ca = cls.get_ca()
3495- crt, key = ca.get_or_create_cert(service_name)
3496- return crt, key, ca.get_ca_bundle()
3497-
3498- ###############
3499-
3500- def init(self):
3501- log("initializing service ca", level=DEBUG)
3502- if not exists(self.ca_dir):
3503- self._init_ca_dir(self.ca_dir)
3504- self._init_ca()
3505-
3506- @property
3507- def ca_key(self):
3508- return path_join(self.ca_dir, 'private', 'cacert.key')
3509-
3510- @property
3511- def ca_cert(self):
3512- return path_join(self.ca_dir, 'cacert.pem')
3513-
3514- @property
3515- def ca_conf(self):
3516- return path_join(self.ca_dir, 'ca.cnf')
3517-
3518- @property
3519- def signing_conf(self):
3520- return path_join(self.ca_dir, 'signing.cnf')
3521-
3522- def _init_ca_dir(self, ca_dir):
3523- os.mkdir(ca_dir)
3524- for i in ['certs', 'crl', 'newcerts', 'private']:
3525- sd = path_join(ca_dir, i)
3526- if not exists(sd):
3527- os.mkdir(sd)
3528-
3529- if not exists(path_join(ca_dir, 'serial')):
3530- with open(path_join(ca_dir, 'serial'), 'w') as fh:
3531- fh.write('02\n')
3532-
3533- if not exists(path_join(ca_dir, 'index.txt')):
3534- with open(path_join(ca_dir, 'index.txt'), 'w') as fh:
3535- fh.write('')
3536-
3537- def _init_ca(self):
3538- """Generate the root ca's cert and key.
3539- """
3540- if not exists(path_join(self.ca_dir, 'ca.cnf')):
3541- with open(path_join(self.ca_dir, 'ca.cnf'), 'w') as fh:
3542- fh.write(
3543- CA_CONF_TEMPLATE % (self.get_conf_variables()))
3544-
3545- if not exists(path_join(self.ca_dir, 'signing.cnf')):
3546- with open(path_join(self.ca_dir, 'signing.cnf'), 'w') as fh:
3547- fh.write(
3548- SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
3549-
3550- if exists(self.ca_cert) or exists(self.ca_key):
3551- raise RuntimeError("Initialized called when CA already exists")
3552- cmd = ['openssl', 'req', '-config', self.ca_conf,
3553- '-x509', '-nodes', '-newkey', 'rsa',
3554- '-days', self.default_ca_expiry,
3555- '-keyout', self.ca_key, '-out', self.ca_cert,
3556- '-outform', 'PEM']
3557- output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
3558- log("CA Init:\n %s" % output, level=DEBUG)
3559-
3560- def get_conf_variables(self):
3561- return dict(
3562- org_name="juju",
3563- org_unit_name="%s service" % self.name,
3564- common_name=self.name,
3565- ca_dir=self.ca_dir)
3566-
3567- def get_or_create_cert(self, common_name):
3568- if common_name in self:
3569- return self.get_certificate(common_name)
3570- return self.create_certificate(common_name)
3571-
3572- def create_certificate(self, common_name):
3573- if common_name in self:
3574- return self.get_certificate(common_name)
3575- key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
3576- crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
3577- csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
3578- self._create_certificate(common_name, key_p, csr_p, crt_p)
3579- return self.get_certificate(common_name)
3580-
3581- def get_certificate(self, common_name):
3582- if common_name not in self:
3583- raise ValueError("No certificate for %s" % common_name)
3584- key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
3585- crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
3586- with open(crt_p) as fh:
3587- crt = fh.read()
3588- with open(key_p) as fh:
3589- key = fh.read()
3590- return crt, key
3591-
3592- def __contains__(self, common_name):
3593- crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
3594- return exists(crt_p)
3595-
3596- def _create_certificate(self, common_name, key_p, csr_p, crt_p):
3597- template_vars = self.get_conf_variables()
3598- template_vars['common_name'] = common_name
3599- subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
3600- template_vars)
3601-
3602- log("CA Create Cert %s" % common_name, level=DEBUG)
3603- cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
3604- '-nodes', '-days', self.default_expiry,
3605- '-keyout', key_p, '-out', csr_p, '-subj', subj]
3606- subprocess.check_call(cmd, stderr=subprocess.PIPE)
3607- cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
3608- subprocess.check_call(cmd, stderr=subprocess.PIPE)
3609-
3610- log("CA Sign Cert %s" % common_name, level=DEBUG)
3611- if self.cert_type == MYSQL_CERT:
3612- cmd = ['openssl', 'x509', '-req',
3613- '-in', csr_p, '-days', self.default_expiry,
3614- '-CA', self.ca_cert, '-CAkey', self.ca_key,
3615- '-set_serial', '01', '-out', crt_p]
3616- else:
3617- cmd = ['openssl', 'ca', '-config', self.signing_conf,
3618- '-extensions', 'req_extensions',
3619- '-days', self.default_expiry, '-notext',
3620- '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
3621- log("running %s" % " ".join(cmd), level=DEBUG)
3622- subprocess.check_call(cmd, stderr=subprocess.PIPE)
3623-
3624- def get_ca_bundle(self):
3625- with open(self.ca_cert) as fh:
3626- return fh.read()
3627-
3628-
3629-CA_CONF_TEMPLATE = """
3630-[ ca ]
3631-default_ca = CA_default
3632-
3633-[ CA_default ]
3634-dir = %(ca_dir)s
3635-policy = policy_match
3636-database = $dir/index.txt
3637-serial = $dir/serial
3638-certs = $dir/certs
3639-crl_dir = $dir/crl
3640-new_certs_dir = $dir/newcerts
3641-certificate = $dir/cacert.pem
3642-private_key = $dir/private/cacert.key
3643-RANDFILE = $dir/private/.rand
3644-default_md = default
3645-
3646-[ req ]
3647-default_bits = 1024
3648-default_md = sha1
3649-
3650-prompt = no
3651-distinguished_name = ca_distinguished_name
3652-
3653-x509_extensions = ca_extensions
3654-
3655-[ ca_distinguished_name ]
3656-organizationName = %(org_name)s
3657-organizationalUnitName = %(org_unit_name)s Certificate Authority
3658-
3659-
3660-[ policy_match ]
3661-countryName = optional
3662-stateOrProvinceName = optional
3663-organizationName = match
3664-organizationalUnitName = optional
3665-commonName = supplied
3666-
3667-[ ca_extensions ]
3668-basicConstraints = critical,CA:true
3669-subjectKeyIdentifier = hash
3670-authorityKeyIdentifier = keyid:always, issuer
3671-keyUsage = cRLSign, keyCertSign
3672-"""
3673-
3674-
3675-SIGNING_CONF_TEMPLATE = """
3676-[ ca ]
3677-default_ca = CA_default
3678-
3679-[ CA_default ]
3680-dir = %(ca_dir)s
3681-policy = policy_match
3682-database = $dir/index.txt
3683-serial = $dir/serial
3684-certs = $dir/certs
3685-crl_dir = $dir/crl
3686-new_certs_dir = $dir/newcerts
3687-certificate = $dir/cacert.pem
3688-private_key = $dir/private/cacert.key
3689-RANDFILE = $dir/private/.rand
3690-default_md = default
3691-
3692-[ req ]
3693-default_bits = 1024
3694-default_md = sha1
3695-
3696-prompt = no
3697-distinguished_name = req_distinguished_name
3698-
3699-x509_extensions = req_extensions
3700-
3701-[ req_distinguished_name ]
3702-organizationName = %(org_name)s
3703-organizationalUnitName = %(org_unit_name)s machine resources
3704-commonName = %(common_name)s
3705-
3706-[ policy_match ]
3707-countryName = optional
3708-stateOrProvinceName = optional
3709-organizationName = match
3710-organizationalUnitName = optional
3711-commonName = supplied
3712-
3713-[ req_extensions ]
3714-basicConstraints = CA:false
3715-subjectKeyIdentifier = hash
3716-authorityKeyIdentifier = keyid:always, issuer
3717-keyUsage = digitalSignature, keyEncipherment, keyAgreement
3718-extendedKeyUsage = serverAuth, clientAuth
3719-"""
3720
3721=== removed directory 'tests/charmhelpers/core'
3722=== removed file 'tests/charmhelpers/core/__init__.py'
3723--- tests/charmhelpers/core/__init__.py 2015-04-13 22:11:34 +0000
3724+++ tests/charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000
3725@@ -1,15 +0,0 @@
3726-# Copyright 2014-2015 Canonical Limited.
3727-#
3728-# This file is part of charm-helpers.
3729-#
3730-# charm-helpers is free software: you can redistribute it and/or modify
3731-# it under the terms of the GNU Lesser General Public License version 3 as
3732-# published by the Free Software Foundation.
3733-#
3734-# charm-helpers is distributed in the hope that it will be useful,
3735-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3736-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3737-# GNU Lesser General Public License for more details.
3738-#
3739-# You should have received a copy of the GNU Lesser General Public License
3740-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3741
3742=== removed file 'tests/charmhelpers/core/decorators.py'
3743--- tests/charmhelpers/core/decorators.py 2015-04-13 22:11:34 +0000
3744+++ tests/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
3745@@ -1,57 +0,0 @@
3746-# Copyright 2014-2015 Canonical Limited.
3747-#
3748-# This file is part of charm-helpers.
3749-#
3750-# charm-helpers is free software: you can redistribute it and/or modify
3751-# it under the terms of the GNU Lesser General Public License version 3 as
3752-# published by the Free Software Foundation.
3753-#
3754-# charm-helpers is distributed in the hope that it will be useful,
3755-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3756-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3757-# GNU Lesser General Public License for more details.
3758-#
3759-# You should have received a copy of the GNU Lesser General Public License
3760-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3761-
3762-#
3763-# Copyright 2014 Canonical Ltd.
3764-#
3765-# Authors:
3766-# Edward Hope-Morley <opentastic@gmail.com>
3767-#
3768-
3769-import time
3770-
3771-from charmhelpers.core.hookenv import (
3772- log,
3773- INFO,
3774-)
3775-
3776-
3777-def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
3778- """If the decorated function raises exception exc_type, allow num_retries
3779- retry attempts before raise the exception.
3780- """
3781- def _retry_on_exception_inner_1(f):
3782- def _retry_on_exception_inner_2(*args, **kwargs):
3783- retries = num_retries
3784- multiplier = 1
3785- while True:
3786- try:
3787- return f(*args, **kwargs)
3788- except exc_type:
3789- if not retries:
3790- raise
3791-
3792- delay = base_delay * multiplier
3793- multiplier += 1
3794- log("Retrying '%s' %d more times (delay=%s)" %
3795- (f.__name__, retries, delay), level=INFO)
3796- retries -= 1
3797- if delay:
3798- time.sleep(delay)
3799-
3800- return _retry_on_exception_inner_2
3801-
3802- return _retry_on_exception_inner_1
3803
3804=== removed file 'tests/charmhelpers/core/files.py'
3805--- tests/charmhelpers/core/files.py 2015-07-29 10:48:05 +0000
3806+++ tests/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
3807@@ -1,45 +0,0 @@
3808-#!/usr/bin/env python
3809-# -*- coding: utf-8 -*-
3810-
3811-# Copyright 2014-2015 Canonical Limited.
3812-#
3813-# This file is part of charm-helpers.
3814-#
3815-# charm-helpers is free software: you can redistribute it and/or modify
3816-# it under the terms of the GNU Lesser General Public License version 3 as
3817-# published by the Free Software Foundation.
3818-#
3819-# charm-helpers is distributed in the hope that it will be useful,
3820-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3821-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3822-# GNU Lesser General Public License for more details.
3823-#
3824-# You should have received a copy of the GNU Lesser General Public License
3825-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3826-
3827-__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
3828-
3829-import os
3830-import subprocess
3831-
3832-
3833-def sed(filename, before, after, flags='g'):
3834- """
3835- Search and replaces the given pattern on filename.
3836-
3837- :param filename: relative or absolute file path.
3838- :param before: expression to be replaced (see 'man sed')
3839- :param after: expression to replace with (see 'man sed')
3840- :param flags: sed-compatible regex flags in example, to make
3841- the search and replace case insensitive, specify ``flags="i"``.
3842- The ``g`` flag is always specified regardless, so you do not
3843- need to remember to include it when overriding this parameter.
3844- :returns: If the sed command exit code was zero then return,
3845- otherwise raise CalledProcessError.
3846- """
3847- expression = r's/{0}/{1}/{2}'.format(before,
3848- after, flags)
3849-
3850- return subprocess.check_call(["sed", "-i", "-r", "-e",
3851- expression,
3852- os.path.expanduser(filename)])
3853
3854=== removed file 'tests/charmhelpers/core/fstab.py'
3855--- tests/charmhelpers/core/fstab.py 2015-04-13 22:11:34 +0000
3856+++ tests/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
3857@@ -1,134 +0,0 @@
3858-#!/usr/bin/env python
3859-# -*- coding: utf-8 -*-
3860-
3861-# Copyright 2014-2015 Canonical Limited.
3862-#
3863-# This file is part of charm-helpers.
3864-#
3865-# charm-helpers is free software: you can redistribute it and/or modify
3866-# it under the terms of the GNU Lesser General Public License version 3 as
3867-# published by the Free Software Foundation.
3868-#
3869-# charm-helpers is distributed in the hope that it will be useful,
3870-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3871-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3872-# GNU Lesser General Public License for more details.
3873-#
3874-# You should have received a copy of the GNU Lesser General Public License
3875-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3876-
3877-import io
3878-import os
3879-
3880-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
3881-
3882-
3883-class Fstab(io.FileIO):
3884- """This class extends file in order to implement a file reader/writer
3885- for file `/etc/fstab`
3886- """
3887-
3888- class Entry(object):
3889- """Entry class represents a non-comment line on the `/etc/fstab` file
3890- """
3891- def __init__(self, device, mountpoint, filesystem,
3892- options, d=0, p=0):
3893- self.device = device
3894- self.mountpoint = mountpoint
3895- self.filesystem = filesystem
3896-
3897- if not options:
3898- options = "defaults"
3899-
3900- self.options = options
3901- self.d = int(d)
3902- self.p = int(p)
3903-
3904- def __eq__(self, o):
3905- return str(self) == str(o)
3906-
3907- def __str__(self):
3908- return "{} {} {} {} {} {}".format(self.device,
3909- self.mountpoint,
3910- self.filesystem,
3911- self.options,
3912- self.d,
3913- self.p)
3914-
3915- DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
3916-
3917- def __init__(self, path=None):
3918- if path:
3919- self._path = path
3920- else:
3921- self._path = self.DEFAULT_PATH
3922- super(Fstab, self).__init__(self._path, 'rb+')
3923-
3924- def _hydrate_entry(self, line):
3925- # NOTE: use split with no arguments to split on any
3926- # whitespace including tabs
3927- return Fstab.Entry(*filter(
3928- lambda x: x not in ('', None),
3929- line.strip("\n").split()))
3930-
3931- @property
3932- def entries(self):
3933- self.seek(0)
3934- for line in self.readlines():
3935- line = line.decode('us-ascii')
3936- try:
3937- if line.strip() and not line.strip().startswith("#"):
3938- yield self._hydrate_entry(line)
3939- except ValueError:
3940- pass
3941-
3942- def get_entry_by_attr(self, attr, value):
3943- for entry in self.entries:
3944- e_attr = getattr(entry, attr)
3945- if e_attr == value:
3946- return entry
3947- return None
3948-
3949- def add_entry(self, entry):
3950- if self.get_entry_by_attr('device', entry.device):
3951- return False
3952-
3953- self.write((str(entry) + '\n').encode('us-ascii'))
3954- self.truncate()
3955- return entry
3956-
3957- def remove_entry(self, entry):
3958- self.seek(0)
3959-
3960- lines = [l.decode('us-ascii') for l in self.readlines()]
3961-
3962- found = False
3963- for index, line in enumerate(lines):
3964- if line.strip() and not line.strip().startswith("#"):
3965- if self._hydrate_entry(line) == entry:
3966- found = True
3967- break
3968-
3969- if not found:
3970- return False
3971-
3972- lines.remove(line)
3973-
3974- self.seek(0)
3975- self.write(''.join(lines).encode('us-ascii'))
3976- self.truncate()
3977- return True
3978-
3979- @classmethod
3980- def remove_by_mountpoint(cls, mountpoint, path=None):
3981- fstab = cls(path=path)
3982- entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
3983- if entry:
3984- return fstab.remove_entry(entry)
3985- return False
3986-
3987- @classmethod
3988- def add(cls, device, mountpoint, filesystem, options=None, path=None):
3989- return cls(path=path).add_entry(Fstab.Entry(device,
3990- mountpoint, filesystem,
3991- options=options))
3992
3993=== removed file 'tests/charmhelpers/core/hookenv.py'
3994--- tests/charmhelpers/core/hookenv.py 2015-08-19 13:49:53 +0000
3995+++ tests/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
3996@@ -1,896 +0,0 @@
3997-# Copyright 2014-2015 Canonical Limited.
3998-#
3999-# This file is part of charm-helpers.
4000-#
4001-# charm-helpers is free software: you can redistribute it and/or modify
4002-# it under the terms of the GNU Lesser General Public License version 3 as
4003-# published by the Free Software Foundation.
4004-#
4005-# charm-helpers is distributed in the hope that it will be useful,
4006-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4007-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4008-# GNU Lesser General Public License for more details.
4009-#
4010-# You should have received a copy of the GNU Lesser General Public License
4011-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4012-
4013-"Interactions with the Juju environment"
4014-# Copyright 2013 Canonical Ltd.
4015-#
4016-# Authors:
4017-# Charm Helpers Developers <juju@lists.ubuntu.com>
4018-
4019-from __future__ import print_function
4020-import copy
4021-from distutils.version import LooseVersion
4022-from functools import wraps
4023-import glob
4024-import os
4025-import json
4026-import yaml
4027-import subprocess
4028-import sys
4029-import errno
4030-import tempfile
4031-from subprocess import CalledProcessError
4032-
4033-import six
4034-if not six.PY3:
4035- from UserDict import UserDict
4036-else:
4037- from collections import UserDict
4038-
4039-CRITICAL = "CRITICAL"
4040-ERROR = "ERROR"
4041-WARNING = "WARNING"
4042-INFO = "INFO"
4043-DEBUG = "DEBUG"
4044-MARKER = object()
4045-
4046-cache = {}
4047-
4048-
4049-def cached(func):
4050- """Cache return values for multiple executions of func + args
4051-
4052- For example::
4053-
4054- @cached
4055- def unit_get(attribute):
4056- pass
4057-
4058- unit_get('test')
4059-
4060- will cache the result of unit_get + 'test' for future calls.
4061- """
4062- @wraps(func)
4063- def wrapper(*args, **kwargs):
4064- global cache
4065- key = str((func, args, kwargs))
4066- try:
4067- return cache[key]
4068- except KeyError:
4069- pass # Drop out of the exception handler scope.
4070- res = func(*args, **kwargs)
4071- cache[key] = res
4072- return res
4073- wrapper._wrapped = func
4074- return wrapper
4075-
4076-
4077-def flush(key):
4078- """Flushes any entries from function cache where the
4079- key is found in the function+args """
4080- flush_list = []
4081- for item in cache:
4082- if key in item:
4083- flush_list.append(item)
4084- for item in flush_list:
4085- del cache[item]
4086-
4087-
4088-def log(message, level=None):
4089- """Write a message to the juju log"""
4090- command = ['juju-log']
4091- if level:
4092- command += ['-l', level]
4093- if not isinstance(message, six.string_types):
4094- message = repr(message)
4095- command += [message]
4096- # Missing juju-log should not cause failures in unit tests
4097- # Send log output to stderr
4098- try:
4099- subprocess.call(command)
4100- except OSError as e:
4101- if e.errno == errno.ENOENT:
4102- if level:
4103- message = "{}: {}".format(level, message)
4104- message = "juju-log: {}".format(message)
4105- print(message, file=sys.stderr)
4106- else:
4107- raise
4108-
4109-
4110-class Serializable(UserDict):
4111- """Wrapper, an object that can be serialized to yaml or json"""
4112-
4113- def __init__(self, obj):
4114- # wrap the object
4115- UserDict.__init__(self)
4116- self.data = obj
4117-
4118- def __getattr__(self, attr):
4119- # See if this object has attribute.
4120- if attr in ("json", "yaml", "data"):
4121- return self.__dict__[attr]
4122- # Check for attribute in wrapped object.
4123- got = getattr(self.data, attr, MARKER)
4124- if got is not MARKER:
4125- return got
4126- # Proxy to the wrapped object via dict interface.
4127- try:
4128- return self.data[attr]
4129- except KeyError:
4130- raise AttributeError(attr)
4131-
4132- def __getstate__(self):
4133- # Pickle as a standard dictionary.
4134- return self.data
4135-
4136- def __setstate__(self, state):
4137- # Unpickle into our wrapper.
4138- self.data = state
4139-
4140- def json(self):
4141- """Serialize the object to json"""
4142- return json.dumps(self.data)
4143-
4144- def yaml(self):
4145- """Serialize the object to yaml"""
4146- return yaml.dump(self.data)
4147-
4148-
4149-def execution_environment():
4150- """A convenient bundling of the current execution context"""
4151- context = {}
4152- context['conf'] = config()
4153- if relation_id():
4154- context['reltype'] = relation_type()
4155- context['relid'] = relation_id()
4156- context['rel'] = relation_get()
4157- context['unit'] = local_unit()
4158- context['rels'] = relations()
4159- context['env'] = os.environ
4160- return context
4161-
4162-
4163-def in_relation_hook():
4164- """Determine whether we're running in a relation hook"""
4165- return 'JUJU_RELATION' in os.environ
4166-
4167-
4168-def relation_type():
4169- """The scope for the current relation hook"""
4170- return os.environ.get('JUJU_RELATION', None)
4171-
4172-
4173-@cached
4174-def relation_id(relation_name=None, service_or_unit=None):
4175- """The relation ID for the current or a specified relation"""
4176- if not relation_name and not service_or_unit:
4177- return os.environ.get('JUJU_RELATION_ID', None)
4178- elif relation_name and service_or_unit:
4179- service_name = service_or_unit.split('/')[0]
4180- for relid in relation_ids(relation_name):
4181- remote_service = remote_service_name(relid)
4182- if remote_service == service_name:
4183- return relid
4184- else:
4185- raise ValueError('Must specify neither or both of relation_name and service_or_unit')
4186-
4187-
4188-def local_unit():
4189- """Local unit ID"""
4190- return os.environ['JUJU_UNIT_NAME']
4191-
4192-
4193-def remote_unit():
4194- """The remote unit for the current relation hook"""
4195- return os.environ.get('JUJU_REMOTE_UNIT', None)
4196-
4197-
4198-def service_name():
4199- """The name service group this unit belongs to"""
4200- return local_unit().split('/')[0]
4201-
4202-
4203-@cached
4204-def remote_service_name(relid=None):
4205- """The remote service name for a given relation-id (or the current relation)"""
4206- if relid is None:
4207- unit = remote_unit()
4208- else:
4209- units = related_units(relid)
4210- unit = units[0] if units else None
4211- return unit.split('/')[0] if unit else None
4212-
4213-
4214-def hook_name():
4215- """The name of the currently executing hook"""
4216- return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
4217-
4218-
4219-class Config(dict):
4220- """A dictionary representation of the charm's config.yaml, with some
4221- extra features:
4222-
4223- - See which values in the dictionary have changed since the previous hook.
4224- - For values that have changed, see what the previous value was.
4225- - Store arbitrary data for use in a later hook.
4226-
4227- NOTE: Do not instantiate this object directly - instead call
4228- ``hookenv.config()``, which will return an instance of :class:`Config`.
4229-
4230- Example usage::
4231-
4232- >>> # inside a hook
4233- >>> from charmhelpers.core import hookenv
4234- >>> config = hookenv.config()
4235- >>> config['foo']
4236- 'bar'
4237- >>> # store a new key/value for later use
4238- >>> config['mykey'] = 'myval'
4239-
4240-
4241- >>> # user runs `juju set mycharm foo=baz`
4242- >>> # now we're inside subsequent config-changed hook
4243- >>> config = hookenv.config()
4244- >>> config['foo']
4245- 'baz'
4246- >>> # test to see if this val has changed since last hook
4247- >>> config.changed('foo')
4248- True
4249- >>> # what was the previous value?
4250- >>> config.previous('foo')
4251- 'bar'
4252- >>> # keys/values that we add are preserved across hooks
4253- >>> config['mykey']
4254- 'myval'
4255-
4256- """
4257- CONFIG_FILE_NAME = '.juju-persistent-config'
4258-
4259- def __init__(self, *args, **kw):
4260- super(Config, self).__init__(*args, **kw)
4261- self.implicit_save = True
4262- self._prev_dict = None
4263- self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
4264- if os.path.exists(self.path):
4265- self.load_previous()
4266- atexit(self._implicit_save)
4267-
4268- def load_previous(self, path=None):
4269- """Load previous copy of config from disk.
4270-
4271- In normal usage you don't need to call this method directly - it
4272- is called automatically at object initialization.
4273-
4274- :param path:
4275-
4276- File path from which to load the previous config. If `None`,
4277- config is loaded from the default location. If `path` is
4278- specified, subsequent `save()` calls will write to the same
4279- path.
4280-
4281- """
4282- self.path = path or self.path
4283- with open(self.path) as f:
4284- self._prev_dict = json.load(f)
4285- for k, v in copy.deepcopy(self._prev_dict).items():
4286- if k not in self:
4287- self[k] = v
4288-
4289- def changed(self, key):
4290- """Return True if the current value for this key is different from
4291- the previous value.
4292-
4293- """
4294- if self._prev_dict is None:
4295- return True
4296- return self.previous(key) != self.get(key)
4297-
4298- def previous(self, key):
4299- """Return previous value for this key, or None if there
4300- is no previous value.
4301-
4302- """
4303- if self._prev_dict:
4304- return self._prev_dict.get(key)
4305- return None
4306-
4307- def save(self):
4308- """Save this config to disk.
4309-
4310- If the charm is using the :mod:`Services Framework <services.base>`
4311- or :meth:'@hook <Hooks.hook>' decorator, this
4312- is called automatically at the end of successful hook execution.
4313- Otherwise, it should be called directly by user code.
4314-
4315- To disable automatic saves, set ``implicit_save=False`` on this
4316- instance.
4317-
4318- """
4319- with open(self.path, 'w') as f:
4320- json.dump(self, f)
4321-
4322- def _implicit_save(self):
4323- if self.implicit_save:
4324- self.save()
4325-
4326-
4327-@cached
4328-def config(scope=None):
4329- """Juju charm configuration"""
4330- config_cmd_line = ['config-get']
4331- if scope is not None:
4332- config_cmd_line.append(scope)
4333- config_cmd_line.append('--format=json')
4334- try:
4335- config_data = json.loads(
4336- subprocess.check_output(config_cmd_line).decode('UTF-8'))
4337- if scope is not None:
4338- return config_data
4339- return Config(config_data)
4340- except ValueError:
4341- return None
4342-
4343-
4344-@cached
4345-def relation_get(attribute=None, unit=None, rid=None):
4346- """Get relation information"""
4347- _args = ['relation-get', '--format=json']
4348- if rid:
4349- _args.append('-r')
4350- _args.append(rid)
4351- _args.append(attribute or '-')
4352- if unit:
4353- _args.append(unit)
4354- try:
4355- return json.loads(subprocess.check_output(_args).decode('UTF-8'))
4356- except ValueError:
4357- return None
4358- except CalledProcessError as e:
4359- if e.returncode == 2:
4360- return None
4361- raise
4362-
4363-
4364-def relation_set(relation_id=None, relation_settings=None, **kwargs):
4365- """Set relation information for the current unit"""
4366- relation_settings = relation_settings if relation_settings else {}
4367- relation_cmd_line = ['relation-set']
4368- accepts_file = "--file" in subprocess.check_output(
4369- relation_cmd_line + ["--help"], universal_newlines=True)
4370- if relation_id is not None:
4371- relation_cmd_line.extend(('-r', relation_id))
4372- settings = relation_settings.copy()
4373- settings.update(kwargs)
4374- for key, value in settings.items():
4375- # Force value to be a string: it always should, but some call
4376- # sites pass in things like dicts or numbers.
4377- if value is not None:
4378- settings[key] = "{}".format(value)
4379- if accepts_file:
4380- # --file was introduced in Juju 1.23.2. Use it by default if
4381- # available, since otherwise we'll break if the relation data is
4382- # too big. Ideally we should tell relation-set to read the data from
4383- # stdin, but that feature is broken in 1.23.2: Bug #1454678.
4384- with tempfile.NamedTemporaryFile(delete=False) as settings_file:
4385- settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
4386- subprocess.check_call(
4387- relation_cmd_line + ["--file", settings_file.name])
4388- os.remove(settings_file.name)
4389- else:
4390- for key, value in settings.items():
4391- if value is None:
4392- relation_cmd_line.append('{}='.format(key))
4393- else:
4394- relation_cmd_line.append('{}={}'.format(key, value))
4395- subprocess.check_call(relation_cmd_line)
4396- # Flush cache of any relation-gets for local unit
4397- flush(local_unit())
4398-
4399-
4400-def relation_clear(r_id=None):
4401- ''' Clears any relation data already set on relation r_id '''
4402- settings = relation_get(rid=r_id,
4403- unit=local_unit())
4404- for setting in settings:
4405- if setting not in ['public-address', 'private-address']:
4406- settings[setting] = None
4407- relation_set(relation_id=r_id,
4408- **settings)
4409-
4410-
4411-@cached
4412-def relation_ids(reltype=None):
4413- """A list of relation_ids"""
4414- reltype = reltype or relation_type()
4415- relid_cmd_line = ['relation-ids', '--format=json']
4416- if reltype is not None:
4417- relid_cmd_line.append(reltype)
4418- return json.loads(
4419- subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
4420- return []
4421-
4422-
4423-@cached
4424-def related_units(relid=None):
4425- """A list of related units"""
4426- relid = relid or relation_id()
4427- units_cmd_line = ['relation-list', '--format=json']
4428- if relid is not None:
4429- units_cmd_line.extend(('-r', relid))
4430- return json.loads(
4431- subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
4432-
4433-
4434-@cached
4435-def relation_for_unit(unit=None, rid=None):
4436- """Get the json represenation of a unit's relation"""
4437- unit = unit or remote_unit()
4438- relation = relation_get(unit=unit, rid=rid)
4439- for key in relation:
4440- if key.endswith('-list'):
4441- relation[key] = relation[key].split()
4442- relation['__unit__'] = unit
4443- return relation
4444-
4445-
4446-@cached
4447-def relations_for_id(relid=None):
4448- """Get relations of a specific relation ID"""
4449- relation_data = []
4450- relid = relid or relation_ids()
4451- for unit in related_units(relid):
4452- unit_data = relation_for_unit(unit, relid)
4453- unit_data['__relid__'] = relid
4454- relation_data.append(unit_data)
4455- return relation_data
4456-
4457-
4458-@cached
4459-def relations_of_type(reltype=None):
4460- """Get relations of a specific type"""
4461- relation_data = []
4462- reltype = reltype or relation_type()
4463- for relid in relation_ids(reltype):
4464- for relation in relations_for_id(relid):
4465- relation['__relid__'] = relid
4466- relation_data.append(relation)
4467- return relation_data
4468-
4469-
4470-@cached
4471-def metadata():
4472- """Get the current charm metadata.yaml contents as a python object"""
4473- with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
4474- return yaml.safe_load(md)
4475-
4476-
4477-@cached
4478-def relation_types():
4479- """Get a list of relation types supported by this charm"""
4480- rel_types = []
4481- md = metadata()
4482- for key in ('provides', 'requires', 'peers'):
4483- section = md.get(key)
4484- if section:
4485- rel_types.extend(section.keys())
4486- return rel_types
4487-
4488-
4489-@cached
4490-def relation_to_interface(relation_name):
4491- """
4492- Given the name of a relation, return the interface that relation uses.
4493-
4494- :returns: The interface name, or ``None``.
4495- """
4496- return relation_to_role_and_interface(relation_name)[1]
4497-
4498-
4499-@cached
4500-def relation_to_role_and_interface(relation_name):
4501- """
4502- Given the name of a relation, return the role and the name of the interface
4503- that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
4504-
4505- :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
4506- """
4507- _metadata = metadata()
4508- for role in ('provides', 'requires', 'peer'):
4509- interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
4510- if interface:
4511- return role, interface
4512- return None, None
4513-
4514-
4515-@cached
4516-def role_and_interface_to_relations(role, interface_name):
4517- """
4518- Given a role and interface name, return a list of relation names for the
4519- current charm that use that interface under that role (where role is one
4520- of ``provides``, ``requires``, or ``peer``).
4521-
4522- :returns: A list of relation names.
4523- """
4524- _metadata = metadata()
4525- results = []
4526- for relation_name, relation in _metadata.get(role, {}).items():
4527- if relation['interface'] == interface_name:
4528- results.append(relation_name)
4529- return results
4530-
4531-
4532-@cached
4533-def interface_to_relations(interface_name):
4534- """
4535- Given an interface, return a list of relation names for the current
4536- charm that use that interface.
4537-
4538- :returns: A list of relation names.
4539- """
4540- results = []
4541- for role in ('provides', 'requires', 'peer'):
4542- results.extend(role_and_interface_to_relations(role, interface_name))
4543- return results
4544-
4545-
4546-@cached
4547-def charm_name():
4548- """Get the name of the current charm as is specified on metadata.yaml"""
4549- return metadata().get('name')
4550-
4551-
4552-@cached
4553-def relations():
4554- """Get a nested dictionary of relation data for all related units"""
4555- rels = {}
4556- for reltype in relation_types():
4557- relids = {}
4558- for relid in relation_ids(reltype):
4559- units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
4560- for unit in related_units(relid):
4561- reldata = relation_get(unit=unit, rid=relid)
4562- units[unit] = reldata
4563- relids[relid] = units
4564- rels[reltype] = relids
4565- return rels
4566-
4567-
4568-@cached
4569-def is_relation_made(relation, keys='private-address'):
4570- '''
4571- Determine whether a relation is established by checking for
4572- presence of key(s). If a list of keys is provided, they
4573- must all be present for the relation to be identified as made
4574- '''
4575- if isinstance(keys, str):
4576- keys = [keys]
4577- for r_id in relation_ids(relation):
4578- for unit in related_units(r_id):
4579- context = {}
4580- for k in keys:
4581- context[k] = relation_get(k, rid=r_id,
4582- unit=unit)
4583- if None not in context.values():
4584- return True
4585- return False
4586-
4587-
4588-def open_port(port, protocol="TCP"):
4589- """Open a service network port"""
4590- _args = ['open-port']
4591- _args.append('{}/{}'.format(port, protocol))
4592- subprocess.check_call(_args)
4593-
4594-
4595-def close_port(port, protocol="TCP"):
4596- """Close a service network port"""
4597- _args = ['close-port']
4598- _args.append('{}/{}'.format(port, protocol))
4599- subprocess.check_call(_args)
4600-
4601-
4602-@cached
4603-def unit_get(attribute):
4604- """Get the unit ID for the remote unit"""
4605- _args = ['unit-get', '--format=json', attribute]
4606- try:
4607- return json.loads(subprocess.check_output(_args).decode('UTF-8'))
4608- except ValueError:
4609- return None
4610-
4611-
4612-def unit_public_ip():
4613- """Get this unit's public IP address"""
4614- return unit_get('public-address')
4615-
4616-
4617-def unit_private_ip():
4618- """Get this unit's private IP address"""
4619- return unit_get('private-address')
4620-
4621-
4622-class UnregisteredHookError(Exception):
4623- """Raised when an undefined hook is called"""
4624- pass
4625-
4626-
4627-class Hooks(object):
4628- """A convenient handler for hook functions.
4629-
4630- Example::
4631-
4632- hooks = Hooks()
4633-
4634- # register a hook, taking its name from the function name
4635- @hooks.hook()
4636- def install():
4637- pass # your code here
4638-
4639- # register a hook, providing a custom hook name
4640- @hooks.hook("config-changed")
4641- def config_changed():
4642- pass # your code here
4643-
4644- if __name__ == "__main__":
4645- # execute a hook based on the name the program is called by
4646- hooks.execute(sys.argv)
4647- """
4648-
4649- def __init__(self, config_save=None):
4650- super(Hooks, self).__init__()
4651- self._hooks = {}
4652-
4653- # For unknown reasons, we allow the Hooks constructor to override
4654- # config().implicit_save.
4655- if config_save is not None:
4656- config().implicit_save = config_save
4657-
4658- def register(self, name, function):
4659- """Register a hook"""
4660- self._hooks[name] = function
4661-
4662- def execute(self, args):
4663- """Execute a registered hook based on args[0]"""
4664- _run_atstart()
4665- hook_name = os.path.basename(args[0])
4666- if hook_name in self._hooks:
4667- try:
4668- self._hooks[hook_name]()
4669- except SystemExit as x:
4670- if x.code is None or x.code == 0:
4671- _run_atexit()
4672- raise
4673- _run_atexit()
4674- else:
4675- raise UnregisteredHookError(hook_name)
4676-
4677- def hook(self, *hook_names):
4678- """Decorator, registering them as hooks"""
4679- def wrapper(decorated):
4680- for hook_name in hook_names:
4681- self.register(hook_name, decorated)
4682- else:
4683- self.register(decorated.__name__, decorated)
4684- if '_' in decorated.__name__:
4685- self.register(
4686- decorated.__name__.replace('_', '-'), decorated)
4687- return decorated
4688- return wrapper
4689-
4690-
4691-def charm_dir():
4692- """Return the root directory of the current charm"""
4693- return os.environ.get('CHARM_DIR')
4694-
4695-
4696-@cached
4697-def action_get(key=None):
4698- """Gets the value of an action parameter, or all key/value param pairs"""
4699- cmd = ['action-get']
4700- if key is not None:
4701- cmd.append(key)
4702- cmd.append('--format=json')
4703- action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
4704- return action_data
4705-
4706-
4707-def action_set(values):
4708- """Sets the values to be returned after the action finishes"""
4709- cmd = ['action-set']
4710- for k, v in list(values.items()):
4711- cmd.append('{}={}'.format(k, v))
4712- subprocess.check_call(cmd)
4713-
4714-
4715-def action_fail(message):
4716- """Sets the action status to failed and sets the error message.
4717-
4718- The results set by action_set are preserved."""
4719- subprocess.check_call(['action-fail', message])
4720-
4721-
4722-def action_name():
4723- """Get the name of the currently executing action."""
4724- return os.environ.get('JUJU_ACTION_NAME')
4725-
4726-
4727-def action_uuid():
4728- """Get the UUID of the currently executing action."""
4729- return os.environ.get('JUJU_ACTION_UUID')
4730-
4731-
4732-def action_tag():
4733- """Get the tag for the currently executing action."""
4734- return os.environ.get('JUJU_ACTION_TAG')
4735-
4736-
4737-def status_set(workload_state, message):
4738- """Set the workload state with a message
4739-
4740- Use status-set to set the workload state with a message which is visible
4741- to the user via juju status. If the status-set command is not found then
4742- assume this is juju < 1.23 and juju-log the message unstead.
4743-
4744- workload_state -- valid juju workload state.
4745- message -- status update message
4746- """
4747- valid_states = ['maintenance', 'blocked', 'waiting', 'active']
4748- if workload_state not in valid_states:
4749- raise ValueError(
4750- '{!r} is not a valid workload state'.format(workload_state)
4751- )
4752- cmd = ['status-set', workload_state, message]
4753- try:
4754- ret = subprocess.call(cmd)
4755- if ret == 0:
4756- return
4757- except OSError as e:
4758- if e.errno != errno.ENOENT:
4759- raise
4760- log_message = 'status-set failed: {} {}'.format(workload_state,
4761- message)
4762- log(log_message, level='INFO')
4763-
4764-
4765-def status_get():
4766- """Retrieve the previously set juju workload state
4767-
4768- If the status-set command is not found then assume this is juju < 1.23 and
4769- return 'unknown'
4770- """
4771- cmd = ['status-get']
4772- try:
4773- raw_status = subprocess.check_output(cmd, universal_newlines=True)
4774- status = raw_status.rstrip()
4775- return status
4776- except OSError as e:
4777- if e.errno == errno.ENOENT:
4778- return 'unknown'
4779- else:
4780- raise
4781-
4782-
4783-def translate_exc(from_exc, to_exc):
4784- def inner_translate_exc1(f):
4785- def inner_translate_exc2(*args, **kwargs):
4786- try:
4787- return f(*args, **kwargs)
4788- except from_exc:
4789- raise to_exc
4790-
4791- return inner_translate_exc2
4792-
4793- return inner_translate_exc1
4794-
4795-
4796-@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4797-def is_leader():
4798- """Does the current unit hold the juju leadership
4799-
4800- Uses juju to determine whether the current unit is the leader of its peers
4801- """
4802- cmd = ['is-leader', '--format=json']
4803- return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
4804-
4805-
4806-@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4807-def leader_get(attribute=None):
4808- """Juju leader get value(s)"""
4809- cmd = ['leader-get', '--format=json'] + [attribute or '-']
4810- return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
4811-
4812-
4813-@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
4814-def leader_set(settings=None, **kwargs):
4815- """Juju leader set value(s)"""
4816- # Don't log secrets.
4817- # log("Juju leader-set '%s'" % (settings), level=DEBUG)
4818- cmd = ['leader-set']
4819- settings = settings or {}
4820- settings.update(kwargs)
4821- for k, v in settings.items():
4822- if v is None:
4823- cmd.append('{}='.format(k))
4824- else:
4825- cmd.append('{}={}'.format(k, v))
4826- subprocess.check_call(cmd)
4827-
4828-
4829-@cached
4830-def juju_version():
4831- """Full version string (eg. '1.23.3.1-trusty-amd64')"""
4832- # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
4833- jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
4834- return subprocess.check_output([jujud, 'version'],
4835- universal_newlines=True).strip()
4836-
4837-
4838-@cached
4839-def has_juju_version(minimum_version):
4840- """Return True if the Juju version is at least the provided version"""
4841- return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
4842-
4843-
4844-_atexit = []
4845-_atstart = []
4846-
4847-
4848-def atstart(callback, *args, **kwargs):
4849- '''Schedule a callback to run before the main hook.
4850-
4851- Callbacks are run in the order they were added.
4852-
4853- This is useful for modules and classes to perform initialization
4854- and inject behavior. In particular:
4855-
4856- - Run common code before all of your hooks, such as logging
4857- the hook name or interesting relation data.
4858- - Defer object or module initialization that requires a hook
4859- context until we know there actually is a hook context,
4860- making testing easier.
4861- - Rather than requiring charm authors to include boilerplate to
4862- invoke your helper's behavior, have it run automatically if
4863- your object is instantiated or module imported.
4864-
4865- This is not at all useful after your hook framework as been launched.
4866- '''
4867- global _atstart
4868- _atstart.append((callback, args, kwargs))
4869-
4870-
4871-def atexit(callback, *args, **kwargs):
4872- '''Schedule a callback to run on successful hook completion.
4873-
4874- Callbacks are run in the reverse order that they were added.'''
4875- _atexit.append((callback, args, kwargs))
4876-
4877-
4878-def _run_atstart():
4879- '''Hook frameworks must invoke this before running the main hook body.'''
4880- global _atstart
4881- for callback, args, kwargs in _atstart:
4882- callback(*args, **kwargs)
4883- del _atstart[:]
4884-
4885-
4886-def _run_atexit():
4887- '''Hook frameworks must invoke this after the main hook body has
4888- successfully completed. Do not invoke it if the hook fails.'''
4889- global _atexit
4890- for callback, args, kwargs in reversed(_atexit):
4891- callback(*args, **kwargs)
4892- del _atexit[:]
4893
4894=== removed file 'tests/charmhelpers/core/host.py'
4895--- tests/charmhelpers/core/host.py 2015-08-19 13:49:53 +0000
4896+++ tests/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
4897@@ -1,570 +0,0 @@
4898-# Copyright 2014-2015 Canonical Limited.
4899-#
4900-# This file is part of charm-helpers.
4901-#
4902-# charm-helpers is free software: you can redistribute it and/or modify
4903-# it under the terms of the GNU Lesser General Public License version 3 as
4904-# published by the Free Software Foundation.
4905-#
4906-# charm-helpers is distributed in the hope that it will be useful,
4907-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4908-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4909-# GNU Lesser General Public License for more details.
4910-#
4911-# You should have received a copy of the GNU Lesser General Public License
4912-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4913-
4914-"""Tools for working with the host system"""
4915-# Copyright 2012 Canonical Ltd.
4916-#
4917-# Authors:
4918-# Nick Moffitt <nick.moffitt@canonical.com>
4919-# Matthew Wedgwood <matthew.wedgwood@canonical.com>
4920-
4921-import os
4922-import re
4923-import pwd
4924-import glob
4925-import grp
4926-import random
4927-import string
4928-import subprocess
4929-import hashlib
4930-from contextlib import contextmanager
4931-from collections import OrderedDict
4932-
4933-import six
4934-
4935-from .hookenv import log
4936-from .fstab import Fstab
4937-
4938-
4939-def service_start(service_name):
4940- """Start a system service"""
4941- return service('start', service_name)
4942-
4943-
4944-def service_stop(service_name):
4945- """Stop a system service"""
4946- return service('stop', service_name)
4947-
4948-
4949-def service_restart(service_name):
4950- """Restart a system service"""
4951- return service('restart', service_name)
4952-
4953-
4954-def service_reload(service_name, restart_on_failure=False):
4955- """Reload a system service, optionally falling back to restart if
4956- reload fails"""
4957- service_result = service('reload', service_name)
4958- if not service_result and restart_on_failure:
4959- service_result = service('restart', service_name)
4960- return service_result
4961-
4962-
4963-def service_pause(service_name, init_dir=None):
4964- """Pause a system service.
4965-
4966- Stop it, and prevent it from starting again at boot."""
4967- if init_dir is None:
4968- init_dir = "/etc/init"
4969- stopped = service_stop(service_name)
4970- # XXX: Support systemd too
4971- override_path = os.path.join(
4972- init_dir, '{}.override'.format(service_name))
4973- with open(override_path, 'w') as fh:
4974- fh.write("manual\n")
4975- return stopped
4976-
4977-
4978-def service_resume(service_name, init_dir=None):
4979- """Resume a system service.
4980-
4981- Reenable starting again at boot. Start the service"""
4982- # XXX: Support systemd too
4983- if init_dir is None:
4984- init_dir = "/etc/init"
4985- override_path = os.path.join(
4986- init_dir, '{}.override'.format(service_name))
4987- if os.path.exists(override_path):
4988- os.unlink(override_path)
4989- started = service_start(service_name)
4990- return started
4991-
4992-
4993-def service(action, service_name):
4994- """Control a system service"""
4995- cmd = ['service', service_name, action]
4996- return subprocess.call(cmd) == 0
4997-
4998-
4999-def service_running(service):
5000- """Determine whether a system service is running"""
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches