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

Reverse-spec · Services

skyline-apiserver — Interactions

Interactions — skyline-apiserver — branch stable/2026.2

Branch stable/2026.2 @ 4af72bfd4577 — repository https://opendev.org/openstack/skyline-apiserver — analysed on 2026-10-28

skyline-apiserver has no message bus and no cache server. It talks to Keystone (a lot), to Nova, Cinder, Glance, Neutron and Prometheus over HTTP, and to its own small SQL database. The sequences below were recorded by running the real application against a fake OpenStack (_rewrite/tools/outbound_capture.py, results in _rewrite/outbound-calls_stable-2026.2.md): they are exact in order, method, path, query and headers; response bodies of real services are [inferred] from the client libraries.

1. Keystone sessions and tokens

Three kinds of credentials are used. Which one a call uses matters for the rewrite: it decides whose permissions apply.

SessionHow it is builtUsed forDefined at
System sessionpassword of openstack.system_user_name (default skyline) in system_user_domain, scoped to project system_project (default service) in system_project_domain, against openstack.keystone_url; one per worker process, cached in a global and re-authenticated by keystoneauth when the token nears expiry (reauthenticate=True); each request wraps it in a new Session carrying the client addresscatalog reading (every identity endpoint lookup), validation of user tokens, users and projects lookups, domains, regions, Neutron extensions, recycle bin, compute services, Glance images, Cinder lookups by IDskyline_apiserver/client/utils.py:58
User sessionthe user's project-scoped token, re-scoped to profile.project.id with the token method; get_auth_ref forces a POST /v3/auth/tokens every time it is builtthe user's own lists (servers, volumes, snapshots, ports, networks), project names with all_projects, logout, policy credentialsskyline_apiserver/client/utils.py:36
Unscoped / login sessionspassword, TOTP + receipt, or token (WebSSO, re-scope)login stepsskyline_apiserver/api/v1/login.py:247, skyline_apiserver/api/v1/login.py:202, skyline_apiserver/client/openstack/system.py:30

Common to all sessions: TLS CA = default.cafile (empty means the system store), timeout 30 s (skyline_apiserver/types/constants.py:43), no retry configured in Skyline (keystoneauth defaults: no retry on HTTP errors, connection retries 0) [inferred], endpoint interface openstack.interface_type (default public, skyline_apiserver/config/openstack.py:83).

Endpoint discovery. Every service endpoint is taken from the system session's catalog filtered by region and interface (skyline_apiserver/client/utils.py:111), even when the call is then made with the user session. An empty answer raises ValueError("Endpoint not found"). Note that keystone_url is only used for the system password authentication; all other identity calls go to the identity endpoint of the catalog for the session's region.

Version discovery. Each keystoneclient.Client(...) creation runs a GET <identity endpoint> (version document) before the real call (skyline_apiserver/client/utils.py:131); openstacksdk runs a GET <image endpoint> without token before listing images (skyline_apiserver/client/utils.py:147). Nova, Cinder and Neutron clients are given endpoint_override and do no discovery.

Request headers (recorded):

