diff --git a/watcher/api/controllers/v1/audit.py b/watcher/api/controllers/v1/audit.py index bcee64f83..801838767 100644 --- a/watcher/api/controllers/v1/audit.py +++ b/watcher/api/controllers/v1/audit.py @@ -69,6 +69,8 @@ class AuditPostType(wtypes.Base): scope = wtypes.wsattr(types.jsontype, readonly=True) + auto_trigger = wtypes.wsattr(bool, mandatory=False) + def as_audit(self, context): audit_type_values = [val.value for val in objects.audit.AuditType] if self.audit_type not in audit_type_values: @@ -115,7 +117,8 @@ class AuditPostType(wtypes.Base): goal_id=self.goal, strategy_id=self.strategy, interval=self.interval, - scope=self.scope,) + scope=self.scope, + auto_trigger=self.auto_trigger) class AuditPatchType(types.JsonPatchType): @@ -257,6 +260,9 @@ class Audit(base.APIBase): scope = wsme.wsattr(types.jsontype, mandatory=False) """Audit Scope""" + auto_trigger = wsme.wsattr(bool, mandatory=False, default=False) + """Autoexecute action plan once audit is succeeded""" + def __init__(self, **kwargs): self.fields = [] fields = list(objects.Audit.fields) @@ -313,7 +319,8 @@ class Audit(base.APIBase): deleted_at=None, updated_at=datetime.datetime.utcnow(), interval=7200, - scope=[]) + scope=[], + auto_trigger=False) sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae' sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff' diff --git a/watcher/common/exception.py b/watcher/common/exception.py index 75892d244..3c1c3857c 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -258,6 +258,12 @@ class ActionPlanReferenced(Invalid): "multiple actions") +class ActionPlanIsOngoing(Conflict): + msg_fmt = _("Action Plan %(action_plan)s is currently running. " + "New Action Plan %(new_action_plan)s will be set as " + "SUPERSEDED") + + class ActionNotFound(ResourceNotFound): msg_fmt = _("Action %(action)s could not be found") diff --git a/watcher/db/sqlalchemy/api.py b/watcher/db/sqlalchemy/api.py index 6a1b03c42..9601ec266 100644 --- a/watcher/db/sqlalchemy/api.py +++ b/watcher/db/sqlalchemy/api.py @@ -663,6 +663,9 @@ class Connection(api.BaseConnection): if values.get('state') is None: values['state'] = objects.audit.State.PENDING + if not values.get('auto_trigger'): + values['auto_trigger'] = False + try: audit = self._create(models.Audit, values) except db_exc.DBDuplicateEntry: diff --git a/watcher/db/sqlalchemy/models.py b/watcher/db/sqlalchemy/models.py index 59a8c08a1..8e65a5913 100644 --- a/watcher/db/sqlalchemy/models.py +++ b/watcher/db/sqlalchemy/models.py @@ -19,6 +19,7 @@ SQLAlchemy models for watcher service from oslo_db.sqlalchemy import models from oslo_serialization import jsonutils import six.moves.urllib.parse as urlparse +from sqlalchemy import Boolean from sqlalchemy import Column from sqlalchemy import DateTime from sqlalchemy.ext.declarative import declarative_base @@ -176,6 +177,7 @@ class Audit(Base): goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False) strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True) scope = Column(JSONEncodedList, nullable=True) + auto_trigger = Column(Boolean, nullable=False) goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None) strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None) diff --git a/watcher/decision_engine/audit/base.py b/watcher/decision_engine/audit/base.py index f9ffdbc3a..ec827c4a9 100644 --- a/watcher/decision_engine/audit/base.py +++ b/watcher/decision_engine/audit/base.py @@ -22,6 +22,8 @@ import six from oslo_log import log +from watcher.applier import rpcapi +from watcher.common import exception from watcher.decision_engine.planner import manager as planner_manager from watcher.decision_engine.strategy.context import default as default_context from watcher import notifications @@ -79,11 +81,13 @@ class AuditHandler(BaseAuditHandler): request_context, audit, action=fields.NotificationAction.PLANNER, phase=fields.NotificationPhase.START) - self.planner.schedule(request_context, audit.id, solution) + action_plan = self.planner.schedule(request_context, audit.id, + solution) notifications.audit.send_action_notification( request_context, audit, action=fields.NotificationAction.PLANNER, phase=fields.NotificationPhase.END) + return action_plan except Exception: notifications.audit.send_action_notification( request_context, audit, @@ -104,15 +108,30 @@ class AuditHandler(BaseAuditHandler): self.update_audit_state(audit, objects.audit.State.ONGOING) def post_execute(self, audit, solution, request_context): - self.do_schedule(request_context, audit, solution) - # change state of the audit to SUCCEEDED - self.update_audit_state(audit, objects.audit.State.SUCCEEDED) + action_plan = self.do_schedule(request_context, audit, solution) + a_plan_filters = {'state': objects.action_plan.State.ONGOING} + ongoing_action_plans = objects.ActionPlan.list( + request_context, filters=a_plan_filters) + if ongoing_action_plans: + action_plan.state = objects.action_plan.State.SUPERSEDED + action_plan.save() + raise exception.ActionPlanIsOngoing( + action_plan=ongoing_action_plans[0].uuid, + new_action_plan=action_plan.uuid) + elif audit.auto_trigger: + applier_client = rpcapi.ApplierAPI() + applier_client.launch_action_plan(request_context, + action_plan.uuid) def execute(self, audit, request_context): try: self.pre_execute(audit, request_context) solution = self.do_execute(audit, request_context) self.post_execute(audit, solution, request_context) + except exception.ActionPlanIsOngoing as e: + LOG.exception(e) + if audit.audit_type == objects.audit.AuditType.ONESHOT.value: + self.update_audit_state(audit, objects.audit.State.CANCELLED) except Exception as e: LOG.exception(e) self.update_audit_state(audit, objects.audit.State.FAILED) diff --git a/watcher/decision_engine/audit/continuous.py b/watcher/decision_engine/audit/continuous.py index 92441d989..06493a155 100644 --- a/watcher/decision_engine/audit/continuous.py +++ b/watcher/decision_engine/audit/continuous.py @@ -82,9 +82,6 @@ class ContinuousAuditHandler(base.AuditHandler): if not self._is_audit_inactive(audit): self.execute(audit, request_context) - def post_execute(self, audit, solution, request_context): - self.do_schedule(request_context, audit, solution) - def launch_audits_periodically(self): audit_context = context.RequestContext(is_admin=True) audit_filters = { diff --git a/watcher/decision_engine/audit/oneshot.py b/watcher/decision_engine/audit/oneshot.py index c689d6f13..a5ac2d7eb 100644 --- a/watcher/decision_engine/audit/oneshot.py +++ b/watcher/decision_engine/audit/oneshot.py @@ -15,6 +15,7 @@ # limitations under the License. from watcher.decision_engine.audit import base +from watcher import objects class OneShotAuditHandler(base.AuditHandler): @@ -24,3 +25,9 @@ class OneShotAuditHandler(base.AuditHandler): audit, request_context) return solution + + def post_execute(self, audit, solution, request_context): + super(OneShotAuditHandler, self).post_execute(audit, solution, + request_context) + # change state of the audit to SUCCEEDED + self.update_audit_state(audit, objects.audit.State.SUCCEEDED) diff --git a/watcher/objects/action_plan.py b/watcher/objects/action_plan.py index 7c1779c7c..a97154cfb 100644 --- a/watcher/objects/action_plan.py +++ b/watcher/objects/action_plan.py @@ -85,6 +85,7 @@ class State(object): SUCCEEDED = 'SUCCEEDED' DELETED = 'DELETED' CANCELLED = 'CANCELLED' + SUPERSEDED = 'SUPERSEDED' @base.WatcherObjectRegistry.register diff --git a/watcher/objects/audit.py b/watcher/objects/audit.py index a133daf74..0dc489c5c 100644 --- a/watcher/objects/audit.py +++ b/watcher/objects/audit.py @@ -79,7 +79,8 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject, # Version 1.0: Initial version # Version 1.1: Added 'goal' and 'strategy' object field - VERSION = '1.1' + # Version 1.2 Added 'auto_trigger' boolean field + VERSION = '1.2' dbapi = db_api.get_instance() @@ -93,6 +94,7 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject, 'scope': wfields.FlexibleListOfDictField(nullable=True), 'goal_id': wfields.IntegerField(), 'strategy_id': wfields.IntegerField(nullable=True), + 'auto_trigger': wfields.BooleanField(), 'goal': wfields.ObjectField('Goal', nullable=True), 'strategy': wfields.ObjectField('Strategy', nullable=True), diff --git a/watcher/tests/db/utils.py b/watcher/tests/db/utils.py index 1e796bfda..e44ca8056 100644 --- a/watcher/tests/db/utils.py +++ b/watcher/tests/db/utils.py @@ -93,6 +93,7 @@ def get_test_audit(**kwargs): 'goal_id': kwargs.get('goal_id', 1), 'strategy_id': kwargs.get('strategy_id', None), 'scope': kwargs.get('scope', []), + 'auto_trigger': kwargs.get('auto_trigger', False) } # ObjectField doesn't allow None nor dict, so if we want to simulate a # non-eager object loading, the field should not be referenced at all. diff --git a/watcher/tests/decision_engine/audit/test_audit_handlers.py b/watcher/tests/decision_engine/audit/test_audit_handlers.py index 6e15178b8..48e4413e4 100644 --- a/watcher/tests/decision_engine/audit/test_audit_handlers.py +++ b/watcher/tests/decision_engine/audit/test_audit_handlers.py @@ -18,6 +18,8 @@ from apscheduler.schedulers import background import mock from oslo_utils import uuidutils +from watcher.applier import rpcapi +from watcher.common import exception from watcher.decision_engine.audit import continuous from watcher.decision_engine.audit import oneshot from watcher.decision_engine.model.collector import manager @@ -150,6 +152,65 @@ class TestOneShotAuditHandler(base.DbTestCase): self.m_audit_notifications.send_action_notification.call_args_list) +class TestAutoTriggerActionPlan(base.DbTestCase): + + def setUp(self): + super(TestAutoTriggerActionPlan, self).setUp() + self.goal = obj_utils.create_test_goal( + self.context, id=1, name=dummy_strategy.DummyStrategy.get_name()) + self.strategy = obj_utils.create_test_strategy( + self.context, name=dummy_strategy.DummyStrategy.get_name(), + goal_id=self.goal.id) + audit_template = obj_utils.create_test_audit_template( + self.context) + self.audit = obj_utils.create_test_audit( + self.context, + id=0, + uuid=uuidutils.generate_uuid(), + audit_template_id=audit_template.id, + goal_id=self.goal.id, + audit_type=objects.audit.AuditType.CONTINUOUS.value, + goal=self.goal, + auto_trigger=True) + self.ongoing_action_plan = obj_utils.create_test_action_plan( + self.context, + uuid=uuidutils.generate_uuid(), + audit_id=self.audit.id) + 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 + ) + + @mock.patch.object(objects.action_plan.ActionPlan, 'list') + @mock.patch.object(objects.audit.Audit, 'get_by_id') + def test_trigger_action_plan_with_ongoing(self, mock_get_by_id, mock_list): + mock_get_by_id.return_value = self.audit + mock_list.return_value = [self.ongoing_action_plan] + auto_trigger_handler = oneshot.OneShotAuditHandler(mock.MagicMock()) + with mock.patch.object(auto_trigger_handler, 'do_schedule'): + self.assertRaises(exception.ActionPlanIsOngoing, + auto_trigger_handler.post_execute, + self.audit, mock.MagicMock(), self.context) + + @mock.patch.object(rpcapi.ApplierAPI, 'launch_action_plan') + @mock.patch.object(objects.action_plan.ActionPlan, 'list') + @mock.patch.object(objects.audit.Audit, 'get_by_id') + def test_trigger_action_plan_without_ongoing(self, mock_get_by_id, + mock_list, mock_applier): + mock_get_by_id.return_value = self.audit + mock_list.return_value = [] + auto_trigger_handler = oneshot.OneShotAuditHandler(mock.MagicMock()) + with mock.patch.object(auto_trigger_handler, 'do_schedule', + new_callable=mock.PropertyMock) as m_schedule: + m_schedule().uuid = self.recommended_action_plan.uuid + auto_trigger_handler.post_execute(self.audit, mock.MagicMock(), + self.context) + mock_applier.assert_called_once_with(self.context, + self.recommended_action_plan.uuid) + + class TestContinuousAuditHandler(base.DbTestCase): def setUp(self): diff --git a/watcher/tests/decision_engine/test_sync.py b/watcher/tests/decision_engine/test_sync.py index 5771f5e8d..7e1b8d16a 100644 --- a/watcher/tests/decision_engine/test_sync.py +++ b/watcher/tests/decision_engine/test_sync.py @@ -330,21 +330,21 @@ class TestSyncer(base.DbTestCase): self.ctx, id=1, uuid=utils.generate_uuid(), audit_type=objects.audit.AuditType.ONESHOT.value, state=objects.audit.State.PENDING, - goal_id=goal1.id, strategy_id=strategy1.id) + goal_id=goal1.id, strategy_id=strategy1.id, auto_trigger=False) # Should be modified by the sync() because its associated goal # has been modified (compared to the defined fake goals) audit2 = objects.Audit( self.ctx, id=2, uuid=utils.generate_uuid(), audit_type=objects.audit.AuditType.ONESHOT.value, state=objects.audit.State.PENDING, - goal_id=goal2.id, strategy_id=strategy2.id) + goal_id=goal2.id, strategy_id=strategy2.id, auto_trigger=False) # Should be modified by the sync() because its associated strategy # has been modified (compared to the defined fake strategies) audit3 = objects.Audit( self.ctx, id=3, uuid=utils.generate_uuid(), audit_type=objects.audit.AuditType.ONESHOT.value, state=objects.audit.State.PENDING, - goal_id=goal1.id, strategy_id=strategy3.id) + goal_id=goal1.id, strategy_id=strategy3.id, auto_trigger=False) # Modified because of both because its associated goal and associated # strategy should be modified (compared to the defined fake # goals/strategies) @@ -352,7 +352,7 @@ class TestSyncer(base.DbTestCase): self.ctx, id=4, uuid=utils.generate_uuid(), audit_type=objects.audit.AuditType.ONESHOT.value, state=objects.audit.State.PENDING, - goal_id=goal2.id, strategy_id=strategy4.id) + goal_id=goal2.id, strategy_id=strategy4.id, auto_trigger=False) audit1.create() audit2.create() @@ -560,13 +560,13 @@ class TestSyncer(base.DbTestCase): self.ctx, id=1, uuid=utils.generate_uuid(), audit_type=objects.audit.AuditType.ONESHOT.value, state=objects.audit.State.PENDING, - goal_id=goal1.id, strategy_id=strategy1.id) + goal_id=goal1.id, strategy_id=strategy1.id, auto_trigger=False) # Stale after syncing because the goal has been soft deleted audit2 = objects.Audit( self.ctx, id=2, uuid=utils.generate_uuid(), audit_type=objects.audit.AuditType.ONESHOT.value, state=objects.audit.State.PENDING, - goal_id=goal2.id, strategy_id=strategy2.id) + goal_id=goal2.id, strategy_id=strategy2.id, auto_trigger=False) audit1.create() audit2.create() diff --git a/watcher/tests/objects/test_objects.py b/watcher/tests/objects/test_objects.py index 29dc05859..3872573b3 100644 --- a/watcher/tests/objects/test_objects.py +++ b/watcher/tests/objects/test_objects.py @@ -412,7 +412,7 @@ expected_object_fingerprints = { 'Goal': '1.0-93881622db05e7b67a65ca885b4a022e', 'Strategy': '1.1-73f164491bdd4c034f48083a51bdeb7b', 'AuditTemplate': '1.1-b291973ffc5efa2c61b24fe34fdccc0b', - 'Audit': '1.1-dc246337c8d511646cb537144fcb0f3a', + 'Audit': '1.2-910522db78b7b1cb59df614754656db4', 'ActionPlan': '1.2-42709eadf6b2bd228ea87817e8c3e31e', 'Action': '1.1-52c77e4db4ce0aa9480c9760faec61a1', 'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0',