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

Reverse-spec · Services

placement — Summary

Reverse-specification of Placement — branch stable/2026.2

Branch stable/2026.2 @ f4a89d3803cb (tag 16.0.0-2) — repository https://opendev.org/openstack/placement (package openstack-placement) — analysed on 2026-10-28

  • Scope: all non-test code (~15,000 lines): WSGI API and middlewares, business objects, database schema, placement-manage and placement-status CLIs, and the change history since 15.0.0 (see change history).
  • References are file:line relative to the repository root. [inferred] marks a statement that was not followed through to the end in the code.

Files of this specification

FileContent
01-rest-api_stable-2026.2.mdthe 37 method + path pairs, functional description per resource, candidate search algorithm, generations
02-errors_stable-2026.2.mdper-handler exception → HTTP conversion, the 9 placement.* codes, catalogue by status
03-interactions_stable-2026.2.mdHTTP frames, Keystone (indirect), 12 tables, reconstructed SQL
04-scenarios_stable-2026.2.md5 end-to-end scenarios
05-async-processing_stable-2026.2.mdno background processing; startup work; admin commands
06-change-history_stable-2026.2.mdthe 15 commits since 15.0.0, as change specifications, with a carry-over plan
07-rewrite-pack_stable-2026.2.mdrewrite 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

ProcessEntry pointRole
placement-apiWSGI 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 moreREST 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-manageplacement.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-statusplacement.cmd.status:mainpre-upgrade checks: missing root_provider_ids, incomplete consumers, policy.json (placement/cmd/status.py:38-109)

External dependencies

SystemProtocolLibraryConfigurationUsage
Placement database (MariaDB/MySQL; PostgreSQL possible)SQLSQLAlchemy 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
KeystoneHTTP (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 admin or service, the /reshaper rule is service only, and the GET /usages rule 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

  1. Every write relies on the provider generation.
    • The client must send back resource_provider_generation; otherwise it gets **409 placement.concurrent_update** and must re-read and retry.
    • For allocations only, Placement retries by itself up to [placement]/allocation_conflict_retry_count times (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).
  2. An inventory can be reduced below current usage. PUT .../inventories accepts a total lower than what is already allocated. Placement only logs WARNING … 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).
  3. 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**, not placement.inventory.inuse (placement/handlers/inventory.py:240-245). A client that retries automatically on that code loops forever. DELETE .../inventories (all classes) and PUT .../inventories do return placement.inventory.inuse (placement/handlers/inventory.py:365-367, placement/handlers/inventory.py:403).
  4. **GET /allocation_candidates can be expensive.** By default there is no limit on the number of candidates generated (max_allocation_candidates = -1), and limit only 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 the breadth-first strategy. The fix for bug 2160721 removes one of these slow cases (see 06).
  5. **/reshaper is restricted to the service role.** The admin role alone is not enough (placement/policies/reshaper.py:23). An administrator trying to repair a tree by hand gets 403.
  6. "Incomplete" consumers skew per-project usages. Before 1.8, an allocation without project_id/user_id is 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 check reports these consumers.
  7. 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 by DBDuplicateEntry (placement/objects/trait.py:298, placement/objects/resource_class.py:254). The result is noise and concurrent writes at startup, harmless [inferred].
  8. **GET /traits/{name} returns 204 with no body**, not 200 (placement/handlers/trait.py:119). A classic trap for clients.
  9. No business audit trail. No notification is emitted. To know who changed an allocation, only the access logs (placement/requestlog.py) and the X-Openstack-Request-Id remain.
  10. RBAC scope is always enforced on this branch when oslo.policy ≥ 6.0.0 is installed, which requirements.txt now requires. A deployment that relied on [oslo_policy] enforce_scope = False changes 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.