Add API tests for protected nodes
Depends-On: https://review.openstack.org/611662
Change-Id: I2e8f1b0b99b349be60f179a5fe7157ac327ae8d5
Story: #2003869
Task: #27611
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 @@
'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 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 @@
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)
@@ -184,6 +192,63 @@
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,
portgroup_uuid=None, physical_network=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 @@
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))