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

Reverse-spec · Services

placement — REST API

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 a routes.Mapper. An unknown path yields 404; an undeclared method yields 405 with an Allow header (placement/handler.py:136-174).
  • Middleware order, from the outside in (placement/deploy.py:62-82):
    1. osprofiler (optional);
    2. HTTPProxyToWSGI;
    3. RequestLog (access log);
    4. CORS (optional);
    5. **keystonemiddleware auth_token**, except for / (placement/auth.py:93-116);
    6. PlacementKeystoneContext, which builds the request context and returns 401 without X_USER_ID (placement/auth.py:75-90);
    7. FaultWrapper: any non-HTTP exception becomes a 500 (placement/fault_wrap.py);
    8. **MicroversionMiddleware** (microversion-parse);
    9. 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, with min_version/max_version in 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 …/inventories answers 405 in that case (placement/microversion.py:130-170).
  • Header checks:
    • a non-empty body without Content-Type yields 400 (placement/handler.py:209-218);
    • a Content-Type other than application/json yields 415 (util.require_content);
    • an incompatible Accept yields 406 (util.check_accept).
  • 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-Modified and Cache-Control: no-cache.
  • Authorisation (placement/policies/*.py): almost everything is rule:admin_or_service_api (role:admin or role:service), with the project scope. The exceptions are /reshaper (role:service only) and GET /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 **409 placement.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.

MethodPathMicroversionHandlerPolicySuccessMain errors
GET/all, no authenticationplacement/handlers/root.py:24—200—
GET/resource_providers1.0+ (filters: member_of 1.3, resources 1.4, in_tree 1.14, required 1.18…)placement/handlers/resource_provider.py:212resource_providers:list (A/S)200400
POST/resource_providers1.0+placement/handlers/resource_provider.py:77resource_providers:create201 (≤ 1.19), 200 with body (≥ 1.20)400, 409 duplicate_name
GET/resource_providers/{uuid}1.0+placement/handlers/resource_provider.py:184:show200404
PUT/resource_providers/{uuid}1.0+ (re-parenting or un-parenting: 1.37)placement/handlers/resource_provider.py:276:update200400, 404, 409 duplicate_name
DELETE/resource_providers/{uuid}1.0+placement/handlers/resource_provider.py:152:delete204404, 409 resource_provider.inuse, 409 cannot_delete_parent
GET/resource_providers/{uuid}/inventories1.0+placement/handlers/inventory.py:259inventories:list200404
POST/resource_providers/{uuid}/inventories1.0+placement/handlers/inventory.py:180inventories:create201400, 404, 409 concurrent_update
PUT/resource_providers/{uuid}/inventories1.0+placement/handlers/inventory.py:312inventories:update200400, 404, 409 concurrent_update / inventory.inuse
DELETE/resource_providers/{uuid}/inventories1.5+ (405 below)placement/handlers/inventory.py:380inventories:delete204404, 409 concurrent_update / inventory.inuse
GET/resource_providers/{uuid}/inventories/{rc}1.0+placement/handlers/inventory.py:282inventories:show200404
PUT/resource_providers/{uuid}/inventories/{rc}1.0+placement/handlers/inventory.py:415inventories:update200400, 404, 409 concurrent_update
DELETE/resource_providers/{uuid}/inventories/{rc}1.0+placement/handlers/inventory.py:224inventories:delete204404, 409 concurrent_update (including when the class is allocated)
GET/resource_providers/{uuid}/usages1.0+placement/handlers/usage.py:40resource_providers:usages200404
GET/resource_providers/{uuid}/aggregates1.1+placement/handlers/aggregate.py:84resource_providers:aggregates:list200404
PUT/resource_providers/{uuid}/aggregates1.1+ (generation in the body: 1.19)placement/handlers/aggregate.py:105:aggregates:update200400, 404, 409 concurrent_update
GET/resource_providers/{uuid}/allocations1.0+placement/handlers/allocation.py:291resource_providers:allocations:list200404
GET/resource_providers/{uuid}/traits1.6+placement/handlers/trait.py:184resource_providers:traits:list200404
PUT/resource_providers/{uuid}/traits1.6+placement/handlers/trait.py:219:traits:update200400, 404, 409 concurrent_update
DELETE/resource_providers/{uuid}/traits1.6+placement/handlers/trait.py:260:traits:delete204404, 409 concurrent_update
GET/resource_classes1.2+placement/handlers/resource_class.py:146resource_classes:list200—
POST/resource_classes1.2+placement/handlers/resource_class.py:58:create201400, 409
GET/resource_classes/{name}1.2+placement/handlers/resource_class.py:116:show200404
PUT/resource_classes/{name}1.2–1.6: rename; 1.7+: idempotent create with no bodyplacement/handlers/resource_class.py:171, placement/handlers/resource_class.py:209:update200 / 201 or 204400, 404, 409
DELETE/resource_classes/{name}1.2+placement/handlers/resource_class.py:90:delete204400 (standard class), 404, 409 (in use)
GET/traits1.6+placement/handlers/trait.py:152traits:list200400
GET/traits/{name}1.6+placement/handlers/trait.py:108traits:show204 (no body)404
PUT/traits/{name}1.6+placement/handlers/trait.py:67traits:update201 if created, 204 if it existed400 (no CUSTOM_ prefix)
DELETE/traits/{name}1.6+placement/handlers/trait.py:129traits:delete204400 (standard trait), 404, 409 (in use)
GET/allocations/{consumer_uuid}1.0+placement/handlers/allocation.py:262allocations:list200— (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-537allocations:update204400, 409
DELETE/allocations/{consumer_uuid}1.0+placement/handlers/allocation.py:607allocations:delete204404
POST/allocations1.13+placement/handlers/allocation.py:543allocations:manage204400, 409
GET/allocation_candidates1.10+placement/handlers/allocation_candidate.py:259allocation_candidates:list200400
GET/usages1.9+placement/handlers/usage.py:86usages (A/S or project reader)200400
POST/reshaper1.30+placement/handlers/reshaper.py:45reshaper:reshape (service only)204400, 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-compute at startup and on each cycle of its resource tracker, neutron-server for QoS or bandwidth resources), or an administrator.
  • Business rules:
    • **name and uuid are unique.** A duplicate yields 409 placement.duplicate_name, with the conflicting field in the message (placement/handlers/resource_provider.py:104-128). The uuid is 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 (409 resource_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**: only name and the parent can change (placement/objects/resource_provider.py:586-601).
  • **GET filters**: 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. generation is 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-compute periodically replaces the whole inventory with PUT …/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 satisfy min_unit ≤ x ≤ max_unit and x % step_size == 0 (placement/objects/allocation.py:229-291).
    • **reserved must be lower than total** (400). Since 1.26, reserved may equal total, 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) requires resource_provider_generation (409 otherwise). It computes additions, updates and deletions in one transaction. It refuses to delete a class that has allocations (409 placement.inventory.inuse) (placement/objects/resource_provider.py:210-260, placement/objects/resource_provider.py:61-93).
    • **It does accept reducing total below current usage**: only a WARNING 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 409 concurrent_update, through a uniqueness violation (placement/handlers/inventory.py:203-207).
    • **DELETE …/inventories/{rc}** on an allocated class yields **409 with the code placement.concurrent_update** (placement/handlers/inventory.py:240-245). This is inconsistent: DELETE …/inventories does return placement.inventory.inuse.
  • 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-conductor and nova-scheduler reserve for scheduling and migration; nova-compute and Neutron reserve too.
  • Preconditions: the providers exist (otherwise 400, not 404) and have an inventory of each requested class (otherwise 409).
  • Business rules:
    1. **PUT replaces 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).
    2. 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 the min_unit, max_unit and step_size constraints 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).
    3. Consumer identity:
      • 1.8+: project_id and user_id are required. Below that, placeholder IDs are used, which creates an "incomplete consumer" (placement/handlers/util.py:130-132).
      • 1.38+: consumer_type is required (for example INSTANCE, 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).
    4. Consumer generation (1.28+): consumer_generation is required. It is null for a new consumer and must equal the current generation otherwise. A mismatch yields **409 placement.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).
    5. **Empty allocations body** (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).
    6. 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_count times (10). Beyond that, 409 placement.concurrent_update (placement/objects/allocation.py:544-596). On top of that, up to 5 retries on SQL deadlock (placement/objects/allocation.py:372).
    7. **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].
    8. On failure, the consumers created during the request are deleted (placement/handlers/allocation.py:376-389,466-476).
  • **PUT flow** (entirely synchronous):
    1. Validation of the schema (per microversion) and of the rights.
    2. Creation or lookup of the project, user, type and consumer, with a check of its generation.
    3. 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.
    4. 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"}}}, plus project_id and user_id (1.12), consumer_generation (1.28) and consumer_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 in placement/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 (trait MISC_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). With isolate, 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:
    1. 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 clause placement/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).
    2. Restrict to the trees that satisfy every group (get_trees_matching_all, placement/objects/research_context.py:617-760), then apply root_required and in_tree.
    3. 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 in placement/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.
    4. Generate the Cartesian product of the choices per group and per root, filtered by group_policy, same_subtree and total capacity (placement/objects/allocation_candidate.py:731-976). Roots are visited depth-first or breadth-first ([placement]/allocation_candidates_generation_strategy). The number of candidates is capped by [placement]/max_allocation_candidates, unlimited by default.
    5. Apply limit, optionally with random sampling ([placement]/randomize_allocation_candidates, placement/objects/research_context.py:294-326), then build provider_summaries (placement/objects/allocation_candidate.py:495-588).
  • Errors: an unknown class or trait, invalid syntax, an orphan group (required2 without resources2) or a duplicated parameter yield 400. The codes are placement.query.bad_value, placement.query.missing_value and placement.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 requires resource_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**, and associated=true|false (placement/handlers/trait.py:152-180).
  • Usage example: Nova sets COMPUTE_STATUS_DISABLED on a disabled host, and the scheduler excludes it with required=!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 custom CUSTOM_* 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).

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:
    • PUT replaces 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_placement is 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 VGPU inventory 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 practice nova-compute during 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).

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.