Add manual clean and automated verify steps to set BMC clock
via Redfish Manager This patch adds two new capabilities to the Redfish management interface in Ironic for setting the BMC clock: 1. A manual cleaning step (`set_bmc_clock`) that allows operators to set the BMC clock explicitly by providing datetime and timezone offset. 2. An automated verify step (`verify_bmc_clock`) that, if enabled via configuration, sets the BMC clock during node verification using the current UTC time. These steps aim to prevent certificate validation failures caused by incorrect BMC time, particularly when dealing with TLS certificates. A new configuration option `redfish.enable_verify_bmc_clock` has been added to control the automated verify behavior. The minimum version of `sushy` has also been updated to is 5.7.0 to support these features. Related patches: - https://review.opendev.org/c/openstack/sushy/+/950539 (Add support for Manager DateTime fields in sushy) - https://review.opendev.org/c/openstack/sushy-tools/+/950925 (Fix Manager DateTime field handling in sushy-tools) Partial-Bug: #2041904 Change-Id: I75cbd39a60f8470224dc5a2fe0a4f17c22acd1cd Signed-off-by: Queensly Kyerewaa Acheampongmaa <qacheampong@gmail.com>
This commit is contained in:
@@ -129,6 +129,17 @@ opts = [
|
||||
cfg.StrOpt('verify_ca',
|
||||
help=_('The default verify_ca path when redfish_verify_ca '
|
||||
'in driver_info is missing or set to True.')),
|
||||
cfg.BoolOpt('enable_verify_bmc_clock',
|
||||
default=False,
|
||||
help=_('Whether to enable the automated verify step '
|
||||
'that checks and sets the BMC clock. '
|
||||
'When enabled, Ironic will automatically attempt '
|
||||
'to set the BMC\'s clock during node registration '
|
||||
'(in the verify phase) using Redfish\'s '
|
||||
'DateTime fields. '
|
||||
'This helps avoid TLS certificate issues '
|
||||
'caused by incorrect BMC time.')),
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
@@ -14,9 +14,11 @@
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
from datetime import timezone
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dateutil import parser
|
||||
from oslo_log import log
|
||||
from oslo_utils import timeutils
|
||||
import sushy
|
||||
@@ -448,6 +450,101 @@ class RedfishManagement(base.ManagementInterface):
|
||||
for attr in [getattr(resource, field)]
|
||||
}
|
||||
|
||||
@base.clean_step(priority=0, abortable=False, argsinfo={
|
||||
'target_datetime': {
|
||||
'description': 'The datetime to set in ISO8601 format',
|
||||
'required': True
|
||||
},
|
||||
'datetime_local_offset': {
|
||||
'description': 'The local time offset from UTC',
|
||||
'required': False
|
||||
}
|
||||
})
|
||||
@task_manager.require_exclusive_lock
|
||||
def set_bmc_clock(self, task, target_datetime, datetime_local_offset=None):
|
||||
"""Set the BMC clock using Redfish Manager resource.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:param target_datetime: The datetime to set in ISO8601 format
|
||||
:param datetime_local_offset: The local time offset from UTC (optional)
|
||||
:raises: RedfishError if the operation fails
|
||||
"""
|
||||
try:
|
||||
system = redfish_utils.get_system(task.node)
|
||||
manager = redfish_utils.get_manager(task.node, system)
|
||||
LOG.debug("Setting BMC clock to %s (offset: %s)",
|
||||
target_datetime, datetime_local_offset)
|
||||
manager._conn.timeout = 30
|
||||
manager.set_datetime(
|
||||
target_datetime,
|
||||
datetime_local_offset
|
||||
)
|
||||
|
||||
manager.refresh()
|
||||
if manager.datetime != target_datetime:
|
||||
raise exception.RedfishError(
|
||||
"BMC clock update failed: mismatch after setting datetime")
|
||||
|
||||
LOG.info(
|
||||
"Successfully updated BMC clock for node %s",
|
||||
task.node.uuid
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.exception("BMC clock update failed: %s", e)
|
||||
raise exception.RedfishError(error=str(e))
|
||||
|
||||
@base.verify_step(priority=1)
|
||||
@task_manager.require_exclusive_lock
|
||||
def verify_bmc_clock(self, task):
|
||||
"""Verify and auto-set the BMC clock to the current UTC time.
|
||||
|
||||
This step compares the system UTC time to the BMC's Redfish datetime.
|
||||
If the difference exceeds 1 second, it attempts to sync the time.
|
||||
Verification fails only if the BMC time remains incorrect
|
||||
after the update.
|
||||
"""
|
||||
if not CONF.redfish.enable_verify_bmc_clock:
|
||||
LOG.info("Skipping BMC clock verify step: disabled via config")
|
||||
return
|
||||
|
||||
try:
|
||||
system_time = timeutils.utcnow().replace(
|
||||
tzinfo=timezone.utc).isoformat()
|
||||
system = redfish_utils.get_system(task.node)
|
||||
manager = redfish_utils.get_manager(task.node, system)
|
||||
manager.refresh()
|
||||
|
||||
manager_time = parser.isoparse(manager.datetime)
|
||||
local_time = parser.isoparse(system_time)
|
||||
|
||||
LOG.debug("BMC time: %s, Local time: %s",
|
||||
manager_time, local_time)
|
||||
LOG.debug("manager.datetime_local_offset: %s",
|
||||
manager.datetimelocaloffset)
|
||||
|
||||
# Fail if the BMC clock differs from system time
|
||||
# by more than 1 second
|
||||
if abs((manager_time - local_time).total_seconds()) > 1:
|
||||
LOG.info("BMC clock is out of sync. Updating...")
|
||||
manager.set_datetime(system_time,
|
||||
datetime_local_offset="+00:00")
|
||||
manager.refresh()
|
||||
|
||||
updated_time = parser.isoparse(manager.datetime)
|
||||
if abs((updated_time - local_time).total_seconds()) > 1:
|
||||
raise exception.RedfishError(
|
||||
"BMC clock still incorrect after update")
|
||||
|
||||
LOG.info("BMC clock update successful for node %s",
|
||||
task.node.uuid)
|
||||
|
||||
except Exception as e:
|
||||
LOG.exception("BMC clock auto-update failed during verify: %s", e)
|
||||
raise exception.NodeVerifyFailure(
|
||||
node=getattr(task.node, 'uuid', 'unknown'),
|
||||
reason="BMC clock verify step failed: %s" % str(e)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_sensors_fan(cls, chassis):
|
||||
"""Get fan sensors reading.
|
||||
|
@@ -555,6 +555,140 @@ class RedfishManagementTestCase(db_base.DbTestCase):
|
||||
expected = boot_modes.LEGACY_BIOS
|
||||
self.assertEqual(expected, response)
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_manager', autospec=True)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
def test_set_bmc_clock_success(self, mock_get_system, mock_get_manager):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
mock_system = mock.Mock()
|
||||
mock_manager = mock.Mock()
|
||||
mock_manager.datetime = '2025-06-27T12:00:00'
|
||||
|
||||
mock_get_system.return_value = mock_system
|
||||
mock_get_manager.return_value = mock_manager
|
||||
|
||||
target_datetime = '2025-06-27T12:00:00'
|
||||
task.driver.management.set_bmc_clock(
|
||||
task, target_datetime=target_datetime)
|
||||
|
||||
mock_manager.set_datetime.assert_called_once_with(
|
||||
target_datetime, None)
|
||||
mock_manager.refresh.assert_called_once()
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_manager', autospec=True)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
def test_set_bmc_clock_datetime_mismatch_raises(self, mock_get_system,
|
||||
mock_get_manager):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
mock_system = mock.Mock()
|
||||
mock_manager = mock.Mock()
|
||||
mock_manager.datetime = 'wrong-time'
|
||||
|
||||
mock_get_system.return_value = mock_system
|
||||
mock_get_manager.return_value = mock_manager
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
exception.RedfishError,
|
||||
'BMC clock update failed: mismatch after setting '
|
||||
'datetime'):
|
||||
task.driver.management.set_bmc_clock(
|
||||
task, target_datetime='2025-06-27T12:00:00')
|
||||
|
||||
mock_manager.set_datetime.assert_called_once()
|
||||
mock_manager.refresh.assert_called_once()
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_manager', autospec=True)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
@mock.patch.object(CONF.redfish, 'enable_verify_bmc_clock', new=False)
|
||||
def test_verify_bmc_clock_disabled_in_config(self, mock_get_system,
|
||||
mock_get_manager):
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
task.driver.management.verify_bmc_clock(task)
|
||||
|
||||
# Ensure Redfish was never called
|
||||
mock_get_system.assert_not_called()
|
||||
mock_get_manager.assert_not_called()
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_manager', autospec=True)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
@mock.patch.object(timeutils, 'utcnow', autospec=True)
|
||||
def test_verify_bmc_clock_success_when_time_matches(self, mock_utcnow,
|
||||
mock_get_system,
|
||||
mock_get_manager):
|
||||
|
||||
self.config(enable_verify_bmc_clock=True, group='redfish')
|
||||
current_datetime = datetime.datetime(2025, 7, 13, 3, 0, 0)
|
||||
mock_utcnow.return_value = current_datetime
|
||||
|
||||
self.node.updated_at = datetime.datetime.utcnow()
|
||||
self.node.save()
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
mock_system = mock.Mock()
|
||||
mock_manager = mock.Mock()
|
||||
|
||||
mock_manager.datetime = current_datetime.replace(
|
||||
tzinfo=datetime.timezone.utc).isoformat()
|
||||
mock_manager.datetimelocaloffset = '+00:00'
|
||||
|
||||
mock_get_system.return_value = mock_system
|
||||
mock_get_manager.return_value = mock_manager
|
||||
|
||||
task.driver.management.verify_bmc_clock(task)
|
||||
|
||||
mock_manager.set_datetime.assert_not_called()
|
||||
mock_manager.refresh.assert_called_once()
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_manager', autospec=True)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
@mock.patch.object(timeutils, 'utcnow', autospec=True)
|
||||
def test_verify_bmc_clock_fails_on_mismatch(self, mock_utcnow,
|
||||
mock_get_system,
|
||||
mock_get_manager):
|
||||
self.config(enable_verify_bmc_clock=True, group='redfish')
|
||||
|
||||
current_datetime = datetime.datetime(2025, 7, 13, 3, 0, 0)
|
||||
mock_utcnow.return_value = current_datetime
|
||||
|
||||
self.node.updated_at = datetime.datetime.utcnow()
|
||||
self.node.save()
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
|
||||
if task.node is None:
|
||||
task.node = self.node
|
||||
if not getattr(task.node, 'uuid', None):
|
||||
task.node.uuid = self.node.uuid
|
||||
|
||||
mock_system = mock.Mock()
|
||||
mock_manager = mock.Mock()
|
||||
|
||||
# Mismatched time (e.g., 5 seconds later)
|
||||
mock_manager.datetime = (
|
||||
current_datetime + datetime.timedelta(seconds=5)).replace(
|
||||
tzinfo=datetime.timezone.utc).isoformat()
|
||||
mock_manager.datetimelocaloffset = "+00:00"
|
||||
|
||||
mock_get_system.return_value = mock_system
|
||||
mock_get_manager.return_value = mock_manager
|
||||
|
||||
management = redfish_mgmt.RedfishManagement()
|
||||
|
||||
with self.assertRaisesRegex(exception.NodeVerifyFailure,
|
||||
"BMC clock verify step failed"):
|
||||
management.verify_bmc_clock(task)
|
||||
|
||||
self.assertEqual(mock_manager.refresh.call_count, 2)
|
||||
mock_manager.set_datetime.assert_called_once_with(
|
||||
current_datetime.replace(
|
||||
tzinfo=datetime.timezone.utc).isoformat(),
|
||||
datetime_local_offset="+00:00")
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
def test_inject_nmi(self, mock_get_system):
|
||||
fake_system = mock.Mock()
|
||||
|
@@ -0,0 +1,17 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds two new capabilities to the Redfish managemnet interface
|
||||
for managing the BMC clock.
|
||||
|
||||
1. A manual cleaning step ``set_bmc_clock`` that allows operators
|
||||
to set the BMC's hardware clock to a specific datetime (in ISO8601 format),
|
||||
optionally including a datetimelocaloffset.
|
||||
|
||||
2. An automated verify step ``verify_bmc_clock`` that compares the
|
||||
BMC's Redfish datetime to the system UTC time, and automatically
|
||||
updates the BMC clock if needed. Verification fails if the difference
|
||||
exceeds 1 second after the update.
|
||||
|
||||
These steps helps ensure BMC clock synchronization in baremetal environments
|
||||
where incorrect or drifting BMC clocks may lead to TLS certificate validation failures.
|
@@ -38,7 +38,7 @@ psutil>=3.2.2 # BSD
|
||||
futurist>=1.2.0 # Apache-2.0
|
||||
tooz>=2.7.0 # Apache-2.0
|
||||
openstacksdk>=0.99.0 # Apache-2.0
|
||||
sushy>=4.8.0
|
||||
sushy>=5.7.0
|
||||
construct>=2.9.39 # MIT
|
||||
netaddr>=0.9.0 # BSD
|
||||
microversion-parse>=1.0.1 # Apache-2.0
|
||||
|
Reference in New Issue
Block a user