Reverse-specification of python-ironicclient — branch stable/2026.2
Branch
stable/2026.2@af2ceeb9c4f2(6.3.0-2-gaf2ceeb, installed as version 6.3.1.dev2) — repository https://opendev.org/openstack/python-ironicclient — analysed on 2026-11-01
- Scope: all non-test code of the repository: the
openstack baremetalplugin, the Python API it uses (ironicclient.v1) and the separatebaremetalshell. The change history (step 7) was not requested, so there is no file 06. - Dependencies read at fixed versions: osc-lib 4.7.0, cliff 4.16.0, python-openstackclient 10.3.1, keystoneauth1 5.17.0, openstacksdk 4.20.0, requests 2.34.2. They are cited with the prefixes
osc-lib:,cliff:,openstackclient:,keystoneauth1:andrequests:. The server side is cited asironic:(Ironic branchstable/2026.2@39ce09ec3), read only, never run. - References are
file:linefrom the repository root. [observed] marks a behaviour seen by running the real CLI against a local stub on 127.0.0.1. [inferred] marks a statement not followed to the end. - Observed behaviour says nothing about a real Ironic. The stub answers
{}, a 404, or what a scenario scripts. An [observed] result shows how the client reacts to that response. No Ironic, lab or cloud was contacted.
Files of this specification
| File | Content |
|---|---|
| 01-cli-commands_stable-2026.2.md | shared mechanics, functional description per resource, and the 118 commands with class, arguments and observed requests |
| 02-errors_stable-2026.2.md | exit codes, how failures reach the user, exception classes, error messages, and the defects found |
| 03-interactions_stable-2026.2.md | Keystone, Ironic (headers, negotiation, retries, pagination), local files, stdin, local binaries |
| 04-scenarios_stable-2026.2.md | 5 scenarios with sequence diagrams and diagnostics |
| 05-async-processing_stable-2026.2.md | no background processing; the client-side polling that exists |
| 07-rewrite-pack_stable-2026.2.md | client contract: what the client requires from a Bare Metal service, test data, proof and limits |
| _verification_stable-2026.2.md | output of the reference checker |
Overview
python-ironicclient is the client of Ironic, the OpenStack Bare Metal service. It gives three ways in:
- **The
openstack baremetal …commands**, a plugin of python-openstackclient. This is the main interface. - **The
baremetalshell**, a separate program with the same commands, without thebaremetalprefix. It works without Keystone. - A Python API (
ironicclient.client.get_client, thenclient.node.list()and so on). Both shells call it, and it holds the HTTP, retry and negotiation logic.
It uses neither openstacksdk proxies nor a generated client: ironicclient/common/http.py:368 is its own keystoneauth adapter, and the ironicclient/v1/*.py managers build each URL by hand. It has no database, bus, server or background process.
Architecture
flowchart LR user[User or script] -->|argv, env, stdin| osc[openstack process] user -->|argv, env, stdin| legacy[baremetal process] cfg[clouds.yaml, OS_* variables] --> osc cfg --> legacy osc -->|entry points| cmds[ironicclient.osc.v1 commands] legacy -->|same entry points| cmds cmds --> mgr[ironicclient.v1 managers] mgr --> http[SessionClient] http --> ksa[keystoneauth1 session] ksa -->|HTTPS, X-Auth-Token| ks[Keystone] ksa -->|HTTPS, X-OpenStack-Ironic-API-Version| ironic[Ironic] http <--> cache[(ironic-api-version.dbm)] mgr -.->|config drive from a directory| iso[genisoimage, mkisofs or xorrisofs]
Processes
| Process | Entry point | Role | Listens to / consumes |
|---|---|---|---|
openstack (python-openstackclient) | plugin baremetal = ironicclient.osc.plugin at pyproject.toml:45, commands from pyproject.toml:47 | runs one command | argv, environment, stdin for - arguments; listens on nothing |
baremetal | pyproject.toml:42 → ironicclient/shell.py:192 | same commands, prefix stripped (ironicclient/shell.py:95), default auth type none (ironicclient/shell.py:33) | same |
External dependencies
| System | Protocol | Library | Configuration | Usage |
|---|---|---|---|---|
| Keystone | HTTPS REST | keystoneauth1 | --os-auth-url, clouds.yaml | token and catalog; the baremetal shell can skip it with --os-auth-type none |
| Ironic | HTTPS REST | ironicclient/common/http.py:368 | catalog type baremetal, or baremetal_endpoint_override (osc-lib:osc_lib/clientmanager.py:285), or --os-endpoint on the baremetal shell | every command |
| Local cache file | dbm through dogpile.cache | ironicclient/common/filecache.py:63 | IRONICCLIENT_CACHE_EXPIRY | negotiated version; written by both shells, read only by the Python API (see point 11) |
genisoimage, mkisofs, xorrisofs | child process | ironicclient/common/utils.py:282 | PATH | only for --config-drive <directory> |
Key figures
| Item | Count |
|---|---|
Commands in pyproject.toml | 118 (116 classes: 2 old trait names are aliases), 546 arguments |
| Commands reaching a request on the stub | 118 of 118, in both 404 and {} modes; 397 further runs with one option each |
| Routes in the surface | 114: 89 from plain commands, 6 only with an option, 19 only from the Python API |
| Latest known API version | 1.115 (ironicclient/common/http.py:45), which matches Ironic (ironic:ironic/api/controllers/v1/versions.py:288) |
| Client-side version gates | 7 sites, all for runbooks at 1.112; everything else is left to the server |
| Exception classes | 43 in ironicclient/common/apiclient/exceptions.py, plus 4 in ironicclient/exc.py |
| Unit tests | 1,155 of 1,155 pass in two runs; 632 vectors, on 110 of 116 classes |
| Scripted scenarios | 85, in _rewrite/scenarios_stable-2026.2.json |
| Defects reproduced | 13, see 02 § 5 |
Points of attention
- **
baremetal driver passthru callfails on any JSON answer [observed]**: the command gives aDriverobject todict2columns, which needs a dict (ironicclient/osc/v1/baremetal_driver.py:208,cliff:cliff/show.py:70). The stderr says onlyidand the exit code is 1, after the vendor method has run. A script will retry an action that already happened. The unit test mocks the return with a plain dict (ironicclient/tests/unit/osc/v1/test_baremetal_driver.py:298), so it passes. - **
baremetal node children listshows only the first child [observed]**: it is aShowOnewith one column, but returns one row per child (ironicclient/osc/v1/baremetal_node.py:2640), and cliff pairs columns with values. With childrenc1andc2, the output holds['c1']and the exit code is 0. - **A
Retry-Afterheader turns a 429 or 503 into a Python error [observed]**:from_responsepassesretry_afterto a constructor that onlyRequestEntityTooLargeaccepts (ironicclient/common/apiclient/exceptions.py:449,ironicclient/common/apiclient/exceptions.py:304). The user readsHttpError.__init__() got an unexpected keyword argument 'retry_after', and the retry on 503 does not happen. A proxy in front of the service can trigger this [inferred]; Ironic's API code has noRetry-Afterof its own (grep ofironic/api). - **The
error_messagefield must be a string [observed]**: an object givesthe JSON object must be str, bytes or bytearray, not dict(ironicclient/common/http.py:106). Ironic sends a string (ironic:ironic/api/middleware/parsable_error.py:71). A compatible service must do the same: this is the first rule of the client contract. - Writes are retried silently [observed]: 409 and 503 are retried 5 times, 2 seconds apart, on every method including
POST(ironicclient/common/http.py:324).baremetal node createagainst a constant 409 sent 6POSTin 10 seconds. The plugin offers no option for this (ironicclient/osc/plugin.py:77); only thebaremetalshell has--max-retries(ironicclient/shell.py:139). - Query values are not URL-encoded [observed]:
--description-contains 'a&retired=true b'sent?description_contains=a&retired=true%20b, which adds aretiredfilter (ironicclient/v1/node.py:1493). - **
--name 123becomes a JSON number onset, but stays a string oncreate[observed]**:setcommands parse each value as JSON (ironicclient/common/utils.py:66).--description truesends a boolean. - **
baremetal inspection rule delete alldoes not delete all rules [observed]**: the test compares a list with a string, so it is never true (ironicclient/osc/v1/baremetal_inspection_rule.py:329). The client sendsDELETE /v1/inspection_rules/all. These six commands have no unit test at all. - **
--config-drive <base64 string>is refused [observed]**: the help offers it (ironicclient/osc/v1/baremetal_node.py:37), but any string that is not a file, a directory or a JSON object raisesValueErrorbefore any request (ironicclient/v1/node.py:797). - **Without
--limit, a list returns only the first page [observed]**:nextis followed only when a limit is given, including--limit 0(ironicclient/v1/node.py:1502). The help hints at this; a script counting nodes on a large cloud gets at most the server's page size. - The negotiation cache does nothing for the two shells [observed]: both always pass a version, and the cache is read only when none is given (
ironicclient/v1/client.py:64). Against an older server, every process sends the 406 round trip again. - Show commands crash on an empty object [observed]:
200 {}gives'NoneType' object has no attribute '_info'on 15 commands. Ironic does not answer that for a resource [inferred]; a partial implementation might. - **
--waitwithout a timeout never ends if nothing changes**: timeout 0 means no limit (ironicclient/common/utils.py:438). With--wait, a node that still shows the expected state just after the request counts as done [observed forservice,ironicclient/v1/node.py:1134].
Blind spots / [inferred]
- No real service was contacted, as instructed. Nothing here proves that a command works against Ironic, or that a body is accepted by it. Statements about Ironic come from reading its code.
- The probes stop early: the stub has no resources, so anything after the first response is known only from scripted scenarios, or from reading. Output columns were not checked against real data.
- Not run:
--config-drive <directory>, which executes a local binary; argument values were synthetic everywhere else. Authentication with anything but a password against the stub was not tried.clouds.yamlwas not exercised, sinceHOMEwas empty. - **
baremetalshell**: only 8 scenarios ran through it, plus the shared code. It was not probed command by command. - **
ironicclient/common/apiclient/base.py** lazy loading: seen to do nothing when the response has noid[observed,shape-create-partial-response]; a response with anidfield was not tried. - Server-side microversion requirements per option are not listed here, because the client does not hold them. See the Ironic spec in
reverse-spec-ironic/. - Inspection rule commands have no unit tests, so their vectors are absent. Their requests come from the probes only.
- Output formats (
-f json,-c, and so on) belong to cliff and were not specified per command.
Source: reverse-spec-python-ironicclient/00-summary_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.