Merge lp:~mariosplivalo/charms/trusty/mongodb/replsets-fix-try into lp:charms/trusty/mongodb

Proposed by Mario Splivalo
Status: Merged
Merged at revision: 66
Proposed branch: lp:~mariosplivalo/charms/trusty/mongodb/replsets-fix-try
Merge into: lp:charms/trusty/mongodb
Diff against target: 1639 lines (+1138/-145)
18 files modified
.bzrignore (+5/-0)
README.md (+62/-23)
charm-helpers-sync.yaml (+1/-0)
config.yaml (+1/-1)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+26/-12)
hooks/charmhelpers/contrib/python/packages.py (+80/-0)
hooks/charmhelpers/core/decorators.py (+41/-0)
hooks/charmhelpers/core/host.py (+16/-5)
hooks/charmhelpers/core/templating.py (+1/-1)
hooks/charmhelpers/fetch/__init__.py (+8/-1)
hooks/hooks.py (+322/-92)
setup.cfg (+6/-0)
test_requirements.txt (+1/-1)
tests/00_setup.sh (+0/-9)
tests/03_deploy_replicaset.py (+114/-0)
unit_tests/__init__.py (+2/-0)
unit_tests/test_hooks.py (+341/-0)
unit_tests/test_utils.py (+111/-0)
To merge this branch: bzr merge lp:~mariosplivalo/charms/trusty/mongodb/replsets-fix-try
Reviewer Review Type Date Requested Status
Felipe Reyes (community) Approve
Jorge Niedbalski (community) Approve
James Page Pending
Edward Hope-Morley Pending
Ryan Beisner Pending
Review via email: mp+247238@code.launchpad.net

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

Description of the change

This merge fixes lp #1379604. Mongodb charm now provides correct information to Ceilometer charm regarding MongoDB replicaset.

It also makes replicaset deploying more robust, fixing lp #1403698.

Also adds some unittest (for the changed code) and amulet tests for deploying replicaset.

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_unit_test #300 trusty-mongodb for mariosplivalo mp245059
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  Storing debug log for failure in /tmp/tmpeQOGBT
  make: *** [.venv] Error 1

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

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

charm_lint_check #272 trusty-mongodb for mariosplivalo mp245059
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #399 trusty-mongodb for mariosplivalo mp245059
    AMULET OK: passed

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

Revision history for this message
Billy Olsen (billy-olsen) wrote : Posted in a previous version of this proposal

Hey Mario - added a few comments of my own. A few things should be improved throughout here.

Revision history for this message
Jorge Niedbalski (niedbalski) wrote : Posted in a previous version of this proposal

Hello Mario,

Thanks for contributing to this charm. Next are my observations regarding to this first iteration on the review:

1) Lint has failures.

tests/03_deploy_replicaset.py:34:1: E302 expected 2 blank lines, found 1
tests/03_deploy_replicaset.py:45:20: F821 undefined name 'sentry_dict'
tests/03_deploy_replicaset.py:46:18: F821 undefined name 'sentry_dict'
tests/03_deploy_replicaset.py:77:1: E265 block comment should start with '# '
tests/03_deploy_replicaset.py:78:1: E265 block comment should start with '# '
tests/03_deploy_replicaset.py:79:1: E265 block comment should start with '# '
make: *** [lint] Error 1

2) Please change the makefile to just cover the hooks package.

 test: .venv
        @echo Starting unit tests...
- .venv/bin/nosetests -s --nologcapture --with-coverage $(EXTRA) unit_tests/
+ .venv/bin/nosetests -s --nologcapture --with-coverage $(EXTRA) --cover-package hooks unit_tests/

3) Please review the inline comments.

review: Needs Fixing
Revision history for this message
Mario Splivalo (mariosplivalo) wrote : Posted in a previous version of this proposal

Added my comments inline - stuff I haven't commented make perfect sense to me. Thank you, guys, for the inputs!

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

charm_lint_check #680 trusty-mongodb for mariosplivalo mp245059
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  tests/03_deploy_replicaset.py:84:1: E265 block comment should start with '# '
  make: *** [lint] Error 1

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

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

charm_unit_test #709 trusty-mongodb for mariosplivalo mp245059
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  FAILED (errors=2)
  make: *** [test] Error 1

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

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

charm_amulet_test #866 trusty-mongodb for mariosplivalo mp245059
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
  ERROR subprocess encountered error code 1
  make: *** [functional_test] Error 1

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

Revision history for this message
Jorge Niedbalski (niedbalski) wrote : Posted in a previous version of this proposal

Hello Mario,

Aside from the following lint errors:

tests/03_deploy_replicaset.py:39:1: E302 expected 2 blank lines, found 1
tests/03_deploy_replicaset.py:50:20: F821 undefined name 'sentry_dict'
tests/03_deploy_replicaset.py:51:18: F821 undefined name 'sentry_dict'
tests/03_deploy_replicaset.py:82:1: E265 block comment should start with '# '
tests/03_deploy_replicaset.py:83:1: E265 block comment should start with '# '
tests/03_deploy_replicaset.py:84:1: E265 block comment should start with '# '
hooks/hooks.py:17:1: F401 'operator' imported but unused
hooks/hooks.py:33:1: F401 'fatal' imported but unused
hooks/hooks.py:49:1: F401 'local_unit' imported but unused
hooks/hooks.py:96:33: E113 unexpected indentation
make: *** [lint] Error 1

I made a few observations regarding to the code, I hope you can drive those in order to keep reviewing this proposal.

thanks.

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

charm_lint_check #701 trusty-mongodb for mariosplivalo mp245059
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  unit_tests/test_hooks.py:269:5: E303 too many blank lines (2)
  make: *** [lint] Error 1

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

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

charm_unit_test #730 trusty-mongodb for mariosplivalo mp245059
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #887 trusty-mongodb for mariosplivalo mp245059
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
  ERROR subprocess encountered error code 1
  make: *** [functional_test] Error 1

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

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

charm_lint_check #702 trusty-mongodb for mariosplivalo mp245059
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_unit_test #731 trusty-mongodb for mariosplivalo mp245059
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #888 trusty-mongodb for mariosplivalo mp245059
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
  ERROR subprocess encountered error code 1
  make: *** [functional_test] Error 1

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

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

charm_lint_check #704 trusty-mongodb for mariosplivalo mp245059
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_unit_test #733 trusty-mongodb for mariosplivalo mp245059
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #889 trusty-mongodb for mariosplivalo mp245059
    AMULET OK: passed

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

Revision history for this message
Jorge Niedbalski (niedbalski) wrote : Posted in a previous version of this proposal

A few more comments on the code. Please review.

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

charm_unit_test #813 trusty-mongodb for mariosplivalo mp246519
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_lint_check #784 trusty-mongodb for mariosplivalo mp246519
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #969 trusty-mongodb for mariosplivalo mp246519
    AMULET OK: passed

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

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

Precise deploy tests fail with this MP with:

# from deployer output
2015-01-15 12:36:24 [DEBUG] deployer.import: Waiting for units before adding relations
2015-01-15 12:36:24 [ERROR] deployer.env: The following units had errors:
   unit: mongodb/0: machine: 9 agent-state: error details: hook failed: "install"
2015-01-15 12:36:24 [INFO] deployer.cli: Deployment stopped. run time: 431.20

# from mongodb juju unit log
2015-01-15 12:35:41 INFO install Setting up python-pymongo (2.1-1ubuntu0.1) ...
2015-01-15 12:35:41 INFO install Traceback (most recent call last):
2015-01-15 12:35:41 INFO install File "/var/lib/juju/agents/unit-mongodb-0/charm/hooks/install", line 38, in <module>
2015-01-15 12:35:41 INFO install from pymongo import MongoClient
2015-01-15 12:35:41 INFO install ImportError: cannot import name MongoClient
2015-01-15 12:35:41 ERROR juju.worker.uniter uniter.go:486 hook failed: exit status 1

Trusty, Juno deploy tests are happy.

review: Needs Fixing
Revision history for this message
Jorge Niedbalski (niedbalski) wrote : Posted in a previous version of this proposal

Please make sure to include 'execd_preinstall' on the TO_PATCH list for /home/niedbalski/src/charms/trusty/mongodb/unit_tests/test_hooks.py

