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

Reverse-spec · Clients

python-neutronclient — Rewrite pack

Client contract (rewrite pack) — python-neutronclient — branch stable/2026.2

Branch stable/2026.2 @ 596a9d99b188 (14.0.0 + 2 maintenance commits) — repository https://opendev.org/openstack/python-neutronclient — analysed on 2026-11-01

This pack states what a Neutron replacement must do so that these keep working unmodified:

  • the 34 commands of the plugin;
  • the projects that import neutronclient.v2_0.client.Client.

No replay against a real Neutron was possible. The client ran against a local stub. See § 6 for what was proved and what was not.

1. Contents of _rewrite/

FileContentUse
[cli-commands_stable-2026.2.json](_rewrite/cli-commands_stable-2026.2.json)the 34 commands: group, class, purpose, the 94 arguments (names, type, default, choices, required, exclusive groups, help), client calls with file:line, error sitesgenerate a parser; check argument names
[client-contract_stable-2026.2.json](_rewrite/client-contract_stable-2026.2.json)per command: stack, client methods, observed requests, static requests, sample bodies, vectors, exit codes per stub moderequirements per command
[api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md)part 1: the 48 routes seen from commands; part 2: the 372 methods of Client, with path, definitions match and callersgap analysis of a Neutron service
[python-api_stable-2026.2.json](_rewrite/python-api_stable-2026.2.json)the same 372 methods with signatures, plus the path constants, EXTED_PLURALS, and the consumers' commits and importsPython API reference
[unit-vectors_stable-2026.2.json](_rewrite/unit-vectors_stable-2026.2.json)90 vectors: argv → ordered client calls with arguments, UUIDs normalisedtest argument mapping without Python
[wire-probe-404_stable-2026.2.json](_rewrite/wire-probe-404_stable-2026.2.json)each command against a stub answering 404 and NeutronErrorrequests up to the first error, then lookups
[wire-probe-200_stable-2026.2.json](_rewrite/wire-probe-200_stable-2026.2.json)each command against a stub answering 200 {}reaction to an empty object
[wire-probe-rich_stable-2026.2.json](_rewrite/wire-probe-rich_stable-2026.2.json)each command against a stub with canned resources, stdout includedlast routes of each command
[wire-probe-rich-all-options_stable-2026.2.json](_rewrite/wire-probe-rich-all-options_stable-2026.2.json)same, with every optional argumentrequest bodies
[defect-repros_stable-2026.2.json](_rewrite/defect-repros_stable-2026.2.json)96 runs of the real CLI and class, with chosen responsesevidence for 02 and § 2
ScriptRole
[extract_plugin_stable-2026.2.py](_rewrite/extract_plugin_stable-2026.2.py)imports the entry points, dumps parsers, walks the code with ast (unchanged copy)
[probe_plugin_stable-2026.2.py](_rewrite/probe_plugin_stable-2026.2.py)local stub and real shell. Adapted: Neutron error body, mode rich, --all-options, headers recorded, /v2.0 kept in routes
[capture_plugin_stable-2026.2.py](_rewrite/capture_plugin_stable-2026.2.py)unit tests → vectors (unchanged copy)
[repro_defects_stable-2026.2.py](_rewrite/repro_defects_stable-2026.2.py)subprocess of the real shell for each chosen response
[sweep_client_stable-2026.2.py](_rewrite/sweep_client_stable-2026.2.py)calls each public method with a recorder, sending nothing; searches consumers by name
[build_contract_stable-2026.2.py](_rewrite/build_contract_stable-2026.2.py)merges everything

