Rewrite pack — ironic — branch stable/2026.2
Branch
stable/2026.2@39ce09ec3b0d— repository https://opendev.org/openstack/ironic — analysed on 2026-10-29 Target:openkeel-ironic(Go): an API Deployment plus a conductor StatefulSet on the provisioning network, Redfish (virtual media, HTTP boot) or IPMI; ironic-python-agent stays in Python in the ramdisk; Nova routes bare metal flavors to Ironic. Client repositories cited (prefix: commit):nova:8df1922926dd (34.0.0+7),ipa:= ironic-python-agent daa18e2103ef (13.0.0+3),ironicclient:= python-ironicclient af2ceeb9c4f2 (6.3.0+2),sdk:= openstacksdk fbdc403819e0 (4.20.0+3),heat:5872396e548c (27.0.1),skyline:= skyline-console 806d200392ee (9.0.0+1),backend:=~/.claude/skills/generate-go-from-spec/references/backends/(skill directory).
1. Contents of _rewrite/
| File | Use in a rewrite |
|---|---|
[routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json) | the route table: 131 operations discovered by probing the live Pecan app, with real handler file:line, success status, @args.validate validators, @validation.* schemas and microversion bounds, policies used, allow_* gates called, RPC methods called, and a version_probe (status and fault at every microversion 1.1–1.115); plus 107 dispatch_artifact pairs and 2 documented-but-unrouted candidates |
[routes-list_stable-2026.2.json](_rewrite/routes-list_stable-2026.2.json) | the 131 operations as a plain list (router generation, conformance matching) |
[openapi_stable-2026.2.json](_rewrite/openapi_stable-2026.2.json) | OpenAPI 3.0.3 built from the routes, the api-ref and the schemas; x-microversions, x-version-gates, x-policies, x-schema-selection; types and stubs only, not the authority on behaviour |
[request-schemas_stable-2026.2.json](_rewrite/request-schemas_stable-2026.2.json) | 198 JSON schemas: ironic/api/schemas/v1/* (request query/body and response schemas of the newer validation framework) and the module-level schemas of the controllers (node, port, VIF, vmedia, traits, steps…) |
[schema-selection_stable-2026.2.json](_rewrite/schema-selection_stable-2026.2.json) | which schema each handler applies (AST of the handlers) |
[version-gates_stable-2026.2.json](_rewrite/version-gates_stable-2026.2.json) | the 71 allow_* functions with the microversion(s) they test, VERSIONED_FIELDS (field → minimum microversion) and every MINOR_* constant |
[microversions_stable-2026.2.md](_rewrite/microversions_stable-2026.2.md) | microversion history 1.1–1.115 |
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json) | 118 documented operations and their parameters |
[state-machine_stable-2026.2.json](_rewrite/state-machine_stable-2026.2.json) | formal provision state machine: 30 states (stable flag, default target), 93 transitions with file:line, the 11 state sets, verbs |
[state-machine-vectors_stable-2026.2.json](_rewrite/state-machine-vectors_stable-2026.2.json) | 630 vectors: every (state, event) pair → new state, FSM target and the resulting target_provision_state, or InvalidState; 9 explicit-target vectors |
[step-order-vectors_stable-2026.2.json](_rewrite/step-order-vectors_stable-2026.2.json) | step ordering on the real out-of-band step lists of redfish/ipmi + direct/ramdisk |
[drivers_stable-2026.2.json](_rewrite/drivers_stable-2026.2.json) | 6 hardware types (supported/default interfaces) and 66 interface implementations with their driver_info properties and declared clean/deploy/service/verify steps |
[hash-ring-vectors_stable-2026.2.json](_rewrite/hash-ring-vectors_stable-2026.2.json) | node UUID → conductor for 5 conductor sets × 3 ring settings (tooz) |
[errors_stable-2026.2.json](_rewrite/errors_stable-2026.2.json) | 214 exception classes, HTTP code, message template, every raise site |
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [.yaml](_rewrite/policies_stable-2026.2.yaml) | 133 default policy rules with operations (oslopolicy-sample-generator) |
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf) | configuration sample (oslo-config-generator, tools/config/ironic-config-generator.conf) |
[schema-mariadb-migrated_stable-2026.2.sql](_rewrite/schema-mariadb-migrated_stable-2026.2.sql) | DDL to use: all Alembic revisions applied to MariaDB 11.4 (head 9fb44677ef15) |
[schema-mysql_stable-2026.2.sql](_rewrite/schema-mysql_stable-2026.2.sql), [schema-postgresql_stable-2026.2.sql](_rewrite/schema-postgresql_stable-2026.2.sql) | DDL generated from the models |
[schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md) | migrated vs models: 1 difference (an index name) |
[exchanges-api-tests_stable-2026.2.json](_rewrite/exchanges-api-tests_stable-2026.2.json) | 2 215 HTTP exchanges recorded from 2 021 API unit tests (evidence and examples; the tests mock the conductor) |
[response-schemas-api-tests_stable-2026.2.{json,md}](_rewrite/response-schemas-api-tests_stable-2026.2.md), [observed-errors-api-tests_stable-2026.2.md](_rewrite/observed-errors-api-tests_stable-2026.2.md) | response schemas inferred per operation and status, merged over microversions; observed (operation, status, fault) |
[exchanges-rbac_stable-2026.2.json](_rewrite/exchanges-rbac_stable-2026.2.json), [fixture-seeds-rbac_stable-2026.2.json](_rewrite/fixture-seeds-rbac_stable-2026.2.json) | the RBAC matrix: 613 persona × operation cases from the three YAML suites, each with its database seed and configuration |
[response-schemas-rbac_*](_rewrite/response-schemas-rbac_stable-2026.2.md), [observed-errors-rbac_*](_rewrite/observed-errors-rbac_stable-2026.2.md) | same inference on the RBAC run |
[replay-report_stable-2026.2.json](_rewrite/replay-report_stable-2026.2.json) | replay of the RBAC seeds against the original: 613/613 statuses identical |
tools/ | ironic_api.py (routes, probe, schemas, gates, OpenAPI), ironic_core.py (state machine, hash ring, drivers, step order), ironic_errors.py, schema_diff.py, render_tables.py (tables of files 01–07) |
Library versions used for the extraction (Python 3.12 venv, upper-constraints stable/2026.2): pecan 1.8.0, WebOb 1.8.11, automaton 3.5.0, tooz 9.1.0, oslo.messaging 18.3.1, oslo.policy 6.0.1, oslo.db 18.1.1, SQLAlchemy 2.0.52, alembic 1.19.1, keystonemiddleware 13.0.1, sushy 5.13.1.dev6 (local stable/2026.2 clone), futurist 3.5.0, jsonschema 4.26.0, os-traits 3.9.0.
2. HTTP behaviour that the contracts do not show
2.1 Middleware pipeline (outermost first, ironic/api/app.py:80)
- custom
[api]/middlewareentry points (none by default); RequestLogMiddleware;JsonExtensionMiddleware(strips.jsonsuffix);IronicCORS;IronicRequestId(x-openstack-request-id+openstack-request-id);/healthcheckdispatcher (before auth, when[healthcheck]/enabled);HTTPProxyToWSGI; 8. osprofiler (optional);AuthPublicRoutes→ keystonemiddlewareauth_token/ HTTP basic, skipped for/,/v1,/v1/lookup,/v1/heartbeat/*,/v1/continue_inspection;- audit (optional);
JsonDepthMiddleware(depth and size, before parsing);ParsableErrorMiddleware(error body wrapping, defaultAccept);- Pecan with hooks
ConfigHook,DBHook,ContextHook,RPCHook,NoExceptionTracebackHook,PublicUrlHook(ironic/api/hooks.py), then the v1 controller which sets the version headers and checks the version (406) before routing to resources (ironic/api/controllers/v1/__init__.py:177).
Which error wins: 401 (auth) before 400/413 (JSON size/depth) before 406 (version) before 404 (unknown resource) before policy (403/404) before validation (400).
2.2 Error body
{"error_message": "{\"faultcode\": \"Client\", \"faultstring\": \"Node … could not be found.\", \"debuginfo\": null}"} — the inner document is a JSON string. Every client in § 4 parses it this way (ironicclient decodes error_message then reads faultstring, IPA too, the SDK reads faultstring). debuginfo carries a traceback only for 5xx with [DEFAULT]/debug_tracebacks_in_api. Response Content-Type: application/json with the length recomputed.
2.3 Other cross-cutting rules
- No header = microversion 1.1 (not latest).
latestis accepted. Version headers on every response including errors (needed by ironicclient's 406 negotiation,ironicclient:ironicclient/common/http.py:131). - Field visibility by microversion:
VERSIONED_FIELDS(ironic/api/controllers/v1/utils.py:850) — a field newer than the request is removed from responses and rejected in requests (406). - Secrets:
driver_infopasswords masked as"******", all ofdriver_inforedacted withoutbaremetal:node:get:driver_info,instance_infosecrets andconfigdrivemasked (ironic/api/controllers/v1/node.py:1905). - Foreign project → 404, never 403, for node-scoped resources (
conceal_node), while a missing role on an owned node gives 403. Locationheader on 201/202 (…/statesfor provision changes).- Public URL: links use
[api]/public_endpointor the request host (PublicUrlHook).
3. Core algorithms: formal specifications
3.1 Provision state machine
Model. An automaton finite machine wrapped by ironic/common/fsm.py:50 and populated in ironic/common/state_machine.py. Each state has stable (bool) and a default target (a stable state). Each TaskManager copies the machine and initialises it from node.provision_state and node.target_provision_state (ironic/conductor/task_manager.py:256).
States (30):
<!-- GEN:sm-states -->
| State | Stable | Default target | Declared at |
|---|---|---|---|
active | yes | — | ironic/common/state_machine.py:62 |
adopt failed | no | active | ironic/common/state_machine.py:95 |
adopting | no | active | ironic/common/state_machine.py:94 |
available | yes | — | ironic/common/state_machine.py:62 |
clean failed | no | available | ironic/common/state_machine.py:79 |
clean hold | no | available | ironic/common/state_machine.py:80 |
clean wait | no | available | ironic/common/state_machine.py:78 |
cleaning | no | available | ironic/common/state_machine.py:77 |
deleting | no | available | ironic/common/state_machine.py:83 |
deploy failed | no | active | ironic/common/state_machine.py:73 |
deploy hold | no | active | ironic/common/state_machine.py:74 |
deploying | no | active | ironic/common/state_machine.py:71 |
enroll | yes | — | ironic/common/state_machine.py:62 |
error | yes | — | ironic/common/state_machine.py:62 |
inspect failed | no | manageable | ironic/common/state_machine.py:90 |
inspect wait | no | manageable | ironic/common/state_machine.py:91 |
inspecting | no | manageable | ironic/common/state_machine.py:89 |
manageable | yes | — | ironic/common/state_machine.py:62 |
rescue | yes | — | ironic/common/state_machine.py:62 |
rescue failed | no | rescue | ironic/common/state_machine.py:100 |
rescue wait | no | rescue | ironic/common/state_machine.py:99 |
rescuing | no | rescue | ironic/common/state_machine.py:98 |
service failed | no | active | ironic/common/state_machine.py:314 |
service hold | no | active | ironic/common/state_machine.py:315 |
service wait | no | active | ironic/common/state_machine.py:313 |
servicing | no | active | ironic/common/state_machine.py:312 |
unrescue failed | no | active | ironic/common/state_machine.py:102 |
unrescuing | no | active | ironic/common/state_machine.py:101 |
verifying | no | manageable | ironic/common/state_machine.py:66 |
wait call-back | no | active | ironic/common/state_machine.py:72 |
<!-- /GEN:sm-states -->
Transitions (93, the complete relation; any other (state, event) pair raises InvalidState):
<!-- GEN:sm-transitions -->
| # | From | Event | To | Declared at |
|---|---|---|---|---|
| 1 | active | delete | deleting | ironic/common/state_machine.py:140 |
| 2 | active | rebuild | deploying | ironic/common/state_machine.py:136 |
| 3 | active | rescue | rescuing | ironic/common/state_machine.py:234 |
| 4 | active | service | servicing | ironic/common/state_machine.py:321 |
| 5 | adopt failed | adopt | adopting | ironic/common/state_machine.py:306 |
| 6 | adopt failed | manage | manageable | ironic/common/state_machine.py:309 |
| 7 | adopting | done | active | ironic/common/state_machine.py:299 |
| 8 | adopting | fail | adopt failed | ironic/common/state_machine.py:303 |
| 9 | available | deploy | deploying | ironic/common/state_machine.py:86 |
| 10 | available | manage | manageable | ironic/common/state_machine.py:192 |
| 11 | clean failed | manage | manageable | ironic/common/state_machine.py:180 |
| 12 | clean hold | abort | clean failed | ironic/common/state_machine.py:173 |
| 13 | clean hold | unhold | clean wait | ironic/common/state_machine.py:176 |
| 14 | clean wait | abort | clean failed | ironic/common/state_machine.py:164 |
| 15 | clean wait | fail | clean failed | ironic/common/state_machine.py:161 |
| 16 | clean wait | hold | clean hold | ironic/common/state_machine.py:169 |
| 17 | clean wait | resume | cleaning | ironic/common/state_machine.py:170 |
| 18 | cleaning | done | available | ironic/common/state_machine.py:157 |
| 19 | cleaning | fail | clean failed | ironic/common/state_machine.py:160 |
| 20 | cleaning | hold | clean hold | ironic/common/state_machine.py:168 |
| 21 | cleaning | manage | manageable | ironic/common/state_machine.py:189 |
| 22 | cleaning | wait | clean wait | ironic/common/state_machine.py:167 |
| 23 | deleting | clean | cleaning | ironic/common/state_machine.py:154 |
| 24 | deleting | fail | error | ironic/common/state_machine.py:151 |
| 25 | deploy failed | delete | deleting | ironic/common/state_machine.py:148 |
| 26 | deploy failed | deploy | deploying | ironic/common/state_machine.py:111 |
| 27 | deploy failed | rebuild | deploying | ironic/common/state_machine.py:109 |
| 28 | deploy hold | abort | deploy failed | ironic/common/state_machine.py:126 |
| 29 | deploy hold | unhold | wait call-back | ironic/common/state_machine.py:123 |
| 30 | deploying | done | active | ironic/common/state_machine.py:132 |
| 31 | deploying | fail | deploy failed | ironic/common/state_machine.py:105 |
| 32 | deploying | hold | deploy hold | ironic/common/state_machine.py:115 |
| 33 | deploying | wait | wait call-back | ironic/common/state_machine.py:114 |
| 34 | enroll | manage | verifying | ironic/common/state_machine.py:287 |
| 35 | error | delete | deleting | ironic/common/state_machine.py:199 |
| 36 | error | rebuild | deploying | ironic/common/state_machine.py:196 |
| 37 | inspect failed | inspect | inspecting | ironic/common/state_machine.py:231 |
| 38 | inspect failed | manage | manageable | ironic/common/state_machine.py:228 |
| 39 | inspect wait | abort | inspect failed | ironic/common/state_machine.py:221 |
| 40 | inspect wait | done | manageable | ironic/common/state_machine.py:215 |
| 41 | inspect wait | fail | inspect failed | ironic/common/state_machine.py:218 |
| 42 | inspect wait | resume | inspecting | ironic/common/state_machine.py:224 |
| 43 | inspecting | done | manageable | ironic/common/state_machine.py:206 |
| 44 | inspecting | fail | inspect failed | ironic/common/state_machine.py:209 |
| 45 | inspecting | wait | inspect wait | ironic/common/state_machine.py:212 |
| 46 | manageable | adopt | adopting | ironic/common/state_machine.py:296 |
| 47 | manageable | clean | cleaning | ironic/common/state_machine.py:188 |
| 48 | manageable | inspect | inspecting | ironic/common/state_machine.py:203 |
| 49 | manageable | provide | cleaning | ironic/common/state_machine.py:184 |
| 50 | rescue | delete | deleting | ironic/common/state_machine.py:247 |
| 51 | rescue | rescue | rescuing | ironic/common/state_machine.py:244 |
| 52 | rescue | unrescue | unrescuing | ironic/common/state_machine.py:269 |
| 53 | rescue failed | delete | deleting | ironic/common/state_machine.py:263 |
| 54 | rescue failed | rescue | rescuing | ironic/common/state_machine.py:257 |
| 55 | rescue failed | unrescue | unrescuing | ironic/common/state_machine.py:260 |
| 56 | rescue wait | abort | rescue failed | ironic/common/state_machine.py:254 |
| 57 | rescue wait | delete | deleting | ironic/common/state_machine.py:266 |
| 58 | rescue wait | fail | rescue failed | ironic/common/state_machine.py:250 |
| 59 | rescue wait | resume | rescuing | ironic/common/state_machine.py:241 |
| 60 | rescuing | done | rescue | ironic/common/state_machine.py:237 |
| 61 | rescuing | fail | rescue failed | ironic/common/state_machine.py:251 |
| 62 | rescuing | wait | rescue wait | ironic/common/state_machine.py:240 |
| 63 | service failed | abort | active | ironic/common/state_machine.py:370 |
| 64 | service failed | delete | deleting | ironic/common/state_machine.py:367 |
| 65 | service failed | hold | service hold | ironic/common/state_machine.py:364 |
| 66 | service failed | rescue | rescuing | ironic/common/state_machine.py:358 |
| 67 | service failed | service | servicing | ironic/common/state_machine.py:355 |
| 68 | service failed | wait | service wait | ironic/common/state_machine.py:361 |
| 69 | service hold | abort | service failed | ironic/common/state_machine.py:352 |
| 70 | service hold | fail | service failed | ironic/common/state_machine.py:346 |
| 71 | service hold | service | servicing | ironic/common/state_machine.py:334 |
| 72 | service hold | unhold | service wait | ironic/common/state_machine.py:337 |
| 73 | service wait | abort | service failed | ironic/common/state_machine.py:349 |
| 74 | service wait | delete | deleting | ironic/common/state_machine.py:373 |
| 75 | service wait | fail | service failed | ironic/common/state_machine.py:343 |
| 76 | service wait | hold | service hold | ironic/common/state_machine.py:331 |
| 77 | service wait | resume | servicing | ironic/common/state_machine.py:340 |
| 78 | servicing | done | active | ironic/common/state_machine.py:318 |
| 79 | servicing | fail | service failed | ironic/common/state_machine.py:324 |
| 80 | servicing | hold | service hold | ironic/common/state_machine.py:330 |
| 81 | servicing | wait | service wait | ironic/common/state_machine.py:327 |
| 82 | unrescue failed | delete | deleting | ironic/common/state_machine.py:284 |
| 83 | unrescue failed | rescue | rescuing | ironic/common/state_machine.py:278 |
| 84 | unrescue failed | unrescue | unrescuing | ironic/common/state_machine.py:281 |
| 85 | unrescuing | done | active | ironic/common/state_machine.py:272 |
| 86 | unrescuing | fail | unrescue failed | ironic/common/state_machine.py:275 |
| 87 | verifying | done | manageable | ironic/common/state_machine.py:290 |
| 88 | verifying | fail | enroll | ironic/common/state_machine.py:293 |
| 89 | wait call-back | abort | deploy failed | ironic/common/state_machine.py:129 |
| 90 | wait call-back | delete | deleting | ironic/common/state_machine.py:144 |
| 91 | wait call-back | fail | deploy failed | ironic/common/state_machine.py:120 |
| 92 | wait call-back | hold | deploy hold | ironic/common/state_machine.py:116 |
| 93 | wait call-back | resume | deploying | ironic/common/state_machine.py:117 |
<!-- /GEN:sm-transitions -->
State sets used by the rest of the code:
<!-- GEN:sm-sets -->
| Set | States | Declared at |
|---|---|---|
STABLE_STATES | active, available, enroll, error, manageable, rescue | ironic/common/states.py:254 |
UNSTABLE_STATES | adopting, clean wait, cleaning, deleting, deploying, inspect wait, inspecting, rescue wait, rescuing, service wait, servicing, unrescuing, verifying, wait call-back | ironic/common/states.py:257 |
STUCK_STATES_TREATED_AS_FAIL | adopting, cleaning, deleting, deploying, inspecting, rescuing, servicing, unrescuing, verifying | ironic/common/states.py:262 |
UPDATE_ALLOWED_STATES | adopt failed, clean failed, deploy failed, error, inspect failed, inspect wait, inspecting, rescue failed, service, service failed, service hold, unrescue failed, verifying | ironic/common/states.py:246 |
DELETE_ALLOWED_STATES | adopt failed, enroll, manageable | ironic/common/states.py:251 |
LOOKUP_ALLOWED_STATES | clean wait, cleaning, deploying, inspect wait, inspecting, rescue wait, rescuing, service wait, wait call-back | ironic/common/states.py:272 |
FASTTRACK_LOOKUP_ALLOWED_STATES | available, clean wait, cleaning, deploying, enroll, inspect wait, inspecting, manageable, rescue wait, rescuing, service hold, service wait, servicing, wait call-back | ironic/common/states.py:279 |
FAILURE_STATES | adopt failed, clean failed, deploy failed, inspect failed, rescue failed, service failed, unrescue failed | ironic/common/states.py:289 |
SERVICING_STATES | service failed, service hold, service wait, servicing | ironic/common/states.py:237 |
INSPECTION_STATES | inspect failed, inspect wait, inspecting | ironic/common/states.py:241 |
ALL_STATES | active, adopt failed, adopting, available, clean failed, clean hold, clean wait, cleaning, deleted, deleting, deploy complete, deploy failed, deploy hold, deploying, enroll, error, inspect failed, inspect wait, inspecting, manageable, rescue, rescue failed, rescue wait, rescuing, service failed, service hold, service wait, servicing, unrescue failed, unrescuing, verifying, wait call-back | ironic/common/states.py:295 |
<!-- /GEN:sm-sets -->
Semantics (ironic/common/fsm.py:101, ironic/conductor/task_manager.py:553):
process_event(e, target_state=t): if(current, e)is not a transition →InvalidState(409). Elsecurrent := dest.- FSM target: if the new state is the previous target, clear it; if the new state has a default target, it becomes the target; an explicit
target_state(must be a stable state) overrides it. This is how manual cleaning keepstarget = manageablethroughcleaning/clean wait(ironic/conductor/cleaning.py:183,ironic/conductor/task_manager.py:636) while automated cleaning targetsavailable. - Node update by TaskManager:
provision_state := current;target_provision_state := Noneif the new state is stable and no background callback is attached, else the FSM target;last_errorset when given;node.save(); then the provision notification (startwhen going stable→unstable,endunstable→stable,successotherwise,errorforfail), emitted after the spawned worker is accepted (ironic/conductor/task_manager.py:508). - The API pre-checks with
is_actionable_eventon a fresh copy (ironic/api/controllers/v1/node.py:1282); the conductor re-checks under the node lock (authoritative). - Verbs → events:
VERBS(ironic/common/states.py:24): APIactiveanddeploy→deploy,deletedandundeploy→delete, others identical;rebuildis an event, not a state. - Conductor-side dispatch of
do_provisioning_action(ironic/conductor/manager.py:1458):providefrommanageable(refused in maintenance unless allowed, refused for retired nodes) →provide+ automated cleaning;managefromenroll→ verify;adoptfrommanageable/adopt failed;abortfromclean wait,rescue wait,inspect wait,clean hold,wait call-back,deploy hold,service wait,service hold,service failed→_do_abort(§ 3.3.6);unhold; anything else → plainprocess_event, withInvalidStateturned intoInvalidStateRequested(400). - Stuck states: on conductor start, nodes in
STUCK_STATES_TREATED_AS_FAILgetfail(ironic/conductor/base_manager.py:327); forverifyingthefailevent goes back toenroll, fordeletingtoerror.
Vectors. state-machine-vectors_stable-2026.2.json: 30 states × 21 events = 630 cases, 93 valid; each valid case gives new_state, fsm_target, target_provision_state_no_callback and target_provision_state_with_callback. A Go implementation is conformant when it reproduces all 630 outcomes and the 9 target_overrides cases. They are generated from the live machine, so they are the machine; they do not encode the conductor-side guards of item 6 (maintenance, retired, abortable steps), which are covered by the unit tests listed in § 7.
3.2 Node locks (TaskManager)
acquire(ctx, node, shared=False, purpose, retry=True, patient=False)(ironic/conductor/task_manager.py:158).- Exclusive:
UPDATE nodes SET reservation=<conductor host> WHERE id=? AND reservation IS NULL(ironic/db/sqlalchemy/api.py:752); failure →NodeLocked; retriednode_locked_retry_attempts(3) ×node_locked_retry_interval(1 s) withretry, forever withpatient, once without (ironic/conductor/task_manager.py:343). - Shared: no DB write;
upgrade_lock(retry)turns it exclusive and reloads the node. - Release when the context exits, or when the spawned worker finishes (
spawn_after,ironic/conductor/task_manager.py:411); a worker exception is written tolast_error("Async execution of %(method)s failed with error: %(error)s"). - Lock holder name = the conductor
host(with:portunder JSON-RPC). A Go conductor must use a stable per-pod name (StatefulSet ordinal) so that a restarted pod can clear its own reservations (ironic/db/sqlalchemy/api.py:1538). require_exclusive_lockguards (ironic/conductor/task_manager.py:128) →ExclusiveLockRequired(403).
3.3 Step framework (clean, deploy, service, verify)
3.3.1 Step model
A step is {"interface", "step", "priority", "args", "abortable"?, "argsinfo"?, "requires_ramdisk"?, "execute_on_child_nodes"?, "limit_child_node_execution"?}, declared on driver methods by @clean_step, @deploy_step, @service_step, @verify_step (ironic/drivers/base.py:2214, ironic/drivers/base.py:2298, ironic/drivers/base.py:2369, ironic/drivers/base.py:2410), or returned at run time by the agent (clean.get_clean_steps, deploy.get_deploy_steps, service.get_service_steps), cached in driver_internal_info.agent_cached_*_steps. Step id = "<interface>.<step>" (ironic/conductor/steps.py:571).
3.3.2 Ordering
- Interface priorities (tie-breakers, higher first): clean, deploy and service
vendor 7, power 6, management 5, firmware 4, deploy 3, bios 2, raid 1(service addsnetwork 8); verifypower 13, management 12, firmware 11, inspect 10, deploy 9, boot 8, bios 7, raid 6, vendor 5, network 4, storage 3, console 2, rescue 1(ironic/conductor/steps.py:30). - Sort key
(priority, interface_priority), descending, stable sort (Pythonsorted(..., reverse=True)keeps input order for full ties),ironic/conductor/steps.py:125. Service steps are not sorted (user order). - Priority 0 = disabled, removed when
enabled=Trueafter applying[conductor]/clean_step_priority_override/verify_step_priority_override({"<iface>.<step>": prio}),ironic/conductor/steps.py:148. - Operator deny lists
[api]/disallow_{clean,deploy,service}_steps: user steps →StepNotAllowed(400), automated/runbook steps silently filtered (ironic/conductor/steps.py:582).
3.3.3 Composition per flow
- Automated cleaning: enabled driver clean steps sorted (
ironic/conductor/steps.py:198), or a runbook's steps whenprovideuses one (declarative cleaning), validated as user steps. Skipped entirely whennode.automated_cleanis false or[conductor]/automated_cleanfalse (ironic/conductor/utils.py:1227). - Manual cleaning: the user's list in the user's order (no sort), each validated against the driver's steps (
ironic/conductor/steps.py:925);abortableandprioritycopied from the driver step. - Deploy (
ironic/conductor/steps.py:457): user deploy steps (deploy_stepsargument, 1.69+) ∪ steps of deploy templates whose name is ininstance_info.traits(user wins on the sameinterface.step) — then driver enabled steps not overridden — then user/template steps with priority > 0 — sorted by the deploy key. Duplicateinterface.stepacross templates → error. Overriding the coredeploy.deploystep with priority > 0 → error ("core step"). - Service: user list, unsorted, validated against
get_service_steps. - Verify (on
manage): enabled verify steps sorted by the verify key.
3.3.4 Validation of a user step (ironic/conductor/steps.py:650, ironic/conductor/steps.py:814)
- Skip validation for
execute_on_child_nodesand reserved namespower_on,power_off,reboot,hold,wait. - Unknown
interface.step→ error "node does not support this <type> step", unlessskip_missing(in-band steps not yet known before the agent boots; revalidated after the first heartbeat). - Unexpected
args(not inargsinfo) → error; missingrequiredargs → error (for deploy steps only when priority > 0);disable_ramdiskwith a step requiring the ramdisk → error. - All errors joined with
"; "into oneInvalidParameterValue(400) with an optional prefix. - Per-step RBAC: if policy rule
baremetal:step:execute:<iface>.<step>exists, it must pass withnode.owner/node.lesseein the target (ironic/conductor/steps.py:753).
3.3.5 Execution loop
See 05 § 3.2. State variables: node.<kind>_step (current), driver_internal_info.<kind>_steps (list), <kind>_step_index, <kind>ing_reboot, skip_current_<kind>_step, <kind>ing_polling, agent_cached_<kind>_steps. Results: None → next; *WAIT → wait event; anything else → error "step returned invalid value". Reserved steps (ironic/conductor/steps.py:87): power_on/power_off/reboot call node_power_action; hold → hold event and exit; wait → wait, sleep min(args.seconds, [conductor]/max_conductor_wait_step_seconds=30), resume. Next index after an asynchronous step: update_next_step_index (ironic/conductor/utils.py:1496) skips the current step unless skip_current_<kind>_step is false (agent was rebooting).
3.3.6 Abort (ironic/conductor/manager.py:1553)
| State | Behaviour |
|---|---|
clean wait, clean hold | current step not abortable → set clean_step.abort_after = true, return (abort happens in continue_node_clean when the step finishes: fail then do_node_clean_abort, ironic/conductor/cleaning.py:514); otherwise abort event (target kept manageable for manual cleaning) + worker do_node_clean_abort (tear down cleaning, last_error "By request, the clean operation was aborted [during or after the completion of step "<id>"]") |
service wait, service hold | non-abortable → abort_after; else fail then abort → active via do_node_service_abort |
service failed | refused in maintenance (InvalidState); else abort → active |
rescue wait | remove rescue password, abort → rescue failed + _do_node_rescue_abort |
inspect wait | abort_inspection (interface-specific) → inspect failed |
wait call-back | non-abortable deploy step → abort_after (1.110+ allows aborting here); else remove agent_url, abort → deploy failed + do_node_deploy_abort |
deploy hold | remove agent_url, abort → deploy failed + tear down |
3.4 Conductor hash ring
- Input: online conductors (
updated_atnewer thanheartbeat_timeout) with their enabled hardware types and group; key"<group>:<hw_type>"(ironic/db/sqlalchemy/api.py:1573). tooz.hashring.HashRing(hosts, partitions=2**hash_partition_exponent, hash_function=hash_ring_algorithm); owner =get_nodes(node.uuid.encode('utf-8'))(ironic/conductor/rpcapi.py:247). Cached 15 s, rebuilt on miss.- Vectors:
hash-ring-vectors_stable-2026.2.json. Reproducing tooz bit-for-bit is required only if Python and Go conductors share a deployment during migration; otherwise any consistent hashing with the same inputs works, but the API and all conductors must agree. - In Kubernetes the conductor identity should be the StatefulSet pod name (stable), not the pod IP.
3.5 Agent token
- Generated once per ramdisk session:
secrets.token_urlsafe()stored indriver_internal_info.agent_secret_token(ironic/conductor/utils.py:1509); pre-generated for virtual media (embedded in the ISO kernel parameters,agent_secret_token_pregenerated). - Returned only by the first lookup after generation; later lookups get
"******"(ironic/conductor/manager.py:4013). - Required on heartbeat (
ironic/conductor/utils.py:1543, compared with==: a rewrite should use a constant-time comparison) and sent by the conductor on every agent command (agent_tokenquery parameter,ironic/drivers/modules/agent_client.py:200). - Wiped with
agent_urlwhen the node powers off or the flow ends (ironic/conductor/utils.py:655).
3.6 Microversion gating
Two layers to reproduce: route availability (version_probe in routes_*.json: status 404/405/406 below the first version, faults such as "The API version does not allow allocations", "Request not acceptable.") and field/parameter availability (VERSIONED_FIELDS, allow_* functions in version-gates_*.json). The status used for "too old" is inconsistent across resources (404 for GET, 405 for writes of allocations/deploy templates/runbooks/inspection rules, 406 for verbs and fields) and must be copied per route from the probe data, not unified.
4. What the clients expect
| Client | Expectation | Where the client relies on it |
|---|---|---|
| Nova ironic driver | minimum microversion 1.46 (conductor groups); 1.82 for shards; SDK negotiates up to its own max (node 1.109) | nova:nova/virt/ironic/driver.py:64, nova:nova/virt/ironic/driver.py:2273, sdk:openstack/baremetal/v1/node.py:110 |
| Nova | GET /v1/nodes?fields=uuid,power_state,target_power_state,provision_state,target_provision_state,last_error,maintenance,properties,instance_uuid,traits,resource_class (+ conductor_group, shard, instance_uuid filters) must honour fields | nova:nova/virt/ironic/driver.py:81, nova:nova/virt/ironic/driver.py:590 |
| Nova | reservation by PATCH instance_uuid must answer 409 when another instance holds the node (no retry: retry_on_conflict=False) | nova:nova/virt/ironic/driver.py:364 |
| Nova | JSON-patch of instance_info/* keys (image_source, root_gb, swap_mb, capabilities as JSON string, traits list, configdrive via provision) — free-form dict, no schema | nova:nova/virt/ironic/patcher.py |
| Nova | GET …/validate returns {"deploy": {"result": bool, "reason"}, "power": {...}, ...} | nova:nova/virt/ironic/driver.py:1208 |
| Nova | provision verbs active (+ configdrive string/dict), rebuild, deleted, rescue (+ rescue_password), unrescue; 400 with a "deprovisioned state" message on tear-down of an already-clean node is tolerated | nova:nova/virt/ironic/driver.py:1257, nova:nova/virt/ironic/driver.py:1295, nova:nova/virt/ironic/driver.py:2317 |
| Nova | power: power on, power off, rebooting, soft rebooting (400 → falls back to hard reboot), soft power off with timeout | nova:nova/virt/ironic/driver.py:1431 |
| Nova | VIF attach POST …/vifs (400 on failure is fatal, 409 for "already attached" is handled), detach 400 when not attached is ignored | nova:nova/virt/ironic/driver.py:1586, nova:nova/virt/ironic/driver.py:1630 |
| Nova | console GET/PUT …/states/console; volume connectors/targets for boot from volume | nova:nova/virt/ironic/driver.py:1861, nova:nova/virt/ironic/driver.py:458 |
| Nova | provision states copied in nova/virt/ironic/ironic_states.py (same strings) | nova:nova/virt/ironic/ironic_states.py |
| Nova | inventory: one provider per node, CUSTOM_<normalised resource_class> total 1, reserved when in use or unavailable; traits copied from the node | nova:nova/virt/ironic/driver.py:874 |
| openstacksdk | retries 409 and 503 (node locked, no free worker) on most calls; wait_for_provision_state polls until the stable state of EXPECTED_STATES; reads faultstring from the error body | sdk:openstack/baremetal/v1/_common.py:22, sdk:openstack/baremetal/v1/node.py:586 |
| openstacksdk | per-verb minimum microversions (abort 1.13, adopt 1.17, clean 1.15, inspect 1.6, manage/provide 1.4, rescue 1.38, unhold 1.85, service 1.87, configdrive dict 1.56, deploy steps 1.69) | sdk:openstack/baremetal/v1/_common.py:31 |
| ironic-python-agent | GET / → default_version.version to pick the version; lookup at min(server, 1.62); heartbeat at min(server, 1.68) with callback_url, agent_token (1.62+), agent_version (1.36+), agent_verify_ca (1.68+) | ipa:ironic_python_agent/ironic_api_client.py:34, ipa:ironic_python_agent/ironic_api_client.py:159 |
| ironic-python-agent | heartbeat success = 202; 409 = locked, retry soon; anything else = error | ipa:ironic_python_agent/ironic_api_client.py:189 |
| ironic-python-agent | lookup: 200 with node.uuid and config (or legacy heartbeat_timeout); 409 → back off 5/10/30 s; other → retry with exponential backoff until timeout | ipa:ironic_python_agent/ironic_api_client.py:212 |
| ironic-python-agent | error parsing: error_message (string or object) → faultstring | ipa:ironic_python_agent/ironic_api_client.py:139 |
| ironic-python-agent | config keys read by the agent: agent_token, agent_token_required, heartbeat_timeout, metrics, agent_md5_checksum_enable, permitted_image_formats, disable_deep_image_inspection, agent_skip_bmc_detect, tls_min_version, tls_cipher_suites, agent_containers | ironic/api/controllers/v1/ramdisk.py:46 |
| ironic-python-agent (served) | the conductor's command contract: POST /v1/commands/?wait=&agent_token= with {"name": "<ext>.<cmd>", "params"}; GET /v1/commands/; 409 or "agent is busy" = in progress | ipa:ironic_python_agent/api/app.py:140, ironic/drivers/modules/agent_client.py:200 |
| ironic-python-agent | inspection: POST <api>/v1/continue_inspection; 5xx and 409 retried | ipa:ironic_python_agent/inspector.py:141 |
python-ironicclient / OSC baremetal | default microversion 1.9 when none is requested, latest = 1.115; on 406 negotiates using X-OpenStack-Ironic-API-Minimum/Maximum-Version (GET /v1 when absent) | ironicclient:ironicclient/common/http.py:44, ironicclient:ironicclient/common/http.py:131 |
| python-ironicclient | retries Conflict (409) and ServiceUnavailable (503) 5 times every 2 s | ironicclient:ironicclient/common/http.py:324 |
| python-ironicclient | decodes error_message (JSON string) → faultstring/debuginfo | ironicclient:ironicclient/common/http.py:96 |
| Heat | only OS::Ironic::Port (properties node, address, portgroup 1.24, local_link_connection/pxe_enabled 1.19, physical_network 1.34, is_smartnic 1.53) and constraints ironic.node/ironic.portgroup (GET by name or UUID); client at ironicclient's latest version | heat:heat/engine/resources/openstack/ironic/port.py:24, heat:heat/engine/clients/os/ironic.py:28 |
| Skyline console | pins X-Openstack-Ironic-Api-Version: 1.58; uses nodes (list/detail/create/patch), states, validate, states/provision (targets provide, manage, delete), states/power, maintenance, management/boot_device[/supported], traits PUT, ports and port groups | skyline:src/client/client/constants.js:103, skyline:src/client/ironic/index.js:18, skyline:src/pages/compute/containers/BareMetalNode/actions/ManageState.jsx:75 |
Statuses that must not change: 202 for heartbeat; 409 for locked nodes (retried by 3 clients); 404 (not 403) for foreign nodes; 406 with version headers for unsupported versions; 400 InvalidStateRequested vs 409 NodeLocked on refused transitions.
5. Database strategy
- Reuse the migrated schema (
schema-mariadb-migrated_stable-2026.2.sql); models and migrations agree except one index name (schema-diff). JSON columns (driver_info,instance_info,driver_internal_info,properties,extra, step lists) areTEXTwith JSON content (JsonEncodedDict): keep them opaque JSON in Go. - Concurrency: no optimistic versioning; the only node mutual exclusion is
nodes.reservation(§ 3.2). Allocations use compare-and-swap onconductor_affinity. Writes by the API (chassis, deploy templates, runbooks, inspection rules) are plain. - Galera: the reservation
UPDATE … WHERE reservation IS NULLis a single-row write; Galera certification conflicts surface as deadlocks, retried by oslo.dbretry_on_deadlock(ironic/db/sqlalchemy/api.py:780): the Go layer must retry deadlocks too. - Secrets at rest:
driver_info(BMC passwords) anddriver_internal_info.agent_secret_tokenare stored in clear in the database; only API output is masked (ironic/api/controllers/v1/node.py:1905). - Startup data: none (no standard rows); conductors register themselves.
- Online data migrations:
ironic-dbsync online_data_migrationsexists for upgrades from older releases; a new OpenKeel deployment starts at the head revision.
6. Authentication, policies, configuration and CLI
- Tokens: keystonemiddleware
auth_token(validation and caching against Keystone) or HTTP basic; the API also needs the service catalog of the token (X-Service-Catalog) for some outbound calls with the user's token. - Policies: 133 oslo.policy rules with the oslo rule language (
role:,project_id:%(node.owner)s,system_scope:all,rule:), deprecated rules andenforce_new_defaults/enforce_scope; targets built per request withnode.owner,node.lessee,allocation.owner,runbook.owner(ironic/api/controllers/v1/utils.py:1751). Per-field PATCH policies (ironic/api/controllers/v1/node.py:3147) and per-step policies (baremetal:step:execute:*) are dynamic and must be implemented. - Configuration: oslo.config sample in
_rewrite/config_stable-2026.2.conf. The Go service should accept the same option names in YAML for the options it implements; § 10 lists the ones OpenKeel needs. - CLI:
ironic-dbsync(upgrade, online migrations),ironic-status upgrade check,ironic-manage— replace by an init Job (migrate) in the Helm chart.
7. Conformance strategy
- RBAC matrix (replayable):
exchanges-rbac+fixture-seeds-rbac= 613 cases fromironic/tests/unit/api/test_rbac_legacy.yaml,ironic/tests/unit/api/test_rbac_system_scoped.yaml,ironic/tests/unit/api/test_rbac_project_scoped.yamlandironic/tests/unit/api/test_acl_basic.yaml, run byironic/tests/unit/api/test_acl.py:33. Each case: database rows (load by natural key:uuid,name; re-pointnode_id,portgroup_idforeign keys), configuration overrides, persona headers (X-Roles,X-Project-Id,OpenStack-System-Scope, as keystonemiddleware would set them), request, expected status and body. Proof: replayed from the seeds alone against the original (adapters/ironic.py replay_seeds): 613/613 statuses identical, 590 bodies identical, 23 identical status with differing bodies (timestamps and generated links) (replay-report_stable-2026.2.json). The conductor is absent in this suite (get_topic_forraisesTemporaryFailure): write operations end in 503 after the policy check, which is exactly what the matrix tests. - API examples:
exchanges-api-tests(2 215 exchanges from 2 021 tests ofironic/tests/unit/api/controllers/v1/andironic/tests/unit/api/test_root.py,ironic/tests/unit/api/test_hooks.py,ironic/tests/unit/api/test_middleware.py) with inferred response schemas per microversion and observed errors. The RPC layer is mocked in these tests, so they show API validation and serialisation, not conductor effects; they are differential-testing material, not replayable cases. - State machine: 630 + 9 vectors (§ 3.1). Step ordering:
step-order-vectors. Hash ring:hash-ring-vectors. - Route availability per microversion:
version_probeof every route (§ 3.6). - IPA contract: the agent's own tests mock Ironic; the contract is in § 4 and in
ironic/tests/unit/api/controllers/v1/test_ramdisk.py(exchanges recorded inexchanges-api-tests). A live conformance run needs a VM booting the real IPA ramdisk (sushy-tools Redfish emulator + libvirt), which is the recommended end-to-end gate (§ 8). - Original fixtures:
ironic/tests/unit/api/base.py:37(BaseApiTest, Pecan test app, noauth),ironic/tests/unit/db/base.py:34(SQLite schema from the models, cached and re-applied per test),ironic/tests/unit/api/test_acl.py:33(keystone auth with stubbedprocess_request, no conductor),ironic/tests/base.py:144(config fixture: enabled interfacesfake…, policy file withipa_lookup/ipa_heartbeat/ipa_continue_inspection=@).
8. Gaps found while building the pack
- No route table in the code: routes were discovered (probe); a route reachable only through a code path the candidates do not name would be missed. Candidates = api-ref ∪ policy operations ∪ all verbs on those paths.
- api-ref typos and the indicator path mismatch (01 § 3.1);
DELETE /v1/inspection_rulesanswers 500 in the probe. rewrite_pack.py --schemas-package ironic.api.schemas.v1fails with a circular import (schemas are lower-case attributes and import each other): replaced bytools/ironic_api.py;--version-historydoes not parse Ironic's1.x (Release)headings: same tool.ipmitoolinterfaces:get_properties()could not be introspected without the binary path (empty property lists indrivers_*.jsonforpower:ipmitool/management:ipmitool); the property list is inironic/drivers/modules/ipmitool.py:350.- In-band step lists (erase_devices, write_image internals) come from IPA at run time and are not in
drivers_*.json. - No conformance data for the conductor flows (deploy/clean/inspect end-to-end): the unit tests mock drivers and the agent. Recommended: Metal3-style e2e with sushy-tools + virtual BMC + real IPA ramdisk, recording conductor ↔ IPA and conductor ↔ BMC exchanges.
- Release note/commit CVE id mismatch for the maintenance policy fix (06 § 22).
9. Non-functional requirements and Kubernetes design notes
- Conductor identity: StatefulSet ordinal names (
ironic-conductor-0…) ashost; under JSON-RPC the ring entry ishost:port, so the conductor must be reachable by that name (headless Service). - RPC transport: JSON-RPC over HTTPS with Keystone or mTLS between API and conductor removes RabbitMQ; the API → conductor addressing is by hash ring;
cast= JSON-RPC notification (noid, 204). - Local state per conductor:
/tftpboot,/httpboot(iPXE scripts, ISOs, configdrives), image cache (master copies + per-node links). It is rebuilt on takeover (_do_takeover), so a per-pod PVC is an optimisation, not a correctness requirement; but the HTTP server that IPA and the BMC download from must serve the owning conductor's files: one HTTP sidecar per conductor pod, URL in[deploy]/http_urlper pod (orexternal_http_urlper node). - Network reachability: conductor → BMC (HTTPS/IPMI UDP 623), conductor → IPA
callback_url(port 9999 on the provisioning network), IPA → API (lookup/heartbeat) and IPA → conductor HTTP (images), BMC → conductor HTTP (virtual media ISO). With Redfish virtual media no DHCP/TFTP is needed if the ramdisk gets its IP from the ISO's network config or the provisioning network's DHCP. - Timers: keep the defaults of § 05 (heartbeat 10 s / timeout 60 s, callback timeouts 1800 s, power sync 120 s).
- Workers: bounded pool (300 + 5 % reserved) with 503 on saturation; heartbeats must not consume the reserved pool.
- Throttling: IPMI one command per 5 s per BMC; Redfish sessions cached per BMC (
ironic/drivers/modules/redfish/utils.py:285). - Secrets: BMC credentials from Kubernetes Secrets rather than
driver_infoin clear (a deliberate divergence: keepdriver_info.redfish_passwordaccepted and masked in the API, but store it encrypted or as a Secret reference), agent token compared in constant time, step arguments masked in logs (change 16 of 06), BMC URL components validated (change 21).
10. Relation to backend:ironic-baremetal.md
| Statement of the backend reference | Verdict from this spec |
|---|---|
Ironic stable/2026.2 39.0.0+29, microversions up to 1.115 | fits (ironic/api/controllers/v1/versions.py:300) |
| ironic-inspector gone, inspection built in, hooks registered as entry points | fits (ironic/drivers/modules/inspector/interface.py:154, ironic.inspection.hooks in pyproject.toml); add: inspection rules (/v1/inspection_rules, 1.96+) replace inspector's rules, and /v1/continue_inspection (1.84+) replaces inspector's /v1/continue |
| IPA kept in Python; contract = agent REST API (9999) + lookup/heartbeat | fits; the precise contract (versions, statuses, token lifecycle, config keys, command names) is § 3.5 and § 4 |
| sushy replaced by a Go Redfish library | fits; the pack lists the Redfish calls actually used (03 § 6) and the asynchronous task-monitor polling (firmware, BIOS, RAID) that the Go layer must reproduce |
| conductor = StatefulSet, Multus or hostNetwork, dnsmasq + HTTP sidecars | fits; missing: per-conductor HTTP URL (§ 9.3), conductor identity = ring entry (host:port under JSON-RPC, § 9.1), and IPA → conductor and BMC → conductor flows on top of conductor → BMC |
prefer redfish-virtual-media / redfish-https / http-ipxe | fits, but **the default boot interface of the redfish hardware type is ipxe** (first in supported_boot_interfaces, drivers_stable-2026.2.json): OpenKeel must set enabled_boot_interfaces and default_boot_interface (or per-node boot_interface) explicitly |
| BMC credentials "encrypted as Ironic does" | does not fit: Ironic stores driver_info in clear and only masks it on output (§ 5) — the encryption or Secret indirection is an OpenKeel addition |
Nova: one RP per node with CUSTOM_BAREMETAL_*, VCPU/MEMORY/DISK 0 | fits Nova's driver (nova:nova/virt/ironic/driver.py:874): the RC is CUSTOM_ + normalised node.resource_class, total 1, reserved while in use or in maintenance; the Go "Ironic compute agent" must also copy node traits and keep the reservation rules |
Neutron: ironic-networking with generic-switch, or Kube-OVN underlay | fits; add: ironic-networking is a separate process with its own RPC topic (ironic.networking_manager) and switch configuration file; the neutron interface's binding contract (binding:host_id = node uuid, vnic_type=baremetal, local_link_information) is what Neutron-Go would have to accept (§ 03 § 4) |
| Cinder boot from volume: gap | fits (storage interface cinder exists; not shipped) |
| API resources and RBAC (owner/lessee) | fits; the RBAC matrix (613 replayable cases) is the acceptance test |
hardware types redfish, ipmi (+idrac) | fits; drivers_*.json gives their interfaces and defaults |
| Missing in the reference | JSON-RPC transport option (no RabbitMQ), the hash ring and takeover semantics, [agent]/require_tls default true (plain-HTTP agents are refused), the agent token flow for virtual media (pre-generated token in the ISO), the conductor periodic tasks and their defaults, Trait Based Networking (new in 2026.2, optional), node history/inventory/firmware caches, the two CVE fixes of the branch |
11. Rewrite checklist
- [ ] Router from
routes-list_*.json; return 405 on dispatch artifacts; keep per-route "too old" statuses fromversion_probe. - [ ] Microversion middleware: default 1.1,
latest, 406 + min/max headers on every response. - [ ] Error envelope
{"error_message": "<json string>"}withfaultcode/faultstring/debuginfo. - [ ]
VERSIONED_FIELDSfiltering on output, 406 on input. - [ ] oslo.policy-compatible engine, 133 default rules, owner/lessee targets, per-field PATCH policies (incl.
/maintenance), per-step policies, 404 concealment. - [ ] State machine from
state-machine_*.json; pass all 639 vectors. - [ ] TaskManager lock semantics on
nodes.reservation; deadlock retries; startup clearing of own reservations. - [ ] Step framework: ordering keys, overrides, deny lists, composition per flow, validation messages, reserved steps, abort/abort_after.
- [ ] Hash ring over live conductors per group/hardware type; takeover periodic; orphan handling.
- [ ] JSON-RPC server/client (or keep oslo.messaging-compatible frames if Python conductors must coexist).
- [ ] Ramdisk API: lookup, heartbeat (202/409), continue_inspection; token generate-once, constant-time compare;
configdocument. - [ ] Agent client: command contract, timeouts, busy (409) handling, last-command recovery.
- [ ] Redfish (power, boot override, virtual media insert/eject, HTTP boot, secure boot, task monitors) and IPMI (ipmitool or Go IPMI with 5 s throttle).
- [ ] Image pipeline: Glance metadata/download, cache, HTTP publishing per conductor, Swift temp URLs optional; ISO build for virtual media.
- [ ] Network interfaces
flatandneutron(provisioning/cleaning networks, tenant binding); DHCP provider choice. - [ ] Periodic tasks with the same defaults; power sync with Nova
power-update. - [ ] Inspection: hooks (
ramdisk-error,validate-interfaces,ports,architecture+ chosen extras), inspection rules engine (15 operators, 18 actions). - [ ] Pass the RBAC matrix (613), then an e2e with sushy-tools + real IPA, then Nova's ironic driver flows (§ 4).
Source: reverse-spec-ironic/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.