Add tempest tests for runbooks
Change-Id: Ic1694a28542f83a08a3c1f77f4397ad285a628c0
diff --git a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index 988dc66..d8f1cfa 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -139,6 +139,11 @@
return self._list_request('deploy_templates', **kwargs)
@base.handle_errors
+ def list_runbooks(self, **kwargs):
+ """List all runbooks."""
+ return self._list_request('runbooks', **kwargs)
+
+ @base.handle_errors
def show_node(self, uuid, api_version=None):
"""Gets a specific node.
@@ -267,6 +272,14 @@
"""
return self._show_request('deploy_templates', deploy_template_ident)
+ def show_runbook(self, runbook_ident):
+ """Gets a specific runbook.
+
+ :param runbook_ident: Name or UUID of runbook.
+ :return: Serialized runbook as a dictionary.
+ """
+ return self._show_request('runbooks', runbook_ident)
+
@base.handle_errors
def create_node(self, chassis_id=None, **kwargs):
"""Create a baremetal node with the specified parameters.
@@ -444,6 +457,23 @@
return self._create_request('deploy_templates', kwargs)
@base.handle_errors
+ def create_runbook(self, name, **kwargs):
+ """Create a runbook with the specified parameters.
+
+ :param name: The name of the runbook.
+ :param kwargs:
+ steps: steps of the runbook.
+ uuid: UUID of the runbook. Optional.
+ public: An optional boolean value indicating whether the runbook
+ is public (accessible to others)
+ or private (restricted to the owner).
+ extra: meta-data of the runbook. Optional.
+ :return: A tuple with the server response and the created runbook.
+ """
+ kwargs['name'] = name
+ return self._create_request('runbooks', kwargs)
+
+ @base.handle_errors
def delete_node(self, uuid):
"""Deletes a node having the specified UUID.
@@ -511,6 +541,15 @@
return self._delete_request('deploy_templates', deploy_template_ident)
@base.handle_errors
+ def delete_runbook(self, runbook_ident):
+ """Deletes a runbook having the specified name or UUID.
+
+ :param runbook_ident: Name or UUID of the runbook.
+ :return: A tuple with the server response and the response body.
+ """
+ return self._delete_request('runbooks', runbook_ident)
+
+ @base.handle_errors
def update_node(self, uuid, patch=None, **kwargs):
"""Update the specified node.
@@ -598,6 +637,18 @@
patch)
@base.handle_errors
+ def update_runbook(self, runbook_ident, patch):
+ """Update the specified runbook.
+
+ :param runbook_ident: Name or UUID of the runbook.
+ :param patch: List of dicts representing json patches. Each dict
+ has keys 'path', 'op' and 'value'; to update a field.
+ :return: A tuple with the server response and the updated runbook.
+ """
+
+ return self._patch_request('runbooks', runbook_ident, patch)
+
+ @base.handle_errors
def set_node_power_state(self, node_uuid, state):
"""Set power state of the specified node.
@@ -624,7 +675,8 @@
@base.handle_errors
def set_node_provision_state(self, node_uuid, state, configdrive=None,
- clean_steps=None, rescue_password=None):
+ clean_steps=None, rescue_password=None,
+ runbook=None):
"""Set provision state of the specified node.
:param node_uuid: The unique identifier of the node.
@@ -634,6 +686,7 @@
configuration drive string.
:param clean_steps: A list with clean steps to execute.
:param rescue_password: user password used to rescue.
+ :param runbook: The unique identifier of a runbook.
"""
data = {'target': state}
# NOTE (vsaienk0): Add both here if specified, do not check anything.
@@ -644,6 +697,8 @@
data['clean_steps'] = clean_steps
if rescue_password is not None:
data['rescue_password'] = rescue_password
+ if runbook is not None:
+ data['runbook'] = runbook
return self._put_request('nodes/%s/states/provision' % node_uuid,
data)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_runbooks.py b/ironic_tempest_plugin/tests/api/admin/test_runbooks.py
new file mode 100644
index 0000000..af3c37c
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_runbooks.py
@@ -0,0 +1,352 @@
+# 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.
+
+import copy
+
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+EXAMPLE_STEPS = [{
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 1
+}]
+
+
+def _get_random_trait():
+ return data_utils.rand_name('CUSTOM', '').replace('-', '_')
+
+
+class TestRunbooks(base.BaseBaremetalTest):
+ """Tests for runbooks."""
+
+ min_microversion = '1.92'
+
+ def setUp(self):
+ super(TestRunbooks, self).setUp()
+ self.name = _get_random_trait()
+ self.steps = copy.deepcopy(EXAMPLE_STEPS)
+ _, self.runbook = self.create_runbook(self.name,
+ steps=self.steps)
+
+ _, self.chassis = self.create_chassis()
+ _, self.test_node = self.create_node(self.chassis['uuid'])
+
+ @decorators.idempotent_id('5a0cfe27-b7e8-d4f0-d3c5-e6e1eef63241')
+ def test_create_runbook_specifying_uuid(self):
+ name = _get_random_trait()
+ uuid = data_utils.rand_uuid()
+
+ _, runbook = self.create_runbook(name=name, steps=self.steps,
+ uuid=uuid)
+
+ _, body = self.client.show_runbook(uuid)
+ self._assertExpected(runbook, body)
+
+ @decorators.idempotent_id('b8bfb388-97b0-aa6e-5130-1a1ed34f94db')
+ def test_delete_runbook(self):
+ self.delete_runbook(self.runbook['uuid'])
+ self.assertRaises(lib_exc.NotFound, self.client.show_runbook,
+ self.runbook['uuid'])
+
+ @decorators.idempotent_id('f1cba93a-2894-7296-0f4b-58867359480b')
+ def test_show_runbook(self):
+ _, runbook = self.client.show_runbook(self.runbook['uuid'])
+ self._assertExpected(self.runbook, runbook)
+ self.assertEqual(self.name, runbook['name'])
+ self.assertEqual(self.steps, runbook['steps'])
+ self.assertIn('uuid', runbook)
+ self.assertEqual({}, runbook['extra'])
+
+ @decorators.idempotent_id('c95e2631-24e2-914b-db00-c8dafc35a677')
+ def test_show_runbook_with_links(self):
+ _, runbook = self.client.show_runbook(self.runbook['uuid'])
+ self.assertIn('links', runbook)
+ self.assertEqual(2, len(runbook['links']))
+ self.assertIn(runbook['uuid'], runbook['links'][0]['href'])
+
+ @decorators.idempotent_id('7b7951b3-e177-21d7-933a-1f29891dea52')
+ def test_list_runbooks(self):
+ _, body = self.client.list_runbooks()
+ self.assertIn(self.runbook['uuid'],
+ [i['uuid'] for i in body['runbooks']])
+
+ for runbook in body['runbooks']:
+ self.validate_self_link('runbooks', runbook['uuid'],
+ runbook['links'][0]['href'])
+
+ @decorators.idempotent_id('6aafc619-0d98-5341-7d94-b293e194dcf7')
+ def test_list_with_limit(self):
+ for _ in range(2):
+ name = _get_random_trait()
+ self.create_runbook(name, steps=self.steps)
+
+ _, body = self.client.list_runbooks(limit=3)
+
+ next_marker = body['runbooks'][-1]['uuid']
+ self.assertIn(next_marker, body['next'])
+
+ @decorators.idempotent_id('ebd762c3-c10e-b71f-5efd-af14ac9f6092')
+ def test_list_runbooks_detail(self):
+ uuids = [
+ self.create_runbook(_get_random_trait(), steps=self.steps)
+ [1]['uuid'] for _ in range(0, 5)]
+
+ _, body = self.client.list_runbooks(detail=True)
+
+ runbooks_dict = dict((runbook['uuid'], runbook)
+ for runbook in body['runbooks']
+ if runbook['uuid'] in uuids)
+
+ for uuid in uuids:
+ self.assertIn(uuid, runbooks_dict)
+ runbook = runbooks_dict[uuid]
+ self.assertIn('name', runbook)
+ self.assertEqual(self.steps, runbook['steps'])
+ self.assertIn('uuid', runbook)
+ self.assertEqual({}, runbook['extra'])
+ self.validate_self_link('runbooks', runbook['uuid'],
+ runbook['links'][0]['href'])
+
+ @decorators.idempotent_id('fb192fdd-ea6a-c637-a36e-390c46a7663b')
+ def test_update_runbook_replace(self):
+ new_name = _get_random_trait()
+ new_steps = [{
+ 'interface': 'raid',
+ 'step': 'create_configuration',
+ 'args': {},
+ 'order': 2,
+ }]
+
+ patch = [{'path': '/name', 'op': 'replace', 'value': new_name},
+ {'path': '/steps', 'op': 'replace', 'value': new_steps}]
+
+ self.client.update_runbook(self.runbook['uuid'], patch)
+
+ _, body = self.client.show_runbook(self.runbook['uuid'])
+ self.assertEqual(new_name, body['name'])
+ self.assertEqual(new_steps, body['steps'])
+
+ @decorators.idempotent_id('99f52546-9906-6ded-9186-7262591b99ec')
+ def test_update_runbook_add(self):
+ new_steps = [
+ {
+ 'interface': 'bios',
+ 'step': 'cache_bios_settings',
+ 'args': {},
+ 'order': 2
+ },
+ {
+ 'interface': 'bios',
+ 'step': 'factory_reset',
+ 'args': {},
+ 'order': 3
+ },
+ ]
+
+ patch = [{'path': '/steps/1', 'op': 'add', 'value': new_steps[0]},
+ {'path': '/steps/2', 'op': 'add', 'value': new_steps[1]}]
+
+ self.client.update_runbook(self.runbook['uuid'], patch)
+
+ _, body = self.client.show_runbook(self.runbook['uuid'])
+ self.assertEqual(self.steps + new_steps, body['steps'])
+
+ @decorators.idempotent_id('ec1550c3-264e-fcce-b131-d2815fdb733b')
+ def test_update_runbook_mixed_ops(self):
+ new_name = _get_random_trait()
+ new_steps = [
+ {
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 2
+ },
+ {
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 3
+ },
+ ]
+
+ patch = [{'path': '/name', 'op': 'replace', 'value': new_name},
+ {'path': '/steps/0', 'op': 'replace', 'value': new_steps[0]},
+ {'path': '/steps/0', 'op': 'remove'},
+ {'path': '/steps/0', 'op': 'add', 'value': new_steps[1]}]
+
+ self.client.update_runbook(self.runbook['uuid'], patch)
+
+ _, body = self.client.show_runbook(self.runbook['uuid'])
+ self.assertEqual(new_name, body['name'])
+ self.assertEqual([new_steps[1]], body['steps'])
+
+ @decorators.idempotent_id('5c7f0aca-cee3-d083-ef2a-e33a8dc467c5')
+ def test_combining_runbook_and_explicit_steps(self):
+ explicit_steps = [{'interface': 'deploy', 'step': 'deploy',
+ 'args': {}}]
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.set_node_provision_state,
+ self.test_node['uuid'], 'active',
+ runbook=self.runbook['uuid'],
+ clean_steps=explicit_steps)
+
+ @decorators.idempotent_id('ddf4a9b7-144d-386a-fd3fcf8960d77199')
+ def test_create_runbook_with_invalid_step_format(self):
+ name = _get_random_trait()
+ invalid_steps = [{'invalid_key': 'value'}]
+
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name, steps=invalid_steps)
+
+
+class TestRunbooksOldAPI(base.BaseBaremetalTest):
+ """Negative tests for runbooks using an old API version."""
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e9481a0d-23e0-4757-bc11-c3c9ab9d3839')
+ def test_create_runbook_old_api(self):
+ # With runbooks support, ironic returns 404. Without, 405.
+ self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+ self.create_runbook,
+ name=_get_random_trait(), steps=EXAMPLE_STEPS)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('0d3af2aa-ba53-4c8a-92d4-91f9b4179fe7')
+ def test_update_runbook_old_api(self):
+ patch = [{'path': '/name', 'op': 'replace',
+ 'value': _get_random_trait()}]
+
+ # With runbooks support, ironic returns 404. Without, 405.
+ self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+ self.client.update_runbook,
+ _get_random_trait(), patch)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('1646b1e5-ab81-45a8-9ea0-30444a4dcaa2')
+ def test_delete_runbook_old_api(self):
+ # With runbooks support, ironic returns 404. Without, 405.
+ self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+ self.client.delete_runbook,
+ _get_random_trait())
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('819480ac-f36a-4402-b1d5-504d7cf55b1f')
+ def test_list_runbooks_old_api(self):
+ self.assertRaises(lib_exc.NotFound,
+ self.client.list_runbooks)
+
+
+class TestRunbooksNegative(base.BaseBaremetalTest):
+ """Negative tests for runbooks."""
+
+ min_microversion = '1.92'
+
+ def setUp(self):
+ super(TestRunbooksNegative, self).setUp()
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture(
+ self.min_microversion)
+ )
+ self.steps = EXAMPLE_STEPS
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('a4085c08-e718-4c2f-a796-0e115b659243')
+ def test_create_runbook_invalid_name(self):
+ name = 'invalid-name'
+ self.assertRaises(lib_exc.BadRequest,
+ self.create_runbook, name=name,
+ steps=self.steps)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('6390acc4-9490-4b23-8b4c-41888a78c9b7')
+ def test_create_runbook_duplicated_name(self):
+ name = _get_random_trait()
+ self.create_runbook(name=name, steps=self.steps)
+ self.assertRaises(lib_exc.Conflict, self.create_runbook,
+ name=name, steps=self.steps)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ed3f0cec-13e8-4175-9fdb-d129e7b7fe10')
+ def test_create_runbook_no_mandatory_field_name(self):
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name=None, steps=self.steps)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('af5dd0df-d903-463f-9535-9e4e9d6fd576')
+ def test_create_runbook_no_mandatory_field_steps(self):
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name=_get_random_trait())
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('cbd33bc5-7602-40b7-943e-3e92217567a3')
+ def test_create_runbook_malformed_steps(self):
+ steps = {'key': 'value'}
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name=_get_random_trait(), steps=steps)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2a562fca-f377-4a6e-b332-37ee82d3a983')
+ def test_create_runbook_malformed_runbook_uuid(self):
+ uuid = 'malformed:uuid'
+ self.assertRaises(lib_exc.BadRequest, self.create_runbook,
+ name=_get_random_trait(), steps=self.steps,
+ uuid=uuid)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('2c006994-88ca-43b7-b605-897d479229d9')
+ def test_show_runbook_nonexistent(self):
+ self.assertRaises(lib_exc.NotFound, self.client.show_runbook,
+ data_utils.rand_uuid())
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('5a815f37-f015-4d68-9b22-099504f74805')
+ def test_update_runbook_remove_mandatory_field_steps(self):
+ name = _get_random_trait()
+ _, runbook = self.create_runbook(name=name, steps=self.steps)
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.update_runbook,
+ runbook['uuid'],
+ [{'path': '/steps/0', 'op': 'remove'}])
+
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.update_runbook,
+ runbook['uuid'],
+ [{'path': '/steps', 'op': 'remove'}])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ee852ebb-a601-4593-9d59-063fcbc8f964')
+ def test_update_runbook_remove_mandatory_field_name(self):
+ name = _get_random_trait()
+ _, runbook = self.create_runbook(name=name, steps=self.steps)
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.update_runbook,
+ runbook['uuid'],
+ [{'path': '/name', 'op': 'remove'}])
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('e59bf38d-272f-4490-b21e-9db217f11378')
+ def test_update_runbook_replace_empty_name(self):
+ name = _get_random_trait()
+ _, runbook = self.create_runbook(name=name, steps=self.steps)
+ self.assertRaises(lib_exc.BadRequest,
+ self.client.update_runbook,
+ runbook['uuid'],
+ [{'path': '/name', 'op': 'replace', 'value': ''}])
diff --git a/ironic_tempest_plugin/tests/api/base.py b/ironic_tempest_plugin/tests/api/base.py
index 59f2f94..11ce859 100644
--- a/ironic_tempest_plugin/tests/api/base.py
+++ b/ironic_tempest_plugin/tests/api/base.py
@@ -38,7 +38,7 @@
# NOTE(jroll): resources must be deleted in a specific order, this list
# defines the resource types to clean up, and the correct order.
RESOURCE_TYPES = ['port', 'portgroup', 'node', 'volume_connector',
- 'volume_target', 'chassis', 'deploy_template']
+ 'volume_target', 'chassis', 'deploy_template', 'runbook']
def creates(resource):
@@ -368,6 +368,18 @@
return resp, body
@classmethod
+ @creates('runbook')
+ def create_runbook(cls, name, **kwargs):
+ """Wrapper utility for creating test runbook.
+
+ :param name: The name of the runbook.
+ :return: A tuple with the server response and the created runbook.
+ """
+ resp, body = cls.client.create_runbook(name=name, **kwargs)
+
+ return resp, body
+
+ @classmethod
def delete_chassis(cls, chassis_id):
"""Deletes a chassis having the specified UUID.
@@ -473,6 +485,20 @@
return resp
+ @classmethod
+ def delete_runbook(cls, runbook_ident):
+ """Deletes a runbook having the specified name or UUID.
+
+ :param runbook_ident: Name or UUID of the runbook.
+ :return: Server response.
+ """
+ resp, body = cls.client.delete_runbook(runbook_ident)
+
+ if runbook_ident in cls.created_objects['runbook']:
+ cls.created_objects['runbook'].remove(runbook_ident)
+
+ return resp
+
def validate_self_link(self, resource, uuid, link):
"""Check whether the given self link formatted correctly."""
expected_link = "{base}/{pref}/{res}/{uuid}".format(
diff --git a/ironic_tempest_plugin/tests/api/rbac_defaults/test_runbooks.py b/ironic_tempest_plugin/tests/api/rbac_defaults/test_runbooks.py
new file mode 100644
index 0000000..eb1476c
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/rbac_defaults/test_runbooks.py
@@ -0,0 +1,100 @@
+# 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 tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.tests.api import base
+
+
+EXAMPLE_STEPS = [{
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 1
+}]
+
+
+def _get_random_trait():
+ return data_utils.rand_name('CUSTOM', '').replace('-', '_')
+
+
+class TestRunbookRBAC(base.BaseBaremetalRBACTest):
+ min_microversion = '1.92'
+ credentials = ['system_admin',
+ 'system_reader',
+ 'project_admin',
+ 'project_member']
+
+ def setUp(self):
+ super(TestRunbookRBAC, self).setUp()
+ self.system_admin_client = (
+ self.os_system_admin.baremetal.BaremetalClient())
+
+ self.system_reader_client = (
+ self.os_system_reader.baremetal.BaremetalClient())
+
+ self.project_admin_client = (
+ self.os_project_admin.baremetal.BaremetalClient())
+
+ self.project_member_client = (
+ self.os_project_member.baremetal.BaremetalClient())
+
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis['uuid'])
+
+ _, self.public_runbook = self.system_admin_client.create_runbook(
+ _get_random_trait(), steps=EXAMPLE_STEPS, public=True)
+
+ _, self.private_runbook = self.project_admin_client.create_runbook(
+ _get_random_trait(), steps=EXAMPLE_STEPS)
+
+ @decorators.idempotent_id('0410fbed-3454-ded3-6f8b3192abfd3fcd')
+ def test_runbook_visibility_and_access_control(self):
+ # Project-scoped user can see both public and owned runbooks
+ _, project_runbooks = self.project_member_client.list_runbooks()
+ self.assertIn(self.public_runbook['uuid'],
+ [r['uuid'] for r in project_runbooks['runbooks']])
+ self.assertIn(self.private_runbook['uuid'],
+ [r['uuid'] for r in project_runbooks['runbooks']])
+
+ # System-scoped user can see all runbooks
+ _, system_runbooks = self.system_reader_client.list_runbooks()
+ self.assertIn(self.public_runbook['uuid'],
+ [r['uuid'] for r in system_runbooks['runbooks']])
+ self.assertIn(self.private_runbook['uuid'],
+ [r['uuid'] for r in system_runbooks['runbooks']])
+
+ @decorators.idempotent_id('903d0027-9265-9f87-c86d-09867aa24edd')
+ def test_runbook_ownership_and_public_flag(self):
+ # Only system-scoped users can set a runbook as public
+
+ patch_public = [{'path': '/public', 'op': 'replace', 'value': True}]
+ self.assertRaises(lib_exc.Forbidden,
+ self.project_admin_client.update_runbook,
+ self.private_runbook['uuid'],
+ patch=patch_public)
+
+ # Setting a runbook as public nullifies its owner field
+ self.system_admin_client.update_runbook(self.private_runbook['uuid'],
+ patch=patch_public)
+ _, updated_runbook = self.system_admin_client.show_runbook(
+ self.private_runbook['uuid'])
+ self.assertIsNone(updated_runbook['owner'])
+
+ # Project-scoped user cannot change the owner of a runbook
+ patch_owner = [{'path': '/public', 'op': 'replace', 'value': True}]
+ self.assertRaises(lib_exc.Forbidden,
+ self.project_admin_client.update_runbook,
+ self.public_runbook['uuid'],
+ patch=patch_owner)
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index d0cfa06..ddb550b 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -459,7 +459,7 @@
timeout=CONF.baremetal.unrescue_timeout,
interval=1)
- def manual_cleaning(self, node, clean_steps):
+ def manual_cleaning(self, node, clean_steps=None, runbook=None):
"""Performs manual cleaning.
The following actions are executed:
@@ -470,15 +470,25 @@
:param node: Ironic node to associate instance_uuid with.
:param clean_steps: clean steps for manual cleaning.
+ :param runbook: unique identifier of a runbook.
"""
+ if clean_steps is None and runbook is None:
+ raise ValueError("Either clean_steps or runbook must be provided.")
+
self.set_node_provision_state(node['uuid'], 'manage')
self.wait_provisioning_state(
node['uuid'],
[bm.BaremetalProvisionStates.MANAGEABLE],
timeout=CONF.baremetal.unprovision_timeout,
interval=30)
- self.set_node_provision_state(
- node['uuid'], 'clean', clean_steps=clean_steps)
+
+ if runbook:
+ self.set_node_provision_state(
+ node['uuid'], 'clean', runbook=runbook)
+ else:
+ self.set_node_provision_state(
+ node['uuid'], 'clean', clean_steps=clean_steps)
+
self.wait_provisioning_state(
node['uuid'],
[bm.BaremetalProvisionStates.MANAGEABLE],
@@ -492,6 +502,17 @@
timeout=CONF.baremetal.unprovision_timeout,
interval=30)
+ def manual_cleaning_with_runbook(self, node):
+ steps = [{
+ 'interface': 'bios',
+ 'step': 'apply_configuration',
+ 'args': {},
+ 'order': 1
+ }]
+ _, runbook = self.baremetal_client.create_runbook('CUSTOM_AWESOME',
+ steps=steps)
+ self.manual_cleaning(node, runbook=runbook)
+
def check_manual_partition_cleaning(self, node):
"""Tests the cleanup step for erasing devices metadata.