Merge "Swap maintenance fix to cleanup step"
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 1d705f4..01c2a4b 100755
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -41,7 +41,7 @@
 master_doc = 'index'
 
 # General information about the project.
-copyright = u'2016, OpenStack Foundation'
+copyright = '2016, OpenStack Foundation'
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
 add_function_parentheses = True
@@ -70,8 +70,8 @@
 latex_documents = [
     ('index',
      'doc-ironic-tempest-plugin.tex',
-     u'Ironic Tempest Plugin Documentation',
-     u'OpenStack Foundation', 'manual'),
+     'Ironic Tempest Plugin Documentation',
+     'OpenStack Foundation', 'manual'),
 ]
 
 # Example configuration for intersphinx: refer to the Python standard library.
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index d5c1ebf..19070b3 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -79,6 +79,9 @@
                         'publicURL', 'adminURL', 'internalURL'],
                help="The endpoint type to use for the baremetal provisioning"
                     " service"),
+    cfg.StrOpt('root_device_name',
+               default='/dev/md0',
+               help="Root device name to be used for node deployment"),
     cfg.IntOpt('deploywait_timeout',
                default=15,
                help="Timeout for Ironic node to reach the "
@@ -134,6 +137,8 @@
     cfg.StrOpt('ramdisk_iso_image_ref',
                help=("UUID (or url) of an ISO image for the ramdisk boot "
                      "tests.")),
+    cfg.StrOpt('storage_inventory_file',
+               help="Path to storage inventory file for RAID cleaning tests."),
     cfg.ListOpt('enabled_drivers',
                 default=['fake', 'pxe_ipmitool', 'agent_ipmitool'],
                 help="List of Ironic enabled drivers."),
@@ -163,6 +168,18 @@
                 help="List of Ironic enabled power interfaces."),
     cfg.StrOpt('default_rescue_interface',
                help="Ironic default rescue interface."),
+    cfg.StrOpt('firmware_image_url',
+               help="Image URL of firmware image file supported by "
+                    "update_firmware clean step."),
+    cfg.StrOpt('firmware_image_checksum',
+               help="SHA1 checksum of firmware image file."),
+    cfg.StrOpt('firmware_rollback_image_url',
+               help="Image URL of firmware update cleaning step's "
+                    "rollback image. Optional. If not provided, "
+                    "rollback is skipped."),
+    cfg.StrOpt('firmware_rollback_image_checksum',
+               help="SHA1 checksum of firmware rollback image file. "
+                    "This is required if firmware_rollback_image_url is set."),
     cfg.IntOpt('adjusted_root_disk_size_gb',
                min=0,
                help="Ironic adjusted disk size to use in the standalone tests "
diff --git a/ironic_tempest_plugin/exceptions.py b/ironic_tempest_plugin/exceptions.py
index ac08d54..50a4468 100644
--- a/ironic_tempest_plugin/exceptions.py
+++ b/ironic_tempest_plugin/exceptions.py
@@ -23,3 +23,7 @@
 
 class HypervisorUpdateTimeout(exceptions.TempestException):
     message = "Hypervisor stats update time out"
+
+
+class RaidCleaningInventoryValidationFailed(exceptions.TempestException):
+    message = "RAID cleaning storage inventory validation failed"
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index e4d8717..27568a3 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -642,7 +642,8 @@
         self.assertTrue(self.ping_ip_address(self.node_ip,
                                              should_succeed=should_succeed))
 
-    def build_raid_and_verify_node(self, config=None, deploy_time=False):
+    def build_raid_and_verify_node(self, config=None, deploy_time=False,
+                                   erase_device_metadata=True):
         config = config or self.raid_config
         if deploy_time:
             steps = [
@@ -671,14 +672,14 @@
                     "step": "delete_configuration"
                 },
                 {
-                    "interface": "deploy",
-                    "step": "erase_devices_metadata",
-                },
-                {
                     "interface": "raid",
                     "step": "create_configuration",
                 }
             ]
+            if erase_device_metadata:
+                steps.insert(1, {
+                    "interface": "deploy",
+                    "step": "erase_devices_metadata"})
             self.baremetal_client.set_node_raid_config(self.node['uuid'],
                                                        config)
             self.manual_cleaning(self.node, clean_steps=steps)
@@ -686,12 +687,14 @@
         # The node has been changed, anything at this point, we need to back
         # out the raid configuration.
         if not deploy_time:
