From 8e960e4a2bae259e30ea792d5f7e470836e5b23b Mon Sep 17 00:00:00 2001 From: Steve Noyes Date: Wed, 30 May 2018 16:23:39 -0400 Subject: [PATCH] Add unit tests for host commands As part of getting away from using calls to the shell in the tests, we will need code coverage of the commands library which will no longer be used when the shell is no longer called. This change is the first of serveral to provide unnit test coverage for commands/host. As a result of writing these tests, I needed to tweak a few things, some to make mocking possible, and fix some minor issues: - move delete confirmation prompt to its own method so it can be mocked. - remove extra space from failed Host check message - fix return code to 0 in Host setup, when the host is already setup. - in shell, move inventory check to a method so it can be mocked. Change-Id: I63cb1afd5313959a6fdda11e9c2b03317c60197a --- .zuul.yaml | 2 + kolla_cli/commands/host.py | 25 +-- kolla_cli/shell.py | 5 +- kolla_cli/tests/unit/common.py | 32 ++++ kolla_cli/tests/unit/test_host_cmd.py | 225 ++++++++++++++++++++++++++ tox.ini | 5 +- 6 files changed, 281 insertions(+), 13 deletions(-) create mode 100644 kolla_cli/tests/unit/common.py create mode 100644 kolla_cli/tests/unit/test_host_cmd.py diff --git a/.zuul.yaml b/.zuul.yaml index e14019c..399f4db 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -40,12 +40,14 @@ check: jobs: - openstack-tox-pep8 + - openstack-tox-py27 - kollacli-tox-mypy - kollacli-tox-functional - kollacli-tox-functional-py35 gate: jobs: - openstack-tox-pep8 + - openstack-tox-py27 - kollacli-tox-mypy - kollacli-tox-functional - kollacli-tox-functional-py35 diff --git a/kolla_cli/commands/host.py b/kolla_cli/commands/host.py index 70c70f6..9ac9e47 100644 --- a/kolla_cli/commands/host.py +++ b/kolla_cli/commands/host.py @@ -90,15 +90,10 @@ class HostDestroy(Command): if parsed_args.removeimages: remove_images = True - if include_data: - question = ('This will delete all containers and data' - ', are you sure? (y/n)') - answer = raw_input(question) - while answer != 'y' and answer != 'n': - answer = raw_input(question) - if answer is 'n': - LOG.info('Aborting destroy') - return + if include_data and not self._is_ok_to_delete_data(): + LOG.info('Aborting destroy') + return + verbose_level = self.app.options.verbose_level job = CLIENT.host_destroy(hostnames, destroy_type, @@ -123,6 +118,14 @@ class HostDestroy(Command): except Exception as e: raise Exception(traceback.format_exc()) + def _is_ok_to_delete_data(self): + question = ('This will delete all containers and data' + ', are you sure? (y/n)') + answer = raw_input(question) + while answer != 'y' and answer != 'n': + answer = raw_input(question) + return True if answer == 'y' else False + class HostRemove(Command): """Remove host from openstack-kolla.""" @@ -231,7 +234,7 @@ class HostCheck(Command): status = u._('success') msg = '' if not info['success']: - status = u._('failed- ') + status = u._('failed-') msg = info['msg'] all_ok = False LOG.info(u._('Host {host}: {sts} {msg}') @@ -280,7 +283,7 @@ class HostSetup(Command): LOG.info( u._LI('Skipping setup of host ({host}) as ' 'ssh check is ok.').format(host=hostname)) - return True + return 0 if parsed_args.insecure: password = parsed_args.insecure.strip() diff --git a/kolla_cli/shell.py b/kolla_cli/shell.py index e7c4117..ad70015 100755 --- a/kolla_cli/shell.py +++ b/kolla_cli/shell.py @@ -41,7 +41,7 @@ class KollaCli(App): inventory_path = os.path.join(get_kolla_cli_etc(), INVENTORY_PATH) - if os.path.isfile(inventory_path) is False: + if not self._is_inventory_present(inventory_path): err_string = u._( 'Required file ({inventory}) does not exist.\n' 'Please re-install the kollacli to ' @@ -57,6 +57,9 @@ class KollaCli(App): self.dump_stack_trace = False + def _is_inventory_present(self, inventory_path): + return os.path.isfile(inventory_path) + def main(argv=sys.argv[1:]): shell = KollaCli() diff --git a/kolla_cli/tests/unit/common.py b/kolla_cli/tests/unit/common.py new file mode 100644 index 0000000..49e8f52 --- /dev/null +++ b/kolla_cli/tests/unit/common.py @@ -0,0 +1,32 @@ +# Copyright (c) 2018 OpenStack Foundation +# +# 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 testtools + +from kolla_cli.api.host import Host +from kolla_cli.common.ansible.job import AnsibleJob +from kolla_cli import shell + + +class KollaCliUnitTest(testtools.TestCase): + + def run_cli_command(self, command_string): + # return 0 if command succeeded, non-0 if failed + args = command_string.split() + return shell.main(args) + + def get_fake_job(self): + return AnsibleJob(None, None, None, None) + + def get_fake_host(self, hostname='foo'): + return Host(hostname) diff --git a/kolla_cli/tests/unit/test_host_cmd.py b/kolla_cli/tests/unit/test_host_cmd.py new file mode 100644 index 0000000..0389aae --- /dev/null +++ b/kolla_cli/tests/unit/test_host_cmd.py @@ -0,0 +1,225 @@ +# Copyright (c) 2018 OpenStack Foundation +# +# 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 + +from kolla_cli.tests.unit.common import KollaCliUnitTest + + +class TestUnit(KollaCliUnitTest): + @mock.patch('kolla_cli.api.client.ClientApi.host_add') + @mock.patch('kolla_cli.shell.KollaCli._is_inventory_present', + return_value=True) + def test_host_add(self, _, mock_add): + hostname = 'foo' + ret = self.run_cli_command('host add %s' % hostname) + self.assertEqual(ret, 0) + mock_add.assert_called_once_with([hostname]) + + @mock.patch('kolla_cli.api.client.ClientApi.host_remove') + @mock.patch('kolla_cli.shell.KollaCli._is_inventory_present', + return_value=True) + def test_host_remove(self, _, mock_remove): + hostname = 'foo' + ret = self.run_cli_command('host remove %s' % hostname) + self.assertEqual(ret, 0) + mock_remove.assert_called_once_with([hostname]) + + @mock.patch('kolla_cli.api.client.ClientApi.host_get_all') + @mock.patch('kolla_cli.api.client.ClientApi.host_get') + @mock.patch('kolla_cli.shell.KollaCli._is_inventory_present', + return_value=True) + def test_host_list(self, _, mock_get, mock_get_all): + # get all hosts + mock_get_all.return_value = [] + ret = self.run_cli_command('host list') + self.assertEqual(ret, 0) + mock_get_all.assert_called_once_with() + + # get a specific host + hostname = 'foo' + mock_get.return_value = [] + ret = self.run_cli_command('host list %s' % hostname) + self.assertEqual(ret, 0) + mock_get.assert_called_once_with([hostname]) + + @mock.patch('kolla_cli.commands.host.HostDestroy._is_ok_to_delete_data', + return_value='y') + @mock.patch('kolla_cli.common.ansible.job.AnsibleJob.get_status') + @mock.patch('kolla_cli.api.client.ClientApi.host_get_all') + @mock.patch('kolla_cli.api.client.ClientApi.host_destroy') + @mock.patch('kolla_cli.shell.KollaCli._is_inventory_present', + return_value=True) + def test_host_destroy(self, _, mock_destroy, mock_get_all, + mock_get_status, mock_prompt): + hostname = 'foo' + mock_get_all.return_value = [self.get_fake_host(hostname)] + mock_destroy.return_value = self.get_fake_job() + mock_get_status.return_value = 0 + + # default destroy hostname + ret = self.run_cli_command('host destroy %s' % hostname) + self.assertEqual(ret, 0) + mock_destroy.assert_called_once_with([hostname], 'kill', 1, + False, False) + # destroy all + mock_destroy.reset_mock() + ret = self.run_cli_command('host destroy all') + self.assertEqual(ret, 0) + mock_destroy.assert_called_once_with([hostname], 'kill', 1, + False, False) + # destroy --stop + mock_destroy.reset_mock() + ret = self.run_cli_command('host destroy %s --stop' % hostname) + self.assertEqual(ret, 0) + mock_destroy.assert_called_once_with([hostname], 'stop', 1, + False, False) + # destroy --includedata + mock_destroy.reset_mock() + ret = self.run_cli_command('host destroy %s --includedata' % hostname) + self.assertEqual(ret, 0) + mock_destroy.assert_called_once_with([hostname], 'kill', 1, + True, False) + + # destroy --removeimages + mock_destroy.reset_mock() + ret = self.run_cli_command('host destroy %s --removeimages' + % hostname) + self.assertEqual(ret, 0) + mock_destroy.assert_called_once_with([hostname], 'kill', 1, + False, True) + + @mock.patch('kolla_cli.commands.host.LOG.info') + @mock.patch('kolla_cli.api.client.ClientApi.host_get_all') + @mock.patch('kolla_cli.api.client.ClientApi.host_ssh_check') + @mock.patch('kolla_cli.shell.KollaCli._is_inventory_present', + return_value=True) + def test_host_ssh_check(self, _, mock_ssh_check, mock_get_all, mock_log): + hostname = 'foo' + check_ok_response = {hostname: {'success': True}} + check_bad_response = {hostname: {'success': False, 'msg': 'FAILED'}} + mock_get_all.return_value = [self.get_fake_host(hostname)] + + # host check hostname (success) + mock_ssh_check.return_value = check_ok_response + ret = self.run_cli_command('host check %s' % hostname) + self.assertEqual(ret, 0) + mock_ssh_check.assert_called_once_with([hostname]) + mock_log.assert_called_once_with('Host %s: success ' % hostname) + + # host check all (success) + mock_ssh_check.reset_mock() + mock_log.reset_mock() + mock_ssh_check.return_value = check_ok_response + ret = self.run_cli_command('host check all') + self.assertEqual(ret, 0) + mock_ssh_check.assert_called_once_with([hostname]) + mock_log.assert_called_once_with('Host %s: success ' % hostname) + + # host check hostname (fail) + mock_ssh_check.reset_mock() + mock_log.reset_mock() + mock_ssh_check.return_value = check_bad_response + ret = self.run_cli_command('host check %s' % hostname) + self.assertEqual(ret, 1) + mock_ssh_check.assert_called_once_with([hostname]) + mock_log.assert_called_once_with('Host %s: failed- FAILED' % hostname) + + @mock.patch('kolla_cli.common.ansible.job.AnsibleJob.get_status') + @mock.patch('kolla_cli.commands.host.ClientApi.host_precheck') + @mock.patch('kolla_cli.api.client.ClientApi.host_get_all') + @mock.patch('kolla_cli.shell.KollaCli._is_inventory_present', + return_value=True) + def test_host_precheck(self, _, mock_get_all, mock_precheck, + mock_get_status): + hostname = 'foo' + mock_get_all.return_value = [self.get_fake_host(hostname)] + + # host check hostname --predeploy (success) + mock_precheck.return_value = self.get_fake_job() + mock_get_status.return_value = 0 + ret = self.run_cli_command('host check %s --predeploy' % hostname) + self.assertEqual(ret, 0) + mock_precheck.assert_called_once_with([hostname], 1) + + # host check hostname --predeploy (fail) + mock_precheck.reset_mock() + mock_get_status.return_value = 1 + ret = self.run_cli_command('host check %s --predeploy' % hostname) + self.assertEqual(ret, 1) + mock_precheck.assert_called_once_with([hostname], 1) + + @mock.patch('kolla_cli.commands.host.HostSetup._get_yml_data') + @mock.patch('getpass.getpass') + @mock.patch('kolla_cli.commands.host.ClientApi.host_ssh_check') + @mock.patch('kolla_cli.commands.host.ClientApi.host_setup') + @mock.patch('kolla_cli.api.client.ClientApi.host_get_all') + @mock.patch('kolla_cli.shell.KollaCli._is_inventory_present', + return_value=True) + def test_host_setup(self, _, mock_get_all, mock_setup, mock_ssh_check, + mock_passwd, mock_yml): + password = 'PASSWORD' + hostname = 'foo' + mock_get_all.return_value = [self.get_fake_host(hostname)] + mock_passwd.return_value = password + + # single host setup (host not yet setup) + mock_ssh_check.return_value = {hostname: {'success': False}} + ret = self.run_cli_command('host setup %s' % hostname) + self.assertEqual(ret, 0) + mock_ssh_check.assert_called_once_with([hostname]) + mock_setup.assert_called_once_with({hostname: {'password': password}}) + + # single host setup --insecure (host already setup) + mock_ssh_check.reset_mock() + mock_setup.reset_mock() + mock_ssh_check.return_value = {hostname: {'success': True}} + ret = self.run_cli_command('host setup %s --insecure %s' + % (hostname, password)) + self.assertEqual(ret, 0) + mock_ssh_check.assert_called_once_with([hostname]) + mock_setup.assert_not_called() + + # multi-host setup + mock_ssh_check.reset_mock() + mock_setup.reset_mock() + fake_path = '/bogus' + mock_yml.return_value = {hostname: {'password': password}} + ret = self.run_cli_command('host setup --file %s' % fake_path) + self.assertEqual(ret, 0) + mock_setup.assert_called_once_with({hostname: {'password': password}}) + mock_yml.assert_called_once_with(fake_path) + mock_ssh_check.assert_not_called() + + @mock.patch('kolla_cli.common.ansible.job.AnsibleJob.get_status') + @mock.patch('kolla_cli.api.client.ClientApi.host_get_all') + @mock.patch('kolla_cli.api.client.ClientApi.host_stop') + @mock.patch('kolla_cli.shell.KollaCli._is_inventory_present', + return_value=True) + def test_host_stop(self, _, mock_stop, mock_get_all, + mock_get_status): + hostname = 'foo' + mock_get_all.return_value = [self.get_fake_host(hostname)] + mock_get_status.return_value = 0 + mock_stop.return_value = self.get_fake_job() + + # host stop hostname + ret = self.run_cli_command('host stop %s' % hostname) + self.assertEqual(ret, 0) + mock_stop.assert_called_once_with([hostname], 1) + + # host stop all + mock_stop.reset_mock() + ret = self.run_cli_command('host stop all') + self.assertEqual(ret, 0) + mock_stop.assert_called_once_with([hostname], 1) diff --git a/tox.ini b/tox.ini index 776fd4b..87f7547 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 1.6 skipsdist = True -envlist = pep8,mypy,functional,functional-py35 +envlist = pep8,mypy,functional,functional-py35,py27 [testenv] usedevelop=True @@ -17,6 +17,9 @@ commands = find . -type f -name "*.py[c|o]" -delete find . -type d -name "__pycache__" -delete +[testenv:py27] +commands = ostestr {posargs} + [testenv:functional] whitelist_externals = {[testenv]whitelist_externals}