Add universal VM template manager script
Introduces vm-manager.sh, a comprehensive Bash script for Proxmox VE to create, deploy, and list VM templates with optional cloud-init and post-install automation. Supports multiple Linux distributions, customizable resources, and post-install options for Docker, Podman, and Portainer.
This commit is contained in:
parent
62ac9e9470
commit
f77c19be52
637
vm/vm-manager.sh
Normal file
637
vm/vm-manager.sh
Normal file
@ -0,0 +1,637 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Universal VM Template Manager - Create, Deploy, List Templates with optional Cloud-Init
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OS IMAGE CATALOG
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
declare -A OS_IMAGES=(
|
||||||
|
# Debian - Cloud-Init enabled
|
||||||
|
["debian-13"]="https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2"
|
||||||
|
["debian-12"]="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2"
|
||||||
|
|
||||||
|
# Debian - NoCloud variants (without cloud-init pre-installed)
|
||||||
|
["debian-13-nocloud"]="https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2"
|
||||||
|
["debian-12-nocloud"]="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.qcow2"
|
||||||
|
|
||||||
|
# Ubuntu - Cloud-Init enabled
|
||||||
|
["ubuntu-24.04"]="https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
|
||||||
|
["ubuntu-22.04"]="https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
|
||||||
|
["ubuntu-20.04"]="https://cloud-images.ubuntu.com/releases/20.04/release/ubuntu-20.04-server-cloudimg-amd64.img"
|
||||||
|
|
||||||
|
# AlmaLinux
|
||||||
|
["alma-9"]="https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2"
|
||||||
|
["alma-8"]="https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2"
|
||||||
|
|
||||||
|
# Rocky Linux
|
||||||
|
["rocky-9"]="https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2"
|
||||||
|
["rocky-8"]="https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2"
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
["fedora-41"]="https://download.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-41-1.4.x86_64.qcow2"
|
||||||
|
["fedora-40"]="https://download.fedoraproject.org/pub/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-40-1.14.x86_64.qcow2"
|
||||||
|
|
||||||
|
# Arch Linux
|
||||||
|
["arch"]="https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2"
|
||||||
|
|
||||||
|
# CentOS Stream
|
||||||
|
["centos-9"]="https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2"
|
||||||
|
|
||||||
|
# Alpine Linux
|
||||||
|
["alpine-3.20"]="https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/cloud/generic_alpine-3.20.0-x86_64-bios-cloudinit-r0.qcow2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
MODE=""
|
||||||
|
TEMPLATE_PREFIX="template"
|
||||||
|
TEMPLATE_ID_START=900
|
||||||
|
VMID=""
|
||||||
|
OS_KEY=""
|
||||||
|
HOSTNAME=""
|
||||||
|
CORES=2
|
||||||
|
MEMORY=2048
|
||||||
|
DISK_SIZE=30
|
||||||
|
STORAGE=""
|
||||||
|
BRIDGE="vmbr0"
|
||||||
|
START_VM="no"
|
||||||
|
MACHINE_TYPE="q35"
|
||||||
|
|
||||||
|
# Cloud-Init Options
|
||||||
|
ENABLE_CLOUDINIT="yes" # yes|no - Enable/disable cloud-init drive
|
||||||
|
CI_USER=""
|
||||||
|
CI_PASSWORD=""
|
||||||
|
CI_SSH_KEY=""
|
||||||
|
CI_FILE=""
|
||||||
|
|
||||||
|
# Post-Install Scripts
|
||||||
|
POST_INSTALL="" # none|docker|podman|portainer
|
||||||
|
POST_INSTALL_TIMEOUT=300 # Timeout in seconds for post-install completion
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;36m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# HELPER FUNCTIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}ℹ${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}✓${NC} $*"; }
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}✗${NC} $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
||||||
|
|
||||||
|
get_next_vmid() {
|
||||||
|
local start_id=${1:-100}
|
||||||
|
local id=$start_id
|
||||||
|
while [ -f "/etc/pve/qemu-server/${id}.conf" ] || [ -f "/etc/pve/lxc/${id}.conf" ]; do
|
||||||
|
id=$((id + 1))
|
||||||
|
done
|
||||||
|
echo "$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_default_storage() {
|
||||||
|
pvesm status -content images 2>/dev/null | awk 'NR==2 {print $1}' || echo "local-lvm"
|
||||||
|
}
|
||||||
|
|
||||||
|
list_storage_pools() {
|
||||||
|
pvesm status -content images 2>/dev/null | awk 'NR>1 {print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
get_snippet_storage() {
|
||||||
|
pvesm status -content snippets 2>/dev/null | awk 'NR==2 {print $1}' || echo "local"
|
||||||
|
}
|
||||||
|
|
||||||
|
find_template_by_name() {
|
||||||
|
local name=$1
|
||||||
|
qm list 2>/dev/null | awk -v n="$name" '$2 == n {print $1; exit}'
|
||||||
|
}
|
||||||
|
|
||||||
|
is_template() {
|
||||||
|
local vmid=$1
|
||||||
|
qm config "$vmid" 2>/dev/null | grep -q "^template: 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_vm() {
|
||||||
|
if [ -n "${VMID:-}" ] && qm status "$VMID" &>/dev/null; then
|
||||||
|
warn "Cleanup: Removing VM $VMID"
|
||||||
|
qm destroy "$VMID" &>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_vm_ready() {
|
||||||
|
local vmid=$1
|
||||||
|
local timeout=${2:-120}
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
info "Waiting for VM $vmid to be ready..."
|
||||||
|
|
||||||
|
while [ $elapsed -lt $timeout ]; do
|
||||||
|
if qm guest exec $vmid -- test -f /usr/bin/systemctl &>/dev/null; then
|
||||||
|
ok "VM is ready"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
elapsed=$((elapsed + 5))
|
||||||
|
done
|
||||||
|
|
||||||
|
warn "VM readiness timeout after ${timeout}s"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_post_install() {
|
||||||
|
local vmid=$1
|
||||||
|
local script_type=$2
|
||||||
|
|
||||||
|
[ -z "$script_type" ] || [ "$script_type" = "none" ] && return 0
|
||||||
|
|
||||||
|
info "Running post-install: $script_type"
|
||||||
|
|
||||||
|
# Wait for VM to be ready
|
||||||
|
wait_for_vm_ready "$vmid" 180 || {
|
||||||
|
warn "VM not ready, skipping post-install"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$script_type" in
|
||||||
|
docker)
|
||||||
|
info "Installing Docker..."
|
||||||
|
qm guest exec "$vmid" -- bash -c '
|
||||||
|
curl -fsSL https://get.docker.com | sh && \
|
||||||
|
systemctl enable --now docker && \
|
||||||
|
usermod -aG docker $(whoami) 2>/dev/null || true
|
||||||
|
' || {
|
||||||
|
warn "Docker installation failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
ok "Docker installed successfully"
|
||||||
|
;;
|
||||||
|
|
||||||
|
podman)
|
||||||
|
info "Installing Podman..."
|
||||||
|
qm guest exec "$vmid" -- bash -c '
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
apt-get update && apt-get install -y podman
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
dnf install -y podman
|
||||||
|
elif command -v yum &>/dev/null; then
|
||||||
|
yum install -y podman
|
||||||
|
else
|
||||||
|
echo "Package manager not supported"
|
||||||
|
exit 1
|
||||||
|
fi && \
|
||||||
|
systemctl enable --now podman || true
|
||||||
|
' || {
|
||||||
|
warn "Podman installation failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
ok "Podman installed successfully"
|
||||||
|
;;
|
||||||
|
|
||||||
|
portainer)
|
||||||
|
info "Installing Portainer (requires Docker)..."
|
||||||
|
# First install Docker
|
||||||
|
run_post_install "$vmid" "docker" || return 1
|
||||||
|
|
||||||
|
info "Deploying Portainer container..."
|
||||||
|
qm guest exec "$vmid" -- bash -c '
|
||||||
|
docker volume create portainer_data && \
|
||||||
|
docker run -d \
|
||||||
|
-p 8000:8000 \
|
||||||
|
-p 9443:9443 \
|
||||||
|
--name portainer \
|
||||||
|
--restart=always \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v portainer_data:/data \
|
||||||
|
portainer/portainer-ce:latest
|
||||||
|
' || {
|
||||||
|
warn "Portainer deployment failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
ok "Portainer deployed successfully"
|
||||||
|
info "Access Portainer at: https://<vm-ip>:9443"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
warn "Unknown post-install script: $script_type"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TEMPLATE OPERATIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
list_templates() {
|
||||||
|
echo -e "\n${BOLD}${CYAN}Available VM Templates:${NC}\n"
|
||||||
|
echo "┌──────┬────────────────────────────┬─────────┬────────┬──────────┬────────────┐"
|
||||||
|
echo "│ VMID │ Name │ Cores │ Memory │ Disk │ Cloud-Init │"
|
||||||
|
echo "├──────┼────────────────────────────┼─────────┼────────┼──────────┼────────────┤"
|
||||||
|
|
||||||
|
local found=0
|
||||||
|
while IFS= read -r line; do
|
||||||
|
local vmid=$(echo "$line" | awk '{print $1}')
|
||||||
|
local name=$(echo "$line" | awk '{print $2}')
|
||||||
|
|
||||||
|
if is_template "$vmid"; then
|
||||||
|
local config=$(qm config "$vmid" 2>/dev/null)
|
||||||
|
local cores=$(echo "$config" | grep "^cores:" | awk '{print $2}')
|
||||||
|
local memory=$(echo "$config" | grep "^memory:" | awk '{print $2}')
|
||||||
|
local disk=$(echo "$config" | grep "scsi0:" | grep -oP '\d+G' | head -1)
|
||||||
|
local has_ci=$(echo "$config" | grep -q "ide2:.*cloudinit" && echo "Yes" || echo "No")
|
||||||
|
|
||||||
|
printf "│ %-4s │ %-26s │ %-7s │ %-6s │ %-8s │ %-10s │\n" \
|
||||||
|
"$vmid" "$name" "${cores:-N/A}" "${memory:-N/A}MB" "${disk:-N/A}" "$has_ci"
|
||||||
|
found=$((found + 1))
|
||||||
|
fi
|
||||||
|
done < <(qm list 2>/dev/null | tail -n +2)
|
||||||
|
|
||||||
|
echo "└──────┴────────────────────────────┴─────────┴────────┴──────────┴────────────┘"
|
||||||
|
|
||||||
|
if [ $found -eq 0 ]; then
|
||||||
|
echo -e "\n${YELLOW}No templates found.${NC}"
|
||||||
|
echo -e "Create one with: $0 create --os <os-key>\n"
|
||||||
|
else
|
||||||
|
echo -e "\n${GREEN}Total: $found template(s)${NC}\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
list_os_options() {
|
||||||
|
echo -e "\n${BOLD}${CYAN}Available OS Images:${NC}\n"
|
||||||
|
local i=1
|
||||||
|
for key in $(echo "${!OS_IMAGES[@]}" | tr ' ' '\n' | sort); do
|
||||||
|
printf "%2d) %-20s %s\n" $i "$key" "${OS_IMAGES[$key]}"
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
create_template() {
|
||||||
|
# Validate OS
|
||||||
|
[ -z "$OS_KEY" ] && error "OS not specified. Use --os <os-key>"
|
||||||
|
[ -z "${OS_IMAGES[$OS_KEY]:-}" ] && error "Unknown OS: $OS_KEY (use --list-os)"
|
||||||
|
|
||||||
|
local image_url="${OS_IMAGES[$OS_KEY]}"
|
||||||
|
local template_name="${TEMPLATE_PREFIX}-${OS_KEY}"
|
||||||
|
|
||||||
|
# Check if template already exists
|
||||||
|
local existing_id=$(find_template_by_name "$template_name")
|
||||||
|
if [ -n "$existing_id" ]; then
|
||||||
|
warn "Template '$template_name' already exists (ID: $existing_id)"
|
||||||
|
read -p "Overwrite? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
info "Aborted"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
qm destroy "$existing_id" &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get VM ID
|
||||||
|
[ -z "$VMID" ] && VMID=$(get_next_vmid $TEMPLATE_ID_START)
|
||||||
|
[ -z "$STORAGE" ] && STORAGE=$(get_default_storage)
|
||||||
|
|
||||||
|
info "Creating template: $template_name (ID: $VMID)"
|
||||||
|
[ "$ENABLE_CLOUDINIT" = "yes" ] && info "Cloud-Init: Enabled" || info "Cloud-Init: Disabled"
|
||||||
|
|
||||||
|
# Download/cache image
|
||||||
|
local cache_dir="/var/lib/vz/template/cache"
|
||||||
|
local image_file="$cache_dir/$(basename "$image_url")"
|
||||||
|
mkdir -p "$cache_dir"
|
||||||
|
|
||||||
|
if [ ! -f "$image_file" ]; then
|
||||||
|
info "Downloading image..."
|
||||||
|
curl -fL --progress-bar -o "$image_file" "$image_url" || error "Download failed"
|
||||||
|
ok "Image downloaded"
|
||||||
|
else
|
||||||
|
ok "Using cached image"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create VM
|
||||||
|
info "Creating VM shell"
|
||||||
|
qm create "$VMID" \
|
||||||
|
--name "$template_name" \
|
||||||
|
--machine "$MACHINE_TYPE" \
|
||||||
|
--bios ovmf \
|
||||||
|
--cores "$CORES" \
|
||||||
|
--memory "$MEMORY" \
|
||||||
|
--net0 "virtio,bridge=$BRIDGE" \
|
||||||
|
--scsihw virtio-scsi-single \
|
||||||
|
--ostype l26 \
|
||||||
|
--agent enabled=1 \
|
||||||
|
>/dev/null || error "VM creation failed"
|
||||||
|
|
||||||
|
ok "VM shell created"
|
||||||
|
|
||||||
|
# Import disk
|
||||||
|
info "Importing disk"
|
||||||
|
local import_out
|
||||||
|
if command -v "qm" &>/dev/null && qm disk import --help &>/dev/null 2>&1; then
|
||||||
|
import_out=$(qm disk import "$VMID" "$image_file" "$STORAGE" --format qcow2 2>&1 || true)
|
||||||
|
else
|
||||||
|
import_out=$(qm importdisk "$VMID" "$image_file" "$STORAGE" 2>&1 || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local disk_ref=$(echo "$import_out" | grep -oP "vm-$VMID-disk-\d+" | head -1)
|
||||||
|
[ -z "$disk_ref" ] && disk_ref=$(pvesm list "$STORAGE" | awk -v id="$VMID" '$5 ~ ("vm-"id"-disk-") {print $5}' | sort | tail -n1)
|
||||||
|
[ -z "$disk_ref" ] && error "Disk import failed"
|
||||||
|
|
||||||
|
ok "Disk imported: $disk_ref"
|
||||||
|
|
||||||
|
# Configure disks
|
||||||
|
info "Configuring disks"
|
||||||
|
if [ "$ENABLE_CLOUDINIT" = "yes" ]; then
|
||||||
|
qm set "$VMID" \
|
||||||
|
--scsi0 "${STORAGE}:${disk_ref},discard=on" \
|
||||||
|
--boot order=scsi0 \
|
||||||
|
--ide2 "${STORAGE}:cloudinit" \
|
||||||
|
>/dev/null || error "Disk configuration failed"
|
||||||
|
else
|
||||||
|
qm set "$VMID" \
|
||||||
|
--scsi0 "${STORAGE}:${disk_ref},discard=on" \
|
||||||
|
--boot order=scsi0 \
|
||||||
|
>/dev/null || error "Disk configuration failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resize disk
|
||||||
|
qm resize "$VMID" scsi0 "${DISK_SIZE}G" >/dev/null 2>&1 || warn "Disk resize failed"
|
||||||
|
ok "Disk configured (${DISK_SIZE}G)"
|
||||||
|
|
||||||
|
# Cloud-Init configuration
|
||||||
|
if [ "$ENABLE_CLOUDINIT" = "yes" ]; then
|
||||||
|
if [ -n "$CI_USER" ] && [ -n "$CI_SSH_KEY" ]; then
|
||||||
|
info "Configuring Cloud-Init credentials"
|
||||||
|
qm set "$VMID" --ciuser "$CI_USER" >/dev/null
|
||||||
|
qm set "$VMID" --sshkeys <(echo "$CI_SSH_KEY") >/dev/null
|
||||||
|
[ -n "$CI_PASSWORD" ] && qm set "$VMID" --cipassword "$CI_PASSWORD" >/dev/null
|
||||||
|
ok "Cloud-Init configured"
|
||||||
|
else
|
||||||
|
info "Cloud-Init drive created (configure after deployment)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert to template
|
||||||
|
info "Converting to template"
|
||||||
|
qm template "$VMID" >/dev/null || error "Template conversion failed"
|
||||||
|
|
||||||
|
ok "Template created successfully!"
|
||||||
|
echo ""
|
||||||
|
echo " Template ID: $VMID"
|
||||||
|
echo " Template Name: $template_name"
|
||||||
|
echo " OS: $OS_KEY"
|
||||||
|
echo " Cloud-Init: $ENABLE_CLOUDINIT"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_from_template() {
|
||||||
|
local template_name="${TEMPLATE_PREFIX}-${OS_KEY}"
|
||||||
|
local template_id=$(find_template_by_name "$template_name")
|
||||||
|
|
||||||
|
[ -z "$template_id" ] && error "Template '$template_name' not found"
|
||||||
|
|
||||||
|
if ! is_template "$template_id"; then
|
||||||
|
error "VM $template_id is not a template"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$VMID" ] && VMID=$(get_next_vmid)
|
||||||
|
[ -z "$HOSTNAME" ] && HOSTNAME="${OS_KEY}-vm-${VMID}"
|
||||||
|
|
||||||
|
info "Cloning template $template_id -> VM $VMID ($HOSTNAME)"
|
||||||
|
|
||||||
|
# Full clone
|
||||||
|
qm clone "$template_id" "$VMID" --name "$HOSTNAME" --full 1 >/dev/null || error "Clone failed"
|
||||||
|
ok "VM cloned"
|
||||||
|
|
||||||
|
# Reconfigure network (remove MAC to get new one)
|
||||||
|
qm set "$VMID" --delete net0 >/dev/null
|
||||||
|
qm set "$VMID" --net0 "virtio,bridge=$BRIDGE" >/dev/null
|
||||||
|
ok "Network reconfigured"
|
||||||
|
|
||||||
|
# Resize if different from template
|
||||||
|
local template_size=$(qm config "$template_id" | grep "scsi0:" | grep -oP '\d+G' | head -1)
|
||||||
|
template_size=${template_size%G}
|
||||||
|
if [ "$DISK_SIZE" -gt "$template_size" ]; then
|
||||||
|
local diff=$((DISK_SIZE - template_size))
|
||||||
|
info "Expanding disk by ${diff}G"
|
||||||
|
qm resize "$VMID" scsi0 "+${diff}G" >/dev/null 2>&1 || warn "Resize failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start VM if requested or if post-install is needed
|
||||||
|
local need_start="no"
|
||||||
|
[ "$START_VM" = "yes" ] && need_start="yes"
|
||||||
|
[ -n "$POST_INSTALL" ] && [ "$POST_INSTALL" != "none" ] && need_start="yes"
|
||||||
|
|
||||||
|
if [ "$need_start" = "yes" ]; then
|
||||||
|
info "Starting VM"
|
||||||
|
qm start "$VMID" || { warn "Start failed"; }
|
||||||
|
ok "VM started"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Execute post-install scripts if specified
|
||||||
|
if [ -n "$POST_INSTALL" ] && [ "$POST_INSTALL" != "none" ]; then
|
||||||
|
if run_post_install "$VMID" "$POST_INSTALL"; then
|
||||||
|
ok "Post-install completed: $POST_INSTALL"
|
||||||
|
else
|
||||||
|
warn "Post-install had issues, but VM is deployed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "VM deployed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo " VM ID: $VMID"
|
||||||
|
echo " Hostname: $HOSTNAME"
|
||||||
|
echo " Template: $template_name (ID: $template_id)"
|
||||||
|
[ -n "$POST_INSTALL" ] && [ "$POST_INSTALL" != "none" ] && echo " Post-Install: $POST_INSTALL"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# USAGE
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
${BOLD}Usage:${NC} $0 <COMMAND> [OPTIONS]
|
||||||
|
|
||||||
|
${BOLD}Commands:${NC}
|
||||||
|
create Create a new VM template
|
||||||
|
deploy Deploy VM from template
|
||||||
|
list List all templates
|
||||||
|
list-os Show available OS images
|
||||||
|
|
||||||
|
${BOLD}Options:${NC}
|
||||||
|
--os KEY OS from catalog (e.g. debian-12, ubuntu-24.04)
|
||||||
|
Use -nocloud suffix for images without cloud-init
|
||||||
|
--vmid ID VM/Template ID (auto-assigned if not specified)
|
||||||
|
--hostname NAME Hostname for deployed VM
|
||||||
|
--cores NUM CPU cores (default: $CORES)
|
||||||
|
--memory MB RAM in MB (default: $MEMORY)
|
||||||
|
--disk GB Disk size in GB (default: $DISK_SIZE)
|
||||||
|
--storage NAME Storage pool
|
||||||
|
--bridge NAME Network bridge (default: $BRIDGE)
|
||||||
|
--start Start VM after deployment
|
||||||
|
--no-cloudinit Disable cloud-init drive (default: enabled)
|
||||||
|
|
||||||
|
${BOLD}Cloud-Init:${NC}
|
||||||
|
--ci-user USER Cloud-Init username
|
||||||
|
--ci-password PASS Cloud-Init password
|
||||||
|
--ci-ssh-key KEY SSH public key
|
||||||
|
|
||||||
|
${BOLD}Post-Install:${NC}
|
||||||
|
--post-install PKG Install software after first boot
|
||||||
|
Options: docker, podman, portainer
|
||||||
|
Note: Requires cloud-init + SSH access
|
||||||
|
|
||||||
|
${BOLD}Examples:${NC}
|
||||||
|
${BOLD}# Create templates${NC}
|
||||||
|
$0 create --os debian-12
|
||||||
|
$0 create --os debian-12-nocloud --no-cloudinit
|
||||||
|
$0 create --os ubuntu-24.04 --cores 4 --memory 4096
|
||||||
|
$0 create --os debian-12 --ci-user admin --ci-ssh-key "ssh-rsa AAA..."
|
||||||
|
|
||||||
|
${BOLD}# Deploy VMs${NC}
|
||||||
|
$0 deploy --os debian-12 --hostname webserver --start
|
||||||
|
$0 deploy --os ubuntu-24.04 --hostname docker-host --post-install docker
|
||||||
|
$0 deploy --os debian-12 --hostname portainer --post-install portainer --start
|
||||||
|
$0 deploy --os rocky-9 --hostname podman-host --post-install podman --disk 100
|
||||||
|
|
||||||
|
${BOLD}# List resources${NC}
|
||||||
|
$0 list
|
||||||
|
$0 list-os
|
||||||
|
|
||||||
|
${BOLD}Post-Install Details:${NC}
|
||||||
|
docker - Installs Docker CE via get.docker.com
|
||||||
|
podman - Installs Podman via system package manager
|
||||||
|
portainer - Installs Docker + Portainer CE container
|
||||||
|
Access at https://<vm-ip>:9443
|
||||||
|
|
||||||
|
${BOLD}NoCloud Images:${NC}
|
||||||
|
NoCloud variants (e.g., debian-12-nocloud) are minimal images
|
||||||
|
without cloud-init pre-installed. Use --no-cloudinit with these.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ARGUMENT PARSING
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[ $# -eq 0 ] && usage
|
||||||
|
|
||||||
|
MODE="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--os)
|
||||||
|
OS_KEY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--vmid)
|
||||||
|
VMID="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--hostname)
|
||||||
|
HOSTNAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--cores)
|
||||||
|
CORES="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--memory)
|
||||||
|
MEMORY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--disk)
|
||||||
|
DISK_SIZE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--storage)
|
||||||
|
STORAGE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--bridge)
|
||||||
|
BRIDGE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--start)
|
||||||
|
START_VM="yes"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-cloudinit)
|
||||||
|
ENABLE_CLOUDINIT="no"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--ci-user)
|
||||||
|
CI_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ci-password)
|
||||||
|
CI_PASSWORD="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ci-ssh-key)
|
||||||
|
CI_SSH_KEY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--post-install)
|
||||||
|
POST_INSTALL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help) usage ;;
|
||||||
|
*) error "Unknown option: $1 (use --help)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CHECKS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[ "$(id -u)" -ne 0 ] && error "Root privileges required"
|
||||||
|
command -v qm >/dev/null 2>&1 || error "qm not found - Is Proxmox VE installed?"
|
||||||
|
command -v pvesm >/dev/null 2>&1 || error "pvesm not found"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
trap cleanup_vm EXIT
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
create)
|
||||||
|
create_template
|
||||||
|
;;
|
||||||
|
deploy)
|
||||||
|
deploy_from_template
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
list_templates
|
||||||
|
;;
|
||||||
|
list-os)
|
||||||
|
list_os_options
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unknown command: $MODE (use --help)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Loading…
x
Reference in New Issue
Block a user