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