REST API — placement — branch stable/2026.2
Branch
stable/2026.2@f4a89d3803cb— repository https://opendev.org/openstack/placement — analysed on 2026-10-28
Common mechanics
- Routing: a static table,
ROUTE_DECLARATIONS(placement/handler.py:51-133), compiled into aroutes.Mapper. An unknown path yields 404; an undeclared method yields 405 with anAllowheader (placement/handler.py:136-174). - Middleware order, from the outside in (
placement/deploy.py:62-82):- osprofiler (optional);
HTTPProxyToWSGI;RequestLog(access log);- CORS (optional);
- **keystonemiddleware
auth_token**, except for/(placement/auth.py:93-116); PlacementKeystoneContext, which builds the request context and returns 401 withoutX_USER_ID(placement/auth.py:75-90);FaultWrapper: any non-HTTP exception becomes a 500 (placement/fault_wrap.py);- **
MicroversionMiddleware** (microversion-parse); PlacementHandler.
- Microversions: the header is
OpenStack-API-Version: placement 1.39. Without the header, version 1.0 applies. A version outside 1.0–1.39 yields 406, withmin_version/max_versionin the error body (placement/util.py,json_error_formatter). A handler decorated with@microversion.version_handler(min, max)answers 404 when the requested version does not match;DELETE …/inventoriesanswers 405 in that case (placement/microversion.py:130-170). - Header checks:
- a non-empty body without
Content-Typeyields 400 (placement/handler.py:209-218); - a
Content-Typeother thanapplication/jsonyields 415 (util.require_content); - an incompatible
Acceptyields 406 (util.check_accept).
- a non-empty body without
- Validation: every body and every query string is validated with jsonschema, with one schema per microversion (
placement/schemas/*.py). On failure, 400 "JSON does not validate" or "Invalid query string parameters" (placement/util.py:69-84,210-217). - HTTP caching: from 1.15, responses carry
Last-ModifiedandCache-Control: no-cache. - Authorisation (
placement/policies/*.py): almost everything isrule:admin_or_service_api(role:admin or role:service), with theprojectscope. The exceptions are/reshaper(role:serviceonly) andGET /usages(admin, service, or reader of the requested project). A refusal yields 403 (placement/handler.py:226-229). On this branch, scope checks are always enforced with oslo.policy ≥ 6.0.0 (see change history, change 6). - Generation: each provider carries an integer
generation, incremented on every change to its inventories, allocations, traits or aggregates. Requests that modify this data must send back the generation they read; a mismatch yields **409placement.concurrent_update**. Since 1.28, consumers have a generation too.
Summary table
Paths are relative to the service root, often /placement behind a proxy. The "Policy" column gives the suffix of placement:…. A/S means admin or service.
| Method | Path | Microversion | Handler | Policy | Success | Main errors |
|---|---|---|---|---|---|---|
| GET | / | all, no authentication | placement/handlers/root.py:24 | — | 200 | — |
| GET | /resource_providers | 1.0+ (filters: member_of 1.3, resources 1.4, in_tree 1.14, required 1.18…) | placement/handlers/resource_provider.py:212 | resource_providers:list (A/S) | 200 | 400 |
| POST | /resource_providers | 1.0+ | placement/handlers/resource_provider.py:77 | resource_providers:create | 201 (≤ 1.19), 200 with body (≥ 1.20) | 400, 409 duplicate_name |
| GET | /resource_providers/{uuid} | 1.0+ | placement/handlers/resource_provider.py:184 | :show | 200 | 404 |
| PUT | /resource_providers/{uuid} | 1.0+ (re-parenting or un-parenting: 1.37) | placement/handlers/resource_provider.py:276 | :update | 200 | 400, 404, 409 duplicate_name |
| DELETE | /resource_providers/{uuid} | 1.0+ | placement/handlers/resource_provider.py:152 | :delete | 204 | 404, 409 resource_provider.inuse, 409 cannot_delete_parent |
| GET | /resource_providers/{uuid}/inventories | 1.0+ | placement/handlers/inventory.py:259 | inventories:list | 200 | 404 |
| POST | /resource_providers/{uuid}/inventories | 1.0+ | placement/handlers/inventory.py:180 | inventories:create | 201 | 400, 404, 409 concurrent_update |
| PUT | /resource_providers/{uuid}/inventories | 1.0+ | placement/handlers/inventory.py:312 | inventories:update | 200 | 400, 404, 409 concurrent_update / inventory.inuse |
| DELETE | /resource_providers/{uuid}/inventories | 1.5+ (405 below) | placement/handlers/inventory.py:380 | inventories:delete | 204 | 404, 409 concurrent_update / inventory.inuse |
| GET | /resource_providers/{uuid}/inventories/{rc} | 1.0+ | placement/handlers/inventory.py:282 | inventories:show | 200 | 404 |
| PUT | /resource_providers/{uuid}/inventories/{rc} | 1.0+ | placement/handlers/inventory.py:415 | inventories:update | 200 | 400, 404, 409 concurrent_update |
| DELETE | /resource_providers/{uuid}/inventories/{rc} | 1.0+ | placement/handlers/inventory.py:224 | inventories:delete | 204 | 404, 409 concurrent_update (including when the class is allocated) |
| GET | /resource_providers/{uuid}/usages | 1.0+ | placement/handlers/usage.py:40 | resource_providers:usages | 200 | 404 |
| GET | /resource_providers/{uuid}/aggregates | 1.1+ | placement/handlers/aggregate.py:84 | resource_providers:aggregates:list | 200 | 404 |
| PUT | /resource_providers/{uuid}/aggregates | 1.1+ (generation in the body: 1.19) | placement/handlers/aggregate.py:105 | :aggregates:update | 200 | 400, 404, 409 concurrent_update |
| GET | /resource_providers/{uuid}/allocations | 1.0+ | placement/handlers/allocation.py:291 | resource_providers:allocations:list | 200 | 404 |
| GET | /resource_providers/{uuid}/traits | 1.6+ | placement/handlers/trait.py:184 | resource_providers:traits:list | 200 | 404 |
| PUT | /resource_providers/{uuid}/traits | 1.6+ | placement/handlers/trait.py:219 | :traits:update | 200 | 400, 404, 409 concurrent_update |
| DELETE | /resource_providers/{uuid}/traits | 1.6+ | placement/handlers/trait.py:260 | :traits:delete | 204 | 404, 409 concurrent_update |
| GET | /resource_classes | 1.2+ | placement/handlers/resource_class.py:146 | resource_classes:list | 200 | — |
| POST | /resource_classes | 1.2+ | placement/handlers/resource_class.py:58 | :create | 201 | 400, 409 |
| GET | /resource_classes/{name} | 1.2+ | placement/handlers/resource_class.py:116 | :show | 200 | 404 |
| PUT | /resource_classes/{name} | 1.2–1.6: rename; 1.7+: idempotent create with no body | placement/handlers/resource_class.py:171, placement/handlers/resource_class.py:209 | :update | 200 / 201 or 204 | 400, 404, 409 |
| DELETE | /resource_classes/{name} | 1.2+ | placement/handlers/resource_class.py:90 | :delete | 204 | 400 (standard class), 404, 409 (in use) |
| GET | /traits | 1.6+ | placement/handlers/trait.py:152 | traits:list | 200 | 400 |
| GET | /traits/{name} | 1.6+ | placement/handlers/trait.py:108 | traits:show | 204 (no body) | 404 |
| PUT | /traits/{name} | 1.6+ | placement/handlers/trait.py:67 | traits:update | 201 if created, 204 if it existed | 400 (no CUSTOM_ prefix) |
| DELETE | /traits/{name} | 1.6+ | placement/handlers/trait.py:129 | traits:delete | 204 | 400 (standard trait), 404, 409 (in use) |
| GET | /allocations/{consumer_uuid} | 1.0+ | placement/handlers/allocation.py:262 | allocations:list | 200 | — (an unknown consumer yields 200 with an empty body) |
| PUT | /allocations/{consumer_uuid} | 1.0+ (6 variants: 1.8, 1.12, 1.28, 1.34, 1.38) | placement/handlers/allocation.py:501-537 | allocations:update | 204 | 400, 409 |
| DELETE | /allocations/{consumer_uuid} | 1.0+ | placement/handlers/allocation.py:607 | allocations:delete | 204 | 404 |
| POST | /allocations | 1.13+ | placement/handlers/allocation.py:543 | allocations:manage | 204 | 400, 409 |
| GET | /allocation_candidates | 1.10+ | placement/handlers/allocation_candidate.py:259 | allocation_candidates:list | 200 | 400 |
| GET | /usages | 1.9+ | placement/handlers/usage.py:86 | usages (A/S or project reader) | 200 | 400 |
| POST | /reshaper | 1.30+ | placement/handlers/reshaper.py:45 | reshaper:reshape (service only) | 204 | 400, 409 |
Resource providers: /resource_providers
Functional description
- Purpose: declare an entity that owns resources (host, storage pool, network card, GPU…) and optionally organise providers as a tree (parent → children). Example: a compute host with its network physical functions or its GPUs as children.
- Actor: a service (
nova-computeat startup and on each cycle of its resource tracker,neutron-serverfor QoS or bandwidth resources), or an administrator. - Business rules:
- **
nameanduuidare unique.** A duplicate yields 409placement.duplicate_name, with the conflicting field in the message (placement/handlers/resource_provider.py:104-128). Theuuidis generated when not supplied (placement/handlers/resource_provider.py:95-100). - **
parent_provider_uuid** (1.14+) must exist and differ from the created uuid (400). The new provider inherits the parent's root (placement/objects/resource_provider.py:690-724). - Changing the parent: going from "no parent" to "a parent" is always allowed. Changing the parent or removing it is only allowed from 1.37 (400 otherwise). Any change that would create a loop in the tree is refused (400). The whole subtree then gets the new root (
placement/objects/resource_provider.py:780-876). - Deletion is refused if the provider has children (409
cannot_delete_parent) or allocations (409resource_provider.inuse). Otherwise it also deletes its inventories, aggregate links and traits (placement/objects/resource_provider.py:735-778). - **
PUT /resource_providers/{uuid}does not check the generation**: onlynameand the parent can change (placement/objects/resource_provider.py:586-601).
- **
- **
GETfilters**:uuid,name,member_of(1.3;in:1.21; several values 1.24; negation!1.32),resources=VCPU:4,MEMORY_MB:2048(1.4: enough free capacity),in_tree(1.14),required(traits 1.18; forbidden!1.22;in:1.39) (placement/handlers/resource_provider.py:220-253). - Outcome: immediate effect, no status.
generationis 0 at creation.
Technical sheet
POST /resource_providers
OpenStack-API-Version: placement 1.39
X-Auth-Token: …
Content-Type: application/json
{"name": "compute-12.example.org", "uuid": "4e8e5957-…", "parent_provider_uuid": null}
200 OK
Location: /resource_providers/4e8e5957-…
{"uuid": "4e8e5957-…", "name": "compute-12.example.org", "generation": 0,
"parent_provider_uuid": null, "root_provider_uuid": "4e8e5957-…",
"links": [{"rel": "self", "href": "/resource_providers/4e8e5957-…"}, {"rel": "inventories", …}, …]}
Inventories: /resource_providers/{uuid}/inventories[/{resource_class}]
Functional description
- Purpose: declare how much of each resource class a provider has, and under which conditions it can be consumed.
- Actor: the service that owns the provider. For Nova,
nova-computeperiodically replaces the whole inventory withPUT …/inventories. - Business rules:
- Defaults:
reserved= 0,min_unit= 1,max_unit= MAX_INT,step_size= 1,allocation_ratio= 1.0 (placement/handlers/inventory.py:45-51). - Usable capacity =
(total − reserved) × allocation_ratio. A single allocation must satisfymin_unit ≤ x ≤ max_unitandx % step_size == 0(placement/objects/allocation.py:229-291). - **
reservedmust be lower thantotal** (400). Since 1.26,reservedmay equaltotal, which makes a provider unavailable without deleting its inventory (placement/handlers/inventory.py:152-176). - The resource class must exist, otherwise 400 (
placement/handlers/inventory.py:347-352). - **
PUT …/inventories** (full replacement) requiresresource_provider_generation(409 otherwise). It computes additions, updates and deletions in one transaction. It refuses to delete a class that has allocations (409placement.inventory.inuse) (placement/objects/resource_provider.py:210-260,placement/objects/resource_provider.py:61-93). - **It does accept reducing
totalbelow current usage**: only aWARNING Resource provider … is now over-capacity for …is logged (placement/objects/resource_provider.py:121-166,624-641). - **
POST** (adding one class) needs no generation. A class that already exists yields 409concurrent_update, through a uniqueness violation (placement/handlers/inventory.py:203-207). - **
DELETE …/inventories/{rc}** on an allocated class yields **409 with the codeplacement.concurrent_update** (placement/handlers/inventory.py:240-245). This is inconsistent:DELETE …/inventoriesdoes returnplacement.inventory.inuse.
- Defaults:
- Effects: every write increments the provider generation (
placement/objects/resource_provider.py:168-208,258).
Technical sheet
PUT /resource_providers/4e8e5957-…/inventories
{"resource_provider_generation": 3,
"inventories": {"VCPU": {"total": 64, "reserved": 4, "allocation_ratio": 4.0, "max_unit": 64},
"MEMORY_MB": {"total": 515000, "reserved": 8192},
"DISK_GB": {"total": 2000, "reserved": 0}}}
200 OK
{"resource_provider_generation": 4, "inventories": {"VCPU": {"total": 64, "reserved": 4, "min_unit": 1, "max_unit": 64, "step_size": 1, "allocation_ratio": 4.0}, …}}
Allocations: /allocations/{consumer_uuid}, POST /allocations, /resource_providers/{uuid}/allocations
Functional description
- Purpose: reserve quantities of resources on one or more providers for a consumer (Nova instance, migration, Neutron port…), atomically and without ever exceeding capacity.
- Actor: a service.
nova-conductorandnova-schedulerreserve for scheduling and migration;nova-computeand Neutron reserve too. - Preconditions: the providers exist (otherwise 400, not 404) and have an inventory of each requested class (otherwise 409).
- Business rules:
- **
PUTreplaces all of the consumer's allocations** with those of the body (placement/objects/allocation.py:544-556). **POST /allocations** (1.13+) does the same for several consumers at once, in a single transaction. This is the migration tool: it moves the instance's allocations to the migration consumer, then gives the target host to the instance (placement/handlers/allocation.py:543-603). - Capacity check, after deleting the consumer's old allocations and in the same transaction: for each (provider, class) pair,
used + requested ≤ (total − reserved) × allocation_ratio, and themin_unit,max_unitandstep_sizeconstraints are checked (placement/objects/allocation.py:108-295,372-410).- Exception: if the provider is already over capacity, an allocation that reduces the overage is accepted, with a
WARNING(placement/objects/allocation.py:272-284).
- Exception: if the provider is already over capacity, an allocation that reduces the overage is accepted, with a
- Consumer identity:
- 1.8+:
project_idanduser_idare required. Below that, placeholder IDs are used, which creates an "incomplete consumer" (placement/handlers/util.py:130-132). - 1.38+:
consumer_typeis required (for exampleINSTANCE,MIGRATION), and the type is created on the fly (placement/handlers/util.py:33-50). - If the project, user or type differ from the existing record, the record is updated (
placement/handlers/util.py:179-240).
- 1.8+:
- Consumer generation (1.28+):
consumer_generationis required. It isnullfor a new consumer and must equal the current generation otherwise. A mismatch yields **409placement.concurrent_update**, with no server-side retry (placement/handlers/util.py:139-163). It is incremented on every write (placement/objects/allocation.py:451-452). - **Empty
allocationsbody** (1.28+): all of the consumer's allocations are deleted (placement/handlers/allocation.py:430-441). A consumer with no allocation left is deleted (placement/objects/allocation.py:454-461,placement/objects/consumer.py:66-87). - Automatic retry: if a provider generation changed during the write (concurrent allocation on the same host), Placement re-reads the providers and retries up to
[placement]/allocation_conflict_retry_counttimes (10). Beyond that, 409placement.concurrent_update(placement/objects/allocation.py:544-596). On top of that, up to 5 retries on SQL deadlock (placement/objects/allocation.py:372). - **
mappings** (1.34+): the body may carry the mapping between request groups and providers. It is validated by the schema but not stored [inferred: no model persists it,placement/db/sqlalchemy/models.py]. - On failure, the consumers created during the request are deleted (
placement/handlers/allocation.py:376-389,466-476).
- **
- **
PUTflow** (entirely synchronous):- Validation of the schema (per microversion) and of the rights.
- Creation or lookup of the project, user, type and consumer, with a check of its generation.
- In one transaction: consumer update, deletion of its allocations, capacity check, insertion of the new rows, increment of the generation of each provider touched and of the consumer.
- 204 response.
- Outcome: the reservation is effective as soon as the response is sent. There is no status and no deferred processing.
- **
DELETE /allocations/{consumer}: deletes all of the consumer's allocations, then the consumer. It returns 404 when there was nothing. **It does not check the consumer generation (placement/handlers/allocation.py:607-631). - **
GET /allocations/{consumer}**:{"allocations": {rp_uuid: {"generation", "resources"}}}, plusproject_idanduser_id(1.12),consumer_generation(1.28) andconsumer_type(1.38). An unknown consumer yields 200 with an empty object, not 404.
Technical sheet
PUT /allocations/8d3b2c6e-… (consumer = instance)
OpenStack-API-Version: placement 1.39
{"allocations": {"4e8e5957-…": {"resources": {"VCPU": 4, "MEMORY_MB": 8192, "DISK_GB": 40}}},
"project_id": "c1f…", "user_id": "a9e…", "consumer_generation": null, "consumer_type": "INSTANCE"}
204 No Content
POST /allocations (atomic swap during a migration)
{"8d3b2c6e-…": {"allocations": {"<target host>": {"resources": {…}}}, "consumer_generation": 1, …},
"f00d…(migration)": {"allocations": {"<source host>": {"resources": {…}}}, "consumer_generation": null, …}}
Allocation candidates: GET /allocation_candidates (1.10+)
Functional description
- Purpose: answer the question "where can I place this request?". The service returns the combinations of providers that satisfy all the quantity, trait, aggregate and tree constraints, with a summary of their capacity. The client then chooses and reserves with
PUT /allocations. This call reserves nothing. - Actor:
nova-scheduler, once per scheduling request. - Parameters (schema per microversion,
placement/handlers/allocation_candidate.py:245-253; parsing inplacement/lib.py:55-460):resources=VCPU:4,MEMORY_MB:8192: the unnumbered group. Resources may come from several providers of the same tree, or from a sharing provider (traitMISC_SHARES_VIA_AGGREGATE,placement/objects/research_context.py:1194-1300).resources{N},required{N},member_of{N},in_tree{N}(1.25; string suffixes 1.33): granular groups. Each group must be satisfied by a single provider.group_policy=none|isolate: required as soon as there are several numbered groups, otherwise 400 (placement/handlers/allocation_candidate.py:274-281). Withisolate, the groups land on different providers (placement/objects/allocation_candidate.py:978-1021).required(required traits,!forbidden,in:"at least one" in 1.39),member_of(aggregates),in_tree(1.31),root_required(root traits, 1.35),same_subtree(groups within a single subtree, 1.36),limit(1.16).
- Algorithm, step by step:
- For each group, find the providers with enough free capacity for each class (
get_providers_with_resource,placement/objects/research_context.py:447-501; capacity clauseplacement/objects/research_context.py:435-446), then filter by traits and aggregates (placement/objects/research_context.py:1132-1192). While doing so, add up the total amount requested per class across all groups, and note the classes requested by more than one group (placement/objects/allocation_candidate.py:151-155,placement/objects/research_context.py:211-214). - Restrict to the trees that satisfy every group (
get_trees_matching_all,placement/objects/research_context.py:617-760), then applyroot_requiredandin_tree. - Pre-check each tree's overall capacity: for every class requested by several groups, drop the tree if its total free capacity is below the total requested (
_pre_check_overall_capacity,placement/objects/allocation_candidate.py:794-844, applied inplacement/objects/allocation_candidate.py:747-791). This step was added on this branch by the fix for bug 2160721 (see change history, change 10). It is a no-op when no class appears in several groups. - Generate the Cartesian product of the choices per group and per root, filtered by
group_policy,same_subtreeand total capacity (placement/objects/allocation_candidate.py:731-976). Roots are visiteddepth-firstorbreadth-first([placement]/allocation_candidates_generation_strategy). The number of candidates is capped by[placement]/max_allocation_candidates, unlimited by default. - Apply
limit, optionally with random sampling ([placement]/randomize_allocation_candidates,placement/objects/research_context.py:294-326), then buildprovider_summaries(placement/objects/allocation_candidate.py:495-588).
- For each group, find the providers with enough free capacity for each class (
- Errors: an unknown class or trait, invalid syntax, an orphan group (
required2withoutresources2) or a duplicated parameter yield 400. The codes areplacement.query.bad_value,placement.query.missing_valueandplacement.query.duplicate_key(placement/lib.py:153-232,420-455). - Outcome:
{"allocation_requests": [{"allocations": {rp: {"resources": …}}, "mappings": {suffix: [rp]}}], "provider_summaries": {rp: {"resources": {rc: {"capacity", "used"}}, "traits", "parent_provider_uuid", "root_provider_uuid"}}}. No candidate yields 200 with empty lists, not an error. - Limitations: the cost is proportional to the number of combinations. See point of attention 4 in the summary.
Traits: /traits, /resource_providers/{uuid}/traits (1.6+)
Functional description
- Purpose: describe qualitative capabilities (
HW_CPU_X86_AVX2,COMPUTE_STATUS_DISABLED,CUSTOM_GOLD…) and attach them to providers, to filter candidates. - Business rules:
- Standard traits come from the os-traits library (≥ 3.9.0 on this branch, see 06) and are copied into the database at startup of each API process (
placement/deploy.py:122-130,placement/objects/trait.py:125-147,265-299). They can be neither created nor deleted (400 on deletion). - Custom traits: the name must start with
CUSTOM_, be at most 255 characters and use[A-Z0-9_]; otherwise 400 (placement/handlers/trait.py:72-79).PUT /traits/{name}is idempotent: 201 when it creates the trait, 204 when it already existed. - Deleting a trait still attached to a provider: 409 (
placement/handlers/trait.py:129-145). - **
PUT /resource_providers/{uuid}/traits** replaces the provider's whole set of traits. It requiresresource_provider_generation(409 if it differs) and existing traits (400 otherwise) (placement/handlers/trait.py:219-253). It increments the generation. - **
GET /traits?name=startswith:CUSTOM_** or **in:A,B**, andassociated=true|false(placement/handlers/trait.py:152-180).
- Standard traits come from the os-traits library (≥ 3.9.0 on this branch, see 06) and are copied into the database at startup of each API process (
- Usage example: Nova sets
COMPUTE_STATUS_DISABLEDon a disabled host, and the scheduler excludes it withrequired=!COMPUTE_STATUS_DISABLED.
Resource classes: /resource_classes (1.2+)
Functional description
- Purpose: define resource units: standard classes (
VCPU,MEMORY_MB,DISK_GB,PCI_DEVICE,NET_BW_EGR_KILOBIT_PER_SEC…) from os-resource-classes, or customCUSTOM_*classes. - Business rules:
- Standard classes are copied into the database at startup and cannot be modified or deleted (400) (
placement/objects/resource_class.py:209-255,placement/handlers/resource_class.py:101-107,193-196). - Custom classes: the
CUSTOM_prefix is required by the schema (placement/schemas/resource_class.py:24). - Creation: a duplicate yields 409. Repeated failure to allocate an ID yields 409 "Max retries" (
placement/handlers/resource_class.py:70-80). - **Since 1.7,
PUT /resource_classes/{name}with no body creates the class idempotently** (201 or 204). Renaming (1.2–1.6) no longer exists. - Deleting a class used in an inventory: 409 (
placement/handlers/resource_class.py:105-107).
- Standard classes are copied into the database at startup and cannot be modified or deleted (400) (
Aggregates: /resource_providers/{uuid}/aggregates (1.1+)
- Purpose: attach a provider to logical groups identified by a UUID. Uses: Nova host aggregates, availability zones, and resource sharing (a storage pool shared with every host of an aggregate).
- Business rules:
PUTreplaces the list of aggregates.- Since 1.19, the body is
{"aggregates": [...], "resource_provider_generation": N}: a generation mismatch yields 409 and the generation is incremented. Before 1.19, the body is a plain list and there is no generation check (placement/handlers/aggregate.py:105-132). - Aggregates are created on the fly in the database (
placement_aggregates), with up to 10 retries on duplicates (placement/objects/resource_provider.py:309-410).
Usages: /resource_providers/{uuid}/usages, GET /usages (1.9+)
- **
/resource_providers/{uuid}/usages**: the sum of allocations per class on one provider, used for monitoring and capacity checks (placement/handlers/usage.py:40-82). - **
GET /usages?project_id=…[&user_id=…][&consumer_type=…]**: a project's total consumption, grouped by consumer type from 1.38 (placement/handlers/usage.py:86-143). It is the only route open to the project reader (placement/policies/usage.py:35). Nova uses it for quota counting when[quota]/count_usage_from_placementis enabled [inferred: a Nova-side setting].
Reshaper: POST /reshaper (1.30+)
- Purpose: reorganise in a single transaction the inventories and the allocations of several providers. The typical case: move a host's
VGPUinventory to new child providers, together with the allocations of the instances that use it, without ever going through an inconsistent state. - Actor: the service only (
role:service), in practicenova-computeduring an upgrade that changes the resource model (placement/policies/reshaper.py:23). - Business rules:
- Every provider cited must exist: 400
placement.resource_provider.not_found. - Every generation must match: 409
concurrent_update. - Allocations follow the same rules as
POST /allocations: consumers, generations and capacity. - Everything is written in a single transaction (
placement/handlers/reshaper.py:45-149,placement/objects/reshaper.py).
- Every provider cited must exist: 400
Root: GET /
Version discovery ({"versions": [{"id": "v1.0", "max_version": "1.39", "min_version": "1.0", "status": "CURRENT", "links": …}]}) without authentication, because / is excluded from auth_token (placement/auth.py:101-105, placement/handlers/root.py:24-48).
Lifecycle and generation
Placement has no asynchronous status: a resource exists or not as soon as the response is sent. The "lifecycle" that matters to an operator is that of a provider's generation, which serves as an optimistic lock.
stateDiagram-v2
[*] --> G0: POST /resource_providers (generation = 0)
G0 --> Gn: any write of inventories, allocations, traits or aggregates (≥ 1.19)
Gn --> Gn1: next write, generation + 1 (UPDATE … WHERE generation = n)
Gn --> Conflict: write with a stale generation
Conflict --> Gn: the client re-reads (GET), then retries
Gn --> [*]: DELETE /resource_providers/{uuid} (no allocation, no child)The consumer follows the same principle since 1.28: consumer_generation is null at creation, then incremented on each PUT or POST of allocations. The consumer is deleted as soon as it has no allocation left.
Source: reverse-spec-placement/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.