OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

ironic — REST API

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

RuleBehaviourSource
Microversion headerX-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 responseX-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 gatingTwo 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 bodyEvery 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 typeJSON 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 limitsJSON 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
Authorisationoslo.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 idBoth x-openstack-request-id and the historical openstack-request-id response headers.ironic/api/app.py:64
Paginationlimit (≤ [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 -->

MethodPathHandlerPolicyMin versionSuccessDocumented 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/allocationsironic/api/controllers/v1/allocation.py:253 AllocationsController.get_allbaremetal:allocation:list, baremetal:allocation:list_all1.52 (decorator)200400, 401, 403, 404
POST/v1/allocationsironic/api/controllers/v1/allocation.py:365 AllocationsController.postbaremetal:allocation:create, baremetal:allocation:create_restricted, baremetal:node:get1.52 (decorator)201400, 401, 403, 409, 503
GET/v1/allocations/{allocation_id}ironic/api/controllers/v1/allocation.py:307 AllocationsController.get_onebaremetal:allocation:get1.52 (decorator)200400, 401, 403, 404
PATCH/v1/allocations/{allocation_id}ironic/api/controllers/v1/allocation.py:510 AllocationsController.patchbaremetal:allocation:create_pre_rbac, baremetal:allocation:update1.57 (decorator)200400, 401, 403, 404, 409, 503
DELETE/v1/allocations/{allocation_id}ironic/api/controllers/v1/allocation.py:554 AllocationsController.deletebaremetal:allocation:delete1.52 (decorator)204400, 401, 403, 404, 409, 503
GET/v1/chassisironic/api/controllers/v1/chassis.py:149 ChassisController.get_allbaremetal:chassis:get1.1 (probe)200—
POST/v1/chassisironic/api/controllers/v1/chassis.py:227 ChassisController.postbaremetal:chassis:create1.1 (probe)201201, 413, 415, 405, 404, 403, 401, 400, 503, 409
GET/v1/chassis/detailironic/api/controllers/v1/chassis.py:180 ChassisController.detailbaremetal:chassis:get1.1 (probe)200—
GET/v1/chassis/{chassis_id}ironic/api/controllers/v1/chassis.py:207 ChassisController.get_onebaremetal:chassis:get1.1 (probe)200—
PATCH/v1/chassis/{chassis_id}ironic/api/controllers/v1/chassis.py:255 ChassisController.patchbaremetal:chassis:update1.1 (probe)200—
DELETE/v1/chassis/{chassis_id}ironic/api/controllers/v1/chassis.py:288 ChassisController.deletebaremetal:chassis:delete1.1 (probe)204—
GET/v1/conductorsironic/api/controllers/v1/conductor.py:115 ConductorsController.get_allbaremetal:conductor:get1.49 (decorator)200—
GET/v1/conductors/{hostname}ironic/api/controllers/v1/conductor.py:154 ConductorsController.get_onebaremetal:conductor:get1.49 (decorator)200—
POST/v1/continue_inspectionironic/api/controllers/v1/ramdisk.py:402 ContinueInspectionController.postbaremetal:driver:ipa_continue_inspection1.1 (probe)202undocumented
GET/v1/deploy_templatesironic/api/controllers/v1/deploy_template.py:141 DeployTemplatesController.get_allbaremetal:deploy_template:get1.55 (probe)200400, 401, 403, 404
POST/v1/deploy_templatesironic/api/controllers/v1/deploy_template.py:218 DeployTemplatesController.postbaremetal:deploy_template:create1.55 (probe)201400, 401, 403, 409
GET/v1/deploy_templates/{deploy_template_id}ironic/api/controllers/v1/deploy_template.py:196 DeployTemplatesController.get_onebaremetal:deploy_template:get1.55 (probe)200400, 401, 403, 404
PATCH/v1/deploy_templates/{deploy_template_id}ironic/api/controllers/v1/deploy_template.py:250 DeployTemplatesController.patchbaremetal:deploy_template:update1.55 (probe)200400, 401, 403, 404, 409
DELETE/v1/deploy_templates/{deploy_template_id}ironic/api/controllers/v1/deploy_template.py:308 DeployTemplatesController.deletebaremetal:deploy_template:delete1.55 (probe)204400, 401, 403, 404
GET/v1/driversironic/api/controllers/v1/driver.py:354 DriversController.get_allbaremetal:driver:get1.1 (probe)200—
GET/v1/drivers/{driver_name}ironic/api/controllers/v1/driver.py:397 DriversController.get_onebaremetal:driver:get1.1 (probe)200—
GET/v1/drivers/{driver_name}/propertiesironic/api/controllers/v1/driver.py:421 DriversController.propertiesbaremetal:driver:get_properties1.1 (probe)200—
GET/v1/drivers/{driver_name}/raid/logical_disk_propertiesironic/api/controllers/v1/driver.py:288 DriverRaidController.logical_disk_propertiesbaremetal:driver:get_raid_logical_disk_properties1.12 (probe)200—
GET/v1/drivers/{driver_name}/vendor_passthruironic/api/controllers/v1/driver.py:259 DriverPassthruController._defaultbaremetal:driver:vendor_passthru1.1 (probe)200undocumented
POST/v1/drivers/{driver_name}/vendor_passthruironic/api/controllers/v1/driver.py:259 DriverPassthruController._defaultbaremetal:driver:vendor_passthru1.1 (probe)200—
PUT/v1/drivers/{driver_name}/vendor_passthruironic/api/controllers/v1/driver.py:259 DriverPassthruController._defaultbaremetal:driver:vendor_passthru1.1 (probe)200undocumented
PATCH/v1/drivers/{driver_name}/vendor_passthruironic/api/controllers/v1/driver.py:259 DriverPassthruController._defaultbaremetal:driver:vendor_passthru1.1 (probe)200undocumented
DELETE/v1/drivers/{driver_name}/vendor_passthruironic/api/controllers/v1/driver.py:259 DriverPassthruController._defaultbaremetal:driver:vendor_passthru1.1 (probe)200undocumented
GET/v1/drivers/{driver_name}/vendor_passthru/methodsironic/api/controllers/v1/driver.py:233 DriverPassthruController.methodsbaremetal:driver:vendor_passthru1.1 (probe)200—
POST/v1/eventsironic/api/controllers/v1/event.py:38 EventsController.postbaremetal:events:post1.54 (decorator)204undocumented
POST/v1/heartbeat/{node_ident}ironic/api/controllers/v1/ramdisk.py:215 HeartbeatController.postbaremetal:node:ipa_heartbeat1.1 (probe)202400, 404
GET/v1/inspection_rulesironic/api/controllers/v1/inspection_rule.py:143 InspectionRuleController.get_allbaremetal:inspection_rule:get, baremetal:inspection_rule:list_all1.96 (decorator)200400, 401, 403, 404
POST/v1/inspection_rulesironic/api/controllers/v1/inspection_rule.py:230 InspectionRuleController.postbaremetal:inspection_rule:create1.96 (decorator)201400, 401, 403, 409
DELETE/v1/inspection_rulesironic/api/controllers/v1/inspection_rule.py:320 InspectionRuleController.deletebaremetal:inspection_rule:delete1.96 (decorator)204400, 401, 403
GET/v1/inspection_rules/{rule_id}ironic/api/controllers/v1/inspection_rule.py:207 InspectionRuleController.get_onebaremetal:inspection_rule:get1.96 (decorator)200400, 401, 403, 404
PATCH/v1/inspection_rules/{rule_id}ironic/api/controllers/v1/inspection_rule.py:267 InspectionRuleController.patchbaremetal:inspection_rule:update1.96 (decorator)200400, 401, 403, 404, 409
DELETE/v1/inspection_rules/{rule_id}ironic/api/controllers/v1/inspection_rule.py:320 InspectionRuleController.deletebaremetal:inspection_rule:delete1.96 (decorator)204400, 401, 403, 404
GET/v1/lookupironic/api/controllers/v1/ramdisk.py:143 LookupController.get_allbaremetal:driver:ipa_lookup1.22 (probe)200400, 404
GET/v1/nodesironic/api/controllers/v1/node.py:2734 NodesController.get_allbaremetal:node:list, baremetal:node:list_all1.1 (probe)200—
POST/v1/nodesironic/api/controllers/v1/node.py:2994 NodesController.postbaremetal:node:create, baremetal:node:create:self_owned_node1.1 (probe)201—
GET/v1/nodes/detailironic/api/controllers/v1/node.py:2849 NodesController.detailbaremetal:node:list, baremetal:node:list_all1.1 (probe)200—
GET/v1/nodes/{node_ident}ironic/api/controllers/v1/node.py:2972 NodesController.get_onebaremetal: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.patchbaremetal: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.deletebaremetal:node:delete, baremetal:node:delete:self_owned_node1.1 (probe)204—
GET/v1/nodes/{node_ident}/allocationironic/api/controllers/v1/allocation.py:601 NodeAllocationController.get_allbaremetal:allocation:get1.52 (probe)200400, 401, 403, 404
DELETE/v1/nodes/{node_ident}/allocationironic/api/controllers/v1/allocation.py:623 NodeAllocationController.deletebaremetal:allocation:delete, baremetal:node:get1.52 (probe)204400, 401, 403, 404, 409, 503
GET/v1/nodes/{node_ident}/biosironic/api/controllers/v1/bios.py:80 NodeBiosController.get_allbaremetal:node:bios:get1.40 (decorator)200—
GET/v1/nodes/{node_ident}/bios/{bios_setting}ironic/api/controllers/v1/bios.py:104 NodeBiosController.get_onebaremetal:node:bios:get1.40 (decorator)200—
GET/v1/nodes/{node_ident}/firmwareironic/api/controllers/v1/firmware.py:79 NodeFirmwareController.get_allbaremetal:node:firmware:get1.86 (decorator)200—
GET/v1/nodes/{node_ident}/historyironic/api/controllers/v1/node.py:2232 NodeHistoryController.get_allbaremetal:node:history:get1.78 (probe)200—
GET/v1/nodes/{node_ident}/history/{history_event_uuid}ironic/api/controllers/v1/node.py:2271 NodeHistoryController.get_onebaremetal:node:history:get1.78 (probe)200—
GET/v1/nodes/{node_ident}/inventoryironic/api/controllers/v1/node.py:2292 NodeInventoryController.getbaremetal:node:inventory:get1.81 (probe)200—
PUT/v1/nodes/{node_ident}/maintenanceironic/api/controllers/v1/node.py:2097 NodeMaintenanceController.putbaremetal:node:set_maintenance1.1 (probe)202—
DELETE/v1/nodes/{node_ident}/maintenanceironic/api/controllers/v1/node.py:2112 NodeMaintenanceController.deletebaremetal:node:clear_maintenance1.1 (probe)202—
GET/v1/nodes/{node_ident}/management/boot_deviceironic/api/controllers/v1/node.py:506 BootDeviceController.getbaremetal:node:get_boot_device1.1 (probe)200—
PUT/v1/nodes/{node_ident}/management/boot_deviceironic/api/controllers/v1/node.py:480 BootDeviceController.putbaremetal:node:set_boot_device1.1 (probe)204—
GET/v1/nodes/{node_ident}/management/boot_device/supportedironic/api/controllers/v1/node.py:526 BootDeviceController.supportedbaremetal:node:get_boot_device1.1 (probe)200—
GET/v1/nodes/{node_ident}/management/indicatorsironic/api/controllers/v1/node.py:658 IndicatorController.get_allbaremetal:node:get_indicator_state1.1 (probe)200404
GET/v1/nodes/{node_ident}/management/indicators/{component}/{indicator}ironic/api/controllers/v1/node.py:635 IndicatorController.get_onebaremetal:node:get_indicator_state1.1 (probe)200undocumented
PUT/v1/nodes/{node_ident}/management/indicators/{component}/{indicator}ironic/api/controllers/v1/node.py:612 IndicatorController.putbaremetal:node:set_indicator_state1.1 (probe)204undocumented
GET/v1/nodes/{node_ident}/management/indicators/{ind_ident}@{component}ironic/api/controllers/v1/node.py:635 IndicatorController.get_onebaremetal:node:get_indicator_state1.1 (probe)200404
PUT/v1/nodes/{node_ident}/management/indicators/{ind_ident}@{component}ironic/api/controllers/v1/node.py:612 IndicatorController.putbaremetal:node:set_indicator_state1.1 (probe)204—
PUT/v1/nodes/{node_ident}/management/inject_nmiironic/api/controllers/v1/node.py:683 InjectNmiController.putbaremetal:node:inject_nmi1.29 (probe)204—
GET/v1/nodes/{node_ident}/portgroupsironic/api/controllers/v1/portgroup.py:312 PortgroupsController.get_allbaremetal:portgroup:get1.23 (decorator)200—
GET/v1/nodes/{node_ident}/portgroups/detailironic/api/controllers/v1/portgroup.py:371 PortgroupsController.detailbaremetal:portgroup:get1.23 (decorator)200—
GET/v1/nodes/{node_ident}/portsironic/api/controllers/v1/port.py:448 PortsController.get_allbaremetal:port:get1.1 (probe)200—
GET/v1/nodes/{node_ident}/ports/detailironic/api/controllers/v1/port.py:531 PortsController.detailbaremetal:port:get1.1 (probe)200—
GET/v1/nodes/{node_ident}/statesironic/api/controllers/v1/node.py:802 NodeStatesController.getbaremetal:node:get_states1.1 (probe)200—
PUT/v1/nodes/{node_ident}/states/boot_modeironic/api/controllers/v1/node.py:910 NodeStatesController.boot_modebaremetal:node:set_boot_mode1.1 (probe)202—
GET/v1/nodes/{node_ident}/states/consoleironic/api/controllers/v1/node.py:729 NodeConsoleController.getbaremetal:node:get_console1.1 (probe)200—
PUT/v1/nodes/{node_ident}/states/consoleironic/api/controllers/v1/node.py:751 NodeConsoleController.putbaremetal:node:set_console_state1.1 (probe)202—
PUT/v1/nodes/{node_ident}/states/powerironic/api/controllers/v1/node.py:853 NodeStatesController.powerbaremetal:node:set_power_state1.1 (probe)202—
PUT/v1/nodes/{node_ident}/states/provisionironic/api/controllers/v1/node.py:1148 NodeStatesController.provisionbaremetal: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/raidironic/api/controllers/v1/node.py:820 NodeStatesController.raidbaremetal:node:set_raid_state1.12 (probe)204—
PUT/v1/nodes/{node_ident}/states/secure_bootironic/api/controllers/v1/node.py:961 NodeStatesController.secure_bootbaremetal:node:set_secure_boot1.1 (probe)202—
GET/v1/nodes/{node_ident}/traitsironic/api/controllers/v1/node.py:1520 NodeTraitsController.get_allbaremetal:node:traits:list1.37 (probe)200—
PUT/v1/nodes/{node_ident}/traitsironic/api/controllers/v1/node.py:1533 NodeTraitsController.putbaremetal:node:traits:set1.37 (probe)204—
DELETE/v1/nodes/{node_ident}/traitsironic/api/controllers/v1/node.py:1593 NodeTraitsController.deletebaremetal:node:traits:delete1.37 (probe)204—
PUT/v1/nodes/{node_ident}/traits/{trait}ironic/api/controllers/v1/node.py:1533 NodeTraitsController.putbaremetal:node:traits:set1.37 (probe)204—
DELETE/v1/nodes/{node_ident}/traits/{trait}ironic/api/controllers/v1/node.py:1593 NodeTraitsController.deletebaremetal:node:traits:delete1.37 (probe)204—
GET/v1/nodes/{node_ident}/validateironic/api/controllers/v1/node.py:2946 NodesController.validatebaremetal:node:validate1.1 (probe)200—
GET/v1/nodes/{node_ident}/vendor_passthruironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._defaultbaremetal:node:vendor_passthru1.1 (probe)200undocumented
POST/v1/nodes/{node_ident}/vendor_passthruironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._defaultbaremetal:node:vendor_passthru1.1 (probe)200—
PUT/v1/nodes/{node_ident}/vendor_passthruironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._defaultbaremetal:node:vendor_passthru1.1 (probe)200undocumented
PATCH/v1/nodes/{node_ident}/vendor_passthruironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._defaultbaremetal:node:vendor_passthru1.1 (probe)200undocumented
DELETE/v1/nodes/{node_ident}/vendor_passthruironic/api/controllers/v1/node.py:2057 NodeVendorPassthruController._defaultbaremetal:node:vendor_passthru1.1 (probe)200undocumented
GET/v1/nodes/{node_ident}/vendor_passthru/methodsironic/api/controllers/v1/node.py:2033 NodeVendorPassthruController.methodsbaremetal:node:vendor_passthru1.1 (probe)200—
GET/v1/nodes/{node_ident}/vifsironic/api/controllers/v1/node.py:2131 NodeVIFController.get_allbaremetal:node:vif:list1.28 (probe)200—
POST/v1/nodes/{node_ident}/vifsironic/api/controllers/v1/node.py:2142 NodeVIFController.postbaremetal:node:vif:attach1.28 (probe)204—
DELETE/v1/nodes/{node_ident}/vifs/{node_vif_ident}ironic/api/controllers/v1/node.py:2160 NodeVIFController.deletebaremetal:node:vif:detach1.28 (probe)204—
GET/v1/nodes/{node_ident}/vmediaironic/api/controllers/v1/node.py:2354 NodeVmediaController.getbaremetal:node:vmedia:get1.89 (probe)200—
POST/v1/nodes/{node_ident}/vmediaironic/api/controllers/v1/node.py:2373 NodeVmediaController.postbaremetal:node:vmedia:attach1.89 (probe)204—
DELETE/v1/nodes/{node_ident}/vmediaironic/api/controllers/v1/node.py:2398 NodeVmediaController.deletebaremetal:node:vmedia:detach1.89 (probe)204—
GET/v1/nodes/{node_ident}/volumeironic/api/controllers/v1/volume.py:75 VolumeController.getbaremetal:volume:get1.32 (decorator)200—
GET/v1/nodes/{node_ident}/volume/connectorsironic/api/controllers/v1/volume_connector.py:167 VolumeConnectorsController.get_allbaremetal:volume:get, baremetal:volume:list, baremetal:volume:list_all1.32 (decorator)200—
GET/v1/nodes/{node_ident}/volume/targetsironic/api/controllers/v1/volume_target.py:201 VolumeTargetsController.get_allbaremetal:volume:get, baremetal:volume:list, baremetal:volume:list_all1.32 (decorator)200—
GET/v1/portgroupsironic/api/controllers/v1/portgroup.py:312 PortgroupsController.get_allbaremetal:portgroup:get, baremetal:portgroup:list, baremetal:portgroup:list_all1.23 (decorator)200—
POST/v1/portgroupsironic/api/controllers/v1/portgroup.py:451 PortgroupsController.postbaremetal:portgroup:create1.23 (decorator)201—
GET/v1/portgroups/detailironic/api/controllers/v1/portgroup.py:371 PortgroupsController.detailbaremetal:portgroup:get, baremetal:portgroup:list, baremetal:portgroup:list_all1.23 (decorator)200—
GET/v1/portgroups/{portgroup_ident}ironic/api/controllers/v1/portgroup.py:425 PortgroupsController.get_onebaremetal:portgroup:get1.23 (decorator)200—
PATCH/v1/portgroups/{portgroup_ident}ironic/api/controllers/v1/portgroup.py:523 PortgroupsController.patchbaremetal:portgroup:update1.23 (decorator)200—
DELETE/v1/portgroups/{portgroup_ident}ironic/api/controllers/v1/portgroup.py:611 PortgroupsController.deletebaremetal:portgroup:delete1.23 (decorator)204—
GET/v1/portgroups/{portgroup_ident}/portsironic/api/controllers/v1/port.py:448 PortsController.get_allbaremetal:port:get1.24 (probe)200—
GET/v1/portgroups/{portgroup_ident}/ports/detailironic/api/controllers/v1/port.py:531 PortsController.detailbaremetal:port:get1.24 (probe)200—
GET/v1/portsironic/api/controllers/v1/port.py:448 PortsController.get_allbaremetal:port:list, baremetal:port:list_all1.1 (probe)200—
POST/v1/portsironic/api/controllers/v1/port.py:620 PortsController.postbaremetal:port:create1.1 (probe)201—
GET/v1/ports/detailironic/api/controllers/v1/port.py:531 PortsController.detailbaremetal:port:list, baremetal:port:list_all1.1 (probe)200—
GET/v1/ports/{port_id}ironic/api/controllers/v1/port.py:597 PortsController.get_onebaremetal:port:get1.1 (probe)200—
PATCH/v1/ports/{port_id}ironic/api/controllers/v1/port.py:741 PortsController.patchbaremetal:port:update1.1 (probe)200—
DELETE/v1/ports/{port_id}ironic/api/controllers/v1/port.py:852 PortsController.deletebaremetal:port:delete1.1 (probe)204—
GET/v1/runbooksironic/api/controllers/v1/runbook.py:313 RunbooksController.get_allbaremetal:runbook:list, baremetal:runbook:list_all1.92 (probe)200400, 401, 403, 404
POST/v1/runbooksironic/api/controllers/v1/runbook.py:399 RunbooksController.postbaremetal:runbook:create1.92 (probe)201400, 401, 403, 409
GET/v1/runbooks/{runbook_id}ironic/api/controllers/v1/runbook.py:374 RunbooksController.get_onebaremetal:runbook:get1.92 (probe)200400, 401, 403, 404
PATCH/v1/runbooks/{runbook_id}ironic/api/controllers/v1/runbook.py:492 RunbooksController.patchbaremetal:runbook:update1.92 (probe)200400, 401, 403, 404, 409
DELETE/v1/runbooks/{runbook_id}ironic/api/controllers/v1/runbook.py:573 RunbooksController.deletebaremetal:runbook:delete1.92 (probe)204400, 401, 403, 404
PATCH/v1/runbooks/{runbook_ident}/ownerironic/api/controllers/v1/runbook.py:492 RunbooksController.patchbaremetal:runbook:update:owner1.92 (probe)200undocumented
PATCH/v1/runbooks/{runbook_ident}/publicironic/api/controllers/v1/runbook.py:492 RunbooksController.patchbaremetal:runbook:update:public1.92 (probe)200undocumented
GET/v1/shardsironic/api/controllers/v1/shard.py:43 ShardController.get_allbaremetal:shards:get1.82 (decorator)200400, 403, 404
GET/v1/volumeironic/api/controllers/v1/volume.py:75 VolumeController.getbaremetal:volume:get1.32 (decorator)200—
GET/v1/volume/connectorsironic/api/controllers/v1/volume_connector.py:167 VolumeConnectorsController.get_allbaremetal:volume:get, baremetal:volume:list, baremetal:volume:list_all1.32 (decorator)200—
POST/v1/volume/connectorsironic/api/controllers/v1/volume_connector.py:245 VolumeConnectorsController.postbaremetal:volume:create1.32 (decorator)201—
GET/v1/volume/connectors/{volume_connector_id}ironic/api/controllers/v1/volume_connector.py:213 VolumeConnectorsController.get_onebaremetal:volume:get, baremetal:volume:view_target_properties1.32 (decorator)200—
PATCH/v1/volume/connectors/{volume_connector_id}ironic/api/controllers/v1/volume_connector.py:308 VolumeConnectorsController.patchbaremetal:volume:update1.32 (decorator)200—
DELETE/v1/volume/connectors/{volume_connector_id}ironic/api/controllers/v1/volume_connector.py:390 VolumeConnectorsController.deletebaremetal:volume:delete1.32 (decorator)204undocumented
GET/v1/volume/targetsironic/api/controllers/v1/volume_target.py:201 VolumeTargetsController.get_allbaremetal:volume:get, baremetal:volume:list, baremetal:volume:list_all1.32 (decorator)200—
POST/v1/volume/targetsironic/api/controllers/v1/volume_target.py:287 VolumeTargetsController.postbaremetal:volume:create1.32 (decorator)201—
GET/v1/volume/targets/{volume_target_id}ironic/api/controllers/v1/volume_target.py:251 VolumeTargetsController.get_onebaremetal:volume:get, baremetal:volume:view_target_properties1.32 (decorator)200—
PATCH/v1/volume/targets/{volume_target_id}ironic/api/controllers/v1/volume_target.py:348 VolumeTargetsController.patchbaremetal:volume:update, baremetal:volume:view_target_properties1.32 (decorator)200—
DELETE/v1/volume/targets/{volume_target_id}ironic/api/controllers/v1/volume_target.py:432 VolumeTargetsController.deletebaremetal:volume:delete1.32 (decorator)204undocumented

<!-- /GEN:routes-table -->

3. Spec/code mismatches found while probing

3.1 Routes documented but not served

DocumentedRealitySource
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 passthrudeclared without the leading / and with a query string (nodes/{node_ident}/vendor_passthru?method=…), so tools that match policy operations to routes miss themironic/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.version to 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 POST answers 400 "No valid host was found" (NoValidHost turned into 400), ironic/api/controllers/v1/node.py:3092.
  • Business rules:
    • name must be a valid logical name (unique, 1.5+; special characters relaxed in 1.10), ironic/api/controllers/v1/node.py:2644.
    • POST validates 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_state is never accepted in the body: new nodes start in enroll (1.11+) or available (older versions), ironic/db/sqlalchemy/api.py:816.
    • PATCH is a JSON patch; each patched path needs its own policy (baremetal:node:update:<field>, …:update_extra, …:update_instance_info, …:set_maintenance for /maintenance since the CVE fix of this branch), ironic/api/controllers/v1/node.py:3147. Updating driver, interfaces or driver_info is refused in unstable states (states.UPDATE_ALLOWED_STATES, ironic/common/states.py:246).
    • DELETE only in manageable, enroll or adopt failed (ironic/common/states.py:251) and only when no instance is associated and the node is not protected.
  • Flow:
    1. (synchronous, API) policy check, schema validation, name/UUID resolution.
    2. (synchronous, RPC call) create_node / update_node / destroy_node to 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.
    3. Response 201 (create, with Location), 200 (patch) or 204 (delete).
  • Outcome: the node exists in enroll with power_state: null; the periodic power sync fills power_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 → 409 NodeLocked after [conductor]/node_locked_retry_attempts retries.

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_steps and service_steps need extra policies; per-step policies baremetal: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):
    1. Node policy and retrieval (404 for a foreign project).
    2. The verb must exist in the requested microversion (check_allow_management_verbs, ironic/api/controllers/v1/utils.py:935): manage/provide 1.4, inspect 1.6, abort 1.13, clean 1.15, adopt 1.17, rescue 1.38, deploy/undeploy 1.73, unhold 1.85, service 1.87; otherwise 406.
    3. active/rebuild refused in maintenance (400 NodeInMaintenance, ironic/api/controllers/v1/node.py:1274).
    4. The transition must be actionable from the current state in the state machine (ironic/api/controllers/v1/node.py:1282); if not, **409 NodeLocked** when the node is reserved, else **400 InvalidStateRequested**.
    5. configdrive only with active/rebuild (dict form 1.56+, vendor_data 1.59+), ironic/api/controllers/v1/node.py:1298.
    6. runbook (1.92+) replaces clean_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, and do_provisioning_action for manage, 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…) with target_provision_state set. The client polls GET /v1/nodes/{id} until a stable state or a *failed state.
  • 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 400 InvalidStateRequested through the RPC error.

