Add auto_trigger support to watcher

This patch set adds support of auto-triggering of action plans.

Change-Id: I36b7dff8eab5f6ebb18f6f4e752cf4b263456293
Partially-Implements: blueprint automatic-triggering-audit
This commit is contained in:
Alexander Chadin
2016-11-28 17:00:44 +03:00
parent c5ff387ae9
commit d0bca1f2ab
13 changed files with 123 additions and 17 deletions

View File

@@ -69,6 +69,8 @@ class AuditPostType(wtypes.Base):
scope = wtypes.wsattr(types.jsontype, readonly=True) scope = wtypes.wsattr(types.jsontype, readonly=True)
auto_trigger = wtypes.wsattr(bool, mandatory=False)
def as_audit(self, context): def as_audit(self, context):
audit_type_values = [val.value for val in objects.audit.AuditType] audit_type_values = [val.value for val in objects.audit.AuditType]
if self.audit_type not in audit_type_values: if self.audit_type not in audit_type_values:
@@ -115,7 +117,8 @@ class AuditPostType(wtypes.Base):
goal_id=self.goal, goal_id=self.goal,
strategy_id=self.strategy, strategy_id=self.strategy,
interval=self.interval, interval=self.interval,
scope=self.scope,) scope=self.scope,
auto_trigger=self.auto_trigger)
class AuditPatchType(types.JsonPatchType): class AuditPatchType(types.JsonPatchType):
@@ -257,6 +260,9 @@ class Audit(base.APIBase):
scope = wsme.wsattr(types.jsontype, mandatory=False) scope = wsme.wsattr(types.jsontype, mandatory=False)
"""Audit Scope""" """Audit Scope"""
auto_trigger = wsme.wsattr(bool, mandatory=False, default=False)
"""Autoexecute action plan once audit is succeeded"""
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.fields = [] self.fields = []
fields = list(objects.Audit.fields) fields = list(objects.Audit.fields)
@@ -313,7 +319,8 @@ class Audit(base.APIBase):
deleted_at=None, deleted_at=None,
updated_at=datetime.datetime.utcnow(), updated_at=datetime.datetime.utcnow(),
interval=7200, interval=7200,
scope=[]) scope=[],
auto_trigger=False)
sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae' sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff' sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff'

View File

@@ -258,6 +258,12 @@ class ActionPlanReferenced(Invalid):
"multiple actions") "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): class ActionNotFound(ResourceNotFound):
msg_fmt = _("Action %(action)s could not be found") msg_fmt = _("Action %(action)s could not be found")

View File

@@ -663,6 +663,9 @@ class Connection(api.BaseConnection):
if values.get('state') is None: if values.get('state') is None:
values['state'] = objects.audit.State.PENDING values['state'] = objects.audit.State.PENDING
if not values.get('auto_trigger'):
values['auto_trigger'] = False
try: try:
audit = self._create(models.Audit, values) audit = self._create(models.Audit, values)
except db_exc.DBDuplicateEntry: except db_exc.DBDuplicateEntry:

View File

@@ -19,6 +19,7 @@ SQLAlchemy models for watcher service
from oslo_db.sqlalchemy import models from oslo_db.sqlalchemy import models
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import six.moves.urllib.parse as urlparse import six.moves.urllib.parse as urlparse
from sqlalchemy import Boolean
from sqlalchemy import Column from sqlalchemy import Column
from sqlalchemy import DateTime from sqlalchemy import DateTime
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
@@ -176,6 +177,7 @@ class Audit(Base):
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False) goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True) strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
scope = Column(JSONEncodedList, nullable=True) scope = Column(JSONEncodedList, nullable=True)
auto_trigger = Column(Boolean, nullable=False)
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None) goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None) strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)

View File

@@ -22,6 +22,8 @@ import six
from oslo_log import log 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.planner import manager as planner_manager
from watcher.decision_engine.strategy.context import default as default_context from watcher.decision_engine.strategy.context import default as default_context
from watcher import notifications from watcher import notifications
@@ -79,11 +81,13 @@ class AuditHandler(BaseAuditHandler):
request_context, audit, request_context, audit,
action=fields.NotificationAction.PLANNER, action=fields.NotificationAction.PLANNER,
phase=fields.NotificationPhase.START) 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( notifications.audit.send_action_notification(
request_context, audit, request_context, audit,
action=fields.NotificationAction.PLANNER, action=fields.NotificationAction.PLANNER,
phase=fields.NotificationPhase.END) phase=fields.NotificationPhase.END)
return action_plan
except Exception: except Exception:
notifications.audit.send_action_notification( notifications.audit.send_action_notification(
request_context, audit, request_context, audit,
@@ -104,15 +108,30 @@ class AuditHandler(BaseAuditHandler):
self.update_audit_state(audit, objects.audit.State.ONGOING) self.update_audit_state(audit, objects.audit.State.ONGOING)
def post_execute(self, audit, solution, request_context): def post_execute(self, audit, solution, request_context):
self.do_schedule(request_context, audit, solution) action_plan = self.do_schedule(request_context, audit, solution)
# change state of the audit to SUCCEEDED a_plan_filters = {'state': objects.action_plan.State.ONGOING}
self.update_audit_state(audit, objects.audit.State.SUCCEEDED) 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): def execute(self, audit, request_context):
try: try:
self.pre_execute(audit, request_context) self.pre_execute(audit, request_context)
solution = self.do_execute(audit, request_context) solution = self.do_execute(audit, request_context)
self.post_execute(audit, solution, 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: except Exception as e:
LOG.exception(e) LOG.exception(e)
self.update_audit_state(audit, objects.audit.State.FAILED) self.update_audit_state(audit, objects.audit.State.FAILED)

View File

@@ -82,9 +82,6 @@ class ContinuousAuditHandler(base.AuditHandler):
if not self._is_audit_inactive(audit): if not self._is_audit_inactive(audit):
self.execute(audit, request_context) 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): def launch_audits_periodically(self):
audit_context = context.RequestContext(is_admin=True) audit_context = context.RequestContext(is_admin=True)
audit_filters = { audit_filters = {

View File

@@ -15,6 +15,7 @@
# limitations under the License. # limitations under the License.
from watcher.decision_engine.audit import base from watcher.decision_engine.audit import base
from watcher import objects
class OneShotAuditHandler(base.AuditHandler): class OneShotAuditHandler(base.AuditHandler):
@@ -24,3 +25,9 @@ class OneShotAuditHandler(base.AuditHandler):
audit, request_context) audit, request_context)
return solution 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)

View File

@@ -85,6 +85,7 @@ class State(object):
SUCCEEDED = 'SUCCEEDED' SUCCEEDED = 'SUCCEEDED'
DELETED = 'DELETED' DELETED = 'DELETED'
CANCELLED = 'CANCELLED' CANCELLED = 'CANCELLED'
SUPERSEDED = 'SUPERSEDED'
@base.WatcherObjectRegistry.register @base.WatcherObjectRegistry.register

View File

@@ -79,7 +79,8 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
# Version 1.0: Initial version # Version 1.0: Initial version
# Version 1.1: Added 'goal' and 'strategy' object field # 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() dbapi = db_api.get_instance()
@@ -93,6 +94,7 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
'scope': wfields.FlexibleListOfDictField(nullable=True), 'scope': wfields.FlexibleListOfDictField(nullable=True),
'goal_id': wfields.IntegerField(), 'goal_id': wfields.IntegerField(),
'strategy_id': wfields.IntegerField(nullable=True), 'strategy_id': wfields.IntegerField(nullable=True),
'auto_trigger': wfields.BooleanField(),
'goal': wfields.ObjectField('Goal', nullable=True), 'goal': wfields.ObjectField('Goal', nullable=True),
'strategy': wfields.ObjectField('Strategy', nullable=True), 'strategy': wfields.ObjectField('Strategy', nullable=True),

View File

@@ -93,6 +93,7 @@ def get_test_audit(**kwargs):
'goal_id': kwargs.get('goal_id', 1), 'goal_id': kwargs.get('goal_id', 1),
'strategy_id': kwargs.get('strategy_id', None), 'strategy_id': kwargs.get('strategy_id', None),
'scope': kwargs.get('scope', []), '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 # 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. # non-eager object loading, the field should not be referenced at all.

View File

@@ -18,6 +18,8 @@ from apscheduler.schedulers import background
import mock import mock
from oslo_utils import uuidutils 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 continuous
from watcher.decision_engine.audit import oneshot from watcher.decision_engine.audit import oneshot
from watcher.decision_engine.model.collector import manager 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) 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): class TestContinuousAuditHandler(base.DbTestCase):
def setUp(self): def setUp(self):

View File

@@ -330,21 +330,21 @@ class TestSyncer(base.DbTestCase):
self.ctx, id=1, uuid=utils.generate_uuid(), self.ctx, id=1, uuid=utils.generate_uuid(),
audit_type=objects.audit.AuditType.ONESHOT.value, audit_type=objects.audit.AuditType.ONESHOT.value,
state=objects.audit.State.PENDING, 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 # Should be modified by the sync() because its associated goal
# has been modified (compared to the defined fake goals) # has been modified (compared to the defined fake goals)
audit2 = objects.Audit( audit2 = objects.Audit(
self.ctx, id=2, uuid=utils.generate_uuid(), self.ctx, id=2, uuid=utils.generate_uuid(),
audit_type=objects.audit.AuditType.ONESHOT.value, audit_type=objects.audit.AuditType.ONESHOT.value,
state=objects.audit.State.PENDING, 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 # Should be modified by the sync() because its associated strategy
# has been modified (compared to the defined fake strategies) # has been modified (compared to the defined fake strategies)
audit3 = objects.Audit( audit3 = objects.Audit(
self.ctx, id=3, uuid=utils.generate_uuid(), self.ctx, id=3, uuid=utils.generate_uuid(),
audit_type=objects.audit.AuditType.ONESHOT.value, audit_type=objects.audit.AuditType.ONESHOT.value,
state=objects.audit.State.PENDING, 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 # Modified because of both because its associated goal and associated
# strategy should be modified (compared to the defined fake # strategy should be modified (compared to the defined fake
# goals/strategies) # goals/strategies)
@@ -352,7 +352,7 @@ class TestSyncer(base.DbTestCase):
self.ctx, id=4, uuid=utils.generate_uuid(), self.ctx, id=4, uuid=utils.generate_uuid(),
audit_type=objects.audit.AuditType.ONESHOT.value, audit_type=objects.audit.AuditType.ONESHOT.value,
state=objects.audit.State.PENDING, 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() audit1.create()
audit2.create() audit2.create()
@@ -560,13 +560,13 @@ class TestSyncer(base.DbTestCase):
self.ctx, id=1, uuid=utils.generate_uuid(), self.ctx, id=1, uuid=utils.generate_uuid(),
audit_type=objects.audit.AuditType.ONESHOT.value, audit_type=objects.audit.AuditType.ONESHOT.value,
state=objects.audit.State.PENDING, 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 # Stale after syncing because the goal has been soft deleted
audit2 = objects.Audit( audit2 = objects.Audit(
self.ctx, id=2, uuid=utils.generate_uuid(), self.ctx, id=2, uuid=utils.generate_uuid(),
audit_type=objects.audit.AuditType.ONESHOT.value, audit_type=objects.audit.AuditType.ONESHOT.value,
state=objects.audit.State.PENDING, 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() audit1.create()
audit2.create() audit2.create()

View File

@@ -412,7 +412,7 @@ expected_object_fingerprints = {
'Goal': '1.0-93881622db05e7b67a65ca885b4a022e', 'Goal': '1.0-93881622db05e7b67a65ca885b4a022e',
'Strategy': '1.1-73f164491bdd4c034f48083a51bdeb7b', 'Strategy': '1.1-73f164491bdd4c034f48083a51bdeb7b',
'AuditTemplate': '1.1-b291973ffc5efa2c61b24fe34fdccc0b', 'AuditTemplate': '1.1-b291973ffc5efa2c61b24fe34fdccc0b',
'Audit': '1.1-dc246337c8d511646cb537144fcb0f3a', 'Audit': '1.2-910522db78b7b1cb59df614754656db4',
'ActionPlan': '1.2-42709eadf6b2bd228ea87817e8c3e31e', 'ActionPlan': '1.2-42709eadf6b2bd228ea87817e8c3e31e',
'Action': '1.1-52c77e4db4ce0aa9480c9760faec61a1', 'Action': '1.1-52c77e4db4ce0aa9480c9760faec61a1',
'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0', 'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0',