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