Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions ironic/api/controllers/v1/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,14 @@ def _check_clean_steps(clean_steps):
"""
_check_steps(clean_steps, 'clean', _STEPS_SCHEMA)

disallowed_steps = CONF.api.disallow_clean_steps
if disallowed_steps:
for step in clean_steps:
step_id = '%s.%s' % (step['interface'], step['step'])
if step_id in disallowed_steps:
raise exception.StepNotAllowed(step=step_id,
step_type='clean')


def _check_deploy_steps(deploy_steps):
"""Ensure all necessary keys are present and correct in steps for deploy
Expand All @@ -1337,6 +1345,14 @@ def _check_deploy_steps(deploy_steps):
"""
_check_steps(deploy_steps, 'deploy', _DEPLOY_STEPS_SCHEMA)

disallowed_steps = CONF.api.disallow_deploy_steps
if disallowed_steps:
for step in deploy_steps:
step_id = '%s.%s' % (step['interface'], step['step'])
if step_id in disallowed_steps:
raise exception.StepNotAllowed(step=step_id,
step_type='deploy')


def _check_service_steps(service_steps):
"""Ensure all necessary keys are present and correct in steps for service
Expand All @@ -1347,6 +1363,14 @@ def _check_service_steps(service_steps):
"""
_check_steps(service_steps, 'service', _STEPS_SCHEMA)

disallowed_steps = CONF.api.disallow_service_steps
if disallowed_steps:
for step in service_steps:
step_id = '%s.%s' % (step['interface'], step['step'])
if step_id in disallowed_steps:
raise exception.StepNotAllowed(step=step_id,
step_type='service')


def _check_steps(steps, step_type, schema):
"""Ensure all necessary keys are present and correct in steps.
Expand Down
7 changes: 7 additions & 0 deletions ironic/common/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,13 @@ class BootModeNotAllowed(Invalid):
_msg_fmt = _("'%(mode)s' boot mode is not allowed for %(op)s operation.")


class StepNotAllowed(Invalid):
_msg_fmt = _("%(step_type)s step '%(step)s' is not allowed. Disallowed "
"by operator configuration "
"[api]disallow_%(step_type)s_steps.")
code = http_client.BAD_REQUEST


class InvalidImage(ImageUnacceptable):
_msg_fmt = _("The requested image is not valid for use.")

Expand Down
28 changes: 25 additions & 3 deletions ironic/conductor/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,8 @@ def _do_node_rescue_abort(self, task):
exception.InstanceDeployFailure,
exception.InvalidStateRequested,
exception.NodeProtected,
exception.ConcurrentActionLimit)
exception.ConcurrentActionLimit,
exception.StepNotAllowed)
def do_node_deploy(self, context, node_id, rebuild=False,
configdrive=None, deploy_steps=None):
"""RPC method to initiate deployment to a node.
Expand Down Expand Up @@ -933,6 +934,12 @@ def do_node_deploy(self, context, node_id, rebuild=False,
with task_manager.acquire(context, node_id, shared=False,
purpose='node deployment') as task:
deployments.validate_node(task, event=event)
# Check user-provided deploy steps against the disallow
# list before transitioning state so the node stays in
# its current state on rejection.
if deploy_steps:
conductor_steps.check_disallowed_steps(
deploy_steps, 'deploy', raise_on_disallowed=True)
deployments.start_deploy(task, self, configdrive, event=event,
deploy_steps=deploy_steps)

Expand Down Expand Up @@ -1178,7 +1185,8 @@ def _do_node_tear_down(self, task, initial_state):
exception.NodeInMaintenance,
exception.NodeLocked,
exception.NoFreeConductorWorker,
exception.ConcurrentActionLimit)
exception.ConcurrentActionLimit,
exception.StepNotAllowed)
def do_node_clean(self, context, node_id, clean_steps,
disable_ramdisk=False):
"""RPC method to initiate manual cleaning.
Expand Down Expand Up @@ -1232,6 +1240,12 @@ def do_node_clean(self, context, node_id, clean_steps,
{'node': node.uuid, 'msg': e})
raise exception.InvalidParameterValue(msg)

# Check user-provided clean steps against the disallow
# list before transitioning state so the node stays in
# its current state on rejection.
conductor_steps.check_disallowed_steps(
clean_steps, 'clean', raise_on_disallowed=True)

try:
task.process_event(
'clean',
Expand Down Expand Up @@ -3806,7 +3820,8 @@ def continue_inspection(self, context, node_id, inventory,
exception.NodeInMaintenance,
exception.NodeLocked,
exception.NoFreeConductorWorker,
exception.ConcurrentActionLimit)
exception.ConcurrentActionLimit,
exception.StepNotAllowed)
def do_node_service(self, context, node_id, service_steps,
disable_ramdisk=False):
"""RPC method to initiate node service.
Expand Down Expand Up @@ -3858,6 +3873,13 @@ def do_node_service(self, context, node_id, service_steps,
'failed: %(msg)s') %
{'node': node.uuid, 'msg': e})
raise exception.InvalidParameterValue(msg)

# Check user-provided service steps against the disallow
# list before transitioning state so the node stays in
# its current state on rejection.
conductor_steps.check_disallowed_steps(
service_steps, 'service', raise_on_disallowed=True)

try:
task.process_event(
'service',
Expand Down
58 changes: 56 additions & 2 deletions ironic/conductor/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,12 @@ def set_node_cleaning_steps(task, disable_ramdisk=False):
task, node.driver_internal_info['clean_steps'],
disable_ramdisk=disable_ramdisk)

# Filter out operator-disallowed clean steps
disallowed = CONF.api.disallow_clean_steps
if disallowed:
steps = [s for s in steps
if step_id(s) not in disallowed]

LOG.debug('List of the steps for %(type)s cleaning of node %(node)s: '
'%(steps)s', {'type': 'manual' if manual_clean else 'automated',
'node': node.uuid,
Expand Down Expand Up @@ -464,8 +470,15 @@ def set_node_deployment_steps(task, reset_current=True, skip_missing=False):
deployment steps.
"""
node = task.node
node.set_driver_internal_info('deploy_steps', _get_all_deployment_steps(
task, skip_missing=skip_missing))
steps = _get_all_deployment_steps(task, skip_missing=skip_missing)

