diff --git a/setup.cfg b/setup.cfg index 5c19646..4e6bf4b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,9 @@ openstack.infra_optim.v1 = optimize_scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine optimize_scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine + optimize_service_show = watcherclient.v1.service_shell:ShowService + optimize_service_list = watcherclient.v1.service_shell:ListService + # The same as above but used by the 'watcher' command watcherclient.v1 = goal_show = watcherclient.v1.goal_shell:ShowGoal @@ -94,6 +97,9 @@ watcherclient.v1 = scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine + service_show = watcherclient.v1.service_shell:ShowService + service_list = watcherclient.v1.service_shell:ListService + [pbr] autodoc_index_modules = True diff --git a/watcherclient/tests/v1/test_service.py b/watcherclient/tests/v1/test_service.py new file mode 100644 index 0000000..791a4d2 --- /dev/null +++ b/watcherclient/tests/v1/test_service.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# 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 testtools import matchers + +from watcherclient.tests import utils +import watcherclient.v1.service + +SERVICE1 = { + 'id': 1, + 'name': 'watcher-applier', + 'host': 'controller', + 'status': 'ACTIVE', +} + +SERVICE2 = { + 'id': 2, + 'name': 'watcher-decision-engine', + 'host': 'controller', + 'status': 'FAILED', +} + +fake_responses = { + '/v1/services': + { + 'GET': ( + {}, + {"services": [SERVICE1]}, + ), + }, + '/v1/services/detail': + { + 'GET': ( + {}, + {"services": [SERVICE1]}, + ) + }, + '/v1/services/%s' % SERVICE1['id']: + { + 'GET': ( + {}, + SERVICE1, + ), + }, + '/v1/services/%s' % SERVICE1['name']: + { + 'GET': ( + {}, + SERVICE1, + ), + }, +} + +fake_responses_pagination = { + '/v1/services': + { + 'GET': ( + {}, + {"services": [SERVICE1], + "next": "http://127.0.0.1:6385/v1/services/?limit=1"} + ), + }, + '/v1/services/?limit=1': + { + 'GET': ( + {}, + {"services": [SERVICE2]} + ), + }, +} + +fake_responses_sorting = { + '/v1/services/?sort_key=id': + { + 'GET': ( + {}, + {"services": [SERVICE1, SERVICE2]} + ), + }, + '/v1/services/?sort_dir=desc': + { + 'GET': ( + {}, + {"services": [SERVICE2, SERVICE1]} + ), + }, +} + + +class ServiceManagerTest(testtools.TestCase): + + def setUp(self): + super(ServiceManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = watcherclient.v1.service.ServiceManager(self.api) + + def test_services_list(self): + services = self.mgr.list() + expect = [ + ('GET', '/v1/services', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(services)) + + def test_services_list_detail(self): + services = self.mgr.list(detail=True) + expect = [ + ('GET', '/v1/services/detail', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(services)) + + def test_services_list_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = watcherclient.v1.service.ServiceManager(self.api) + services = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/services/?limit=1', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(services, matchers.HasLength(1)) + + def test_services_list_pagination_no_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = watcherclient.v1.service.ServiceManager(self.api) + services = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/services', {}, None), + ('GET', '/v1/services/?limit=1', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(services, matchers.HasLength(2)) + + def test_services_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = watcherclient.v1.service.ServiceManager(self.api) + services = self.mgr.list(sort_key='id') + expect = [ + ('GET', '/v1/services/?sort_key=id', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(services)) + + def test_services_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = watcherclient.v1.service.ServiceManager(self.api) + services = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/services/?sort_dir=desc', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(services)) + + def test_services_show(self): + service = self.mgr.get(SERVICE1['id']) + expect = [ + ('GET', '/v1/services/%s' % SERVICE1['id'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(SERVICE1['id'], service.id) + + def test_services_show_by_name(self): + service = self.mgr.get(SERVICE1['name']) + expect = [ + ('GET', '/v1/services/%s' % SERVICE1['name'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(SERVICE1['name'], service.name) diff --git a/watcherclient/tests/v1/test_service_shell.py b/watcherclient/tests/v1/test_service_shell.py new file mode 100644 index 0000000..89e11c6 --- /dev/null +++ b/watcherclient/tests/v1/test_service_shell.py @@ -0,0 +1,123 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 Servionica +# +# 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 datetime + +import mock +import six + +from watcherclient import shell +from watcherclient.tests.v1 import base +from watcherclient import v1 as resource +from watcherclient.v1 import resource_fields + +SERVICE_1 = { + 'name': 'watcher-applier', + 'host': 'controller', + 'status': 'ACTIVE', + 'last_seen_up': None, + 'created_at': datetime.datetime.now().isoformat(), + 'updated_at': None, + 'deleted_at': None, +} + +SERVICE_2 = { + 'name': 'watcher-decision-engine', + 'host': 'controller', + 'status': 'FAILED', + 'last_seen_up': None, + 'created_at': datetime.datetime.now().isoformat(), + 'updated_at': None, + 'deleted_at': None, +} + + +class ServiceShellTest(base.CommandTestCase): + + SHORT_LIST_FIELDS = resource_fields.SERVICE_SHORT_LIST_FIELDS + SHORT_LIST_FIELD_LABELS = ( + resource_fields.SERVICE_SHORT_LIST_FIELD_LABELS) + FIELDS = resource_fields.SERVICE_FIELDS + FIELD_LABELS = resource_fields.SERVICE_FIELD_LABELS + + def setUp(self): + super(self.__class__, self).setUp() + + p_service_manager = mock.patch.object(resource, 'ServiceManager') + self.m_service_mgr_cls = p_service_manager.start() + self.addCleanup(p_service_manager.stop) + + self.m_service_mgr = mock.Mock() + self.m_service_mgr_cls.return_value = self.m_service_mgr + + self.stdout = six.StringIO() + self.cmd = shell.WatcherShell(stdout=self.stdout) + + def test_do_service_list(self): + service1 = resource.Service(mock.Mock(), SERVICE_1) + service2 = resource.Service(mock.Mock(), SERVICE_2) + self.m_service_mgr.list.return_value = [ + service1, service2] + + exit_code, results = self.run_cmd('service list') + + for res in results: + del res['ID'] + + self.assertEqual(0, exit_code) + self.assertEqual( + [self.resource_as_dict(service1, self.SHORT_LIST_FIELDS, + self.SHORT_LIST_FIELD_LABELS), + self.resource_as_dict(service2, self.SHORT_LIST_FIELDS, + self.SHORT_LIST_FIELD_LABELS)], + results) + + self.m_service_mgr.list.assert_called_once_with(detail=False) + + def test_do_service_list_detail(self): + service1 = resource.Service(mock.Mock(), SERVICE_1) + service2 = resource.Service(mock.Mock(), SERVICE_2) + self.m_service_mgr.list.return_value = [ + service1, service2] + + exit_code, results = self.run_cmd('service list --detail') + + for res in results: + del res['ID'] + + self.assertEqual(0, exit_code) + self.assertEqual( + [self.resource_as_dict(service1, self.FIELDS, + self.FIELD_LABELS), + self.resource_as_dict(service2, self.FIELDS, + self.FIELD_LABELS)], + results) + + self.m_service_mgr.list.assert_called_once_with(detail=True) + + def test_do_service_show_by_name(self): + service = resource.Service(mock.Mock(), SERVICE_1) + self.m_service_mgr.get.return_value = service + + exit_code, result = self.run_cmd('service show watcher-applier') + + del result['ID'] + + self.assertEqual(0, exit_code) + self.assertEqual( + self.resource_as_dict(service, self.FIELDS, self.FIELD_LABELS), + result) + self.m_service_mgr.get.assert_called_once_with('watcher-applier') diff --git a/watcherclient/v1/__init__.py b/watcherclient/v1/__init__.py index 327b892..9d8a2d7 100644 --- a/watcherclient/v1/__init__.py +++ b/watcherclient/v1/__init__.py @@ -20,6 +20,7 @@ from watcherclient.v1 import audit from watcherclient.v1 import audit_template from watcherclient.v1 import goal from watcherclient.v1 import scoring_engine +from watcherclient.v1 import service from watcherclient.v1 import strategy Action = action.Action @@ -34,6 +35,8 @@ Goal = goal.Goal GoalManager = goal.GoalManager ScoringEngine = scoring_engine.ScoringEngine ScoringEngineManager = scoring_engine.ScoringEngineManager +Service = service.Service +ServiceManager = service.ServiceManager Strategy = strategy.Strategy StrategyManager = strategy.StrategyManager @@ -41,4 +44,4 @@ __all__ = ( "Action", "ActionManager", "ActionPlan", "ActionPlanManager", "Audit", "AuditManager", "AuditTemplate", "AuditTemplateManager", "Goal", "GoalManager", "ScoringEngine", "ScoringEngineManager", - "Strategy", "StrategyManager") + "Service", "ServiceManager", "Strategy", "StrategyManager") diff --git a/watcherclient/v1/client.py b/watcherclient/v1/client.py index 89e01fb..4c61f27 100644 --- a/watcherclient/v1/client.py +++ b/watcherclient/v1/client.py @@ -38,6 +38,7 @@ class Client(object): self.action_plan = v1.ActionPlanManager(self.http_client) self.goal = v1.GoalManager(self.http_client) self.scoring_engine = v1.ScoringEngineManager(self.http_client) + self.service = v1.ServiceManager(self.http_client) self.strategy = v1.StrategyManager(self.http_client) # self.metric_collector = v1.MetricCollectorManager(self.http_client) diff --git a/watcherclient/v1/resource_fields.py b/watcherclient/v1/resource_fields.py index 2ceede7..e90d79f 100644 --- a/watcherclient/v1/resource_fields.py +++ b/watcherclient/v1/resource_fields.py @@ -118,3 +118,9 @@ SCORING_ENGINE_FIELD_LABELS = ['UUID', 'Name', 'Description', 'Metainfo'] SCORING_ENGINE_SHORT_LIST_FIELDS = ['uuid', 'name', 'description'] SCORING_ENGINE_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Description'] + +# Services +SERVICE_FIELDS = ['id', 'name', 'host', 'status', 'last_seen_up'] +SERVICE_FIELD_LABELS = ['ID', 'Name', 'Host', 'Status', 'Last seen up'] +SERVICE_SHORT_LIST_FIELDS = ['id', 'name', 'host', 'status'] +SERVICE_SHORT_LIST_FIELD_LABELS = ['ID', 'Name', 'Host', 'Status'] diff --git a/watcherclient/v1/service.py b/watcherclient/v1/service.py new file mode 100644 index 0000000..896c883 --- /dev/null +++ b/watcherclient/v1/service.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Servionica +# +# 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 watcherclient.common import base +from watcherclient.common import utils + + +class Service(base.Resource): + def __repr__(self): + return "" % self._info + + +class ServiceManager(base.Manager): + resource_class = Service + + @staticmethod + def _path(service=None): + return ('/v1/services/%s' % service + if service else '/v1/services') + + def list(self, limit=None, sort_key=None, sort_dir=None, detail=False): + """Retrieve a list of services. + + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of services to return. + 2) limit == 0, return the entire list of services. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Watcher API + (see Watcher's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about services. + + :returns: A list of services. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(limit, sort_key, sort_dir) + + path = '' + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "services") + else: + return self._list_pagination(self._path(path), "services", + limit=limit) + + def get(self, service): + try: + return self._list(self._path(service))[0] + except IndexError: + return None diff --git a/watcherclient/v1/service_shell.py b/watcherclient/v1/service_shell.py new file mode 100644 index 0000000..bb9c99d --- /dev/null +++ b/watcherclient/v1/service_shell.py @@ -0,0 +1,103 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 Servionica +# +# 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 osc_lib import utils + +from watcherclient._i18n import _ +from watcherclient.common import command +from watcherclient.common import utils as common_utils +from watcherclient import exceptions +from watcherclient.v1 import resource_fields as res_fields + + +class ShowService(command.ShowOne): + """Show detailed information about a given service.""" + + def get_parser(self, prog_name): + parser = super(ShowService, self).get_parser(prog_name) + parser.add_argument( + 'service', + metavar='', + help=_('ID or name of the service'), + ) + return parser + + def take_action(self, parsed_args): + client = getattr(self.app.client_manager, "infra-optim") + + try: + service = client.service.get(parsed_args.service) + except exceptions.HTTPNotFound as exc: + raise exceptions.CommandError(str(exc)) + + columns = res_fields.SERVICE_FIELDS + column_headers = res_fields.SERVICE_FIELD_LABELS + + return column_headers, utils.get_item_properties(service, columns) + + +class ListService(command.Lister): + """List information on retrieved services.""" + + def get_parser(self, prog_name): + parser = super(ListService, self).get_parser(prog_name) + parser.add_argument( + '--detail', + dest='detail', + action='store_true', + default=False, + help=_("Show detailed information about each service.")) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Maximum number of services to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Watcher API Service.')) + parser.add_argument( + '--sort-key', + metavar='', + help=_('Goal field that will be used for sorting.')) + parser.add_argument( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help='Sort direction: "asc" (the default) or "desc".') + + return parser + + def take_action(self, parsed_args): + client = getattr(self.app.client_manager, "infra-optim") + + params = {} + if parsed_args.detail: + fields = res_fields.SERVICE_FIELDS + field_labels = res_fields.SERVICE_FIELD_LABELS + else: + fields = res_fields.SERVICE_SHORT_LIST_FIELDS + field_labels = res_fields.SERVICE_SHORT_LIST_FIELD_LABELS + + params.update( + common_utils.common_params_for_list( + parsed_args, fields, field_labels)) + + try: + data = client.service.list(**params) + except exceptions.HTTPNotFound as ex: + raise exceptions.CommandError(str(ex)) + + return (field_labels, + (utils.get_item_properties(item, fields) for item in data))