HeaderValueSent to
X-Auth-Tokentoken of the session (system, user or login)all except unauthenticated discovery and password/TOTP authentication
X-Subject-Tokenthe user token being validatedGET /v3/auth/tokens
X-OpenStack-Request-IDthe inbound X-Openstack-Request-Id, empty string when the client sent noneNova, Cinder, Glance, Neutron (via the clients' global_request_id)
X-OpenStack-Nova-API-Version + OpenStack-API-Version2.79 / compute 2.79Nova (skyline_apiserver/types/constants.py:20)
OpenStack-API-Versionvolume 3.59Cinder (skyline_apiserver/types/constants.py:21)
Openstack-Auth-Receiptreceipt from the first login stepKeystone, TOTP step
Forwardedfor=<client address>;by=<client library> (keystoneauth original_ip)all OpenStack calls; the address is secure_proxy_addr_header if configured, else the socket peer (skyline_apiserver/api/deps.py:28)

The user-facing Keystone requests carry no OpenStack-API-Version; KEYSTONE_API_VERSION = "3.13" (skyline_apiserver/types/constants.py:19) only selects the client class.

Sample frames (recorded)

System password authentication (first call of a worker):

POST /identity/v3/auth/tokens HTTP/1.1
Content-Type: application/json
Forwarded: for=10.0.0.7;by=keystoneauth1/5.17.0 python-requests/2.34.2 CPython/3.13.0

{"auth": {"identity": {"methods": ["password"], "password": {"user": {"name": "skyline", "domain": {"name": "Default"}, "password": "***"}}},
          "scope": {"project": {"name": "service", "domain": {"name": "Default"}}}}}

Validation of the user's token on every authenticated request (skyline_apiserver/core/security.py:66):

GET /identity/v3/auth/tokens HTTP/1.1
X-Auth-Token: <system token>
X-Subject-Token: <user token from the JWT or X-Auth-Token>

Re-scoping (user session, switch_project, login):

POST /identity/v3/auth/tokens HTTP/1.1
Content-Type: application/json

{"auth": {"identity": {"methods": ["token"], "token": {"id": "<user token>"}}, "scope": {"project": {"id": "<project id>"}}}}

TOTP second step (skyline_apiserver/api/v1/login.py:215):

POST /identity/v3/auth/tokens HTTP/1.1
Openstack-Auth-Receipt: <receipt>

{"auth": {"identity": {"methods": ["totp"], "totp": {"user": {"name": "mfa", "domain": {"name": "Default"}, "passcode": "123456"}}}}}

The first step answered 401 with header Openstack-Auth-Receipt and body {"receipt": {...}, "required_auth_methods": [["password", "totp"]]}; keystoneauth raises MissingAuthMethods from it (skyline_apiserver/api/v1/login.py:287).

2. Outbound REST per endpoint

Paths are relative to the catalog endpoint of each service. Session: S = system, U = user (re-scoped), L = login (unscoped or being scoped). Every authenticated endpoint starts with the middleware's validation pair GET /identity/v3 (S) + GET /v3/auth/tokens (S), not repeated below. Counts are the totals of the recorded scenarios, validation pair included; each keystoneclient call is preceded by a GET /identity/v3 discovery (not always repeated below).

Inbound endpointOutbound calls in order (session)CountDefined at
POST /loginPOST /v3/auth/tokens password, unscoped (L) → GET /identity/v3 (L) → GET /v3/auth/projects (L) → validation pair (S) → GET /identity/v3, GET /v3/users/{uid} (S) → POST /v3/auth/tokens token→project (L) → validation pair (S) → POST /v3/auth/tokens token→project (U, region list) → Neutron GET /v2.0/extensions (S) → GET /identity/v3, GET /v3/users/{uid}/projects (S) → GET /identity/v3, GET /v3/users/{uid} (S)16skyline_apiserver/api/v1/login.py:395
POST /login/totpPOST /v3/auth/tokens TOTP + receipt (L), then as /login from the second call17skyline_apiserver/api/v1/login.py:448
POST /webssoPOST /v3/auth/tokens token method, no scope, with the posted token (L) → GET /identity/v3, GET /v3/auth/projects (L) and the rest as /login16skyline_apiserver/api/v1/login.py:577
GET /profilePOST /v3/auth/tokens (U, region list) → Neutron GET /v2.0/extensions (S) → GET /v3/users/{uid}/projects (S) → GET /v3/users/{uid} (S) (+ discovery each)8skyline_apiserver/api/v1/login.py:638
POST /logoutvalidation pair again (S, re-parse of the JWT) → POST /v3/auth/tokens (U) → GET /identity/v3 (U) → DELETE /v3/auth/tokens with X-Subject-Token = user token (U)7skyline_apiserver/api/v1/login.py:664
POST /switch_project/{id}POST /v3/auth/tokens token→{id} (L) → validation pair (S) → /profile enrichment11skyline_apiserver/api/v1/login.py:699
POST /switch_region/{r}POST /v3/auth/tokens (U, region list) → validation through the identity endpoint of {r} (S) → /profile enrichment11skyline_apiserver/api/v1/login.py:756
GET /extension/serversPOST /v3/auth/tokens (U) → [all_projects + project_name: GET /v3/projects?name= (S)] → Nova GET /servers/detail?… (U) → Glance GET / (none) + GET /v2/images?id=in:<≤100 ids> (S) per batch → Cinder GET /volumes/detail?all_tenants=True&id=[<≤100 ids>] (S) per batch → [all_projects: GET /v3/projects (U)]7–10skyline_apiserver/api/v1/extension.py:62
GET /extension/recycle_serversPOST /v3/auth/tokens (U, unused) → Nova GET /servers/detail?all_tenants=True&deleted=True&project_id=<current>&status=soft_deleted (S) → Glance and Cinder as servers (S) → [all_projects: GET /v3/projects (U)]4+skyline_apiserver/api/v1/extension.py:284
GET /extension/volumesPOST /v3/auth/tokens (U) → Cinder GET /volumes/detail?with_count=True&sort=… (U; S if uuid or reader with all_projects) → per attached server: Nova GET /servers/detail?uuid=<id> and GET /servers/detail?uuid=<id>&status=soft_deleted&deleted=True (U)4 + 2·serversskyline_apiserver/api/v1/extension.py:501
GET /extension/volume_snapshotsPOST /v3/auth/tokens (U) → [uuid, own project: Cinder GET /snapshots/{uuid} (U)] → Cinder GET /snapshots/detail?with_count=True… (U, S with uuid) → [all_projects: GET /v3/projects (U)] → Cinder GET /volumes/detail?all_tenants=True&id=[…] (S) per batch → Cinder GET /volumes/detail?all_tenants=True&snapshot_id=[…] (S) per batch5–8skyline_apiserver/api/v1/extension.py:677
GET /extension/portsPOST /v3/auth/tokens (U) → [network_name: Neutron GET /v2.0/networks?name= (U)] → Neutron GET /v2.0/ports?… (U) → GET /v2.0/networks?shared=True (U) → GET /v2.0/networks?project_id=…&id=… per batch of 100 (U) → per server port: Nova GET /servers/detail?uuid=<id> (U)5 + serversskyline_apiserver/api/v1/extension.py:867
GET /extension/compute-servicesNova GET /os-services?binary=&host= (S)1skyline_apiserver/api/v1/extension.py:1055
GET /contrib/keystone_endpoints[first call of the worker: system password auth] → per region: Neutron GET /v2.0/extensions (S)regionsskyline_apiserver/api/v1/contrib.py:44
GET /contrib/domainsGET /identity/v3 + GET /v3/domains?enabled=True (S)2skyline_apiserver/client/openstack/system.py:109
GET /contrib/regionsnone beyond the system authentication (catalog of the system token)0–1skyline_apiserver/client/openstack/system.py:123
GET /policies, POST /policies/checkPOST /v3/auth/tokens token→project (U) → POST /v3/auth/tokens token→{"system": {"all": true}} (U)2skyline_apiserver/client/utils.py:88
GET /query, GET /query_rangePrometheus GET <prometheus_endpoint>/api/v1/query[_range]?…, optional basic auth, TLS not verified, httpx default timeout (5 s) [inferred], new connection per call1skyline_apiserver/utils/httpclient.py:24
GET /config, GET /sso, settingsnone0—

Calls by target service

TargetMethod + pathQuery / bodySessionTimeout / retryErrors handledDefined at
KeystonePOST /v3/auth/tokenspassword, TOTP + receipt, token; scope project (ID or name), system or noneL, U, S30 s / noneMissingAuthMethods, Unauthorized, other → 401skyline_apiserver/api/v1/login.py:277
KeystoneGET /v3/auth/tokensX-Subject-TokenS30 s→ 401skyline_apiserver/client/openstack/keystone.py:87
KeystoneDELETE /v3/auth/tokensX-Subject-TokenU30 sswallowedskyline_apiserver/client/openstack/keystone.py:57
KeystoneGET /v3/auth/projects—L30 s→ 401skyline_apiserver/api/v1/login.py:296
KeystoneGET /v3/users/{id}—S30 s→ 401skyline_apiserver/client/openstack/keystone.py:92
KeystoneGET /v3/users/{id}/projects—S30 s→ 401skyline_apiserver/client/openstack/system.py:94
KeystoneGET /v3/projectsname=; user → /v3/users/{id}/projects when not all_projectsS or U30 s401 / 500skyline_apiserver/client/openstack/keystone.py:28
KeystoneGET /v3/domainsenabled=TrueS30 s→ 500skyline_apiserver/client/openstack/system.py:109
NovaGET /servers/detailname, status, host, flavor, project_id, all_tenants, uuid, ip, deleted, limit, marker, sort_key, sort_dir (null values dropped)U or S30 s400, 401, 403, else 500skyline_apiserver/client/openstack/nova.py:29
NovaGET /os-servicesbinary, hostS30 s401, else 500skyline_apiserver/client/openstack/nova.py:74
CinderGET /volumes/detailwith_count, name, multiattach, status, all_tenants, project_id, bootable, id=[…], snapshot_id=[…], limit, marker, sortU or S30 s401, else 500skyline_apiserver/client/openstack/cinder.py:29
CinderGET /snapshots/detailwith_count, name, status, volume_id, all_tenants, project_id, id, limit, marker, sortU or S30 s401, else 500skyline_apiserver/client/openstack/cinder.py:62
CinderGET /snapshots/{id}—U30 s401; 404 re-raised and caught by the handlerskyline_apiserver/client/openstack/cinder.py:95
GlanceGET /v2/imagesid=in:<id>,<id>…S30 s401, else 500skyline_apiserver/client/openstack/glance.py:28
NeutronGET /v2.0/extensions—S30 s→ 401 (profile) / 500 (contrib)skyline_apiserver/client/openstack/system.py:79
NeutronGET /v2.0/networksname, shared=True, project_id, id (repeated)U30 s401, else 500skyline_apiserver/client/openstack/neutron.py:29
NeutronGET /v2.0/portslimit, marker, project_id, name, status, device_owner (repeated), id (repeated), network_id (repeated), device_id, sort_key/sort_dir (repeated)U30 s401, else 500skyline_apiserver/client/openstack/neutron.py:54
PrometheusGET /api/v1/query, /api/v1/query_rangepassed throughnone / basic authhttpx defaultnon-200 status forwardedskyline_apiserver/api/v1/prometheus.py:132

Wire details a re-implementation must reproduce:

  • Cinder list filters id and snapshot_id are sent as the Python repr of a list: id=['a3e5…', 'b1c2…'] (python-cinderclient formats search_opts with str()). Cinder evaluates every filter value with ast.literal_eval (cinder:cinder/volume/api.py:2369), which turns it into an IN filter.
  • Glance's id filter uses the in: operator: id=in:<id1>,<id2>.
  • Neutron is mandatory for login. Every profile build calls Neutron GET /v2.0/extensions with the system session (skyline_apiserver/client/openstack/system.py:78). If the catalog has no network endpoint in the region, get_endpoint raises ValueError("Endpoint not found") (skyline_apiserver/client/utils.py:120), and _patch_profile turns it into a 401 (skyline_apiserver/api/v1/login.py:377): POST /login answers 401 {"detail": "Endpoint not found"} although Keystone has already issued the token. Checked in kind by the skyline-console reverse-spec session (reverse-spec-skyline-console/07-rewrite-pack_stable-2026.2.md § 3.1, § 6.3).
  • Neutron list filters with several values are repeated keys (id=a&id=b); sort_key/sort_dir are repeated in pairs.
  • Booleans to Cinder are sent as the strings True/False (skyline_apiserver/api/v1/extension.py:578).
  • limit/marker are passed through to the service; Skyline fetches one page only (Nova and Neutron) and Cinder's count is the total.

Load per inbound call

The recorded counts give the cost model: every authenticated call = 2 Keystone calls (discovery + validation) + 1 SQL DELETE + 1 SQL SELECT (cookie path); every endpoint that builds a user session adds a re-scope POST /v3/auth/tokens; profile-returning endpoints add 6 more Keystone and 1 Neutron call. Nothing is cached between requests except the system token (per worker) and the policy files (by modification time). Keystone validation results are not cached (no keystonemiddleware, no memcached).

3. Database

TableColumnsRoleWritten byRead by
revoked_tokenuuid VARCHAR(128) PK, expire INTEGER (epoch s)sessions logged out before their expiryPOST /logout (skyline_apiserver/db/api.py:56); expired rows deleted on every authenticated request (skyline_apiserver/db/api.py:65)middleware, cookie path (skyline_apiserver/db/api.py:41)
settingskey VARCHAR(128) PK, value JSON NULLrun-time overrides of setting.*PUT /setting (upsert, skyline_apiserver/db/api.py:93), DELETE /setting/{key} (skyline_apiserver/db/api.py:111)GET /settings, GET /setting/{key}
  • Engine: synchronous SQLAlchemy, database_url (default sqlite:////tmp/skyline.db; mysql://… for MariaDB, mapped to mysql+pymysql by Alembic only, skyline_apiserver/db/alembic/env.py:33); pool_pre_ping for non-SQLite (skyline_apiserver/db/base.py:71). The databases, aiomysql and aiosqlite requirements are not used by the code [inferred: leftovers of the former async layer].
  • Transactions: none in practice. db.transaction() opens a session that no query uses (skyline_apiserver/db/base.py:36); each execute/fetch_* opens its own session and commits (skyline_apiserver/db/base.py:40). The SELECT … FOR UPDATE in update_setting is committed before the INSERT/UPDATE runs.
  • Queries (reconstructed SQL):
-- every authenticated request
DELETE FROM revoked_token WHERE expire < :now_minus_1;
SELECT count(revoked_token.uuid) AS revoked_count FROM revoked_token WHERE revoked_token.uuid = :uuid;  -- cookie path only
-- logout
INSERT INTO revoked_token (uuid, expire) VALUES (:uuid, :exp);
-- settings
SELECT settings.key, settings.value FROM settings;
SELECT settings.key, settings.value FROM settings WHERE settings.key = :key;
SELECT settings.key, settings.value FROM settings WHERE settings.key = :key FOR UPDATE;  -- then
INSERT INTO settings (key, value) VALUES (:key, :value);  -- or UPDATE settings SET value=:value WHERE key=:key
DELETE FROM settings WHERE settings.key = :key;
  • Migrations: Alembic 000 creates both tables with non-unique indexes; 001 replaces them with primary keys pk_revoked_token, pk_settings (skyline_apiserver/db/alembic/versions/001_add_primary_keys_to_skyline_tables.py:32). The migrated schema equals the model DDL except constraint names (_rewrite/schema-sqlite-migrated_stable-2026.2.sql).

4. nginx reverse proxy (deployment glue)

skyline-nginx-generator (skyline_apiserver/cmd/generate_nginx.py:230), run at container start, reads the full endpoint list from Keystone with the system account (GET /v3/endpoints?interface=<interface_type>, GET /v3/services, skyline_apiserver/cmd/generate_nginx.py:118) and renders skyline_apiserver/templates/nginx.conf.j2:

LocationTargetNotes
/skyline-console static files, SPA fallback to index.htmlskyline_apiserver/templates/nginx.conf.j2:83
/api/openstack/skyline/http://skyline/ (unix socket /var/lib/skyline/skyline.sock)prefix stripped (skyline_apiserver/templates/nginx.conf.j2:91)
/api/v1/http://skyline/api/v1/direct access, same API (skyline_apiserver/templates/nginx.conf.j2:101)
/<nginx_prefix>/<region lower>/<service>/the endpoint URL with its version and %(project_id)s parts strippedone per catalog endpoint of a mapped service type; Host rewritten; TLS 1.2/1.3 and SNI for https targets (skyline_apiserver/templates/nginx.conf.j2:111)

The paths in the profile's endpoints are exactly these locations (skyline_apiserver/client/openstack/system.py:76); the console prefixes its OpenStack calls with them (skyline-console:src/client/client/constants.js:46). Two endpoints of the same service type in one region make the generator fail (skyline_apiserver/cmd/generate_nginx.py:136). The proxy forwards the browser's X-Auth-Token unchanged: the OpenStack services authenticate the user themselves.

5. Files and other resources

ResourceAccessDefined at
$OS_CONFIG_DIR/skyline.yaml (default /etc/skyline/skyline.yaml)read once per worker at start; missing file → start-up failsskyline_apiserver/config/base.py:108
<policy_file_path>/<service>_<policy_file_suffix>stat on every rule evaluation, re-read when modifiedskyline_apiserver/policy/base.py:105
<log_dir>/<log_file>appendskyline_apiserver/main.py:47
/var/lib/skyline/skyline.sockgunicorn listen socketetc/gunicorn.py:17
/etc/nginx/nginx.confwritten by the generatorcontainer/start_service.sh:16

No subprocess, no privileged operation, no outbound connection other than those listed.

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