From e8f2120405a4ee5702c239fc65908097d4c6216a Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Mon, 15 Apr 2019 15:49:59 +0200 Subject: [PATCH] Add indicator management to redfish hw type Implements necessary indicator management calls to redfish driver to actually read/set system/chassis and drive LEDs through Redfish. The spec: https://review.opendev.org/#/c/655685/7/specs/approved/expose-hardware-indicators.rst Change-Id: Ib9ccdfa41974cd353af8a517dc2917f129e49e03 Story: 2005342 Task: 30471 --- ironic/drivers/modules/redfish/management.py | 209 ++++++++++++++++++ .../modules/redfish/test_management.py | 115 ++++++++++ .../drivers/third_party_driver_mock_specs.py | 12 + .../unit/drivers/third_party_driver_mocks.py | 4 + 4 files changed, 340 insertions(+) diff --git a/ironic/drivers/modules/redfish/management.py b/ironic/drivers/modules/redfish/management.py index f17f6c6545..591f46ae08 100644 --- a/ironic/drivers/modules/redfish/management.py +++ b/ironic/drivers/modules/redfish/management.py @@ -20,8 +20,10 @@ from oslo_utils import importutils from ironic.common import boot_devices from ironic.common import boot_modes +from ironic.common import components from ironic.common import exception from ironic.common.i18n import _ +from ironic.common import indicator_states from ironic.conductor import task_manager from ironic.drivers import base from ironic.drivers.modules.redfish import utils as redfish_utils @@ -55,6 +57,16 @@ if sushy: BOOT_DEVICE_PERSISTENT_MAP_REV = {v: k for k, v in BOOT_DEVICE_PERSISTENT_MAP.items()} + INDICATOR_MAP = { + sushy.INDICATOR_LED_LIT: indicator_states.ON, + sushy.INDICATOR_LED_OFF: indicator_states.OFF, + sushy.INDICATOR_LED_BLINKING: indicator_states.BLINKING, + sushy.INDICATOR_LED_UNKNOWN: indicator_states.UNKNOWN + } + + INDICATOR_MAP_REV = { + v: k for k, v in INDICATOR_MAP.items()} + class RedfishManagement(base.ManagementInterface): @@ -393,3 +405,200 @@ class RedfishManagement(base.ManagementInterface): 'error': e}) LOG.error(error_msg) raise exception.RedfishError(error=error_msg) + + def get_supported_indicators(self, task, component=None): + """Get a map of the supported indicators (e.g. LEDs). + + :param task: A task from TaskManager. + :param component: If not `None`, return indicator information + for just this component, otherwise return indicators for + all existing components. + :returns: A dictionary of hardware components + (:mod:`ironic.common.components`) as keys with values + being dictionaries having indicator IDs as keys and indicator + properties as values. + + :: + + { + 'chassis': { + 'enclosure-0': { + "readonly": true, + "states": [ + "OFF", + "ON" + ] + } + }, + 'system': + 'blade-A': { + "readonly": true, + "states": [ + "OFF", + "ON" + ] + } + }, + 'drive': + 'ssd0': { + "readonly": true, + "states": [ + "OFF", + "ON" + ] + } + } + } + """ + properties = { + "readonly": False, + "states": [ + indicator_states.BLINKING, + indicator_states.OFF, + indicator_states.ON + ] + } + + indicators = {} + + system = redfish_utils.get_system(task.node) + + try: + if component in (None, components.CHASSIS) and system.chassis: + indicators[components.CHASSIS] = { + chassis.uuid: properties for chassis in system.chassis + if chassis.indicator_led + } + + except sushy.exceptions.SushyError as e: + LOG.debug('Chassis indicator not available for node %(node)s: ' + '%(error)s', {'node': task.node.uuid, 'error': e}) + + try: + if component in (None, components.SYSTEM) and system.indicator_led: + indicators[components.SYSTEM] = { + system.uuid: properties + } + + except sushy.exceptions.SushyError as e: + LOG.debug('System indicator not available for node %(node)s: ' + '%(error)s', {'node': task.node.uuid, 'error': e}) + + try: + if (component in (None, components.DISK) and + system.simple_storage and system.simple_storage.drives): + indicators[components.DISK] = { + drive.uuid: properties + for drive in system.simple_storage.drives + if drive.indicator_led + } + + except sushy.exceptions.SushyError as e: + LOG.debug('Drive indicator not available for node %(node)s: ' + '%(error)s', {'node': task.node.uuid, 'error': e}) + + return indicators + + def set_indicator_state(self, task, component, indicator, state): + """Set indicator on the hardware component to the desired state. + + :param task: A task from TaskManager. + :param component: The hardware component, one of + :mod:`ironic.common.components`. + :param indicator: Indicator ID (as reported by + `get_supported_indicators`). + :param state: Desired state of the indicator, one of + :mod:`ironic.common.indicator_states`. + :raises: InvalidParameterValue if an invalid component, indicator + or state is specified. + :raises: MissingParameterValue if a required parameter is missing + :raises: RedfishError on an error from the Sushy library + """ + system = redfish_utils.get_system(task.node) + + try: + if (component == components.SYSTEM + and indicator == system.uuid): + system.set_indicator_led(INDICATOR_MAP_REV[state]) + return + + elif (component == components.CHASSIS + and system.chassis): + for chassis in system.chassis: + if chassis.uuid == indicator: + chassis.set_indicator_led( + INDICATOR_MAP_REV[state]) + return + + elif (component == components.DISK and + system.simple_storage and system.simple_storage.drives): + for drive in system.simple_storage.drives: + if drive.uuid == indicator: + drive.set_indicator_led( + INDICATOR_MAP_REV[state]) + return + + except sushy.exceptions.SushyError as e: + error_msg = (_('Redfish set %(component)s indicator %(indicator)s ' + 'state %(state)s failed for node %(node)s. Error: ' + '%(error)s') % {'component': component, + 'indicator': indicator, + 'state': state, + 'node': task.node.uuid, + 'error': e}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) + + raise exception.MissingParameterValue(_( + "Unknown indicator %(indicator)s for component %(component)s of " + "node %(uuid)s") % {'indicator': indicator, + 'component': component, + 'uuid': task.node.uuid}) + + def get_indicator_state(self, task, component, indicator): + """Get current state of the indicator of the hardware component. + + :param task: A task from TaskManager. + :param component: The hardware component, one of + :mod:`ironic.common.components`. + :param indicator: Indicator ID (as reported by + `get_supported_indicators`). + :raises: MissingParameterValue if a required parameter is missing + :raises: RedfishError on an error from the Sushy library + :returns: Current state of the indicator, one of + :mod:`ironic.common.indicator_states`. + """ + system = redfish_utils.get_system(task.node) + + try: + if (component == components.SYSTEM + and indicator == system.uuid): + return INDICATOR_MAP[system.indicator_led] + + if (component == components.CHASSIS + and system.chassis): + for chassis in system.chassis: + if chassis.uuid == indicator: + return INDICATOR_MAP[chassis.indicator_led] + + if (component == components.DISK and + system.simple_storage and system.simple_storage.drives): + for drive in system.simple_storage.drives: + if drive.uuid == indicator: + return INDICATOR_MAP[drive.indicator_led] + + except sushy.exceptions.SushyError as e: + error_msg = (_('Redfish get %(component)s indicator %(indicator)s ' + 'state failed for node %(node)s. Error: ' + '%(error)s') % {'component': component, + 'indicator': indicator, + 'node': task.node.uuid, + 'error': e}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) + + raise exception.MissingParameterValue(_( + "Unknown indicator %(indicator)s for component %(component)s of " + "node %(uuid)s") % {'indicator': indicator, + 'component': component, + 'uuid': task.node.uuid}) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py index 170557f28b..e05f5626da 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_management.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py @@ -18,7 +18,9 @@ from oslo_utils import importutils from ironic.common import boot_devices from ironic.common import boot_modes +from ironic.common import components from ironic.common import exception +from ironic.common import indicator_states from ironic.conductor import task_manager from ironic.drivers.modules.redfish import management as redfish_mgmt from ironic.drivers.modules.redfish import utils as redfish_utils @@ -44,6 +46,10 @@ class RedfishManagementTestCase(db_base.DbTestCase): self.node = obj_utils.create_test_node( self.context, driver='redfish', driver_info=INFO_DICT) + self.system_uuid = 'ZZZ--XXX-YYY' + self.chassis_uuid = 'XXX-YYY-ZZZ' + self.drive_uuid = 'ZZZ-YYY-XXX' + @mock.patch.object(redfish_mgmt, 'sushy', None) def test_loading_error(self): self.assertRaisesRegex( @@ -437,3 +443,112 @@ class RedfishManagementTestCase(db_base.DbTestCase): fake_system.reset_system.assert_called_once_with( sushy.RESET_NMI) mock_get_system.assert_called_once_with(task.node) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_get_supported_indicators(self, mock_get_system): + fake_chassis = mock.Mock( + uuid=self.chassis_uuid, + indicator_led=sushy.INDICATOR_LED_LIT) + fake_drive = mock.Mock( + uuid=self.drive_uuid, + indicator_led=sushy.INDICATOR_LED_LIT) + fake_system = mock.Mock( + uuid=self.system_uuid, + chassis=[fake_chassis], + simple_storage=mock.MagicMock(drives=[fake_drive]), + indicator_led=sushy.INDICATOR_LED_LIT) + + mock_get_system.return_value = fake_system + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + supported_indicators = ( + task.driver.management.get_supported_indicators(task)) + + expected = { + components.CHASSIS: { + 'XXX-YYY-ZZZ': { + "readonly": False, + "states": [ + indicator_states.BLINKING, + indicator_states.OFF, + indicator_states.ON + ] + } + }, + components.SYSTEM: { + 'ZZZ--XXX-YYY': { + "readonly": False, + "states": [ + indicator_states.BLINKING, + indicator_states.OFF, + indicator_states.ON + ] + } + }, + components.DISK: { + 'ZZZ-YYY-XXX': { + "readonly": False, + "states": [ + indicator_states.BLINKING, + indicator_states.OFF, + indicator_states.ON + ] + } + } + } + + self.assertEqual(expected, supported_indicators) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_set_indicator_state(self, mock_get_system): + fake_chassis = mock.Mock( + uuid=self.chassis_uuid, + indicator_led=sushy.INDICATOR_LED_LIT) + fake_drive = mock.Mock( + uuid=self.drive_uuid, + indicator_led=sushy.INDICATOR_LED_LIT) + fake_system = mock.Mock( + uuid=self.system_uuid, + chassis=[fake_chassis], + simple_storage=mock.MagicMock(drives=[fake_drive]), + indicator_led=sushy.INDICATOR_LED_LIT) + + mock_get_system.return_value = fake_system + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_indicator_state( + task, components.SYSTEM, self.system_uuid, indicator_states.ON) + + fake_system.set_indicator_led.assert_called_once_with( + sushy.INDICATOR_LED_LIT) + + mock_get_system.assert_called_once_with(task.node) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_get_indicator_state(self, mock_get_system): + fake_chassis = mock.Mock( + uuid=self.chassis_uuid, + indicator_led=sushy.INDICATOR_LED_LIT) + fake_drive = mock.Mock( + uuid=self.drive_uuid, + indicator_led=sushy.INDICATOR_LED_LIT) + fake_system = mock.Mock( + uuid=self.system_uuid, + chassis=[fake_chassis], + simple_storage=mock.MagicMock(drives=[fake_drive]), + indicator_led=sushy.INDICATOR_LED_LIT) + + mock_get_system.return_value = fake_system + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + state = task.driver.management.get_indicator_state( + task, components.SYSTEM, self.system_uuid) + + mock_get_system.assert_called_once_with(task.node) + + self.assertEqual(indicator_states.ON, state) diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index 6699c74ffc..2a0378f48d 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -119,6 +119,18 @@ SUSHY_SPEC = ( 'BOOT_SOURCE_TARGET_HDD', 'BOOT_SOURCE_TARGET_CD', 'BOOT_SOURCE_TARGET_BIOS_SETUP', + 'CHASSIS_INDICATOR_LED_LIT', + 'CHASSIS_INDICATOR_LED_BLINKING', + 'CHASSIS_INDICATOR_LED_OFF', + 'CHASSIS_INDICATOR_LED_UNKNOWN', + 'DRIVE_INDICATOR_LED_LIT', + 'DRIVE_INDICATOR_LED_BLINKING', + 'DRIVE_INDICATOR_LED_OFF', + 'DRIVE_INDICATOR_LED_UNKNOWN', + 'INDICATOR_LED_LIT', + 'INDICATOR_LED_BLINKING', + 'INDICATOR_LED_OFF', + 'INDICATOR_LED_UNKNOWN', 'SYSTEM_POWER_STATE_ON', 'SYSTEM_POWER_STATE_POWERING_ON', 'SYSTEM_POWER_STATE_OFF', diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index e0e5385d6f..960846189e 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -181,6 +181,10 @@ if not sushy: BOOT_SOURCE_TARGET_HDD='Hdd', BOOT_SOURCE_TARGET_CD='Cd', BOOT_SOURCE_TARGET_BIOS_SETUP='BiosSetup', + INDICATOR_LED_LIT='indicator led lit', + INDICATOR_LED_BLINKING='indicator led blinking', + INDICATOR_LED_OFF='indicator led off', + INDICATOR_LED_UNKNOWN='indicator led unknown', SYSTEM_POWER_STATE_ON='on', SYSTEM_POWER_STATE_POWERING_ON='powering on', SYSTEM_POWER_STATE_OFF='off',