qm-template
A Python CLI that downloads cloud images, creates Proxmox VE VM templates and prepares VirtualBox artifacts, with Cloud-Init support.
Features
- Multi-distro downloads: Debian, Ubuntu, Rocky Linux, AlmaLinux, Fedora, CentOS Stream, Alpine, openSUSE and Arch Linux
- Checksum verification: SHA-256/SHA-512 fetched from each distro's official
checksum files and saved next to the image (
<image>.sha256/.sha512) - Pinned builds: dated builds are selected where the upstream offers them, and images mirror the upstream directory layout
- Resumable downloads: uses the first available of
axel,aria2c,wgetorcurl, with a configurable number of parallel connections, progress output by default (silence it with--quiet) and a from-scratch retry with the same downloader when a download fails or fails checksum verification - Retrying metadata fetches: checksum and directory listings survive transient 5xx/network errors with exponential backoff
- Complete
qm createcommand: the whole template is assembled into a singleqm create ... --template 1invocation instead of a chain ofqm setcalls - Automatic VM IDs: the next free ID is picked from
qm list(or the Proxmox config directory), starting atvmid.start(default 9000) - Configurable CPU type:
create.cpu/--cpuoverrides the defaultcputype=hostwhen migration across CPU generations matters - Local VM artifacts:
prepareconverts an image to VDI, VMDK, QCOW2, raw or VHDX withqemu-imgand builds a NoCloud seed ISO (user-data, meta-data and a DHCP network-config) withgenisoimage, both written next to the source image - Validated TOML configuration: settings and per-distro overrides are
validated with
pydantic-settings(unknown keys are rejected with a hint), optional[distro.<name>]overrides, andQM_TEMPLATE_*environment variables that take precedence over the file - Shell completion:
argcompletecompletes commands, options and per-distro parameter values - Discoverable distro parameters:
qm-template distroslists every parameter with its default and accepted values;qm-template distros debiandescribes a single distro - Mirror-friendly: point any distro at an upstream or mirror through
[distro.<name>] base_url, including the checksum file - Configuration template:
qm-template configprints a commented starting-point configuration and--fulladds the per-distro default tables; the file is never written automatically
Requirements
- Python >= 3.11
- uv to install and develop
- A Proxmox VE host, normally running as root
- Proxmox VE (
qm,pvesm) for thecreatecommand - One of
axel,aria2c,wgetorcurlfor thedownloadcommand qemu-imgandgenisoimagefor thepreparecommand
Installation
Install the CLI as a uv tool:
uv tool install qm-template
For development, sync the project and run it from the virtual environment:
uv sync --group dev
uv run qm-template --help
Configuration
The configuration file is optional and loaded from
/etc/qm-template/config.toml when it exists; use --config PATH/-c or
QM_TEMPLATE_CONFIG to point elsewhere. All commands work without a
configuration file. Settings are validated with pydantic-settings; unknown
keys and invalid values are rejected with the file and key path in the error.
Every setting can also be overridden with a QM_TEMPLATE_* environment
variable that uses __ for nesting, for example
QM_TEMPLATE_DOWNLOAD__CONNECTIONS=4; environment variables take precedence
over the file, and command-line options take precedence over both. Downloaded
images are stored in /var/lib/qm-template unless paths.images_dir
overrides it, mirroring the upstream layout:
<images_dir>/<distro>/<release>/[<tag>/]<filename>
download.preferred orders the downloaders and download.connections sets the
number of parallel connections for axel and aria2c. Downloads show progress
by default; set download.quiet = true or pass -q/--quiet to hide it.
cloudinit.user/password configure the Cloud-Init user, and
cloudinit.sshkeys/sshkeys_files list inline SSH public keys and key files
whose contents are merged and deduplicated by fingerprint; at least one key is
required. create.cpu sets the CPU type passed as cputype=... (default
host), and vmid.start/vmid.step drive automatic VM ID selection (default
9000 and 1). Optional [distro.<name>] overrides are not written to the
generated file:
[distro.debian]
release = "bookworm-backports"
arch = "arm64"
base_url = "https://mirror.example.org/debian-cloud"
base_url points a distro at an upstream or mirror that mirrors the expected
directory layout; the checksum file is fetched from the same base. Since the
upstream sites generally do not GPG-sign checksum files, a mirror serves both
the image and its checksum; use a trusted mirror if authenticity matters.
Print a starting-point configuration to stdout and redirect it; the file is never written automatically:
install -d /etc/qm-template
qm-template config > /etc/qm-template/config.toml
# also append the per-distro default tables
qm-template config --full > /etc/qm-template/config.toml
Shell completion is provided by argcomplete; enable it once per shell:
eval "$(register-python-argcomplete qm-template)" # bash; zsh needs bashcompinit
Usage
Download a cloud image
# default distro from the configuration file
qm-template download
# pick a distro, optionally override parameters
qm-template download debian
qm-template download ubuntu --release noble --variant minimal
qm-template download debian --release bookworm --tag 20260907-2594
# print the first available downloader's command without running it
qm-template download -n alpine
# hide the downloader progress output
qm-template download -q alpine
# list distros with the values each parameter accepts
qm-template distros
# print a commented starting-point configuration
qm-template config > config.toml
--dry-run resolves the image and pretty prints the command of the first
available downloader, one argument group per line, without downloading anything.
Builds are pinned where the upstream provides dated snapshots (Debian, Ubuntu
server, Arch Linux, openSUSE Tumbleweed): the newest build is selected, and a
newer build is downloaded alongside the old one instead of overwriting it.
Interrupted downloads are resumed on the next run; partial files are stored as
<image>.part. A failed download is retried from scratch with the same
downloader and never switches to another one; a completed download that fails
checksum verification is downloaded once more from scratch before the command
fails. Checksum files and directory listings are retried with exponential
backoff on transient 5xx and network errors. The checksum fetched from the
upstream source is saved next to the image as <image>.sha256 or
<image>.sha512, depending on the upstream algorithm.
Create a VM template
# interactive image selection; the VM ID is chosen automatically
qm-template create
# filter images with a regular expression on the relative path
qm-template create debian-13
# non-interactive with an explicit ID
qm-template create --vm-id 9000 --vm-name debian-13-template
# use a migration-friendly CPU type
qm-template create --cpu x86-64-v2-AES
# inspect the assembled command without running it
qm-template create --dry-run --vm-id 9000
When --vm-id is omitted, qm-template collects the IDs in use from
qm list (falling back to /etc/pve/qemu-server/*.conf) and picks the first
free ID at or after vmid.start (default 9000), advancing by vmid.step
(default 1); the 9000+ range keeps templates away from regular VMs. If
qm create fails, an existing but incomplete VM config is reported with the
qm destroy command needed to clean it up.
The resulting command is a single qm create invocation, which --dry-run
pretty prints as:
qm create 9000 \
--name debian-13-template \
--cpu cputype=host \
--cores 1 \
--balloon 1024 \
--memory 1024 \
--net0 model=virtio,firewall=1,bridge=vmbr0 \
--scsihw virtio-scsi-single \
--agent type=virtio,enabled=1 \
--machine q35 \
--ostype l26 \
--serial0 socket \
--vga serial0 \
--scsi0 local-lvm:0,import-from=/path/to/image.qcow2 \
--scsi1 local-lvm:cloudinit \
--boot order=scsi0 \
--ipconfig0 ip=dhcp \
--ciupgrade 0 \
--ciuser debian \
--cipassword debian \
--sshkeys ~/.ssh/id_ed25519.pub \
--template 1
Prepare local VM artifacts
Most hypervisors cannot boot .qcow2 directly, so guest disks have to be
converted. The hypervisor-agnostic prepare command selects a downloaded
image, converts it to a guest disk with qemu-img and packs a NoCloud seed
into a CIDATA-labelled ISO with genisoimage: user-data/meta-data built
from the [cloudinit] settings plus a DHCP network-config (needed because
Debian cloud images do not fall back to a generated network configuration).
Both artifacts are written next to the source image and only differ in suffix:
# debian-13.qcow2 -> debian-13.vdi + debian-13.iso
qm-template prepare debian-13
# other hypervisors: vmdk (VMware/VirtualBox), raw or vhdx (Hyper-V)
qm-template prepare --format vmdk debian-13
# override the hostname recorded in the seed
qm-template prepare --vm-name debian-13-vbox debian-13
# preview every command without running or writing anything
qm-template prepare --dry-run debian-13
Supported formats are vdi (default), vmdk, qcow2, raw and vhdx; the
extension follows the format (--format vdi writes <image>.vdi). Choosing
qcow2 for a .qcow2 source is rejected because it would overwrite the source
image. For VirtualBox, attach the .vdi as a SATA hard disk and the seed ISO
as a CD-ROM. Use the generic/genericcloud image variants: Debian's
nocloud variant does not run Cloud-Init. An existing guest disk is kept and
only the seed ISO is rebuilt, since converting is expensive and the seed
derives from the [cloudinit] settings; pass --force/-f to convert again.
Supported distros
qm-template distros is the authoritative list, including the accepted values
of every parameter. Only Debian and Fedora accept --tag for a specific
build; using it with any other distro is an error. Defaults:
| Name | Default release | Default variant | Notes |
|---|---|---|---|
debian |
trixie |
genericcloud |
--release accepts -backports |
ubuntu |
resolute |
server |
variant minimal also supported |
rocky |
10 |
GenericCloud |
variant GenericCloud-LVM |
almalinux |
10 |
GenericCloud |
variant GenericCloud-ext4 |
fedora |
44 |
Generic |
variant UEFI-UKI |
centos |
10 |
GenericCloud |
CentOS Stream |
alpine |
3.24 |
generic |
BIOS firmware, Cloud-Init enabled |
opensuse |
tumbleweed |
Minimal |
--release 15.6 for Leap |
archlinux |
latest |
cloudimg |
variant basic also supported |
Development
uv sync --group dev
just lint # ruff check --fix + ruff format
just typecheck # ty check src/
just test # pytest
just docs-build # mkdocs build
Project layout:
qm-template/
├── pyproject.toml
├── config.example.toml
├── src/qm_template/
│ ├── cli.py # argument parsing, completion and entry point
│ ├── commands.py # download / create / prepare / distros / config commands
│ ├── config.py # pydantic-settings models and configuration loading
│ ├── checksum.py # checksum parsing and verification
│ ├── cloudinit.py # SSH key collection and seed user-data/meta-data
│ ├── config.default.toml # default configuration shipped in the wheel
│ ├── download.py # axel / aria2c / wget / curl wrappers
│ ├── http.py # HTTP helpers, retries and directory listings
│ ├── images.py # local image discovery
│ ├── log.py # logging setup
│ ├── pve.py # qm/pvesm integration and VM ID selection
│ ├── shell.py # grouped command rendering and execution
│ ├── prepare.py # qemu-img / genisoimage wrappers
│ └── distros/ # one module per distro family
└── tests/
Documentation
The published documentation site lives at https://ak1ra-lab.github.io/qm-template/.
References
Metadata
Release files for qm-template 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qm_template-0.3.1.tar.gz | 128.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qm_template-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 162.5 kB
Release files / qm_template-0.3.1.tar.gz
| Download URL | qm_template-0.3.1.tar.gz |
|---|---|
| Size | 128.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
27739d99b06f4b30e8e81808b25420a98b737771f3cb52d168bf782886fd529d
|
|
BLAKE2b-256 checksum How to use checksums |
3f03327ae8e0a8417ca1b2d5690f72d9628318c566dc8fd62a10410879ae1f44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency logRelease files / qm_template-0.3.1-py3-none-any.whl
| Download URL | qm_template-0.3.1-py3-none-any.whl |
|---|---|
| Size | 34.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f886f1dc44f25b9a5e79ab4393538be4e95b6bd1fd0de3b51951603aeb37f025
|
|
BLAKE2b-256 checksum How to use checksums |
40e5a27aaf1482b62ae778e620c7558e6f878b079a73581d636876ca7f3123f9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency log