Coverage for watcher/objects/action_plan.py: 99%
121 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
1# -*- encoding: utf-8 -*-
2# Copyright 2013 IBM Corp.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
17"""
18An :ref:`Action Plan <action_plan_definition>` is a flow of
19:ref:`Actions <action_definition>` that should be executed in order to satisfy
20a given :ref:`Goal <goal_definition>`.
22An :ref:`Action Plan <action_plan_definition>` is generated by Watcher when an
23:ref:`Audit <audit_definition>` is successful which implies that the
24:ref:`Strategy <strategy_definition>`
25which was used has found a :ref:`Solution <solution_definition>` to achieve the
26:ref:`Goal <goal_definition>` of this :ref:`Audit <audit_definition>`.
28In the default implementation of Watcher, an
29:ref:`Action Plan <action_plan_definition>`
30is only composed of successive :ref:`Actions <action_definition>`
31(i.e., a Workflow of :ref:`Actions <action_definition>` belonging to a unique
32branch).
34However, Watcher provides abstract interfaces for many of its components,
35allowing other implementations to generate and handle more complex
36:ref:`Action Plan(s) <action_plan_definition>`
37composed of two types of Action Item(s):
39- simple :ref:`Actions <action_definition>`: atomic tasks, which means it
40 can not be split into smaller tasks or commands from an OpenStack point of
41 view.
42- composite Actions: which are composed of several simple
43 :ref:`Actions <action_definition>`
44 ordered in sequential and/or parallel flows.
46An :ref:`Action Plan <action_plan_definition>` may be described using
47standard workflow model description formats such as
48`Business Process Model and Notation 2.0 (BPMN 2.0)
49<http://www.omg.org/spec/BPMN/2.0/>`_ or `Unified Modeling Language (UML)
50<http://www.uml.org/>`_.
52An :ref:`Action Plan <action_plan_definition>` has a life-cycle and its current
53state may be one of the following:
55- **RECOMMENDED** : the :ref:`Action Plan <action_plan_definition>` is waiting
56 for a validation from the :ref:`Administrator <administrator_definition>`
57- **ONGOING** : the :ref:`Action Plan <action_plan_definition>` is currently
58 being processed by the :ref:`Watcher Applier <watcher_applier_definition>`
59- **SUCCEEDED** : the :ref:`Action Plan <action_plan_definition>` has been
60 executed successfully (i.e. all :ref:`Actions <action_definition>` that it
61 contains have been executed successfully)
62- **FAILED** : an error occurred while executing the
63 :ref:`Action Plan <action_plan_definition>`
64- **DELETED** : the :ref:`Action Plan <action_plan_definition>` is still
65 stored in the :ref:`Watcher database <watcher_database_definition>` but is
66 not returned any more through the Watcher APIs.
67- **CANCELLED** : the :ref:`Action Plan <action_plan_definition>` was in
68 **PENDING** or **ONGOING** state and was cancelled by the
69 :ref:`Administrator <administrator_definition>`
70- **SUPERSEDED** : the :ref:`Action Plan <action_plan_definition>` was in
71 **RECOMMENDED** state and was superseded by the
72 :ref:`Administrator <administrator_definition>`
73"""
74import datetime
76from oslo_utils import timeutils
78from watcher.common import exception
79from watcher.common import utils
80from watcher import conf
81from watcher.db import api as db_api
82from watcher import notifications
83from watcher import objects
84from watcher.objects import base
85from watcher.objects import fields as wfields
87CONF = conf.CONF
90class State(object):
91 RECOMMENDED = 'RECOMMENDED'
92 PENDING = 'PENDING'
93 ONGOING = 'ONGOING'
94 FAILED = 'FAILED'
95 SUCCEEDED = 'SUCCEEDED'
96 DELETED = 'DELETED'
97 CANCELLED = 'CANCELLED'
98 SUPERSEDED = 'SUPERSEDED'
99 CANCELLING = 'CANCELLING'
102@base.WatcherObjectRegistry.register
103class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
104 base.WatcherObjectDictCompat):
106 # Version 1.0: Initial version
107 # Version 1.1: Added 'audit' and 'strategy' object field
108 # Version 1.2: audit_id is not nullable anymore
109 # Version 2.0: Removed 'first_action_id' object field
110 # Version 2.1: Changed global_efficacy type
111 # Version 2.2: Added 'hostname' field
112 VERSION = '2.2'
114 dbapi = db_api.get_instance()
116 fields = {
117 'id': wfields.IntegerField(),
118 'uuid': wfields.UUIDField(),
119 'audit_id': wfields.IntegerField(),
120 'strategy_id': wfields.IntegerField(),
121 'state': wfields.StringField(nullable=True),
122 'global_efficacy': wfields.FlexibleListOfDictField(nullable=True),
123 'hostname': wfields.StringField(nullable=True),
125 'audit': wfields.ObjectField('Audit', nullable=True),
126 'strategy': wfields.ObjectField('Strategy', nullable=True),
127 }
129 object_fields = {
130 'audit': (objects.Audit, 'audit_id'),
131 'strategy': (objects.Strategy, 'strategy_id'),
132 }
134 # Proxified field so we can keep the previous value after an update
135 _state = None
136 _old_state = None
138 # NOTE(v-francoise): The way oslo.versionedobjects works is by using a
139 # __new__ that will automatically create the attributes referenced in
140 # fields. These attributes are properties that raise an exception if no
141 # value has been assigned, which means that they store the actual field
142 # value in an "_obj_%(field)s" attribute. So because we want to proxify a
143 # value that is already proxified, we have to do what you see below.
144 @property
145 def _obj_state(self):
146 return self._state
148 @property
149 def _obj_old_state(self):
150 return self._old_state
152 @property
153 def old_state(self):
154 return self._old_state
156 @_obj_old_state.setter
157 def _obj_old_state(self, value):
158 self._old_state = value
160 @_obj_state.setter
161 def _obj_state(self, value):
162 if self._old_state is None and self._state is None:
163 self._state = value
164 else:
165 self._old_state, self._state = self._state, value
167 @base.remotable_classmethod
168 def get(cls, context, action_plan_id, eager=False):
169 """Find a action_plan based on its id or uuid and return a Action object.
171 :param action_plan_id: the id *or* uuid of a action_plan.
172 :param eager: Load object fields if True (Default: False)
173 :returns: a :class:`Action` object.
174 """ # noqa: E501
175 if utils.is_int_like(action_plan_id):
176 return cls.get_by_id(context, action_plan_id, eager=eager)
177 elif utils.is_uuid_like(action_plan_id):
178 return cls.get_by_uuid(context, action_plan_id, eager=eager)
179 else:
180 raise exception.InvalidIdentity(identity=action_plan_id)
182 @base.remotable_classmethod
183 def get_by_id(cls, context, action_plan_id, eager=False):
184 """Find a action_plan based on its integer id and return a ActionPlan object.
186 :param action_plan_id: the id of a action_plan.
187 :param eager: Load object fields if True (Default: False)
188 :returns: a :class:`ActionPlan` object.
189 """ # noqa: E501
190 db_action_plan = cls.dbapi.get_action_plan_by_id(
191 context, action_plan_id, eager=eager)
192 action_plan = cls._from_db_object(
193 cls(context), db_action_plan, eager=eager)
194 return action_plan
196 @base.remotable_classmethod
197 def get_by_uuid(cls, context, uuid, eager=False):
198 """Find a action_plan based on uuid and return a :class:`ActionPlan` object.
200 :param uuid: the uuid of a action_plan.
201 :param context: Security context
202 :param eager: Load object fields if True (Default: False)
203 :returns: a :class:`ActionPlan` object.
204 """ # noqa: E501
205 db_action_plan = cls.dbapi.get_action_plan_by_uuid(
206 context, uuid, eager=eager)
207 action_plan = cls._from_db_object(
208 cls(context), db_action_plan, eager=eager)
209 return action_plan
211 @base.remotable_classmethod
212 def list(cls, context, limit=None, marker=None, filters=None,
213 sort_key=None, sort_dir=None, eager=False):
214 """Return a list of ActionPlan objects.
216 :param context: Security context.
217 :param limit: maximum number of resources to return in a single result.
218 :param marker: pagination marker for large data sets.
219 :param filters: Filters to apply. Defaults to None.
220 :param sort_key: column to sort results by.
221 :param sort_dir: direction to sort. "asc" or "desc".
222 :param eager: Load object fields if True (Default: False)
223 :returns: a list of :class:`ActionPlan` object.
224 """
225 db_action_plans = cls.dbapi.get_action_plan_list(context,
226 limit=limit,
227 marker=marker,
228 filters=filters,
229 sort_key=sort_key,
230 sort_dir=sort_dir,
231 eager=eager)
233 return [cls._from_db_object(cls(context), obj, eager=eager)
234 for obj in db_action_plans]
236 @base.remotable
237 def create(self):
238 """Create an :class:`ActionPlan` record in the DB.
240 :returns: An :class:`ActionPlan` object.
241 """
242 values = self.obj_get_changes()
243 db_action_plan = self.dbapi.create_action_plan(values)
244 # Note(v-francoise): Always load eagerly upon creation so we can send
245 # notifications containing information about the related relationships
246 self._from_db_object(self, db_action_plan, eager=True)
248 def _notify():
249 notifications.action_plan.send_create(self._context, self)
251 _notify()
253 @base.remotable
254 def destroy(self):
255 """Delete the action plan from the DB"""
256 related_efficacy_indicators = objects.EfficacyIndicator.list(
257 context=self._context,
258 filters={"action_plan_uuid": self.uuid})
260 # Cascade soft_delete of related efficacy indicators
261 for related_efficacy_indicator in related_efficacy_indicators:
262 related_efficacy_indicator.destroy()
264 self.dbapi.destroy_action_plan(self.uuid)
265 self.obj_reset_changes()
267 @base.remotable
268 def save(self):
269 """Save updates to this Action plan.
271 Updates will be made column by column based on the result
272 of self.what_changed().
273 """
274 updates = self.obj_get_changes()
275 db_obj = self.dbapi.update_action_plan(self.uuid, updates)
276 obj = self._from_db_object(
277 self.__class__(self._context), db_obj, eager=False)
278 self.obj_refresh(obj)
280 def _notify():
281 notifications.action_plan.send_update(
282 self._context, self, old_state=self.old_state)
284 _notify()
286 self.obj_reset_changes()
288 @base.remotable
289 def refresh(self, eager=False):
290 """Loads updates for this Action plan.
292 Loads a action_plan with the same uuid from the database and
293 checks for updated attributes. Updates are applied from
294 the loaded action_plan column by column, if there are any updates.
295 :param eager: Load object fields if True (Default: False)
296 """
297 current = self.get_by_uuid(self._context, uuid=self.uuid, eager=eager)
298 self.obj_refresh(current)
300 @base.remotable
301 def soft_delete(self):
302 """Soft Delete the Action plan from the DB"""
303 related_actions = objects.Action.list(
304 context=self._context,
305 filters={"action_plan_uuid": self.uuid},
306 eager=True)
308 # Cascade soft_delete of related actions
309 for related_action in related_actions:
310 related_action.soft_delete()
312 related_efficacy_indicators = objects.EfficacyIndicator.list(
313 context=self._context,
314 filters={"action_plan_uuid": self.uuid})
316 # Cascade soft_delete of related efficacy indicators
317 for related_efficacy_indicator in related_efficacy_indicators:
318 related_efficacy_indicator.soft_delete()
320 self.state = State.DELETED
321 self.save()
322 db_obj = self.dbapi.soft_delete_action_plan(self.uuid)
323 obj = self._from_db_object(
324 self.__class__(self._context), db_obj, eager=False)
325 self.obj_refresh(obj)
327 def _notify():
328 notifications.action_plan.send_delete(self._context, self)
330 _notify()
333class StateManager(object):
334 def check_expired(self, context):
335 action_plan_expiry = (
336 CONF.watcher_decision_engine.action_plan_expiry)
337 date_created = timeutils.utcnow() - datetime.timedelta(
338 hours=action_plan_expiry)
339 filters = {'state__eq': State.RECOMMENDED,
340 'created_at__lt': date_created}
341 action_plans = objects.ActionPlan.list(
342 context, filters=filters, eager=True)
343 for action_plan in action_plans:
344 action_plan.state = State.SUPERSEDED
345 action_plan.save()