Files
watcher/watcher/objects/audit.py
Alfredo Moralejo 5048a6e3ba Add status_message to objects and notifications
This patch is part of the skipped action blueprint. It adds the
`status_message` field to the Audit, ActionPlan and Action objects and
all related notifications.

It bumps the versions of all the affected objects and notifications and
update the tests to include the new fields.

Change-Id: I3b9467e7e37188e647379cd9c4cbbda8ed75383f
Signed-off-by: Alfredo Moralejo <amoralej@redhat.com>
2025-08-19 13:01:00 +02:00

366 lines
14 KiB
Python

# -*- encoding: utf-8 -*-
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
In the Watcher system, an :ref:`Audit <audit_definition>` is a request for
optimizing a :ref:`Cluster <cluster_definition>`.
The optimization is done in order to satisfy one :ref:`Goal <goal_definition>`
on a given :ref:`Cluster <cluster_definition>`.
For each :ref:`Audit <audit_definition>`, the Watcher system generates an
:ref:`Action Plan <action_plan_definition>`.
An :ref:`Audit <audit_definition>` has a life-cycle and its current state may
be one of the following:
- **PENDING** : a request for an :ref:`Audit <audit_definition>` has been
submitted (either manually by the
:ref:`Administrator <administrator_definition>` or automatically via some
event handling mechanism) and is in the queue for being processed by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
- **ONGOING** : the :ref:`Audit <audit_definition>` is currently being
processed by the
:ref:`Watcher Decision Engine <watcher_decision_engine_definition>`
- **SUCCEEDED** : the :ref:`Audit <audit_definition>` has been executed
successfully (note that it may not necessarily produce a
:ref:`Solution <solution_definition>`).
- **FAILED** : an error occurred while executing the
:ref:`Audit <audit_definition>`
- **DELETED** : the :ref:`Audit <audit_definition>` is still stored in the
:ref:`Watcher database <watcher_database_definition>` but is not returned
any more through the Watcher APIs.
- **CANCELLED** : the :ref:`Audit <audit_definition>` was in **PENDING** or
**ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>`
- **SUSPENDED** : the :ref:`Audit <audit_definition>` was in **ONGOING**
state and was suspended by the
:ref:`Administrator <administrator_definition>`
"""
import enum
from watcher.common import exception
from watcher.common import utils
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
class State(object):
ONGOING = 'ONGOING'
SUCCEEDED = 'SUCCEEDED'
FAILED = 'FAILED'
CANCELLED = 'CANCELLED'
DELETED = 'DELETED'
PENDING = 'PENDING'
SUSPENDED = 'SUSPENDED'
class AuditType(enum.Enum):
ONESHOT = 'ONESHOT'
CONTINUOUS = 'CONTINUOUS'
EVENT = 'EVENT'
@base.WatcherObjectRegistry.register
class Audit(base.WatcherPersistentObject, base.WatcherObject,
base.WatcherObjectDictCompat):
# Version 1.0: Initial version
# Version 1.1: Added 'goal' and 'strategy' object field
# Version 1.2: Added 'auto_trigger' boolean field
# Version 1.3: Added 'next_run_time' DateTime field,
# 'interval' type has been changed from Integer to String
# Version 1.4: Added 'name' string field
# Version 1.5: Added 'hostname' field
# Version 1.6: Added 'start_time' and 'end_time' DateTime fields
# Version 1.7: Added 'force' boolean field
# Version 1.8: Added 'status_message' string field
VERSION = '1.8'
dbapi = db_api.get_instance()
fields = {
'id': wfields.IntegerField(),
'uuid': wfields.UUIDField(),
'name': wfields.StringField(),
'audit_type': wfields.StringField(),
'state': wfields.StringField(),
'parameters': wfields.FlexibleDictField(nullable=True),
'interval': wfields.StringField(nullable=True),
'scope': wfields.FlexibleListOfDictField(nullable=True),
'goal_id': wfields.IntegerField(),
'strategy_id': wfields.IntegerField(nullable=True),
'auto_trigger': wfields.BooleanField(),
'next_run_time': wfields.DateTimeField(nullable=True,
tzinfo_aware=False),
'hostname': wfields.StringField(nullable=True),
'start_time': wfields.DateTimeField(nullable=True, tzinfo_aware=False),
'end_time': wfields.DateTimeField(nullable=True, tzinfo_aware=False),
'force': wfields.BooleanField(default=False, nullable=False),
'goal': wfields.ObjectField('Goal', nullable=True),
'strategy': wfields.ObjectField('Strategy', nullable=True),
'status_message': wfields.StringField(nullable=True),
}
object_fields = {
'goal': (objects.Goal, 'goal_id'),
'strategy': (objects.Strategy, 'strategy_id'),
}
def __init__(self, *args, **kwargs):
if 'force' not in kwargs:
kwargs['force'] = False
super(Audit, self).__init__(*args, **kwargs)
# Proxified field so we can keep the previous value after an update
_state = None
_old_state = None
# NOTE(v-francoise): The way oslo.versionedobjects works is by using a
# __new__ that will automatically create the attributes referenced in
# fields. These attributes are properties that raise an exception if no
# value has been assigned, which means that they store the actual field
# value in an "_obj_%(field)s" attribute. So because we want to proxify a
# value that is already proxified, we have to do what you see below.
@property
def _obj_state(self):
return self._state
@property
def _obj_old_state(self):
return self._old_state
@property
def old_state(self):
return self._old_state
@_obj_old_state.setter
def _obj_old_state(self, value):
self._old_state = value
@_obj_state.setter
def _obj_state(self, value):
if self._old_state is None and self._state is None:
self._state = value
else:
self._old_state, self._state = self._state, value
@base.remotable_classmethod
def get(cls, context, audit_id, eager=False):
"""Find a audit based on its id or uuid and return a Audit object.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Audit(context)
:param audit_id: the id *or* uuid of a audit.
:param eager: Load object fields if True (Default: False)
:returns: a :class:`Audit` object.
"""
if utils.is_int_like(audit_id):
return cls.get_by_id(context, audit_id, eager=eager)
elif utils.is_uuid_like(audit_id):
return cls.get_by_uuid(context, audit_id, eager=eager)
else:
raise exception.InvalidIdentity(identity=audit_id)
@base.remotable_classmethod
def get_by_id(cls, context, audit_id, eager=False):
"""Find a audit based on its integer id and return a Audit object.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Audit(context)
:param audit_id: the id of a audit.
:param eager: Load object fields if True (Default: False)
:returns: a :class:`Audit` object.
"""
db_audit = cls.dbapi.get_audit_by_id(context, audit_id, eager=eager)
audit = cls._from_db_object(cls(context), db_audit, eager=eager)
return audit
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid, eager=False):
"""Find a audit based on uuid and return a :class:`Audit` object.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Audit(context)
:param uuid: the uuid of a audit.
:param eager: Load object fields if True (Default: False)
:returns: a :class:`Audit` object.
"""
db_audit = cls.dbapi.get_audit_by_uuid(context, uuid, eager=eager)
audit = cls._from_db_object(cls(context), db_audit, eager=eager)
return audit
@base.remotable_classmethod
def get_by_name(cls, context, name, eager=False):
"""Find an audit based on name and return a :class:`Audit` object.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Audit(context)
:param name: the name of an audit.
:param eager: Load object fields if True (Default: False)
:returns: a :class:`Audit` object.
"""
db_audit = cls.dbapi.get_audit_by_name(context, name, eager=eager)
audit = cls._from_db_object(cls(context), db_audit, eager=eager)
return audit
@base.remotable_classmethod
def list(cls, context, limit=None, marker=None, filters=None,
sort_key=None, sort_dir=None, eager=False):
"""Return a list of Audit objects.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Audit(context)
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param filters: Filters to apply. Defaults to None.
:param sort_key: column to sort results by.
:param sort_dir: direction to sort. "asc" or "desc".
:param eager: Load object fields if True (Default: False)
:returns: a list of :class:`Audit` object.
"""
db_audits = cls.dbapi.get_audit_list(context,
limit=limit,
marker=marker,
filters=filters,
sort_key=sort_key,
sort_dir=sort_dir,
eager=eager)
return [cls._from_db_object(cls(context), obj, eager=eager)
for obj in db_audits]
@base.remotable
def create(self):
"""Create an :class:`Audit` record in the DB.
:returns: An :class:`Audit` object.
"""
values = self.obj_get_changes()
db_audit = self.dbapi.create_audit(values)
# Note(v-francoise): Always load eagerly upon creation so we can send
# notifications containing information about the related relationships
self._from_db_object(self, db_audit, eager=True)
def _notify():
notifications.audit.send_create(self._context, self)
_notify()
@base.remotable
def destroy(self):
"""Delete the Audit from the DB."""
self.dbapi.destroy_audit(self.uuid)
self.obj_reset_changes()
@base.remotable
def save(self):
"""Save updates to this Audit.
Updates will be made column by column based on the result
of self.what_changed().
"""
updates = self.obj_get_changes()
db_obj = self.dbapi.update_audit(self.uuid, updates)
obj = self._from_db_object(
self.__class__(self._context), db_obj, eager=False)
self.obj_refresh(obj)
def _notify():
notifications.audit.send_update(
self._context, self, old_state=self.old_state)
_notify()
self.obj_reset_changes()
@base.remotable
def refresh(self, eager=False):
"""Loads updates for this Audit.
Loads a audit with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded audit column by column, if there are any updates.
:param eager: Load object fields if True (Default: False)
"""
current = self.get_by_uuid(self._context, uuid=self.uuid, eager=eager)
self.obj_refresh(current)
@base.remotable
def soft_delete(self):
"""Soft Delete the Audit from the DB."""
self.state = State.DELETED
self.save()
db_obj = self.dbapi.soft_delete_audit(self.uuid)
obj = self._from_db_object(
self.__class__(self._context), db_obj, eager=False)
self.obj_refresh(obj)
def _notify():
notifications.audit.send_delete(self._context, self)
_notify()
class AuditStateTransitionManager(object):
TRANSITIONS = {
State.PENDING: [State.ONGOING, State.CANCELLED],
State.ONGOING: [State.FAILED, State.SUCCEEDED,
State.CANCELLED, State.SUSPENDED],
State.FAILED: [State.DELETED],
State.SUCCEEDED: [State.DELETED],
State.CANCELLED: [State.DELETED],
State.SUSPENDED: [State.ONGOING, State.DELETED],
}
INACTIVE_STATES = (State.CANCELLED, State.DELETED,
State.FAILED, State.SUSPENDED)
def check_transition(self, initial, new):
return new in self.TRANSITIONS.get(initial, [])
def is_inactive(self, audit):
return audit.state in self.INACTIVE_STATES