Refactor vendor detection and add Redfish implementation

Get rid of the TODO in the code and prepare for more management
interfaces supporting detect_vendor(). Vendor detecting now runs
during transition to manageable and on power state sync (essentially
same as before but for all drivers not only IPMI).

Update the IPMI implementation to no longer hide exceptions since
they're not handled on the upper level. Simplify the regex and fix
the docstring.

Add the Redfish implementation as a foundation for future
vendor-specific changes.

Change-Id: Ie521cf2295613dde5842cbf9a053540a40be4b9c
(cherry picked from commit 121b3348c8)
This commit is contained in:
Dmitry Tantsur 2021-01-20 12:11:54 +01:00 committed by Bob Fournier
parent 8bf3f47476
commit 4df73e47e1
8 changed files with 167 additions and 56 deletions

View File

@ -1237,6 +1237,9 @@ class ConductorManager(base_manager.BaseConductorManager):
{'node': node.uuid, 'msg': e}) {'node': node.uuid, 'msg': e})
if error is None: if error is None:
# Cache the vendor if possible
utils.node_cache_vendor(task)
if power_state != node.power_state: if power_state != node.power_state:
old_power_state = node.power_state old_power_state = node.power_state
node.power_state = power_state node.power_state = power_state
@ -3646,6 +3649,10 @@ def do_sync_power_state(task, count):
'retries': max_retries, 'err': e}) 'retries': max_retries, 'err': e})
return count return count
# Make sure we have the vendor cached (if for some reason it failed during
# the transition to manageable or a really old API version was used).
utils.node_cache_vendor(task)
if node.power_state and node.power_state == power_state: if node.power_state and node.power_state == power_state:
# No action is needed # No action is needed
return 0 return 0

View File

@ -1174,3 +1174,34 @@ def hash_password(password=''):
:param value: Value to be hashed :param value: Value to be hashed
""" """
return crypt.crypt(password, make_salt()) return crypt.crypt(password, make_salt())
def node_cache_vendor(task):
"""Cache the vendor if it can be detected."""
properties = task.node.properties
if properties.get('vendor'):
return # assume that vendors don't change on fly
try:
# We have no vendor stored, so we'll go ahead and
# call to store it.
vendor = task.driver.management.detect_vendor(task)
if not vendor:
return
# This function may be called without an exclusive lock, so get one
task.upgrade_lock(purpose='caching node vendor')
except exception.UnsupportedDriverExtension:
return
except Exception as exc:
LOG.warning('Unexpected exception when trying to detect vendor '
'for node %(node)s. %(class)s: %(exc)s',
{'node': task.node.uuid,
'class': type(exc).__name__, 'exc': exc},
exc_info=not isinstance(exc, exception.IronicException))
return
props = task.node.properties
props['vendor'] = vendor
task.node.properties = props
task.node.save()

View File

