Merge lp:~tealeg/charms/trusty/percona-cluster/pause-and-resume into lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next

Proposed by Geoff Teale
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
Reviewer Review Type Date Requested Status
Chris Glass (community) Approve
Adam Collard (community) Approve
Review via email: mp+268238@code.launchpad.net

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.

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

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/

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

charm_lint_check #8197 percona-cluster-next for tealeg mp268238
    LINT OK: passed

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

Revision history for this message
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/contrib/openstack/amulet/utils.py.

If there are non-OpenStack-specific, amulet-specific helpers which are useful in other charm tests, please land those in charmhelpers/contrib/amulet/utils.py.

Feel free to holler with any questions. Thanks again!

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

charm_amulet_test #5843 percona-cluster-next for tealeg mp268238
    AMULET OK: passed

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

Revision history for this message
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/contrib/openstack/amulet/utils.py.
>
> If there are non-OpenStack-specific, amulet-specific helpers which are useful
> in other charm tests, please land those in
> charmhelpers/contrib/amulet/utils.py.
>
> Feel free to holler with any questions. Thanks again!

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

charm_unit_test #7817 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #8423 percona-cluster-next for tealeg mp268238
    LINT OK: passed

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

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

charm_amulet_test #5930 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12136124/
Build: http://10.245.162.77:8080/job/charm_amulet_test/5930/

Revision history for this message
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://paste.ubuntu.com/12136867/

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

Otherwise, pending passing tests, looks good to me. Thank you for your work on this!

Revision history for this message
Geoff Teale (tealeg) wrote :

Cheers Ryan, will do!

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

charm_lint_check #8477 percona-cluster-next for tealeg mp268238
    LINT OK: passed

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

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

charm_unit_test #7868 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_unit_test #7869 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #8478 percona-cluster-next for tealeg mp268238
    LINT OK: passed

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

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

charm_amulet_test #5936 percona-cluster-next for tealeg mp268238
    AMULET FAIL: amulet-test failed

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

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

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

charm_amulet_test #5938 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12139740/
Build: http://10.245.162.77:8080/job/charm_amulet_test/5938/

Revision history for this message
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.

Revision history for this message
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.

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

charm_lint_check #9091 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12237361/
Build: http://10.245.162.77:8080/job/charm_lint_check/9091/

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

charm_unit_test #8400 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #9092 percona-cluster-next for tealeg mp268238
    LINT OK: passed

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

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

charm_unit_test #8401 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_amulet_test #6147 percona-cluster-next for tealeg mp268238
    AMULET FAIL: amulet-test failed

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

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

Revision history for this message
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://bazaar.launchpad.net/~1chb1n/charms/trusty/openstack-dashboard/next-amulet-fixup-1507/view/head:/tests/00-setup

Revision history for this message
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://bazaar.launchpad.net/~1chb1n/charms/trusty/openstack-dashboard/next-
> amulet-fixup-1507/view/head:/tests/00-setup

Hi Ryan,

It should already have exactly that content as I've merged in the most recent percona-cluster/next which has that. I'll poke around a little and see if I can find out the issue.

Revision history for this message
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-distro-info. Let's see if that gets the Amulet tests to pass here too.

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

charm_unit_test #8463 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #9157 percona-cluster-next for tealeg mp268238
    LINT OK: passed

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

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

charm_amulet_test #6169 percona-cluster-next for tealeg mp268238
    AMULET OK: passed

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

Revision history for this message
Adam Collard (adam-collard) wrote :

N/F for the missing asserts about status, otherwise looks good.

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

charm_unit_test #8529 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #9228 percona-cluster-next for tealeg mp268238
    LINT OK: passed

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

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

charm_amulet_test #6190 percona-cluster-next for tealeg mp268238
    AMULET FAIL: amulet-test failed

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

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

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

charm_lint_check #9239 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12254011/
Build: http://10.245.162.77:8080/job/charm_lint_check/9239/

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

charm_unit_test #8540 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #9242 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12254270/
Build: http://10.245.162.77:8080/job/charm_lint_check/9242/

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

charm_unit_test #8543 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_amulet_test #6201 percona-cluster-next for tealeg mp268238
    AMULET FAIL: amulet-test failed

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

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

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

charm_lint_check #9321 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12262221/
Build: http://10.245.162.77:8080/job/charm_lint_check/9321/

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

charm_unit_test #8620 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #9324 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12262815/
Build: http://10.245.162.77:8080/job/charm_lint_check/9324/

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

charm_unit_test #8622 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_amulet_test #6234 percona-cluster-next for tealeg mp268238
    AMULET OK: passed

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

Revision history for this message
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.

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

charm_unit_test #8932 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #9714 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12328261/
Build: http://10.245.162.77:8080/job/charm_lint_check/9714/

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

charm_amulet_test #6339 percona-cluster-next for tealeg mp268238
    AMULET FAIL: amulet-test failed

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

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

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

charm_unit_test #9001 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #9773 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12337429/
Build: http://10.245.162.77:8080/job/charm_lint_check/9773/

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

charm_amulet_test #6355 percona-cluster-next for tealeg mp268238
    AMULET FAIL: amulet-test failed

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

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

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

charm_unit_test #9213 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #10047 percona-cluster-next for tealeg mp268238
    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://paste.ubuntu.com/12417154/
Build: http://10.245.162.77:8080/job/charm_lint_check/10047/

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

charm_unit_test #9214 percona-cluster-next for tealeg mp268238
    UNIT OK: passed

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

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

charm_lint_check #10048 percona-cluster-next for tealeg mp268238
    LINT OK: passed

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

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

charm_amulet_test #6433 percona-cluster-next for tealeg mp268238
    AMULET OK: passed

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

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

charm_amulet_test #6434 percona-cluster-next for tealeg mp268238
    AMULET OK: passed

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

Revision history for this message
Adam Collard (adam-collard) wrote :

Looks good, thanks! +1

review: Approve
Revision history for this message
Chris Glass (tribaal) wrote :

Looks good! Thanks for your contribution.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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[:]

Subscribers

People subscribed via source and target branches