Add a test for software RAID

Change-Id: Idef3f137dbeeaa5f84675cb31f390836f6f4af1a
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index dc709b6..d5cdcfa 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -132,6 +132,9 @@
     cfg.ListOpt('enabled_boot_interfaces',
                 default=['fake', 'pxe'],
                 help="List of Ironic enabled boot interfaces."),
+    cfg.ListOpt('enabled_raid_interfaces',
+                default=['no-raid', 'agent'],
+                help="List of Ironic enabled RAID interfaces."),
     cfg.StrOpt('default_rescue_interface',
                help="Ironic default rescue interface."),
     cfg.IntOpt('adjusted_root_disk_size_gb',
@@ -152,6 +155,11 @@
                 # requires the plugin to be able to read ipmi_password.
                 default=False,
                 help="Defines if adoption is enabled"),
+    cfg.BoolOpt('software_raid',
+                default=False,
+                help="Defines if software RAID is enabled (available "
+                     "starting with Train). Requires at least two disks "
+                     "on testing nodes."),
 ]
 
 BaremetalIntrospectionGroup = [
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 8d5fec5..9f001b8 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -509,6 +509,7 @@
                            'driver',
                            'bios_interface',
                            'deploy_interface',
+                           'raid_interface',
                            'rescue_interface',
                            'instance_uuid',
                            'resource_class',
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index 4cf500d..bbd7782 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -424,6 +424,12 @@
     # set via a different test).
     boot_interface = None
 
+    # The raid interface to use by the HW type. The raid interface of the
+    # node used in the test will be set to this value. If set to None, the
+    # node will retain its existing raid_interface value (which may have been
+    # set via a different test).
+    raid_interface = None
+
     # Boolean value specify if image is wholedisk or not.
     wholedisk_image = None
 
@@ -476,6 +482,13 @@
                 "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 not cls.wholedisk_image and CONF.baremetal.use_provision_network:
             raise cls.skipException(
                 'Partitioned images are not supported with multitenancy.')
@@ -512,6 +525,8 @@
             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
 
         # just get an available node
         cls.node = cls.get_and_reserve_node()
@@ -540,3 +555,34 @@
         self.set_node_to_active(image_ref, image_checksum)
         self.assertTrue(self.ping_ip_address(self.node_ip,
                                              should_succeed=should_succeed))
+
+    def build_raid_and_verify_node(self, config=None, clean_steps=None):
+        config = config or self.raid_config
+        clean_steps = clean_steps or [
+            {
+                "interface": "raid",
+                "step": "delete_configuration"
+            },
+            # NOTE(dtantsur): software RAID building fails if any
+            # partitions exist on holder devices.
+            {
+                "interface": "deploy",
+                "step": "erase_devices_metadata"
+            },
+            {
+                "interface": "raid",
+                "step": "create_configuration"
+            }
+        ]
+
+        self.baremetal_client.set_node_raid_config(self.node['uuid'], config)
+        self.manual_cleaning(self.node, clean_steps=clean_steps)
+
+        # 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'}}]
+        self.update_node(self.node['uuid'], patch=patch)
+        # NOTE(dtantsur): apparently cirros cannot boot from md devices :(
+        # So we only move the node to active (verifying deployment).
+        self.set_node_to_active()
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 e30b9ef..7df98ce 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py
@@ -69,3 +69,66 @@
     @utils.services('image', 'network')
     def test_manual_cleaning(self):
         self.check_manual_partition_cleaning(self.node)
+
+
+class SoftwareRaidIscsi(bsm.BaremetalStandaloneScenarioTest):
+
+    driver = 'ipmi'
+    image_ref = CONF.baremetal.whole_disk_image_ref
+    wholedisk_image = True
+    deploy_interface = 'iscsi'
+    raid_interface = 'agent'
+    api_microversion = '1.31'
+
+    raid_config = {
+        "logical_disks": [
+            {
+                "size_gb": "MAX",
+                "raid_level": "1",
+                "controller": "software"
+            },
+        ]
+    }
+
+    @classmethod
+    def skip_checks(cls):
+        super(SoftwareRaidIscsi, cls).skip_checks()
+        if not CONF.baremetal_feature_enabled.software_raid:
+            raise cls.skipException("Software RAID feature is not enabled")
+
+    @decorators.idempotent_id('7ecba4f7-98b8-4ea1-b95e-3ec399f46798')
+    @utils.services('image', 'network')
+    def test_software_raid(self):
+        self.build_raid_and_verify_node()
+
+
+class SoftwareRaidDirect(bsm.BaremetalStandaloneScenarioTest):
+
+    driver = 'ipmi'
+    image_ref = CONF.baremetal.whole_disk_image_ref
+    wholedisk_image = True
+    deploy_interface = 'direct'
+    raid_interface = 'agent'
+    api_microversion = '1.31'
+
+    # TODO(dtantsur): more complex layout in this job
+    raid_config = {
+        "logical_disks": [
+            {
+                "size_gb": "MAX",
+                "raid_level": "1",
+                "controller": "software"
+            },
+        ]
+    }
+
+    @classmethod
+    def skip_checks(cls):
+        super(SoftwareRaidDirect, cls).skip_checks()
+        if not CONF.baremetal_feature_enabled.software_raid:
+            raise cls.skipException("Software RAID feature is not enabled")
+
+    @decorators.idempotent_id('125361ac-0eb3-4d79-8be2-a91936aa3f46')
+    @utils.services('image', 'network')
+    def test_software_raid(self):
+        self.build_raid_and_verify_node()