An open source virtualization center to manage hundreds of thousands of VMs
Website · Architecture · Run it · Docker Compose · Kubernetes · Release Notes
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.
| 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 |
FastAPI · Pydantic v2 · SQLAlchemy 2.0 (async) · asyncpg · Alembic · aio-pika · redis-py · Python 3.11+.
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.
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}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 2The 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.
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 |
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 /meGET /clusters,/clusters/{id}GET /hosts?clusterId,/hosts/{id},/hosts/{id}/{vms,templates,isos}GET /hosts/{id}/agent-config- agentconfig.inifor onboarding (host.manage; RabbitMQ creds)GET /folders?hostId&clusterIdGET /tag-categories,/tags- the global tag catalog (Tag.vmCountwithin 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.manageglobally)POST /hosts{name, clusterId?}·PATCH /hosts/{id}any subset of{name, fqdn, clusterId}(clusterId: null= standalone;fqdnnormally left to the agent) ·DELETE /hosts/{id}(cascades VMs/folders/templates/ISOs). Global privileges only (host.manage;host.configfor 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.managePOST /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.managePUT /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 needsGET /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.manageon the object)GET /rbac/groups?q- group names to suggestGET /me/privileges?ids=vm:<id>,host:<id>,global- the caller's privileges per objectGET /rbac/effective?group=<name>&object=vm:<id>- why a group may do whatGET /vms/{id}/console(vm.console) ·GET /hosts/{id}/console(host.console) - where the console connects
Responses are camelCase JSON. Errors: { "error": { "code", "message", "details"? } }.
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).
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.
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).
- Start from an up-to-date, clean
mainwhereruff check appandpython -m pytestpass. - Changelog: in
CHANGELOG.md, write the release's notes under a new## [0.1.2] - YYYY-MM-DDheading (Added / Changed / Fixed), fromgit 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". - Bump and tag:
scripts/release.sh 0.1.2. It checks the tree is clean and the changelog has the section, setspyproject.toml's version, commits it as🔖 Release 0.1.2and creates the annotated tag0.1.2. - Push:
git push origin main --follow-tags. CI builds and publishes the:0.1.2image from the tag. - 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>".
See LLM.md for an annotated map of app/ and the async task lifecycle.
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.