From bcdb740e5d40e3dedfc73a1e6fe6b80585621fc2 Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Fri, 19 Jan 2018 15:08:21 +0000 Subject: [PATCH] Add node trait tests Adds admin API tests for the node traits API added in Bare Metal REST API microversion 1.37. Change-Id: Ied30a56c93c874d036ca82569321660d6aa23906 Partial-Bug: #1722194 Depends-On: I313fa01fbf20bf0ff19f102ea63b02e72ac2b856 --- .../baremetal/v1/json/baremetal_client.py | 55 +++ .../tests/api/admin/test_nodes.py | 322 ++++++++++++++++++ 2 files changed, 377 insertions(+) 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 c03b49b7..550128a2 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 @@ class BaremetalClient(base.BaremetalClient): 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 d992a65e..b9e4d39f 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 @@ class TestNodesVif(base.BaseBaremetalTest): 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, 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, 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, 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, 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, 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)