Merge "Replace Ocata jobs with Rocky"
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 1f8d95e..f60361a 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -104,6 +104,11 @@
return self._list_request('drivers')
@base.handle_errors
+ def list_conductors(self, **kwargs):
+ """List all registered conductors."""
+ return self._list_request('conductors', **kwargs)
+
+ @base.handle_errors
def show_node(self, uuid, api_version=None):
"""Gets a specific node.
@@ -199,6 +204,14 @@
"""
return self._show_request('drivers', driver_name)
+ def show_conductor(self, hostname):
+ """Gets a specific conductor.
+
+ :param hostname: Hostname of conductor.
+ :return: Serialized conductor as a dictionary.
+ """
+ return self._show_request('conductors', hostname)
+
@base.handle_errors
def create_node(self, chassis_id=None, **kwargs):
"""Create a baremetal node with the specified parameters.
@@ -434,7 +447,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_conductor.py b/ironic_tempest_plugin/tests/api/admin/test_conductor.py
new file mode 100644
index 0000000..ee9f439
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_conductor.py
@@ -0,0 +1,56 @@
+# 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 import config
+from tempest.lib import decorators
+
+from ironic_tempest_plugin.tests.api.admin import base
+
+CONF = config.CONF
+
+
+class TestConductors(base.BaseBaremetalTest):
+ """Tests for conductors."""
+
+ min_microversion = '1.49'
+
+ @decorators.idempotent_id('b6d62be4-53a6-43c6-ae78-bff9d1b4efc1')
+ def test_list_conductors(self):
+ _, conductors = self.client.list_conductors()
+ self.assertTrue(len(conductors['conductors']) > 0)
+ cond = conductors['conductors'].pop()
+ self.assertIn('hostname', cond)
+ self.assertIn('conductor_group', cond)
+ self.assertIn('alive', cond)
+ self.assertNotIn('drivers', cond)
+
+ @decorators.idempotent_id('ca3de366-d80a-4e97-b19b-42d594e8d148')
+ def test_list_conductors_detail(self):
+ _, conductors = self.client.list_conductors(detail=True)
+ self.assertTrue(len(conductors['conductors']) > 0)
+ cond = conductors['conductors'].pop()
+ self.assertIn('hostname', cond)
+ self.assertIn('conductor_group', cond)
+ self.assertIn('alive', cond)
+ self.assertIn('drivers', cond)
+
+ @decorators.idempotent_id('7e1829e2-3945-4508-a3d9-c8ebe9463fd8')
+ def test_show_conductor(self):
+ _, conductors = self.client.list_conductors()
+ self.assertTrue(len(conductors['conductors']) > 0)
+ conductor = conductors['conductors'].pop()
+
+ _, conductor = self.client.show_conductor(conductor['hostname'])
+ self.assertIn('hostname', conductor)
+ self.assertIn('conductor_group', conductor)
+ self.assertIn('alive', conductor)
+ self.assertIn('drivers', conductor)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodes.py b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
index 8c1343f..31a9d61 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
@@ -170,6 +170,11 @@
_, loaded_node = self.client.show_node(self.node['uuid'])
self.assertNotIn('fault', loaded_node)
+ @decorators.idempotent_id('e5470656-bb65-4173-be83-2df3fc9aed24')
+ def test_conductor_hidden(self):
+ _, loaded_node = self.client.show_node(self.node['uuid'])
+ self.assertNotIn('conductor', loaded_node)
+
class TestNodesResourceClass(base.BaseBaremetalTest):
@@ -850,3 +855,123 @@
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))
+
+
+class TestNodeConductor(base.BaseBaremetalTest):
+ """Tests for conductor field of baremetal nodes."""
+
+ min_microversion = '1.49'
+
+ def setUp(self):
+ super(TestNodeConductor, self).setUp()
+
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis['uuid'])
+
+ @decorators.idempotent_id('1af888b2-2a19-43da-8181-a5381d6ff536')
+ def test_conductor_exposed(self):
+ _, loaded_node = self.client.show_node(self.node['uuid'])
+ self.assertIn('conductor', loaded_node)
+
+ @decorators.idempotent_id('53bcef99-2989-4755-aa8f-c31037cd15de')
+ def test_list_nodes_by_conductor(self):
+ _, loaded_node = self.client.show_node(self.node['uuid'])
+ hostname = loaded_node['conductor']
+
+ _, nodes = self.client.list_nodes(conductor=hostname)
+ self.assertIn(self.node['uuid'],
+ [n['uuid'] for n in nodes['nodes']])
diff --git a/setup.cfg b/setup.cfg
index 99f34dc..53ca735 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,7 +4,7 @@
description-file =
README.rst
author = OpenStack
-author-email = openstack-dev@lists.openstack.org
+author-email = openstack-discuss@lists.openstack.org
home-page = https://docs.openstack.org/ironic-tempest-plugin/latest/
classifier =
Environment :: OpenStack