368 lines
8.5 KiB
Bash
Executable File
368 lines
8.5 KiB
Bash
Executable File
#!/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 <<SUMMARY
|
|
|
|
VM initialisation:
|
|
Host vars: $HOST_VARS_DIR/*.yml
|
|
Template VMID: $TEMPLATE_VMID
|
|
Default storage: $TEMPLATE_STORAGE
|
|
Default bridge: $TEMPLATE_BRIDGE
|
|
PBS storage: ${PBS_STORAGE:-not set}
|
|
Mode: $([ "$DRY_RUN" -eq 1 ] && printf 'dry-run' || printf 'apply')
|
|
|
|
SUMMARY
|
|
|
|
if ! prompt_confirm "Initialise VMs from host vars?"; then
|
|
die "cancelled"
|
|
fi
|
|
|
|
ensure_template
|
|
|
|
local found=0
|
|
for file in "$HOST_VARS_DIR"/*.yml; do
|
|
[ -e "$file" ] || continue
|
|
found=1
|
|
init_vm_from_host_vars "$file"
|
|
done
|
|
[ "$found" -eq 1 ] || die "no .yml files found in $HOST_VARS_DIR"
|
|
}
|
|
|
|
main "$@"
|