-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvmctl.sh
More file actions
executable file
·339 lines (292 loc) · 15.3 KB
/
vmctl.sh
File metadata and controls
executable file
·339 lines (292 loc) · 15.3 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
#!/usr/bin/env bash
# vmctl.sh - Unified Debian VM control: preseed generation, install, and launch
#
# Operational modes (mutually exclusive):
# [default] Full pipeline: preseed → download → install → [boot]
# --preseed-only Generate preseed file only, then exit
# --deploy-only FILE Skip wizard; use an existing preseed file
# --launch DISK Boot an already-installed disk image
#
# Quick-start:
# ./vmctl.sh # interactive full run
# ./vmctl.sh --yes # fully non-interactive with defaults
# ./vmctl.sh --config my.cfg # pre-populated from config file
# ./vmctl.sh --preseed-only --yes # generate preseed, no VM
# ./vmctl.sh --deploy-only preseeds/debian-vm.cfg --yes
# ./vmctl.sh --launch .vms/debian-vm.qcow2
# ./vmctl.sh --launch # interactive VM picker
set -euo pipefail
# ---------------------------------------------------------------------------
# Bootstrap
# ---------------------------------------------------------------------------
VMCTL_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
LIB_DIR="$VMCTL_DIR/lib"
source "$LIB_DIR/common.sh"
source "$LIB_DIR/config.sh"
source "$LIB_DIR/preseed.sh"
source "$LIB_DIR/cache.sh"
source "$LIB_DIR/qemu.sh"
# ---------------------------------------------------------------------------
# Usage
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
OPERATIONAL MODES (pick one, or omit for full pipeline):
--preseed-only Generate preseed file and exit
--deploy-only PRESEED Use existing preseed, skip wizard
--launch [DISK] Boot an installed disk image; omit DISK to pick from saved VMs
CONFIG & IDENTITY:
--config FILE Load KEY=value config file
--name NAME VM name (default: $DEFAULT_VM_NAME)
--hostname HOST Guest hostname (preseed)
--domain DOMAIN Guest domain (preseed)
--username USER Guest username (preseed)
--fullname "FULL NAME" User full name (preseed)
--password PASS User password (preseed, plain — will be hashed)
--sudo-nopasswd Enable passwordless sudo
--uid UID User UID override
--gid GID User GID override
LOCALIZATION (preseed):
--locale LOCALE e.g. en_US.UTF-8 (default: $DEFAULT_LOCALE)
--keymap MAP e.g. us (default: $DEFAULT_KEYMAP)
--timezone TZ e.g. UTC (default: $DEFAULT_TIMEZONE)
INSTALLER TARGET (preseed):
--disk-target DEV Target disk inside VM (default: $DEFAULT_DISK)
--proxy URL APT proxy, e.g. http://proxy:3128
--extra-packages "pkg1 …" Extra packages to install
--docker Install Docker via get.docker.com
--add-docker-group Add guest user to the docker group (requires --docker)
--share-name TAG VirtFS 9p mount tag
--share-guest-path PATH Mount point inside guest
--post "CMD" Post-install command (run as guest user)
--poweroff / --reboot After install: power off (default) or reboot
--cleanup-level LEVEL none | standard (default) | aggressive
VM RESOURCES:
--ram MB RAM in MB (default: $DEFAULT_RAM)
--cpus N vCPU count (default: $DEFAULT_CPUS)
--disk-size GB Disk image size (default: $DEFAULT_DISK_SIZE GB)
--version VER Debian release (default: $DEFAULT_DEBIAN_VERSION)
Choices: stable bookworm bullseye buster trixie sid
(or any valid Debian codename)
NETWORKING:
--ports MAP Port forwards e.g. 2222:22,8080:80
--net-mode user|tap|bridge Network backend (default: $DEFAULT_NET_MODE)
--tap-if IFNAME TAP interface (default: $DEFAULT_TAP_IF)
--bridge NAME Bridge name (default: $DEFAULT_BRIDGE)
--mac MAC MAC address (optional)
PATHS:
--cachedir DIR Installer cache (default: \$BASE/.cache)
--vmdir DIR VM disk dir (default: \$BASE/.vms)
--share-path DIR Host VirtFS dir (default: \$BASE)
DISPLAY:
--display nographic|gtk|sdl Display mode (default: $DEFAULT_DISPLAY)
EXECUTION:
--yes Skip all prompts, use defaults
--dry-run Print commands without executing
--wait-ssh Poll SSH after install until port is open
-h, --help Show this help
EOF
exit 0
}
# ---------------------------------------------------------------------------
# Global state
# ---------------------------------------------------------------------------
VMCTL_MODE="full" # full | preseed-only | deploy-only | launch
AUTO_CONFIRM=false
DRY_RUN=false
WAIT_SSH=false
TMP_INITRD=""
# ---------------------------------------------------------------------------
# Auto-load ~/.vmctlrc if present
# ---------------------------------------------------------------------------
[[ -f "$HOME/.vmctlrc" ]] && load_config "$HOME/.vmctlrc"
# ---------------------------------------------------------------------------
# Parse CLI args (may override config file values)
# ---------------------------------------------------------------------------
parse_args "$@"
# ---------------------------------------------------------------------------
# Resolve all unset variables to defaults
# ---------------------------------------------------------------------------
apply_defaults
# ---------------------------------------------------------------------------
# Set up logging
# ---------------------------------------------------------------------------
mkdir -p "${DEFAULT_LOG_DIR}"
LOG_FILE="${DEFAULT_LOG_DIR}/vmctl-$(date +'%Y%m%d-%H%M%S').log"
ln -sf "$LOG_FILE" "${DEFAULT_LOG_DIR}/latest.log"
export LOG_FILE
log "vmctl started — mode: ${VMCTL_MODE}"
# ---------------------------------------------------------------------------
# Cleanup trap for temporary initrd
# ---------------------------------------------------------------------------
_cleanup() {
if [[ -n "$TMP_INITRD" && -f "$TMP_INITRD" ]]; then
rm -f "$TMP_INITRD"
log "Cleaned up temporary initrd."
fi
}
trap _cleanup EXIT
# ---------------------------------------------------------------------------
# ── MODE: launch ──────────────────────────────────────────────────────────
# Boot an already-installed disk image immediately.
# ---------------------------------------------------------------------------
if [[ "$VMCTL_MODE" == "launch" ]]; then
DISK_IMG="${DISK_IMG:-}"
# No disk supplied — present the user with available VMs
if [[ -z "$DISK_IMG" ]]; then
select_vm # sources chosen .vmcfg; sets DISK_IMG and other launch params
fi
[[ -z "$DISK_IMG" || ! -f "$DISK_IMG" ]] && error "Disk image not found: ${DISK_IMG:-<not set>}"
parse_ports "${PORTS:-}"
HOSTFWD_ARGS="${RET_HOSTFWD:-}"
log "Launching disk image: $DISK_IMG"
run_vm "$VM_NAME" "$RAM" "$VCPUS" "$DISK_IMG" \
"$HOSTFWD_ARGS" "${VIRTFS_ARGS:-}" \
"$DISPLAY_MODE" "$NET_MODE" "$TAP_IF" "$BRIDGE_NAME" "${MAC_ADDR:-}"
exit 0
fi
# ---------------------------------------------------------------------------
# ── PHASES 1–2: Preseed wizard + generation ──────────────────────────────
# Skipped in deploy-only mode (preseed file supplied by user).
# ---------------------------------------------------------------------------
if [[ "$VMCTL_MODE" != "deploy-only" ]]; then
# Dependency check covers preseed-gen tools (awk, openssl) + QEMU
check_deps
collect_preseed_config
generate_preseed "${VM_NAME}" || exit 1
PRESEED_FILE="$RET_PRESEED_OUT"
else
# deploy-only: PRESEED_FILE already set by --deploy-only via parse_args
check_deps
[[ -z "${PRESEED_FILE:-}" ]] && error "--deploy-only requires a preseed file path."
[[ -f "$PRESEED_FILE" ]] || error "Preseed file not found: $PRESEED_FILE"
log "Deploy-only mode: using preseed $PRESEED_FILE"
fi
# ── MODE: preseed-only ──────────────────────────────────────────────────────
if [[ "$VMCTL_MODE" == "preseed-only" ]]; then
log "Preseed-only mode complete. File: $PRESEED_FILE"
exit 0
fi
# ---------------------------------------------------------------------------
# ── PHASE 3: Detect installer poweroff preference ────────────────────────
# ---------------------------------------------------------------------------
detect_poweroff "$PRESEED_FILE"
# ---------------------------------------------------------------------------
# ── PHASE 4: Download / verify installer cache ───────────────────────────
# ---------------------------------------------------------------------------
FORCE_REDOWNLOAD=false
VERSION_DIR="$CACHE_DIR/$DEBIAN_VERSION"
if [[ -f "$VERSION_DIR/linux" && -f "$VERSION_DIR/initrd.gz" ]]; then
if verify_cache_integrity "$DEBIAN_VERSION" "$VERSION_DIR"; then
if [[ "$AUTO_CONFIRM" != "true" ]]; then
printf "Verified installer cache for Debian ${DEBIAN_VERSION}. Redownload? [y/${BOLD}N${NC}]: " >&2
read -r _ans
[[ "$_ans" =~ ^[Yy]$ ]] && FORCE_REDOWNLOAD=true
fi
else
warn "Cached installer is missing or invalid — redownloading."
FORCE_REDOWNLOAD=true
fi
fi
setup_cache "$CACHE_DIR" "$DEBIAN_VERSION" "$AUTO_CONFIRM" "$FORCE_REDOWNLOAD"
KERNEL_PATH="$RET_KERNEL"
INITRD_PATH="$RET_INITRD"
# ---------------------------------------------------------------------------
# ── PHASE 5: Port forwarding ─────────────────────────────────────────────
# ---------------------------------------------------------------------------
collect_ports
parse_ports "${PORTS:-}"
HOSTFWD_ARGS="${RET_HOSTFWD:-}"
# ---------------------------------------------------------------------------
# ── PHASE 6: VirtFS host directory ─────────────────────────────────────
# ---------------------------------------------------------------------------
SHARE_PATH="${SHARE_PATH:-$DEFAULT_SHARE_PATH}"
if [[ -n "${SHARE_NAME:-}" && "$AUTO_CONFIRM" != "true" && -z "${_VMCTL_SHARE_SET:-}" ]]; then
printf "VirtFS tag '%s' configured. Host directory to share [${BOLD}%s${NC}]: " \
"$SHARE_NAME" "$SHARE_PATH" >&2
read -r _sp_input
SHARE_PATH="${_sp_input:-$SHARE_PATH}"
fi
get_virtfs_args "$PRESEED_FILE" "$SHARE_PATH"
VIRTFS_ARGS="${RET_VIRTFS:-}"
# ---------------------------------------------------------------------------
# ── PHASE 6b: Display interface ─────────────────────────────────────────
# ---------------------------------------------------------------------------
if [[ "$AUTO_CONFIRM" != "true" ]]; then
header "Display Interface"
printf " [${GREEN}1${NC}] TTY / headless ${DIM}(nographic — recommended for servers)${NC}\n" >&2
printf " [${GREEN}2${NC}] Graphical window ${DIM}(gtk — requires a desktop)${NC}\n" >&2
printf " [${GREEN}3${NC}] SDL window ${DIM}(sdl — lightweight graphical)${NC}\n" >&2
_cur_label=""
case "${DISPLAY_MODE:-nographic}" in
gtk) _cur_label="2 (gtk)" ;;
sdl) _cur_label="3 (sdl)" ;;
*) _cur_label="1 (nographic)" ;;
esac
printf "\nSelection [${DIM}%s${NC}]: " "$_cur_label" >&2
read -r _disp_sel
case "${_disp_sel:-}" in
2) DISPLAY_MODE="gtk" ;;
3) DISPLAY_MODE="sdl" ;;
1) DISPLAY_MODE="nographic" ;;
"") ;; # keep current value
gtk|sdl|nographic) DISPLAY_MODE="$_disp_sel" ;;
*) warn "Unknown selection '$_disp_sel'. Keeping: $DISPLAY_MODE" ;;
esac
fi
log "Display mode: $DISPLAY_MODE"
# ---------------------------------------------------------------------------
# ── PHASE 7: Create disk image ──────────────────────────────────────────
# ---------------------------------------------------------------------------
mkdir -p "$VM_DIR"
DISK_IMG="${VM_DIR}/${VM_NAME}.qcow2"
create_disk "$DISK_IMG" "$DISK_SIZE"
# Save VM config right after the disk is created so the VM is discoverable
# even if the installation is interrupted.
save_vm_config
# ---------------------------------------------------------------------------
# ── PHASE 8: Inject preseed into initrd ─────────────────────────────────
# ---------------------------------------------------------------------------
TMP_INITRD=$(mktemp --suffix=-initrd.gz)
inject_preseed "$PRESEED_FILE" "$INITRD_PATH" "$TMP_INITRD"
# ---------------------------------------------------------------------------
# ── PHASE 9: Installer phase ────────────────────────────────────────────
# ---------------------------------------------------------------------------
header "Starting Installation"
if [[ "$AUTO_CONFIRM" != "true" ]]; then
printf "\nAll configuration is ready. Launch installer now? [${BOLD}Y${NC}/n]: " >&2
read -r _launch_ans
[[ "${_launch_ans:-y}" =~ ^[Nn]$ ]] && { log "Launch cancelled by user."; exit 0; }
fi
run_installer \
"$VM_NAME" "$RAM" "$VCPUS" "$DISK_IMG" \
"$KERNEL_PATH" "$TMP_INITRD" \
"$HOSTFWD_ARGS" "$VIRTFS_ARGS" \
"$DISPLAY_MODE" "$NET_MODE" "$TAP_IF" "$BRIDGE_NAME" \
"installer" "${MAC_ADDR:-}"
# ---------------------------------------------------------------------------
# ── PHASE 10: Disk-boot phase (when preseed does not power off) ──────────
# ---------------------------------------------------------------------------
if [[ "$INSTALLER_POWEROFF" == "false" ]]; then
log "Installer finished. Starting disk-boot phase..."
run_vm \
"$VM_NAME" "$RAM" "$VCPUS" "$DISK_IMG" \
"$HOSTFWD_ARGS" "$VIRTFS_ARGS" \
"$DISPLAY_MODE" "$NET_MODE" "$TAP_IF" "$BRIDGE_NAME" "${MAC_ADDR:-}"
else
log "Installer finished. VM powered off as requested by preseed."
fi
# ---------------------------------------------------------------------------
# ── PHASE 11: Optional SSH readiness check ──────────────────────────────
# ---------------------------------------------------------------------------
if [[ "${WAIT_SSH:-false}" == "true" ]]; then
local _ssh_port="2222"
# Try to extract SSH port from port mappings
if [[ -n "${PORTS:-}" ]]; then
_ssh_port=$(echo "$PORTS" | grep -oP '\d+(?=:22\b)' | head -1 || echo "2222")
fi
export USERNAME
bash "$VMCTL_DIR/tools/verify_deploy.sh" "$_ssh_port" "${USERNAME:-user}"
fi
log "vmctl finished."