Follow-up: Apply Inspection Rules
Migrating Inspector to Ironic Change-Id: I6ff32eb2739d68d1e922b0b64e7c104f7b3a27a6
This commit is contained in:
@@ -1095,3 +1095,24 @@ class InspectionRuleAlreadyExists(Conflict):
|
||||
class InspectionRuleNotFound(NotFound):
|
||||
"""The requested rule was not found."""
|
||||
_msg_fmt = _("Rule %(rule)s could not be found.")
|
||||
|
||||
|
||||
class InspectionRuleValidationFailure(IronicException):
|
||||
"""Inspection rule validation fails during creation or execution."""
|
||||
_msg_fmt = _("Inspection rule validation failed. Reason: %(reason)s")
|
||||
|
||||
|
||||
class InspectionRuleExecutionFailure(HardwareInspectionFailure):
|
||||
"""Raised when an inspection rule fails during execution."""
|
||||
_msg_fmt = _("Inspection rule execution failed. Reason: %(reason)s")
|
||||
|
||||
|
||||
class RuleActionExecutionFailure(InspectionRuleExecutionFailure):
|
||||
"""Raised when an inspection rule action fails during execution."""
|
||||
_msg_fmt = _("Inspection rule action execution failed. "
|
||||
"Reason: %(reason)s")
|
||||
|
||||
|
||||
class RuleConditionCheckFailure(InspectionRuleExecutionFailure):
|
||||
"""Raised when an inspection rule condition fails during execution."""
|
||||
_msg_fmt = _("Inspection rule condition check failed. Reason: %(reason)s")
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
@@ -21,6 +17,8 @@ from oslo_log import log
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common.inspection_rules import base
|
||||
from ironic.common.inspection_rules import utils
|
||||
from ironic.drivers import utils as driver_utils
|
||||
from ironic import objects
|
||||
|
||||
|
||||
@@ -62,9 +60,6 @@ def update_nested_dict(d, key_path, value):
|
||||
class ActionBase(base.Base, metaclass=abc.ABCMeta):
|
||||
"""Abstract base class for rule action plugins."""
|
||||
|
||||
OPTIONAL_ARGS = set()
|
||||
"""Set with names of optional parameters."""
|
||||
|
||||
FORMATTED_ARGS = []
|
||||
"""List of params to be formatted with python format."""
|
||||
|
||||
@@ -72,7 +67,7 @@ class ActionBase(base.Base, metaclass=abc.ABCMeta):
|
||||
def __call__(self, task, *args, **kwargs):
|
||||
"""Run action on successful rule match."""
|
||||
|
||||
def _execute_with_loop(self, task, action, inventory, plugin_data):
|
||||
def execute_with_loop(self, task, action, inventory, plugin_data):
|
||||
loop_items = action.get('loop', [])
|
||||
results = []
|
||||
|
||||
@@ -80,55 +75,43 @@ class ActionBase(base.Base, metaclass=abc.ABCMeta):
|
||||
for item in loop_items:
|
||||
action_copy = action.copy()
|
||||
action_copy['args'] = item
|
||||
results.append(self._execute_action(task, action_copy,
|
||||
inventory, plugin_data))
|
||||
results.append(self.execute_action(task, action_copy,
|
||||
inventory, plugin_data))
|
||||
return results
|
||||
|
||||
def _execute_action(self, task, action, inventory, plugin_data):
|
||||
def execute_action(self, task, action, inventory, plugin_data):
|
||||
processed_args = self._process_args(task, action, inventory,
|
||||
plugin_data)
|
||||
|
||||
arg_values = [processed_args[arg_name]
|
||||
for arg_name in self.get_arg_names()]
|
||||
|
||||
for optional_arg in self.OPTIONAL_ARGS:
|
||||
arg_values.append(processed_args.get(optional_arg, False))
|
||||
|
||||
return self(task, *arg_values)
|
||||
return self(task, **processed_args)
|
||||
|
||||
|
||||
class LogAction(ActionBase):
|
||||
FORMATTED_ARGS = ['msg']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['msg']
|
||||
FORMATTED_ARGS = ['msg']
|
||||
VALID_LOG_LEVELS = {'debug', 'info', 'warning', 'error', 'critical'}
|
||||
|
||||
def __call__(self, task, msg, level='info'):
|
||||
level = level.lower()
|
||||
if level not in self.VALID_LOG_LEVELS:
|
||||
raise exception.InspectionRuleExecutionFailure(
|
||||
_("Invalid log level: %(level)s. Choose from %(levels)s") % {
|
||||
'level': level, 'levels': self.VALID_LOG_LEVELS})
|
||||
getattr(LOG, level)(msg)
|
||||
|
||||
|
||||
class FailAction(ActionBase):
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['msg']
|
||||
|
||||
def __call__(self, task, msg):
|
||||
msg = _('%(msg)s') % {'msg': msg}
|
||||
raise exception.HardwareInspectionFailure(error=msg)
|
||||
raise exception.HardwareInspectionFailure(error=str(msg))
|
||||
|
||||
|
||||
class SetAttributeAction(ActionBase):
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['path', 'value']
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
def __call__(self, task, path, value):
|
||||
try:
|
||||
attr_path_parts = path.strip('/').split('/')
|
||||
attr_path_parts = utils.normalize_path(path)
|
||||
if len(attr_path_parts) == 1:
|
||||
setattr(task.node, attr_path_parts[0], value)
|
||||
else:
|
||||
@@ -140,25 +123,20 @@ class SetAttributeAction(ActionBase):
|
||||
setattr(task.node, attr_path_parts[0], base_attr)
|
||||
task.node.save()
|
||||
except Exception as exc:
|
||||
msg = ("Failed to set attribute %(path)s "
|
||||
"with value %(value)s: %(exc)s" %
|
||||
{'path': path, 'value': value, 'exc': exc})
|
||||
msg = _("Failed to set attribute %(path)s "
|
||||
"with value %(value)s: %(exc)s") % {
|
||||
'path': path, 'value': value, 'exc': exc}
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class ExtendAttributeAction(ActionBase):
|
||||
|
||||
OPTIONAL_ARGS = {'unique'}
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['path', 'value']
|
||||
|
||||
def __call__(self, task, path, value, unique=False):
|
||||
try:
|
||||
attr_path_parts = path.strip('/').split('/')
|
||||
attr_path_parts = utils.normalize_path(path)
|
||||
if len(attr_path_parts) == 1:
|
||||
current = getattr(task.node, attr_path_parts[0], [])
|
||||
else:
|
||||
@@ -169,7 +147,9 @@ class ExtendAttributeAction(ActionBase):
|
||||
current = current.setdefault(attr_path_parts[-1], [])
|
||||
|
||||
if not isinstance(current, list):
|
||||
current = []
|
||||
msg = _("Cannot extend non-list attribute %(path)s with "
|
||||
"value %(value)s") % {'path': path, 'value': value}
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
if not unique or value not in current:
|
||||
current.append(value)
|
||||
|
||||
@@ -179,20 +159,16 @@ class ExtendAttributeAction(ActionBase):
|
||||
setattr(task.node, attr_path_parts[0], base_attr)
|
||||
task.node.save()
|
||||
except Exception as exc:
|
||||
msg = ("Failed to extend attribute %(path)s: %(exc)s") % {
|
||||
msg = _("Failed to extend attribute %(path)s: %(exc)s") % {
|
||||
'path': path, 'exc': exc}
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class DelAttributeAction(ActionBase):
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['path']
|
||||
|
||||
def __call__(self, task, path):
|
||||
try:
|
||||
attr_path_parts = path.strip('/').split('/')
|
||||
attr_path_parts = utils.normalize_path(path)
|
||||
if len(attr_path_parts) == 1:
|
||||
delattr(task.node, attr_path_parts[0])
|
||||
else:
|
||||
@@ -204,34 +180,26 @@ class DelAttributeAction(ActionBase):
|
||||
setattr(task.node, attr_path_parts[0], base_attr)
|
||||
task.node.save()
|
||||
except Exception as exc:
|
||||
msg = ("Failed to delete attribute at %(path)s: %(exc)s") % {
|
||||
msg = _("Failed to delete attribute at %(path)s: %(exc)s") % {
|
||||
'path': path, 'exc': exc}
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class AddTraitAction(ActionBase):
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['name']
|
||||
|
||||
def __call__(self, task, name):
|
||||
try:
|
||||
new_trait = objects.Trait(task.context, node_id=task.node.id,
|
||||
trait=name)
|
||||
new_trait.create()
|
||||
except Exception as exc:
|
||||
msg = (_("Failed to add new trait %(name)s: %(exc)s") %
|
||||
{'name': name, 'exc': exc})
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
msg = _("Failed to add new trait %(name)s: %(exc)s") % {
|
||||
'name': name, 'exc': exc}
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class RemoveTraitAction(ActionBase):
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['name']
|
||||
|
||||
def __call__(self, task, name):
|
||||
try:
|
||||
objects.Trait.destroy(task.context, node_id=task.node.id,
|
||||
@@ -240,100 +208,65 @@ class RemoveTraitAction(ActionBase):
|
||||
LOG.warning(_("Failed to remove trait %(name)s: %(exc)s"),
|
||||
{'name': name, 'exc': exc})
|
||||
except Exception as exc:
|
||||
msg = (_("Failed to remove trait %(name)s: %(exc)s") %
|
||||
{'name': name, 'exc': exc})
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
msg = _("Failed to remove trait %(name)s: %(exc)s") % {
|
||||
'name': name, 'exc': exc}
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class SetCapabilityAction(ActionBase):
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['name', 'value']
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
def __call__(self, task, name, value):
|
||||
try:
|
||||
properties = task.node.properties.copy()
|
||||
capabilities = properties.get('capabilities', '')
|
||||
caps = dict(cap.split(':', 1)
|
||||
for cap in capabilities.split(',') if cap)
|
||||
caps[name] = value
|
||||
properties['capabilities'] = ','.join('%s:%s' % (k, v)
|
||||
for k, v in caps.items())
|
||||
task.node.properties = properties
|
||||
task.node.save()
|
||||
driver_utils.add_node_capability(task, name, value)
|
||||
except Exception as exc:
|
||||
raise exception.InvalidParameterValue(
|
||||
"Failed to set capability %(name)s: %(exc)s" %
|
||||
{'name': name, 'exc': exc})
|
||||
msg = _("Failed to set capability %(name)s: %(exc)s") % {
|
||||
'name': name, 'exc': exc}
|
||||
raise exception.RuleActionExecutionFailure(
|
||||
reason=msg)
|
||||
|
||||
|
||||
class UnsetCapabilityAction(ActionBase):
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['name']
|
||||
|
||||
def __call__(self, task, name):
|
||||
try:
|
||||
properties = task.node.properties.copy()
|
||||
capabilities = properties.get('capabilities', '')
|
||||
caps = dict(cap.split(':', 1)
|
||||
for cap in capabilities.split(',') if cap)
|
||||
caps.pop(name, None)
|
||||
properties['capabilities'] = ','.join('%s:%s' % (k, v)
|
||||
for k, v in caps.items())
|
||||
task.node.properties = properties
|
||||
task.node.save()
|
||||
driver_utils.remove_node_capability(task, name)
|
||||
except Exception as exc:
|
||||
raise exception.InvalidParameterValue(
|
||||
"Failed to unset capability %(name)s: %(exc)s" %
|
||||
{'name': name, 'exc': exc})
|
||||
msg = _("Failed to unset capability %(name)s: %(exc)s") % {
|
||||
'name': name, 'exc': exc}
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class SetPluginDataAction(ActionBase):
|
||||
|
||||
REQUIRES_PLUGIN_DATA = True
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['path', 'value', 'plugin_data']
|
||||
|
||||
def __call__(self, task, path, value, plugin_data):
|
||||
try:
|
||||
update_nested_dict(plugin_data, path, value)
|
||||
return {'plugin_data': plugin_data}
|
||||
except Exception as exc:
|
||||
msg = ("Failed to set plugin data at %(path)s: %(exc)s" % {
|
||||
'path': path, 'exc': exc})
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
msg = _("Failed to set plugin data at %(path)s: %(exc)s") % {
|
||||
'path': path, 'exc': exc}
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class ExtendPluginDataAction(ActionBase):
|
||||
|
||||
OPTIONAL_ARGS = {'unique'}
|
||||
REQUIRES_PLUGIN_DATA = True
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['path', 'value', 'plugin_data']
|
||||
|
||||
def __call__(self, task, path, value, plugin_data, unique=False):
|
||||
try:
|
||||
current = self._get_nested_value(plugin_data, path)
|
||||
if current is None:
|
||||
current = []
|
||||
update_nested_dict(plugin_data, path, current)
|
||||
elif not isinstance(current, list):
|
||||
current = []
|
||||
update_nested_dict(plugin_data, path, current)
|
||||
if not unique or value not in current:
|
||||
current = self._get_nested_value(plugin_data, path) or []
|
||||
update_nested_dict(plugin_data, path, current)
|
||||
if not unique or (value not in current):
|
||||
current.append(value)
|
||||
return {'plugin_data': plugin_data}
|
||||
except Exception as exc:
|
||||
msg = ("Failed to extend plugin data at %(path)s: %(exc)s") % {
|
||||
msg = _("Failed to extend plugin data at %(path)s: %(exc)s") % {
|
||||
'path': path, 'exc': exc}
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
@staticmethod
|
||||
def _get_nested_value(d, key_path, default=None):
|
||||
@@ -349,19 +282,16 @@ class ExtendPluginDataAction(ActionBase):
|
||||
|
||||
class UnsetPluginDataAction(ActionBase):
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['path', 'plugin_data']
|
||||
REQUIRES_PLUGIN_DATA = True
|
||||
|
||||
def __call__(self, task, path, plugin_data):
|
||||
try:
|
||||
if not self._unset_nested_dict(plugin_data, path):
|
||||
LOG.warning("Path %s not found", path)
|
||||
return {'plugin_data': plugin_data}
|
||||
except Exception as exc:
|
||||
msg = ("Failed to unset plugin data at %(path)s: %(exc)s") % {
|
||||
msg = _("Failed to unset plugin data at %(path)s: %(exc)s") % {
|
||||
'path': path, 'exc': exc}
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
@staticmethod
|
||||
def _unset_nested_dict(d, key_path):
|
||||
@@ -387,18 +317,15 @@ class UnsetPluginDataAction(ActionBase):
|
||||
|
||||
|
||||
class SetPortAttributeAction(ActionBase):
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['port_id', 'path', 'value']
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
def __call__(self, task, port_id, path, value):
|
||||
port = next((p for p in task.ports if p.uuid == port_id), None)
|
||||
if not port:
|
||||
raise exception.PortNotFound(port=port_id)
|
||||
try:
|
||||
attr_path_parts = path.strip('/').split('/')
|
||||
attr_path_parts = utils.normalize_path(path)
|
||||
if len(attr_path_parts) == 1:
|
||||
setattr(port, attr_path_parts[0], value)
|
||||
else:
|
||||
@@ -410,27 +337,23 @@ class SetPortAttributeAction(ActionBase):
|
||||
setattr(port, attr_path_parts[0], base_attr)
|
||||
port.save()
|
||||
except Exception as exc:
|
||||
msg = ("Failed to set attribute %(path)s for port "
|
||||
"%(port_id)s: %(exc)s") % {'path': path,
|
||||
'port_id': port_id,
|
||||
'exc': str(exc)}
|
||||
LOG.warning(msg)
|
||||
msg = _("Failed to set attribute %(path)s for port "
|
||||
"%(port_id)s: %(exc)s") % {
|
||||
'path': path, 'port_id': port_id, 'exc': exc}
|
||||
LOG.error(msg)
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class ExtendPortAttributeAction(ActionBase):
|
||||
OPTIONAL_ARGS = {'unique'}
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['port_id', 'path', 'value']
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
def __call__(self, task, port_id, path, value, unique=False):
|
||||
port = next((p for p in task.ports if p.uuid == port_id), None)
|
||||
if not port:
|
||||
raise exception.PortNotFound(port=port_id)
|
||||
try:
|
||||
attr_path_parts = path.strip('/').split('/')
|
||||
attr_path_parts = utils.normalize_path(path)
|
||||
if len(attr_path_parts) == 1:
|
||||
current = getattr(port, attr_path_parts[0], [])
|
||||
else:
|
||||
@@ -441,7 +364,9 @@ class ExtendPortAttributeAction(ActionBase):
|
||||
current = current.setdefault(attr_path_parts[-1], [])
|
||||
|
||||
if not isinstance(current, list):
|
||||
current = []
|
||||
msg = (_("Cannot extend non-list attribute %(path)s with "
|
||||
" value %(value)s") % {'path': path, 'value': value})
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
if not unique or value not in current:
|
||||
current.append(value)
|
||||
|
||||
@@ -451,25 +376,21 @@ class ExtendPortAttributeAction(ActionBase):
|
||||
setattr(port, attr_path_parts[0], base_attr)
|
||||
port.save()
|
||||
except Exception as exc:
|
||||
msg = ("Failed to extend attribute %(path)s for port "
|
||||
"%(port_id)s: %(exc)s") % {'path': path,
|
||||
'port_id': port_id,
|
||||
'exc': str(exc)}
|
||||
LOG.warning(msg)
|
||||
msg = _("Failed to extend attribute %(path)s for port "
|
||||
"%(port_id)s: %(exc)s") % {
|
||||
'path': path, 'port_id': port_id, 'exc': exc}
|
||||
LOG.error(msg)
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
|
||||
class DelPortAttributeAction(ActionBase):
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['port_id', 'path']
|
||||
|
||||
def __call__(self, task, port_id, path):
|
||||
port = next((p for p in task.ports if p.uuid == port_id), None)
|
||||
if not port:
|
||||
raise exception.PortNotFound(port=port_id)
|
||||
try:
|
||||
attr_path_parts = path.strip('/').split('/')
|
||||
attr_path_parts = utils.normalize_path(path)
|
||||
if len(attr_path_parts) == 1:
|
||||
delattr(port, attr_path_parts[0])
|
||||
else:
|
||||
@@ -482,7 +403,7 @@ class DelPortAttributeAction(ActionBase):
|
||||
port.save()
|
||||
except Exception as exc:
|
||||
msg = ("Failed to delete attribute %(path)s for port "
|
||||
"%(port_id)s: %(exc)s") % {'path': path,
|
||||
'port_id': port_id,
|
||||
'exc': str(exc)}
|
||||
LOG.warning(msg)
|
||||
"%(port_id)s: %(exc)s") % {
|
||||
'path': path, 'port_id': port_id, 'exc': str(exc)}
|
||||
LOG.error(msg)
|
||||
raise exception.RuleActionExecutionFailure(reason=msg)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
@@ -13,12 +10,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
import inspect
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import utils as common_utils
|
||||
from ironic.common.inspection_rules import utils
|
||||
import ironic.conf
|
||||
|
||||
|
||||
@@ -29,70 +27,69 @@ SENSITIVE_FIELDS = ['password', 'auth_token', 'bmc_password']
|
||||
|
||||
class Base(object):
|
||||
|
||||
USES_PLUGIN_DATA = False
|
||||
REQUIRES_PLUGIN_DATA = False
|
||||
"""Flag to indicate if this action needs plugin_data as an arg."""
|
||||
|
||||
OPTIONAL_ARGS = set()
|
||||
"""Set with names of optional parameters."""
|
||||
def _get_validation_signature(self):
|
||||
"""Get the signature to validate against."""
|
||||
signature = inspect.signature(self.__call__)
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_arg_names(cls):
|
||||
"""Return list of argument names in order expected."""
|
||||
raise NotImplementedError
|
||||
# Strip off 'task' parameter.
|
||||
parameters = list(signature.parameters.values())[1:]
|
||||
|
||||
def _normalize_list_args(self, *args, **kwargs):
|
||||
"""Convert list arguments into dictionary format.
|
||||
required_args = [p.name for p in parameters
|
||||
if p.default is inspect.Parameter.empty]
|
||||
optional_args = [p.name for p in parameters
|
||||
if p.default is not inspect.Parameter.empty]
|
||||
return required_args, optional_args
|
||||
|
||||
"""
|
||||
op_name = kwargs['op']
|
||||
arg_list = kwargs['args']
|
||||
if not isinstance(arg_list, list):
|
||||
if isinstance(arg_list, dict) and 'plugin-data' in op_name:
|
||||
arg_list['plugin_data'] = {}
|
||||
return arg_list
|
||||
def _normalize_list_args(self, required_args, optional_args, op_args):
|
||||
"""Convert list arguments into dictionary format."""
|
||||
if not isinstance(op_args, list):
|
||||
# Initialize required context fields if needed
|
||||
if isinstance(op_args, dict) and self.REQUIRES_PLUGIN_DATA:
|
||||
op_args['plugin_data'] = {}
|
||||
return op_args
|
||||
|
||||
# plugin_data is a required argument during validation but since
|
||||
# it comes from the inspection data and added later, we need to
|
||||
# make sure validation does not fail for that sake.
|
||||
if 'plugin-data' in op_name:
|
||||
arg_list.append('{}')
|
||||
# Initialize required context fields if needed
|
||||
if self.REQUIRES_PLUGIN_DATA:
|
||||
op_args.append({})
|
||||
|
||||
arg_names = set(self.__class__.get_arg_names())
|
||||
if len(arg_list) < len(arg_names):
|
||||
missing = arg_names[len(arg_list):]
|
||||
if len(op_args) < len(required_args):
|
||||
missing = [p for p in required_args[len(op_args):]]
|
||||
msg = (_("Not enough arguments provided. Missing: %s"),
|
||||
", ".join(missing))
|
||||
LOG.error(msg)
|
||||
raise ValueError(msg)
|
||||
raise exception.InspectionRuleValidationFailure(msg)
|
||||
|
||||
arg_list = {name: arg_list[i] for i, name in enumerate(arg_names)}
|
||||
normalized_args = {name: op_args[i]
|
||||
for i, name in enumerate(required_args)}
|
||||
|
||||
# Add optional args if they exist in the input
|
||||
start_idx = len(arg_names)
|
||||
for i, opt_arg in enumerate(self.OPTIONAL_ARGS):
|
||||
if start_idx + i < len(arg_list):
|
||||
arg_list[opt_arg] = arg_list[start_idx + i]
|
||||
normalized_args.update(
|
||||
zip(optional_args, op_args[len(required_args):])
|
||||
)
|
||||
|
||||
return arg_list
|
||||
return normalized_args
|
||||
|
||||
def validate(self, *args, **kwargs):
|
||||
def validate(self, op_args):
|
||||
"""Validate args passed during creation.
|
||||
|
||||
Default implementation checks for presence of required fields.
|
||||
|
||||
:param args: args as a dictionary
|
||||
:param kwargs: used for extensibility without breaking existing plugins
|
||||
:raises: ValueError on validation failure
|
||||
:param op_args: Operator args as a dictionary
|
||||
:raises: InspectionRuleValidationFailure on validation failure
|
||||
"""
|
||||
required_args = set(self.__class__.get_arg_names())
|
||||
required_args, optional_args = self._get_validation_signature()
|
||||
normalized_args = self._normalize_list_args(
|
||||
args=kwargs.get('args', {}), op=kwargs['op'])
|
||||
required_args=required_args, optional_args=optional_args,
|
||||
op_args=op_args)
|
||||
|
||||
# If after normalization attempt, we still do not have a dictionary,
|
||||
# then it was never a list, so, not a supported type.
|
||||
if isinstance(normalized_args, dict):
|
||||
provided = set(normalized_args.keys())
|
||||
missing = required_args - provided
|
||||
unexpected = provided - (required_args | self.OPTIONAL_ARGS)
|
||||
provided = set(normalized_args)
|
||||
missing = set(required_args) - provided
|
||||
unexpected = provided - (set(required_args) | set(optional_args))
|
||||
|
||||
msg = []
|
||||
if missing:
|
||||
@@ -102,11 +99,12 @@ class Base(object):
|
||||
msg.append(_('unexpected argument(s): %s')
|
||||
% ', '.join(unexpected))
|
||||
if msg:
|
||||
raise ValueError('; '.join(msg))
|
||||
raise exception.InspectionRuleValidationFailure(
|
||||
'; '.join(msg))
|
||||
else:
|
||||
raise ValueError(_("args must be either a list or dictionary"))
|
||||
raise exception.InspectionRuleValidationFailure(
|
||||
_("args must be either a list or dictionary"))
|
||||
|
||||
@staticmethod
|
||||
def interpolate_variables(value, node, inventory, plugin_data):
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
@@ -135,21 +133,25 @@ class Base(object):
|
||||
|
||||
op = operation.get('op')
|
||||
if not op:
|
||||
raise ValueError("Operation must contain 'op' key")
|
||||
raise exception.InspectionRuleExecutionFailure(
|
||||
_("Operation must contain 'op' key"))
|
||||
|
||||
op, invtd = common_utils.parse_inverted_operator(op)
|
||||
dict_args = self._normalize_list_args(args=operation.get('args', {}),
|
||||
op=op)
|
||||
required_args, optional_args = self._get_validation_signature()
|
||||
|
||||
op, invtd = utils.parse_inverted_operator(op)
|
||||
dict_args = self._normalize_list_args(
|
||||
required_args=required_args, optional_args=optional_args,
|
||||
op_args=operation.get('args', {}))
|
||||
|
||||
# plugin-data becomes available during inspection,
|
||||
# we need to populate with the actual value.
|
||||
if 'plugin_data' in dict_args or 'plugin-data' in op:
|
||||
if self.REQUIRES_PLUGIN_DATA:
|
||||
dict_args['plugin_data'] = plugin_data
|
||||
|
||||
node = task.node
|
||||
formatted_args = getattr(self, 'FORMATTED_ARGS', [])
|
||||
return {
|
||||
k: (self.interpolate_variables(v, node, inventory, plugin_data)
|
||||
k: (Base.interpolate_variables(v, node, inventory, plugin_data)
|
||||
if k in formatted_args else v)
|
||||
for k, v in dict_args.items()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
@@ -13,7 +10,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from oslo_log import log
|
||||
import yaml
|
||||
|
||||
@@ -21,8 +17,8 @@ from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common.inspection_rules import actions
|
||||
from ironic.common.inspection_rules import operators
|
||||
from ironic.common.inspection_rules import utils
|
||||
from ironic.common.inspection_rules import validation
|
||||
from ironic.common import utils as common_utils
|
||||
from ironic.conf import CONF
|
||||
from ironic import objects
|
||||
|
||||
@@ -56,7 +52,7 @@ def get_built_in_rules():
|
||||
'conditions': rule_data.get('conditions', []),
|
||||
'built_in': True
|
||||
}
|
||||
validation.validate_inspection_rule(rule)
|
||||
validation.validate_rule(rule)
|
||||
built_in_rules.append(rule)
|
||||
except Exception as e:
|
||||
LOG.error(_("Error parsing built-in rule: %s"), e)
|
||||
@@ -77,24 +73,13 @@ def get_built_in_rules():
|
||||
return built_in_rules
|
||||
|
||||
|
||||
def _mask_sensitive_data(data):
|
||||
"""Recursively mask sensitive fields in data."""
|
||||
if isinstance(data, dict):
|
||||
return {key: (_mask_sensitive_data(value)
|
||||
if key not in SENSITIVE_FIELDS else '***')
|
||||
for key, value in data.items()}
|
||||
elif isinstance(data, list):
|
||||
return [_mask_sensitive_data(item) for item in data]
|
||||
return data
|
||||
|
||||
|
||||
def check_conditions(task, rule, inventory, plugin_data):
|
||||
try:
|
||||
if not rule.get('conditions', None):
|
||||
return True
|
||||
|
||||
for condition in rule['conditions']:
|
||||
op, invtd = common_utils.parse_inverted_operator(
|
||||
op, invtd = utils.parse_inverted_operator(
|
||||
condition['op'])
|
||||
|
||||
if op not in operators.OPERATORS:
|
||||
@@ -104,14 +89,13 @@ def check_conditions(task, rule, inventory, plugin_data):
|
||||
'op': op, 'supported_ops': supported_ops})
|
||||
raise ValueError(msg)
|
||||
|
||||
result = False
|
||||
plugin = operators.get_operator(op)
|
||||
if 'loop' in condition:
|
||||
result = plugin()._check_with_loop(task, condition, inventory,
|
||||
plugin_data)
|
||||
result = plugin().check_with_loop(task, condition, inventory,
|
||||
plugin_data)
|
||||
else:
|
||||
result = plugin()._check_condition(task, condition, inventory,
|
||||
plugin_data)
|
||||
result = plugin().check_condition(task, condition, inventory,
|
||||
plugin_data)
|
||||
if not result:
|
||||
LOG.debug("Skipping rule %(rule)s on node %(node)s: "
|
||||
"condition check '%(op)s': '%(args)s' failed ",
|
||||
@@ -128,7 +112,6 @@ def check_conditions(task, rule, inventory, plugin_data):
|
||||
|
||||
def apply_actions(task, rule, inventory, plugin_data):
|
||||
|
||||
result = {'plugin_data': plugin_data}
|
||||
for action in rule['actions']:
|
||||
try:
|
||||
op = action['op']
|
||||
@@ -141,15 +124,11 @@ def apply_actions(task, rule, inventory, plugin_data):
|
||||
|
||||
plugin = actions.get_action(op)
|
||||
if 'loop' in action:
|
||||
action_result = plugin()._execute_with_loop(
|
||||
task, action, inventory, result['plugin_data'])
|
||||
plugin().execute_with_loop(task, action, inventory,
|
||||
plugin_data)
|
||||
else:
|
||||
action_result = plugin()._execute_action(
|
||||
task, action, inventory, result['plugin_data'])
|
||||
|
||||
if action_result is not None and isinstance(action_result, dict):
|
||||
result['plugin_data'] = action_result.get(
|
||||
'plugin_data', result['plugin_data'])
|
||||
plugin().execute_action(task, action, inventory,
|
||||
plugin_data)
|
||||
except exception.IronicException as err:
|
||||
LOG.error("Error applying action on node %(node)s: %(err)s.",
|
||||
{'node': task.node.uuid, 'err': err})
|
||||
@@ -159,7 +138,6 @@ def apply_actions(task, rule, inventory, plugin_data):
|
||||
"%(node)s: %(err)s.", {'node': task.node.uuid,
|
||||
'err': err})
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def apply_rules(task, inventory, plugin_data, inspection_phase):
|
||||
@@ -180,47 +158,45 @@ def apply_rules(task, inventory, plugin_data, inspection_phase):
|
||||
'node': node.uuid})
|
||||
return
|
||||
|
||||
mask_secrets = CONF.inspection_rules.mask_secrets
|
||||
if mask_secrets == 'always':
|
||||
inventory = _mask_sensitive_data(inventory)
|
||||
plugin_data = _mask_sensitive_data(plugin_data)
|
||||
elif mask_secrets == 'sensitive':
|
||||
# Mask secrets unless the rule is marked as sensitive
|
||||
for rule in rules:
|
||||
if not rule.get('sensitive', False):
|
||||
inventory = _mask_sensitive_data(inventory)
|
||||
plugin_data = _mask_sensitive_data(plugin_data)
|
||||
break
|
||||
|
||||
rules.sort(key=lambda rule: rule['priority'], reverse=True)
|
||||
rules.sort(key=lambda rule: rule.get('priority', 0), reverse=True)
|
||||
LOG.debug("Applying %(count)d inspection rules to node %(node)s",
|
||||
{'count': len(rules), 'node': node.uuid})
|
||||
|
||||
result = {'plugin_data': plugin_data}
|
||||
mask_secrets = CONF.inspection_rules.mask_secrets
|
||||
for rule in rules:
|
||||
try:
|
||||
if not check_conditions(task, rule, inventory, plugin_data):
|
||||
|
||||
should_mask = False
|
||||
is_sensitive_rule = rule.get('sensitive', False)
|
||||
|
||||
if (mask_secrets == 'always'
|
||||
or mask_secrets == 'sensitive' and not is_sensitive_rule):
|
||||
should_mask = True
|
||||
|
||||
masked_inventory = utils.ShallowMaskDict(
|
||||
inventory, sensitive_fields=SENSITIVE_FIELDS,
|
||||
mask_enabled=should_mask)
|
||||
|
||||
masked_plugin_data = utils.ShallowMaskDict(
|
||||
plugin_data, sensitive_fields=SENSITIVE_FIELDS,
|
||||
mask_enabled=should_mask)
|
||||
|
||||
if not check_conditions(task, rule, masked_inventory,
|
||||
masked_plugin_data):
|
||||
continue
|
||||
|
||||
LOG.info("Applying actions for rule %(rule)s to node %(node)s",
|
||||
{'rule': rule['uuid'], 'node': node.uuid})
|
||||
|
||||
rule_result = apply_actions(task, rule, inventory, plugin_data)
|
||||
if rule_result and 'plugin_data' in rule_result:
|
||||
result['plugin_data'] = rule_result['plugin_data']
|
||||
apply_actions(task, rule, masked_inventory, masked_plugin_data)
|
||||
|
||||
except exception.HardwareInspectionFailure:
|
||||
raise
|
||||
except exception.IronicException as e:
|
||||
if rule['sensitive']:
|
||||
LOG.error("Error applying sensitive rule %(rule)s to node "
|
||||
"%(node)s", {'rule': rule['uuid'],
|
||||
'node': node.uuid})
|
||||
else:
|
||||
LOG.error("Error applying rule %(rule)s to node "
|
||||
"%(node)s: %(error)s", {'rule': rule['uuid'],
|
||||
'node': node.uuid,
|
||||
'error': e})
|
||||
LOG.error(_("Error applying rule %(rule)s to node "
|
||||
"%(node)s: %(error)s"), {'rule': rule['uuid'],
|
||||
'node': node.uuid,
|
||||
'error': e})
|
||||
raise
|
||||
except Exception as e:
|
||||
msg = ("Failed to apply rule %(rule)s to node %(node)s: "
|
||||
@@ -232,4 +208,3 @@ def apply_rules(task, inventory, plugin_data, inspection_phase):
|
||||
raise exception.IronicException(msg)
|
||||
|
||||
LOG.info("Finished applying inspection rules to node %s", node.uuid)
|
||||
return result
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
@@ -20,9 +17,10 @@ import re
|
||||
import netaddr
|
||||
from oslo_log import log
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common.inspection_rules import base
|
||||
from ironic.common import utils as common_utils
|
||||
from ironic.common.inspection_rules import utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
@@ -59,14 +57,11 @@ def coerce(value, expected):
|
||||
class OperatorBase(base.Base, metaclass=abc.ABCMeta):
|
||||
"""Abstract base class for rule condition plugins."""
|
||||
|
||||
OPTIONAL_ARGS = set()
|
||||
"""Set with names of optional parameters."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def check(self, *args, **kwargs):
|
||||
"""Check if condition holds for a given field."""
|
||||
def __call__(self, task, *args, **kwargs):
|
||||
"""Checks if condition holds for a given field."""
|
||||
|
||||
def _check_with_loop(self, task, condition, inventory, plugin_data):
|
||||
def check_with_loop(self, task, condition, inventory, plugin_data):
|
||||
loop_items = condition.get('loop', [])
|
||||
multiple = condition.get('multiple', 'any')
|
||||
results = []
|
||||
@@ -75,8 +70,8 @@ class OperatorBase(base.Base, metaclass=abc.ABCMeta):
|
||||
for item in loop_items:
|
||||
condition_copy = condition.copy()
|
||||
condition_copy['args'] = item
|
||||
result = self._check_condition(task, condition_copy,
|
||||
inventory, plugin_data)
|
||||
result = self.check_condition(task, condition_copy,
|
||||
inventory, plugin_data)
|
||||
results.append(result)
|
||||
|
||||
if multiple == 'first' and result:
|
||||
@@ -89,9 +84,9 @@ class OperatorBase(base.Base, metaclass=abc.ABCMeta):
|
||||
elif multiple == 'all':
|
||||
return all(results)
|
||||
return results[0] if results else False
|
||||
return self._check_condition(task, condition, inventory, plugin_data)
|
||||
return self.check_condition(task, condition, inventory, plugin_data)
|
||||
|
||||
def _check_condition(self, task, condition, inventory, plugin_data):
|
||||
def check_condition(self, task, condition, inventory, plugin_data):
|
||||
"""Process condition arguments and apply the check logic.
|
||||
|
||||
:param task: TaskManger instance
|
||||
@@ -99,37 +94,41 @@ class OperatorBase(base.Base, metaclass=abc.ABCMeta):
|
||||
:param args: parameters as a dictionary, changing it here will change
|
||||
what will be stored in database
|
||||
:param kwargs: used for extensibility without breaking existing plugins
|
||||
:raises ValueError: on unacceptable field value
|
||||
:raises InspectionRuleExecutionFailure: on unacceptable field value
|
||||
:returns: True if check succeeded, otherwise False
|
||||
"""
|
||||
op, is_inverted = common_utils.parse_inverted_operator(
|
||||
op, is_inverted = utils.parse_inverted_operator(
|
||||
condition['op'])
|
||||
|
||||
processed_args = self._process_args(task, condition, inventory,
|
||||
plugin_data)
|
||||
arg_values = [processed_args[arg_name]
|
||||
for arg_name in self.get_arg_names()]
|
||||
|
||||
for optional_arg in self.OPTIONAL_ARGS:
|
||||
arg_values.append(processed_args.get(optional_arg, False))
|
||||
|
||||
result = self.check(*arg_values)
|
||||
result = self(task, **processed_args)
|
||||
return not result if is_inverted else result
|
||||
|
||||
|
||||
class SimpleOperator(OperatorBase):
|
||||
|
||||
op = None
|
||||
OPTIONAL_ARGS = {'force_strings'}
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['values']
|
||||
def __call__(self, task, values, force_strings=False):
|
||||
if not isinstance(values, list):
|
||||
msg = _("Failed to check condition: '%(op)s' on values: "
|
||||
"%(values)s: Expected list for 'values', got: "
|
||||
"%(invalid_type)s") % {
|
||||
'op': self.op.__name__, 'values': values,
|
||||
"invalid_type": type(values).__name__}
|
||||
LOG.error(msg)
|
||||
raise exception.RuleConditionCheckFailure(reason=msg)
|
||||
|
||||
if len(values) < 2:
|
||||
return True
|
||||
|
||||
def check(self, values, force_strings=False):
|
||||
if force_strings:
|
||||
values = [coerce(value, str) for value in values]
|
||||
return self.op(values)
|
||||
|
||||
return all(self.op(values[i], values[i + 1])
|
||||
for i in range(len(values) - 1))
|
||||
|
||||
|
||||
class EqOperator(SimpleOperator):
|
||||
@@ -145,42 +144,31 @@ class GtOperator(SimpleOperator):
|
||||
|
||||
|
||||
class EmptyOperator(OperatorBase):
|
||||
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['value']
|
||||
|
||||
def check(self, value):
|
||||
def __call__(self, task, value):
|
||||
return str(value) in ("", 'None', '[]', '{}')
|
||||
|
||||
|
||||
class NetOperator(OperatorBase):
|
||||
|
||||
FORMATTED_ARGS = ['address', 'subnet']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['address', 'subnet']
|
||||
|
||||
def validate(self, address, subnet):
|
||||
def __call__(self, task, address, subnet):
|
||||
try:
|
||||
netaddr.IPNetwork(subnet)
|
||||
network = netaddr.IPNetwork(subnet)
|
||||
except netaddr.AddrFormatError as exc:
|
||||
LOG.error(_('invalid value: %s'), exc)
|
||||
|
||||
def check(self, address, subnet):
|
||||
network = netaddr.IPNetwork(subnet)
|
||||
raise exception.InspectionRuleExecutionFailure(
|
||||
_('invalid value: %s') % exc)
|
||||
return netaddr.IPAddress(address) in network
|
||||
|
||||
|
||||
class IsTrueOperator(OperatorBase):
|
||||
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['value']
|
||||
|
||||
def check(self, value):
|
||||
def __call__(self, task, value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
@@ -191,13 +179,10 @@ class IsTrueOperator(OperatorBase):
|
||||
|
||||
|
||||
class IsFalseOperator(OperatorBase):
|
||||
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['value']
|
||||
|
||||
def check(self, value):
|
||||
def __call__(self, task, value):
|
||||
if isinstance(value, bool):
|
||||
return not value
|
||||
if isinstance(value, (int, float)):
|
||||
@@ -208,44 +193,36 @@ class IsFalseOperator(OperatorBase):
|
||||
|
||||
|
||||
class IsNoneOperator(OperatorBase):
|
||||
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['value']
|
||||
|
||||
def check(self, value):
|
||||
def __call__(self, task, value):
|
||||
return str(value) == 'None'
|
||||
|
||||
|
||||
class OneOfOperator(OperatorBase):
|
||||
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['value', 'values']
|
||||
|
||||
def check(self, value, values=[]):
|
||||
def __call__(self, task, value, values=[]):
|
||||
return value in values
|
||||
|
||||
|
||||
class ReOperator(OperatorBase):
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
@classmethod
|
||||
def get_arg_names(cls):
|
||||
return ['value', 'regex']
|
||||
FORMATTED_ARGS = ['value']
|
||||
|
||||
def validate_regex(self, regex):
|
||||
try:
|
||||
re.compile(regex)
|
||||
except re.error as exc:
|
||||
raise ValueError(_('invalid regular expression: %s') % exc)
|
||||
raise exception.InspectionRuleExecutionFailure(
|
||||
_('invalid regular expression: %s') % exc)
|
||||
|
||||
|
||||
class MatchesOperator(ReOperator):
|
||||
|
||||
def check(self, value, regex):
|
||||
def __call__(self, task, value, regex):
|
||||
self.validate_regex(regex)
|
||||
if regex[-1] != '$':
|
||||
regex += '$'
|
||||
@@ -254,6 +231,6 @@ class MatchesOperator(ReOperator):
|
||||
|
||||
class ContainsOperator(ReOperator):
|
||||
|
||||
def check(self, value, regex):
|
||||
def __call__(self, task, value, regex):
|
||||
self.validate_regex(regex)
|
||||
return re.search(regex, str(value)) is not None
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# 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.
|
||||
|
||||
"""Utilities and helper functions for rules."""
|
||||
|
||||
from collections import abc
|
||||
|
||||
from ironic.common.i18n import _
|
||||
|
||||
|
||||
class ShallowMaskList(abc.MutableSequence):
|
||||
"""A proxy list to maintain original list and applies masking on the fly.
|
||||
|
||||
This class implements the MutableSequence ABC to provide a complete
|
||||
list-like interface while handling sensitive data masking consistently
|
||||
with ShallowMaskDict.
|
||||
"""
|
||||
def __init__(self, original_list, sensitive_fields=None,
|
||||
mask_enabled=True):
|
||||
self._original = original_list
|
||||
self._sensitive_fields = sensitive_fields or []
|
||||
self._mask_enabled = mask_enabled
|
||||
|
||||
def _mask_value(self, value):
|
||||
"""Apply masking to value on demand."""
|
||||
if isinstance(value, dict):
|
||||
return ShallowMaskDict(
|
||||
value,
|
||||
sensitive_fields=self._sensitive_fields,
|
||||
mask_enabled=self._mask_enabled
|
||||
)
|
||||
elif isinstance(value, list):
|
||||
return ShallowMaskList(
|
||||
value,
|
||||
sensitive_fields=self._sensitive_fields,
|
||||
mask_enabled=self._mask_enabled
|
||||
)
|
||||
return value
|
||||
|
||||
def __getitem__(self, index):
|
||||
value = self._original[index]
|
||||
return self._mask_value(value)
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
self._original[index] = value
|
||||
|
||||
def __delitem__(self, index):
|
||||
del self._original[index]
|
||||
|
||||
def __iter__(self):
|
||||
for item in self._original:
|
||||
yield self._mask_value(item)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._original)
|
||||
|
||||
def insert(self, index, value):
|
||||
self._original.insert(index, value)
|
||||
|
||||
def copy(self):
|
||||
return ShallowMaskList(
|
||||
self._original.copy(),
|
||||
sensitive_fields=self._sensitive_fields,
|
||||
mask_enabled=self._mask_enabled
|
||||
)
|
||||
|
||||
def set_mask_enabled(self, mask_enabled):
|
||||
self._mask_enabled = mask_enabled
|
||||
|
||||
def __repr__(self):
|
||||
items = [repr(self._mask_value(item)) for item in self._original]
|
||||
return "[%s]" % ", ".join(items)
|
||||
|
||||
|
||||
class ShallowMaskDict(abc.MutableMapping):
|
||||
"""Dictionary wrapper to mask sensitive fields on the fly.
|
||||
|
||||
This class implements the MutableMapping ABC to provide a complete
|
||||
dict-like interface while masking sensitive fields when accessed.
|
||||
"""
|
||||
def __init__(self, data, sensitive_fields=None, mask_enabled=True):
|
||||
self._data = data
|
||||
self._sensitive_fields = sensitive_fields or []
|
||||
self._mask_enabled = mask_enabled
|
||||
|
||||
def _mask_value(self, key, value):
|
||||
if self._mask_enabled and key in self._sensitive_fields:
|
||||
return '***'
|
||||
|
||||
if isinstance(value, dict):
|
||||
return ShallowMaskDict(
|
||||
value,
|
||||
sensitive_fields=self._sensitive_fields,
|
||||
mask_enabled=self._mask_enabled
|
||||
)
|
||||
elif isinstance(value, list):
|
||||
return ShallowMaskList(
|
||||
value,
|
||||
sensitive_fields=self._sensitive_fields,
|
||||
mask_enabled=self._mask_enabled
|
||||
)
|
||||
return value
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = self._data[key]
|
||||
return self._mask_value(key, value)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._data[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._data[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def copy(self):
|
||||
return ShallowMaskDict(
|
||||
self._data.copy(),
|
||||
sensitive_fields=self._sensitive_fields,
|
||||
mask_enabled=self._mask_enabled,
|
||||
)
|
||||
|
||||
def set_mask_enabled(self, mask_enabled):
|
||||
self._mask_enabled = mask_enabled
|
||||
|
||||
def __repr__(self):
|
||||
items = ["%s: %s" % (repr(k), repr(self._mask_value(k, v)))
|
||||
for k, v in self._data.items()]
|
||||
return "{%s}" % ", ".join(items)
|
||||
|
||||
|
||||
def parse_inverted_operator(op):
|
||||
"""Handle inverted operators.
|
||||
|
||||
Parses a logical condition operator to determine if it has been negated
|
||||
using a single leading exclamation mark ('!'). Ensures only one
|
||||
exclamation mark is allowed.
|
||||
|
||||
Example Usage:
|
||||
parse_inverted_operator("!eq") # Returns ("eq", True)
|
||||
parse_inverted_operator(" eq ") # Returns ("eq", False)
|
||||
parse_inverted_operator("!!eq") # Raises ValueError
|
||||
|
||||
raises: ValueError: If multiple exclamation marks are present
|
||||
returns: A tuple containing the cleaned operator and a
|
||||
boolean indicating whether negation was applied.
|
||||
"""
|
||||
op = op.strip()
|
||||
|
||||
if op.count('!') > 1:
|
||||
msg = _("Multiple exclamation marks are not allowed. "
|
||||
"To apply the invert of an operation, simply add an "
|
||||
"exclamation mark (with an optional space) before "
|
||||
"the operator, e.g. eq - !eq.")
|
||||
raise ValueError(msg)
|
||||
|
||||
is_inverted = op.startswith('!')
|
||||
op = op.lstrip('!').strip()
|
||||
return op, is_inverted
|
||||
|
||||
|
||||
def normalize_path(path):
|
||||
"""Convert a path (dot or slash notation) to a list of path parts"""
|
||||
if '/' in path:
|
||||
parts = path.strip('/').split('/')
|
||||
else:
|
||||
parts = path.split('.')
|
||||
return parts
|
||||
@@ -1,6 +1,3 @@
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
@@ -22,7 +19,7 @@ from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common.inspection_rules import actions
|
||||
from ironic.common.inspection_rules import operators
|
||||
from ironic.common import utils as common_utils
|
||||
from ironic.common.inspection_rules import utils
|
||||
|
||||
|
||||
_CONDITIONS_SCHEMA = None
|
||||
@@ -125,7 +122,7 @@ VALIDATOR = args.and_valid(
|
||||
)
|
||||
|
||||
|
||||
def validate_inspection_rule(rule):
|
||||
def validate_rule(rule):
|
||||
"""Validate an inspection rule using the JSON schema.
|
||||
|
||||
:param rule: The inspection rule to validate.
|
||||
@@ -158,14 +155,14 @@ def validate_inspection_rule(rule):
|
||||
|
||||
# Additional plugin-specific validation
|
||||
for condition in rule.get('conditions', []):
|
||||
op, invtd = common_utils.parse_inverted_operator(
|
||||
op, invtd = utils.parse_inverted_operator(
|
||||
condition['op'])
|
||||
plugin = operators.get_operator(op)
|
||||
if not plugin or not callable(plugin):
|
||||
errors.append(
|
||||
_('Unsupported condition operator: %s') % op)
|
||||
try:
|
||||
plugin().validate(**condition)
|
||||
plugin().validate(condition.get('args', {}))
|
||||
except ValueError as exc:
|
||||
errors.append(_('Invalid parameters for condition operator '
|
||||
'%(op)s: %(error)s') % {'op': op,
|
||||
@@ -176,7 +173,7 @@ def validate_inspection_rule(rule):
|
||||
if not plugin or not callable(plugin):
|
||||
errors.append(_('Unsupported action operator: %s') % action['op'])
|
||||
try:
|
||||
plugin().validate(**action)
|
||||
plugin().validate(action.get('args', {}))
|
||||
except ValueError as exc:
|
||||
errors.append(_('Invalid parameters for action operator %(op)s: '
|
||||
'%(error)s') % {'op': action['op'], 'error': exc})
|
||||
|
||||
@@ -1147,18 +1147,3 @@ def get_route_source(dest, ignore_link_local=True):
|
||||
except (IndexError, ValueError):
|
||||
LOG.debug('No route to host %(dest)s, route record: %(rec)s',
|
||||
{'dest': dest, 'rec': out})
|
||||
|
||||
|
||||
def parse_inverted_operator(op):
|
||||
"""Handle inverted operators."""
|
||||
op = op.strip()
|
||||
if op.count('!') > 1:
|
||||
msg = _("Multiple exclamation marks are not allowed. "
|
||||
"To apply the invert of an operation, simply put an "
|
||||
"exclamation mark (with an optional space) before "
|
||||
"the op, e.g. eq - !eq.")
|
||||
raise ValueError(msg)
|
||||
|
||||
is_inverted = op.startswith('!')
|
||||
op = op.lstrip('!').strip()
|
||||
return op, is_inverted
|
||||
|
||||
@@ -132,9 +132,7 @@ def continue_inspection(task, inventory, plugin_data):
|
||||
'asynchronously for node %s', node.uuid)
|
||||
return
|
||||
|
||||
result = engine.apply_rules(task, inventory, plugin_data, 'main')
|
||||
if result and 'plugin_data' in result:
|
||||
plugin_data = result['plugin_data']
|
||||
engine.apply_rules(task, inventory, plugin_data, 'main')
|
||||
|
||||
# NOTE(dtantsur): logs can be huge and are stored separately
|
||||
plugin_data.pop('logs', None)
|
||||
|
||||
@@ -185,6 +185,27 @@ def add_node_capability(task, capability, value):
|
||||
node.save()
|
||||
|
||||
|
||||
def remove_node_capability(task, name):
|
||||
"""Remove 'capability' from node's 'capabilities' property.
|
||||
|
||||
If 'capability' is empty, do nothing.
|
||||
|
||||
:param task: Task object.
|
||||
:param capability: Capability key.
|
||||
|
||||
"""
|
||||
node = task.node
|
||||
properties = node.properties
|
||||
capabilities = properties.get('capabilities', '').split(',')
|
||||
if not capabilities:
|
||||
return
|
||||
updated_capabilities = [cap for cap in capabilities
|
||||
if not cap.startswith("%s:" % name)]
|
||||
properties['capabilities'] = ','.join(updated_capabilities or [])
|
||||
node.properties = properties
|
||||
node.save()
|
||||
|
||||
|
||||
def ensure_next_boot_device(task, driver_info):
|
||||
"""Ensure boot from correct device if persistent is True
|
||||
|
||||
|
||||
@@ -0,0 +1,784 @@
|
||||
# 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 unittest import mock
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import inspection_rules
|
||||
from ironic.common.inspection_rules import engine
|
||||
from ironic.common.inspection_rules import utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.tests.unit.db import base as db_base
|
||||
from ironic.tests.unit.objects import utils as obj_utils
|
||||
|
||||
|
||||
class TestApplyRules(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestApplyRules, self).setUp()
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake-hardware')
|
||||
self.sensitive_fields = ['password', 'auth_token', 'bmc_password']
|
||||
self.test_data = {
|
||||
'username': 'testuser',
|
||||
'password': 'secret123',
|
||||
'nested': {
|
||||
'normal': 'value',
|
||||
'password': 'nested_secret'
|
||||
},
|
||||
'list_data': [
|
||||
{'name': 'item1', 'password': 'item1_secret'},
|
||||
{'name': 'item2', 'normal': 'value2'}
|
||||
],
|
||||
'auth_token': 'abc123token'
|
||||
}
|
||||
|
||||
self.inventory = {
|
||||
'cpu': {'count': 4, 'architecture': 'x86_64'},
|
||||
'memory': {'total': 8192, 'physical_mb': 8192},
|
||||
'interfaces': [
|
||||
{'name': 'eth0', 'mac_address': '2a:03:9c:53:4e:46'},
|
||||
{'name': 'eth1', 'mac_address': 'a2:67:c1:b8:c1:bd'}
|
||||
],
|
||||
'disks': [
|
||||
{'name': '/dev/sda', 'size': 1000000, 'model': 'test-disk-1'},
|
||||
{'name': '/dev/sdb', 'size': 2000000, 'model': 'test-disk-2'}
|
||||
],
|
||||
'bmc_address': '192.168.1.100',
|
||||
'bmc_password': 'secret'
|
||||
}
|
||||
self.plugin_data = {"plugin": "data", "logs": "test logs",
|
||||
"password": "plugin_secret"}
|
||||
|
||||
self.rule1 = obj_utils.create_test_inspection_rule(self.context)
|
||||
self.rule2 = obj_utils.create_test_inspection_rule(self.context)
|
||||
self.sensitive_rule = obj_utils.create_test_inspection_rule(
|
||||
self.context, sensitive=True)
|
||||
|
||||
def test_set_mask_enabled(self):
|
||||
"""Test that set_mask_enabled properly toggles masking."""
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
self.test_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=True)
|
||||
|
||||
self.assertEqual('***', masked_dict['password'])
|
||||
|
||||
masked_dict.set_mask_enabled(False)
|
||||
self.assertEqual('secret123', masked_dict['password'])
|
||||
|
||||
masked_dict.set_mask_enabled(True)
|
||||
self.assertEqual('***', masked_dict['password'])
|
||||
|
||||
def test_getitem_masked(self):
|
||||
"""Test that __getitem__ masks sensitive fields."""
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
self.test_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=True)
|
||||
|
||||
self.assertEqual('***', masked_dict['password'])
|
||||
self.assertEqual('***', masked_dict['auth_token'])
|
||||
|
||||
self.assertEqual('testuser', masked_dict['username'])
|
||||
|
||||
def test_getitem_not_masked(self):
|
||||
"""Test that __getitem__ doesn't mask when disabled."""
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
self.test_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=False)
|
||||
|
||||
self.assertEqual('secret123', masked_dict['password'])
|
||||
self.assertEqual('abc123token', masked_dict['auth_token'])
|
||||
|
||||
def test_nested_dict_masking(self):
|
||||
"""Test that nested dictionaries are properly masked."""
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
self.test_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=True)
|
||||
|
||||
nested = masked_dict['nested']
|
||||
self.assertIsInstance(nested, utils.ShallowMaskDict)
|
||||
self.assertEqual('***', nested['password'])
|
||||
|
||||
self.assertEqual('value', nested['normal'])
|
||||
|
||||
def test_list_masking(self):
|
||||
"""Test that lists containing dictionaries are properly masked."""
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
self.test_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=True)
|
||||
|
||||
list_data = masked_dict['list_data']
|
||||
self.assertEqual('***', list_data[0]['password'])
|
||||
|
||||
self.assertEqual('item1', list_data[0]['name'])
|
||||
self.assertEqual('value2', list_data[1]['normal'])
|
||||
|
||||
def test_items_masked(self):
|
||||
"""Test that items() method returns masked values."""
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
self.test_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=True)
|
||||
|
||||
items = dict(masked_dict.items())
|
||||
|
||||
self.assertEqual('***', items['password'])
|
||||
self.assertEqual('***', items['auth_token'])
|
||||
self.assertEqual('***', items['nested']['password'])
|
||||
|
||||
def test_values_masked(self):
|
||||
"""Test that values() method masks sensitive values."""
|
||||
test_data = {'username': 'user', 'password': 'secret'}
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
test_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=True)
|
||||
|
||||
values = list(masked_dict.values())
|
||||
|
||||
self.assertIn('user', values)
|
||||
self.assertIn('***', values)
|
||||
self.assertNotIn('secret', values)
|
||||
|
||||
def test_get_method_masked(self):
|
||||
"""Test that the get() method properly masks sensitive fields."""
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
self.test_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=True)
|
||||
|
||||
self.assertEqual('***', masked_dict.get('password'))
|
||||
self.assertEqual('testuser', masked_dict.get('username'))
|
||||
|
||||
# Non-existent field should return default
|
||||
self.assertIsNone(masked_dict.get('nonexistent'))
|
||||
self.assertEqual('default', masked_dict.get('nonexistent', 'default'))
|
||||
|
||||
def test_modifying_dict(self):
|
||||
"""Test that modifications affect the original data."""
|
||||
original_data = {'username': 'user', 'data': [1, 2, 3]}
|
||||
masked_dict = utils.ShallowMaskDict(
|
||||
original_data, sensitive_fields=self.sensitive_fields,
|
||||
mask_enabled=True)
|
||||
|
||||
masked_dict['new_key'] = 'new_value'
|
||||
masked_dict['data'].append(4)
|
||||
|
||||
self.assertEqual('new_value', original_data['new_key'])
|
||||
self.assertEqual([1, 2, 3, 4], original_data['data'])
|
||||
|
||||
@mock.patch.object(engine, 'get_built_in_rules', autospec=True)
|
||||
@mock.patch.object(engine, 'check_conditions', autospec=True)
|
||||
@mock.patch.object(engine, 'apply_actions', autospec=True)
|
||||
@mock.patch('ironic.objects.InspectionRule.list', autospec=True)
|
||||
def test_apply_rules_no_rules(self, mock_list, mock_apply_actions,
|
||||
mock_check_conditions, mock_get_built_in):
|
||||
mock_list.return_value = []
|
||||
mock_get_built_in.return_value = []
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = engine.apply_rules(task, self.inventory,
|
||||
self.plugin_data, 'main')
|
||||
|
||||
mock_list.assert_called_once_with(
|
||||
context=self.context,
|
||||
filters={'phase': 'main'})
|
||||
mock_get_built_in.assert_called_once()
|
||||
|
||||
mock_check_conditions.assert_not_called()
|
||||
mock_apply_actions.assert_not_called()
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch.object(engine, 'get_built_in_rules', autospec=True)
|
||||
@mock.patch.object(engine, 'check_conditions', autospec=True)
|
||||
@mock.patch.object(engine, 'apply_actions', autospec=True)
|
||||
@mock.patch('ironic.objects.InspectionRule.list', autospec=True)
|
||||
def test_apply_rules_success(self, mock_list, mock_apply_actions,
|
||||
mock_check_conditions, mock_get_built_in):
|
||||
rule1 = {'uuid': 'rule-1', 'priority': 100, 'conditions': [],
|
||||
'actions': [{'op': 'set-attribute',
|
||||
'args': {'path': 'a', 'value': 'b'}}]}
|
||||
rule2 = {'uuid': 'rule-2', 'priority': 50, 'conditions': [],
|
||||
'actions': [
|
||||
{'op': 'set-capability',
|
||||
'args': {'name': 'boot_mode', 'value': 'uefi'}}]}
|
||||
mock_list.return_value = [rule1]
|
||||
mock_get_built_in.return_value = [rule2]
|
||||
mock_check_conditions.return_value = True
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
engine.apply_rules(task, self.inventory, self.plugin_data, 'main')
|
||||
|
||||
mock_get_built_in.assert_called_once()
|
||||
mock_list.assert_called_once_with(
|
||||
context=self.context,
|
||||
filters={'phase': 'main'})
|
||||
|
||||
@mock.patch.object(engine, 'get_built_in_rules', autospec=True)
|
||||
@mock.patch.object(engine, 'check_conditions', autospec=True)
|
||||
@mock.patch.object(engine, 'apply_actions', autospec=True)
|
||||
@mock.patch('ironic.objects.InspectionRule.list', autospec=True)
|
||||
def test_apply_rules_condition_false(self, mock_list, mock_apply_actions,
|
||||
mock_check_conditions,
|
||||
mock_get_built_in):
|
||||
"""Test that rules are skipped when conditions don't match."""
|
||||
mock_list.return_value = [self.rule1]
|
||||
mock_get_built_in.return_value = [self.rule2]
|
||||
|
||||
mock_check_conditions.side_effect = [False, True]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
engine.apply_rules(task, self.inventory, self.plugin_data, 'main')
|
||||
|
||||
self.assertEqual(2, mock_check_conditions.call_count)
|
||||
|
||||
@mock.patch.object(engine, 'LOG', autospec=True)
|
||||
@mock.patch.object(engine, 'get_built_in_rules', autospec=True)
|
||||
@mock.patch.object(engine, 'check_conditions', autospec=True)
|
||||
@mock.patch.object(engine, 'apply_actions', autospec=True)
|
||||
@mock.patch('ironic.objects.InspectionRule.list', autospec=True)
|
||||
def test_apply_rules_ironic_exception(self, mock_list,
|
||||
mock_apply_actions,
|
||||
mock_check_conditions,
|
||||
mock_get_built_in,
|
||||
mock_log):
|
||||
"""Test that IronicException is re-raised."""
|
||||
mock_list.return_value = [self.rule1, self.rule2]
|
||||
mock_get_built_in.return_value = []
|
||||
mock_check_conditions.return_value = True
|
||||
|
||||
mock_apply_actions.side_effect = [
|
||||
exception.IronicException("Expected error"),
|
||||
{'plugin_data': {'updated': 'data'}}
|
||||
]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaises(exception.IronicException,
|
||||
engine.apply_rules, task, self.inventory,
|
||||
self.plugin_data, 'main')
|
||||
|
||||
mock_log.error.assert_called_once()
|
||||
|
||||
self.assertEqual(1, mock_apply_actions.call_count)
|
||||
|
||||
@mock.patch.object(utils, 'ShallowMaskDict', autospec=True)
|
||||
@mock.patch.object(engine, 'get_built_in_rules', autospec=True)
|
||||
@mock.patch.object(engine, 'check_conditions', autospec=True)
|
||||
@mock.patch.object(engine, 'apply_actions', autospec=True)
|
||||
@mock.patch('ironic.objects.InspectionRule.list', autospec=True)
|
||||
def test_apply_rules_with_always_mask(self, mock_list, mock_apply_actions,
|
||||
mock_check_conditions,
|
||||
mock_get_built_in,
|
||||
mock_masked_dict):
|
||||
"""Test apply_rules with mask_secrets='always'."""
|
||||
self.config(mask_secrets='always', group='inspection_rules')
|
||||
|
||||
mock_list.return_value = [self.rule1]
|
||||
mock_get_built_in.return_value = [self.rule2]
|
||||
mock_check_conditions.return_value = True
|
||||
|
||||
masked_inventory = mock.MagicMock()
|
||||
masked_plugin_data = mock.MagicMock()
|
||||
mock_masked_dict.side_effect = [masked_inventory, masked_plugin_data,
|
||||
mock.MagicMock(), mock.MagicMock()]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
engine.apply_rules(task, self.inventory, self.plugin_data, 'main')
|
||||
|
||||
mock_masked_dict.assert_has_calls([
|
||||
mock.call(self.inventory,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=True),
|
||||
mock.call(self.plugin_data,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=True),
|
||||
mock.call(self.inventory,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=True),
|
||||
mock.call(self.plugin_data,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=True)
|
||||
])
|
||||
|
||||
@mock.patch.object(utils, 'ShallowMaskDict', autospec=True)
|
||||
@mock.patch.object(engine, 'get_built_in_rules', autospec=True)
|
||||
@mock.patch.object(engine, 'check_conditions', autospec=True)
|
||||
@mock.patch.object(engine, 'apply_actions', autospec=True)
|
||||
@mock.patch('ironic.objects.InspectionRule.list', autospec=True)
|
||||
def test_apply_rules_with_never_mask(self, mock_list, mock_apply_actions,
|
||||
mock_check_conditions,
|
||||
mock_get_built_in, mock_masked_dict):
|
||||
"""Test apply_rules with mask_secrets='never'."""
|
||||
self.config(mask_secrets='never', group='inspection_rules')
|
||||
|
||||
mock_list.return_value = [self.rule1]
|
||||
mock_get_built_in.return_value = [self.rule2]
|
||||
mock_check_conditions.return_value = True
|
||||
|
||||
masked_inventory = mock.MagicMock()
|
||||
masked_plugin_data = mock.MagicMock()
|
||||
mock_masked_dict.side_effect = [masked_inventory, masked_plugin_data,
|
||||
mock.MagicMock(), mock.MagicMock()]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
engine.apply_rules(task, self.inventory, self.plugin_data, 'main')
|
||||
|
||||
mock_masked_dict.assert_has_calls([
|
||||
mock.call(self.inventory,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=False),
|
||||
mock.call(self.plugin_data,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=False),
|
||||
mock.call(self.inventory,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=False),
|
||||
mock.call(self.plugin_data,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=False)
|
||||
])
|
||||
|
||||
@mock.patch.object(utils, 'ShallowMaskDict', autospec=True)
|
||||
@mock.patch.object(engine, 'get_built_in_rules', autospec=True)
|
||||
@mock.patch.object(engine, 'check_conditions', autospec=True)
|
||||
@mock.patch.object(engine, 'apply_actions', autospec=True)
|
||||
@mock.patch('ironic.objects.InspectionRule.list', autospec=True)
|
||||
def test_apply_rules_with_sensitive_mask(self, mock_list,
|
||||
mock_apply_actions,
|
||||
mock_check_conditions,
|
||||
mock_get_built_in,
|
||||
mock_masked_dict):
|
||||
"""Test apply_rules with mask_secrets='sensitive'."""
|
||||
self.config(mask_secrets='sensitive', group='inspection_rules')
|
||||
|
||||
mock_list.return_value = [self.rule1, self.sensitive_rule]
|
||||
mock_get_built_in.return_value = []
|
||||
mock_check_conditions.return_value = True
|
||||
|
||||
masked_inventory1 = mock.MagicMock()
|
||||
masked_plugin_data1 = mock.MagicMock()
|
||||
masked_inventory2 = mock.MagicMock()
|
||||
masked_plugin_data2 = mock.MagicMock()
|
||||
mock_masked_dict.side_effect = [
|
||||
masked_inventory1, masked_plugin_data1,
|
||||
masked_inventory2, masked_plugin_data2
|
||||
]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
engine.apply_rules(task, self.inventory, self.plugin_data, 'main')
|
||||
|
||||
mock_masked_dict.assert_has_calls([
|
||||
mock.call(self.inventory,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=True),
|
||||
mock.call(self.plugin_data,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=True),
|
||||
mock.call(self.inventory,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=False),
|
||||
mock.call(self.plugin_data,
|
||||
sensitive_fields=engine.SENSITIVE_FIELDS,
|
||||
mask_enabled=False)
|
||||
])
|
||||
|
||||
|
||||
class TestOperators(db_base.DbTestCase):
|
||||
def setUp(self):
|
||||
super(TestOperators, self).setUp()
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake-hardware')
|
||||
|
||||
def test_operator_exceptions(self):
|
||||
"""Test that operators raise proper exceptions for invalid inputs."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
# NetOperator with invalid subnet
|
||||
net_op = inspection_rules.operators.NetOperator()
|
||||
self.assertRaises(
|
||||
exception.InspectionRuleExecutionFailure,
|
||||
net_op, task, address='192.168.1.1', subnet='invalid-subnet'
|
||||
)
|
||||
|
||||
# MatchesOperator with invalid regex
|
||||
matches_op = inspection_rules.operators.MatchesOperator()
|
||||
self.assertRaises(
|
||||
exception.InspectionRuleExecutionFailure,
|
||||
matches_op, task, value='test', regex='[unclosed'
|
||||
)
|
||||
|
||||
# ContainsOperator with invalid regex
|
||||
contains_op = inspection_rules.operators.ContainsOperator()
|
||||
self.assertRaises(
|
||||
exception.InspectionRuleExecutionFailure,
|
||||
contains_op, task, value='test', regex='[unclosed'
|
||||
)
|
||||
|
||||
# SimpleOperator with non-list values
|
||||
eq_op = inspection_rules.operators.EqOperator()
|
||||
self.assertRaises(
|
||||
exception.RuleConditionCheckFailure,
|
||||
eq_op, task, values="not-a-list"
|
||||
)
|
||||
|
||||
def test_oneofoperator_edge_cases(self):
|
||||
"""Test OneOfOperator with edge cases."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
op = inspection_rules.operators.OneOfOperator()
|
||||
|
||||
self.assertFalse(op(task, value='test', values=[]))
|
||||
self.assertFalse(op(task, value=None, values=['a', 'b']))
|
||||
self.assertTrue(op(task, value='a', values=['a', 'b']))
|
||||
|
||||
def test_is_true_false_operators_edge_cases(self):
|
||||
"""Test IsTrueOperator and IsFalseOperator."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
true_op = inspection_rules.operators.IsTrueOperator()
|
||||
false_op = inspection_rules.operators.IsFalseOperator()
|
||||
|
||||
self.assertTrue(true_op(task, value='yes'))
|
||||
self.assertTrue(true_op(task, value='TRUE'))
|
||||
self.assertFalse(true_op(task, value='no'))
|
||||
|
||||
self.assertTrue(false_op(task, value='no'))
|
||||
self.assertTrue(false_op(task, value='FALSE'))
|
||||
self.assertFalse(false_op(task, value='yes'))
|
||||
|
||||
self.assertTrue(true_op(task, value=1))
|
||||
self.assertTrue(true_op(task, value=0.1))
|
||||
self.assertFalse(true_op(task, value=0))
|
||||
|
||||
self.assertTrue(false_op(task, value=0))
|
||||
self.assertFalse(false_op(task, value=1))
|
||||
|
||||
self.assertFalse(true_op(task, value=None))
|
||||
self.assertTrue(false_op(task, value=None))
|
||||
|
||||
self.assertFalse(true_op(task, value={}))
|
||||
self.assertFalse(true_op(task, value=[]))
|
||||
|
||||
def test_operator_with_loop(self):
|
||||
"""Test operator check_with_loop method."""
|
||||
condition = {
|
||||
'op': 'eq',
|
||||
'loop': [
|
||||
{'values': [1, 1]},
|
||||
{'values': [2, 2]},
|
||||
{'values': [3, 4]}
|
||||
],
|
||||
'multiple': 'any'
|
||||
}
|
||||
|
||||
inventory = {'data': 'test'}
|
||||
plugin_data = {'plugin': 'data'}
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
op = inspection_rules.operators.EqOperator()
|
||||
|
||||
# 'any' multiple (should return True)
|
||||
self.assertTrue(op.check_with_loop(task, condition, inventory,
|
||||
plugin_data))
|
||||
|
||||
# 'all' multiple (should return False)
|
||||
condition['multiple'] = 'all'
|
||||
self.assertFalse(op.check_with_loop(task, condition, inventory,
|
||||
plugin_data))
|
||||
|
||||
# 'first' multiple (should return True)
|
||||
condition['multiple'] = 'first'
|
||||
self.assertTrue(op.check_with_loop(task, condition, inventory,
|
||||
plugin_data))
|
||||
|
||||
# 'last' multiple (should return False)
|
||||
condition['multiple'] = 'last'
|
||||
self.assertFalse(op.check_with_loop(task, condition, inventory,
|
||||
plugin_data))
|
||||
|
||||
def test_rule_operators(self):
|
||||
"""Test all inspection_rules.operators with True and False cases."""
|
||||
operator_tests = {
|
||||
inspection_rules.operators.EqOperator: [
|
||||
{'values': [5, 5]},
|
||||
{'values': [5, 10]}
|
||||
],
|
||||
inspection_rules.operators.LtOperator: [
|
||||
{'values': [5, 10]},
|
||||
{'values': [10, 5]}
|
||||
],
|
||||
inspection_rules.operators.GtOperator: [
|
||||
{'values': [10, 5]},
|
||||
{'values': [5, 10]}
|
||||
],
|
||||
inspection_rules.operators.EmptyOperator: [
|
||||
{'value': ''},
|
||||
{'value': 'not empty'}
|
||||
],
|
||||
inspection_rules.operators.NetOperator: [
|
||||
{'address': '192.168.1.5', 'subnet': '192.168.1.0/24'},
|
||||
{'address': '10.0.0.1', 'subnet': '192.168.1.0/24'}
|
||||
],
|
||||
inspection_rules.operators.MatchesOperator: [
|
||||
{'value': 'abc123', 'regex': r'abc\d+'},
|
||||
{'value': 'xyz123', 'regex': r'abc\d+'}
|
||||
],
|
||||
inspection_rules.operators.ContainsOperator: [
|
||||
{'value': 'test-abc123-end', 'regex': r'abc\d+'},
|
||||
{'value': 'test-xyz-end', 'regex': r'abc\d+'}
|
||||
],
|
||||
inspection_rules.operators.OneOfOperator: [
|
||||
{'value': 'b', 'values': ['a', 'b', 'c']},
|
||||
{'value': 'z', 'values': ['a', 'b', 'c']}
|
||||
],
|
||||
inspection_rules.operators.IsNoneOperator: [
|
||||
{'value': 'None'},
|
||||
{'value': 'something'}
|
||||
],
|
||||
inspection_rules.operators.IsTrueOperator: [
|
||||
{'value': True},
|
||||
{'value': False}
|
||||
],
|
||||
inspection_rules.operators.IsFalseOperator: [
|
||||
{'value': False},
|
||||
{'value': True}
|
||||
]
|
||||
}
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
for op_class, test_cases in operator_tests.items():
|
||||
op = op_class()
|
||||
result = op(task, **test_cases[0])
|
||||
self.assertTrue(result)
|
||||
|
||||
result = op(task, **test_cases[1])
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
class TestActions(db_base.DbTestCase):
|
||||
"""Test inspection rule actions"""
|
||||
def setUp(self):
|
||||
super(TestActions, self).setUp()
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake-hardware')
|
||||
|
||||
@mock.patch.object(inspection_rules.actions.LOG, 'info', autospec=True)
|
||||
def test_log_action(self, mock_log):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
action = inspection_rules.actions.LogAction()
|
||||
test_msg = "Test log message"
|
||||
action(task, msg=test_msg)
|
||||
mock_log.assert_called_once_with(test_msg)
|
||||
|
||||
def test_fail_action(self):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
action = inspection_rules.actions.FailAction()
|
||||
error_msg = "Test failure"
|
||||
self.assertRaises(exception.HardwareInspectionFailure,
|
||||
action, task, msg=error_msg)
|
||||
|
||||
def test_set_attribute_action(self):
|
||||
"""Test SetAttributeAction sets node attribute."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
action = inspection_rules.actions.SetAttributeAction()
|
||||
action(task, path='extra', value={'test_key': 'test_value'})
|
||||
self.assertEqual({'test_key': 'test_value'}, task.node.extra)
|
||||
|
||||
def test_extend_attribute_action(self):
|
||||
"""Test ExtendAttributeAction extends a list attribute."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
action = inspection_rules.actions.ExtendAttributeAction()
|
||||
task.node.tags = ['existing']
|
||||
action(task, path='tags', value='new_tag')
|
||||
self.assertEqual(['existing', 'new_tag'], task.node.tags)
|
||||
|
||||
def test_del_attribute_action(self):
|
||||
"""Test DelAttributeAction deletes a node attribute."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
# Set up a value to delete
|
||||
task.node.extra = {'to_delete': 'value'}
|
||||
action = inspection_rules.actions.DelAttributeAction()
|
||||
action(task, path='extra/to_delete')
|
||||
self.assertEqual({}, task.node.extra)
|
||||
|
||||
@mock.patch.object(inspection_rules.actions.objects.Trait, 'create',
|
||||
autospec=True)
|
||||
def test_add_trait_action(self, mock_create):
|
||||
"""Test AddTraitAction adds a node trait."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
action = inspection_rules.actions.AddTraitAction()
|
||||
trait_name = 'CUSTOM_AWESOME_TRAIT'
|
||||
action(task, name=trait_name)
|
||||
mock_create.assert_called_once()
|
||||
trait = mock_create.call_args[0][0]
|
||||
self.assertEqual(trait_name, trait.trait)
|
||||
|
||||
@mock.patch.object(inspection_rules.actions.objects.Trait, 'destroy',
|
||||
autospec=True)
|
||||
def test_remove_trait_action(self, mock_destroy):
|
||||
"""Test RemoveTraitAction removes a node trait."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
action = inspection_rules.actions.RemoveTraitAction()
|
||||
trait_name = 'CUSTOM_AWESOME_TRAIT'
|
||||
action(task, name=trait_name)
|
||||
mock_destroy.assert_called_once_with(
|
||||
task.context, node_id=task.node.id, trait=trait_name)
|
||||
|
||||
@mock.patch.object(inspection_rules.actions.driver_utils,
|
||||
'add_node_capability', autospec=True)
|
||||
def test_set_capability_action(self, mock_add):
|
||||
"""Test SetCapabilityAction sets a node capability."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
action = inspection_rules.actions.SetCapabilityAction()
|
||||
action(task, name='boot_mode', value='uefi')
|
||||
mock_add.assert_called_once_with(task, 'boot_mode', 'uefi')
|
||||
|
||||
def test_unset_capability_action(self):
|
||||
"""Test UnsetCapabilityAction removes a node capability."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.node.properties = {
|
||||
'capabilities': 'boot_mode:uefi,other:value'}
|
||||
action = inspection_rules.actions.UnsetCapabilityAction()
|
||||
action(task, name='boot_mode')
|
||||
self.assertEqual('other:value',
|
||||
task.node.properties['capabilities'])
|
||||
|
||||
def test_set_port_attribute_action(self):
|
||||
"""Test SetPortAttributeAction sets a port attribute."""
|
||||
fake_port = mock.Mock()
|
||||
fake_port.uuid = uuidutils.generate_uuid()
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.ports = [fake_port]
|
||||
action = inspection_rules.actions.SetPortAttributeAction()
|
||||
action(task, port_id=fake_port.uuid, path='extra',
|
||||
value='test_value')
|
||||
setattr(fake_port, 'extra', 'test_value')
|
||||
fake_port.save.assert_called_once()
|
||||
|
||||
def test_extend_port_attribute_action(self):
|
||||
"""Test ExtendPortAttributeAction extends a port attribute list."""
|
||||
fake_port = mock.Mock()
|
||||
fake_port.uuid = uuidutils.generate_uuid()
|
||||
fake_port.tags = ['existing']
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.ports = [fake_port]
|
||||
action = inspection_rules.actions.ExtendPortAttributeAction()
|
||||
action(task, port_id=fake_port.uuid, path='tags', value='new_tag')
|
||||
setattr(fake_port, 'tags', ['existing', 'new_tag'])
|
||||
fake_port.save.assert_called_once()
|
||||
|
||||
def test_del_port_attribute_action(self):
|
||||
"""Test DelPortAttributeAction deletes a port attribute."""
|
||||
fake_port = mock.Mock()
|
||||
fake_port.uuid = uuidutils.generate_uuid()
|
||||
fake_port.extra = {'to_delete': 'value'}
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.ports = [fake_port]
|
||||
action = inspection_rules.actions.DelPortAttributeAction()
|
||||
action(task, port_id=fake_port.uuid, path='extra/to_delete')
|
||||
fake_port.save.assert_called_once()
|
||||
|
||||
def test_set_plugin_data_action(self):
|
||||
"""Test SetPluginDataAction sets plugin data."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
plugin_data = {'existing': 'data'}
|
||||
action = inspection_rules.actions.SetPluginDataAction()
|
||||
action(task, path='test_key', value='test_value',
|
||||
plugin_data=plugin_data)
|
||||
expected = {'existing': 'data', 'test_key': 'test_value'}
|
||||
self.assertEqual(expected, plugin_data)
|
||||
|
||||
def test_extend_plugin_data_action(self):
|
||||
"""Test ExtendPluginDataAction extends a plugin data list."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
plugin_data = {'test_list': ['item1']}
|
||||
action = inspection_rules.actions.ExtendPluginDataAction()
|
||||
action(task, path='test_list', value='item2',
|
||||
plugin_data=plugin_data)
|
||||
expected = {'test_list': ['item1', 'item2']}
|
||||
self.assertEqual(expected, plugin_data)
|
||||
|
||||
def test_unset_plugin_data_action(self):
|
||||
"""Test UnsetPluginDataAction removes plugin data."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
plugin_data = {'to_remove': 'value', 'keep': 'value'}
|
||||
action = inspection_rules.actions.UnsetPluginDataAction()
|
||||
action(task, path='to_remove', plugin_data=plugin_data)
|
||||
self.assertEqual({'keep': 'value'}, plugin_data)
|
||||
|
||||
def test_action_error_cases(self):
|
||||
"""Test that actions properly handle error cases."""
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
# SetAttributeAction nested path on a non-dict
|
||||
set_attr = inspection_rules.actions.SetAttributeAction()
|
||||
task.node.driver = 'fake-hardware'
|
||||
self.assertRaises(
|
||||
exception.RuleActionExecutionFailure,
|
||||
set_attr, task, path='driver.some_key', value='test'
|
||||
)
|
||||
|
||||
# ExtendAttributeAction non-list attribute
|
||||
task.node.driver = 'fake-hardware'
|
||||
extend_attr = inspection_rules.actions.ExtendAttributeAction()
|
||||
self.assertRaises(
|
||||
exception.RuleActionExecutionFailure,
|
||||
extend_attr, task, path='driver', value='new_item'
|
||||
)
|
||||
|
||||
# DelAttributeAction nested path on a non-dict
|
||||
del_attr = inspection_rules.actions.DelAttributeAction()
|
||||
task.node.driver = 'fake-hardware'
|
||||
self.assertRaises(
|
||||
exception.RuleActionExecutionFailure,
|
||||
del_attr, task, path='driver.nonexistent_key'
|
||||
)
|
||||
|
||||
# SetPortAttributeAction non-existent port
|
||||
set_port = inspection_rules.actions.SetPortAttributeAction()
|
||||
fake_port_id = uuidutils.generate_uuid()
|
||||
self.assertRaises(
|
||||
exception.PortNotFound,
|
||||
set_port, task, port_id=fake_port_id, path='extra',
|
||||
value='test'
|
||||
)
|
||||
|
||||
# LogAction with invalid log level
|
||||
log_action = inspection_rules.actions.LogAction()
|
||||
self.assertRaises(
|
||||
exception.InspectionRuleExecutionFailure,
|
||||
log_action, task, msg='test message', level='invalid_level'
|
||||
)
|
||||
|
||||
def test_action_with_loop(self):
|
||||
"""Test action execute_with_loop method."""
|
||||
action_data = {
|
||||
'op': 'set-attribute',
|
||||
'loop': [
|
||||
{'path': 'extra/test1', 'value': 'value1'},
|
||||
{'path': 'extra/test2', 'value': 'value2'}
|
||||
]
|
||||
}
|
||||
|
||||
inventory = {'data': 'test'}
|
||||
plugin_data = {'plugin': 'data'}
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.node.extra = {}
|
||||
|
||||
# execute_with_loop
|
||||
action = inspection_rules.actions.SetAttributeAction()
|
||||
results = action.execute_with_loop(task, action_data, inventory,
|
||||
plugin_data)
|
||||
|
||||
# verify both loop items were processed
|
||||
self.assertEqual(2, len(results))
|
||||
self.assertEqual('value1', task.node.extra['test1'])
|
||||
self.assertEqual('value2', task.node.extra['test2'])
|
||||
@@ -27,6 +27,7 @@ from ironic.objects import chassis
|
||||
from ironic.objects import conductor
|
||||
from ironic.objects import deploy_template
|
||||
from ironic.objects import firmware
|
||||
from ironic.objects import inspection_rule as rule
|
||||
from ironic.objects import node
|
||||
from ironic.objects import node_history
|
||||
from ironic.objects import node_inventory
|
||||
@@ -822,3 +823,60 @@ def get_test_firmware_component_list():
|
||||
{'component': 'BIOS', 'initial_version': 'v1.5.0',
|
||||
'current_version': 'v1.5.0', 'last_version_flashed': None},
|
||||
]
|
||||
|
||||
|
||||
def get_test_inspection_rule(**kw):
|
||||
default_uuid = uuidutils.generate_uuid()
|
||||
return {
|
||||
'version': kw.get('version', rule.InspectionRule.VERSION),
|
||||
'uuid': kw.get('uuid', default_uuid),
|
||||
'description': kw.get('description', 'an inspection rule'),
|
||||
'sensitive': kw.get('sensitive', False),
|
||||
'phase': kw.get('phase', 'main'),
|
||||
'priority': kw.get('priority', 0),
|
||||
'actions': kw.get('actions', [get_test_inspection_rule_action(
|
||||
inspection_rule_id=kw.get('uuid'))]),
|
||||
'conditions': kw.get('conditions', [
|
||||
get_test_inspection_rule_condition(
|
||||
inspection_rule_id=kw.get('uuid'))]),
|
||||
}
|
||||
|
||||
|
||||
def get_test_inspection_rule_action(**kw):
|
||||
action = {
|
||||
'op': kw.get('op', 'set-attribute'),
|
||||
'args': kw.get('args', ["/driver", "idrac"]),
|
||||
}
|
||||
|
||||
if 'id' in kw:
|
||||
action['id'] = kw['id']
|
||||
|
||||
action.update({k: v for k, v in kw.items() if k not in action})
|
||||
return action
|
||||
|
||||
|
||||
def get_test_inspection_rule_condition(**kw):
|
||||
condition = {
|
||||
'op': kw.get('op', 'is-true'),
|
||||
'args': kw.get('args', ["{node.auto_discovered}"]),
|
||||
'multiple': kw.get('multiple', 'any'),
|
||||
}
|
||||
|
||||
if 'id' in kw:
|
||||
condition['id'] = kw['id']
|
||||
|
||||
condition.update({k: v for k, v in kw.items() if k not in condition})
|
||||
return condition
|
||||
|
||||
|
||||
def create_test_inspection_rule(**kw):
|
||||
"""Create a inspection rule in the DB and return InspectionRule model.
|
||||
|
||||
:param kw: kwargs with overriding values for the inspection rule.
|
||||
:returns: Test InspectionRule DB object.
|
||||
"""
|
||||
inspection_rule = get_test_inspection_rule(**kw)
|
||||
dbapi = db_api.get_instance()
|
||||
if 'uuid' not in kw:
|
||||
del inspection_rule['uuid']
|
||||
return dbapi.create_inspection_rule(inspection_rule)
|
||||
|
||||
@@ -459,3 +459,27 @@ def create_test_firmware_component(ctxt, **kw):
|
||||
fw_cmp = get_test_firmware_component(ctxt, **kw)
|
||||
fw_cmp.create()
|
||||
return fw_cmp
|
||||
|
||||
|
||||
def get_test_inspection_rule(ctxt, **kw):
|
||||
"""Return an InpsectionRule object with appropriate attributes.
|
||||
|
||||
NOTE: The object leaves the attributes marked as changed, such
|
||||
that a create() could be used to commit it to the DB.
|
||||
"""
|
||||
db_inspection_rule = db_utils.get_test_inspection_rule(**kw)
|
||||
inspection_rule = objects.InspectionRule(ctxt)
|
||||
for key in db_inspection_rule:
|
||||
setattr(inspection_rule, key, db_inspection_rule[key])
|
||||
return inspection_rule
|
||||
|
||||
|
||||
def create_test_inspection_rule(ctxt, **kw):
|
||||
"""Create and return a test inspection rule object.
|
||||
|
||||
NOTE: The object leaves the attributes marked as changed, such
|
||||
that a create() could be used to commit it to the DB.
|
||||
"""
|
||||
inspection_rule = get_test_inspection_rule(ctxt, **kw)
|
||||
inspection_rule.create()
|
||||
return inspection_rule
|
||||
|
||||
Reference in New Issue
Block a user