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

Proposed by Ryan Beisner
Status: Rejected
Rejected by: James Page
Proposed branch: lp:~1chb1n/charms/trusty/rabbitmq-server/amulet-refactor-1508
Merge into: lp:~openstack-charmers-archive/charms/trusty/rabbitmq-server/next
Diff against target: 6016 lines (+2687/-3010) (has conflicts)
43 files modified
Makefile (+5/-4)
charm-helpers-tests.yaml (+2/-3)
metadata.yaml (+4/-1)
tests/00-setup (+16/-0)
tests/014-basic-precise-icehouse (+11/-0)
tests/015-basic-trusty-icehouse (+9/-0)
tests/016-basic-trusty-juno (+11/-0)
tests/017-basic-trusty-kilo (+11/-0)
tests/019-basic-vivid-kilo (+9/-0)
tests/020-basic-trusty-liberty (+11/-0)
tests/021-basic-wily-liberty (+9/-0)
tests/basic_deployment.py (+492/-0)
tests/charmhelpers/__init__.py (+0/-38)
tests/charmhelpers/cli/__init__.py (+0/-191)
tests/charmhelpers/cli/benchmark.py (+0/-36)
tests/charmhelpers/cli/commands.py (+0/-32)
tests/charmhelpers/cli/hookenv.py (+0/-23)
tests/charmhelpers/cli/host.py (+0/-31)
tests/charmhelpers/cli/unitdata.py (+0/-39)
tests/charmhelpers/contrib/__init__.py (+0/-15)
tests/charmhelpers/contrib/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+93/-0)
tests/charmhelpers/contrib/amulet/utils.py (+778/-0)
tests/charmhelpers/contrib/openstack/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+198/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+963/-0)
tests/charmhelpers/contrib/ssl/__init__.py (+0/-94)
tests/charmhelpers/contrib/ssl/service.py (+0/-279)
tests/charmhelpers/core/__init__.py (+0/-15)
tests/charmhelpers/core/decorators.py (+0/-57)
tests/charmhelpers/core/files.py (+0/-45)
tests/charmhelpers/core/fstab.py (+0/-134)
tests/charmhelpers/core/host.py (+0/-570)
tests/charmhelpers/core/hugepage.py (+0/-62)
tests/charmhelpers/core/services/__init__.py (+0/-18)
tests/charmhelpers/core/services/base.py (+0/-353)
tests/charmhelpers/core/services/helpers.py (+0/-283)
tests/charmhelpers/core/strutils.py (+0/-42)
tests/charmhelpers/core/sysctl.py (+0/-56)
tests/charmhelpers/core/templating.py (+0/-68)
tests/charmhelpers/core/unitdata.py (+0/-521)
tests/tests.yaml (+20/-0)
Conflict: can't delete tests/charmhelpers.moved because it is not empty.  Not deleting.
Conflict adding file tests/charmhelpers.  Moved existing file to tests/charmhelpers.moved.
Conflict because tests/charmhelpers.moved is not versioned, but has versioned children.  Versioned directory.
Conflict: can't delete tests/charmhelpers.moved/core because it is not empty.  Not deleting.
Conflict because tests/charmhelpers.moved/core is not versioned, but has versioned children.  Versioned directory.
Contents conflict in tests/charmhelpers.moved/core/hookenv.py
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/rabbitmq-server/amulet-refactor-1508
Reviewer Review Type Date Requested Status
David Ames (community) Approve
Billy Olsen Pending
OpenStack Charmers Pending
Review via email: mp+269749@code.launchpad.net

This proposal supersedes a proposal from 2015-08-21.

Description of the change

** Do Not Merge, replaced by MP:
https://code.launchpad.net/~1chb1n/charms/trusty/rabbitmq-server/amulet-refactor-1509b/+merge/270102

For a clean break from growing conflict due to multiple other landings.
**

Original Description:
Refactor amulet tests, deprecate old tests, pull in thedac's cluster race fix and collect_rabbitmq_stats script fix, sync charm-helpers.

Cluster race bug (fix proposed by thedac, merged into this):
https://bugs.launchpad.net/charms/+source/rabbitmq-server/+bug/1486177

PID file bug for Vivid and later (fix landed in rmq/next, rebased into this):
https://bugs.launchpad.net/charms/+source/rabbitmq-server/+bug/1485722

This charm merge proposal is dependent on charm-helpers mp @:
https://code.launchpad.net/~1chb1n/charm-helpers/amulet-rmq-helpers

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

charm_lint_check #8491 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #7882 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #5952 rabbitmq-server-next for 1chb1n mp268811
    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/12146373/