Versions: python-neutronclient 14.0.1.dev2 (the venv's copy is identical to the clone), python-openstackclient 10.3.1, osc-lib 4.7.0, cliff 4.16.0, openstacksdk 4.20.0, keystoneauth1 5.17.0, python-keystoneclient 6.0.0, Python 3.13.

To regenerate, with HOME set to an empty directory and no OS_* variable:

python -I -B _rewrite/extract_plugin_stable-2026.2.py <repo> python-neutronclient neutronclient _rewrite
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-404> --mode 404
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-200> --mode 200
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-rich> --mode rich
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-all> --mode rich --all-options
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-1>/c.json <repo>/neutronclient/tests/unit <repo>
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-2>/c.json <repo>/neutronclient/tests/unit <repo>
python -I -B _rewrite/sweep_client_stable-2026.2.py <repo> <dir-of-clones> _rewrite/python-api_stable-2026.2.json
python -I -B _rewrite/repro_defects_stable-2026.2.py _rewrite <repro.json>
python -I -B _rewrite/build_contract_stable-2026.2.py . <gen-dir> <dir-404>/wire-probe_stable-2026.2.json <dir-200>/wire-probe_stable-2026.2.json <dir-rich>/wire-probe_stable-2026.2.json <dir-all>/wire-probe_stable-2026.2.json <run-1>/c.json <run-2>/c.json <repro.json>

2. What a Neutron service must do

#RequirementWhySource
1The catalog URL of type network must not end in /v2.0the class appends it, giving /v2.0/v2.0/…; the SDK copes with both[observed catalog-v2-log], neutronclient/v2_0/client.py:260
2All routes live under /v2.0 with these exact spellings: /log/logs, /log/loggable-resources, /subnetpools/{id}/onboard_network_subnets, /sfc/flow_classifiers, /sfc/port_pairs, /sfc/port_pair_groups, /sfc/port_chains, /sfc/service_graphshard-coded; hyphen in one, underscores in the othersneutronclient/v2_0/client.py:530-539, neutronclient/v2_0/client.py:679-681, openstacksdk:openstack/network/v2/sfc_port_pair_group.py:19
3Responses use 200, 201, 202 or 204 for successothers take the error pathneutronclient/v2_0/client.py:291-294
4An error is {"NeutronError": {"type": T, "message": M, "detail": D}}, with all three keysa missing detail displays a Python dict and loses the type[observed error-no-detail], neutronclient/v2_0/client.py:64-72
5type keeps the original names of Neutron exceptions, at least the 14 in 02 § 2<type>Client decides the class caught by Nova, Heat, Manila, Zunneutronclient/v2_0/client.py:70, § 4
6message is present at the top of the error objectthe SDK takes it from any top-level objectopenstacksdk:openstack/exceptions.py:197
7Error bodies are JSON objects, never lists[observed error-json-list]neutronclient/v2_0/client.py:56
8Lists always hold the collection key, also when empty; they are never {}otherwise tuple indices… or 'logs'[observed log-list-empty-object, sdk-list-no-key], neutronclient/v2_0/client.py:376
9Single resources are wrapped in the singular key; POST and PUT return them['log'], and resource_key of the SDK[observed log-show-no-object], neutronclient/osc/v2/logging/network_log.py:152
10GET /x?id=<uuid> filters by ID, and GET /x?name=<text> by exact namethe class's lookup; a filter ignored by the server gives Multiple … matches[observed log-show-two-names], neutronclient/v2_0/client.py:430, neutronclient/v2_0/client.py:452
11GET /x/{id} answers 404 for a name, never 400the SDK then tries ?name=[observed ppg-show-missing-control], openstacksdk:openstack/resource.py:2492
12fields=id is honoured or harmlessly ignored, keeping idfind_resourceid_by_name_or_id reads ['id']neutronclient/neutron/v2_0/__init__.py:53-58
13A log always holds event, target_id, resource_type and resource_id, with event not null§ 4 of the defects in 02[observed log-list-event-none-first, log-list-no-target-key]
14A flow classifier always holds l7_parameters as an object; a port chain holds flow_classifiers and port_pair_groups as lists, and a port pair group holds port_pairsclient-side join and list operations[observed fc-list-no-l7], neutronclient/osc/v2/sfc/sfc_port_chain.py:216
15PUT {"log": {}} is accepted with no changenetwork log set x sends it[observed log-set-no-option]
16String values are coerced: "symmetric": "true", "weight": "2"the client does not convert them[observed pc-create-params, pp-create-params], neutronclient/osc/v2/sfc/sfc_port_chain.py:380
17Service graphs: a real cycle check, since the client's misses longer cycles[observed graph-cycle-3-missed]neutronclient/osc/v2/sfc/sfc_service_graph.py:233
18<collection>_links with rel = next, and href whose query alone is reusedpagination; the host and path of the link are ignored by the class[observed log-list-pagination], neutronclient/v2_0/client.py:396
19GET /quotas/tenant and GET /quotas/{id}/details.jsonhard-coded in the class; Neutron keeps tenant as a special case for this clientneutronclient/v2_0/client.py:770, neutronclient/v2_0/client.py:514, neutron:neutron/pecan_wsgi/controllers/quota.py:137
20If-Match: revision_number=N is honoured on updatesmethods given revision_numberneutronclient/v2_0/client.py:2546
21A 401 for an invalid token; a new token then workskeystoneauth retries once[observed error-401-once], keystoneauth1:keystoneauth1/session.py:1130
22X-OpenStack-Request-ID is returnedstored in request_ids, shown in errorsneutronclient/v2_0/client.py:121, neutronclient/common/exceptions.py:80

Which requests a command sends

Read observed_requests in the contract first, then static_requests. The {} mode is excluded from observed_requests: there the SDK uses the name as ID, which gives routes such as DELETE /sfc/port_chains/x that a real service would not see.

3. Coverage of OpenKeel

Read in openkeel-neutron:internal/api/api.go:72-77, not run:

  • log/logs and log/loggable_resources are stubs: list empty, other calls 501. So network log list would print an empty table, and network log create would fail.
  • No SFC route was found by search, and neither was onboard_network_subnets, so the 27 sfc commands and network onboard subnets would get what an unknown route answers [inferred].
  • The OSC spec's lab run gave 404 for extension commands (OSC 07 § 3). This plugin was not run on the lab.

Which matters for OpenKeel: the Python class matters more than these 34 commands, because Nova and Heat depend on it unless replaced too. Requirements 4, 5, 8 and 10 to 12 carry the weight.

4. Consumers of the library

Name search in non-test Python code, at these commits. A method is counted when a call with its name exists, so false positives are possible [inferred], and no consumer was run.

ProjectCommitBranchMethods by nameImportsKey usage
Heat5872396e548cstable/2026.2112Client, exceptions, neutronclient.neutron.v2_0heat:heat/engine/clients/os/neutron/__init__.py:15, heat:heat/engine/clients/os/neutron/__init__.py:113; is_not_found, is_conflict, is_no_unique read the classes (heat:heat/engine/clients/os/neutron/__init__.py:71-90)
ec2-api32740781f605pre-retirement46Client, exceptionsec2-api:ec2api/clients.py:77
Nova8df1922926ddstable/2026.230Client, exceptions, neutronclient.neutron.v2_0nova:nova/network/neutron.py:30; Unauthorized translation (nova:nova/network/neutron.py:189); security groups by name (nova:nova/network/security_group_api.py:67)
Manila4fbc4b4f3688stable/2026.228Client, exceptionsmanila:manila/network/neutron/api.py:20; adds its own exception class (manila:manila/network/neutron/api.py:80)
Zun34135a563699stable/2026.217Client, exceptions, neutronclient.neutron.v2_0zun:zun/common/clients.py:93, zun:zun/network/neutron.py:271
Skyline4af72bfd4577stable/2026.23Client, private _GeneratorWithMetaskyline-apiserver:skyline_apiserver/client/utils.py:195, skyline-apiserver:skyline_apiserver/client/openstack/neutron.py:23
heat-tempest-pluginceb78843237cmaster1Clientheat-tempest-plugin:heat_tempest_plugin/services/clients.py:152

Not consumers at these commits: python-openstackclient (in doc/requirements.txt only, openstackclient:doc/requirements.txt:22, plus IGNORED_MODULES), and Horizon and Octavia, which only mention it in comments (horizon:openstack_dashboard/api/neutron.py:1032, octavia:octavia/common/keystone.py:63). openstacksdk keeps a comment about the hard-coded /v2.0 (openstacksdk:openstack/config/cloud_region.py:1210). In Neutron it is a test dependency only (neutron:test-requirements.txt:11). requirements:upper-constraints.txt:370 pins 14.0.0.

Exceptions caught by Nova, by count: NeutronClientException 18, PortNotFoundClient 4, NetworkNotFoundClient 4, Unauthorized 3, Conflict 3 and others. These names and their inheritance are a Python API that a drop-in replacement of this package must keep.

The Python surface, by path family

Counts come from the surface. "Matched" is a text search in neutron-lib definitions and neutron extensions, so it is a hint: a path can match and not be served.

FamilyMethodsWith a caller by nameNotes
/networks, /subnets, /ports, /subnetpools, /routers, /floatingips, /security-groups, /security-group-rules5848core; what the consumers really need
/qos, /trunks, /segments, /rbac-policies, /address-scopes, /quotas, /extensions, /agents, /tags, /flavors, /service_profiles, /metering, and others10945extensions; 23 have no match
/lbaas, /lb631LBaaS; 47 have no match; show_loadbalancer matches Heat by name only
/fw, /fwaas, /vpn, /bgpvpn, /bgp-speakers, /bgp-peers9524the matching OSC commands now use the SDK; callers are by name; /fwaas has no caller, apart from the lookup in network log create
/network-gateways, /gateway-devices, /qos-queues165no match in definitions; callers are Heat, by name
/sfc, /log310the commands use /log, by Client; /sfc methods are not on any command's path

5. For a rewrite of the commands

TopicRuleSource
Namessfc_port_chain_create → sfc port chain create; scripts depend on these and on argument namessetup.cfg:40
Exit codes0, 1, 2 as in 02; do not copy its § 4 defectscliff:cliff/command.py:196
Case--resource-type lower-cased, --event upper-cased before the checkneutronclient/osc/v2/logging/network_log.py:123
DisplaySummary columns in network log list and sfc flow classifier list are built on the clientneutronclient/osc/v2/logging/network_log.py:243, neutronclient/osc/v2/sfc/sfc_flow_classifier.py:209
Column namesBranching Points for port_chains, Tap Enabled for is_tap_enabledneutronclient/osc/v2/sfc/sfc_service_graph.py:40, neutronclient/osc/v2/sfc/sfc_port_pair_group.py:47

Recommendation: keep the plugin as it is, and fix the service side first. The surface is small, but it runs inside OSC, and the class's users are Python services that OpenKeel may replace on its own schedule.

6. Conformance: what was proved

EvidenceResultWhat it provesWhat it does not prove
Parser dump34 of 34 classes imported, 0 failuresthe catalogue is complete for the declared entry points
Real shell, command listexactly the 34they load with the 8 plugins present, and none is ignoredanother plugin set
Unit tests with recorder154 of 154 pass, 90 vectors for 34 of 34 classesargv → client callsanything about a service: the clients are mocks
Comparison of the two runs90 of 90 have the same test, call sequence and statusmethod sequences are deterministicarguments holding random IDs were not compared
Wire probes34 of 34 reach a request in each mode; exit 0: 404 mode 0, {} mode 22, rich 34, all options 34; 54, 49, 54 and 79 requestsreal routes, headers and bodiesacceptance by a server
Chosen responses96 runsevery defect and trap of 02that a real Neutron ever answers this way
Sweep of the class372 methods, 0 raisedpath templates are well-formedthose routes exist on a server

Replay against a real service was not done, and the unit vectors were not replayed against a rewrite, since none exists. The usual "replay is mandatory" bar is not met for service behaviour. The mocks hide real defects: items 1 to 7 of 02 § 4 all pass the unit tests.

Unit tests exercise no 3-node cycle and no empty event: neutronclient/tests/unit/osc/v2/sfc/test_service_graph.py:1 has no test named for a cycle, and the fake log event is always set (neutronclient/tests/unit/osc/v2/logging/fakes.py:77).

To close the gap: run probe_plugin_stable-2026.2.py against a devstack with networking-sfc and logging enabled, and compare it with wire-probe-rich_stable-2026.2.json.

7. Gaps found while building the pack

  • The extractor gives no REST route for neutronclient.* calls, because it resolves only SDK methods; the sweep fills that in the contract's static_requests. find_resource depends on its argument, so only the observed routes show it.
  • 64 of the 154 tests record no check_parser call, and so give no vector.
  • --project-domain and --l7-parameters were left out of the all-options run; --l7-parameters has its own run, and --project-domain was not run.
  • The stub never served a real tap_enabled default, chain_id or group_id.

8. Non-functional requirements

  • Cost per command: authentication once per process; an SFC update costs up to 7 GET before the PUT [observed pc-set-append].
  • Client startup: every Client scans the neutronclient.extension entry points (neutronclient/v2_0/client.py:2544, neutronclient/common/extension.py:22).
  • No timeout is set by the plugin, so a stuck service can block a command [inferred].

9. Checklist for an OpenKeel Neutron

  • [ ] The catalog URL of type network has no version suffix.
  • [ ] Errors hold NeutronError.type, message and detail; type keeps the original names.
  • [ ] Lists always hold the collection key; single objects are wrapped.
  • [ ] ?id= and ?name= filter exactly; GET /x/{name} gives 404.
  • [ ] Each operation of api-surface_stable-2026.2.md part 1 is implemented, or its absence is documented.
  • [ ] Each method that Nova, Heat, Manila and Zun call is served, checked against part 2.
  • [ ] /quotas/tenant and /quotas/{id}/details.json respond, if the library stays in use.
  • [ ] The cycle check of service graphs is done on the server, if SFC is implemented.
  • [ ] The probe, pointed at the lab, gives the same routes, and exit 0 with real resources.

Source: reverse-spec-python-neutronclient/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.