From e51e7e43172f5f121262e78724978532828f51ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Fran=C3=A7oise?= Date: Tue, 24 Jan 2017 11:08:08 +0100 Subject: [PATCH] Added action_plan.create|update|delete notifs In this changeset, I added 3 notifications: - action_plan.create - action_plan.update - action_plan.delete Partially Implements: blueprint action-plan-versioned-notifications-api Change-Id: I8821fc6f47e7486037839d81bed9e28020b02fdd --- .../action_plan-create.json | 54 ++++ .../action_plan-delete.json | 54 ++++ .../action_plan-update.json | 63 +++++ doc/notification_samples/audit-create.json | 2 + doc/notification_samples/audit-delete.json | 2 + .../audit-planner-end.json | 2 + .../audit-planner-error.json | 2 + .../audit-planner-start.json | 2 + .../audit-strategy-end.json | 2 + .../audit-strategy-error.json | 2 + .../audit-strategy-start.json | 2 + doc/notification_samples/audit-update.json | 2 + watcher/api/controllers/v1/action_plan.py | 7 +- watcher/applier/action_plan/default.py | 3 +- watcher/common/exception.py | 8 + watcher/decision_engine/sync.py | 6 +- watcher/notifications/__init__.py | 1 + watcher/notifications/action_plan.py | 267 ++++++++++++++++++ watcher/notifications/audit.py | 49 +++- watcher/notifications/base.py | 6 +- watcher/objects/action_plan.py | 54 +++- watcher/objects/fields.py | 3 +- watcher/tests/db/utils.py | 2 +- .../audit/test_audit_handlers.py | 11 +- .../decision_engine/model/monasca_metrics.py | 1 - .../planner/test_weight_planner.py | 1 + .../test_workload_stabilization_planner.py | 1 + .../test_action_plan_notification.py | 261 +++++++++++++++++ .../notifications/test_audit_notification.py | 42 ++- .../tests/notifications/test_notification.py | 12 +- watcher/tests/objects/test_action_plan.py | 45 ++- watcher/tests/objects/test_audit.py | 1 + watcher/tests/objects/utils.py | 128 ++++----- 33 files changed, 986 insertions(+), 112 deletions(-) create mode 100644 doc/notification_samples/action_plan-create.json create mode 100644 doc/notification_samples/action_plan-delete.json create mode 100644 doc/notification_samples/action_plan-update.json create mode 100644 watcher/notifications/action_plan.py create mode 100644 watcher/tests/notifications/test_action_plan_notification.py diff --git a/doc/notification_samples/action_plan-create.json b/doc/notification_samples/action_plan-create.json new file mode 100644 index 000000000..b3de9b7bf --- /dev/null +++ b/doc/notification_samples/action_plan-create.json @@ -0,0 +1,54 @@ +{ + "publisher_id": "infra-optim:node0", + "payload": { + "watcher_object.version": "1.0", + "watcher_object.data": { + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.version": "1.0", + "watcher_object.data": { + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "display_name": "test strategy", + "name": "TEST", + "updated_at": null, + "parameters_spec": {}, + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": null + }, + "watcher_object.namespace": "watcher", + "watcher_object.name": "StrategyPayload" + }, + "created_at": null, + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.version": "1.0", + "watcher_object.data": { + "audit_type": "ONESHOT", + "scope": [], + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", + "parameters": {}, + "interval": null, + "deleted_at": null, + "state": "PENDING", + "created_at": "2016-10-18T09:52:05Z", + "updated_at": null + }, + "watcher_object.namespace": "watcher", + "watcher_object.name": "TerseAuditPayload" + }, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "global_efficacy": {}, + "deleted_at": null, + "state": "RECOMMENDED", + "updated_at": null + }, + "watcher_object.namespace": "watcher", + "watcher_object.name": "ActionPlanCreatePayload" + }, + "priority": "INFO", + "message_id": "5148bff1-ea06-4ad6-8e4e-8c85ca5eb629", + "event_type": "action_plan.create", + "timestamp": "2016-10-18 09:52:05.219414" +} diff --git a/doc/notification_samples/action_plan-delete.json b/doc/notification_samples/action_plan-delete.json new file mode 100644 index 000000000..29d0762b7 --- /dev/null +++ b/doc/notification_samples/action_plan-delete.json @@ -0,0 +1,54 @@ +{ + "publisher_id": "infra-optim:node0", + "timestamp": "2016-10-18 09:52:05.219414", + "payload": { + "watcher_object.data": { + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "created_at": "2016-10-18T09:52:05Z", + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.data": { + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", + "interval": null, + "audit_type": "ONESHOT", + "scope": [], + "updated_at": null, + "deleted_at": null, + "state": "PENDING", + "created_at": "2016-10-18T09:52:05Z", + "parameters": {} + }, + "watcher_object.version": "1.0", + "watcher_object.name": "TerseAuditPayload", + "watcher_object.namespace": "watcher" + }, + "global_efficacy": {}, + "updated_at": null, + "deleted_at": null, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.data": { + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "created_at": "2016-10-18T09:52:05Z", + "name": "TEST", + "display_name": "test strategy", + "deleted_at": null, + "updated_at": null, + "parameters_spec": {} + }, + "watcher_object.version": "1.0", + "watcher_object.name": "StrategyPayload", + "watcher_object.namespace": "watcher" + }, + "state": "DELETED" + }, + "watcher_object.version": "1.0", + "watcher_object.name": "ActionPlanDeletePayload", + "watcher_object.namespace": "watcher" + }, + "event_type": "action_plan.delete", + "message_id": "3d137686-a1fd-4683-ab40-c4210aac2140", + "priority": "INFO" +} diff --git a/doc/notification_samples/action_plan-update.json b/doc/notification_samples/action_plan-update.json new file mode 100644 index 000000000..60f7eece5 --- /dev/null +++ b/doc/notification_samples/action_plan-update.json @@ -0,0 +1,63 @@ +{ + "payload": { + "watcher_object.version": "1.0", + "watcher_object.data": { + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.version": "1.0", + "watcher_object.data": { + "audit_type": "ONESHOT", + "scope": [], + "created_at": "2016-10-18T09:52:05Z", + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", + "interval": null, + "updated_at": null, + "state": "PENDING", + "deleted_at": null, + "parameters": {} + }, + "watcher_object.namespace": "watcher", + "watcher_object.name": "TerseAuditPayload" + }, + "created_at": "2016-10-18T09:52:05Z", + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "updated_at": null, + "state_update": { + "watcher_object.version": "1.0", + "watcher_object.data": { + "old_state": "PENDING", + "state": "ONGOING" + }, + "watcher_object.namespace": "watcher", + "watcher_object.name": "ActionPlanStateUpdatePayload" + }, + "state": "ONGOING", + "deleted_at": null, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.version": "1.0", + "watcher_object.data": { + "name": "TEST", + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "display_name": "test strategy", + "created_at": "2016-10-18T09:52:05Z", + "updated_at": null, + "deleted_at": null, + "parameters_spec": {} + }, + "watcher_object.namespace": "watcher", + "watcher_object.name": "StrategyPayload" + }, + "global_efficacy": {} + }, + "watcher_object.namespace": "watcher", + "watcher_object.name": "ActionPlanUpdatePayload" + }, + "publisher_id": "infra-optim:node0", + "priority": "INFO", + "timestamp": "2016-10-18 09:52:05.219414", + "event_type": "action_plan.update", + "message_id": "0a8a7329-fd5a-4ec6-97d7-2b776ce51a4c" +} diff --git a/doc/notification_samples/audit-create.json b/doc/notification_samples/audit-create.json index f74516b78..dd655ea16 100644 --- a/doc/notification_samples/audit-create.json +++ b/doc/notification_samples/audit-create.json @@ -10,6 +10,7 @@ "state": "PENDING", "updated_at": null, "deleted_at": null, + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.data": { "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", @@ -26,6 +27,7 @@ }, "interval": null, "scope": [], + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.data": { "parameters_spec": { diff --git a/doc/notification_samples/audit-delete.json b/doc/notification_samples/audit-delete.json index 70b66cc35..752782947 100644 --- a/doc/notification_samples/audit-delete.json +++ b/doc/notification_samples/audit-delete.json @@ -10,6 +10,7 @@ "state": "DELETED", "updated_at": null, "deleted_at": null, + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.data": { "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", @@ -26,6 +27,7 @@ }, "interval": null, "scope": [], + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.data": { "parameters_spec": { diff --git a/doc/notification_samples/audit-planner-end.json b/doc/notification_samples/audit-planner-end.json index 8b820ee30..d3307c037 100644 --- a/doc/notification_samples/audit-planner-end.json +++ b/doc/notification_samples/audit-planner-end.json @@ -11,6 +11,7 @@ "updated_at": null, "deleted_at": null, "fault": null, + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.data": { "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", @@ -27,6 +28,7 @@ }, "interval": null, "scope": [], + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.data": { "parameters_spec": { diff --git a/doc/notification_samples/audit-planner-error.json b/doc/notification_samples/audit-planner-error.json index 5f56604bd..d3b16359a 100644 --- a/doc/notification_samples/audit-planner-error.json +++ b/doc/notification_samples/audit-planner-error.json @@ -21,6 +21,7 @@ "watcher_object.namespace": "watcher", "watcher_object.version": "1.0" }, + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.data": { "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", @@ -37,6 +38,7 @@ }, "interval": null, "scope": [], + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.data": { "parameters_spec": { diff --git a/doc/notification_samples/audit-planner-start.json b/doc/notification_samples/audit-planner-start.json index e5a93806e..93644dde0 100644 --- a/doc/notification_samples/audit-planner-start.json +++ b/doc/notification_samples/audit-planner-start.json @@ -11,6 +11,7 @@ "updated_at": null, "deleted_at": null, "fault": null, + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.data": { "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", @@ -27,6 +28,7 @@ }, "interval": null, "scope": [], + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.data": { "parameters_spec": { diff --git a/doc/notification_samples/audit-strategy-end.json b/doc/notification_samples/audit-strategy-end.json index 3cf13b8c2..3874fbfef 100644 --- a/doc/notification_samples/audit-strategy-end.json +++ b/doc/notification_samples/audit-strategy-end.json @@ -11,6 +11,7 @@ "updated_at": null, "deleted_at": null, "fault": null, + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.data": { "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", @@ -27,6 +28,7 @@ }, "interval": null, "scope": [], + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.data": { "parameters_spec": { diff --git a/doc/notification_samples/audit-strategy-error.json b/doc/notification_samples/audit-strategy-error.json index 91bb6f8b6..4c6fd1853 100644 --- a/doc/notification_samples/audit-strategy-error.json +++ b/doc/notification_samples/audit-strategy-error.json @@ -21,6 +21,7 @@ "watcher_object.namespace": "watcher", "watcher_object.version": "1.0" }, + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.data": { "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", @@ -37,6 +38,7 @@ }, "interval": null, "scope": [], + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.data": { "parameters_spec": { diff --git a/doc/notification_samples/audit-strategy-start.json b/doc/notification_samples/audit-strategy-start.json index 37646e37c..43322a7bd 100644 --- a/doc/notification_samples/audit-strategy-start.json +++ b/doc/notification_samples/audit-strategy-start.json @@ -11,6 +11,7 @@ "updated_at": null, "deleted_at": null, "fault": null, + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.data": { "uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", @@ -27,6 +28,7 @@ }, "interval": null, "scope": [], + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.data": { "parameters_spec": { diff --git a/doc/notification_samples/audit-update.json b/doc/notification_samples/audit-update.json index 5cf90ac10..3dc4b0ba8 100644 --- a/doc/notification_samples/audit-update.json +++ b/doc/notification_samples/audit-update.json @@ -4,6 +4,7 @@ "payload": { "watcher_object.name": "AuditUpdatePayload", "watcher_object.data": { + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", "strategy": { "watcher_object.name": "StrategyPayload", "watcher_object.data": { @@ -36,6 +37,7 @@ "scope": [], "created_at": "2016-11-04T16:51:21Z", "uuid": "f1e0d912-afd9-4bf2-91ef-c99cd08cc1ef", + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", "goal": { "watcher_object.name": "GoalPayload", "watcher_object.data": { diff --git a/watcher/api/controllers/v1/action_plan.py b/watcher/api/controllers/v1/action_plan.py index 47a121654..5a8745a59 100644 --- a/watcher/api/controllers/v1/action_plan.py +++ b/watcher/api/controllers/v1/action_plan.py @@ -455,7 +455,8 @@ class ActionPlansController(rest.RestController): :param action_plan_uuid: UUID of a action. """ context = pecan.request.context - action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid) + action_plan = api_utils.get_resource( + 'ActionPlan', action_plan_uuid, eager=True) policy.enforce(context, 'action_plan:delete', action_plan, action='action_plan:delete') @@ -474,8 +475,8 @@ class ActionPlansController(rest.RestController): raise exception.OperationNotPermitted context = pecan.request.context - action_plan_to_update = api_utils.get_resource('ActionPlan', - action_plan_uuid) + action_plan_to_update = api_utils.get_resource( + 'ActionPlan', action_plan_uuid, eager=True) policy.enforce(context, 'action_plan:update', action_plan_to_update, action='action_plan:update') diff --git a/watcher/applier/action_plan/default.py b/watcher/applier/action_plan/default.py index 0b1d428d8..36437e208 100644 --- a/watcher/applier/action_plan/default.py +++ b/watcher/applier/action_plan/default.py @@ -33,7 +33,8 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler): self.action_plan_uuid = action_plan_uuid def update_action_plan(self, uuid, state): - action_plan = objects.ActionPlan.get_by_uuid(self.ctx, uuid) + action_plan = objects.ActionPlan.get_by_uuid( + self.ctx, uuid, eager=True) action_plan.state = state action_plan.save() diff --git a/watcher/common/exception.py b/watcher/common/exception.py index bf2679eee..c4b1dd6f7 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -174,6 +174,14 @@ class EagerlyLoadedAuditRequired(InvalidAudit): msg_fmt = _("Audit %(audit)s was not eagerly loaded") +class InvalidActionPlan(Invalid): + msg_fmt = _("Action plan %(action_plan)s is invalid") + + +class EagerlyLoadedActionPlanRequired(InvalidActionPlan): + msg_fmt = _("Action plan %(action_plan)s was not eagerly loaded") + + class InvalidUUID(Invalid): msg_fmt = _("Expected a uuid but received %(uuid)s") diff --git a/watcher/decision_engine/sync.py b/watcher/decision_engine/sync.py index 371433d86..079431597 100644 --- a/watcher/decision_engine/sync.py +++ b/watcher/decision_engine/sync.py @@ -350,7 +350,7 @@ class Syncer(object): for strategy_id, synced_strategy in self.strategy_mapping.items(): filters = {"strategy_id": strategy_id} stale_action_plans = objects.ActionPlan.list( - self.ctx, filters=filters) + self.ctx, filters=filters, eager=True) # Update strategy IDs for all stale action plans (w/o saving) for action_plan in stale_action_plans: @@ -369,7 +369,7 @@ class Syncer(object): for audit_id, synced_audit in self.stale_audits_map.items(): filters = {"audit_id": audit_id} stale_action_plans = objects.ActionPlan.list( - self.ctx, filters=filters) + self.ctx, filters=filters, eager=True) # Update audit IDs for all stale action plans (w/o saving) for action_plan in stale_action_plans: @@ -448,7 +448,7 @@ class Syncer(object): audit.id].state = objects.audit.State.CANCELLED stale_action_plans = objects.ActionPlan.list( - self.ctx, filters=filters) + self.ctx, filters=filters, eager=True) for action_plan in stale_action_plans: LOG.warning( _LW("Action Plan '%(action_plan)s' references a " diff --git a/watcher/notifications/__init__.py b/watcher/notifications/__init__.py index 14151440a..d33d07d17 100644 --- a/watcher/notifications/__init__.py +++ b/watcher/notifications/__init__.py @@ -20,6 +20,7 @@ # need to be changed after we moved these function inside the package # Todo(gibi): remove these imports after legacy notifications using these are # transformed to versioned notifications +from watcher.notifications import action_plan # noqa from watcher.notifications import audit # noqa from watcher.notifications import exception # noqa from watcher.notifications import goal # noqa diff --git a/watcher/notifications/action_plan.py b/watcher/notifications/action_plan.py new file mode 100644 index 000000000..fe5f2ef3b --- /dev/null +++ b/watcher/notifications/action_plan.py @@ -0,0 +1,267 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2017 b<>com +# +# Authors: Vincent FRANCOISE +# +# 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 oslo_config import cfg + +from watcher.common import context as wcontext +from watcher.common import exception +from watcher.notifications import audit as audit_notifications +from watcher.notifications import base as notificationbase +from watcher.notifications import strategy as strategy_notifications +from watcher import objects +from watcher.objects import base +from watcher.objects import fields as wfields + +CONF = cfg.CONF + + +@base.WatcherObjectRegistry.register_notification +class ActionPlanPayload(notificationbase.NotificationPayloadBase): + SCHEMA = { + 'uuid': ('action_plan', 'uuid'), + + 'state': ('action_plan', 'state'), + 'global_efficacy': ('action_plan', 'global_efficacy'), + 'audit_uuid': ('audit', 'uuid'), + 'strategy_uuid': ('strategy', 'uuid'), + + 'created_at': ('action_plan', 'created_at'), + 'updated_at': ('action_plan', 'updated_at'), + 'deleted_at': ('action_plan', 'deleted_at'), + } + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'uuid': wfields.UUIDField(), + 'state': wfields.StringField(), + 'global_efficacy': wfields.FlexibleDictField(nullable=True), + 'audit_uuid': wfields.UUIDField(), + 'strategy_uuid': wfields.UUIDField(), + 'audit': wfields.ObjectField('TerseAuditPayload'), + 'strategy': wfields.ObjectField('StrategyPayload'), + + 'created_at': wfields.DateTimeField(nullable=True), + 'updated_at': wfields.DateTimeField(nullable=True), + 'deleted_at': wfields.DateTimeField(nullable=True), + } + + def __init__(self, action_plan, audit, strategy, **kwargs): + super(ActionPlanPayload, self).__init__( + audit=audit, strategy=strategy, **kwargs) + self.populate_schema( + action_plan=action_plan, audit=audit, strategy=strategy) + + +@base.WatcherObjectRegistry.register_notification +class ActionPlanStateUpdatePayload(notificationbase.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'old_state': wfields.StringField(nullable=True), + 'state': wfields.StringField(nullable=True), + } + + +@base.WatcherObjectRegistry.register_notification +class ActionPlanCreatePayload(ActionPlanPayload): + # Version 1.0: Initial version + VERSION = '1.0' + fields = {} + + def __init__(self, action_plan, audit, strategy): + super(ActionPlanCreatePayload, self).__init__( + action_plan=action_plan, + audit=audit, + strategy=strategy) + + +@base.WatcherObjectRegistry.register_notification +class ActionPlanUpdatePayload(ActionPlanPayload): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'state_update': wfields.ObjectField('ActionPlanStateUpdatePayload'), + } + + def __init__(self, action_plan, state_update, audit, strategy): + super(ActionPlanUpdatePayload, self).__init__( + action_plan=action_plan, + state_update=state_update, + audit=audit, + strategy=strategy) + + +@base.WatcherObjectRegistry.register_notification +class ActionPlanActionPayload(ActionPlanPayload): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'fault': wfields.ObjectField('ExceptionPayload', nullable=True), + } + + def __init__(self, action_plan, audit, strategy, **kwargs): + super(ActionPlanActionPayload, self).__init__( + action_plan=action_plan, + audit=audit, + strategy=strategy, + **kwargs) + + +@base.WatcherObjectRegistry.register_notification +class ActionPlanDeletePayload(ActionPlanPayload): + # Version 1.0: Initial version + VERSION = '1.0' + fields = {} + + def __init__(self, action_plan, audit, strategy): + super(ActionPlanDeletePayload, self).__init__( + action_plan=action_plan, + audit=audit, + strategy=strategy) + + +@notificationbase.notification_sample('action_plan-create.json') +@base.WatcherObjectRegistry.register_notification +class ActionPlanCreateNotification(notificationbase.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': wfields.ObjectField('ActionPlanCreatePayload') + } + + +@notificationbase.notification_sample('action_plan-update.json') +@base.WatcherObjectRegistry.register_notification +class ActionPlanUpdateNotification(notificationbase.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': wfields.ObjectField('ActionPlanUpdatePayload') + } + + +@notificationbase.notification_sample('action_plan-delete.json') +@base.WatcherObjectRegistry.register_notification +class ActionPlanDeleteNotification(notificationbase.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': wfields.ObjectField('ActionPlanDeletePayload') + } + + +def _get_common_payload(action_plan): + audit = None + strategy = None + try: + audit = action_plan.audit + strategy = action_plan.strategy + except NotImplementedError: + raise exception.EagerlyLoadedActionPlanRequired( + action_plan=action_plan.uuid) + + goal = objects.Goal.get( + wcontext.make_context(show_deleted=True), audit.goal_id) + audit_payload = audit_notifications.TerseAuditPayload( + audit=audit, goal_uuid=goal.uuid) + + strategy_payload = strategy_notifications.StrategyPayload( + strategy=strategy) + + return audit_payload, strategy_payload + + +def send_create(context, action_plan, service='infra-optim', host=None): + """Emit an action_plan.create notification.""" + audit_payload, strategy_payload = _get_common_payload(action_plan) + + versioned_payload = ActionPlanCreatePayload( + action_plan=action_plan, + audit=audit_payload, + strategy=strategy_payload, + ) + + notification = ActionPlanCreateNotification( + priority=wfields.NotificationPriority.INFO, + event_type=notificationbase.EventType( + object='action_plan', + action=wfields.NotificationAction.CREATE), + publisher=notificationbase.NotificationPublisher( + host=host or CONF.host, + binary=service), + payload=versioned_payload) + + notification.emit(context) + + +def send_update(context, action_plan, service='infra-optim', + host=None, old_state=None): + """Emit an action_plan.update notification.""" + audit_payload, strategy_payload = _get_common_payload(action_plan) + + state_update = ActionPlanStateUpdatePayload( + old_state=old_state, + state=action_plan.state if old_state else None) + + versioned_payload = ActionPlanUpdatePayload( + action_plan=action_plan, + state_update=state_update, + audit=audit_payload, + strategy=strategy_payload, + ) + + notification = ActionPlanUpdateNotification( + priority=wfields.NotificationPriority.INFO, + event_type=notificationbase.EventType( + object='action_plan', + action=wfields.NotificationAction.UPDATE), + publisher=notificationbase.NotificationPublisher( + host=host or CONF.host, + binary=service), + payload=versioned_payload) + + notification.emit(context) + + +def send_delete(context, action_plan, service='infra-optim', host=None): + """Emit an action_plan.delete notification.""" + audit_payload, strategy_payload = _get_common_payload(action_plan) + + versioned_payload = ActionPlanDeletePayload( + action_plan=action_plan, + audit=audit_payload, + strategy=strategy_payload, + ) + + notification = ActionPlanDeleteNotification( + priority=wfields.NotificationPriority.INFO, + event_type=notificationbase.EventType( + object='action_plan', + action=wfields.NotificationAction.DELETE), + publisher=notificationbase.NotificationPublisher( + host=host or CONF.host, + binary=service), + payload=versioned_payload) + + notification.emit(context) diff --git a/watcher/notifications/audit.py b/watcher/notifications/audit.py index 1b05333fd..cc5878a20 100644 --- a/watcher/notifications/audit.py +++ b/watcher/notifications/audit.py @@ -30,7 +30,7 @@ CONF = cfg.CONF @base.WatcherObjectRegistry.register_notification -class AuditPayload(notificationbase.NotificationPayloadBase): +class TerseAuditPayload(notificationbase.NotificationPayloadBase): SCHEMA = { 'uuid': ('audit', 'uuid'), @@ -57,19 +57,54 @@ class AuditPayload(notificationbase.NotificationPayloadBase): 'scope': wfields.FlexibleListOfDictField(nullable=True), 'goal_uuid': wfields.UUIDField(), 'strategy_uuid': wfields.UUIDField(nullable=True), - 'goal': wfields.ObjectField('GoalPayload'), - 'strategy': wfields.ObjectField('StrategyPayload', nullable=True), 'created_at': wfields.DateTimeField(nullable=True), 'updated_at': wfields.DateTimeField(nullable=True), 'deleted_at': wfields.DateTimeField(nullable=True), } - def __init__(self, audit, **kwargs): - super(AuditPayload, self).__init__(**kwargs) + def __init__(self, audit, goal_uuid, strategy_uuid=None, **kwargs): + super(TerseAuditPayload, self).__init__( + goal_uuid=goal_uuid, strategy_uuid=strategy_uuid, **kwargs) self.populate_schema(audit=audit) +@base.WatcherObjectRegistry.register_notification +class AuditPayload(TerseAuditPayload): + SCHEMA = { + 'uuid': ('audit', 'uuid'), + + 'audit_type': ('audit', 'audit_type'), + 'state': ('audit', 'state'), + 'parameters': ('audit', 'parameters'), + 'interval': ('audit', 'interval'), + 'scope': ('audit', 'scope'), + + 'created_at': ('audit', 'created_at'), + 'updated_at': ('audit', 'updated_at'), + 'deleted_at': ('audit', 'deleted_at'), + } + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'goal': wfields.ObjectField('GoalPayload'), + 'strategy': wfields.ObjectField('StrategyPayload', nullable=True), + } + + def __init__(self, audit, goal, strategy=None, **kwargs): + if not kwargs.get('goal_uuid'): + kwargs['goal_uuid'] = goal.uuid + + if strategy and not kwargs.get('strategy_uuid'): + kwargs['strategy_uuid'] = strategy.uuid + + super(AuditPayload, self).__init__( + audit=audit, goal=goal, + strategy=strategy, **kwargs) + + @base.WatcherObjectRegistry.register_notification class AuditStateUpdatePayload(notificationbase.NotificationPayloadBase): # Version 1.0: Initial version @@ -91,6 +126,7 @@ class AuditCreatePayload(AuditPayload): super(AuditCreatePayload, self).__init__( audit=audit, goal=goal, + goal_uuid=goal.uuid, strategy=strategy) @@ -107,6 +143,7 @@ class AuditUpdatePayload(AuditPayload): audit=audit, state_update=state_update, goal=goal, + goal_uuid=goal.uuid, strategy=strategy) @@ -122,6 +159,7 @@ class AuditActionPayload(AuditPayload): super(AuditActionPayload, self).__init__( audit=audit, goal=goal, + goal_uuid=goal.uuid, strategy=strategy, **kwargs) @@ -136,6 +174,7 @@ class AuditDeletePayload(AuditPayload): super(AuditDeletePayload, self).__init__( audit=audit, goal=goal, + goal_uuid=goal.uuid, strategy=strategy) diff --git a/watcher/notifications/base.py b/watcher/notifications/base.py index 69b66a0ef..cb035c92f 100644 --- a/watcher/notifications/base.py +++ b/watcher/notifications/base.py @@ -13,6 +13,7 @@ # under the License. from oslo_config import cfg +from oslo_log import log from watcher.common import exception from watcher.common import rpc @@ -20,6 +21,7 @@ from watcher.objects import base from watcher.objects import fields as wfields CONF = cfg.CONF +LOG = log.getLogger(__name__) # Definition of notification levels in increasing order of severity NOTIFY_LEVELS = { @@ -59,7 +61,8 @@ class EventType(NotificationObject): # Version 1.0: Initial version # Version 1.1: Added STRATEGY action in NotificationAction enum # Version 1.2: Added PLANNER action in NotificationAction enum - VERSION = '1.2' + # Version 1.3: Added EXECUTION action in NotificationAction enum + VERSION = '1.3' fields = { 'object': wfields.StringField(), @@ -171,6 +174,7 @@ class NotificationBase(NotificationObject): def _emit(self, context, event_type, publisher_id, payload): notifier = rpc.get_notifier(publisher_id) notify = getattr(notifier, self.priority) + LOG.debug("Emitting notification `%s`", event_type) notify(context, event_type=event_type, payload=payload) def emit(self, context): diff --git a/watcher/objects/action_plan.py b/watcher/objects/action_plan.py index 9e1f1414f..bfb61a16f 100644 --- a/watcher/objects/action_plan.py +++ b/watcher/objects/action_plan.py @@ -72,6 +72,7 @@ state may be one of the following: from watcher.common import exception from watcher.common import utils from watcher.db import api as db_api +from watcher import notifications from watcher import objects from watcher.objects import base from watcher.objects import fields as wfields @@ -117,6 +118,39 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject, 'strategy': (objects.Strategy, 'strategy_id'), } + # Proxified field so we can keep the previous value after an update + _state = None + _old_state = None + + # NOTE(v-francoise): The way oslo.versionedobjects works is by using a + # __new__ that will automatically create the attributes referenced in + # fields. These attributes are properties that raise an exception if no + # value has been assigned, which means that they store the actual field + # value in an "_obj_%(field)s" attribute. So because we want to proxify a + # value that is already proxified, we have to do what you see below. + @property + def _obj_state(self): + return self._state + + @property + def _obj_old_state(self): + return self._old_state + + @property + def old_state(self): + return self._old_state + + @_obj_old_state.setter + def _obj_old_state(self, value): + self._old_state = value + + @_obj_state.setter + def _obj_state(self, value): + if self._old_state is None and self._state is None: + self._state = value + else: + self._old_state, self._state = self._state, value + @base.remotable_classmethod def get(cls, context, action_plan_id, eager=False): """Find a action_plan based on its id or uuid and return a Action object. @@ -198,6 +232,11 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject, # notifications containing information about the related relationships self._from_db_object(self, db_action_plan, eager=True) + def _notify(): + notifications.action_plan.send_create(self._context, self) + + _notify() + @base.remotable def destroy(self): """Delete the action plan from the DB""" @@ -221,8 +260,16 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject, """ updates = self.obj_get_changes() db_obj = self.dbapi.update_action_plan(self.uuid, updates) - obj = self._from_db_object(self, db_obj, eager=False) + obj = self._from_db_object( + self.__class__(self._context), db_obj, eager=False) self.obj_refresh(obj) + + def _notify(): + notifications.action_plan.send_update( + self._context, self, old_state=self.old_state) + + _notify() + self.obj_reset_changes() @base.remotable @@ -262,3 +309,8 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject, obj = self._from_db_object( self.__class__(self._context), db_obj, eager=False) self.obj_refresh(obj) + + def _notify(): + notifications.action_plan.send_delete(self._context, self) + + _notify() diff --git a/watcher/objects/fields.py b/watcher/objects/fields.py index cdcfb07bf..1d26209dc 100644 --- a/watcher/objects/fields.py +++ b/watcher/objects/fields.py @@ -128,8 +128,9 @@ class NotificationAction(BaseWatcherEnum): STRATEGY = 'strategy' PLANNER = 'planner' + EXECUTION = 'execution' - ALL = (CREATE, UPDATE, EXCEPTION, DELETE, STRATEGY, PLANNER) + ALL = (CREATE, UPDATE, EXCEPTION, DELETE, STRATEGY, PLANNER, EXECUTION) class NotificationPriorityField(BaseEnumField): diff --git a/watcher/tests/db/utils.py b/watcher/tests/db/utils.py index f94bbc82c..4cb66170a 100644 --- a/watcher/tests/db/utils.py +++ b/watcher/tests/db/utils.py @@ -89,7 +89,7 @@ def get_test_audit(**kwargs): 'updated_at': kwargs.get('updated_at'), 'deleted_at': kwargs.get('deleted_at'), 'parameters': kwargs.get('parameters', {}), - 'interval': kwargs.get('period', 3600), + 'interval': kwargs.get('interval', 3600), 'goal_id': kwargs.get('goal_id', 1), 'strategy_id': kwargs.get('strategy_id', None), 'scope': kwargs.get('scope', []), diff --git a/watcher/tests/decision_engine/audit/test_audit_handlers.py b/watcher/tests/decision_engine/audit/test_audit_handlers.py index 48e4413e4..c5b334e4f 100644 --- a/watcher/tests/decision_engine/audit/test_audit_handlers.py +++ b/watcher/tests/decision_engine/audit/test_audit_handlers.py @@ -175,12 +175,19 @@ class TestAutoTriggerActionPlan(base.DbTestCase): self.ongoing_action_plan = obj_utils.create_test_action_plan( self.context, uuid=uuidutils.generate_uuid(), - audit_id=self.audit.id) + audit_id=self.audit.id, + strategy_id=self.strategy.id, + audit=self.audit, + strategy=self.strategy, + ) self.recommended_action_plan = obj_utils.create_test_action_plan( self.context, uuid=uuidutils.generate_uuid(), state=objects.action_plan.State.ONGOING, - audit_id=self.audit.id + audit_id=self.audit.id, + strategy_id=self.strategy.id, + audit=self.audit, + strategy=self.strategy, ) @mock.patch.object(objects.action_plan.ActionPlan, 'list') diff --git a/watcher/tests/decision_engine/model/monasca_metrics.py b/watcher/tests/decision_engine/model/monasca_metrics.py index bf6c3f02f..12ebb27df 100644 --- a/watcher/tests/decision_engine/model/monasca_metrics.py +++ b/watcher/tests/decision_engine/model/monasca_metrics.py @@ -153,7 +153,6 @@ class FakeMonascaMetrics(object): # measurements[uuid] = random.randint(1, 4) measurements[uuid] = 8 - # import ipdb; ipdb.set_trace() return [{'columns': ['avg'], 'statistics': [[float(measurements[str(uuid)])]]}] # return float(measurements[str(uuid)]) diff --git a/watcher/tests/decision_engine/planner/test_weight_planner.py b/watcher/tests/decision_engine/planner/test_weight_planner.py index fdfd2447a..526f83203 100644 --- a/watcher/tests/decision_engine/planner/test_weight_planner.py +++ b/watcher/tests/decision_engine/planner/test_weight_planner.py @@ -60,6 +60,7 @@ class TestActionScheduling(base.DbTestCase): def setUp(self): super(TestActionScheduling, self).setUp() + self.goal = db_utils.create_test_goal(name="dummy") self.strategy = db_utils.create_test_strategy(name="dummy") self.audit = db_utils.create_test_audit( uuid=utils.generate_uuid(), strategy_id=self.strategy.id) diff --git a/watcher/tests/decision_engine/planner/test_workload_stabilization_planner.py b/watcher/tests/decision_engine/planner/test_workload_stabilization_planner.py index f53b98d01..4956fef6d 100644 --- a/watcher/tests/decision_engine/planner/test_workload_stabilization_planner.py +++ b/watcher/tests/decision_engine/planner/test_workload_stabilization_planner.py @@ -61,6 +61,7 @@ class TestActionScheduling(base.DbTestCase): def setUp(self): super(TestActionScheduling, self).setUp() + self.goal = db_utils.create_test_goal(name="dummy") self.strategy = db_utils.create_test_strategy(name="dummy") self.audit = db_utils.create_test_audit( uuid=utils.generate_uuid(), strategy_id=self.strategy.id) diff --git a/watcher/tests/notifications/test_action_plan_notification.py b/watcher/tests/notifications/test_action_plan_notification.py new file mode 100644 index 000000000..384a37b43 --- /dev/null +++ b/watcher/tests/notifications/test_action_plan_notification.py @@ -0,0 +1,261 @@ +# 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 freezegun +import mock +import oslo_messaging as om + +from watcher.common import exception +from watcher.common import rpc +from watcher import notifications +from watcher import objects +from watcher.tests.db import base +from watcher.tests.objects import utils + + +@freezegun.freeze_time('2016-10-18T09:52:05.219414') +class TestActionPlanNotification(base.DbTestCase): + + def setUp(self): + super(TestActionPlanNotification, self).setUp() + p_get_notifier = mock.patch.object(rpc, 'get_notifier') + m_get_notifier = p_get_notifier.start() + self.addCleanup(p_get_notifier.stop) + self.m_notifier = mock.Mock(spec=om.Notifier) + + def fake_get_notifier(publisher_id): + self.m_notifier.publisher_id = publisher_id + return self.m_notifier + + m_get_notifier.side_effect = fake_get_notifier + self.goal = utils.create_test_goal(mock.Mock()) + self.audit = utils.create_test_audit(mock.Mock(), interval=None) + self.strategy = utils.create_test_strategy(mock.Mock()) + + def test_send_invalid_action_plan(self): + action_plan = utils.get_test_action_plan( + mock.Mock(), state='DOESNOTMATTER', audit_id=1) + + self.assertRaises( + exception.InvalidActionPlan, + notifications.action_plan.send_update, + mock.MagicMock(), action_plan, host='node0') + + def test_send_action_plan_update(self): + action_plan = utils.create_test_action_plan( + mock.Mock(), state=objects.action_plan.State.ONGOING, + audit_id=self.audit.id, strategy_id=self.strategy.id, + audit=self.audit, strategy=self.strategy) + notifications.action_plan.send_update( + mock.MagicMock(), action_plan, host='node0', + old_state=objects.action_plan.State.PENDING) + + # The 1st notification is because we created the object. + # The 2nd notification is because we created the action plan object. + self.assertEqual(3, self.m_notifier.info.call_count) + notification = self.m_notifier.info.call_args[1] + payload = notification['payload'] + + self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id) + self.assertDictEqual( + { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.data": { + "global_efficacy": {}, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.data": { + "updated_at": None, + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "name": "TEST", + "parameters_spec": {}, + "created_at": "2016-10-18T09:52:05Z", + "display_name": "test strategy", + "deleted_at": None + }, + "watcher_object.name": "StrategyPayload" + }, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.data": { + "interval": None, + "parameters": {}, + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "strategy_uuid": None, + "goal_uuid": ( + "f7ad87ae-4298-91cf-93a0-f35a852e3652"), + "deleted_at": None, + "scope": [], + "state": "PENDING", + "updated_at": None, + "created_at": "2016-10-18T09:52:05Z", + "audit_type": "ONESHOT" + }, + "watcher_object.name": "TerseAuditPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + }, + "deleted_at": None, + "state": "ONGOING", + "updated_at": None, + "created_at": "2016-10-18T09:52:05Z", + "state_update": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.data": { + "old_state": "PENDING", + "state": "ONGOING" + }, + "watcher_object.name": "ActionPlanStateUpdatePayload" + }, + }, + "watcher_object.name": "ActionPlanUpdatePayload" + }, + payload + ) + + def test_send_action_plan_create(self): + action_plan = utils.get_test_action_plan( + mock.Mock(), state=objects.action_plan.State.PENDING, + audit_id=self.audit.id, strategy_id=self.strategy.id, + audit=self.audit.as_dict(), strategy=self.strategy.as_dict()) + notifications.action_plan.send_create( + mock.MagicMock(), action_plan, host='node0') + + self.assertEqual(2, self.m_notifier.info.call_count) + notification = self.m_notifier.info.call_args[1] + payload = notification['payload'] + + self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id) + self.assertDictEqual( + { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.data": { + "global_efficacy": {}, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.data": { + "updated_at": None, + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "name": "TEST", + "parameters_spec": {}, + "created_at": "2016-10-18T09:52:05Z", + "display_name": "test strategy", + "deleted_at": None + }, + "watcher_object.name": "StrategyPayload" + }, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.data": { + "interval": None, + "parameters": {}, + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "strategy_uuid": None, + "goal_uuid": ( + "f7ad87ae-4298-91cf-93a0-f35a852e3652"), + "deleted_at": None, + "scope": [], + "state": "PENDING", + "updated_at": None, + "created_at": "2016-10-18T09:52:05Z", + "audit_type": "ONESHOT" + }, + "watcher_object.name": "TerseAuditPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + }, + "deleted_at": None, + "state": "PENDING", + "updated_at": None, + "created_at": None, + }, + "watcher_object.name": "ActionPlanCreatePayload" + }, + payload + ) + + def test_send_action_plan_delete(self): + action_plan = utils.create_test_action_plan( + mock.Mock(), state=objects.action_plan.State.DELETED, + audit_id=self.audit.id, strategy_id=self.strategy.id) + notifications.action_plan.send_delete( + mock.MagicMock(), action_plan, host='node0') + + # The 1st notification is because we created the audit object. + # The 2nd notification is because we created the action plan object. + self.assertEqual(3, self.m_notifier.info.call_count) + notification = self.m_notifier.info.call_args[1] + payload = notification['payload'] + + self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id) + self.assertDictEqual( + { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.data": { + "global_efficacy": {}, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.data": { + "updated_at": None, + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "name": "TEST", + "parameters_spec": {}, + "created_at": "2016-10-18T09:52:05Z", + "display_name": "test strategy", + "deleted_at": None + }, + "watcher_object.name": "StrategyPayload" + }, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.data": { + "interval": None, + "parameters": {}, + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "strategy_uuid": None, + "goal_uuid": ( + "f7ad87ae-4298-91cf-93a0-f35a852e3652"), + "deleted_at": None, + "scope": [], + "state": "PENDING", + "updated_at": None, + "created_at": "2016-10-18T09:52:05Z", + "audit_type": "ONESHOT" + }, + "watcher_object.name": "TerseAuditPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + }, + "deleted_at": None, + "state": "DELETED", + "updated_at": None, + "created_at": "2016-10-18T09:52:05Z", + }, + "watcher_object.name": "ActionPlanDeletePayload" + }, + payload + ) diff --git a/watcher/tests/notifications/test_audit_notification.py b/watcher/tests/notifications/test_audit_notification.py index 1573dc5b1..7fa29f8f2 100644 --- a/watcher/tests/notifications/test_audit_notification.py +++ b/watcher/tests/notifications/test_audit_notification.py @@ -43,8 +43,8 @@ class TestAuditNotification(base.DbTestCase): self.strategy = utils.create_test_strategy(mock.Mock()) def test_send_invalid_audit(self): - audit = utils.get_test_audit(mock.Mock(), state='DOESNOTMATTER', - goal_id=1) + audit = utils.get_test_audit( + mock.Mock(), interval=None, state='DOESNOTMATTER', goal_id=1) self.assertRaises( exception.InvalidAudit, @@ -53,7 +53,7 @@ class TestAuditNotification(base.DbTestCase): def test_send_audit_update_with_strategy(self): audit = utils.create_test_audit( - mock.Mock(), state=objects.audit.State.ONGOING, + mock.Mock(), interval=None, state=objects.audit.State.ONGOING, goal_id=self.goal.id, strategy_id=self.strategy.id, goal=self.goal, strategy=self.strategy) notifications.audit.send_update( @@ -71,7 +71,8 @@ class TestAuditNotification(base.DbTestCase): "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", "watcher_object.data": { - "interval": 3600, + "interval": None, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", "strategy": { "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", @@ -88,6 +89,7 @@ class TestAuditNotification(base.DbTestCase): }, "parameters": {}, "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652", "goal": { "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", @@ -125,7 +127,7 @@ class TestAuditNotification(base.DbTestCase): def test_send_audit_update_without_strategy(self): audit = utils.get_test_audit( - mock.Mock(), state=objects.audit.State.ONGOING, + mock.Mock(), interval=None, state=objects.audit.State.ONGOING, goal_id=self.goal.id, goal=self.goal) notifications.audit.send_update( mock.MagicMock(), audit, host='node0', @@ -141,9 +143,10 @@ class TestAuditNotification(base.DbTestCase): "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", "watcher_object.data": { - "interval": 3600, + "interval": None, "parameters": {}, "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652", "goal": { "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", @@ -158,6 +161,7 @@ class TestAuditNotification(base.DbTestCase): }, "watcher_object.name": "GoalPayload" }, + "strategy_uuid": None, "strategy": None, "deleted_at": None, "scope": [], @@ -182,7 +186,7 @@ class TestAuditNotification(base.DbTestCase): def test_send_audit_create(self): audit = utils.get_test_audit( - mock.Mock(), state=objects.audit.State.PENDING, + mock.Mock(), interval=None, state=objects.audit.State.PENDING, goal_id=self.goal.id, strategy_id=self.strategy.id, goal=self.goal.as_dict(), strategy=self.strategy.as_dict()) notifications.audit.send_create( @@ -198,7 +202,8 @@ class TestAuditNotification(base.DbTestCase): "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", "watcher_object.data": { - "interval": 3600, + "interval": None, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", "strategy": { "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", @@ -215,6 +220,7 @@ class TestAuditNotification(base.DbTestCase): }, "parameters": {}, "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652", "goal": { "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", @@ -243,7 +249,7 @@ class TestAuditNotification(base.DbTestCase): def test_send_audit_delete(self): audit = utils.create_test_audit( - mock.Mock(), state=objects.audit.State.DELETED, + mock.Mock(), interval=None, state=objects.audit.State.DELETED, goal_id=self.goal.id, strategy_id=self.strategy.id) notifications.audit.send_delete( mock.MagicMock(), audit, host='node0') @@ -259,7 +265,8 @@ class TestAuditNotification(base.DbTestCase): "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", "watcher_object.data": { - "interval": 3600, + "interval": None, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", "strategy": { "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", @@ -276,6 +283,7 @@ class TestAuditNotification(base.DbTestCase): }, "parameters": {}, "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652", "goal": { "watcher_object.namespace": "watcher", "watcher_object.version": "1.0", @@ -304,7 +312,7 @@ class TestAuditNotification(base.DbTestCase): def test_send_audit_action(self): audit = utils.create_test_audit( - mock.Mock(), state=objects.audit.State.ONGOING, + mock.Mock(), interval=None, state=objects.audit.State.ONGOING, goal_id=self.goal.id, strategy_id=self.strategy.id, goal=self.goal, strategy=self.strategy) notifications.audit.send_action_notification( @@ -326,6 +334,7 @@ class TestAuditNotification(base.DbTestCase): "created_at": "2016-10-18T09:52:05Z", "deleted_at": None, "fault": None, + "goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652", "goal": { "watcher_object.data": { "created_at": "2016-10-18T09:52:05Z", @@ -340,10 +349,12 @@ class TestAuditNotification(base.DbTestCase): "watcher_object.namespace": "watcher", "watcher_object.version": "1.0" }, - "interval": 3600, + "interval": None, "parameters": {}, "scope": [], "state": "ONGOING", + "strategy_uuid": ( + "cb3d0b58-4415-4d90-b75b-1e96878730e3"), "strategy": { "watcher_object.data": { "created_at": "2016-10-18T09:52:05Z", @@ -371,7 +382,7 @@ class TestAuditNotification(base.DbTestCase): def test_send_audit_action_with_error(self): audit = utils.create_test_audit( - mock.Mock(), state=objects.audit.State.ONGOING, + mock.Mock(), interval=None, state=objects.audit.State.ONGOING, goal_id=self.goal.id, strategy_id=self.strategy.id, goal=self.goal, strategy=self.strategy) @@ -407,6 +418,7 @@ class TestAuditNotification(base.DbTestCase): "watcher_object.namespace": "watcher", "watcher_object.version": "1.0" }, + "goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652", "goal": { "watcher_object.data": { "created_at": "2016-10-18T09:52:05Z", @@ -421,10 +433,12 @@ class TestAuditNotification(base.DbTestCase): "watcher_object.namespace": "watcher", "watcher_object.version": "1.0" }, - "interval": 3600, + "interval": None, "parameters": {}, "scope": [], "state": "ONGOING", + "strategy_uuid": ( + "cb3d0b58-4415-4d90-b75b-1e96878730e3"), "strategy": { "watcher_object.data": { "created_at": "2016-10-18T09:52:05Z", diff --git a/watcher/tests/notifications/test_notification.py b/watcher/tests/notifications/test_notification.py index 770d25270..828606a05 100644 --- a/watcher/tests/notifications/test_notification.py +++ b/watcher/tests/notifications/test_notification.py @@ -250,10 +250,11 @@ class TestNotificationBase(testbase.TestCase): expected_notification_fingerprints = { - 'EventType': '1.2-633c2d32fa849d2a6f8bda3b0db88332', + 'EventType': '1.3-4258a2c86eca79fd34a7dffe1278eab9', 'ExceptionNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b', 'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545', + 'TerseAuditPayload': '1.0-aaf31166b8698f08d12cae98c380b8e0', 'AuditPayload': '1.0-30c85c834648c8ca11f54fc5e084d86b', 'AuditStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5', 'AuditUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866', @@ -266,6 +267,15 @@ expected_notification_fingerprints = { 'AuditActionPayload': '1.0-09f5d005f94ba9e5f6b9200170332c52', 'GoalPayload': '1.0-fa1fecb8b01dd047eef808ded4d50d1a', 'StrategyPayload': '1.0-94f01c137b083ac236ae82573c1fcfc1', + 'ActionPlanActionPayload': '1.0-34871caf18e9b43a28899953c1c9733a', + 'ActionPlanCreateNotification': '1.0-9b69de0724fda8310d05e18418178866', + 'ActionPlanCreatePayload': '1.0-ffc3087acd73351b14f3dcc30e105027', + 'ActionPlanDeleteNotification': '1.0-9b69de0724fda8310d05e18418178866', + 'ActionPlanDeletePayload': '1.0-ffc3087acd73351b14f3dcc30e105027', + 'ActionPlanPayload': '1.0-ffc3087acd73351b14f3dcc30e105027', + 'ActionPlanStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5', + 'ActionPlanUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866', + 'ActionPlanUpdatePayload': '1.0-7912a45fe53775c721f42aa87f06a023', } diff --git a/watcher/tests/objects/test_action_plan.py b/watcher/tests/objects/test_action_plan.py index d6b26494d..7c8ee0ecc 100644 --- a/watcher/tests/objects/test_action_plan.py +++ b/watcher/tests/objects/test_action_plan.py @@ -20,6 +20,7 @@ import mock from watcher.common import exception from watcher.db.sqlalchemy import api as db_api +from watcher import notifications from watcher import objects from watcher.tests.db import base from watcher.tests.db import utils @@ -34,16 +35,19 @@ class TestActionPlanObject(base.DbTestCase): ('non_eager', dict( eager=False, fake_action_plan=utils.get_test_action_plan( + created_at=datetime.datetime.utcnow(), audit_id=audit_id, strategy_id=strategy_id))), ('eager_with_non_eager_load', dict( eager=True, fake_action_plan=utils.get_test_action_plan( + created_at=datetime.datetime.utcnow(), audit_id=audit_id, strategy_id=strategy_id))), ('eager_with_eager_load', dict( eager=True, fake_action_plan=utils.get_test_action_plan( + created_at=datetime.datetime.utcnow(), strategy_id=strategy_id, strategy=utils.get_test_strategy(id=strategy_id), audit_id=audit_id, @@ -52,6 +56,13 @@ class TestActionPlanObject(base.DbTestCase): def setUp(self): super(TestActionPlanObject, self).setUp() + + p_action_plan_notifications = mock.patch.object( + notifications, 'action_plan', autospec=True) + self.m_action_plan_notifications = p_action_plan_notifications.start() + self.addCleanup(p_action_plan_notifications.stop) + self.m_send_update = self.m_action_plan_notifications.send_update + self.fake_audit = utils.create_test_audit(id=self.audit_id) self.fake_strategy = utils.create_test_strategy( id=self.strategy_id, name="DUMMY") @@ -80,6 +91,7 @@ class TestActionPlanObject(base.DbTestCase): self.context, action_plan_id, eager=self.eager) self.assertEqual(self.context, action_plan._context) self.eager_load_action_plan_assert(action_plan) + self.assertEqual(0, self.m_send_update.call_count) @mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid') def test_get_by_uuid(self, mock_get_action_plan): @@ -91,6 +103,7 @@ class TestActionPlanObject(base.DbTestCase): self.context, uuid, eager=self.eager) self.assertEqual(self.context, action_plan._context) self.eager_load_action_plan_assert(action_plan) + self.assertEqual(0, self.m_send_update.call_count) def test_get_bad_id_and_uuid(self): self.assertRaises(exception.InvalidIdentity, @@ -107,14 +120,26 @@ class TestActionPlanObject(base.DbTestCase): self.assertEqual(self.context, action_plans[0]._context) for action_plan in action_plans: self.eager_load_action_plan_assert(action_plan) + self.assertEqual(0, self.m_send_update.call_count) @mock.patch.object(db_api.Connection, 'update_action_plan') @mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid') def test_save(self, mock_get_action_plan, mock_update_action_plan): mock_get_action_plan.return_value = self.fake_action_plan fake_saved_action_plan = self.fake_action_plan.copy() - fake_saved_action_plan['deleted_at'] = datetime.datetime.utcnow() + fake_saved_action_plan['state'] = objects.action_plan.State.SUCCEEDED + fake_saved_action_plan['updated_at'] = datetime.datetime.utcnow() + mock_update_action_plan.return_value = fake_saved_action_plan + + expected_action_plan = fake_saved_action_plan.copy() + expected_action_plan[ + 'created_at'] = expected_action_plan['created_at'].replace( + tzinfo=iso8601.iso8601.Utc()) + expected_action_plan[ + 'updated_at'] = expected_action_plan['updated_at'].replace( + tzinfo=iso8601.iso8601.Utc()) + uuid = self.fake_action_plan['uuid'] action_plan = objects.ActionPlan.get_by_uuid( self.context, uuid, eager=self.eager) @@ -127,6 +152,14 @@ class TestActionPlanObject(base.DbTestCase): uuid, {'state': objects.action_plan.State.SUCCEEDED}) self.assertEqual(self.context, action_plan._context) self.eager_load_action_plan_assert(action_plan) + self.m_send_update.assert_called_once_with( + self.context, action_plan, + old_state=self.fake_action_plan['state']) + self.assertEqual( + {k: v for k, v in expected_action_plan.items() + if k not in action_plan.object_fields}, + {k: v for k, v in action_plan.as_dict().items() + if k not in action_plan.object_fields}) @mock.patch.object(db_api.Connection, 'get_action_plan_by_uuid') def test_refresh(self, mock_get_action_plan): @@ -150,6 +183,13 @@ class TestCreateDeleteActionPlanObject(base.DbTestCase): def setUp(self): super(TestCreateDeleteActionPlanObject, self).setUp() + + p_action_plan_notifications = mock.patch.object( + notifications, 'action_plan', autospec=True) + self.m_action_plan_notifications = p_action_plan_notifications.start() + self.addCleanup(p_action_plan_notifications.stop) + self.m_send_update = self.m_action_plan_notifications.send_update + self.fake_strategy = utils.create_test_strategy(name="DUMMY") self.fake_audit = utils.create_test_audit() self.fake_action_plan = utils.get_test_action_plan( @@ -202,7 +242,8 @@ class TestCreateDeleteActionPlanObject(base.DbTestCase): del expected_action_plan['strategy'] m_get_efficacy_indicator_list.return_value = [efficacy_indicator] - action_plan = objects.ActionPlan.get_by_uuid(self.context, uuid) + action_plan = objects.ActionPlan.get_by_uuid( + self.context, uuid, eager=False) action_plan.soft_delete() m_get_action_plan.assert_called_once_with( diff --git a/watcher/tests/objects/test_audit.py b/watcher/tests/objects/test_audit.py index 179969978..0b0148009 100644 --- a/watcher/tests/objects/test_audit.py +++ b/watcher/tests/objects/test_audit.py @@ -282,6 +282,7 @@ class TestAuditObjectSendNotifications(base.DbTestCase): def test_send_create_notification(self, m_create_audit): audit = objutils.get_test_audit( self.context, + id=1, goal_id=self.fake_goal.id, strategy_id=self.fake_strategy.id, goal=self.fake_goal.as_dict(), diff --git a/watcher/tests/objects/utils.py b/watcher/tests/objects/utils.py index dfba81608..18ec4aa97 100644 --- a/watcher/tests/objects/utils.py +++ b/watcher/tests/objects/utils.py @@ -30,21 +30,27 @@ def _load_related_objects(context, cls, db_data): return obj_data +def _load_test_obj(context, cls, obj_data, **kw): + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del obj_data['id'] + obj = cls(context) + for key in obj_data: + setattr(obj, key, obj_data[key]) + return obj + + def get_test_audit_template(context, **kw): """Return a AuditTemplate object with appropriate attributes. NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_audit_template = db_utils.get_test_audit_template(**kw) - # Let DB generate ID if it isn't specified explicitly - if 'id' not in kw: - del db_audit_template['id'] - audit_template = objects.AuditTemplate(context) - for key in db_audit_template: - setattr(audit_template, key, db_audit_template[key]) + obj_cls = objects.AuditTemplate + db_data = db_utils.get_test_audit_template(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) - return audit_template + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_audit_template(context, **kw): @@ -64,16 +70,11 @@ def get_test_audit(context, **kw): NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_audit = db_utils.get_test_audit(**kw) - obj_data = _load_related_objects(context, objects.Audit, db_audit) + obj_cls = objects.Audit + db_data = db_utils.get_test_audit(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) - # Let DB generate ID if it isn't specified explicitly - if 'id' not in kw: - del db_audit['id'] - audit = objects.Audit(context) - for key in obj_data: - setattr(audit, key, obj_data[key]) - return audit + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_audit(context, **kw): @@ -93,14 +94,11 @@ def get_test_action_plan(context, **kw): NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_action_plan = db_utils.get_test_action_plan(**kw) - # Let DB generate ID if it isn't specified explicitly - if 'id' not in kw: - del db_action_plan['id'] - action_plan = objects.ActionPlan(context) - for key in db_action_plan: - setattr(action_plan, key, db_action_plan[key]) - return action_plan + obj_cls = objects.ActionPlan + db_data = db_utils.get_test_action_plan(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) + + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_action_plan(context, **kw): @@ -120,14 +118,11 @@ def get_test_action(context, **kw): NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_action = db_utils.get_test_action(**kw) - # Let DB generate ID if it isn't specified explicitly - if 'id' not in kw: - del db_action['id'] - action = objects.Action(context) - for key in db_action: - setattr(action, key, db_action[key]) - return action + obj_cls = objects.Action + db_data = db_utils.get_test_action(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) + + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_action(context, **kw): @@ -147,14 +142,11 @@ def get_test_goal(context, **kw): NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_goal = db_utils.get_test_goal(**kw) - # Let DB generate ID if it isn't specified explicitly - if 'id' not in kw: - del db_goal['id'] - goal = objects.Goal(context) - for key in db_goal: - setattr(goal, key, db_goal[key]) - return goal + obj_cls = objects.Goal + db_data = db_utils.get_test_goal(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) + + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_goal(context, **kw): @@ -174,11 +166,11 @@ def get_test_scoring_engine(context, **kw): NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_scoring_engine = db_utils.get_test_scoring_engine(**kw) - scoring_engine = objects.ScoringEngine(context) - for key in db_scoring_engine: - setattr(scoring_engine, key, db_scoring_engine[key]) - return scoring_engine + obj_cls = objects.ScoringEngine + db_data = db_utils.get_test_scoring_engine(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) + + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_scoring_engine(context, **kw): @@ -198,13 +190,11 @@ def get_test_service(context, **kw): NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_service = db_utils.get_test_service(**kw) - service = objects.Service(context) - for key in db_service: - if key == 'last_seen_up': - db_service[key] = None - setattr(service, key, db_service[key]) - return service + obj_cls = objects.Service + db_data = db_utils.get_test_service(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) + + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_service(context, **kw): @@ -224,22 +214,11 @@ def get_test_strategy(context, **kw): NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_strategy = db_utils.get_test_strategy(**kw) - # Let DB generate ID if it isn't specified explicitly - if 'id' not in kw: - del db_strategy['id'] - strategy = objects.Strategy(context) - for key in db_strategy: - setattr(strategy, key, db_strategy[key]) + obj_cls = objects.Strategy + db_data = db_utils.get_test_strategy(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) - # ObjectField checks for the object type, so if we want to simulate a - # non-eager object loading, the field should not be referenced at all. - # Contrarily, eager loading need the data to be casted to the object type - # that was specified by the ObjectField. - if kw.get('goal'): - strategy.goal = objects.Goal(context, **kw.get('goal')) - - return strategy + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_strategy(context, **kw): @@ -259,14 +238,11 @@ def get_test_efficacy_indicator(context, **kw): NOTE: The object leaves the attributes marked as changed, such that a create() could be used to commit it to the DB. """ - db_efficacy_indicator = db_utils.get_test_efficacy_indicator(**kw) - # Let DB generate ID if it isn't specified explicitly - if 'id' not in kw: - del db_efficacy_indicator['id'] - efficacy_indicator = objects.EfficacyIndicator(context) - for key in db_efficacy_indicator: - setattr(efficacy_indicator, key, db_efficacy_indicator[key]) - return efficacy_indicator + obj_cls = objects.EfficacyIndicator + db_data = db_utils.get_test_efficacy_indicator(**kw) + obj_data = _load_related_objects(context, obj_cls, db_data) + + return _load_test_obj(context, obj_cls, obj_data, **kw) def create_test_efficacy_indicator(context, **kw):