From 0325dbd269116abe192fc9104a972621ffb03ec9 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 24 Oct 2018 15:13:46 +0200 Subject: [PATCH] Add API tests for protected nodes Depends-On: https://review.openstack.org/611662 Change-Id: I2e8f1b0b99b349be60f179a5fe7157ac327ae8d5 Story: #2003869 Task: #27611 --- .../baremetal/v1/json/baremetal_client.py | 7 +- ironic_tempest_plugin/tests/api/admin/base.py | 67 ++++++++++++- .../tests/api/admin/test_nodes.py | 94 +++++++++++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) 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 b31b433..3b0b356 100644 --- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py +++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py @@ -424,7 +424,12 @@ class BaremetalClient(base.BaremetalClient): 'deploy_interface', 'rescue_interface', 'instance_uuid', - 'resource_class') + 'resource_class', + 'protected', + 'protected_reason', + # TODO(dtantsur): maintenance is set differently + # in newer API versions. + 'maintenance') if not patch: patch = self._make_patch(node_attributes, **kwargs) diff --git a/ironic_tempest_plugin/tests/api/admin/base.py b/ironic_tempest_plugin/tests/api/admin/base.py index 8850c55..6f8586f 100644 --- a/ironic_tempest_plugin/tests/api/admin/base.py +++ b/ironic_tempest_plugin/tests/api/admin/base.py @@ -18,6 +18,7 @@ from tempest.lib import exceptions as lib_exc from tempest import test from ironic_tempest_plugin import clients +from ironic_tempest_plugin.common import waiters from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture CONF = config.CONF @@ -104,12 +105,19 @@ class BaseBaremetalTest(api_version_utils.BaseMicroversionTest, cls.created_objects = {} for resource in RESOURCE_TYPES: cls.created_objects[resource] = set() + cls.deployed_nodes = set() @classmethod def resource_cleanup(cls): """Ensure that all created objects get destroyed.""" - try: + for node in cls.deployed_nodes: + try: + cls.set_node_provision_state(node, 'deleted', + ['available', None]) + except lib_exc.BadRequest: + pass + for resource in RESOURCE_TYPES: uuids = cls.created_objects[resource] delete_method = getattr(cls.client, 'delete_%s' % resource) @@ -183,6 +191,63 @@ class BaseBaremetalTest(api_version_utils.BaseMicroversionTest, return resp, body + @classmethod + def set_node_provision_state(cls, node_id, target, expected, timeout=None, + interval=None): + """Sets the node's provision state. + + :param node_id: The unique identifier of the node. + :param target: Target provision state. + :param expected: Expected final provision state or list of states. + :param timeout: The timeout for reaching the expected state. + Defaults to client.build_timeout. + :param interval: An interval between show_node calls for status check. + Defaults to client.build_interval. + """ + cls.client.set_node_provision_state(node_id, target) + waiters.wait_for_bm_node_status(cls.client, node_id, + 'provision_state', expected, + timeout=timeout, interval=interval) + + @classmethod + def provide_node(cls, node_id, cleaning_timeout=None): + """Make the node available. + + :param node_id: The unique identifier of the node. + :param cleaning_timeout: The timeout to wait for cleaning. + Defaults to client.build_timeout. + """ + _, body = cls.client.show_node(node_id) + current_state = body['provision_state'] + if current_state == 'enroll': + cls.set_node_provision_state(node_id, 'manage', 'manageable', + timeout=60, interval=1) + current_state = 'manageable' + if current_state == 'manageable': + cls.set_node_provision_state(node_id, 'provide', + ['available', None], + timeout=cleaning_timeout) + current_state = 'available' + if current_state not in ('available', None): + raise RuntimeError("Cannot reach state 'available': node %(node)s " + "is in unexpected state %(state)s" % + {'node': node_id, 'state': current_state}) + + @classmethod + def deploy_node(cls, node_id, cleaning_timeout=None, deploy_timeout=None): + """Deploy the node. + + :param node_id: The unique identifier of the node. + :param cleaning_timeout: The timeout to wait for cleaning. + Defaults to client.build_timeout. + :param deploy_timeout: The timeout to wait for deploy. + Defaults to client.build_timeout. + """ + cls.provide_node(node_id, cleaning_timeout=cleaning_timeout) + cls.set_node_provision_state(node_id, 'active', 'active', + timeout=deploy_timeout) + cls.deployed_nodes.add(node_id) + @classmethod @creates('port') def create_port(cls, node_id, address, extra=None, uuid=None, diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodes.py b/ironic_tempest_plugin/tests/api/admin/test_nodes.py index 8c1343f..f3ba2bd 100644 --- a/ironic_tempest_plugin/tests/api/admin/test_nodes.py +++ b/ironic_tempest_plugin/tests/api/admin/test_nodes.py @@ -850,3 +850,97 @@ class TestNodeFault(base.BaseBaremetalTest): self.assertRaises( lib_exc.BadRequest, self.client.list_nodes, fault='somefake') + + +class TestNodeProtected(base.BaseBaremetalTest): + """Tests for protected baremetal nodes.""" + + min_microversion = '1.48' + + def setUp(self): + super(TestNodeProtected, self).setUp() + + _, self.chassis = self.create_chassis() + _, self.node = self.create_node(self.chassis['uuid']) + self.provide_node(self.node['uuid']) + + def tearDown(self): + try: + self.client.update_node(self.node['uuid'], protected=False) + except Exception: + pass + super(TestNodeProtected, self).tearDown() + + @decorators.idempotent_id('52f0cb1c-ad7b-43dc-8e22-a76438b67716') + def test_node_protected_set_unset(self): + self.deploy_node(self.node['uuid']) + _, self.node = self.client.show_node(self.node['uuid']) + self.assertFalse(self.node['protected']) + self.assertIsNone(self.node['protected_reason']) + + self.client.update_node(self.node['uuid'], protected=True, + protected_reason='reason!') + _, self.node = self.client.show_node(self.node['uuid']) + self.assertTrue(self.node['protected']) + self.assertEqual('reason!', self.node['protected_reason']) + + self.client.update_node(self.node['uuid'], protected=False) + _, self.node = self.client.show_node(self.node['uuid']) + self.assertFalse(self.node['protected']) + self.assertIsNone(self.node['protected_reason']) + + @decorators.idempotent_id('8fbd101e-90e6-4843-b41a-556b34802972') + def test_node_protected(self): + self.deploy_node(self.node['uuid']) + self.client.update_node(self.node['uuid'], protected=True) + + self.assertRaises(lib_exc.Forbidden, + self.set_node_provision_state, + self.node['uuid'], 'deleted', 'available') + self.assertRaises(lib_exc.Forbidden, + self.set_node_provision_state, + self.node['uuid'], 'rebuild', 'active') + + @decorators.idempotent_id('04a21b51-2991-4213-8c2f-a96cfdada802') + def test_node_protected_from_deletion(self): + self.deploy_node(self.node['uuid']) + self.client.update_node(self.node['uuid'], protected=True, + maintenance=True) + + self.assertRaises(lib_exc.Forbidden, + self.client.delete_node, + self.node['uuid']) + + @decorators.attr(type='negative') + @decorators.idempotent_id('1c819f4c-6c1d-4150-ba4a-3b0dcb3c8694') + def test_node_protected_negative(self): + # Cannot be set for available nodes + self.assertRaises(lib_exc.Conflict, + self.client.update_node, + self.node['uuid'], protected=True) + + self.deploy_node(self.node['uuid']) + + # Reason cannot be set for nodes that are not protected + self.assertRaises(lib_exc.BadRequest, + self.client.update_node, + self.node['uuid'], protected_reason='reason!') + + +class TestNodesProtectedOldApi(base.BaseBaremetalTest): + + def setUp(self): + super(TestNodesProtectedOldApi, self).setUp() + _, self.chassis = self.create_chassis() + _, self.node = self.create_node(self.chassis['uuid']) + self.deploy_node(self.node['uuid']) + _, self.node = self.client.show_node(self.node['uuid']) + + @decorators.attr(type='negative') + @decorators.idempotent_id('08971546-27cc-40ab-851e-ba7bb52c00ab') + def test_node_protected_old_api(self): + exc = self.assertRaises( + lib_exc.RestClientException, + self.client.update_node, self.node['uuid'], protected=True) + # 400 for old ironic, 406 for new ironic with old microversion. + self.assertIn(exc.resp.status, (400, 406))