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

Reverse-spec · Services

placement — Interactions

Interactions with other components — placement — branch stable/2026.2

Branch stable/2026.2 @ f4a89d3803cb — repository https://opendev.org/openstack/placement — analysed on 2026-10-28

Placement exchanges with only two systems:

  • its HTTP clients (Nova, Neutron, Cyborg, administrators), which call it;
  • its SQL database.

Keystone token validation is done by the auth_token middleware. There is no RabbitMQ, no notification, no Redis or memcached, and no outbound REST call in the code:

  • oslo_messaging, redis, pymemcache and keystoneauth1 are not imported outside tests;
  • requests only appears in placement/direct.py, an in-process client used by tests and by tools that embed Placement.

Inbound HTTP (REST)

ItemValueSource
TransportHTTP/1.1 through an external WSGI server (uwsgi or mod_wsgi). Port 8778 is the usual one [inferred: devstack and deployment convention], often behind the /placement prefixplacement/wsgi/api.py
AuthenticationX-Auth-Token, validated by keystonemiddleware, except GET /placement/auth.py:93-116
VersionOpenStack-API-Version: placement 1.x in the request and the response; the response also carries Vary: OpenStack-API-Version [inferred: microversion-parse behaviour]microversion-parse, placement/deploy.py:58-60
CorrelationX-Openstack-Request-Id, generated by oslo_middleware.request_id and returned in errors (request_id); an ID supplied by the client is reused [inferred]placement/util.py (json_error_formatter)
Contentapplication/json, enforced on writes (415 otherwise) and on reads through Accept (406 otherwise)placement/util.py
CachingLast-Modified and Cache-Control: no-cache from 1.15handlers
Access logone line per request: method, path, status, size, duration, microversionplacement/requestlog.py

Sample frame [reconstructed]: an allocation made by Nova after scheduling.

PUT /placement/allocations/8d3b2c6e-0f7e-4b5c-9a3e-2f1c6c1a7b10 HTTP/1.1
Host: controller:8778
X-Auth-Token: gAAAAAB…
OpenStack-API-Version: placement 1.39
Content-Type: application/json
X-Openstack-Request-Id: req-2c9…

{"allocations": {"4e8e5957-…": {"resources": {"VCPU": 2, "MEMORY_MB": 4096, "DISK_GB": 20}}},
 "project_id": "c1f…", "user_id": "a9e…", "consumer_generation": null, "consumer_type": "INSTANCE"}

HTTP/1.1 204 No Content
OpenStack-API-Version: placement 1.39
Vary: OpenStack-API-Version
X-Openstack-Request-Id: req-2c9…

Keystone (indirect)

keystonemiddleware.auth_token validates each token against Keystone, with GET /v3/auth/tokens using Placement's service token, then caches it. The cache is in memory by default, or in memcached when [keystone_authtoken]/memcached_servers is set. All of this is configured in the [keystone_authtoken] section of placement.conf. It is the only outbound traffic of the process. It does not appear in Placement's code, only in the middleware wiring (placement/auth.py:93-116).

The token's scope (project or system) is checked against each policy's scope_types. On this branch, oslo.policy ≥ 6.0.0 is required (requirements.txt:19), and that version no longer lets an operator disable scope checks (see change history, change 6).

Database (SQLAlchemy, oslo.db)

  • Connection: [placement_database]/connection, for example mysql+pymysql://placement:…@db/placement. Also tunable: slave_connection (read-only), the pool (max_pool_size, max_overflow, pool_timeout), connection retries (max_retries = 10, retry_interval = 10 s) and mysql_sql_mode=TRADITIONAL (placement/conf/database.py:59-109).
  • Transactions: enginefacade.transaction_context(), through the placement_context_manager.reader and .writer decorators (placement/db_api.py:24-40). Nested transactions join the enclosing one. reader.independent opens a separate transaction, used to re-read providers during allocation retries (placement/objects/allocation.py:579-582).
  • Schema: Alembic migrations in placement/db/sqlalchemy/alembic/versions/ (5 files), applied by placement-manage db sync. There are two online data migrations: set_root_provider_ids and create_incomplete_consumers (placement/cmd/manage.py:31-34). No migration was added on this branch since 15.0.0 (see 06).

Tables (placement/db/sqlalchemy/models.py)

TableRoleNotable keys and constraintsWritten byRead by
resource_providersproviders, tree (parent_provider_id, root_provider_id), **generation**unique uuid and name (placement/db/sqlalchemy/models.py:50-73)/resource_providers, and every write related to the provider (generation)almost everything
inventoriescapacity per (provider, class): total, reserved, min_unit, max_unit, step_size, allocation_ratiounique (provider, class) (placement/db/sqlalchemy/models.py:76-101)/inventories, reshaperallocations, candidates, usages
allocationsreservations: (provider, class, consumer_id = UUID, used)indexes (provider, class, used) and consumer_id (placement/db/sqlalchemy/models.py:104-126)/allocations, reshapercandidates (capacity), usages
consumersconsumer: uuid, project_id, user_id, **generation**, consumer_type_idunique uuid (placement/db/sqlalchemy/models.py:212-230)/allocations/allocations, /usages
projects, usersmapping between the Keystone ID (external_id) and the internal IDunique external_id (placement/db/sqlalchemy/models.py:182-209)created on the fly by /allocations/usages
consumer_typesINSTANCE, MIGRATION…unique name (placement/db/sqlalchemy/models.py:233-239)created on the fly (1.38+)/usages
resource_classescustom classes, and standard ones copied from os-resource-classesunique name (placement/db/sqlalchemy/models.py:38-47)/resource_classes, startupeverywhere, through the per-request cache
traits, resource_provider_traitstraits and their association with providersunique traits.name (placement/db/sqlalchemy/models.py:153-179)/traits, startuptrait filters
placement_aggregates, resource_provider_aggregatesaggregates and their associationunique uuid (placement/db/sqlalchemy/models.py:129-150)/aggregatesmember_of filters, sharing providers

