Add support for API microversions in Tempest tests

This adds support for testing Ironic API microversions, specified
as an additional 'X-OpenStack-Ironic-API-Version' header. This change
also adds tests for Ironic API /v1/nodes/(node_ident)/states/*
endpoint for microversions that were changing state machine.

Co-Authored-By: Vladyslav Drok <vdrok@mirantis.com>
Change-Id: Ibf0c73aa6795aaa52e945fd6baa821de20a599e7
diff --git a/ironic_tempest_plugin/services/baremetal/base.py b/ironic_tempest_plugin/services/baremetal/base.py
index edb9ecb..b7a9c32 100644
--- a/ironic_tempest_plugin/services/baremetal/base.py
+++ b/ironic_tempest_plugin/services/baremetal/base.py
@@ -15,8 +15,11 @@
 from oslo_serialization import jsonutils as json
 import six
 from six.moves.urllib import parse as urllib
+from tempest.lib.common import api_version_utils
 from tempest.lib.common import rest_client
 
+BAREMETAL_MICROVERSION = None
+
 
 def handle_errors(f):
     """A decorator that allows to ignore certain types of errors."""
@@ -41,8 +44,27 @@
 class BaremetalClient(rest_client.RestClient):
     """Base Tempest REST client for Ironic API."""
 
+    api_microversion_header_name = 'X-OpenStack-Ironic-API-Version'
     uri_prefix = ''
 
+    def get_headers(self):
+        headers = super(BaremetalClient, self).get_headers()
+        if BAREMETAL_MICROVERSION:
+            headers[self.api_microversion_header_name] = BAREMETAL_MICROVERSION
+        return headers
+
+    def request(self, method, url, extra_headers=False, headers=None,
+                body=None):
+        resp, resp_body = super(BaremetalClient, self).request(
+            method, url, extra_headers, headers, body)
+        if (BAREMETAL_MICROVERSION and
+            BAREMETAL_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
+            api_version_utils.assert_version_header_matches_request(
+                self.api_microversion_header_name,
+                BAREMETAL_MICROVERSION,
+                resp)
+        return resp, resp_body
+
     def serialize(self, object_dict):
         """Serialize an Ironic object."""
 
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 cea449a..1863fc2 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -265,6 +265,29 @@
                                  target)
 
     @base.handle_errors
+    def set_node_provision_state(self, node_uuid, state, configdrive=None):
+        """Set provision state of the specified node.
+
+        :param node_uuid: The unique identifier of the node.
+        :state: desired state to set
+                (active/rebuild/deleted/inspect/manage/provide).
+        :config_drive: A gzipped, base64-encoded configuration drive string.
+        """
+        data = {'target': state, 'configdrive': configdrive}
+        return self._put_request('nodes/%s/states/provision' % node_uuid,
+                                 data)
+
+    @base.handle_errors
+    def set_node_raid_config(self, node_uuid, target_raid_config):
+        """Set raid config of the specified node.
+
+        :param node_uuid: The unique identifier of the node.
+        :target_raid_config: desired RAID configuration of the node.
+        """
+        return self._put_request('nodes/%s/states/raid' % node_uuid,
+                                 target_raid_config)
+
+    @base.handle_errors
     def validate_driver_interface(self, node_uuid):
         """Get all driver interfaces of a specific node.