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

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

Description of the change

Refactor amulet tests, deprecate old tests

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ignore amulet fail #6238. Undercloud woes.

112. By Ryan Beisner

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

113. By Ryan Beisner

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

114. By Ryan Beisner

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches