diff --git a/releasenotes/notes/stale-action-plan-b6a6b08df873c128.yaml b/releasenotes/notes/stale-action-plan-b6a6b08df873c128.yaml index 1ddf8cdc9..9ef9a4c77 100644 --- a/releasenotes/notes/stale-action-plan-b6a6b08df873c128.yaml +++ b/releasenotes/notes/stale-action-plan-b6a6b08df873c128.yaml @@ -1,4 +1,4 @@ --- features: - - Add superseded state for an action plan if the cluster data model has - changed after it has been created. + - Check the creation time of the action plan, + and set its state to SUPERSEDED if it has expired. diff --git a/watcher/conf/decision_engine.py b/watcher/conf/decision_engine.py index 4ff90da22..162dc29d4 100644 --- a/watcher/conf/decision_engine.py +++ b/watcher/conf/decision_engine.py @@ -42,6 +42,15 @@ WATCHER_DECISION_ENGINE_OPTS = [ required=True, help='The maximum number of threads that can be used to ' 'execute strategies'), + cfg.IntOpt('action_plan_expiry', + default=24, + help='An expiry timespan(hours). Watcher invalidates any ' + 'action plan for which its creation time ' + '-whose number of hours has been offset by this value-' + ' is older that the current time.'), + cfg.IntOpt('check_periodic_interval', + default=30*60, + help='Interval (in seconds) for checking action plan expiry.') ] WATCHER_CONTINUOUS_OPTS = [ diff --git a/watcher/decision_engine/scheduling.py b/watcher/decision_engine/scheduling.py index d5fa1f965..4ef0481cd 100644 --- a/watcher/decision_engine/scheduling.py +++ b/watcher/decision_engine/scheduling.py @@ -19,12 +19,17 @@ import datetime import eventlet from oslo_log import log +from watcher.common import context from watcher.common import exception from watcher.common import scheduling from watcher.decision_engine.model.collector import manager +from watcher import objects + +from watcher import conf LOG = log.getLogger(__name__) +CONF = conf.CONF class DecisionEngineSchedulingService(scheduling.BackgroundSchedulerService): @@ -73,9 +78,20 @@ class DecisionEngineSchedulingService(scheduling.BackgroundSchedulerService): return _sync + def add_checkstate_job(self): + # 30 minutes interval + interval = CONF.watcher_decision_engine.check_periodic_interval + ap_manager = objects.action_plan.StateManager() + if CONF.watcher_decision_engine.action_plan_expiry != 0: + self.add_job(ap_manager.check_expired, 'interval', + args=[context.make_context()], + seconds=interval, + next_run_time=datetime.datetime.now()) + def start(self): """Start service.""" self.add_sync_jobs() + self.add_checkstate_job() super(DecisionEngineSchedulingService, self).start() def stop(self): diff --git a/watcher/objects/action_plan.py b/watcher/objects/action_plan.py index b08ec3319..dc171f263 100644 --- a/watcher/objects/action_plan.py +++ b/watcher/objects/action_plan.py @@ -71,15 +71,19 @@ state may be one of the following: **RECOMMENDED** state and was superseded by the :ref:`Administrator <administrator_definition>` """ +import datetime from watcher.common import exception from watcher.common import utils +from watcher import conf 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 +CONF = conf.CONF + class State(object): RECOMMENDED = 'RECOMMENDED' @@ -317,3 +321,18 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject, notifications.action_plan.send_delete(self._context, self) _notify() + + +class StateManager(object): + def check_expired(self, context): + action_plan_expiry = ( + CONF.watcher_decision_engine.action_plan_expiry) + date_created = datetime.datetime.utcnow() - datetime.timedelta( + hours=action_plan_expiry) + filters = {'state__eq': State.RECOMMENDED, + 'created_at__lt': date_created} + action_plans = objects.ActionPlan.list( + context, filters=filters, eager=True) + for action_plan in action_plans: + action_plan.state = State.SUPERSEDED + action_plan.save() diff --git a/watcher/tests/decision_engine/test_scheduling.py b/watcher/tests/decision_engine/test_scheduling.py index f414ad300..d4a057cd9 100644 --- a/watcher/tests/decision_engine/test_scheduling.py +++ b/watcher/tests/decision_engine/test_scheduling.py @@ -48,7 +48,7 @@ class TestDecisionEngineSchedulingService(base.TestCase): m_start.assert_called_once_with(scheduler) jobs = scheduler.get_jobs() - self.assertEqual(1, len(jobs)) + self.assertEqual(2, len(jobs)) job = jobs[0] self.assertTrue(bool(fake_collector.cluster_data_model)) @@ -77,7 +77,7 @@ class TestDecisionEngineSchedulingService(base.TestCase): m_start.assert_called_once_with(scheduler) jobs = scheduler.get_jobs() - self.assertEqual(1, len(jobs)) + self.assertEqual(2, len(jobs)) job = jobs[0] job.func() diff --git a/watcher/tests/objects/test_action_plan.py b/watcher/tests/objects/test_action_plan.py index 7c8ee0ecc..a06c94813 100644 --- a/watcher/tests/objects/test_action_plan.py +++ b/watcher/tests/objects/test_action_plan.py @@ -19,12 +19,16 @@ import iso8601 import mock from watcher.common import exception +from watcher.common import utils as common_utils +from watcher import conf 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 +CONF = conf.CONF + class TestActionPlanObject(base.DbTestCase): @@ -290,3 +294,31 @@ class TestCreateDeleteActionPlanObject(base.DbTestCase): m_destroy_efficacy_indicator.assert_called_once_with( efficacy_indicator['uuid']) self.assertEqual(self.context, action_plan._context) + + +@mock.patch.object(notifications.action_plan, 'send_update', mock.Mock()) +class TestStateManager(base.DbTestCase): + + def setUp(self): + super(TestStateManager, self).setUp() + self.state_manager = objects.action_plan.StateManager() + + def test_check_expired(self): + CONF.set_default('action_plan_expiry', 0, + group='watcher_decision_engine') + strategy_1 = utils.create_test_strategy( + uuid=common_utils.generate_uuid()) + audit_1 = utils.create_test_audit( + uuid=common_utils.generate_uuid()) + action_plan_1 = utils.create_test_action_plan( + state=objects.action_plan.State.RECOMMENDED, + uuid=common_utils.generate_uuid(), + audit_id=audit_1.id, + strategy_id=strategy_1.id) + + self.state_manager.check_expired(self.context) + + action_plan = objects.action_plan.ActionPlan.get_by_uuid( + self.context, action_plan_1.uuid) + self.assertEqual(objects.action_plan.State.SUPERSEDED, + action_plan.state)