added proxmox cloud image script
This commit is contained in:
@@ -0,0 +1,188 @@
|
|||||||
|
# Proxmox Cloud Image Manager
|
||||||
|
|
||||||
|
Download, customize, and create VM templates from cloud images for popular Linux distributions. Enables fast VM deployment with cloud-init — just like a real cloud provider.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Proxmox VE host** (script auto-elevates with `sudo` if needed)
|
||||||
|
- Packages: `curl`, `qemu-utils`, `libguestfs-tools` (optional, for qemu-guest-agent injection)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: pve-cloudimg.sh [options] <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list [distro] List available distros, cached images, and VM templates
|
||||||
|
download <distro> [ver] Download image into cache only (no VM, no modifications).
|
||||||
|
Use "all" to download every supported distro.
|
||||||
|
create <distro> [ver] Download cloud image (if needed) and create VM template
|
||||||
|
update <distro> [ver] Re-download and re-create a template (remove + create)
|
||||||
|
remove <distro> [ver] Remove cached image and VM template for a distro
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-d, --dir DIR Image cache directory (default: <script-dir>/cloud-images/; set \$PVE_CLOUDIMG_DIR to change default)
|
||||||
|
--storage STORAGE Proxmox storage for VM disks (default: main-storage)
|
||||||
|
--bridge BRIDGE Network bridge (default: vmbr0)
|
||||||
|
--vlan VLAN VLAN tag for the network interface
|
||||||
|
--bios BIOS Firmware: seabios (default) or ovmf (UEFI)
|
||||||
|
--vm-id VMID VM ID for the template (default: auto from 9000)
|
||||||
|
--cores CORES CPU cores (default: 2)
|
||||||
|
--memory MEMORY Memory in MB (default: 2048)
|
||||||
|
--disk-size SIZE Disk size in GB (default: 10)
|
||||||
|
--user USER Cloud-init default user (default: distro-specific)
|
||||||
|
--ssh-key FILE SSH public key file to inject via cloud-init
|
||||||
|
--no-agent Skip installing qemu-guest-agent
|
||||||
|
--packages PKGS Extra packages to install (comma/space-separated, passed to virt-customize)
|
||||||
|
-f, --force Re-download image even if already cached
|
||||||
|
-y, --yes Skip confirmation prompts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List available distros, cached images, and templates
|
||||||
|
./pve-cloudimg.sh list
|
||||||
|
|
||||||
|
# Pre-cache an image (download only, no modifications)
|
||||||
|
./pve-cloudimg.sh download debian
|
||||||
|
|
||||||
|
# Pre-cache every supported distro
|
||||||
|
./pve-cloudimg.sh download all
|
||||||
|
|
||||||
|
# Create a template from the default distro version (Ubuntu 26.04)
|
||||||
|
./pve-cloudimg.sh create ubuntu
|
||||||
|
|
||||||
|
# Create from a specific version with custom settings
|
||||||
|
./pve-cloudimg.sh create ubuntu 24.04 --storage local-btrfs --disk-size 64 --ssh-key ~/.ssh/id_rsa.pub
|
||||||
|
|
||||||
|
# Create with custom user and resources
|
||||||
|
./pve-cloudimg.sh create debian --user admin --cores 4 --memory 8192 --bridge vmbr1
|
||||||
|
|
||||||
|
# Create on a specific VLAN
|
||||||
|
./pve-cloudimg.sh create ubuntu --vlan 100
|
||||||
|
|
||||||
|
# Install extra packages alongside the guest agent
|
||||||
|
./pve-cloudimg.sh create ubuntu --packages "htop nfs-common"
|
||||||
|
|
||||||
|
# Live dangerously — Arch Linux rolling release
|
||||||
|
./pve-cloudimg.sh create arch
|
||||||
|
|
||||||
|
# Force re-download even if cached (adds --force to any command)
|
||||||
|
./pve-cloudimg.sh download ubuntu --force
|
||||||
|
./pve-cloudimg.sh create ubuntu --force
|
||||||
|
|
||||||
|
# Update to the latest image (re-download and re-create template)
|
||||||
|
./pve-cloudimg.sh update ubuntu
|
||||||
|
|
||||||
|
# Replace an existing template with a completely different distro
|
||||||
|
./pve-cloudimg.sh remove ubuntu
|
||||||
|
./pve-cloudimg.sh create debian
|
||||||
|
|
||||||
|
# Remove a template and its cached image
|
||||||
|
./pve-cloudimg.sh remove ubuntu 24.04
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Distributions
|
||||||
|
|
||||||
|
| Distro | Default | Versions | Default User |
|
||||||
|
|--------|---------|----------|-------------|
|
||||||
|
| Ubuntu | 26.04 | 26.04, 24.04 | `ubuntu` |
|
||||||
|
| Debian | 13 | 13, 12 | `debian` |
|
||||||
|
| Arch Linux | rolling | rolling | `arch` |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Download** (download command) — Fetches the official cloud image from the distro's mirrors and caches it locally. No modifications.
|
||||||
|
2. **Resize** (create command only) — Expands the disk to the requested size on a working copy (cloud-init's `growpart` resizes the partition on first boot)
|
||||||
|
3. **First-boot script** (create command only) — Writes a systemd oneshot service via `virt-customize` that installs packages (qemu-guest-agent + any extras) on first boot of each cloned VM
|
||||||
|
4. **Create VM** (create command only) — Configures CPU, memory, VirtIO SCSI controller, serial console, and QEMU guest agent
|
||||||
|
5. **Cloud-Init** (create command only) — Sets default user, optional SSH key, and DHCP networking
|
||||||
|
6. **Template** (create command only) — Converts the VM to a Proxmox template ready for cloning
|
||||||
|
|
||||||
|
Use `download` to just fetch and cache the image without any modifications. Use `create` for the full pipeline (resize + firstboot installer + import + template).
|
||||||
|
|
||||||
|
## Deploying a VM from a Template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the template — repeat for as many VMs as you need
|
||||||
|
qm clone <template-vmid> <new-vmid> --name my-vm --full
|
||||||
|
|
||||||
|
# Or with a specific target storage
|
||||||
|
qm clone <template-vmid> <new-vmid> --name my-vm --full --storage local-btrfs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customizing Cloud-Init Per-VM
|
||||||
|
|
||||||
|
After cloning, edit the clone's cloud-init config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qm set <new-vmid> --ciuser myuser --sshkeys ~/.ssh/id_rsa.pub
|
||||||
|
qm set <new-vmid> --ipconfig0 ip=192.168.1.100/24,gw=192.168.1.1
|
||||||
|
qm set <new-vmid> --nameserver 1.1.1.1
|
||||||
|
qm set <new-vmid> --searchdomain example.com
|
||||||
|
qm start <new-vmid>
|
||||||
|
```
|
||||||
|
|
||||||
|
## QEMU Guest Agent
|
||||||
|
|
||||||
|
Rather than installing packages inside a chroot during image preparation (which breaks on many distros due to Landlock, missing dnf, incompatible arch detection, etc.), the `create` command writes a first-boot installer into the working copy via `virt-customize --firstboot-command`. This creates a systemd oneshot service that runs on the first boot of each cloned VM:
|
||||||
|
|
||||||
|
| Distro family | Runs on first boot |
|
||||||
|
|---------------|-------------------|
|
||||||
|
| Ubuntu, Debian | `apt-get update && apt-get install -y qemu-guest-agent` |
|
||||||
|
| Arch Linux | `pacman -S --noconfirm qemu-guest-agent` |
|
||||||
|
|
||||||
|
The installer runs in the actual VM environment with full kernel support, network access, and proper repos — no chroot limitations.
|
||||||
|
|
||||||
|
The `download` command does **not** apply any first-boot scripts — it only fetches and caches the raw cloud image.
|
||||||
|
|
||||||
|
To skip agent installation: `--no-agent`
|
||||||
|
|
||||||
|
## Installing Extra Packages
|
||||||
|
|
||||||
|
Two ways to add packages beyond `qemu-guest-agent`:
|
||||||
|
|
||||||
|
**Per-run** with `--packages`:
|
||||||
|
```bash
|
||||||
|
./pve-cloudimg.sh create ubuntu --packages "htop nfs-common"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Per-distro** by adding entries right under each `DISTROS` definition in the script:
|
||||||
|
```bash
|
||||||
|
DISTROS["ubuntu:26.04"]="..."
|
||||||
|
DISTRO_PACKAGES["ubuntu:26.04"]="htop nfs-common"
|
||||||
|
```
|
||||||
|
|
||||||
|
Packages with different names across distros get their own `DISTRO_PACKAGES` entry.
|
||||||
|
|
||||||
|
## Adding a New Distribution
|
||||||
|
|
||||||
|
Edit `pve-cloudimg.sh` and add entries near the top of the file. Each distro gets one or more `DISTROS` lines, with an optional `DISTRO_PACKAGES` line directly below:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Image definition
|
||||||
|
DISTROS["mydistro:1.0"]="https://example.com/image.qcow2|filename.qcow2|default-user|qcow2"
|
||||||
|
# Extra packages (optional, right below its DISTROS entry)
|
||||||
|
DISTRO_PACKAGES["mydistro:1.0"]="my-package other-package"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `DISTROS` format is: `url|filename|default_user|image_format`
|
||||||
|
|
||||||
|
Then add a default version so `create mydistro` (without a version) works:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DISTRO_DEFAULT_VERSION[mydistro]="1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Run on the Proxmox host — the script auto-elevates with `sudo` if not run as root
|
||||||
|
- Images are cached in `<script-dir>/cloud-images/` by default — override with `-d` or `$PVE_CLOUDIMG_DIR`
|
||||||
|
- If you add the script to PATH (e.g. `/usr/local/bin/`), set `PVE_CLOUDIMG_DIR=/var/cache/proxmox-cloudimg` in your shell rc so it doesn't try to write into `/usr/local/bin/cloud-images/`
|
||||||
|
- The script auto-increments VM IDs starting at 9000
|
||||||
|
- Cloud images from official distro mirrors — always up to date
|
||||||
|
- Use `-f`/`--force` to re-download an image even if already cached (e.g., to refresh a stale image)
|
||||||
|
- Resized disks use cloud-init's built-in `growpart` for automatic partition resizing on first boot
|
||||||
|
- Templates, clones, and cached images are independent — removing a template or cached image **does not** affect running VMs cloned from it
|
||||||
|
- To replace a template with a different distro, `remove` the old one then `create` the new one — they get separate VM IDs
|
||||||
Executable
+692
@@ -0,0 +1,692 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
SCRIPT_NAME=$(basename "$0")
|
||||||
|
|
||||||
|
# Default images directory — override with PVE_CLOUDIMG_DIR env var or -d flag
|
||||||
|
DEFAULT_IMAGES_DIR="${PVE_CLOUDIMG_DIR:-$SCRIPT_DIR/cloud-images}"
|
||||||
|
IMAGES_DIR="$DEFAULT_IMAGES_DIR"
|
||||||
|
|
||||||
|
# Auto-sudo if not root
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
echo "[*] Elevating to root via sudo..." >&2
|
||||||
|
exec sudo "$0" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Default settings
|
||||||
|
PVE_STORAGE="main-storage"
|
||||||
|
PVE_BRIDGE="vmbr0"
|
||||||
|
PVE_VLAN=""
|
||||||
|
PVE_BIOS="seabios"
|
||||||
|
VM_CORES=2
|
||||||
|
VM_MEMORY=2048
|
||||||
|
VM_DISK_SIZE=10
|
||||||
|
VM_ID=""
|
||||||
|
CLOUD_USER=""
|
||||||
|
SSH_KEY_FILE=""
|
||||||
|
INSTALL_AGENT=1
|
||||||
|
EXTRA_PACKAGES=""
|
||||||
|
FORCE_RELOAD=0
|
||||||
|
FORCE_YES=0
|
||||||
|
|
||||||
|
# Distro definitions (distro:version => URL, filename, user, format)
|
||||||
|
# Format: url|filename|default_user|image_format
|
||||||
|
declare -A DISTROS
|
||||||
|
declare -A DISTRO_PACKAGES
|
||||||
|
|
||||||
|
# Each distro gets: most current version + one back.
|
||||||
|
# Each DISTROS entry can be followed by a DISTRO_PACKAGES entry for extra packages (space-separated).
|
||||||
|
|
||||||
|
# Ubuntu LTS — current 26.04 (resolute), previous 24.04 (noble)
|
||||||
|
DISTROS["ubuntu:26.04"]="https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-amd64.img|resolute-server-cloudimg-amd64.img|ubuntu|img"
|
||||||
|
DISTRO_PACKAGES["ubuntu:26.04"]=""
|
||||||
|
# ---
|
||||||
|
DISTROS["ubuntu:24.04"]="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img|noble-server-cloudimg-amd64.img|ubuntu|img"
|
||||||
|
DISTRO_PACKAGES["ubuntu:24.04"]=""
|
||||||
|
|
||||||
|
# Debian — current stable 13 (trixie), previous 12 (bookworm)
|
||||||
|
DISTROS["debian:13"]="https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2|debian-13-genericcloud-amd64.qcow2|debian|qcow2"
|
||||||
|
DISTRO_PACKAGES["debian:13"]=""
|
||||||
|
# ---
|
||||||
|
DISTROS["debian:12"]="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2|debian-12-genericcloud-amd64.qcow2|debian|qcow2"
|
||||||
|
DISTRO_PACKAGES["debian:12"]=""
|
||||||
|
|
||||||
|
# Arch Linux — rolling release
|
||||||
|
DISTROS["arch:rolling"]="https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2|Arch-Linux-x86_64-cloudimg.qcow2|arch|qcow2"
|
||||||
|
DISTRO_PACKAGES["arch:rolling"]=""
|
||||||
|
|
||||||
|
declare -A DISTRO_DEFAULT_VERSION=(
|
||||||
|
[ubuntu]="26.04"
|
||||||
|
[debian]="13"
|
||||||
|
[arch]="rolling"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Proxmox template name prefix
|
||||||
|
TEMPLATE_PREFIX="cloud"
|
||||||
|
|
||||||
|
log() { echo "[*] $*" >&2; }
|
||||||
|
warn() { echo "[!] $*" >&2; }
|
||||||
|
die() { echo "[FATAL] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $SCRIPT_NAME [options] <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list [distro] List available distros, cached images, and VM templates
|
||||||
|
download <distro> [ver] Download image into cache only (no modifications)
|
||||||
|
Use "all" to download every supported distro
|
||||||
|
create <distro> [ver] Download cloud image (if needed) and create VM template
|
||||||
|
update <distro> [ver] Re-download and re-create a template (remove + create)
|
||||||
|
remove <distro> [ver] Remove cached image and VM template for a distro
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-d, --dir DIR Image cache directory (default: script-dir/cloud-images; set \$PVE_CLOUDIMG_DIR to change default)
|
||||||
|
--storage STORAGE Proxmox storage for VM disks (default: $PVE_STORAGE)
|
||||||
|
--bridge BRIDGE Network bridge (default: $PVE_BRIDGE)
|
||||||
|
--vlan VLAN VLAN tag for the network interface
|
||||||
|
--bios BIOS Firmware: seabios (default) or ovmf (UEFI)
|
||||||
|
--vm-id VMID VM ID for the template (default: auto from 9000)
|
||||||
|
--cores CORES CPU cores (default: $VM_CORES)
|
||||||
|
--memory MEMORY Memory in MB (default: $VM_MEMORY)
|
||||||
|
--disk-size SIZE Disk size in GB (default: $VM_DISK_SIZE)
|
||||||
|
--user USER Cloud-init default user (default: distro-specific)
|
||||||
|
--ssh-key FILE SSH public key file to inject via cloud-init
|
||||||
|
--no-agent Skip installing qemu-guest-agent
|
||||||
|
--packages PKGS Extra packages to install (comma/space-separated, passed to virt-customize)
|
||||||
|
-f, --force Re-download image even if already cached
|
||||||
|
-y, --yes Skip confirmation prompts
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$SCRIPT_NAME list
|
||||||
|
$SCRIPT_NAME list ubuntu
|
||||||
|
$SCRIPT_NAME create ubuntu 24.04
|
||||||
|
$SCRIPT_NAME create ubuntu 24.04 --storage local-btrfs --disk-size 64
|
||||||
|
$SCRIPT_NAME create debian --user admin --ssh-key ~/.ssh/id_rsa.pub
|
||||||
|
$SCRIPT_NAME remove ubuntu 24.04
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Distro helpers ----
|
||||||
|
|
||||||
|
get_distro_field() {
|
||||||
|
local distro=$1 version=$2 field=$3
|
||||||
|
local key="${distro}:${version}"
|
||||||
|
local data="${DISTROS[$key]:-}"
|
||||||
|
if [[ -z "$data" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
case "$field" in
|
||||||
|
url) echo "${data%%|*}" ;;
|
||||||
|
file) echo "${data#*|}" | cut -d'|' -f1 ;;
|
||||||
|
user) echo "${data#*|}" | cut -d'|' -f2 ;;
|
||||||
|
format) echo "${data#*|}" | cut -d'|' -f3 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_distro_version() {
|
||||||
|
local distro=$1 version=$2
|
||||||
|
local key="${distro}:${version}"
|
||||||
|
if [[ -z "${DISTROS[$key]:-}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
list_known_versions() {
|
||||||
|
local distro=$1
|
||||||
|
local versions=()
|
||||||
|
for key in "${!DISTROS[@]}"; do
|
||||||
|
if [[ "$key" == "${distro}:"* ]]; then
|
||||||
|
versions+=("${key#*:}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "${versions[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
list_known_distros() {
|
||||||
|
local distros=()
|
||||||
|
for key in "${!DISTROS[@]}"; do
|
||||||
|
local d="${key%%:*}"
|
||||||
|
local seen=0
|
||||||
|
for seen_d in "${distros[@]}"; do
|
||||||
|
if [[ "$seen_d" == "$d" ]]; then seen=1; break; fi
|
||||||
|
done
|
||||||
|
if [[ $seen -eq 0 ]]; then
|
||||||
|
distros+=("$d")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "${distros[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_default_version() {
|
||||||
|
local distro=$1
|
||||||
|
echo "${DISTRO_DEFAULT_VERSION[$distro]:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
image_cache_dir() {
|
||||||
|
local distro=$1 version=$2
|
||||||
|
echo "$IMAGES_DIR/${distro}-${version}"
|
||||||
|
}
|
||||||
|
|
||||||
|
template_name() {
|
||||||
|
local distro=$1 version=$2
|
||||||
|
# Replace dots with dashes for VM name
|
||||||
|
local ver_safe="${version//./-}"
|
||||||
|
echo "${TEMPLATE_PREFIX}-${distro}-${ver_safe}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Proxmox helpers ----
|
||||||
|
|
||||||
|
check_proxmox() {
|
||||||
|
if ! command -v qm &>/dev/null; then
|
||||||
|
die "Proxmox VE not found — this script must be run on a PVE host"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_storage() {
|
||||||
|
if ! pvesm status 2>/dev/null | awk -v s="$PVE_STORAGE" 'NR>1 && $1==s {f=1} END{exit !f}'; then
|
||||||
|
die "Storage '$PVE_STORAGE' not found — available: $(pvesm status 2>/dev/null | awk 'NR>1{print $1}' | tr '\n' ' ')"
|
||||||
|
fi
|
||||||
|
log "Storage '$PVE_STORAGE' is ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_virt_customize() {
|
||||||
|
if ! command -v virt-customize &>/dev/null; then
|
||||||
|
warn "virt-customize not found — install libguestfs-tools to enable guest agent injection"
|
||||||
|
warn " apt-get update && apt-get install -y libguestfs-tools"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
next_available_vmid() {
|
||||||
|
local start=${1:-9000}
|
||||||
|
while qm status "$start" &>/dev/null; do
|
||||||
|
start=$((start + 1))
|
||||||
|
done
|
||||||
|
echo "$start"
|
||||||
|
}
|
||||||
|
|
||||||
|
template_exists() {
|
||||||
|
local name=$1
|
||||||
|
qm list 2>/dev/null | awk -v name="$name" '$2 == name && $3 == "template" {print $1}' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Commands ----
|
||||||
|
|
||||||
|
list_templates() {
|
||||||
|
local prefix="${1:-}"
|
||||||
|
for conf in /etc/pve/nodes/*/qemu-server/*.conf; do
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
grep -q "^template: 1" "$conf" 2>/dev/null || continue
|
||||||
|
local vmid name
|
||||||
|
vmid=$(basename "$conf" .conf)
|
||||||
|
name=$(grep "^name:" "$conf" 2>/dev/null | sed 's/^name: //')
|
||||||
|
if [[ -z "$prefix" ]] || [[ "$name" == "${prefix}"* ]]; then
|
||||||
|
echo " VM $vmid $name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
local filter="${1:-}"
|
||||||
|
|
||||||
|
if [[ -n "$filter" ]]; then
|
||||||
|
if [[ -z "${DISTRO_DEFAULT_VERSION[$filter]:-}" ]]; then
|
||||||
|
warn "Unknown distro: $filter"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local versions
|
||||||
|
versions=$(list_known_versions "$filter")
|
||||||
|
echo "Distribution: $filter"
|
||||||
|
echo " Known versions: $versions"
|
||||||
|
echo " Default version: $(get_default_version "$filter")"
|
||||||
|
echo ""
|
||||||
|
echo " Cached images:"
|
||||||
|
if [[ -d "$IMAGES_DIR" ]]; then
|
||||||
|
local found=0
|
||||||
|
for dir in "$IMAGES_DIR/${filter}-"*/; do
|
||||||
|
if [[ -d "$dir" ]] && (compgen -G "$dir"*.img &>/dev/null || compgen -G "$dir"*.qcow2 &>/dev/null || compgen -G "$dir"*.raw &>/dev/null); then
|
||||||
|
local dirname size
|
||||||
|
dirname=$(basename "$dir")
|
||||||
|
size=$(du -sh "$dir" 2>/dev/null | cut -f1)
|
||||||
|
echo " $dirname ($size)"
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ $found -eq 0 ]] && echo " (none)"
|
||||||
|
else
|
||||||
|
echo " (none)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo " Templates:"
|
||||||
|
local tmpl
|
||||||
|
tmpl=$(list_templates "${TEMPLATE_PREFIX}-${filter}")
|
||||||
|
echo "${tmpl:- (none)}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Available distributions:"
|
||||||
|
local distros
|
||||||
|
distros=$(list_known_distros)
|
||||||
|
for d in $distros; do
|
||||||
|
local versions
|
||||||
|
versions=$(list_known_versions "$d")
|
||||||
|
local default
|
||||||
|
default=$(get_default_version "$d")
|
||||||
|
echo " $d (default: $default, versions: $versions)"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Cached images:"
|
||||||
|
if [[ -d "$IMAGES_DIR" ]]; then
|
||||||
|
local found=0
|
||||||
|
for dir in "$IMAGES_DIR"/*/; do
|
||||||
|
local dirname
|
||||||
|
dirname=$(basename "$dir")
|
||||||
|
local has_files=0
|
||||||
|
for ext in img qcow2 raw; do
|
||||||
|
if compgen -G "$dir"*."$ext" &>/dev/null; then
|
||||||
|
has_files=1; break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ $has_files -eq 1 ]]; then
|
||||||
|
local size
|
||||||
|
size=$(du -sh "$dir" 2>/dev/null | cut -f1)
|
||||||
|
echo " $dirname ($size)"
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ $found -eq 0 ]] && echo " (none)"
|
||||||
|
else
|
||||||
|
echo " (none)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Proxmox templates:"
|
||||||
|
local tmpl
|
||||||
|
tmpl=$(list_templates)
|
||||||
|
echo "${tmpl:- (none)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Shared image preparation ----
|
||||||
|
|
||||||
|
# Resolve distro metadata, download to cache, optionally customize.
|
||||||
|
# Returns: <cache_path>|<ci_user>|<tname>
|
||||||
|
prepare_image() {
|
||||||
|
local distro=$1 version=${2:-}
|
||||||
|
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
version=$(get_default_version "$distro")
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
die "Unknown distro '$distro'. Available: $(list_known_distros)"
|
||||||
|
fi
|
||||||
|
log "Using default version: $distro $version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! validate_distro_version "$distro" "$version"; then
|
||||||
|
die "Unknown distro/version: $distro $version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local image_file
|
||||||
|
image_file=$(get_distro_field "$distro" "$version" "file")
|
||||||
|
local image_url
|
||||||
|
image_url=$(get_distro_field "$distro" "$version" "url")
|
||||||
|
local default_user
|
||||||
|
default_user=$(get_distro_field "$distro" "$version" "user")
|
||||||
|
|
||||||
|
local cache_dir
|
||||||
|
cache_dir=$(image_cache_dir "$distro" "$version")
|
||||||
|
local cache_path="$cache_dir/$image_file"
|
||||||
|
local tname
|
||||||
|
tname=$(template_name "$distro" "$version")
|
||||||
|
local ci_user="${CLOUD_USER:-$default_user}"
|
||||||
|
|
||||||
|
log "Distribution: $distro $version"
|
||||||
|
log "Cloud-init user: $ci_user"
|
||||||
|
|
||||||
|
# Download image if not cached (or force re-download)
|
||||||
|
if [[ -f "$cache_path" ]]; then
|
||||||
|
if [[ "$FORCE_RELOAD" -eq 1 ]]; then
|
||||||
|
log "Forcing re-download: removing cached image..."
|
||||||
|
rm -f "$cache_path"
|
||||||
|
else
|
||||||
|
log "Image already cached: $cache_path"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$cache_path" ]]; then
|
||||||
|
log "Downloading $distro $version..."
|
||||||
|
mkdir -p "$cache_dir"
|
||||||
|
log " URL: $image_url"
|
||||||
|
curl -fL -o "$cache_path" "$image_url" || {
|
||||||
|
rm -f "$cache_path"
|
||||||
|
die "Download failed"
|
||||||
|
}
|
||||||
|
log "Downloaded: $(du -h "$cache_path" | cut -f1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${cache_path}|${ci_user}|${tname}|${image_file}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_download() {
|
||||||
|
local distro=$1 version=${2:-}
|
||||||
|
|
||||||
|
# Special case: download --all or download all
|
||||||
|
if [[ "$distro" == "all" ]] || [[ "$distro" == "--all" ]]; then
|
||||||
|
log "Downloading all supported distro versions..."
|
||||||
|
local all_keys=()
|
||||||
|
for key in "${!DISTROS[@]}"; do
|
||||||
|
all_keys+=("$key")
|
||||||
|
done
|
||||||
|
# Sort for consistent order
|
||||||
|
IFS=$'\n' all_keys=($(sort <<<"${all_keys[*]}")); unset IFS
|
||||||
|
for key in "${all_keys[@]}"; do
|
||||||
|
local d="${key%%:*}" v="${key#*:}"
|
||||||
|
echo ""
|
||||||
|
log "=== $d $v ==="
|
||||||
|
cmd_download "$d" "$v"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
log "All images downloaded."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(prepare_image "$distro" "$version")
|
||||||
|
local cache_path ci_user tname image_file
|
||||||
|
cache_path=$(echo "$result" | cut -d'|' -f1)
|
||||||
|
ci_user=$(echo "$result" | cut -d'|' -f2)
|
||||||
|
tname=$(echo "$result" | cut -d'|' -f3)
|
||||||
|
image_file=$(echo "$result" | cut -d'|' -f4)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "Image ready at: $cache_path"
|
||||||
|
echo " Distro: $distro $version"
|
||||||
|
echo " Size: $(du -h "$cache_path" | cut -f1)"
|
||||||
|
echo " Cloud-init: user=$ci_user"
|
||||||
|
echo ""
|
||||||
|
echo "To create a VM template from this image:"
|
||||||
|
echo " $SCRIPT_NAME create $distro $version"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_create() {
|
||||||
|
local distro=$1 version=${2:-}
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(prepare_image "$distro" "$version")
|
||||||
|
local cache_path ci_user tname image_file
|
||||||
|
cache_path=$(echo "$result" | cut -d'|' -f1)
|
||||||
|
ci_user=$(echo "$result" | cut -d'|' -f2)
|
||||||
|
tname=$(echo "$result" | cut -d'|' -f3)
|
||||||
|
image_file=$(echo "$result" | cut -d'|' -f4)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "Template name: $tname"
|
||||||
|
|
||||||
|
# Check for existing template
|
||||||
|
local existing_vmid
|
||||||
|
existing_vmid=$(template_exists "$tname")
|
||||||
|
if [[ -n "$existing_vmid" ]]; then
|
||||||
|
if [[ "$FORCE_YES" -ne 1 ]]; then
|
||||||
|
warn "Template '$tname' already exists as VM $existing_vmid"
|
||||||
|
read -r -p "Overwrite? This will destroy the existing template. [y/N] " reply
|
||||||
|
if [[ ! "$reply" =~ ^[Yy]$ ]]; then
|
||||||
|
die "Aborted."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log "Removing existing template VM $existing_vmid..."
|
||||||
|
qm stop "$existing_vmid" &>/dev/null || true
|
||||||
|
qm destroy "$existing_vmid" --purge
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve VM ID
|
||||||
|
local vmid="$VM_ID"
|
||||||
|
if [[ -z "$vmid" ]]; then
|
||||||
|
vmid=$(next_available_vmid 9000)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build network config
|
||||||
|
local netdev="virtio,bridge=${PVE_BRIDGE}"
|
||||||
|
if [[ -n "$PVE_VLAN" ]]; then
|
||||||
|
netdev+=",tag=${PVE_VLAN}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create VM
|
||||||
|
local machine_args=()
|
||||||
|
if [[ "$PVE_BIOS" == "ovmf" ]]; then
|
||||||
|
machine_args=(--machine q35 --efidisk0 "${PVE_STORAGE}:0,format=raw,efitype=4m")
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Creating VM $vmid..."
|
||||||
|
qm create "$vmid" \
|
||||||
|
--name "$tname" \
|
||||||
|
--memory "$VM_MEMORY" \
|
||||||
|
--cores "$VM_CORES" \
|
||||||
|
--net0 "$netdev" \
|
||||||
|
--serial0 socket \
|
||||||
|
--vga serial0 \
|
||||||
|
--agent enabled=1 \
|
||||||
|
"${machine_args[@]}" \
|
||||||
|
--description "Cloud image template: $distro $version (created $(date -u +%Y-%m-%d))"
|
||||||
|
|
||||||
|
# Build package list for first-boot install
|
||||||
|
local PKGLIST=""
|
||||||
|
[[ "$INSTALL_AGENT" -eq 1 ]] && PKGLIST="qemu-guest-agent"
|
||||||
|
local distro_pkgs="${DISTRO_PACKAGES[${distro}:${version}]:-}"
|
||||||
|
if [[ -n "$distro_pkgs" ]]; then PKGLIST="$PKGLIST $distro_pkgs"; fi
|
||||||
|
if [[ -n "$EXTRA_PACKAGES" ]]; then PKGLIST="$PKGLIST $EXTRA_PACKAGES"; fi
|
||||||
|
|
||||||
|
# Create a working copy: resize + firstboot installer, then import
|
||||||
|
local import_image="$cache_path"
|
||||||
|
local workdir=""
|
||||||
|
if [[ "$VM_DISK_SIZE" -gt 0 ]] || [[ -n "$PKGLIST" ]]; then
|
||||||
|
workdir=$(mktemp -d)
|
||||||
|
import_image="$workdir/$image_file"
|
||||||
|
cp "$cache_path" "$import_image"
|
||||||
|
|
||||||
|
if [[ "$VM_DISK_SIZE" -gt 0 ]]; then
|
||||||
|
log "Resizing disk to ${VM_DISK_SIZE}G..."
|
||||||
|
qemu-img resize "$import_image" "${VM_DISK_SIZE}G" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$PKGLIST" ]] && check_virt_customize; then
|
||||||
|
log "Scheduling first-boot install:${PKGLIST}"
|
||||||
|
virt-customize -a "$import_image" --firstboot-command "
|
||||||
|
set -eux
|
||||||
|
command -v apt-get && apt-get update && apt-get install -y ${PKGLIST} && { systemctl enable --now qemu-guest-agent 2>/dev/null || true; } && exit 0
|
||||||
|
command -v pacman && pacman -S --noconfirm ${PKGLIST} && { systemctl enable --now qemu-guest-agent 2>/dev/null || true; } && exit 0
|
||||||
|
" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import disk
|
||||||
|
check_storage
|
||||||
|
log "Importing disk to $PVE_STORAGE..."
|
||||||
|
qm importdisk "$vmid" "$import_image" "$PVE_STORAGE"
|
||||||
|
|
||||||
|
# Read the exact volume reference from the VM config (handles all storage types)
|
||||||
|
local volume_ref
|
||||||
|
volume_ref=$(qm config "$vmid" | awk '/^unused/ {print $2; exit}')
|
||||||
|
if [[ -z "$volume_ref" ]]; then
|
||||||
|
die "Disk import succeeded but no unused disk found in VM $vmid config"
|
||||||
|
fi
|
||||||
|
log "Imported volume: $volume_ref"
|
||||||
|
|
||||||
|
# Attach disk (VirtIO SCSI — required by images lacking virtio-blk driver)
|
||||||
|
qm set "$vmid" \
|
||||||
|
--scsihw virtio-scsi-pci \
|
||||||
|
--scsi0 "$volume_ref,discard=on,ssd=1" \
|
||||||
|
--ide2 "$PVE_STORAGE:cloudinit" \
|
||||||
|
--boot order=scsi0
|
||||||
|
|
||||||
|
# Configure cloud-init
|
||||||
|
log "Configuring cloud-init..."
|
||||||
|
qm set "$vmid" --ciuser "$ci_user"
|
||||||
|
|
||||||
|
if [[ -n "$SSH_KEY_FILE" ]]; then
|
||||||
|
if [[ -f "$SSH_KEY_FILE" ]]; then
|
||||||
|
qm set "$vmid" --sshkeys "$SSH_KEY_FILE"
|
||||||
|
else
|
||||||
|
warn "SSH key file not found: $SSH_KEY_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
qm set "$vmid" --ipconfig0 ip=dhcp
|
||||||
|
|
||||||
|
# Convert to template
|
||||||
|
log "Converting VM $vmid to template..."
|
||||||
|
qm template "$vmid"
|
||||||
|
|
||||||
|
# Clean up working copy
|
||||||
|
if [[ -n "$workdir" ]]; then
|
||||||
|
rm -rf "$workdir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "Template created successfully!"
|
||||||
|
echo " VM ID: $vmid"
|
||||||
|
echo " Name: $tname"
|
||||||
|
echo " Distro: $distro $version"
|
||||||
|
echo " User: $ci_user"
|
||||||
|
echo " Disk: ${VM_DISK_SIZE}G on $PVE_STORAGE"
|
||||||
|
echo " Memory: ${VM_MEMORY}MB"
|
||||||
|
echo " Cores: $VM_CORES"
|
||||||
|
echo ""
|
||||||
|
echo "To clone: qm clone $vmid <new-vmid> --name <vm-name>"
|
||||||
|
echo "To create from CLI: qm clone $vmid 100 --name my-vm --full"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_remove() {
|
||||||
|
local distro=$1 version=${2:-}
|
||||||
|
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
version=$(get_default_version "$distro")
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
die "Unknown distro '$distro'"
|
||||||
|
fi
|
||||||
|
log "Using default version: $distro $version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! validate_distro_version "$distro" "$version"; then
|
||||||
|
die "Unknown distro/version: $distro $version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tname
|
||||||
|
tname=$(template_name "$distro" "$version")
|
||||||
|
|
||||||
|
# Remove VM template
|
||||||
|
local existing_vmid
|
||||||
|
existing_vmid=$(template_exists "$tname")
|
||||||
|
if [[ -n "$existing_vmid" ]]; then
|
||||||
|
if [[ "$FORCE_YES" -ne 1 ]]; then
|
||||||
|
warn "Template '$tname' exists as VM $existing_vmid"
|
||||||
|
read -r -p "Destroy this template? [y/N] " reply
|
||||||
|
if [[ ! "$reply" =~ ^[Yy]$ ]]; then
|
||||||
|
log "Skipping template removal."
|
||||||
|
else
|
||||||
|
log "Destroying template VM $existing_vmid..."
|
||||||
|
qm stop "$existing_vmid" &>/dev/null || true
|
||||||
|
qm destroy "$existing_vmid" --purge
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Destroying template VM $existing_vmid..."
|
||||||
|
qm stop "$existing_vmid" &>/dev/null || true
|
||||||
|
qm destroy "$existing_vmid" --purge
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "No template found for $distro $version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove cached image
|
||||||
|
local cache_dir
|
||||||
|
cache_dir=$(image_cache_dir "$distro" "$version")
|
||||||
|
if [[ -d "$cache_dir" ]]; then
|
||||||
|
if [[ "$FORCE_YES" -ne 1 ]]; then
|
||||||
|
read -r -p "Remove cached image at $cache_dir? [y/N] " reply2
|
||||||
|
if [[ "$reply2" =~ ^[Yy]$ ]]; then
|
||||||
|
rm -rf "$cache_dir"
|
||||||
|
log "Removed: $cache_dir"
|
||||||
|
else
|
||||||
|
log "Keeping cache."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rm -rf "$cache_dir"
|
||||||
|
log "Removed: $cache_dir"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "No cached image for $distro $version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Done."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Main ----
|
||||||
|
|
||||||
|
# Gather all args, parse options anywhere (before or after command)
|
||||||
|
ARGS=()
|
||||||
|
COMMAND=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-d|--dir) IMAGES_DIR="$2"; shift 2 ;;
|
||||||
|
--storage) PVE_STORAGE="$2"; shift 2 ;;
|
||||||
|
--bridge) PVE_BRIDGE="$2"; shift 2 ;;
|
||||||
|
--vlan) PVE_VLAN="$2"; shift 2 ;;
|
||||||
|
--bios) PVE_BIOS="$2"; shift 2 ;;
|
||||||
|
--vm-id) VM_ID="$2"; shift 2 ;;
|
||||||
|
--cores) VM_CORES="$2"; shift 2 ;;
|
||||||
|
--memory) VM_MEMORY="$2"; shift 2 ;;
|
||||||
|
--disk-size) VM_DISK_SIZE="$2"; shift 2 ;;
|
||||||
|
--user) CLOUD_USER="$2"; shift 2 ;;
|
||||||
|
--ssh-key) SSH_KEY_FILE="$2"; shift 2 ;;
|
||||||
|
--no-agent) INSTALL_AGENT=0; shift ;;
|
||||||
|
--packages) EXTRA_PACKAGES="$2"; shift 2 ;;
|
||||||
|
-f|--force) FORCE_RELOAD=1; shift ;;
|
||||||
|
-y|--yes) FORCE_YES=1; shift ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
-*) usage ;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$COMMAND" ]]; then
|
||||||
|
COMMAND="$1"
|
||||||
|
else
|
||||||
|
ARGS+=("$1")
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
# Append any remaining args after --
|
||||||
|
ARGS+=("$@")
|
||||||
|
|
||||||
|
[[ -z "$COMMAND" ]] && usage
|
||||||
|
set -- "${ARGS[@]}"
|
||||||
|
|
||||||
|
case "$COMMAND" in
|
||||||
|
list)
|
||||||
|
cmd_list "$@"
|
||||||
|
;;
|
||||||
|
download)
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
cmd_download "$@"
|
||||||
|
;;
|
||||||
|
create|update)
|
||||||
|
check_proxmox
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
if [[ "$COMMAND" == "update" ]]; then
|
||||||
|
cmd_remove "$@" || true
|
||||||
|
fi
|
||||||
|
cmd_create "$@"
|
||||||
|
;;
|
||||||
|
remove)
|
||||||
|
check_proxmox
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
cmd_remove "$@"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user