xo-apply — configuration-as-code for Xen Orchestra (looking for feedback & testers)
-
Hi all,
I've been working on a project I'd like to share and get feedback on: xo-apply — configuration-as-code for Xen Orchestra.
The idea: XO keeps its entire configuration locked inside its own database. There's no way to write any of it down in a reviewable file, so rebuilding an XO means re-creating it all by hand in the UI. xo-apply treats XO as code: declare your XO in a YAML file, keep it in git, and reconcile any instance to match it — the same model Terraform uses for cloud accounts.
Backups are where we started — that whole surface is complete today (remotes, jobs, schedules, DR/CR, metadata, mirror, sequences)
What you can do with it:
Rebuild fast — reinstall XO, reconnect your pools, then xo-apply apply config.yaml and your remotes/jobs/schedules are back without clicking through the UI.
Detect drift — xo-apply diff tells you when someone changed a job in the UI and it no longer matches the file (exits non-zero on drift, so it drops straight into CI).
Review changes in git — every config change is a commit with an author and a diff.
Clone configs — apply one file to several XO instances (e.g. keep staging and production identical).
A quick taste of the diff output:Remotes:
- create nas-backups (nfs://192.168.1.50:/export/xo-backups)
Backup jobs: - create nightly-critical (delta, 1 schedule)
~ update weekly-full
~ schedule weekly
retention: 4 → 8
Plan: 2 to create, 1 to update, 0 untracked
There's also an interactive terminal UI — just run xo-apply with no arguments and it walks you through connecting, picking a config file, and running diff/apply/export. And a plain export command to write down what you already have on an existing XO, so you can adopt it without starting from scratch.What's covered today (the complete backup surface):
Backup remotes (NFS, SMB, S3, local)
VM backup jobs + schedules (delta/full, smart-mode tag selection, explicit VM lists)
Disaster Recovery / Continuous Replication (SR targets)
Pool metadata + XO config backups
Mirror backups
Backup sequences (run schedules one after another)On the roadmap — this is where the project grows beyond backups toward managing the rest of XO, and where I'd love input on priorities:
- Users & groups
- Servers / pool connections
- ACLs / RBAC
- Backup job health checks
How it works under the hood: it talks to XO over the network — the REST API where writes are supported, and XO's JSON-RPC websocket via xo-lib (the same client xo-cli uses) for backup jobs/schedules, since the REST API doesn't expose those writes yet. No state file is kept: the running XO is always the source of truth. It runs anywhere Node.js ≥ 20 does — Linux, macOS, Windows — and works against any current XO from sources or XOA.
Install is a one-liner from GitHub (details + full docs in the README):
npm install -g https://github.com/acebmxer/xo-apply/archive/refs/heads/main.tar.gz
What I'm looking for: feedback from this community — is this something people want? And if anyone's willing to test what's there today against their own XO, I'd love to hear how it goes. diff and export are read-only and safe to try; apply shows you the full plan and asks for confirmation before it changes anything (and refuses to run unattended without --yes). One heads-up for testers rebuilding onto a fresh XO: connect your pool(s) and set any remote-secret env vars first — the README covers both.It's an independent community project, AGPL-3.0 (the same license as XO itself), and not affiliated with or endorsed by Vates. It stands on Vates' own xo-lib and xo-remote-parser libraries.
Repo: https://github.com/acebmxer/xo-apply
Note: this project was built with Claude Code from the ground up.
- create nas-backups (nfs://192.168.1.50:/export/xo-backups)
-
This looks genuinely useful, thanks for building it and opening it up.

The "XO config is locked in its own database" pain is real, I've watched people rebuild remotes and backup jobs by hand after a reinstall and it's a slog.
Treating the backup surface as declarative YAML with drift detection that drops into CI is a clever angle. I'm curious how it talks to XO underneath, is it going through the JSON-RPC API? There's also the Terraform provider for XO (https://github.com/vatesfr/terraform-provider-xenorchestra), but that leans toward infra provisioning rather than backup config, so this seems to fill a different gap.
I'd like to try it against a test XO out of my own curiosity when I get a moment.
Really nice work, and folks running bigger XO fleets here will have sharper feedback than I can give. -
Yes I am interested in any and all feedback. As of now it uses the JSON-RPC API and auth token. It will pull the current backup job / settings and create them in the new XO. If there is decent interested in this I will work on adding in the other features as well.
As my home lab I am not using all the backup features so can only test basic backup job creating and such. IF other can test the Mirror jobs, DR, or Continuous Replication.
Edit - It uses both Rest API and JSON -RPC API..
REST API (/rest/v0) — used wherever it supports the operation: remote (backup repository) create/read/update, VM listing, etc. Client in src/client/rest.ts.
JSON-RPC (websocket, via Vates' xo-lib) — used where REST is still read-only (backup jobs, schedules) or missing an operation entirely (e.g. remote deletion — REST only has "forget"). Client in src/client/jsonrpc.ts.
Authentication is a REST token (POST /rest/v0/users/me/authentication_tokens) that covers both transports (cli.ts:33).The design intent is that REST is preferred, and JSON-RPC fills the gaps until the REST API reaches parity — at which point only the facade file needs to change.
-
Added local user support.
Current sample config file.
# Example xo-apply configuration. # Copy this into your own PRIVATE repo, edit, then: # export XO_URL=https://xo.example.lan XO_TOKEN=... # xo-apply diff config.yaml # xo-apply apply config.yaml # # Secrets never go in this file: use ${env:VAR_NAME} placeholders, # resolved from environment variables when the tool runs. # # A section that is ABSENT is unmanaged (xo-apply won't touch or report that # resource type). A present-but-empty section means "manage this type, none # should exist" (only deleted when you pass --prune). remotes: # NFS share on a NAS - name: nas-backups type: nfs host: 192.168.1.50 path: /export/xo-backups # port: 2049 # optional # mountOptions: vers=4 # optional mount(8) options # SMB / Windows share — host is "HOST\share" (single backslash in YAML # double-quoted strings must be written as \\) - name: windows-share type: smb host: "192.168.1.60\\backups" domain: WORKGROUP username: backup password: ${env:SMB_BACKUP_PASSWORD} # path: xo # optional subfolder inside the share # S3-compatible object storage (AWS, MinIO, Backblaze B2, ...) - name: offsite-s3 type: s3 host: s3.us-east-1.amazonaws.com path: my-bucket/xo-backups # bucket/directory accessKey: AKIAEXAMPLE secretKey: ${env:S3_SECRET_KEY} region: us-east-1 # protocol: http # for http-only endpoints (e.g. local MinIO) # Directory local to the XO VM (e.g. a mounted USB disk) - name: local-disk type: local path: /mnt/backup-disk backupJobs: # Delta (incremental) backup of every VM tagged "critical", every night - name: nightly-critical mode: delta vms: tag: critical remotes: [nas-backups, offsite-s3] settings: # optional global job settings, passed through to XO concurrency: 2 # timezone: America/New_York # maxExportRate: 104857600 # bytes/s # nRetriesVmBackupFailures: 2 # reportWhen: failure schedules: - name: nightly cron: "0 2 * * *" retention: 14 # backups kept on the remotes # snapshotRetention: 3 # snapshots kept on the pool # timezone: America/New_York # enabled: false # schedules are enabled by default # Weekly full backup of specific VMs, selected by name - name: weekly-full mode: full compression: zstd vms: names: [dc-01, mail-01] # ...or select by uuid: uuids: [770aa52a-fd42-8faf-f167-8c5c4a237cac] # ...or pass a raw XO smart-mode pattern for anything more complex: # raw: # type: VM # tags: # __or: [[prod]] remotes: [nas-backups] schedules: - name: weekly cron: "0 3 * * 0" retention: 8 # Disaster Recovery / Continuous Replication: instead of (or in addition to) # remotes, target one or more SRs. mode:full => DR, mode:delta => CR. - name: dr-critical mode: full # delta = Continuous Replication vms: tag: critical srs: [4991d4aa-ed84-599b-7d19-97f2f943a366] # target SR UUID(s) # remotes: [] # SR-only is fine; may be combined with remotes schedules: - name: hourly-dr cron: "0 * * * *" retention: 3 # replicas kept on the SR # Metadata backups: pool metadata and/or XO's own configuration. metadataBackups: - name: xo-config xoMetadata: true # back up XO's own config pools: [939ed551-fbd6-9868-52d8-d3997b7bf7da] # pool UUID(s) for pool metadata remotes: [nas-backups] schedules: - name: daily cron: "0 21 * * *" xoRetention: 7 # XO metadata backups kept poolRetention: 7 # pool metadata backups kept # Mirror backups: copy an existing remote's backups onto other remote(s), # e.g. push local backups offsite to S3. mirrorBackups: - name: offsite-mirror mode: full # full or delta, to match the source backups sourceRemote: nas-backups remotes: [offsite-s3] schedules: - name: nightly-mirror cron: "0 5 * * *" retention: 14 # Sequences: run backup schedules one after another. Each step names a job and # one of its schedules (from any job kind above, or already in XO). The sequence # has its own cron for when the whole chain runs. sequences: - name: nightly-then-metadata steps: - { job: nightly-critical, schedule: nightly } - { job: xo-config, schedule: daily } cron: "0 22 * * *" # enabled: false # sequences are enabled by default # timezone: America/New_York # Local users (XO's internal auth provider). Users created by an external auth # plugin (LDAP/SAML/GitHub) are NOT managed here and are never pruned. # # Passwords are write-only: XO never returns them, so a real password can't be # exported or diffed — but XO REQUIRES a password to create a user. So `export` # writes the placeholder `password: ChangeMe` for every user. # ⚠️ CHANGE these before importing into a real XO (or use a ${env:...} ref), # otherwise every new user is created with the password "ChangeMe". # The password is only used when a user is CREATED; for an existing user it is # ignored (change it in the XO UI). Only `permission` is compared for drift. users: - email: ops@example.com password: ChangeMe # required by XO; change before import permission: admin # none | read | write | admin (default: none) # Local groups. Members are referenced by email and resolved to ids at apply # time; each member must be a user defined above or already present in XO. groups: - name: operators users: [ops@example.com]
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login