# Filter out operator-disallowed deploy steps
disallowed = CONF.api.disallow_deploy_steps
if disallowed:
steps = [s for s in steps
if step_id(s) not in disallowed]

node.set_driver_internal_info('deploy_steps', steps)

LOG.debug('List of the deploy steps for node %(node)s: %(steps)s', {
'node': node.uuid,
Expand Down Expand Up @@ -495,6 +508,13 @@ def set_node_service_steps(task, disable_ramdisk=False):
steps = _validate_user_service_steps(
task, node.driver_internal_info.get('service_steps', []),
disable_ramdisk=disable_ramdisk)

# Filter out operator-disallowed service steps
disallowed = CONF.api.disallow_service_steps
if disallowed:
steps = [s for s in steps
if step_id(s) not in disallowed]

LOG.debug('List of the steps for service of node %(node)s: '
'%(steps)s', {'node': node.uuid,
'steps': steps})
Expand All @@ -516,6 +536,40 @@ def step_id(step):
return '.'.join([step['interface'], step['step']])


def check_disallowed_steps(steps, step_type, raise_on_disallowed=True):
"""Check and optionally filter steps against the operator disallow list.

When raise_on_disallowed is True (user-provided steps), raises
StepNotAllowed for the first disallowed step found. When False
(automated / runbook steps), silently filters them out.

:param steps: list of step dicts, or None.
:param step_type: one of 'clean', 'deploy', 'service'.
:param raise_on_disallowed: if True, raise on first disallowed step;
if False, silently remove disallowed steps.
:returns: the (possibly filtered) list of steps, or None.
:raises: StepNotAllowed if raise_on_disallowed and a step is disallowed.
"""
if steps is None:
return None
config_map = {
'clean': CONF.api.disallow_clean_steps,
'deploy': CONF.api.disallow_deploy_steps,
'service': CONF.api.disallow_service_steps,
}
disallowed = config_map[step_type]
if not disallowed:
return steps
if raise_on_disallowed:
for step in steps:
sid = step_id(step)
if sid in disallowed:
raise exception.StepNotAllowed(step=sid, step_type=step_type)
return steps
else:
return [s for s in steps if step_id(s) not in disallowed]


def _validate_deploy_steps_unique(user_steps):
"""Validate that deploy steps from deploy templates are unique.

Expand Down
71 changes: 71 additions & 0 deletions ironic/conf/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,77 @@ def __call__(self, value):
mutable=True,
help=_("Specifies a list of boot modes that are not allowed "
"during enrollment. Eg: ['bios']")),
cfg.ListOpt('disallow_deploy_steps',
default=[],
mutable=True,
help=_("List of steps not allowed across the deploy "
"workflow. Each entry should be in 'interface.step' "
"format, e.g. ['raid.apply_configuration']. "
"Applies to user-requested steps, deploy template "
"steps, and driver steps alike.")),
cfg.ListOpt('disallow_service_steps',
default=[],
mutable=True,
help=_("List of steps not allowed across the service "
"workflow. Each entry should be in 'interface.step' "
"format, e.g. "
"['bios.factory_reset','bios.apply_configuration']. "
"Applies to user-requested steps and driver steps "
"alike.")),
cfg.ListOpt('disallow_clean_steps',
default=[],
mutable=True,
help=_("List of steps not allowed across the clean "
"workflow. Each entry should be in 'interface.step' "
"format, e.g. "
"['bios.factory_reset','bios.apply_configuration']. "
"Applies to user-requested (manual) steps, "
"automated cleaning steps, and runbook steps "
"alike.")),
cfg.IntOpt('max_json_body_depth',
default=25,
min=8,
mutable=True,
help=_('Maximum JSON nesting depth allowed in API '
'request bodies. Requests exceeding this '
'depth are rejected with HTTP 400 to prevent '
'recursion-based crashes in the JSON parser. '
'The deepest known legitimate structure in '
'the Ironic API is approximately 7 levels '
'(configdrive network_data).')),
cfg.IntOpt('max_json_body_size',
default=1024,
min=4,
mutable=True,
help=_('Maximum size of a JSON request body, '
'in KiB. Requests with a Content-Length '
'exceeding this value are rejected with '
'HTTP 413 before the body is read into '
'memory. The node provision and inspection '
'endpoints use the separate '
'[api]max_json_body_size_provision and '
'[api]max_json_body_size_inspection limits '
'respectively. Defaults to 1 MiB.')),
cfg.IntOpt('max_json_body_size_provision',
default=65536,
min=4,
mutable=True,
help=_('Maximum size of a JSON request body '
'for the node provision state endpoint, '
'in KiB. This endpoint may carry '
'configdrive data and deploy steps. '
'Defaults to 64 MiB.')),
cfg.IntOpt('max_json_body_size_inspection',
default=16384,
min=4,
mutable=True,
help=_('Maximum size of a JSON request body '
'for the continue_inspection endpoint, '
'in KiB. Inspection data from the '
'ramdisk may include system logs such '
'as the journal, making the payload '
'significantly larger than normal API '
'requests. Defaults to 16 MiB.')),
]

opt_group = cfg.OptGroup(name='api',
Expand Down
Loading