Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7a21245
PLDM channel not initialized after BMC update
iurygregory Feb 13, 2026
12e5c72
tox: switch to recommended constraints parameter
cardoe Apr 23, 2026
3555991
Fix RBAC field redaction when owner not in requested fields
juliakreger Apr 29, 2026
e1d35c1
chore: update license and classifier per pyproject.toml standards
cardoe May 1, 2026
fb5392e
Replace OVN Metadata agent with OVN agent
kajinamit May 3, 2026
589a428
bump sushy to work around timeout parameter issue
cardoe May 5, 2026
15e1b21
trivial: add follow-up release note for ari/aki confusion
juliakreger May 5, 2026
08067f0
Cleanup [conductor]file_url_allowed_path
jayofdoom May 5, 2026
a1f9a1e
security: Use sandbox rendering for jinja2
juliakreger Apr 28, 2026
62153c7
fix: oci image service handling webserver_verify_ca when it's string
1nv0k32 Apr 29, 2026
c086a15
Merge "Replace OVN Metadata agent with OVN agent"
May 7, 2026
ed97747
Merge "fix: oci image service handling webserver_verify_ca when it's …
May 7, 2026
bd8be22
Merge "tox: switch to recommended constraints parameter"
May 7, 2026
bd6b93f
Merge "bump sushy to work around timeout parameter issue"
May 7, 2026
b6ef35e
Merge "PLDM channel not initialized after BMC update"
May 7, 2026
e83bb1f
Fix redfish sensor data crash when redfish_system_id is None
nicholaskuechler May 6, 2026
af6a4e6
Merge "trivial: add follow-up release note for ari/aki confusion"
May 7, 2026
cb1bd8a
ci: mark metal3-integration non-voting
juliakreger May 7, 2026
69292c8
Merge "chore: update license and classifier per pyproject.toml standa…
May 7, 2026
287977c
Merge "security: Use sandbox rendering for jinja2"
May 7, 2026
e85ea44
Merge "Fix RBAC field redaction when owner not in requested fields"
May 8, 2026
78201d7
Merge "Fix redfish sensor data crash when redfish_system_id is None"
May 8, 2026
01994a5
merge upstream/master into main
May 8, 2026
0cab838
UPSTREAM: <carry>: add OWNERS and test dockerfile back again
elfosardo May 5, 2023
c08266a
UPSTREAM: <carry>: Do not upgrade test image
elfosardo May 24, 2023
13d3e09
DPU modeling - parent_node DB/Model/API
juliakreger Apr 11, 2023
6eeabfa
follow-up on DPU change api-ref
juliakreger May 24, 2023
74162f9
UPSTREAM: <carry>: update base image for OCP 4.15
elfosardo Oct 18, 2023
71aceb4
UPSTREAM: <carry>: add ci-operator config
elfosardo Dec 12, 2023
7042b5c
UPSTREAM: <carry>: install distro pbr
elfosardo Dec 13, 2023
a337fcd
Revert "UPSTREAM: <carry>: install distro pbr"
elfosardo Dec 14, 2023
af698b2
UPSTREAM: <carry>: fix ci operator config
elfosardo Dec 14, 2023
c56d13f
UPSTREAM: <carry>: update base image for OCP 4.16
elfosardo Mar 18, 2024
70defc3
UPSTREAM: <carry>: pin upper-constraints
elfosardo May 5, 2025
bbdad63
UPSTREAM: <carry>: update base image for tests
elfosardo May 9, 2025
42fe486
UPSTREAM: <carry>: Run tests using Python 3.12
elfosardo May 9, 2025
c20b077
UPSTREAM: <carry>: unpin upper-constraints
elfosardo Jul 11, 2025
4fc8f05
UPSTREAM: <carry>: Run tests using Python 3.12
elfosardo May 9, 2025
1bd3a5a
Fix the ability to escape service fail
juliakreger Aug 8, 2025
b2191da
Fix servicing abort to respect abortable flag
jacob-anders Aug 13, 2025
0d9b2c8
UPSTREAM: <carry>: update base image for tests
iurygregory Sep 8, 2025
7e4c076
UPSTREAM: <carry>: Update test base image for 4.22
elfosardo Jan 20, 2026
470f037
UPSTREAM: <carry>: Update username user by Jacob.
jacob-anders Jan 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion devstack/lib/ironic
Original file line number Diff line number Diff line change
Expand Up @@ -1801,7 +1801,7 @@ EOF
restart_libvirt
fi
# Standalone jobs may use some different paths, that is okay
iniset $IRONIC_CONF_FILE conductor file_url_allowed_paths /var/lib/ironic,/shared/html,/templates,/opt/cache/files,/vagrant,/opt/stack/ironic
iniset $IRONIC_CONF_FILE conductor file_url_allowed_paths /var/lib/ironic,/opt/stack/ironic