Build: http://10.245.162.77:8080/job/charm_amulet_test/5952/

Revision history for this message
Ryan Beisner (1chb1n) wrote : Posted in a previous version of this proposal

In amulet test 5952, all combos except Vivid-Kilo pass all tests. All combos, including V-K cluster ok. The V-K fail is a packaging issue (bug 1485722).

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #8541 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #5953 rabbitmq-server-next for 1chb1n mp268811
    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/12148380/
Build: http://10.245.162.77:8080/job/charm_amulet_test/5953/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #7930 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #5958 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-08-22 06:21:18,640 run_cmd_unit DEBUG: rabbitmq-server/2 `bash -c "$(egrep -oh /usr/local.* /etc/nagios/nrpe.d/check_rabbitmq.cfg)"` command returned 0 (OK)
ERROR:root:Make target returned non-zero.

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8039 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #8706 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6029 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-08-25 20:07:58,933 publish_amqp_message_by_unit DEBUG: Publishing message to test queue:
ERROR:root:Make target returned non-zero.

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8089 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #8758 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #8763 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8094 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6041 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #8814 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8142 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6043 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6044 rabbitmq-server-next for 1chb1n mp268811
    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/12203258/
Build: http://10.245.162.77:8080/job/charm_amulet_test/6044/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6047 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #8817 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8145 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6049 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-08-27 04:27:53,775 _test_rmq_amqp_messages_all_units DEBUG: Publish message to: 172.17.107.154 (rabbitmq-server/1 juju-osci-sv07-machine-3)
ERROR:root:Make target returned non-zero.

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6052 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
MESSAGE 7@172.17.117.126 [62A5E9AD-3A96-4865-B303-DC431ECF9D43-1440653092.55]
ERROR:root:Make target returned non-zero.

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #8828 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8156 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6062 rabbitmq-server-next for 1chb1n mp268811
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-08-27 16:27:34,911 connect_amqp_by_unit DEBUG: Connecting to amqp on 172.17.105.60:5999 (rabbitmq-server/0) as testuser1...
ERROR:root:Make target returned non-zero.

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8251 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #8932 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8409 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #9100 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_lint_check #9152 rabbitmq-server-next for 1chb1n mp268811
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_unit_test #8458 rabbitmq-server-next for 1chb1n mp268811
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6163 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote : Posted in a previous version of this proposal

charm_amulet_test #6164 rabbitmq-server-next for 1chb1n mp268811
    AMULET OK: passed

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

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

charm_unit_test #8464 rabbitmq-server-next for 1chb1n mp269749
    UNIT OK: passed

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

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

charm_lint_check #9158 rabbitmq-server-next for 1chb1n mp269749
    LINT OK: passed

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

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

charm_amulet_test #6170 rabbitmq-server-next for 1chb1n mp269749
    AMULET OK: passed

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

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

charm_lint_check #9160 rabbitmq-server-next for 1chb1n mp269749
    LINT OK: passed

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

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

charm_unit_test #8466 rabbitmq-server-next for 1chb1n mp269749
    UNIT OK: passed

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

122. By Ryan Beisner

resync tests/charmhelpers for lint fixes

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

charm_unit_test #8468 rabbitmq-server-next for 1chb1n mp269749
    UNIT OK: passed

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

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

charm_lint_check #9166 rabbitmq-server-next for 1chb1n mp269749
    LINT OK: passed

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

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

charm_lint_check #9167 rabbitmq-server-next for 1chb1n mp269749
    LINT FAIL: lint-test failed

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

Full lint test output: http://paste.ubuntu.com/12246735/
Build: http://10.245.162.77:8080/job/charm_lint_check/9167/

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

charm_lint_check #9168 rabbitmq-server-next for 1chb1n mp269749
    LINT FAIL: lint-test failed

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

Full lint test output: http://paste.ubuntu.com/12246906/
Build: http://10.245.162.77:8080/job/charm_lint_check/9168/

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

charm_unit_test #8472 rabbitmq-server-next for 1chb1n mp269749
    UNIT FAIL: unit-test failed

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

Full unit test output: http://paste.ubuntu.com/12246907/
Build: http://10.245.162.77:8080/job/charm_unit_test/8472/

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

charm_lint_check #9169 rabbitmq-server-next for 1chb1n mp269749
    LINT OK: passed

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

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

charm_amulet_test #6175 rabbitmq-server-next for 1chb1n mp269749
    AMULET OK: passed

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

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

