REST API — ironic — branch stable/2026.2
Branch
stable/2026.2@39ce09ec3b0d— repository https://opendev.org/openstack/ironic — analysed on 2026-10-29
Ironic exposes one API, /v1, served by ironic-api (or by the all-in-one ironic process). It is a Pecan application (ironic/api/app.py:80) with object dispatch: the v1 root controller maps the first path segment to a sub-controller (ironic/api/controllers/v1/__init__.py:126), and each sub-controller is a Pecan RestController whose verbs, _custom_actions, _subcontroller_map and _lookup produce the routes. There is no route table in the code, so the table below was discovered by probing the live application (_rewrite/tools/ironic_api.py, see 07 § 1): 131 operations, plus 107 (method, path) pairs that Pecan dispatches to a handler although nobody documents them ("dispatch artifacts", § 3.3).
1. Cross-cutting rules
| Rule | Behaviour | Source |
|---|---|---|
| Microversion header | X-OpenStack-Ironic-API-Version: 1.<n> or latest. Absent header = 1.1 (the minimum), not the maximum. Range 1.1–1.115; out of range → 406 with X-OpenStack-Ironic-API-Minimum-Version / -Maximum-Version in the response. Pinning ([DEFAULT]/pin_release_version) lowers the maximum. | ironic/api/controllers/base.py:51, ironic/api/controllers/v1/__init__.py:155, ironic/api/controllers/v1/versions.py:300 |
| Version headers on every response | X-OpenStack-Ironic-API-Minimum-Version, -Maximum-Version and X-OpenStack-Ironic-API-Version (the version used) are set even on errors; python-ironicclient negotiates from them after a 406. | ironic/api/controllers/v1/__init__.py:177 |
| Version gating | Two mechanisms: the newer @validation.api_version(min, max) decorator (404 by default, ironic/api/validation/__init__.py:33) and the older api_utils.allow_*() checks in handler bodies (71 functions, _rewrite/version-gates_stable-2026.2.json), which raise 404 (NotFound), 405 or 406 depending on the handler. Fields added by a microversion are hidden from older clients through VERSIONED_FIELDS (ironic/api/controllers/v1/utils.py:850). | |
| Error body | Every 4xx/5xx body becomes {"error_message": "<JSON string>"} whose inner JSON is {"faultcode": "Client"|"Server", "faultstring": "...", "debuginfo": null}. The inner document is a string, not an object. | ironic/api/middleware/parsable_error.py:36, ironic/api/method.py:114 |
| Content type | JSON only; requests with no Accept or */* are treated as application/json. A .json suffix on the last path segment is stripped for compatibility (except names allowed to end in .json since 1.91). | ironic/api/middleware/parsable_error.py:65, ironic/api/middleware/json_ext.py |
| Body limits | JSON depth ≤ [api]/max_json_body_depth (25), size ≤ [api]/max_json_body_size KiB (1024) with larger limits for provisioning and inspection payloads; checked before parsing. | ironic/api/app.py:112 |
| Authentication | [DEFAULT]/auth_strategy = keystone (keystonemiddleware auth_token), http_basic or noauth. Public routes bypass authentication: /, /v1, /v1/lookup, /v1/heartbeat/<uuid>, /v1/continue_inspection. | ironic/api/app.py:135, ironic/api/config.py |
| Authorisation | oslo.policy, 133 default rules (_rewrite/policies_stable-2026.2.json), scope-aware (system scope, project owner and lessee of the node). A project that is neither owner nor lessee gets 404, not 403, for node-scoped resources (conceal_node), so that existence is not leaked. | ironic/api/controllers/v1/utils.py:1794, ironic/api/controllers/v1/utils.py:1751 |
| Request id | Both x-openstack-request-id and the historical openstack-request-id response headers. | ironic/api/app.py:64 |
| Pagination | limit (≤ [api]/max_limit = 1000), marker (UUID), sort_key, sort_dir; collections carry a next link. fields= selects returned fields (1.8+), detail=true (1.43+). | ironic/api/controllers/v1/collection.py |
2. Endpoint table
The Min version column is the first microversion at which the route stops being rejected as "not available in this version" (from the api_version decorator when present, otherwise from the version probe). Documented errors come from the api-ref; the observed ones are in _rewrite/observed-errors-api-tests_stable-2026.2.md. Error meanings are in 02-errors.
<!-- GEN:routes-table -->
| Method | Path | Handler | Policy | Min version | Success | Documented errors |
|---|---|---|---|---|---|---|
| GET | / | ironic/api/controllers/root.py:39 RootController.index | — | 1.1 (probe) | 200 | — |
| GET | /v1/ | ironic/api/controllers/v1/__init__.py:146 Controller.index | — | 1.1 (probe) | 200 | — |
| GET | /v1/allocations | ironic/api/controllers/v1/allocation.py:253 AllocationsController.get_all | baremetal:allocation:list, baremetal:allocation:list_all | 1.52 (decorator) | 200 | 400, 401, 403, 404 |
| POST | /v1/allocations | ironic/api/controllers/v1/allocation.py:365 AllocationsController.post | baremetal:allocation:create, baremetal:allocation:create_restricted, baremetal:node:get | 1.52 (decorator) | 201 | 400, 401, 403, 409, 503 |
| GET | /v1/allocations/{allocation_id} | ironic/api/controllers/v1/allocation.py:307 AllocationsController.get_one | baremetal:allocation:get | 1.52 (decorator) | 200 | 400, 401, 403, 404 |
| PATCH | /v1/allocations/{allocation_id} | ironic/api/controllers/v1/allocation.py:510 AllocationsController.patch | baremetal:allocation:create_pre_rbac, baremetal:allocation:update | 1.57 (decorator) | 200 | 400, 401, 403, 404, 409, 503 |
| DELETE | /v1/allocations/{allocation_id} | ironic/api/controllers/v1/allocation.py:554 AllocationsController.delete | baremetal:allocation:delete | 1.52 (decorator) | 204 | 400, 401, 403, 404, 409, 503 |
| GET | /v1/chassis | ironic/api/controllers/v1/chassis.py:149 ChassisController.get_all | baremetal:chassis:get | 1.1 (probe) | 200 | — |
| POST | /v1/chassis | ironic/api/controllers/v1/chassis.py:227 ChassisController.post | baremetal:chassis:create | 1.1 (probe) | 201 | 201, 413, 415, 405, 404, 403, 401, 400, 503, 409 |
| GET | /v1/chassis/detail | ironic/api/controllers/v1/chassis.py:180 ChassisController.detail | baremetal:chassis:get | 1.1 (probe) | 200 | — |
| GET | /v1/chassis/{chassis_id} | ironic/api/controllers/v1/chassis.py:207 ChassisController.get_one | baremetal:chassis:get | 1.1 (probe) | 200 | — |
| PATCH | /v1/chassis/{chassis_id} | ironic/api/controllers/v1/chassis.py:255 ChassisController.patch | baremetal:chassis:update | 1.1 (probe) | 200 | — |
| DELETE | /v1/chassis/{chassis_id} | ironic/api/controllers/v1/chassis.py:288 ChassisController.delete | baremetal:chassis:delete | 1.1 (probe) | 204 | — |
| GET | /v1/conductors | ironic/api/controllers/v1/conductor.py:115 ConductorsController.get_all | baremetal:conductor:get | 1.49 (decorator) | 200 | — |
| GET | /v1/conductors/{hostname} | ironic/api/controllers/v1/conductor.py:154 ConductorsController.get_one | baremetal:conductor:get | 1.49 (decorator) | 200 | — |
| POST | /v1/continue_inspection | ironic/api/controllers/v1/ramdisk.py:402 ContinueInspectionController.post | baremetal:driver:ipa_continue_inspection | 1.1 (probe) | 202 | undocumented |
| GET | /v1/deploy_templates | ironic/api/controllers/v1/deploy_template.py:141 DeployTemplatesController.get_all | baremetal:deploy_template:get | 1.55 (probe) | 200 | 400, 401, 403, 404 |
| POST | /v1/deploy_templates | ironic/api/controllers/v1/deploy_template.py:218 DeployTemplatesController.post | baremetal:deploy_template:create | 1.55 (probe) | 201 | 400, 401, 403, 409 |
| GET | /v1/deploy_templates/{deploy_template_id} | ironic/api/controllers/v1/deploy_template.py:196 DeployTemplatesController.get_one | baremetal:deploy_template:get | 1.55 (probe) | 200 | 400, 401, 403, 404 |
| PATCH | /v1/deploy_templates/{deploy_template_id} | ironic/api/controllers/v1/deploy_template.py:250 DeployTemplatesController.patch | baremetal:deploy_template:update | 1.55 (probe) | 200 | 400, 401, 403, 404, 409 |
| DELETE | /v1/deploy_templates/{deploy_template_id} | ironic/api/controllers/v1/deploy_template.py:308 DeployTemplatesController.delete | baremetal:deploy_template:delete | 1.55 (probe) | 204 | 400, 401, 403, 404 |
| GET | /v1/drivers | ironic/api/controllers/v1/driver.py:354 DriversController.get_all | baremetal:driver:get | 1.1 (probe) | 200 | — |
| GET | /v1/drivers/{driver_name} | ironic/api/controllers/v1/driver.py:397 DriversController.get_one | baremetal:driver:get | 1.1 (probe) | 200 | — |
| GET | /v1/drivers/{driver_name}/properties | ironic/api/controllers/v1/driver.py:421 DriversController.properties | baremetal:driver:get_properties | 1.1 (probe) | 200 | — |
| GET | /v1/drivers/{driver_name}/raid/logical_disk_properties | ironic/api/controllers/v1/driver.py:288 DriverRaidController.logical_disk_properties | baremetal:driver:get_raid_logical_disk_properties | 1.12 (probe) | 200 | — |
| GET | /v1/drivers/{driver_name}/vendor_passthru | ironic/api/controllers/v1/driver.py:259 DriverPassthruController._default | baremetal:driver:vendor_passthru | 1.1 (probe) | 200 | undocumented |
| POST | /v1/drivers/{driver_name}/vendor_passthru | ironic/api/controllers/v1/driver.py:259 DriverPassthruController._default | baremetal:driver:vendor_passthru | 1.1 (probe) | 200 | — |
| PUT | /v1/drivers/{driver_name}/vendor_passthru | ironic/api/controllers/v1/driver.py:259 DriverPassthruController._default | baremetal:driver:vendor_passthru | 1.1 (probe) | 200 | undocumented |
| PATCH | /v1/drivers/{driver_name}/vendor_passthru | ironic/api/controllers/v1/driver.py:259 DriverPassthruController._default | baremetal:driver:vendor_passthru | 1.1 (probe) | 200 | undocumented |
| DELETE | /v1/drivers/{driver_name}/vendor_passthru | ironic/api/controllers/v1/driver.py:259 DriverPassthruController._default | baremetal:driver:vendor_passthru | 1.1 (probe) | 200 | undocumented |
| GET | /v1/drivers/{driver_name}/vendor_passthru/methods | ironic/api/controllers/v1/driver.py:233 DriverPassthruController.methods | baremetal:driver:vendor_passthru | 1.1 (probe) | 200 | — |
| POST | /v1/events | ironic/api/controllers/v1/event.py:38 EventsController.post | baremetal:events:post | 1.54 (decorator) | 204 | undocumented |
| POST | /v1/heartbeat/{node_ident} | ironic/api/controllers/v1/ramdisk.py:215 HeartbeatController.post | baremetal:node:ipa_heartbeat | 1.1 (probe) | 202 | 400, 404 |
| GET | /v1/inspection_rules | ironic/api/controllers/v1/inspection_rule.py:143 InspectionRuleController.get_all | baremetal:inspection_rule:get, baremetal:inspection_rule:list_all | 1.96 (decorator) | 200 | 400, 401, 403, 404 |
| POST | /v1/inspection_rules | ironic/api/controllers/v1/inspection_rule.py:230 InspectionRuleController.post | baremetal:inspection_rule:create | 1.96 (decorator) | 201 | 400, 401, 403, 409 |
| DELETE | /v1/inspection_rules | ironic/api/controllers/v1/inspection_rule.py:320 InspectionRuleController.delete | baremetal:inspection_rule:delete | 1.96 (decorator) | 204 | 400, 401, 403 |
| GET | /v1/inspection_rules/{rule_id} | ironic/api/controllers/v1/inspection_rule.py:207 InspectionRuleController.get_one | baremetal:inspection_rule:get | 1.96 (decorator) | 200 | 400, 401, 403, 404 |
| PATCH | /v1/inspection_rules/{rule_id} | ironic/api/controllers/v1/inspection_rule.py:267 InspectionRuleController.patch | baremetal:inspection_rule:update | 1.96 (decorator) | 200 | 400, 401, 403, 404, 409 |
| DELETE | /v1/inspection_rules/{rule_id} | ironic/api/controllers/v1/inspection_rule.py:320 InspectionRuleController.delete | baremetal:inspection_rule:delete | 1.96 (decorator) | 204 | 400, 401, 403, 404 |
| GET | /v1/lookup | ironic/api/controllers/v1/ramdisk.py:143 LookupController.get_all | baremetal:driver:ipa_lookup | 1.22 (probe) | 200 | 400, 404 |
| GET | /v1/nodes | ironic/api/controllers/v1/node.py:2734 NodesController.get_all | baremetal:node:list, baremetal:node:list_all | 1.1 (probe) | 200 | — |
| POST | /v1/nodes | ironic/api/controllers/v1/node.py:2994 NodesController.post | baremetal:node:create, baremetal:node:create:self_owned_node | 1.1 (probe) | 201 | — |
| GET | /v1/nodes/detail | ironic/api/controllers/v1/node.py:2849 NodesController.detail | baremetal:node:list, baremetal:node:list_all | 1.1 (probe) | 200 | — |
| GET | /v1/nodes/{node_ident} | ironic/api/controllers/v1/node.py:2972 NodesController.get_one | baremetal:node:get, baremetal:node:get:driver_info, baremetal:node:get:driver_internal_info … | 1.1 (probe) | 200 | — |
| PATCH | /v1/nodes/{node_ident} | ironic/api/controllers/v1/node.py:3214 NodesController.patch | baremetal:node:disable_cleaning, baremetal:node:update, baremetal:node:update:chassis_uuid … | 1.1 (probe) | 200 | — |
| DELETE | /v1/nodes/{node_ident} | ironic/api/controllers/v1/node.py:3356 NodesController.delete | baremetal:node:delete, baremetal:node:delete:self_owned_node | 1.1 (probe) | 204 | — |
| GET | /v1/nodes/{node_ident}/allocation | ironic/api/controllers/v1/allocation.py:601 NodeAllocationController.get_all | baremetal:allocation:get | 1.52 (probe) | 200 | 400, 401, 403, 404 |
| DELETE | /v1/nodes/{node_ident}/allocation | ironic/api/controllers/v1/allocation.py:623 NodeAllocationController.delete | baremetal:allocation:delete, baremetal:node:get | 1.52 (probe) | 204 | 400, 401, 403, 404, 409, 503 |
| GET | /v1/nodes/{node_ident}/bios | ironic/api/controllers/v1/bios.py:80 NodeBiosController.get_all | baremetal:node:bios:get | 1.40 (decorator) | 200 | — |
| GET | /v1/nodes/{node_ident}/bios/{bios_setting} | ironic/api/controllers/v1/bios.py:104 NodeBiosController.get_one | baremetal:node:bios:get | 1.40 (decorator) | 200 | — |
| GET | /v1/nodes/{node_ident}/firmware | ironic/api/controllers/v1/firmware.py:79 NodeFirmwareController.get_all | baremetal:node:firmware:get | 1.86 (decorator) | 200 | — |
| GET | /v1/nodes/{node_ident}/history | ironic/api/controllers/v1/node.py:2232 NodeHistoryController.get_all | baremetal:node:history:get | 1.78 (probe) | 200 | — |
| GET | /v1/nodes/{node_ident}/history/{history_event_uuid} | ironic/api/controllers/v1/node.py:2271 NodeHistoryController.get_one | baremetal:node:history:get | 1.78 (probe) | 200 | — |
| GET | /v1/nodes/{node_ident}/inventory | ironic/api/controllers/v1/node.py:2292 NodeInventoryController.get | baremetal:node:inventory:get | 1.81 (probe) | 200 | — |
| PUT | /v1/nodes/{node_ident}/maintenance | ironic/api/controllers/v1/node.py:2097 NodeMaintenanceController.put | baremetal:node:set_maintenance | 1.1 (probe) | 202 | — |
| DELETE | /v1/nodes/{node_ident}/maintenance | ironic/api/controllers/v1/node.py:2112 NodeMaintenanceController.delete | baremetal:node:clear_maintenance | 1.1 (probe) | 202 | — |
| GET | /v1/nodes/{node_ident}/management/boot_device | ironic/api/controllers/v1/node.py:506 BootDeviceController.get | baremetal:node:get_boot_device | 1.1 (probe) | 200 | — |
| PUT | /v1/nodes/{node_ident}/management/boot_device | ironic/api/controllers/v1/node.py:480 BootDeviceController.put | baremetal:node:set_boot_device | 1.1 (probe) | 204 | — |
| GET | /v1/nodes/{node_ident}/management/boot_device/supported | ironic/api/controllers/v1/node.py:526 BootDeviceController.supported | baremetal:node:get_boot_device | 1.1 (probe) | 200 | — |
| GET | /v1/nodes/{node_ident}/management/indicators | ironic/api/controllers/v1/node.py:658 IndicatorController.get_all | baremetal:node:get_indicator_state | 1.1 (probe) | 200 | 404 |
| GET | /v1/nodes/{node_ident}/management/indicators/{component}/{indicator} | ironic/api/controllers/v1/node.py:635 IndicatorController.get_one | baremetal:node:get_indicator_state | 1.1 (probe) | 200 | undocumented |
| PUT | /v1/nodes/{node_ident}/management/indicators/{component}/{indicator} | ironic/api/controllers/v1/node.py:612 IndicatorController.put | baremetal:node:set_indicator_state | 1.1 (probe) | 204 | undocumented |
| GET | /v1/nodes/{node_ident}/management/indicators/{ind_ident}@{component} | ironic/api/controllers/v1/node.py:635 IndicatorController.get_one | baremetal:node:get_indicator_state | 1.1 (probe) | 200 | 404 |
| PUT | /v1/nodes/{node_ident}/management/indicators/{ind_ident}@{component} | ironic/api/controllers/v1/node.py:612 IndicatorController.put | baremetal:node:set_indicator_state | 1.1 (probe) | 204 | — |
| PUT | /v1/nodes/{node_ident}/management/inject_nmi | ironic/api/controllers/v1/node.py:683 InjectNmiController.put | baremetal:node:inject_nmi | 1.29 (probe) | 204 | — |
| GET | /v1/nodes/{node_ident}/portgroups | ironic/api/controllers/v1/portgroup.py:312 PortgroupsController.get_all | baremetal:portgroup:get | 1.23 (decorator) | 200 | — |
| GET | /v1/nodes/{node_ident}/portgroups/detail | ironic/api/controllers/v1/portgroup.py:371 PortgroupsController.detail | baremetal:portgroup:get | 1.23 (decorator) | 200 | — |
| GET | /v1/nodes/{node_ident}/ports | ironic/api/controllers/v1/port.py:448 PortsController.get_all | baremetal:port:get | 1.1 (probe) | 200 | — |
| GET | /v1/nodes/{node_ident}/ports/detail | ironic/api/controllers/v1/port.py:531 PortsController.detail | baremetal:port:get | 1.1 (probe) | 200 | — |
| GET | /v1/nodes/{node_ident}/states | ironic/api/controllers/v1/node.py:802 NodeStatesController.get | baremetal:node:get_states | 1.1 (probe) | 200 | — |
| PUT | /v1/nodes/{node_ident}/states/boot_mode | ironic/api/controllers/v1/node.py:910 NodeStatesController.boot_mode | baremetal:node:set_boot_mode | 1.1 (probe) | 202 | — |
| GET | /v1/nodes/{node_ident}/states/console | ironic/api/controllers/v1/node.py:729 NodeConsoleController.get | baremetal:node:get_console | 1.1 (probe) | 200 | — |
| PUT | /v1/nodes/{node_ident}/states/console | ironic/api/controllers/v1/node.py:751 NodeConsoleController.put | baremetal:node:set_console_state | 1.1 (probe) | 202 | — |
| PUT | /v1/nodes/{node_ident}/states/power | ironic/api/controllers/v1/node.py:853 NodeStatesController.power | baremetal:node:set_power_state | 1.1 (probe) | 202 | — |
| PUT | /v1/nodes/{node_ident}/states/provision | ironic/api/controllers/v1/node.py:1148 NodeStatesController.provision | baremetal:node:set_provision_state, baremetal:node:set_provision_state:clean_steps, baremetal:node:set_provision_state:service_steps … | 1.1 (probe) | 202 | — |
| PUT | /v1/nodes/{node_ident}/states/raid | ironic/api/controllers/v1/node.py:820 NodeStatesController.raid | baremetal:node:set_raid_state | 1.12 (probe) | 204 | — |
| PUT | /v1/nodes/{node_ident}/states/secure_boot | ironic/api/controllers/v1/node.py:961 NodeStatesController.secure_boot | baremetal:node:set_secure_boot | 1.1 (probe) | 202 | — |
| GET | /v1/nodes/{node_ident}/traits | ironic/api/controllers/v1/node.py:1520 NodeTraitsController.get_all | baremetal:node:traits:list | 1.37 (probe) | 200 | — |
| PUT | /v1/nodes/{node_ident}/traits | ironic/api/controllers/v1/node.py:1533 NodeTraitsController.put | baremetal:node:traits:set | 1.37 (probe) | 204 | — |
| DELETE | /v1/nodes/{node_ident}/traits | ironic/api/controllers/v1/node.py:1593 NodeTraitsController.delete | baremetal:node:traits:delete | 1.37 (probe) | 204 | — |
| PUT | /v1/nodes/{node_ident}/traits/{trait} | ironic/api/controllers/v1/node.py:1533 NodeTraitsController.put | baremetal:node:traits:set | 1.37 (probe) | 204 | — |
| DELETE | /v1/nodes/{node_ident}/traits/{trait} | ironic/api/controllers/v1/node.py:1593 NodeTraitsController.delete | baremetal:node:traits:delete | 1.37 (probe) | 204 | — |
| GET | /v1/nodes/{node_ident}/validate | ironic/api/controllers/v1/node.py:2946 NodesController.validate | baremetal:node:validate | 1.1 (probe) | 200 | — |
| GET | /v1/nodes/{node_ident}/vendor_passthru | ironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._default | baremetal:node:vendor_passthru | 1.1 (probe) | 200 | undocumented |
| POST | /v1/nodes/{node_ident}/vendor_passthru | ironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._default | baremetal:node:vendor_passthru | 1.1 (probe) | 200 | — |
| PUT | /v1/nodes/{node_ident}/vendor_passthru | ironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._default | baremetal:node:vendor_passthru | 1.1 (probe) | 200 | undocumented |
| PATCH | /v1/nodes/{node_ident}/vendor_passthru | ironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._default | baremetal:node:vendor_passthru | 1.1 (probe) | 200 | undocumented |
| DELETE | /v1/nodes/{node_ident}/vendor_passthru | ironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._default | baremetal:node:vendor_passthru | 1.1 (probe) | 200 | undocumented |
| GET | /v1/nodes/{node_ident}/vendor_passthru/methods | ironic/api/controllers/v1/node.py:2033 NodeVendorPassthruController.methods | baremetal:node:vendor_passthru | 1.1 (probe) | 200 | — |
| GET | /v1/nodes/{node_ident}/vifs | ironic/api/controllers/v1/node.py:2131 NodeVIFController.get_all | baremetal:node:vif:list | 1.28 (probe) | 200 | — |
| POST | /v1/nodes/{node_ident}/vifs | ironic/api/controllers/v1/node.py:2142 NodeVIFController.post | baremetal:node:vif:attach | 1.28 (probe) | 204 | — |
| DELETE | /v1/nodes/{node_ident}/vifs/{node_vif_ident} | ironic/api/controllers/v1/node.py:2160 NodeVIFController.delete | baremetal:node:vif:detach | 1.28 (probe) | 204 | — |
| GET | /v1/nodes/{node_ident}/vmedia | ironic/api/controllers/v1/node.py:2354 NodeVmediaController.get | baremetal:node:vmedia:get | 1.89 (probe) | 200 | — |
| POST | /v1/nodes/{node_ident}/vmedia | ironic/api/controllers/v1/node.py:2373 NodeVmediaController.post | baremetal:node:vmedia:attach | 1.89 (probe) | 204 | — |
| DELETE | /v1/nodes/{node_ident}/vmedia | ironic/api/controllers/v1/node.py:2398 NodeVmediaController.delete | baremetal:node:vmedia:detach | 1.89 (probe) | 204 | — |
| GET | /v1/nodes/{node_ident}/volume | ironic/api/controllers/v1/volume.py:75 VolumeController.get | baremetal:volume:get | 1.32 (decorator) | 200 | — |
| GET | /v1/nodes/{node_ident}/volume/connectors | ironic/api/controllers/v1/volume_connector.py:167 VolumeConnectorsController.get_all | baremetal:volume:get, baremetal:volume:list, baremetal:volume:list_all | 1.32 (decorator) | 200 | — |
| GET | /v1/nodes/{node_ident}/volume/targets | ironic/api/controllers/v1/volume_target.py:201 VolumeTargetsController.get_all | baremetal:volume:get, baremetal:volume:list, baremetal:volume:list_all | 1.32 (decorator) | 200 | — |
| GET | /v1/portgroups | ironic/api/controllers/v1/portgroup.py:312 PortgroupsController.get_all | baremetal:portgroup:get, baremetal:portgroup:list, baremetal:portgroup:list_all | 1.23 (decorator) | 200 | — |
| POST | /v1/portgroups | ironic/api/controllers/v1/portgroup.py:451 PortgroupsController.post | baremetal:portgroup:create | 1.23 (decorator) | 201 | — |
| GET | /v1/portgroups/detail | ironic/api/controllers/v1/portgroup.py:371 PortgroupsController.detail | baremetal:portgroup:get, baremetal:portgroup:list, baremetal:portgroup:list_all | 1.23 (decorator) | 200 | — |
| GET | /v1/portgroups/{portgroup_ident} | ironic/api/controllers/v1/portgroup.py:425 PortgroupsController.get_one | baremetal:portgroup:get | 1.23 (decorator) | 200 | — |
| PATCH | /v1/portgroups/{portgroup_ident} | ironic/api/controllers/v1/portgroup.py:523 PortgroupsController.patch | baremetal:portgroup:update | 1.23 (decorator) | 200 | — |
| DELETE | /v1/portgroups/{portgroup_ident} | ironic/api/controllers/v1/portgroup.py:611 PortgroupsController.delete | baremetal:portgroup:delete | 1.23 (decorator) | 204 | — |
| GET | /v1/portgroups/{portgroup_ident}/ports | ironic/api/controllers/v1/port.py:448 PortsController.get_all | baremetal:port:get | 1.24 (probe) | 200 | — |
| GET | /v1/portgroups/{portgroup_ident}/ports/detail | ironic/api/controllers/v1/port.py:531 PortsController.detail | baremetal:port:get | 1.24 (probe) | 200 | — |
| GET | /v1/ports | ironic/api/controllers/v1/port.py:448 PortsController.get_all | baremetal:port:list, baremetal:port:list_all | 1.1 (probe) | 200 | — |
| POST | /v1/ports | ironic/api/controllers/v1/port.py:620 PortsController.post | baremetal:port:create | 1.1 (probe) | 201 | — |
| GET | /v1/ports/detail | ironic/api/controllers/v1/port.py:531 PortsController.detail | baremetal:port:list, baremetal:port:list_all | 1.1 (probe) | 200 | — |
| GET | /v1/ports/{port_id} | ironic/api/controllers/v1/port.py:597 PortsController.get_one | baremetal:port:get | 1.1 (probe) | 200 | — |
| PATCH | /v1/ports/{port_id} | ironic/api/controllers/v1/port.py:741 PortsController.patch | baremetal:port:update | 1.1 (probe) | 200 | — |
| DELETE | /v1/ports/{port_id} | ironic/api/controllers/v1/port.py:852 PortsController.delete | baremetal:port:delete | 1.1 (probe) | 204 | — |
| GET | /v1/runbooks | ironic/api/controllers/v1/runbook.py:313 RunbooksController.get_all | baremetal:runbook:list, baremetal:runbook:list_all | 1.92 (probe) | 200 | 400, 401, 403, 404 |
| POST | /v1/runbooks | ironic/api/controllers/v1/runbook.py:399 RunbooksController.post | baremetal:runbook:create | 1.92 (probe) | 201 | 400, 401, 403, 409 |
| GET | /v1/runbooks/{runbook_id} | ironic/api/controllers/v1/runbook.py:374 RunbooksController.get_one | baremetal:runbook:get | 1.92 (probe) | 200 | 400, 401, 403, 404 |
| PATCH | /v1/runbooks/{runbook_id} | ironic/api/controllers/v1/runbook.py:492 RunbooksController.patch | baremetal:runbook:update | 1.92 (probe) | 200 | 400, 401, 403, 404, 409 |
| DELETE | /v1/runbooks/{runbook_id} | ironic/api/controllers/v1/runbook.py:573 RunbooksController.delete | baremetal:runbook:delete | 1.92 (probe) | 204 | 400, 401, 403, 404 |
| PATCH | /v1/runbooks/{runbook_ident}/owner | ironic/api/controllers/v1/runbook.py:492 RunbooksController.patch | baremetal:runbook:update:owner | 1.92 (probe) | 200 | undocumented |
| PATCH | /v1/runbooks/{runbook_ident}/public | ironic/api/controllers/v1/runbook.py:492 RunbooksController.patch | baremetal:runbook:update:public | 1.92 (probe) | 200 | undocumented |
| GET | /v1/shards | ironic/api/controllers/v1/shard.py:43 ShardController.get_all | baremetal:shards:get | 1.82 (decorator) | 200 | 400, 403, 404 |
| GET | /v1/volume | ironic/api/controllers/v1/volume.py:75 VolumeController.get | baremetal:volume:get | 1.32 (decorator) | 200 | — |
| GET | /v1/volume/connectors | ironic/api/controllers/v1/volume_connector.py:167 VolumeConnectorsController.get_all | baremetal:volume:get, baremetal:volume:list, baremetal:volume:list_all | 1.32 (decorator) | 200 | — |
| POST | /v1/volume/connectors | ironic/api/controllers/v1/volume_connector.py:245 VolumeConnectorsController.post | baremetal:volume:create | 1.32 (decorator) | 201 | — |
| GET | /v1/volume/connectors/{volume_connector_id} | ironic/api/controllers/v1/volume_connector.py:213 VolumeConnectorsController.get_one | baremetal:volume:get, baremetal:volume:view_target_properties | 1.32 (decorator) | 200 | — |
| PATCH | /v1/volume/connectors/{volume_connector_id} | ironic/api/controllers/v1/volume_connector.py:308 VolumeConnectorsController.patch | baremetal:volume:update | 1.32 (decorator) | 200 | — |
| DELETE | /v1/volume/connectors/{volume_connector_id} | ironic/api/controllers/v1/volume_connector.py:390 VolumeConnectorsController.delete | baremetal:volume:delete | 1.32 (decorator) | 204 | undocumented |
| GET | /v1/volume/targets | ironic/api/controllers/v1/volume_target.py:201 VolumeTargetsController.get_all | baremetal:volume:get, baremetal:volume:list, baremetal:volume:list_all | 1.32 (decorator) | 200 | — |
| POST | /v1/volume/targets | ironic/api/controllers/v1/volume_target.py:287 VolumeTargetsController.post | baremetal:volume:create | 1.32 (decorator) | 201 | — |
| GET | /v1/volume/targets/{volume_target_id} | ironic/api/controllers/v1/volume_target.py:251 VolumeTargetsController.get_one | baremetal:volume:get, baremetal:volume:view_target_properties | 1.32 (decorator) | 200 | — |
| PATCH | /v1/volume/targets/{volume_target_id} | ironic/api/controllers/v1/volume_target.py:348 VolumeTargetsController.patch | baremetal:volume:update, baremetal:volume:view_target_properties | 1.32 (decorator) | 200 | — |
| DELETE | /v1/volume/targets/{volume_target_id} | ironic/api/controllers/v1/volume_target.py:432 VolumeTargetsController.delete | baremetal:volume:delete | 1.32 (decorator) | 204 | undocumented |
<!-- /GEN:routes-table -->
3. Spec/code mismatches found while probing
3.1 Routes documented but not served
| Documented | Reality | Source |
|---|---|---|
DELETE /v1/volume/connector/{volume_connector_id} (api-ref) | 405: the collection is /v1/volume/connectors/{id}; the api-ref has a typo (singular) | api-ref/source/baremetal-api-v1-volume.inc |
DELETE /v1/volume/target/{volume_target_id} (api-ref) | 405, same typo; the route is /v1/volume/targets/{id} | api-ref/source/baremetal-api-v1-volume.inc |
GET/PUT /v1/nodes/{node_ident}/management/indicators/{component}/{indicator} (api-ref) | the controller expects one segment {indicator}@{component}: GET with two segments answers 500 "IndicatorController.get_one() takes 3 positional arguments" | ironic/api/controllers/v1/node.py:635 |
| policy operations for vendor passthru | declared without the leading / and with a query string (nodes/{node_ident}/vendor_passthru?method=…), so tools that match policy operations to routes miss them | ironic/common/policy.py:1457 |
3.2 Unexpected 500s seen by the probe
DELETE /v1/inspection_rules(collection, 1.96+) is documented as "delete all rules" but answers 500 "tuple index out of range" when called without an id in this configuration [inferred: the handler indexes a missing argument],ironic/api/controllers/v1/inspection_rule.py:320.GET …/indicators/{component}/{indicator}above.
3.3 Pecan dispatch artifacts
Pecan's RestController hands any verb on a known path to the controller's verb method: PATCH /v1/nodes reaches NodesController.patch without an identifier, GET /v1/nodes/{id}/states/power reaches NodeStatesController.get, POST /v1/ports/{id} reaches PortsController.post. They answer 400/404/405/500 depending on the handler's argument checks; their exact outcomes per microversion are in _rewrite/routes_stable-2026.2.json (dispatch_artifact: true, version_probe). A rewrite should answer 405 on them; no client in the client set relies on them (see 07 § 4).
4. Functional description by resource
4.1 Root and version discovery — GET /, GET /v1/
- Purpose: tell clients which API versions exist and where the resources are.
- Actor: anyone (public routes, no token).
- Flow:
GET /returns{"name", "description", "versions": [...], "default_version": {"id": "v1", "version": "1.115", "min_version": "1.1", "status": "CURRENT", "links": [...]}}(ironic/api/controllers/root.py:39).GET /v1/returns links to every top-level collection, adding the versioned ones only when the requested microversion allows them (ironic/api/controllers/v1/__init__.py:117). - Clients relying on it: ironic-python-agent reads
default_version.versionto choose the microversion of its lookup and heartbeat calls (ipa:ironic_python_agent/ironic_api_client.py:121); python-ironicclient reads the version headers.
4.2 Nodes — /v1/nodes
- Purpose: register bare metal machines, describe how to reach their BMC (
driver,driver_info), their hardware (properties), what is deployed on them (instance_info,instance_uuid), and who may use them (owner,lessee). - Actor: system admin/member; project admins of the owner project (
baremetal:node:create:self_owned_node, 1.80+,ironic/api/controllers/v1/node.py:3018); readers list and show. Nova's ironic virt driver lists and patches nodes with a service token. - Preconditions: the driver (hardware type) must be enabled on at least one live conductor, otherwise
POSTanswers 400 "No valid host was found" (NoValidHostturned into 400),ironic/api/controllers/v1/node.py:3092. - Business rules:
namemust be a valid logical name (unique, 1.5+; special characters relaxed in 1.10),ironic/api/controllers/v1/node.py:2644.POSTvalidates against the node JSON schema built from versioned fields (ironic/api/controllers/v1/node.py:250); fields not allowed in the requested version are rejected with 406.provision_stateis never accepted in the body: new nodes start inenroll(1.11+) oravailable(older versions),ironic/db/sqlalchemy/api.py:816.PATCHis a JSON patch; each patched path needs its own policy (baremetal:node:update:<field>,…:update_extra,…:update_instance_info,…:set_maintenancefor/maintenancesince the CVE fix of this branch),ironic/api/controllers/v1/node.py:3147. Updatingdriver, interfaces ordriver_infois refused in unstable states (states.UPDATE_ALLOWED_STATES,ironic/common/states.py:246).DELETEonly inmanageable,enrolloradopt failed(ironic/common/states.py:251) and only when no instance is associated and the node is notprotected.
- Flow:
- (synchronous, API) policy check, schema validation, name/UUID resolution.
- (synchronous, RPC call)
create_node/update_node/destroy_nodeto the conductor chosen by the hash ring for(conductor_group, driver)(ironic/conductor/rpcapi.py:247); the conductor validates the driver interfaces and writes the row. - Response 201 (create, with
Location), 200 (patch) or 204 (delete).
- Outcome: the node exists in
enrollwithpower_state: null; the periodic power sync fillspower_state(see 05 §_sync_power_states). - Effects visible elsewhere: versioned notifications
baremetal.node.create|update|delete.{start,end,error}; Nova's resource tracker sees the node on its next poll. - Limitations: no conductor alive → 503 "Resource temporarily unavailable" on every write (
TemporaryFailure,ironic/conductor/rpcapi.py:278); a node locked by an operation → 409NodeLockedafter[conductor]/node_locked_retry_attemptsretries.
4.3 Node provision state — PUT /v1/nodes/{node_ident}/states/provision
- Purpose: drive the node through its lifecycle:
manage,provide,inspect,clean,active/deploy,rebuild,deleted/undeploy,rescue,unrescue,adopt,abort,unhold,service. - Actor: policy
baremetal:node:set_provision_state(system member, owner member, lessee admin);clean_stepsandservice_stepsneed extra policies; per-step policiesbaremetal:step:execute:<interface>.<step>apply when defined (ironic/conductor/steps.py:753). - Preconditions and rules, in this order (
ironic/api/controllers/v1/node.py:1148):- Node policy and retrieval (404 for a foreign project).
- The verb must exist in the requested microversion (
check_allow_management_verbs,ironic/api/controllers/v1/utils.py:935):manage/provide1.4,inspect1.6,abort1.13,clean1.15,adopt1.17,rescue1.38,deploy/undeploy1.73,unhold1.85,service1.87; otherwise 406. active/rebuildrefused in maintenance (400NodeInMaintenance,ironic/api/controllers/v1/node.py:1274).- The transition must be actionable from the current state in the state machine (
ironic/api/controllers/v1/node.py:1282); if not, **409NodeLocked** when the node is reserved, else **400InvalidStateRequested**. configdriveonly withactive/rebuild(dict form 1.56+, vendor_data 1.59+),ironic/api/controllers/v1/node.py:1298.runbook(1.92+) replacesclean_steps/service_steps; since 1.112 the node must share a trait with the runbook (ironic/api/controllers/v1/node.py:1007).
- Flow: one RPC per verb (
ironic/api/controllers/v1/node.py:1073):do_node_deploy(active/rebuild/deploy),do_node_tear_down(deleted/undeploy),inspect_hardware,do_node_clean,do_node_service,do_node_rescue,do_node_unrescue, anddo_provisioning_actionformanage,provide,adopt,abort,unhold(ironic/conductor/manager.py:1458). All are RPC calls: the conductor takes the node lock, applies the state machine event, saves the node and spawns a worker for the long part, then returns. - Outcome: 202 with
Location: /v1/nodes/<id>/states; the node is already in the transient state (deploying,cleaning,inspecting…) withtarget_provision_stateset. The client pollsGET /v1/nodes/{id}until a stable state or a*failedstate. - Limitations: the API-side state check races with the conductor (comment in
ironic/api/controllers/v1/node.py:1073): a conductor-side refusal still answers 400InvalidStateRequestedthrough the RPC error.
4.4 Power, boot mode, secure boot, RAID, console
| Operation | Purpose and rules | RPC | Source |
|---|---|---|---|
PUT …/states/power {"target": "power on"|"power off"|"rebooting"|"soft power off"|"soft rebooting", "timeout"} | soft targets 1.27+, timeout 1.27+; refused when disable_power_off is set and the target powers off (1.95+) | call change_node_power_state, then async worker; 202 | ironic/api/controllers/v1/node.py:853 |
PUT …/states/boot_mode, …/states/secure_boot | 1.76+; 409 Conflict while the node is in an unstable state | call change_node_boot_mode / change_node_secure_boot; 202 | ironic/api/controllers/v1/node.py:910, ironic/api/controllers/v1/node.py:961 |
PUT …/states/raid | 1.12+; validated against the RAID schema of the driver | call set_target_raid_config; 204 | ironic/api/controllers/v1/node.py:820 |
GET/PUT …/states/console | console info / enable-disable; 202 | get_console_information, set_console_mode | ironic/api/controllers/v1/node.py:729 |
GET …/states | power, provision, boot mode, RAID and console states in one document | none | ironic/api/controllers/v1/node.py:802 |
4.5 Node management, traits, VIFs, virtual media, history, inventory
| Operation | Purpose and rules | Source |
|---|---|---|
GET/PUT …/management/boot_device, GET …/boot_device/supported | read/set the next boot device through the BMC (persistent flag); RPC calls get_boot_device, set_boot_device | ironic/api/controllers/v1/node.py:480 |
GET/PUT …/management/indicators[/{ind}@{component}] | LEDs (1.63+) | ironic/api/controllers/v1/node.py:612 |
PUT …/management/inject_nmi | 1.29+ | ironic/api/controllers/v1/node.py:683 |
GET/PUT/DELETE …/traits[/{trait}] | node traits (1.37+); PUT with body replaces the set, PUT …/traits/<trait> adds one; names validated as os-traits standard or CUSTOM_*; RPC add_node_traits/remove_node_traits | ironic/api/controllers/v1/node.py:1533 |
GET/POST/DELETE …/vifs[/{vif}] | attach a Neutron port (VIF) to a node port or port group (1.28+, port_uuid/portgroup_uuid 1.67+); RPC vif_attach → network interface; this is what Nova calls for each instance port | ironic/api/controllers/v1/node.py:2142 |
GET/POST/DELETE …/vmedia | attach/detach a virtual media image through the BMC (1.89+, GET 1.93+) | ironic/api/controllers/v1/node.py:2373 |
GET …/history[/{uuid}] | event history (1.78+; project 1.113+; state, target_provision_state, duration_seconds 1.115+) | ironic/api/controllers/v1/node.py:2232 |
GET …/inventory | hardware inventory and plugin data collected by inspection (1.81+) | ironic/api/controllers/v1/node.py:2292 |
GET …/bios[/{name}], GET …/firmware | cached BIOS settings (1.40+), firmware components (1.86+; vendor/model/serial 1.114+) | ironic/api/controllers/v1/bios.py:80, ironic/api/controllers/v1/firmware.py:79 |
GET …/children | child nodes (1.83+) | ironic/api/controllers/v1/node.py:2318 |
GET …/validate | driver interface validation result per interface ({"power": {"result": true}, …}); Nova reads deploy and power | ironic/api/controllers/v1/node.py:2946 |
PUT/DELETE …/maintenance | set/clear maintenance with a reason; RPC update_node | ironic/api/controllers/v1/node.py:2097 |
* …/vendor_passthru?method=, GET …/vendor_passthru/methods | driver vendor methods (Redfish event subscriptions, IPMI send_raw…) | ironic/api/controllers/v1/node.py:2057 |
4.6 Ports and port groups — /v1/ports, /v1/portgroups, /v1/nodes/{id}/ports, /v1/portgroups/{id}/ports
- Purpose: record the node's NICs (MAC address,
pxe_enabled,local_link_connection= switch id/port,physical_network) used for PXE/DHCP and to bind Neutron ports to the right switch port. - Rules: MAC unique (409
MACAlreadyExists);physical_network1.34+;is_smartnic1.53+;name1.88+;description1.97+;vendor1.100+,category1.101+; port groups 1.23+ (mode,properties1.26+,physical_network1.102+, dynamic port groups 1.111+). Creating and updating ports goes through the conductor (create_port,update_port) because a port attached to a VIF of an active node cannot be changed freely. - Effects: none on Neutron until a VIF is attached (see § 4.5 and 03 § Neutron).
4.7 Allocations — /v1/allocations, /v1/nodes/{id}/allocation
- Purpose: let a client (Metal3-like tooling, standalone use) ask Ironic to pick a free node matching a resource class, traits and candidate list, instead of choosing itself. Nova does not use allocations; it schedules through Placement.
- Flow:
POST(1.52+) creates the allocation inallocatingand casts nothing: the API callscreate_allocationon a random conductor, which spawnsdo_allocate(ironic/conductor/allocations.py:34): filteravailable, not-maintenance, unreserved nodes with the resource class (ironic/conductor/allocations.py:103), keep those whose traits are a superset, shuffle (ironic/conductor/allocations.py:141), try to reserve them one by one and setinstance_uuid/allocation_id. Final stateactive(node chosen) orerrorwithlast_error. Backfill (1.58+) attaches an existing deployment's node. - Owner: 1.60+; project-scoped creators get their project as owner and only see nodes they own or lease.
- Lifecycle: see § 5.3.
4.8 Deploy templates and runbooks — /v1/deploy_templates (1.55+), /v1/runbooks (1.92+)
- Deploy templates: named lists of deploy steps; a template whose name matches one of the instance traits (
instance_info.traits, set by Nova from the flavor) adds its steps to the deployment (ironic/conductor/steps.py:383). Name must be a valid trait name (CUSTOM_*). - Runbooks: named lists of clean/service steps that a project may run on its nodes; owner/public (
PATCH …/owner,…/public), traits (1.112+) select which nodes may use them; steps validated against per-step policies at creation (ironic/conductor/steps.py:791).
4.9 Other resources
| Resource | Purpose | Source |
|---|---|---|
/v1/chassis | grouping of nodes, no behaviour; only DB | ironic/api/controllers/v1/chassis.py:149 |
/v1/drivers[/{name}[/properties|/raid/logical_disk_properties|/vendor_passthru]] | enabled hardware types (from live conductors), their interfaces and driver_info properties | ironic/api/controllers/v1/driver.py:354 |
/v1/volume, /connectors, /targets | boot-from-volume connectors (iqn, wwpn…) and targets (Cinder volume ids); 1.32+ | ironic/api/controllers/v1/volume.py:75 |
/v1/conductors[/{hostname}] | live and dead conductors with their hardware types and group (1.49+) | ironic/api/controllers/v1/conductor.py:115 |
/v1/shards | node counts per shard (1.82+); Nova's driver can be restricted to a shard | ironic/api/controllers/v1/shard.py:43 |
/v1/events | Neutron sends network.bind_port/unbind_port/delete_port events (1.54+) | ironic/api/controllers/v1/event.py:38 |
/v1/inspection_rules | conditions/actions applied to inspection data (1.96+), see 05 § inspection | ironic/api/controllers/v1/inspection_rule.py:143 |
4.10 Ramdisk API used by ironic-python-agent
These three endpoints are public (no Keystone token): the ramdisk has no credentials. They are protected by [api]/enable_ramdisk_endpoints, by the provision state of the node and by the agent token.
GET /v1/lookup?addresses=<mac,...>&node_uuid=<uuid> (1.22+)
- Purpose: the agent, just booted, finds which node it is running on and gets its configuration and secret token.
- Rules (
ironic/api/controllers/v1/ramdisk.py:143): MACs are normalised, invalid ones ignored; neither MAC nor UUID → 400IncompleteLookup; node not found → 404 (no detail on purpose); with[api]/restrict_lookup(default true) the node must be inLOOKUP_ALLOWED_STATES(orFASTTRACK_LOOKUP_ALLOWED_STATESwith fast track),ironic/common/states.py:272. - Flow: from 1.62 the API calls
get_node_with_tokenon the node's conductor (ironic/conductor/manager.py:4013), which generates the token once (ironic/conductor/utils.py:1509); a second lookup gets"******"instead of the token. - Response:
{"node": {"uuid", "properties", "instance_info", "driver_internal_info"}, "config": {"heartbeat_timeout", "agent_token", "agent_token_required": true, "metrics"…, "permitted_image_formats", "tls_min_version"…}}(ironic/api/controllers/v1/ramdisk.py:46).
POST /v1/heartbeat/{node_ident} (1.22+), body {"callback_url", "agent_token", "agent_version", "agent_verify_ca", "agent_status", "agent_status_message"}
- Purpose: the agent says "I am alive at this URL"; each heartbeat is also the trigger that moves deployments, cleaning, rescue and servicing forward.
- Rules (
ironic/api/controllers/v1/ramdisk.py:215):agent_versionneeds 1.36,agent_token1.62,agent_verify_ca1.68,agent_status*1.72;callback_urlmust be http(s) and must not change once recorded (400 "Detected change in ramdisk provided callback_url"); missing token → 400. - Flow: RPC call
heartbeat(ironic/conductor/manager.py:3554): token check (ironic/conductor/utils.py:1543), TLS requirement ([agent]/require_tls), early 409 when the node is locked and noagent_urlis known yet, then a spawned worker runs the deploy interface'sheartbeat(05 § heartbeat). - Response: 202 with no body. IPA treats 409 as "retry soon" and any other non-202 as an error.
POST /v1/continue_inspection[?node_uuid=] (1.84+; node_uuid 1.88+ with node+config response)
- Purpose: the agent posts the hardware inventory collected during inspection (replaces ironic-inspector's
/v1/continue). - Rules (
ironic/api/controllers/v1/ramdisk.py:402): body must containinventory.interfaces[].mac_address; lookup by MACs and BMC addresses (ironic/drivers/modules/inspect_utils.py); unknown node and[auto_discovery]/enabled→ enrol a new node (ironic/api/controllers/v1/ramdisk.py:360). - Flow: RPC call
continue_inspection→ hooks, inspection rules, storage of inventory (05). - Response: 202
{"uuid"}before 1.88; node + config (with a new token) since.
5. Lifecycles
5.1 Node provision state
Every state, event and transition of ironic/common/state_machine.py, generated from the live state machine. The formal specification (tables, vectors) is in 07 § 3.
<!-- GEN:sm-mermaid -->
stateDiagram-v2 state "wait call-back" as wait_call_back state "deploy failed" as deploy_failed state "deploy hold" as deploy_hold state "clean wait" as clean_wait state "clean failed" as clean_failed state "clean hold" as clean_hold state "inspect failed" as inspect_failed state "inspect wait" as inspect_wait state "adopt failed" as adopt_failed state "rescue wait" as rescue_wait state "rescue failed" as rescue_failed state "unrescue failed" as unrescue_failed state "service wait" as service_wait state "service failed" as service_failed state "service hold" as service_hold [*] --> enroll: POST /v1/nodes (API) active --> deleting: delete active --> deploying: rebuild active --> rescuing: rescue active --> servicing: service adopt_failed --> adopting: adopt adopt_failed --> manageable: manage adopting --> active: done adopting --> adopt_failed: fail available --> deploying: deploy available --> manageable: manage clean_failed --> manageable: manage clean_hold --> clean_failed: abort clean_hold --> clean_wait: unhold clean_wait --> clean_failed: abort clean_wait --> clean_failed: fail clean_wait --> clean_hold: hold clean_wait --> cleaning: resume cleaning --> available: done cleaning --> clean_failed: fail cleaning --> clean_hold: hold cleaning --> manageable: manage cleaning --> clean_wait: wait deleting --> cleaning: clean deleting --> error: fail deploy_failed --> deleting: delete deploy_failed --> deploying: deploy deploy_failed --> deploying: rebuild deploy_hold --> deploy_failed: abort deploy_hold --> wait_call_back: unhold deploying --> active: done deploying --> deploy_failed: fail deploying --> deploy_hold: hold deploying --> wait_call_back: wait enroll --> verifying: manage error --> deleting: delete error --> deploying: rebuild inspect_failed --> inspecting: inspect inspect_failed --> manageable: manage inspect_wait --> inspect_failed: abort inspect_wait --> manageable: done inspect_wait --> inspect_failed: fail inspect_wait --> inspecting: resume inspecting --> manageable: done inspecting --> inspect_failed: fail inspecting --> inspect_wait: wait manageable --> adopting: adopt manageable --> cleaning: clean manageable --> inspecting: inspect manageable --> cleaning: provide rescue --> deleting: delete rescue --> rescuing: rescue rescue --> unrescuing: unrescue rescue_failed --> deleting: delete rescue_failed --> rescuing: rescue rescue_failed --> unrescuing: unrescue rescue_wait --> rescue_failed: abort rescue_wait --> deleting: delete rescue_wait --> rescue_failed: fail rescue_wait --> rescuing: resume rescuing --> rescue: done rescuing --> rescue_failed: fail rescuing --> rescue_wait: wait service_failed --> active: abort service_failed --> deleting: delete service_failed --> service_hold: hold service_failed --> rescuing: rescue service_failed --> servicing: service service_failed --> service_wait: wait service_hold --> service_failed: abort service_hold --> service_failed: fail service_hold --> servicing: service service_hold --> service_wait: unhold service_wait --> service_failed: abort service_wait --> deleting: delete service_wait --> service_failed: fail service_wait --> service_hold: hold service_wait --> servicing: resume servicing --> active: done servicing --> service_failed: fail servicing --> service_hold: hold servicing --> service_wait: wait unrescue_failed --> deleting: delete unrescue_failed --> rescuing: rescue unrescue_failed --> unrescuing: unrescue unrescuing --> active: done unrescuing --> unrescue_failed: fail verifying --> manageable: done verifying --> enroll: fail wait_call_back --> deploy_failed: abort wait_call_back --> deleting: delete wait_call_back --> deploy_failed: fail wait_call_back --> deploy_hold: hold wait_call_back --> deploying: resume
<!-- /GEN:sm-mermaid -->
What triggers the events:
| Event | Triggered by |
|---|---|
manage, provide, inspect, clean, deploy, rebuild, delete, rescue, unrescue, adopt, abort, unhold, service | API PUT …/states/provision (verb), applied by the conductor RPC handler |
wait | a step or driver call returned *WAIT (asynchronous work in the ramdisk or the BMC) |
resume | a heartbeat or continue_node_* RPC continues after a wait |
hold | a hold step (ironic/conductor/steps.py:1063) |
done | the conductor worker finished the last step |
fail | a step failed, a callback timed out (periodic _check_*_timeouts), or the conductor died (_correct_stuck_states, _check_orphan_nodes) |
5.2 Power state
power_state ∈ {power on, power off, null}; target_power_state is set while a change is running (rebooting, soft power off, soft rebooting are only targets). Changes come from the API (change_node_power_state), from steps and deployments (ironic/conductor/utils.py:286), and from the periodic sync, which either forces the hardware back to the recorded state ([conductor]/force_power_state_during_sync, default true) or records the observed one and notifies Nova (ironic/conductor/manager.py:4574). After [conductor]/power_state_sync_max_retries failures the node goes to maintenance with fault power failure.
5.3 Allocation
stateDiagram-v2
[*] --> allocating: POST /v1/allocations (API) / create_allocation (RPC call)
allocating --> active: do_allocate reserved a node (conductor worker)
allocating --> error: no suitable node / reservation failed
allocating --> active: backfill (node given)
active --> [*]: DELETE /v1/allocations/{id} or node deletion
error --> [*]: DELETE
allocating --> allocating: _check_orphan_allocations takes over a dead conductor's allocation (periodic)Source: reverse-spec-ironic/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.