Coverage for watcher/objects/audit.py: 98%
118 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"""
18In the Watcher system, an :ref:`Audit <audit_definition>` is a request for
19optimizing a :ref:`Cluster <cluster_definition>`.
21The optimization is done in order to satisfy one :ref:`Goal <goal_definition>`
22on a given :ref:`Cluster <cluster_definition>`.
24For each :ref:`Audit <audit_definition>`, the Watcher system generates an
25:ref:`Action Plan <action_plan_definition>`.
27An :ref:`Audit <audit_definition>` has a life-cycle and its current state may
28be one of the following:
30- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been
31 submitted (either manually by the
32 :ref:`Administrator <administrator_definition>` or automatically via some
33 event handling mechanism) and is in the queue for being processed by the
34 :ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
35- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
36 processed by the
37 :ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
38- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
39 successfully (note that it may not necessarily produce a
40 :ref:`Solution <solution_definition>`).
41- **FAILED** : an error occurred while executing the
42 :ref:`Audit <audit_definition>`
43- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the
44 :ref:`Watcher database <watcher_database_definition>` but is not returned
45 any more through the Watcher APIs.
46- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
47 **ONGOING** state and was cancelled by the
48 :ref:`Administrator <administrator_definition>`
49- **SUSPENDED** : the :ref:`Audit <audit_definition>` was in **ONGOING**
50 state and was suspended by the
51 :ref:`Administrator <administrator_definition>`
52"""
54import enum
56from watcher.common import exception
57from watcher.common import utils
58from watcher.db import api as db_api
59from watcher import notifications
60from watcher import objects
61from watcher.objects import base
62from watcher.objects import fields as wfields
65class State(object):
66 ONGOING = 'ONGOING'
67 SUCCEEDED = 'SUCCEEDED'
68 FAILED = 'FAILED'
69 CANCELLED = 'CANCELLED'
70 DELETED = 'DELETED'
71 PENDING = 'PENDING'
72 SUSPENDED = 'SUSPENDED'
75class AuditType(enum.Enum):
76 ONESHOT = 'ONESHOT'
77 CONTINUOUS = 'CONTINUOUS'
78 EVENT = 'EVENT'
81@base.WatcherObjectRegistry.register
82class Audit(base.WatcherPersistentObject, base.WatcherObject,
83 base.WatcherObjectDictCompat):
85 # Version 1.0: Initial version
86 # Version 1.1: Added 'goal' and 'strategy' object field
87 # Version 1.2: Added 'auto_trigger' boolean field
88 # Version 1.3: Added 'next_run_time' DateTime field,
89 # 'interval' type has been changed from Integer to String
90 # Version 1.4: Added 'name' string field
91 # Version 1.5: Added 'hostname' field
92 # Version 1.6: Added 'start_time' and 'end_time' DateTime fields
93 # Version 1.7: Added 'force' boolean field
94 VERSION = '1.7'
96 dbapi = db_api.get_instance()
98 fields = {
99 'id': wfields.IntegerField(),
100 'uuid': wfields.UUIDField(),
101 'name': wfields.StringField(),
102 'audit_type': wfields.StringField(),
103 'state': wfields.StringField(),
104 'parameters': wfields.FlexibleDictField(nullable=True),
105 'interval': wfields.StringField(nullable=True),
106 'scope': wfields.FlexibleListOfDictField(nullable=True),
107 'goal_id': wfields.IntegerField(),
108 'strategy_id': wfields.IntegerField(nullable=True),
109 'auto_trigger': wfields.BooleanField(),
110 'next_run_time': wfields.DateTimeField(nullable=True,
111 tzinfo_aware=False),
112 'hostname': wfields.StringField(nullable=True),
113 'start_time': wfields.DateTimeField(nullable=True, tzinfo_aware=False),
114 'end_time': wfields.DateTimeField(nullable=True, tzinfo_aware=False),
115 'force': wfields.BooleanField(default=False, nullable=False),
117 'goal': wfields.ObjectField('Goal', nullable=True),
118 'strategy': wfields.ObjectField('Strategy', nullable=True),
119 }
121 object_fields = {
122 'goal': (objects.Goal, 'goal_id'),
123 'strategy': (objects.Strategy, 'strategy_id'),
124 }
126 def __init__(self, *args, **kwargs):
127 if 'force' not in kwargs:
128 kwargs['force'] = False
129 super(Audit, self).__init__(*args, **kwargs)
131 # Proxified field so we can keep the previous value after an update
132 _state = None
133 _old_state = None
135 # NOTE(v-francoise): The way oslo.versionedobjects works is by using a
136 # __new__ that will automatically create the attributes referenced in
137 # fields. These attributes are properties that raise an exception if no
138 # value has been assigned, which means that they store the actual field
139 # value in an "_obj_%(field)s" attribute. So because we want to proxify a
140 # value that is already proxified, we have to do what you see below.
141 @property
142 def _obj_state(self):
143 return self._state
145 @property
146 def _obj_old_state(self):
147 return self._old_state
149 @property
150 def old_state(self):
151 return self._old_state
153 @_obj_old_state.setter
154 def _obj_old_state(self, value):
155 self._old_state = value
157 @_obj_state.setter
158 def _obj_state(self, value):
159 if self._old_state is None and self._state is None:
160 self._state = value
161 else:
162 self._old_state, self._state = self._state, value
164 @base.remotable_classmethod
165 def get(cls, context, audit_id, eager=False):
166 """Find a audit based on its id or uuid and return a Audit object.
168 :param context: Security context. NOTE: This should only
169 be used internally by the indirection_api.
170 Unfortunately, RPC requires context as the first
171 argument, even though we don't use it.
172 A context should be set when instantiating the
173 object, e.g.: Audit(context)
174 :param audit_id: the id *or* uuid of a audit.
175 :param eager: Load object fields if True (Default: False)
176 :returns: a :class:`Audit` object.
177 """
178 if utils.is_int_like(audit_id):
179 return cls.get_by_id(context, audit_id, eager=eager)
180 elif utils.is_uuid_like(audit_id):
181 return cls.get_by_uuid(context, audit_id, eager=eager)
182 else:
183 raise exception.InvalidIdentity(identity=audit_id)
185 @base.remotable_classmethod
186 def get_by_id(cls, context, audit_id, eager=False):
187 """Find a audit based on its integer id and return a Audit object.
189 :param context: Security context. NOTE: This should only
190 be used internally by the indirection_api.
191 Unfortunately, RPC requires context as the first
192 argument, even though we don't use it.
193 A context should be set when instantiating the
194 object, e.g.: Audit(context)
195 :param audit_id: the id of a audit.
196 :param eager: Load object fields if True (Default: False)
197 :returns: a :class:`Audit` object.
198 """
199 db_audit = cls.dbapi.get_audit_by_id(context, audit_id, eager=eager)
200 audit = cls._from_db_object(cls(context), db_audit, eager=eager)
201 return audit
203 @base.remotable_classmethod
204 def get_by_uuid(cls, context, uuid, eager=False):
205 """Find a audit based on uuid and return a :class:`Audit` object.
207 :param context: Security context. NOTE: This should only
208 be used internally by the indirection_api.
209 Unfortunately, RPC requires context as the first
210 argument, even though we don't use it.
211 A context should be set when instantiating the
212 object, e.g.: Audit(context)
213 :param uuid: the uuid of a audit.
214 :param eager: Load object fields if True (Default: False)
215 :returns: a :class:`Audit` object.
216 """
218 db_audit = cls.dbapi.get_audit_by_uuid(context, uuid, eager=eager)
219 audit = cls._from_db_object(cls(context), db_audit, eager=eager)
220 return audit
222 @base.remotable_classmethod
223 def get_by_name(cls, context, name, eager=False):
224 """Find an audit based on name and return a :class:`Audit` object.
226 :param context: Security context. NOTE: This should only
227 be used internally by the indirection_api.
228 Unfortunately, RPC requires context as the first
229 argument, even though we don't use it.
230 A context should be set when instantiating the
231 object, e.g.: Audit(context)
232 :param name: the name of an audit.
233 :param eager: Load object fields if True (Default: False)
234 :returns: a :class:`Audit` object.
235 """
237 db_audit = cls.dbapi.get_audit_by_name(context, name, eager=eager)
238 audit = cls._from_db_object(cls(context), db_audit, eager=eager)
239 return audit
241 @base.remotable_classmethod
242 def list(cls, context, limit=None, marker=None, filters=None,
243 sort_key=None, sort_dir=None, eager=False):
244 """Return a list of Audit objects.
246 :param context: Security context. NOTE: This should only
247 be used internally by the indirection_api.
248 Unfortunately, RPC requires context as the first
249 argument, even though we don't use it.
250 A context should be set when instantiating the
251 object, e.g.: Audit(context)
252 :param limit: maximum number of resources to return in a single result.
253 :param marker: pagination marker for large data sets.
254 :param filters: Filters to apply. Defaults to None.
255 :param sort_key: column to sort results by.
256 :param sort_dir: direction to sort. "asc" or "desc".
257 :param eager: Load object fields if True (Default: False)
258 :returns: a list of :class:`Audit` object.
260 """
261 db_audits = cls.dbapi.get_audit_list(context,
262 limit=limit,
263 marker=marker,
264 filters=filters,
265 sort_key=sort_key,
266 sort_dir=sort_dir,
267 eager=eager)
268 return [cls._from_db_object(cls(context), obj, eager=eager)
269 for obj in db_audits]
271 @base.remotable
272 def create(self):
273 """Create an :class:`Audit` record in the DB.
275 :returns: An :class:`Audit` object.
276 """
277 values = self.obj_get_changes()
278 db_audit = self.dbapi.create_audit(values)
279 # Note(v-francoise): Always load eagerly upon creation so we can send
280 # notifications containing information about the related relationships
281 self._from_db_object(self, db_audit, eager=True)
283 def _notify():
284 notifications.audit.send_create(self._context, self)
286 _notify()
288 @base.remotable
289 def destroy(self):
290 """Delete the Audit from the DB."""
291 self.dbapi.destroy_audit(self.uuid)
292 self.obj_reset_changes()
294 @base.remotable
295 def save(self):
296 """Save updates to this Audit.
298 Updates will be made column by column based on the result
299 of self.what_changed().
300 """
301 updates = self.obj_get_changes()
302 db_obj = self.dbapi.update_audit(self.uuid, updates)
303 obj = self._from_db_object(
304 self.__class__(self._context), db_obj, eager=False)
305 self.obj_refresh(obj)
307 def _notify():
308 notifications.audit.send_update(
309 self._context, self, old_state=self.old_state)
311 _notify()
313 self.obj_reset_changes()
315 @base.remotable
316 def refresh(self, eager=False):
317 """Loads updates for this Audit.
319 Loads a audit with the same uuid from the database and
320 checks for updated attributes. Updates are applied from
321 the loaded audit column by column, if there are any updates.
322 :param eager: Load object fields if True (Default: False)
323 """
324 current = self.get_by_uuid(self._context, uuid=self.uuid, eager=eager)
325 self.obj_refresh(current)
327 @base.remotable
328 def soft_delete(self):
329 """Soft Delete the Audit from the DB."""
330 self.state = State.DELETED
331 self.save()
332 db_obj = self.dbapi.soft_delete_audit(self.uuid)
333 obj = self._from_db_object(
334 self.__class__(self._context), db_obj, eager=False)
335 self.obj_refresh(obj)
337 def _notify():
338 notifications.audit.send_delete(self._context, self)
340 _notify()
343class AuditStateTransitionManager(object):
345 TRANSITIONS = {
346 State.PENDING: [State.ONGOING, State.CANCELLED],
347 State.ONGOING: [State.FAILED, State.SUCCEEDED,
348 State.CANCELLED, State.SUSPENDED],
349 State.FAILED: [State.DELETED],
350 State.SUCCEEDED: [State.DELETED],
351 State.CANCELLED: [State.DELETED],
352 State.SUSPENDED: [State.ONGOING, State.DELETED],
353 }
355 INACTIVE_STATES = (State.CANCELLED, State.DELETED,
356 State.FAILED, State.SUSPENDED)
358 def check_transition(self, initial, new):
359 return new in self.TRANSITIONS.get(initial, [])
361 def is_inactive(self, audit):
362 return audit.state in self.INACTIVE_STATES