======================================================================
ERROR: test_install_hook (mongodb.unit_tests.test_hooks.MongoHooksTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/niedbalski/src/charms/trusty/mongodb/.venv/local/lib/python2.7/site-packages/mock.py", line 1201, in patched
    return func(*args, **keywargs)
  File "/home/niedbalski/src/charms/trusty/mongodb/unit_tests/test_hooks.py", line 154, in test_install_hook
    hooks.install_hook()
  File "hooks/hooks.py", line 915, in install_hook
    execd_preinstall()
  File "hooks/charmhelpers/payload/execd.py", line 50, in execd_preinstall
    execd_run('charm-pre-install', execd_dir=execd_dir)
  File "hooks/charmhelpers/payload/execd.py", line 38, in execd_run
    for submodule_path in execd_submodule_paths(command, execd_dir):
  File "hooks/charmhelpers/payload/execd.py", line 30, in execd_submodule_paths
    for module_path in execd_module_paths(execd_dir):
  File "hooks/charmhelpers/payload/execd.py", line 16, in execd_module_paths
    execd_dir = default_execd_dir()
  File "hooks/charmhelpers/payload/execd.py", line 10, in default_execd_dir
    return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
  File "/home/niedbalski/src/charms/trusty/mongodb/.venv/lib/python2.7/UserDict.py", line 23, in __getitem__
    raise KeyError(key)
KeyError: 'CHARM_DIR'

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

charm_unit_test #887 trusty-mongodb for mariosplivalo mp246519
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  FAILED (errors=2)
  make: *** [test] Error 1

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

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

charm_lint_check #858 trusty-mongodb for mariosplivalo mp246519
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #1082 trusty-mongodb for mariosplivalo mp246519
    AMULET OK: passed

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

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

charm_lint_check #860 trusty-mongodb for mariosplivalo mp246956
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_unit_test #889 trusty-mongodb for mariosplivalo mp246956
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #1084 trusty-mongodb for mariosplivalo mp246956
    AMULET OK: passed

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

Revision history for this message
James Page (james-page) wrote : Posted in a previous version of this proposal

Mario

Thanks for the merge proposal; however we do have some complexity in what the precise and trusty versions of this charm need to support.

Specifically, the Ubuntu Server Team provides MongoDB from 14.04 as a backport to 12.04 via the Icehouse cloud-archive; so any change in behaviour really needs to be driven by the version of mongodb installed, rather than the Ubuntu release being used.

I'd prefer we did not start using pip to install things by default; if we can't configure replica-sets correctly with the 12.04 version of mongodb, lets just say we can't do that rather than starting to pull in non-distribution software.

review: Needs Fixing
Revision history for this message
Mario Splivalo (mariosplivalo) wrote : Posted in a previous version of this proposal

The issue with precise is not mongodb version - it is older and things are different but setting up replicaset is pretty much the same on 2.0 (precise) as it is on 2.4 (trusty).

The issue is that on precise pymongo is very old (it doesn't have MongoClient), also, portalocker is not packaged in precise - hence using pip to install those two dependencies.

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

FYI: mongodb install hook failed on Precise due to pip install timeout.

Revision history for this message
Mario Splivalo (mariosplivalo) wrote : Posted in a previous version of this proposal

Yup, becuase in ctsstack/serverstack pip can't reach outside world. Will add pip_proxy in config.yaml so that one can set http proxy specific to deployment environment.

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

charm_unit_test #966 trusty-mongodb for mariosplivalo mp247136
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_lint_check #937 trusty-mongodb for mariosplivalo mp247136
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #1159 trusty-mongodb for mariosplivalo mp247136
    AMULET OK: passed

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

Revision history for this message
Review Queue (review-queue) wrote : Posted in a previous version of this proposal

This items has failed automated testing! Results available here http://reports.vapour.ws/charm-tests/charm-bundle-test-10968-results

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

charm_unit_test #978 trusty-mongodb for mariosplivalo mp247238
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  FAILED (errors=2)
  make: *** [test] Error 1

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

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

charm_lint_check #949 trusty-mongodb for mariosplivalo mp247238
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  hooks/hooks.py:1245:1: W293 blank line contains whitespace
  make: *** [lint] Error 1

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

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

charm_amulet_test #1171 trusty-mongodb for mariosplivalo mp247238
    AMULET OK: passed

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

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

charm_unit_test #981 trusty-mongodb for mariosplivalo mp247238
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_lint_check #952 trusty-mongodb for mariosplivalo mp247238
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  Storing debug log for failure in /var/lib/jenkins/.pip/pip.log
  make: *** [.venv] Error 1

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

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

charm_amulet_test #1174 trusty-mongodb for mariosplivalo mp247238
    AMULET OK: passed

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

Revision history for this message
Jorge Niedbalski (niedbalski) wrote :

Hello,

Thanks Mario for submitting this proposal. I have run several tests in order to test the replicaset consistency.

1) 3, 5 , 7 units with juju deploy.
2) 3 units and then incrementally adding new units until 7.
3) 3 units, add 1, remove primary.
4) 3 units, add 1, remove primary, remove primary
4) 3 units, remove primary

All of the before mentioned cases seems to leave the replica set in consistent state, fixing LP: #1370542

I also validated that unit, lint and amulet tests on both series trusty and precise seems to work as expected.

LGTM

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

FYI, all deploy tests are happy. [P-I, T-I, T-J, U-J] x [Stable & Next] charms.

Revision history for this message
Felipe Reyes (freyes) wrote :

Hi Mario,

Good work, checked that deploys OK with 3 and 5 units, also resizing the cluster, all worked.

Best,

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

charm_unit_test #982 trusty-mongodb for mariosplivalo mp247238
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  FAILED (errors=2)
  make: *** [test] Error 1

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

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

charm_lint_check #1032 trusty-mongodb for mariosplivalo mp247238
    LINT OK: passed

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

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

charm_unit_test #1025 trusty-mongodb for mariosplivalo mp247136
    UNIT OK: passed

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

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

