The VM engine
Reference for the engine files behind VM scripts on Proxmox VE and Incus, function by function.
VM scripts do not use core/build.func. They load their own, smaller engine:
| File | Loaded by | Contains | Platform |
|---|---|---|---|
pve/vm-core.func | Every Proxmox VE VM script | Messages, host checks, the wizard, image download and preparation, storage, disk import, provisioning | Proxmox VE |
vm/common.func | Both vm-core.func files | Helpers that call neither qm nor incus: host tools, release lookups, first-boot units, the closing summary | Both |
vm/cloud-init.func | pve/vm-core.func, on demand | Cloud-Init through qm: drive, user, password, SSH keys, network | Proxmox VE |
pve/vm-app.func | A script that sources it | Installing a container app into a VM image | Proxmox VE |
incus/vm-core.func | Every Incus VM script | The same function names on top of incus, plus incus_vm_create | Incus |
host/validate.func | Both vm-core.func files | validate_mac_address, validate_vlan_tag, validate_mtu | Both |
For a script that uses all of this, see Anatomy of a VM script.
Loading
A script sources pve/vm-core.func and calls load_functions. At load time the file:
- Checks the platform. With
pveversionpresent it continues. Without it, but with a workingincus, it sourcesincus/vm-core.funcand returns, so the script gets the Incus implementations of the same names. - Turns on
set -euo pipefail. - Sources
host/validate.funcandvm/common.func. - Sets
BUILD_LOG(default/tmp/cs-vm-<pid>.log) andTELEMETRY_TYPE="vm".
Each file is taken from COMMUNITY_SCRIPTS_CORE_DIR when that points to a
local checkout, otherwise downloaded from COMMUNITY_SCRIPTS_CORE_URL
(default https://raw.githubusercontent.com/community-scripts/core/main).
| Function | What it does |
|---|---|
load_functions | Runs load_api_functions, color, formatting, icons, default_vars, set_std_mode, shell_check. Runs once. |
load_api_functions | Sources api/api.func unless post_to_api_vm already exists |
load_cloud_init_functions | Sources vm/cloud-init.func. Tries the local checkout, the configured URL, the refs/heads/main form of that URL and the jsDelivr mirror. Returns 1 when setup_cloud_init is still undefined. |
pve/vm-core.func
Output and logging
| Function | What it does |
|---|---|
color, formatting, icons | Define the colour, spacing and icon variables (YW, GN, CL, TAB, CM, CROSS, INFO, ...) |
set_std_mode | STD="" when VERBOSE=yes, otherwise STD="silent" |
default_vars | RETRY_NUM=10, RETRY_EVERY=3 |
msg_info <text> | Start a spinner line. The text is also recorded as the current step for telemetry. |
msg_ok <text>, msg_error <text>, msg_warn <text> | Finish the spinner line with a result. All write to the log. |
msg_custom <symbol> <color> <text> | A line with your own symbol |
msg_debug <text> | Printed only with var_full_verbose=1 |
start_spinner, stop_spinner, spinner_guard, cleanup_spinner | Spinner internals |
clear_line | Clear the current terminal line |
is_verbose_mode | True when VERBOSE or var_verbose is not no, or stderr is not a terminal |
get_active_logfile | Print the log path |
silent <command>... | What $STD expands to. Runs the command with output into the log. On failure it records command, line and the output tail in <log>.errinfo and returns the command's exit code. |
get_header [name], header_info [name] | Fetch headers/vm/<slug> from core, cache it under /usr/local/community-scripts/headers/vm/, clear the screen and print it. A trailing version or VM in APP is dropped when looking for the banner. |
run_curl, curl_handler <curl args> | curl with a readable error message per curl exit code. A failure ends the script with exit code 1. |
Errors and cleanup
| Function | What it does |
|---|---|
error_handler <line> <command> | The ERR trap. Sends the "failed" telemetry status, prints line, exit code and command, then calls cleanup_vmid. |
cleanup_vmid | qm stop and qm destroy for VMID when that VM exists |
cleanup | The EXIT trap. Stops the spinner, leaves and removes TEMP_DIR, and sends the final telemetry status when none was sent: "failed" on a non-zero exit; after a clean exit "done" when the VM's config file exists and "aborted" when it does not. |
vm_mark_created | Call right after qm create. Replaces the ERR trap with vm_post_create_error and applies the keyboard layout to the VM. |
vm_post_create_error <line> <command> | Warns, leaves the VM in place, reports "failed" and exits with the failed command's status |
fatal <text> | msg_error, then kill -INT on the script |
exit_script [reason] | Prints the reason (default "User exited script") and exits |
Host checks
| Function | Checks | On failure |
|---|---|---|
shell_check | The shell is Bash | Exit 103 |
check_root | UID 0 and the parent process is not sudo | Exit 104 |
pve_check | Proxmox VE 8.0 to 8.9 or 9.0 to 9.2 | Exit 105 |
arch_check | dpkg --print-architecture is amd64 | Prints a note about PiMox and exits without an error code |
kvm_check | /dev/kvm is a character device | Exit 236. Enable VT-x or AMD-V in the firmware; a nested Proxmox VE needs nested virtualization on its own host. |
ssh_check | SSH_CLIENT is set | Asks whether to continue over SSH; "No" exits. Skipped with VM_UNATTENDED=1. |
vm_preflight | Runs check_root, arch_check, pve_check, kvm_check, ssh_check, once | arch_check is skipped when var_arm64="yes" |
vm_check_dialog_env | whiptail is installed, a terminal exists, and it is at least 60 columns by 18 rows | Exit 1 with the reason |
vm_require_arch <amd64/arm64> | Host architecture equals the argument | Exit 106 |
vm_fit_cores | CORE_COUNT is not above the number of processor lines in /proc/cpuinfo | Lowers CORE_COUNT and warns. qm start refuses more cores than the host has threads. |
check_hostname_conflict <name> | No VM in qm list has that name | Exit 1 |
Architecture helpers:
| Function | Prints |
|---|---|
vm_arch | arm64 when VM_ARCH or dpkg --print-architecture says arm64 or aarch64, otherwise amd64 |
vm_arch_resolve <amd64 value> <arm64 value> | The value for the host architecture |
Storage is checked when it is selected, see storage.
VM IDs
| Function | What it does |
|---|---|
get_valid_nextid | Starts at pvesh get /cluster/nextid and skips IDs that have a config file under /etc/pve/qemu-server or /etc/pve/lxc, or an LVM volume with that number |
vm_claim_vmid | Checks VMID again just before creation. If it was taken: METHOD=advanced exits 205, otherwise the next free ID is used and the disk names are rebuilt. |
The wizard
| Function | What it does |
|---|---|
vm_dialog <box> <title> <args>... | The one entry point for whiptail. Returns 0 (OK) or 1 (Cancel) and stores input in VM_DIALOG_RESULT. Esc exits through exit_script. A dialog that cannot be drawn exits 1 and names the terminal size. |
vm_start_script [message] [height] [width] | Runs vm_preflight. Unattended: default_settings, vm_apply_env_overrides, vm_fit_cores. Interactive: vm_check_dialog_env, vm_choose_settings_mode, then default_settings and vm_fit_cores, or vm_prompt_cloud_init_advanced and advanced_settings. |
vm_choose_settings_mode [message] [height] [width] | The "SETTINGS" dialog with Yes and Advanced. Sets VM_SETTINGS_MODE to default or advanced. |
vm_apply_env_overrides | Copies VM_VMID, VM_DISK_SIZE, VM_DISK_CACHE, VM_HOSTNAME, VM_CORES, VM_RAM, VM_BRIDGE, VM_MAC, VM_VLAN, VM_MTU, VM_START, VM_STORAGE, VM_MACHINE and VM_CPU over the script's defaults |
vm_echo_default_settings | Prints the settings summary from the current variables |
vm_confirm_new_vm <title> <message> [height] [width] | Yes/no dialog; returns 0 without asking when unattended |
vm_confirm_advanced_settings <message> [height] [width] | The "ADVANCED SETTINGS COMPLETE" dialog with Yes and Do-Over |
vm_prompt_* | One question each. See every wizard question. |
vm_prompt_cloud_init [user] | The Cloud-Init question, see below |
vm_prompt_cloud_init_advanced | Network mode, DNS and SSH keys. Runs once, only with Cloud-Init enabled and not unattended. |
sanitize_vm_hostname <name> | Lower case, everything outside a-z0-9- becomes -, cut to 63 characters |
VLAN and MTU hold -net0 fragments. vm_prompt_vlan stores ,tag=20,
vm_prompt_mtu stores ,mtu=9000, and VM_VLAN=20 or VM_MTU=9000 are
converted the same way.
Machine type, CPU, keyboard, time zone
| Function | What it does |
|---|---|
vm_apply_machine_type [type] | Sets the fragments for qm create, see the table below |
vm_machine_type_label <type> | i440fx, Q35 (Modern) or virt (ARM64) |
vm_cpu_model_label | The model in CPU_TYPE; when empty, KVM64 on amd64 and Host on arm64 |
vm_keyboard_default | VM_KEYBOARD, else the keyboard: line of /etc/pve/datacenter.cfg |
vm_timezone_default | VM_TIMEZONE, else /etc/timezone, else timedatectl; empty when the zone is unknown |
| Host | Argument | MACHINE_TYPE | MACHINE | FORMAT |
|---|---|---|---|---|
| amd64 | i440fx or anything else | i440fx | empty | ,efitype=4m |
| amd64 | q35 | q35 | -machine q35 | empty |
| arm64 | anything | virt | -machine virt | empty |
MACHINE goes into qm create, FORMAT is appended to -efidisk0. On arm64
there is nothing to choose: aarch64 QEMU has neither chipset.
The CPU model works the same way. CPU_TYPE is empty (Proxmox VE's default
model) or a fragment such as -cpu host. vm_prompt_cpu_model offers
host, x86-64-v2-AES, x86-64-v3 and x86-64-v4 on amd64 and leaves
CPU_TYPE empty on arm64.
Images
| Function | What it does |
|---|---|
vm_image_cache_path <url> | Prints /var/lib/vz/template/cache/<file name>; VM_IMAGE_CACHE_DIR changes the directory |
vm_image_checksum <url> | Looks the file up in the vendor's checksum list for cloud.debian.org, cloud-images.ubuntu.com, download.truenas.com, downloads.openwrt.org, download.freebsd.org VM images and mirror.turnkeylinux.org. Prints the tool and the hash. |
vm_fetch_image <url> <target> [options] | Downloads into <target>.part with curl --retry 3, checks size, checksum and optionally the xz container, then renames. Returns 1 with a message on any failure. Options are listed in the script guide. |
vm_extract_image <archive> [target] | Unpacks .xz, .gz, .bz2 or .zst by file name, with pv as progress display when installed. The default target is /var/lib/vz/template/tmp/ (VM_IMAGE_TMP_DIR). Sets VM_IMAGE_FILE; other file types are passed through unchanged. |
vm_ensure_virt_customize | Installs libguestfs-tools with apt-get when virt-customize is missing. Returns 1 if that fails. |
vm_prepare_cloud_image <image> [hostname] | Hostname, empty machine-id, root and password login in sshd_config, time zone, keyboard, then vm_enable_consoles and vm_install_guest_agent. Failed steps are listed; the return code is 1 only when virt-customize is unavailable. |
vm_enable_consoles <image> | Enables getty@tty1 and serial-getty@ttyS0, adds root autologin on both when the image has no cloud-init, and puts console=tty1 on the kernel command line through grubby or /etc/default/grub |
vm_install_guest_agent <image> | Installs and enables qemu-guest-agent. Needs network inside the libguestfs appliance; returns 1 when the install fails. |
vm_expand_image <image> <size> | Finds the largest ext2/3/4, xfs or btrfs filesystem, expands its partition with virt-resize into a new qcow2 and keeps the result only if the filesystem really grew. LVM roots are left alone. Every failure is a warning and return code 1. |
With --cache, vm_fetch_image reuses an existing file that still passes the
checks. If it does not, the "Image Cache" dialog offers "Download" or "Keep
cached"; "Keep cached" is remembered in <file>.keep until the upstream
checksum changes. Unattended runs and runs without a terminal download the new
file.
Storage and disks
| Function | What it does |
|---|---|
vm_select_storage [hostname] | Uses a preset STORAGE if it exists (exit 119 if not). Otherwise lists pvesm status -content images: none exits 119, one is taken, several open the "Storage Pools" dialog. Sets STORAGE, STORAGE_TYPE and calls vm_apply_storage_layout. |
vm_apply_storage_layout <type> | Sets DISK_EXT, DISK_REF, DISK_IMPORT_FORMAT and THIN |
vm_select_iso_storage <file.iso> [hostname] | Finds an active, enabled storage with iso content. Order: VM_ISO_STORAGE or ISO_STORAGE, the disk storage if it can hold ISOs, the only candidate, a dialog. Sets ISO_STORAGE, ISO_VOLUME, ISO_PATH. Returns 119 on every failure. |
vm_define_disk_references [count] | Sets DISK<n> to vm-<vmid>-disk-<n> plus DISK_EXT and DISK<n>_REF to <storage>: plus DISK_REF plus that name. Default count 2. |
vm_alloc_efi_disk [name] | pvesm alloc for the EFI variables: 4M on amd64, 64M on arm64. Default name DISK0. |
vm_import_disk <vmid> <image> <storage> [format] | sync, then qm disk import (or qm importdisk). Reads the created volume from the command's output, or from the new unused entry in the VM config, into VM_IMPORTED_DISK. The format defaults to DISK_IMPORT_FORMAT. Returns 1 on failure. |
vm_resize_disk [disk] [size] | qm resize, default scsi0 and DISK_SIZE. A failure is a warning. |
vm_boot_order <disk> [image/installer] | Prints <disk>, or <disk>;ide2 for installer |
How the storage type decides the layout:
| Storage type | Volume name | Import format | THIN |
|---|---|---|---|
nfs, dir, cifs | <storage>:<vmid>/vm-<vmid>-disk-<n>.qcow2 | qcow2 | empty |
btrfs | <storage>:<vmid>/vm-<vmid>-disk-<n>.raw | raw | empty |
| anything else (block storage) | <storage>:vm-<vmid>-disk-<n> | raw | the script's value, default discard=on,ssd=1, |
There is no helper for a TPM state disk on Proxmox VE. Scripts that want one
allocate a third volume and pass -tpmstate0 <volume>,version=v2.0 to
qm set; see TPM.
Provisioning, start and description
| Function | What it does |
|---|---|
vm_provision <vmid> | Returns 0 when USE_CLOUD_INIT is not yes. Otherwise loads vm/cloud-init.func and calls setup_cloud_init <vmid> $STORAGE $HN yes $CLOUDINIT_USER $CLOUDINIT_NETWORK_MODE $CLOUDINIT_IP $CLOUDINIT_GW $CLOUDINIT_DNS. Returns 1 with a warning when the helpers cannot be loaded; it never removes the VM. |
vm_start_vm [label] | qm start when START_VM is yes |
vm_wait_guest_agent <vmid> [seconds] | Polls qm guest cmd <vmid> ping every 3 seconds, default 120 |
vm_guest_exec <vmid> <seconds> [--] <command>... | qm guest exec. Prints the command's stdout and returns its exit code; 124 when it was still running at the timeout. |
vm_wait_for_ip [seconds] | Sets VM_IP through get_vm_ip, matching the NIC by MAC. Default 120 seconds or VM_IP_TIMEOUT. |
set_description | Writes the HTML description with qm set <vmid> -description: logo, APP as title, donation badge, link to https://community-scripts.org/scripts/<slug>, GitHub, Discussions, Issues. The slug is SCRIPT_SLUG, else NSAPP, else APP, lower case without spaces. |
vm/common.func
Loaded on both platforms. The image helpers in it need virt-customize.
| Function | What it does |
|---|---|
vm_require_tools <command>... | Maps each missing command to a package and installs them with apt-get. Returns 1 for an unknown command, a host without apt-get, or a failed install. |
vm_customize <label> <image> <virt-customize args>... | One virt-customize call. Warns with libguestfs' reason and returns non-zero on failure. |
vm_release_asset <github/gitlab/codeberg> <owner/repo> <asset regex> [tag] | Reads a release through lib/forge.func and sets VM_RELEASE_TAG, VM_RELEASE_VERSION (tag without a leading v), VM_RELEASE_ASSET, VM_RELEASE_URL and, from GitHub asset digests, VM_RELEASE_SHA256 |
vm_latest_from_index <url> <pcre> [--sort-by <pcre>] [--probe <url template>]... | Newest grep -oP match on a page, sorted as versions. With --probe, the placeholder {} in the template is replaced by each match and the newest one whose URL answers wins. Sets VM_INDEX_LATEST and VM_INDEX_URL. |
vm_firstboot_unit <image> <name> <script> [options] | Uploads the script as /usr/local/sbin/<name> and enables a oneshot <name>.service. The marker /var/lib/community-scripts/<name>.done is written only after success, so a failed run is retried on the next boot. Sets VM_FIRSTBOOT_MARKER. |
vm_wait_http <url> [seconds] [--insecure] | Returns 0 once the URL answers with 2xx, 3xx, 401 or 403. Default 120 seconds. |
vm_print_summary ["Label=value"]... | VM ID, hostname, resources, state, IP address, then the extra rows |
vm_next_steps <step>... | Numbered list |
vm_finish [message] | Sends "done" and prints the closing line |
vm_firstboot_unit options: --description <text>, --after <unit>
(repeatable), --requires-path <path> and --cloud-init yes or no. With
Cloud-Init the unit is ordered after cloud-final.service and wanted by
cloud-init.target; without it, by multi-user.target.
vm/cloud-init.func
Proxmox VE only. It configures Cloud-Init with qm set, so nothing in it runs
on Incus.
The questions
vm_prompt_cloud_init [default user] in pve/vm-core.func drives three
functions from this file.
| Function | Asked | Dialogs | Sets |
|---|---|---|---|
configure_cloud_init_interactive [user] | Always | "CLOUD-INIT" yes/no (skipped with CLOUDINIT_REQUIRED=1), "USERNAME", "PASSWORD" and "CONFIRM PASSWORD" | CLOUDINIT_ENABLE, CLOUDINIT_USER, CLOUDINIT_PASSWORD, and the defaults CLOUDINIT_NETWORK_MODE=dhcp, CLOUDINIT_DNS |
configure_cloud_init_advanced | Advanced path | "NETWORK MODE" (DHCP yes/no), then "IP ADDRESS" in CIDR form and "GATEWAY" for static, then "DNS SERVERS" | CLOUDINIT_NETWORK_MODE, CLOUDINIT_IP, CLOUDINIT_GW, CLOUDINIT_DNS |
configure_cloudinit_ssh_keys | Advanced path | "SSH KEY SOURCE": select from detected keys, paste one key, scan a folder or glob, or none | CLOUDINIT_SSH_KEYS, the path of a temporary file with the chosen keys |
Defaults that can be set before the file is loaded:
| Variable | Default |
|---|---|
CLOUDINIT_DEFAULT_USER | root |
CLOUDINIT_DNS_SERVERS | 1.1.1.1 8.8.8.8 |
CLOUDINIT_SEARCH_DOMAIN | local |
CLOUDINIT_SSH_KEYS | empty |
Users and passwords. An empty password field means "generate one". A cancelled static IP or gateway dialog falls back to DHCP.
SSH keys. No key is imported unless you choose one. Detected keys come from
/root/.ssh/authorized_keys, /root/.ssh/authorized_keys2, /root/.ssh/*.pub,
/etc/ssh/authorized_keys and /etc/ssh/authorized_keys.d/*.
Unattended. With VM_UNATTENDED=1 no dialog runs. USE_CLOUD_INIT is
VM_CLOUD_INIT, else yes with CLOUDINIT_REQUIRED=1, else no. The
CLOUDINIT_* variables can be exported directly.
setup_cloud_init
setup_cloud_init <vmid> <storage> [hostname] [yes/no] [user] [dhcp/static] [ip/cidr] [gateway] [nameservers]When the fourth argument is yes it runs, in this order:
| Step | Command | Notes |
|---|---|---|
| Drive | qm set <vmid> --ide2 <storage>:cloudinit | Falls back to --scsi1 |
| User | --ciuser | Default CLOUDINIT_DEFAULT_USER |
| Password | --cipassword | CLOUDINIT_PASSWORD if set, otherwise 8 random alphanumeric characters |
| SSH keys | --sshkeys <file> | Only when CLOUDINIT_SSH_KEYS names an existing file. A failure is ignored. |
| Network | --ipconfig0 ip=<ip>,gw=<gateway> | Only for static with both values; otherwise ip=dhcp |
| DNS | --nameserver, --searchdomain | CLOUDINIT_SEARCH_DOMAIN |
| Upgrade | --ciupgrade 1 | Package upgrade on first boot; ignored where unsupported |
It then writes /tmp/<hostname>-<vmid>-cloud-init-credentials.txt with mode
600. A generated password is in that file; a password you typed is not. The
function exports CLOUDINIT_USER, CLOUDINIT_PASSWORD and
CLOUDINIT_CRED_FILE, and returns 1 for a malformed static IP or gateway.
The cicustom network snippet
setup_cloud_init_network_no_rename <vmid> <mac> <ip/cidr> <gateway> [nameservers] [search domain]Proxmox VE's generated network configuration renames the NIC to eth0. On
Ubuntu 26.04 with cloud-init 26.1 and netplan 1.2 that rename fails. This
function writes its own network configuration that matches the interface by
MAC address and does not rename it:
version: 2
ethernets:
primary:
match:
macaddress: "02:ab:cd:ef:01:23"
dhcp4: false
addresses:
- "192.168.1.100/24"
routes:
- to: default
via: "192.168.1.1"
nameservers:
addresses:
- "1.1.1.1"
- "8.8.8.8"
search:
- "local"The file is stored as vm-<vmid>-network.yaml on a storage with snippets
content: CLOUDINIT_SNIPPET_STORAGE, or the first active one. It is attached
with qm set <vmid> --cicustom network=<storage>:snippets/vm-<vmid>-network.yaml,
keeping any existing user, vendor or meta entries, and followed by
qm cloudinit update. CLOUDINIT_NETWORK_SNIPPET holds the volume ID.
Without a snippets storage the function returns 1. Enable Snippets on a
directory storage under Datacenter, Storage. The two scripts that call it
handle the missing storage differently: vm/ubuntu-vm.sh stops before
anything is created, vm/debian-vm.sh warns and keeps Proxmox VE's own
configuration.
After the VM is running
| Function | What it does |
|---|---|
get_vm_ip <vmid> [seconds] [mac] | Reads the IPv4 address from the guest agent, default 30 seconds. With a MAC only that NIC counts. Without one, interfaces named like docker, br-, veth, podman, cni, wg, tailscale and similar are skipped. Loopback and link-local never count. |
wait_for_cloud_init <vmid> [seconds] [ip] | Runs cloud-init status --wait through the guest agent, default 300 seconds; falls back to SSH for guests without an agent |
display_cloud_init_info | Prints user, password, the credentials file and the hint to use the xterm.js console |
cleanup_cloud_init_credentials | Deletes the credentials file |
has_cloud_init <vmid> | True when ide2 or scsi1 is a Cloud-Init drive |
regenerate_cloud_init <vmid> | qm cloudinit update |
validate_ip_cidr <ip/cidr>, validate_ip <ip> | IPv4 format checks |
pve/vm-app.func
Proxmox VE only. This library installs an application that normally goes into
an LXC container into a VM instead, by running the container's
install/<app>-install.sh inside a cloud image with virt-customize.
No script in ProxmoxVE/vm/ or DevScripts/vm/ sources it today. It expects a
calling script that has loaded pve/vm-core.func and has already set VMID,
HN, CORE_COUNT, RAM_SIZE, DISK_SIZE, BRG, MAC, VLAN, MTU,
STORAGE, MACHINE, CPU_TYPE and START_VM.
| Function | What it does |
|---|---|
list_available_apps | Lists install/*-install.sh through the GitHub contents API of the repository in COMMUNITY_SCRIPTS_URL, without the base-OS install scripts |
select_app [app] | With an argument: checks that the install script exists. Without: a "SELECT APPLICATION" list. Sets APP_INSTALL_SCRIPT. |
get_app_metadata | Reads APP, var_cpu, var_ram, var_disk, var_os and var_version from ct/<app>.sh with grep into APP_NAME, APP_CPU, APP_RAM, APP_DISK, APP_OS, APP_VERSION. Fallbacks: 2 cores, 2048 MiB, 10 GB, Debian 13. |
select_vm_os | "SELECT OS": Debian 13, Debian 12, Ubuntu 24.04, Ubuntu 22.04, preselected from the app's metadata. Sets OS_TYPE, OS_VERSION, OS_CODENAME, OS_DISPLAY. Ubuntu sets USE_CLOUD_INIT=yes, Debian no. |
get_image_url | Debian generic (Cloud-Init) or nocloud qcow2, or the Ubuntu server-cloudimg image, for the host architecture |
download_and_cache_image | vm_fetch_image --cache into CACHE_FILE |
ensure_virt_customize | vm_require_tools virt-customize |
customize_vm_image | Builds WORK_FILE, see below |
create_app_vm | qm create, vm_import_disk, qm set, set_description, setup_cloud_init, qm start |
wait_for_vm_ip | Sets VM_IP through get_vm_ip with a 60 second limit |
show_app_vm_summary | Summary plus the update command |
build_app_vm | Runs ensure_virt_customize, download_and_cache_image, customize_vm_image, create_app_vm, wait_for_vm_ip and show_app_vm_summary, then reports "done". The caller runs select_app, get_app_metadata and select_vm_os first. |
customize_vm_image copies the cached image and then:
- installs
qemu-guest-agent,curl,sudo,ca-certificates,gnupg2,jqandmc; - uploads
lxc/install.funcandlib/tools.functo/opt/community-scripts/; - runs a wrapper with
virt-customize --runthat exports what a container install script expects (FUNCTIONS_FILE_PATH,APPLICATION,app,CTTYPE,PCT_OSTYPE,PCT_OSVERSION,DEPLOY_TARGET="vm") and pipesinstall/<app>-install.shintobash. The output is kept in/tmp/vm-app-install.log; - sets the hostname, empties the machine-id, allows root SSH login for
Cloud-Init images or adds console autologin for
nocloudimages, and callsvm_enable_consoles; - grows the image with
qemu-img resizetoDISK_SIZE.
The install script therefore runs offline, inside the libguestfs appliance,
not in a booted system. The resulting VM is tagged
community-script;app-deployer;<app>. To update the app later, the summary
tells you to run the ct/<app>.sh script inside the VM.
Exit codes
Codes that the VM path uses. The full list is on the exit codes page.
| Code | Raised by | Meaning on the VM path |
|---|---|---|
| 1 | vm_dialog, vm_check_dialog_env, check_hostname_conflict, curl_handler, script checks | Dialog cannot be shown, no terminal, host name in use, unsupported release |
| 103 | shell_check | Shell is not Bash |
| 104 | check_root | Not root, or started through sudo |
| 105 | pve_check | Proxmox VE version not supported. Container scripts exit 1 for the same condition. |
| 106 | vm_require_arch | The image exists for the other architecture only |
| 115 | Scripts, after vm_fetch_image, vm_extract_image or a version lookup | Download failed |
| 119 | vm_select_storage; returned by vm_select_iso_storage | No storage with images content, the preset storage does not exist, or no usable ISO storage. On Incus also returned by the qm-only functions. |
| 205 | vm_claim_vmid | The VM ID typed in the wizard was taken before creation |
| 226 | Scripts, after vm_import_disk | Disk import or a step after creation failed |
| 236 | kvm_check | No /dev/kvm |
| 129, 130, 143 | The signal traps in each script | Terminal closed, Ctrl+C, terminated |
Three things that look like exit codes but are not:
arch_check,ssh_checkandexit_scripton Proxmox VE end the script with a plainexit, without a dedicated code.vm_guest_execreturns 124 when the command in the guest timed out.vm_fetch_image,vm_latest_from_indexandvm_firstboot_unitreturn 2 for an unknown option.
The text the central list gives for 119 and 205 was written for containers ("rootdir content", "Invalid CTID"). On the VM path they mean what the table above says.
What the Incus path does differently
incus/vm-core.func defines the same function names, so shared helpers work on
both hosts. What differs is how a VM comes into existence: the script collects
settings and then calls incus_vm_create <image> <name> <disk-size> once.
incus_vm_create does this:
- Checks
/dev/kvm. - Resolves the image. A file path is converted to qcow2 if needed and imported
with
incus image import. An alias such asimages:debian/13is switched to its/cloudvariant when Cloud-Init is enabled. - Picks a free instance name. An existing name gets a new one instead of an error.
- Runs
incus init <image> <name> --vmwith CPU, memory, firmware, network, storage pool, root disk size and the optional settings. - Sets NIC properties and adds devices (TPM, GPU, USB, PCI, shared folders, port forwards, second disk, agent CD-ROM).
- Writes Cloud-Init into
cloud-init.user-dataanduser.user-data; a static address also intocloud-init.network-config. - Sets the description, starts the VM, waits for the agent
(
VM_AGENT_TIMEOUT, default 180 seconds) and reads the address intoVM_IP.
If the instance was created but could not be started, it is deleted again.
Feature availability
| Feature | Proxmox VE | Incus |
|---|---|---|
Messages, spinner, header_info, telemetry | Yes | Yes |
vm_preflight | Root, amd64 (unless var_arm64), Proxmox VE version, KVM, SSH warning | incus_root_check, x86_64 or aarch64, incus_check, KVM |
VM_UNATTENDED=1 and the VM_* overrides | Yes | Yes. VLAN and MTU hold bare numbers. |
| Wizard dialogs | whiptail through vm_dialog | whiptail through prompt_confirm, prompt_input, prompt_select; text prompts with VM_UI=text or without a terminal |
| Numeric VM ID | Yes | No. get_valid_nextid returns a number for logs only; vm_claim_vmid does nothing. |
Machine type (i440fx, q35, virt) | Yes | No. Recorded, warned about once, not applied. |
| CPU model | Yes | No. Recorded, not applied. |
| Display adapter, NIC model, memory ballooning prompts | Yes | No. Warn once and do nothing. |
| Keyboard layout | vm_prompt_keyboard, applied to the VM and the image | No |
| SeaBIOS / legacy boot | -bios seabios in the script | vm_prompt_csm or vm_prompt_bios set security.csm |
| Secure Boot switch | No prompt | vm_prompt_secureboot, off by default |
| EFI variables disk | vm_alloc_efi_disk or -efidisk0 | Not needed; Incus manages it |
| TPM | By hand: -tpmstate0 | vm_prompt_vtpm adds a tpm device |
| Storage selection | Storages with images content | Incus storage pools, with driver and free space |
| ISO storage, installer ISOs | vm_select_iso_storage | No. Returns 119. |
vm_fetch_image, vm_image_checksum, vm_expand_image, vm_prepare_cloud_image, vm_enable_consoles | Yes | Yes, same code |
| Image cache | /var/lib/vz/template/cache | vm-images under the directory community_scripts_dir returns |
vm_extract_image, vm_alloc_efi_disk, vm_fit_cores | Yes | Not defined |
| Guest agent | qemu-guest-agent installed into the image | incus-agent. vm_install_guest_agent only prints a hint; vm_prompt_agent_disk attaches the agent CD-ROM. |
vm_import_disk, vm_start_vm, vm_wait_guest_agent, vm_guest_exec, vm_wait_for_ip | Yes | No. Each prints an error and returns 119. incus_vm_create covers import, start and waiting. |
vm/common.func (vm_require_tools, vm_release_asset, vm_latest_from_index, vm_firstboot_unit, vm_wait_http, summary) | Yes | Yes |
| Disk resize | qm resize on the named disk | incus config device set <name> root size=... |
| Cloud-Init | vm/cloud-init.func: drive plus qm set | Instance config keys. load_cloud_init_functions returns 1. |
| Cloud-Init SSH keys | Chosen from host keys, pasted or scanned; several | One pasted key (CLOUDINIT_SSHKEY) |
| Cloud-Init static IP | --ipconfig0, optional cicustom snippet | cloud-init.network-config |
| Cloud-Init vendor data | No | vm_prompt_vendor_data |
vm_provision | Calls setup_cloud_init | Sets the instance config keys |
| Description | HTML panel | Plain text with the script page link |
qm sendkey console automation (OpenWrt, OPNsense) | Yes | No equivalent |
pve/vm-app.func | Yes | No |
| GPU, USB and PCI passthrough prompts | No | vm_prompt_gpu, vm_prompt_usb, vm_prompt_pci |
| Shared folders, port forwards, second disk | No | vm_prompt_bind_mounts, vm_prompt_proxy, vm_prompt_extra_disk |
| Disk bus | Fixed by the script | vm_prompt_disk_bus: virtio-blk, virtio-scsi, nvme |
| Disk cache | None or Write Through | none, writeback, unsafe |
| Autostart, snapshot schedule, profiles, cluster member, stateful migration, delete protection, instance type, extra config keys | No | One vm_prompt_* each |
| Network default | vmbr0 in the script | vm_resolve_bridge picks a bridge that exists on the host and asks when several qualify |
| Failure after creation | VM destroyed, or kept after vm_mark_created | Instance deleted when it was created but did not start |
For running Incus scripts, see the Incus guide.
Platforms and architectures
How the engine decides whether it runs on Proxmox VE, Incus or inside a container, which CPU architectures it accepts, and which Proxmox VE versions pass the check.
Telemetry and diagnostics
What the scripts report, when, from which machine, how failures are described, and how to switch reporting off.