Skip to content

Termix Proxmox Sync

Generates a Termix JSON import from the Proxmox dynamic inventory, so every VM and LXC appears in Termix without being added by hand — or pushes them straight in with --push.

Hosts are emitted with authType: "credential", referencing credential IDs that already exist in Termix. No secret passes through this script: the JSON holds hostnames, IPs, tags and numeric credential references only.

Termix has a built-in Proxmox import that is fine for a one-off. This script exists to reuse ansible/inventory/proxmox.proxmox.yml — so guest IPs resolve exactly as Ansible resolves them — to turn Proxmox tags into Termix tags and per-host settings, and to be re-runnable.

Terminal window
pip install -r requirements.txt
cp config.example.yml config.yml # then set the credential IDs

Credential IDs come from Termix under Credentials → open one → the ID: field.

Terminal window
export $(cat ../../ansible/.env | grep -v '^#' | xargs)
./termix_proxmox_sync.py -c config.yml -o termix-hosts.json

Then in Termix: Host Manager → import → upload the file.

VariableNeeded forNotes
PROXMOX_URLinventoryRequired — the plugin’s default() never fires
PROXMOX_TOKEN_SECRETinventoryRequired
TERMIX_API_KEY--push onlyStarts with tmx_

The inventory hardcodes user: root@pam and token_id: ansible, so the API token must be root@pam!ansible; PROXMOX_USER / PROXMOX_PASSWORD in .env.example are ignored here. The script warns when either required variable is missing.

FlagEffect
-o -Write JSON to stdout instead of a file
--dry-runPrint the summary, write nothing
--from-json FILERead a saved ansible-inventory --list dump instead of querying Proxmox
--ansible-dir DIRPoint at a different checkout (default ../../ansible)
--inventory PATHUse a different inventory file

The run summary goes to stderr, so -o - stays pipeable:

warning: skipped (stopped): semaphore
warning: skipped (no IP from Proxmox): talos-plane-1
14 host(s) across folder(s): pve, pve2

config.example.yml is the annotated version. The shape:

default_credential_id: 1 # "pve" / root
default_username: root
include_stopped: false
skip_unresolved: true
exclude: [pve-dell]
exclude_tags: [community-script]
defaults: # applied to every host
enable_terminal: true
enable_file_manager: true
overrides: # matching rules, applied in order
- match: { tag: docker }
enable_docker: true
- match: { hostname: traefik }
pin: true
notes: Reverse proxy — recreate via `bws run` only.

Every matching rule applies, in order, and later rules win per field. Rules layer rather than replace, so a broad tag rule and a narrow hostname rule coexist — traefik above gets both enableDocker and pin.

Match keys are hostname, tag, node and type (lxc/qemu); hostname and tag also take lists. Settable fields: credential_id, username, override_credential_username, port, folder, default_path, notes, pin, tags_extra, enable_terminal, enable_tunnel, enable_file_manager, enable_docker, and the five show_*_in_sidebar toggles. Unknown keys warn rather than fail quietly.

Only reference credential IDs that exist — a host pointing at a missing one is created and then simply fails to connect:

Terminal window
curl -H "Authorization: Bearer $TERMIX_API_KEY" http://192.168.1.102:8080/credentials
  • Stopped guests are skipped unless include_stopped: true. LXC status comes from proxmox_status, QEMU from proxmox_qmpstatus.
  • Guests with no resolvable IP are skipped, since the inventory falls back to the hostname and the resulting host could not connect. skip_unresolved: false emits them anyway; either way they are named in a warning.
  • IP-address tags are stripped. Community-script installers record addresses as Proxmox tags; anything parsing as an IP is dropped. Use exclude_tags for name-based noise.
  • Guests sharing a name collapse — Ansible keys hostvars by name, so the second sonarr is lost before this script sees it. Rename one in Proxmox.
  • username is cosmetic under authType: "credential" unless override_credential_username: true.
  • Ansible’s unsafe-string wrapping is undone on load, so comparisons on status, type and tags work on both input paths.
  • Nothing is ever deleted. Guests removed from Proxmox stay in Termix until pruned there.
Terminal window
export TERMIX_API_KEY=tmx_your_token_here
./termix_proxmox_sync.py -c config.yml --push --overwrite \
--termix-url http://192.168.1.102:8080

Set termix_url in config.yml to skip the flag. The API key is read only from the environment — there is no config field, so no token can land in git.

FlagEffect
--pushPOST to Termix (still writes the JSON file as well)
--no-fileWith --push, skip writing the file
--overwriteUpdate hosts that already exist instead of skipping them
--termix-url URLBase URL
--insecureSkip TLS verification, for internal step-CA certs
--timeout NPer-request seconds (default 60)
--endpoint-path PATHOverride the route (default /host/bulk-import)

Use --overwrite on every run after the first. Termix only builds its existing-host lookup when it is set, so a second push without it duplicates every host rather than skipping. The script warns before pushing without it, and matches on ip:port:username — renaming a guest updates in place, but re-addressing one creates a second host under the same name.

Requests are capped at 100 hosts by the API, so larger fleets are chunked automatically. The skipped counter in the response is always 0 and means nothing. A non-zero exit means Termix reported failures, so this is safe to run from cron.

The documented route POST /ssh/bulk-import on docs.termix.site is wrong — that prefix is frontend routing and returns 405 from nginx. --endpoint-path exists in case a future version moves the real one.