-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup_ubuntu_cloudinit_image.sh
More file actions
367 lines (326 loc) · 13.9 KB
/
setup_ubuntu_cloudinit_image.sh
File metadata and controls
367 lines (326 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#!/usr/bin/env -S bash -eEuo pipefail
# =============================================================================
# Proxmox Ubuntu Cloud‑Init Template Builder
# Version: 4.1 — 2025‑04‑20
# =============================================================================
# - flock‑based locking with configurable path
# - Cleanup on EXIT, INT, TERM, and ERR (with function+line reporting)
# - "inherit_errexit" where available for safer pipelines
# - Storage‑aware free‑space checks (pvesm) plus release validation
# - Bigger 4 MiB EFI disk for OVMF
# - Password‑less Cloud‑Init user (SSH key only); aborts if no key
# - Optional --dry‑run mode (prints qm / pvesm commands only)
# - Optional --color=auto|always|never and NO_COLOR env support
# - Optional --keyfile/-k to select SSH public key
# - Config file /etc/proxmox-template.conf to persist defaults
# - Log file rotation‑friendly naming
# =============================================================================
# shellcheck disable=SC1090
shopt -s inherit_errexit nullglob 2>/dev/null || true
umask 077
# -----------------------------------------------------------------------------
# Optional global defaults – override any of the CLI flags below.
# Put key=value pairs in /etc/proxmox-template.conf (no quotes needed).
# -----------------------------------------------------------------------------
[[ -f /etc/proxmox-template.conf ]] && source /etc/proxmox-template.conf
###############################################################################
# Globals
###############################################################################
readonly SCRIPT_PATH="$(readlink -f "$0")"
readonly SCRIPT_NAME="$(basename "$SCRIPT_PATH")"
readonly SCRIPT_BASE="${SCRIPT_NAME%.*}"
readonly LOG_FILE="/var/log/${SCRIPT_BASE}.log"
readonly LOCK_FILE="/run/${SCRIPT_BASE}.lock"
readonly RETRY_COUNT=3
readonly WAIT_TIME=5
DRY_RUN=${DRY_RUN:-0}
COLOR_MODE="${COLOR_MODE:-auto}" # auto|always|never; overridden by --color
###############################################################################
# Colour handling (NO_COLOR & --color)
###############################################################################
declare -Ag C=( [BOLD]="" [GREEN]="" [RED]="" [YELLOW]="" [RESET]="" )
setup_colors() {
if [[ -n ${NO_COLOR:-} ]]; then COLOR_MODE="never"; fi
if [[ $COLOR_MODE != "never" && ( $COLOR_MODE == "always" || -t 1 ) ]]; then
C[BOLD]="$(tput bold)"
C[GREEN]="$(tput setaf 2)"
C[RED]="$(tput setaf 1)"
C[YELLOW]="$(tput setaf 3)"
C[RESET]="$(tput sgr0)"
fi
}
setup_colors
###############################################################################
# Logging helpers
###############################################################################
log() {
local lvl="$1"; shift
printf '[%(%F %T)T] [%s] %b%b%b\n' -1 "$lvl" "${C[BOLD]}" "$*" "${C[RESET]}" \
| tee -a "$LOG_FILE" >&2
}
die() { log ERROR "$*"; exit 1; }
warn() { log WARN "$*"; }
info() { log INFO "$*"; }
error_exit() { die "$@"; }
###############################################################################
# Command wrapper (honours --dry-run)
###############################################################################
run() {
if (( DRY_RUN )); then
info "(dry-run) $*"
return 0
fi
"$@"
}
###############################################################################
# Locking via flock
###############################################################################
exec 9>"$LOCK_FILE" || die "Cannot open lock file $LOCK_FILE"
flock -n 9 || die "Another instance is already running (lock: $LOCK_FILE)"
###############################################################################
# Global temp dir
###############################################################################
TEMP_DIR="$(mktemp -d -t "${SCRIPT_BASE}.XXXXXX")"
###############################################################################
# Cleanup traps
###############################################################################
cleanup() {
info "Running cleanup …"
[[ -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR"
[[ -f "${IMAGENAME:-}" ]] && rm -f "${IMAGENAME}"
if [[ -n "${VMID:-}" ]] && qm status "$VMID" &>/dev/null; then
warn "Removing partially created VM $VMID …"
run qm destroy "$VMID" --purge >/dev/null 2>&1 || true
fi
}
trap 'die "Unexpected error in ${FUNCNAME[0]} at line ${LINENO}"' ERR
trap cleanup EXIT INT TERM
###############################################################################
# Helper: storage-space check
###############################################################################
check_storage_space() {
local storage="$1" required_gb="$2" avail
avail="$(pvesm status --storage "$storage" --verbose 2>/dev/null | awk '/Avail/ {print $2}' | sed 's/G//')"
if [[ -z "$avail" ]]; then
warn "Could not determine free space on $storage (skipping size check)."
return 0
fi
(( avail < required_gb )) && die "Not enough free space on $storage: need ${required_gb}G, have ${avail}G"
}
###############################################################################
# Helper: ensure disk operations done
###############################################################################
ensure_disk_ready() {
local vmid="$1" storage="$2" volume
if pvesm path "$storage:base-${vmid}-disk-0" &>/dev/null; then
volume="$(pvesm path "$storage:base-${vmid}-disk-0")"
else
volume="$(pvesm path "$storage:vm-${vmid}-disk-0")"
fi
if [[ -n "$volume" && -b "$volume" ]] && command -v lvs >/dev/null 2>&1; then
info "Waiting for LVM volume to settle …"
for _ in {1..30}; do
lvs --noheadings -o lv_path "$volume" &>/dev/null && { sleep 1; return; } || true
sleep 1
done
die "Timeout waiting for volume $volume"
else
sync; sleep 2
fi
}
###############################################################################
# Helper: verify network
###############################################################################
verify_network() {
local bridge="$1"
ip link show "$bridge" &>/dev/null || die "Bridge $bridge does not exist"
curl -fsSLI --max-time 3 https://cloud-images.ubuntu.com/ >/dev/null || \
die "No internet connectivity to cloud‑image mirror"
}
###############################################################################
# Helper: requirement checks
###############################################################################
check_requirements() {
local cmds=(wget qm pvesm sha256sum curl ip awk sed pvesh)
for c in "${cmds[@]}"; do
if ! command -v "$c" >/dev/null 2>&1; then
die "Required command '$c' not found. Please install it and retry."
fi
done
[[ $(id -u) -eq 0 ]] || die "Script must run as root"
pveversion >/dev/null 2>&1 || die "Not a Proxmox VE system (pveversion missing)"
}
###############################################################################
# Usage
###############################################################################
usage() {
cat <<EOF
${C[BOLD]}Usage:${C[RESET]} ${SCRIPT_NAME} [options]
Options:
-i VMID Explicit VMID (default: next free ID)
-n NAME VM/Template name (default: ubuntu-2404-template)
-s STORAGE Proxmox storage (default: local-lvm)
-b BRIDGE Network bridge (default: vmbr0)
-m MEMORY Memory in MB (default: 2048)
-c CORES CPU cores (default: 1)
-d DISKSIZE Extra disk size in GB (default: 10)
-r RELEASE Ubuntu release (noble, jammy, mantic …; default: noble)
-k KEYFILE SSH public‑key file (default: first .pub in ~/.ssh)
-x Dry-run (print commands only)
-C MODE Color output: auto|always|never (default: auto)
-h Show this help
EOF
exit 0
}
###############################################################################
# Defaults (may be overridden by config or CLI)
###############################################################################
VMNAME="${VMNAME:-ubuntu-2404-template}"
STORAGE="${STORAGE:-local-lvm}"
BRIDGE="${BRIDGE:-vmbr0}"
MEMORY="${MEMORY:-2048}"
CORES="${CORES:-1}"
DISK_SIZE="${DISK_SIZE:-10}"
RELEASE="${RELEASE:-noble}"
VMID="${VMID:-}"
KEYFILE="${KEYFILE:-}"
###############################################################################
# Parse CLI
###############################################################################
while getopts ":i:n:s:b:m:c:d:r:k:xC:h" opt; do
case "$opt" in
i) VMID="${OPTARG}" ;;
n) VMNAME="${OPTARG}" ;;
s) STORAGE="${OPTARG}" ;;
b) BRIDGE="${OPTARG}" ;;
m) MEMORY="${OPTARG}" ;;
c) CORES="${OPTARG}" ;;
d) DISK_SIZE="${OPTARG}" ;;
r) RELEASE="${OPTARG}" ;;
k) KEYFILE="${OPTARG}" ;;
x) DRY_RUN=1 ;;
C) COLOR_MODE="${OPTARG}"; setup_colors ;;
h) usage ;;
*) die "Invalid option -$OPTARG" ;;
esac
done
###############################################################################
# Basic numeric validations
###############################################################################
[[ $MEMORY =~ ^[0-9]+$ ]] || die "MEMORY must be an integer"
[[ $CORES =~ ^[0-9]+$ ]] || die "CORES must be an integer"
[[ $DISK_SIZE =~ ^[0-9]+$ ]] || die "DISKSIZE must be an integer"
###############################################################################
# Release validation
###############################################################################
case "${RELEASE,,}" in
noble|jammy|mantic|kinetic|focal) ;;
*) die "Unknown or unsupported Ubuntu release '$RELEASE'" ;;
esac
###############################################################################
# Auto‑assign free VMID if none given
###############################################################################
if [[ -z "$VMID" ]]; then
VMID="$(pvesh get /cluster/nextid)" || die "Could not fetch next free VMID"
fi
###############################################################################
# Derived vars
###############################################################################
IMAGEPATH="https://cloud-images.ubuntu.com/${RELEASE}/current/"
IMAGENAME="${RELEASE}-server-cloudimg-amd64.img"
CHECKSUMURL="${IMAGEPATH}SHA256SUMS"
###############################################################################
# Main
###############################################################################
main() {
local start_ts
start_ts="$(date +%s)"
check_requirements
verify_network "$BRIDGE"
check_storage_space "$STORAGE" "$DISK_SIZE"
cd "$TEMP_DIR"
log INFO "Creating Ubuntu ${RELEASE} template (VMID=$VMID) …"
# Download cloud image with built‑in retry
log INFO "Downloading cloud image …"
if ! curl -fsSLI "$IMAGEPATH$IMAGENAME" >/dev/null; then
error_exit "Image URL unreachable: $IMAGEPATH$IMAGENAME"
fi
if ! wget --tries=$RETRY_COUNT --timeout=15 --waitretry=$WAIT_TIME -q "$IMAGEPATH$IMAGENAME" -O "$IMAGENAME"; then
error_exit "Failed to download image after ${RETRY_COUNT} attempts"
fi
if ! curl -fsSLI "$CHECKSUMURL" >/dev/null; then
error_exit "Checksum URL unreachable: $CHECKSUMURL"
fi
wget -q "$CHECKSUMURL" -O SHA256SUMS || error_exit "Failed to download checksum list"
grep " $IMAGENAME$" SHA256SUMS | sha256sum -c --ignore-missing - || error_exit "Checksum verification failed"
# Ensure we have an SSH key, otherwise abort (password‑less templates only)
if [[ -n "$KEYFILE" ]]; then
[[ -f "$KEYFILE" ]] || error_exit "Specified key file $KEYFILE not found"
PUBKEY="$(cat "$KEYFILE")"
else
for k in ~/.ssh/*.pub; do
[[ -f "$k" ]] || continue
KEYFILE="$k"
PUBKEY="$(cat "$k")"
break
done
fi
if [[ -z "$PUBKEY" ]]; then
error_exit "No SSH public key found. Specify one with -k"
fi
if ! grep -Eq '^ssh-(rsa|ed25519|ecdsa|dss)' <<< "$PUBKEY"; then
error_exit "File $KEYFILE does not contain a valid SSH public key"
fi
# Create VM
local create_args=(
--name "$VMNAME"
--memory "$MEMORY"
--cores "$CORES"
--net0 "virtio,bridge=${BRIDGE},firewall=1"
--ostype l26
--agent enabled=1,fstrim_cloned_disks=1
)
run qm create "$VMID" "${create_args[@]}" || error_exit "qm create failed"
# Import disk
run qm importdisk "$VMID" "$IMAGENAME" "$STORAGE" || error_exit "importdisk failed"
ensure_disk_ready "$VMID" "$STORAGE"
# Detect disk volume name
if pvesm path "$STORAGE:base-${VMID}-disk-0" &>/dev/null; then
DISK_VOL="base-${VMID}-disk-0"
else
DISK_VOL="vm-${VMID}-disk-0"
fi
# Configure VM (virtio‑scsi, cloud‑init, EFI)
local set_args=(
--scsihw virtio-scsi-single
--scsi0 "${STORAGE}:${DISK_VOL},discard=on,iothread=1"
--ide2 "${STORAGE}:cloudinit"
--boot order=scsi0
--serial0 socket
--vga serial0
--balloon 1024
--bios ovmf
--efidisk0 "${STORAGE}:0,format=raw,size=4M"
)
run qm set "$VMID" "${set_args[@]}" || error_exit "qm set failed"
# Resize disk
run qm resize "$VMID" scsi0 "+${DISK_SIZE}G" || error_exit "qm resize failed"
ensure_disk_ready "$VMID" "$STORAGE"
# Cloud‑Init defaults
local ci_args=(
--ciuser ubuntu
--cipassword "*"
--sshkeys "$PUBKEY"
--description "Ubuntu ${RELEASE} template built $(date +%F)"
--tags "template,ubuntu"
)
run qm set "$VMID" "${ci_args[@]}" || error_exit "qm set cloud‑init failed"
# Convert to template
run qm template "$VMID" || error_exit "Failed to convert to template"
# Log resulting config
run qm config "$VMID" | tee -a "$LOG_FILE"
local dur
dur=$(( $(date +%s) - start_ts ))
log INFO "Template $VMNAME (ID $VMID) created in ${dur}s ✔"
}
main "$@"