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.
pip install -r requirements.txtcp config.example.yml config.yml # then set the credential IDsCredential IDs come from Termix under Credentials → open one → the ID: field.
export $(cat ../../ansible/.env | grep -v '^#' | xargs)./termix_proxmox_sync.py -c config.yml -o termix-hosts.jsonThen in Termix: Host Manager → import → upload the file.
| Variable | Needed for | Notes |
|---|---|---|
PROXMOX_URL | inventory | Required — the plugin’s default() never fires |
PROXMOX_TOKEN_SECRET | inventory | Required |
TERMIX_API_KEY | --push only | Starts 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.
| Flag | Effect |
|---|---|
-o - | Write JSON to stdout instead of a file |
--dry-run | Print the summary, write nothing |
--from-json FILE | Read a saved ansible-inventory --list dump instead of querying Proxmox |
--ansible-dir DIR | Point at a different checkout (default ../../ansible) |
--inventory PATH | Use a different inventory file |
The run summary goes to stderr, so -o - stays pipeable:
warning: skipped (stopped): semaphorewarning: skipped (no IP from Proxmox): talos-plane-114 host(s) across folder(s): pve, pve2Configuration
Section titled “Configuration”config.example.yml is the annotated version. The shape:
default_credential_id: 1 # "pve" / rootdefault_username: rootinclude_stopped: falseskip_unresolved: trueexclude: [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:
curl -H "Authorization: Bearer $TERMIX_API_KEY" http://192.168.1.102:8080/credentialsBehaviour worth knowing
Section titled “Behaviour worth knowing”- Stopped guests are skipped unless
include_stopped: true. LXC status comes fromproxmox_status, QEMU fromproxmox_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: falseemits 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_tagsfor name-based noise. - Guests sharing a name collapse — Ansible keys hostvars by name, so the second
sonarris lost before this script sees it. Rename one in Proxmox. usernameis cosmetic underauthType: "credential"unlessoverride_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.
Pushing directly
Section titled “Pushing directly”export TERMIX_API_KEY=tmx_your_token_here./termix_proxmox_sync.py -c config.yml --push --overwrite \ --termix-url http://192.168.1.102:8080Set 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.
| Flag | Effect |
|---|---|
--push | POST to Termix (still writes the JSON file as well) |
--no-file | With --push, skip writing the file |
--overwrite | Update hosts that already exist instead of skipping them |
--termix-url URL | Base URL |
--insecure | Skip TLS verification, for internal step-CA certs |
--timeout N | Per-request seconds (default 60) |
--endpoint-path PATH | Override 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.