@ -950,24 +950,6 @@ class IPMIPower(base.PowerInterface):
call). call).
""" """
# NOTE(TheJulia): Temporary until we promote detect_vendor as
# a higher level management method and able to automatically
# run detection upon either power state sync or in the enrollment
# to management step.
try:
properties = task.node.properties
if not properties.get('vendor'):
# We have no vendor stored, so we'll go ahead and
# call to store it.
vendor = task.driver.management.detect_vendor(task)
if vendor:
props = task.node.properties
props['vendor'] = vendor
task.node.properties = props
task.node.save()
except exception.UnsupportedDriverExtension:
pass
driver_info = _parse_driver_info(task.node) driver_info = _parse_driver_info(task.node)
return _power_status(driver_info) return _power_status(driver_info)
@ -1255,12 +1237,7 @@ class IPMIManagement(base.ManagementInterface):
@task_manager.require_exclusive_lock @task_manager.require_exclusive_lock
@METRICS.timer('IPMIManagement.detect_vendor') @METRICS.timer('IPMIManagement.detect_vendor')
def detect_vendor(self, task): def detect_vendor(self, task):
"""Detects, stores, and returns the hardware vendor. """Detects and returns the hardware vendor.
If the Node object ``properties`` field does not already contain
a ``vendor`` field, then this method is intended to query
Detects the BMC hardware vendor and stores the returned value
with-in the Node object ``properties`` field if detected.
:param task: A task from TaskManager. :param task: A task from TaskManager.
:raises: InvalidParameterValue if an invalid component, indicator :raises: InvalidParameterValue if an invalid component, indicator
@ -1269,20 +1246,11 @@ class IPMIManagement(base.ManagementInterface):
:returns: String representing the BMC reported Vendor or :returns: String representing the BMC reported Vendor or
Manufacturer, otherwise returns None. Manufacturer, otherwise returns None.
""" """
try: driver_info = _parse_driver_info(task.node)
driver_info = _parse_driver_info(task.node) out, err = _exec_ipmitool(driver_info, "mc info")
out, err = _exec_ipmitool(driver_info, "mc info") re_obj = re.search("Manufacturer Name *: (.+)", out)
re_obj = re.search("Manufacturer Name .*: (.+)", out) if re_obj:
if re_obj: return re_obj.group(1).lower()
bmc_vendor = str(re_obj.groups('')[0]).lower().split(':')
# Pull unparsed data and save the vendor
return bmc_vendor[-1]
except (exception.PasswordFileFailedToCreate,
processutils.ProcessExecutionError) as e:
LOG.warning('IPMI get boot device failed to detect vendor '
'of bmc for %(node)s. Error %(err)s',
{'node': task.node.uuid,
'err': e})
@METRICS.timer('IPMIManagement.get_sensors_data') @METRICS.timer('IPMIManagement.get_sensors_data')
def get_sensors_data(self, task): def get_sensors_data(self, task):

View File

@ -668,3 +668,18 @@ class RedfishManagement(base.ManagementInterface):
"node %(uuid)s") % {'indicator': indicator, "node %(uuid)s") % {'indicator': indicator,
'component': component, 'component': component,
'uuid': task.node.uuid}) 'uuid': task.node.uuid})
def detect_vendor(self, task):
"""Detects and returns the hardware vendor.
Uses the System's Manufacturer field.
:param task: A task from TaskManager.
:raises: InvalidParameterValue if an invalid component, indicator
or state is specified.
:raises: MissingParameterValue if a required parameter is missing
:raises: RedfishError on driver-specific problems.
:returns: String representing the BMC reported Vendor or
Manufacturer, otherwise returns None.
"""
return redfish_utils.get_system(task.node).manufacturer

View File

