Basic API tests for sharding

Validates:
- Unfiltered requests get sharded nodes
- Filtering by a single shard
- Filtering by >1 shards
- Filtering by sharded nodes
- Filtering by unsharded nodes

Change-Id: Id37e8d6db098bb8decae87b6184bab4482321316
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 db06a9e..628a075 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -261,7 +261,7 @@
 
         """
         node = {}
-        for field in ('resource_class', 'name', 'description'):
+        for field in ('resource_class', 'name', 'description', 'shard'):
             if kwargs.get(field):
                 node[field] = kwargs[field]
 
@@ -518,7 +518,8 @@
                            # TODO(dtantsur): maintenance is set differently
                            # in newer API versions.
                            'maintenance',
-                           'description')
+                           'description',
+                           'shard')
         if not patch:
             patch = self._make_patch(node_attributes, **kwargs)
 
diff --git a/ironic_tempest_plugin/tests/api/admin/test_shards.py b/ironic_tempest_plugin/tests/api/admin/test_shards.py
new file mode 100644
index 0000000..ffe6914
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_shards.py
@@ -0,0 +1,128 @@
+#    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 import base
+
+CONF = config.CONF
+
+
+class TestAddShardsToNode(base.BaseBaremetalTest):
+    """Tests for baremetal shards."""
+
+    min_microversion = '1.82'
+
+    def setUp(self):
+        super(TestAddShardsToNode, self).setUp()
+        # set a minimum API version
+        _, self.chassis = self.create_chassis()
+
+    @decorators.idempotent_id('6f1e241d-4386-4730-b9ff-28c6a3dcad31')
+    def test_add_shard_to_node_at_create(self):
+        shard = 'at-create'
+
+        _, body = self.create_node(self.chassis['uuid'], shard=shard)
+        self.assertEqual(shard, body['shard'])
+
+    @decorators.idempotent_id('2eb91d29-e0a5-472b-aeb8-ef6d98eb0f3c')
+    def test_add_shard_to_node_post_create(self):
+        shard = 'post-create'
+
+        _, node = self.create_node(self.chassis['uuid'])
+        _, before = self.client.show_node(node['uuid'])
+        self.assertIsNone(before['shard'])
+
+        self.client.update_node(node['uuid'], shard=shard)
+
+        _, after = self.client.show_node(node['uuid'])
+        self.assertEqual(shard, after['shard'])
+
+
+class TestNodeShardQueries(base.BaseBaremetalTest):
+    """Tests for baremetal shards."""
+
+    min_microversion = '1.82'
+
+    def setUp(self):
+        super(TestNodeShardQueries, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, bad_node = self.create_node(self.chassis['uuid'], shard='bad')
+        _, none_node = self.create_node(self.chassis['uuid'])  # shard=None
+        self.bad_node_id = bad_node['uuid']
+        self.none_node_id = none_node['uuid']
+
+    def _setup_nodes(self, good_shard, num=2):
+        good_node_ids = []
+        for i in range(num):
+            _, node = self.create_node(self.chassis['uuid'], shard=good_shard)
+            good_node_ids.append(node['uuid'])
+
+        return good_node_ids
+
+    @decorators.idempotent_id('df74c989-6972-4104-a8d6-bd8e8d811353')
+    def test_show_all_nodes(self):
+        """Validate unfiltered API query will return nodes with a shard."""
+        shard = "oneshardtest"
+        good_node_ids = self._setup_nodes(shard)
+
+        _, fetched_nodes = self.client.list_nodes()
+        fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
+
+        for node_id in good_node_ids:
+            self.assertIn(node_id, fetched_node_ids)
+
+    @decorators.idempotent_id('6f1e241d-4386-4730-b9ff-28c6a3dcad31')
+    def test_only_show_requested_shard(self):
+        """Validate filtering nodes by shard."""
+        shard = "oneshardtest"
+        good_node_ids = self._setup_nodes(shard)
+
+        _, fetched_nodes = self.client.list_nodes(shard=shard)
+        fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
+
+        self.assertItemsEqual(good_node_ids, fetched_node_ids)
+
+    @decorators.idempotent_id('6f1e241d-4386-4730-b9ff-28c6a3dcad31')
+    def test_only_show_multiple_requested_shards(self):
+        """Validate filtering nodes by multiple shards."""
+        shard = "multishardtest"
+        second_shard = "multishardtest2"
+        good_node_ids = self._setup_nodes(shard)
+        _, second_shard_node = self.create_node(
+            self.chassis['uuid'], shard=second_shard)
+        good_node_ids.append(second_shard_node['uuid'])
+
+        _, fetched_nodes = self.client.list_nodes(
+            shard=','.join([shard, second_shard]))
+        fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
+
+        self.assertItemsEqual(good_node_ids, fetched_node_ids)
+        self.assertNotIn(self.bad_node_id, fetched_node_ids)
+        self.assertNotIn(self.none_node_id, fetched_node_ids)
+
+    @decorators.idempotent_id('f7a2eeb7-d16e-480c-b698-3448491c73a1')
+    def test_show_sharded_nodes(self):
+        _, fetched_nodes = self.client.list_nodes(sharded=True)
+        fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
+
+        # NOTE(JayF): All other nodes under test are sharded
+        self.assertNotIn(self.none_node_id, fetched_node_ids)
+
+    @decorators.idempotent_id('f7a2eeb7-d16e-480c-b698-3448491c73a1')
+    def test_show_unsharded_nodes(self):
+        _, fetched_nodes = self.client.list_nodes(sharded=False)
+        fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
+
+        self.assertIn(self.none_node_id, fetched_node_ids)
+        self.assertNotIn(self.bad_node_id, fetched_node_ids)