#!/usr/bin/env bash set -euo pipefail HOST_VARS_DIR=host_vars TEMPLATE_VMID=9000 TEMPLATE_STORAGE=local-lvm TEMPLATE_BRIDGE=vmbr0 PBS_STORAGE= DRY_RUN=0 ASSUME_YES=0 INTERACTIVE=1 usage() { cat <<'USAGE' Initialise Proxmox VMs from host_vars/*.yml. Run this on a Proxmox node as root from the repository root. Each host var file must set proxmox_vmid to be managed by this script. Files without proxmox_vmid are skipped. Supported host vars: proxmox_vmid: 101 proxmox_name: cloud proxmox_template_vmid: 9000 proxmox_storage: local-lvm proxmox_bridge: vmbr0 proxmox_memory: 4096 proxmox_cores: 2 proxmox_cpu: host proxmox_disk_size: 64G proxmox_backup_enabled: true proxmox_ipconfig0: ip=dhcp proxmox_onboot: true proxmox_start: false Options: --host-vars-dir PATH Host vars directory. Default: host_vars --template-vmid ID Default template VMID. Default: 9000 --template-storage NAME Storage used if template must be created. Default: local-lvm --template-bridge NAME Bridge used if template must be created. Default: vmbr0 --pbs-storage NAME PBS storage/datastore ID. Passed to template script and used as default backup target note. --dry-run Print commands without executing them. --yes Accept defaults for prompts. --non-interactive Do not prompt. -h, --help Show this help. USAGE } log() { printf '%s\n' "$*" } die() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } run() { if [ "$DRY_RUN" -eq 1 ]; then printf '+' printf ' %q' "$@" printf '\n' return 0 fi "$@" } need_command() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" } prompt_value() { local label=$1 local default_value=$2 local value if [ "$ASSUME_YES" -eq 1 ] || [ "$INTERACTIVE" -eq 0 ] || [ ! -t 0 ]; then printf '%s\n' "$default_value" return 0 fi printf '%s [%s]: ' "$label" "$default_value" >&2 read -r value if [ -z "$value" ]; then printf '%s\n' "$default_value" else printf '%s\n' "$value" fi } prompt_optional_value() { local label=$1 local value if [ "$ASSUME_YES" -eq 1 ] || [ "$INTERACTIVE" -eq 0 ] || [ ! -t 0 ]; then printf '\n' return 0 fi printf '%s [leave empty]: ' "$label" >&2 read -r value printf '%s\n' "$value" } prompt_confirm() { local label=$1 local value if [ "$ASSUME_YES" -eq 1 ] || [ "$INTERACTIVE" -eq 0 ] || [ ! -t 0 ]; then return 0 fi printf '%s [Y/n]: ' "$label" >&2 read -r value case "$value" in ""|y|Y|yes|YES) return 0 ;; *) return 1 ;; esac } yaml_scalar() { local file=$1 local key=$2 awk -v key="$key" ' $0 ~ "^[[:space:]]*" key ":" { sub("^[[:space:]]*" key ":[[:space:]]*", "", $0) sub("[[:space:]]+#.*$", "", $0) gsub(/^["'\'']|["'\'']$/, "", $0) print $0 exit } ' "$file" } bool_true() { case "${1:-}" in true|True|TRUE|yes|Yes|YES|1|on|On|ON) return 0 ;; *) return 1 ;; esac } parse_args() { while [ "$#" -gt 0 ]; do case "$1" in --host-vars-dir) HOST_VARS_DIR=${2:?missing value for --host-vars-dir} shift 2 ;; --template-vmid) TEMPLATE_VMID=${2:?missing value for --template-vmid} shift 2 ;; --template-storage) TEMPLATE_STORAGE=${2:?missing value for --template-storage} shift 2 ;; --template-bridge) TEMPLATE_BRIDGE=${2:?missing value for --template-bridge} shift 2 ;; --pbs-storage) PBS_STORAGE=${2:?missing value for --pbs-storage} shift 2 ;; --dry-run) DRY_RUN=1 shift ;; --yes) ASSUME_YES=1 shift ;; --non-interactive) INTERACTIVE=0 shift ;; -h|--help) usage exit 0 ;; *) die "unknown option: $1" ;; esac done } collect_config() { TEMPLATE_VMID=$(prompt_value "Default template VMID" "$TEMPLATE_VMID") TEMPLATE_STORAGE=$(prompt_value "Default template/storage name" "$TEMPLATE_STORAGE") TEMPLATE_BRIDGE=$(prompt_value "Default bridge" "$TEMPLATE_BRIDGE") if [ -z "$PBS_STORAGE" ]; then PBS_STORAGE=$(prompt_optional_value "PBS storage/datastore ID") fi } template_exists() { if [ "$DRY_RUN" -eq 1 ]; then return 1 fi qm status "$TEMPLATE_VMID" >/dev/null 2>&1 } vm_exists() { local vmid=$1 if [ "$DRY_RUN" -eq 1 ]; then return 1 fi qm status "$vmid" >/dev/null 2>&1 } ensure_template() { if template_exists; then log "Template VMID $TEMPLATE_VMID already exists." return 0 fi log "Template VMID $TEMPLATE_VMID not found; creating it first." local args=( scripts/create-proxmox-template.sh --vmid "$TEMPLATE_VMID" --storage "$TEMPLATE_STORAGE" --bridge "$TEMPLATE_BRIDGE" --yes --non-interactive ) if [ -n "$PBS_STORAGE" ]; then args+=(--pbs-storage "$PBS_STORAGE") fi if [ "$DRY_RUN" -eq 1 ]; then args+=(--dry-run) fi run "${args[@]}" } disk_volume() { local vmid=$1 local storage=$2 if [ "$DRY_RUN" -eq 1 ]; then printf '%s:vm-%s-disk-0\n' "$storage" "$vmid" return 0 fi qm config "$vmid" | awk -F': ' '/^scsi0:/{split($2, parts, ","); print parts[1]; exit}' } init_vm_from_host_vars() { local file=$1 local stem stem=$(basename "$file" .yml) local vmid vmid=$(yaml_scalar "$file" proxmox_vmid) if [ -z "$vmid" ]; then log "Skipping $file: proxmox_vmid not set" return 0 fi local name template_vmid storage bridge memory cores cpu disk_size backup_enabled ipconfig0 onboot start_vm name=$(yaml_scalar "$file" proxmox_name) [ -n "$name" ] || name=$(yaml_scalar "$file" nbops_hostname) [ -n "$name" ] || name=$stem template_vmid=$(yaml_scalar "$file" proxmox_template_vmid) [ -n "$template_vmid" ] || template_vmid=$TEMPLATE_VMID storage=$(yaml_scalar "$file" proxmox_storage) [ -n "$storage" ] || storage=$TEMPLATE_STORAGE bridge=$(yaml_scalar "$file" proxmox_bridge) [ -n "$bridge" ] || bridge=$TEMPLATE_BRIDGE memory=$(yaml_scalar "$file" proxmox_memory) [ -n "$memory" ] || memory=2048 cores=$(yaml_scalar "$file" proxmox_cores) [ -n "$cores" ] || cores=2 cpu=$(yaml_scalar "$file" proxmox_cpu) [ -n "$cpu" ] || cpu=host disk_size=$(yaml_scalar "$file" proxmox_disk_size) [ -n "$disk_size" ] || disk_size=32G backup_enabled=$(yaml_scalar "$file" proxmox_backup_enabled) [ -n "$backup_enabled" ] || backup_enabled=false ipconfig0=$(yaml_scalar "$file" proxmox_ipconfig0) [ -n "$ipconfig0" ] || ipconfig0=ip=dhcp onboot=$(yaml_scalar "$file" proxmox_onboot) [ -n "$onboot" ] || onboot=true start_vm=$(yaml_scalar "$file" proxmox_start) [ -n "$start_vm" ] || start_vm=false if vm_exists "$vmid"; then log "Skipping $name ($vmid): VM already exists" return 0 fi log "Initialising $name ($vmid) from $file" run qm clone "$template_vmid" "$vmid" --name "$name" --full true --storage "$storage" run qm set "$vmid" --memory "$memory" --cores "$cores" --cpu "$cpu" run qm set "$vmid" --net0 "virtio,bridge=$bridge" run qm set "$vmid" --ipconfig0 "$ipconfig0" if bool_true "$onboot"; then run qm set "$vmid" --onboot 1 else run qm set "$vmid" --onboot 0 fi local volume backup_flag volume=$(disk_volume "$vmid" "$storage") backup_flag=0 if bool_true "$backup_enabled"; then backup_flag=1 fi run qm set "$vmid" --scsi0 "$volume,discard=on,ssd=1,backup=$backup_flag" if [ -n "$disk_size" ]; then run qm resize "$vmid" scsi0 "$disk_size" fi if bool_true "$start_vm"; then run qm start "$vmid" fi } main() { parse_args "$@" collect_config if [ "$DRY_RUN" -eq 0 ]; then need_command qm need_command awk [ "$(id -u)" -eq 0 ] || die "run this script as root on a Proxmox node" fi [ -d "$HOST_VARS_DIR" ] || die "host vars directory not found: $HOST_VARS_DIR" cat <