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+.
Postgres, Valkey, RabbitMQ (and Keycloak) live in ../infra-containers, not
here. Start them once:
cd ../infra-containers && ./start-containers.shThat publishes on localhost: postgres :5432 (ovc / ovc123 / db ovc),
rabbitmq :5672 (+ UI :15672), valkey :6379, pgAdmin :8088
([email protected] / admin), keycloak :8080.
ovc-backend/docker-compose.yml runs only the API and worker; they reach the
infra 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}Optional stand-in for the Go agent (responds for hosts you've already created, posts synthetic inventory + executes power actions - no Windows needed):
docker compose --profile demo up -dRequires Python 3.11+ and the infra from ../infra-containers up.
python3 -m venv .venv && source .venv/bin/activate
pip3 install -e ".[dev]" # or: uv sync
cp .env.example .env # defaults already match ../infra-containers
alembic upgrade head
uvicorn app.main:app --reload --port 8000 # terminal 1
python -m app.worker # terminal 2
python scripts/fake_agent.py # terminal 3 (optional demo agent)The DB starts empty - create a host with POST /api/hosts {name}, then use
its Setup Agent config on the real host (or run fake_agent.py for a demo).
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 principal (global scope grant + OVC_ADMIN_ROLE). 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_ROLES_CLAIM |
resource_access.${client_id}.roles |
dot-path to the roles array in the token (${client_id} substituted); e.g. realm_access.roles, roles |
OVC_ADMIN_ROLE |
ADMINISTRATOR |
this role bypasses every scope filter (full access, no ScopeGrant) |
OVC_AGENT_OFFLINE_AFTER_SECONDS |
120 |
host is "offline" if agent_status is older than this |
OVC_TASK_TIMEOUT_SECONDS |
300 |
worker marks a task timeout after this |
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 (admin only; 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 (admin only)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)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)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) - admin onlyPOST /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) - admin onlyPUT /vms/{id}/tags{tagIds}- replace a VM's tags (at most one per category; anyone who sees the VM)
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 asRelease 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.