-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcloud-init.func
More file actions
822 lines (712 loc) · 29.5 KB
/
Copy pathcloud-init.func
File metadata and controls
822 lines (712 loc) · 29.5 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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/branch/main/LICENSE
# Revision: 1
# ==============================================================================
# CLOUD-INIT.FUNC - VM CLOUD-INIT CONFIGURATION LIBRARY
# ==============================================================================
#
# Universal helper library for Cloud-Init configuration in Proxmox VMs.
# Provides functions for:
#
# - Native Proxmox Cloud-Init setup (user, password, network, SSH keys)
# - Interactive configuration dialogs (whiptail)
# - IP address retrieval via qemu-guest-agent
# - Cloud-Init status monitoring and waiting
#
# Usage:
# source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/core/main/vm/cloud-init.func)
# setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
#
# Compatible with: Debian, Ubuntu, and all Cloud-Init enabled distributions
# ==============================================================================
# ==============================================================================
# SECTION 1: CONFIGURATION DEFAULTS
# ==============================================================================
# These can be overridden before sourcing this library
# Disable 'unbound variable' errors for this library (restored at end)
_OLD_SET_STATE=$(set +o | grep -E 'set -(e|u|o)')
set +u
CLOUDINIT_DEFAULT_USER="${CLOUDINIT_DEFAULT_USER:-root}"
CLOUDINIT_DNS_SERVERS="${CLOUDINIT_DNS_SERVERS:-1.1.1.1 8.8.8.8}"
CLOUDINIT_SEARCH_DOMAIN="${CLOUDINIT_SEARCH_DOMAIN:-local}"
CLOUDINIT_SSH_KEYS="${CLOUDINIT_SSH_KEYS:-}" # Empty by default - user must explicitly provide keys
# ==============================================================================
# SECTION 2: SSH KEY DISCOVERY AND SELECTION
# ==============================================================================
# ------------------------------------------------------------------------------
# _ci_ssh_extract_keys_from_file - Extracts valid SSH public keys from a file
# ------------------------------------------------------------------------------
function _ci_ssh_extract_keys_from_file() {
local file="$1"
[[ -f "$file" && -r "$file" ]] || return 0
grep -E '^(ssh-(rsa|ed25519|dss|ecdsa)|ecdsa-sha2-)' "$file" 2>/dev/null || true
}
# ------------------------------------------------------------------------------
# _ci_ssh_discover_files - Scans standard paths for SSH keys
# ------------------------------------------------------------------------------
function _ci_ssh_discover_files() {
local -a cand=()
shopt -s nullglob
cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
cand+=(/root/.ssh/*.pub)
cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
shopt -u nullglob
printf '%s\0' "${cand[@]}"
}
# ------------------------------------------------------------------------------
# _ci_ssh_build_choices - Builds whiptail checklist from SSH key files
#
# Sets: CI_SSH_CHOICES (array), CI_SSH_COUNT (int), CI_SSH_MAPFILE (path)
# ------------------------------------------------------------------------------
function _ci_ssh_build_choices() {
local -a files=("$@")
CI_SSH_CHOICES=()
CI_SSH_COUNT=0
CI_SSH_MAPFILE="$(mktemp)"
local id key typ fp cmt base
for f in "${files[@]}"; do
[[ -f "$f" && -r "$f" ]] || continue
base="$(basename -- "$f")"
# Skip known_hosts and private keys
case "$base" in
known_hosts | known_hosts.* | config) continue ;;
id_*) [[ "$f" != *.pub ]] && continue ;;
esac
while IFS= read -r key; do
[[ -n "$key" ]] || continue
typ=""
fp=""
cmt=""
read -r _typ _b64 _cmt <<<"$key"
typ="${_typ:-key}"
cmt="${_cmt:-}"
# Get fingerprint via ssh-keygen if available
if command -v ssh-keygen >/dev/null 2>&1; then
fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
fi
# Shorten long comments
[[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
CI_SSH_COUNT=$((CI_SSH_COUNT + 1))
id="K${CI_SSH_COUNT}"
echo "${id}|${key}" >>"$CI_SSH_MAPFILE"
CI_SSH_CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
done < <(_ci_ssh_extract_keys_from_file "$f")
done
}
# ------------------------------------------------------------------------------
# configure_cloudinit_ssh_keys - Interactive SSH key selection for Cloud-Init
#
# Usage: configure_cloudinit_ssh_keys
# Sets: CLOUDINIT_SSH_KEYS (path to temporary file with selected keys)
# ------------------------------------------------------------------------------
function configure_cloudinit_ssh_keys() {
local backtitle="Proxmox VE Helper Scripts"
local ssh_key_mode
# Create temp file for selected keys
CLOUDINIT_SSH_KEYS_TEMP="$(mktemp)"
: >"$CLOUDINIT_SSH_KEYS_TEMP"
# Discover keys and build choices
IFS=$'\0' read -r -d '' -a _def_files < <(_ci_ssh_discover_files && printf '\0')
_ci_ssh_build_choices "${_def_files[@]}"
local default_key_count="$CI_SSH_COUNT"
if [[ "$default_key_count" -gt 0 ]]; then
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
"Provision SSH keys for Cloud-Init VM:" 14 72 4 \
"found" "Select from detected keys (${default_key_count})" \
"manual" "Paste a single public key" \
"folder" "Scan another folder (path or glob)" \
"none" "No SSH keys (password auth only)" 3>&1 1>&2 2>&3) || return 1
else
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
"No host keys detected. Choose:" 12 72 3 \
"manual" "Paste a single public key" \
"folder" "Scan another folder (path or glob)" \
"none" "No SSH keys (password auth only)" 3>&1 1>&2 2>&3) || return 1
fi
case "$ssh_key_mode" in
found)
# Show checklist with individual keys
local selection
selection=$(whiptail --backtitle "$backtitle" --title "SELECT SSH KEYS" \
--checklist "Select one or more keys to import:" 20 140 10 "${CI_SSH_CHOICES[@]}" 3>&1 1>&2 2>&3) || return 1
for tag in $selection; do
tag="${tag%\"}"
tag="${tag#\"}"
local line
line=$(grep -E "^${tag}\|" "$CI_SSH_MAPFILE" | head -n1 | cut -d'|' -f2-)
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$CLOUDINIT_SSH_KEYS_TEMP"
done
local imported
imported=$(wc -l <"$CLOUDINIT_SSH_KEYS_TEMP")
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}${imported} key(s) selected${CL}"
;;
manual)
local pubkey
pubkey=$(whiptail --backtitle "$backtitle" --title "PASTE SSH PUBLIC KEY" \
--inputbox "Paste your SSH public key (ssh-rsa, ssh-ed25519, etc.):" 10 76 3>&1 1>&2 2>&3) || return 1
if [[ -n "$pubkey" ]]; then
echo "$pubkey" >"$CLOUDINIT_SSH_KEYS_TEMP"
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}1 key added manually${CL}"
else
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}none (empty input)${CL}"
CLOUDINIT_SSH_KEYS=""
rm -f "$CLOUDINIT_SSH_KEYS_TEMP" "$CI_SSH_MAPFILE" 2>/dev/null
return 0
fi
;;
folder)
local glob_path
glob_path=$(whiptail --backtitle "$backtitle" --title "SCAN FOLDER/GLOB" \
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub):" 10 72 3>&1 1>&2 2>&3) || return 1
if [[ -n "$glob_path" ]]; then
shopt -s nullglob
local -a _scan_files=($glob_path)
shopt -u nullglob
if [[ "${#_scan_files[@]}" -gt 0 ]]; then
_ci_ssh_build_choices "${_scan_files[@]}"
if [[ "$CI_SSH_COUNT" -gt 0 ]]; then
local folder_selection
folder_selection=$(whiptail --backtitle "$backtitle" --title "SELECT FOLDER KEYS" \
--checklist "Select key(s) to import:" 20 140 10 "${CI_SSH_CHOICES[@]}" 3>&1 1>&2 2>&3) || return 1
for tag in $folder_selection; do
tag="${tag%\"}"
tag="${tag#\"}"
local line
line=$(grep -E "^${tag}\|" "$CI_SSH_MAPFILE" | head -n1 | cut -d'|' -f2-)
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$CLOUDINIT_SSH_KEYS_TEMP"
done
local imported
imported=$(wc -l <"$CLOUDINIT_SSH_KEYS_TEMP")
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}${imported} key(s) from folder${CL}"
else
whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60
fi
else
whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60
fi
fi
;;
none | *)
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}none (password auth only)${CL}"
CLOUDINIT_SSH_KEYS=""
rm -f "$CLOUDINIT_SSH_KEYS_TEMP" "$CI_SSH_MAPFILE" 2>/dev/null
return 0
;;
esac
# Cleanup mapfile
rm -f "$CI_SSH_MAPFILE" 2>/dev/null
# Set the variable for setup_cloud_init to use
if [[ -s "$CLOUDINIT_SSH_KEYS_TEMP" ]]; then
CLOUDINIT_SSH_KEYS="$CLOUDINIT_SSH_KEYS_TEMP"
else
CLOUDINIT_SSH_KEYS=""
rm -f "$CLOUDINIT_SSH_KEYS_TEMP"
fi
return 0
}
# ==============================================================================
# SECTION 3: HELPER FUNCTIONS
# ==============================================================================
# ------------------------------------------------------------------------------
# _ci_msg - Internal message helper with fallback
# ------------------------------------------------------------------------------
function _ci_msg_info() { msg_info "$1" 2>/dev/null || echo "[INFO] $1"; }
function _ci_msg_ok() { msg_ok "$1" 2>/dev/null || echo "[OK] $1"; }
function _ci_msg_warn() { msg_warn "$1" 2>/dev/null || echo "[WARN] $1"; }
function _ci_msg_error() { msg_error "$1" 2>/dev/null || echo "[ERROR] $1"; }
# ------------------------------------------------------------------------------
# validate_ip_cidr - Validate IP address in CIDR format
# Usage: validate_ip_cidr "192.168.1.100/24" && echo "Valid"
# Returns: 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
function validate_ip_cidr() {
local ip_cidr="$1"
# Match: 0-255.0-255.0-255.0-255/0-32
if [[ "$ip_cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
# Validate each octet is 0-255
local ip="${ip_cidr%/*}"
IFS='.' read -ra octets <<<"$ip"
for octet in "${octets[@]}"; do
((octet > 255)) && return 1
done
return 0
fi
return 1
}
# ------------------------------------------------------------------------------
# validate_ip - Validate plain IP address (no CIDR)
# Usage: validate_ip "192.168.1.1" && echo "Valid"
# ------------------------------------------------------------------------------
function validate_ip() {
local ip="$1"
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
IFS='.' read -ra octets <<<"$ip"
for octet in "${octets[@]}"; do
((octet > 255)) && return 1
done
return 0
fi
return 1
}
# ==============================================================================
# SECTION 3: MAIN CLOUD-INIT FUNCTIONS
# ==============================================================================
# ------------------------------------------------------------------------------
# setup_cloud_init - Configures Proxmox Native Cloud-Init
# ------------------------------------------------------------------------------
# Parameters:
# $1 - VMID (required)
# $2 - Storage name (required)
# $3 - Hostname (optional, default: vm-<vmid>)
# $4 - Enable Cloud-Init (yes/no, default: no)
# $5 - User (optional, default: root)
# $6 - Network mode (dhcp/static, default: dhcp)
# $7 - Static IP (optional, format: 192.168.1.100/24)
# $8 - Gateway (optional)
# $9 - Nameservers (optional, default: 1.1.1.1 8.8.8.8)
#
# Returns: 0 on success, 1 on failure
# Exports: CLOUDINIT_USER, CLOUDINIT_PASSWORD, CLOUDINIT_CRED_FILE
# ==============================================================================
function setup_cloud_init() {
local vmid="$1"
local storage="$2"
local hostname="${3:-vm-${vmid}}"
local enable="${4:-no}"
local ciuser="${5:-$CLOUDINIT_DEFAULT_USER}"
local network_mode="${6:-dhcp}"
local static_ip="${7:-}"
local gateway="${8:-}"
local nameservers="${9:-$CLOUDINIT_DNS_SERVERS}"
# Skip if not enabled
if [ "$enable" != "yes" ]; then
return 0
fi
# Validate static IP if provided
if [ "$network_mode" = "static" ]; then
if [ -n "$static_ip" ] && ! validate_ip_cidr "$static_ip"; then
_ci_msg_error "Invalid static IP format: $static_ip (expected: x.x.x.x/xx)"
return 1
fi
if [ -n "$gateway" ] && ! validate_ip "$gateway"; then
_ci_msg_error "Invalid gateway IP format: $gateway"
return 1
fi
fi
_ci_msg_info "Configuring Cloud-Init"
# Create Cloud-Init drive (try ide2 first, then scsi1 as fallback)
if ! qm set "$vmid" --ide2 "${storage}:cloudinit" >/dev/null 2>&1; then
qm set "$vmid" --scsi1 "${storage}:cloudinit" >/dev/null 2>&1
fi
# Set user
qm set "$vmid" --ciuser "$ciuser" >/dev/null
# Generate and set secure random password
local cipassword=$(openssl rand -base64 16)
qm set "$vmid" --cipassword "$cipassword" >/dev/null
# Add SSH keys only if explicitly provided (not auto-imported from host)
if [ -n "${CLOUDINIT_SSH_KEYS:-}" ] && [ -f "$CLOUDINIT_SSH_KEYS" ]; then
qm set "$vmid" --sshkeys "$CLOUDINIT_SSH_KEYS" >/dev/null 2>&1 || true
_ci_msg_info "SSH keys imported from: $CLOUDINIT_SSH_KEYS"
fi
# Configure network
if [ "$network_mode" = "static" ] && [ -n "$static_ip" ] && [ -n "$gateway" ]; then
qm set "$vmid" --ipconfig0 "ip=${static_ip},gw=${gateway}" >/dev/null
else
qm set "$vmid" --ipconfig0 "ip=dhcp" >/dev/null
fi
# Set DNS servers
qm set "$vmid" --nameserver "$nameservers" >/dev/null
# Set search domain
qm set "$vmid" --searchdomain "$CLOUDINIT_SEARCH_DOMAIN" >/dev/null
# Enable package upgrades on first boot (if supported by Proxmox version)
qm set "$vmid" --ciupgrade 1 >/dev/null 2>&1 || true
# Save credentials to file (with restrictive permissions)
local cred_file="/tmp/${hostname}-${vmid}-cloud-init-credentials.txt"
umask 077
cat >"$cred_file" <<EOF
╔══════════════════════════════════════════════════════════════════╗
║ ⚠️ SECURITY WARNING: DELETE THIS FILE AFTER NOTING CREDENTIALS ║
╚══════════════════════════════════════════════════════════════════╝
Cloud-Init Credentials
────────────────────────────────────────
VM ID: ${vmid}
Hostname: ${hostname}
Created: $(date)
Username: ${ciuser}
Password: ${cipassword}
Network: ${network_mode}$([ "$network_mode" = "static" ] && echo " (IP: ${static_ip}, GW: ${gateway})" || echo " (DHCP)")
DNS: ${nameservers}
────────────────────────────────────────
SSH Access (if keys configured):
ssh ${ciuser}@<vm-ip>
Proxmox UI Configuration:
VM ${vmid} > Cloud-Init > Edit
- User, Password, SSH Keys
- Network (IP Config)
- DNS, Search Domain
────────────────────────────────────────
🗑️ To delete this file:
rm -f ${cred_file}
────────────────────────────────────────
EOF
chmod 600 "$cred_file"
_ci_msg_ok "Cloud-Init configured (User: ${ciuser})"
# Export for use in calling script (DO NOT display password here - will be shown in summary)
export CLOUDINIT_USER="$ciuser"
export CLOUDINIT_PASSWORD="$cipassword"
export CLOUDINIT_CRED_FILE="$cred_file"
return 0
}
# ==============================================================================
# SECTION 4: INTERACTIVE CONFIGURATION
# ==============================================================================
# ------------------------------------------------------------------------------
# configure_cloud_init_interactive - Whiptail dialog for Cloud-Init setup
# ------------------------------------------------------------------------------
# Prompts user for Cloud-Init configuration choices
# Returns configuration via exported variables:
# - CLOUDINIT_ENABLE (yes/no)
# - CLOUDINIT_USER
# - CLOUDINIT_NETWORK_MODE (dhcp/static)
# - CLOUDINIT_IP (if static)
# - CLOUDINIT_GW (if static)
# - CLOUDINIT_DNS
# ------------------------------------------------------------------------------
function configure_cloud_init_interactive() {
local default_user="${1:-root}"
# Check if whiptail is available
if ! command -v whiptail >/dev/null 2>&1; then
echo "Warning: whiptail not available, skipping interactive configuration"
export CLOUDINIT_ENABLE="no"
return 1
fi
# Ask if user wants to enable Cloud-Init
if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
--yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n• User accounts and passwords\n• SSH keys\n• Network settings (DHCP/Static)\n• DNS configuration\n\nYou can also configure these settings later in Proxmox UI." 16 68); then
export CLOUDINIT_ENABLE="no"
return 0
fi
export CLOUDINIT_ENABLE="yes"
# Username
if CLOUDINIT_USER=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
"Cloud-Init Username" 8 58 "$default_user" --title "USERNAME" 3>&1 1>&2 2>&3); then
export CLOUDINIT_USER="${CLOUDINIT_USER:-$default_user}"
else
export CLOUDINIT_USER="$default_user"
fi
# Network configuration
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "NETWORK MODE" \
--yesno "Use DHCP for network configuration?\n\nSelect 'No' for static IP configuration." 10 58); then
export CLOUDINIT_NETWORK_MODE="dhcp"
else
export CLOUDINIT_NETWORK_MODE="static"
# Static IP with validation
while true; do
if CLOUDINIT_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
"Static IP Address (CIDR format)\nExample: 192.168.1.100/24" 9 58 "" --title "IP ADDRESS" 3>&1 1>&2 2>&3); then
if validate_ip_cidr "$CLOUDINIT_IP"; then
export CLOUDINIT_IP
break
else
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID IP" \
--msgbox "Invalid IP format: $CLOUDINIT_IP\n\nPlease use CIDR format: x.x.x.x/xx\nExample: 192.168.1.100/24" 10 50
fi
else
_ci_msg_warn "Static IP required, falling back to DHCP"
export CLOUDINIT_NETWORK_MODE="dhcp"
break
fi
done
# Gateway with validation
if [ "$CLOUDINIT_NETWORK_MODE" = "static" ]; then
while true; do
if CLOUDINIT_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
"Gateway IP Address\nExample: 192.168.1.1" 8 58 "" --title "GATEWAY" 3>&1 1>&2 2>&3); then
if validate_ip "$CLOUDINIT_GW"; then
export CLOUDINIT_GW
break
else
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID GATEWAY" \
--msgbox "Invalid gateway format: $CLOUDINIT_GW\n\nPlease use format: x.x.x.x\nExample: 192.168.1.1" 10 50
fi
else
_ci_msg_warn "Gateway required, falling back to DHCP"
export CLOUDINIT_NETWORK_MODE="dhcp"
break
fi
done
fi
fi
# DNS Servers
if CLOUDINIT_DNS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
"DNS Servers (space-separated)" 8 58 "1.1.1.1 8.8.8.8" --title "DNS SERVERS" 3>&1 1>&2 2>&3); then
export CLOUDINIT_DNS="${CLOUDINIT_DNS:-1.1.1.1 8.8.8.8}"
else
export CLOUDINIT_DNS="1.1.1.1 8.8.8.8"
fi
return 0
}
# ==============================================================================
# SECTION 5: UTILITY FUNCTIONS
# ==============================================================================
# ------------------------------------------------------------------------------
# display_cloud_init_info - Show Cloud-Init summary after setup
# ------------------------------------------------------------------------------
function display_cloud_init_info() {
local vmid="$1"
local hostname="${2:-}"
if [ -n "$CLOUDINIT_CRED_FILE" ] && [ -f "$CLOUDINIT_CRED_FILE" ]; then
if [ -n "${INFO:-}" ]; then
echo -e "\n${INFO}${BOLD:-}${GN:-} Cloud-Init Configuration:${CL:-}"
echo -e "${TAB:- }${DGN:-}User: ${BGN:-}${CLOUDINIT_USER:-root}${CL:-}"
echo -e "${TAB:- }${DGN:-}Password: ${BGN:-}${CLOUDINIT_PASSWORD}${CL:-}"
echo -e "${TAB:- }${DGN:-}Credentials: ${BL:-}${CLOUDINIT_CRED_FILE}${CL:-}"
echo -e "${TAB:- }${RD:-}⚠️ Delete credentials file after noting password!${CL:-}"
echo -e "${TAB:- }${YW:-}💡 Configure in Proxmox UI: VM ${vmid} > Cloud-Init${CL:-}"
else
echo ""
echo "[INFO] Cloud-Init Configuration:"
echo " User: ${CLOUDINIT_USER:-root}"
echo " Password: ${CLOUDINIT_PASSWORD}"
echo " Credentials: ${CLOUDINIT_CRED_FILE}"
echo " ⚠️ Delete credentials file after noting password!"
echo " Configure in Proxmox UI: VM ${vmid} > Cloud-Init"
fi
fi
}
# ------------------------------------------------------------------------------
# cleanup_cloud_init_credentials - Remove credentials file
# ------------------------------------------------------------------------------
# Usage: cleanup_cloud_init_credentials
# Call this after user has noted/saved the credentials
# ------------------------------------------------------------------------------
function cleanup_cloud_init_credentials() {
if [ -n "$CLOUDINIT_CRED_FILE" ] && [ -f "$CLOUDINIT_CRED_FILE" ]; then
rm -f "$CLOUDINIT_CRED_FILE"
_ci_msg_ok "Credentials file removed: $CLOUDINIT_CRED_FILE"
unset CLOUDINIT_CRED_FILE
return 0
fi
return 1
}
# ------------------------------------------------------------------------------
# has_cloud_init - Check if VM has Cloud-Init configured
# ------------------------------------------------------------------------------
function has_cloud_init() {
local vmid="$1"
qm config "$vmid" 2>/dev/null | grep -qE "(ide2|scsi1):.*cloudinit"
}
# ------------------------------------------------------------------------------
# regenerate_cloud_init - Regenerate Cloud-Init configuration
# ------------------------------------------------------------------------------
function regenerate_cloud_init() {
local vmid="$1"
if has_cloud_init "$vmid"; then
_ci_msg_info "Regenerating Cloud-Init configuration"
qm cloudinit update "$vmid" >/dev/null 2>&1 || true
_ci_msg_ok "Cloud-Init configuration regenerated"
return 0
else
_ci_msg_warn "VM $vmid does not have Cloud-Init configured"
return 1
fi
}
# ------------------------------------------------------------------------------
# Creates a custom Cloud-Init network configuration without renaming the NIC.
#
# Workaround for Ubuntu 26.04 / cloud-init 26.1 / netplan 1.2:
# Prevents the failing ens18 -> eth0 rename by matching the interface by MAC only.
#
# Parameters:
# $1 - VMID
# $2 - MAC address
# $3 - Static IP in CIDR format
# $4 - Gateway
# $5 - Space-separated DNS servers
# $6 - Search domain
#
# Optional:
# CLOUDINIT_SNIPPET_STORAGE - Proxmox storage supporting snippets
# ------------------------------------------------------------------------------
function setup_cloud_init_network_no_rename() {
local vmid="$1"
local mac="$2"
local static_ip="$3"
local gateway="$4"
local nameservers="${5:-$CLOUDINIT_DNS_SERVERS}"
local search_domain="${6:-$CLOUDINIT_SEARCH_DOMAIN}"
local snippet_storage="${CLOUDINIT_SNIPPET_STORAGE:-}"
local snippet_name="vm-${vmid}-network.yaml"
local snippet_volid
local snippet_path
local existing_cicustom
local dns
if ! validate_ip_cidr "$static_ip"; then
_ci_msg_error "Invalid static IP format: $static_ip"
return 1
fi
if ! validate_ip "$gateway"; then
_ci_msg_error "Invalid gateway format: $gateway"
return 1
fi
if [[ -z "$snippet_storage" ]]; then
snippet_storage="$(
pvesm status -content snippets 2>/dev/null |
awk 'NR > 1 && $3 == "active" {print $1; exit}'
)"
fi
if [[ -z "$snippet_storage" ]]; then
_ci_msg_error "No active Proxmox storage supporting snippets was found"
return 1
fi
snippet_volid="${snippet_storage}:snippets/${snippet_name}"
snippet_path="$(pvesm path "$snippet_volid")"
mkdir -p "$(dirname "$snippet_path")"
{
cat <<EOF
version: 2
ethernets:
primary:
match:
macaddress: "${mac,,}"
dhcp4: false
addresses:
- "${static_ip}"
routes:
- to: default
via: "${gateway}"
nameservers:
addresses:
EOF
for dns in $nameservers; do
printf ' - "%s"\n' "$dns"
done
if [[ -n "$search_domain" ]]; then
cat <<EOF
search:
- "${search_domain}"
EOF
fi
} >"$snippet_path"
chmod 600 "$snippet_path"
# Preserve existing custom user/vendor/meta configuration.
existing_cicustom="$(
qm config "$vmid" |
sed -n 's/^cicustom: //p' |
tr ',' '\n' |
grep -v '^network=' |
paste -sd, - || true
)"
if [[ -n "$existing_cicustom" ]]; then
existing_cicustom+=","
fi
qm set "$vmid" \
--cicustom "${existing_cicustom}network=${snippet_volid}" \
>/dev/null
qm cloudinit update "$vmid" >/dev/null
export CLOUDINIT_NETWORK_SNIPPET="$snippet_volid"
_ci_msg_ok "Configured Cloud-Init network without interface renaming"
}
# ------------------------------------------------------------------------------
# get_vm_ip - Get VM IP address via qemu-guest-agent
# ------------------------------------------------------------------------------
function get_vm_ip() {
local vmid="$1"
local timeout="${2:-30}"
local elapsed=0
while [ $elapsed -lt $timeout ]; do
local vm_ip=$(qm guest cmd "$vmid" network-get-interfaces 2>/dev/null |
jq -r '.[] | select(.name != "lo") | ."ip-addresses"[]? | select(."ip-address-type" == "ipv4") | ."ip-address"' 2>/dev/null | head -1)
if [ -n "$vm_ip" ]; then
echo "$vm_ip"
return 0
fi
sleep 2
elapsed=$((elapsed + 2))
done
return 1
}
# ------------------------------------------------------------------------------
# wait_for_cloud_init - Wait for Cloud-Init to complete (requires SSH access)
# ------------------------------------------------------------------------------
function wait_for_cloud_init() {
local vmid="$1"
local timeout="${2:-300}"
local vm_ip="${3:-}"
# Get IP if not provided
if [ -z "$vm_ip" ]; then
vm_ip=$(get_vm_ip "$vmid" 60)
fi
if [ -z "$vm_ip" ]; then
_ci_msg_warn "Unable to determine VM IP address"
return 1
fi
_ci_msg_info "Waiting for Cloud-Init to complete on ${vm_ip}"
local elapsed=0
while [ $elapsed -lt $timeout ]; do
if timeout 10 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"${CLOUDINIT_USER:-root}@${vm_ip}" "cloud-init status --wait" 2>/dev/null; then
_ci_msg_ok "Cloud-Init completed successfully"
return 0
fi
sleep 10
elapsed=$((elapsed + 10))
done
_ci_msg_warn "Cloud-Init did not complete within ${timeout}s"
return 1
}
# ==============================================================================
# SECTION 6: EXPORTS
# ==============================================================================
# Export all functions for use in other scripts
export -f setup_cloud_init 2>/dev/null || true
export -f configure_cloud_init_interactive 2>/dev/null || true
export -f display_cloud_init_info 2>/dev/null || true
export -f cleanup_cloud_init_credentials 2>/dev/null || true
export -f has_cloud_init 2>/dev/null || true
export -f regenerate_cloud_init 2>/dev/null || true
export -f get_vm_ip 2>/dev/null || true
export -f wait_for_cloud_init 2>/dev/null || true
export -f validate_ip_cidr 2>/dev/null || true
export -f validate_ip 2>/dev/null || true
# Restore previous shell options if they were saved
if [ -n "${_OLD_SET_STATE:-}" ]; then
eval "$_OLD_SET_STATE"
fi
# ==============================================================================
# SECTION 7: EXAMPLES & DOCUMENTATION
# ==============================================================================
: <<'EXAMPLES'
# Example 1: Simple DHCP setup (most common)
setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
# Example 2: Static IP setup
setup_cloud_init "$VMID" "$STORAGE" "myserver" "yes" "root" "static" "192.168.1.100/24" "192.168.1.1"
# Example 3: Interactive configuration in advanced_settings()
configure_cloud_init_interactive "admin"
if [ "$CLOUDINIT_ENABLE" = "yes" ]; then
setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" "$CLOUDINIT_USER" \
"$CLOUDINIT_NETWORK_MODE" "$CLOUDINIT_IP" "$CLOUDINIT_GW" "$CLOUDINIT_DNS"
fi
# Example 4: Display info after VM creation
display_cloud_init_info "$VMID" "$HN"
# Example 5: Check if VM has Cloud-Init
if has_cloud_init "$VMID"; then
echo "Cloud-Init is configured"
fi
# Example 6: Wait for Cloud-Init to complete after VM start
if [ "$START_VM" = "yes" ]; then
qm start "$VMID"
sleep 30
wait_for_cloud_init "$VMID" 300
fi
# Example 7: Cleanup credentials file after user has noted password
display_cloud_init_info "$VMID" "$HN"
read -p "Have you saved the credentials? (y/N): " -r </dev/tty
[[ $REPLY =~ ^[Yy]$ ]] && cleanup_cloud_init_credentials
# Example 8: Validate IP before using
if validate_ip_cidr "192.168.1.100/24"; then
echo "Valid IP/CIDR"
fi
EXAMPLES