diff --git a/components/images-openstack.yaml b/components/images-openstack.yaml index 41cadc93a..4b20f34bd 100644 --- a/components/images-openstack.yaml +++ b/components/images-openstack.yaml @@ -22,12 +22,12 @@ images: keystone_fernet_setup: "ghcr.io/rackerlabs/understack/keystone:2025.2" # ironic - ironic_api: "ghcr.io/rackerlabs/understack/ironic:2025.2" - ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:2025.2" - ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:2025.2" + ironic_api: "ghcr.io/rackerlabs/understack/ironic:pr-1671" + ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:pr-1671" + ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:pr-1671" ironic_pxe_init: "ghcr.io/rackerlabs/understack/ironic:2025.2" ironic_pxe_http: "docker.io/nginx:1.29.4" - ironic_db_sync: "ghcr.io/rackerlabs/understack/ironic:2025.2" + ironic_db_sync: "ghcr.io/rackerlabs/understack/ironic:pr-1671" # these want curl which apparently is in the openstack-client image ironic_manage_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" ironic_retrive_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" diff --git a/containers/ironic/Dockerfile b/containers/ironic/Dockerfile index e2aab70eb..0f5cfdac9 100644 --- a/containers/ironic/Dockerfile +++ b/containers/ironic/Dockerfile @@ -33,7 +33,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ COPY containers/ironic/patches /tmp/patches/ RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ - patch -p1 < /tmp/patches/0001-Add-portgroup-name-validation-middleware-2025.2.patch + patch -p1 < /tmp/patches/0001-pluggable-api-middleware.patch RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ patch -p1 < /tmp/patches/0002-Solve-IPMI-call-issue-results-in-UTF-8-format-error-.patch RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ diff --git a/containers/ironic/patches/0001-pluggable-api-middleware.patch b/containers/ironic/patches/0001-pluggable-api-middleware.patch new file mode 100644 index 000000000..dda341296 --- /dev/null +++ b/containers/ironic/patches/0001-pluggable-api-middleware.patch @@ -0,0 +1,91 @@ +diff --git a/ironic/api/app.py b/ironic/api/app.py +index 7f7bc0c3d..6370fa140 100644 +--- a/ironic/api/app.py ++++ b/ironic/api/app.py +@@ -18,11 +18,13 @@ + import keystonemiddleware.audit as audit_middleware + from keystonemiddleware import auth_token + from oslo_config import cfg ++from oslo_log import log as logging + import oslo_middleware.cors as cors_middleware + from oslo_middleware import healthcheck + from oslo_middleware import http_proxy_to_wsgi + import osprofiler.web as osprofiler_web + import pecan ++import stevedore + + from ironic.api import config + from ironic.api.controllers import base +@@ -33,8 +35,10 @@ from ironic.api.middleware import json_ext + from ironic.api.middleware import request_log + from ironic.common import auth_basic + from ironic.common import exception ++from ironic.common.i18n import _ + from ironic.conf import CONF + ++LOG = logging.getLogger(__name__) + + class IronicCORS(cors_middleware.CORS): + """Ironic-specific CORS class +@@ -143,8 +147,36 @@ def setup_app(pecan_config=None, extra_hooks=None): + # Add request logging middleware + app = request_log.RequestLogMiddleware(app) + ++ if CONF.api.middleware: ++ app = _load_custom_middleware(app, CONF.api.middleware) ++ + return app + ++def _missing_middleware_callback(names): ++ """Raise RuntimeError with list of missing middleware.""" ++ error = _('The following middleware failed to load: %s') ++ raise RuntimeError(error % ', '.join(names)) ++ ++ ++def _load_custom_middleware(app, middleware_names): ++ """Load custom WSGI middleware via stevedore entry points. ++ ++ :param app: The WSGI application to wrap ++ :param middleware_names: List of middleware names to load ++ :returns: The wrapped WSGI application ++ """ ++ LOG.info('Loading custom API middleware: %s', middleware_names) ++ mgr = stevedore.NamedExtensionManager( ++ 'ironic.api.middleware', ++ names=middleware_names, ++ invoke_on_load=False, ++ on_missing_entrypoints_callback=_missing_middleware_callback, ++ name_order=True, ++ ) ++ for ext in mgr: ++ LOG.info('Applying middleware: %s (%s)', ext.name, ext.plugin) ++ app = ext.plugin(app) ++ return app + + class VersionSelectorApplication(object): + def __init__(self): +diff --git a/ironic/conf/api.py b/ironic/conf/api.py +index 9fae0fcc5..aec1b60a1 100644 +--- a/ironic/conf/api.py ++++ b/ironic/conf/api.py +@@ -109,6 +109,19 @@ opts = [ + cfg.StrOpt('key_file', + help="Private key file to use when starting " + "the server securely."), ++ cfg.ListOpt('middleware', ++ default=[], ++ help=_('Comma-separated list of WSGI middleware to load ' ++ 'from the ironic.api.middleware entry point namespace. ' ++ 'Middleware are applied in the order specified, ' ++ 'wrapping the API application. This allows operators ' ++ 'to add custom request processing (validation, ' ++ 'logging, ' ++ 'rate limiting, etc.) without modifying Ironic code. ' ++ 'External packages can register middleware by adding ' ++ 'entry points to the ironic.api.middleware namespace. ' ++ 'Each middleware must be a callable that accepts a ' ++ 'WSGI application and returns a wrapped application.')), + ] + + +