Reverse-specification of Placement — branch stable/2026.2
Branch
stable/2026.2@f4a89d3803cb(tag16.0.0-2) — repository https://opendev.org/openstack/placement (packageopenstack-placement) — analysed on 2026-10-28
- Scope: all non-test code (~15,000 lines): WSGI API and middlewares, business objects, database schema,
placement-manageandplacement-statusCLIs, and the change history since15.0.0(see change history). - References are
file:linerelative to the repository root. [inferred] marks a statement that was not followed through to the end in the code.
Files of this specification
| File | Content |
|---|---|
| 01-rest-api_stable-2026.2.md | the 37 method + path pairs, functional description per resource, candidate search algorithm, generations |
| 02-errors_stable-2026.2.md | per-handler exception → HTTP conversion, the 9 placement.* codes, catalogue by status |
| 03-interactions_stable-2026.2.md | HTTP frames, Keystone (indirect), 12 tables, reconstructed SQL |
| 04-scenarios_stable-2026.2.md | 5 end-to-end scenarios |
| 05-async-processing_stable-2026.2.md | no background processing; startup work; admin commands |
| 06-change-history_stable-2026.2.md | the 15 commits since 15.0.0, as change specifications, with a carry-over plan |
| 07-rewrite-pack_stable-2026.2.md | rewrite pack: machine-readable contracts in _rewrite/, formal allocation-candidates algorithm, client expectations, database strategy, conformance strategy |
Overview
Placement is the central registry of consumable resources of an OpenStack cloud. It acts as the ledger of capacities and reservations:
- Resource providers declare what they have. They are compute hosts, storage pools, network segments, GPUs…
- Inventories describe those capacities: vCPU, RAM, disk, bandwidth…
- Traits (qualitative capabilities) and aggregates (groupings) characterise the providers.
- Consumers, mostly Nova instances but also migrations or Neutron ports, reserve quantities through allocations.
- Candidate search: Placement finds the combinations of providers that can satisfy a request (
GET /allocation_candidates). The Nova scheduler depends on it.
Placement is stateless at process level. All the truth is in the SQL database, and consistency under concurrent writes relies on generation numbers (optimistic locking). It calls no other service: the other services call it.
Architecture
flowchart LR nova[nova-compute\nresource tracker] -->|REST: inventories, traits, aggregates, reshaper| api sched[nova-scheduler / nova-conductor] -->|REST: allocation_candidates, allocations| api neutron[neutron-server\nqos, ports] -->|REST: providers, inventories, allocations| api cyborg[Cyborg, Zun, Blazar, others] -->|REST| api admin[Administrator / osc-placement] -->|REST| api api[placement-api\nWSGI: uwsgi or mod_wsgi] -->|SQL| db[(MariaDB / MySQL: placement database)] api -.->|keystonemiddleware: token validation| ks[Keystone] cli[placement-manage / placement-status] -->|SQL| db
Processes
| Process | Entry point | Role |
|---|---|---|
| placement-api | WSGI module placement.wsgi.api:application (placement/wsgi/api.py:19-22), which calls init_application (placement/wsgi/__init__.py:120-147). No placement-api script is declared in pyproject.toml any more | REST API, served by an external WSGI server (uwsgi, mod_wsgi). At startup, it syncs the standard traits and resource classes into the database (placement/deploy.py:122-130) |
| placement-manage | placement.cmd.manage:main (pyproject.toml:56, placement/cmd/manage.py:213) | db sync, db version, db stamp, db online_data_migrations (placement/cmd/manage.py:166-209) |
| placement-status | placement.cmd.status:main | pre-upgrade checks: missing root_provider_ids, incomplete consumers, policy.json (placement/cmd/status.py:38-109) |
External dependencies
| System | Protocol | Library | Configuration | Usage |
|---|---|---|---|---|
| Placement database (MariaDB/MySQL; PostgreSQL possible) | SQL | SQLAlchemy and oslo.db enginefacade (placement/db_api.py:24-40) | [placement_database]/connection (placement/conf/database.py:59); sync_on_startup (False by default, placement/conf/database.py:109) | 12 tables; all the business logic |
| Keystone | HTTP (keystonemiddleware.auth_token) | keystonemiddleware | [keystone_authtoken]; [api]/auth_strategy is keystone by default, or noauth2 for tests (placement/conf/api.py:26, placement/deploy.py:43-47) | validation of incoming tokens; the only outbound network exchange, made by the middleware |
| os-traits, os-resource-classes | — (Python libraries) | — | os-traits>=3.9.0 (requirements.txt:23) | lists of standard traits and resource classes, copied into the database at startup |
| oslo.policy | — (Python library) | — | oslo.policy>=6.0.0 (requirements.txt:19) | authorisation; since 6.0.0, scope checks can no longer be disabled (see 06) |
What is absent: RabbitMQ, notifications, Redis or memcached, and REST calls to other services. placement/direct.py is an in-process client, used by tests and tools. Files 03 and 05 give the details.
Key figures
- REST API: 19 routing entries (two of them for the root,
/and the empty string) and 37 method + path pairs (placement/handler.py:51-133). - Microversions: 1.0 to 1.39 (
placement/microversion.py:24-64). - Authorisation: 34 API rules. 32 default to
adminorservice, the/reshaperrule isserviceonly, and theGET /usagesrule is also open to the project reader. - Errors: 35 internal exception classes and 9 structured
placement.*error codes (placement/errors.py:42-53). - Database: 12 tables, 2 online data migrations.
- Processing outside the API: no periodic task, no bus consumer.
- History since 15.0.0: 15 commits. 1 functional fix (bug 2160721), 2 upgrade-impacting changes (Python ≥ 3.11, RBAC scope always enforced), 3 dependency changes, 9 maintenance commits.
Points of attention
- Every write relies on the provider generation.
- The client must send back
resource_provider_generation; otherwise it gets **409placement.concurrent_update** and must re-read and retry. - For allocations only, Placement retries by itself up to
[placement]/allocation_conflict_retry_counttimes (10 by default) when a provider's generation changed (placement/objects/allocation.py:544-596). - A conflict on the consumer generation (microversion ≥ 1.28) is never retried server-side (
placement/handlers/util.py:139-163).
- The client must send back
- An inventory can be reduced below current usage.
PUT .../inventoriesaccepts atotallower than what is already allocated. Placement only logsWARNING … is now over-capacity(placement/objects/resource_provider.py:121-166,624-641). After that, every new allocation on this provider fails with 409, unless it reduces the overage (placement/objects/allocation.py:272-291). - A misleading error code.
DELETE /resource_providers/{uuid}/inventories/{rc}on a class that is still allocated returns 409 with the code **placement.concurrent_update**, notplacement.inventory.inuse(placement/handlers/inventory.py:240-245). A client that retries automatically on that code loops forever.DELETE .../inventories(all classes) andPUT .../inventoriesdo returnplacement.inventory.inuse(placement/handlers/inventory.py:365-367,placement/handlers/inventory.py:403). - **
GET /allocation_candidatescan be expensive.** By default there is no limit on the number of candidates generated (max_allocation_candidates = -1), andlimitonly applies after generation (placement/conf/placement.py:66-91). On wide and symmetric provider trees (mdev GPUs, PCI), memory and response time can blow up. The option's documentation recommends about 100,000 and thebreadth-firststrategy. The fix for bug 2160721 removes one of these slow cases (see 06). - **
/reshaperis restricted to theservicerole.** Theadminrole alone is not enough (placement/policies/reshaper.py:23). An administrator trying to repair a tree by hand gets 403. - "Incomplete" consumers skew per-project usages. Before 1.8, an allocation without
project_id/user_idis attached to placeholder project and user IDs ([placement]/incomplete_consumer_project_id/_user_id,placement/handlers/util.py:130-132).GET /usages?project_id=therefore does not count them for the real project.placement-status upgrade checkreports these consumers. - Each WSGI process writes to the database at startup (sync of the standard traits and classes,
placement/deploy.py:122-130), under a process-local lock only. Duplicates between processes are absorbed byDBDuplicateEntry(placement/objects/trait.py:298,placement/objects/resource_class.py:254). The result is noise and concurrent writes at startup, harmless [inferred]. - **
GET /traits/{name}returns 204 with no body**, not 200 (placement/handlers/trait.py:119). A classic trap for clients. - No business audit trail. No notification is emitted. To know who changed an allocation, only the access logs (
placement/requestlog.py) and theX-Openstack-Request-Idremain. - RBAC scope is always enforced on this branch when oslo.policy ≥ 6.0.0 is installed, which
requirements.txtnow requires. A deployment that relied on[oslo_policy] enforce_scope = Falsechanges behaviour at upgrade (see 06).
Blind spots / [inferred]
- **The detailed algorithm of
GET /allocation_candidates** (placement/objects/research_context.py,placement/objects/allocation_candidate.py, ~2,500 lines) is described step by step in principle. Its exact SQL queries were not reconstructed line by line. - The JSON schemas of each microversion (
placement/schemas/*.py) were read for the main fields, not exhaustively. The official reference is the api-ref (api-ref/source/*.inc), which was not compared line by line with the code. - The transactional behaviour of allocation retries inside an enclosing transaction is [inferred] (see 03).
- **
placement/direct.py** (in-process client,PlacementDirect) was only skimmed.
Source: reverse-spec-placement/00-summary_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.