DB: inspection rules migration

Change-Id: I83b7e37ad44dca395144ce14510a1de0f6eeefa5
This commit is contained in:
cid 2025-01-22 23:05:11 +01:00
parent 5262536417
commit d6a692e3fa
9 changed files with 433 additions and 0 deletions

@ -1085,3 +1085,13 @@ class ImageServiceAuthenticationRequired(ImageUnacceptable):
_msg_fmt = _("The requested image %(image_ref)s requires "
"authentication which has not been provided. "
"Unable to proceed.")
class InspectionRuleAlreadyExists(Conflict):
"""Rule requested already exists in the database."""
_msg_fmt = _("A rule with UUID %(uuid)s already exists.")
class InspectionRuleNotFound(NotFound):
"""The requested rule was not found."""
_msg_fmt = _("Rule %(rule)s could not be found.")

@ -796,6 +796,7 @@ RELEASE_MAPPING = {
'VolumeTarget': ['1.0'],
'FirmwareComponent': ['1.0'],
'Runbook': ['1.0'],
'InspectionRule': ['1.0'],
}
},
}

@ -1637,3 +1637,63 @@ class Connection(object, metaclass=abc.ABCMeta):
should include child nodes with their own power supplies.
:returns: A list of tuples.
"""
@abc.abstractmethod
def create_inspection_rule(self, values):
"""Create an inspection rule.
:param values: A dict describing the rule.
:raises: InspectionRuleAlreadyExists if an inspection rule with the
same UUID exists.
:returns: A rule.
"""
@abc.abstractmethod
def update_inspection_rule(self, inspection_rule_id, values):
"""Update an inspection rule.
:param inspection_rule_id: The id or uuid of an inspection rule.
:param values: Dict of values to update.
:raises: InspectionRuleNotFound if the rule does not exist.
:returns: A rule.
"""
@abc.abstractmethod
def get_inspection_rule_by_uuid(self, inspection_rule_uuid):
"""Retrieve an inspection rule by UUID.
:param inspection_rule_uuid: UUID of the rule to retrieve.
:raises: InspectionRuleNotFound if the rule does not exist.
:returns: A rule.
"""
@abc.abstractmethod
def get_inspection_rule_by_id(self, inspection_rule_id):
"""Retrieve an inspection rule by id.
:param inspection_rule_id: id of the rule to retrieve.
:raises: InspectionRuleNotFound if the rule does not exist.
:returns: A rule.
"""
@abc.abstractmethod
def get_inspection_rule_list(self, limit=None, marker=None, filters=None,
sort_key=None, sort_dir=None):
"""Retrieve a list of inspection rules.
:param limit: Maximum number of rules to return.
:param marker: The last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:returns: A list of inspection rules.
"""
@abc.abstractmethod
def destroy_inspection_rule(self, inspection_rule_id):
"""Destroy an inspection rule.
:param inspection_rule_id: ID of the inspection_rule to destroy.
:raises: inspection_ruleNotFound if the inspection_rule does not exist.
"""

@ -0,0 +1,54 @@
# 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.
"""Add inspection rules
Revision ID: 21c48150dea9
Revises: 66bd9c5604d5
Create Date: 2024-08-14 14:13:24.462303
"""
from alembic import op
from oslo_db.sqlalchemy import types
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '21c48150dea9'
down_revision = '6e9cf6acce0b'
def upgrade():
op.create_table(
'inspection_rules',
sa.Column('version', sa.String(length=15), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False,
autoincrement=True),
sa.Column('uuid', sa.String(36), nullable=False),
sa.Column('priority', sa.Integer(), nullable=False, default=0),
sa.Column('description', sa.String(255), nullable=True),
sa.Column('scope', sa.String(255), nullable=True),
sa.Column('sensitive', sa.Boolean(), default=False),
sa.Column('phase', sa.String(16), nullable=True),
sa.Column('conditions', types.JsonEncodedList(mysql_as_long=True),
nullable=True),
sa.Column('actions', types.JsonEncodedList(mysql_as_long=True),
nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_inspection_rules0uuid'),
sa.Index('inspection_rule_scope_idx', 'scope'),
sa.Index('inspection_rule_phase_idx', 'phase'),
mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8'
)

@ -3171,3 +3171,76 @@ class Connection(api.Connection):
continue
nodes.append(r[0])
return nodes
def create_inspection_rule(self, values):
"""Create new rule"""
inspection_rule = models.InspectionRule()
inspection_rule.update(values)
with _session_for_write() as session:
try:
session.add(inspection_rule)
session.flush()
except db_exc.DBDuplicateEntry:
raise exception.InspectionRuleAlreadyExists(
uuid=values['uuid'])
return inspection_rule
def update_inspection_rule(self, rule_uuid, values):
"""Update an existing inspection rule.
:param values: Dict of values to update with.
:param rule_id: The rule id.
"""
with _session_for_write() as session:
query = session.query(models.InspectionRule).filter_by(
uuid=rule_uuid)
try:
ref = query.with_for_update().one()
except NoResultFound:
raise exception.InspectionRuleNotFound(
rule=rule_uuid)
ref.update(values)
return ref
def _get_inspection_rule(self, field, value):
"""Helper method for retrieving an inspection rule."""
query = sa.select(models.InspectionRule).where(field == value)
try:
with _session_for_read() as session:
res = session.execute(query).one()[0]
return res
except NoResultFound:
raise exception.InspectionRuleNotFound(rule=value)
def get_inspection_rule_by_uuid(self, inspection_rule_uuid):
return self._get_inspection_rule(models.InspectionRule.uuid,
inspection_rule_uuid)
def get_inspection_rule_by_id(self, inspection_rule_id):
return self._get_inspection_rule(models.InspectionRule.id,
inspection_rule_id)
def get_inspection_rule_list(self, limit=None, marker=None, filters=None,
sort_key=None, sort_dir=None):
query = (sa.select(models.InspectionRule))
if filters is None:
filters = dict()
supported_filters = {'phase', 'scope'}
unsupported_filters = set(filters).difference(supported_filters)
if unsupported_filters:
msg = _("SqlAlchemy API does not support "
"filtering by %s") % ', '.join(unsupported_filters)
raise ValueError(msg)
for field in filters:
query = query.filter_by(**{field: filters[field]})
return _paginate_query(models.InspectionRule, limit, marker,
sort_key, sort_dir, query)
def destroy_inspection_rule(self, inspection_rule_id):
with _session_for_write() as session:
count = session.query(models.InspectionRule).filter_by(
id=inspection_rule_id).delete()
if count == 0:
raise exception.InspectionRuleNotFound(
rule=inspection_rule_id)

@ -563,6 +563,26 @@ class RunbookStep(Base):
)
class InspectionRule(Base):
__tablename__ = 'inspection_rules'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_inspection_rules0uuid'),
Index('inspection_rule_scope_idx', 'scope'),
Index('inspection_rule_phase_idx', 'phase'),
table_args())
id = Column(Integer, primary_key=True)
uuid = Column(String(36), nullable=False)
priority = Column(Integer, nullable=False, default=0)
description = Column(String(255), nullable=True)
scope = Column(String(255), nullable=True)
sensitive = Column(Boolean, default=False)
phase = Column(String(16), nullable=True, default='main')
conditions = Column(db_types.JsonEncodedList(mysql_as_long=True),
nullable=True)
actions = Column(db_types.JsonEncodedList(mysql_as_long=True),
nullable=False)
def get_class(model_name):
"""Returns the model class with the specified name.