-            self.addCleanup(self.remove_raid_configuration, self.node)
+            self.addCleanup(self.remove_raid_configuration, self.node,
+                            erase_device_metadata=erase_device_metadata)
 
         # NOTE(dtantsur): this is not required, but it allows us to check that
         # the RAID device was in fact created and is used for deployment.
         patch = [{'path': '/properties/root_device',
-                  'op': 'add', 'value': {'name': '/dev/md0'}}]
+                  'op': 'add', 'value': {
+                      'name': CONF.baremetal.root_device_name}}]
         if deploy_time:
             patch.append({'path': '/instance_info/traits',
                           'op': 'add', 'value': ['CUSTOM_RAID']})
@@ -707,18 +710,18 @@
                   'op': 'remove'}]
         self.update_node(self.node['uuid'], patch=patch)
 
-    def remove_raid_configuration(self, node):
+    def remove_raid_configuration(self, node, erase_device_metadata=True):
         self.baremetal_client.set_node_raid_config(node['uuid'], {})
         steps = [
             {
                 "interface": "raid",
                 "step": "delete_configuration",
-            },
-            {
-                "interface": "deploy",
-                "step": "erase_devices_metadata",
             }
         ]
+        if erase_device_metadata:
+            steps.append({
+                "interface": "deploy",
+                "step": "erase_devices_metadata"})
         self.manual_cleaning(node, clean_steps=steps)
 
     def rescue_unrescue(self):
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/storage_inventory_schema.json b/ironic_tempest_plugin/tests/scenario/ironic_standalone/storage_inventory_schema.json
new file mode 100644
index 0000000..6e15b01
--- /dev/null
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/storage_inventory_schema.json
@@ -0,0 +1,101 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "Storage inventory JSON schema",
+    "type": "object",
+    "properties": {
+        "storage_inventory": {
+            "type": "object",
+            "properties": {
+                "controllers": {
+                    "type": ["array", "null"],
+                    "items": {
+                        "type": "object",
+                        "properties": {
+                            "id": {
+                                "description": "The unique identifier for this storage controller.",
+                                "type": "string",
+                                "minLength": 1
+                            },
+                            "serial_number": {
+                                "description": "The serial number for this storage controller.",
+                                "type": ["string", "null"]
+                            },
+                            "manufacturer": {
+                                "description": "The manufacturer of this storage controller.",
+                                "type": ["string", "null"]
+                            },
+                            "model": {
+                                "description": "The model of the storage controller.",
+                                "type": ["string", "null"]
+                            },
+                            "supported_device_protocols": {
+                                "description": "The protocols that the storage controller can use to communicate with attached devices.",
+                                "type": ["array", "null"],
+                                "items": {
+                                    "type": "string",
+                                    "enum": ["sas", "sata", "scsi"]
+                                },
+                                "minItems": 1
+                            },
+                            "supported_raid_types": {
+                                "description": "The set of RAID types supported by the storage controller.",
+                                "type": ["array", "null"],
+                                "items": {
+                                    "type": "string",
+                                    "enum": ["JBOD", "0", "1", "2", "5", "6", "1+0", "5+0", "6+0"]
+                                },
+                                "minItems": 1
+                            },
+                            "drives": {
+                                "type": "array",
+                                "items": {
+                                    "type": "object",
+                                    "properties": {
+                                        "id": {
+                                            "description": "The unique identifier for the physical drive.",
+                                            "type": "string",
+                                            "minLength": 1
+                                        },
+                                        "size_gb": {
+                                            "description": "The size in GiB of the physical drive.",
+                                            "type": ["number", "null"],
+                                            "minimum": 0
+                                        },
+                                        "model": {
+                                            "description": "The model for the physical drive.",
+                                            "type": ["string", "null"]
+                                        },
+                                        "media_type": {
+                                            "description": "The media type for the physical drive.",
+                                            "enum": ["hdd", "ssd", null]
+                                        },
+                                        "serial_number": {
+                                            "description": "The serial number for the physical drive.",
+                                            "type": ["string", "null"]
+                                        },
+                                        "protocol": {
+                                            "description": "The protocol that this drive currently uses to communicate to the storage controller.",
+                                            "enum": ["sas", "sata", "scsi", null]
+                                        }
+                                    },
+                                    "required": ["id", "size_gb", "model", "media_type", "serial_number", "protocol"],
+                                    "additionalProperties": false
+                                }
+                            }
+                        },
+                        "required": ["id", "serial_number", "manufacturer", "model", "supported_device_protocols", "supported_raid_types"],
+                        "additionalProperties": false,
+                        "dependencies": {
+                            "drives": ["id"]
+                        }
+                    },
+                    "minItems": 1
+                }
+            },
+            "required": ["controllers"],
+            "additionalProperties": false
+        }
+    },
+    "required": ["storage_inventory"],
+    "additionalProperties": false
+}
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
index c12d3d7..8ad613f 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
@@ -1,6 +1,8 @@
 #
 # Copyright 2017 Mirantis Inc.
 #