fi

Expand Down
2 changes: 1 addition & 1 deletion doc/source/contributor/dev-quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ manage your locales, make sure you have enabled ``en_US.UTF8`` in
Python Prerequisites
====================

We suggest to use at least tox 3.9, if your distribution has an older version,
Ironic requires at least tox 4.28.0, if your distribution has an older version,
you can install it using pip system-wise or better per user using the --user
option that by default will install the binary under $HOME/.local/bin, so you
need to be sure to have that path in $PATH; for example::
Expand Down
2 changes: 1 addition & 1 deletion doc/source/contributor/devstack-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ enabled and use the ``ipmi`` hardware type with this config::
# Configure networking by disabling OVN and enabling Neutron w/OVS.
disable_service ovn-controller
disable_service ovn-northd
disable_service neutron-ovn-metadata-agent
disable_service neutron-ovn-agent

enable_service neutron-agent q-agt
enable_service neutron-dhcp q-dhcp
Expand Down
13 changes: 13 additions & 0 deletions ironic/api/controllers/v1/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,19 @@ def _get_fields_for_node_query(fields=None):
msg = 'Field %s is not a valid field.' % field
raise exception.Invalid(msg)

# NOTE(TheJulia): Bugfix for LP#2150573. owner and
# lessee must always be extracted from the RPC object
# so that node_sanitize() can populate target_dict
# with node.owner/node.lessee for RBAC policy checks.
# Without these, project-scoped owner/lessee policy
# rules always fail and fields like last_error get
# incorrectly redacted. sanitize_dict() will strip
# these from the response if not requested by the
# caller.
for rbac_field in ('owner', 'lessee'):
if rbac_field not in object_fields:
object_fields.append(rbac_field)

return object_fields


Expand Down
7 changes: 5 additions & 2 deletions ironic/common/image_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,11 @@ class OciImageService(BaseImageService):
_client = None

def __init__(self):
verify = strutils.bool_from_string(CONF.webserver_verify_ca,
strict=True)
try:
verify = strutils.bool_from_string(CONF.webserver_verify_ca,
strict=True)
except ValueError:
verify = CONF.webserver_verify_ca
# Creates a client which we can use for actions.
# Note, this is not yet authenticated!
self._client = oci_registry.OciClient(verify=verify)
Expand Down
7 changes: 4 additions & 3 deletions ironic/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import warnings

import jinja2
from jinja2 import sandbox as jinja2sandbox
from oslo_concurrency import processutils
from oslo_log import log as logging
from oslo_serialization import jsonutils
Expand Down Expand Up @@ -501,18 +502,18 @@ def render_template(template, params, is_file=True, strict=False):
:param strict: Enable strict template rendering. Default is False
:returns: Rendered template
:raises: jinja2.exceptions.UndefinedError
:raises: jinja2.exceptions.SecurityError when the template has insecure
operations detected.
"""
if is_file:
tmpl_path, tmpl_name = os.path.split(template)
loader = jinja2.FileSystemLoader(tmpl_path)
else:
tmpl_name = 'template'
loader = jinja2.DictLoader({tmpl_name: template})
# NOTE(pas-ha) bandit does not seem to cope with such syntaxis
# and still complains with B701 for that line
# NOTE(pas-ha) not using default_for_string=False as we set the name
# of the template above for strings too.
env = jinja2.Environment( # nosec B701
env = jinja2sandbox.SandboxedEnvironment(
loader=loader,
autoescape=jinja2.select_autoescape(),
undefined=jinja2.StrictUndefined if strict else jinja2.Undefined
Expand Down
3 changes: 1 addition & 2 deletions ironic/conf/conductor.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,8 +635,7 @@
'automatically applied by the conductor should a '
'compressed artifact be detected.')),
cfg.ListOpt('file_url_allowed_paths',
default=['/var/lib/ironic', '/shared/html', '/templates',
'/opt/cache/files', '/vagrant'],
default=['/var/lib/ironic'],
item_type=ir_types.ExplicitAbsolutePath(),
help=_(
'List of paths that are allowed to be used as file:// '
Expand Down
65 changes: 55 additions & 10 deletions ironic/drivers/modules/redfish/firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
NIC_STARTING_TIMESTAMP = 'nic_starting_timestamp'
NIC_REBOOT_TRIGGERED = 'nic_reboot_triggered'
BIOS_REBOOT_TRIGGERED = 'bios_reboot_triggered'
BMC_UPDATE_COMPLETED = 'bmc_update_completed'


class RedfishFirmware(base.FirmwareInterface):
Expand Down Expand Up @@ -454,6 +455,9 @@ def _handle_bmc_update_completion(self, task, update_service,
:param settings: firmware update settings
:param current_update: the current firmware update being processed
"""
# Upgrade the lock to ensure we are using the latest info from
# the node.
task.upgrade_lock()
node = task.node

