From be9058f3e32b89076a8c41d964e8df483a0ce52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Fran=C3=A7oise?= Date: Wed, 23 Mar 2016 17:01:46 +0100 Subject: [PATCH] Added Goal model into Watcher DB In this changeset, I added the Goal model into Watcher. This implies a change into the Watcher DB schema Partially Implements: blueprint get-goal-from-strategy Change-Id: I5b5b0ffc7cff8affb59f17743e1af0e1277c2878 --- watcher/common/exception.py | 8 + watcher/db/api.py | 158 +++++++++++++--- watcher/db/sqlalchemy/api.py | 172 ++++++++++++++++- watcher/db/sqlalchemy/models.py | 17 +- watcher/tests/db/test_goal.py | 324 ++++++++++++++++++++++++++++++++ watcher/tests/db/utils.py | 26 ++- 6 files changed, 665 insertions(+), 40 deletions(-) create mode 100644 watcher/tests/db/test_goal.py diff --git a/watcher/common/exception.py b/watcher/common/exception.py index 53e99d9e7..6d33cb993 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -166,6 +166,14 @@ class InvalidUuidOrName(Invalid): msg_fmt = _("Expected a logical name or uuid but received %(name)s") +class GoalNotFound(ResourceNotFound): + msg_fmt = _("Goal %(goal)s could not be found") + + +class GoalAlreadyExists(Conflict): + msg_fmt = _("A goal with UUID %(uuid)s already exists") + + class AuditTemplateNotFound(ResourceNotFound): msg_fmt = _("AuditTemplate %(audit_template)s could not be found") diff --git a/watcher/db/api.py b/watcher/db/api.py index 9b543294b..93353cfd6 100644 --- a/watcher/db/api.py +++ b/watcher/db/api.py @@ -34,6 +34,102 @@ def get_instance(): class BaseConnection(object): """Base class for storage system connections.""" + @abc.abstractmethod + def get_goal_list(self, context, filters=None, limit=None, + marker=None, sort_key=None, sort_dir=None): + """Get specific columns for matching goals. + + Return a list of the specified columns for all goals that + match the specified filters. + + :param context: The security context + :param filters: Filters to apply. Defaults to None. + + :param limit: Maximum number of goals to return. + :param marker: the last item of the previous page; we return the next + result set. + :param sort_key: Attribute by which results should be sorted. + :param sort_dir: direction in which results should be sorted. + (asc, desc) + :returns: A list of tuples of the specified columns. + """ + + @abc.abstractmethod + def create_goal(self, values): + """Create a new goal. + + :param values: A dict containing several items used to identify + and track the goal. For example: + + :: + + { + 'uuid': utils.generate_uuid(), + 'name': 'DUMMY', + 'display_name': 'Dummy', + } + :returns: A goal + :raises: :py:class:`~.GoalAlreadyExists` + """ + + @abc.abstractmethod + def get_goal_by_id(self, context, goal_id): + """Return a goal given its ID. + + :param context: The security context + :param goal_id: The ID of a goal + :returns: A goal + :raises: :py:class:`~.GoalNotFound` + """ + + @abc.abstractmethod + def get_goal_by_uuid(self, context, goal_uuid): + """Return a goal given its UUID. + + :param context: The security context + :param goal_uuid: The UUID of a goal + :returns: A goal + :raises: :py:class:`~.GoalNotFound` + """ + + @abc.abstractmethod + def get_goal_by_name(self, context, goal_name): + """Return a goal given its name. + + :param context: The security context + :param goal_name: The name of a goal + :returns: A goal + :raises: :py:class:`~.GoalNotFound` + """ + + @abc.abstractmethod + def destroy_goal(self, goal_uuid): + """Destroy a goal. + + :param goal_uuid: The UUID of a goal + :raises: :py:class:`~.GoalNotFound` + """ + + @abc.abstractmethod + def update_goal(self, goal_uuid, values): + """Update properties of a goal. + + :param goal_uuid: The UUID of a goal + :param values: A dict containing several items used to identify + and track the goal. For example: + + :: + + { + 'uuid': utils.generate_uuid(), + 'name': 'DUMMY', + 'display_name': 'Dummy', + } + :returns: A goal + :raises: :py:class:`~.GoalNotFound` + :raises: :py:class:`~.Invalid` + """ + @abc.abstractmethod def get_audit_template_list(self, context, columns=None, filters=None, limit=None, marker=None, sort_key=None, @@ -75,7 +171,7 @@ class BaseConnection(object): 'extra': {'automatic': True} } :returns: An audit template. - :raises: AuditTemplateAlreadyExists + :raises: :py:class:`~.AuditTemplateAlreadyExists` """ @abc.abstractmethod @@ -85,7 +181,7 @@ class BaseConnection(object): :param context: The security context :param audit_template_id: The id of an audit template. :returns: An audit template. - :raises: AuditTemplateNotFound + :raises: :py:class:`~.AuditTemplateNotFound` """ @abc.abstractmethod @@ -95,7 +191,7 @@ class BaseConnection(object): :param context: The security context :param audit_template_uuid: The uuid of an audit template. :returns: An audit template. - :raises: AuditTemplateNotFound + :raises: :py:class:`~.AuditTemplateNotFound` """ def get_audit_template_by_name(self, context, audit_template_name): @@ -104,7 +200,7 @@ class BaseConnection(object): :param context: The security context :param audit_template_name: The name of an audit template. :returns: An audit template. - :raises: AuditTemplateNotFound + :raises: :py:class:`~.AuditTemplateNotFound` """ @abc.abstractmethod @@ -112,7 +208,7 @@ class BaseConnection(object): """Destroy an audit_template. :param audit_template_id: The id or uuid of an audit template. - :raises: AuditTemplateNotFound + :raises: :py:class:`~.AuditTemplateNotFound` """ @abc.abstractmethod @@ -121,8 +217,8 @@ class BaseConnection(object): :param audit_template_id: The id or uuid of an audit template. :returns: An audit template. - :raises: AuditTemplateNotFound - :raises: Invalid + :raises: :py:class:`~.AuditTemplateNotFound` + :raises: :py:class:`~.Invalid` """ @abc.abstractmethod @@ -130,7 +226,7 @@ class BaseConnection(object): """Soft delete an audit_template. :param audit_template_id: The id or uuid of an audit template. - :raises: AuditTemplateNotFound + :raises: :py:class:`~.AuditTemplateNotFound` """ @abc.abstractmethod @@ -171,7 +267,7 @@ class BaseConnection(object): 'deadline': None } :returns: An audit. - :raises: AuditAlreadyExists + :raises: :py:class:`~.AuditAlreadyExists` """ @abc.abstractmethod @@ -181,7 +277,7 @@ class BaseConnection(object): :param context: The security context :param audit_id: The id of an audit. :returns: An audit. - :raises: AuditNotFound + :raises: :py:class:`~.AuditNotFound` """ @abc.abstractmethod @@ -191,7 +287,7 @@ class BaseConnection(object): :param context: The security context :param audit_uuid: The uuid of an audit. :returns: An audit. - :raises: AuditNotFound + :raises: :py:class:`~.AuditNotFound` """ @abc.abstractmethod @@ -199,7 +295,7 @@ class BaseConnection(object): """Destroy an audit and all associated action plans. :param audit_id: The id or uuid of an audit. - :raises: AuditNotFound + :raises: :py:class:`~.AuditNotFound` """ @abc.abstractmethod @@ -208,8 +304,8 @@ class BaseConnection(object): :param audit_id: The id or uuid of an audit. :returns: An audit. - :raises: AuditNotFound - :raises: Invalid + :raises: :py:class:`~.AuditNotFound` + :raises: :py:class:`~.Invalid` """ def soft_delete_audit(self, audit_id): @@ -217,7 +313,7 @@ class BaseConnection(object): :param audit_id: The id or uuid of an audit. :returns: An audit. - :raises: AuditNotFound + :raises: :py:class:`~.AuditNotFound` """ @abc.abstractmethod @@ -259,7 +355,7 @@ class BaseConnection(object): 'aggregate': 'nova aggregate name or uuid' } :returns: A action. - :raises: ActionAlreadyExists + :raises: :py:class:`~.ActionAlreadyExists` """ @abc.abstractmethod @@ -269,7 +365,7 @@ class BaseConnection(object): :param context: The security context :param action_id: The id of a action. :returns: A action. - :raises: ActionNotFound + :raises: :py:class:`~.ActionNotFound` """ @abc.abstractmethod @@ -279,7 +375,7 @@ class BaseConnection(object): :param context: The security context :param action_uuid: The uuid of a action. :returns: A action. - :raises: ActionNotFound + :raises: :py:class:`~.ActionNotFound` """ @abc.abstractmethod @@ -287,8 +383,8 @@ class BaseConnection(object): """Destroy a action and all associated interfaces. :param action_id: The id or uuid of a action. - :raises: ActionNotFound - :raises: ActionReferenced + :raises: :py:class:`~.ActionNotFound` + :raises: :py:class:`~.ActionReferenced` """ @abc.abstractmethod @@ -297,9 +393,9 @@ class BaseConnection(object): :param action_id: The id or uuid of a action. :returns: A action. - :raises: ActionNotFound - :raises: ActionReferenced - :raises: Invalid + :raises: :py:class:`~.ActionNotFound` + :raises: :py:class:`~.ActionReferenced` + :raises: :py:class:`~.Invalid` """ @abc.abstractmethod @@ -332,7 +428,7 @@ class BaseConnection(object): :param values: A dict containing several items used to identify and track the action plan. :returns: An action plan. - :raises: ActionPlanAlreadyExists + :raises: :py:class:`~.ActionPlanAlreadyExists` """ @abc.abstractmethod @@ -342,7 +438,7 @@ class BaseConnection(object): :param context: The security context :param action_plan_id: The id of an action plan. :returns: An action plan. - :raises: ActionPlanNotFound + :raises: :py:class:`~.ActionPlanNotFound` """ @abc.abstractmethod @@ -352,7 +448,7 @@ class BaseConnection(object): :param context: The security context :param action_plan__uuid: The uuid of an action plan. :returns: An action plan. - :raises: ActionPlanNotFound + :raises: :py:class:`~.ActionPlanNotFound` """ @abc.abstractmethod @@ -360,8 +456,8 @@ class BaseConnection(object): """Destroy an action plan and all associated interfaces. :param action_plan_id: The id or uuid of a action plan. - :raises: ActionPlanNotFound - :raises: ActionPlanReferenced + :raises: :py:class:`~.ActionPlanNotFound` + :raises: :py:class:`~.ActionPlanReferenced` """ @abc.abstractmethod @@ -370,7 +466,7 @@ class BaseConnection(object): :param action_plan_id: The id or uuid of an action plan. :returns: An action plan. - :raises: ActionPlanNotFound - :raises: ActionPlanReferenced - :raises: Invalid + :raises: :py:class:`~.ActionPlanNotFound` + :raises: :py:class:`~.ActionPlanReferenced` + :raises: :py:class:`~.Invalid` """ diff --git a/watcher/db/sqlalchemy/api.py b/watcher/db/sqlalchemy/api.py index 2d7288297..568573981 100644 --- a/watcher/db/sqlalchemy/api.py +++ b/watcher/db/sqlalchemy/api.py @@ -182,6 +182,94 @@ class Connection(api.BaseConnection): return query + def __add_simple_filter(self, query, model, fieldname, value): + return query.filter(getattr(model, fieldname) == value) + + def __add_join_filter(self, query, model, join_model, fieldname, value): + query = query.join(join_model) + return self.__add_simple_filter(query, model, fieldname, value) + + def _add_filters(self, query, model, filters=None, + plain_fields=None, join_fieldmap=None): + filters = filters or {} + plain_fields = plain_fields or () + join_fieldmap = join_fieldmap or {} + + for fieldname, value in filters.items(): + if fieldname in plain_fields: + query = self.__add_simple_filter( + query, model, fieldname, value) + elif fieldname in join_fieldmap: + join_model = join_fieldmap[fieldname] + query = self.__add_join_filter( + query, model, join_model, fieldname, value) + + query = self.__add_soft_delete_mixin_filters(query, filters, model) + query = self.__add_timestamp_mixin_filters(query, filters, model) + + return query + + def _get(self, context, model, fieldname, value): + query = model_query(model) + query = query.filter(getattr(model, fieldname) == value) + if not context.show_deleted: + query = query.filter(model.deleted_at.is_(None)) + + try: + obj = query.one() + except exc.NoResultFound: + raise exception.ResourceNotFound(name=model.__name__, id=value) + + return obj + + def _update(self, model, id_, values): + session = get_session() + with session.begin(): + query = model_query(model, session=session) + query = add_identity_filter(query, id_) + try: + ref = query.with_lockmode('update').one() + except exc.NoResultFound: + raise exception.ResourceNotFound(name=model.__name__, id=id_) + + ref.update(values) + return ref + + def _soft_delete(self, model, id_): + session = get_session() + with session.begin(): + query = model_query(model, session=session) + query = add_identity_filter(query, id_) + try: + query.one() + except exc.NoResultFound: + raise exception.ResourceNotFound(name=model.__name__, id=id_) + + query.soft_delete() + + def _destroy(self, model, id_): + session = get_session() + with session.begin(): + query = model_query(model, session=session) + query = add_identity_filter(query, id_) + + try: + query.one() + except exc.NoResultFound: + raise exception.ResourceNotFound(name=model.__name__, id=id_) + + query.delete() + + def _add_goals_filters(self, query, filters): + if filters is None: + filters = {} + + plain_fields = ['uuid', 'name', 'display_name'] + + return self._add_filters( + query=query, model=models.Goal, filters=filters, + plain_fields=plain_fields) + def _add_audit_templates_filters(self, query, filters): if filters is None: filters = [] @@ -289,6 +377,72 @@ class Connection(api.BaseConnection): return query + # ### GOALS ### # + + def get_goal_list(self, context, filters=None, limit=None, + marker=None, sort_key=None, sort_dir=None): + + query = model_query(models.Goal) + query = self._add_goals_filters(query, filters) + if not context.show_deleted: + query = query.filter_by(deleted_at=None) + return _paginate_query(models.Goal, limit, marker, + sort_key, sort_dir, query) + + def create_goal(self, values): + # ensure defaults are present for new goals + if not values.get('uuid'): + values['uuid'] = utils.generate_uuid() + + goal = models.Goal() + goal.update(values) + + try: + goal.save() + except db_exc.DBDuplicateEntry: + raise exception.GoalAlreadyExists(uuid=values['uuid']) + return goal + + def _get_goal(self, context, fieldname, value): + try: + return self._get(context, model=models.Goal, + fieldname=fieldname, value=value) + except exception.ResourceNotFound: + raise exception.GoalNotFound(goal=value) + + def get_goal_by_id(self, context, goal_id): + return self._get_goal(context, fieldname="id", value=goal_id) + + def get_goal_by_uuid(self, context, goal_uuid): + return self._get_goal(context, fieldname="uuid", value=goal_uuid) + + def get_goal_by_name(self, context, goal_name): + return self._get_goal(context, fieldname="name", value=goal_name) + + def destroy_goal(self, goal_id): + try: + return self._destroy(models.Goal, goal_id) + except exception.ResourceNotFound: + raise exception.GoalNotFound(goal=goal_id) + + def update_goal(self, goal_id, values): + if 'uuid' in values: + raise exception.Invalid( + message=_("Cannot overwrite UUID for an existing Goal.")) + + try: + return self._update(models.Goal, goal_id, values) + except exception.ResourceNotFound: + raise exception.GoalNotFound(goal=goal_id) + + def soft_delete_goal(self, goal_id): + try: + self._soft_delete(models.Goal, goal_id) + except exception.ResourceNotFound: + raise exception.GoalNotFound(goal=goal_id) + + # ### AUDIT TEMPLATES ### # + def get_audit_template_list(self, context, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): @@ -373,7 +527,8 @@ class Connection(api.BaseConnection): try: query.one() except exc.NoResultFound: - raise exception.AuditTemplateNotFound(node=audit_template_id) + raise exception.AuditTemplateNotFound( + audit_template=audit_template_id) query.delete() @@ -408,10 +563,13 @@ class Connection(api.BaseConnection): try: query.one() except exc.NoResultFound: - raise exception.AuditTemplateNotFound(node=audit_template_id) + raise exception.AuditTemplateNotFound( + audit_template=audit_template_id) query.soft_delete() + # ### AUDITS ### # + def get_audit_list(self, context, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): query = model_query(models.Audit) @@ -518,10 +676,12 @@ class Connection(api.BaseConnection): try: query.one() except exc.NoResultFound: - raise exception.AuditNotFound(node=audit_id) + raise exception.AuditNotFound(audit=audit_id) query.soft_delete() + # ### ACTIONS ### # + def get_action_list(self, context, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): query = model_query(models.Action) @@ -611,10 +771,12 @@ class Connection(api.BaseConnection): try: query.one() except exc.NoResultFound: - raise exception.ActionNotFound(node=action_id) + raise exception.ActionNotFound(action=action_id) query.soft_delete() + # ### ACTION PLANS ### # + def get_action_plan_list( self, context, columns=None, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): @@ -722,6 +884,6 @@ class Connection(api.BaseConnection): try: query.one() except exc.NoResultFound: - raise exception.ActionPlanNotFound(node=action_plan_id) + raise exception.ActionPlanNotFound(action_plan=action_plan_id) query.soft_delete() diff --git a/watcher/db/sqlalchemy/models.py b/watcher/db/sqlalchemy/models.py index bf63f8dd0..411230af0 100644 --- a/watcher/db/sqlalchemy/models.py +++ b/watcher/db/sqlalchemy/models.py @@ -110,6 +110,20 @@ class WatcherBase(models.SoftDeleteMixin, Base = declarative_base(cls=WatcherBase) +class Goal(Base): + """Represents a goal.""" + + __tablename__ = 'goals' + __table_args__ = ( + schema.UniqueConstraint('uuid', name='uniq_goals0uuid'), + table_args(), + ) + id = Column(Integer, primary_key=True) + uuid = Column(String(36)) + name = Column(String(63), nullable=False) + display_name = Column(String(63), nullable=False) + + class AuditTemplate(Base): """Represents an audit template.""" @@ -175,9 +189,6 @@ class ActionPlan(Base): id = Column(Integer, primary_key=True) uuid = Column(String(36)) first_action_id = Column(Integer) - # first_action_id = Column(Integer, ForeignKeyConstraint( - # ['first_action_id'], ['actions.id'], name='fk_first_action_id'), - # nullable=True) audit_id = Column(Integer, ForeignKey('audits.id'), nullable=True) state = Column(String(20), nullable=True) diff --git a/watcher/tests/db/test_goal.py b/watcher/tests/db/test_goal.py new file mode 100644 index 000000000..44451a747 --- /dev/null +++ b/watcher/tests/db/test_goal.py @@ -0,0 +1,324 @@ +# 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. + +"""Tests for manipulating Goal via the DB API""" + +import freezegun +import six + +from watcher.common import exception +from watcher.common import utils as w_utils +from watcher.tests.db import base +from watcher.tests.db import utils + + +class TestDbGoalFilters(base.DbTestCase): + + FAKE_OLDER_DATE = '2014-01-01T09:52:05.219414' + FAKE_OLD_DATE = '2015-01-01T09:52:05.219414' + FAKE_TODAY = '2016-02-24T09:52:05.219414' + + def setUp(self): + super(TestDbGoalFilters, self).setUp() + self.context.show_deleted = True + self._data_setup() + + def _data_setup(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.goal1 = utils.create_test_goal( + id=1, uuid=w_utils.generate_uuid(), name="GOAL_1", + display_name="Goal 1") + with freezegun.freeze_time(self.FAKE_OLD_DATE): + self.goal2 = utils.create_test_goal( + id=2, uuid=w_utils.generate_uuid(), + name="GOAL_2", display_name="Goal 2") + with freezegun.freeze_time(self.FAKE_OLDER_DATE): + self.goal3 = utils.create_test_goal( + id=3, uuid=w_utils.generate_uuid(), + name="GOAL_3", display_name="Goal 3") + + def _soft_delete_goals(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.dbapi.soft_delete_goal(self.goal1.uuid) + with freezegun.freeze_time(self.FAKE_OLD_DATE): + self.dbapi.soft_delete_goal(self.goal2.uuid) + with freezegun.freeze_time(self.FAKE_OLDER_DATE): + self.dbapi.soft_delete_goal(self.goal3.uuid) + + def _update_goals(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.dbapi.update_goal( + self.goal1.uuid, values={"display_name": "goal1"}) + with freezegun.freeze_time(self.FAKE_OLD_DATE): + self.dbapi.update_goal( + self.goal2.uuid, values={"display_name": "goal2"}) + with freezegun.freeze_time(self.FAKE_OLDER_DATE): + self.dbapi.update_goal( + self.goal3.uuid, values={"display_name": "goal3"}) + + def test_get_goal_list_filter_deleted_true(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.dbapi.soft_delete_goal(self.goal1.uuid) + + res = self.dbapi.get_goal_list( + self.context, filters={'deleted': True}) + + self.assertEqual([self.goal1.uuid], [r.uuid for r in res]) + + def test_get_goal_list_filter_deleted_false(self): + with freezegun.freeze_time(self.FAKE_TODAY): + self.dbapi.soft_delete_goal(self.goal1.uuid) + + res = self.dbapi.get_goal_list( + self.context, filters={'deleted': False}) + + self.assertEqual( + set([self.goal2.uuid, self.goal3.uuid]), + set([r.uuid for r in res])) + + def test_get_goal_list_filter_deleted_at_eq(self): + self._soft_delete_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'deleted_at__eq': self.FAKE_TODAY}) + + self.assertEqual([self.goal1.uuid], [r.uuid for r in res]) + + def test_get_goal_list_filter_deleted_at_lt(self): + self._soft_delete_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'deleted_at__lt': self.FAKE_TODAY}) + + self.assertEqual( + set([self.goal2.uuid, self.goal3.uuid]), + set([r.uuid for r in res])) + + def test_get_goal_list_filter_deleted_at_lte(self): + self._soft_delete_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'deleted_at__lte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.goal2.uuid, self.goal3.uuid]), + set([r.uuid for r in res])) + + def test_get_goal_list_filter_deleted_at_gt(self): + self._soft_delete_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'deleted_at__gt': self.FAKE_OLD_DATE}) + + self.assertEqual([self.goal1.uuid], [r.uuid for r in res]) + + def test_get_goal_list_filter_deleted_at_gte(self): + self._soft_delete_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'deleted_at__gte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.goal1.uuid, self.goal2.uuid]), + set([r.uuid for r in res])) + + # created_at # + + def test_get_goal_list_filter_created_at_eq(self): + res = self.dbapi.get_goal_list( + self.context, filters={'created_at__eq': self.FAKE_TODAY}) + + self.assertEqual([self.goal1.uuid], [r.uuid for r in res]) + + def test_get_goal_list_filter_created_at_lt(self): + res = self.dbapi.get_goal_list( + self.context, filters={'created_at__lt': self.FAKE_TODAY}) + + self.assertEqual( + set([self.goal2.uuid, self.goal3.uuid]), + set([r.uuid for r in res])) + + def test_get_goal_list_filter_created_at_lte(self): + res = self.dbapi.get_goal_list( + self.context, filters={'created_at__lte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.goal2.uuid, self.goal3.uuid]), + set([r.uuid for r in res])) + + def test_get_goal_list_filter_created_at_gt(self): + res = self.dbapi.get_goal_list( + self.context, filters={'created_at__gt': self.FAKE_OLD_DATE}) + + self.assertEqual([self.goal1.uuid], [r.uuid for r in res]) + + def test_get_goal_list_filter_created_at_gte(self): + res = self.dbapi.get_goal_list( + self.context, filters={'created_at__gte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.goal1.uuid, self.goal2.uuid]), + set([r.uuid for r in res])) + + # updated_at # + + def test_get_goal_list_filter_updated_at_eq(self): + self._update_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'updated_at__eq': self.FAKE_TODAY}) + + self.assertEqual([self.goal1.uuid], [r.uuid for r in res]) + + def test_get_goal_list_filter_updated_at_lt(self): + self._update_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'updated_at__lt': self.FAKE_TODAY}) + + self.assertEqual( + set([self.goal2.uuid, self.goal3.uuid]), + set([r.uuid for r in res])) + + def test_get_goal_list_filter_updated_at_lte(self): + self._update_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'updated_at__lte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.goal2.uuid, self.goal3.uuid]), + set([r.uuid for r in res])) + + def test_get_goal_list_filter_updated_at_gt(self): + self._update_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'updated_at__gt': self.FAKE_OLD_DATE}) + + self.assertEqual([self.goal1.uuid], [r.uuid for r in res]) + + def test_get_goal_list_filter_updated_at_gte(self): + self._update_goals() + + res = self.dbapi.get_goal_list( + self.context, filters={'updated_at__gte': self.FAKE_OLD_DATE}) + + self.assertEqual( + set([self.goal1.uuid, self.goal2.uuid]), + set([r.uuid for r in res])) + + +class DbGoalTestCase(base.DbTestCase): + + def _create_test_goal(self, **kwargs): + goal = utils.get_test_goal(**kwargs) + self.dbapi.create_goal(goal) + return goal + + def test_get_goal_list(self): + uuids = [] + for i in range(1, 6): + goal = utils.create_test_goal( + id=i, + uuid=w_utils.generate_uuid(), + name="GOAL_%s" % i, + display_name='My Goal %s' % i) + uuids.append(six.text_type(goal['uuid'])) + res = self.dbapi.get_goal_list(self.context) + res_uuids = [r.uuid for r in res] + self.assertEqual(uuids.sort(), res_uuids.sort()) + + def test_get_goal_list_with_filters(self): + goal1_uuid = w_utils.generate_uuid() + goal2_uuid = w_utils.generate_uuid() + goal1 = self._create_test_goal( + id=1, + uuid=goal1_uuid, + name="GOAL_1", + display_name='Goal 1', + ) + goal2 = self._create_test_goal( + id=2, + uuid=goal2_uuid, + name="GOAL_2", + display_name='Goal 2', + ) + + res = self.dbapi.get_goal_list(self.context, + filters={'display_name': 'Goal 1'}) + self.assertEqual([goal1['uuid']], [r.uuid for r in res]) + + res = self.dbapi.get_goal_list(self.context, + filters={'display_name': 'Goal 3'}) + self.assertEqual([], [r.uuid for r in res]) + + res = self.dbapi.get_goal_list( + self.context, filters={'name': 'GOAL_1'}) + self.assertEqual([goal1['uuid']], [r.uuid for r in res]) + + res = self.dbapi.get_goal_list( + self.context, + filters={'display_name': 'Goal 2'}) + self.assertEqual([goal2['uuid']], [r.uuid for r in res]) + + def test_get_goal_by_uuid(self): + created_goal = self._create_test_goal() + goal = self.dbapi.get_goal_by_uuid(self.context, created_goal['uuid']) + self.assertEqual(goal.uuid, created_goal['uuid']) + + def test_get_goal_that_does_not_exist(self): + random_uuid = w_utils.generate_uuid() + self.assertRaises(exception.GoalNotFound, + self.dbapi.get_goal_by_uuid, + self.context, random_uuid) + + def test_update_goal(self): + goal = self._create_test_goal() + res = self.dbapi.update_goal(goal['uuid'], + {'display_name': 'updated-model'}) + self.assertEqual('updated-model', res.display_name) + + def test_update_goal_id(self): + goal = self._create_test_goal() + self.assertRaises(exception.Invalid, + self.dbapi.update_goal, goal['uuid'], + {'uuid': 'NEW_GOAL'}) + + def test_update_goal_that_does_not_exist(self): + random_uuid = w_utils.generate_uuid() + self.assertRaises(exception.GoalNotFound, + self.dbapi.update_goal, + random_uuid, + {'display_name': ''}) + + def test_destroy_goal(self): + goal = self._create_test_goal() + self.dbapi.destroy_goal(goal['uuid']) + self.assertRaises(exception.GoalNotFound, + self.dbapi.get_goal_by_uuid, + self.context, goal['uuid']) + + def test_destroy_goal_that_does_not_exist(self): + random_uuid = w_utils.generate_uuid() + self.assertRaises(exception.GoalNotFound, + self.dbapi.destroy_goal, random_uuid) + + def test_create_goal_already_exists(self): + goal_uuid = w_utils.generate_uuid() + self._create_test_goal(uuid=goal_uuid) + self.assertRaises(exception.GoalAlreadyExists, + self._create_test_goal, + uuid=goal_uuid) diff --git a/watcher/tests/db/utils.py b/watcher/tests/db/utils.py index dd8e2f48c..0014c3c16 100644 --- a/watcher/tests/db/utils.py +++ b/watcher/tests/db/utils.py @@ -127,7 +127,7 @@ def get_test_action_plan(**kwargs): def create_test_action_plan(**kwargs): - """Create test action plan entry in DB and return Action DB object. + """Create test action plan entry in DB and return Action Plan DB object. Function to be used to create test Action objects in the database. :param kwargs: kwargsargs with overriding values for action's attributes. @@ -139,3 +139,27 @@ def create_test_action_plan(**kwargs): del action['id'] dbapi = db_api.get_instance() return dbapi.create_action_plan(action) + + +def get_test_goal(**kwargs): + return { + 'id': kwargs.get('id', 1), + 'uuid': kwargs.get('uuid', 'f7ad87ae-4298-91cf-93a0-f35a852e3652'), + 'name': kwargs.get('name', 'TEST'), + 'display_name': kwargs.get('display_name', 'test goal'), + 'created_at': kwargs.get('created_at'), + 'updated_at': kwargs.get('updated_at'), + 'deleted_at': kwargs.get('deleted_at'), + } + + +def create_test_goal(**kwargs): + """Create test goal entry in DB and return Goal DB object. + + Function to be used to create test Goal objects in the database. + :param kwargs: kwargs which override default goal values of its attributes. + :returns: Test Goal DB object. + """ + goal = get_test_goal(**kwargs) + dbapi = db_api.get_instance() + return dbapi.create_goal(goal)