diff --git a/fuelclient/__init__.py b/fuelclient/__init__.py index 57974642..07d1bc0c 100644 --- a/fuelclient/__init__.py +++ b/fuelclient/__init__.py @@ -67,6 +67,7 @@ def get_client(resource, version='v1', connection=None): 'extension': v1.extension, 'fuel-version': v1.fuelversion, 'graph': v1.graph, + 'health': v1.health, 'network-configuration': v1.network_configuration, 'network-group': v1.network_group, 'node': v1.node, diff --git a/fuelclient/cli/formatting.py b/fuelclient/cli/formatting.py index ad663735..7788b97a 100644 --- a/fuelclient/cli/formatting.py +++ b/fuelclient/cli/formatting.py @@ -116,6 +116,7 @@ def quote_and_join(words): return '"{0}"'.format(words[0]) +# TODO(vkulanov): remove when deprecate old cli def print_health_check(env): tests_states = [{"status": "not finished"}] finished_tests = set() diff --git a/fuelclient/client.py b/fuelclient/client.py index ff8138ca..36a77720 100644 --- a/fuelclient/client.py +++ b/fuelclient/client.py @@ -162,14 +162,15 @@ class APIClient(object): return self._decode_content(resp) - def put_request(self, api, data, **params): + def put_request(self, api, data, ostf=False, **params): """Make PUT request to specific API with some data. :param api: API endpoint (path) :param data: Data send in request, will be serialized to JSON + :param ostf: is this a call to OSTF API :param params: Params of query string """ - url = self.api_root + api + url = (self.ostf_root if ostf else self.api_root) + api data_json = json.dumps(data) resp = self.session.put(url, data=data_json, params=params) diff --git a/fuelclient/commands/health.py b/fuelclient/commands/health.py new file mode 100644 index 00000000..64c0b1d5 --- /dev/null +++ b/fuelclient/commands/health.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 abc +import six + +from fuelclient.commands import base +from fuelclient.common import data_utils + + +class HealthMixIn(object): + + entity_name = 'health' + + +class HealthTestSetsList(HealthMixIn, base.BaseListCommand): + """List of all available test sets for a given environment.""" + + columns = ("id", + "name") + + filters = {'environment_id': 'env'} + + def get_parser(self, prog_name): + parser = super(HealthTestSetsList, self).get_parser(prog_name) + parser.add_argument('-e', + '--env', + type=int, + required=True, + help='Id of the environment.') + return parser + + +class HealthCheckStart(HealthMixIn, base.BaseListCommand): + """Run specified test sets for a given environment.""" + + columns = ("id", + "testset", + "cluster_id") + + def get_parser(self, prog_name): + parser = super(HealthCheckStart, self).get_parser(prog_name) + parser.add_argument('-e', + '--env', + type=int, + required=True, + help='Id of the environment.') + parser.add_argument('--force', + action='store_true', + help='Force run health test sets.') + parser.add_argument('-t', + '--tests', + nargs='+', + help='Name of the test sets to run.') + parser.add_argument('--ostf-username', + default=None, + help='OSTF username.') + parser.add_argument('--ostf-password', + default=None, + help='OSTF password.') + parser.add_argument('--ostf-tenant-name', + default=None, + help='OSTF tenant name.') + return parser + + def take_action(self, parsed_args): + ostf_credentials = {} + if parsed_args.ostf_tenant_name is not None: + ostf_credentials['tenant'] = parsed_args.ostf_tenant_name + if parsed_args.ostf_username is not None: + ostf_credentials['username'] = parsed_args.ostf_username + if parsed_args.ostf_password is not None: + ostf_credentials['password'] = parsed_args.ostf_password + + if not ostf_credentials: + self.app.stdout.write("WARNING: ostf credentials are going to be " + "mandatory in the next release.\n") + + data = self.client.start(parsed_args.env, + ostf_credentials=ostf_credentials, + test_sets=parsed_args.tests, + force=parsed_args.force) + + msg = ("\nHealth check tests for environment with id {0} has been " + "started:\n".format(parsed_args.env)) + self.app.stdout.write(msg) + data = data_utils.get_display_data_multi(self.columns, data) + return self.columns, data + + +@six.add_metaclass(abc.ABCMeta) +class HealthCheckBaseAction(HealthMixIn, base.BaseShowCommand): + """Base class for implementing action over a given test set.""" + + columns = ("id", + "testset", + "cluster_id", + "status") + + @abc.abstractproperty + def action_status(self): + """String with the name of the action.""" + pass + + def take_action(self, parsed_args): + data = self.client.action(parsed_args.id, self.action_status) + + data = data_utils.get_display_data_single(self.columns, data) + return self.columns, data + + +class HealthCheckStop(HealthCheckBaseAction): + """Stop test set with given id.""" + + action_status = "stopped" + + +class HealthCheckRestart(HealthCheckBaseAction): + """Restart test set with given id.""" + + action_status = "restarted" + + +class HealthTestSetsStatusList(HealthMixIn, base.BaseListCommand): + """Show list of statuses of all test sets ever been executed in Fuel.""" + + columns = ("id", + "testset", + "cluster_id", + "status", + "started_at", + "ended_at") + + def get_parser(self, prog_name): + parser = super(HealthTestSetsStatusList, self).get_parser(prog_name) + parser.add_argument('-e', + '--env', + type=int, + help='Id of the environment.') + return parser + + def take_action(self, parsed_args): + data = self.client.get_status_all(parsed_args.env) + + data = data_utils.get_display_data_multi(self.columns, data) + return self.columns, data + + +class HealthTestSetsStatusShow(HealthMixIn, base.BaseShowCommand): + """Show status about a test set with given id.""" + + columns = ("id", + "testset", + "cluster_id", + "status", + "started_at", + "ended_at", + "tests") + + def take_action(self, parsed_args): + data = self.client.get_status_single(parsed_args.id) + + data = data_utils.get_display_data_single(self.columns, data) + return self.columns, data diff --git a/fuelclient/objects/__init__.py b/fuelclient/objects/__init__.py index 69f99cde..be03f9d3 100644 --- a/fuelclient/objects/__init__.py +++ b/fuelclient/objects/__init__.py @@ -19,6 +19,7 @@ functionality from nailgun objects. from fuelclient.objects.base import BaseObject from fuelclient.objects.environment import Environment from fuelclient.objects.extension import Extension +from fuelclient.objects.health import Health from fuelclient.objects.node import Node from fuelclient.objects.node import NodeCollection from fuelclient.objects.openstack_config import OpenstackConfig diff --git a/fuelclient/objects/environment.py b/fuelclient/objects/environment.py index 4bc40116..a60978f9 100644 --- a/fuelclient/objects/environment.py +++ b/fuelclient/objects/environment.py @@ -398,6 +398,7 @@ class Environment(BaseObject): "nodes": node_facts } + # TODO(vkulanov): remove method when deprecate old cli def get_testsets(self): return self.connection.get_request( 'testsets/{0}'.format(self.id), @@ -409,9 +410,11 @@ class Environment(BaseObject): data = self.get_fresh_data() return data["is_customized"] + # TODO(vkulanov): remove method when deprecate old cli def is_in_running_test_sets(self, test_set): return test_set["testset"] in self._test_sets_to_run + # TODO(vkulanov): remove method when deprecate old cli def run_test_sets(self, test_sets_to_run, ostf_credentials=None): self._test_sets_to_run = test_sets_to_run @@ -440,6 +443,7 @@ class Environment(BaseObject): self._testruns_ids = [tr['id'] for tr in testruns] return testruns + # TODO(vkulanov): remove method when deprecate old cli def get_state_of_tests(self): return [ self.connection.get_request( diff --git a/fuelclient/objects/health.py b/fuelclient/objects/health.py new file mode 100644 index 00000000..1592cbd2 --- /dev/null +++ b/fuelclient/objects/health.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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. + +from fuelclient.objects.base import BaseObject + + +class Health(BaseObject): + + class_api_path = "testruns/" + instance_api_path = "testruns/{0}/" + test_sets_api_path = "testsets/{0}/" + + @classmethod + def get_test_sets(cls, environment_id): + return cls.connection.get_request( + cls.test_sets_api_path.format(environment_id), + ostf=True + ) + + @classmethod + def get_tests_status_all(cls): + return cls.connection.get_request(cls.class_api_path, ostf=True) + + def get_tests_status_single(self): + return self.connection.get_request( + self.instance_api_path.format(self.id), + ostf=True + ) + + @classmethod + def get_last_tests_status(cls, environment_id): + return cls.connection.get_request( + 'testruns/last/{0}'.format(environment_id), + ostf=True + ) + + @classmethod + def run_test_sets(cls, environment_id, test_sets_to_run, + ostf_credentials=None): + + def make_test_set(name): + result = { + "testset": name, + "metadata": { + "config": {}, + "cluster_id": environment_id, + } + } + if ostf_credentials: + creds = result['metadata'].setdefault( + 'ostf_os_access_creds', {}) + if 'tenant' in ostf_credentials: + creds['ostf_os_tenant_name'] = ostf_credentials['tenant'] + if 'username' in ostf_credentials: + creds['ostf_os_username'] = ostf_credentials['username'] + if 'password' in ostf_credentials: + creds['ostf_os_password'] = ostf_credentials['password'] + return result + + tests_data = [make_test_set(ts) for ts in test_sets_to_run] + test_runs = cls.connection.post_request(cls.class_api_path, + tests_data, + ostf=True) + return test_runs + + def action_test(self, action_status): + data = [{ + "id": self.id, + "status": action_status + }] + return self.connection.put_request( + 'testruns/', data, ostf=True + ) diff --git a/fuelclient/tests/unit/v2/cli/test_health.py b/fuelclient/tests/unit/v2/cli/test_health.py new file mode 100644 index 00000000..346b96c5 --- /dev/null +++ b/fuelclient/tests/unit/v2/cli/test_health.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 fuelclient.tests.unit.v2.cli import test_engine +from fuelclient.tests.utils import fake_health + + +class TestHealthCommand(test_engine.BaseCLITest): + """Tests for fuel2 health * commands.""" + + def test_health_list_for_cluster(self): + self.m_client.get_all.return_value = fake_health.get_fake_test_sets(10) + cluster_id = 45 + args = 'health list -e {id}'.format(id=cluster_id) + self.exec_command(args) + self.m_client.get_all.assert_called_once_with( + environment_id=cluster_id) + self.m_get_client.assert_called_once_with('health', mock.ANY) + + @mock.patch('sys.stderr') + def test_health_list_for_cluster_fail(self, mocked_stderr): + args = 'health list' + self.assertRaises(SystemExit, self.exec_command, args) + self.assertIn('-e/--env', + mocked_stderr.write.call_args_list[-1][0][0]) + + def test_health_status_list(self): + self.m_client.get_status_all.return_value = [ + fake_health.get_fake_test_set_item(testset_id=12, cluster_id=30), + fake_health.get_fake_test_set_item(testset_id=13, cluster_id=32), + fake_health.get_fake_test_set_item(testset_id=14, cluster_id=35) + ] + args = 'health status list' + self.exec_command(args) + self.m_client.get_status_all.assert_called_once_with(None) + self.m_get_client.assert_called_once_with('health', mock.ANY) + + def test_health_status_list_for_cluster(self): + cluster_id = 45 + self.m_client.get_status_all.return_value = [ + fake_health.get_fake_test_set_item(testset_id=12, + cluster_id=cluster_id), + fake_health.get_fake_test_set_item(testset_id=13, + cluster_id=cluster_id), + fake_health.get_fake_test_set_item(testset_id=14, + cluster_id=cluster_id) + ] + args = 'health status list -e {id}'.format(id=cluster_id) + self.exec_command(args) + self.m_client.get_status_all.assert_called_once_with(cluster_id) + self.m_get_client.assert_called_once_with('health', mock.ANY) + + def test_health_status_show(self): + testset_id = 66 + self.m_client.get_status_single.return_value = \ + fake_health.get_fake_test_set_item(testset_id=testset_id) + args = 'health status show {id}'.format(id=testset_id) + self.exec_command(args) + self.m_client.get_status_single.assert_called_once_with(testset_id) + self.m_get_client.assert_called_once_with('health', mock.ANY) + + @mock.patch('sys.stderr') + def test_health_status_show_fail(self, mocked_stderr): + args = 'health status show' + self.assertRaises(SystemExit, self.exec_command, args) + self.assertIn('id', mocked_stderr.write.call_args_list[0][0][0]) + + def test_health_start_force(self): + cluster_id = 45 + testset = ['fake_test_set1', 'fake_test_set2'] + args = 'health start -e {id} -t {testset} --force'.format( + id=cluster_id, testset=' '.join(testset)) + self.exec_command(args) + self.m_client.start.assert_called_once_with(cluster_id, + ostf_credentials={}, + test_sets=testset, + force=True) + self.m_get_client.assert_called_once_with('health', mock.ANY) + + @mock.patch('sys.stderr') + def test_health_start_w_wrong_parameters(self, mocked_stderr): + args = 'health start' + self.assertRaises(SystemExit, self.exec_command, args) + self.assertIn('-e/--env', + mocked_stderr.write.call_args_list[-1][0][0]) + + def test_health_start_wo_force(self): + cluster_id = 45 + testset = ['fake_test_set1', 'fake_test_set2'] + args = 'health start -e {id} -t {testset}'.format( + id=cluster_id, testset=' '.join(testset)) + self.exec_command(args) + self.m_client.start.assert_called_once_with(cluster_id, + ostf_credentials={}, + test_sets=testset, + force=False) + self.m_get_client.assert_called_once_with('health', mock.ANY) + + def test_health_start_all_wo_force(self): + cluster_id = 45 + args = 'health start -e {id}'.format(id=cluster_id) + self.exec_command(args) + self.m_client.start.assert_called_once_with(cluster_id, + ostf_credentials={}, + test_sets=None, + force=False) + self.m_get_client.assert_called_once_with('health', mock.ANY) + + def test_health_start_force_w_ostf_credentials(self): + cluster_id = 45 + testset = ['fake_test_set1', 'fake_test_set2'] + ostf_credentials = {'username': 'fake_user', + 'password': 'fake_password', + 'tenant': 'fake_tenant_name'} + + args = ('health start -e {id} -t {testset} --force --ostf-username ' + 'fake_user --ostf-password fake_password --ostf-tenant-name ' + 'fake_tenant_name'.format(id=cluster_id, + testset=' '.join(testset))) + + self.exec_command(args) + self.m_client.start.assert_called_once_with( + cluster_id, + ostf_credentials=ostf_credentials, + test_sets=testset, + force=True + ) + self.m_get_client.assert_called_once_with('health', mock.ANY) + + def test_health_stop(self): + testset_id = 66 + args = 'health stop {id}'.format(id=testset_id) + self.exec_command(args) + self.m_client.action.assert_called_once_with(testset_id, 'stopped') + self.m_get_client.assert_called_once_with('health', mock.ANY) + + @mock.patch('sys.stderr') + def test_health_stop_fail(self, mocked_stderr): + args = 'health stop' + self.assertRaises(SystemExit, self.exec_command, args) + self.assertIn('id', mocked_stderr.write.call_args_list[0][0][0]) + + def test_health_restart(self): + testset_id = 66 + args = 'health restart {id}'.format(id=testset_id) + self.exec_command(args) + self.m_client.action.assert_called_once_with(testset_id, 'restarted') + self.m_get_client.assert_called_once_with('health', mock.ANY) + + @mock.patch('sys.stderr') + def test_health_restart_fail(self, mocked_stderr): + args = 'health restart' + self.assertRaises(SystemExit, self.exec_command, args) + self.assertIn('id', mocked_stderr.write.call_args_list[0][0][0]) diff --git a/fuelclient/tests/unit/v2/lib/test_health.py b/fuelclient/tests/unit/v2/lib/test_health.py new file mode 100644 index 00000000..c9ebb4d4 --- /dev/null +++ b/fuelclient/tests/unit/v2/lib/test_health.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 fuelclient +from fuelclient.cli import error +from fuelclient.tests.unit.v2.lib import test_api +from fuelclient.tests import utils + + +class TestHealthFacade(test_api.BaseLibTest): + + def setUp(self): + super(TestHealthFacade, self).setUp() + + self.version = 'v1' + self.res_uri = '/ostf/' + self.fake_test_sets = utils.get_fake_test_sets(10) + self.fake_test_sets_items = utils.get_fake_test_set_items(10) + + self.client = fuelclient.get_client('health', self.version) + + def test_health_list_for_cluster(self): + cluster_id = 65 + expected_uri = self.res_uri + 'testsets/{0}/'.format(cluster_id) + matcher = self.m_request.get(expected_uri, json=self.fake_test_sets) + + data = self.client.get_all(cluster_id) + + self.assertTrue(matcher.called) + self.assertEqual(len(data), 10) + + def test_health_status_list(self): + expected_uri = self.res_uri + 'testruns/' + matcher = self.m_request.get(expected_uri, + json=self.fake_test_sets_items) + + data = self.client.get_status_all() + + self.assertTrue(matcher.called) + self.assertEqual(len(data), 10) + + def test_health_status_list_for_cluster(self): + cluster_id = 32 + expected_uri = self.res_uri + 'testruns/' + fake_test_sets_items = [ + utils.get_fake_test_set_item(testset_id=12, cluster_id=cluster_id), + utils.get_fake_test_set_item(testset_id=13, cluster_id=cluster_id), + utils.get_fake_test_set_item(testset_id=14, cluster_id=35) + ] + matcher = self.m_request.get(expected_uri, + json=fake_test_sets_items) + + data = self.client.get_status_all(cluster_id) + + self.assertTrue(matcher.called) + self.assertEqual(len(data), 2) + + def test_health_status_show(self): + testrun_id = 65 + cluster_id = 32 + fake_test_set_item = utils.get_fake_test_set_item( + testset_id=testrun_id, cluster_id=cluster_id) + expected_uri = self.get_object_uri(self.res_uri + 'testruns/', + testrun_id) + matcher = self.m_request.get(expected_uri, + json=fake_test_set_item) + + data = self.client.get_status_single(testrun_id) + + self.assertTrue(matcher.called) + self.assertEqual(testrun_id, data["id"]) + self.assertEqual(cluster_id, data["cluster_id"]) + + def test_health_status_show_non_existing_testrun(self): + testrun_id = 65 + expected_uri = self.get_object_uri(self.res_uri + 'testruns/', + testrun_id) + matcher = self.m_request.get(expected_uri, json={}) + + msg = "Test sets with id {id} does not exist".format(id=testrun_id) + self.assertRaisesRegexp(error.ActionException, + msg, + self.client.get_status_single, + testrun_id) + self.assertTrue(matcher.called) + + @mock.patch('fuelclient.objects.Environment') + def test_health_start(self, m_env_obj): + cluster_id = 32 + cluster_state = 'operational' + test_sets = ['fake_test_set1', 'fake_test_set2'] + expected_uri = self.res_uri + 'testruns/' + type(m_env_obj.return_value).status = mock.PropertyMock( + return_value=cluster_state) + type(m_env_obj.return_value).is_customized = mock.PropertyMock( + return_value=False) + expected_body = [ + {'testset': test_sets[0], + 'metadata': {'cluster_id': cluster_id, 'config': {}}}, + {'testset': test_sets[1], + 'metadata': {'cluster_id': cluster_id, 'config': {}}} + ] + matcher = self.m_request.post(expected_uri, json=expected_body) + + data = self.client.start(cluster_id, + ostf_credentials={}, + test_sets=test_sets, + force=False) + self.assertTrue(matcher.called) + self.assertEqual(expected_body, matcher.last_request.json()) + self.assertEqual(data, matcher.last_request.json()) + + @mock.patch('fuelclient.objects.Environment') + def test_health_start_fail_not_allowed_env_status(self, m_env_obj): + cluster_id = 32 + cluster_state = 'new' + test_sets = ['fake_test_set1', 'fake_test_set2'] + type(m_env_obj.return_value).status = mock.PropertyMock( + return_value=cluster_state) + + msg = ("Environment is not ready to run health check " + "because it is in '{0}' state.".format(cluster_state)) + self.assertRaisesRegexp(error.EnvironmentException, + msg, + self.client.start, + cluster_id, + ostf_credentials={}, + test_sets=test_sets, + force=False) + + @mock.patch('fuelclient.objects.Environment') + def test_health_start_not_allowed_env_status_w_force(self, m_env_obj): + cluster_id = 32 + cluster_state = 'new' + test_sets = ['fake_test_set1', 'fake_test_set2'] + expected_uri = self.res_uri + 'testruns/' + type(m_env_obj.return_value).status = mock.PropertyMock( + return_value=cluster_state) + type(m_env_obj.return_value).is_customized = mock.PropertyMock( + return_value=False) + expected_body = [ + {'testset': test_sets[0], + 'metadata': {'cluster_id': cluster_id, 'config': {}}}, + {'testset': test_sets[1], + 'metadata': {'cluster_id': cluster_id, 'config': {}}} + ] + matcher = self.m_request.post(expected_uri, json=expected_body) + + data = self.client.start(cluster_id, + ostf_credentials={}, + test_sets=test_sets, + force=True) + self.assertTrue(matcher.called) + self.assertEqual(expected_body, matcher.last_request.json()) + self.assertEqual(data, matcher.last_request.json()) + + @mock.patch('fuelclient.objects.Environment') + def test_health_start_fail_customized_env(self, m_env_obj): + cluster_id = 32 + cluster_state = 'operational' + is_customized = True + test_sets = ['fake_test_set1', 'fake_test_set2'] + type(m_env_obj.return_value).status = mock.PropertyMock( + return_value=cluster_state) + type(m_env_obj.return_value).is_customized = mock.PropertyMock( + return_value=is_customized) + msg = ("Environment deployment facts were updated. " + "Health check is likely to fail because of that.") + self.assertRaisesRegexp(error.EnvironmentException, + msg, + self.client.start, + cluster_id, + ostf_credentials={}, + test_sets=test_sets, + force=False) + + @mock.patch('fuelclient.objects.Environment') + def test_health_start_customized_env_w_force(self, m_env_obj): + cluster_id = 32 + cluster_state = 'operational' + is_customized = True + test_sets = ['fake_test_set1', 'fake_test_set2'] + expected_uri = self.res_uri + 'testruns/' + type(m_env_obj.return_value).status = mock.PropertyMock( + return_value=cluster_state) + type(m_env_obj.return_value).is_customized = mock.PropertyMock( + return_value=is_customized) + expected_body = [ + {'testset': test_sets[0], + 'metadata': {'cluster_id': cluster_id, 'config': {}}}, + {'testset': test_sets[1], + 'metadata': {'cluster_id': cluster_id, 'config': {}}} + ] + matcher = self.m_request.post(expected_uri, json=expected_body) + + data = self.client.start(cluster_id, + ostf_credentials={}, + test_sets=test_sets, + force=True) + self.assertTrue(matcher.called) + self.assertEqual(expected_body, matcher.last_request.json()) + self.assertEqual(data, matcher.last_request.json()) + + @mock.patch('fuelclient.objects.Environment') + def test_health_start_w_ostf_credentials(self, m_env_obj): + cluster_id = 32 + cluster_state = 'operational' + is_customized = False + test_sets = ['fake_test_set1', 'fake_test_set2'] + ostf_credentials = { + 'username': 'admin', + 'password': 'admin', + 'tenant': 'admin' + } + expected_uri = self.res_uri + 'testruns/' + type(m_env_obj.return_value).status = mock.PropertyMock( + return_value=cluster_state) + type(m_env_obj.return_value).is_customized = mock.PropertyMock( + return_value=is_customized) + expected_body = [ + {'testset': test_sets[0], + 'metadata': { + 'ostf_os_access_creds': + {'ostf_os_username': 'admin', + 'ostf_os_tenant_name': 'admin', + 'ostf_os_password': 'admin'}, + 'cluster_id': cluster_id, + 'config': {}}}, + {'testset': test_sets[1], + 'metadata': { + 'ostf_os_access_creds': + {'ostf_os_username': 'admin', + 'ostf_os_tenant_name': 'admin', + 'ostf_os_password': 'admin'}, + 'cluster_id': cluster_id, + 'config': {}}} + ] + + matcher = self.m_request.post(expected_uri, json=expected_body) + + data = self.client.start(cluster_id, + ostf_credentials=ostf_credentials, + test_sets=test_sets, + force=False) + self.assertTrue(matcher.called) + self.assertEqual(expected_body, matcher.last_request.json()) + self.assertEqual(data, matcher.last_request.json()) + + def test_health_stop_action(self): + testrun_id = 65 + cluster_id = 32 + expected_uri = self.res_uri + 'testruns/' + fake_test_set_item = utils.get_fake_test_set_item( + testset_id=testrun_id, cluster_id=cluster_id) + matcher = self.m_request.put(expected_uri, + json=[fake_test_set_item]) + + data = self.client.action(testrun_id, action_status='stopped') + + self.assertTrue(matcher.called) + self.assertEqual(testrun_id, data["id"]) + self.assertEqual(cluster_id, data["cluster_id"]) + + def test_health_restart_action(self): + testrun_id = 65 + cluster_id = 32 + expected_uri = self.res_uri + 'testruns/' + fake_test_set_item = utils.get_fake_test_set_item( + testset_id=testrun_id, cluster_id=cluster_id) + matcher = self.m_request.put(expected_uri, + json=[fake_test_set_item]) + + data = self.client.action(testrun_id, action_status='restarted') + + self.assertTrue(matcher.called) + self.assertEqual(testrun_id, data["id"]) + self.assertEqual(cluster_id, data["cluster_id"]) diff --git a/fuelclient/tests/utils/__init__.py b/fuelclient/tests/utils/__init__.py index 9c5f5cda..ff9f138c 100644 --- a/fuelclient/tests/utils/__init__.py +++ b/fuelclient/tests/utils/__init__.py @@ -36,6 +36,10 @@ from fuelclient.tests.utils.fake_extension import get_fake_env_extensions from fuelclient.tests.utils.fake_extension import get_fake_extension from fuelclient.tests.utils.fake_extension import get_fake_extensions from fuelclient.tests.utils.fake_fuel_version import get_fake_fuel_version +from fuelclient.tests.utils.fake_health import get_fake_test_set +from fuelclient.tests.utils.fake_health import get_fake_test_sets +from fuelclient.tests.utils.fake_health import get_fake_test_set_item +from fuelclient.tests.utils.fake_health import get_fake_test_set_items from fuelclient.tests.utils.fake_task import get_fake_task from fuelclient.tests.utils.fake_node_group import get_fake_node_group from fuelclient.tests.utils.fake_node_group import get_fake_node_groups @@ -59,6 +63,10 @@ __all__ = (get_fake_deployment_history, get_fake_yaml_network_conf, get_fake_env, get_fake_env_network_conf, + get_fake_test_set, + get_fake_test_sets, + get_fake_test_set_item, + get_fake_test_set_items, get_fake_release, get_fake_releases, get_fake_attributes_metadata, diff --git a/fuelclient/tests/utils/fake_health.py b/fuelclient/tests/utils/fake_health.py new file mode 100644 index 00000000..711ee24e --- /dev/null +++ b/fuelclient/tests/utils/fake_health.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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. + + +def get_fake_test_set(testset_id=None, name=None): + """Create a random fake test set for environment.""" + return { + "id": testset_id or "fake_test_set", + "name": name or "Fake tests. Duration 30 sec - 2 min" + } + + +def get_fake_test_sets(testsets_count, **kwargs): + """Create a random fake list of test sets for environment.""" + return [get_fake_test_set(**kwargs) + for _ in range(testsets_count)] + + +def get_fake_test_set_item(testset_id=None, testset=None, cluster_id=None, + status=None, tests=None): + """Create a random fake test set item.""" + return { + "id": testset_id or 45, + "testset": testset or "fake_test_set", + "cluster_id": cluster_id or 65, + "status": status or "finished", + "started_at": "2016-09-15 09:03:07.697393", + "ended_at": "2016-09-15 09:03:19.280296", + "tests": tests or [ + { + "status": "failure", + "taken": 1.0, + "testset": "fake_test_set", + "name": "Create fake instance", + "duration": "30 s.", + "message": "Fake test message", + "id": "fuel_health.tests.fake_test", + "description": "fake description" + }, + { + "status": "stopped", + "taken": 0.5, + "testset": "fake_test_set", + "name": "Check create, update and delete fake instance image", + "duration": "70 s.", + "message": "Can not set proxy for Health Check.", + "id": "fuel_health.tests.fake_test.test_update_fake_images", + "description": "fake description" + } + ] + } + + +def get_fake_test_set_items(items_count, **kwargs): + """Create a random fake list of test sets items.""" + return [get_fake_test_set_item(**kwargs) + for _ in range(items_count)] diff --git a/fuelclient/v1/__init__.py b/fuelclient/v1/__init__.py index b5df34f5..1fbc2239 100644 --- a/fuelclient/v1/__init__.py +++ b/fuelclient/v1/__init__.py @@ -19,6 +19,7 @@ from fuelclient.v1 import environment from fuelclient.v1 import extension from fuelclient.v1 import fuelversion from fuelclient.v1 import graph +from fuelclient.v1 import health from fuelclient.v1 import network_configuration from fuelclient.v1 import network_group from fuelclient.v1 import node @@ -39,6 +40,7 @@ __all__ = ('cluster_settings', 'extension', 'fuelversion', 'graph', + 'health', 'network_configuration', 'network_group', 'node', diff --git a/fuelclient/v1/health.py b/fuelclient/v1/health.py new file mode 100644 index 00000000..f9ebda2d --- /dev/null +++ b/fuelclient/v1/health.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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. + +from fuelclient.cli import error +from fuelclient import objects +from fuelclient.v1 import base_v1 + + +class HealthClient(base_v1.BaseV1Client): + + _entity_wrapper = objects.Health + _allowed_statuses = ( + 'error', + 'operational', + 'update_error', + ) + + def get_all(self, environment_id): + """Get list of test sets for a given environment. + + :param environment_id: Id of environment + :type environment_id: int + :return: health test sets as a list of dict + :rtype: list + """ + return self._entity_wrapper.get_test_sets(environment_id) + + def get_status_all(self, environment_id=None): + """Get test sets statuses. If environment_id is None then statuses of + all test sets will be retrieved. + + :param environment_id: Id of environment + :type environment_id: int + :return: health test sets as a list of dict + :rtype: list + """ + data = self._entity_wrapper.get_tests_status_all() + # OSTF API doesn't support filtering by cluster, then do it 'manually' + if environment_id is not None: + data = [i for i in data if i['cluster_id'] == environment_id] + return data + + def get_status_single(self, testset_id): + testrun_obj = self._entity_wrapper(testset_id) + data = testrun_obj.get_tests_status_single() + if data: + result = [] + # Retrieve and re-format 'tests' from nested data for clarity + for tests in data.get('tests'): + result.append("\n* {} - {}, ('{}')".format(tests["status"], + tests["name"], + tests["message"])) + else: + msg = "Test sets with id {0} does not exist".format(testset_id) + raise error.ActionException(msg) + data['tests'] = ' '.join(result) + return data + + def get_last_test_status(self, environment_id): + return self._entity_wrapper.get_last_tests_status(environment_id) + + def start(self, environment_id, ostf_credentials=None, test_sets=None, + force=False): + """Run test sets for a given environment. If test_sets is None then + all test sets will be run + + :param environment_id: Id of environment + :type environment_id: int + :param ostf_credentials: ostf credentials + :type ostf_credentials: dict + :param test_sets: list of test sets + :type test_sets: list + :param force: + :type force: bool + :return: running health test sets as a list of dict + :rtype: list + """ + env_obj = objects.Environment(environment_id) + + if env_obj.status not in self._allowed_statuses and not force: + raise error.EnvironmentException( + "Environment is not ready to run health check " + "because it is in '{0}' state. Health check is likely " + "to fail because of this. Use '--force' flag " + "to proceed anyway.".format(env_obj.status) + ) + + if env_obj.is_customized and not force: + raise error.EnvironmentException( + "Environment deployment facts were updated. " + "Health check is likely to fail because of " + "that. Use '--force' flag to proceed anyway." + ) + test_sets_to_run = test_sets or set(ts['id'] for ts in + self.get_all(environment_id)) + + return self._entity_wrapper.run_test_sets(environment_id, + test_sets_to_run, + ostf_credentials) + + def action(self, testrun_id, action_status): + """Make an action on specific test set. + + :param testrun_id: id of test set + :type testrun_id: int + :param action_status: the type of action ('stopped', 'restarted') + :type action_status: str + """ + testrun_obj = self._entity_wrapper(obj_id=testrun_id) + return testrun_obj.action_test(action_status)[0] + + +def get_client(connection): + return HealthClient(connection) diff --git a/setup.cfg b/setup.cfg index 1354fb15..991e1995 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,6 +66,12 @@ fuelclient = graph_execute=fuelclient.commands.graph:GraphExecute graph_list=fuelclient.commands.graph:GraphList graph_upload=fuelclient.commands.graph:GraphUpload + health_list=fuelclient.commands.health:HealthTestSetsList + health_restart=fuelclient.commands.health:HealthCheckRestart + health_start=fuelclient.commands.health:HealthCheckStart + health_status_list=fuelclient.commands.health:HealthTestSetsStatusList + health_status_show=fuelclient.commands.health:HealthTestSetsStatusShow + health_stop=fuelclient.commands.health:HealthCheckStop network-group_create=fuelclient.commands.network_group:NetworkGroupCreate network-group_delete=fuelclient.commands.network_group:NetworkGroupDelete network-group_list=fuelclient.commands.network_group:NetworkGroupList