From 604d29bc9b71fd4c78fad325b4ff1a5f0b2a690b Mon Sep 17 00:00:00 2001 From: cid Date: Thu, 15 Aug 2024 15:11:29 +0100 Subject: [PATCH] Add support for the runbooks feature Adds support for managing runbooks within the OpenStack SDK. Related-Change: #922142 Change-Id: Ia590918c2e4bd629724c2e50146a904099858477 --- openstack/baremetal/v1/_common.py | 4 + openstack/baremetal/v1/node.py | 37 +++++++++- openstack/baremetal/v1/runbooks.py | 54 ++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 32 ++++++++ .../tests/unit/baremetal/v1/test_runbooks.py | 73 +++++++++++++++++++ ...service-via-runbooks-66ca5f6fda681228.yaml | 6 ++ 6 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 openstack/baremetal/v1/runbooks.py create mode 100644 openstack/tests/unit/baremetal/v1/test_runbooks.py create mode 100644 releasenotes/notes/self-service-via-runbooks-66ca5f6fda681228.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 3eb3c4334..dcfc18e58 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -94,6 +94,10 @@ CHANGE_BOOT_MODE_VERSION = '1.76' FIRMWARE_VERSION = '1.86' """API version in which firmware components of a node can be accessed""" +RUNBOOKS_VERSION = '1.92' +"""API version in which a runbook can be used in place of arbitrary steps +for provisioning""" + class Resource(resource.Resource): base_path: str diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 47c0b48b6..c3e0684d2 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -100,8 +100,8 @@ class Node(_common.Resource): is_maintenance='maintenance', ) - # Ability to have a firmware_interface on a node. - _max_microversion = '1.87' + # Ability to run predefined sets of steps on a node using runbooks. + _max_microversion = '1.92' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -207,9 +207,13 @@ class Node(_common.Resource): #: A string to be used by external schedulers to identify this node as a #: unit of a specific type of resource. Added in API microversion 1.21. resource_class = resource.Body("resource_class") - #: A string represents the current service step being executed upon. + #: A string representing the current service step being executed upon. #: Added in API microversion 1.87. service_step = resource.Body("service_step") + #: A string representing the uuid or logical name of a runbook as an + #: alternative to providing ``clean_steps`` or ``service_steps``. + #: Added in API microversion 1.92. + runbook = resource.Body("runbook") #: A string indicating the shard this node belongs to. Added in API #: microversion 1,82. shard = resource.Body("shard") @@ -407,6 +411,7 @@ class Node(_common.Resource): timeout=None, deploy_steps=None, service_steps=None, + runbook=None, ): """Run an action modifying this node's provision state. @@ -431,6 +436,7 @@ class Node(_common.Resource): and ``rebuild`` target. :param service_steps: Service steps to execute, only valid for ``service`` target. + :param ``runbook``: UUID or logical name of a runbook. :return: This :class:`Node` instance. :raises: ValueError if ``config_drive``, ``clean_steps``, @@ -460,6 +466,31 @@ class Node(_common.Resource): version = self._assert_microversion_for(session, 'commit', version) body = {'target': target} + if runbook: + version = self._assert_microversion_for( + session, 'commit', _common.RUNBOOKS_VERSION + ) + + if clean_steps is not None: + raise ValueError( + 'Please provide either clean steps or a ' + 'runbook, but not both.' + ) + if service_steps is not None: + raise ValueError( + 'Please provide either service steps or a ' + 'runbook, but not both.' + ) + + if target != 'clean' and target != 'service': + msg = ( + 'A runbook can only be provided when setting target ' + 'provision state to any of "[clean, service]"' + ) + raise ValueError(msg) + + body['runbook'] = runbook + if config_drive: if target not in ('active', 'rebuild'): raise ValueError( diff --git a/openstack/baremetal/v1/runbooks.py b/openstack/baremetal/v1/runbooks.py new file mode 100644 index 000000000..e2ae0dc02 --- /dev/null +++ b/openstack/baremetal/v1/runbooks.py @@ -0,0 +1,54 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack.baremetal.v1 import _common +from openstack import resource + + +class Runbook(_common.Resource): + resources_key = 'runbooks' + base_path = '/runbooks' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'detail', + fields={'type': _common.fields_type}, + ) + + # Runbooks is available since 1.92 + _max_microversion = '1.92' + name = resource.Body('name') + #: Timestamp at which the runbook was created. + created_at = resource.Body('created_at') + #: A set of one or more arbitrary metadata key and value pairs. + extra = resource.Body('extra') + #: A list of relative links. Includes the self and bookmark links. + links = resource.Body('links', type=list) + #: A set of physical information of the runbook. + steps = resource.Body('steps', type=list) + #: Indicates whether the runbook is publicly accessible. + public = resource.Body('public', type=bool) + #: The name or ID of the project that owns the runbook. + owner = resource.Body('owner', type=str) + #: Timestamp at which the runbook was last updated. + updated_at = resource.Body('updated_at') + #: The UUID of the resource. + id = resource.Body('uuid', alternate_id=True) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 5e8af118e..f86f33c4f 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -83,6 +83,7 @@ FAKE = { "service_step": {}, "secure_boot": True, "shard": "TestShard", + "runbook": None, "states": [ { "href": "http://127.0.0.1:6385/v1/nodes//states", @@ -161,6 +162,7 @@ class TestNode(base.TestCase): self.assertEqual(FAKE['resource_class'], sot.resource_class) self.assertEqual(FAKE['service_step'], sot.service_step) self.assertEqual(FAKE['secure_boot'], sot.is_secure_boot) + self.assertEqual(FAKE['runbook'], sot.runbook) self.assertEqual(FAKE['states'], sot.states) self.assertEqual( FAKE['target_provision_state'], sot.target_provision_state @@ -438,6 +440,36 @@ class TestNodeSetProvisionState(base.TestCase): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) + def test_set_provision_state_clean_runbook(self): + runbook = 'CUSTOM_AWESOME' + result = self.node.set_provision_state( + self.session, 'clean', runbook=runbook + ) + + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'clean', 'runbook': runbook}, + headers=mock.ANY, + microversion='1.92', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + def test_set_provision_state_service_runbook(self): + runbook = 'CUSTOM_AWESOME' + result = self.node.set_provision_state( + self.session, 'service', runbook=runbook + ) + + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'service', 'runbook': runbook}, + headers=mock.ANY, + microversion='1.92', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + @mock.patch.object(node.Node, '_translate_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) diff --git a/openstack/tests/unit/baremetal/v1/test_runbooks.py b/openstack/tests/unit/baremetal/v1/test_runbooks.py new file mode 100644 index 000000000..eed9e73be --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_runbooks.py @@ -0,0 +1,73 @@ +# 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 openstack.baremetal.v1 import runbooks +from openstack.tests.unit import base + + +FAKE = { + "created_at": "2024-08-18T22:28:48.643434+11:11", + "extra": {}, + "links": [ + { + "href": """http://10.60.253.180:6385/v1/runbooks + /bbb45f41-d4bc-4307-8d1d-32f95ce1e920""", + "rel": "self", + }, + { + "href": """http://10.60.253.180:6385/runbooks + /bbb45f41-d4bc-4307-8d1d-32f95ce1e920""", + "rel": "bookmark", + }, + ], + "name": "CUSTOM_AWESOME", + "public": False, + "owner": "blah", + "steps": [ + { + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "interface": "bios", + "order": 1, + "step": "apply_configuration", + } + ], + "updated_at": None, + "uuid": "32f95ce1-4307-d4bc-8d1d-e920bbb45f41", +} + + +class Runbooks(base.TestCase): + def test_basic(self): + sot = runbooks.Runbook() + self.assertIsNone(sot.resource_key) + self.assertEqual('runbooks', sot.resources_key) + self.assertEqual('/runbooks', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + def test_instantiate(self): + sot = runbooks.Runbook(**FAKE) + self.assertEqual(FAKE['steps'], sot.steps) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['extra'], sot.extra) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['name'], sot.name) + self.assertEqual(FAKE['public'], sot.public) + self.assertEqual(FAKE['owner'], sot.owner) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) diff --git a/releasenotes/notes/self-service-via-runbooks-66ca5f6fda681228.yaml b/releasenotes/notes/self-service-via-runbooks-66ca5f6fda681228.yaml new file mode 100644 index 000000000..87ed2c6c5 --- /dev/null +++ b/releasenotes/notes/self-service-via-runbooks-66ca5f6fda681228.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds support for runbooks; an API feature that enables project members + to self-serve maintenance tasks via predefined step lists in lieu of + an arbitrary list of clean/service steps.