From d3e4fe50b5ba679383daca5b203e70806c2398d3 Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Wed, 3 Apr 2019 19:13:10 +0200 Subject: [PATCH] Add indicator management harness to ManagementInterface Adds ``get_indicator_state``, ``set_indicator_state`` and ``get_supported_indicators`` methods to driver management interface. Drivers can override these methods implementing indicators (e.g. LEDs) management calls against the BMC of the baremetal nodes being managed. The spec: https://review.opendev.org/#/c/655685/7/specs/approved/expose-hardware-indicators.rst Change-Id: I952de78312f09897e6b743e11c9cc5dac977dc36 Story: 2005342 Task: 30311 --- ironic/common/components.py | 33 ++++++++ ironic/common/indicator_states.py | 30 +++++++ ironic/drivers/base.py | 83 +++++++++++++++++++ ironic/drivers/modules/fake.py | 40 +++++++++ ironic/tests/unit/drivers/test_base.py | 49 +++++++++++ .../tests/unit/drivers/test_fake_hardware.py | 41 +++++++++ 6 files changed, 276 insertions(+) create mode 100644 ironic/common/components.py create mode 100644 ironic/common/indicator_states.py diff --git a/ironic/common/components.py b/ironic/common/components.py new file mode 100644 index 0000000000..d9136c1962 --- /dev/null +++ b/ironic/common/components.py @@ -0,0 +1,33 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Mapping of common hardware components of a computer system. +""" + +CHASSIS = 'chassis' +"""Chassis enclosing one or more hardware components""" + +SYSTEM = 'system' +"""Computing system""" + +DISK = 'disk' +"""Storage drive""" + +POWER = 'power' +"""Power supply unit""" + +NIC = 'nic' +"""Network interface""" diff --git a/ironic/common/indicator_states.py b/ironic/common/indicator_states.py new file mode 100644 index 0000000000..592e30cc00 --- /dev/null +++ b/ironic/common/indicator_states.py @@ -0,0 +1,30 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Mapping of the indicator LED states. +""" + +OFF = 'off' +"""LED is off""" + +ON = 'on' +"""LED is on""" + +BLINKING = 'blinking' +"""LED is blinking""" + +UNKNOWN = 'unknown' +"""LED state is not known""" diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py index 44175fdbaa..81ee2006cf 100644 --- a/ironic/drivers/base.py +++ b/ironic/drivers/base.py @@ -1000,6 +1000,89 @@ class ManagementInterface(BaseInterface): raise exception.UnsupportedDriverExtension( driver=task.node.driver, extension='inject_nmi') + 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": [ + "pff", + "on" + ] + } + }, + 'drive': + 'ssd0': { + "readonly": true, + "states": [ + "off", + "on" + ] + } + } + } + + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='get_supported_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`). + :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 + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='set_indicator_state') + + 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: InvalidParameterValue if an invalid component or indicator + is specified. + :raises: MissingParameterValue if a required parameter is missing + :returns: Current state of the indicator, one of + :mod:`ironic.common.indicator_states`. + + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='get_indicator_state') + class InspectInterface(BaseInterface): """Interface for inspection-related actions.""" diff --git a/ironic/drivers/modules/fake.py b/ironic/drivers/modules/fake.py index acb3a59835..dffd9065df 100644 --- a/ironic/drivers/modules/fake.py +++ b/ironic/drivers/modules/fake.py @@ -27,8 +27,10 @@ on separate vendor_passthru methods. from oslo_log import log from ironic.common import boot_devices +from ironic.common import components from ironic.common import exception from ironic.common.i18n import _ +from ironic.common import indicator_states from ironic.common import states from ironic.drivers import base from ironic import objects @@ -219,6 +221,44 @@ class FakeManagement(base.ManagementInterface): def get_sensors_data(self, task): return {} + def get_supported_indicators(self, task, component=None): + indicators = { + components.CHASSIS: { + 'led-0': { + "readonly": True, + "states": [ + indicator_states.OFF, + indicator_states.ON + ] + } + }, + components.SYSTEM: { + 'led': { + "readonly": False, + "states": [ + indicator_states.BLINKING, + indicator_states.OFF, + indicator_states.ON + ] + } + } + } + + return {c: indicators[c] for c in indicators + if not component or component == c} + + def get_indicator_state(self, task, component, indicator): + indicators = self.get_supported_indicators(task) + if component not in indicators: + raise exception.InvalidParameterValue(_( + "Invalid component %s specified.") % component) + + if indicator not in indicators[component]: + raise exception.InvalidParameterValue(_( + "Invalid indicator %s specified.") % indicator) + + return indicator_states.ON + class FakeInspect(base.InspectInterface): diff --git a/ironic/tests/unit/drivers/test_base.py b/ironic/tests/unit/drivers/test_base.py index 1cf00fc10b..19fca2067b 100644 --- a/ironic/tests/unit/drivers/test_base.py +++ b/ironic/tests/unit/drivers/test_base.py @@ -17,7 +17,9 @@ import json import mock +from ironic.common import components from ironic.common import exception +from ironic.common import indicator_states from ironic.common import raid from ironic.common import states from ironic.drivers import base as driver_base @@ -783,6 +785,53 @@ class TestManagementInterface(base.TestCase): self.assertRaises(exception.UnsupportedDriverExtension, management.get_boot_mode, task_mock) + def test_get_supported_indicators_default_impl(self): + management = fake.FakeManagement() + task_mock = mock.MagicMock(spec_set=['node']) + + expected = { + components.CHASSIS: { + 'led-0': { + "readonly": True, + "states": [ + indicator_states.OFF, + indicator_states.ON + ] + } + }, + components.SYSTEM: { + 'led': { + "readonly": False, + "states": [ + indicator_states.BLINKING, + indicator_states.OFF, + indicator_states.ON + ] + } + } + } + + self.assertEqual( + expected, management.get_supported_indicators(task_mock)) + + def test_set_indicator_state_default_impl(self): + management = fake.FakeManagement() + task_mock = mock.MagicMock(spec_set=['node']) + + self.assertRaises(exception.UnsupportedDriverExtension, + management.set_indicator_state, task_mock, + components.CHASSIS, 'led-0', indicator_states.ON) + + def test_get_indicator_state_default_impl(self): + management = fake.FakeManagement() + task_mock = mock.MagicMock(spec_set=['node']) + + expected = indicator_states.ON + + self.assertEqual( + expected, management.get_indicator_state( + task_mock, components.CHASSIS, 'led-0')) + class TestBareDriver(base.TestCase): diff --git a/ironic/tests/unit/drivers/test_fake_hardware.py b/ironic/tests/unit/drivers/test_fake_hardware.py index 792940de0a..cf4f9a904f 100644 --- a/ironic/tests/unit/drivers/test_fake_hardware.py +++ b/ironic/tests/unit/drivers/test_fake_hardware.py @@ -20,7 +20,9 @@ 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.common import states from ironic.conductor import task_manager from ironic.drivers import base as driver_base @@ -119,6 +121,45 @@ class FakeHardwareTestCase(db_base.DbTestCase): self.task, boot_modes.LEGACY_BIOS ) + def test_management_interface_get_supported_indicators(self): + expected = { + components.CHASSIS: { + 'led-0': { + "readonly": True, + "states": [ + indicator_states.OFF, + indicator_states.ON + ] + } + }, + components.SYSTEM: { + 'led': { + "readonly": False, + "states": [ + indicator_states.BLINKING, + indicator_states.OFF, + indicator_states.ON + ] + } + } + } + + self.assertEqual( + expected, + self.driver.management.get_supported_indicators(self.task)) + + def test_management_interface_get_indicator_state(self): + expected = indicator_states.ON + self.assertEqual( + expected, self.driver.management.get_indicator_state( + self.task, components.CHASSIS, 'led-0')) + + def test_management_interface_set_indicator_state_good(self): + self.assertRaises( + exception.UnsupportedDriverExtension, + self.driver.management.set_indicator_state, + self.task, components.CHASSIS, 'led-0', indicator_states.ON) + def test_inspect_interface(self): self.assertEqual({}, self.driver.inspect.get_properties()) self.driver.inspect.validate(self.task)