Optimistic locking: the generation [reconstructed from the ORM]

Every write that touches a provider ends with a "compare-and-swap" on its generation (placement/objects/resource_provider.py:669-688):

UPDATE resource_providers SET generation = :n_plus_1
 WHERE id = :rp_id AND generation = :n;
-- 0 rows modified → ResourceProviderConcurrentUpdateDetected → rollback of the whole transaction

The consumer follows the same model (placement/objects/consumer.py:221-240). No SELECT … FOR UPDATE is used: consistency rests entirely on these conditional updates and on uniqueness constraints.

Writing allocations [reconstructed] (placement/objects/allocation.py:372-461)

-- in one transaction (automatic retry on deadlock, up to 5 times)
SELECT resource_provider_id, resource_class_id, SUM(used) FROM allocations
 WHERE resource_provider_id IN (:rps) AND resource_class_id IN (:rcs) GROUP BY 1,2;   -- usage before the operation
DELETE FROM allocations WHERE consumer_id = :consumer_uuid;                          -- full replacement
SELECT rp.id, rp.uuid, rp.generation, inv.resource_class_id, inv.total, inv.reserved,
       inv.allocation_ratio, inv.min_unit, inv.max_unit, inv.step_size, usage.used
  FROM resource_providers rp
  JOIN inventories inv ON rp.id = inv.resource_provider_id AND inv.resource_class_id IN (:rcs)
  LEFT JOIN (SELECT resource_provider_id, resource_class_id, SUM(used) AS used FROM allocations
              WHERE resource_provider_id IN (:rps) AND resource_class_id IN (:rcs) GROUP BY 1,2) usage
         ON usage.resource_provider_id = inv.resource_provider_id
        AND usage.resource_class_id = inv.resource_class_id
 WHERE rp.id IN (:rps);                                                             -- capacity check
INSERT INTO allocations (resource_provider_id, resource_class_id, consumer_id, used) VALUES (…);
UPDATE resource_providers SET generation = generation + 1 WHERE id = :rp AND generation = :g;   -- for each provider
UPDATE consumers SET generation = generation + 1 WHERE id = :c AND generation = :cg;           -- for each consumer
DELETE FROM consumers WHERE uuid IN (:consumers_without_allocations) …;            -- cleanup
COMMIT;

On a provider generation conflict, replace_all re-reads the providers in an independent transaction, then replays _set_allocations, up to [placement]/allocation_conflict_retry_count times (placement/objects/allocation.py:544-596). When the write itself runs inside the handler's enclosing transaction (placement/handlers/allocation.py:454-461), the retry happens within that same transaction [inferred, not verified at run time].

Capacity search for candidates [reconstructed] (placement/objects/research_context.py:421-501)

For each requested class:

SELECT rp.id, rp.root_provider_id FROM resource_providers rp
  JOIN inventories inv ON inv.resource_provider_id = rp.id AND inv.resource_class_id = :rc
  LEFT JOIN (SELECT resource_provider_id, SUM(used) AS used FROM allocations
              WHERE resource_class_id = :rc GROUP BY resource_provider_id) u
         ON u.resource_provider_id = rp.id
 WHERE COALESCE(u.used, 0) + :amount <= (inv.total - inv.reserved) * inv.allocation_ratio
   AND inv.min_unit <= :amount AND inv.max_unit >= :amount AND MOD(:amount, inv.step_size) = 0;

These queries are followed by trait filters (resource_provider_traits) and aggregate filters (resource_provider_aggregates), then by the computation of usages per tree (get_usages_by_provider_trees, placement/objects/research_context.py:1319). The per-tree overall-capacity pre-check added by the fix for bug 2160721 works in memory on those usages; it adds no SQL query [inferred from placement/objects/allocation_candidate.py:794-844].

Cost

  • One allocation: 4 to 6 queries and one transaction, whatever the number of providers.
  • **GET /allocation_candidates: one query per class and per group, plus the filters, plus the **in-memory Cartesian product. This is the hot spot of the service.
  • Per-request cache: ResourceClassCache and ConsumerTypeCache live in each request's context (placement/context.py:26-27). They reload the whole resource_classes table on first use, then on each unknown name (placement/attribute_cache.py). The cost is small but repeated on every request: there is no cache shared between requests.

What does not exist

Kind of exchangePresent?Note
RabbitMQ / RPC / notificationsnoclients must poll the API; no event is published when a capacity changes
Redis / memcached (application code)noonly keystonemiddleware may use memcached for its token cache
Outbound RESTnoPlacement knows neither Nova nor Neutron
Files, external processesnoapart from configuration and logs

Source: reverse-spec-placement/03-interactions_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.