A simple standalone test for in-band inspection

Redfish is chosen because it is virtually guaranteed to support managed
inspection, unlike IPMI which may require a separate PXE setup.

Adds support for setting enabled interfaces, which is already relied
upon by the iDRAC tests but is not actually implemented.

Depends-On: https://review.opendev.org/c/openstack/ironic/+/927265
Change-Id: Ib66ac41c2919bade7c0c1ca3d8bb4fdfd2acf858
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index a728bef..9e96863 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -98,7 +98,7 @@
                help="Timeout for association of Nova instance and Ironic "
                     "node"),
     cfg.IntOpt('inspect_timeout',
-               default=10,
+               default=300,
                help="Timeout for inspecting an Ironic node."),
     cfg.IntOpt('power_timeout',
                default=60,
@@ -203,6 +203,9 @@
     cfg.ListOpt('enabled_power_interfaces',
                 default=['fake', 'ipmitool'],
                 help="List of Ironic enabled power interfaces."),
+    cfg.ListOpt('enabled_inspect_interfaces',
+                default=['no-inspect'],
+                help="List of Ironic enabled inspect interfaces."),
     cfg.StrOpt('default_rescue_interface',
                help="Ironic default rescue interface."),
     cfg.StrOpt('firmware_image_url',
diff --git a/ironic_tempest_plugin/services/baremetal/base.py b/ironic_tempest_plugin/services/baremetal/base.py
index f23310f..f2cffd5 100644
--- a/ironic_tempest_plugin/services/baremetal/base.py
+++ b/ironic_tempest_plugin/services/baremetal/base.py
@@ -23,6 +23,11 @@
 # separate processes so global variables are not shared among them.
 BAREMETAL_MICROVERSION = None
 
+# Interfaces that can be set via the baremetal client and by logic in scenario
+# managers.
+SUPPORTED_INTERFACES = ['bios', 'deploy', 'rescue', 'boot', 'raid',
+                        'management', 'power', 'inspect']
+
 
 def set_baremetal_api_microversion(baremetal_microversion):
     global BAREMETAL_MICROVERSION
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 5715609..988dc66 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -20,6 +20,26 @@
     version = '1'
     uri_prefix = 'v1'
 
+    node_attributes = (
+        'properties/cpu_arch',
+        'properties/cpus',
+        'properties/local_gb',
+        'properties/memory_mb',
+        'driver',
+        'instance_uuid',
+        'resource_class',
+        'protected',
+        'protected_reason',
+        # TODO(dtantsur): maintenance is set differently
+        # in newer API versions.
+        'maintenance',
+        'description',
+        'shard'
+    ) + tuple(
+        f'{iface}_interface'
+        for iface in base.SUPPORTED_INTERFACES
+    )
+
     @staticmethod
     def _get_headers(api_version):
         """Return headers for a request.
@@ -507,26 +527,8 @@
         else:
             params = {}
 
-        node_attributes = ('properties/cpu_arch',
-                           'properties/cpus',
-                           'properties/local_gb',
-                           'properties/memory_mb',
-                           'driver',
-                           'bios_interface',
-                           'deploy_interface',
-                           'raid_interface',
-                           'rescue_interface',
-                           'instance_uuid',
-                           'resource_class',
-                           'protected',
-                           'protected_reason',
-                           # TODO(dtantsur): maintenance is set differently
-                           # in newer API versions.
-                           'maintenance',
-                           'description',
-                           'shard')
         if not patch:
-            patch = self._make_patch(node_attributes, **kwargs)
+            patch = self._make_patch(self.node_attributes, **kwargs)
 
         return self._patch_request('nodes', uuid, patch, params=params)
 
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index 01289ce..d0cfa06 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -607,6 +607,12 @@
     # set via a different test).
     power_interface = None
 
+    # The inspect interface to use by the HW type. The inspect interface of the
+    # node used in the test will be set to this value. If set to None, the
+    # node will retain its existing inspect_interface value (which may have
+    # been set via a different test).
+    inspect_interface = None
+
     # Boolean value specify if image is wholedisk or not.
     wholedisk_image = None
 
@@ -632,55 +638,18 @@
                     'driver': cls.driver,
                     'enabled_drivers': CONF.baremetal.enabled_drivers,
                     'enabled_hw_types': CONF.baremetal.enabled_hardware_types})
-        if (cls.bios_interface and cls.bios_interface not in
-                CONF.baremetal.enabled_bios_interfaces):
-            raise cls.skipException(
-                "Bios interface %(iface)s required by the test is not in the "
-                "list of enabled bios interfaces %(enabled)s" % {
-                    'iface': cls.bios_interface,
-                    'enabled': CONF.baremetal.enabled_bios_interfaces})
-        if (cls.deploy_interface and cls.deploy_interface not in
-                CONF.baremetal.enabled_deploy_interfaces):
-            raise cls.skipException(
-                "Deploy interface %(iface)s required by test is not "
-                "in the list of enabled deploy interfaces %(enabled)s" % {
-                    'iface': cls.deploy_interface,
-                    'enabled': CONF.baremetal.enabled_deploy_interfaces})
-        if (cls.rescue_interface and cls.rescue_interface not in
-                CONF.baremetal.enabled_rescue_interfaces):
-            raise cls.skipException(
-                "Rescue interface %(iface)s required by test is not "
-                "in the list of enabled rescue interfaces %(enabled)s" % {
-                    'iface': cls.rescue_interface,
-                    'enabled': CONF.baremetal.enabled_rescue_interfaces})
-        if (cls.boot_interface and cls.boot_interface not in
-                CONF.baremetal.enabled_boot_interfaces):
-            raise cls.skipException(
-                "Boot interface %(iface)s required by test is not "
-                "in the list of enabled boot interfaces %(enabled)s" % {
-                    'iface': cls.boot_interface,
-                    'enabled': CONF.baremetal.enabled_boot_interfaces})
-        if (cls.raid_interface and cls.raid_interface not in
-                CONF.baremetal.enabled_raid_interfaces):
-            raise cls.skipException(
-                "RAID interface %(iface)s required by test is not "
-                "in the list of enabled RAID interfaces %(enabled)s" % {
-                    'iface': cls.raid_interface,
-                    'enabled': CONF.baremetal.enabled_raid_interfaces})
-        if (cls.management_interface and cls.management_interface not in
-                CONF.baremetal.enabled_management_interfaces):
-            raise cls.skipException(
-                "Management interface %(iface)s required by test is not "
-                "in the list of enabled management interfaces %(enabled)s" % {
-                    'iface': cls.management_interface,
-                    'enabled': CONF.baremetal.enabled_management_interfaces})
-        if (cls.power_interface and cls.power_interface not in
-                CONF.baremetal.enabled_power_interfaces):
-            raise cls.skipException(
-                "Power interface %(iface)s required by test is not "
-                "in the list of enabled power interfaces %(enabled)s" % {
-                    'iface': cls.power_interface,
-                    'enabled': CONF.baremetal.enabled_power_interfaces})
+        for iface in base.SUPPORTED_INTERFACES:
+            requested = getattr(cls, f'{iface}_interface')
+            enabled = getattr(CONF.baremetal, f'enabled_{iface}_interfaces')
+            if requested and requested not in enabled:
+                raise cls.skipException(
+                    "%(type)s interface %(iface)s required by the test is not "
+                    "in the list of enabled %(type)s interfaces "
+                    "%(enabled)s" % {
+                        'iface': requested,
+                        'type': iface,
+                        'enabled': ', '.join(enabled),
+                    })
         if (cls.wholedisk_image is not None
                 and not cls.wholedisk_image
                 and CONF.baremetal.use_provision_network):
@@ -720,20 +689,9 @@
         if not uuidutils.is_uuid_like(cls.image_ref):
             image_checksum = cls.image_checksum
         boot_kwargs = {'image_checksum': image_checksum}
-        if cls.bios_interface:
-            boot_kwargs['bios_interface'] = cls.bios_interface
-        if cls.deploy_interface:
-            boot_kwargs['deploy_interface'] = cls.deploy_interface
-        if cls.rescue_interface:
-            boot_kwargs['rescue_interface'] = cls.rescue_interface
-        if cls.boot_interface:
-            boot_kwargs['boot_interface'] = cls.boot_interface
-        if cls.raid_interface:
-            boot_kwargs['raid_interface'] = cls.raid_interface
-        if cls.management_interface:
-            boot_kwargs['management_interface'] = cls.management_interface
-        if cls.power_interface:
-            boot_kwargs['power_interface'] = cls.power_interface
+        for iface in base.SUPPORTED_INTERFACES:
+            if requested := getattr(cls, f'{iface}_interface'):
+                boot_kwargs[f'{iface}_interface'] = requested
 
         # just get an available node
         cls.node = cls.get_and_reserve_node()
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_inspection_basic.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_inspection_basic.py
index 97f0fa3..2f367b1 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_inspection_basic.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_inspection_basic.py
@@ -21,23 +21,16 @@
 CONF = config.CONF
 
 
-class BaremetalIdracInspect(bsm.BaremetalStandaloneScenarioTest):
+class BaremetalInspectBase:
 
-    driver = 'idrac'
     mandatory_attr = ['driver', 'inspect_interface']
-    # The test cases clean up at the end by detaching the VIF.
-    # Support for VIFs was introduced by version 1.28
-    # (# v1.28: Add vifs subcontroller to node).
-    api_microversion = '1.28'
+    # (# v1.31: Support for updating inspect_interface).
+    api_microversion = '1.31'
     delete_node = False
+    wait_provisioning_state_interval = 1
 
-    def _verify_node_inspection_data(self):
-        _, node = self.baremetal_client.show_node(self.node['uuid'])
-
-        self.assertEqual(node['properties']['cpu_arch'], 'x86_64')
-        self.assertGreater(int(node['properties']['memory_mb']), 0)
-        self.assertGreater(int(node['properties']['cpus']), 0)
-        self.assertGreater(int(node['properties']['local_gb']), 0)
+    def _verify_node_inspection_data(self, node):
+        self.assertIn(node['properties']['cpu_arch'], ['x86_64', 'aarch64'])
 
     @decorators.idempotent_id('47ea4487-4720-43e8-a024-53ae82f8c264')
     def test_baremetal_inspect(self):
@@ -50,19 +43,48 @@
         """
         self.baremetal_client.set_node_provision_state(self.node['uuid'],
                                                        'manage')
+        _, node = self.baremetal_client.show_node(self.node['uuid'])
+        if 'cpu_arch' in node['properties']:
+            new_properties = node['properties'].copy()
+            new_properties.pop('cpu_arch')
+            self.baremetal_client.update_node(self.node['uuid'],
+                                              properties=new_properties)
+
         self.baremetal_client.set_node_provision_state(self.node['uuid'],
                                                        'inspect')
+        self.wait_provisioning_state(
+            self.node['uuid'], 'manageable',
+            timeout=CONF.baremetal.inspect_timeout,
+            interval=self.wait_provisioning_state_interval)
 
-        self.wait_provisioning_state(self.node['uuid'], 'manageable',
-                                     timeout=CONF.baremetal.inspect_timeout)
-
-        self._verify_node_inspection_data()
+        _, node = self.baremetal_client.show_node(self.node['uuid'])
+        self._verify_node_inspection_data(node)
 
         self.baremetal_client.set_node_provision_state(self.node['uuid'],
                                                        'provide')
         self.wait_provisioning_state(self.node['uuid'], 'available')
 
 
+class BaremetalRedfishAgentInspect(BaremetalInspectBase,
+                                   bsm.BaremetalStandaloneScenarioTest):
+    driver = 'redfish'
+    inspect_interface = 'agent'
+    wait_provisioning_state_interval = 15
+
+    # TODO(dtantsur): test aborting inspection and fetching inspection data
+
+
+class BaremetalIdracInspect(BaremetalInspectBase,
+                            bsm.BaremetalStandaloneScenarioTest):
+    driver = 'idrac'
+
+    def _verify_node_inspection_data(self, node):
+        super()._verify_node_inspection_data(node)
+        self.assertGreater(int(node['properties']['memory_mb']), 0)
+        self.assertGreater(int(node['properties']['cpus']), 0)
+        self.assertGreater(int(node['properties']['local_gb']), 0)
+
+
 class BaremetalIdracRedfishInspect(BaremetalIdracInspect):
     inspect_interface = 'idrac-redfish'