diff --git a/releasenotes/notes/dynamic-action-description-0e947b9e7ef2a134.yaml b/releasenotes/notes/dynamic-action-description-0e947b9e7ef2a134.yaml new file mode 100644 index 000000000..219247495 --- /dev/null +++ b/releasenotes/notes/dynamic-action-description-0e947b9e7ef2a134.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add description property for dynamic action. Admin can see detail information + of any specify action. diff --git a/watcher/api/controllers/v1/action.py b/watcher/api/controllers/v1/action.py index 3e96fdb95..73bcd92cc 100644 --- a/watcher/api/controllers/v1/action.py +++ b/watcher/api/controllers/v1/action.py @@ -118,6 +118,9 @@ class Action(base.APIBase): action_type = wtypes.text """Action type""" + description = wtypes.text + """Action description""" + input_parameters = types.jsontype """One or more key/value pairs """ @@ -141,6 +144,7 @@ class Action(base.APIBase): setattr(self, field, kwargs.get(field, wtypes.Unset)) self.fields.append('action_plan_id') + self.fields.append('description') setattr(self, 'action_plan_uuid', kwargs.get('action_plan_id', wtypes.Unset)) @@ -162,6 +166,14 @@ class Action(base.APIBase): @classmethod def convert_with_links(cls, action, expand=True): action = Action(**action.as_dict()) + try: + obj_action_desc = objects.ActionDescription.get_by_type( + pecan.request.context, action.action_type) + description = obj_action_desc.description + except exception.ActionDescriptionNotFound: + description = "" + setattr(action, 'description', description) + return cls._convert_with_links(action, pecan.request.host_url, expand) @classmethod diff --git a/watcher/applier/sync.py b/watcher/applier/sync.py new file mode 100644 index 000000000..3edc05e33 --- /dev/null +++ b/watcher/applier/sync.py @@ -0,0 +1,44 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2017 ZTE +# +# 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.applier.loading import default +from watcher.common import context +from watcher.common import exception +from watcher import objects + + +class Syncer(object): + """Syncs all available actions with the Watcher DB""" + + def sync(self): + ctx = context.make_context() + action_loader = default.DefaultActionLoader() + available_actions = action_loader.list_available() + for action_type in available_actions.keys(): + load_action = action_loader.load(action_type) + load_description = load_action.get_description() + try: + action_desc = objects.ActionDescription.get_by_type( + ctx, action_type) + if action_desc.description != load_description: + action_desc.description = load_description + action_desc.save() + except exception.ActionDescriptionNotFound: + obj_action_desc = objects.ActionDescription(ctx) + obj_action_desc.action_type = action_type + obj_action_desc.description = load_description + obj_action_desc.create() diff --git a/watcher/cmd/applier.py b/watcher/cmd/applier.py index 364a9ba10..088a9a91a 100644 --- a/watcher/cmd/applier.py +++ b/watcher/cmd/applier.py @@ -23,6 +23,7 @@ import sys from oslo_log import log as logging from watcher.applier import manager +from watcher.applier import sync from watcher.common import service as watcher_service from watcher import conf @@ -37,6 +38,9 @@ def main(): applier_service = watcher_service.Service(manager.ApplierManager) + syncer = sync.Syncer() + syncer.sync() + # Only 1 process launcher = watcher_service.launch(CONF, applier_service) launcher.wait() diff --git a/watcher/common/exception.py b/watcher/common/exception.py index 22f1bd383..dabfae897 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -426,6 +426,15 @@ class CronFormatIsInvalid(WatcherException): msg_fmt = _("Provided cron is invalid: %(message)s") +class ActionDescriptionAlreadyExists(Conflict): + msg_fmt = _("An action description with type %(action_type)s is " + "already exist.") + + +class ActionDescriptionNotFound(ResourceNotFound): + msg_fmt = _("The action description %(action_id)s cannot be found.") + + # Model class ComputeResourceNotFound(WatcherException): diff --git a/watcher/db/sqlalchemy/alembic/versions/d09a5945e4a0_add_action_description_table.py b/watcher/db/sqlalchemy/alembic/versions/d09a5945e4a0_add_action_description_table.py new file mode 100644 index 000000000..e0c18709f --- /dev/null +++ b/watcher/db/sqlalchemy/alembic/versions/d09a5945e4a0_add_action_description_table.py @@ -0,0 +1,32 @@ +"""add action description table + +Revision ID: d09a5945e4a0 +Revises: d098df6021e2 +Create Date: 2017-07-13 20:33:01.473711 + +""" + +# revision identifiers, used by Alembic. +revision = 'd09a5945e4a0' +down_revision = 'd098df6021e2' + +from alembic import op +import oslo_db +import sqlalchemy as sa + +def upgrade(): + op.create_table('action_descriptions', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('deleted', oslo_db.sqlalchemy.types.SoftDeleteInteger(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('action_type', sa.String(length=255), nullable=False), + sa.Column('description', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('action_type', name='uniq_action_description0action_type') + ) + + +def downgrade(): + op.drop_table('action_descriptions') diff --git a/watcher/db/sqlalchemy/api.py b/watcher/db/sqlalchemy/api.py index ebe919773..d14e27da7 100644 --- a/watcher/db/sqlalchemy/api.py +++ b/watcher/db/sqlalchemy/api.py @@ -1127,3 +1127,74 @@ class Connection(api.BaseConnection): return self._soft_delete(models.Service, service_id) except exception.ResourceNotFound: raise exception.ServiceNotFound(service=service_id) + + # ### ACTION_DESCRIPTIONS ### # + + def _add_action_descriptions_filters(self, query, filters): + if not filters: + filters = {} + + plain_fields = ['id', 'action_type'] + + return self._add_filters( + query=query, model=models.ActionDescription, filters=filters, + plain_fields=plain_fields) + + def get_action_description_list(self, context, filters=None, limit=None, + marker=None, sort_key=None, + sort_dir=None, eager=False): + query = model_query(models.ActionDescription) + if eager: + query = self._set_eager_options(models.ActionDescription, query) + query = self._add_action_descriptions_filters(query, filters) + if not context.show_deleted: + query = query.filter_by(deleted_at=None) + return _paginate_query(models.ActionDescription, limit, marker, + sort_key, sort_dir, query) + + def create_action_description(self, values): + try: + action_description = self._create(models.ActionDescription, values) + except db_exc.DBDuplicateEntry: + raise exception.ActionDescriptionAlreadyExists( + action_type=values['action_type']) + return action_description + + def _get_action_description(self, context, fieldname, value, eager): + try: + return self._get(context, model=models.ActionDescription, + fieldname=fieldname, value=value, eager=eager) + except exception.ResourceNotFound: + raise exception.ActionDescriptionNotFound(action_id=value) + + def get_action_description_by_id(self, context, + action_id, eager=False): + return self._get_action_description( + context, fieldname="id", value=action_id, eager=eager) + + def get_action_description_by_type(self, context, + action_type, eager=False): + return self._get_action_description( + context, fieldname="action_type", value=action_type, eager=eager) + + def destroy_action_description(self, action_id): + try: + return self._destroy(models.ActionDescription, action_id) + except exception.ResourceNotFound: + raise exception.ActionDescriptionNotFound( + action_id=action_id) + + def update_action_description(self, action_id, values): + try: + return self._update(models.ActionDescription, + action_id, values) + except exception.ResourceNotFound: + raise exception.ActionDescriptionNotFound( + action_id=action_id) + + def soft_delete_action_description(self, action_id): + try: + return self._soft_delete(models.ActionDescription, action_id) + except exception.ResourceNotFound: + raise exception.ActionDescriptionNotFound( + action_id=action_id) diff --git a/watcher/db/sqlalchemy/models.py b/watcher/db/sqlalchemy/models.py index dbe972b65..7ad0c16a9 100644 --- a/watcher/db/sqlalchemy/models.py +++ b/watcher/db/sqlalchemy/models.py @@ -278,3 +278,17 @@ class Service(Base): name = Column(String(255), nullable=False) host = Column(String(255), nullable=False) last_seen_up = Column(DateTime, nullable=True) + + +class ActionDescription(Base): + """Represents a action description""" + + __tablename__ = 'action_descriptions' + __table_args__ = ( + UniqueConstraint('action_type', + name="uniq_action_description0action_type"), + table_args() + ) + id = Column(Integer, primary_key=True) + action_type = Column(String(255), nullable=False) + description = Column(String(255), nullable=False) diff --git a/watcher/objects/__init__.py b/watcher/objects/__init__.py index 11c8a862e..b533f9c17 100644 --- a/watcher/objects/__init__.py +++ b/watcher/objects/__init__.py @@ -33,3 +33,4 @@ def register_all(): __import__('watcher.objects.efficacy_indicator') __import__('watcher.objects.scoring_engine') __import__('watcher.objects.service') + __import__('watcher.objects.action_description') diff --git a/watcher/objects/action_description.py b/watcher/objects/action_description.py new file mode 100644 index 000000000..1d9e1937b --- /dev/null +++ b/watcher/objects/action_description.py @@ -0,0 +1,141 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2017 ZTE +# +# 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 db_api +from watcher.objects import base +from watcher.objects import fields as wfields + + +@base.WatcherObjectRegistry.register +class ActionDescription(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = { + 'id': wfields.IntegerField(), + 'action_type': wfields.StringField(), + 'description': wfields.StringField(), + } + + @base.remotable_classmethod + def get(cls, context, action_id): + """Find a action description based on its id + + :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 + :param action_id: the id of a action description. + :returns: a :class:`ActionDescription` object. + """ + if utils.is_int_like(action_id): + db_action = cls.dbapi.get_action_description_by_id( + context, action_id) + action = ActionDescription._from_db_object(cls(context), db_action) + return action + else: + raise exception.InvalidIdentity(identity=action_id) + + @base.remotable_classmethod + def get_by_type(cls, context, action_type): + """Find a action description based on action type + + :param action_type: the action type of a action description. + :param context: Security context + :returns: a :class:`ActionDescription` object. + """ + + db_action = cls.dbapi.get_action_description_by_type( + context, action_type) + action = cls._from_db_object(cls(context), db_action) + return action + + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, filters=None, + sort_key=None, sort_dir=None): + """Return a list of :class:`ActionDescription` objects. + + :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.: ActionDescription(context) + :param filters: dict mapping the filter key to a value. + :param limit: maximum number of resources to return in a single result. + :param marker: pagination marker for large data sets. + :param sort_key: column to sort results by. + :param sort_dir: direction to sort. "asc" or "desc". + :returns: a list of :class:`ActionDescription` object. + """ + db_actions = cls.dbapi.get_action_description_list( + context, + filters=filters, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + + return [cls._from_db_object(cls(context), obj) for obj in db_actions] + + @base.remotable + def create(self): + """Create a :class:`ActionDescription` record in the DB.""" + values = self.obj_get_changes() + db_action = self.dbapi.create_action_description(values) + self._from_db_object(self, db_action) + + @base.remotable + def save(self): + """Save updates to this :class:`ActionDescription`. + + Updates will be made column by column based on the result + of self.what_changed(). + """ + updates = self.obj_get_changes() + db_obj = self.dbapi.update_action_description(self.id, updates) + obj = self._from_db_object(self, db_obj, eager=False) + self.obj_refresh(obj) + self.obj_reset_changes() + + def refresh(self): + """Loads updates for this :class:`ActionDescription`. + + Loads a action description with the same id from the database and + checks for updated attributes. Updates are applied from + the loaded action description column by column, if there + are any updates. + """ + current = self.get(self._context, action_id=self.id) + 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): + """Soft Delete the :class:`ActionDescription` from the DB.""" + db_obj = self.dbapi.soft_delete_action_description(self.id) + obj = self._from_db_object( + self.__class__(self._context), db_obj, eager=False) + self.obj_refresh(obj) diff --git a/watcher/tests/cmd/test_applier.py b/watcher/tests/cmd/test_applier.py index 25690ebfa..dbf128dc1 100644 --- a/watcher/tests/cmd/test_applier.py +++ b/watcher/tests/cmd/test_applier.py @@ -22,6 +22,7 @@ import types import mock from oslo_config import cfg from oslo_service import service +from watcher.applier import sync from watcher.common import service as watcher_service from watcher.cmd import applier @@ -49,6 +50,7 @@ class TestApplier(base.BaseTestCase): super(TestApplier, self).tearDown() self.conf._parse_cli_opts = self._parse_cli_opts + @mock.patch.object(sync.Syncer, "sync", mock.Mock()) @mock.patch.object(service, "launch") def test_run_applier_app(self, m_launch): applier.main() diff --git a/watcher/tests/db/test_action_description.py b/watcher/tests/db/test_action_description.py new file mode 100644 index 000000000..e9ea7f2a6 --- /dev/null +++ b/watcher/tests/db/test_action_description.py @@ -0,0 +1,293 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2017 ZTE +# +# 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. + + +"""Tests for manipulating ActionDescription via the DB API""" + +import freezegun + +from watcher.common import exception +from watcher.tests.db import base +from watcher.tests.db import utils + + +class TestDbActionDescriptionFilters(base.DbTestCase): + + FAKE_OLDER_DATE = '2015-01-01T09:52:05.219414' + FAKE_OLD_DATE = '2016-01-01T09:52:05.219414' + FAKE_TODAY = '2017-02-24T09:52:05.219414' + + def setUp(self): + super(TestDbActionDescriptionFilters, self).setUp() + self.context.show_deleted = True + self._data_setup() + + def _data_setup(self): + action_desc1_type = "nop" + action_desc2_type = "sleep" + action_desc3_type = "resize" + + with freezegun.freeze_time(self.FAKE_TODAY): + self.action_desc1 = utils.create_test_action_desc( + id=1, action_type=action_desc1_type, + description="description") + with freezegun.freeze_time(self.FAKE_OLD_DATE): + self.action_desc2 = utils.create_test_action_desc( + id=2, action_type=action_desc2_type, + description="description") + with freezegun.freeze_time(self.FAKE_OLDER_DATE): + self.action_desc3 = utils.create_test_action_desc( + id=3, action_type=action_desc3_type, + description="description") + + def _soft_delete_action_descs(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.dbapi.soft_delete_action_description(self.action_desc1.id) + with freezegun.freeze_time(self.FAKE_OLD_DATE): + self.dbapi.soft_delete_action_description(self.action_desc2.id) + with freezegun.freeze_time(self.FAKE_OLDER_DATE): + self.dbapi.soft_delete_action_description(self.action_desc3.id) + + def _update_action_descs(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.dbapi.update_action_description( + self.action_desc1.id, values={"description": + "nop description"}) + with freezegun.freeze_time(self.FAKE_OLD_DATE): + self.dbapi.update_action_description( + self.action_desc2.id, values={"description": + "sleep description"}) + with freezegun.freeze_time(self.FAKE_OLDER_DATE): + self.dbapi.update_action_description( + self.action_desc3.id, values={"description": + "resize description"}) + + def test_get_action_desc_list_filter_deleted_true(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.dbapi.soft_delete_action_description(self.action_desc1.id) + + res = self.dbapi.get_action_description_list( + self.context, filters={'deleted': True}) + + self.assertEqual([self.action_desc1['action_type']], + [r.action_type for r in res]) + + def test_get_action_desc_list_filter_deleted_false(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.dbapi.soft_delete_action_description(self.action_desc1.id) + + res = self.dbapi.get_action_description_list( + self.context, filters={'deleted': False}) + + self.assertEqual( + set([self.action_desc2['action_type'], + self.action_desc3['action_type']]), + set([r.action_type for r in res])) + + def test_get_action_desc_list_filter_deleted_at_eq(self): + self._soft_delete_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'deleted_at__eq': self.FAKE_TODAY}) + + self.assertEqual([self.action_desc1['id']], [r.id for r in res]) + + def test_get_action_desc_list_filter_deleted_at_lt(self): + self._soft_delete_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'deleted_at__lt': self.FAKE_TODAY}) + + self.assertEqual( + set([self.action_desc2['id'], self.action_desc3['id']]), + set([r.id for r in res])) + + def test_get_action_desc_list_filter_deleted_at_lte(self): + self._soft_delete_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'deleted_at__lte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.action_desc2['id'], self.action_desc3['id']]), + set([r.id for r in res])) + + def test_get_action_desc_list_filter_deleted_at_gt(self): + self._soft_delete_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'deleted_at__gt': self.FAKE_OLD_DATE}) + + self.assertEqual([self.action_desc1['id']], [r.id for r in res]) + + def test_get_action_desc_list_filter_deleted_at_gte(self): + self._soft_delete_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'deleted_at__gte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.action_desc1['id'], self.action_desc2['id']]), + set([r.id for r in res])) + + # created_at # + + def test_get_action_desc_list_filter_created_at_eq(self): + res = self.dbapi.get_action_description_list( + self.context, filters={'created_at__eq': self.FAKE_TODAY}) + + self.assertEqual([self.action_desc1['id']], [r.id for r in res]) + + def test_get_action_desc_list_filter_created_at_lt(self): + res = self.dbapi.get_action_description_list( + self.context, filters={'created_at__lt': self.FAKE_TODAY}) + + self.assertEqual( + set([self.action_desc2['id'], self.action_desc3['id']]), + set([r.id for r in res])) + + def test_get_action_desc_list_filter_created_at_lte(self): + res = self.dbapi.get_action_description_list( + self.context, filters={'created_at__lte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.action_desc2['id'], self.action_desc3['id']]), + set([r.id for r in res])) + + def test_get_action_desc_list_filter_created_at_gt(self): + res = self.dbapi.get_action_description_list( + self.context, filters={'created_at__gt': self.FAKE_OLD_DATE}) + + self.assertEqual([self.action_desc1['id']], [r.id for r in res]) + + def test_get_action_desc_list_filter_created_at_gte(self): + res = self.dbapi.get_action_description_list( + self.context, filters={'created_at__gte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.action_desc1['id'], self.action_desc2['id']]), + set([r.id for r in res])) + + # updated_at # + + def test_get_action_desc_list_filter_updated_at_eq(self): + self._update_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'updated_at__eq': self.FAKE_TODAY}) + + self.assertEqual([self.action_desc1['id']], [r.id for r in res]) + + def test_get_action_desc_list_filter_updated_at_lt(self): + self._update_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'updated_at__lt': self.FAKE_TODAY}) + + self.assertEqual( + set([self.action_desc2['id'], self.action_desc3['id']]), + set([r.id for r in res])) + + def test_get_action_desc_list_filter_updated_at_lte(self): + self._update_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'updated_at__lte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.action_desc2['id'], self.action_desc3['id']]), + set([r.id for r in res])) + + def test_get_action_desc_list_filter_updated_at_gt(self): + self._update_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'updated_at__gt': self.FAKE_OLD_DATE}) + + self.assertEqual([self.action_desc1['id']], [r.id for r in res]) + + def test_get_action_desc_list_filter_updated_at_gte(self): + self._update_action_descs() + + res = self.dbapi.get_action_description_list( + self.context, filters={'updated_at__gte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.action_desc1['id'], self.action_desc2['id']]), + set([r.id for r in res])) + + +class DbActionDescriptionTestCase(base.DbTestCase): + + def _create_test_action_desc(self, **kwargs): + action_desc = utils.get_test_action_desc(**kwargs) + self.dbapi.create_action_description(action_desc) + return action_desc + + def test_get_action_desc_list(self): + ids = [] + for i in range(1, 4): + action_desc = utils.create_test_action_desc( + id=i, + action_type="action_%s" % i, + description="description_{0}".format(i)) + ids.append(action_desc['id']) + action_descs = self.dbapi.get_action_description_list(self.context) + action_desc_ids = [s.id for s in action_descs] + self.assertEqual(sorted(ids), sorted(action_desc_ids)) + + def test_get_action_desc_list_with_filters(self): + action_desc1 = self._create_test_action_desc( + id=1, + action_type="action_1", + description="description_1", + ) + action_desc2 = self._create_test_action_desc( + id=2, + action_type="action_2", + description="description_2", + ) + + res = self.dbapi.get_action_description_list( + self.context, filters={'action_type': 'action_1'}) + self.assertEqual([action_desc1['id']], [r.id for r in res]) + + res = self.dbapi.get_action_description_list( + self.context, filters={'action_type': 'action_3'}) + self.assertEqual([], [r.id for r in res]) + + res = self.dbapi.get_action_description_list( + self.context, + filters={'action_type': 'action_2'}) + self.assertEqual([action_desc2['id']], [r.id for r in res]) + + def test_get_action_desc_by_type(self): + created_action_desc = self._create_test_action_desc() + action_desc = self.dbapi.get_action_description_by_type( + self.context, created_action_desc['action_type']) + self.assertEqual(action_desc.action_type, + created_action_desc['action_type']) + + def test_get_action_desc_that_does_not_exist(self): + self.assertRaises(exception.ActionDescriptionNotFound, + self.dbapi.get_action_description_by_id, + self.context, 404) + + def test_update_action_desc(self): + action_desc = self._create_test_action_desc() + res = self.dbapi.update_action_description( + action_desc['id'], {'description': 'description_test'}) + self.assertEqual('description_test', res.description) diff --git a/watcher/tests/db/utils.py b/watcher/tests/db/utils.py index 65b88c6d8..435e9d30e 100644 --- a/watcher/tests/db/utils.py +++ b/watcher/tests/db/utils.py @@ -331,3 +331,26 @@ def create_test_efficacy_indicator(**kwargs): del efficacy_indicator['id'] dbapi = db_api.get_instance() return dbapi.create_efficacy_indicator(efficacy_indicator) + + +def get_test_action_desc(**kwargs): + return { + 'id': kwargs.get('id', 1), + 'action_type': kwargs.get('action_type', 'nop'), + 'description': kwargs.get('description', 'Logging a NOP message'), + 'created_at': kwargs.get('created_at'), + 'updated_at': kwargs.get('updated_at'), + 'deleted_at': kwargs.get('deleted_at'), + } + + +def create_test_action_desc(**kwargs): + """Create test action description entry in DB and return ActionDescription. + + Function to be used to create test ActionDescription objects in the DB. + :param kwargs: kwargs with overriding values for service's attributes. + :returns: Test ActionDescription DB object. + """ + action_desc = get_test_action_desc(**kwargs) + dbapi = db_api.get_instance() + return dbapi.create_action_description(action_desc) diff --git a/watcher/tests/objects/test_action_description.py b/watcher/tests/objects/test_action_description.py new file mode 100644 index 000000000..98b571d3e --- /dev/null +++ b/watcher/tests/objects/test_action_description.py @@ -0,0 +1,120 @@ +# -*- encoding: utf-8 -*- +# Copyright 2017 ZTE +# 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 datetime + +import iso8601 +import mock + +from watcher.db.sqlalchemy import api as db_api +from watcher import objects +from watcher.tests.db import base +from watcher.tests.db import utils + + +class TestActionDescriptionObject(base.DbTestCase): + + def setUp(self): + super(TestActionDescriptionObject, self).setUp() + self.fake_action_desc = utils.get_test_action_desc( + created_at=datetime.datetime.utcnow()) + + @mock.patch.object(db_api.Connection, 'get_action_description_by_id') + def test_get_by_id(self, mock_get_action_desc): + action_desc_id = self.fake_action_desc['id'] + mock_get_action_desc.return_value = self.fake_action_desc + action_desc = objects.ActionDescription.get( + self.context, action_desc_id) + mock_get_action_desc.assert_called_once_with( + self.context, action_desc_id) + self.assertEqual(self.context, action_desc._context) + + @mock.patch.object(db_api.Connection, 'get_action_description_list') + def test_list(self, mock_get_list): + mock_get_list.return_value = [self.fake_action_desc] + action_desc = objects.ActionDescription.list(self.context) + self.assertEqual(1, mock_get_list.call_count) + self.assertEqual(1, len(action_desc)) + self.assertIsInstance(action_desc[0], objects.ActionDescription) + self.assertEqual(self.context, action_desc[0]._context) + + @mock.patch.object(db_api.Connection, 'create_action_description') + def test_create(self, mock_create_action_desc): + mock_create_action_desc.return_value = self.fake_action_desc + action_desc = objects.ActionDescription( + self.context, **self.fake_action_desc) + + action_desc.create() + expected_action_desc = self.fake_action_desc.copy() + expected_action_desc['created_at'] = expected_action_desc[ + 'created_at'].replace(tzinfo=iso8601.iso8601.Utc()) + + mock_create_action_desc.assert_called_once_with(expected_action_desc) + self.assertEqual(self.context, action_desc._context) + + @mock.patch.object(db_api.Connection, 'update_action_description') + @mock.patch.object(db_api.Connection, 'get_action_description_by_id') + def test_save(self, mock_get_action_desc, mock_update_action_desc): + mock_get_action_desc.return_value = self.fake_action_desc + fake_saved_action_desc = self.fake_action_desc.copy() + fake_saved_action_desc['updated_at'] = datetime.datetime.utcnow() + mock_update_action_desc.return_value = fake_saved_action_desc + _id = self.fake_action_desc['id'] + action_desc = objects.ActionDescription.get(self.context, _id) + action_desc.description = 'This is a test' + action_desc.save() + + mock_get_action_desc.assert_called_once_with(self.context, _id) + mock_update_action_desc.assert_called_once_with( + _id, {'description': 'This is a test'}) + self.assertEqual(self.context, action_desc._context) + + @mock.patch.object(db_api.Connection, 'get_action_description_by_id') + def test_refresh(self, mock_get_action_desc): + returns = [dict(self.fake_action_desc, description="Test message1"), + dict(self.fake_action_desc, description="Test message2")] + mock_get_action_desc.side_effect = returns + _id = self.fake_action_desc['id'] + expected = [mock.call(self.context, _id), + mock.call(self.context, _id)] + action_desc = objects.ActionDescription.get(self.context, _id) + self.assertEqual("Test message1", action_desc.description) + action_desc.refresh() + self.assertEqual("Test message2", action_desc.description) + self.assertEqual(expected, mock_get_action_desc.call_args_list) + self.assertEqual(self.context, action_desc._context) + + @mock.patch.object(db_api.Connection, 'soft_delete_action_description') + @mock.patch.object(db_api.Connection, 'get_action_description_by_id') + def test_soft_delete(self, mock_get_action_desc, mock_soft_delete): + mock_get_action_desc.return_value = self.fake_action_desc + fake_deleted_action_desc = self.fake_action_desc.copy() + fake_deleted_action_desc['deleted_at'] = datetime.datetime.utcnow() + mock_soft_delete.return_value = fake_deleted_action_desc + + expected_action_desc = fake_deleted_action_desc.copy() + expected_action_desc['created_at'] = expected_action_desc[ + 'created_at'].replace(tzinfo=iso8601.iso8601.Utc()) + expected_action_desc['deleted_at'] = expected_action_desc[ + 'deleted_at'].replace(tzinfo=iso8601.iso8601.Utc()) + + _id = self.fake_action_desc['id'] + action_desc = objects.ActionDescription.get(self.context, _id) + action_desc.soft_delete() + mock_get_action_desc.assert_called_once_with(self.context, _id) + mock_soft_delete.assert_called_once_with(_id) + self.assertEqual(self.context, action_desc._context) + self.assertEqual(expected_action_desc, action_desc.as_dict()) diff --git a/watcher/tests/objects/test_objects.py b/watcher/tests/objects/test_objects.py index cc61f4681..50dd62045 100644 --- a/watcher/tests/objects/test_objects.py +++ b/watcher/tests/objects/test_objects.py @@ -419,6 +419,7 @@ expected_object_fingerprints = { 'ScoringEngine': '1.0-4abbe833544000728e17bd9e83f97576', 'Service': '1.0-4b35b99ada9677a882c9de2b30212f35', 'MyObj': '1.5-23c516d1e842f365f694e688d34e47c3', + 'ActionDescription': '1.0-5761a3d16651046e7a0c357b57a6583e' }