diff --git a/.gitignore b/.gitignore index aad4c2b..276135e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ build/ dist/ .local -zaza.egg-info/ +zaza.openstack.egg-info/ .coverage .vscode/ # Sphinx diff --git a/.travis.yml b/.travis.yml index b31dccf..4f8dbb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,23 +6,5 @@ python: install: pip install tox-travis env: - ENV=pep8 - - ENV=py3 - - ENV=func-travis -comment: | - install dependencies in script phase saving time on simpler test environments - sudo back to ourself to activate lxd group membership executable search path script: - - if [ $ENV = 'func-travis' ]; then - sudo apt remove -y --purge lxd lxd-client; - sudo snap install lxd; - sudo snap install juju --classic; - sudo sh -c 'echo PATH=/snap/bin:$PATH >> /etc/environment'; - sudo lxd waitready; - sudo lxd init --auto; - sudo usermod -a -G lxd travis; - sudo su travis -c 'juju bootstrap --debug --no-gui localhost'; - fi - - tox -c tox.ini -e $ENV - - if [ $ENV = 'func-travis' ]; then - sudo su travis -c 'juju status -m $(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/)'; - fi + - tox -c tox.ini -e $ENV \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ad6b942..307fa94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ + aiounittest async_generator juju @@ -35,3 +36,4 @@ paramiko # Documentation requirements sphinx sphinxcontrib-asyncio +git+https://github.com/openstack-charmers/zaza#egg=zaza \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 3aaec77..d429998 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,12 @@ [metadata] -name = zaza -summary = A Python3-only functional test framework for OpenStack Charms -version = 0.0.2.dev1 +name = zaza.openstack +summary = Zaza tests for the OpenStack Charms project +version = 0.0.1.dev1 description-file = README.rst author = OpenStack Charmers author-email = openstack-charmers@lists.ubuntu.com -url = https://github.com/openstack-charmers/zaza +url = https://github.com/openstack-charmers/zaza-openstack-tests classifier = Development Status :: 2 - Pre-Alpha Intended Audience :: Developers @@ -26,18 +26,18 @@ all_files = 1 upload-dir = doc/build/html [compile_catalog] -directory = zaza/locale -domain = zaza +directory = zaza_openstack_tests/locale +domain = zaza_openstack_tests [update_catalog] -domain = zaza -output_dir = zaza/locale -input_file = zaza/locale/zaza.pot +domain = zaza_openstack_tests +output_dir = zaza_openstack_tests/locale +input_file = zaza_openstack_tests/locale/zaza.pot [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg -output_file = zaza/locale/zaza.pot +output_file = zaza_openstack_tests/locale/zaza.pot [nosetests] -nologcapture=1 +nologcapture=1 \ No newline at end of file diff --git a/setup.py b/setup.py index a1ee7be..0980b96 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module used to setup the zaza framework.""" +"""Module used to setup the zaza framework tests.""" from __future__ import print_function @@ -41,6 +41,7 @@ install_require = [ 'python-octaviaclient', 'python-cinderclient', 'python-swiftclient', + 'zaza@git+https://github.com/openstack-charmers/zaza.git#egg=zaza', ] tests_require = [ @@ -92,19 +93,6 @@ if sys.argv[-1] == 'tag': setup( - entry_points={ - 'console_scripts': [ - 'functest-run-suite = zaza.charm_lifecycle.func_test_runner:main', - 'functest-deploy = zaza.charm_lifecycle.deploy:main', - 'functest-configure = zaza.charm_lifecycle.configure:main', - 'functest-destroy = zaza.charm_lifecycle.destroy:main', - 'functest-prepare = zaza.charm_lifecycle.prepare:main', - 'functest-test = zaza.charm_lifecycle.test:main', - 'current-apps = zaza.model:main', - 'tempest-config = zaza.tempest_config:main', - 'remove-placement = zaza.openstack.utilities.bundle:main', - ] - }, license='Apache-2.0: http://www.apache.org/licenses/LICENSE-2.0', packages=find_packages(exclude=["unit_tests"]), zip_safe=False, @@ -114,4 +102,4 @@ setup( 'testing': tests_require, }, tests_require=tests_require, -) +) \ No newline at end of file diff --git a/tox.ini b/tox.ini index e2b6f65..2447271 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pep8,py3 +envlist = pep8, py3 skipsdist = True [testenv] @@ -7,6 +7,7 @@ setenv = VIRTUAL_ENV={envdir} PYTHONHASHSEED=0 install_command = pip install {opts} {packages} + commands = nosetests --with-coverage --cover-package=zaza {posargs} {toxinidir}/unit_tests [testenv:py3] @@ -33,28 +34,4 @@ basepython = python3 changedir = doc/source deps = -r{toxinidir}/requirements.txt -commands = sphinx-build -W -b html -d {toxinidir}/doc/build/doctrees . {toxinidir}/doc/build/html - -[testenv:func] -basepython = python3 -deps = -r{toxinidir}/requirements.txt -commands = - {envdir}/bin/python3 setup.py install - functest-run-suite --keep-model - -[testenv:func-travis] -basepython = python3 -deps = -r{toxinidir}/requirements.txt -# sudo back to ourself to activate lxd group membership executable search path -whitelist_externals = sudo -passenv = USER -commands = - {envdir}/bin/python3 setup.py install - sudo su {env:USER} -c 'source {envdir}/bin/activate && functest-run-suite --keep-model' - -[testenv:remove-placement] -basepython = python3 -deps = -r{toxinidir}/requirements.txt -commands = - {envdir}/bin/python3 setup.py install - remove-placement {posargs} \ No newline at end of file +commands = sphinx-build -W -b html -d {toxinidir}/doc/build/doctrees . {toxinidir}/doc/build/html \ No newline at end of file diff --git a/unit_tests/test_zaza_charm_lifecycle_configure.py b/unit_tests/test_zaza_charm_lifecycle_configure.py deleted file mode 100644 index 6c9c10e..0000000 --- a/unit_tests/test_zaza_charm_lifecycle_configure.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import mock - -import zaza.charm_lifecycle.configure as lc_configure -import unit_tests.utils as ut_utils - - -class TestCharmLifecycleConfigure(ut_utils.BaseTestCase): - - def test_run_configure_list(self): - self.patch_object(lc_configure.utils, 'get_class') - self.get_class.side_effect = lambda x: x - mock1 = mock.MagicMock() - mock2 = mock.MagicMock() - lc_configure.run_configure_list([mock1, mock2]) - self.assertTrue(mock1.called) - self.assertTrue(mock2.called) - - def test_configure(self): - self.patch_object(lc_configure, 'run_configure_list') - mock1 = mock.MagicMock() - mock2 = mock.MagicMock() - lc_configure.configure('modelname', [mock1, mock2]) - self.run_configure_list.assert_called_once_with([mock1, mock2]) - - def test_parser(self): - args = lc_configure.parse_args( - ['-m', 'modelname', '-c', 'my.func1', 'my.func2']) - self.assertEqual(args.configfuncs, ['my.func1', 'my.func2']) - self.assertEqual(args.model_name, 'modelname') - - def test_parser_logging(self): - # Using defaults - args = lc_configure.parse_args(['-m', 'model']) - self.assertEqual(args.loglevel, 'INFO') - # Using args - args = lc_configure.parse_args(['-m', 'model', '--log', 'DEBUG']) - self.assertEqual(args.loglevel, 'DEBUG') diff --git a/unit_tests/test_zaza_charm_lifecycle_deploy.py b/unit_tests/test_zaza_charm_lifecycle_deploy.py deleted file mode 100644 index df84ab7..0000000 --- a/unit_tests/test_zaza_charm_lifecycle_deploy.py +++ /dev/null @@ -1,280 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import jinja2 -import mock - -import zaza.charm_lifecycle.deploy as lc_deploy -import unit_tests.utils as ut_utils - - -class TestCharmLifecycleDeploy(ut_utils.BaseTestCase): - - def test_is_valid_env_key(self): - self.assertTrue(lc_deploy.is_valid_env_key('OS_VIP04')) - self.assertTrue(lc_deploy.is_valid_env_key('FIP_RANGE')) - self.assertTrue(lc_deploy.is_valid_env_key('GATEWAY')) - self.assertTrue(lc_deploy.is_valid_env_key('NAME_SERVER')) - self.assertTrue(lc_deploy.is_valid_env_key('NET_ID')) - self.assertTrue(lc_deploy.is_valid_env_key('VIP_RANGE')) - self.assertTrue(lc_deploy.is_valid_env_key('AMULET_OS_VIP')) - self.assertFalse(lc_deploy.is_valid_env_key('ZAZA_TEMPLATE_VIP00')) - self.assertFalse(lc_deploy.is_valid_env_key('PATH')) - - def test_get_template_context_from_env(self): - self.patch_object(lc_deploy.os, 'environ') - self.environ.items.return_value = [ - ('AMULET_OS_VIP', '10.10.0.2'), - ('OS_VIP04', '10.10.0.2'), - ('ZAZA_TEMPLATE_VIP00', '20.3.4.5'), - ('PATH', 'aa')] - self.assertEqual( - lc_deploy.get_template_context_from_env(), - {'OS_VIP04': '10.10.0.2', - 'AMULET_OS_VIP': '10.10.0.2'} - ) - - def test_get_charm_config_context(self): - self.patch_object(lc_deploy.utils, 'get_charm_config') - self.get_charm_config.return_value = { - 'charm_name': 'mycharm'} - self.assertEqual( - lc_deploy.get_charm_config_context(), - {'charm_location': '../../../mycharm', 'charm_name': 'mycharm'}) - - def test_get_template_overlay_context(self): - self.patch_object(lc_deploy, 'get_template_context_from_env') - self.patch_object(lc_deploy, 'get_charm_config_context') - self.get_template_context_from_env.return_value = { - 'OS_VIP04': '10.10.0.2'} - self.get_charm_config_context.return_value = { - 'charm_location': '../../../mycharm', - 'charm_name': 'mycharm'} - self.assertEqual( - lc_deploy.get_template_overlay_context(), - { - 'OS_VIP04': '10.10.0.2', - 'charm_location': '../../../mycharm', - 'charm_name': 'mycharm'}) - - def test_get_overlay_template_dir(self): - self.assertEqual( - lc_deploy.get_overlay_template_dir(), - 'tests/bundles/overlays') - - def test_get_jinja2_env(self): - self.patch_object(lc_deploy, 'get_overlay_template_dir') - self.get_overlay_template_dir.return_value = 'mytemplatedir' - self.patch_object(lc_deploy.jinja2, 'Environment') - self.patch_object(lc_deploy.jinja2, 'FileSystemLoader') - jinja_env_mock = mock.MagicMock() - self.Environment.return_value = jinja_env_mock - self.assertEqual( - lc_deploy.get_jinja2_env(), - jinja_env_mock) - self.FileSystemLoader.assert_called_once_with('mytemplatedir') - - def test_get_template_name(self): - self.assertEqual( - lc_deploy.get_template_name('mybundles/mybundle.yaml'), - 'mybundle.yaml.j2') - - def test_get_template(self): - self.patch_object(lc_deploy, 'get_jinja2_env') - jinja_env_mock = mock.MagicMock() - self.get_jinja2_env.return_value = jinja_env_mock - jinja_env_mock.get_template.return_value = 'mytemplate' - self.assertEqual( - lc_deploy.get_template('mybundle.yaml'), - 'mytemplate') - - def test_get_template_missing_template(self): - self.patch_object(lc_deploy, 'get_jinja2_env') - jinja_env_mock = mock.MagicMock() - self.get_jinja2_env.return_value = jinja_env_mock - jinja_env_mock.get_template.side_effect = \ - jinja2.exceptions.TemplateNotFound(name='bob') - self.assertIsNone(lc_deploy.get_template('mybundle.yaml')) - - def test_render_template(self): - self.patch_object(lc_deploy, 'get_template_overlay_context') - template_mock = mock.MagicMock() - template_mock.render.return_value = 'Template contents' - m = mock.mock_open() - with mock.patch('zaza.charm_lifecycle.deploy.open', m, create=True): - lc_deploy.render_template(template_mock, '/tmp/mybundle.yaml') - m.assert_called_once_with('/tmp/mybundle.yaml', 'w') - handle = m() - handle.write.assert_called_once_with('Template contents') - - def test_render_overlay(self): - self.patch_object(lc_deploy, 'render_template') - template_mock = mock.MagicMock() - self.patch_object(lc_deploy, 'get_template') - self.get_template.return_value = template_mock - lc_deploy.render_overlay('my_overlay.yaml', '/tmp/special-dir') - self.render_template.assert_called_once_with( - template_mock, - '/tmp/special-dir/my_overlay.yaml') - - def test_template_missing_required_variables(self): - self.patch_object(lc_deploy, 'get_template_overlay_context') - self.get_template_overlay_context.return_value = {} - self.patch_object(lc_deploy.sys, 'exit') - self.patch_object(lc_deploy.logging, 'error') - jinja2_env = lc_deploy.get_jinja2_env() - template = jinja2_env.from_string('{{required_variable}}') - m = mock.mock_open() - with mock.patch('zaza.charm_lifecycle.deploy.open', m, create=True): - lc_deploy.render_template(template, '/tmp/mybundle.yaml') - m.assert_called_once_with('/tmp/mybundle.yaml', 'w') - self.error.assert_called_once_with( - "Template error. You may be missing" - " a mandatory environment variable : " - "'required_variable' is undefined") - self.exit.assert_called_once_with(1) - - def test_render_overlay_no_template(self): - self.patch_object(lc_deploy, 'get_template') - self.get_template.return_value = None - self.assertIsNone(lc_deploy.render_overlay('mybundle.yaml', '/tmp/')) - - def test_render_local_overlay(self): - self.patch_object(lc_deploy.utils, 'get_charm_config') - self.get_charm_config.return_value = { - 'charm_name': 'mycharm'} - self.patch_object(lc_deploy.jinja2, 'Environment') - self.patch_object(lc_deploy, 'get_template', return_value='atemplate') - self.patch_object(lc_deploy, 'render_template') - self.assertEqual( - lc_deploy.render_local_overlay('/target'), - '/target/local-charm-overlay.yaml') - self.assertFalse(self.Environment.called) - self.render_template.assert_called_once_with( - 'atemplate', - '/target/local-charm-overlay.yaml') - - def test_render_local_overlay_default(self): - jenv_mock = mock.MagicMock() - jenv_mock.from_string.return_value = 'atemplate' - self.patch_object(lc_deploy.utils, 'get_charm_config') - self.get_charm_config.return_value = { - 'charm_name': 'mycharm'} - self.patch_object(lc_deploy.jinja2, 'Environment', - return_value=jenv_mock) - self.patch_object(lc_deploy, 'get_template', return_value=None) - self.patch_object(lc_deploy, 'render_template') - self.assertEqual( - lc_deploy.render_local_overlay('/target'), - '/target/local-charm-overlay.yaml') - jenv_mock.from_string.assert_called_once_with(mock.ANY) - self.render_template.assert_called_once_with( - 'atemplate', - '/target/local-charm-overlay.yaml') - - def test_render_overlays(self): - RESP = { - 'mybundles/mybundle.yaml': '/tmp/mybundle.yaml'} - self.patch_object(lc_deploy, 'render_local_overlay') - self.render_local_overlay.return_value = '/tmp/local-overlay.yaml' - self.patch_object(lc_deploy, 'render_overlay') - self.render_overlay.side_effect = lambda x, y: RESP[x] - self.assertEqual( - lc_deploy.render_overlays('mybundles/mybundle.yaml', '/tmp'), - ['/tmp/local-overlay.yaml', '/tmp/mybundle.yaml']) - - def test_render_overlays_missing(self): - RESP = {'mybundles/mybundle.yaml': None} - self.patch_object(lc_deploy, 'render_overlay') - self.patch_object(lc_deploy, 'render_local_overlay') - self.render_local_overlay.return_value = '/tmp/local.yaml' - self.render_overlay.side_effect = lambda x, y: RESP[x] - self.assertEqual( - lc_deploy.render_overlays('mybundles/mybundle.yaml', '/tmp'), - ['/tmp/local.yaml']) - - def test_deploy_bundle(self): - self.patch_object(lc_deploy.utils, 'get_charm_config') - self.get_charm_config.return_value = {} - self.patch_object(lc_deploy, 'render_overlays') - self.patch_object(lc_deploy.subprocess, 'check_call') - self.render_overlays.return_value = [] - lc_deploy.deploy_bundle('bun.yaml', 'newmodel') - self.check_call.assert_called_once_with( - ['juju', 'deploy', '-m', 'newmodel', 'bun.yaml']) - - def test_deploy(self): - self.patch_object(lc_deploy.zaza.model, 'wait_for_application_states') - self.patch_object(lc_deploy.utils, 'get_charm_config') - self.get_charm_config.return_value = {} - self.patch_object(lc_deploy, 'deploy_bundle') - lc_deploy.deploy('bun.yaml', 'newmodel') - self.deploy_bundle.assert_called_once_with('bun.yaml', 'newmodel') - self.wait_for_application_states.assert_called_once_with( - 'newmodel', - {}) - - def test_deploy_bespoke_states(self): - self.patch_object(lc_deploy.zaza.model, 'wait_for_application_states') - self.patch_object(lc_deploy.utils, 'get_charm_config') - self.get_charm_config.return_value = { - 'target_deploy_status': { - 'vault': { - 'workload-status': 'blocked', - 'workload-status-message': 'Vault needs to be inited'}}} - self.patch_object(lc_deploy, 'deploy_bundle') - lc_deploy.deploy('bun.yaml', 'newmodel') - self.deploy_bundle.assert_called_once_with('bun.yaml', 'newmodel') - self.wait_for_application_states.assert_called_once_with( - 'newmodel', - {'vault': { - 'workload-status': 'blocked', - 'workload-status-message': 'Vault needs to be inited'}}) - - def test_deploy_nowait(self): - self.patch_object(lc_deploy.zaza.model, 'wait_for_application_states') - self.patch_object(lc_deploy, 'deploy_bundle') - lc_deploy.deploy('bun.yaml', 'newmodel', wait=False) - self.deploy_bundle.assert_called_once_with('bun.yaml', 'newmodel') - self.assertFalse(self.wait_for_application_states.called) - - def test_parser(self): - args = lc_deploy.parse_args([ - '-m', 'mymodel', - '-b', 'bun.yaml']) - self.assertEqual(args.model, 'mymodel') - self.assertEqual(args.bundle, 'bun.yaml') - self.assertTrue(args.wait) - - def test_parser_nowait(self): - args = lc_deploy.parse_args([ - '-m', 'mymodel', - '-b', 'bun.yaml', - '--no-wait']) - self.assertFalse(args.wait) - - def test_parser_logging(self): - args = lc_deploy.parse_args([ - '-m', 'mymodel', - '-b', 'bun.yaml' - ]) - # Using defaults - self.assertEqual(args.loglevel, 'INFO') - # Specify the parameter - args = lc_deploy.parse_args([ - '-m', 'mymodel', - '-b', 'bun.yaml', - '--log', 'DEBUG' - ]) - self.assertEqual(args.loglevel, 'DEBUG') diff --git a/unit_tests/test_zaza_charm_lifecycle_destroy.py b/unit_tests/test_zaza_charm_lifecycle_destroy.py deleted file mode 100644 index 7762ba2..0000000 --- a/unit_tests/test_zaza_charm_lifecycle_destroy.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import zaza.charm_lifecycle.destroy as lc_destroy -import unit_tests.utils as ut_utils - - -class TestCharmLifecycleDestroy(ut_utils.BaseTestCase): - - def test_destroy(self): - self.patch_object(lc_destroy.zaza.controller, 'destroy_model') - lc_destroy.destroy('doomed') - self.destroy_model.assert_called_once_with('doomed') - - def test_parser(self): - args = lc_destroy.parse_args(['-m', 'doomed']) - self.assertEqual(args.model_name, 'doomed') - - def test_parser_logging(self): - # Using defaults - args = lc_destroy.parse_args(['-m', 'doomed']) - self.assertEqual(args.loglevel, 'INFO') - # Using args - args = lc_destroy.parse_args(['-m', 'doomed', '--log', 'DEBUG']) - self.assertEqual(args.loglevel, 'DEBUG') diff --git a/unit_tests/test_zaza_charm_lifecycle_func_test_runner.py b/unit_tests/test_zaza_charm_lifecycle_func_test_runner.py deleted file mode 100644 index 896c3f8..0000000 --- a/unit_tests/test_zaza_charm_lifecycle_func_test_runner.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import mock - -import zaza.charm_lifecycle.func_test_runner as lc_func_test_runner -import unit_tests.utils as ut_utils - - -class TestCharmLifecycleFuncTestRunner(ut_utils.BaseTestCase): - - def test_parser(self): - # Test defaults - args = lc_func_test_runner.parse_args([]) - self.assertFalse(args.keep_model) - self.assertFalse(args.smoke) - self.assertFalse(args.dev) - self.assertIsNone(args.bundle) - # Test flags - args = lc_func_test_runner.parse_args(['--keep-model']) - self.assertTrue(args.keep_model) - args = lc_func_test_runner.parse_args(['--smoke']) - self.assertTrue(args.smoke) - args = lc_func_test_runner.parse_args(['--dev']) - self.assertTrue(args.dev) - args = lc_func_test_runner.parse_args(['--bundle', 'mybundle']) - self.assertEqual(args.bundle, 'mybundle') - args = lc_func_test_runner.parse_args(['--log', 'DEBUG']) - self.assertEqual(args.loglevel, 'DEBUG') - - def test_func_test_runner(self): - self.patch_object(lc_func_test_runner.utils, 'get_charm_config') - self.patch_object(lc_func_test_runner.utils, 'generate_model_name') - self.patch_object(lc_func_test_runner.prepare, 'prepare') - self.patch_object(lc_func_test_runner.deploy, 'deploy') - self.patch_object(lc_func_test_runner.configure, 'configure') - self.patch_object(lc_func_test_runner.test, 'test') - self.patch_object(lc_func_test_runner.destroy, 'destroy') - self.generate_model_name.return_value = 'newmodel' - self.get_charm_config.return_value = { - 'charm_name': 'mycharm', - 'gate_bundles': ['bundle1', 'bundle2'], - 'smoke_bundles': ['bundle2'], - 'dev_bundles': ['bundle3', 'bundle4'], - 'configure': [ - 'zaza.charm_tests.mycharm.setup.basic_setup' - 'zaza.charm_tests.othercharm.setup.setup'], - 'tests': [ - 'zaza.charm_tests.mycharm.tests.SmokeTest', - 'zaza.charm_tests.mycharm.tests.ComplexTest']} - lc_func_test_runner.func_test_runner() - prepare_calls = [ - mock.call('newmodel'), - mock.call('newmodel')] - deploy_calls = [ - mock.call('./tests/bundles/bundle1.yaml', 'newmodel'), - mock.call('./tests/bundles/bundle2.yaml', 'newmodel')] - configure_calls = [ - mock.call('newmodel', [ - 'zaza.charm_tests.mycharm.setup.basic_setup' - 'zaza.charm_tests.othercharm.setup.setup']), - mock.call('newmodel', [ - 'zaza.charm_tests.mycharm.setup.basic_setup' - 'zaza.charm_tests.othercharm.setup.setup'])] - test_calls = [ - mock.call('newmodel', [ - 'zaza.charm_tests.mycharm.tests.SmokeTest', - 'zaza.charm_tests.mycharm.tests.ComplexTest']), - mock.call('newmodel', [ - 'zaza.charm_tests.mycharm.tests.SmokeTest', - 'zaza.charm_tests.mycharm.tests.ComplexTest'])] - destroy_calls = [ - mock.call('newmodel'), - mock.call('newmodel')] - self.prepare.assert_has_calls(prepare_calls) - self.deploy.assert_has_calls(deploy_calls) - self.configure.assert_has_calls(configure_calls) - self.test.assert_has_calls(test_calls) - self.destroy.assert_has_calls(destroy_calls) - - def test_func_test_runner_smoke(self): - self.patch_object(lc_func_test_runner.utils, 'get_charm_config') - self.patch_object(lc_func_test_runner.utils, 'generate_model_name') - self.patch_object(lc_func_test_runner.prepare, 'prepare') - self.patch_object(lc_func_test_runner.deploy, 'deploy') - self.patch_object(lc_func_test_runner.configure, 'configure') - self.patch_object(lc_func_test_runner.test, 'test') - self.patch_object(lc_func_test_runner.destroy, 'destroy') - self.generate_model_name.return_value = 'newmodel' - self.get_charm_config.return_value = { - 'charm_name': 'mycharm', - 'gate_bundles': ['bundle1', 'bundle2'], - 'smoke_bundles': ['bundle2'], - 'dev_bundles': ['bundle3', 'bundle4'], - 'configure': [ - 'zaza.charm_tests.mycharm.setup.basic_setup' - 'zaza.charm_tests.othercharm.setup.setup'], - 'tests': [ - 'zaza.charm_tests.mycharm.tests.SmokeTest', - 'zaza.charm_tests.mycharm.tests.ComplexTest']} - lc_func_test_runner.func_test_runner(smoke=True) - deploy_calls = [ - mock.call('./tests/bundles/bundle2.yaml', 'newmodel')] - self.deploy.assert_has_calls(deploy_calls) - - def test_func_test_runner_dev(self): - self.patch_object(lc_func_test_runner.utils, 'get_charm_config') - self.patch_object(lc_func_test_runner.utils, 'generate_model_name') - self.patch_object(lc_func_test_runner.prepare, 'prepare') - self.patch_object(lc_func_test_runner.deploy, 'deploy') - self.patch_object(lc_func_test_runner.configure, 'configure') - self.patch_object(lc_func_test_runner.test, 'test') - self.patch_object(lc_func_test_runner.destroy, 'destroy') - self.generate_model_name.return_value = 'newmodel' - self.get_charm_config.return_value = { - 'charm_name': 'mycharm', - 'gate_bundles': ['bundle1', 'bundle2'], - 'smoke_bundles': ['bundle2'], - 'dev_bundles': ['bundle3', 'bundle4'], - 'configure': [ - 'zaza.charm_tests.mycharm.setup.basic_setup' - 'zaza.charm_tests.othercharm.setup.setup'], - 'tests': [ - 'zaza.charm_tests.mycharm.tests.SmokeTest', - 'zaza.charm_tests.mycharm.tests.ComplexTest']} - lc_func_test_runner.func_test_runner(dev=True) - deploy_calls = [ - mock.call('./tests/bundles/bundle3.yaml', 'newmodel'), - mock.call('./tests/bundles/bundle4.yaml', 'newmodel')] - self.deploy.assert_has_calls(deploy_calls) - - def test_func_test_runner_specify_bundle(self): - self.patch_object(lc_func_test_runner.utils, 'get_charm_config') - self.patch_object(lc_func_test_runner.utils, 'generate_model_name') - self.patch_object(lc_func_test_runner.prepare, 'prepare') - self.patch_object(lc_func_test_runner.deploy, 'deploy') - self.patch_object(lc_func_test_runner.configure, 'configure') - self.patch_object(lc_func_test_runner.test, 'test') - self.patch_object(lc_func_test_runner.destroy, 'destroy') - self.generate_model_name.return_value = 'newmodel' - self.get_charm_config.return_value = { - 'charm_name': 'mycharm', - 'gate_bundles': ['bundle1', 'bundle2'], - 'smoke_bundles': ['bundle2'], - 'dev_bundles': ['bundle3', 'bundle4'], - 'configure': [ - 'zaza.charm_tests.mycharm.setup.basic_setup' - 'zaza.charm_tests.othercharm.setup.setup'], - 'tests': [ - 'zaza.charm_tests.mycharm.tests.SmokeTest', - 'zaza.charm_tests.mycharm.tests.ComplexTest']} - lc_func_test_runner.func_test_runner(bundle='maveric-filebeat') - deploy_calls = [ - mock.call('./tests/bundles/maveric-filebeat.yaml', 'newmodel')] - self.deploy.assert_has_calls(deploy_calls) - - def test_main_loglevel(self): - self.patch_object(lc_func_test_runner, 'parse_args') - self.patch_object(lc_func_test_runner, 'logging') - self.patch_object(lc_func_test_runner, 'func_test_runner') - self.patch_object(lc_func_test_runner, 'asyncio') - _args = mock.Mock() - _args.loglevel = 'DeBuG' - _args.dev = False - _args.smoke = False - self.parse_args.return_value = _args - self.logging.DEBUG = 10 - lc_func_test_runner.main() - self.logging.basicConfig.assert_called_with(level=10) - - def test_main_loglevel_invalid(self): - self.patch_object(lc_func_test_runner, 'parse_args') - self.patch_object(lc_func_test_runner, 'logging') - self.patch_object(lc_func_test_runner, 'func_test_runner') - self.patch_object(lc_func_test_runner, 'asyncio') - _args = mock.Mock() - _args.loglevel = 'invalid' - self.parse_args.return_value = _args - with self.assertRaises(ValueError) as context: - lc_func_test_runner.main() - self.assertEqual( - 'Invalid log level: "invalid"', - str(context.exception)) - self.assertFalse(self.logging.basicConfig.called) - - def test_main_smoke_dev_ambiguous(self): - self.patch_object(lc_func_test_runner, 'parse_args') - self.patch_object(lc_func_test_runner, 'logging') - self.patch_object(lc_func_test_runner, 'func_test_runner') - self.patch_object(lc_func_test_runner, 'asyncio') - _args = mock.Mock() - _args.loglevel = 'DEBUG' - _args.dev = True - _args.smoke = True - self.parse_args.return_value = _args - self.logging.DEBUG = 10 - with self.assertRaises(ValueError) as context: - lc_func_test_runner.main() - self.assertEqual( - 'Ambiguous arguments: --smoke and --dev cannot be used together', - str(context.exception)) - - def test_main_bundle_dev_ambiguous(self): - self.patch_object(lc_func_test_runner, 'parse_args') - self.patch_object(lc_func_test_runner, 'logging') - self.patch_object(lc_func_test_runner, 'func_test_runner') - self.patch_object(lc_func_test_runner, 'asyncio') - _args = mock.Mock() - _args.loglevel = 'DEBUG' - _args.dev = True - _args.smoke = False - _args.bundle = 'foo.yaml' - self.parse_args.return_value = _args - self.logging.DEBUG = 10 - with self.assertRaises(ValueError) as context: - lc_func_test_runner.main() - self.assertEqual( - ('Ambiguous arguments: --bundle and --dev ' - 'cannot be used together'), - str(context.exception)) - - def test_main_bundle_smoke_ambiguous(self): - self.patch_object(lc_func_test_runner, 'parse_args') - self.patch_object(lc_func_test_runner, 'logging') - self.patch_object(lc_func_test_runner, 'func_test_runner') - self.patch_object(lc_func_test_runner, 'asyncio') - _args = mock.Mock() - _args.loglevel = 'DEBUG' - _args.dev = False - _args.smoke = True - _args.bundle = 'foo.yaml' - self.parse_args.return_value = _args - self.logging.DEBUG = 10 - with self.assertRaises(ValueError) as context: - lc_func_test_runner.main() - self.assertEqual( - ('Ambiguous arguments: --bundle and --smoke ' - 'cannot be used together'), - str(context.exception)) diff --git a/unit_tests/test_zaza_charm_lifecycle_prepare.py b/unit_tests/test_zaza_charm_lifecycle_prepare.py deleted file mode 100644 index 8213828..0000000 --- a/unit_tests/test_zaza_charm_lifecycle_prepare.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import copy -import mock - -import zaza.charm_lifecycle.prepare as lc_prepare -import unit_tests.utils as ut_utils - - -class TestCharmLifecyclePrepare(ut_utils.BaseTestCase): - - MODEL_CONFIG_DEFAULTS = lc_prepare.MODEL_DEFAULTS - - def base_parse_option_list_string(self, env, expect): - with mock.patch.dict(lc_prepare.os.environ, env): - self.assertEqual(lc_prepare.get_model_settings(), expect) - - def test_parse_option_list_string_empty_config(self): - self.assertEqual( - lc_prepare.parse_option_list_string(option_list=""), - {}) - - def test_parse_option_list_string_single_value(self): - self.assertEqual( - lc_prepare.parse_option_list_string( - option_list='image-stream=released'), - {'image-stream': 'released'}) - - def test_parse_option_list_string_multiple_values(self): - self.assertEqual( - lc_prepare.parse_option_list_string( - option_list='image-stream=released;no-proxy=jujucharms.com'), - { - 'image-stream': 'released', - 'no-proxy': 'jujucharms.com'}) - - def test_parse_option_list_string_whitespace(self): - self.assertEqual( - lc_prepare.parse_option_list_string( - option_list=' test-mode= false ; image-stream= released'), - { - 'test-mode': 'false', - 'image-stream': 'released'}) - - def test_get_model_settings_no_config(self): - self.base_parse_option_list_string({}, self.MODEL_CONFIG_DEFAULTS) - - def test_get_model_settings_multiple_values_override(self): - expect_config = copy.deepcopy(self.MODEL_CONFIG_DEFAULTS) - expect_config.update({'test-mode': 'false'}) - self.base_parse_option_list_string( - {'MODEL_SETTINGS': 'test-mode=false'}, - expect_config) - - def test_prepare(self): - self.patch_object(lc_prepare.zaza.controller, 'add_model') - self.patch_object(lc_prepare, 'get_model_settings') - self.patch_object(lc_prepare, 'get_model_constraints') - self.patch_object(lc_prepare.zaza.model, 'set_model_constraints') - self.get_model_settings.return_value = lc_prepare.MODEL_DEFAULTS - self.get_model_constraints.return_value = {'image-stream': 'released'} - lc_prepare.prepare('newmodel') - self.add_model.assert_called_once_with( - 'newmodel', - config={ - 'default-series': 'xenial', - 'image-stream': 'daily', - 'test-mode': 'true', - 'transmit-vendor-metrics': 'false', - 'enable-os-upgrade': 'false', - 'automatically-retry-hooks': 'false', - 'use-default-secgroup': 'true'}) - self.set_model_constraints.assert_called_once_with( - constraints={'image-stream': 'released'}, - model_name='newmodel') - - def test_parser(self): - args = lc_prepare.parse_args([]) - self.assertTrue(args.model_name.startswith('zaza-')) - - def test_parser_model(self): - args = lc_prepare.parse_args(['-m', 'newmodel']) - self.assertEqual(args.model_name, 'newmodel') - - def test_parser_logging(self): - # Using defaults - args = lc_prepare.parse_args(['-m', 'model']) - self.assertEqual(args.loglevel, 'INFO') - # Using args - args = lc_prepare.parse_args(['-m', 'model', '--log', 'DEBUG']) - self.assertEqual(args.loglevel, 'DEBUG') diff --git a/unit_tests/test_zaza_charm_lifecycle_test.py b/unit_tests/test_zaza_charm_lifecycle_test.py deleted file mode 100644 index cf6a8e8..0000000 --- a/unit_tests/test_zaza_charm_lifecycle_test.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import mock - -import zaza.charm_lifecycle.test as lc_test -import unit_tests.utils as ut_utils - - -class TestCharmLifecycleTest(ut_utils.BaseTestCase): - - def test_run_test_list(self): - loader_mock = mock.MagicMock() - runner_mock = mock.MagicMock() - self.patch_object(lc_test.unittest, 'TestLoader') - self.patch_object(lc_test.unittest, 'TextTestRunner') - self.TestLoader.return_value = loader_mock - self.TextTestRunner.return_value = runner_mock - self.patch_object(lc_test.utils, 'get_class') - self.get_class.side_effect = lambda x: x - test_class1_mock = mock.MagicMock() - test_class2_mock = mock.MagicMock() - lc_test.run_test_list([test_class1_mock, test_class2_mock]) - loader_calls = [ - mock.call(test_class1_mock), - mock.call(test_class2_mock)] - loader_mock.loadTestsFromTestCase.assert_has_calls(loader_calls) - - def test_test(self): - self.patch_object(lc_test, 'run_test_list') - lc_test.run_test_list(['test_class1', 'test_class2']) - self.run_test_list.assert_called_once_with( - ['test_class1', 'test_class2']) - - def test_parser(self): - args = lc_test.parse_args( - ['-m', 'modelname', '-t', 'my.test_class1', 'my.test_class2']) - self.assertEqual( - args.tests, - ['my.test_class1', 'my.test_class2']) - self.assertEqual(args.model_name, 'modelname') - - def test_parser_logging(self): - # Using defaults - args = lc_test.parse_args(['-m', 'model']) - self.assertEqual(args.loglevel, 'INFO') - # Using args - args = lc_test.parse_args(['-m', 'model', '--log', 'DEBUG']) - self.assertEqual(args.loglevel, 'DEBUG') diff --git a/unit_tests/test_zaza_charm_lifecycle_utils.py b/unit_tests/test_zaza_charm_lifecycle_utils.py deleted file mode 100644 index 073a60c..0000000 --- a/unit_tests/test_zaza_charm_lifecycle_utils.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import mock - -import zaza.charm_lifecycle.utils as lc_utils -import unit_tests.utils as ut_utils - - -class TestCharmLifecycleUtils(ut_utils.BaseTestCase): - - def test_generate_model_name(self): - self.patch_object(lc_utils.uuid, "uuid4") - self.uuid4.return_value = "longer-than-12characters" - self.assertEqual(lc_utils.generate_model_name(), - "zaza-12characters") - - def test_get_charm_config(self): - self.patch("builtins.open", - new_callable=mock.mock_open(), - name="_open") - self.patch_object(lc_utils, 'yaml') - _yaml = "testconfig: someconfig" - _yaml_dict = {'test_config': 'someconfig'} - self.yaml.safe_load.return_value = _yaml_dict - _filename = "filename" - _fileobj = mock.MagicMock() - _fileobj.__enter__.return_value = _yaml - self._open.return_value = _fileobj - - self.assertEqual(lc_utils.get_charm_config(yaml_file=_filename), - _yaml_dict) - self._open.assert_called_once_with(_filename, "r") - self.yaml.safe_load.assert_called_once_with(_yaml) - - def test_get_class(self): - self.assertEqual( - type(lc_utils.get_class('unit_tests.' - 'test_zaza_charm_lifecycle_utils.' - 'TestCharmLifecycleUtils')()), - type(self)) diff --git a/unit_tests/test_zaza_controller.py b/unit_tests/test_zaza_controller.py deleted file mode 100644 index f68ff22..0000000 --- a/unit_tests/test_zaza_controller.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import mock - -import unit_tests.utils as ut_utils - -import zaza.controller as controller - - -class TestController(ut_utils.BaseTestCase): - - def setUp(self): - super(TestController, self).setUp() - - async def _disconnect(): - return - - async def _connect(): - return - - async def _list_models(): - return self.models - - async def _add_model(model_name, config=None): - return self.model1 - - async def _destroy_model(model_name): - return - - async def _get_cloud(): - return self.cloud - - # Cloud - self.cloud = "FakeCloud" - - # Model - self.Model_mock = mock.MagicMock() - self.Model_mock.connect.side_effect = _connect - self.Model_mock.disconnect.side_effect = _disconnect - self.Model_mock.disconnect.side_effect = _disconnect - self.model1 = self.Model_mock - self.model2 = mock.MagicMock() - self.model1.info.name = "model1" - self.model2.info.name = "model2" - self.models = [self.model1.info.name, self.model2.info.name] - - # Controller - self.Controller_mock = mock.MagicMock() - self.Controller_mock.connect.side_effect = _connect - self.Controller_mock.disconnect.side_effect = _disconnect - self.Controller_mock.add_model.side_effect = _add_model - self.Controller_mock.destroy_model.side_effect = _destroy_model - self.Controller_mock.list_models.side_effect = _list_models - self.Controller_mock.get_cloud.side_effect = _get_cloud - self.controller_name = "testcontroller" - self.Controller_mock.info.name = self.controller_name - self.patch_object(controller, 'Controller') - self.Controller.return_value = self.Controller_mock - - def test_add_model(self): - self.patch_object(controller, 'go_list_models') - controller.add_model(self.model1.info.name) - self.Controller_mock.add_model.assert_called_once_with( - self.model1.info.name, - config=None) - - def test_add_model_config(self): - self.patch_object(controller, 'go_list_models') - controller.add_model(self.model1.info.name, - {'run-faster': 'true'}) - self.Controller_mock.add_model.assert_called_once_with( - self.model1.info.name, - config={'run-faster': 'true'}) - self.go_list_models.assert_called_once() - - def test_destroy_model(self): - controller.destroy_model(self.model1.info.name) - self.Controller_mock.destroy_model.assert_called_once_with( - self.model1.info.name) - - def test_get_cloud(self): - self.assertEqual( - controller.get_cloud(), - self.cloud) - self.Controller_mock.get_cloud.assert_called_once() - - def test_list_models(self): - self.assertEqual( - controller.list_models(), - self.models) - self.Controller_mock.list_models.assert_called_once() - - def test_go_list_models(self): - self.patch_object(controller, 'subprocess') - controller.go_list_models() - self.subprocess.check_call.assert_called_once_with([ - "juju", "models"]) diff --git a/unit_tests/test_zaza_model.py b/unit_tests/test_zaza_model.py deleted file mode 100644 index 85760fd..0000000 --- a/unit_tests/test_zaza_model.py +++ /dev/null @@ -1,1136 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import aiounittest -import asyncio.futures -import concurrent -import mock - -import unit_tests.utils as ut_utils -from juju import loop - -import zaza.model as model - - -class TestModel(ut_utils.BaseTestCase): - - def tearDown(self): - super(TestModel, self).tearDown() - # Clear cached model name - model.CURRENT_MODEL = None - - def setUp(self): - super(TestModel, self).setUp() - - async def _scp_to(source, destination, user=None, proxy=None, - scp_opts=None): - return - - async def _scp_from(source, destination, user=None, proxy=None, - scp_opts=None): - return - - async def _run(command, timeout=None): - return self.action - - async def _run_action(command, **params): - return self.run_action - - async def _wait(): - return - - def _is_leader(leader): - async def _inner_is_leader(): - return leader - return _inner_is_leader - - self.run_action = mock.MagicMock() - self.run_action.wait.side_effect = _wait - self.action = mock.MagicMock() - self.action.data = { - 'model-uuid': '1a035018-71ff-473e-8aab-d1a8d6b6cda7', - 'id': 'e26ffb69-6626-4e93-8840-07f7e041e99d', - 'receiver': 'glance/0', - 'name': 'juju-run', - 'parameters': { - 'command': 'somecommand someargument', 'timeout': 0}, - 'status': 'completed', - 'message': '', - 'results': {'Code': '0', 'Stderr': '', 'Stdout': 'RESULT'}, - 'enqueued': '2018-04-11T23:13:42Z', - 'started': '2018-04-11T23:13:42Z', - 'completed': '2018-04-11T23:13:43Z'} - - self.unit1 = mock.MagicMock() - self.unit1.public_address = 'ip1' - self.unit1.name = 'app/2' - self.unit1.entity_id = 'app/2' - self.unit1.machine = 'machine3' - self.unit2 = mock.MagicMock() - self.unit2.public_address = 'ip2' - self.unit2.name = 'app/4' - self.unit2.entity_id = 'app/4' - self.unit2.machine = 'machine7' - self.unit2.run.side_effect = _run - self.unit1.run.side_effect = _run - self.unit1.scp_to.side_effect = _scp_to - self.unit2.scp_to.side_effect = _scp_to - self.unit1.scp_from.side_effect = _scp_from - self.unit2.scp_from.side_effect = _scp_from - self.unit1.run_action.side_effect = _run_action - self.unit2.run_action.side_effect = _run_action - self.unit1.is_leader_from_status.side_effect = _is_leader(False) - self.unit2.is_leader_from_status.side_effect = _is_leader(True) - self.unit1.data = {'agent-status': {'current': 'idle'}} - self.unit2.data = {'agent-status': {'current': 'idle'}} - self.units = [self.unit1, self.unit2] - self.relation1 = mock.MagicMock() - self.relation1.id = 42 - self.relation1.matches.side_effect = \ - lambda x: True if x == 'app' else False - self.relation2 = mock.MagicMock() - self.relation2.id = 51 - self.relation2.matches.side_effect = \ - lambda x: True if x == 'app:interface' else False - self.relations = [self.relation1, self.relation2] - _units = mock.MagicMock() - _units.units = self.units - _units.relations = self.relations - self.mymodel = mock.MagicMock() - self.mymodel.applications = { - 'app': _units - } - self.Model_mock = mock.MagicMock() - - # Juju Status Object and data - self.key = "instance-id" - self.key_data = "machine-uuid" - self.machine = "1" - self.machine_data = {self.key: self.key_data} - self.unit = "app/1" - self.unit_data = { - "workload-status": {"status": "active"}, - "machine": self.machine} - self.application = "app" - self.application_data = {"units": {self.unit: self.unit_data}} - self.subordinate_application = "subordinate_application" - self.subordinate_application_data = { - "subordinate-to": [self.application]} - self.juju_status = mock.MagicMock() - self.juju_status.applications = { - self.application: self.application_data} - self.juju_status.machines = self.machine_data - - async def _connect_model(model_name): - return model_name - - async def _disconnect(): - return - - async def _connect(): - return - - async def _ctrl_connect(): - return - - async def _ctrl_add_model(model_name, config=None): - return - - async def _ctrl_destroy_models(model_name): - return - - self.Model_mock.connect.side_effect = _connect - self.Model_mock.connect_model.side_effect = _connect_model - self.Model_mock.disconnect.side_effect = _disconnect - self.Model_mock.applications = self.mymodel.applications - self.Model_mock.units = { - 'app/2': self.unit1, - 'app/4': self.unit2} - self.model_name = "testmodel" - self.Model_mock.info.name = self.model_name - - self.Controller_mock = mock.MagicMock() - self.Controller_mock.connect.side_effect = _ctrl_connect - self.Controller_mock.add_model.side_effect = _ctrl_add_model - self.Controller_mock.destroy_models.side_effect = _ctrl_destroy_models - - def test_get_juju_model(self): - self.patch_object(model.os, 'environ') - self.patch_object(model, 'get_current_model') - self.get_current_model.return_value = 'modelsmodel' - - def _get_env(key): - return _env[key] - self.environ.__getitem__.side_effect = _get_env - _env = {"JUJU_MODEL": 'envmodel'} - - # JUJU_ENV environment variable set - self.assertEqual(model.get_juju_model(), 'envmodel') - self.get_current_model.assert_not_called() - - def test_get_juju_model_alt(self): - self.patch_object(model.os, 'environ') - self.patch_object(model, 'get_current_model') - self.get_current_model.return_value = 'modelsmodel' - - def _get_env(key): - return _env[key] - self.environ.__getitem__.side_effect = _get_env - _env = {"MODEL_NAME": 'envmodel'} - - # JUJU_ENV environment variable set - self.assertEqual(model.get_juju_model(), 'envmodel') - self.get_current_model.assert_not_called() - - def test_get_juju_model_noenv(self): - self.patch_object(model.os, 'environ') - self.patch_object(model, 'get_current_model') - self.get_current_model.return_value = 'modelsmodel' - - # No envirnment variable - self.environ.__getitem__.side_effect = KeyError - self.assertEqual(model.get_juju_model(), 'modelsmodel') - self.get_current_model.assert_called_once() - - def test_run_in_model(self): - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - - async def _wrapper(): - async with model.run_in_model('modelname') as mymodel: - return mymodel - self.assertEqual(loop.run(_wrapper()), self.Model_mock) - self.Model_mock.connect_model.assert_called_once_with('modelname') - self.Model_mock.disconnect.assert_called_once_with() - - def test_scp_to_unit(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.patch_object(model, 'get_unit_from_name') - self.get_unit_from_name.return_value = self.unit1 - self.Model.return_value = self.Model_mock - model.scp_to_unit('app/1', '/tmp/src', '/tmp/dest') - self.unit1.scp_to.assert_called_once_with( - '/tmp/src', '/tmp/dest', proxy=False, scp_opts='', user='ubuntu') - - def test_scp_to_all_units(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - model.scp_to_all_units('app', '/tmp/src', '/tmp/dest') - self.unit1.scp_to.assert_called_once_with( - '/tmp/src', '/tmp/dest', proxy=False, scp_opts='', user='ubuntu') - self.unit2.scp_to.assert_called_once_with( - '/tmp/src', '/tmp/dest', proxy=False, scp_opts='', user='ubuntu') - - def test_scp_from_unit(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.patch_object(model, 'get_unit_from_name') - self.get_unit_from_name.return_value = self.unit1 - self.Model.return_value = self.Model_mock - model.scp_from_unit('app/1', '/tmp/src', '/tmp/dest') - self.unit1.scp_from.assert_called_once_with( - '/tmp/src', '/tmp/dest', proxy=False, scp_opts='', user='ubuntu') - - def test_get_units(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.assertEqual( - model.get_units('app'), - self.units) - - def test_get_machines(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.assertEqual( - model.get_machines('app'), - ['machine3', 'machine7']) - - def test_get_first_unit_name(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'get_units') - self.get_units.return_value = self.units - self.assertEqual( - model.get_first_unit_name('model', 'app'), - 'app/2') - - def test_get_lead_unit_name(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'get_units') - self.get_units.return_value = self.units - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.assertEqual( - model.get_lead_unit_name('app', 'model'), - 'app/4') - - def test_get_unit_from_name(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - # Normal case - self.assertEqual( - model.get_unit_from_name('app/4', model_name='mname'), - self.unit2) - - # Normal case with Model() - self.assertEqual( - model.get_unit_from_name('app/4', self.mymodel), - self.unit2) - - # Normal case, using default - self.assertEqual( - model.get_unit_from_name('app/4'), - self.unit2) - - # Unit does not exist - with self.assertRaises(model.UnitNotFound): - model.get_unit_from_name('app/10', model_name='mname') - - # Application does not exist - with self.assertRaises(model.UnitNotFound): - model.get_unit_from_name('bad_name', model_name='mname') - - def test_get_app_ips(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'get_units') - self.get_units.return_value = self.units - self.assertEqual(model.get_app_ips('model', 'app'), ['ip1', 'ip2']) - - def test_run_on_unit(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - expected = {'Code': '0', 'Stderr': '', 'Stdout': 'RESULT'} - self.cmd = cmd = 'somecommand someargument' - self.patch_object(model, 'Model') - self.patch_object(model, 'get_unit_from_name') - self.get_unit_from_name.return_value = self.unit1 - self.Model.return_value = self.Model_mock - self.assertEqual(model.run_on_unit('app/2', cmd), - expected) - self.unit1.run.assert_called_once_with(cmd, timeout=None) - - def test_run_on_leader(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - expected = {'Code': '0', 'Stderr': '', 'Stdout': 'RESULT'} - self.cmd = cmd = 'somecommand someargument' - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.assertEqual(model.run_on_leader('app', cmd), - expected) - self.unit2.run.assert_called_once_with(cmd, timeout=None) - - def test_get_relation_id(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.assertEqual(model.get_relation_id('app', 'app'), 42) - - def test_get_relation_id_interface(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.assertEqual( - model.get_relation_id('app', 'app', - remote_interface_name='interface'), - 51) - - def test_run_action(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.patch_object(model, 'get_unit_from_name') - self.get_unit_from_name.return_value = self.unit1 - self.Model.return_value = self.Model_mock - model.run_action( - 'app/2', - 'backup', - action_params={'backup_dir': '/dev/null'}) - self.unit1.run_action.assert_called_once_with( - 'backup', - backup_dir='/dev/null') - - def test_get_actions(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model.subprocess, 'check_output') - self.check_output.return_value = 'action: "action desc"' - self.assertEqual( - model.get_actions('myapp'), - {'action': "action desc"}) - self.check_output.assert_called_once_with( - ['juju', 'actions', '-m', 'mname', 'myapp', '--format', 'yaml']) - - def test_run_action_on_leader(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - model.run_action_on_leader( - 'app', - 'backup', - action_params={'backup_dir': '/dev/null'}) - self.assertFalse(self.unit1.called) - self.unit2.run_action.assert_called_once_with( - 'backup', - backup_dir='/dev/null') - - def _application_states_setup(self, setup, units_idle=True): - self.system_ready = True - self._block_until_calls = 0 - - async def _block_until(f, timeout=0): - # Mimic timeouts - timeout = timeout + self._block_until_calls - self._block_until_calls += 1 - if timeout == -1: - raise concurrent.futures._base.TimeoutError("Timeout", 1) - result = f() - if not result: - self.system_ready = False - return - - async def _all_units_idle(): - return units_idle - self.Model_mock.block_until.side_effect = _block_until - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.Model_mock.all_units_idle.return_value = _all_units_idle - p_mock_ws = mock.PropertyMock( - return_value=setup['workload-status']) - p_mock_wsmsg = mock.PropertyMock( - return_value=setup['workload-status-message']) - type(self.unit1).workload_status = p_mock_ws - type(self.unit1).workload_status_message = p_mock_wsmsg - type(self.unit2).workload_status = p_mock_ws - type(self.unit2).workload_status_message = p_mock_wsmsg - - def test_units_with_wl_status_state(self): - self._application_states_setup({ - 'workload-status': 'active', - 'workload-status-message': 'Unit is ready'}) - units = model.units_with_wl_status_state(self.Model_mock, 'active') - self.assertTrue(len(units) == 2) - self.assertIn(self.unit1, units) - self.assertIn(self.unit2, units) - - def test_units_with_wl_status_state_no_match(self): - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Unit is ready'}) - units = model.units_with_wl_status_state(self.Model_mock, 'active') - self.assertTrue(len(units) == 0) - - def test_check_model_for_hard_errors(self): - self.patch_object(model, 'units_with_wl_status_state') - self.units_with_wl_status_state.return_value = [] - # Test will fail if an Exception is raised - model.check_model_for_hard_errors(self.Model_mock) - - def test_check_model_for_hard_errors_found(self): - self.patch_object(model, 'units_with_wl_status_state') - self.units_with_wl_status_state.return_value = [self.unit1] - with self.assertRaises(model.UnitError): - model.check_model_for_hard_errors(self.Model_mock) - - def test_check_unit_workload_status(self): - self.patch_object(model, 'check_model_for_hard_errors') - self._application_states_setup({ - 'workload-status': 'active', - 'workload-status-message': 'Unit is ready'}) - self.assertTrue( - model.check_unit_workload_status(self.Model_mock, - self.unit1, ['active'])) - - def test_check_unit_workload_status_no_match(self): - self.patch_object(model, 'check_model_for_hard_errors') - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Unit is ready'}) - self.assertFalse( - model.check_unit_workload_status(self.Model_mock, - self.unit1, ['active'])) - - def test_check_unit_workload_status_multi(self): - self.patch_object(model, 'check_model_for_hard_errors') - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Unit is ready'}) - self.assertTrue( - model.check_unit_workload_status( - self.Model_mock, - self.unit1, ['active', 'blocked'])) - - def test_check_unit_workload_status_message_message(self): - self.patch_object(model, 'check_model_for_hard_errors') - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Unit is ready'}) - self.assertTrue( - model.check_unit_workload_status_message(self.Model_mock, - self.unit1, - message='Unit is ready')) - - def test_check_unit_workload_status_message_message_not_found(self): - self.patch_object(model, 'check_model_for_hard_errors') - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Something else'}) - self.assertFalse( - model.check_unit_workload_status_message(self.Model_mock, - self.unit1, - message='Unit is ready')) - - def test_check_unit_workload_status_message_prefix(self): - self.patch_object(model, 'check_model_for_hard_errors') - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Unit is ready (OSD Count 23)'}) - self.assertTrue( - model.check_unit_workload_status_message( - self.Model_mock, - self.unit1, - prefixes=['Readyish', 'Unit is ready'])) - - def test_check_unit_workload_status_message_prefix_no_match(self): - self.patch_object(model, 'check_model_for_hard_errors') - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'On my holidays'}) - self.assertFalse( - model.check_unit_workload_status_message( - self.Model_mock, - self.unit1, - prefixes=['Readyish', 'Unit is ready'])) - - def test_wait_for_application_states(self): - self._application_states_setup({ - 'workload-status': 'active', - 'workload-status-message': 'Unit is ready'}) - model.wait_for_application_states('modelname', timeout=1) - self.assertTrue(self.system_ready) - - def test_wait_for_application_states_not_ready_ws(self): - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Unit is ready'}) - model.wait_for_application_states('modelname', timeout=1) - self.assertFalse(self.system_ready) - - def test_wait_for_application_states_errored_unit(self): - self._application_states_setup({ - 'workload-status': 'error', - 'workload-status-message': 'Unit is ready'}) - with self.assertRaises(model.UnitError): - model.wait_for_application_states('modelname', timeout=1) - self.assertFalse(self.system_ready) - - def test_wait_for_application_states_not_ready_wsmsg(self): - self._application_states_setup({ - 'workload-status': 'active', - 'workload-status-message': 'Unit is not ready'}) - model.wait_for_application_states('modelname', timeout=1) - self.assertFalse(self.system_ready) - - def test_wait_for_application_states_blocked_ok(self): - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Unit is ready'}) - model.wait_for_application_states( - 'modelname', - states={'app': { - 'workload-status': 'blocked'}}, - timeout=1) - self.assertTrue(self.system_ready) - - def test_wait_for_application_states_bespoke_msg(self): - self._application_states_setup({ - 'workload-status': 'active', - 'workload-status-message': 'Sure, I could do something'}) - model.wait_for_application_states( - 'modelname', - states={'app': { - 'workload-status-message': 'Sure, I could do something'}}, - timeout=1) - self.assertTrue(self.system_ready) - - def test_wait_for_application_states_bespoke_msg_blocked_ok(self): - self._application_states_setup({ - 'workload-status': 'blocked', - 'workload-status-message': 'Sure, I could do something'}) - model.wait_for_application_states( - 'modelname', - states={'app': { - 'workload-status': 'blocked', - 'workload-status-message': 'Sure, I could do something'}}, - timeout=1) - self.assertTrue(self.system_ready) - - def test_wait_for_application_states_idle_timeout(self): - self._application_states_setup({ - 'agent-status': 'executing', - 'workload-status': 'blocked', - 'workload-status-message': 'Sure, I could do something'}) - with self.assertRaises(model.ModelTimeout) as timeout: - model.wait_for_application_states('modelname', timeout=-2) - self.assertEqual( - timeout.exception.args[0], - "Zaza has timed out waiting on the model to reach idle state.") - - def test_wait_for_application_states_timeout(self): - self._application_states_setup({ - 'agent-status': 'executing', - 'workload-status': 'blocked', - 'workload-status-message': 'Sure, I could do something'}) - with self.assertRaises(model.ModelTimeout) as timeout: - model.wait_for_application_states('modelname', timeout=-3) - self.assertEqual( - timeout.exception.args[0], - "Zaza has timed out waiting on the model to reach expected " - "workload statuses.") - - def test_get_current_model(self): - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.assertEqual(model.get_current_model(), self.model_name) - - def test_block_until_file_has_contents(self): - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch("builtins.open", - new_callable=mock.mock_open(), - name="_open") - _fileobj = mock.MagicMock() - _fileobj.__enter__().read.return_value = "somestring" - self._open.return_value = _fileobj - model.block_until_file_has_contents( - 'app', - '/tmp/src/myfile.txt', - 'somestring', - timeout=0.1) - self.unit1.scp_from.assert_called_once_with( - '/tmp/src/myfile.txt', mock.ANY) - self.unit2.scp_from.assert_called_once_with( - '/tmp/src/myfile.txt', mock.ANY) - - def test_block_until_file_has_contents_missing(self): - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch("builtins.open", - new_callable=mock.mock_open(), - name="_open") - _fileobj = mock.MagicMock() - _fileobj.__enter__().read.return_value = "anything else" - self._open.return_value = _fileobj - with self.assertRaises(asyncio.futures.TimeoutError): - model.block_until_file_has_contents( - 'app', - '/tmp/src/myfile.txt', - 'somestring', - timeout=0.1) - self.unit1.scp_from.assert_called_once_with( - '/tmp/src/myfile.txt', mock.ANY) - - def test_async_block_until_all_units_idle(self): - - async def _block_until(f, timeout=None): - if not f(): - raise asyncio.futures.TimeoutError - - def _all_units_idle(): - return True - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.Model_mock.all_units_idle.side_effect = _all_units_idle - self.Model_mock.block_until.side_effect = _block_until - # Check exception is not raised: - model.block_until_all_units_idle('modelname') - - def test_async_block_until_all_units_idle_false(self): - - async def _block_until(f, timeout=None): - if not f(): - raise asyncio.futures.TimeoutError - - def _all_units_idle(): - return False - self.Model_mock.all_units_idle.side_effect = _all_units_idle - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.Model_mock.block_until.side_effect = _block_until - # Confirm exception is raised: - with self.assertRaises(asyncio.futures.TimeoutError): - model.block_until_all_units_idle('modelname') - - def test_async_block_until_all_units_idle_errored_unit(self): - - async def _block_until(f, timeout=None): - if not f(): - raise asyncio.futures.TimeoutError - - def _all_units_idle(): - return True - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.Model_mock.all_units_idle.side_effect = _all_units_idle - self.patch_object(model, 'units_with_wl_status_state') - unit = mock.MagicMock() - unit.entity_id = 'aerroredunit/0' - self.units_with_wl_status_state.return_value = [unit] - self.Model_mock.block_until.side_effect = _block_until - with self.assertRaises(model.UnitError): - model.block_until_all_units_idle('modelname') - - def block_until_service_status_base(self, rou_return): - - async def _block_until(f, timeout=None): - rc = await f() - if not rc: - raise asyncio.futures.TimeoutError - - async def _run_on_unit(unit_name, cmd, model_name=None, timeout=None): - return rou_return - self.patch_object(model, 'async_run_on_unit') - self.async_run_on_unit.side_effect = _run_on_unit - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.patch_object(model, 'async_block_until') - self.async_block_until.side_effect = _block_until - - def test_block_until_service_status_check_running(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.block_until_service_status_base({'Stdout': '152 409 54'}) - model.block_until_service_status( - 'app/2', - ['test_svc'], - 'running') - - def test_block_until_service_status_check_running_fail(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.block_until_service_status_base({'Stdout': ''}) - with self.assertRaises(asyncio.futures.TimeoutError): - model.block_until_service_status( - 'app/2', - ['test_svc'], - 'running') - - def test_block_until_service_status_check_stopped(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.block_until_service_status_base({'Stdout': ''}) - model.block_until_service_status( - 'app/2', - ['test_svc'], - 'stopped') - - def test_block_until_service_status_check_stopped_fail(self): - self.patch_object(model, 'get_juju_model', return_value='mname') - self.block_until_service_status_base({'Stdout': '152 409 54'}) - with self.assertRaises(asyncio.futures.TimeoutError): - model.block_until_service_status( - 'app/2', - ['test_svc'], - 'stopped') - - def test_get_unit_time(self): - async def _run_on_unit( - unit_name, - command, - model_name=None, - timeout=None - ): - return {'Stdout': '1524409654'} - self.patch_object(model, 'async_run_on_unit') - self.async_run_on_unit.side_effect = _run_on_unit - self.assertEqual( - model.get_unit_time('app/2'), - 1524409654) - self.async_run_on_unit.assert_called_once_with( - unit_name='app/2', - command="date +'%s'", - model_name=None, - timeout=None - ) - - def test_get_unit_service_start_time(self): - async def _run_on_unit( - unit_name, - command, - model_name=None, - timeout=None - ): - return {'Stdout': '1524409654'} - self.patch_object(model, 'async_run_on_unit') - self.async_run_on_unit.side_effect = _run_on_unit - self.assertEqual( - model.get_unit_service_start_time('app/2', 'mysvc1'), 1524409654) - cmd = "stat -c %Y /proc/$(pidof -x mysvc1 | cut -f1 -d ' ')" - self.async_run_on_unit.assert_called_once_with( - unit_name='app/2', - command=cmd, - model_name=None, - timeout=None - ) - - def test_get_unit_service_start_time_not_running(self): - async def _run_on_unit( - unit_name, - command, - model_name=None, - timeout=None - ): - return {'Stdout': ''} - self.patch_object(model, 'async_run_on_unit') - self.async_run_on_unit.side_effect = _run_on_unit - with self.assertRaises(model.ServiceNotRunning): - model.get_unit_service_start_time('app/2', 'mysvc1') - - def block_until_oslo_config_entries_match_base(self, file_contents, - expected_contents): - async def _scp_from(remote_file, tmpdir): - with open('{}/myfile.txt'.format(tmpdir), 'w') as f: - f.write(file_contents) - self.patch_object(model, 'Model') - self.patch_object(model, 'get_juju_model', return_value='mname') - self.Model.return_value = self.Model_mock - self.unit1.scp_from.side_effect = _scp_from - self.unit2.scp_from.side_effect = _scp_from - model.block_until_oslo_config_entries_match( - 'app', - '/tmp/src/myfile.txt', - expected_contents, - timeout=0.1) - - def test_block_until_oslo_config_entries_match(self): - file_contents = """ -[DEFAULT] -verbose = False -use_syslog = False -debug = False -workers = 4 -bind_host = 0.0.0.0 - -[glance_store] -filesystem_store_datadir = /var/lib/glance/images/ -stores = glance.store.filesystem.Store,glance.store.http.Store -default_store = file - -[image_format] -disk_formats = ami,ari,aki,vhd,vmdk,raw,qcow2,vdi,iso,root-tar -""" - expected_contents = { - 'DEFAULT': { - 'debug': ['False']}, - 'glance_store': { - 'filesystem_store_datadir': ['/var/lib/glance/images/'], - 'default_store': ['file']}} - self.block_until_oslo_config_entries_match_base( - file_contents, - expected_contents) - self.unit1.scp_from.assert_called_once_with( - '/tmp/src/myfile.txt', mock.ANY) - self.unit2.scp_from.assert_called_once_with( - '/tmp/src/myfile.txt', mock.ANY) - - def test_block_until_oslo_config_entries_match_fail(self): - file_contents = """ -[DEFAULT] -verbose = False -use_syslog = False -debug = True -workers = 4 -bind_host = 0.0.0.0 - -[glance_store] -filesystem_store_datadir = /var/lib/glance/images/ -stores = glance.store.filesystem.Store,glance.store.http.Store -default_store = file - -[image_format] -disk_formats = ami,ari,aki,vhd,vmdk,raw,qcow2,vdi,iso,root-tar -""" - expected_contents = { - 'DEFAULT': { - 'debug': ['False']}, - 'glance_store': { - 'filesystem_store_datadir': ['/var/lib/glance/images/'], - 'default_store': ['file']}} - with self.assertRaises(asyncio.futures.TimeoutError): - self.block_until_oslo_config_entries_match_base( - file_contents, - expected_contents) - self.unit1.scp_from.assert_called_once_with( - '/tmp/src/myfile.txt', mock.ANY) - - def test_block_until_oslo_config_entries_match_missing_entry(self): - file_contents = """ -[DEFAULT] -verbose = False -use_syslog = False -workers = 4 -bind_host = 0.0.0.0 - -[glance_store] -filesystem_store_datadir = /var/lib/glance/images/ -stores = glance.store.filesystem.Store,glance.store.http.Store -default_store = file - -[image_format] -disk_formats = ami,ari,aki,vhd,vmdk,raw,qcow2,vdi,iso,root-tar -""" - expected_contents = { - 'DEFAULT': { - 'debug': ['False']}, - 'glance_store': { - 'filesystem_store_datadir': ['/var/lib/glance/images/'], - 'default_store': ['file']}} - with self.assertRaises(asyncio.futures.TimeoutError): - self.block_until_oslo_config_entries_match_base( - file_contents, - expected_contents) - self.unit1.scp_from.assert_called_once_with( - '/tmp/src/myfile.txt', mock.ANY) - - def test_block_until_oslo_config_entries_match_missing_section(self): - file_contents = """ -[DEFAULT] -verbose = False -use_syslog = False -workers = 4 -bind_host = 0.0.0.0 - -[image_format] -disk_formats = ami,ari,aki,vhd,vmdk,raw,qcow2,vdi,iso,root-tar -""" - expected_contents = { - 'DEFAULT': { - 'debug': ['False']}, - 'glance_store': { - 'filesystem_store_datadir': ['/var/lib/glance/images/'], - 'default_store': ['file']}} - with self.assertRaises(asyncio.futures.TimeoutError): - self.block_until_oslo_config_entries_match_base( - file_contents, - expected_contents) - self.unit1.scp_from.assert_called_once_with( - '/tmp/src/myfile.txt', mock.ANY) - - def block_until_services_restarted_base(self, gu_return=None, - gu_raise_exception=False): - async def _block_until(f, timeout=None): - rc = await f() - if not rc: - raise asyncio.futures.TimeoutError - self.patch_object(model, 'async_block_until') - self.async_block_until.side_effect = _block_until - - async def _async_get_unit_service_start_time(model_name, unit, svc): - if gu_raise_exception: - raise model.ServiceNotRunning('sv1') - else: - return gu_return - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'async_get_unit_service_start_time') - self.async_get_unit_service_start_time.side_effect = \ - _async_get_unit_service_start_time - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - - def test_block_until_services_restarted(self): - self.block_until_services_restarted_base(gu_return=10) - model.block_until_services_restarted( - 'app', - 8, - ['svc1', 'svc2']) - - def test_block_until_services_restarted_fail(self): - self.block_until_services_restarted_base(gu_return=10) - with self.assertRaises(asyncio.futures.TimeoutError): - model.block_until_services_restarted( - 'app', - 12, - ['svc1', 'svc2']) - - def test_block_until_services_restarted_not_running(self): - self.block_until_services_restarted_base(gu_raise_exception=True) - with self.assertRaises(asyncio.futures.TimeoutError): - model.block_until_services_restarted( - 'app', - 12, - ['svc1', 'svc2']) - - def test_block_until_unit_wl_status(self): - async def _block_until(f, timeout=None): - rc = await f() - if not rc: - raise asyncio.futures.TimeoutError - - async def _get_status(): - return self.juju_status - - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'get_unit_from_name') - self.patch_object(model, 'async_get_status') - self.async_get_status.side_effect = _get_status - self.patch_object(model, 'async_block_until') - self.async_block_until.side_effect = _block_until - model.block_until_unit_wl_status( - 'app/1', - 'active', - timeout=0.1) - - def test_block_until_unit_wl_status_fail(self): - async def _block_until(f, timeout=None): - rc = await f() - if not rc: - raise asyncio.futures.TimeoutError - - async def _get_status(): - return self.juju_status - - (self.juju_status.applications[self.application] - ["units"][self.unit]["workload-status"]["status"]) = "blocked" - - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'get_unit_from_name') - self.patch_object(model, 'async_get_status') - self.async_get_status.side_effect = _get_status - self.patch_object(model, 'async_block_until') - self.async_block_until.side_effect = _block_until - with self.assertRaises(asyncio.futures.TimeoutError): - model.block_until_unit_wl_status( - 'app/1', - 'active', - timeout=0.1) - - def test_wait_for_agent_status(self): - async def _block_until(f, timeout=None): - if not f(): - raise asyncio.futures.TimeoutError - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.unit1.data = {'agent-status': {'current': 'idle'}} - self.unit2.data = {'agent-status': {'current': 'executing'}} - self.Model.return_value = self.Model_mock - self.Model_mock.block_until.side_effect = _block_until - model.wait_for_agent_status(timeout=0.1) - - def test_wait_for_agent_status_timeout(self): - async def _block_until(f, timeout=None): - if not f(): - raise asyncio.futures.TimeoutError - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.Model.return_value = self.Model_mock - self.Model_mock.block_until.side_effect = _block_until - with self.assertRaises(asyncio.futures.TimeoutError): - model.wait_for_agent_status(timeout=0.1) - - def test_upgrade_charm(self): - async def _upgrade_charm(channel=None, force_series=False, - force_units=False, path=None, - resources=None, revision=None, - switch=None, model_name=None): - return - self.patch_object(model, 'get_juju_model', return_value='mname') - self.patch_object(model, 'Model') - self.patch_object(model, 'get_unit_from_name') - self.get_unit_from_name.return_value = self.unit1 - self.Model.return_value = self.Model_mock - app_mock = mock.MagicMock() - app_mock.upgrade_charm.side_effect = _upgrade_charm - self.mymodel.applications['myapp'] = app_mock - model.upgrade_charm( - 'myapp', - switch='cs:~me/new-charm-45') - app_mock.upgrade_charm.assert_called_once_with( - channel=None, - force_series=False, - force_units=False, - path=None, - resources=None, - revision=None, - switch='cs:~me/new-charm-45') - - def test_prepare_series_upgrade(self): - self.patch_object(model, 'subprocess') - self.patch_object(model, 'get_juju_model', - return_value=self.model_name) - _machine_num = "1" - _to_series = "bionic" - model.prepare_series_upgrade(_machine_num, to_series=_to_series) - self.subprocess.check_call.assert_called_once_with( - ["juju", "upgrade-series", "-m", self.model_name, - _machine_num, "prepare", _to_series, "--yes"]) - - def test_complete_series_upgrade(self): - self.patch_object(model, 'get_juju_model', - return_value=self.model_name) - self.patch_object(model, 'subprocess') - _machine_num = "1" - model.complete_series_upgrade(_machine_num) - self.subprocess.check_call.assert_called_once_with( - ["juju", "upgrade-series", "-m", self.model_name, - _machine_num, "complete"]) - - def test_set_series(self): - self.patch_object(model, 'get_juju_model', - return_value=self.model_name) - self.patch_object(model, 'subprocess') - _application = "application" - _to_series = "bionic" - model.set_series(_application, _to_series) - self.subprocess.check_call.assert_called_once_with( - ["juju", "set-series", "-m", self.model_name, - _application, _to_series]) - - def test_attach_resource(self): - self.patch_object(model, 'get_juju_model', - return_value=self.model_name) - self.patch_object(model, 'subprocess') - _application = "application" - _resource_name = "myresource" - _resource_path = "/path/to/{}.tar.gz".format(_resource_name) - model.attach_resource(_application, _resource_name, _resource_path) - self.subprocess.check_call.assert_called_once_with( - ["juju", "attach-resource", "-m", self.model_name, - _application, "{}={}".format(_resource_name, _resource_path)]) - - -class AsyncModelTests(aiounittest.AsyncTestCase): - - async def test_async_block_until_timeout(self): - - async def _f(): - return False - - async def _g(): - return True - - with self.assertRaises(asyncio.futures.TimeoutError): - await model.async_block_until(_f, _g, timeout=0.1) - - async def test_async_block_until_pass(self): - - async def _f(): - return True - - async def _g(): - return True - - await model.async_block_until(_f, _g, timeout=0.1) diff --git a/zaza/charm_lifecycle/__init__.py b/zaza/charm_lifecycle/__init__.py deleted file mode 100644 index 58d1c15..0000000 --- a/zaza/charm_lifecycle/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Modules for running lifecycle phases.""" diff --git a/zaza/charm_lifecycle/collect.py b/zaza/charm_lifecycle/collect.py deleted file mode 100644 index ebfdba9..0000000 --- a/zaza/charm_lifecycle/collect.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Run collect phase.""" diff --git a/zaza/charm_lifecycle/configure.py b/zaza/charm_lifecycle/configure.py deleted file mode 100644 index 14c962f..0000000 --- a/zaza/charm_lifecycle/configure.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Run configuration phase.""" -import asyncio -import argparse -import logging -import sys - -import zaza.model -import zaza.charm_lifecycle.utils as utils - - -def run_configure_list(functions): - """Run the configure scripts. - - Run the configure scripts as defined in the list of test classes in - series. - - :param functions: List of configure functions functions - :type tests: ['zaza.charms_tests.svc.setup', ...] - """ - for func in functions: - utils.get_class(func)() - - -def configure(model_name, functions): - """Run all post-deployment configuration steps. - - :param functions: List of configure functions functions - :type tests: ['zaza.charms_tests.svc.setup', ...] - """ - zaza.model.set_juju_model(model_name) - run_configure_list(functions) - - -def parse_args(args): - """Parse command line arguments. - - :param args: List of configure functions functions - :type list: [str1, str2,...] List of command line arguments - :returns: Parsed arguments - :rtype: Namespace - """ - parser = argparse.ArgumentParser() - parser.add_argument('-c', '--configfuncs', nargs='+', - help='Space separated list of config functions', - required=False) - parser.add_argument('-m', '--model-name', help='Name of model to remove', - required=True) - parser.add_argument('--log', dest='loglevel', - help='Loglevel [DEBUG|INFO|WARN|ERROR|CRITICAL]') - parser.set_defaults(loglevel='INFO') - return parser.parse_args(args) - - -def main(): - """Run the configuration defined by the command line args. - - Run the configuration defined by the command line args or if none were - provided read the configuration functions from the charms tests.yaml - config file - """ - args = parse_args(sys.argv[1:]) - level = getattr(logging, args.loglevel.upper(), None) - if not isinstance(level, int): - raise ValueError('Invalid log level: "{}"'.format(args.loglevel)) - logging.basicConfig(level=level) - funcs = args.configfuncs or utils.get_charm_config()['configure'] - configure(args.model_name, funcs) - asyncio.get_event_loop().close() diff --git a/zaza/charm_lifecycle/deploy.py b/zaza/charm_lifecycle/deploy.py deleted file mode 100755 index 8749251..0000000 --- a/zaza/charm_lifecycle/deploy.py +++ /dev/null @@ -1,304 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Run deploy phase.""" -import argparse -import jinja2 -import logging -import os -import subprocess -import sys -import tempfile - -import zaza.model -import zaza.charm_lifecycle.utils as utils - -DEFAULT_OVERLAY_TEMPLATE_DIR = 'tests/bundles/overlays' -VALID_ENVIRONMENT_KEY_PREFIXES = [ - 'FIP_RANGE', - 'GATEWAY', - 'NAME_SERVER', - 'NET_ID', - 'OS_', - 'VIP_RANGE', - 'AMULET_', - 'MOJO_', -] -LOCAL_OVERLAY_TEMPLATE = """ -applications: - {{ charm_name }}: - charm: {{ charm_location }} -""" -LOCAL_OVERLAY_TEMPLATE_NAME = 'local-charm-overlay.yaml' - - -def is_valid_env_key(key): - """Check if key is a valid environment variable name for use with template. - - :param key: List of configure functions functions - :type key: str - :returns: Whether key is a valid environment variable name - :rtype: bool - """ - valid = False - for _k in VALID_ENVIRONMENT_KEY_PREFIXES: - if key.startswith(_k): - valid = True - break - return valid - - -def get_template_context_from_env(): - """Return environment vars from the current env for template rendering. - - :returns: Environment variable key values for use with template rendering - :rtype: dict - """ - return {k: v for k, v in os.environ.items() if is_valid_env_key(k)} - - -def get_charm_config_context(): - """Return settings from charm config file. - - :returns: Context for template rendering - :rtype: dict - """ - test_config = utils.get_charm_config() - ctxt = { - 'charm_name': test_config['charm_name'], - 'charm_location': '../../../{}'.format(test_config['charm_name'])} - return ctxt - - -def get_template_overlay_context(): - """Combine contexts which can be used for overlay template rendering. - - :returns: Context for template rendering - :rtype: dict - """ - context = {} - contexts = [ - get_template_context_from_env(), - ] - try: - contexts.append(get_charm_config_context()) - except KeyError: - pass - - for c in contexts: - context.update(c) - return context - - -def get_overlay_template_dir(): - """Return the directory to look for overlay template files in. - - :returns: Overlay template file dir - :rtype: str - """ - return DEFAULT_OVERLAY_TEMPLATE_DIR - - -def get_jinja2_env(): - """Return a jinja2 environment that can be used to render templates from. - - :returns: Jinja2 template loader - :rtype: jinja2.Environment - """ - template_dir = get_overlay_template_dir() - return jinja2.Environment( - loader=jinja2.FileSystemLoader(template_dir), - undefined=jinja2.StrictUndefined - ) - - -def get_template_name(target_file): - """Return the template name for target_file. - - Return the expected name of the template used to generate the - target_file - - :param target_file: File to be rendered - :type target_file: str - :returns: Name of template used to render target_file - :rtype: str - """ - return '{}.j2'.format(os.path.basename(target_file)) - - -def get_template(target_file): - """Return the jinja2 template for the given file. - - :returns: Template object used to generate target_file - :rtype: jinja2.Template - """ - jinja2_env = get_jinja2_env() - try: - template = jinja2_env.get_template(get_template_name(target_file)) - except jinja2.exceptions.TemplateNotFound: - template = None - return template - - -def render_template(template, target_file): - """Render the template to the file supplied. - - :param template: Template to be rendered - :type template: jinja2.Template - :param target_file: File name for rendered template - :type target_file: str - """ - try: - with open(target_file, "w") as fh: - fh.write( - template.render(get_template_overlay_context())) - except jinja2.exceptions.UndefinedError as e: - logging.error("Template error. You may be missing" - " a mandatory environment variable : {}".format(e)) - sys.exit(1) - logging.info("Rendered template '{}' to file '{}'".format(template, - target_file)) - - -def render_overlay(overlay_name, target_dir): - """Render the overlay template in the directory supplied. - - :param overlay_name: Name of overlay to be rendered - :type overlay_name: str - :param target_dir: Directory to render overlay in - :type overlay_name: str - :returns: Path to rendered overlay - :rtype: str - """ - template = get_template(overlay_name) - if not template: - return - rendered_template_file = os.path.join( - target_dir, - os.path.basename(overlay_name)) - render_template(template, rendered_template_file) - return rendered_template_file - - -def render_local_overlay(target_dir): - """Render the local overlay template in the directory supplied. - - :param target_dir: Directory to render overlay in - :type overlay_name: str - :returns: Path to rendered overlay - :rtype: str - """ - template = get_template(LOCAL_OVERLAY_TEMPLATE_NAME) - if not template: - template = jinja2.Environment(loader=jinja2.BaseLoader).from_string( - LOCAL_OVERLAY_TEMPLATE) - rendered_template_file = os.path.join( - target_dir, - os.path.basename(LOCAL_OVERLAY_TEMPLATE_NAME)) - if utils.get_charm_config().get('charm_name', None): - render_template(template, rendered_template_file) - return rendered_template_file - - -def render_overlays(bundle, target_dir): - """Render the overlays for the given bundle in the directory provided. - - :param bundle: Name of bundle being deployed - :type bundle: str - :param target_dir: Directory to render overlay in - :type overlay_name: str - :returns: List of rendered overlays - :rtype: [str, str,...] - """ - overlays = [] - local_overlay = render_local_overlay(target_dir) - if local_overlay: - overlays.append(local_overlay) - rendered_bundle_overlay = render_overlay(bundle, target_dir) - if rendered_bundle_overlay: - overlays.append(rendered_bundle_overlay) - return overlays - - -def deploy_bundle(bundle, model): - """Deploy the given bundle file in the specified model. - - :param bundle: Path to bundle file - :type bundle: str - :param model: Name of model to deploy bundle in - :type model: str - """ - logging.info("Deploying bundle '{}' on to '{}' model" - .format(bundle, model)) - cmd = ['juju', 'deploy', '-m', model, bundle] - with tempfile.TemporaryDirectory() as tmpdirname: - for overlay in render_overlays(bundle, tmpdirname): - logging.info("Deploying overlay '{}' on to '{}' model" - .format(overlay, model)) - cmd.extend(['--overlay', overlay]) - subprocess.check_call(cmd) - - -def deploy(bundle, model, wait=True): - """Run all steps to complete deployment. - - :param bundle: Path to bundle file - :type bundle: str - :param model: Name of model to deploy bundle in - :type model: str - :param wait: Whether to wait until deployment completes - :type model: bool - """ - deploy_bundle(bundle, model) - if wait: - test_config = utils.get_charm_config() - logging.info("Waiting for environment to settle") - zaza.model.set_juju_model(model) - zaza.model.wait_for_application_states( - model, - test_config.get('target_deploy_status', {})) - - -def parse_args(args): - """Parse command line arguments. - - :param args: List of configure functions functions - :type list: [str1, str2,...] List of command line arguments - :returns: Parsed arguments - :rtype: Namespace - """ - parser = argparse.ArgumentParser() - parser.add_argument('-m', '--model', - help='Model to deploy to', - required=True) - parser.add_argument('-b', '--bundle', - help='Bundle name (excluding file ext)', - required=True) - parser.add_argument('--no-wait', dest='wait', - help='Do not wait for deployment to settle', - action='store_false') - parser.add_argument('--log', dest='loglevel', - help='Loglevel [DEBUG|INFO|WARN|ERROR|CRITICAL]') - parser.set_defaults(wait=True, loglevel='INFO') - return parser.parse_args(args) - - -def main(): - """Deploy bundle.""" - args = parse_args(sys.argv[1:]) - level = getattr(logging, args.loglevel.upper(), None) - if not isinstance(level, int): - raise ValueError('Invalid log level: "{}"'.format(args.loglevel)) - logging.basicConfig(level=level) - deploy(args.bundle, args.model, wait=args.wait) diff --git a/zaza/charm_lifecycle/destroy.py b/zaza/charm_lifecycle/destroy.py deleted file mode 100644 index acc1be4..0000000 --- a/zaza/charm_lifecycle/destroy.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Run destroy phase.""" -import argparse -import logging -import sys - -import zaza.controller - - -def destroy(model_name): - """Run all steps to cleaup after a test run. - - :param model: Name of model to remove - :type bundle: str - """ - zaza.controller.destroy_model(model_name) - - -def parse_args(args): - """Parse command line arguments. - - :param args: List of configure functions functions - :type list: [str1, str2,...] List of command line arguments - :returns: Parsed arguments - :rtype: Namespace - """ - parser = argparse.ArgumentParser() - parser.add_argument('-m', '--model-name', help='Name of model to remove', - required=True) - parser.add_argument('--log', dest='loglevel', - help='Loglevel [DEBUG|INFO|WARN|ERROR|CRITICAL]') - parser.set_defaults(loglevel='INFO') - return parser.parse_args(args) - - -def main(): - """Cleanup after test run.""" - args = parse_args(sys.argv[1:]) - level = getattr(logging, args.loglevel.upper(), None) - if not isinstance(level, int): - raise ValueError('Invalid log level: "{}"'.format(args.loglevel)) - logging.basicConfig(level=level) - destroy(args.model_name) diff --git a/zaza/charm_lifecycle/func_test_runner.py b/zaza/charm_lifecycle/func_test_runner.py deleted file mode 100644 index f99c90d..0000000 --- a/zaza/charm_lifecycle/func_test_runner.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Run full test lifecycle.""" -import argparse -import asyncio -import logging -import os -import sys - -import zaza.charm_lifecycle.configure as configure -import zaza.charm_lifecycle.destroy as destroy -import zaza.charm_lifecycle.utils as utils -import zaza.charm_lifecycle.prepare as prepare -import zaza.charm_lifecycle.deploy as deploy -import zaza.charm_lifecycle.test as test - - -def func_test_runner(keep_model=False, smoke=False, dev=False, bundle=None): - """Deploy the bundles and run the tests as defined by the charms tests.yaml. - - :param keep_model: Whether to destroy model at end of run - :type keep_model: boolean - :param smoke: Whether to just run smoke test. - :param dev: Whether to just run dev test. - :type smoke: boolean - :type dev: boolean - """ - test_config = utils.get_charm_config() - if bundle: - bundles = [bundle] - else: - if smoke: - bundle_key = 'smoke_bundles' - elif dev: - bundle_key = 'dev_bundles' - else: - bundle_key = 'gate_bundles' - bundles = test_config[bundle_key] - last_test = bundles[-1] - for t in bundles: - model_name = utils.generate_model_name() - # Prepare - prepare.prepare(model_name) - # Deploy - deploy.deploy( - os.path.join(utils.BUNDLE_DIR, '{}.yaml'.format(t)), - model_name) - if 'configure' in test_config: - # Configure - configure.configure(model_name, test_config['configure']) - # Test - test.test(model_name, test_config['tests']) - # Destroy - # Keep the model from the last run if keep_model is true, this is to - # maintian compat with osci and should change when the zaza collect - # functions take over from osci for artifact collection. - if keep_model and t == last_test: - pass - else: - destroy.destroy(model_name) - - -def parse_args(args): - """Parse command line arguments. - - :param args: List of configure functions functions - :type list: [str1, str2,...] List of command line arguments - :returns: Parsed arguments - :rtype: Namespace - """ - parser = argparse.ArgumentParser() - parser.add_argument('--keep-model', dest='keep_model', - help='Keep model at the end of the run', - action='store_true') - parser.add_argument('--smoke', dest='smoke', - help='Just run smoke test(s)', - action='store_true') - parser.add_argument('--dev', dest='dev', - help='Just run dev test(s)', - action='store_true') - parser.add_argument('-b', '--bundle', dest='bundle', - help='Override the bundle to be run', - required=False) - parser.add_argument('--log', dest='loglevel', - help='Loglevel [DEBUG|INFO|WARN|ERROR|CRITICAL]') - parser.set_defaults(keep_model=False, - smoke=False, - dev=False, - loglevel='INFO') - return parser.parse_args(args) - - -def main(): - """Execute full test run.""" - args = parse_args(sys.argv[1:]) - - level = getattr(logging, args.loglevel.upper(), None) - if not isinstance(level, int): - raise ValueError('Invalid log level: "{}"'.format(args.loglevel)) - logging.basicConfig(level=level) - - if args.dev and args.smoke: - raise ValueError('Ambiguous arguments: --smoke and ' - '--dev cannot be used together') - - if args.dev and args.bundle: - raise ValueError('Ambiguous arguments: --bundle and ' - '--dev cannot be used together') - - if args.smoke and args.bundle: - raise ValueError('Ambiguous arguments: --bundle and ' - '--smoke cannot be used together') - - func_test_runner( - keep_model=args.keep_model, - smoke=args.smoke, - dev=args.dev, - bundle=args.bundle) - asyncio.get_event_loop().close() diff --git a/zaza/charm_lifecycle/prepare.py b/zaza/charm_lifecycle/prepare.py deleted file mode 100644 index 32778b7..0000000 --- a/zaza/charm_lifecycle/prepare.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Run prepare phase.""" -import argparse -import copy -import logging -import os -import sys - -import zaza.controller -import zaza.model - -import zaza.charm_lifecycle.utils as utils - -MODEL_DEFAULTS = { - # Model defaults from charm-test-infra - # https://jujucharms.com/docs/2.1/models-config - 'default-series': 'xenial', - 'image-stream': 'daily', - 'test-mode': 'true', - 'transmit-vendor-metrics': 'false', - # https://bugs.launchpad.net/juju/+bug/1685351 - # enable-os-refresh-update: false - 'enable-os-upgrade': 'false', - 'automatically-retry-hooks': 'false', - 'use-default-secgroup': 'true', -} - - -def parse_option_list_string(option_list, delimiter=None): - """Convert the given string to a dictionary of options. - - Each pair must be of the form 'k=v', the delimiter seperates the - pairs from each other not the key from the value. - - :param option_list: A string representation of key value pairs. - :type option_list: str - :param delimiter: Delimiter to use to seperate each pair. - :type delimiter: str - :returns: A dictionary of settings. - :rtype: Dict - """ - settings = {} - if delimiter is None: - delimiter = ';' - for setting in option_list.split(delimiter): - if not setting: - continue - key, value = setting.split('=') - settings[key.strip()] = value.strip() - return settings - - -def get_model_settings(): - """Construct settings for model from defaults and env variables. - - :returns: Settings to use for model - :rtype: Dict - """ - model_settings = copy.deepcopy(MODEL_DEFAULTS) - model_settings.update( - parse_option_list_string(os.environ.get('MODEL_SETTINGS', ''))) - return model_settings - - -def get_model_constraints(): - """Construct constraints for model. - - :returns: Constraints to apply to model - :rtype: Dict - """ - return parse_option_list_string(os.environ.get('MODEL_CONSTRAINTS', '')) - - -def prepare(model_name): - """Run all steps to prepare the environment before a functional test run. - - :param model: Name of model to add - :type bundle: str - """ - zaza.controller.add_model(model_name, config=get_model_settings()) - zaza.model.set_model_constraints( - model_name=model_name, - constraints=get_model_constraints()) - - -def parse_args(args): - """Parse command line arguments. - - :param args: List of configure functions functions - :type list: [str1, str2,...] List of command line arguments - :returns: Parsed arguments - :rtype: Namespace - """ - parser = argparse.ArgumentParser() - parser.add_argument('-m', '--model-name', help='Name of model to add') - parser.add_argument('--log', dest='loglevel', - help='Loglevel [DEBUG|INFO|WARN|ERROR|CRITICAL]') - parser.set_defaults(loglevel='INFO') - parser.set_defaults(model_name=utils.generate_model_name()) - return parser.parse_args(args) - - -def main(): - """Add a new model.""" - args = parse_args(sys.argv[1:]) - level = getattr(logging, args.loglevel.upper(), None) - if not isinstance(level, int): - raise ValueError('Invalid log level: "{}"'.format(args.loglevel)) - logging.basicConfig(level=level) - print('model_name: {}'.format(args.model_name)) - prepare(args.model_name) diff --git a/zaza/charm_lifecycle/test.py b/zaza/charm_lifecycle/test.py deleted file mode 100644 index b8bcfdd..0000000 --- a/zaza/charm_lifecycle/test.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Run test phase.""" -import asyncio -import argparse -import logging -import unittest -import sys - -import zaza.model -import zaza.charm_lifecycle.utils as utils - - -def run_test_list(tests): - """Run the tests as defined in the list of test classes in series. - - :param tests: List of test class strings - :type tests: ['zaza.charms_tests.svc.TestSVCClass1', ...] - :raises: AssertionError if test run fails - """ - for _testcase in tests: - logging.info('## Running Test {} ##'.format(_testcase)) - testcase = utils.get_class(_testcase) - suite = unittest.TestLoader().loadTestsFromTestCase(testcase) - test_result = unittest.TextTestRunner(verbosity=2).run(suite) - assert test_result.wasSuccessful(), "Test run failed" - - -def test(model_name, tests): - """Run all steps to execute tests against the model.""" - zaza.model.set_juju_model(model_name) - run_test_list(tests) - - -def parse_args(args): - """Parse command line arguments. - - :param args: List of configure functions functions - :type list: [str1, str2,...] List of command line arguments - :returns: Parsed arguments - :rtype: Namespace - """ - parser = argparse.ArgumentParser() - parser.add_argument('-t', '--tests', nargs='+', - help='Space separated list of test classes', - required=False) - parser.add_argument('-m', '--model-name', help='Name of model to remove', - required=True) - parser.add_argument('--log', dest='loglevel', - help='Loglevel [DEBUG|INFO|WARN|ERROR|CRITICAL]') - parser.set_defaults(loglevel='INFO') - return parser.parse_args(args) - - -def main(): - """Run the tests defined by the command line args. - - Run the tests defined by the command line args or if none were provided - read the tests from the charms tests.yaml config file - """ - args = parse_args(sys.argv[1:]) - level = getattr(logging, args.loglevel.upper(), None) - if not isinstance(level, int): - raise ValueError('Invalid log level: "{}"'.format(args.loglevel)) - logging.basicConfig(level=level) - tests = args.tests or utils.get_charm_config()['tests'] - test(args.model_name, tests) - asyncio.get_event_loop().close() diff --git a/zaza/charm_lifecycle/utils.py b/zaza/charm_lifecycle/utils.py deleted file mode 100644 index d39eae7..0000000 --- a/zaza/charm_lifecycle/utils.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utilities to support running lifecycle phases.""" -import importlib -import uuid -import sys -import yaml - -BUNDLE_DIR = "./tests/bundles/" -DEFAULT_TEST_CONFIG = "./tests/tests.yaml" - - -def get_charm_config(yaml_file=None): - """Read the yaml test config file and return the resulting config. - - :param yaml_file: File to be read - :type yaml_file: str - :returns: Config dictionary - :rtype: dict - """ - if not yaml_file: - yaml_file = DEFAULT_TEST_CONFIG - with open(yaml_file, 'r') as stream: - return yaml.safe_load(stream) - - -def get_class(class_str): - """Get the class represented by the given string. - - For example, get_class('zaza.charms_tests.svc.TestSVCClass1') - returns zaza.charms_tests.svc.TestSVCClass1 - - :param class_str: Class to be returned - :type class_str: str - :returns: Test class - :rtype: class - """ - old_syspath = sys.path - sys.path.append('.') - module_name = '.'.join(class_str.split('.')[:-1]) - class_name = class_str.split('.')[-1] - module = importlib.import_module(module_name) - sys.path = old_syspath - return getattr(module, class_name) - - -def generate_model_name(): - """Generate a unique model name. - - :returns: Model name - :rtype: str - """ - return 'zaza-{}'.format(str(uuid.uuid4())[-12:]) diff --git a/zaza/controller.py b/zaza/controller.py deleted file mode 100644 index 59d78ce..0000000 --- a/zaza/controller.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Module for interacting with a juju controller.""" - -import logging -from juju.controller import Controller -import subprocess -from zaza import sync_wrapper - - -async def async_add_model(model_name, config=None): - """Add a model to the current controller. - - :param model_name: Name to give the new model. - :type model_name: str - :param config: Model configuration. - :type config: dict - """ - controller = Controller() - await controller.connect() - logging.debug("Adding model {}".format(model_name)) - model = await controller.add_model(model_name, config=config) - await model.disconnect() - await controller.disconnect() - # NOTE: This is necessary to guarantee juju is aware of the newly created - # model. - go_list_models() - -add_model = sync_wrapper(async_add_model) - - -async def async_destroy_model(model_name): - """Remove a model from the current controller. - - :param model_name: Name of model to remove - :type model_name: str - """ - controller = Controller() - await controller.connect() - logging.debug("Destroying model {}".format(model_name)) - await controller.destroy_model(model_name) - await controller.disconnect() - -destroy_model = sync_wrapper(async_destroy_model) - - -async def async_get_cloud(): - """Return the name of the current cloud. - - :returns: Name of cloud - :rtype: str - """ - controller = Controller() - await controller.connect() - cloud = await controller.get_cloud() - await controller.disconnect() - return cloud - -get_cloud = sync_wrapper(async_get_cloud) - - -async def async_list_models(): - """Return a list of tha available clouds. - - :returns: List of clouds - :rtype: list - """ - controller = Controller() - await controller.connect() - models = await controller.list_models() - await controller.disconnect() - return models - -list_models = sync_wrapper(async_list_models) - - -def go_list_models(): - """Execute juju models. - - NOTE: Excuting the juju models command updates the local cache of models. - Python-juju currently does not update the local cache on add model. - https://github.com/juju/python-libjuju/issues/267 - - :returns: None - :rtype: None - """ - cmd = ["juju", "models"] - subprocess.check_call(cmd) diff --git a/zaza/model.py b/zaza/model.py deleted file mode 100644 index c5d8f76..0000000 --- a/zaza/model.py +++ /dev/null @@ -1,1361 +0,0 @@ -# Copyright 2018 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Module for interacting with a juju model. - -This module contains a number of functions for interacting with a Juju model -mostly via libjuju. Async function also provice a non-async alternative via -'sync_wrapper'. -""" - -import asyncio -from async_generator import async_generator, yield_, asynccontextmanager -import logging -import os -import subprocess -import tempfile -import yaml -from oslo_config import cfg -import concurrent - -from juju.errors import JujuError -from juju.model import Model - -from zaza import sync_wrapper - -CURRENT_MODEL = None - - -class ModelTimeout(Exception): - """Model timeout exception.""" - - pass - - -def set_juju_model(model_name): - """Point environment at the given model. - - :param model_name: Model to point environment at - :type model_name: str - """ - global CURRENT_MODEL - os.environ["JUJU_MODEL"] = model_name - CURRENT_MODEL = model_name - - -def get_juju_model(): - """Retrieve current model. - - First check the environment for JUJU_MODEL. If this is not set, get the - current active model. - - :returns: In focus model name - :rtype: str - """ - global CURRENT_MODEL - if CURRENT_MODEL: - return CURRENT_MODEL - # LY: I think we should remove the KeyError handling. I don't think we - # should ever fall back to the model in focus because it will lead - # to functions being added which do not explicitly set a model and - # zaza will loose the ability to do concurrent runs. - try: - # Check the environment - CURRENT_MODEL = os.environ["JUJU_MODEL"] - except KeyError: - try: - CURRENT_MODEL = os.environ["MODEL_NAME"] - except KeyError: - # If unset connect get the current active model - CURRENT_MODEL = get_current_model() - return CURRENT_MODEL - - -async def deployed(): - """List deployed applications.""" - # Create a Model instance. We need to connect our Model to a Juju api - # server before we can use it. - model = Model() - # Connect to the currently active Juju model - await model.connect_current() - try: - # list currently deploeyd services - return list(model.applications.keys()) - finally: - # Disconnect from the api server and cleanup. - await model.disconnect() - -sync_deployed = sync_wrapper(deployed) - - -def get_unit_from_name(unit_name, model=None, model_name=None): - """Return the units that corresponds to the name in the given model. - - :param unit_name: Name of unit to match - :type unit_name: str - :param model: Model to perform lookup in - :type model: model.Model() - :param model_name: Name of the model to perform lookup in - :type model_name: string - :returns: Unit matching given name - :rtype: juju.unit.Unit or None - :raises: UnitNotFound - """ - app = unit_name.split('/')[0] - unit = None - try: - if model is not None: - units = model.applications[app].units - else: - units = get_units(application_name=app, model_name=model_name) - except KeyError: - msg = ('Application: {} does not exist in current model'. - format(app)) - logging.error(msg) - raise UnitNotFound(unit_name) - for u in units: - if u.entity_id == unit_name: - unit = u - break - else: - raise UnitNotFound(unit_name) - return unit - - -@asynccontextmanager -@async_generator -async def run_in_model(model_name): - """Context manager for executing code inside a libjuju model. - - Example of using run_in_model: - async with run_in_model(model_name) as model: - model.do_something() - - :param model_name: Name of model to run function in - :type model_name: str - :returns: The juju Model object correcsponding to model_name - :rtype: Iterator[:class:'juju.Model()'] - """ - model = Model() - if not model_name: - model_name = get_juju_model() - await model.connect_model(model_name) - await yield_(model) - await model.disconnect() - - -async def async_scp_to_unit(unit_name, source, destination, model_name=None, - user='ubuntu', proxy=False, scp_opts=''): - """Transfer files to unit_name in model_name. - - :param model_name: Name of model unit is in - :type model_name: str - :param unit_name: Name of unit to scp to - :type unit_name: str - :param source: Local path of file(s) to transfer - :type source: str - :param destination: Remote destination of transferred files - :type source: str - :param user: Remote username - :type source: str - :param proxy: Proxy through the Juju API server - :type proxy: bool - :param scp_opts: Additional options to the scp command - :type scp_opts: str - """ - async with run_in_model(model_name) as model: - unit = get_unit_from_name(unit_name, model) - await unit.scp_to(source, destination, user=user, proxy=proxy, - scp_opts=scp_opts) - -scp_to_unit = sync_wrapper(async_scp_to_unit) - - -async def async_scp_to_all_units(application_name, source, destination, - model_name=None, user='ubuntu', proxy=False, - scp_opts=''): - """Transfer files from to all units of an application. - - :param model_name: Name of model unit is in - :type model_name: str - :param application_name: Name of application to scp file to - :type application_name: str - :param source: Local path of file(s) to transfer - :type source: str - :param destination: Remote destination of transferred files - :type source: str - :param user: Remote username - :type source: str - :param proxy: Proxy through the Juju API server - :type proxy: bool - :param scp_opts: Additional options to the scp command - :type scp_opts: str - """ - async with run_in_model(model_name) as model: - for unit in model.applications[application_name].units: - # FIXME: Should scp in parallel - await unit.scp_to(source, destination, user=user, proxy=proxy, - scp_opts=scp_opts) - -scp_to_all_units = sync_wrapper(async_scp_to_all_units) - - -async def async_scp_from_unit(unit_name, source, destination, model_name=None, - user='ubuntu', proxy=False, scp_opts=''): - """Transfer files from to unit_name in model_name. - - :param model_name: Name of model unit is in - :type model_name: str - :param unit_name: Name of unit to scp from - :type unit_name: str - :param source: Remote path of file(s) to transfer - :type source: str - :param destination: Local destination of transferred files - :type source: str - :param user: Remote username - :type source: str - :param proxy: Proxy through the Juju API server - :type proxy: bool - :param scp_opts: Additional options to the scp command - :type scp_opts: str - """ - async with run_in_model(model_name) as model: - unit = get_unit_from_name(unit_name, model) - await unit.scp_from(source, destination, user=user, proxy=proxy, - scp_opts=scp_opts) - - -scp_from_unit = sync_wrapper(async_scp_from_unit) - - -async def async_run_on_unit(unit_name, command, model_name=None, timeout=None): - """Juju run on unit. - - :param model_name: Name of model unit is in - :type model_name: str - :param unit_name: Name of unit to match - :type unit: str - :param command: Command to execute - :type command: str - :param timeout: How long in seconds to wait for command to complete - :type timeout: int - :returns: action.data['results'] {'Code': '', 'Stderr': '', 'Stdout': ''} - :rtype: dict - """ - async with run_in_model(model_name) as model: - unit = get_unit_from_name(unit_name, model) - action = await unit.run(command, timeout=timeout) - if action.data.get('results'): - return action.data.get('results') - else: - return {} - -run_on_unit = sync_wrapper(async_run_on_unit) - - -async def async_run_on_leader(application_name, command, model_name=None, - timeout=None): - """Juju run on leader unit. - - :param application_name: Application to match - :type application_name: str - :param command: Command to execute - :type command: str - :param model_name: Name of model unit is in - :type model_name: str - :param timeout: How long in seconds to wait for command to complete - :type timeout: int - :returns: action.data['results'] {'Code': '', 'Stderr': '', 'Stdout': ''} - :rtype: dict - """ - async with run_in_model(model_name) as model: - for unit in model.applications[application_name].units: - is_leader = await unit.is_leader_from_status() - if is_leader: - action = await unit.run(command, timeout=timeout) - if action.data.get('results'): - return action.data.get('results') - else: - return {} - -run_on_leader = sync_wrapper(async_run_on_leader) - - -async def async_get_unit_time(unit_name, model_name=None, timeout=None): - """Get the current time (in seconds since Epoch) on the given unit. - - :param model_name: Name of model to query. - :type model_name: str - :param unit_name: Name of unit to run action on - :type unit_name: str - :returns: time in seconds since Epoch on unit - :rtype: int - """ - out = await async_run_on_unit( - unit_name=unit_name, - command="date +'%s'", - model_name=model_name, - timeout=timeout) - return int(out['Stdout']) - -get_unit_time = sync_wrapper(async_get_unit_time) - - -async def async_get_unit_service_start_time(unit_name, service, - model_name=None, timeout=None): - """Return the time that the given service was started on a unit. - - Return the time (in seconds since Epoch) that the given service was - started on the given unit. If the service is not running raise - ServiceNotRunning exception. - - :param model_name: Name of model to query. - :type model_name: str - :param unit_name: Name of unit to run action on - :type unit_name: str - :param service: Name of service to check is running - :type service: str - :param timeout: Time to wait for status to be achieved - :type timeout: int - :returns: time in seconds since Epoch on unit - :rtype: int - :raises: ServiceNotRunning - """ - cmd = "stat -c %Y /proc/$(pidof -x {} | cut -f1 -d ' ')".format(service) - out = await async_run_on_unit( - unit_name=unit_name, - command=cmd, - model_name=model_name, - timeout=timeout) - out = out['Stdout'].strip() - if out: - return int(out) - else: - raise ServiceNotRunning(service) - -get_unit_service_start_time = sync_wrapper(async_get_unit_service_start_time) - - -async def async_get_application(application_name, model_name=None): - """Return an application object. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application to retrieve units for - :type application_name: str - :returns: Application object - :rtype: object - """ - async with run_in_model(model_name) as model: - return model.applications[application_name] - -get_application = sync_wrapper(async_get_application) - - -async def async_get_units(application_name, model_name=None): - """Return all the units of a given application. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application to retrieve units for - :type application_name: str - :returns: List of juju units - :rtype: [juju.unit.Unit, juju.unit.Unit,...] - """ - async with run_in_model(model_name) as model: - return model.applications[application_name].units - -get_units = sync_wrapper(async_get_units) - - -async def async_get_machines(application_name, model_name=None): - """Return all the machines of a given application. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application to retrieve units for - :type application_name: str - :returns: List of juju machines - :rtype: [juju.machine.Machine, juju.machine.Machine,...] - """ - async with run_in_model(model_name) as model: - machines = [] - for unit in model.applications[application_name].units: - machines.append(unit.machine) - return machines - -get_machines = sync_wrapper(async_get_machines) - - -def get_first_unit_name(application_name, model_name=None): - """Return name of lowest numbered unit of given application. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :returns: Name of lowest numbered unit - :rtype: str - """ - return get_units(application_name, model_name=model_name)[0].name - - -async def async_get_lead_unit_name(application_name, model_name=None): - """Return name of lowest numbered unit of given application. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :returns: Name of lowest numbered unit - :rtype: str - """ - async with run_in_model(model_name) as model: - for unit in model.applications[application_name].units: - is_leader = await unit.is_leader_from_status() - if is_leader: - return unit.entity_id - -get_lead_unit_name = sync_wrapper(async_get_lead_unit_name) - - -def get_app_ips(application_name, model_name=None): - """Return public address of all units of an application. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :returns: List of ip addresses - :rtype: [str, str,...] - """ - return [u.public_address - for u in get_units(application_name, model_name=model_name)] - - -async def async_get_application_config(application_name, model_name=None): - """Return application configuration. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :returns: Dictionary of configuration - :rtype: dict - """ - async with run_in_model(model_name) as model: - return await model.applications[application_name].get_config() - -get_application_config = sync_wrapper(async_get_application_config) - - -async def async_set_application_config(application_name, configuration, - model_name=None): - """Set application configuration. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :param configuration: Dictionary of configuration setting(s) - :type configuration: dict - """ - async with run_in_model(model_name) as model: - return await (model.applications[application_name] - .set_config(configuration)) - -set_application_config = sync_wrapper(async_set_application_config) - - -async def async_get_status(model_name=None): - """Return full status. - - :param model_name: Name of model to query. - :type model_name: str - :returns: dictionary of juju status - :rtype: dict - """ - async with run_in_model(model_name) as model: - return await model.get_status() - -get_status = sync_wrapper(async_get_status) - - -async def async_run_action(unit_name, action_name, model_name=None, - action_params={}): - """Run action on given unit. - - :param unit_name: Name of unit to run action on - :type unit_name: str - :param action_name: Name of action to run - :type action_name: str - :param model_name: Name of model to query. - :type model_name: str - :param action_params: Dictionary of config options for action - :type action_params: dict - :returns: Action object - :rtype: juju.action.Action - """ - async with run_in_model(model_name) as model: - unit = get_unit_from_name(unit_name, model) - action_obj = await unit.run_action(action_name, **action_params) - await action_obj.wait() - return action_obj - -run_action = sync_wrapper(async_run_action) - - -async def async_run_action_on_leader(application_name, action_name, - model_name=None, action_params=None): - """Run action on lead unit of the given application. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :param action_name: Name of action to run - :type action_name: str - :param action_params: Dictionary of config options for action - :type action_params: dict - :returns: Action object - :rtype: juju.action.Action - """ - async with run_in_model(model_name) as model: - for unit in model.applications[application_name].units: - is_leader = await unit.is_leader_from_status() - if is_leader: - action_obj = await unit.run_action(action_name, - **action_params) - await action_obj.wait() - return action_obj - -run_action_on_leader = sync_wrapper(async_run_action_on_leader) - - -async def async_remove_application(application_name, model_name=None, - forcefully_remove_machines=False): - """Remove application from model. - - :param application_name: Name of application - :type application_name: str - :param model_name: Name of model to query. - :type model_name: str - :param forcefully_remove_machines: Forcefully remove the machines the - application is runing on. - :type forcefully_remove_machines: bool - """ - async with run_in_model(model_name) as model: - application = model.applications[application_name] - if forcefully_remove_machines: - for unit in model.applications[application_name].units: - await unit.machine.destroy(force=True) - else: - await application.remove() - -remove_application = sync_wrapper(async_remove_application) - - -class UnitError(Exception): - """Exception raised for units in error state.""" - - def __init__(self, units): - """Set units in error state in messgae and raise.""" - message = "Units {} in error state".format( - ','.join([u.entity_id for u in units])) - super(UnitError, self).__init__(message) - - -class ServiceNotRunning(Exception): - """Exception raised when service not running.""" - - def __init__(self, service): - """Set not running services in messgae and raise.""" - message = "Service {} not running".format(service) - super(ServiceNotRunning, self).__init__(message) - - -class CommandRunFailed(Exception): - """Command failed to run.""" - - def __init__(self, cmd, result): - """Create Command run failed exception. - - :param cmd: Command that was run - :type cmd: string - :param result: Dict containing the output of the command - :type result: dict - {'Code': '0', 'Stdout': '', 'Stderr':''} - """ - code = result.get('Code') - output = result.get('Stdout') - err = result.get('Stderr') - msg = ('Command {} failed with code {}, output {} and error {}' - .format(cmd, code, output, err)) - super(CommandRunFailed, self).__init__(msg) - - -def units_with_wl_status_state(model, state): - """Return a list of unit which have a matching workload status. - - :returns: Units in error state - :rtype: [juju.Unit, ...] - """ - matching_units = [] - for unit in model.units.values(): - wl_status = unit.workload_status - if wl_status == state: - matching_units.append(unit) - return matching_units - - -def check_model_for_hard_errors(model): - """Check model for any hard errors that should halt a deployment. - - The only check currently implemented is checking for units in an - error state - - :raises: UnitError - """ - errored_units = units_with_wl_status_state(model, 'error') - if errored_units: - raise UnitError(errored_units) - - -def check_unit_workload_status(model, unit, states): - """Check that the units workload status matches the supplied state. - - This function has the side effect of also checking for *any* units - in an error state and aborting if any are found. - - :param model: Model object to check in - :type model: juju.Model - :param unit: Unit to check wl status of - :type unit: juju.Unit - :param states: Acceptable unit work load states - :type states: list - :raises: UnitError - :returns: Whether units workload status matches desired state - :rtype: bool - """ - check_model_for_hard_errors(model) - return unit.workload_status in states - - -def check_unit_workload_status_message(model, unit, message=None, - prefixes=None): - """Check that the units workload status message. - - Check that the units workload status message matches the supplied - message or starts with one of the supplied prefixes. Raises an exception - if neither prefixes or message is set. This function has the side effect - of also checking for *any* units in an error state and aborting if any - are found. - - :param model: Model object to check in - :type model: juju.Model - :param unit: Unit to check wl status of - :type unit: juju.Unit - :param message: Expected message text - :type message: str - :param prefixes: Prefixes to match message against - :type prefixes: list - :raises: ValueError, UnitError - :returns: Whether message matches desired string - :rtype: bool - """ - check_model_for_hard_errors(model) - if message is not None: - return unit.workload_status_message == message - elif prefixes is not None: - return unit.workload_status_message.startswith(tuple(prefixes)) - else: - raise ValueError("Must be called with message or prefixes") - - -async def async_wait_for_agent_status(model_name=None, status='executing', - timeout=60): - """Wait for at least one unit to enter a specific agent status. - - This is useful for awaiting execution after mutating charm configuration. - - :param model_name: Name of model to query. - :type model_name: str - :param status: The desired agent status we are looking for. - :type status: str - :param timeout: Time to wait for status to be achieved. - :type timeout: int - """ - def one_agent_status(model, status): - check_model_for_hard_errors(model) - for app in model.applications: - for unit in model.applications[app].units: - agent_status = unit.data.get('agent-status', {}) - if agent_status.get('current', None) == status: - break - else: - continue - break - else: - return False - return True - - async with run_in_model(model_name) as model: - logging.info('Waiting for at least one unit with agent status "{}"' - .format(status)) - await model.block_until( - lambda: one_agent_status(model, status), timeout=timeout) - -wait_for_agent_status = sync_wrapper(async_wait_for_agent_status) - - -async def async_wait_for_application_states(model_name=None, states=None, - timeout=2700): - """Wait for model to achieve the desired state. - - Check the workload status and workload status message for every unit of - every application. By default look for an 'active' workload status and a - message that starts with one of the approved_message_prefixes. - - Bespoke statuses and messages can be passed in with states. states takes - the form:: - - states = { - 'app': { - 'workload-status': 'blocked', - 'workload-status-message': 'No requests without a prod'} - 'anotherapp': { - 'workload-status-message': 'Unit is super ready'}} - wait_for_application_states('modelname', states=states) - - :param model_name: Name of model to query. - :type model_name: str - :param states: States to look for - :type states: dict - :param timeout: Time to wait for status to be achieved - :type timeout: int - """ - approved_message_prefixes = ['ready', 'Ready', 'Unit is ready'] - approved_statuses = ['active'] - - if not states: - states = {} - async with run_in_model(model_name) as model: - check_model_for_hard_errors(model) - logging.info("Waiting for a unit to appear") - await model.block_until( - lambda: len(model.units) > 0 - ) - logging.info("Waiting for all units to be idle") - try: - await model.block_until( - lambda: units_with_wl_status_state( - model, 'error') or model.all_units_idle(), - timeout=timeout) - except concurrent.futures._base.TimeoutError: - raise ModelTimeout("Zaza has timed out waiting on the model to " - "reach idle state.") - errored_units = units_with_wl_status_state(model, 'error') - if errored_units: - raise UnitError(errored_units) - try: - for application, app_data in model.applications.items(): - check_info = states.get(application, {}) - for unit in app_data.units: - app_wls = check_info.get('workload-status') - if app_wls: - all_approved_statuses = approved_statuses + [app_wls] - else: - all_approved_statuses = approved_statuses - logging.info("Checking workload status of {}".format( - unit.entity_id)) - await model.block_until( - lambda: check_unit_workload_status( - model, - unit, - all_approved_statuses), - timeout=timeout) - check_msg = check_info.get('workload-status-message') - logging.info("Checking workload status message of {}" - .format(unit.entity_id)) - prefixes = approved_message_prefixes - if check_msg is not None: - prefixes = approved_message_prefixes + [check_msg] - else: - prefixes = approved_message_prefixes - await model.block_until( - lambda: check_unit_workload_status_message( - model, - unit, - prefixes=prefixes), - timeout=timeout) - except concurrent.futures._base.TimeoutError: - raise ModelTimeout("Zaza has timed out waiting on the model to " - "reach expected workload statuses.") - -wait_for_application_states = sync_wrapper(async_wait_for_application_states) - - -async def async_block_until_all_units_idle(model_name=None, timeout=2700): - """Block until all units in the given model are idle. - - An example accessing this function via its sync wrapper:: - - block_until_all_units_idle('modelname') - - :param model_name: Name of model to query. - :type model_name: str - :param timeout: Time to wait for status to be achieved - :type timeout: float - """ - async with run_in_model(model_name) as model: - await model.block_until( - lambda: units_with_wl_status_state( - model, 'error') or model.all_units_idle(), - timeout=timeout) - errored_units = units_with_wl_status_state(model, 'error') - if errored_units: - raise UnitError(errored_units) - -block_until_all_units_idle = sync_wrapper(async_block_until_all_units_idle) - - -async def async_block_until_service_status(unit_name, services, target_status, - model_name=None, timeout=2700): - """Block until all services on the unit are in the desired state. - - Block until all services on the unit are in the desired state (stopped - or running):: - - block_until_service_status( - first_unit, - ['glance-api'], - 'running', - model_name='modelname') - - :param unit_name: Name of unit to run action on - :type unit_name: str - :param services: List of services to check - :type services: [] - :param target_status: State services should be in (stopped or running) - :type target_status: str - :param model_name: Name of model to query. - :type model_name: str - :param timeout: Time to wait for status to be achieved - :type timeout: int - """ - async def _check_service(): - for service in services: - out = await async_run_on_unit( - unit_name, - "pidof -x {}".format(service), - model_name=model_name, - timeout=timeout) - response_size = len(out['Stdout'].strip()) - if target_status == "running" and response_size == 0: - return False - elif target_status == "stopped" and response_size > 0: - return False - return True - async with run_in_model(model_name): - await async_block_until(_check_service, timeout=timeout) - -block_until_service_status = sync_wrapper(async_block_until_service_status) - - -def get_actions(application_name, model_name=None): - """Get the actions an applications supports. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :returns: Dictionary of actions and their descriptions - :rtype: dict - """ - if not model_name: - model_name = get_juju_model() - # libjuju has not implemented get_actions yet - # https://github.com/juju/python-libjuju/issues/226 - cmd = ['juju', 'actions', '-m', model_name, application_name, - '--format', 'yaml'] - return yaml.safe_load(subprocess.check_output(cmd)) - - -async def async_get_current_model(): - """Return the current active model name. - - Connect to the current active model and return its name. - - :returns: String curenet model name - :rtype: str - """ - model = Model() - await model.connect() - model_name = model.info.name - await model.disconnect() - return model_name - - -get_current_model = sync_wrapper(async_get_current_model) - - -async def async_block_until(*conditions, timeout=None, wait_period=0.5, - loop=None): - """Return only after all async conditions are true. - - Based on juju.utils.block_until which currently does not support - async methods as conditions. - - :param conditions: Functions to evaluate. - :type conditions: functions - :param timeout: Timeout in seconds - :type timeout: float - :param wait_period: Time to wait between re-assessing conditions. - :type wait_period: float - :param loop: The event loop to use - :type loop: An event loop - """ - async def _block(): - while True: - evaluated = [] - for c in conditions: - result = await c() - evaluated.append(result) - if all(evaluated): - return - else: - await asyncio.sleep(wait_period, loop=loop) - await asyncio.wait_for(_block(), timeout, loop=loop) - - -async def async_block_until_file_ready(application_name, remote_file, - check_function, model_name=None, - timeout=2700): - """Block until the check_function passes against. - - Block until the check_function passes against the provided file. It is - unlikely that a test would call this function directly, rather it is - provided as scaffolding for tests with a more specialised purpose. - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :param remote_file: Remote path of file to transfer - :type remote_file: str - :param check_function: Function to use to check if file is ready, must take - exactly one argument which is the file contents. - :type check_function: function - :param timeout: Time to wait for contents to appear in file - :type timeout: float - """ - async def _check_file(): - file_name = os.path.basename(remote_file) - units = model.applications[application_name].units - for unit in units: - with tempfile.TemporaryDirectory() as tmpdir: - try: - await unit.scp_from(remote_file, tmpdir) - with open(os.path.join(tmpdir, file_name), 'r') as lf: - contents = lf.read() - if not check_function(contents): - return False - # libjuju throws a generic error for scp failure. So we cannot - # differentiate between a connectivity issue and a target file - # not existing error. For now just assume the latter. - except JujuError as e: - return False - else: - return True - - async with run_in_model(model_name) as model: - await async_block_until(_check_file, timeout=timeout) - - -async def async_block_until_file_has_contents(application_name, remote_file, - expected_contents, - model_name=None, timeout=2700): - """Block until the expected_contents are present on all units. - - Block until the given string (expected_contents) is present in the file - (remote_file) on all units of the given application. - - An example accessing this function via its sync wrapper:: - - block_until_file_has_contents( - 'modelname' - 'keystone', - '/etc/apache2/apache2.conf', - 'KeepAlive On') - - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :param remote_file: Remote path of file to transfer - :type remote_file: str - :param expected_contents: String to look for in file - :type expected_contents: str - :param timeout: Time to wait for contents to appear in file - :type timeout: float - """ - def f(x): - return expected_contents in x - return await async_block_until_file_ready( - application_name, - remote_file, - f, - timeout=timeout, - model_name=model_name) - -block_until_file_has_contents = sync_wrapper( - async_block_until_file_has_contents) - - -async def async_block_until_oslo_config_entries_match(application_name, - remote_file, - expected_contents, - model_name=None, - timeout=2700): - """Block until dict is represented in the file using oslo.config parser. - - Block until the expected_contents are in the given file on all units of - the application. For example to check for the following configuration:: - - [DEFAULT] - debug = False - - [glance_store] - filesystem_store_datadir = /var/lib/glance/images/ - default_store = file - - Call the check via its sync wrapper:: - - expected_contents = { - 'DEFAULT': { - 'debug': ['False']}, - 'glance_store': { - 'filesystem_store_datadir': ['/var/lib/glance/images/'], - 'default_store': ['file']}} - - block_until_oslo_config_entries_match( - 'modelname', - 'glance', - '/etc/glance/glance-api.conf', - expected_contents) - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :param remote_file: Remote path of file to transfer - :type remote_file: str - :param expected_contents: The key value pairs in their corresponding - sections to be looked for in the remote_file - :type expected_contents: {} - :param timeout: Time to wait for contents to appear in file - :type timeout: float - - """ - def f(x): - # Writing out the file that was just read is suboptimal - with tempfile.NamedTemporaryFile(mode='w', delete=True) as fp: - fp.write(x) - fp.seek(0) - sections = {} - parser = cfg.ConfigParser(fp.name, sections) - parser.parse() - for section, entries in expected_contents.items(): - for key, value in entries.items(): - if sections.get(section, {}).get(key) != value: - return False - return True - return await async_block_until_file_ready( - application_name, - remote_file, - f, - timeout=timeout, - model_name=model_name) - - -block_until_oslo_config_entries_match = sync_wrapper( - async_block_until_oslo_config_entries_match) - - -async def async_block_until_services_restarted(application_name, mtime, - services, model_name=None, - timeout=2700): - """Block until the given services have a start time later then mtime. - - For example to check that the glance-api service has been restarted:: - - block_until_services_restarted( - 'modelname' - 'glance', - 1528294585, - ['glance-api']) - - :param model_name: Name of model to query. - :type model_name: str - :param application_name: Name of application - :type application_name: str - :param mtime: Time in seconds since Epoch to check against - :type mtime: int - :param services: Listr of services to check restart time of - :type services: [] - :param timeout: Time to wait for services to be restarted - :type timeout: float - """ - async def _check_service(): - units = model.applications[application_name].units - for unit in units: - for service in services: - try: - svc_mtime = await async_get_unit_service_start_time( - model_name, - unit.entity_id, - service) - except ServiceNotRunning: - return False - if svc_mtime < mtime: - return False - return True - async with run_in_model(model_name) as model: - await async_block_until(_check_service, timeout=timeout) - - -block_until_services_restarted = sync_wrapper( - async_block_until_services_restarted) - - -async def async_block_until_unit_wl_status(unit_name, status, model_name=None, - timeout=2700): - """Block until the given unit has the desired workload status. - - A units workload status may change during a given action. This function - blocks until the given unit has the desired workload status:: - - block_until_unit_wl_status( - aunit, - 'active' - model_name='modelname') - - NOTE: unit.workload_status was actually reporting the application workload - status. Using the full status output from model.get_status() gives us - unit by unit workload status. - - :param unit_name: Name of unit - :type unit_name: str - :param status: Status to wait for (active, maintenance etc) - :type status: str - :param model_name: Name of model to query. - :type model_name: str - :param timeout: Time to wait for unit to achieved desired status - :type timeout: float - """ - async def _unit_status(): - app = unit_name.split("/")[0] - model_status = await async_get_status() - return (model_status.applications[app]['units'][unit_name] - ['workload-status']['status'] == status) - - async with run_in_model(model_name): - await async_block_until(_unit_status, timeout=timeout) - -block_until_unit_wl_status = sync_wrapper( - async_block_until_unit_wl_status) - - -async def async_get_relation_id(application_name, remote_application_name, - model_name=None, - remote_interface_name=None): - """ - Get relation id of relation from model. - - :param model_name: Name of model to operate on - :type model_name: str - :param application_name: Name of application on this side of relation - :type application_name: str - :param remote_application_name: Name of application on other side of - relation - :type remote_application_name: str - :param remote_interface_name: Name of interface on remote end of relation - :type remote_interface_name: Optional(str) - :returns: Relation id of relation if found or None - :rtype: any - """ - async with run_in_model(model_name) as model: - for rel in model.applications[application_name].relations: - spec = '{}'.format(remote_application_name) - if remote_interface_name is not None: - spec += ':{}'.format(remote_interface_name) - if rel.matches(spec): - return(rel.id) - -get_relation_id = sync_wrapper(async_get_relation_id) - - -def set_model_constraints(constraints, model_name=None): - """ - Set constraints on a model. - - Note: Subprocessing out to 'juju' is a temporary solution until - https://bit.ly/2ujbVPA lands in libjuju. - - :param model_name: Name of model to operate on - :type model_name: str - :param constraints: Constraints to be applied to model - :type constraints: dict - - """ - if not constraints: - return - if not model_name: - model_name = get_juju_model() - cmd = ['juju', 'set-model-constraints', '-m', model_name] - cmd.extend(['{}={}'.format(k, v) for k, v in constraints.items()]) - subprocess.check_call(cmd) - - -async def async_upgrade_charm(application_name, channel=None, - force_series=False, force_units=False, - path=None, resources=None, revision=None, - switch=None, model_name=None): - """ - Upgrade the given charm. - - :param application_name: Name of application on this side of relation - :type application_name: str - :param channel: Channel to use when getting the charm from the charm store, - e.g. 'development' - :type channel: str - :param force_series: Upgrade even if series of deployed application is not - supported by the new charm - :type force_series: bool - :param force_units: Upgrade all units immediately, even if in error state - :type force_units: bool - :param path: Uprade to a charm located at path - :type path: str - :param resources: Dictionary of resource name/filepath pairs - :type resources: dict - :param revision: Explicit upgrade revision - :type revision: int - :param switch: Crossgrade charm url - :type switch: str - :param model_name: Name of model to operate on - :type model_name: str - """ - async with run_in_model(model_name) as model: - app = model.applications[application_name] - await app.upgrade_charm( - channel=channel, - force_series=force_series, - force_units=force_units, - path=path, - resources=resources, - revision=revision, - switch=switch) - -upgrade_charm = sync_wrapper(async_upgrade_charm) - - -class UnitNotFound(Exception): - """Unit was not found in model.""" - - def __init__(self, unit_name): - """Create a UnitNotFound exception. - - :param unit_name: Name of the unit - :type unit_name: string - """ - msg = ('Unit: {} was not found in current model'. - format(unit_name)) - super(UnitNotFound, self).__init__(msg) - - -# NOTE: The following are series upgrade related functions which are new -# features in juju. We can migrate to libjuju calls when the feature -# stabilizes. -def prepare_series_upgrade(machine_num, to_series="xenial"): - """Execute juju series-upgrade prepare on machine. - - NOTE: This is a new feature in juju behind a feature flag and not yet in - libjuju. - export JUJU_DEV_FEATURE_FLAGS=upgrade-series - - :param machine_num: Machine number - :type machine_num: str - :param to_series: The series to which to upgrade - :type to_series: str - :returns: None - :rtype: None - """ - juju_model = get_juju_model() - cmd = ["juju", "upgrade-series", "-m", juju_model, - machine_num, "prepare", to_series, "--yes"] - subprocess.check_call(cmd) - - -def complete_series_upgrade(machine_num): - """Execute juju series-upgrade complete on machine. - - NOTE: This is a new feature in juju behind a feature flag and not yet in - libjuju. - export JUJU_DEV_FEATURE_FLAGS=upgrade-series - - :param machine_num: Machine number - :type machine_num: str - :returns: None - :rtype: None - """ - juju_model = get_juju_model() - cmd = ["juju", "upgrade-series", "-m", juju_model, - machine_num, "complete"] - subprocess.check_call(cmd) - - -def set_series(application, to_series): - """Execute juju set-series complete on application. - - NOTE: This is a new feature in juju and not yet in libjuju. - - :param application: Name of application to upgrade series - :type application: str - :param to_series: The series to which to upgrade - :type to_series: str - :returns: None - :rtype: None - """ - juju_model = get_juju_model() - cmd = ["juju", "set-series", "-m", juju_model, - application, to_series] - subprocess.check_call(cmd) - - -def attach_resource(application, resource_name, resource_path): - """Attach resource to charm. - - :param application: Application to get leader settings from. - :type application: str - :param resource_name: The name of the resource as defined in metadata.yaml - :type resource_name: str - :param resource_path: The path to the resource on disk - :type resource_path: str - :returns: None - :rtype: None - """ - juju_model = get_juju_model() - cmd = ["juju", "attach-resource", "-m", juju_model, - application, "{}={}".format(resource_name, resource_path)] - subprocess.check_call(cmd) diff --git a/zaza/charm_tests/saml_mellon/__init__.py b/zaza/openstack/charm_tests/saml_mellon/__init__.py similarity index 100% rename from zaza/charm_tests/saml_mellon/__init__.py rename to zaza/openstack/charm_tests/saml_mellon/__init__.py diff --git a/zaza/charm_tests/saml_mellon/setup.py b/zaza/openstack/charm_tests/saml_mellon/setup.py similarity index 100% rename from zaza/charm_tests/saml_mellon/setup.py rename to zaza/openstack/charm_tests/saml_mellon/setup.py diff --git a/zaza/charm_tests/saml_mellon/tests.py b/zaza/openstack/charm_tests/saml_mellon/tests.py similarity index 100% rename from zaza/charm_tests/saml_mellon/tests.py rename to zaza/openstack/charm_tests/saml_mellon/tests.py