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