From 84b12f8f1e04de601cdf813644d566fc79830485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Fran=C3=A7oise?= Date: Wed, 18 May 2016 11:42:49 +0200 Subject: [PATCH] Added EfficacyIndicator object In this changeset, I created the Watcher object for the efficacy indicators as well as the API object which will be embedded into the /action_plans endpoint. Partially Implements: blueprint efficacy-indicator Change-Id: Ibbe47613317d51a3829fe9de12540c048e8d7117 --- watcher/api/controllers/v1/__init__.py | 3 +- watcher/api/controllers/v1/action.py | 1 + .../api/controllers/v1/efficacy_indicator.py | 72 +++++++ watcher/objects/__init__.py | 6 +- watcher/objects/efficacy_indicator.py | 199 ++++++++++++++++++ .../tests/objects/test_efficacy_indicator.py | 150 +++++++++++++ 6 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 watcher/api/controllers/v1/efficacy_indicator.py create mode 100644 watcher/objects/efficacy_indicator.py create mode 100644 watcher/tests/objects/test_efficacy_indicator.py diff --git a/watcher/api/controllers/v1/__init__.py b/watcher/api/controllers/v1/__init__.py index 37cb77454..eff8f3051 100644 --- a/watcher/api/controllers/v1/__init__.py +++ b/watcher/api/controllers/v1/__init__.py @@ -167,4 +167,5 @@ class Controller(rest.RestController): # the request object to make the links. return V1.convert() -__all__ = (Controller) + +__all__ = ("Controller", ) diff --git a/watcher/api/controllers/v1/action.py b/watcher/api/controllers/v1/action.py index 152bba25f..535637fda 100644 --- a/watcher/api/controllers/v1/action.py +++ b/watcher/api/controllers/v1/action.py @@ -63,6 +63,7 @@ import wsme from wsme import types as wtypes import wsmeext.pecan as wsme_pecan +from watcher._i18n import _ from watcher.api.controllers import base from watcher.api.controllers import link from watcher.api.controllers.v1 import collection diff --git a/watcher/api/controllers/v1/efficacy_indicator.py b/watcher/api/controllers/v1/efficacy_indicator.py new file mode 100644 index 000000000..54fafac9c --- /dev/null +++ b/watcher/api/controllers/v1/efficacy_indicator.py @@ -0,0 +1,72 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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. + +""" +An efficacy indicator is a single value +that gives an indication on how the :ref:`solution ` +produced by a given :ref:`strategy ` performed. These +efficacy indicators are specific to a given :ref:`goal ` and +are usually used to compute the :ref:`gobal efficacy ` of +the resulting :ref:`action plan `. + +In Watcher, these efficacy indicators are specified alongside the goal they +relate to. When a strategy (which always relates to a goal) is executed, it +produces a solution containing the efficacy indicators specified by the +goal. This solution, which has been translated by the :ref:`Watcher Planner +` into an action plan, will see its +indicators and global efficacy stored and would now be accessible through the +:ref:`Watcher API `. +""" + +import numbers + +from wsme import types as wtypes + +from watcher.api.controllers import base +from watcher import objects + + +class EfficacyIndicator(base.APIBase): + """API representation of a efficacy indicator. + + This class enforces type checking and value constraints, and converts + between the internal object model and the API representation of an + efficacy indicator. + """ + + name = wtypes.wsattr(wtypes.text, mandatory=True) + """Name of this efficacy indicator""" + + description = wtypes.wsattr(wtypes.text, mandatory=False) + """Description of this efficacy indicator""" + + unit = wtypes.wsattr(wtypes.text, mandatory=False) + """Unit of this efficacy indicator""" + + value = wtypes.wsattr(numbers.Number, mandatory=True) + """Value of this efficacy indicator""" + + def __init__(self, **kwargs): + super(EfficacyIndicator, self).__init__() + + self.fields = [] + fields = list(objects.EfficacyIndicator.fields) + for field in fields: + # Skip fields we do not expose. + if not hasattr(self, field): + continue + self.fields.append(field) + setattr(self, field, kwargs.get(field, wtypes.Unset)) diff --git a/watcher/objects/__init__.py b/watcher/objects/__init__.py index ba73def62..ef443de4b 100644 --- a/watcher/objects/__init__.py +++ b/watcher/objects/__init__.py @@ -17,6 +17,7 @@ from watcher.objects import action from watcher.objects import action_plan from watcher.objects import audit from watcher.objects import audit_template +from watcher.objects import efficacy_indicator from watcher.objects import goal from watcher.objects import strategy @@ -26,6 +27,7 @@ Action = action.Action ActionPlan = action_plan.ActionPlan Goal = goal.Goal Strategy = strategy.Strategy +EfficacyIndicator = efficacy_indicator.EfficacyIndicator -__all__ = ("Audit", "AuditTemplate", "Action", - "ActionPlan", "Goal", "Strategy") +__all__ = ("Audit", "AuditTemplate", "Action", "ActionPlan", + "Goal", "Strategy", "EfficacyIndicator") diff --git a/watcher/objects/efficacy_indicator.py b/watcher/objects/efficacy_indicator.py new file mode 100644 index 000000000..39de4eb45 --- /dev/null +++ b/watcher/objects/efficacy_indicator.py @@ -0,0 +1,199 @@ +# -*- encoding: utf-8 -*- +# Copyright 2013 IBM Corp. +# +# 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 watcher.common import exception +from watcher.common import utils +from watcher.db import api as dbapi +from watcher.objects import base +from watcher.objects import utils as obj_utils + + +class EfficacyIndicator(base.WatcherObject): + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = dbapi.get_instance() + + fields = { + 'id': int, + 'uuid': obj_utils.str_or_none, + 'action_plan_id': obj_utils.int_or_none, + 'name': obj_utils.str_or_none, + 'description': obj_utils.str_or_none, + 'unit': obj_utils.str_or_none, + 'value': obj_utils.numeric_or_none, + } + + @staticmethod + def _from_db_object(efficacy_indicator, db_efficacy_indicator): + """Converts a database entity to a formal object.""" + for field in efficacy_indicator.fields: + efficacy_indicator[field] = db_efficacy_indicator[field] + + efficacy_indicator.obj_reset_changes() + return efficacy_indicator + + @staticmethod + def _from_db_object_list(db_objects, cls, context): + """Converts a list of database entities to a list of formal objects.""" + return [EfficacyIndicator._from_db_object(cls(context), obj) + for obj in db_objects] + + @classmethod + def get(cls, context, efficacy_indicator_id): + """Find an efficacy indicator object given its ID or UUID + + :param efficacy_indicator_id: the ID or UUID of an efficacy indicator. + :returns: a :class:`EfficacyIndicator` object. + """ + if utils.is_int_like(efficacy_indicator_id): + return cls.get_by_id(context, efficacy_indicator_id) + elif utils.is_uuid_like(efficacy_indicator_id): + return cls.get_by_uuid(context, efficacy_indicator_id) + else: + raise exception.InvalidIdentity(identity=efficacy_indicator_id) + + @classmethod + def get_by_id(cls, context, efficacy_indicator_id): + """Find an efficacy indicator given its integer ID + + :param efficacy_indicator_id: the id of an efficacy indicator. + :returns: a :class:`EfficacyIndicator` object. + """ + db_efficacy_indicator = cls.dbapi.get_efficacy_indicator_by_id( + context, efficacy_indicator_id) + efficacy_indicator = EfficacyIndicator._from_db_object( + cls(context), db_efficacy_indicator) + return efficacy_indicator + + @classmethod + def get_by_uuid(cls, context, uuid): + """Find an efficacy indicator given its UUID + + :param uuid: the uuid of an efficacy indicator. + :param context: Security context + :returns: a :class:`EfficacyIndicator` object. + """ + db_efficacy_indicator = cls.dbapi.get_efficacy_indicator_by_uuid( + context, uuid) + efficacy_indicator = EfficacyIndicator._from_db_object( + cls(context), db_efficacy_indicator) + return efficacy_indicator + + @classmethod + def list(cls, context, limit=None, marker=None, filters=None, + sort_key=None, sort_dir=None): + """Return a list of EfficacyIndicator objects. + + :param context: Security context. + :param limit: maximum number of resources to return in a single result. + :param marker: pagination marker for large data sets. + :param filters: Filters to apply. Defaults to None. + :param sort_key: column to sort results by. + :param sort_dir: direction to sort. "asc" or "desc". + :returns: a list of :class:`EfficacyIndicator` object. + + """ + db_efficacy_indicators = cls.dbapi.get_efficacy_indicator_list( + context, + limit=limit, + marker=marker, + filters=filters, + sort_key=sort_key, + sort_dir=sort_dir) + return EfficacyIndicator._from_db_object_list( + db_efficacy_indicators, cls, context) + + def create(self, context=None): + """Create a EfficacyIndicator record in the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: EfficacyIndicator(context) + + """ + values = self.obj_get_changes() + db_efficacy_indicator = self.dbapi.create_efficacy_indicator(values) + self._from_db_object(self, db_efficacy_indicator) + + def destroy(self, context=None): + """Delete the EfficacyIndicator from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: EfficacyIndicator(context) + """ + self.dbapi.destroy_efficacy_indicator(self.uuid) + self.obj_reset_changes() + + def save(self, context=None): + """Save updates to this EfficacyIndicator. + + Updates will be made column by column based on the result + of self.what_changed(). + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: EfficacyIndicator(context) + """ + updates = self.obj_get_changes() + self.dbapi.update_efficacy_indicator(self.uuid, updates) + + self.obj_reset_changes() + + def refresh(self, context=None): + """Loads updates for this EfficacyIndicator. + + Loads an efficacy indicator with the same uuid from the database and + checks for updated attributes. Updates are applied to the loaded + efficacy indicator column by column, if there are any updates. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: EfficacyIndicator(context) + """ + current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) + for field in self.fields: + if (hasattr(self, base.get_attrname(field)) and + self[field] != current[field]): + self[field] = current[field] + + def soft_delete(self, context=None): + """Soft Delete the efficacy indicator from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Audit(context) + """ + self.dbapi.soft_delete_efficacy_indicator(self.uuid) + self.state = "DELETED" + self.save() diff --git a/watcher/tests/objects/test_efficacy_indicator.py b/watcher/tests/objects/test_efficacy_indicator.py new file mode 100644 index 000000000..d3fca4578 --- /dev/null +++ b/watcher/tests/objects/test_efficacy_indicator.py @@ -0,0 +1,150 @@ +# Copyright 2015 OpenStack Foundation +# 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 mock +from testtools.matchers import HasLength + +from watcher.common import exception +# from watcher.common import utils as w_utils +from watcher import objects +from watcher.tests.db import base +from watcher.tests.db import utils + + +class TestEfficacyIndicatorObject(base.DbTestCase): + + def setUp(self): + super(TestEfficacyIndicatorObject, self).setUp() + self.fake_efficacy_indicator = utils.get_test_efficacy_indicator() + + def test_get_by_id(self): + efficacy_indicator_id = self.fake_efficacy_indicator['id'] + with mock.patch.object(self.dbapi, 'get_efficacy_indicator_by_id', + autospec=True) as mock_get_efficacy_indicator: + mock_get_efficacy_indicator.return_value = ( + self.fake_efficacy_indicator) + efficacy_indicator = objects.EfficacyIndicator.get( + self.context, efficacy_indicator_id) + mock_get_efficacy_indicator.assert_called_once_with( + self.context, efficacy_indicator_id) + self.assertEqual(self.context, efficacy_indicator._context) + + def test_get_by_uuid(self): + uuid = self.fake_efficacy_indicator['uuid'] + with mock.patch.object(self.dbapi, 'get_efficacy_indicator_by_uuid', + autospec=True) as mock_get_efficacy_indicator: + mock_get_efficacy_indicator.return_value = ( + self.fake_efficacy_indicator) + efficacy_indicator = objects.EfficacyIndicator.get( + self.context, uuid) + mock_get_efficacy_indicator.assert_called_once_with( + self.context, uuid) + self.assertEqual(self.context, efficacy_indicator._context) + + def test_get_bad_id_and_uuid(self): + self.assertRaises( + exception.InvalidIdentity, + objects.EfficacyIndicator.get, self.context, 'not-a-uuid') + + def test_list(self): + with mock.patch.object(self.dbapi, 'get_efficacy_indicator_list', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.fake_efficacy_indicator] + efficacy_indicators = objects.EfficacyIndicator.list(self.context) + self.assertEqual(1, mock_get_list.call_count) + self.assertThat(efficacy_indicators, HasLength(1)) + self.assertIsInstance( + efficacy_indicators[0], objects.EfficacyIndicator) + self.assertEqual(self.context, efficacy_indicators[0]._context) + + def test_create(self): + with mock.patch.object( + self.dbapi, 'create_efficacy_indicator', + autospec=True + ) as mock_create_efficacy_indicator: + mock_create_efficacy_indicator.return_value = ( + self.fake_efficacy_indicator) + efficacy_indicator = objects.EfficacyIndicator( + self.context, **self.fake_efficacy_indicator) + + efficacy_indicator.create() + mock_create_efficacy_indicator.assert_called_once_with( + self.fake_efficacy_indicator) + self.assertEqual(self.context, efficacy_indicator._context) + + def test_destroy(self): + uuid = self.fake_efficacy_indicator['uuid'] + with mock.patch.object( + self.dbapi, 'get_efficacy_indicator_by_uuid', + autospec=True + ) as mock_get_efficacy_indicator: + mock_get_efficacy_indicator.return_value = ( + self.fake_efficacy_indicator) + with mock.patch.object( + self.dbapi, 'destroy_efficacy_indicator', + autospec=True + ) as mock_destroy_efficacy_indicator: + efficacy_indicator = objects.EfficacyIndicator.get_by_uuid( + self.context, uuid) + efficacy_indicator.destroy() + mock_get_efficacy_indicator.assert_called_once_with( + self.context, uuid) + mock_destroy_efficacy_indicator.assert_called_once_with(uuid) + self.assertEqual(self.context, efficacy_indicator._context) + + def test_save(self): + uuid = self.fake_efficacy_indicator['uuid'] + with mock.patch.object( + self.dbapi, 'get_efficacy_indicator_by_uuid', + autospec=True + ) as mock_get_efficacy_indicator: + mock_get_efficacy_indicator.return_value = ( + self.fake_efficacy_indicator) + with mock.patch.object( + self.dbapi, 'update_efficacy_indicator', + autospec=True + ) as mock_update_efficacy_indicator: + efficacy_indicator = objects.EfficacyIndicator.get_by_uuid( + self.context, uuid) + efficacy_indicator.description = 'Indicator Description' + efficacy_indicator.save() + + mock_get_efficacy_indicator.assert_called_once_with( + self.context, uuid) + mock_update_efficacy_indicator.assert_called_once_with( + uuid, {'description': 'Indicator Description'}) + self.assertEqual(self.context, efficacy_indicator._context) + + def test_refresh(self): + uuid = self.fake_efficacy_indicator['uuid'] + returns = [dict(self.fake_efficacy_indicator, + description="first description"), + dict(self.fake_efficacy_indicator, + description="second description")] + expected = [mock.call(self.context, uuid), + mock.call(self.context, uuid)] + with mock.patch.object(self.dbapi, 'get_efficacy_indicator_by_uuid', + side_effect=returns, + autospec=True) as mock_get_efficacy_indicator: + efficacy_indicator = objects.EfficacyIndicator.get( + self.context, uuid) + self.assertEqual( + "first description", efficacy_indicator.description) + efficacy_indicator.refresh() + self.assertEqual( + "second description", efficacy_indicator.description) + self.assertEqual( + expected, mock_get_efficacy_indicator.call_args_list) + self.assertEqual(self.context, efficacy_indicator._context)