charm_lint_check #1033 trusty-mongodb for mariosplivalo mp247136
    LINT OK: passed

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2014-08-21 14:26:30 +0000
3+++ .bzrignore 2015-01-22 15:32:18 +0000
4@@ -1,4 +1,9 @@
5 .git
6+.project
7+.pydevproject
8+.coverage
9+.settings
10+.venv/
11 bin/*
12 scripts/charm-helpers-sync.py
13 exec.d/*
14
15=== modified file 'README.md'
16--- README.md 2014-11-20 15:42:07 +0000
17+++ README.md 2015-01-22 15:32:18 +0000
18@@ -12,27 +12,20 @@
19
20 ## Review the configurable options
21
22-The MongoDB charm allows for certain values to be configurable via a config.yaml file. The options provided are extensive, you should [review the options](https://jujucharms.com/fullscreen/search/precise/mongodb-20/?text=mongodb#bws-configuration).
23+The MongoDB charm allows for certain values to be configurable via a config.yaml file. The options provided are extensive, you should [review the options](https://jujucharms.com/fullscreen/search/precise/mongodb-20/?text=mongodb#bws-configuration).
24
25-Specifically the following options are important:
26+Specifically the following options are important:
27
28 - replicaset
29 - ie: myreplicaset
30 - Each replicaset has a unique name to distinguish it’s members from other replicasets available in the network.
31- - The default value of myset should be fine for most single cluster scenarios.
32+ - The default value of "myset" should be fine for most single cluster scenarios.
33
34 - web_admin_ui
35 - MongoDB comes with a basic but very informative web user interface that provides health
36 and status information on the database node as well as the cluster.
37 - The default value of yes will start the Admin web UI on port 28017.
38
39-- replicaset_master
40- - If this node is going to be joining an existing replicaset, you can specify a member of that cluster
41- ( preferably the master node ) so we can join the existing replicaset.
42- - The value should be in the form of host[:port]
43- - ie: hostname ( will connect to hostname on the default port of 27017 )
44- - ie: hostname:port ( will connect to hostname on port number <port> )
45-
46 Most of the options in config.yaml have been modeled after the default configuration file for mongodb (normally in /etc/mongodb.conf) and should be familiar to most mongodb admins. Each option in this charm have a brief description of what it does.
47
48 # Usage
49@@ -46,10 +39,13 @@
50
51 ## Replica Sets
52
53-Deploy the first MongoDB instance
54-
55- juju deploy mongodb
56- juju expose mongodb
57+### Deploying
58+
59+Deploy the first two MongoDB instances that will form replicaset:
60+
61+ juju deployu mongodb -n 2
62+
63+Deploying three or more units at start can sometimes lead to unexpected race-conditions so it's best to start with two nodes.
64
65 Your deployment should look similar to this ( `juju status` ):
66
67@@ -91,18 +87,61 @@
68
69 juju set mongodb replicaset=<new_replicaset_name>
70
71-### Add one more nodes to your replicaset
72+### Add one or more nodes to your replicaset
73
74 juju add-unit mongodb
75-
76-
77-### Add multiple nodes to your replicaset
78-
79- juju add-unit mongodb -n5
80-
81+ juju add-unit mongodb -n2
82
83 We now have a working MongoDB replica-set.
84
85+### Caveats
86+
87+Keep in mind that you need to have odd number of nodes for a properly formed replicaset.
88+
89+Replicaset can't function with only one available node - shall this happens the remaining node is switched to 'read-only' until at least one of the broken nodes is restored.
90+
91+More info can be found in MongoDB documentation at [their website](http://docs.mongodb.org/manual/replication/)
92+
93+### Removing a failed node
94+
95+Working units can be removed from replica set using 'juju remove-unit' command. If the removing unit is primary it will automatically be stepped down (so thath re-election of new primary is performend) before being removed.
96+However, if a unit fails (freezes, gets destroyed and is unbootable), operator needs to manually remove it. The operator would connect to primary unit, and issue rs.remove() for failed unit. Also, operator needs to issue 'juju remove-unit --force' to remove failed unit from juju.
97+
98+### Recovering from degraded replicaset
99+
100+If two members go down replicaset is in read-only state. That is because the
101+remaining node is in SECONDARY state (it can't get promoted/voted to PRIMARY
102+because there is no majority in replicaset). If failed nodes can't be brought
103+back to life we need to manually force remaining node to become a primary. Here
104+is how:
105+
106+ 1. connect to the node that's alive
107+ 2. start 'mongo', a cli utility
108+ 3. upon connecting you'll see that node is SECONDARY
109+ 4. display current configuration with:
110+ rs.config()
111+ - this will show the alive node as well as the nodes that are unreachabble
112+
113+ 5. store the configuration into some temporary json document:
114+ cfg=rs.config()
115+
116+ 6. change the cfg document so that it's members array contain only the unit
117+ that is alive:
118+ cfg.members=[cfg.members[0]]
119+
120+ 7. force reconfiguration of the replicaset:
121+ rs.reconfigure(cfg, {force: true})
122+
123+ 8. wait a few, and press ENTER. You should see that your node becomes PRIMARY.
124+
125+After this clean up the unavailable machines from juju:
126+ juju remove-machine --force XX ## XX is the machine number
127+
128+And add more units to form a proper replicaset. (To avoid race conditions it is
129+best to add units one by one).
130+
131+ juju add-unit mongodb
132+
133 ## Sharding (Scale Out Usage)
134
135 According the the MongoDB documentation found on [their website](http://docs.mongodb.org/manual/tutorial/deploy-shard-cluster/), one way of deploying a Shard Cluster is as follows:
136@@ -127,7 +166,7 @@
137 replicaset: configsvr
138
139 We'll save this one as `~/mongodb-shard.yaml`.
140-
141+
142 ### Bootstrap the environment
143 juju bootstrap
144
145@@ -210,7 +249,7 @@
146
147 ## MongoDB Contact Information
148
149-- [MongoDB website](http://mongodb.org)
150+- [MongoDB website](http://mongodb.org)
151 - [MongoDB documentation](http://www.mongodb.org/display/DOCS/Home)
152 - [MongoDB bug tracker](https://jira.mongodb.org/secure/Dashboard.jspa)
153 - [MongoDB user mailing list](https://groups.google.com/forum/#!forum/mongodb-user)
154
155=== modified file 'charm-helpers-sync.yaml'
156--- charm-helpers-sync.yaml 2015-01-13 17:51:52 +0000
157+++ charm-helpers-sync.yaml 2015-01-22 15:32:18 +0000
158@@ -4,4 +4,5 @@
159 - core
160 - fetch
161 - contrib.hahelpers.cluster
162+ - contrib.python.packages
163 - payload.execd
164
165=== modified file 'config.yaml'
166--- config.yaml 2014-12-09 22:54:36 +0000
167+++ config.yaml 2015-01-22 15:32:18 +0000
168@@ -169,7 +169,7 @@
169 description: "Number of backups to keep. Keeps one week's worth by default."
170 #------------------------------------------------------------------------
171 # Legacy volume management (DEPRECATED)
172- # volume-map, volume-dev_regexp are only used
173+ # volume-map, volume-dev_regexp are only used
174 # if volume-ephemeral-storage == False
175 #------------------------------------------------------------------------
176 volume-ephemeral-storage:
177
178=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
179--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-12-09 23:58:57 +0000
180+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-01-22 15:32:18 +0000
181@@ -13,6 +13,7 @@
182
183 import subprocess
184 import os
185+
186 from socket import gethostname as get_unit_hostname
187
188 import six
189@@ -28,12 +29,19 @@
190 WARNING,
191 unit_get,
192 )
193+from charmhelpers.core.decorators import (
194+ retry_on_exception,
195+)
196
197
198 class HAIncompleteConfig(Exception):
199 pass
200
201
202+class CRMResourceNotFound(Exception):
203+ pass
204+
205+
206 def is_elected_leader(resource):
207 """
208 Returns True if the charm executing this is the elected cluster leader.
209@@ -68,24 +76,30 @@
210 return False
211
212
213-def is_crm_leader(resource):
214+@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
215+def is_crm_leader(resource, retry=False):
216 """
217 Returns True if the charm calling this is the elected corosync leader,
218 as returned by calling the external "crm" command.
219+
220+ We allow this operation to be retried to avoid the possibility of getting a
221+ false negative. See LP #1396246 for more info.
222 """
223- cmd = [
224- "crm", "resource",
225- "show", resource
226- ]
227+ cmd = ['crm', 'resource', 'show', resource]
228 try:
229- status = subprocess.check_output(cmd).decode('UTF-8')
230+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
231+ if not isinstance(status, six.text_type):
232+ status = six.text_type(status, "utf-8")
233 except subprocess.CalledProcessError:
234- return False
235- else:
236- if get_unit_hostname() in status:
237- return True
238- else:
239- return False
240+ status = None
241+
242+ if status and get_unit_hostname() in status:
243+ return True
244+
245+ if status and "resource %s is NOT running" % (resource) in status:
246+ raise CRMResourceNotFound("CRM resource %s not found" % (resource))
247+
248+ return False
249
250
251 def is_leader(resource):
252
253=== added directory 'hooks/charmhelpers/contrib/python'
254=== added file 'hooks/charmhelpers/contrib/python/__init__.py'
255=== added file 'hooks/charmhelpers/contrib/python/packages.py'
256--- hooks/charmhelpers/contrib/python/packages.py 1970-01-01 00:00:00 +0000
257+++ hooks/charmhelpers/contrib/python/packages.py 2015-01-22 15:32:18 +0000
258@@ -0,0 +1,80 @@
259+#!/usr/bin/env python
260+# coding: utf-8
261+
262+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
263+
264+from charmhelpers.fetch import apt_install, apt_update
265+from charmhelpers.core.hookenv import log
266+
267+try:
268+ from pip import main as pip_execute
269+except ImportError:
270+ apt_update()
271+ apt_install('python-pip')
272+ from pip import main as pip_execute
273+
274+
275+def parse_options(given, available):
276+ """Given a set of options, check if available"""
277+ for key, value in sorted(given.items()):
278+ if key in available:
279+ yield "--{0}={1}".format(key, value)
280+
281+
282+def pip_install_requirements(requirements, **options):
283+ """Install a requirements file """
284+ command = ["install"]
285+
286+ available_options = ('proxy', 'src', 'log', )
287+ for option in parse_options(options, available_options):
288+ command.append(option)
289+
290+ command.append("-r {0}".format(requirements))
291+ log("Installing from file: {} with options: {}".format(requirements,
292+ command))
293+ pip_execute(command)
294+
295+
296+def pip_install(package, fatal=False, upgrade=False, **options):
297+ """Install a python package"""
298+ command = ["install"]
299+
300+ available_options = ('proxy', 'src', 'log', "index-url", )
301+ for option in parse_options(options, available_options):
302+ command.append(option)
303+
304+ if upgrade:
305+ command.append('--upgrade')
306+
307+ if isinstance(package, list):
308+ command.extend(package)
309+ else:
310+ command.append(package)
311+
312+ log("Installing {} package with options: {}".format(package,
313+ command))
314+ pip_execute(command)
315+
316+
317+def pip_uninstall(package, **options):
318+ """Uninstall a python package"""
319+ command = ["uninstall", "-q", "-y"]
320+
321+ available_options = ('proxy', 'log', )
322+ for option in parse_options(options, available_options):
323+ command.append(option)
324+
325+ if isinstance(package, list):
326+ command.extend(package)
327+ else:
328+ command.append(package)
329+
330+ log("Uninstalling {} package with options: {}".format(package,
331+ command))
332+ pip_execute(command)
333+
334+
335+def pip_list():
336+ """Returns the list of current python installed packages
337+ """
338+ return pip_execute(["list"])
339
340=== added file 'hooks/charmhelpers/core/decorators.py'
341--- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
342+++ hooks/charmhelpers/core/decorators.py 2015-01-22 15:32:18 +0000
343@@ -0,0 +1,41 @@
344+#
345+# Copyright 2014 Canonical Ltd.
346+#
347+# Authors:
348+# Edward Hope-Morley <opentastic@gmail.com>
349+#
350+
351+import time
352+
353+from charmhelpers.core.hookenv import (
354+ log,
355+ INFO,
356+)
357+
358+
359+def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
360+ """If the decorated function raises exception exc_type, allow num_retries
361+ retry attempts before raise the exception.
362+ """
363+ def _retry_on_exception_inner_1(f):
364+ def _retry_on_exception_inner_2(*args, **kwargs):
365+ retries = num_retries
366+ multiplier = 1
367+ while True:
368+ try:
369+ return f(*args, **kwargs)
370+ except exc_type:
371+ if not retries:
372+ raise
373+
374+ delay = base_delay * multiplier
375+ multiplier += 1
376+ log("Retrying '%s' %d more times (delay=%s)" %
377+ (f.__name__, retries, delay), level=INFO)
378+ retries -= 1
379+ if delay:
380+ time.sleep(delay)
381+
382+ return _retry_on_exception_inner_2
383+
384+ return _retry_on_exception_inner_1
385
386=== modified file 'hooks/charmhelpers/core/host.py'
387--- hooks/charmhelpers/core/host.py 2014-12-09 23:58:57 +0000
388+++ hooks/charmhelpers/core/host.py 2015-01-22 15:32:18 +0000
389@@ -162,13 +162,16 @@
390 uid = pwd.getpwnam(owner).pw_uid
391 gid = grp.getgrnam(group).gr_gid
392 realpath = os.path.abspath(path)
393- if os.path.exists(realpath):
394- if force and not os.path.isdir(realpath):
395+ path_exists = os.path.exists(realpath)
396+ if path_exists and force:
397+ if not os.path.isdir(realpath):
398 log("Removing non-directory file {} prior to mkdir()".format(path))
399 os.unlink(realpath)
400- else:
401+ os.makedirs(realpath, perms)
402+ elif not path_exists:
403 os.makedirs(realpath, perms)
404 os.chown(realpath, uid, gid)
405+ os.chmod(realpath, perms)
406
407
408 def write_file(path, content, owner='root', group='root', perms=0o444):
409@@ -404,13 +407,21 @@
410 os.chdir(cur)
411
412
413-def chownr(path, owner, group):
414+def chownr(path, owner, group, follow_links=True):
415 uid = pwd.getpwnam(owner).pw_uid
416 gid = grp.getgrnam(group).gr_gid
417+ if follow_links:
418+ chown = os.chown
419+ else:
420+ chown = os.lchown
421
422 for root, dirs, files in os.walk(path):
423 for name in dirs + files:
424 full = os.path.join(root, name)
425 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
426 if not broken_symlink:
427- os.chown(full, uid, gid)
428+ chown(full, uid, gid)
429+
430+
431+def lchownr(path, owner, group):
432+ chownr(path, owner, group, follow_links=False)
433
434=== modified file 'hooks/charmhelpers/core/templating.py'
435--- hooks/charmhelpers/core/templating.py 2014-12-09 23:58:57 +0000
436+++ hooks/charmhelpers/core/templating.py 2015-01-22 15:32:18 +0000
437@@ -48,5 +48,5 @@
438 level=hookenv.ERROR)
439 raise e
440 content = template.render(context)
441- host.mkdir(os.path.dirname(target))
442+ host.mkdir(os.path.dirname(target), owner, group)
443 host.write_file(target, content, owner, group, perms)
444
445=== modified file 'hooks/charmhelpers/fetch/__init__.py'
446--- hooks/charmhelpers/fetch/__init__.py 2014-12-09 23:58:57 +0000
447+++ hooks/charmhelpers/fetch/__init__.py 2015-01-22 15:32:18 +0000
448@@ -64,9 +64,16 @@
449 'trusty-juno/updates': 'trusty-updates/juno',
450 'trusty-updates/juno': 'trusty-updates/juno',
451 'juno/proposed': 'trusty-proposed/juno',
452- 'juno/proposed': 'trusty-proposed/juno',
453 'trusty-juno/proposed': 'trusty-proposed/juno',
454 'trusty-proposed/juno': 'trusty-proposed/juno',
455+ # Kilo
456+ 'kilo': 'trusty-updates/kilo',
457+ 'trusty-kilo': 'trusty-updates/kilo',
458+ 'trusty-kilo/updates': 'trusty-updates/kilo',
459+ 'trusty-updates/kilo': 'trusty-updates/kilo',
460+ 'kilo/proposed': 'trusty-proposed/kilo',
461+ 'trusty-kilo/proposed': 'trusty-proposed/kilo',
462+ 'trusty-proposed/kilo': 'trusty-proposed/kilo',
463 }
464
465 # The order of this list is very important. Handlers should be listed in from
466
467=== modified file 'hooks/hooks.py'
468--- hooks/hooks.py 2015-01-13 17:51:52 +0000
469+++ hooks/hooks.py 2015-01-22 15:32:18 +0000
470@@ -28,28 +28,51 @@
471 apt_install
472 )
473
474+import json
475+
476+from charmhelpers.core.host import (
477+ service,
478+ lsb_release,
479+)
480+
481 from charmhelpers.core.hookenv import (
482+ close_port,
483 config,
484+ open_port,
485 unit_get,
486 relation_get,
487 relation_set,
488 relations_of_type,
489 relation_id,
490 relation_ids,
491- open_port,
492- close_port,
493 Hooks,
494+ DEBUG,
495+ WARNING,
496 )
497
498 from charmhelpers.core.hookenv import log as juju_log
499
500 from charmhelpers.payload.execd import execd_preinstall
501
502-from charmhelpers.core.host import (
503- service,
504- lsb_release,
505+from charmhelpers.contrib.hahelpers.cluster import (
506+ oldest_peer,
507+ peer_units
508 )
509
510+try:
511+ from pymongo import Connection
512+ from pymongo.errors import OperationFailure
513+except ImportError:
514+ apt_install("python-pymongo", fatal=True)
515+ from pymongo import Connection
516+ from pymongo.errors import OperationFailure
517+
518+try:
519+ from lockfile import FileLock
520+except ImportError:
521+ apt_install("python-lockfile", fatal=True)
522+ from lockfile import FileLock
523+
524 hooks = Hooks()
525
526 ###############################################################################
527@@ -61,6 +84,33 @@
528 default_wait_for = 10
529 default_max_tries = 5
530
531+INSTALL_PACKAGES = ['mongodb-server', 'python-yaml']
532+
533+INIT_LOCKFILE = '/tmp/mongodb-charm.lock'
534+
535+# number of seconds init_replset will pause while looping to check if
536+# replicaset is initialized
537+INIT_CHECK_DELAY = 1.5
538+
539+# number of times mongo_client_smart will try to execute given statement
540+MONGO_CLIENT_RETRIES = 10
541+
542+# These are MongoDB ReplicaSet states, for convenience:
543+MONGO_STARTUP = 0
544+MONGO_PRIMARY = 1
545+MONGO_SECONDARY = 2
546+MONGO_RECOVERING = 3
547+MONGO_FATAL = 4
548+MONGO_STARTUP2 = 5
549+MONGO_UNKNOWN = 6
550+MONGO_ARBITER = 7
551+MONGO_DOWN = 8
552+MONGO_ROLLBACK = 9
553+MONGO_REMOVED = 10
554+
555+# Ill and quick way to make relatinon-departed and relation-broken communicate
556+was_i_primary = False
557+
558 ###############################################################################
559 # Supporting functions
560 ###############################################################################
561@@ -119,7 +169,7 @@
562
563
564 def update_file(filename=None, new_data=None, old_data=None):
565- juju_log("update_file: %s" % filename)
566+ juju_log("update_file: %s" % filename, level=DEBUG)
567 if filename is None or new_data is None:
568 retVal = False
569 try:
570@@ -131,7 +181,6 @@
571 juju_log(str(e))
572 retVal = False
573 finally:
574- juju_log("update_file %s returns: %s" % (filename, retVal))
575 return(retVal)
576
577
578@@ -161,6 +210,14 @@
579 return((None, None))
580
581
582+class MasterNotFoundException(Exception):
583+ pass
584+
585+
586+class TimeoutException(Exception):
587+ pass
588+
589+
590 ###############################################################################
591 # Charm support functions
592 ###############################################################################
593@@ -336,49 +393,160 @@
594 else:
595 cmd_line = 'mongo'
596 cmd_line += ' --host %s' % host
597- cmd_line += ' --eval \'%s\'' % command
598- juju_log("Executing: %s" % cmd_line)
599+ cmd_line += ' --eval \'printjson(%s)\'' % command
600+ juju_log("Executing: %s" % cmd_line, level=DEBUG)
601 return(subprocess.call(cmd_line, shell=True) == 0)
602
603
604-def init_replset(master_node=None):
605- if master_node is None:
606- juju_log("init_replset: master_node must be defined.")
607- retVal = False
608- else:
609- retVal = mongo_client(master_node, 'rs.initiate()')
610- juju_log("init_replset returns: %s" % retVal)
611+def mongo_client_smart(host='localhost', command=None):
612+ '''
613+ Rework of mongo_client function, it retries the command
614+ MONGO_CLIENT_RETRIES times
615+
616+ :param host: The host to connect to. Defaults to localhost
617+ :param command: The command to be executed. Can't be None
618+ :returns True if command succeeded, False if it failed
619+ '''
620+
621+ if command is None:
622+ return False
623+
624+ cmd_line = ['mongo', '--quiet', '--host', host,
625+ '--eval', 'printjson(%s)' % command]
626+ juju_log("mongo_client_smart executing: %s" % str(cmd_line), level=DEBUG)
627+
628+ for i in xrange(MONGO_CLIENT_RETRIES):
629+ try:
630+ cmd_output = subprocess.check_output(cmd_line)
631+ juju_log('mongo_client_smart executed, output: %s' %
632+ cmd_output)
633+ if json.loads(cmd_output)['ok'] == 1:
634+ return True
635+ except subprocess.CalledProcessError as err:
636+ juju_log('mongo_client_smart failed: %s' %
637+ err.output,
638+ level=DEBUG)
639+ pass
640+ finally:
641+ time.sleep(1.5)
642+
643+ # At this point, the command failed
644+ juju_log('mongo_client_smart failed executing: %s', level=WARNING)
645+ return False
646+
647+
648+def init_replset():
649+ config_data = config()
650+ # Use my IP at rs.initiate(), voids issues with invalid (and/or
651+ # not resolvable by peers) hostnames
652+
653+ rset = config_data['replicaset']
654+ addr = unit_get('private-address')
655+ port = config_data['port']
656+
657+ init = '{_id: "%s", members: [{_id: 0, host: "%s:%s"}]} ' % (rset,
658+ addr,
659+ port)
660+
661+ retVal = mongo_client('localhost', 'rs.initiate(%s)' % init)
662+ time.sleep(1) # give mongod some time to become primary
663+ c = Connection('localhost')
664+ while True:
665+ try:
666+ r = run_admin_command(c, 'replSetGetStatus')
667+ mongo_state = r['myState']
668+ juju_log('init_replset: myState: %s' % mongo_state)
669+ if mongo_state == MONGO_PRIMARY: # we're primary!
670+ break
671+ elif mongo_state in (MONGO_STARTUP,
672+ MONGO_STARTUP2,
673+ MONGO_SECONDARY
674+ ): # we are still initializing
675+ continue
676+ else:
677+ juju_log('init_replset: Unexpected replicaSet state: %s' %
678+ mongo_state)
679+ retVal = False
680+ break
681+ except OperationFailure as e:
682+ juju_log('init_replset: OperationFailure: %s' % e)
683+ if 'Received replSetInitiate' in str(e):
684+ continue
685+ else:
686+ raise
687+ finally:
688+ time.sleep(INIT_CHECK_DELAY)
689+
690+ juju_log("init_replset returns: %s" % retVal, level=DEBUG)
691 return(retVal)
692
693
694+def run_admin_command(client, cmdstr):
695+ """Runs an admin command against the client. Primary purpose is to
696+ simplify the unit testing.
697+ """
698+ return client.admin.command(cmdstr)
699+
700+
701 def join_replset(master_node=None, host=None):
702+ # TODO: This methoud shouldn't accept any arguments becase we don't care
703+ # about master_node - we always run replset admin commands on
704+ # localhost.
705+ # However, that might break other code calling this method.
706+ # This will wait charm rewrite.
707 juju_log("join_replset: master_node: %s, host: %s" %
708 (master_node, host))
709 if master_node is None or host is None:
710 retVal = False
711 else:
712- retVal = mongo_client(master_node, "rs.add(\"%s\")" % host)
713- juju_log("join_replset returns: %s" % retVal)
714+ retVal = mongo_client_smart('localhost', 'rs.add("%s")' % host)
715+ juju_log("join_replset returns: %s" % retVal, level=DEBUG)
716+ return(retVal)
717+
718+
719+def leave_replset(master_node=None, host=None):
720+ juju_log("leave_replset: master_node: %s, host: %s" %
721+ (master_node, host))
722+ if master_node is None or host is None:
723+ retVal = False
724+ else:
725+ retVal = mongo_client(master_node, 'rs.remove("%s")' % host)
726+ juju_log("leave_replset returns: %s" % retVal, level=DEBUG)
727 return(retVal)
728
729
730 def enable_replset(replicaset_name=None):
731+ retVal = False
732 if replicaset_name is None:
733- retVal = False
734+ juju_log('enable_replset: replicaset_name is None, exiting',
735+ level=DEBUG)
736 try:
737- mongodb_init_config = open(default_mongodb_init_config).read()
738- if re.search(' --replSet %s ' % replicaset_name,
739- mongodb_init_config, re.MULTILINE) is None:
740- mongodb_init_config = regex_sub([(' -- ',
741- ' -- --replSet %s ' %
742- replicaset_name)],
743- mongodb_init_config)
744- retVal = update_file(default_mongodb_init_config, mongodb_init_config)
745+ juju_log('enable_replset: trying to get lock on: %s' %
746+ default_mongodb_init_config)
747+ with FileLock(INIT_LOCKFILE):
748+ juju_log('enable_replset: lock acquired', level=DEBUG)
749+ with open(default_mongodb_init_config) as mongo_init_file:
750+ mongodb_init_config = mongo_init_file.read()
751+ if re.search(' --replSet %s ' % replicaset_name,
752+ mongodb_init_config, re.MULTILINE) is None:
753+ juju_log('enable_replset: --replset not preset,'
754+ ' enabling',
755+ level=DEBUG)
756+ mongodb_init_config = regex_sub([(' -- ',
757+ ' -- --replSet %s ' %
758+ replicaset_name)],
759+ mongodb_init_config)
760+ update_file(default_mongodb_init_config,
761+ mongodb_init_config)
762+ retVal = True
763+
764+ juju_log('enable_replset will return: %s' % str(retVal), level=DEBUG)
765+
766 except Exception, e:
767- juju_log(str(e))
768+ juju_log(str(e), level=WARNING)
769 retVal = False
770 finally:
771- return(retVal)
772+ return retVal
773
774
775 def update_daemon_options(daemon_options=None):
776@@ -752,8 +920,7 @@
777 arm64_trusty_quirk()
778
779 apt_update(fatal=True)
780- apt_install(packages=['mongodb-server', 'mongodb-clients', 'python-yaml'],
781- fatal=True)
782+ apt_install(packages=INSTALL_PACKAGES, fatal=True)
783
784
785 @hooks.hook('config-changed')
786@@ -928,28 +1095,34 @@
787 juju_log("my_hostname: %s" % my_hostname)
788 juju_log("my_port: %s" % my_port)
789 juju_log("my_replset: %s" % my_replset)
790-
791- return(relation_set(relation_id(), {
792- 'hostname': my_hostname,
793- 'port': my_port,
794- 'replset': my_replset,
795- 'type': 'database',
796- }))
797+ relation_data = {'hostname': my_hostname,
798+ 'port': my_port,
799+ 'type': 'database',
800+ }
801+
802+ if len(peer_units('replica-set')) > 1:
803+ relation_data['replset'] = my_replset
804+
805+ relation_set(relation_id(), relation_data)
806
807
808 @hooks.hook('replicaset-relation-joined')
809 def replica_set_relation_joined():
810- juju_log("replica_set_relation_joined")
811+ juju_log("replica_set_relation_joined-start")
812 my_hostname = unit_get('private-address')
813 my_port = config('port')
814 my_replset = config('replicaset')
815+
816 my_install_order = os.environ['JUJU_UNIT_NAME'].split('/')[1]
817 juju_log("my_hostname: %s" % my_hostname)
818 juju_log("my_port: %s" % my_port)
819 juju_log("my_replset: %s" % my_replset)
820 juju_log("my_install_order: %s" % my_install_order)
821- enable_replset(my_replset)
822- restart_mongod()
823+ # do not restart mongodb if config has not changed
824+ if enable_replset(my_replset):
825+ juju_log('Restarting mongodb after config change (enable replset)',
826+ level=DEBUG)
827+ restart_mongod()
828
829 relation_set(relation_id(), {
830 'hostname': my_hostname,
831@@ -958,61 +1131,118 @@
832 'install-order': my_install_order,
833 'type': 'replset',
834 })
835+ juju_log("replica_set_relation_joined-finish")
836+
837+
838+def am_i_primary():
839+ c = Connection('localhost')
840+ for i in xrange(10):
841+ try:
842+ r = run_admin_command(c, 'replSetGetStatus')
843+ juju_log('am_i_primary: replSetGetStatus returned: %s' % str(r),
844+ level=DEBUG)
845+ return r['myState'] == MONGO_PRIMARY
846+ except OperationFailure as e:
847+ juju_log('am_i_primary: OperationError: %s' % str(e), level=DEBUG)
848+ if 'replSetInitiate - should come online shortly' in str(e):
849+ # replSet initialization in progress
850+ continue
851+ elif 'EMPTYCONFIG' in str(e):
852+ # replication not
853+ return False
854+ else:
855+ raise
856+ finally:
857+ time.sleep(1.5)
858+
859+ # Raise an error if we exhausted the maximum amount of trials
860+ raise TimeoutException('Unable to determine if local unit is primary')
861
862
863 @hooks.hook('replicaset-relation-changed')
864 def replica_set_relation_changed():
865- juju_log("replica_set_relation_changed")
866- my_hostname = unit_get('private-address')
867- my_port = config('port')
868- my_install_order = os.environ['JUJU_UNIT_NAME'].split('/')[1]
869- my_replicaset_master = config('replicaset_master')
870-
871- # If we are joining an existing replicaset cluster, just join and leave.
872- if my_replicaset_master != "auto":
873- return(join_replset(my_replicaset_master, my_hostname))
874-
875- # Default to this node being the master
876- master_hostname = my_hostname
877- master_port = my_port
878- master_install_order = my_install_order
879-
880- # Check the nodes in the relation to find the master
881- for member in relations_of_type('replica-set'):
882- member = member['__unit__']
883- juju_log("replica_set_relation_changed: member: %s" % member)
884- hostname = relation_get('hostname', member)
885- port = relation_get('port', member)
886- inst_ordr = relation_get('install-order', member)
887- juju_log("replica_set_relation_changed: install_order: %s" % inst_ordr)
888- if inst_ordr is None:
889- juju_log("replica_set_relation_changed: install_order is None."
890- " relation is not ready")
891- break
892- if int(inst_ordr) < int(master_install_order):
893- master_hostname = hostname
894- master_port = port
895- master_install_order = inst_ordr
896-
897- # Initiate the replset
898- init_replset("%s:%s" % (master_hostname, master_port))
899-
900- # Add the rest of the nodes to the replset
901- for member in relations_of_type('replica-set'):
902- hostname = relation_get('hostname', member['__unit__'])
903- port = relation_get('port', member['__unit__'])
904- if master_hostname != hostname:
905- if hostname == my_hostname:
906- subprocess.call(['mongo', '--eval',
907- "rs.add(\"%s\")" % hostname])
908- else:
909- join_replset("%s:%s" % (master_hostname, master_port),
910- "%s:%s" % (hostname, port))
911-
912- # Add this node to the replset ( if needed )
913- if master_hostname != my_hostname:
914- join_replset("%s:%s" % (master_hostname, master_port),
915- "%s:%s" % (my_hostname, my_port))
916+ private_address = unit_get('private-address')
917+ remote_hostname = relation_get('hostname')
918+
919+ juju_log('replica_set_relation_changed-start')
920+ juju_log('local unit: %s, joining_unit: %s' % (private_address,
921+ remote_hostname),
922+ level=DEBUG)
923+
924+ if remote_hostname is None:
925+ juju_log('Joiner not ready yet... bailing out')
926+ return
927+
928+ # Initialize the replicaset - we do this only on the oldest unit in replset
929+ # TODO: figure a way how to avoid race conditions - when unit/1 actually
930+ # comes up before unit/0 does - happens rarely, but can happen
931+ # quickfix - deploy replset with only two units, use 'add-unit' to
932+ # add the rest
933+ if oldest_peer(peer_units('replica-set')):
934+ juju_log('Initializing replicaset')
935+ init_replset()
936+
937+ unit = "%s:%s" % (private_address, config('port'))
938+ unit_remote = "%s:%s" % (remote_hostname, relation_get('port'))
939+
940+ # If this is primary, add joined unit to replicaset
941+ if am_i_primary():
942+ juju_log('Adding new secondary... %s' % unit_remote, level=DEBUG)
943+ join_replset(unit, unit_remote)
944+
945+ juju_log('replica_set_relation_changed-finish')
946+
947+
948+@hooks.hook('replicaset-relation-departed')
949+def replica_set_relation_departed():
950+ juju_log('replica_set_relation_departed-start')
951+
952+ if not am_i_primary():
953+ juju_log('replica_set_relation_departed-finish')
954+ return
955+
956+ unit_address, unit_port = unit_get('private-address'), config('port')
957+ remote_address = relation_get('private-address')
958+ remote_port = relation_get('port')
959+
960+ # If I am the unit being removed, step me down from being primary
961+ if (unit_address, unit_port) == (remote_address, remote_port):
962+ juju_log('Stepping down from being primary...')
963+ global was_i_primary
964+ was_i_primary = True
965+ mongo_client('localhost', 'rs.stepDown()')
966+ juju_log('replica_set_relation_departed-finish')
967+ return
968+
969+ unit = "%s:%s" % (unit_get('private-address'),
970+ config('port'))
971+ unit_remote = "%s:%s" % (relation_get('hostname'),
972+ relation_get('port'))
973+
974+ leave_replset(unit, unit_remote)
975+ juju_log('Removed %s from replicaset' % unit_remote)
976+ juju_log('replica_set_relation_departed-finish')
977+
978+
979+@hooks.hook('replicaset-relation-broken')
980+def replica_set_relation_broken():
981+ juju_log('replica_set_relation_broken-start')
982+
983+ if am_i_primary():
984+ juju_log('I was primary - removing myself via new primary.', 'DEBUG')
985+ mongo_client('localhost', 'rs.stepDown()')
986+ time.sleep(15) # give some time to for re-election to happen
987+
988+ c = Connection('localhost')
989+ r = c.admin.command('isMaster')
990+ master_node = r['primary']
991+ unit = "%s:%s" % (unit_get('private-address'),
992+ config('port'))
993+
994+ juju_log('Removing myself via %s' % (master_node), 'DEBUG')
995+ leave_replset(master_node, unit)
996+
997+ juju_log('replica_set_relation_broken-finish')
998
999
1000 @hooks.hook('data-relation-joined')
1001
1002=== added symlink 'hooks/replica-set-relation-broken'
1003=== target is u'hooks.py'
1004=== added file 'setup.cfg'
1005--- setup.cfg 1970-01-01 00:00:00 +0000
1006+++ setup.cfg 2015-01-22 15:32:18 +0000
1007@@ -0,0 +1,6 @@
1008+[nosetests]
1009+verbosity=2
1010+with-coverage=1
1011+cover-erase=1
1012+cover-package=hooks
1013+
1014
1015=== modified file 'test_requirements.txt'
1016--- test_requirements.txt 2014-12-09 16:08:35 +0000
1017+++ test_requirements.txt 2015-01-22 15:32:18 +0000
1018@@ -2,4 +2,4 @@
1019 mock>=1.0.1
1020 nose>=1.3.1
1021 flake8
1022-
1023+filelock
1024
1025=== added file 'tests/00_setup.sh'
1026--- tests/00_setup.sh 1970-01-01 00:00:00 +0000
1027+++ tests/00_setup.sh 2015-01-22 15:32:18 +0000
1028@@ -0,0 +1,9 @@
1029+#!/bin/bash
1030+
1031+set -e
1032+
1033+sudo apt-get install python-setuptools -y
1034+sudo add-apt-repository ppa:juju/stable -y
1035+
1036+sudo apt-get update
1037+sudo apt-get install amulet python3 python3-requests python3-pymongo juju-core charm-tools python-mock python-pymongo -y
1038
1039=== removed file 'tests/00_setup.sh'
1040--- tests/00_setup.sh 2014-12-09 15:37:16 +0000
1041+++ tests/00_setup.sh 1970-01-01 00:00:00 +0000
1042@@ -1,9 +0,0 @@
1043-#!/bin/bash
1044-
1045-set -e
1046-
1047-sudo apt-get install python-setuptools -y
1048-sudo add-apt-repository ppa:juju/stable -y
1049-
1050-sudo apt-get update
1051-sudo apt-get install amulet python3 python3-requests python3-pymongo juju-core charm-tools python-mock python-pymongo -y
1052
1053=== added file 'tests/03_deploy_replicaset.py'
1054--- tests/03_deploy_replicaset.py 1970-01-01 00:00:00 +0000
1055+++ tests/03_deploy_replicaset.py 2015-01-22 15:32:18 +0000
1056@@ -0,0 +1,114 @@
1057+#!/usr/bin/env python3
1058+
1059+import amulet
1060+import requests
1061+import time
1062+from pymongo import MongoClient
1063+from collections import Counter
1064+
1065+
1066+#########################################################
1067+# Test Quick Config
1068+#########################################################
1069+scale = 3
1070+seconds = 900
1071+
1072+# amount of time to wait before testing for replicaset
1073+# status
1074+wait_for_replicaset = 15
1075+
1076+#########################################################
1077+# 3shard cluster configuration
1078+#########################################################
1079+d = amulet.Deployment(series='trusty')
1080+
1081+d.add('mongodb', charm='mongodb', units=scale)
1082+d.expose('mongodb')
1083+
1084+# Perform the setup for the deployment.
1085+try:
1086+ d.setup(seconds)
1087+ d.sentry.wait(seconds)
1088+except amulet.helpers.TimeoutError:
1089+ message = 'The environment did not setup in %d seconds.', seconds
1090+ amulet.raise_status(amulet.SKIP, msg=message)
1091+except:
1092+ raise
1093+
1094+sentry_dict = {
1095+ 'mongodb0-sentry': d.sentry.unit['mongodb/0'],
1096+ 'mongodb1-sentry': d.sentry.unit['mongodb/1'],
1097+ 'mongodb2-sentry': d.sentry.unit['mongodb/2'],
1098+}
1099+
1100+
1101+#############################################################
1102+# Check presence of MongoDB GUI HEALTH Status
1103+#############################################################
1104+def validate_status_interface():
1105+ r = requests.get("http://{}:28017".format(
1106+ d.sentry.unit['mongodb/0'].info['public-address']),
1107+ verify=False)
1108+ r.raise_for_status
1109+
1110+
1111+#############################################################
1112+# Validate that each unit has an active mongo service
1113+#############################################################
1114+def validate_running_services():
1115+ for service in sentry_dict:
1116+ output = sentry_dict[service].run('service mongodb status')
1117+ service_active = str(output).find('mongodb start/running')
1118+ if service_active == -1:
1119+ message = "Failed to find running MongoDB on host {}".format(
1120+ service)
1121+ amulet.raise_status(amulet.SKIP, msg=message)
1122+
1123+
1124+#############################################################
1125+# Validate proper replicaset setup
1126+#############################################################
1127+def validate_replicaset_setup():
1128+
1129+ time.sleep(wait_for_replicaset)
1130+
1131+ unit_status = []
1132+
1133+ for service in sentry_dict:
1134+ client = MongoClient(sentry_dict[service].info['public-address'])
1135+ r = client.admin.command('replSetGetStatus')
1136+ unit_status.append(r['myState'])
1137+ client.close()
1138+
1139+ primaries = Counter(unit_status)[1]
1140+ if primaries != 1:
1141+ message = "Only one PRIMARY unit allowed! Found: %s" % (primaries)
1142+ amulet.raise_status(amulet.FAIL, message)
1143+
1144+ secondrs = Counter(unit_status)[2]
1145+ if secondrs != 2:
1146+ message = "Only two SECONDARY units allowed! (Found %s)" % (secondrs)
1147+ amulet.raise_status(amulet.FAIL, message)
1148+
1149+
1150+#############################################################
1151+# Validate connectivity from $WORLD
1152+#############################################################
1153+def validate_world_connectivity():
1154+ client = MongoClient(d.sentry.unit['mongodb/0'].info['public-address'])
1155+
1156+ db = client['test']
1157+ # Can we successfully insert?
1158+ insert_id = db.amulet.insert({'assert': True})
1159+ if insert_id is None:
1160+ amulet.raise_status(amulet.FAIL, msg="Failed to insert test data")
1161+ # Can we delete from a shard using the Mongos hub?
1162+ result = db.amulet.remove(insert_id)
1163+ if result['err'] is not None:
1164+ amulet.raise_status(amulet.FAIL, msg="Failed to remove test data")
1165+
1166+
1167+validate_status_interface()
1168+validate_running_services()
1169+validate_replicaset_setup()
1170+validate_world_connectivity()
1171
1172=== added file 'unit_tests/__init__.py'
1173--- unit_tests/__init__.py 1970-01-01 00:00:00 +0000
1174+++ unit_tests/__init__.py 2015-01-22 15:32:18 +0000
1175@@ -0,0 +1,2 @@
1176+import sys
1177+sys.path.append('hooks')
1178
1179=== added file 'unit_tests/test_hooks.py'
1180--- unit_tests/test_hooks.py 1970-01-01 00:00:00 +0000
1181+++ unit_tests/test_hooks.py 2015-01-22 15:32:18 +0000
1182@@ -0,0 +1,341 @@
1183+from mock import patch, call
1184+
1185+import hooks
1186+
1187+from test_utils import CharmTestCase
1188+from pymongo.errors import OperationFailure
1189+from subprocess import CalledProcessError
1190+
1191+# Defines a set of functions to patch on the hooks object. Any of these
1192+# methods will be patched by default on the default invocations of the
1193+# hooks.some_func(). Invoking the the interface change relations will cause
1194+# the hooks context to be created outside of the normal mockery.
1195+TO_PATCH = [
1196+ 'relation_id',
1197+ 'relation_get',
1198+ 'relation_set',
1199+ 'unit_get',
1200+ 'juju_log',
1201+ 'config',
1202+]
1203+
1204+
1205+class MongoHooksTest(CharmTestCase):
1206+
1207+ def setUp(self):
1208+ super(MongoHooksTest, self).setUp(hooks, TO_PATCH)
1209+
1210+ # The self.config object can be used for direct invocations of the
1211+ # hooks methods. The side_effect of invoking the config object within
1212+ # the hooks object will return the value that is set in the test case's
1213+ # test_config dictionary
1214+ self.config.side_effect = self.test_config.get
1215+ self.relation_get.side_effect = self.test_relation.get
1216+
1217+ @patch.object(hooks, 'restart_mongod')
1218+ @patch.object(hooks, 'enable_replset')
1219+ # Note: patching the os.environ dictionary in-line here so there's no
1220+ # additional parameter sent into the function
1221+ @patch.dict('os.environ', JUJU_UNIT_NAME='fake-unit/0')
1222+ def test_replica_set_relation_joined(self, mock_enable_replset,
1223+ mock_restart):
1224+ self.unit_get.return_value = 'private.address'
1225+ self.test_config.set('port', '1234')
1226+ self.test_config.set('replicaset', 'fake-replicaset')
1227+ self.relation_id.return_value = 'fake-relation-id'
1228+
1229+ mock_enable_replset.return_value = False
1230+
1231+ hooks.replica_set_relation_joined()
1232+
1233+ # Verify that mongodb was NOT restarted since the replicaset we claimed
1234+ # was not enabled.
1235+ self.assertFalse(mock_restart.called)
1236+
1237+ exp_rel_vals = {'hostname': 'private.address',
1238+ 'port': '1234',
1239+ 'replset': 'fake-replicaset',
1240+ 'install-order': '0',
1241+ 'type': 'replset'}
1242+ # Check that the relation data was set as we expect it to be set.
1243+ self.relation_set.assert_called_with('fake-relation-id', exp_rel_vals)
1244+
1245+ mock_enable_replset.reset_mock()
1246+ self.relation_set.reset_mock()
1247+ mock_enable_replset.return_value = True
1248+
1249+ hooks.replica_set_relation_joined()
1250+
1251+ self.assertTrue(mock_restart.called)
1252+ self.relation_set.assert_called_with('fake-relation-id', exp_rel_vals)
1253+
1254+ @patch.object(hooks, 'run_admin_command')
1255+ @patch.object(hooks, 'Connection')
1256+ @patch.object(hooks, 'config')
1257+ @patch.object(hooks, 'mongo_client')
1258+ @patch('time.sleep')
1259+ def test_init_repl_set(self, mock_sleep, mock_mongo_client_fn,
1260+ mock_config, mock_mongo_client,
1261+ mock_run_admin_command):
1262+ mock_mongo_client_fn.return_value = False
1263+
1264+ mock_config.return_value = {'replicaset': 'foo',
1265+ 'private-address': 'mongo.local',
1266+ 'port': '12345'}
1267+
1268+ # Put the OK state (1) at the end and check the loop.
1269+ ret_values = [{'myState': x} for x in [0, 2, 5, 1]]
1270+ mock_run_admin_command.side_effect = ret_values
1271+
1272+ hooks.init_replset()
1273+
1274+ mock_run_admin_command.assert_called()
1275+ self.assertEqual(len(ret_values), mock_run_admin_command.call_count)
1276+ self.assertEqual(len(ret_values) + 1, mock_sleep.call_count)
1277+
1278+ mock_run_admin_command.reset_mock()
1279+ exc = [OperationFailure('Received replSetInitiate'),
1280+ OperationFailure('unhandled')]
1281+ mock_run_admin_command.side_effect = exc
1282+
1283+ try:
1284+ hooks.init_replset()
1285+ self.assertTrue(False, msg="Expected error")
1286+ except OperationFailure:
1287+ pass
1288+
1289+ mock_run_admin_command.assert_called()
1290+ self.assertEqual(2, mock_run_admin_command.call_count)
1291+
1292+ @patch.object(hooks, 'mongo_client_smart')
1293+ def test_join_replset(self, mock_mongo_client):
1294+ hooks.join_replset()
1295+ self.assertFalse(mock_mongo_client.called)
1296+
1297+ mock_mongo_client.reset_mock()
1298+ hooks.join_replset(master_node='mongo.local')
1299+ self.assertFalse(mock_mongo_client.called)
1300+
1301+ mock_mongo_client.reset_mock()
1302+ hooks.join_replset(host='fake-host')
1303+ self.assertFalse(mock_mongo_client.called)
1304+
1305+ mock_mongo_client.reset_mock()
1306+ hooks.join_replset(master_node='mongo.local', host='fake-host')
1307+ mock_mongo_client.assert_called_with('localhost',
1308+ 'rs.add("fake-host")')
1309+
1310+ @patch.object(hooks, 'mongo_client')
1311+ def test_leave_replset(self, mock_mongo_client):
1312+ hooks.leave_replset()
1313+ self.assertFalse(mock_mongo_client.called)
1314+
1315+ mock_mongo_client.reset_mock()
1316+ hooks.leave_replset(master_node='mongo.local')
1317+ self.assertFalse(mock_mongo_client.called)
1318+
1319+ mock_mongo_client.reset_mock()
1320+ hooks.leave_replset(host='fake-host')
1321+ self.assertFalse(mock_mongo_client.called)
1322+
1323+ mock_mongo_client.reset_mock()
1324+ hooks.leave_replset('mongo.local', 'fake-host')
1325+ mock_mongo_client.assert_called_with('mongo.local',
1326+ 'rs.remove("fake-host")')
1327+
1328+ @patch.object(hooks, 'apt_install')
1329+ @patch.object(hooks, 'apt_update')
1330+ @patch.object(hooks, 'add_source')
1331+ @patch.dict('os.environ', CHARM_DIR='/tmp/charm/dir')
1332+ def test_install_hook(self, mock_add_source, mock_apt_update,
1333+ mock_apt_install):
1334+ self.test_config.set('source', 'fake-source')
1335+ self.test_config.set('key', 'fake-key')
1336+
1337+ hooks.install_hook()
1338+ mock_add_source.assert_called_with('fake-source', 'fake-key')
1339+ mock_apt_update.assert_called_with(fatal=True)
1340+ mock_apt_install.assert_called_with(packages=hooks.INSTALL_PACKAGES,
1341+ fatal=True)
1342+
1343+ @patch.object(hooks, 'run_admin_command')
1344+ @patch.object(hooks, 'Connection')
1345+ @patch('time.sleep')
1346+ def test_am_i_primary(self, mock_sleep, mock_mongo_client,
1347+ mock_run_admin_cmd):
1348+ mock_run_admin_cmd.side_effect = [{'myState': x} for x in xrange(5)]
1349+ expected_results = [True if x == 1 else False for x in xrange(5)]
1350+
1351+ # Check expected return values each time...
1352+ for exp in expected_results:
1353+ rv = hooks.am_i_primary()
1354+ self.assertEqual(exp, rv)
1355+
1356+ @patch.object(hooks, 'run_admin_command')
1357+ @patch.object(hooks, 'Connection')
1358+ @patch('time.sleep')
1359+ def test_am_i_primary_too_many_attempts(self, mock_sleep,
1360+ mock_mongo_client,
1361+ mock_run_admin_cmd):
1362+ msg = 'replSetInitiate - should come online shortly'
1363+ mock_run_admin_cmd.side_effect = [OperationFailure(msg)
1364+ for x in xrange(10)]
1365+
1366+ try:
1367+ hooks.am_i_primary()
1368+ self.assertTrue(False, 'Expected failure.')
1369+ except hooks.TimeoutException:
1370+ self.assertEqual(mock_run_admin_cmd.call_count, 10)
1371+ pass
1372+
1373+ @patch.object(hooks, 'run_admin_command')
1374+ @patch.object(hooks, 'Connection')
1375+ @patch('time.sleep')
1376+ def test_am_i_primary_operation_failures(self, mock_sleep,
1377+ mock_mongo_client,
1378+ mock_run_admin_cmd):
1379+ mock_run_admin_cmd.side_effect = OperationFailure('EMPTYCONFIG')
1380+
1381+ rv = hooks.am_i_primary()
1382+ mock_run_admin_cmd.assert_called()
1383+ self.assertFalse(rv)
1384+
1385+ mock_run_admin_cmd.reset_mock()
1386+ mock_run_admin_cmd.side_effect = OperationFailure('unexpected failure')
1387+ try:
1388+ hooks.am_i_primary()
1389+ self.assertFalse(True, "Expected OperationFailure to be raised")
1390+ except OperationFailure:
1391+ mock_run_admin_cmd.assert_called()
1392+
1393+ @patch('time.sleep')
1394+ @patch('subprocess.check_output')
1395+ def test_mongo_client_smart_no_command(self, mock_check_output,
1396+ mock_sleep):
1397+ rv = hooks.mongo_client_smart()
1398+ self.assertFalse(rv)
1399+ self.assertEqual(0, mock_check_output.call_count)
1400+
1401+ mock_check_output.reset_mock()
1402+ mock_check_output.return_value = '{"ok": 1}'
1403+
1404+ rv = hooks.mongo_client_smart(command='fake-cmd')
1405+ self.assertTrue(rv)
1406+ mock_check_output.assert_called_once_with(['mongo', '--quiet',
1407+ '--host', 'localhost',
1408+ '--eval',
1409+ 'printjson(fake-cmd)'])
1410+
1411+ @patch('time.sleep')
1412+ @patch('subprocess.check_output')
1413+ def test_mongo_client_smart_error_cases(self, mock_ck_output, mock_sleep):
1414+ mock_ck_output.side_effect = [CalledProcessError(1, 'cmd',
1415+ output='fake-error')
1416+ for x in xrange(11)]
1417+ rv = hooks.mongo_client_smart(command='fake-cmd')
1418+ self.assertFalse(rv)
1419+
1420+ @patch('subprocess.call')
1421+ def test_mongo_client(self, mock_subprocess):
1422+ rv = hooks.mongo_client()
1423+ self.assertFalse(rv)
1424+ self.assertEqual(0, mock_subprocess.call_count)
1425+
1426+ mock_subprocess.reset_mock()
1427+ rv = hooks.mongo_client(host='fake-host')
1428+ self.assertFalse(rv)
1429+ self.assertEqual(0, mock_subprocess.call_count)
1430+
1431+ mock_subprocess.reset_mock()
1432+ rv = hooks.mongo_client(command='fake-command')
1433+ self.assertFalse(rv)
1434+ self.assertEqual(0, mock_subprocess.call_count)
1435+
1436+ mock_subprocess.reset_mock()
1437+ mock_subprocess.return_value = 0
1438+ rv = hooks.mongo_client(host='fake-host', command='fake-command')
1439+ expected_cmd = ("mongo --host %s --eval 'printjson(%s)'"
1440+ % ('fake-host', 'fake-command'))
1441+ mock_subprocess.assert_called_once_with(expected_cmd, shell=True)
1442+ self.assertTrue(rv)
1443+
1444+ mock_subprocess.reset_mock()
1445+ mock_subprocess.return_value = 1
1446+ rv = hooks.mongo_client(host='fake-host', command='fake-command')
1447+ expected_cmd = ("mongo --host %s --eval 'printjson(%s)'"
1448+ % ('fake-host', 'fake-command'))
1449+ mock_subprocess.assert_called_once_with(expected_cmd, shell=True)
1450+ self.assertFalse(rv)
1451+
1452+ @patch.object(hooks, 'am_i_primary')
1453+ @patch.object(hooks, 'init_replset')
1454+ @patch.object(hooks, 'relation_get')
1455+ @patch.object(hooks, 'peer_units')
1456+ @patch.object(hooks, 'oldest_peer')
1457+ @patch.object(hooks, 'join_replset')
1458+ @patch.object(hooks, 'unit_get')
1459+ def test_replica_set_relation_changed(self, mock_unit_get,
1460+ mock_join_replset, mock_oldest_peer,
1461+ mock_peer_units, mock_relation_get,
1462+ mock_init_replset, mock_is_primary):
1463+ # set the unit_get('private-address')
1464+ mock_unit_get.return_value = 'juju-local-unit-0.local'
1465+ mock_relation_get.return_value = None
1466+
1467+ # Test when remote hostname is None, should not join
1468+ hooks.replica_set_relation_changed()
1469+ self.assertEqual(0, mock_join_replset.call_count)
1470+
1471+ # Test remote hostname is valid, but master is somehow not defined
1472+ mock_join_replset.reset_mock()
1473+ mock_relation_get.return_value = 'juju-local-unit-0'
1474+
1475+ hooks.replica_set_relation_changed()
1476+
1477+ self.assertEqual(1, mock_join_replset.call_count)
1478+
1479+ # Test when not oldest peer, don't init replica set
1480+ mock_init_replset.reset_mock()
1481+ mock_oldest_peer.reset_mock()
1482+ mock_peer_units.return_value = ['mongodb/1', 'mongodb/2']
1483+ mock_oldest_peer.return_value = False
1484+
1485+ hooks.replica_set_relation_changed()
1486+
1487+ self.assertEqual(mock_init_replset.call_count, 0)
1488+
1489+ # Test when its also the PRIMARY
1490+ mock_relation_get.reset_mock()
1491+ mock_relation_get.side_effect = ['juju-remote-unit-0', '12345']
1492+ mock_oldest_peer.reset_mock()
1493+ mock_oldest_peer.return_value = False
1494+ mock_is_primary.reset_mock()
1495+ mock_is_primary.return_value = True
1496+
1497+ hooks.replica_set_relation_changed()
1498+ call1 = call('juju-local-unit-0.local:27017',
1499+ 'juju-remote-unit-0:12345')
1500+ mock_join_replset.assert_has_calls(call1)
1501+
1502+ @patch.object(hooks, 'unit_get')
1503+ @patch.object(hooks, 'leave_replset')
1504+ @patch.object(hooks, 'am_i_primary')
1505+ def test_replica_set_relation_departed(self, mock_am_i_primary,
1506+ mock_leave_replset, mock_unit_get):
1507+ mock_am_i_primary.return_value = False
1508+ hooks.replica_set_relation_departed()
1509+
1510+ self.assertEqual(0, mock_leave_replset.call_count)
1511+
1512+ mock_am_i_primary.reset_mock()
1513+ mock_am_i_primary.return_value = True
1514+ mock_unit_get.return_value = 'juju-local'
1515+
1516+ self.test_relation.set({'hostname': 'juju-remote',
1517+ 'port': '27017'})
1518+ mock_leave_replset.reset_mock()
1519+
1520+ hooks.replica_set_relation_departed()
1521+
1522+ call1 = call('juju-local:27017', 'juju-remote:27017')
1523+ mock_leave_replset.assert_has_calls(call1)
1524
1525=== added file 'unit_tests/test_utils.py'
1526--- unit_tests/test_utils.py 1970-01-01 00:00:00 +0000
1527+++ unit_tests/test_utils.py 2015-01-22 15:32:18 +0000
1528@@ -0,0 +1,111 @@
1529+import logging
1530+import unittest
1531+import os
1532+import yaml
1533+import io
1534+
1535+from contextlib import contextmanager
1536+from mock import patch
1537+
1538+
1539+@contextmanager
1540+def mock_open(filename, contents=None):
1541+ ''' Slightly simpler mock of open to return contents for filename '''
1542+ def mock_file(*args):
1543+ if args[0] == filename:
1544+ return io.StringIO(contents)
1545+ else:
1546+ return open(*args)
1547+ with patch('__builtin__.open', mock_file):
1548+ yield
1549+
1550+
1551+def load_config():
1552+ '''
1553+ Walk backwords from __file__ looking for config.yaml, load and return the
1554+ 'options' section'
1555+ '''
1556+ config = None
1557+ f = __file__
1558+ while config is None:
1559+ d = os.path.dirname(f)
1560+ if os.path.isfile(os.path.join(d, 'config.yaml')):
1561+ config = os.path.join(d, 'config.yaml')
1562+ break
1563+ f = d
1564+
1565+ if not config:
1566+ logging.error('Could not find config.yaml in any parent directory '
1567+ 'of %s. ' % file)
1568+ raise Exception
1569+
1570+ return yaml.safe_load(open(config).read())['options']
1571+
1572+
1573+def get_default_config():
1574+ '''
1575+ Load default charm config from config.yaml return as a dict.
1576+ If no default is set in config.yaml, its value is None.
1577+ '''
1578+ default_config = {}
1579+ config = load_config()
1580+ for k, v in config.iteritems():
1581+ if 'default' in v:
1582+ default_config[k] = v['default']
1583+ else:
1584+ default_config[k] = None
1585+ return default_config
1586+
1587+
1588+class CharmTestCase(unittest.TestCase):
1589+ def setUp(self, obj, patches):
1590+ super(CharmTestCase, self).setUp()
1591+ self.patches = patches
1592+ self.obj = obj
1593+ self.test_config = TestConfig()
1594+ self.test_relation = TestRelation()
1595+ self.patch_all()
1596+
1597+ def patch(self, method):
1598+ _m = patch.object(self.obj, method)
1599+ mock = _m.start()
1600+ self.addCleanup(_m.stop)
1601+ return mock
1602+
1603+ def patch_all(self):
1604+ for method in self.patches:
1605+ setattr(self, method, self.patch(method))
1606+
1607+
1608+class TestConfig(object):
1609+ def __init__(self):
1610+ self.config = get_default_config()
1611+
1612+ def get(self, attr):
1613+ try:
1614+ return self.config[attr]
1615+ except KeyError:
1616+ return None
1617+
1618+ def get_all(self):
1619+ return self.config
1620+
1621+ def set(self, attr, value):
1622+ if attr not in self.config:
1623+ raise KeyError
1624+ self.config[attr] = value
1625+
1626+
1627+class TestRelation(object):
1628+ def __init__(self, relation_data={}):
1629+ self.relation_data = relation_data
1630+
1631+ def set(self, relation_data):
1632+ self.relation_data = relation_data
1633+
1634+ def get(self, attr=None, unit=None, rid=None):
1635+ if attr is None:
1636+ return self.relation_data
1637+ elif attr in self.relation_data:
1638+ return self.relation_data.get(attr)
1639+ return None

Subscribers

People subscribed via source and target branches