@ -3037,11 +3037,12 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
@mgr_utils.mock_record_keepalive @mgr_utils.mock_record_keepalive
class DoNodeVerifyTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): class DoNodeVerifyTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
@mock.patch.object(conductor_utils, 'node_cache_vendor', autospec=True)
@mock.patch('ironic.objects.node.NodeCorrectedPowerStateNotification') @mock.patch('ironic.objects.node.NodeCorrectedPowerStateNotification')
@mock.patch('ironic.drivers.modules.fake.FakePower.get_power_state') @mock.patch('ironic.drivers.modules.fake.FakePower.get_power_state')
@mock.patch('ironic.drivers.modules.fake.FakePower.validate') @mock.patch('ironic.drivers.modules.fake.FakePower.validate')
def test__do_node_verify(self, mock_validate, mock_get_power_state, def test__do_node_verify(self, mock_validate, mock_get_power_state,
mock_notif): mock_notif, mock_cache_vendor):
self._start_service() self._start_service()
mock_get_power_state.return_value = states.POWER_OFF mock_get_power_state.return_value = states.POWER_OFF
# Required for exception handling # Required for exception handling
@ -3056,6 +3057,7 @@ class DoNodeVerifyTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
with task_manager.acquire( with task_manager.acquire(
self.context, node['id'], shared=False) as task: self.context, node['id'], shared=False) as task:
self.service._do_node_verify(task) self.service._do_node_verify(task)
mock_cache_vendor.assert_called_once_with(task)
self._stop_service() self._stop_service()
@ -4674,6 +4676,8 @@ class ManagerDoSyncPowerStateTestCase(db_base.DbTestCase):
super(ManagerDoSyncPowerStateTestCase, self).setUp() super(ManagerDoSyncPowerStateTestCase, self).setUp()
self.service = manager.ConductorManager('hostname', 'test-topic') self.service = manager.ConductorManager('hostname', 'test-topic')
self.driver = mock.Mock(spec_set=drivers_base.BareDriver) self.driver = mock.Mock(spec_set=drivers_base.BareDriver)
self.driver.management.detect_vendor.side_effect = \
exception.UnsupportedDriverExtension
self.power = self.driver.power self.power = self.driver.power
self.node = obj_utils.create_test_node( self.node = obj_utils.create_test_node(
self.context, driver='fake-hardware', maintenance=False, self.context, driver='fake-hardware', maintenance=False,
@ -4969,6 +4973,24 @@ class ManagerDoSyncPowerStateTestCase(db_base.DbTestCase):
self.assertFalse(node_power_action.called) self.assertFalse(node_power_action.called)
self.task.upgrade_lock.assert_called_once_with() self.task.upgrade_lock.assert_called_once_with()
@mock.patch.object(nova, 'power_update', autospec=True)
def test_vendor_detection(self, mock_power_update, node_power_action):
self.driver.management.detect_vendor.side_effect = [
"Fake Inc."
]
self._do_sync_power_state(states.POWER_ON, states.POWER_OFF)
self.assertFalse(self.power.validate.called)
self.power.get_power_state.assert_called_once_with(self.task)
self.assertFalse(node_power_action.called)
self.assertEqual(states.POWER_OFF, self.node.power_state)
# node_cache_vendor calls upgrade_lock, then power update does it once
# more (which is safe because TaskManager checks its state)
self.assertEqual(2, self.task.upgrade_lock.call_count)
mock_power_update.assert_called_once_with(
self.task.context, self.node.instance_uuid, states.POWER_OFF)
self.assertEqual("Fake Inc.", self.node.properties['vendor'])
@mock.patch.object(waiters, 'wait_for_all', @mock.patch.object(waiters, 'wait_for_all',
new=mock.MagicMock(return_value=(0, 0))) new=mock.MagicMock(return_value=(0, 0)))

View File

@ -2079,3 +2079,76 @@ class AgentTokenUtilsTestCase(tests_base.TestCase):
conductor_utils.is_agent_token_supported('6.2.1')) conductor_utils.is_agent_token_supported('6.2.1'))
self.assertFalse( self.assertFalse(
conductor_utils.is_agent_token_supported('6.0.0')) conductor_utils.is_agent_token_supported('6.0.0'))
@mock.patch.object(fake.FakeManagement, 'detect_vendor', autospec=True,
return_value="Fake Inc.")
class CacheVendorTestCase(db_base.DbTestCase):
def setUp(self):
super(CacheVendorTestCase, self).setUp()
self.node = obj_utils.create_test_node(self.context,
driver='fake-hardware',
properties={})
def test_ok(self, mock_detect):
with task_manager.acquire(self.context, self.node.id,
shared=True) as task:
conductor_utils.node_cache_vendor(task)
self.assertFalse(task.shared)
mock_detect.assert_called_once_with(task.driver.management, task)
self.node.refresh()
self.assertEqual("Fake Inc.", self.node.properties['vendor'])
def test_already_present(self, mock_detect):
self.node.properties = {'vendor': "Fake GmbH"}
self.node.save()
with task_manager.acquire(self.context, self.node.id,
shared=True) as task:
conductor_utils.node_cache_vendor(task)
self.assertTrue(task.shared)
self.node.refresh()
self.assertEqual("Fake GmbH", self.node.properties['vendor'])
self.assertFalse(mock_detect.called)
def test_empty(self, mock_detect):
mock_detect.return_value = None
with task_manager.acquire(self.context, self.node.id,
shared=True) as task:
conductor_utils.node_cache_vendor(task)
self.assertTrue(task.shared)
mock_detect.assert_called_once_with(task.driver.management, task)
self.node.refresh()
self.assertNotIn('vendor', self.node.properties)
@mock.patch.object(conductor_utils.LOG, 'warning', autospec=True)
def test_unsupported(self, mock_log, mock_detect):
mock_detect.side_effect = exception.UnsupportedDriverExtension
with task_manager.acquire(self.context, self.node.id,
shared=True) as task:
conductor_utils.node_cache_vendor(task)
self.assertTrue(task.shared)
mock_detect.assert_called_once_with(task.driver.management, task)
self.node.refresh()
self.assertNotIn('vendor', self.node.properties)
self.assertFalse(mock_log.called)
@mock.patch.object(conductor_utils.LOG, 'warning', autospec=True)
def test_failed(self, mock_log, mock_detect):
mock_detect.side_effect = RuntimeError
with task_manager.acquire(self.context, self.node.id,
shared=True) as task:
conductor_utils.node_cache_vendor(task)
self.assertTrue(task.shared)
mock_detect.assert_called_once_with(task.driver.management, task)
self.node.refresh()
self.assertNotIn('vendor', self.node.properties)
self.assertTrue(mock_log.called)