+# Copyright (c) 2022 Dell Inc. or its subsidiaries.
+#
 # 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
@@ -582,3 +584,20 @@
     @decorators.idempotent_id('d68a38aa-b731-403a-9d40-b3b49ea75e9b')
     def test_deploy_node(self):
         self.boot_and_verify_node()
+
+
+class BaremetalIdracVirtualMediaWholedisk(
+        bsm.BaremetalStandaloneScenarioTest):
+
+    mandatory_attr = ['driver', 'boot_interface']
+    api_microversion = '1.31'   # to set the deploy_interface
+    driver = 'idrac'
+    boot_interface = 'idrac-redfish-virtual-media'
+    image_ref = CONF.baremetal.whole_disk_image_ref
+    wholedisk_image = True
+    deploy_interface = 'direct'
+
+    @decorators.idempotent_id('b0bc87a5-4324-4134-bd5f-4bb1cf549e5c')
+    @utils.services('image', 'network')
+    def test_deploy_virtual_media_boot(self):
+        self.boot_and_verify_node()
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py
index 13fcd9b..f03d665 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py
@@ -15,11 +15,17 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import json
+import os
+
+import jsonschema
+from jsonschema import exceptions as json_schema_exc
 from oslo_log import log as logging
 from tempest.common import utils
 from tempest import config
 from tempest.lib import decorators
 
+from ironic_tempest_plugin import exceptions
 from ironic_tempest_plugin.tests.scenario import \
     baremetal_standalone_manager as bsm
 
@@ -227,3 +233,186 @@
 
     management_interface = 'idrac-wsman'
     power_interface = 'idrac-wsman'
