Reverse-specification of skyline-apiserver — branch stable/2026.2
Branch
stable/2026.2@4af72bfd4577(tag9.0.0) — repository https://opendev.org/openstack/skyline-apiserver — analysed on 2026-10-28
- Repository: https://opendev.org/openstack/skyline-apiserver — specified branch:
stable/2026.2@4af72bfd4577d115af29e428cd0affa116e22e70— analysed on: 2026-10-28 - Scope: the whole project: the FastAPI API server (26 endpoints), its database, its outbound REST calls (Keystone, Nova, Cinder, Glance, Neutron, Prometheus), the policy engine it embeds, the CLI tools (
skyline-nginx-generator,skyline-policy-manager,skyline-swagger-generator,skyline-sample-config-generator) and the deployment glue (nginx template, gunicorn, container start script). Rewrite pack (step 8) included; change history (step 7) not requested. - Client repository cited:
skyline-consolestable/2026.2@806d2003(prefixskyline-console:), andcinderstable/2026.2@c117f9cf0(prefixcinder:) for one wire detail.
| File | Content |
|---|---|
| 01-rest-api_stable-2026.2.md | the 26 endpoints, technical and functional |
| 02-errors_stable-2026.2.md | error responses, the situations that raise them, logs |
| 03-interactions_stable-2026.2.md | outbound REST per endpoint, Keystone token handling, database, nginx proxy |
| 04-scenarios_stable-2026.2.md | sequence diagrams: login, TOTP, authenticated call, server list, logout, WebSSO |
| 05-async-processing_stable-2026.2.md | processing outside the endpoints: middleware work per request, startup, CLI tools |
| 07-rewrite-pack_stable-2026.2.md | rewrite pack: contracts, console expectations, conformance data, checklist |
[_rewrite/](_rewrite/rewrite-pack-index_stable-2026.2.md) | OpenAPI, routes, DDL, 1 860 embedded policy rules, config sample, outbound-call capture |
Overview
skyline-apiserver is the back end of the Skyline dashboard (skyline-console). It is not the data path for most of the UI: the console talks to Nova, Neutron, Cinder, Glance… directly, through an nginx reverse proxy generated from the Keystone catalog (/api/openstack/<region>/<service>/, skyline_apiserver/templates/nginx.conf.j2:111), with the raw Keystone token it obtained from skyline-apiserver. The API server itself does four things:
- Session management: password, TOTP (two-step with a Keystone receipt) and WebSSO login against Keystone; it returns the project-scoped Keystone token in the body and a signed HS256 JWT in the
sessioncookie, switches project or region by re-scoping the token, revokes it at logout (skyline_apiserver/api/v1/login.py:395). - Aggregated list views (
/extension/*) that join Nova, Cinder, Glance, Neutron and Keystone data into one paginated list, partly with the privileged system session of theskylineservice user (skyline_apiserver/api/v1/extension.py:62). - Policy evaluation for the UI: it embeds the default policy rules of 16 OpenStack services (1 860 rules, overridable by files) and answers "which actions is this user allowed?" so that the console can hide buttons (
skyline_apiserver/api/v1/policy.py:96). - A small settings store and a Prometheus proxy filtered by project (
skyline_apiserver/api/v1/setting.py:52,skyline_apiserver/api/v1/prometheus.py:112).
It has no message bus, no periodic task and no worker. Its own database has two tables: revoked session IDs and setting overrides (skyline_apiserver/db/models.py:22).
Architecture
flowchart LR browser[Browser: skyline-console SPA] -->|HTTPS| nginx[nginx :9999] nginx -->|static files| static[(skyline_console/static)] nginx -->|/api/openstack/skyline/ → unix socket| api[skyline-apiserver<br/>gunicorn + uvicorn workers] nginx -->|/api/openstack/<region>/<service>/<br/>X-Auth-Token from the browser| os[OpenStack APIs<br/>Nova, Neutron, Cinder, Glance…] api -->|REST: auth, token validation, catalog| ks[Keystone] api -->|REST: list servers, services| nova[Nova] api -->|REST: volumes, snapshots| cinder[Cinder] api -->|REST: images| glance[Glance] api -->|REST: networks, ports, extensions| neutron[Neutron] api -->|HTTP GET /api/v1/query*| prom[Prometheus] api -->|SQL| db[(SQLite or MariaDB:<br/>revoked_token, settings)] api -->|reads| files[/etc/skyline/skyline.yaml<br/>/etc/skyline/policy/*_policy.yaml/]
Processes
| Process | Entry point | Role | Listens to / consumes |
|---|---|---|---|
| skyline-apiserver | gunicorn -c /etc/skyline/gunicorn.py skyline_apiserver.main:app (container/start_service.sh:5), FastAPI app at skyline_apiserver/main.py:68 | HTTP API under /api/v1 | unix socket /var/lib/skyline/skyline.sock (etc/gunicorn.py:17), (1 + CPUs) // 2 uvicorn workers (etc/gunicorn.py:21) |
| nginx | generated by skyline-nginx-generator at container start (container/start_service.sh:23) | serves the console, proxies /api/openstack/skyline/ to the API server and /api/openstack/<region>/<service>/ to each catalog endpoint | 0.0.0.0:9999 by default (skyline_apiserver/templates/nginx.conf.j2:72) |
| db_sync (bootstrap) | make db_sync → alembic upgrade head (container/start_service.sh:12, Makefile:145) | creates or upgrades the two tables | — |
skyline-policy-manager | skyline_apiserver/cmd/policy_manager.py:289 | generates sample policy files, regenerates the embedded rules from the services' oslo.policy entry points, validates them | — (offline tool) |
skyline-swagger-generator | skyline_apiserver/cmd/generate_swagger.py:37 | dumps app.openapi() to swagger.json | — |
skyline-sample-config-generator | skyline_apiserver/cmd/generate_sample_config.py:36 | writes skyline.yaml.sample | — |
External dependencies
| System | Protocol | Library | Configuration | Usage |
|---|---|---|---|---|
| Keystone | REST v3 | keystoneauth1, python-keystoneclient | openstack.keystone_url, system_user_*, system_project* (skyline_apiserver/config/openstack.py:24) | user authentication (password, TOTP, token), token validation on every request, catalog, projects, users, domains, token revocation |
| Nova | REST v2.1, microversion 2.79 (skyline_apiserver/types/constants.py:20) | python-novaclient | catalog compute | server lists, compute services |
| Cinder | REST v3, microversion 3.59 (skyline_apiserver/types/constants.py:21) | python-cinderclient | catalog block-storage | volume and snapshot lists |
| Glance | REST v2 | openstacksdk | catalog image | image names and os_distro |
| Neutron | REST v2.0 | python-neutronclient | catalog network | networks, ports, extension list |
| Prometheus | HTTP | httpx, TLS verification off (skyline_apiserver/utils/httpclient.py:28) | default.prometheus_* (skyline_apiserver/config/default.py:100) | instant and range queries |
| Database | SQL | SQLAlchemy (sync), Alembic | default.database_url (skyline_apiserver/config/default.py:93), SQLite by default | session revocation list, settings |
Key figures
- 26 HTTP operations under
/api/v1(+ 4 FastAPI documentation routes:/docs,/docs/oauth2-redirect,/redoc,/api/v1/openapi.json). - 0 RPC methods, 0 notifications, 0 periodic tasks, 0 bus consumers.
- 2 tables (
revoked_token,settings), 2 Alembic revisions (000,001). - 0 exception classes of its own used by the API: every error is a FastAPI
HTTPExceptionwith{"detail": …}(02). - 1 860 embedded policy rules for 16 services (
barbican,cinder,designate,glance,heat,ironic,keystone,magnum,manila,masakari,neutron,nova,octavia,placement,trove,zun). - 45 recorded scenarios issuing 222 outbound requests (
_rewrite/outbound-calls_stable-2026.2.md); 115/115 unit tests pass.
Points of attention
- **
GET /extension/recycle_serversreturns 500 as soon as one soft-deleted server exists** [verified by capture]: each server is validated intoRecycleServersResponseBaseatskyline_apiserver/api/v1/extension.py:405, whosereclaim_timestampis required (skyline_apiserver/schemas/extension.py:292), but that field is only computed later atskyline_apiserver/api/v1/extension.py:457. The unit test mocks the model and does not see it. - Response models require UUID v4 identifiers for servers, images and volumes (
skyline_apiserver/schemas/extension.py:206,skyline_apiserver/schemas/extension.py:211,skyline_apiserver/schemas/extension.py:338): a resource with a non-v4 UUID makes the whole list fail with 500 [verified by capture with non-UUID data]. - CORS cannot be enabled: the middleware is added inside the lifespan hook (
skyline_apiserver/main.py:56), which Starlette 1.x refuses withRuntimeError: Cannot add middleware after an application has started[verified with the stable/2026.2 upper-constraints]. Settingcors_allow_originstherefore prevents start-up. - Every authenticated call costs at least two Keystone round trips (version discovery +
GET /v3/auth/tokensvalidation,skyline_apiserver/core/security.py:66), plus oneDELETEon the database (skyline_apiserver/main.py:117), plus a re-scopingPOST /v3/auth/tokensfor every endpoint that builds a user session (skyline_apiserver/client/utils.py:54). Login costs 16 outbound requests (03). - The middleware turns every failure into 401 (
skyline_apiserver/main.py:172): a Keystone outage or a database error is reported asUnauthorized: …, which makes the console log the user out (skyline-console:src/client/client/request.js:42). - **Hard-coded default
secret_key** (skyline_apiserver/config/default.py:62, repeated inetc/skyline.yaml.sample:18). The JWT only wraps a Keystone token that is re-validated on each request, so a forged JWT gives no access by itself, but it defeats the session revocation list (a newuuidcan be minted) and the cookie expiry. - The raw Keystone token is returned in every profile (
keystone_token,skyline_apiserver/schemas/login.py:69) and kept by the console inlocalStorage(skyline-console:src/stores/root.js:119); thesessioncookie is set withoutHttpOnlyorSecure(skyline_apiserver/api/v1/login.py:172, Starlette defaults). Logout deletessessionbut nottime_expired(skyline_apiserver/api/v1/login.py:684). - Tenant isolation done by Skyline, not by the service, wherever the system session is used: recycle-bin listing (
skyline_apiserver/api/v1/extension.py:374), volume and snapshot lookups by ID (skyline_apiserver/api/v1/extension.py:586,skyline_apiserver/api/v1/extension.py:743), image and root-volume enrichment (skyline_apiserver/api/v1/extension.py:203). Theskylineuser must be admin in Nova, Cinder and Glance for those views to work. - Database "transactions" are not transactions:
Transactionopens a session that the queries do not use, each query opens its own session (skyline_apiserver/db/base.py:36,skyline_apiserver/db/base.py:40), so theSELECT … FOR UPDATEofupdate_settingdoes not hold any lock (skyline_apiserver/db/api.py:95) [inferred: two concurrent first writes of a key can collide on the primary key]. - **
PUT /settingstores any JSON value**: the value is not validated against the option's schema (skyline_apiserver/api/v1/setting.py:90). - Prometheus: TLS verification is disabled (
skyline_apiserver/utils/httpclient.py:28); any PromQL is accepted; non-admin users only see series that carry aproject_idlabel equal to their project (skyline_apiserver/api/v1/prometheus.py:50). - **Cinder
id/snapshot_idfilters are sent as Python list literals** (id=['<uuid>']), which Cinder accepts only because it runsast.literal_evalon filter values (cinder:cinder/volume/api.py:2369) [verified by capture]. A re-implemented Cinder must keep that behaviour, or Skyline's batching breaks. - **
switch_projectaccess check is dead code**: the profile built by the middleware never carriesprojects(skyline_apiserver/core/security.py:73), so the check atskyline_apiserver/api/v1/login.py:712never applies; Keystone's own refusal to re-scope is what protects (→ 401). - **Log messages with
%s**: the code logs with%-style arguments through loguru, which does not interpolate them (skyline_apiserver/api/v1/login.py:113,skyline_apiserver/api/v1/login.py:136); the log showsTOTP authentication failed for user %s in domain %s: %s[verified in the capture log].
Blind spots / [inferred]
- The behaviour against a real OpenStack was not run: outbound calls were recorded against a fake (
_rewrite/tools/outbound_capture.py) that answers with minimal bodies; error bodies of real services, pagination over several pages and Keystone policies (who may list projects, rescope, read system scope) are therefore [inferred] from the client libraries. - The 16 policy modules under
skyline_apiserver/policy/manager/are generated copies of the services' defaults; they were dumped (_rewrite/policies_stable-2026.2.json) but not compared rule by rule with each service'sstable/2026.2. skyline-policy-manager generate-ruleandvalidateneed the services' Python packages installed; not run.- The websso path was exercised with an already-issued unscoped token; the federation round trip through the IdP (Keystone
/auth/OS-FEDERATION/.../websso) was not. skyline_apiserver/context.py:28(RequestContext) is built for every request but itsauthorizemethod is never called by any endpoint: dead code on this branch.
Source: reverse-spec-skyline-apiserver/00-summary_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.