@ -31,6 +31,7 @@ def register_all():
__import__('ironic.objects.deploy_template')
__import__('ironic.objects.deployment')
__import__('ironic.objects.firmware')
__import__('ironic.objects.inspection_rule')
__import__('ironic.objects.node')
__import__('ironic.objects.node_history')
__import__('ironic.objects.node_inventory')

@ -0,0 +1,211 @@
# 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.
from oslo_versionedobjects import base as object_base
from ironic.db import api as db_api
from ironic.objects import base
from ironic.objects import fields as object_fields
from ironic.objects import notification
@base.IronicObjectRegistry.register
class InspectionRule(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = db_api.get_instance()
fields = {
'id': object_fields.IntegerField(),
'uuid': object_fields.UUIDField(nullable=False),
'priority': object_fields.IntegerField(default=0),
'description': object_fields.StringField(nullable=True),
'sensitive': object_fields.BooleanField(default=False),
'phase': object_fields.StringField(nullable=True, default='main'),
'scope': object_fields.StringField(nullable=True),
'actions': object_fields.ListOfFlexibleDictsField(nullable=False),
'conditions': object_fields.ListOfFlexibleDictsField(nullable=True),
}
# NOTE(mgoddard): We don't want to enable RPC on this call just yet.
# Remotable methods can be used in the future to replace current explicit
# RPC calls. Implications of calling new remote procedures should be
# thought through.
# @object_base.remotable
def create(self, context=None):
"""Create a InspectionRule record in the DB.
: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.: InspectionRule(context).
:raises: InspectionRuleName if a inspection rule with the same
name exists.
:raises: InspectionRuleAlreadyExists if a inspection rule with the same
UUID exists.
"""
values = self.do_version_changes_for_db()
db_rule = self.dbapi.create_inspection_rule(values)
self._from_db_object(self._context, self, db_rule)
def save(self, context=None):
"""Save updates to this InspectionRule.
Column-wise updates will be made based on the result of
self.what_changed().
:param context: Security context. NOTE: This should only
be used internally by the indirection_api,
but, 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.: InspectionRule(context)
:raises: InspectionRuleNotFound if the inspection rule does not exist.
"""
updates = self.do_version_changes_for_db()
db_rule = self.dbapi.update_inspection_rule(self.uuid, updates)
self._from_db_object(self._context, self, db_rule)
def destroy(self):
"""Delete the InspectionRule from the DB.
:param context: security context. NOTE: This should only
be used internally by the indirection_api,
but, 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.: InspectionRule(context).
:raises: InspectionRuleNotFound if the inspection_rule no longer
appears in the database.
"""
self.dbapi.destroy_inspection_rule(self.id)
self.obj_reset_changes()
# NOTE(mgoddard): We don't want to enable RPC on this call just yet.
# Remotable methods can be used in the future to replace current explicit
# RPC calls. Implications of calling new remote procedures should be
# thought through.
# @object_base.remotable_classmethod
@classmethod
def get_by_uuid(cls, context, uuid):
"""Find a inspection rule based on its UUID.
: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.: InspectionRule(context).
:param uuid: The UUID of a inspection rule.
:raises: InspectionRuleNotFound if the inspection rule no longer
appears in the database.
:returns: a :class:`InspectionRule` object.
"""
db_rule = cls.dbapi.get_inspection_rule_by_uuid(uuid)
rule = cls._from_db_object(context, cls(), db_rule)
return rule
# NOTE(mgoddard): We don't want to enable RPC on this call just yet.
# Remotable methods can be used in the future to replace current explicit
# RPC calls. Implications of calling new remote procedures should be
# thought through.
# @object_base.remotable_classmethod
@classmethod
def list(cls, context, limit=None, marker=None, sort_key=None,
sort_dir=None, filters=None):
"""Return a list of InspectionRule 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.: InspectionRule(context).
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param sort_key: column to sort results by.
:param sort_dir: direction to sort. "asc" or "desc".
:returns: a list of :class:`InspectionRule` objects.
"""
db_rules = cls.dbapi.get_inspection_rule_list(
limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir,
filters=filters)
return cls._from_db_object_list(context, db_rules)
def refresh(self, context=None):
"""Loads updates for this inspection rule.
Loads a inspection rule with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded rule column by column, if there are any updates.
: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.: Port(context)
:raises: InspectionRuleNotFound if the inspection rule no longer
appears in the database.
"""
current = self.get_by_uuid(self._context, uuid=self.uuid)
self.obj_refresh(current)
self.obj_reset_changes()
@base.IronicObjectRegistry.register
class InspectionRuleCRUDNotification(notification.NotificationBase):
"""Notification emitted on inspection rule API operations."""
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'payload': object_fields.ObjectField('InspectionRuleCRUDPayload')
}
@base.IronicObjectRegistry.register
class InspectionRuleCRUDPayload(notification.NotificationPayloadBase):
# Version 1.0: Initial version
VERSION = '1.0'
SCHEMA = {
'created_at': ('inspection_rule', 'created_at'),
'description': ('inspection_rule', 'description'),
'phase': ('inspection_rule', 'phase'),
'priority': ('inspection_rule', 'priority'),
'scope': ('inspection_rule', 'scope'),
'sensitive': ('inspection_rule', 'sensitive'),
'actions': ('inspection_rule', 'actions'),
'conditions': ('inspection_rule', 'conditions'),
'updated_at': ('inspection_rule', 'updated_at'),
'uuid': ('inspection_rule', 'uuid')
}
fields = {
'created_at': object_fields.DateTimeField(nullable=True),
'description': object_fields.StringField(nullable=True),
'phase': object_fields.StringField(nullable=True, default='main'),
'priority': object_fields.IntegerField(default=0),
'scope': object_fields.StringField(nullable=True),
'sensitive': object_fields.BooleanField(default=False),
'actions': object_fields.ListOfFlexibleDictsField(nullable=False),
'conditions': object_fields.ListOfFlexibleDictsField(nullable=True),
'updated_at': object_fields.DateTimeField(nullable=True),
'uuid': object_fields.UUIDField()
}
def __init__(self, inspection_rule, **kwargs):
super(InspectionRuleCRUDPayload, self).__init__(**kwargs)
self.populate_schema(inspection_rule=inspection_rule)

@ -727,6 +727,9 @@ expected_object_fingerprints = {
'Runbook': '1.0-7a9c65b49b5f7b45686b6a674e703629',
'RunbookCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'RunbookCRUDPayload': '1.0-f0c97f4ff29eb3401e53b34550a95e30',
'InspectionRule': '1.0-517185d327442696e408781343c2b83f',
'InspectionRuleCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'InspectionRuleCRUDPayload': '1.0-85d1cf2105308534a630299a897bf562',
}