# Try to get current BMC version
Expand All @@ -466,15 +470,41 @@ def _handle_bmc_update_completion(self, task, update_service,
if (current_version is not None
and version_before is not None
and current_version != version_before):
LOG.info(
'BMC firmware version for node %(node)s changed from '
'%(old)s to %(new)s. Update complete. Continuing without '
'reboot.',
{'node': node.uuid, 'old': version_before,
'new': current_version})
node.del_driver_internal_info(BMC_FW_VERSION_BEFORE_UPDATE)
node.save()
self._continue_updates(task, update_service, settings)

# Check if more components are pending updates after BMC update
if len(settings) > 1:
# Upgrade the lock to ensure we are using the latest info from
# the node.
task.upgrade_lock()
# More components to update - trigger reboot before continuing
# Some hardware can only execute NIC firmware updates after
# the host reboots following the BMC firmware update.

LOG.info('BMC firmware update complete for node %(node)s. '
'More components pending - triggering reboot before '
'continuing to next component.',
{'node': node.uuid})
# Set flag to indicate reboot completed, ready to continue
# This ensures we reboot and continue with the next component
# update, this is required because we saw cases where NIC
# updates were not being executed after the BMC update.
current_update[BMC_UPDATE_COMPLETED] = True
node.set_driver_internal_info('redfish_fw_updates', settings)
node.save()

manager_utils.node_power_action(task, states.REBOOT)
return
else:
# Last component - no reboot needed
# Servicing/Cleaning will trigger one.
LOG.info('BMC firmware version for node %(node)s changed '
'from %(old)s to %(new)s. Update complete last '
'component',
{'node': node.uuid, 'old': version_before,
'new': current_version})
node.save()
self._continue_updates(task, update_service, settings)
return

# Check if we've been checking for too long
Expand Down Expand Up @@ -1366,7 +1396,6 @@ def _handle_firmware_update_task(self, task, node, current_update,
if msg:
messages.append(msg)

task.upgrade_lock()
self._handle_task_completion(task, sushy_task, messages,
update_service, settings,
current_update)
Expand Down Expand Up @@ -1403,7 +1432,9 @@ def _handle_firmware_update_task(self, task, node, current_update,
@METRICS.timer('RedfishFirmware._check_node_redfish_firmware_update')
def _check_node_redfish_firmware_update(self, task):
"""Check the progress of running firmware update on a node."""

# Upgrade the lock to ensure we are using the latest info from
# the node.
task.upgrade_lock()
node = task.node

# Check overall timeout for firmware update operation
Expand All @@ -1424,6 +1455,20 @@ def _check_node_redfish_firmware_update(self, task):
{'node': node.uuid, 'error': e})
return


# Check if BMC update just completed and node rebooted
# If so, continue with next component update
if current_update.get(BMC_UPDATE_COMPLETED):
LOG.info('BMC firmware update completed and node %(node)s has '
'rebooted. Continuing with next component.',
{'node': node.uuid})
current_update.pop(BMC_UPDATE_COMPLETED, None)
node.set_driver_internal_info('redfish_fw_updates', settings)
node.save()

self._continue_updates(task, update_service, settings)
return

# Touch provisioning to indicate progress is being monitored.
# This prevents heartbeat timeout from triggering for steps that
# don't require the ramdisk agent (requires_ramdisk=False).
Expand Down
6 changes: 2 additions & 4 deletions ironic/drivers/modules/redfish/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,8 +908,7 @@ def _process_storage_sensors(self, node, system):
storage_sensors = {'Drive': {}}

# Get system identity from driver info
driver_info = redfish_utils.parse_driver_info(node)
system_identity = driver_info['system_id'].split('/')[-1]
system_identity = system.path.split('/')[-1]

try:
drives = {}
Expand Down Expand Up @@ -954,8 +953,7 @@ def _process_simple_storage_sensors(self, node, system):
simple_storage_sensors = {'Drive': {}}

# Get system identity from driver info
driver_info = redfish_utils.parse_driver_info(node)
system_identity = driver_info['system_id'].split('/')[-1]
system_identity = system.path.split('/')[-1]

try:
drives = {}
Expand Down
94 changes: 94 additions & 0 deletions ironic/tests/unit/api/controllers/v1/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,100 @@ def test_one_field_specific_santization(self, mock_check_policy,
mock.call().__bool__(),
])

@mock.patch.object(policy, 'check', autospec=True)
@mock.patch.object(policy, 'check_policy', autospec=True)
def test_field_redaction_with_owner_not_in_fields(
self, mock_check_policy, mock_check):
"""Regression test for LP#2150573.

When a node owner requests specific fields without including
'owner' in the list, the RBAC policy checks for fields like
last_error must still have access to the node's owner in the
target_dict. Otherwise the project-scoped ownership rule
(project_id == node.owner) can never match and the fields
are incorrectly redacted.
"""
owner_id = 'test-project-id'
obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id,
owner=owner_id,
last_error='meow',
reservation='fake-conductor')
# filter_threshold=False triggers extended sanitization
mock_check_policy.return_value = False

