docs: define simulator architecture

This commit is contained in:
Sergey Antropoff
2026-07-12 22:36:08 +03:00
commit 3f3dbf1fa9
+199
View File
@@ -0,0 +1,199 @@
# Architecture
## Goals
`proxmox-api-simulator` is a stateful, asynchronous Proxmox VE API emulator. Its
primary design goal is measurable contract compatibility: routes, validation,
authentication, permissions, response shapes, state transitions, and persistent
long-running tasks are verified independently instead of being described as
universally compatible.
The simulator does not require a live Proxmox installation during normal
operation. Official API artifacts and sanitized observations are imported ahead
of time and stored as versioned snapshots.
## System context
```mermaid
flowchart LR
Client["API clients<br/>proxmoxer / Terraform / Ansible"]
Admin["Simulator operator"]
Docs["Official Proxmox API Viewer"]
API["FastAPI application"]
Importer["Contract importer and CLI"]
Contract["Versioned API contract"]
Engine["Simulation engine"]
Worker["Persistent task workers"]
DB[(PostgreSQL)]
Obs["Logs / Prometheus / OpenTelemetry"]
Client -->|"/api2/json"| API
Admin -->|"CLI and /_simulator"| API
Docs -->|"explicit import only"| Importer
Importer --> Contract
Contract --> DB
API --> Contract
API --> Engine
Engine --> DB
Engine --> Worker
Worker --> DB
API --> Obs
Worker --> Obs
```
## Component architecture
```mermaid
flowchart TB
subgraph ContractPlane["API contract plane"]
Sources["Remote, local, and recorded sources"] --> Parse["Source adapters and parser"]
Parse --> Normalize["Version-independent normalized model"]
Normalize --> Validate["Validation, checksums, manifests"]
Validate --> Registry["Contract registry"]
Registry --> Diff["Semantic version diff"]
Registry --> Routes["Dynamic route and schema factory"]
Registry --> Reports["Compatibility reports"]
end
subgraph RequestPlane["Request plane"]
Middleware["Request ID, logging, metrics"] --> Auth["Ticket or API-token authentication"]
Auth --> Permission["ACL and privilege evaluation"]
Permission --> Input["Contract-driven request validation"]
Input --> Handler["Semantic handler registry"]
Handler --> Render["Proxmox response and error renderer"]
end
subgraph SimulationPlane["Simulation plane"]
Handler --> Services["Node, QEMU, LXC, storage services"]
Services --> State["State machines and resource locks"]
Services --> Tasks["Transactional persistent tasks"]
Tasks --> Workers["asyncio workers with PostgreSQL leases"]
Faults["Scenarios, faults, virtual clock"] --> Services
end
Routes --> Input
Registry --> Permission
State --> PG[(PostgreSQL)]
Workers --> PG
Auth --> PG
```
## Boundaries and dependency direction
The contract plane owns declared API facts. It imports source artifacts, retains
unknown source fields, produces deterministic normalized JSON, and exposes
immutable versioned contracts. It does not know about VM state or execute
operations.
The simulation plane owns mutable cluster state and operation semantics. It uses
domain models and repositories that do not depend on FastAPI or source-specific
contract structures. PostgreSQL is the system of record for resources, security
state, locks, scenarios, and tasks.
The API layer is an adapter. It authenticates, authorizes, validates against the
selected contract, dispatches to a semantic handler, and renders a
version-compatible response. A route without a semantic handler is explicitly
reported as unsupported unless an operator enables a non-default fallback mode.
Dependencies point inward: HTTP and CLI adapters depend on application services;
application services depend on domain interfaces; PostgreSQL, contract files,
metrics, and clocks implement those interfaces. Domain services never import
FastAPI.
## Request lifecycle
1. Middleware assigns or validates a request ID and starts safe structured
telemetry.
2. The selected compatibility profile resolves an immutable API snapshot and
version-specific behavior.
3. Authentication resolves a principal without exposing credentials in logs.
4. Contract-declared and handler-specific permissions are evaluated before
revealing or mutating resources.
5. Path, query, and body values are validated by contract-derived schemas.
6. The semantic handler executes through an application service and explicit
transaction boundary.
7. Long operations atomically update the resource lock and create a persistent
task, then return its UPID.
8. The response renderer applies the Proxmox envelope, headers, cookies, and
version-specific error templates.
## Persistence and concurrency
`asyncpg` is used directly. Repositories accept an explicit connection or
transaction context; SQL is parameterized and kept near its repository. Mutable
process globals are not authoritative state.
Workers claim tasks using `FOR UPDATE SKIP LOCKED`, establish renewable leases,
and use idempotency metadata to recover after process failure. Resource state,
resource locks, and task creation are changed in one transaction when required.
Optimistic version columns detect concurrent updates, while database constraints
protect invariants such as VMID uniqueness within a cluster.
Application lifespan owns the connection pool and bounded asyncio worker tasks.
Shutdown stops claims, lets in-flight work reach a safe boundary, cancels only
after a configured grace period, and closes the pool.
## Contract acquisition and trust
Network access is confined to explicit import and recorder commands. Importers
enforce HTTPS, an official-host allowlist by default, response-size and redirect
limits, timeouts, and bounded retries. Every raw artifact is immutable and has a
SHA-256 checksum. Its manifest records provenance, version, parser warnings, and
normalized checksum. Local snapshots keep startup and tests offline.
Declared documentation and sanitized observed behavior remain distinct. A
compatibility profile chooses `strict-docs`, `observed`, or `hybrid` behavior
without scattering version checks through services.
## Security model
- Passwords and API-token secrets are stored only as password hashes.
- Tickets are signed, short-lived, and redacted from telemetry.
- Ticket-authenticated mutations require CSRF validation; API tokens follow the
selected Proxmox compatibility profile.
- Simulator administration uses a separate prefix and credential and can be
disabled completely.
- Recorder mode is opt-in, verifies TLS by default, restricts routes and methods,
and sanitizes secrets and personal identifiers before writing fixtures.
- Containers run as a non-root user and support a read-only root filesystem.
## Observability
JSON logs contain request ID, route template, status, duration, safe principal
identity, task type, and sanitized resource identifiers. Metrics avoid VMID,
UPID, and username labels. OpenTelemetry is optional and has a no-op
implementation so tracing is never required for startup.
## Testing strategy
Unit tests cover deterministic contract processing and domain rules. Integration
tests exercise repositories, transactions, workers, and application lifespan
against PostgreSQL. Contract tests traverse imported endpoints and ensure no
native FastAPI validation response escapes. Compatibility tests compare golden or
live-lab observations after normalizing dynamic values. Concurrency and
property-based tests target task leases, state transitions, serialization, and
parsers.
The first vertical release deliberately supports a small set of endpoints with
complete stateful semantics. All other imported endpoints remain visibly
unsupported until their handlers and compatibility tests exist.
## Deployment model
One Uvicorn process runs per container. Horizontal replicas coordinate through
PostgreSQL rather than local queues. Database migrations and seed operations are
explicit commands and become separate jobs in Kubernetes. PostgreSQL is included
in local Docker Compose but is an external dependency in the production chart.
## Architectural decisions
1. FastAPI routes are registered from normalized snapshots at startup; hundreds
of hand-maintained route declarations are avoided.
2. SQLAlchemy is not used. Direct asyncpg repositories keep transaction and
concurrency behavior explicit.
3. PostgreSQL-backed tasks are the durability boundary; FastAPI background tasks
and in-memory queues are not used for critical work.
4. Compatibility is capability-driven and versioned, not implemented through
scattered version string conditions.
5. Unsupported semantics fail honestly by default; schema-derived or proxy
responses require an explicit operator mode.