View File

@ -696,3 +696,11 @@ class RedfishManagementTestCase(db_base.DbTestCase):
mock_get_system.assert_called_once_with(task.node) mock_get_system.assert_called_once_with(task.node)
self.assertEqual(indicator_states.ON, state) self.assertEqual(indicator_states.ON, state)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_detect_vendor(self, mock_get_system):
mock_get_system.return_value.manufacturer = "Fake GmbH"
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
response = task.driver.management.detect_vendor(task)
self.assertEqual("Fake GmbH", response)

View File

@ -1517,16 +1517,14 @@ class IPMIToolPrivateMethodTestCase(
self.assertEqual(expected, mock_exec.call_args_list) self.assertEqual(expected, mock_exec.call_args_list)
@mock.patch.object(ipmi.IPMIManagement, 'detect_vendor', autospec=True)
@mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True)
@mock.patch('oslo_utils.eventletutils.EventletEvent.wait', autospec=True) @mock.patch('oslo_utils.eventletutils.EventletEvent.wait', autospec=True)
def test__soft_power_off(self, sleep_mock, mock_exec, mock_vendor): def test__soft_power_off(self, sleep_mock, mock_exec):
def side_effect(driver_info, command, **kwargs): def side_effect(driver_info, command, **kwargs):
resp_dict = {"power status": ["Chassis Power is off\n", None], resp_dict = {"power status": ["Chassis Power is off\n", None],
"power soft": [None, None]} "power soft": [None, None]}
return resp_dict.get(command, ["Bad\n", None]) return resp_dict.get(command, ["Bad\n", None])
mock_vendor.return_value = None
mock_exec.side_effect = side_effect mock_exec.side_effect = side_effect
expected = [mock.call(self.info, "power soft"), expected = [mock.call(self.info, "power soft"),
@ -1537,13 +1535,10 @@ class IPMIToolPrivateMethodTestCase(
self.assertEqual(expected, mock_exec.call_args_list) self.assertEqual(expected, mock_exec.call_args_list)
self.assertEqual(states.POWER_OFF, state) self.assertEqual(states.POWER_OFF, state)
self.assertTrue(mock_vendor.called)
@mock.patch.object(ipmi.IPMIManagement, 'detect_vendor', autospec=True)
@mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True)
@mock.patch('oslo_utils.eventletutils.EventletEvent.wait', autospec=True) @mock.patch('oslo_utils.eventletutils.EventletEvent.wait', autospec=True)
def test__soft_power_off_max_retries(self, sleep_mock, mock_exec, def test__soft_power_off_max_retries(self, sleep_mock, mock_exec):
mock_vendor):
def side_effect(driver_info, command, **kwargs): def side_effect(driver_info, command, **kwargs):
resp_dict = {"power status": ["Chassis Power is on\n", None], resp_dict = {"power status": ["Chassis Power is on\n", None],
@ -1562,8 +1557,6 @@ class IPMIToolPrivateMethodTestCase(
ipmi._soft_power_off, task, self.info, timeout=2) ipmi._soft_power_off, task, self.info, timeout=2)
self.assertEqual(expected, mock_exec.call_args_list) self.assertEqual(expected, mock_exec.call_args_list)
# Should be removed when detect_vendor automatic invocation is moved.
self.assertFalse(mock_vendor.called)
@mock.patch.object(ipmi, '_power_status', autospec=True) @mock.patch.object(ipmi, '_power_status', autospec=True)
@mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True)
@ -1603,9 +1596,8 @@ class IPMIToolDriverTestCase(Base):
self.assertEqual(sorted(expected), self.assertEqual(sorted(expected),
sorted(task.driver.get_properties())) sorted(task.driver.get_properties()))
@mock.patch.object(ipmi.IPMIManagement, 'detect_vendor', autospec=True)
@mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True)
def test_get_power_state(self, mock_exec, mock_detect): def test_get_power_state(self, mock_exec):
returns = iter([["Chassis Power is off\n", None], returns = iter([["Chassis Power is off\n", None],
["Chassis Power is on\n", None], ["Chassis Power is on\n", None],
["\n", None]]) ["\n", None]])
@ -1613,7 +1605,6 @@ class IPMIToolDriverTestCase(Base):
mock.call(self.info, "power status", kill_on_timeout=True), mock.call(self.info, "power status", kill_on_timeout=True),
mock.call(self.info, "power status", kill_on_timeout=True)] mock.call(self.info, "power status", kill_on_timeout=True)]
mock_exec.side_effect = returns mock_exec.side_effect = returns
mock_detect.return_value = None
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
pstate = self.power.get_power_state(task) pstate = self.power.get_power_state(task)
@ -1626,20 +1617,16 @@ class IPMIToolDriverTestCase(Base):
self.assertEqual(states.ERROR, pstate) self.assertEqual(states.ERROR, pstate)
self.assertEqual(mock_exec.call_args_list, expected) self.assertEqual(mock_exec.call_args_list, expected)
self.assertEqual(3, mock_detect.call_count)
@mock.patch.object(ipmi.IPMIManagement, 'detect_vendor', autospec=True)
@mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True)
def test_get_power_state_exception(self, mock_exec, mock_vendor): def test_get_power_state_exception(self, mock_exec):
mock_exec.side_effect = processutils.ProcessExecutionError("error") mock_exec.side_effect = processutils.ProcessExecutionError("error")
mock_vendor.return_value = None
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.IPMIFailure, self.assertRaises(exception.IPMIFailure,
self.power.get_power_state, self.power.get_power_state,
task) task)
mock_exec.assert_called_once_with(self.info, "power status", mock_exec.assert_called_once_with(self.info, "power status",
kill_on_timeout=True) kill_on_timeout=True)
self.assertEqual(1, mock_vendor.call_count)
@mock.patch.object(ipmi, '_power_on', autospec=True) @mock.patch.object(ipmi, '_power_on', autospec=True)
@mock.patch.object(ipmi, '_power_off', autospec=True) @mock.patch.object(ipmi, '_power_off', autospec=True)