Skip to content

Repository files navigation

An open source virtualization center to manage hundreds of thousands of VMs

Website · Architecture · Run it · Docker Compose · Kubernetes · Release Notes

ovc-backend

This is a component of the Open vCenter stack.

It is one of three services:

Service Role
ovc-frontend React SSR web UI. Talks to ovc-backend over REST.
ovc-backend (this repo) Python API + worker. Owns Postgres/Valkey, talks to agents over RabbitMQ.
ovc-agent Go binary on each Hyper-V host. Executes operations, replies over RabbitMQ.

ovc-frontend is the only REST client - the contract it expects is ../ovc-frontend/docs/api-contract.md. The backend and the agents communicate only through RabbitMQ:

ovc-frontend ──REST /api/*──▶ ovc-backend ──RabbitMQ──▶ ovc-agent (per host)
                                  │
                          Postgres + Valkey

A pre-built image is published to GHCR as ghcr.io/openvcenter/ovc-backend - see Docker Compose or Kubernetes to run the whole stack with it. Everything from here down is for local development, running this app straight from source.

Two processes

Process Command Role
API uvicorn app.main:app serves /api/*, publishes agent requests, creates tasks; on startup ensures each host's RabbitMQ queues exist
Worker python -m app.worker consumes every host's response + inventory queues, updates Postgres + the Valkey cache, times out stale tasks

Stack

FastAPI · Pydantic v2 · SQLAlchemy 2.0 (async) · asyncpg · Alembic · aio-pika · redis-py · Python 3.11+.

Dependencies

The backend needs these services running before it starts:

Service Version Used for
PostgreSQL 17 system of record (inventory, tasks, roles and permissions, audit log)
RabbitMQ 4 the only channel to the agents (per-host request / response queues)
Valkey (or Redis 7+) 8.0 agent liveness, per-VM locks, cached inventory reads
OIDC provider any token verification when OVC_AUTH_MODE=oidc (Keycloak, Entra ID, Okta...)

See Dependencies for what each one holds and a minimal Docker Compose file that starts them. The defaults in .env.example expect them on localhost.

Run with Docker

ovc-backend/docker-compose.yml runs only the API and worker; they reach the dependencies via host.docker.internal. The API container runs migrations on start. The database starts empty - register hosts with POST /api/hosts.

docker compose up -d
curl localhost:8000/api/health          # {"ok":true,"db":true,"cache":true,"rabbit":true}

Run locally (without Docker)

Requires Python 3.11+ and the dependencies up.

python3 -m venv .venv && source .venv/bin/activate
pip3 install -e ".[dev]"          # or: uv sync

cp .env.example .env             # defaults expect the dependencies on localhost

alembic upgrade head

uvicorn app.main:app --reload --port 8000    # terminal 1
python -m app.worker                          # terminal 2

The DB starts empty - create a host with POST /api/hosts {name}, then use its Setup Agent config on the real host. The host's FQDN and IP are filled in by the agent on its first agent_status.

Environment

All variables are prefixed OVC_ and read from .env (or the process env). Docker Compose sets them itself; .env.example is the template for local runs.

Variable Default Purpose
OVC_DATABASE_URL postgresql+asyncpg://ovc:ovc123@localhost:5432/ovc async Postgres DSN
OVC_VALKEY_URL redis://localhost:6379/0 Valkey / Redis URL
OVC_RABBITMQ_URL amqp://ovc:ovc123@localhost:5672/ RabbitMQ URL (backend ⇄ broker)
OVC_AGENT_RABBITMQ_URL - (derives from OVC_RABBITMQ_URL) AMQP URL agents use to reach the broker, without credentials; shown in the host "Setup Agent" tab
OVC_API_PREFIX /api mount point for all routes
OVC_CORS_ORIGINS ["http://localhost:3000"] JSON list of allowed origins
OVC_AUTH_MODE stub stub = trust every request as OVC_STUB_USER_EMAIL; oidc = verify OIDC JWTs
OVC_STUB_USER_EMAIL [email protected] the dev user (member of OVC_ADMIN_GROUP). Set the same OVC_AUTH_MODE=stub on the frontend too.
OVC_OIDC_ISSUER / _AUDIENCE / _JWKS_URL / _CLIENT_ID - required when OVC_AUTH_MODE=oidc
OVC_OIDC_GROUPS_CLAIM resource_access.${client_id}.roles where the user's groups are in the token (dot-path, ${client_id} substituted) - the names permissions are granted to. Keycloak client roles (default), realm_access.roles, or groups (Keycloak Group Membership mapper, Entra ID, Okta)
OVC_ADMIN_GROUP ADMINISTRATOR members of this group are superusers: every privilege everywhere, no permission needed
OVC_AGENT_OFFLINE_AFTER_SECONDS 120 host is "offline" if agent_status is older than this
OVC_TASK_TIMEOUT_SECONDS 300 default task deadline (worker marks it timeout); long-running kinds and disk work get more (app/task_timeouts.py)
OVC_AUDIT_RETENTION_DAYS 365 days an audit event is kept before the worker prunes it; 0 = keep forever
OVC_AGENT_STORAGE local local | s3 - where agent binaries live (config only for now)
OVC_LOG_LEVEL / OVC_LOG_JSON INFO / false logging

API surface

Everything under OVC_API_PREFIX (default /api). See ../ovc-frontend/docs/api-contract.md for the full contract; interactive docs at /docs.

  • GET /health, GET /me
  • GET /clusters, /clusters/{id}
  • GET /hosts?clusterId, /hosts/{id}, /hosts/{id}/{vms,templates,isos}
  • GET /hosts/{id}/agent-config - agent config.ini for onboarding (host.manage; RabbitMQ creds)
  • GET /folders?hostId&clusterId
  • GET /tag-categories, /tags - the global tag catalog (Tag.vmCount within the caller's scope)
  • GET /vms?hostId&folderId&state, /vms/{id}
  • POST /vms/{id}/actions/{start|stop|shutdown|restart|pause} → 202 { task }
  • DELETE /vms/{id} → 202 { task }
  • GET /tasks?vmId&hostId&status&limit, /tasks/{id}
  • GET /audit-events?actor&action&targetType&targetId&hostId&clusterId&outcome&since&until&q&limit&cursor - the audit log, newest first (audit.read)
  • GET /templates, /isos

Organization (synchronous, DB only - no agent):

  • POST /clusters {name} · PATCH /clusters/{id} {name} · DELETE /clusters/{id} (must be empty - no member hosts; cluster.manage globally)
  • POST /hosts {name, clusterId?} · PATCH /hosts/{id} any subset of {name, fqdn, clusterId} (clusterId: null = standalone; fqdn normally left to the agent) · DELETE /hosts/{id} (cascades VMs/folders/templates/ISOs). Global privileges only (host.manage; host.config for name / fqdn)
  • POST /folders {name, clusterId? | hostId?} · PATCH /folders/{id} {name} · DELETE /folders/{id}
  • PATCH /vms/{id} {folderId} - move a VM into a folder or out (null)
  • POST /tag-categories {name} · PATCH /tag-categories/{id} {name} · DELETE /tag-categories/{id} (also deletes its tags) - tag.manage
  • POST /tags {name, categoryId?, color?} · PATCH /tags/{id} any subset of {name, categoryId, color} (color ∈ models.tag.TAG_COLORS) · DELETE /tags/{id} (removed from every VM) - tag.manage
  • PUT /vms/{id}/tags {tagIds} - replace a VM's tags (at most one per category; vm.config)

Access control (see LLM.md "Access control"):

  • GET /rbac/catalog - privileges + the privilege each VM action needs
  • GET /roles · POST /roles · PATCH /roles/{id} · POST /roles/{id}/clone · DELETE /roles/{id} (rbac.manage; built-in roles are read-only)
  • GET /permissions?scopeType&scopeId (direct + inherited) · POST /permissions · PATCH /permissions/{id} · DELETE /permissions/{id} (rbac.manage on the object)
  • GET /rbac/groups?q - group names to suggest
  • GET /me/privileges?ids=vm:<id>,host:<id>,global - the caller's privileges per object
  • GET /rbac/effective?group=<name>&object=vm:<id> - why a group may do what
  • GET /vms/{id}/console (vm.console) · GET /hosts/{id}/console (host.console) - where the console connects

Responses are camelCase JSON. Errors: { "error": { "code", "message", "details"? } }.

RabbitMQ queues

Per host id (<hostid> = random 10-char [A-Za-z0-9]):

Queue Direction Notes
<hostid>.request backend → agent JSON AgentRequest, correlation_id = task id
<hostid>.response agent → backend task progress + result
<hostid>.agent_status agent → backend last value only
<hostid>.vm_inventory agent → backend last value only
<hostid>.host_inventory agent → backend last value only (hardware + folders)
<hostid>.template_inventory / <hostid>.iso_inventory agent → backend last value only

To lock a per-host agent user to its own queues, set its RabbitMQ permissions to the regex from app.messaging.agent_permission_pattern(host_id).

Migrations

alembic upgrade head
alembic revision --autogenerate -m "describe change"

alembic/env.py takes the DSN from app.config, so OVC_DATABASE_URL must be set.

Releasing

Versions follow Semantic Versioning: patch (0.1.1 → 0.1.2) for fixes and small changes, minor (0.1.x → 0.2.0) for new features, migrations or contract changes, major once the API is stable (1.0.0). A release is three things that always carry the same number: the version in pyproject.toml (the only place it's written - the app reads it back from the installed package for OpenAPI's info.version), a git tag (0.1.2, no v prefix), and a GitHub Release. Pushing the tag makes CI publish the image ghcr.io/openvcenter/ovc-backend:<version> (every push to main still publishes :latest).

  1. Start from an up-to-date, clean main where ruff check app and python -m pytest pass.
  2. Changelog: in CHANGELOG.md, write the release's notes under a new ## [0.1.2] - YYYY-MM-DD heading (Added / Changed / Fixed), from git log --oneline <previous-tag>..main. Leave ## [Unreleased] empty above it and update the compare links at the bottom. Commit it: git commit -am "📝 Changelog for 0.1.2".
  3. Bump and tag: scripts/release.sh 0.1.2. It checks the tree is clean and the changelog has the section, sets pyproject.toml's version, commits it as 🔖 Release 0.1.2 and creates the annotated tag 0.1.2.
  4. Push: git push origin main --follow-tags. CI builds and publishes the :0.1.2 image from the tag.
  5. GitHub Release: create it from the tag, titled with the version and the changelog section as the notes - on GitHub (Releases ▸ Draft a new release ▸ choose the tag) or with gh release create 0.1.2 --title 0.1.2 --notes "<changelog section>".

Project layout

See LLM.md for an annotated map of app/ and the async task lifecycle.

Disclaimer

Open vCenter is an independent, community-driven open source project. It is not affiliated with, endorsed by, sponsored by or supported by Microsoft, VMware, Broadcom or any other vendor whose products it works with or is compared to.

Microsoft, Windows, Windows Server and Hyper-V are trademarks of the Microsoft group of companies. VMware, ESXi, vSphere, vCenter and vMotion are trademarks of Broadcom Inc. and/or its subsidiaries. All other trademarks belong to their respective owners.

The "vCenter" in the project's name refers to the general idea of a virtualization management center, not to VMware's product.

The software is provided "as is", without warranty of any kind - see LICENSE. Use it at your own risk, and try it on machines you can afford to lose before trusting it with the ones you cannot.

Releases

Packages

Contributors

Languages