+
+
+class BaremetalIdracRaidCleaning(bsm.BaremetalStandaloneScenarioTest):
+
+    mandatory_attr = ['driver', 'raid_interface']
+    image_ref = CONF.baremetal.whole_disk_image_ref
+    wholedisk_image = True
+    storage_inventory_info = None
+    driver = 'idrac'
+    api_microversion = '1.31'  # to set raid_interface
+    delete_node = False
+
+    @classmethod
+    def skip_checks(cls):
+        """Validates the storage information passed in file using JSON schema.
+
+        :raises: skipException if,
+            1) storage inventory path is not provided in tempest execution
+            file.
+            2) storage inventory file is not found on given path.
+        :raises: RaidCleaningInventoryValidationFailed if,
+            validation of the storage inventory fails.
+        """
+        super(BaremetalIdracRaidCleaning, cls).skip_checks()
+        storage_inventory = CONF.baremetal.storage_inventory_file
+        if not storage_inventory:
+            raise cls.skipException("Storage inventory file path missing "
+                                    "in tempest configuration file. "
+                                    "Skipping Test case.")
+        try:
+            with open(storage_inventory, 'r') as storage_invent_fobj:
+                cls.storage_inventory_info = json.load(storage_invent_fobj)
+        except IOError:
+            msg = ("Storage Inventory file %(inventory)s is not found. "
+                   "Skipping Test Case." %
+                   {'inventory': storage_inventory})
+            raise cls.skipException(msg)
+        storage_inventory_schema = os.path.join(os.path.dirname(
+            __file__), 'storage_inventory_schema.json')
+        with open(storage_inventory_schema, 'r') as storage_schema_fobj:
+            schema = json.load(storage_schema_fobj)
+        try:
+            jsonschema.validate(cls.storage_inventory_info, schema)
+        except json_schema_exc.ValidationError as e:
+            error_msg = ("Storage Inventory validation error: %(error)s " %
+                         {'error': e})
+            raise exceptions.RaidCleaningInventoryValidationFailed(error_msg)
+
+    def _validate_raid_type_and_drives_count(self, raid_type,
+                                             minimum_drives_required):
+        for controller in (self.storage_inventory_info[
+                           'storage_inventory']['controllers']):
+            supported_raid_types = controller['supported_raid_types']
+            physical_disks = [pdisk['id'] for pdisk in (
+                controller['drives'])]
+            if raid_type in supported_raid_types and (
+                    minimum_drives_required <= len(physical_disks)):
+                return controller
+        error_msg = ("No Controller present in storage inventory which "
+                     "supports RAID type %(raid_type)s "
+                     "and has at least %(disk_count)s drives." %
+                     {'raid_type': raid_type,
+                      'disk_count': minimum_drives_required})
+        raise exceptions.RaidCleaningInventoryValidationFailed(error_msg)
+
+    @decorators.idempotent_id('8a908a3c-f2af-48fb-8553-9163715aa403')
+    @utils.services('image', 'network')
+    def test_hardware_raid(self):
+        controller = self._validate_raid_type_and_drives_count(
+            raid_type='1', minimum_drives_required=2)
+        raid_config = {
+            "logical_disks": [
+                {
+                    "size_gb": 40,
+                    "raid_level": "1",
+                    "controller": controller['id']
+                }
+            ]
+        }
+        self.build_raid_and_verify_node(
+            config=raid_config,
+            deploy_time=CONF.baremetal_feature_enabled.deploy_time_raid,
+            erase_device_metadata=False)
+        self.remove_root_device_hint()
+        self.terminate_node(self.node['uuid'], force_delete=True)
+
+    @decorators.idempotent_id('92fe534d-77f1-422d-84e4-e30fe9e3d928')
+    @utils.services('image', 'network')
+    def test_raid_cleaning_max_size_raid_10(self):
+        controller = self._validate_raid_type_and_drives_count(
+            raid_type='1+0', minimum_drives_required=4)
+        physical_disks = [pdisk['id'] for pdisk in (
+            controller['drives'])]
+        raid_config = {
+            "logical_disks": [
+                {
+                    "size_gb": "MAX",
+                    "raid_level": "1+0",
+                    "controller": controller['id'],
+                    "physical_disks": physical_disks
+                }
+            ]
+        }
+        self.build_raid_and_verify_node(
+            config=raid_config,
+            deploy_time=CONF.baremetal_feature_enabled.deploy_time_raid,
+            erase_device_metadata=False)
+        self.remove_root_device_hint()
+        self.terminate_node(self.node['uuid'], force_delete=True)
+
+
+class BaremetalIdracRedfishRaidCleaning(
+        BaremetalIdracRaidCleaning):
+    raid_interface = 'idrac-redfish'
+
+
+class BaremetalIdracWSManRaidCleaning(
+        BaremetalIdracRaidCleaning):
+    raid_interface = 'idrac-wsman'
+
+
+class BaremetalRedfishFirmwareUpdate(bsm.BaremetalStandaloneScenarioTest):
+
+    api_microversion = '1.68'  # to support redfish firmware update
+    driver = 'redfish'
+    delete_node = False
+    image_ref = CONF.baremetal.whole_disk_image_ref
+    wholedisk_image = True
+
+    @classmethod
+    def skip_checks(cls):
+        super(BaremetalRedfishFirmwareUpdate, cls).skip_checks()
+        if not CONF.baremetal.firmware_image_url:
+            raise cls.skipException("Firmware image URL is not "
+                                    "provided. Skipping test case.")
+        if not CONF.baremetal.firmware_image_checksum:
+            raise cls.skipException("Firmware image SHA1 checksum is not "
+                                    "provided. Skipping test case.")
+
+    def _firmware_update(self, fw_image_url, fw_image_checksum):
+        steps = [
+            {
+                "interface": "management",
+                "step": "update_firmware",
+                "args": {
+                    "firmware_images": [
+                        {
+                            "url": fw_image_url,
+                            "checksum": fw_image_checksum,
+                            "wait": 300
+                        }
+                    ]
+                }
+            }
+        ]
+        self.manual_cleaning(self.node, clean_steps=steps)
+
+    @utils.services('network')
+    @decorators.idempotent_id('360e0c0e-3c17-4d2e-b052-55a932c1a4c7')
+    def test_firmware_update(self):
+        # WARNING: Removing power from a server while it is in the process of
+        # updating firmware may result in devices in the server, or the server
+        # itself becoming inoperable.
+        # Execution of firmware test case needs careful execution as it
+        # changes state of server, may result in break down of server on
+        # interruption. As it deals with firmware of component, make sure
+        # to provide proper image url path while testing with correct SHA1
+        # checksum of image.
+        self._firmware_update(CONF.baremetal.firmware_image_url,
+                              CONF.baremetal.firmware_image_checksum)
+        if (CONF.baremetal.firmware_rollback_image_url
+                and CONF.baremetal.firmware_rollback_image_checksum):
+            self.addCleanup(self._firmware_update,
+                            CONF.baremetal.firmware_rollback_image_url,
+                            CONF.baremetal.firmware_rollback_image_checksum)
+
+
+class BaremetalIdracRedfishFirmwareUpdate(BaremetalRedfishFirmwareUpdate):
+
+    driver = 'idrac'
+    boot_interface = 'ipxe'
+    management_interface = 'idrac-redfish'
+    power_interface = 'idrac-redfish'
diff --git a/requirements.txt b/requirements.txt
index e012abf..cde72ba 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,4 +8,4 @@
 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
 oslo.utils>=3.33.0 # Apache-2.0
 fixtures>=3.0.0 # Apache-2.0/BSD
-tempest>=17.1.0 # Apache-2.0
+tempest>=27.0.0 # Apache-2.0