Merge "Self-Service via Runbooks"
This commit is contained in:
commit
8b296e242b
245
api-ref/source/baremetal-api-v1-runbooks.inc
Normal file
245
api-ref/source/baremetal-api-v1-runbooks.inc
Normal file
@ -0,0 +1,245 @@
|
||||
.. -*- rst -*-
|
||||
|
||||
===================
|
||||
Runbooks (runbooks)
|
||||
===================
|
||||
|
||||
The Runbook resource represents a collection of steps that define a
|
||||
series of actions to be executed on a node. Runbooks enable users to perform
|
||||
complex operations in a predefined, automated manner. A runbook is
|
||||
matched for a node if the runbook's name matches a trait in the node.
|
||||
|
||||
.. versionadded:: 1.92
|
||||
Runbook API was introduced.
|
||||
|
||||
Create Runbook
|
||||
==============
|
||||
|
||||
.. rest_method:: POST /v1/runbooks
|
||||
|
||||
Creates a runbook.
|
||||
|
||||
.. versionadded:: 1.92
|
||||
Runbook API was introduced.
|
||||
|
||||
Normal response codes: 201
|
||||
|
||||
Error response codes: 400, 401, 403, 409
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- name: runbook_name
|
||||
- steps: runbook_steps
|
||||
- disable_ramdisk: req_disable_ramdisk
|
||||
- uuid: req_uuid
|
||||
- extra: req_extra
|
||||
|
||||
Request Step
|
||||
------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- interface: runbook_step_interface
|
||||
- step: runbook_step_step
|
||||
- args: runbook_step_args
|
||||
- order: runbook_step_order
|
||||
|
||||
Request Example
|
||||
---------------
|
||||
|
||||
.. literalinclude:: samples/runbook-create-request.json
|
||||
:language: javascript
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- uuid: uuid
|
||||
- name: runbook_name
|
||||
- steps: runbook_steps
|
||||
- disable_ramdisk: disable_ramdisk
|
||||
- extra: extra
|
||||
- public: runbook_public
|
||||
- owner: runbook_owner
|
||||
- created_at: created_at
|
||||
- updated_at: updated_at
|
||||
- links: links
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/runbook-create-response.json
|
||||
:language: javascript
|
||||
|
||||
List Runbooks
|
||||
=============
|
||||
|
||||
.. rest_method:: GET /v1/runbooks
|
||||
|
||||
Lists all runbooks.
|
||||
|
||||
.. versionadded:: 1.92
|
||||
Runbook API was introduced.
|
||||
|
||||
Normal response codes: 200
|
||||
|
||||
Error response codes: 400, 401, 403, 404
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- fields: fields
|
||||
- limit: limit
|
||||
- marker: marker
|
||||
- sort_dir: sort_dir
|
||||
- sort_key: sort_key
|
||||
- detail: detail
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- uuid: uuid
|
||||
- name: runbook_name
|
||||
- disable_ramdisk: disable_ramdisk
|
||||
- steps: runbook_steps
|
||||
- extra: extra
|
||||
- public: runbook_public
|
||||
- owner: runbook_owner
|
||||
- created_at: created_at
|
||||
- updated_at: updated_at
|
||||
- links: links
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
**Example runbook list response:**
|
||||
|
||||
.. literalinclude:: samples/runbook-list-response.json
|
||||
:language: javascript
|
||||
|
||||
**Example detailed runbook list response:**
|
||||
|
||||
.. literalinclude:: samples/runbook-detail-response.json
|
||||
:language: javascript
|
||||
|
||||
Show Runbook Details
|
||||
====================
|
||||
|
||||
.. rest_method:: GET /v1/runbooks/{runbook_id}
|
||||
|
||||
Shows details for a runbook.
|
||||
|
||||
.. versionadded:: 1.92
|
||||
Runbook API was introduced.
|
||||
|
||||
Normal response codes: 200
|
||||
|
||||
Error response codes: 400, 401, 403, 404
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- fields: fields
|
||||
- runbook_id: runbook_ident
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- uuid: uuid
|
||||
- name: runbook_name
|
||||
- steps: runbook_steps
|
||||
- disable_ramdisk: disable_ramdisk
|
||||
- extra: extra
|
||||
- public: runbook_public
|
||||
- owner: runbook_owner
|
||||
- created_at: created_at
|
||||
- updated_at: updated_at
|
||||
- links: links
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/runbook-show-response.json
|
||||
:language: javascript
|
||||
|
||||
Update a Runbook
|
||||
================
|
||||
|
||||
.. rest_method:: PATCH /v1/runbooks/{runbook_id}
|
||||
|
||||
Update a runbook.
|
||||
|
||||
.. versionadded:: 1.92
|
||||
Runbook API was introduced.
|
||||
|
||||
Normal response code: 200
|
||||
|
||||
Error response codes: 400, 401, 403, 404, 409
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
The BODY of the PATCH request must be a JSON PATCH document, adhering to
|
||||
`RFC 6902 <https://tools.ietf.org/html/rfc6902>`_.
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- runbook_id: runbook_ident
|
||||
|
||||
.. literalinclude:: samples/runbook-update-request.json
|
||||
:language: javascript
|
||||
|
||||
Response
|
||||
--------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- uuid: uuid
|
||||
- name: runbook_name
|
||||
- steps: runbook_steps
|
||||
- disable_ramdisk: disable_ramdisk
|
||||
- extra: extra
|
||||
- public: runbook_public
|
||||
- owner: runbook_owner
|
||||
- created_at: created_at
|
||||
- updated_at: updated_at
|
||||
- links: links
|
||||
|
||||
.. literalinclude:: samples/runbook-update-response.json
|
||||
:language: javascript
|
||||
|
||||
Delete Runbook
|
||||
==============
|
||||
|
||||
.. rest_method:: DELETE /v1/runbooks/{runbook_id}
|
||||
|
||||
Deletes a runbook.
|
||||
|
||||
.. versionadded:: 1.92
|
||||
Runbook API was introduced.
|
||||
|
||||
Normal response codes: 204
|
||||
|
||||
Error response codes: 400, 401, 403, 404
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- runbook_id: runbook_ident
|
@ -209,6 +209,15 @@ In the above example, the node's RAID interface would configure hardware
|
||||
RAID without non-root volumes, and then all devices would be erased
|
||||
(in that order).
|
||||
|
||||
Alternatively, you can specify a runbook instead of clean_steps::
|
||||
|
||||
{
|
||||
"target":"clean",
|
||||
"runbook": "<runbook_name_or_uuid>"
|
||||
}
|
||||
|
||||
The specified runbook must match one of the node's traits to be used.
|
||||
|
||||
Starting manual cleaning via "openstack metal" CLI
|
||||
------------------------------------------------------
|
||||
|
||||
@ -246,6 +255,24 @@ Or with stdin::
|
||||
cat my-clean-steps.txt | baremetal node clean <node> \
|
||||
--clean-steps -
|
||||
|
||||
To use a runbook instead of specifying clean steps:
|
||||
|
||||
baremetal node clean <node> --runbook <runbook_name_or_uuid>
|
||||
|
||||
Runbooks for Manual Cleaning
|
||||
----------------------------
|
||||
Instead of passing a list of clean steps, operators can now use runbooks.
|
||||
Runbooks are curated lists of steps that can be associated with nodes via
|
||||
traits which simplifies the process of performing consistent cleaning
|
||||
operations across similar nodes.
|
||||
|
||||
To use a runbook for manual cleaning:
|
||||
|
||||
baremetal node clean <node> --runbook <runbook_name_or_uuid>
|
||||
|
||||
Runbooks must be created and associated with nodes beforehand. Only runbooks
|
||||
that match the node's traits can be used for cleaning that node.
|
||||
|
||||
Cleaning Network
|
||||
================
|
||||
|
||||
|
@ -109,6 +109,15 @@ configuration, and then the vendor interface's ``send_raw`` step would be
|
||||
called to send a raw command to the BMC. Please note, ``send_raw`` is only
|
||||
available for the ``ipmi`` hardware type.
|
||||
|
||||
Alternatively, you can specify a runbook instead of service_steps::
|
||||
|
||||
{
|
||||
"target":"service",
|
||||
"runbook": "<runbook_name_or_uuid>"
|
||||
}
|
||||
|
||||
The specified runbook must match one of the node's traits to be used.
|
||||
|
||||
Starting servicing via "openstack baremetal" CLI
|
||||
------------------------------------------------
|
||||
|
||||
@ -137,6 +146,23 @@ Or with stdin::
|
||||
cat my-clean-steps.txt | baremetal node service <node> \
|
||||
--service-steps -
|
||||
|
||||
To use a runbook instead of specifying service steps:
|
||||
|
||||
baremetal node service <node> --runbook <runbook_name_or_uuid>
|
||||
|
||||
Using Runbooks for Servicing
|
||||
----------------------------
|
||||
Similar to manual cleaning, you can use runbooks for node servicing.
|
||||
Runbooks provide a predefined list of service steps associated with nodes
|
||||
via traits.
|
||||
|
||||
To use a runbook for servicing:
|
||||
|
||||
baremetal node service <node> --runbook <runbook_name_or_uuid>
|
||||
|
||||
Ensure that the runbook matches one of the node's traits before using it
|
||||
for servicing.
|
||||
|
||||
Available Steps in Ironic
|
||||
-------------------------
|
||||
|
||||
|
@ -36,6 +36,7 @@ from ironic.api.controllers.v1 import node
|
||||
from ironic.api.controllers.v1 import port
|
||||
from ironic.api.controllers.v1 import portgroup
|
||||
from ironic.api.controllers.v1 import ramdisk
|
||||
from ironic.api.controllers.v1 import runbook
|
||||
from ironic.api.controllers.v1 import shard
|
||||
from ironic.api.controllers.v1 import utils
|
||||
from ironic.api.controllers.v1 import versions
|
||||
@ -77,6 +78,7 @@ VERSIONED_CONTROLLERS = {
|
||||
'events': utils.allow_expose_events,
|
||||
'deploy_templates': utils.allow_deploy_templates,
|
||||
'shards': utils.allow_shards_endpoint,
|
||||
'runbooks': utils.allow_runbooks,
|
||||
# NOTE(dtantsur): continue_inspection is available in 1.1 as a
|
||||
# compatibility hack to make it usable with IPA without changes.
|
||||
# Hide this fact from consumers since it was not actually available
|
||||
@ -131,6 +133,7 @@ class Controller(object):
|
||||
'deploy_templates': deploy_template.DeployTemplatesController(),
|
||||
'shards': shard.ShardController(),
|
||||
'continue_inspection': ramdisk.ContinueInspectionController(),
|
||||
'runbooks': runbook.RunbooksController()
|
||||
}
|
||||
|
||||
@method.expose()
|
||||
|
@ -10,7 +10,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
@ -57,46 +56,13 @@ PATCH_ALLOWED_FIELDS = ['extra', 'name', 'steps', 'description']
|
||||
STEP_PATCH_ALLOWED_FIELDS = ['args', 'interface', 'priority', 'step']
|
||||
|
||||
|
||||
def duplicate_steps(name, value):
|
||||
"""Argument validator to check template for duplicate steps"""
|
||||
# TODO(mgoddard): Determine the consequences of allowing duplicate
|
||||
# steps.
|
||||
# * What if one step has zero priority and another non-zero?
|
||||
# * What if a step that is enabled by default is included in a
|
||||
# template? Do we override the default or add a second invocation?
|
||||
|
||||
# Check for duplicate steps. Each interface/step combination can be
|
||||
# specified at most once.
|
||||
counter = collections.Counter((step['interface'], step['step'])
|
||||
for step in value['steps'])
|
||||
duplicates = {key for key, count in counter.items() if count > 1}
|
||||
if duplicates:
|
||||
duplicates = {"interface: %s, step: %s" % (interface, step)
|
||||
for interface, step in duplicates}
|
||||
err = _("Duplicate deploy steps. A deploy template cannot have "
|
||||
"multiple deploy steps with the same interface and step. "
|
||||
"Duplicates: %s") % "; ".join(duplicates)
|
||||
raise exception.InvalidDeployTemplate(err=err)
|
||||
return value
|
||||
|
||||
|
||||
TEMPLATE_VALIDATOR = args.and_valid(
|
||||
args.schema(TEMPLATE_SCHEMA),
|
||||
duplicate_steps,
|
||||
api_utils.duplicate_steps,
|
||||
args.dict_valid(uuid=args.uuid)
|
||||
)
|
||||
|
||||
|
||||
def convert_steps(rpc_steps):
|
||||
for step in rpc_steps:
|
||||
yield {
|
||||
'interface': step['interface'],
|
||||
'step': step['step'],
|
||||
'args': step['args'],
|
||||
'priority': step['priority'],
|
||||
}
|
||||
|
||||
|
||||
def convert_with_links(rpc_template, fields=None, sanitize=True):
|
||||
"""Add links to the deploy template."""
|
||||
template = api_utils.object_to_dict(
|
||||
@ -104,7 +70,7 @@ def convert_with_links(rpc_template, fields=None, sanitize=True):
|
||||
fields=('name', 'extra'),
|
||||
link_resource='deploy_templates',
|
||||
)
|
||||
template['steps'] = list(convert_steps(rpc_template.steps))
|
||||
template['steps'] = list(api_utils.convert_steps(rpc_template.steps))
|
||||
|
||||
if fields is not None:
|
||||
api_utils.check_for_invalid_fields(fields, template)
|
||||
|
@ -86,6 +86,10 @@ _STEPS_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
'order': {'anyOf': [
|
||||
{'type': 'integer', 'minimum': 0},
|
||||
{'type': 'string', 'minLength': 1, 'pattern': '^[0-9]+$'}
|
||||
]},
|
||||
"execute_on_child_nodes": {
|
||||
"description": "Boolean if the step should be executed "
|
||||
"on child nodes.",
|
||||
@ -988,6 +992,41 @@ class NodeStatesController(rest.RestController):
|
||||
url_args = '/'.join([node_ident, 'states'])
|
||||
api.response.location = link.build_url('nodes', url_args)
|
||||
|
||||
def _handle_runbook(self, rpc_node, target, runbook, clean_steps,
|
||||
service_steps):
|
||||
if not api_utils.allow_runbooks():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
rpc_runbook = api_utils.check_runbook_policy_and_retrieve(
|
||||
policy_name='baremetal:runbook:use',
|
||||
runbook_ident=runbook)
|
||||
|
||||
node_traits = rpc_node.traits.get_trait_names() or []
|
||||
if rpc_runbook.name not in node_traits:
|
||||
msg = (_('This runbook has not been approved for '
|
||||
'use on this node %s. Please ask an administrator '
|
||||
'to add it to your node traits.') % rpc_node.uuid)
|
||||
raise exception.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
|
||||
disable_ramdisk = rpc_runbook.disable_ramdisk
|
||||
if target == ir_states.VERBS['clean']:
|
||||
if clean_steps:
|
||||
msg = (_('Please provide either "clean_steps" or a '
|
||||
'runbook, but not both.'))
|
||||
raise exception.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
clean_steps = list(api_utils.convert_steps(rpc_runbook.steps))
|
||||
elif target == ir_states.VERBS['service']:
|
||||
if service_steps:
|
||||
msg = (_('Please provide either "service_steps" or a '
|
||||
'runbook, but not both.'))
|
||||
raise exception.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
service_steps = list(api_utils.convert_steps(
|
||||
rpc_runbook.steps))
|
||||
return clean_steps, service_steps, disable_ramdisk
|
||||
|
||||
def _do_provision_action(self, rpc_node, target, configdrive=None,
|
||||
clean_steps=None, deploy_steps=None,
|
||||
rescue_password=None, disable_ramdisk=None,
|
||||
@ -1061,11 +1100,12 @@ class NodeStatesController(rest.RestController):
|
||||
deploy_steps=args.types(type(None), list),
|
||||
rescue_password=args.string,
|
||||
disable_ramdisk=args.boolean,
|
||||
service_steps=args.types(type(None), list))
|
||||
service_steps=args.types(type(None), list),
|
||||
runbook=args.types(type(None), str))
|
||||
def provision(self, node_ident, target, configdrive=None,
|
||||
clean_steps=None, deploy_steps=None,
|
||||
rescue_password=None, disable_ramdisk=None,
|
||||
service_steps=None):
|
||||
service_steps=None, runbook=None):
|
||||
"""Asynchronous trigger the provisioning of the node.
|
||||
|
||||
This will set the target provision state of the node, and a
|
||||
@ -1142,6 +1182,7 @@ class NodeStatesController(rest.RestController):
|
||||
'args': {'force': True},
|
||||
'priority': 90 }
|
||||
|
||||
:param runbook: UUID or logical name of a runbook.
|
||||
:raises: NodeLocked (HTTP 409) if the node is currently locked.
|
||||
:raises: ClientSideError (HTTP 409) if the node is already being
|
||||
provisioned.
|
||||
@ -1187,9 +1228,26 @@ class NodeStatesController(rest.RestController):
|
||||
api_utils.check_allow_configdrive(target, configdrive)
|
||||
api_utils.check_allow_clean_disable_ramdisk(target, disable_ramdisk)
|
||||
|
||||
if runbook:
|
||||
clean_steps, service_steps, disable_ramdisk = self._handle_runbook(
|
||||
rpc_node, target, runbook, clean_steps, service_steps
|
||||
)
|
||||
else:
|
||||
if clean_steps:
|
||||
api_utils.check_policy(
|
||||
'baremetal:node:set_provision_state:clean_steps')
|
||||
if service_steps:
|
||||
api_utils.check_policy(
|
||||
'baremetal:node:set_provision_state:service_steps')
|
||||
|
||||
if clean_steps and target != ir_states.VERBS['clean']:
|
||||
msg = (_('"clean_steps" is only valid when setting target '
|
||||
'provision state to %s') % ir_states.VERBS['clean'])
|
||||
if runbook:
|
||||
rb_allowed_targets = [ir_states.VERBS['clean'],
|
||||
ir_states.VERBS['service']]
|
||||
msg = (_('"runbooks" is only valid when setting target '
|
||||
'provision state to any of %s') % rb_allowed_targets)
|
||||
raise exception.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
|
||||
@ -1214,6 +1272,17 @@ class NodeStatesController(rest.RestController):
|
||||
if not api_utils.allow_unhold_verb():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
if service_steps and target != ir_states.VERBS['service']:
|
||||
msg = (_('"service_steps" is only valid when setting target '
|
||||
'provision state to %s') % ir_states.VERBS['service'])
|
||||
if runbook:
|
||||
rb_allowed_targets = [ir_states.VERBS['clean'],
|
||||
ir_states.VERBS['service']]
|
||||
msg = (_('"runbooks" is only valid when setting target '
|
||||
'provision state to any of %s') % rb_allowed_targets)
|
||||
raise exception.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
|
||||
if target == ir_states.VERBS['service']:
|
||||
if not api_utils.allow_service_verb():
|
||||
raise exception.NotAcceptable()
|
||||
|
@ -28,6 +28,7 @@ from ironic.objects import node as node_objects
|
||||
from ironic.objects import notification
|
||||
from ironic.objects import port as port_objects
|
||||
from ironic.objects import portgroup as portgroup_objects
|
||||
from ironic.objects import runbook as runbook_objects
|
||||
from ironic.objects import volume_connector as volume_connector_objects
|
||||
from ironic.objects import volume_target as volume_target_objects
|
||||
|
||||
@ -48,6 +49,8 @@ CRUD_NOTIFY_OBJ = {
|
||||
port_objects.PortCRUDPayload),
|
||||
'portgroup': (portgroup_objects.PortgroupCRUDNotification,
|
||||
portgroup_objects.PortgroupCRUDPayload),
|
||||
'runbook': (runbook_objects.RunbookCRUDNotification,
|
||||
runbook_objects.RunbookCRUDPayload),
|
||||
'volumeconnector':
|
||||
(volume_connector_objects.VolumeConnectorCRUDNotification,
|
||||
volume_connector_objects.VolumeConnectorCRUDPayload),
|
||||
|
391
ironic/api/controllers/v1/runbook.py
Normal file
391
ironic/api/controllers/v1/runbook.py
Normal file
@ -0,0 +1,391 @@
|
||||
# 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 http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_log import log
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import uuidutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
from webob import exc as webob_exc
|
||||
|
||||
from ironic import api
|
||||
from ironic.api.controllers import link
|
||||
from ironic.api.controllers.v1 import collection
|
||||
from ironic.api.controllers.v1 import notification_utils as notify
|
||||
from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
import ironic.conf
|
||||
from ironic import objects
|
||||
|
||||
|
||||
CONF = ironic.conf.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
||||
DEFAULT_RETURN_FIELDS = ['uuid', 'name']
|
||||
|
||||
RUNBOOK_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'uuid': {'type': ['string', 'null']},
|
||||
'name': api_utils.TRAITS_SCHEMA,
|
||||
'description': {'type': ['string', 'null'], 'maxLength': 255},
|
||||
'steps': {
|
||||
'type': 'array',
|
||||
'items': api_utils.RUNBOOK_STEP_SCHEMA,
|
||||
'minItems': 1},
|
||||
'disable_ramdisk': {'type': ['boolean', 'null']},
|
||||
'extra': {'type': ['object', 'null']},
|
||||
'public': {'type': ['boolean', 'null']},
|
||||
'owner': {'type': ['string', 'null'], 'maxLength': 255}
|
||||
},
|
||||
'required': ['steps', 'name'],
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
PATCH_ALLOWED_FIELDS = [
|
||||
'extra',
|
||||
'name',
|
||||
'steps',
|
||||
'description',
|
||||
'public',
|
||||
'owner'
|
||||
]
|
||||
STEP_PATCH_ALLOWED_FIELDS = ['args', 'interface', 'order', 'step']
|
||||
|
||||
|
||||
RUNBOOK_VALIDATOR = args.and_valid(
|
||||
args.schema(RUNBOOK_SCHEMA),
|
||||
api_utils.duplicate_steps,
|
||||
args.dict_valid(uuid=args.uuid)
|
||||
)
|
||||
|
||||
|
||||
def convert_with_links(rpc_runbook, fields=None, sanitize=True):
|
||||
"""Add links to the runbook."""
|
||||
runbook = api_utils.object_to_dict(
|
||||
rpc_runbook,
|
||||
fields=('name', 'extra', 'public', 'owner', 'disable_ramdisk'),
|
||||
link_resource='runbooks',
|
||||
)
|
||||
runbook['steps'] = list(api_utils.convert_steps(rpc_runbook.steps))
|
||||
|
||||
if fields is not None:
|
||||
api_utils.check_for_invalid_fields(fields, runbook)
|
||||
|
||||
if sanitize:
|
||||
runbook_sanitize(runbook, fields)
|
||||
|
||||
return runbook
|
||||
|
||||
|
||||
def runbook_sanitize(runbook, fields):
|
||||
"""Removes sensitive and unrequested data.
|
||||
|
||||
Will only keep the fields specified in the ``fields`` parameter.
|
||||
|
||||
:param fields:
|
||||
list of fields to preserve, or ``None`` to preserve them all
|
||||
:type fields: list of str
|
||||
"""
|
||||
api_utils.sanitize_dict(runbook, fields)
|
||||
if runbook.get('steps'):
|
||||
for step in runbook['steps']:
|
||||
step_sanitize(step)
|
||||
|
||||
|
||||
def step_sanitize(step):
|
||||
if step.get('args'):
|
||||
step['args'] = strutils.mask_dict_password(step['args'], "******")
|
||||
|
||||
|
||||
def list_convert_with_links(rpc_runbooks, limit, fields=None, **kwargs):
|
||||
return collection.list_convert_with_links(
|
||||
items=[convert_with_links(t, fields=fields, sanitize=False)
|
||||
for t in rpc_runbooks],
|
||||
item_name='runbooks',
|
||||
url='runbooks',
|
||||
limit=limit,
|
||||
fields=fields,
|
||||
sanitize_func=runbook_sanitize,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class RunbooksController(rest.RestController):
|
||||
"""REST controller for runbooks."""
|
||||
|
||||
invalid_sort_key_list = ['extra', 'steps']
|
||||
|
||||
@pecan.expose()
|
||||
def _route(self, args, request=None):
|
||||
if not api_utils.allow_runbooks():
|
||||
msg = _("The API version does not allow runbooks")
|
||||
if api.request.method == "GET":
|
||||
raise webob_exc.HTTPNotFound(msg)
|
||||
else:
|
||||
raise webob_exc.HTTPMethodNotAllowed(msg)
|
||||
return super(RunbooksController, self)._route(args, request)
|
||||
|
||||
@METRICS.timer('RunbooksController.get_all')
|
||||
@method.expose()
|
||||
@args.validate(marker=args.name, limit=args.integer, sort_key=args.string,
|
||||
sort_dir=args.string, fields=args.string_list,
|
||||
detail=args.boolean, project=args.boolean)
|
||||
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc',
|
||||
fields=None, detail=None, project=None):
|
||||
"""Retrieve a list of runbooks.
|
||||
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
This value cannot be larger than the value of max_limit
|
||||
in the [api] section of the ironic configuration, or only
|
||||
max_limit resources will be returned.
|
||||
:param project: Optional string value that set the project
|
||||
whose runbooks are to be returned.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param fields: Optional, a list with a specified set of fields
|
||||
of the resource to be returned.
|
||||
:param detail: Optional, boolean to indicate whether retrieve a list
|
||||
of runbooks with detail.
|
||||
"""
|
||||
if not api_utils.allow_runbooks():
|
||||
raise exception.NotFound()
|
||||
|
||||
project_id = api_utils.check_list_policy('runbook', project)
|
||||
|
||||
api_utils.check_allowed_fields(fields)
|
||||
api_utils.check_allowed_fields([sort_key])
|
||||
|
||||
fields = api_utils.get_request_return_fields(fields, detail,
|
||||
DEFAULT_RETURN_FIELDS)
|
||||
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
if sort_key in self.invalid_sort_key_list:
|
||||
raise exception.InvalidParameterValue(
|
||||
_("The sort_key value %(key)s is an invalid field for "
|
||||
"sorting") % {'key': sort_key})
|
||||
|
||||
filters = {}
|
||||
if project_id:
|
||||
filters['project'] = project_id
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.Runbook.get_by_uuid(
|
||||
api.request.context, marker)
|
||||
|
||||
runbooks = objects.Runbook.list(
|
||||
api.request.context, limit=limit, marker=marker_obj,
|
||||
sort_key=sort_key, sort_dir=sort_dir, filters=filters)
|
||||
|
||||
parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
|
||||
|
||||
if detail is not None:
|
||||
parameters['detail'] = detail
|
||||
|
||||
return list_convert_with_links(
|
||||
runbooks, limit, fields=fields, **parameters)
|
||||
|
||||
@METRICS.timer('RunbooksController.get_one')
|
||||
@method.expose()
|
||||
@args.validate(runbook_ident=args.uuid_or_name, fields=args.string_list)
|
||||
def get_one(self, runbook_ident, fields=None):
|
||||
"""Retrieve information about the given runbook.
|
||||
|
||||
:param runbook_ident: UUID or logical name of a runbook.
|
||||
:param fields: Optional, a list with a specified set of fields
|
||||
of the resource to be returned.
|
||||
"""
|
||||
if not api_utils.allow_runbooks():
|
||||
raise exception.NotFound()
|
||||
|
||||
try:
|
||||
rpc_runbook = api_utils.check_runbook_policy_and_retrieve(
|
||||
'baremetal:runbook:get', runbook_ident)
|
||||
except exception.NotAuthorized:
|
||||
# If the user is not authorized to access the runbook,
|
||||
# check also, if the runbook is public
|
||||
rpc_runbook = api_utils.check_and_retrieve_public_runbook(
|
||||
runbook_ident)
|
||||
|
||||
api_utils.check_allowed_fields(fields)
|
||||
return convert_with_links(rpc_runbook, fields=fields)
|
||||
|
||||
@METRICS.timer('RunbooksController.post')
|
||||
@method.expose(status_code=http_client.CREATED)
|
||||
@method.body('runbook')
|
||||
@args.validate(runbook=RUNBOOK_VALIDATOR)
|
||||
def post(self, runbook):
|
||||
"""Create a new runbook.
|
||||
|
||||
:param runbook: a runbook within the request body.
|
||||
"""
|
||||
if not api_utils.allow_runbooks():
|
||||
raise exception.NotFound()
|
||||
|
||||
context = api.request.context
|
||||
api_utils.check_policy('baremetal:runbook:create')
|
||||
|
||||
cdict = context.to_policy_values()
|
||||
if cdict.get('system_scope') != 'all':
|
||||
project_id = None
|
||||
requested_owner = runbook.get('owner', None)
|
||||
if cdict.get('project_id', False):
|
||||
project_id = cdict.get('project_id')
|
||||
|
||||
if requested_owner and requested_owner != project_id:
|
||||
# Translation: If project scoped, and an owner has been
|
||||
# requested, and that owner does not match the requester's
|
||||
# project ID value.
|
||||
msg = _("Cannot create a runbook as a project scoped admin "
|
||||
"with an owner other than your own project.")
|
||||
raise exception.Invalid(msg)
|
||||
|
||||
if project_id and runbook.get('public', False):
|
||||
msg = _("Cannot create a public runbook as a project scoped "
|
||||
"admin.")
|
||||
raise exception.Invalid(msg)
|
||||
# Finally, note the project ID
|
||||
runbook['owner'] = project_id
|
||||
|
||||
if not runbook.get('uuid'):
|
||||
runbook['uuid'] = uuidutils.generate_uuid()
|
||||
new_runbook = objects.Runbook(context, **runbook)
|
||||
|
||||
notify.emit_start_notification(context, new_runbook, 'create')
|
||||
with notify.handle_error_notification(context, new_runbook, 'create'):
|
||||
new_runbook.create()
|
||||
|
||||
# Set the HTTP Location Header
|
||||
api.response.location = link.build_url('runbooks', new_runbook.uuid)
|
||||
api_runbook = convert_with_links(new_runbook)
|
||||
notify.emit_end_notification(context, new_runbook, 'create')
|
||||
return api_runbook
|
||||
|
||||
def _authorize_patch_and_get_runbook(self, runbook_ident, patch):
|
||||
# deal with attribute-specific policy rules
|
||||
policy_checks = []
|
||||
generic_update = False
|
||||
|
||||
paths_to_policy = (
|
||||
('/owner', 'baremetal:runbook:update:owner'),
|
||||
('/public', 'baremetal:runbook:update:public'),
|
||||
)
|
||||
for p in patch:
|
||||
# Process general direct path to policy map
|
||||
rule_match_found = False
|
||||
for check_path, policy_name in paths_to_policy:
|
||||
if p['path'].startswith(check_path):
|
||||
policy_checks.append(policy_name)
|
||||
# Break, policy found
|
||||
rule_match_found = True
|
||||
break
|
||||
if not rule_match_found:
|
||||
generic_update = True
|
||||
|
||||
if generic_update or not policy_checks:
|
||||
# If we couldn't find specific policy to apply,
|
||||
# apply the update policy check.
|
||||
policy_checks.append('baremetal:runbook:update')
|
||||
return api_utils.check_multiple_runbook_policies_and_retrieve(
|
||||
policy_checks, runbook_ident)
|
||||
|
||||
@METRICS.timer('RunbooksController.patch')
|
||||
@method.expose()
|
||||
@method.body('patch')
|
||||
@args.validate(runbook_ident=args.uuid_or_name, patch=args.patch)
|
||||
def patch(self, runbook_ident, patch=None):
|
||||
"""Update an existing runbook.
|
||||
|
||||
:param runbook_ident: UUID or logical name of a runbook.
|
||||
:param patch: a json PATCH document to apply to this runbook.
|
||||
"""
|
||||
if not api_utils.allow_runbooks():
|
||||
raise exception.NotFound()
|
||||
|
||||
api_utils.patch_validate_allowed_fields(patch, PATCH_ALLOWED_FIELDS)
|
||||
|
||||
context = api.request.context
|
||||
|
||||
rpc_runbook = self._authorize_patch_and_get_runbook(runbook_ident,
|
||||
patch)
|
||||
runbook = rpc_runbook.as_dict()
|
||||
|
||||
owner = api_utils.get_patch_values(patch, '/owner')
|
||||
public = api_utils.get_patch_values(patch, '/public')
|
||||
|
||||
if owner:
|
||||
# NOTE(cid): There should not be an owner for a public runbook,
|
||||
# but an owned runbook can be set to non-public and assigned an
|
||||
# owner atomically
|
||||
public_value = public[0] if public else False
|
||||
if runbook.get('public') and (not public) or public_value:
|
||||
msg = _("There cannot be an owner for a public runbook")
|
||||
raise exception.PatchError(patch=patch, reason=msg)
|
||||
|
||||
if public:
|
||||
runbook['owner'] = None
|
||||
|
||||
# apply the patch
|
||||
runbook = api_utils.apply_jsonpatch(runbook, patch)
|
||||
|
||||
# validate the result with the patch schema
|
||||
for step in runbook.get('steps', []):
|
||||
api_utils.patched_validate_with_schema(
|
||||
step, api_utils.RUNBOOK_STEP_SCHEMA)
|
||||
api_utils.patched_validate_with_schema(
|
||||
runbook, RUNBOOK_SCHEMA, RUNBOOK_VALIDATOR)
|
||||
|
||||
api_utils.patch_update_changed_fields(
|
||||
runbook, rpc_runbook, fields=objects.Runbook.fields,
|
||||
schema=RUNBOOK_SCHEMA
|
||||
)
|
||||
|
||||
notify.emit_start_notification(context, rpc_runbook, 'update')
|
||||
with notify.handle_error_notification(context, rpc_runbook, 'update'):
|
||||
rpc_runbook.save()
|
||||
|
||||
api_runbook = convert_with_links(rpc_runbook)
|
||||
notify.emit_end_notification(context, rpc_runbook, 'update')
|
||||
|
||||
return api_runbook
|
||||
|
||||
@METRICS.timer('RunbooksController.delete')
|
||||
@method.expose(status_code=http_client.NO_CONTENT)
|
||||
@args.validate(runbook_ident=args.uuid_or_name)
|
||||
def delete(self, runbook_ident):
|
||||
"""Delete a runbook.
|
||||
|
||||
:param runbook_ident: UUID or logical name of a runbook.
|
||||
"""
|
||||
if not api_utils.allow_runbooks():
|
||||
raise exception.NotFound()
|
||||
|
||||
rpc_runbook = api_utils.check_runbook_policy_and_retrieve(
|
||||
policy_name='baremetal:runbook:delete',
|
||||
runbook_ident=runbook_ident)
|
||||
|
||||
context = api.request.context
|
||||
notify.emit_start_notification(context, rpc_runbook, 'delete')
|
||||
with notify.handle_error_notification(context, rpc_runbook, 'delete'):
|
||||
rpc_runbook.destroy()
|
||||
notify.emit_end_notification(context, rpc_runbook, 'delete')
|
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import copy
|
||||
from http import client as http_client
|
||||
import inspect
|
||||
@ -158,6 +159,24 @@ DEPLOY_STEP_SCHEMA = {
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
RUNBOOK_STEP_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'args': {'type': 'object'},
|
||||
'interface': {
|
||||
'type': 'string',
|
||||
'enum': list(conductor_steps.CLEANING_INTERFACE_PRIORITY)
|
||||
},
|
||||
'step': {'type': 'string', 'minLength': 1},
|
||||
'order': {'anyOf': [
|
||||
{'type': 'integer', 'minimum': 0},
|
||||
{'type': 'string', 'minLength': 1, 'pattern': '^[0-9]+$'}
|
||||
]}
|
||||
},
|
||||
'required': ['interface', 'step', 'order'],
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
|
||||
def local_link_normalize(name, value):
|
||||
if not value:
|
||||
@ -685,6 +704,43 @@ def get_rpc_deploy_template_with_suffix(template_ident):
|
||||
exception.DeployTemplateNotFound)
|
||||
|
||||
|
||||
def get_rpc_runbook(runbook_ident):
|
||||
"""Get the RPC runbook from the UUID or logical name.
|
||||
|
||||
:param runbook_ident: the UUID or logical name of a runbook.
|
||||
|
||||
:returns: The RPC runbook.
|
||||
:raises: InvalidUuidOrName if the name or uuid provided is not valid.
|
||||
:raises: RunbookNotFound if the runbook is not found.
|
||||
"""
|
||||
# If runbook_ident is instead a valid UUID, treat it as a UUID.
|
||||
if uuidutils.is_uuid_like(runbook_ident):
|
||||
return objects.Runbook.get_by_uuid(api.request.context,
|
||||
runbook_ident)
|
||||
|
||||
# Else, we can refer to runbooks by their name too
|
||||
if utils.is_valid_logical_name(runbook_ident):
|
||||
return objects.Runbook.get_by_name(api.request.context,
|
||||
runbook_ident)
|
||||
raise exception.InvalidUuidOrName(name=runbook_ident)
|
||||
|
||||
|
||||
def check_runbook_policy_and_retrieve(policy_name, runbook_ident):
|
||||
"""Check if the specified policy authorizes this request on a node.
|
||||
|
||||
:param: policy_name: Name of the policy to check.
|
||||
:param: runbook_ident: the UUID or logical name of a runbook.
|
||||
|
||||
:raises: HTTPForbidden if the policy forbids access.
|
||||
:raises: RunbookNotFound if the runbook is not found.
|
||||
:return: a runbook object
|
||||
"""
|
||||
rpc_runbook = get_rpc_runbook(runbook_ident)
|
||||
check_owner_policy(object_type='runbook', policy_name=policy_name,
|
||||
owner=rpc_runbook['owner'])
|
||||
return rpc_runbook
|
||||
|
||||
|
||||
def is_valid_node_name(name):
|
||||
"""Determine if the provided name is a valid node name.
|
||||
|
||||
@ -1517,6 +1573,53 @@ def check_policy_true(policy_name):
|
||||
return policy.check_policy(policy_name, cdict, api.request.context)
|
||||
|
||||
|
||||
def duplicate_steps(name, value):
|
||||
"""Argument validator to check template for duplicate steps"""
|
||||
# TODO(mgoddard): Determine the consequences of allowing duplicate
|
||||
# steps.
|
||||
# * What if one step has zero priority and another non-zero?
|
||||
# * What if a step that is enabled by default is included in a
|
||||
# template? Do we override the default or add a second invocation?
|
||||
|
||||
# Check for duplicate steps. Each interface/step combination can be
|
||||
# specified at most once.
|
||||
counter = collections.Counter((step['interface'], step['step'])
|
||||
for step in value['steps'])
|
||||
duplicates = {key for key, count in counter.items() if count > 1}
|
||||
if duplicates:
|
||||
duplicates = {"interface: %s, step: %s" % (interface, step)
|
||||
for interface, step in duplicates}
|
||||
err = _("Duplicate deploy steps. A template cannot have multiple "
|
||||
"deploy steps with the same interface and step. "
|
||||
"Duplicates: %s") % "; ".join(duplicates)
|
||||
raise exception.InvalidDeployTemplate(err=err)
|
||||
return value
|
||||
|
||||
|
||||
def convert_steps(rpc_steps):
|
||||
for step in rpc_steps:
|
||||
result = {
|
||||
'interface': step['interface'],
|
||||
'step': step['step'],
|
||||
'args': step['args'],
|
||||
}
|
||||
|
||||
if 'priority' in step:
|
||||
result['priority'] = step['priority']
|
||||
elif 'order' in step:
|
||||
result['order'] = step['order']
|
||||
|
||||
yield result
|
||||
|
||||
|
||||
def allow_runbooks():
|
||||
"""Check if accessing runbook endpoints is allowed.
|
||||
|
||||
Version 1.92 of the API exposed runbook endpoints.
|
||||
"""
|
||||
return api.request.version.minor >= versions.MINOR_92_RUNBOOKS
|
||||
|
||||
|
||||
def check_owner_policy(object_type, policy_name, owner, lessee=None,
|
||||
conceal_node=False):
|
||||
"""Check if the policy authorizes this request on an object.
|
||||
@ -1547,6 +1650,19 @@ def check_owner_policy(object_type, policy_name, owner, lessee=None,
|
||||
raise
|
||||
|
||||
|
||||
def check_and_retrieve_public_runbook(runbook_ident):
|
||||
"""If policy authorization check fails, check if runbook is public.
|
||||
|
||||
:param: runbook_ident: the UUID or logical name of a runbook.
|
||||
:raises: HTTPForbidden if runbook is not public.
|
||||
:return: RPC runbook identified by runbook_ident
|
||||
"""
|
||||
rpc_runbook = get_rpc_runbook(runbook_ident)
|
||||
if not rpc_runbook.public:
|
||||
raise exception.HTTPForbidden
|
||||
return rpc_runbook
|
||||
|
||||
|
||||
def check_node_policy_and_retrieve(policy_name, node_ident,
|
||||
with_suffix=False):
|
||||
"""Check if the specified policy authorizes this request on a node.
|
||||
@ -1635,6 +1751,27 @@ def check_multiple_node_policies_and_retrieve(policy_names,
|
||||
return rpc_node
|
||||
|
||||
|
||||
def check_multiple_runbook_policies_and_retrieve(policy_names,
|
||||
runbook_ident):
|
||||
"""Check if the specified policies authorize this request on a runbook.
|
||||
|
||||
:param: policy_names: List of policy names to check.
|
||||
:param: runbook_ident: the UUID or logical name of a runbook.
|
||||
|
||||
:raises: HTTPForbidden if the policy forbids access.
|
||||
:raises: RunbookNotFound if the runbook is not found.
|
||||
:return: RPC runbook identified by runbook_ident
|
||||
"""
|
||||
rpc_runbook = None
|
||||
for policy_name in policy_names:
|
||||
if rpc_runbook is None:
|
||||
rpc_runbook = check_runbook_policy_and_retrieve(policy_names[0],
|
||||
runbook_ident)
|
||||
else:
|
||||
check_owner_policy('runbook', policy_name, rpc_runbook['owner'])
|
||||
return rpc_runbook
|
||||
|
||||
|
||||
def check_list_policy(object_type, owner=None):
|
||||
"""Check if the list policy authorizes this request on an object.
|
||||
|
||||
|
@ -129,6 +129,7 @@ BASE_VERSION = 1
|
||||
# v1.89: Add API for attaching/detaching virtual media
|
||||
# v1.90: Accept ovn vtep switch metadata schema to port.local_link_connection
|
||||
# v1.91: Remove special treatment of .json for API objects
|
||||
# v1.92: Add runbooks API
|
||||
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
@ -222,6 +223,7 @@ MINOR_88_PORT_NAME = 88
|
||||
MINOR_89_ATTACH_DETACH_VMEDIA = 89
|
||||
MINOR_90_OVN_VTEP = 90
|
||||
MINOR_91_DOT_JSON = 91
|
||||
MINOR_92_RUNBOOKS = 92
|
||||
|
||||
# When adding another version, update:
|
||||
# - MINOR_MAX_VERSION
|
||||
@ -229,7 +231,7 @@ MINOR_91_DOT_JSON = 91
|
||||
# explanation of what changed in the new version
|
||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
||||
|
||||
MINOR_MAX_VERSION = MINOR_91_DOT_JSON
|
||||
MINOR_MAX_VERSION = MINOR_92_RUNBOOKS
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -716,6 +716,22 @@ class InvalidDeployTemplate(Invalid):
|
||||
_msg_fmt = _("Deploy template invalid: %(err)s.")
|
||||
|
||||
|
||||
class RunbookDuplicateName(Conflict):
|
||||
_msg_fmt = _("A runbook with name %(name)s already exists.")
|
||||
|
||||
|
||||
class RunbookAlreadyExists(Conflict):
|
||||
_msg_fmt = _("A runbook with UUID %(uuid)s already exists.")
|
||||
|
||||
|
||||
class RunbookNotFound(NotFound):
|
||||
_msg_fmt = _("Runbook %(runbook)s could not be found.")
|
||||
|
||||
|
||||
class InvalidRunbook(Invalid):
|
||||
_msg_fmt = _("Runbook invalid: %(err)s.")
|
||||
|
||||
|
||||
class InvalidKickstartTemplate(Invalid):
|
||||
_msg_fmt = _("The kickstart template is missing required variables")
|
||||
|
||||
|
@ -49,7 +49,7 @@ SYSTEM_ADMIN = 'role:admin and system_scope:all'
|
||||
|
||||
# Generic policy check string for system users who don't require all the
|
||||
# authorization that system administrators typically have. This persona, or
|
||||
# check string, typically isn't used by default, but it's existence it useful
|
||||
# check string, typically isn't used by default, but it's existence is useful
|
||||
# in the event a deployment wants to offload some administrative action from
|
||||
# system administrator to system members.
|
||||
# The rule:service_role match here is to enable an elevated level of API
|
||||
@ -59,7 +59,7 @@ SYSTEM_MEMBER = '(role:member and system_scope:all) or rule:service_role' # noq
|
||||
|
||||
# Generic policy check string for read-only access to system-level
|
||||
# resources. This persona is useful for someone who needs access
|
||||
# for auditing or even support. These uses are also able to view
|
||||
# for auditing or even support. These users are also able to view
|
||||
# project-specific resources where applicable (e.g., listing all
|
||||
# volumes in the deployment, regardless of the project they belong to).
|
||||
# The rule:service_role match here is to enable an elevated level of API
|
||||
@ -126,6 +126,24 @@ ALLOCATION_OWNER_MANAGER = ('role:manager and project_id:%(allocation.owner)s')
|
||||
ALLOCATION_OWNER_MEMBER = ('role:member and project_id:%(allocation.owner)s')
|
||||
ALLOCATION_OWNER_READER = ('role:reader and project_id:%(allocation.owner)s')
|
||||
|
||||
# Members can create/destroy their runbooks.
|
||||
RUNBOOK_OWNER_ADMIN = ('role:admin and project_id:%(runbook.owner)s')
|
||||
RUNBOOK_OWNER_MANAGER = ('role:manager and project_id:%(runbook.owner)s')
|
||||
RUNBOOK_OWNER_MEMBER = ('role:member and project_id:%(runbook.owner)s')
|
||||
RUNBOOK_OWNER_READER = ('role:reader and project_id:%(runbook.owner)s')
|
||||
|
||||
RUNBOOK_ADMIN = (
|
||||
'(' + SYSTEM_MEMBER + ') or (' + RUNBOOK_OWNER_MANAGER + ') or role:service' # noqa
|
||||
)
|
||||
|
||||
RUNBOOK_READER = (
|
||||
'(' + SYSTEM_READER + ') or (' + RUNBOOK_OWNER_READER + ') or role:service' # noqa
|
||||
)
|
||||
|
||||
RUNBOOK_CREATOR = (
|
||||
'(' + SYSTEM_MEMBER + ') or role:manager or role:service' # noqa
|
||||
)
|
||||
|
||||
# Used for general operations like changing provision state.
|
||||
SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN = (
|
||||
'(' + SYSTEM_MEMBER + ') or (' + SYSTEM_SERVICE + ') or (' + PROJECT_OWNER_MEMBER + ') or (' + PROJECT_LESSEE_ADMIN + ') or (' + PROJECT_LESSEE_MANAGER + ') or (' + PROJECT_SERVICE + ')' # noqa
|
||||
@ -862,6 +880,24 @@ node_policies = [
|
||||
],
|
||||
deprecated_rule=deprecated_node_set_provision_state
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:node:set_provision_state:clean_steps',
|
||||
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
|
||||
scope_types=['system', 'project'],
|
||||
description='Allow execution of arbitrary steps on a node',
|
||||
operations=[
|
||||
{'path': '/nodes/{node_ident}/states/provision', 'method': 'PUT'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:node:set_provision_state:service_steps',
|
||||
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
|
||||
scope_types=['system', 'project'],
|
||||
description='Allow execution of arbitrary steps on a node',
|
||||
operations=[
|
||||
{'path': '/nodes/{node_ident}/states/provision', 'method': 'PUT'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:node:set_raid_state',
|
||||
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
|
||||
@ -1880,6 +1916,89 @@ deploy_template_policies = [
|
||||
),
|
||||
]
|
||||
|
||||
runbook_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:get',
|
||||
check_str=RUNBOOK_READER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Retrieve a single runbook record',
|
||||
operations=[
|
||||
{'path': '/runbooks/{runbook_ident}', 'method': 'GET'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:list',
|
||||
check_str=API_READER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Retrieve multiple runbook records, filtered by '
|
||||
'an explicit owner or the client project_id',
|
||||
operations=[
|
||||
{'path': '/runbooks', 'method': 'GET'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:list_all',
|
||||
check_str=SYSTEM_READER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Retrieve all runbook records',
|
||||
operations=[
|
||||
{'path': '/runbooks', 'method': 'GET'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:create',
|
||||
check_str=RUNBOOK_CREATOR,
|
||||
scope_types=['system', 'project'],
|
||||
description='Create Runbook records',
|
||||
operations=[{'path': '/runbooks', 'method': 'POST'}],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:delete',
|
||||
check_str=RUNBOOK_ADMIN,
|
||||
scope_types=['system', 'project'],
|
||||
description='Delete a runbook record',
|
||||
operations=[
|
||||
{'path': '/runbooks/{runbook_ident}', 'method': 'DELETE'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:update',
|
||||
check_str=RUNBOOK_ADMIN,
|
||||
scope_types=['system', 'project'],
|
||||
description='Update a runbook record',
|
||||
operations=[
|
||||
{'path': '/runbooks/{runbook_ident}', 'method': 'PATCH'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:update:public',
|
||||
check_str=SYSTEM_MEMBER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Set and unset a runbook as public',
|
||||
operations=[
|
||||
{'path': '/runbooks/{runbook_ident}/public', 'method': 'PATCH'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:update:owner',
|
||||
check_str=SYSTEM_MEMBER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Set and unset the owner of a runbook',
|
||||
operations=[
|
||||
{'path': '/runbooks/{runbook_ident}/owner', 'method': 'PATCH'}
|
||||
],
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:runbook:use',
|
||||
check_str=RUNBOOK_ADMIN,
|
||||
scope_types=['system', 'project'],
|
||||
description='Allowed to use a runbook for node operations',
|
||||
operations=[
|
||||
{'path': '/nodes/{node_ident}/states/provision', 'method': 'PUT'}
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def list_policies():
|
||||
policies = itertools.chain(
|
||||
@ -1896,6 +2015,7 @@ def list_policies():
|
||||
allocation_policies,
|
||||
event_policies,
|
||||
deploy_template_policies,
|
||||
runbook_policies,
|
||||
)
|
||||
return policies
|
||||
|
||||
|
@ -709,7 +709,7 @@ RELEASE_MAPPING = {
|
||||
# make it below. To release, we will preserve a version matching
|
||||
# the release as a separate block of text, like above.
|
||||
'master': {
|
||||
'api': '1.91',
|
||||
'api': '1.92',
|
||||
'rpc': '1.60',
|
||||
'objects': {
|
||||
'Allocation': ['1.1'],
|
||||
@ -728,6 +728,7 @@ RELEASE_MAPPING = {
|
||||
'VolumeConnector': ['1.0'],
|
||||
'VolumeTarget': ['1.0'],
|
||||
'FirmwareComponent': ['1.0'],
|
||||
'Runbook': ['1.0'],
|
||||
}
|
||||
},
|
||||
}
|
||||
|