4.4 Power, boot mode, secure boot, RAID, console

OperationPurpose and rulesRPCSource
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; 202ironic/api/controllers/v1/node.py:853
PUT …/states/boot_mode, …/states/secure_boot1.76+; 409 Conflict while the node is in an unstable statecall change_node_boot_mode / change_node_secure_boot; 202ironic/api/controllers/v1/node.py:910, ironic/api/controllers/v1/node.py:961
PUT …/states/raid1.12+; validated against the RAID schema of the drivercall set_target_raid_config; 204ironic/api/controllers/v1/node.py:820
GET/PUT …/states/consoleconsole info / enable-disable; 202get_console_information, set_console_modeironic/api/controllers/v1/node.py:729
GET …/statespower, provision, boot mode, RAID and console states in one documentnoneironic/api/controllers/v1/node.py:802

4.5 Node management, traits, VIFs, virtual media, history, inventory

OperationPurpose and rulesSource
GET/PUT …/management/boot_device, GET …/boot_device/supportedread/set the next boot device through the BMC (persistent flag); RPC calls get_boot_device, set_boot_deviceironic/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_nmi1.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_traitsironic/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 portironic/api/controllers/v1/node.py:2142
GET/POST/DELETE …/vmediaattach/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 …/inventoryhardware inventory and plugin data collected by inspection (1.81+)ironic/api/controllers/v1/node.py:2292
GET …/bios[/{name}], GET …/firmwarecached 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 …/childrenchild nodes (1.83+)ironic/api/controllers/v1/node.py:2318
GET …/validatedriver interface validation result per interface ({"power": {"result": true}, …}); Nova reads deploy and powerironic/api/controllers/v1/node.py:2946
PUT/DELETE …/maintenanceset/clear maintenance with a reason; RPC update_nodeironic/api/controllers/v1/node.py:2097
* …/vendor_passthru?method=, GET …/vendor_passthru/methodsdriver 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_network 1.34+; is_smartnic 1.53+; name 1.88+; description 1.97+; vendor 1.100+, category 1.101+; port groups 1.23+ (mode, properties 1.26+, physical_network 1.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 in allocating and casts nothing: the API calls create_allocation on a random conductor, which spawns do_allocate (ironic/conductor/allocations.py:34): filter available, 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 set instance_uuid/allocation_id. Final state active (node chosen) or error with last_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

ResourcePurposeSource
/v1/chassisgrouping of nodes, no behaviour; only DBironic/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 propertiesironic/api/controllers/v1/driver.py:354
/v1/volume, /connectors, /targetsboot-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/shardsnode counts per shard (1.82+); Nova's driver can be restricted to a shardironic/api/controllers/v1/shard.py:43
/v1/eventsNeutron sends network.bind_port/unbind_port/delete_port events (1.54+)ironic/api/controllers/v1/event.py:38
/v1/inspection_rulesconditions/actions applied to inspection data (1.96+), see 05 § inspectionironic/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 → 400 IncompleteLookup; node not found → 404 (no detail on purpose); with [api]/restrict_lookup (default true) the node must be in LOOKUP_ALLOWED_STATES (or FASTTRACK_LOOKUP_ALLOWED_STATES with fast track), ironic/common/states.py:272.
  • Flow: from 1.62 the API calls get_node_with_token on 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_version needs 1.36, agent_token 1.62, agent_verify_ca 1.68, agent_status* 1.72; callback_url must 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 no agent_url is known yet, then a spawned worker runs the deploy interface's heartbeat (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 contain inventory.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:

EventTriggered by
manage, provide, inspect, clean, deploy, rebuild, delete, rescue, unrescue, adopt, abort, unhold, serviceAPI PUT …/states/provision (verb), applied by the conductor RPC handler
waita step or driver call returned *WAIT (asynchronous work in the ramdisk or the BMC)
resumea heartbeat or continue_node_* RPC continues after a wait
holda hold step (ironic/conductor/steps.py:1063)
donethe conductor worker finished the last step
faila 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.