charm_lint_check #9170 rabbitmq-server-next for 1chb1n mp269749
    LINT OK: passed

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

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

charm_unit_test #8473 rabbitmq-server-next for 1chb1n mp269749
    UNIT OK: passed

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

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

charm_unit_test #8474 rabbitmq-server-next for 1chb1n mp269749
    UNIT OK: passed

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

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

charm_amulet_test #6176 rabbitmq-server-next for 1chb1n mp269749
    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/12247170/
Build: http://10.245.162.77:8080/job/charm_amulet_test/6176/

123. By Ryan Beisner

resync tests/charmhelpers for review changes

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

Ignore amulet fail #6176, it was an undercloud issue:
01:12:36.848 juju-test.conductor.015-basic-trusty-icehouse DEBUG : Killed by timeout after 2700 seconds

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

charm_unit_test #8475 rabbitmq-server-next for 1chb1n mp269749
    UNIT OK: passed

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

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

charm_lint_check #9171 rabbitmq-server-next for 1chb1n mp269749
    LINT OK: passed

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

Revision history for this message
David Ames (thedac) wrote :

Great work adding such thorough test suite.

I have one statement and one question.

I would argue that the new tests encompass all that the old tests covered and more and therefore, keeping them in deprecated is unneeded.

My only question is if we are being too openstack-centric for a general purpose charm like rabbitmq. The only difference that icehouse, juno, kilo, liberty makes is really on the cinder deploy. In this sense we are testing cinder more than rabbitmq. Now I agree we need this test coverage for openstack but is the rabbitmq-server amulet tests the place to do it? This is truly a question as I don't know.

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

Thank you for the review.

# re: Old Tests
There is still one old test which is not represented in the refactored tests, because the deployment topology is completely different than what we're exercising here. It is the non-native rmq cluster with ceph and hacluster relations. I'd like to keep the deprecated tests as reference while I /or someone/ refactors that into a mojo spec. Undertaker beware, as all of the original tests' bugs remain there (such as cluster member checks not checking cluster members, races in ordering, etc.).

I wouldn't be against removing that dir, but also didn't want to lose track of it before we re-captured the intent of that old test. Hence the stash. ;-)

# re: OpenStack vs Generic
I think that the tests are largely rmq-centric in that cluster verification and amqp messaging represent the bulk of the test duration, and 3 of the 5 units deployed are rmq. Cinder is present only to have "something" to exercise the amqp relation of the rabbitmq-server charm, and nrpe for the external-master interface.

The rabbitmq-server charm will receive a lot of benefit from being dragged along the OpenStack charm testing path, in that it will now be exercised against Precise, Trusty, Vivid (and soon Wily), whereas it was only formerly exercised against Trusty in its amulet tests.

For example, while writing these new tests, we identified and resolved a blocker for the rabbitmq-server charm (bug 1485722), where deploys failed on Vivid when nrpe was in the mix. Exercising the supported releases in this way helps us to identify and address issues like this before the next release lands.

...

Thanks again, good questions!

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

charm_amulet_test #6180 rabbitmq-server-next for 1chb1n mp269749
    AMULET OK: passed

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

124. By Ryan Beisner

resync tests/charmhelpers

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

charm_unit_test #8524 rabbitmq-server-next for 1chb1n mp269749
    UNIT OK: passed

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

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

charm_lint_check #9224 rabbitmq-server-next for 1chb1n mp269749
    LINT OK: passed

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

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

charm_amulet_test #6185 rabbitmq-server-next for 1chb1n mp269749
    AMULET OK: passed

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

Revision history for this message
David Ames (thedac) wrote :

Approved on my end

review: Approve
125. By Ryan Beisner

revert hooks, pull from rmq/next

Unmerged revisions

125. By Ryan Beisner

revert hooks, pull from rmq/next

124. By Ryan Beisner

resync tests/charmhelpers

123. By Ryan Beisner

resync tests/charmhelpers for review changes

122. By Ryan Beisner

resync tests/charmhelpers for lint fixes

121. By Ryan Beisner

re-merge lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

120. By Ryan Beisner

update comments

119. By Ryan Beisner

re-merge lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

118. By Ryan Beisner

Re-sync tests/charmhelpers re: updated fix for service_restarted_since bug 1474030

117. By Ryan Beisner

 - close connections and channels when done;
 - update debug feedback;
 - fix math on mgmt plugin check.

116. By Ryan Beisner

re-merge lp:~thedac/charms/trusty/rabbitmq-server/native-cluster-race-fixes

Preview Diff

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

Subscribers

People subscribed via source and target branches