honestly its been months, i found unstaged changes and forgot
This commit is contained in:
1 parent
b5d1ffda95
commit
ef8d5645e3
55 files changed
+1554
-180
No files matched your search
Executable
+425
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VMID=9000
|
||||
NAME=debian-13-genericcloud
|
||||
IMAGE_URL=https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
|
||||
IMAGE_FILE=
|
||||
CACHE_DIR=/var/lib/vz/template/qcow2
|
||||
STORAGE=local-lvm
|
||||
CLOUDINIT_STORAGE=
|
||||
BRIDGE=vmbr0
|
||||
MEMORY=2048
|
||||
CORES=2
|
||||
CPU=host
|
||||
DISK_SIZE=32G
|
||||
MACHINE=q35
|
||||
BIOS=ovmf
|
||||
SCSI_HW=virtio-scsi-pci
|
||||
NET_MODEL=virtio
|
||||
PBS_STORAGE=
|
||||
DRY_RUN=0
|
||||
ASSUME_YES=0
|
||||
INTERACTIVE=1
|
||||
VMID_SET=0
|
||||
NAME_SET=0
|
||||
IMAGE_URL_SET=0
|
||||
IMAGE_FILE_SET=0
|
||||
CACHE_DIR_SET=0
|
||||
STORAGE_SET=0
|
||||
CLOUDINIT_STORAGE_SET=0
|
||||
BRIDGE_SET=0
|
||||
MEMORY_SET=0
|
||||
CORES_SET=0
|
||||
CPU_SET=0
|
||||
DISK_SIZE_SET=0
|
||||
PBS_STORAGE_SET=0
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Create a Proxmox VM template from the Debian 13 GenericCloud image.
|
||||
|
||||
Run this on a Proxmox node as root.
|
||||
|
||||
Usage:
|
||||
scripts/create-proxmox-template.sh [options]
|
||||
|
||||
Options:
|
||||
--vmid ID Template VMID. Default: 9000
|
||||
--name NAME Template name. Default: debian-13-genericcloud
|
||||
--storage STORAGE Disk storage. Default: local-lvm
|
||||
--cloudinit-storage NAME Cloud-Init storage. Default: same as --storage
|
||||
--bridge BRIDGE Network bridge. Default: vmbr0
|
||||
--memory MB Memory in MB. Default: 2048
|
||||
--cores N vCPU count. Default: 2
|
||||
--cpu TYPE CPU type. Default: host
|
||||
--disk-size SIZE Resize imported disk to this size. Default: 32G
|
||||
--image-url URL Debian GenericCloud qcow2 URL.
|
||||
--image-file PATH Use an existing local qcow2 instead of downloading.
|
||||
--cache-dir PATH Download cache directory. Default: /var/lib/vz/template/qcow2
|
||||
--pbs-storage NAME PBS storage/datastore ID. Enables main disk backup flag when set.
|
||||
--dry-run Print commands without executing them.
|
||||
--yes Accept defaults for any unanswered prompts.
|
||||
--non-interactive Do not prompt; use defaults and provided flags.
|
||||
-h, --help Show this help.
|
||||
|
||||
The template is configured with:
|
||||
- Q35 machine type
|
||||
- OVMF / UEFI
|
||||
- VirtIO SCSI
|
||||
- VirtIO network
|
||||
- QEMU guest agent
|
||||
- Cloud-Init drive
|
||||
- serial0 socket
|
||||
- vga serial0 for Proxmox xterm.js console support
|
||||
USAGE
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
download_image() {
|
||||
local url=$1
|
||||
local dest=$2
|
||||
|
||||
if [ -s "$dest" ]; then
|
||||
log "Using cached image: $dest"
|
||||
return 0
|
||||
fi
|
||||
|
||||
run mkdir -p "$(dirname "$dest")"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
run curl -fL --retry 3 --retry-delay 2 -o "$dest" "$url"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
run wget -O "$dest" "$url"
|
||||
else
|
||||
die "curl or wget is required to download the image"
|
||||
fi
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--vmid)
|
||||
VMID=${2:?missing value for --vmid}
|
||||
VMID_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--name)
|
||||
NAME=${2:?missing value for --name}
|
||||
NAME_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--storage)
|
||||
STORAGE=${2:?missing value for --storage}
|
||||
STORAGE_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--cloudinit-storage)
|
||||
CLOUDINIT_STORAGE=${2:?missing value for --cloudinit-storage}
|
||||
CLOUDINIT_STORAGE_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--bridge)
|
||||
BRIDGE=${2:?missing value for --bridge}
|
||||
BRIDGE_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--memory)
|
||||
MEMORY=${2:?missing value for --memory}
|
||||
MEMORY_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--cores)
|
||||
CORES=${2:?missing value for --cores}
|
||||
CORES_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--cpu)
|
||||
CPU=${2:?missing value for --cpu}
|
||||
CPU_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--disk-size)
|
||||
DISK_SIZE=${2:?missing value for --disk-size}
|
||||
DISK_SIZE_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--image-url)
|
||||
IMAGE_URL=${2:?missing value for --image-url}
|
||||
IMAGE_URL_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--image-file)
|
||||
IMAGE_FILE=${2:?missing value for --image-file}
|
||||
IMAGE_FILE_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--cache-dir)
|
||||
CACHE_DIR=${2:?missing value for --cache-dir}
|
||||
CACHE_DIR_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--pbs-storage)
|
||||
PBS_STORAGE=${2:?missing value for --pbs-storage}
|
||||
PBS_STORAGE_SET=1
|
||||
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_interactive_config() {
|
||||
if [ "$VMID_SET" -eq 0 ]; then
|
||||
VMID=$(prompt_value "Template VMID" "$VMID")
|
||||
fi
|
||||
if [ "$NAME_SET" -eq 0 ]; then
|
||||
NAME=$(prompt_value "Template name" "$NAME")
|
||||
fi
|
||||
if [ "$STORAGE_SET" -eq 0 ]; then
|
||||
STORAGE=$(prompt_value "Disk storage" "$STORAGE")
|
||||
fi
|
||||
if [ "$CLOUDINIT_STORAGE_SET" -eq 0 ]; then
|
||||
CLOUDINIT_STORAGE=$(prompt_value "Cloud-Init storage" "${CLOUDINIT_STORAGE:-$STORAGE}")
|
||||
fi
|
||||
if [ "$BRIDGE_SET" -eq 0 ]; then
|
||||
BRIDGE=$(prompt_value "Network bridge" "$BRIDGE")
|
||||
fi
|
||||
if [ "$MEMORY_SET" -eq 0 ]; then
|
||||
MEMORY=$(prompt_value "Memory MB" "$MEMORY")
|
||||
fi
|
||||
if [ "$CORES_SET" -eq 0 ]; then
|
||||
CORES=$(prompt_value "vCPU cores" "$CORES")
|
||||
fi
|
||||
if [ "$CPU_SET" -eq 0 ]; then
|
||||
CPU=$(prompt_value "CPU type" "$CPU")
|
||||
fi
|
||||
if [ "$DISK_SIZE_SET" -eq 0 ]; then
|
||||
DISK_SIZE=$(prompt_value "Disk size after resize" "$DISK_SIZE")
|
||||
fi
|
||||
if [ "$CACHE_DIR_SET" -eq 0 ]; then
|
||||
CACHE_DIR=$(prompt_value "Image cache directory" "$CACHE_DIR")
|
||||
fi
|
||||
if [ "$IMAGE_FILE_SET" -eq 0 ]; then
|
||||
IMAGE_FILE=$(prompt_optional_value "Local qcow2 image file")
|
||||
fi
|
||||
if [ -z "$IMAGE_FILE" ] && [ "$IMAGE_URL_SET" -eq 0 ]; then
|
||||
IMAGE_URL=$(prompt_value "Debian GenericCloud image URL" "$IMAGE_URL")
|
||||
fi
|
||||
if [ "$PBS_STORAGE_SET" -eq 0 ]; then
|
||||
PBS_STORAGE=$(prompt_optional_value "PBS storage/datastore ID for backup flag")
|
||||
fi
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
cat <<SUMMARY
|
||||
|
||||
Template configuration:
|
||||
VMID: $VMID
|
||||
Name: $NAME
|
||||
Disk storage: $STORAGE
|
||||
Cloud-Init storage: $CLOUDINIT_STORAGE
|
||||
Bridge: $BRIDGE
|
||||
Memory: $MEMORY MB
|
||||
Cores: $CORES
|
||||
CPU: $CPU
|
||||
Disk size: $DISK_SIZE
|
||||
Machine: $MACHINE
|
||||
BIOS: $BIOS
|
||||
Serial console: serial0 socket, vga serial0
|
||||
SUMMARY
|
||||
|
||||
if [ -n "$IMAGE_FILE" ]; then
|
||||
printf ' Image file: %s\n' "$IMAGE_FILE"
|
||||
else
|
||||
printf ' Image URL: %s\n' "$IMAGE_URL"
|
||||
printf ' Cache directory: %s\n' "$CACHE_DIR"
|
||||
fi
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
printf ' Mode: dry-run\n'
|
||||
fi
|
||||
if [ -n "$PBS_STORAGE" ]; then
|
||||
printf ' Backup flag: enabled for main disk; PBS storage: %s\n' "$PBS_STORAGE"
|
||||
else
|
||||
printf ' Backup flag: disabled for main disk\n'
|
||||
fi
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
collect_interactive_config
|
||||
|
||||
if [ -z "$CLOUDINIT_STORAGE" ]; then
|
||||
CLOUDINIT_STORAGE=$STORAGE
|
||||
fi
|
||||
|
||||
print_summary
|
||||
if ! prompt_confirm "Create this Proxmox template?"; then
|
||||
die "cancelled"
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
need_command qm
|
||||
need_command awk
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -ne 0 ] && [ "$DRY_RUN" -eq 0 ]; then
|
||||
die "run this script as root on a Proxmox node"
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" -eq 0 ] && qm status "$VMID" >/dev/null 2>&1; then
|
||||
die "VMID $VMID already exists; choose another --vmid or remove it manually"
|
||||
fi
|
||||
|
||||
local image_path
|
||||
if [ -n "$IMAGE_FILE" ]; then
|
||||
image_path=$IMAGE_FILE
|
||||
[ "$DRY_RUN" -eq 1 ] || [ -f "$image_path" ] || die "image file not found: $image_path"
|
||||
else
|
||||
image_path="$CACHE_DIR/$(basename "$IMAGE_URL")"
|
||||
download_image "$IMAGE_URL" "$image_path"
|
||||
fi
|
||||
|
||||
run qm create "$VMID" \
|
||||
--name "$NAME" \
|
||||
--ostype l26 \
|
||||
--machine "$MACHINE" \
|
||||
--bios "$BIOS" \
|
||||
--memory "$MEMORY" \
|
||||
--cores "$CORES" \
|
||||
--cpu "$CPU" \
|
||||
--agent enabled=1 \
|
||||
--scsihw "$SCSI_HW" \
|
||||
--net0 "$NET_MODEL,bridge=$BRIDGE" \
|
||||
--serial0 socket \
|
||||
--vga serial0
|
||||
|
||||
if [ "$BIOS" = "ovmf" ]; then
|
||||
run qm set "$VMID" --efidisk0 "$STORAGE:0,pre-enrolled-keys=0"
|
||||
fi
|
||||
|
||||
run qm importdisk "$VMID" "$image_path" "$STORAGE"
|
||||
|
||||
local imported_disk
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
imported_disk="$STORAGE:vm-$VMID-disk-0"
|
||||
log "Dry-run assumption: imported disk will be $imported_disk"
|
||||
else
|
||||
imported_disk=$(qm config "$VMID" | awk -F': ' '/^unused[0-9]+:/{print $2; exit}')
|
||||
[ -n "$imported_disk" ] || die "could not find imported unused disk in qm config for VMID $VMID"
|
||||
fi
|
||||
|
||||
local disk_backup=0
|
||||
if [ -n "$PBS_STORAGE" ]; then
|
||||
disk_backup=1
|
||||
fi
|
||||
|
||||
run qm set "$VMID" --scsi0 "$imported_disk,discard=on,ssd=1,backup=$disk_backup"
|
||||
run qm set "$VMID" --ide2 "$CLOUDINIT_STORAGE:cloudinit"
|
||||
run qm set "$VMID" --boot order=scsi0
|
||||
run qm set "$VMID" --ipconfig0 ip=dhcp
|
||||
|
||||
if [ -n "$DISK_SIZE" ]; then
|
||||
run qm resize "$VMID" scsi0 "$DISK_SIZE"
|
||||
fi
|
||||
|
||||
run qm template "$VMID"
|
||||
|
||||
log "Created Proxmox template $NAME ($VMID)"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in new issue
Block a user