def check_side_effect(rule, target, creds):
if rule in ('baremetal:node:get:last_error',
'baremetal:node:get:reservation'):
# The target_dict (first positional arg after rule)
# must contain node.owner for ownership policy rules
# to evaluate correctly even though 'owner' was not
# in the requested field list.
assert 'node.owner' in target, (
'node.owner missing from target_dict for '
'policy rule %s' % rule)
assert target['node.owner'] == owner_id
return True
return True

mock_check.side_effect = check_side_effect

# Request fields WITHOUT 'owner' - the bug scenario
data = self.get_json(
'/nodes?fields=uuid,last_error,reservation',
headers={api_base.Version.string:
str(api_v1.max_version())})

# Fields must contain actual values, not redaction text
self.assertEqual('meow',
data['nodes'][0]['last_error'])
self.assertEqual('fake-conductor',
data['nodes'][0]['reservation'])
# Owner must NOT be in the response since it was not
# requested, ensuring no information leak.
self.assertNotIn('owner', data['nodes'][0])

@mock.patch.object(policy, 'check', autospec=True)
@mock.patch.object(policy, 'check_policy', autospec=True)
def test_field_redaction_get_one_owner_not_in_fields(
self, mock_check_policy, mock_check):
"""Regression test for LP#2150573 on single-node GET.

Same scenario as the list test above but exercising the
get_one code path through node_convert_with_links with
sanitize=True.
"""
owner_id = 'test-project-id'
node = obj_utils.create_test_node(
self.context,
chassis_id=self.chassis.id,
owner=owner_id,
last_error='meow',
reservation='fake-conductor')
mock_check_policy.return_value = False

def check_side_effect(rule, target, creds):
if rule in ('baremetal:node:get:last_error',
'baremetal:node:get:reservation'):
assert 'node.owner' in target, (
'node.owner missing from target_dict for '
'policy rule %s' % rule)
assert target['node.owner'] == owner_id
return True
return True

mock_check.side_effect = check_side_effect

data = self.get_json(
'/nodes/%s?fields=uuid,last_error,reservation'
% node.uuid,
headers={api_base.Version.string:
str(api_v1.max_version())})

self.assertEqual('meow', data['last_error'])
self.assertEqual('fake-conductor', data['reservation'])
self.assertNotIn('owner', data)

def test_instance_name_field_with_api_version(self):
instance_name = 'test-instance-name'
obj_utils.create_test_node(self.context,
Expand Down
22 changes: 22 additions & 0 deletions ironic/tests/unit/common/test_image_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,28 @@ def test__validate_url_is_specific_not_specific(self):
self.service._validate_url_is_specific,
'oci://foo/bar@baz:meow')

@mock.patch.object(ociclient, '__init__',
return_value=None, autospec=True)
def test_init_verify_ca_path(self, mock_client):
cfg.CONF.set_override('webserver_verify_ca', '/some/path')
image_service.OciImageService()
mock_client.assert_called_with(
mock.ANY, verify='/some/path')

@mock.patch.object(ociclient, '__init__',
return_value=None, autospec=True)
def test_init_verify_ca_true(self, mock_client):
cfg.CONF.set_override('webserver_verify_ca', 'True')
image_service.OciImageService()
mock_client.assert_called_with(mock.ANY, verify=True)

@mock.patch.object(ociclient, '__init__',
return_value=None, autospec=True)
def test_init_verify_ca_false(self, mock_client):
cfg.CONF.set_override('webserver_verify_ca', 'False')
image_service.OciImageService()
mock_client.assert_called_with(mock.ANY, verify=False)


class ServiceGetterTestCase(base.TestCase):

Expand Down
Loading