Merge "Updated from global requirements"
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 c03b49b..550128a 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -640,3 +640,58 @@
resp, body = self.get(uri)
self.expected_success(200, resp.status)
return resp, self.deserialize(body)
+
+ @base.handle_errors
+ def list_node_traits(self, node_uuid):
+ """List all traits associated with the node.
+
+ :param node_uuid: The unique identifier of the node.
+ """
+ return self._list_request('/nodes/%s/traits' % node_uuid)
+
+ @base.handle_errors
+ def set_node_traits(self, node_uuid, traits):
+ """Set all traits of the specified node.
+
+ :param node_uuid: The unique identifier of the node.
+ :param traits: A list of traits to set.
+ """
+ request = {'traits': traits}
+ resp, body = self._put_request('nodes/%s/traits' %
+ node_uuid, request)
+ self.expected_success(http_client.NO_CONTENT, resp.status)
+ return resp, body
+
+ @base.handle_errors
+ def add_node_trait(self, node_uuid, trait):
+ """Add a trait to the specified node.
+
+ :param node_uuid: The unique identifier of the node.
+ :param trait: A trait to add.
+ """
+ resp, body = self._put_request('nodes/%s/traits/%s' %
+ (node_uuid, trait), {})
+ self.expected_success(http_client.NO_CONTENT, resp.status)
+ return resp, body
+
+ @base.handle_errors
+ def remove_node_traits(self, node_uuid):
+ """Remove all traits from the specified node.
+
+ :param node_uuid: Unique identifier of the node in UUID format.
+ """
+ resp, body = self._delete_request('nodes/%s/traits' % node_uuid, {})
+ self.expected_success(http_client.NO_CONTENT, resp.status)
+ return resp, body
+
+ @base.handle_errors
+ def remove_node_trait(self, node_uuid, trait):
+ """Remove a trait from the specified node.
+
+ :param node_uuid: Unique identifier of the node in UUID format.
+ :param trait: A trait to remove.
+ """
+ resp, body = self._delete_request('nodes/%s/traits/%s' %
+ (node_uuid, trait), {})
+ self.expected_success(http_client.NO_CONTENT, resp.status)
+ return resp, body
diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodes.py b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
index d992a65..75a17ec 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
@@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+from oslo_utils import uuidutils
import six
from tempest import config
from tempest.lib.common.utils import data_utils
@@ -401,3 +402,324 @@
self.node['uuid'], self.nport_id)
self.client.vif_detach(self.node['uuid'], self.nport_id)
+
+
+class TestNodesTraits(base.BaseBaremetalTest):
+
+ min_microversion = '1.37'
+
+ def setUp(self):
+ super(TestNodesTraits, self).setUp()
+ self.useFixture(
+ api_microversion_fixture.APIMicroversionFixture(
+ TestNodesTraits.min_microversion)
+ )
+ _, self.chassis = self.create_chassis()
+ # One standard trait & one custom trait.
+ self.traits = ['CUSTOM_TRAIT1', 'HW_CPU_X86_VMX']
+ _, self.node = self.create_node(self.chassis['uuid'])
+
+ @decorators.idempotent_id('5c3a2dd0-af10-474d-a209-d30426e1eb5d')
+ def test_list_node_traits(self):
+ """List traits for a node."""
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual([], body['traits'])
+
+ self.client.set_node_traits(self.node['uuid'], self.traits)
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual(self.traits, sorted(body['traits']))
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('3b83dbd3-4a89-4173-920a-ca33ed3aad69')
+ def test_list_node_traits_non_existent_node(self):
+ """Try to list traits for a non-existent node."""
+ node_uuid = uuidutils.generate_uuid()
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.list_node_traits, node_uuid)
+
+ @decorators.idempotent_id('aa961bf6-ea2f-484b-961b-eae2da0e6b7e')
+ def test_set_node_traits(self):
+ """Set the traits for a node."""
+ self.client.set_node_traits(self.node['uuid'], self.traits)
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual(self.traits, sorted(body['traits']))
+
+ self.client.set_node_traits(self.node['uuid'], [])
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual([], body['traits'])
+
+ @decorators.idempotent_id('727a5e11-5654-459f-8af6-e14eb987a283')
+ def test_set_node_traits_max_traits(self):
+ """Set the maximum number of traits for a node."""
+ traits = ['CUSTOM_TRAIT%d' % i for i in range(50)]
+ self.client.set_node_traits(self.node['uuid'], traits)
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual(sorted(traits), sorted(body['traits']))
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('75831f5d-ca44-403b-8fd6-f7cad95b1c54')
+ def test_set_node_traits_too_many(self):
+ """Set more than the maximum number of traits for a node."""
+ traits = ['CUSTOM_TRAIT%d' % i for i in range(51)]
+ self.assertRaises(
+ lib_exc.BadRequest,
+ self.client.set_node_traits, self.node['uuid'], traits)
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual([], body['traits'])
+
+ @decorators.idempotent_id('d81ceeab-a50f-427a-bc5a-aa916478d0d3')
+ def test_set_node_traits_duplicate_trait(self):
+ """Set the traits for a node, ensuring duplicates are ignored."""
+ self.client.set_node_traits(self.node['uuid'],
+ ['CUSTOM_TRAIT1', 'CUSTOM_TRAIT1'])
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual(['CUSTOM_TRAIT1'], body['traits'])
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('2fb4c9d9-8e5b-4189-b547-26596014491c')
+ def test_set_node_traits_non_existent_node(self):
+ """Try to set traits for a non-existent node."""
+ node_uuid = uuidutils.generate_uuid()
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.set_node_traits, node_uuid, ['CUSTOM_TRAIT1'])
+
+ @decorators.idempotent_id('47db09d9-af2b-424d-9d51-7efca2920f20')
+ def test_add_node_trait_long(self):
+ """Add a node trait of the largest possible length."""
+ trait_long_name = 'CUSTOM_' + data_utils.arbitrary_string(248).upper()
+ self.client.add_node_trait(self.node['uuid'], trait_long_name)
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual([trait_long_name], body['traits'])
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('2a4daa8d-2b85-40ac-a8a0-0462cc9a57ef')
+ def test_add_node_trait_too_long(self):
+ """Try to add a node trait longer than the largest possible length."""
+ trait_long_name = 'CUSTOM_' + data_utils.arbitrary_string(249).upper()
+ self.assertRaises(
+ lib_exc.BadRequest,
+ self.client.add_node_trait, self.node['uuid'], trait_long_name)
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual([], body['traits'])
+
+ @decorators.idempotent_id('4b737e7f-101e-493e-b5ce-494fbffe18fd')
+ def test_add_node_trait_duplicate_trait(self):
+ """Add a node trait that already exists."""
+ self.client.add_node_trait(self.node['uuid'], 'CUSTOM_TRAIT1')
+ self.client.add_node_trait(self.node['uuid'], 'CUSTOM_TRAIT1')
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual(['CUSTOM_TRAIT1'], body['traits'])
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('65bce181-89ce-435e-a7d8-3ba60aafd08d')
+ def test_add_node_trait_too_many(self):
+ """Add a trait to a node that would exceed the maximum."""
+ traits = ['CUSTOM_TRAIT%d' % i for i in range(50)]
+ self.client.set_node_traits(self.node['uuid'], traits)
+
+ self.assertRaises(
+ lib_exc.BadRequest,
+ self.client.add_node_trait, self.node['uuid'], 'CUSTOM_TRAIT50')
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual(sorted(traits), sorted(body['traits']))
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('cca0e831-32af-4ce9-bfce-d3834fea57aa')
+ def test_add_node_trait_non_existent_node(self):
+ """Try to add a trait to a non-existent node."""
+ node_uuid = uuidutils.generate_uuid()
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.add_node_trait, node_uuid, 'CUSTOM_TRAIT1')
+
+ @decorators.idempotent_id('e4bf8bf0-3004-44bc-8bfe-f9f1a167d999')
+ def test_remove_node_traits(self):
+ """Remove all traits from a node."""
+ self.client.set_node_traits(self.node['uuid'], self.traits)
+
+ self.client.remove_node_traits(self.node['uuid'])
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual([], body['traits'])
+
+ @decorators.idempotent_id('4d8c9a35-0036-4139-85c1-5f242395680f')
+ def test_remove_node_traits_no_traits(self):
+ """Remove all traits from a node that has no traits."""
+ self.client.remove_node_traits(self.node['uuid'])
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual([], body['traits'])
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('625c911a-48e8-4bef-810b-7cf33c0846a2')
+ def test_remove_node_traits_non_existent_node(self):
+ """Try to remove all traits from a non-existent node."""
+ node_uuid = uuidutils.generate_uuid()
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.remove_node_traits, node_uuid)
+
+ @decorators.idempotent_id('3591d514-39b9-425e-9afe-ea74ae347486')
+ def test_remove_node_trait(self):
+ """Remove a trait from a node."""
+ self.client.set_node_traits(self.node['uuid'], self.traits)
+
+ self.client.remove_node_trait(self.node['uuid'], 'CUSTOM_TRAIT1')
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual(['HW_CPU_X86_VMX'], body['traits'])
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('b50ae543-5e5e-4b1a-b2f2-9e00fe55974b')
+ def test_remove_node_trait_non_existent_trait(self):
+ """Try to remove a non-existent trait from a node."""
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.remove_node_trait, self.node['uuid'], 'CUSTOM_TRAIT1')
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('f1469745-7cdf-4cae-9699-73d029c47bc3')
+ def test_remove_node_trait_non_existent_node(self):
+ """Try to remove a trait from a non-existent node."""
+ node_uuid = uuidutils.generate_uuid()
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.remove_node_trait, node_uuid, 'CUSTOM_TRAIT1')
+
+ @decorators.idempotent_id('03f9e57f-e584-448a-926f-53035e583e7e')
+ def test_list_nodes_detail(self):
+ """Get detailed nodes list."""
+ self.client.set_node_traits(self.node['uuid'], self.traits)
+
+ _, body = self.client.list_nodes_detail()
+ self.assertGreaterEqual(len(body['nodes']), 1)
+
+ for node in body['nodes']:
+ self.assertIn('traits', node)
+ if node['uuid'] == self.node['uuid']:
+ self.assertEqual(self.traits, sorted(node['traits']))
+
+ @decorators.idempotent_id('2b82f704-1580-403a-af92-92c29a7eebb7')
+ def test_list_nodes_traits_field(self):
+ """Get nodes list with the traits field."""
+ self.client.set_node_traits(self.node['uuid'], self.traits)
+
+ _, body = self.client.list_nodes(fields='uuid,traits')
+ self.assertGreaterEqual(len(body['nodes']), 1)
+
+ for node in body['nodes']:
+ self.assertIn('traits', node)
+ if node['uuid'] == self.node['uuid']:
+ self.assertEqual(self.traits, sorted(node['traits']))
+
+ @decorators.idempotent_id('c83c537a-76aa-4d8a-8673-128d01ee403d')
+ def test_show_node(self):
+ """Show a node with traits."""
+ self.client.set_node_traits(self.node['uuid'], self.traits)
+
+ _, body = self.client.show_node(self.node['uuid'])
+
+ self.assertIn('traits', body)
+ self.assertEqual(self.traits, sorted(body['traits']))
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('9ab6a19c-83b9-4600-b55b-325a51e2f8f6')
+ def test_update_node_traits(self):
+ """Try updating an existing node with traits."""
+ patch = [{'path': '/traits',
+ 'op': 'add',
+ 'value': ['CUSTOM_TRAIT1']}]
+ self.assertRaises(
+ lib_exc.BadRequest,
+ self.client.update_node, self.node['uuid'], patch)
+
+ _, body = self.client.list_node_traits(self.node['uuid'])
+ self.assertEqual([], body['traits'])
+
+
+class TestNodesTraitsOldApi(base.BaseBaremetalTest):
+
+ def setUp(self):
+ super(TestNodesTraitsOldApi, self).setUp()
+ _, self.chassis = self.create_chassis()
+ _, self.node = self.create_node(self.chassis['uuid'])
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('5419af7b-4e27-4be4-88f6-e01c598a8102')
+ def test_list_node_traits_old_api(self):
+ """Try to list traits for a node using an older api version."""
+ exc = self.assertRaises(
+ lib_exc.UnexpectedResponseCode,
+ self.client.list_node_traits, self.node['uuid'])
+ self.assertEqual(406, exc.resp.status)
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('a4353f3a-bedc-4579-9c7e-4bebcd95903d')
+ def test_add_node_trait_old_api(self):
+ """Try to add a trait to a node using an older api version."""
+ exc = self.assertRaises(
+ lib_exc.UnexpectedResponseCode,
+ self.client.add_node_trait, self.node['uuid'], 'CUSTOM_TRAIT1')
+ self.assertEqual(405, exc.resp.status)
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('91cc43d8-2f6f-4b1b-95e9-68dedca54e6b')
+ def test_set_node_traits_old_api(self):
+ """Try to set traits for a node using an older api version."""
+ exc = self.assertRaises(
+ lib_exc.UnexpectedResponseCode,
+ self.client.set_node_traits, self.node['uuid'], ['CUSTOM_TRAIT1'])
+ self.assertEqual(405, exc.resp.status)
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('0f9af890-a57a-4c25-86c8-6418d1b8f4d4')
+ def test_remove_node_trait_old_api(self):
+ """Try to remove a trait from a node using an older api version."""
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.remove_node_trait, self.node['uuid'], 'CUSTOM_TRAIT1')
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('f8375b3c-1939-4d1c-97c4-d23e10680090')
+ def test_remove_node_traits_old_api(self):
+ """Try to remove all traits from a node using an older api version."""
+ self.assertRaises(
+ lib_exc.NotFound,
+ self.client.remove_node_traits, self.node['uuid'])
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('525eeb59-b7ce-413d-a37b-401e67402a4c')
+ def test_list_nodes_detail_old_api(self):
+ """Get detailed nodes list, ensure they have no traits."""
+ _, body = self.client.list_nodes_detail()
+ self.assertGreaterEqual(len(body['nodes']), 1)
+
+ for node in body['nodes']:
+ self.assertNotIn('traits', node)
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('eb75b3c8-ac9c-4399-90a2-c0030bfde7a6')
+ def test_list_nodes_traits_field(self):
+ """Try to list nodes' traits field using older api version."""
+ exc = self.assertRaises(
+ lib_exc.UnexpectedResponseCode,
+ self.client.list_nodes, fields='traits')
+ self.assertEqual(406, exc.resp.status)
+
+ @decorators.attr(type='negative')
+ @decorators.idempotent_id('214ae7fc-149b-4657-b6bc-66353d49ade8')
+ def test_show_node_old_api(self):
+ """Show a node, ensure it has no traits."""
+ _, body = self.client.show_node(self.node['uuid'])
+ self.assertNotIn('traits', body)
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 8736718..6bb6b07 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -1,5 +1,4 @@
- project:
- name: openstack/ironic-tempest-plugin
check:
jobs:
- ironic-dsvm-standalone
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index 5e055f9..ed9d8df 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -2,35 +2,29 @@
- job:
name: ironic-dsvm-standalone-pike
parent: ironic-dsvm-standalone
- vars:
- branch_override: stable/pike
+ override-checkout: stable/pike
- job:
name: ironic-tempest-dsvm-ipa-wholedisk-bios-agent_ipmitool-tinyipa-pike
parent: ironic-tempest-dsvm-ipa-wholedisk-bios-agent_ipmitool-tinyipa
- vars:
- branch_override: stable/pike
+ override-checkout: stable/pike
- job:
name: ironic-tempest-dsvm-ipa-wholedisk-bios-agent_ipmitool-tinyipa-ocata
parent: ironic-tempest-dsvm-ipa-wholedisk-bios-agent_ipmitool-tinyipa
- vars:
- branch_override: stable/ocata
+ override-checkout: stable/ocata
- job:
name: ironic-tempest-dsvm-ironic-inspector-pike
parent: ironic-tempest-dsvm-ironic-inspector
- vars:
- branch_override: stable/pike
+ override-checkout: stable/pike
- job:
name: ironic-tempest-dsvm-ironic-inspector-ocata
parent: ironic-tempest-dsvm-ironic-inspector
- vars:
- branch_override: stable/ocata
+ override-checkout: stable/ocata
- job:
name: ironic-inspector-grenade-dsvm-pike
parent: ironic-inspector-grenade-dsvm
- vars:
- branch_override: stable/pike
+ override-checkout: stable/pike