From a24b7f0b611201bfca1c70534f3f07d4baa662f7 Mon Sep 17 00:00:00 2001
From: licanwei
Date: Sat, 15 Jul 2017 02:25:43 -0700
Subject: [PATCH] dynamic action description
Add a new table to save the mapping
Add logic to update the table when action loading
Add logic to show the action description
Change-Id: Ia008a8715bcc666ab0fefe444ef612394c775e91
Implements: blueprint dynamic-action-description
---
...c-action-description-0e947b9e7ef2a134.yaml | 4 +
watcher/api/controllers/v1/action.py | 12 +
watcher/applier/sync.py | 44 +++
watcher/cmd/applier.py | 4 +
watcher/common/exception.py | 9 +
...9a5945e4a0_add_action_description_table.py | 32 ++
watcher/db/sqlalchemy/api.py | 71 +++++
watcher/db/sqlalchemy/models.py | 14 +
watcher/objects/__init__.py | 1 +
watcher/objects/action_description.py | 141 +++++++++
watcher/tests/cmd/test_applier.py | 2 +
watcher/tests/db/test_action_description.py | 293 ++++++++++++++++++
watcher/tests/db/utils.py | 23 ++
.../tests/objects/test_action_description.py | 120 +++++++
watcher/tests/objects/test_objects.py | 1 +
15 files changed, 771 insertions(+)
create mode 100644 releasenotes/notes/dynamic-action-description-0e947b9e7ef2a134.yaml
create mode 100644 watcher/applier/sync.py
create mode 100644 watcher/db/sqlalchemy/alembic/versions/d09a5945e4a0_add_action_description_table.py
create mode 100644 watcher/objects/action_description.py
create mode 100644 watcher/tests/db/test_action_description.py
create mode 100644 watcher/tests/objects/test_action_description.py
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'
}