Merge lp:~tealeg/charms/trusty/percona-cluster/pause-and-resume into lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next
- Trusty Tahr (14.04)
- pause-and-resume
- Merge into next
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 74 | ||||
Proposed branch: | lp:~tealeg/charms/trusty/percona-cluster/pause-and-resume | ||||
Merge into: | lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next | ||||
Diff against target: |
2140 lines (+1754/-89) 15 files modified
actions.yaml (+4/-0) actions/actions.py (+51/-0) charm-helpers-tests.yaml (+1/-0) charmhelpers/contrib/network/ip.py (+5/-1) charmhelpers/core/hookenv.py (+11/-9) charmhelpers/core/host.py (+32/-16) charmhelpers/core/kernel.py (+68/-0) tests/00-setup (+2/-0) tests/31-test-pause-and-resume.py (+38/-0) tests/charmhelpers/contrib/amulet/deployment.py (+4/-2) tests/charmhelpers/contrib/amulet/utils.py (+243/-52) tests/charmhelpers/contrib/openstack/amulet/deployment.py (+23/-9) tests/charmhelpers/contrib/openstack/amulet/utils.py (+359/-0) tests/charmhelpers/core/__init__.py (+15/-0) tests/charmhelpers/core/hookenv.py (+898/-0) |
||||
To merge this branch: | bzr merge lp:~tealeg/charms/trusty/percona-cluster/pause-and-resume | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chris Glass (community) | Approve | ||
Adam Collard (community) | Approve | ||
Review via email: mp+268238@code.launchpad.net |
Commit message
Description of the change
This branch adds pause and resume actions.
In addition to that central goal it also:
- includes an updated version of charmhelpers
- defines tests for the actions.
uosci-testing-bot (uosci-testing-bot) wrote : | # |
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #8197 percona-
LINT OK: passed
Ryan Beisner (1chb1n) wrote : | # |
re: amulet tests...
Thank you for your work on this. These will be great test additions.
FYI - The percona-cluster charm's amulet tests aren't consistent with the other os-charms, as you may have noticed. I've got its amulet test refactor on my list for this cycle, to make it consistent with other os-charms in how they exercise each of the currently-supported ubuntu:openstack release combos. Be aware that, as written, the existing and proposed amulet tests will only exercise Trusty-Icehouse in automation. When I refactor the others, I'll be sure to preserve your amulet tests and pull those into the run-on-every-combo pivot.
Questions, suggestions re: this proposal:
Can you re-use existing amulet helpers instead of adding new local helpers? I know a few things just landed there with regard to actions and service checking in amulet tests.
For local helpers which are not yet represented in amulet helpers, yet potentially useful in other charm tests...
If there are OpenStack-specific, amulet-specific helpers which are useful in other charm tests, please land those in charmhelpers/
If there are non-OpenStack-
Feel free to holler with any questions. Thanks again!
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #5843 percona-
AMULET OK: passed
Build: http://
Geoff Teale (tealeg) wrote : | # |
Hi Ryan,
Thanks for your feedback. Sorry we had some much variation and duplication in our submissions, it wasn't the plan.
Hopefully you should find the latest revision of this MP more in line with Adam and Alberto's work.
--
Geoff
> re: amulet tests...
>
> Thank you for your work on this. These will be great test additions.
>
> FYI - The percona-cluster charm's amulet tests aren't consistent with the
> other os-charms, as you may have noticed. I've got its amulet test refactor
> on my list for this cycle, to make it consistent with other os-charms in how
> they exercise each of the currently-supported ubuntu:openstack release combos.
> Be aware that, as written, the existing and proposed amulet tests will only
> exercise Trusty-Icehouse in automation. When I refactor the others, I'll be
> sure to preserve your amulet tests and pull those into the run-on-every-combo
> pivot.
>
> Questions, suggestions re: this proposal:
>
> Can you re-use existing amulet helpers instead of adding new local helpers? I
> know a few things just landed there with regard to actions and service
> checking in amulet tests.
>
> For local helpers which are not yet represented in amulet helpers, yet
> potentially useful in other charm tests...
>
> If there are OpenStack-specific, amulet-specific helpers which are useful in
> other charm tests, please land those in
> charmhelpers/
>
> If there are non-OpenStack-
> in other charm tests, please land those in
> charmhelpers/
>
> Feel free to holler with any questions. Thanks again!
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #7817 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #8423 percona-
LINT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #5930 percona-
AMULET FAIL: amulet-test failed
AMULET Results (max last 2 lines):
make: *** [test] Error 1
ERROR:root:Make target returned non-zero.
Full amulet test output: http://
Build: http://
Ryan Beisner (1chb1n) wrote : | # |
Looks like the 00-setup needs to be updated. We ran into that dep issue elsewhere.
This should be the ticket:
http://
Ryan Beisner (1chb1n) wrote : | # |
Otherwise, pending passing tests, looks good to me. Thank you for your work on this!
Geoff Teale (tealeg) wrote : | # |
Cheers Ryan, will do!
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #8477 percona-
LINT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #7868 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #7869 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #8478 percona-
LINT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #5936 percona-
AMULET FAIL: amulet-test failed
AMULET Results (max last 2 lines):
ERROR subprocess encountered error code 1
ERROR:root:Make target returned non-zero.
Full amulet test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #5938 percona-
AMULET FAIL: amulet-test failed
AMULET Results (max last 2 lines):
make: *** [test] Error 1
ERROR:root:Make target returned non-zero.
Full amulet test output: http://
Build: http://
Ryan Beisner (1chb1n) wrote : | # |
I believe if you rebase and resolve any resulting conflicts, the amulet tests in your branch will probably pass. We landed some necessary updates for other efforts, also fixed some amulet test dependency issues along the way.
Geoff Teale (tealeg) wrote : | # |
> I believe if you rebase and resolve any resulting conflicts, the amulet tests
> in your branch will probably pass. We landed some necessary updates for other
> efforts, also fixed some amulet test dependency issues along the way.
Hi Ryan, it should be good to go now.
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #9091 percona-
LINT FAIL: lint-test failed
LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.
Full lint test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8400 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #9092 percona-
LINT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8401 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6147 percona-
AMULET FAIL: amulet-test failed
AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.
Full amulet test output: http://
Build: http://
Ryan Beisner (1chb1n) wrote : | # |
It looks like the tests/00-setup here needs updated. We're working on updating them all, but if you can do that here, that should resolve the import error on the amulet test.
Example in-flight:
http://
Geoff Teale (tealeg) wrote : | # |
> It looks like the tests/00-setup here needs updated. We're working on
> updating them all, but if you can do that here, that should resolve the import
> error on the amulet test.
>
> Example in-flight:
> http://
> amulet-
Hi Ryan,
It should already have exactly that content as I've merged in the most recent percona-
Geoff Teale (tealeg) wrote : | # |
OK, I managed to reproduce the error by removing the python3-distro-info package from my VM. It seems that we need to explicitly pull in both python-distro-info and python3-
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8463 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #9157 percona-
LINT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6169 percona-
AMULET OK: passed
Build: http://
Adam Collard (adam-collard) wrote : | # |
N/F for the missing asserts about status, otherwise looks good.
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8529 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #9228 percona-
LINT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6190 percona-
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_lint_check #9239 percona-
LINT FAIL: lint-test failed
LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.
Full lint test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8540 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #9242 percona-
LINT FAIL: lint-test failed
LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.
Full lint test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8543 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6201 percona-
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_lint_check #9321 percona-
LINT FAIL: lint-test failed
LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.
Full lint test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8620 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #9324 percona-
LINT FAIL: lint-test failed
LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.
Full lint test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8622 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6234 percona-
AMULET OK: passed
Build: http://
Geoff Teale (tealeg) wrote : | # |
> N/F for the missing asserts about status, otherwise looks good.
It should now be good, utilising the outstanding code from charmhelpers to do status_get.
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #8932 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #9714 percona-
LINT FAIL: lint-test failed
LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.
Full lint test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6339 percona-
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_unit_test #9001 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #9773 percona-
LINT FAIL: lint-test failed
LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.
Full lint test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6355 percona-
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_unit_test #9213 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #10047 percona-
LINT FAIL: lint-test failed
LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.
Full lint test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #9214 percona-
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #10048 percona-
LINT OK: passed
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6433 percona-
AMULET OK: passed
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #6434 percona-
AMULET OK: passed
Build: http://
Adam Collard (adam-collard) wrote : | # |
Looks good, thanks! +1
Chris Glass (tribaal) wrote : | # |
Looks good! Thanks for your contribution.
Preview Diff
1 | === added directory 'actions' |
2 | === added file 'actions.yaml' |
3 | --- actions.yaml 1970-01-01 00:00:00 +0000 |
4 | +++ actions.yaml 2015-09-15 12:31:21 +0000 |
5 | @@ -0,0 +1,4 @@ |
6 | +pause: |
7 | + description: Pause the MySQL service. |
8 | +resume: |
9 | + description: Resume the MySQL service. |
10 | \ No newline at end of file |
11 | |
12 | === added file 'actions/actions.py' |
13 | --- actions/actions.py 1970-01-01 00:00:00 +0000 |
14 | +++ actions/actions.py 2015-09-15 12:31:21 +0000 |
15 | @@ -0,0 +1,51 @@ |
16 | +#!/usr/bin/python |
17 | + |
18 | +import os |
19 | +import sys |
20 | + |
21 | +from charmhelpers.core.host import service_pause, service_resume |
22 | +from charmhelpers.core.hookenv import action_fail, status_set |
23 | + |
24 | + |
25 | +MYSQL_SERVICE = "mysql" |
26 | + |
27 | +def pause(args): |
28 | + """Pause the MySQL service. |
29 | + |
30 | + @raises Exception should the service fail to stop. |
31 | + """ |
32 | + if not service_pause(MYSQL_SERVICE): |
33 | + raise Exception("Failed to pause MySQL service.") |
34 | + status_set( |
35 | + "maintenance", "Paused. Use 'resume' action to resume normal service.") |
36 | + |
37 | +def resume(args): |
38 | + """Resume the MySQL service. |
39 | + |
40 | + @raises Exception should the service fail to start.""" |
41 | + if not service_resume(MYSQL_SERVICE): |
42 | + raise Exception("Failed to resume MySQL service.") |
43 | + status_set("active", "") |
44 | + |
45 | + |
46 | +# A dictionary of all the defined actions to callables (which take |
47 | +# parsed arguments). |
48 | +ACTIONS = {"pause": pause, "resume": resume} |
49 | + |
50 | + |
51 | +def main(args): |
52 | + action_name = os.path.basename(args[0]) |
53 | + try: |
54 | + action = ACTIONS[action_name] |
55 | + except KeyError: |
56 | + return "Action %s undefined" % action_name |
57 | + else: |
58 | + try: |
59 | + action(args) |
60 | + except Exception as e: |
61 | + action_fail(str(e)) |
62 | + |
63 | + |
64 | +if __name__ == "__main__": |
65 | + sys.exit(main(sys.argv)) |
66 | + |
67 | |
68 | === added symlink 'actions/charmhelpers' |
69 | === target is u'../charmhelpers' |
70 | === added symlink 'actions/pause' |
71 | === target is u'actions.py' |
72 | === added symlink 'actions/resume' |
73 | === target is u'actions.py' |
74 | === modified file 'charm-helpers-tests.yaml' |
75 | --- charm-helpers-tests.yaml 2015-04-15 14:23:37 +0000 |
76 | +++ charm-helpers-tests.yaml 2015-09-15 12:31:21 +0000 |
77 | @@ -3,3 +3,4 @@ |
78 | include: |
79 | - contrib.amulet |
80 | - contrib.openstack.amulet |
81 | + - core.hookenv |
82 | |
83 | === renamed directory 'hooks/charmhelpers' => 'charmhelpers' |
84 | === modified file 'charmhelpers/contrib/network/ip.py' |
85 | --- hooks/charmhelpers/contrib/network/ip.py 2015-03-03 02:26:12 +0000 |
86 | +++ charmhelpers/contrib/network/ip.py 2015-09-15 12:31:21 +0000 |
87 | @@ -435,8 +435,12 @@ |
88 | |
89 | rev = dns.reversename.from_address(address) |
90 | result = ns_query(rev) |
91 | + |
92 | if not result: |
93 | - return None |
94 | + try: |
95 | + result = socket.gethostbyaddr(address)[0] |
96 | + except: |
97 | + return None |
98 | else: |
99 | result = address |
100 | |
101 | |
102 | === modified file 'charmhelpers/core/hookenv.py' |
103 | --- hooks/charmhelpers/core/hookenv.py 2015-08-26 13:11:30 +0000 |
104 | +++ charmhelpers/core/hookenv.py 2015-09-15 12:31:21 +0000 |
105 | @@ -767,21 +767,23 @@ |
106 | |
107 | |
108 | def status_get(): |
109 | - """Retrieve the previously set juju workload state |
110 | - |
111 | - If the status-set command is not found then assume this is juju < 1.23 and |
112 | - return 'unknown' |
113 | + """Retrieve the previously set juju workload state and message |
114 | + |
115 | + If the status-get command is not found then assume this is juju < 1.23 and |
116 | + return 'unknown', "" |
117 | + |
118 | """ |
119 | - cmd = ['status-get'] |
120 | + cmd = ['status-get', "--format=json", "--include-data"] |
121 | try: |
122 | - raw_status = subprocess.check_output(cmd, universal_newlines=True) |
123 | - status = raw_status.rstrip() |
124 | - return status |
125 | + raw_status = subprocess.check_output(cmd) |
126 | except OSError as e: |
127 | if e.errno == errno.ENOENT: |
128 | - return 'unknown' |
129 | + return ('unknown', "") |
130 | else: |
131 | raise |
132 | + else: |
133 | + status = json.loads(raw_status.decode("UTF-8")) |
134 | + return (status["status"], status["message"]) |
135 | |
136 | |
137 | def translate_exc(from_exc, to_exc): |
138 | |
139 | === modified file 'charmhelpers/core/host.py' |
140 | --- hooks/charmhelpers/core/host.py 2015-08-26 13:11:30 +0000 |
141 | +++ charmhelpers/core/host.py 2015-09-15 12:31:21 +0000 |
142 | @@ -63,32 +63,48 @@ |
143 | return service_result |
144 | |
145 | |
146 | -def service_pause(service_name, init_dir=None): |
147 | +def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"): |
148 | """Pause a system service. |
149 | |
150 | Stop it, and prevent it from starting again at boot.""" |
151 | - if init_dir is None: |
152 | - init_dir = "/etc/init" |
153 | stopped = service_stop(service_name) |
154 | - # XXX: Support systemd too |
155 | - override_path = os.path.join( |
156 | - init_dir, '{}.override'.format(service_name)) |
157 | - with open(override_path, 'w') as fh: |
158 | - fh.write("manual\n") |
159 | + upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) |
160 | + sysv_file = os.path.join(initd_dir, service_name) |
161 | + if os.path.exists(upstart_file): |
162 | + override_path = os.path.join( |
163 | + init_dir, '{}.override'.format(service_name)) |
164 | + with open(override_path, 'w') as fh: |
165 | + fh.write("manual\n") |
166 | + elif os.path.exists(sysv_file): |
167 | + subprocess.check_call(["update-rc.d", service_name, "disable"]) |
168 | + else: |
169 | + # XXX: Support SystemD too |
170 | + raise ValueError( |
171 | + "Unable to detect {0} as either Upstart {1} or SysV {2}".format( |
172 | + service_name, upstart_file, sysv_file)) |
173 | return stopped |
174 | |
175 | |
176 | -def service_resume(service_name, init_dir=None): |
177 | +def service_resume(service_name, init_dir="/etc/init", |
178 | + initd_dir="/etc/init.d"): |
179 | """Resume a system service. |
180 | |
181 | Reenable starting again at boot. Start the service""" |
182 | - # XXX: Support systemd too |
183 | - if init_dir is None: |
184 | - init_dir = "/etc/init" |
185 | - override_path = os.path.join( |
186 | - init_dir, '{}.override'.format(service_name)) |
187 | - if os.path.exists(override_path): |
188 | - os.unlink(override_path) |
189 | + upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) |
190 | + sysv_file = os.path.join(initd_dir, service_name) |
191 | + if os.path.exists(upstart_file): |
192 | + override_path = os.path.join( |
193 | + init_dir, '{}.override'.format(service_name)) |
194 | + if os.path.exists(override_path): |
195 | + os.unlink(override_path) |
196 | + elif os.path.exists(sysv_file): |
197 | + subprocess.check_call(["update-rc.d", service_name, "enable"]) |
198 | + else: |
199 | + # XXX: Support SystemD too |
200 | + raise ValueError( |
201 | + "Unable to detect {0} as either Upstart {1} or SysV {2}".format( |
202 | + service_name, upstart_file, sysv_file)) |
203 | + |
204 | started = service_start(service_name) |
205 | return started |
206 | |
207 | |
208 | === added file 'charmhelpers/core/kernel.py' |
209 | --- charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000 |
210 | +++ charmhelpers/core/kernel.py 2015-09-15 12:31:21 +0000 |
211 | @@ -0,0 +1,68 @@ |
212 | +#!/usr/bin/env python |
213 | +# -*- coding: utf-8 -*- |
214 | + |
215 | +# Copyright 2014-2015 Canonical Limited. |
216 | +# |
217 | +# This file is part of charm-helpers. |
218 | +# |
219 | +# charm-helpers is free software: you can redistribute it and/or modify |
220 | +# it under the terms of the GNU Lesser General Public License version 3 as |
221 | +# published by the Free Software Foundation. |
222 | +# |
223 | +# charm-helpers is distributed in the hope that it will be useful, |
224 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
225 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
226 | +# GNU Lesser General Public License for more details. |
227 | +# |
228 | +# You should have received a copy of the GNU Lesser General Public License |
229 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
230 | + |
231 | +__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" |
232 | + |
233 | +from charmhelpers.core.hookenv import ( |
234 | + log, |
235 | + INFO |
236 | +) |
237 | + |
238 | +from subprocess import check_call, check_output |
239 | +import re |
240 | + |
241 | + |
242 | +def modprobe(module, persist=True): |
243 | + """Load a kernel module and configure for auto-load on reboot.""" |
244 | + cmd = ['modprobe', module] |
245 | + |
246 | + log('Loading kernel module %s' % module, level=INFO) |
247 | + |
248 | + check_call(cmd) |
249 | + if persist: |
250 | + with open('/etc/modules', 'r+') as modules: |
251 | + if module not in modules.read(): |
252 | + modules.write(module) |
253 | + |
254 | + |
255 | +def rmmod(module, force=False): |
256 | + """Remove a module from the linux kernel""" |
257 | + cmd = ['rmmod'] |
258 | + if force: |
259 | + cmd.append('-f') |
260 | + cmd.append(module) |
261 | + log('Removing kernel module %s' % module, level=INFO) |
262 | + return check_call(cmd) |
263 | + |
264 | + |
265 | +def lsmod(): |
266 | + """Shows what kernel modules are currently loaded""" |
267 | + return check_output(['lsmod'], |
268 | + universal_newlines=True) |
269 | + |
270 | + |
271 | +def is_module_loaded(module): |
272 | + """Checks if a kernel module is already loaded""" |
273 | + matches = re.findall('^%s[ ]+' % module, lsmod(), re.M) |
274 | + return len(matches) > 0 |
275 | + |
276 | + |
277 | +def update_initramfs(version='all'): |
278 | + """Updates an initramfs image""" |
279 | + return check_call(["update-initramfs", "-k", version, "-u"]) |
280 | |
281 | === added symlink 'hooks/charmhelpers' |
282 | === target is u'../charmhelpers' |
283 | === modified file 'tests/00-setup' |
284 | --- tests/00-setup 2015-08-26 13:22:41 +0000 |
285 | +++ tests/00-setup 2015-09-15 12:31:21 +0000 |
286 | @@ -7,6 +7,7 @@ |
287 | sudo apt-get install --yes amulet \ |
288 | python-cinderclient \ |
289 | python-distro-info \ |
290 | + python3-distro-info \ |
291 | python-glanceclient \ |
292 | python-heatclient \ |
293 | python-keystoneclient \ |
294 | @@ -14,3 +15,4 @@ |
295 | python-novaclient \ |
296 | python-pika \ |
297 | python-swiftclient |
298 | + |
299 | |
300 | === added file 'tests/31-test-pause-and-resume.py' |
301 | --- tests/31-test-pause-and-resume.py 1970-01-01 00:00:00 +0000 |
302 | +++ tests/31-test-pause-and-resume.py 2015-09-15 12:31:21 +0000 |
303 | @@ -0,0 +1,38 @@ |
304 | +#!/usr/bin/python3 |
305 | +# test percona-cluster pause and resum |
306 | + |
307 | +import basic_deployment |
308 | +from charmhelpers.contrib.amulet.utils import AmuletUtils |
309 | + |
310 | +utils = AmuletUtils() |
311 | + |
312 | + |
313 | +class PauseResume(basic_deployment.BasicDeployment): |
314 | + |
315 | + def run(self): |
316 | + super(PauseResume, self).run() |
317 | + uid = 'percona-cluster/0' |
318 | + unit = self.d.sentry.unit[uid] |
319 | + assert self.is_mysqld_running(unit), 'mysql not running: %s' % uid |
320 | + assert utils.status_get(unit)[0] == "unknown" |
321 | + |
322 | + action_id = utils.run_action(unit, "pause") |
323 | + assert utils.wait_on_action(action_id), "Pause action failed." |
324 | + |
325 | + # Note that is_mysqld_running will print an error message when |
326 | + # mysqld is not running. This is by design but it looks odd |
327 | + # in the output. |
328 | + assert not self.is_mysqld_running(unit=unit), \ |
329 | + "mysqld is still running!" |
330 | + |
331 | + assert utils.status_get(unit)[0] == "maintenance" |
332 | + action_id = utils.run_action(unit, "resume") |
333 | + assert utils.wait_on_action(action_id), "Resume action failed" |
334 | + assert utils.status_get(unit)[0] == "active" |
335 | + assert self.is_mysqld_running(unit=unit), \ |
336 | + "mysqld not running after resume." |
337 | + |
338 | + |
339 | +if __name__ == "__main__": |
340 | + p = PauseResume() |
341 | + p.run() |
342 | |
343 | === modified file 'tests/charmhelpers/contrib/amulet/deployment.py' |
344 | --- tests/charmhelpers/contrib/amulet/deployment.py 2015-04-15 14:23:37 +0000 |
345 | +++ tests/charmhelpers/contrib/amulet/deployment.py 2015-09-15 12:31:21 +0000 |
346 | @@ -51,7 +51,8 @@ |
347 | if 'units' not in this_service: |
348 | this_service['units'] = 1 |
349 | |
350 | - self.d.add(this_service['name'], units=this_service['units']) |
351 | + self.d.add(this_service['name'], units=this_service['units'], |
352 | + constraints=this_service.get('constraints')) |
353 | |
354 | for svc in other_services: |
355 | if 'location' in svc: |
356 | @@ -64,7 +65,8 @@ |
357 | if 'units' not in svc: |
358 | svc['units'] = 1 |
359 | |
360 | - self.d.add(svc['name'], charm=branch_location, units=svc['units']) |
361 | + self.d.add(svc['name'], charm=branch_location, units=svc['units'], |
362 | + constraints=svc.get('constraints')) |
363 | |
364 | def _add_relations(self, relations): |
365 | """Add all of the relations for the services.""" |
366 | |
367 | === modified file 'tests/charmhelpers/contrib/amulet/utils.py' |
368 | --- tests/charmhelpers/contrib/amulet/utils.py 2015-08-26 13:11:30 +0000 |
369 | +++ tests/charmhelpers/contrib/amulet/utils.py 2015-09-15 12:31:21 +0000 |
370 | @@ -19,9 +19,11 @@ |
371 | import logging |
372 | import os |
373 | import re |
374 | +import socket |
375 | import subprocess |
376 | import sys |
377 | import time |
378 | +import uuid |
379 | |
380 | import amulet |
381 | import distro_info |
382 | @@ -114,7 +116,7 @@ |
383 | # /!\ DEPRECATION WARNING (beisner): |
384 | # New and existing tests should be rewritten to use |
385 | # validate_services_by_name() as it is aware of init systems. |
386 | - self.log.warn('/!\\ DEPRECATION WARNING: use ' |
387 | + self.log.warn('DEPRECATION WARNING: use ' |
388 | 'validate_services_by_name instead of validate_services ' |
389 | 'due to init system differences.') |
390 | |
391 | @@ -269,33 +271,52 @@ |
392 | """Get last modification time of directory.""" |
393 | return sentry_unit.directory_stat(directory)['mtime'] |
394 | |
395 | - def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False): |
396 | - """Get process' start time. |
397 | - |
398 | - Determine start time of the process based on the last modification |
399 | - time of the /proc/pid directory. If pgrep_full is True, the process |
400 | - name is matched against the full command line. |
401 | - """ |
402 | - if pgrep_full: |
403 | - cmd = 'pgrep -o -f {}'.format(service) |
404 | - else: |
405 | - cmd = 'pgrep -o {}'.format(service) |
406 | - cmd = cmd + ' | grep -v pgrep || exit 0' |
407 | - cmd_out = sentry_unit.run(cmd) |
408 | - self.log.debug('CMDout: ' + str(cmd_out)) |
409 | - if cmd_out[0]: |
410 | - self.log.debug('Pid for %s %s' % (service, str(cmd_out[0]))) |
411 | - proc_dir = '/proc/{}'.format(cmd_out[0].strip()) |
412 | - return self._get_dir_mtime(sentry_unit, proc_dir) |
413 | + def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None): |
414 | + """Get start time of a process based on the last modification time |
415 | + of the /proc/pid directory. |
416 | + |
417 | + :sentry_unit: The sentry unit to check for the service on |
418 | + :service: service name to look for in process table |
419 | + :pgrep_full: [Deprecated] Use full command line search mode with pgrep |
420 | + :returns: epoch time of service process start |
421 | + :param commands: list of bash commands |
422 | + :param sentry_units: list of sentry unit pointers |
423 | + :returns: None if successful; Failure message otherwise |
424 | + """ |
425 | + if pgrep_full is not None: |
426 | + # /!\ DEPRECATION WARNING (beisner): |
427 | + # No longer implemented, as pidof is now used instead of pgrep. |
428 | + # https://bugs.launchpad.net/charm-helpers/+bug/1474030 |
429 | + self.log.warn('DEPRECATION WARNING: pgrep_full bool is no ' |
430 | + 'longer implemented re: lp 1474030.') |
431 | + |
432 | + pid_list = self.get_process_id_list(sentry_unit, service) |
433 | + pid = pid_list[0] |
434 | + proc_dir = '/proc/{}'.format(pid) |
435 | + self.log.debug('Pid for {} on {}: {}'.format( |
436 | + service, sentry_unit.info['unit_name'], pid)) |
437 | + |
438 | + return self._get_dir_mtime(sentry_unit, proc_dir) |
439 | |
440 | def service_restarted(self, sentry_unit, service, filename, |
441 | - pgrep_full=False, sleep_time=20): |
442 | + pgrep_full=None, sleep_time=20): |
443 | """Check if service was restarted. |
444 | |
445 | Compare a service's start time vs a file's last modification time |
446 | (such as a config file for that service) to determine if the service |
447 | has been restarted. |
448 | """ |
449 | + # /!\ DEPRECATION WARNING (beisner): |
450 | + # This method is prone to races in that no before-time is known. |
451 | + # Use validate_service_config_changed instead. |
452 | + |
453 | + # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now |
454 | + # used instead of pgrep. pgrep_full is still passed through to ensure |
455 | + # deprecation WARNS. lp1474030 |
456 | + self.log.warn('DEPRECATION WARNING: use ' |
457 | + 'validate_service_config_changed instead of ' |
458 | + 'service_restarted due to known races.') |
459 | + |
460 | time.sleep(sleep_time) |
461 | if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >= |
462 | self._get_file_mtime(sentry_unit, filename)): |
463 | @@ -304,15 +325,15 @@ |
464 | return False |
465 | |
466 | def service_restarted_since(self, sentry_unit, mtime, service, |
467 | - pgrep_full=False, sleep_time=20, |
468 | - retry_count=2): |
469 | + pgrep_full=None, sleep_time=20, |
470 | + retry_count=2, retry_sleep_time=30): |
471 | """Check if service was been started after a given time. |
472 | |
473 | Args: |
474 | sentry_unit (sentry): The sentry unit to check for the service on |
475 | mtime (float): The epoch time to check against |
476 | service (string): service name to look for in process table |
477 | - pgrep_full (boolean): Use full command line search mode with pgrep |
478 | + pgrep_full: [Deprecated] Use full command line search mode with pgrep |
479 | sleep_time (int): Seconds to sleep before looking for process |
480 | retry_count (int): If service is not found, how many times to retry |
481 | |
482 | @@ -321,30 +342,44 @@ |
483 | False if service is older than mtime or if service was |
484 | not found. |
485 | """ |
486 | - self.log.debug('Checking %s restarted since %s' % (service, mtime)) |
487 | + # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now |
488 | + # used instead of pgrep. pgrep_full is still passed through to ensure |
489 | + # deprecation WARNS. lp1474030 |
490 | + |
491 | + unit_name = sentry_unit.info['unit_name'] |
492 | + self.log.debug('Checking that %s service restarted since %s on ' |
493 | + '%s' % (service, mtime, unit_name)) |
494 | time.sleep(sleep_time) |
495 | - proc_start_time = self._get_proc_start_time(sentry_unit, service, |
496 | - pgrep_full) |
497 | - while retry_count > 0 and not proc_start_time: |
498 | - self.log.debug('No pid file found for service %s, will retry %i ' |
499 | - 'more times' % (service, retry_count)) |
500 | - time.sleep(30) |
501 | - proc_start_time = self._get_proc_start_time(sentry_unit, service, |
502 | - pgrep_full) |
503 | - retry_count = retry_count - 1 |
504 | + proc_start_time = None |
505 | + tries = 0 |
506 | + while tries <= retry_count and not proc_start_time: |
507 | + try: |
508 | + proc_start_time = self._get_proc_start_time(sentry_unit, |
509 | + service, |
510 | + pgrep_full) |
511 | + self.log.debug('Attempt {} to get {} proc start time on {} ' |
512 | + 'OK'.format(tries, service, unit_name)) |
513 | + except IOError: |
514 | + # NOTE(beisner) - race avoidance, proc may not exist yet. |
515 | + # https://bugs.launchpad.net/charm-helpers/+bug/1474030 |
516 | + self.log.debug('Attempt {} to get {} proc start time on {} ' |
517 | + 'failed'.format(tries, service, unit_name)) |
518 | + time.sleep(retry_sleep_time) |
519 | + tries += 1 |
520 | |
521 | if not proc_start_time: |
522 | self.log.warn('No proc start time found, assuming service did ' |
523 | 'not start') |
524 | return False |
525 | if proc_start_time >= mtime: |
526 | - self.log.debug('proc start time is newer than provided mtime' |
527 | - '(%s >= %s)' % (proc_start_time, mtime)) |
528 | + self.log.debug('Proc start time is newer than provided mtime' |
529 | + '(%s >= %s) on %s (OK)' % (proc_start_time, |
530 | + mtime, unit_name)) |
531 | return True |
532 | else: |
533 | - self.log.warn('proc start time (%s) is older than provided mtime ' |
534 | - '(%s), service did not restart' % (proc_start_time, |
535 | - mtime)) |
536 | + self.log.warn('Proc start time (%s) is older than provided mtime ' |
537 | + '(%s) on %s, service did not ' |
538 | + 'restart' % (proc_start_time, mtime, unit_name)) |
539 | return False |
540 | |
541 | def config_updated_since(self, sentry_unit, filename, mtime, |
542 | @@ -374,8 +409,9 @@ |
543 | return False |
544 | |
545 | def validate_service_config_changed(self, sentry_unit, mtime, service, |
546 | - filename, pgrep_full=False, |
547 | - sleep_time=20, retry_count=2): |
548 | + filename, pgrep_full=None, |
549 | + sleep_time=20, retry_count=2, |
550 | + retry_sleep_time=30): |
551 | """Check service and file were updated after mtime |
552 | |
553 | Args: |
554 | @@ -383,9 +419,10 @@ |
555 | mtime (float): The epoch time to check against |
556 | service (string): service name to look for in process table |
557 | filename (string): The file to check mtime of |
558 | - pgrep_full (boolean): Use full command line search mode with pgrep |
559 | - sleep_time (int): Seconds to sleep before looking for process |
560 | + pgrep_full: [Deprecated] Use full command line search mode with pgrep |
561 | + sleep_time (int): Initial sleep in seconds to pass to test helpers |
562 | retry_count (int): If service is not found, how many times to retry |
563 | + retry_sleep_time (int): Time in seconds to wait between retries |
564 | |
565 | Typical Usage: |
566 | u = OpenStackAmuletUtils(ERROR) |
567 | @@ -402,15 +439,25 @@ |
568 | mtime, False if service is older than mtime or if service was |
569 | not found or if filename was modified before mtime. |
570 | """ |
571 | - self.log.debug('Checking %s restarted since %s' % (service, mtime)) |
572 | - time.sleep(sleep_time) |
573 | - service_restart = self.service_restarted_since(sentry_unit, mtime, |
574 | - service, |
575 | - pgrep_full=pgrep_full, |
576 | - sleep_time=0, |
577 | - retry_count=retry_count) |
578 | - config_update = self.config_updated_since(sentry_unit, filename, mtime, |
579 | - sleep_time=0) |
580 | + |
581 | + # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now |
582 | + # used instead of pgrep. pgrep_full is still passed through to ensure |
583 | + # deprecation WARNS. lp1474030 |
584 | + |
585 | + service_restart = self.service_restarted_since( |
586 | + sentry_unit, mtime, |
587 | + service, |
588 | + pgrep_full=pgrep_full, |
589 | + sleep_time=sleep_time, |
590 | + retry_count=retry_count, |
591 | + retry_sleep_time=retry_sleep_time) |
592 | + |
593 | + config_update = self.config_updated_since( |
594 | + sentry_unit, |
595 | + filename, |
596 | + mtime, |
597 | + sleep_time=0) |
598 | + |
599 | return service_restart and config_update |
600 | |
601 | def get_sentry_time(self, sentry_unit): |
602 | @@ -428,7 +475,6 @@ |
603 | """Return a list of all Ubuntu releases in order of release.""" |
604 | _d = distro_info.UbuntuDistroInfo() |
605 | _release_list = _d.all |
606 | - self.log.debug('Ubuntu release list: {}'.format(_release_list)) |
607 | return _release_list |
608 | |
609 | def file_to_url(self, file_rel_path): |
610 | @@ -568,6 +614,142 @@ |
611 | |
612 | return None |
613 | |
614 | + def validate_sectionless_conf(self, file_contents, expected): |
615 | + """A crude conf parser. Useful to inspect configuration files which |
616 | + do not have section headers (as would be necessary in order to use |
617 | + the configparser). Such as openstack-dashboard or rabbitmq confs.""" |
618 | + for line in file_contents.split('\n'): |
619 | + if '=' in line: |
620 | + args = line.split('=') |
621 | + if len(args) <= 1: |
622 | + continue |
623 | + key = args[0].strip() |
624 | + value = args[1].strip() |
625 | + if key in expected.keys(): |
626 | + if expected[key] != value: |
627 | + msg = ('Config mismatch. Expected, actual: {}, ' |
628 | + '{}'.format(expected[key], value)) |
629 | + amulet.raise_status(amulet.FAIL, msg=msg) |
630 | + |
631 | + def get_unit_hostnames(self, units): |
632 | + """Return a dict of juju unit names to hostnames.""" |
633 | + host_names = {} |
634 | + for unit in units: |
635 | + host_names[unit.info['unit_name']] = \ |
636 | + str(unit.file_contents('/etc/hostname').strip()) |
637 | + self.log.debug('Unit host names: {}'.format(host_names)) |
638 | + return host_names |
639 | + |
640 | + def run_cmd_unit(self, sentry_unit, cmd): |
641 | + """Run a command on a unit, return the output and exit code.""" |
642 | + output, code = sentry_unit.run(cmd) |
643 | + if code == 0: |
644 | + self.log.debug('{} `{}` command returned {} ' |
645 | + '(OK)'.format(sentry_unit.info['unit_name'], |
646 | + cmd, code)) |
647 | + else: |
648 | + msg = ('{} `{}` command returned {} ' |
649 | + '{}'.format(sentry_unit.info['unit_name'], |
650 | + cmd, code, output)) |
651 | + amulet.raise_status(amulet.FAIL, msg=msg) |
652 | + return str(output), code |
653 | + |
654 | + def file_exists_on_unit(self, sentry_unit, file_name): |
655 | + """Check if a file exists on a unit.""" |
656 | + try: |
657 | + sentry_unit.file_stat(file_name) |
658 | + return True |
659 | + except IOError: |
660 | + return False |
661 | + except Exception as e: |
662 | + msg = 'Error checking file {}: {}'.format(file_name, e) |
663 | + amulet.raise_status(amulet.FAIL, msg=msg) |
664 | + |
665 | + def file_contents_safe(self, sentry_unit, file_name, |
666 | + max_wait=60, fatal=False): |
667 | + """Get file contents from a sentry unit. Wrap amulet file_contents |
668 | + with retry logic to address races where a file checks as existing, |
669 | + but no longer exists by the time file_contents is called. |
670 | + Return None if file not found. Optionally raise if fatal is True.""" |
671 | + unit_name = sentry_unit.info['unit_name'] |
672 | + file_contents = False |
673 | + tries = 0 |
674 | + while not file_contents and tries < (max_wait / 4): |
675 | + try: |
676 | + file_contents = sentry_unit.file_contents(file_name) |
677 | + except IOError: |
678 | + self.log.debug('Attempt {} to open file {} from {} ' |
679 | + 'failed'.format(tries, file_name, |
680 | + unit_name)) |
681 | + time.sleep(4) |
682 | + tries += 1 |
683 | + |
684 | + if file_contents: |
685 | + return file_contents |
686 | + elif not fatal: |
687 | + return None |
688 | + elif fatal: |
689 | + msg = 'Failed to get file contents from unit.' |
690 | + amulet.raise_status(amulet.FAIL, msg) |
691 | + |
692 | + def port_knock_tcp(self, host="localhost", port=22, timeout=15): |
693 | + """Open a TCP socket to check for a listening sevice on a host. |
694 | + |
695 | + :param host: host name or IP address, default to localhost |
696 | + :param port: TCP port number, default to 22 |
697 | + :param timeout: Connect timeout, default to 15 seconds |
698 | + :returns: True if successful, False if connect failed |
699 | + """ |
700 | + |
701 | + # Resolve host name if possible |
702 | + try: |
703 | + connect_host = socket.gethostbyname(host) |
704 | + host_human = "{} ({})".format(connect_host, host) |
705 | + except socket.error as e: |
706 | + self.log.warn('Unable to resolve address: ' |
707 | + '{} ({}) Trying anyway!'.format(host, e)) |
708 | + connect_host = host |
709 | + host_human = connect_host |
710 | + |
711 | + # Attempt socket connection |
712 | + try: |
713 | + knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
714 | + knock.settimeout(timeout) |
715 | + knock.connect((connect_host, port)) |
716 | + knock.close() |
717 | + self.log.debug('Socket connect OK for host ' |
718 | + '{} on port {}.'.format(host_human, port)) |
719 | + return True |
720 | + except socket.error as e: |
721 | + self.log.debug('Socket connect FAIL for' |
722 | + ' {} port {} ({})'.format(host_human, port, e)) |
723 | + return False |
724 | + |
725 | + def port_knock_units(self, sentry_units, port=22, |
726 | + timeout=15, expect_success=True): |
727 | + """Open a TCP socket to check for a listening sevice on each |
728 | + listed juju unit. |
729 | + |
730 | + :param sentry_units: list of sentry unit pointers |
731 | + :param port: TCP port number, default to 22 |
732 | + :param timeout: Connect timeout, default to 15 seconds |
733 | + :expect_success: True by default, set False to invert logic |
734 | + :returns: None if successful, Failure message otherwise |
735 | + """ |
736 | + for unit in sentry_units: |
737 | + host = unit.info['public-address'] |
738 | + connected = self.port_knock_tcp(host, port, timeout) |
739 | + if not connected and expect_success: |
740 | + return 'Socket connect failed.' |
741 | + elif connected and not expect_success: |
742 | + return 'Socket connected unexpectedly.' |
743 | + |
744 | + def get_uuid_epoch_stamp(self): |
745 | + """Returns a stamp string based on uuid4 and epoch time. Useful in |
746 | + generating test messages which need to be unique-ish.""" |
747 | + return '[{}-{}]'.format(uuid.uuid4(), time.time()) |
748 | + |
749 | +# amulet juju action helpers: |
750 | def run_action(self, unit_sentry, action, |
751 | _check_output=subprocess.check_output): |
752 | """Run the named action on a given unit sentry. |
753 | @@ -594,3 +776,12 @@ |
754 | output = _check_output(command, universal_newlines=True) |
755 | data = json.loads(output) |
756 | return data.get(u"status") == "completed" |
757 | + |
758 | + def status_get(self, unit): |
759 | + """Return the current service status of this unit.""" |
760 | + raw_status, return_code = unit.run( |
761 | + "status-get --format=json --include-data") |
762 | + if return_code != 0: |
763 | + return ("unknown", "") |
764 | + status = json.loads(raw_status) |
765 | + return (status["status"], status["message"]) |
766 | |
767 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py' |
768 | --- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-26 13:11:30 +0000 |
769 | +++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-15 12:31:21 +0000 |
770 | @@ -44,20 +44,31 @@ |
771 | Determine if the local branch being tested is derived from its |
772 | stable or next (dev) branch, and based on this, use the corresonding |
773 | stable or next branches for the other_services.""" |
774 | + |
775 | + # Charms outside the lp:~openstack-charmers namespace |
776 | base_charms = ['mysql', 'mongodb', 'nrpe'] |
777 | |
778 | + # Force these charms to current series even when using an older series. |
779 | + # ie. Use trusty/nrpe even when series is precise, as the P charm |
780 | + # does not possess the necessary external master config and hooks. |
781 | + force_series_current = ['nrpe'] |
782 | + |
783 | if self.series in ['precise', 'trusty']: |
784 | base_series = self.series |
785 | else: |
786 | base_series = self.current_next |
787 | |
788 | - if self.stable: |
789 | - for svc in other_services: |
790 | + for svc in other_services: |
791 | + if svc['name'] in force_series_current: |
792 | + base_series = self.current_next |
793 | + # If a location has been explicitly set, use it |
794 | + if svc.get('location'): |
795 | + continue |
796 | + if self.stable: |
797 | temp = 'lp:charms/{}/{}' |
798 | svc['location'] = temp.format(base_series, |
799 | svc['name']) |
800 | - else: |
801 | - for svc in other_services: |
802 | + else: |
803 | if svc['name'] in base_charms: |
804 | temp = 'lp:charms/{}/{}' |
805 | svc['location'] = temp.format(base_series, |
806 | @@ -66,6 +77,7 @@ |
807 | temp = 'lp:~openstack-charmers/charms/{}/{}/next' |
808 | svc['location'] = temp.format(self.current_next, |
809 | svc['name']) |
810 | + |
811 | return other_services |
812 | |
813 | def _add_services(self, this_service, other_services): |
814 | @@ -77,21 +89,23 @@ |
815 | |
816 | services = other_services |
817 | services.append(this_service) |
818 | + |
819 | + # Charms which should use the source config option |
820 | use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph', |
821 | 'ceph-osd', 'ceph-radosgw'] |
822 | - # Most OpenStack subordinate charms do not expose an origin option |
823 | - # as that is controlled by the principle. |
824 | - ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe'] |
825 | + |
826 | + # Charms which can not use openstack-origin, ie. many subordinates |
827 | + no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe'] |
828 | |
829 | if self.openstack: |
830 | for svc in services: |
831 | - if svc['name'] not in use_source + ignore: |
832 | + if svc['name'] not in use_source + no_origin: |
833 | config = {'openstack-origin': self.openstack} |
834 | self.d.configure(svc['name'], config) |
835 | |
836 | if self.source: |
837 | for svc in services: |
838 | - if svc['name'] in use_source and svc['name'] not in ignore: |
839 | + if svc['name'] in use_source and svc['name'] not in no_origin: |
840 | config = {'source': self.source} |
841 | self.d.configure(svc['name'], config) |
842 | |
843 | |
844 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py' |
845 | --- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-08-03 14:52:57 +0000 |
846 | +++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-15 12:31:21 +0000 |
847 | @@ -27,6 +27,7 @@ |
848 | import heatclient.v1.client as heat_client |
849 | import keystoneclient.v2_0 as keystone_client |
850 | import novaclient.v1_1.client as nova_client |
851 | +import pika |
852 | import swiftclient |
853 | |
854 | from charmhelpers.contrib.amulet.utils import ( |
855 | @@ -602,3 +603,361 @@ |
856 | self.log.debug('Ceph {} samples (OK): ' |
857 | '{}'.format(sample_type, samples)) |
858 | return None |
859 | + |
860 | +# rabbitmq/amqp specific helpers: |
861 | + def add_rmq_test_user(self, sentry_units, |
862 | + username="testuser1", password="changeme"): |
863 | + """Add a test user via the first rmq juju unit, check connection as |
864 | + the new user against all sentry units. |
865 | + |
866 | + :param sentry_units: list of sentry unit pointers |
867 | + :param username: amqp user name, default to testuser1 |
868 | + :param password: amqp user password |
869 | + :returns: None if successful. Raise on error. |
870 | + """ |
871 | + self.log.debug('Adding rmq user ({})...'.format(username)) |
872 | + |
873 | + # Check that user does not already exist |
874 | + cmd_user_list = 'rabbitmqctl list_users' |
875 | + output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list) |
876 | + if username in output: |
877 | + self.log.warning('User ({}) already exists, returning ' |
878 | + 'gracefully.'.format(username)) |
879 | + return |
880 | + |
881 | + perms = '".*" ".*" ".*"' |
882 | + cmds = ['rabbitmqctl add_user {} {}'.format(username, password), |
883 | + 'rabbitmqctl set_permissions {} {}'.format(username, perms)] |
884 | + |
885 | + # Add user via first unit |
886 | + for cmd in cmds: |
887 | + output, _ = self.run_cmd_unit(sentry_units[0], cmd) |
888 | + |
889 | + # Check connection against the other sentry_units |
890 | + self.log.debug('Checking user connect against units...') |
891 | + for sentry_unit in sentry_units: |
892 | + connection = self.connect_amqp_by_unit(sentry_unit, ssl=False, |
893 | + username=username, |
894 | + password=password) |
895 | + connection.close() |
896 | + |
897 | + def delete_rmq_test_user(self, sentry_units, username="testuser1"): |
898 | + """Delete a rabbitmq user via the first rmq juju unit. |
899 | + |
900 | + :param sentry_units: list of sentry unit pointers |
901 | + :param username: amqp user name, default to testuser1 |
902 | + :param password: amqp user password |
903 | + :returns: None if successful or no such user. |
904 | + """ |
905 | + self.log.debug('Deleting rmq user ({})...'.format(username)) |
906 | + |
907 | + # Check that the user exists |
908 | + cmd_user_list = 'rabbitmqctl list_users' |
909 | + output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list) |
910 | + |
911 | + if username not in output: |
912 | + self.log.warning('User ({}) does not exist, returning ' |
913 | + 'gracefully.'.format(username)) |
914 | + return |
915 | + |
916 | + # Delete the user |
917 | + cmd_user_del = 'rabbitmqctl delete_user {}'.format(username) |
918 | + output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del) |
919 | + |
920 | + def get_rmq_cluster_status(self, sentry_unit): |
921 | + """Execute rabbitmq cluster status command on a unit and return |
922 | + the full output. |
923 | + |
924 | + :param unit: sentry unit |
925 | + :returns: String containing console output of cluster status command |
926 | + """ |
927 | + cmd = 'rabbitmqctl cluster_status' |
928 | + output, _ = self.run_cmd_unit(sentry_unit, cmd) |
929 | + self.log.debug('{} cluster_status:\n{}'.format( |
930 | + sentry_unit.info['unit_name'], output)) |
931 | + return str(output) |
932 | + |
933 | + def get_rmq_cluster_running_nodes(self, sentry_unit): |
934 | + """Parse rabbitmqctl cluster_status output string, return list of |
935 | + running rabbitmq cluster nodes. |
936 | + |
937 | + :param unit: sentry unit |
938 | + :returns: List containing node names of running nodes |
939 | + """ |
940 | + # NOTE(beisner): rabbitmqctl cluster_status output is not |
941 | + # json-parsable, do string chop foo, then json.loads that. |
942 | + str_stat = self.get_rmq_cluster_status(sentry_unit) |
943 | + if 'running_nodes' in str_stat: |
944 | + pos_start = str_stat.find("{running_nodes,") + 15 |
945 | + pos_end = str_stat.find("]},", pos_start) + 1 |
946 | + str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"') |
947 | + run_nodes = json.loads(str_run_nodes) |
948 | + return run_nodes |
949 | + else: |
950 | + return [] |
951 | + |
952 | + def validate_rmq_cluster_running_nodes(self, sentry_units): |
953 | + """Check that all rmq unit hostnames are represented in the |
954 | + cluster_status output of all units. |
955 | + |
956 | + :param host_names: dict of juju unit names to host names |
957 | + :param units: list of sentry unit pointers (all rmq units) |
958 | + :returns: None if successful, otherwise return error message |
959 | + """ |
960 | + host_names = self.get_unit_hostnames(sentry_units) |
961 | + errors = [] |
962 | + |
963 | + # Query every unit for cluster_status running nodes |
964 | + for query_unit in sentry_units: |
965 | + query_unit_name = query_unit.info['unit_name'] |
966 | + running_nodes = self.get_rmq_cluster_running_nodes(query_unit) |
967 | + |
968 | + # Confirm that every unit is represented in the queried unit's |
969 | + # cluster_status running nodes output. |
970 | + for validate_unit in sentry_units: |
971 | + val_host_name = host_names[validate_unit.info['unit_name']] |
972 | + val_node_name = 'rabbit@{}'.format(val_host_name) |
973 | + |
974 | + if val_node_name not in running_nodes: |
975 | + errors.append('Cluster member check failed on {}: {} not ' |
976 | + 'in {}\n'.format(query_unit_name, |
977 | + val_node_name, |
978 | + running_nodes)) |
979 | + if errors: |
980 | + return ''.join(errors) |
981 | + |
982 | + def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None): |
983 | + """Check a single juju rmq unit for ssl and port in the config file.""" |
984 | + host = sentry_unit.info['public-address'] |
985 | + unit_name = sentry_unit.info['unit_name'] |
986 | + |
987 | + conf_file = '/etc/rabbitmq/rabbitmq.config' |
988 | + conf_contents = str(self.file_contents_safe(sentry_unit, |
989 | + conf_file, max_wait=16)) |
990 | + # Checks |
991 | + conf_ssl = 'ssl' in conf_contents |
992 | + conf_port = str(port) in conf_contents |
993 | + |
994 | + # Port explicitly checked in config |
995 | + if port and conf_port and conf_ssl: |
996 | + self.log.debug('SSL is enabled @{}:{} ' |
997 | + '({})'.format(host, port, unit_name)) |
998 | + return True |
999 | + elif port and not conf_port and conf_ssl: |
1000 | + self.log.debug('SSL is enabled @{} but not on port {} ' |
1001 | + '({})'.format(host, port, unit_name)) |
1002 | + return False |
1003 | + # Port not checked (useful when checking that ssl is disabled) |
1004 | + elif not port and conf_ssl: |
1005 | + self.log.debug('SSL is enabled @{}:{} ' |
1006 | + '({})'.format(host, port, unit_name)) |
1007 | + return True |
1008 | + elif not port and not conf_ssl: |
1009 | + self.log.debug('SSL not enabled @{}:{} ' |
1010 | + '({})'.format(host, port, unit_name)) |
1011 | + return False |
1012 | + else: |
1013 | + msg = ('Unknown condition when checking SSL status @{}:{} ' |
1014 | + '({})'.format(host, port, unit_name)) |
1015 | + amulet.raise_status(amulet.FAIL, msg) |
1016 | + |
1017 | + def validate_rmq_ssl_enabled_units(self, sentry_units, port=None): |
1018 | + """Check that ssl is enabled on rmq juju sentry units. |
1019 | + |
1020 | + :param sentry_units: list of all rmq sentry units |
1021 | + :param port: optional ssl port override to validate |
1022 | + :returns: None if successful, otherwise return error message |
1023 | + """ |
1024 | + for sentry_unit in sentry_units: |
1025 | + if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port): |
1026 | + return ('Unexpected condition: ssl is disabled on unit ' |
1027 | + '({})'.format(sentry_unit.info['unit_name'])) |
1028 | + return None |
1029 | + |
1030 | + def validate_rmq_ssl_disabled_units(self, sentry_units): |
1031 | + """Check that ssl is enabled on listed rmq juju sentry units. |
1032 | + |
1033 | + :param sentry_units: list of all rmq sentry units |
1034 | + :returns: True if successful. Raise on error. |
1035 | + """ |
1036 | + for sentry_unit in sentry_units: |
1037 | + if self.rmq_ssl_is_enabled_on_unit(sentry_unit): |
1038 | + return ('Unexpected condition: ssl is enabled on unit ' |
1039 | + '({})'.format(sentry_unit.info['unit_name'])) |
1040 | + return None |
1041 | + |
1042 | + def configure_rmq_ssl_on(self, sentry_units, deployment, |
1043 | + port=None, max_wait=60): |
1044 | + """Turn ssl charm config option on, with optional non-default |
1045 | + ssl port specification. Confirm that it is enabled on every |
1046 | + unit. |
1047 | + |
1048 | + :param sentry_units: list of sentry units |
1049 | + :param deployment: amulet deployment object pointer |
1050 | + :param port: amqp port, use defaults if None |
1051 | + :param max_wait: maximum time to wait in seconds to confirm |
1052 | + :returns: None if successful. Raise on error. |
1053 | + """ |
1054 | + self.log.debug('Setting ssl charm config option: on') |
1055 | + |
1056 | + # Enable RMQ SSL |
1057 | + config = {'ssl': 'on'} |
1058 | + if port: |
1059 | + config['ssl_port'] = port |
1060 | + |
1061 | + deployment.configure('rabbitmq-server', config) |
1062 | + |
1063 | + # Confirm |
1064 | + tries = 0 |
1065 | + ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port) |
1066 | + while ret and tries < (max_wait / 4): |
1067 | + time.sleep(4) |
1068 | + self.log.debug('Attempt {}: {}'.format(tries, ret)) |
1069 | + ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port) |
1070 | + tries += 1 |
1071 | + |
1072 | + if ret: |
1073 | + amulet.raise_status(amulet.FAIL, ret) |
1074 | + |
1075 | + def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60): |
1076 | + """Turn ssl charm config option off, confirm that it is disabled |
1077 | + on every unit. |
1078 | + |
1079 | + :param sentry_units: list of sentry units |
1080 | + :param deployment: amulet deployment object pointer |
1081 | + :param max_wait: maximum time to wait in seconds to confirm |
1082 | + :returns: None if successful. Raise on error. |
1083 | + """ |
1084 | + self.log.debug('Setting ssl charm config option: off') |
1085 | + |
1086 | + # Disable RMQ SSL |
1087 | + config = {'ssl': 'off'} |
1088 | + deployment.configure('rabbitmq-server', config) |
1089 | + |
1090 | + # Confirm |
1091 | + tries = 0 |
1092 | + ret = self.validate_rmq_ssl_disabled_units(sentry_units) |
1093 | + while ret and tries < (max_wait / 4): |
1094 | + time.sleep(4) |
1095 | + self.log.debug('Attempt {}: {}'.format(tries, ret)) |
1096 | + ret = self.validate_rmq_ssl_disabled_units(sentry_units) |
1097 | + tries += 1 |
1098 | + |
1099 | + if ret: |
1100 | + amulet.raise_status(amulet.FAIL, ret) |
1101 | + |
1102 | + def connect_amqp_by_unit(self, sentry_unit, ssl=False, |
1103 | + port=None, fatal=True, |
1104 | + username="testuser1", password="changeme"): |
1105 | + """Establish and return a pika amqp connection to the rabbitmq service |
1106 | + running on a rmq juju unit. |
1107 | + |
1108 | + :param sentry_unit: sentry unit pointer |
1109 | + :param ssl: boolean, default to False |
1110 | + :param port: amqp port, use defaults if None |
1111 | + :param fatal: boolean, default to True (raises on connect error) |
1112 | + :param username: amqp user name, default to testuser1 |
1113 | + :param password: amqp user password |
1114 | + :returns: pika amqp connection pointer or None if failed and non-fatal |
1115 | + """ |
1116 | + host = sentry_unit.info['public-address'] |
1117 | + unit_name = sentry_unit.info['unit_name'] |
1118 | + |
1119 | + # Default port logic if port is not specified |
1120 | + if ssl and not port: |
1121 | + port = 5671 |
1122 | + elif not ssl and not port: |
1123 | + port = 5672 |
1124 | + |
1125 | + self.log.debug('Connecting to amqp on {}:{} ({}) as ' |
1126 | + '{}...'.format(host, port, unit_name, username)) |
1127 | + |
1128 | + try: |
1129 | + credentials = pika.PlainCredentials(username, password) |
1130 | + parameters = pika.ConnectionParameters(host=host, port=port, |
1131 | + credentials=credentials, |
1132 | + ssl=ssl, |
1133 | + connection_attempts=3, |
1134 | + retry_delay=5, |
1135 | + socket_timeout=1) |
1136 | + connection = pika.BlockingConnection(parameters) |
1137 | + assert connection.server_properties['product'] == 'RabbitMQ' |
1138 | + self.log.debug('Connect OK') |
1139 | + return connection |
1140 | + except Exception as e: |
1141 | + msg = ('amqp connection failed to {}:{} as ' |
1142 | + '{} ({})'.format(host, port, username, str(e))) |
1143 | + if fatal: |
1144 | + amulet.raise_status(amulet.FAIL, msg) |
1145 | + else: |
1146 | + self.log.warn(msg) |
1147 | + return None |
1148 | + |
1149 | + def publish_amqp_message_by_unit(self, sentry_unit, message, |
1150 | + queue="test", ssl=False, |
1151 | + username="testuser1", |
1152 | + password="changeme", |
1153 | + port=None): |
1154 | + """Publish an amqp message to a rmq juju unit. |
1155 | + |
1156 | + :param sentry_unit: sentry unit pointer |
1157 | + :param message: amqp message string |
1158 | + :param queue: message queue, default to test |
1159 | + :param username: amqp user name, default to testuser1 |
1160 | + :param password: amqp user password |
1161 | + :param ssl: boolean, default to False |
1162 | + :param port: amqp port, use defaults if None |
1163 | + :returns: None. Raises exception if publish failed. |
1164 | + """ |
1165 | + self.log.debug('Publishing message to {} queue:\n{}'.format(queue, |
1166 | + message)) |
1167 | + connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl, |
1168 | + port=port, |
1169 | + username=username, |
1170 | + password=password) |
1171 | + |
1172 | + # NOTE(beisner): extra debug here re: pika hang potential: |
1173 | + # https://github.com/pika/pika/issues/297 |
1174 | + # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw |
1175 | + self.log.debug('Defining channel...') |
1176 | + channel = connection.channel() |
1177 | + self.log.debug('Declaring queue...') |
1178 | + channel.queue_declare(queue=queue, auto_delete=False, durable=True) |
1179 | + self.log.debug('Publishing message...') |
1180 | + channel.basic_publish(exchange='', routing_key=queue, body=message) |
1181 | + self.log.debug('Closing channel...') |
1182 | + channel.close() |
1183 | + self.log.debug('Closing connection...') |
1184 | + connection.close() |
1185 | + |
1186 | + def get_amqp_message_by_unit(self, sentry_unit, queue="test", |
1187 | + username="testuser1", |
1188 | + password="changeme", |
1189 | + ssl=False, port=None): |
1190 | + """Get an amqp message from a rmq juju unit. |
1191 | + |
1192 | + :param sentry_unit: sentry unit pointer |
1193 | + :param queue: message queue, default to test |
1194 | + :param username: amqp user name, default to testuser1 |
1195 | + :param password: amqp user password |
1196 | + :param ssl: boolean, default to False |
1197 | + :param port: amqp port, use defaults if None |
1198 | + :returns: amqp message body as string. Raise if get fails. |
1199 | + """ |
1200 | + connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl, |
1201 | + port=port, |
1202 | + username=username, |
1203 | + password=password) |
1204 | + channel = connection.channel() |
1205 | + method_frame, _, body = channel.basic_get(queue) |
1206 | + |
1207 | + if method_frame: |
1208 | + self.log.debug('Retreived message from {} queue:\n{}'.format(queue, |
1209 | + body)) |
1210 | + channel.basic_ack(method_frame.delivery_tag) |
1211 | + channel.close() |
1212 | + connection.close() |
1213 | + return body |
1214 | + else: |
1215 | + msg = 'No message retrieved.' |
1216 | + amulet.raise_status(amulet.FAIL, msg) |
1217 | |
1218 | === added directory 'tests/charmhelpers/core' |
1219 | === added file 'tests/charmhelpers/core/__init__.py' |
1220 | --- tests/charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000 |
1221 | +++ tests/charmhelpers/core/__init__.py 2015-09-15 12:31:21 +0000 |
1222 | @@ -0,0 +1,15 @@ |
1223 | +# Copyright 2014-2015 Canonical Limited. |
1224 | +# |
1225 | +# This file is part of charm-helpers. |
1226 | +# |
1227 | +# charm-helpers is free software: you can redistribute it and/or modify |
1228 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1229 | +# published by the Free Software Foundation. |
1230 | +# |
1231 | +# charm-helpers is distributed in the hope that it will be useful, |
1232 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1233 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1234 | +# GNU Lesser General Public License for more details. |
1235 | +# |
1236 | +# You should have received a copy of the GNU Lesser General Public License |
1237 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1238 | |
1239 | === added file 'tests/charmhelpers/core/hookenv.py' |
1240 | --- tests/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000 |
1241 | +++ tests/charmhelpers/core/hookenv.py 2015-09-15 12:31:21 +0000 |
1242 | @@ -0,0 +1,898 @@ |
1243 | +# Copyright 2014-2015 Canonical Limited. |
1244 | +# |
1245 | +# This file is part of charm-helpers. |
1246 | +# |
1247 | +# charm-helpers is free software: you can redistribute it and/or modify |
1248 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1249 | +# published by the Free Software Foundation. |
1250 | +# |
1251 | +# charm-helpers is distributed in the hope that it will be useful, |
1252 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1253 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1254 | +# GNU Lesser General Public License for more details. |
1255 | +# |
1256 | +# You should have received a copy of the GNU Lesser General Public License |
1257 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1258 | + |
1259 | +"Interactions with the Juju environment" |
1260 | +# Copyright 2013 Canonical Ltd. |
1261 | +# |
1262 | +# Authors: |
1263 | +# Charm Helpers Developers <juju@lists.ubuntu.com> |
1264 | + |
1265 | +from __future__ import print_function |
1266 | +import copy |
1267 | +from distutils.version import LooseVersion |
1268 | +from functools import wraps |
1269 | +import glob |
1270 | +import os |
1271 | +import json |
1272 | +import yaml |
1273 | +import subprocess |
1274 | +import sys |
1275 | +import errno |
1276 | +import tempfile |
1277 | +from subprocess import CalledProcessError |
1278 | + |
1279 | +import six |
1280 | +if not six.PY3: |
1281 | + from UserDict import UserDict |
1282 | +else: |
1283 | + from collections import UserDict |
1284 | + |
1285 | +CRITICAL = "CRITICAL" |
1286 | +ERROR = "ERROR" |
1287 | +WARNING = "WARNING" |
1288 | +INFO = "INFO" |
1289 | +DEBUG = "DEBUG" |
1290 | +MARKER = object() |
1291 | + |
1292 | +cache = {} |
1293 | + |
1294 | + |
1295 | +def cached(func): |
1296 | + """Cache return values for multiple executions of func + args |
1297 | + |
1298 | + For example:: |
1299 | + |
1300 | + @cached |
1301 | + def unit_get(attribute): |
1302 | + pass |
1303 | + |
1304 | + unit_get('test') |
1305 | + |
1306 | + will cache the result of unit_get + 'test' for future calls. |
1307 | + """ |
1308 | + @wraps(func) |
1309 | + def wrapper(*args, **kwargs): |
1310 | + global cache |
1311 | + key = str((func, args, kwargs)) |
1312 | + try: |
1313 | + return cache[key] |
1314 | + except KeyError: |
1315 | + pass # Drop out of the exception handler scope. |
1316 | + res = func(*args, **kwargs) |
1317 | + cache[key] = res |
1318 | + return res |
1319 | + wrapper._wrapped = func |
1320 | + return wrapper |
1321 | + |
1322 | + |
1323 | +def flush(key): |
1324 | + """Flushes any entries from function cache where the |
1325 | + key is found in the function+args """ |
1326 | + flush_list = [] |
1327 | + for item in cache: |
1328 | + if key in item: |
1329 | + flush_list.append(item) |
1330 | + for item in flush_list: |
1331 | + del cache[item] |
1332 | + |
1333 | + |
1334 | +def log(message, level=None): |
1335 | + """Write a message to the juju log""" |
1336 | + command = ['juju-log'] |
1337 | + if level: |
1338 | + command += ['-l', level] |
1339 | + if not isinstance(message, six.string_types): |
1340 | + message = repr(message) |
1341 | + command += [message] |
1342 | + # Missing juju-log should not cause failures in unit tests |
1343 | + # Send log output to stderr |
1344 | + try: |
1345 | + subprocess.call(command) |
1346 | + except OSError as e: |
1347 | + if e.errno == errno.ENOENT: |
1348 | + if level: |
1349 | + message = "{}: {}".format(level, message) |
1350 | + message = "juju-log: {}".format(message) |
1351 | + print(message, file=sys.stderr) |
1352 | + else: |
1353 | + raise |
1354 | + |
1355 | + |
1356 | +class Serializable(UserDict): |
1357 | + """Wrapper, an object that can be serialized to yaml or json""" |
1358 | + |
1359 | + def __init__(self, obj): |
1360 | + # wrap the object |
1361 | + UserDict.__init__(self) |
1362 | + self.data = obj |
1363 | + |
1364 | + def __getattr__(self, attr): |
1365 | + # See if this object has attribute. |
1366 | + if attr in ("json", "yaml", "data"): |
1367 | + return self.__dict__[attr] |
1368 | + # Check for attribute in wrapped object. |
1369 | + got = getattr(self.data, attr, MARKER) |
1370 | + if got is not MARKER: |
1371 | + return got |
1372 | + # Proxy to the wrapped object via dict interface. |
1373 | + try: |
1374 | + return self.data[attr] |
1375 | + except KeyError: |
1376 | + raise AttributeError(attr) |
1377 | + |
1378 | + def __getstate__(self): |
1379 | + # Pickle as a standard dictionary. |
1380 | + return self.data |
1381 | + |
1382 | + def __setstate__(self, state): |
1383 | + # Unpickle into our wrapper. |
1384 | + self.data = state |
1385 | + |
1386 | + def json(self): |
1387 | + """Serialize the object to json""" |
1388 | + return json.dumps(self.data) |
1389 | + |
1390 | + def yaml(self): |
1391 | + """Serialize the object to yaml""" |
1392 | + return yaml.dump(self.data) |
1393 | + |
1394 | + |
1395 | +def execution_environment(): |
1396 | + """A convenient bundling of the current execution context""" |
1397 | + context = {} |
1398 | + context['conf'] = config() |
1399 | + if relation_id(): |
1400 | + context['reltype'] = relation_type() |
1401 | + context['relid'] = relation_id() |
1402 | + context['rel'] = relation_get() |
1403 | + context['unit'] = local_unit() |
1404 | + context['rels'] = relations() |
1405 | + context['env'] = os.environ |
1406 | + return context |
1407 | + |
1408 | + |
1409 | +def in_relation_hook(): |
1410 | + """Determine whether we're running in a relation hook""" |
1411 | + return 'JUJU_RELATION' in os.environ |
1412 | + |
1413 | + |
1414 | +def relation_type(): |
1415 | + """The scope for the current relation hook""" |
1416 | + return os.environ.get('JUJU_RELATION', None) |
1417 | + |
1418 | + |
1419 | +@cached |
1420 | +def relation_id(relation_name=None, service_or_unit=None): |
1421 | + """The relation ID for the current or a specified relation""" |
1422 | + if not relation_name and not service_or_unit: |
1423 | + return os.environ.get('JUJU_RELATION_ID', None) |
1424 | + elif relation_name and service_or_unit: |
1425 | + service_name = service_or_unit.split('/')[0] |
1426 | + for relid in relation_ids(relation_name): |
1427 | + remote_service = remote_service_name(relid) |
1428 | + if remote_service == service_name: |
1429 | + return relid |
1430 | + else: |
1431 | + raise ValueError('Must specify neither or both of relation_name and service_or_unit') |
1432 | + |
1433 | + |
1434 | +def local_unit(): |
1435 | + """Local unit ID""" |
1436 | + return os.environ['JUJU_UNIT_NAME'] |
1437 | + |
1438 | + |
1439 | +def remote_unit(): |
1440 | + """The remote unit for the current relation hook""" |
1441 | + return os.environ.get('JUJU_REMOTE_UNIT', None) |
1442 | + |
1443 | + |
1444 | +def service_name(): |
1445 | + """The name service group this unit belongs to""" |
1446 | + return local_unit().split('/')[0] |
1447 | + |
1448 | + |
1449 | +@cached |
1450 | +def remote_service_name(relid=None): |
1451 | + """The remote service name for a given relation-id (or the current relation)""" |
1452 | + if relid is None: |
1453 | + unit = remote_unit() |
1454 | + else: |
1455 | + units = related_units(relid) |
1456 | + unit = units[0] if units else None |
1457 | + return unit.split('/')[0] if unit else None |
1458 | + |
1459 | + |
1460 | +def hook_name(): |
1461 | + """The name of the currently executing hook""" |
1462 | + return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0])) |
1463 | + |
1464 | + |
1465 | +class Config(dict): |
1466 | + """A dictionary representation of the charm's config.yaml, with some |
1467 | + extra features: |
1468 | + |
1469 | + - See which values in the dictionary have changed since the previous hook. |
1470 | + - For values that have changed, see what the previous value was. |
1471 | + - Store arbitrary data for use in a later hook. |
1472 | + |
1473 | + NOTE: Do not instantiate this object directly - instead call |
1474 | + ``hookenv.config()``, which will return an instance of :class:`Config`. |
1475 | + |
1476 | + Example usage:: |
1477 | + |
1478 | + >>> # inside a hook |
1479 | + >>> from charmhelpers.core import hookenv |
1480 | + >>> config = hookenv.config() |
1481 | + >>> config['foo'] |
1482 | + 'bar' |
1483 | + >>> # store a new key/value for later use |
1484 | + >>> config['mykey'] = 'myval' |
1485 | + |
1486 | + |
1487 | + >>> # user runs `juju set mycharm foo=baz` |
1488 | + >>> # now we're inside subsequent config-changed hook |
1489 | + >>> config = hookenv.config() |
1490 | + >>> config['foo'] |
1491 | + 'baz' |
1492 | + >>> # test to see if this val has changed since last hook |
1493 | + >>> config.changed('foo') |
1494 | + True |
1495 | + >>> # what was the previous value? |
1496 | + >>> config.previous('foo') |
1497 | + 'bar' |
1498 | + >>> # keys/values that we add are preserved across hooks |
1499 | + >>> config['mykey'] |
1500 | + 'myval' |
1501 | + |
1502 | + """ |
1503 | + CONFIG_FILE_NAME = '.juju-persistent-config' |
1504 | + |
1505 | + def __init__(self, *args, **kw): |
1506 | + super(Config, self).__init__(*args, **kw) |
1507 | + self.implicit_save = True |
1508 | + self._prev_dict = None |
1509 | + self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) |
1510 | + if os.path.exists(self.path): |
1511 | + self.load_previous() |
1512 | + atexit(self._implicit_save) |
1513 | + |
1514 | + def load_previous(self, path=None): |
1515 | + """Load previous copy of config from disk. |
1516 | + |
1517 | + In normal usage you don't need to call this method directly - it |
1518 | + is called automatically at object initialization. |
1519 | + |
1520 | + :param path: |
1521 | + |
1522 | + File path from which to load the previous config. If `None`, |
1523 | + config is loaded from the default location. If `path` is |
1524 | + specified, subsequent `save()` calls will write to the same |
1525 | + path. |
1526 | + |
1527 | + """ |
1528 | + self.path = path or self.path |
1529 | + with open(self.path) as f: |
1530 | + self._prev_dict = json.load(f) |
1531 | + for k, v in copy.deepcopy(self._prev_dict).items(): |
1532 | + if k not in self: |
1533 | + self[k] = v |
1534 | + |
1535 | + def changed(self, key): |
1536 | + """Return True if the current value for this key is different from |
1537 | + the previous value. |
1538 | + |
1539 | + """ |
1540 | + if self._prev_dict is None: |
1541 | + return True |
1542 | + return self.previous(key) != self.get(key) |
1543 | + |
1544 | + def previous(self, key): |
1545 | + """Return previous value for this key, or None if there |
1546 | + is no previous value. |
1547 | + |
1548 | + """ |
1549 | + if self._prev_dict: |
1550 | + return self._prev_dict.get(key) |
1551 | + return None |
1552 | + |
1553 | + def save(self): |
1554 | + """Save this config to disk. |
1555 | + |
1556 | + If the charm is using the :mod:`Services Framework <services.base>` |
1557 | + or :meth:'@hook <Hooks.hook>' decorator, this |
1558 | + is called automatically at the end of successful hook execution. |
1559 | + Otherwise, it should be called directly by user code. |
1560 | + |
1561 | + To disable automatic saves, set ``implicit_save=False`` on this |
1562 | + instance. |
1563 | + |
1564 | + """ |
1565 | + with open(self.path, 'w') as f: |
1566 | + json.dump(self, f) |
1567 | + |
1568 | + def _implicit_save(self): |
1569 | + if self.implicit_save: |
1570 | + self.save() |
1571 | + |
1572 | + |
1573 | +@cached |
1574 | +def config(scope=None): |
1575 | + """Juju charm configuration""" |
1576 | + config_cmd_line = ['config-get'] |
1577 | + if scope is not None: |
1578 | + config_cmd_line.append(scope) |
1579 | + config_cmd_line.append('--format=json') |
1580 | + try: |
1581 | + config_data = json.loads( |
1582 | + subprocess.check_output(config_cmd_line).decode('UTF-8')) |
1583 | + if scope is not None: |
1584 | + return config_data |
1585 | + return Config(config_data) |
1586 | + except ValueError: |
1587 | + return None |
1588 | + |
1589 | + |
1590 | +@cached |
1591 | +def relation_get(attribute=None, unit=None, rid=None): |
1592 | + """Get relation information""" |
1593 | + _args = ['relation-get', '--format=json'] |
1594 | + if rid: |
1595 | + _args.append('-r') |
1596 | + _args.append(rid) |
1597 | + _args.append(attribute or '-') |
1598 | + if unit: |
1599 | + _args.append(unit) |
1600 | + try: |
1601 | + return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
1602 | + except ValueError: |
1603 | + return None |
1604 | + except CalledProcessError as e: |
1605 | + if e.returncode == 2: |
1606 | + return None |
1607 | + raise |
1608 | + |
1609 | + |
1610 | +def relation_set(relation_id=None, relation_settings=None, **kwargs): |
1611 | + """Set relation information for the current unit""" |
1612 | + relation_settings = relation_settings if relation_settings else {} |
1613 | + relation_cmd_line = ['relation-set'] |
1614 | + accepts_file = "--file" in subprocess.check_output( |
1615 | + relation_cmd_line + ["--help"], universal_newlines=True) |
1616 | + if relation_id is not None: |
1617 | + relation_cmd_line.extend(('-r', relation_id)) |
1618 | + settings = relation_settings.copy() |
1619 | + settings.update(kwargs) |
1620 | + for key, value in settings.items(): |
1621 | + # Force value to be a string: it always should, but some call |
1622 | + # sites pass in things like dicts or numbers. |
1623 | + if value is not None: |
1624 | + settings[key] = "{}".format(value) |
1625 | + if accepts_file: |
1626 | + # --file was introduced in Juju 1.23.2. Use it by default if |
1627 | + # available, since otherwise we'll break if the relation data is |
1628 | + # too big. Ideally we should tell relation-set to read the data from |
1629 | + # stdin, but that feature is broken in 1.23.2: Bug #1454678. |
1630 | + with tempfile.NamedTemporaryFile(delete=False) as settings_file: |
1631 | + settings_file.write(yaml.safe_dump(settings).encode("utf-8")) |
1632 | + subprocess.check_call( |
1633 | + relation_cmd_line + ["--file", settings_file.name]) |
1634 | + os.remove(settings_file.name) |
1635 | + else: |
1636 | + for key, value in settings.items(): |
1637 | + if value is None: |
1638 | + relation_cmd_line.append('{}='.format(key)) |
1639 | + else: |
1640 | + relation_cmd_line.append('{}={}'.format(key, value)) |
1641 | + subprocess.check_call(relation_cmd_line) |
1642 | + # Flush cache of any relation-gets for local unit |
1643 | + flush(local_unit()) |
1644 | + |
1645 | + |
1646 | +def relation_clear(r_id=None): |
1647 | + ''' Clears any relation data already set on relation r_id ''' |
1648 | + settings = relation_get(rid=r_id, |
1649 | + unit=local_unit()) |
1650 | + for setting in settings: |
1651 | + if setting not in ['public-address', 'private-address']: |
1652 | + settings[setting] = None |
1653 | + relation_set(relation_id=r_id, |
1654 | + **settings) |
1655 | + |
1656 | + |
1657 | +@cached |
1658 | +def relation_ids(reltype=None): |
1659 | + """A list of relation_ids""" |
1660 | + reltype = reltype or relation_type() |
1661 | + relid_cmd_line = ['relation-ids', '--format=json'] |
1662 | + if reltype is not None: |
1663 | + relid_cmd_line.append(reltype) |
1664 | + return json.loads( |
1665 | + subprocess.check_output(relid_cmd_line).decode('UTF-8')) or [] |
1666 | + return [] |
1667 | + |
1668 | + |
1669 | +@cached |
1670 | +def related_units(relid=None): |
1671 | + """A list of related units""" |
1672 | + relid = relid or relation_id() |
1673 | + units_cmd_line = ['relation-list', '--format=json'] |
1674 | + if relid is not None: |
1675 | + units_cmd_line.extend(('-r', relid)) |
1676 | + return json.loads( |
1677 | + subprocess.check_output(units_cmd_line).decode('UTF-8')) or [] |
1678 | + |
1679 | + |
1680 | +@cached |
1681 | +def relation_for_unit(unit=None, rid=None): |
1682 | + """Get the json represenation of a unit's relation""" |
1683 | + unit = unit or remote_unit() |
1684 | + relation = relation_get(unit=unit, rid=rid) |
1685 | + for key in relation: |
1686 | + if key.endswith('-list'): |
1687 | + relation[key] = relation[key].split() |
1688 | + relation['__unit__'] = unit |
1689 | + return relation |
1690 | + |
1691 | + |
1692 | +@cached |
1693 | +def relations_for_id(relid=None): |
1694 | + """Get relations of a specific relation ID""" |
1695 | + relation_data = [] |
1696 | + relid = relid or relation_ids() |
1697 | + for unit in related_units(relid): |
1698 | + unit_data = relation_for_unit(unit, relid) |
1699 | + unit_data['__relid__'] = relid |
1700 | + relation_data.append(unit_data) |
1701 | + return relation_data |
1702 | + |
1703 | + |
1704 | +@cached |
1705 | +def relations_of_type(reltype=None): |
1706 | + """Get relations of a specific type""" |
1707 | + relation_data = [] |
1708 | + reltype = reltype or relation_type() |
1709 | + for relid in relation_ids(reltype): |
1710 | + for relation in relations_for_id(relid): |
1711 | + relation['__relid__'] = relid |
1712 | + relation_data.append(relation) |
1713 | + return relation_data |
1714 | + |
1715 | + |
1716 | +@cached |
1717 | +def metadata(): |
1718 | + """Get the current charm metadata.yaml contents as a python object""" |
1719 | + with open(os.path.join(charm_dir(), 'metadata.yaml')) as md: |
1720 | + return yaml.safe_load(md) |
1721 | + |
1722 | + |
1723 | +@cached |
1724 | +def relation_types(): |
1725 | + """Get a list of relation types supported by this charm""" |
1726 | + rel_types = [] |
1727 | + md = metadata() |
1728 | + for key in ('provides', 'requires', 'peers'): |
1729 | + section = md.get(key) |
1730 | + if section: |
1731 | + rel_types.extend(section.keys()) |
1732 | + return rel_types |
1733 | + |
1734 | + |
1735 | +@cached |
1736 | +def relation_to_interface(relation_name): |
1737 | + """ |
1738 | + Given the name of a relation, return the interface that relation uses. |
1739 | + |
1740 | + :returns: The interface name, or ``None``. |
1741 | + """ |
1742 | + return relation_to_role_and_interface(relation_name)[1] |
1743 | + |
1744 | + |
1745 | +@cached |
1746 | +def relation_to_role_and_interface(relation_name): |
1747 | + """ |
1748 | + Given the name of a relation, return the role and the name of the interface |
1749 | + that relation uses (where role is one of ``provides``, ``requires``, or ``peer``). |
1750 | + |
1751 | + :returns: A tuple containing ``(role, interface)``, or ``(None, None)``. |
1752 | + """ |
1753 | + _metadata = metadata() |
1754 | + for role in ('provides', 'requires', 'peer'): |
1755 | + interface = _metadata.get(role, {}).get(relation_name, {}).get('interface') |
1756 | + if interface: |
1757 | + return role, interface |
1758 | + return None, None |
1759 | + |
1760 | + |
1761 | +@cached |
1762 | +def role_and_interface_to_relations(role, interface_name): |
1763 | + """ |
1764 | + Given a role and interface name, return a list of relation names for the |
1765 | + current charm that use that interface under that role (where role is one |
1766 | + of ``provides``, ``requires``, or ``peer``). |
1767 | + |
1768 | + :returns: A list of relation names. |
1769 | + """ |
1770 | + _metadata = metadata() |
1771 | + results = [] |
1772 | + for relation_name, relation in _metadata.get(role, {}).items(): |
1773 | + if relation['interface'] == interface_name: |
1774 | + results.append(relation_name) |
1775 | + return results |
1776 | + |
1777 | + |
1778 | +@cached |
1779 | +def interface_to_relations(interface_name): |
1780 | + """ |
1781 | + Given an interface, return a list of relation names for the current |
1782 | + charm that use that interface. |
1783 | + |
1784 | + :returns: A list of relation names. |
1785 | + """ |
1786 | + results = [] |
1787 | + for role in ('provides', 'requires', 'peer'): |
1788 | + results.extend(role_and_interface_to_relations(role, interface_name)) |
1789 | + return results |
1790 | + |
1791 | + |
1792 | +@cached |
1793 | +def charm_name(): |
1794 | + """Get the name of the current charm as is specified on metadata.yaml""" |
1795 | + return metadata().get('name') |
1796 | + |
1797 | + |
1798 | +@cached |
1799 | +def relations(): |
1800 | + """Get a nested dictionary of relation data for all related units""" |
1801 | + rels = {} |
1802 | + for reltype in relation_types(): |
1803 | + relids = {} |
1804 | + for relid in relation_ids(reltype): |
1805 | + units = {local_unit(): relation_get(unit=local_unit(), rid=relid)} |
1806 | + for unit in related_units(relid): |
1807 | + reldata = relation_get(unit=unit, rid=relid) |
1808 | + units[unit] = reldata |
1809 | + relids[relid] = units |
1810 | + rels[reltype] = relids |
1811 | + return rels |
1812 | + |
1813 | + |
1814 | +@cached |
1815 | +def is_relation_made(relation, keys='private-address'): |
1816 | + ''' |
1817 | + Determine whether a relation is established by checking for |
1818 | + presence of key(s). If a list of keys is provided, they |
1819 | + must all be present for the relation to be identified as made |
1820 | + ''' |
1821 | + if isinstance(keys, str): |
1822 | + keys = [keys] |
1823 | + for r_id in relation_ids(relation): |
1824 | + for unit in related_units(r_id): |
1825 | + context = {} |
1826 | + for k in keys: |
1827 | + context[k] = relation_get(k, rid=r_id, |
1828 | + unit=unit) |
1829 | + if None not in context.values(): |
1830 | + return True |
1831 | + return False |
1832 | + |
1833 | + |
1834 | +def open_port(port, protocol="TCP"): |
1835 | + """Open a service network port""" |
1836 | + _args = ['open-port'] |
1837 | + _args.append('{}/{}'.format(port, protocol)) |
1838 | + subprocess.check_call(_args) |
1839 | + |
1840 | + |
1841 | +def close_port(port, protocol="TCP"): |
1842 | + """Close a service network port""" |
1843 | + _args = ['close-port'] |
1844 | + _args.append('{}/{}'.format(port, protocol)) |
1845 | + subprocess.check_call(_args) |
1846 | + |
1847 | + |
1848 | +@cached |
1849 | +def unit_get(attribute): |
1850 | + """Get the unit ID for the remote unit""" |
1851 | + _args = ['unit-get', '--format=json', attribute] |
1852 | + try: |
1853 | + return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
1854 | + except ValueError: |
1855 | + return None |
1856 | + |
1857 | + |
1858 | +def unit_public_ip(): |
1859 | + """Get this unit's public IP address""" |
1860 | + return unit_get('public-address') |
1861 | + |
1862 | + |
1863 | +def unit_private_ip(): |
1864 | + """Get this unit's private IP address""" |
1865 | + return unit_get('private-address') |
1866 | + |
1867 | + |
1868 | +class UnregisteredHookError(Exception): |
1869 | + """Raised when an undefined hook is called""" |
1870 | + pass |
1871 | + |
1872 | + |
1873 | +class Hooks(object): |
1874 | + """A convenient handler for hook functions. |
1875 | + |
1876 | + Example:: |
1877 | + |
1878 | + hooks = Hooks() |
1879 | + |
1880 | + # register a hook, taking its name from the function name |
1881 | + @hooks.hook() |
1882 | + def install(): |
1883 | + pass # your code here |
1884 | + |
1885 | + # register a hook, providing a custom hook name |
1886 | + @hooks.hook("config-changed") |
1887 | + def config_changed(): |
1888 | + pass # your code here |
1889 | + |
1890 | + if __name__ == "__main__": |
1891 | + # execute a hook based on the name the program is called by |
1892 | + hooks.execute(sys.argv) |
1893 | + """ |
1894 | + |
1895 | + def __init__(self, config_save=None): |
1896 | + super(Hooks, self).__init__() |
1897 | + self._hooks = {} |
1898 | + |
1899 | + # For unknown reasons, we allow the Hooks constructor to override |
1900 | + # config().implicit_save. |
1901 | + if config_save is not None: |
1902 | + config().implicit_save = config_save |
1903 | + |
1904 | + def register(self, name, function): |
1905 | + """Register a hook""" |
1906 | + self._hooks[name] = function |
1907 | + |
1908 | + def execute(self, args): |
1909 | + """Execute a registered hook based on args[0]""" |
1910 | + _run_atstart() |
1911 | + hook_name = os.path.basename(args[0]) |
1912 | + if hook_name in self._hooks: |
1913 | + try: |
1914 | + self._hooks[hook_name]() |
1915 | + except SystemExit as x: |
1916 | + if x.code is None or x.code == 0: |
1917 | + _run_atexit() |
1918 | + raise |
1919 | + _run_atexit() |
1920 | + else: |
1921 | + raise UnregisteredHookError(hook_name) |
1922 | + |
1923 | + def hook(self, *hook_names): |
1924 | + """Decorator, registering them as hooks""" |
1925 | + def wrapper(decorated): |
1926 | + for hook_name in hook_names: |
1927 | + self.register(hook_name, decorated) |
1928 | + else: |
1929 | + self.register(decorated.__name__, decorated) |
1930 | + if '_' in decorated.__name__: |
1931 | + self.register( |
1932 | + decorated.__name__.replace('_', '-'), decorated) |
1933 | + return decorated |
1934 | + return wrapper |
1935 | + |
1936 | + |
1937 | +def charm_dir(): |
1938 | + """Return the root directory of the current charm""" |
1939 | + return os.environ.get('CHARM_DIR') |
1940 | + |
1941 | + |
1942 | +@cached |
1943 | +def action_get(key=None): |
1944 | + """Gets the value of an action parameter, or all key/value param pairs""" |
1945 | + cmd = ['action-get'] |
1946 | + if key is not None: |
1947 | + cmd.append(key) |
1948 | + cmd.append('--format=json') |
1949 | + action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
1950 | + return action_data |
1951 | + |
1952 | + |
1953 | +def action_set(values): |
1954 | + """Sets the values to be returned after the action finishes""" |
1955 | + cmd = ['action-set'] |
1956 | + for k, v in list(values.items()): |
1957 | + cmd.append('{}={}'.format(k, v)) |
1958 | + subprocess.check_call(cmd) |
1959 | + |
1960 | + |
1961 | +def action_fail(message): |
1962 | + """Sets the action status to failed and sets the error message. |
1963 | + |
1964 | + The results set by action_set are preserved.""" |
1965 | + subprocess.check_call(['action-fail', message]) |
1966 | + |
1967 | + |
1968 | +def action_name(): |
1969 | + """Get the name of the currently executing action.""" |
1970 | + return os.environ.get('JUJU_ACTION_NAME') |
1971 | + |
1972 | + |
1973 | +def action_uuid(): |
1974 | + """Get the UUID of the currently executing action.""" |
1975 | + return os.environ.get('JUJU_ACTION_UUID') |
1976 | + |
1977 | + |
1978 | +def action_tag(): |
1979 | + """Get the tag for the currently executing action.""" |
1980 | + return os.environ.get('JUJU_ACTION_TAG') |
1981 | + |
1982 | + |
1983 | +def status_set(workload_state, message): |
1984 | + """Set the workload state with a message |
1985 | + |
1986 | + Use status-set to set the workload state with a message which is visible |
1987 | + to the user via juju status. If the status-set command is not found then |
1988 | + assume this is juju < 1.23 and juju-log the message unstead. |
1989 | + |
1990 | + workload_state -- valid juju workload state. |
1991 | + message -- status update message |
1992 | + """ |
1993 | + valid_states = ['maintenance', 'blocked', 'waiting', 'active'] |
1994 | + if workload_state not in valid_states: |
1995 | + raise ValueError( |
1996 | + '{!r} is not a valid workload state'.format(workload_state) |
1997 | + ) |
1998 | + cmd = ['status-set', workload_state, message] |
1999 | + try: |
2000 | + ret = subprocess.call(cmd) |
2001 | + if ret == 0: |
2002 | + return |
2003 | + except OSError as e: |
2004 | + if e.errno != errno.ENOENT: |
2005 | + raise |
2006 | + log_message = 'status-set failed: {} {}'.format(workload_state, |
2007 | + message) |
2008 | + log(log_message, level='INFO') |
2009 | + |
2010 | + |
2011 | +def status_get(): |
2012 | + """Retrieve the previously set juju workload state and message |
2013 | + |
2014 | + If the status-get command is not found then assume this is juju < 1.23 and |
2015 | + return 'unknown', "" |
2016 | + |
2017 | + """ |
2018 | + cmd = ['status-get', "--format=json", "--include-data"] |
2019 | + try: |
2020 | + raw_status = subprocess.check_output(cmd) |
2021 | + except OSError as e: |
2022 | + if e.errno == errno.ENOENT: |
2023 | + return ('unknown', "") |
2024 | + else: |
2025 | + raise |
2026 | + else: |
2027 | + status = json.loads(raw_status.decode("UTF-8")) |
2028 | + return (status["status"], status["message"]) |
2029 | + |
2030 | + |
2031 | +def translate_exc(from_exc, to_exc): |
2032 | + def inner_translate_exc1(f): |
2033 | + def inner_translate_exc2(*args, **kwargs): |
2034 | + try: |
2035 | + return f(*args, **kwargs) |
2036 | + except from_exc: |
2037 | + raise to_exc |
2038 | + |
2039 | + return inner_translate_exc2 |
2040 | + |
2041 | + return inner_translate_exc1 |
2042 | + |
2043 | + |
2044 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
2045 | +def is_leader(): |
2046 | + """Does the current unit hold the juju leadership |
2047 | + |
2048 | + Uses juju to determine whether the current unit is the leader of its peers |
2049 | + """ |
2050 | + cmd = ['is-leader', '--format=json'] |
2051 | + return json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
2052 | + |
2053 | + |
2054 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
2055 | +def leader_get(attribute=None): |
2056 | + """Juju leader get value(s)""" |
2057 | + cmd = ['leader-get', '--format=json'] + [attribute or '-'] |
2058 | + return json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
2059 | + |
2060 | + |
2061 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
2062 | +def leader_set(settings=None, **kwargs): |
2063 | + """Juju leader set value(s)""" |
2064 | + # Don't log secrets. |
2065 | + # log("Juju leader-set '%s'" % (settings), level=DEBUG) |
2066 | + cmd = ['leader-set'] |
2067 | + settings = settings or {} |
2068 | + settings.update(kwargs) |
2069 | + for k, v in settings.items(): |
2070 | + if v is None: |
2071 | + cmd.append('{}='.format(k)) |
2072 | + else: |
2073 | + cmd.append('{}={}'.format(k, v)) |
2074 | + subprocess.check_call(cmd) |
2075 | + |
2076 | + |
2077 | +@cached |
2078 | +def juju_version(): |
2079 | + """Full version string (eg. '1.23.3.1-trusty-amd64')""" |
2080 | + # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1 |
2081 | + jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0] |
2082 | + return subprocess.check_output([jujud, 'version'], |
2083 | + universal_newlines=True).strip() |
2084 | + |
2085 | + |
2086 | +@cached |
2087 | +def has_juju_version(minimum_version): |
2088 | + """Return True if the Juju version is at least the provided version""" |
2089 | + return LooseVersion(juju_version()) >= LooseVersion(minimum_version) |
2090 | + |
2091 | + |
2092 | +_atexit = [] |
2093 | +_atstart = [] |
2094 | + |
2095 | + |
2096 | +def atstart(callback, *args, **kwargs): |
2097 | + '''Schedule a callback to run before the main hook. |
2098 | + |
2099 | + Callbacks are run in the order they were added. |
2100 | + |
2101 | + This is useful for modules and classes to perform initialization |
2102 | + and inject behavior. In particular: |
2103 | + |
2104 | + - Run common code before all of your hooks, such as logging |
2105 | + the hook name or interesting relation data. |
2106 | + - Defer object or module initialization that requires a hook |
2107 | + context until we know there actually is a hook context, |
2108 | + making testing easier. |
2109 | + - Rather than requiring charm authors to include boilerplate to |
2110 | + invoke your helper's behavior, have it run automatically if |
2111 | + your object is instantiated or module imported. |
2112 | + |
2113 | + This is not at all useful after your hook framework as been launched. |
2114 | + ''' |
2115 | + global _atstart |
2116 | + _atstart.append((callback, args, kwargs)) |
2117 | + |
2118 | + |
2119 | +def atexit(callback, *args, **kwargs): |
2120 | + '''Schedule a callback to run on successful hook completion. |
2121 | + |
2122 | + Callbacks are run in the reverse order that they were added.''' |
2123 | + _atexit.append((callback, args, kwargs)) |
2124 | + |
2125 | + |
2126 | +def _run_atstart(): |
2127 | + '''Hook frameworks must invoke this before running the main hook body.''' |
2128 | + global _atstart |
2129 | + for callback, args, kwargs in _atstart: |
2130 | + callback(*args, **kwargs) |
2131 | + del _atstart[:] |
2132 | + |
2133 | + |
2134 | +def _run_atexit(): |
2135 | + '''Hook frameworks must invoke this after the main hook body has |
2136 | + successfully completed. Do not invoke it if the hook fails.''' |
2137 | + global _atexit |
2138 | + for callback, args, kwargs in reversed(_atexit): |
2139 | + callback(*args, **kwargs) |
2140 | + del _atexit[:] |
charm_unit_test #7599 percona- cluster- next for tealeg mp268238
UNIT OK: passed
Build: http:// 10.245. 162.77: 8080/job/ charm_unit_ test/7599/