-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathvm-core.func
More file actions
2270 lines (1988 loc) · 75.5 KB
/
Copy pathvm-core.func
File metadata and controls
2270 lines (1988 loc) · 75.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
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/LICENSE
# Unified entry point: Proxmox VE and Incus VM scripts share vm/ templates.
[[ -n "${_VM_CORE_DISPATCHED:-}" ]] && return
_VM_CORE_DISPATCHED=1
# Repeated per file rather than shared: there is nowhere to put a shared copy
# that does not itself need downloading first. See core/build.func.
_CS_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
if command -v pveversion &>/dev/null; then
:
elif command -v incus &>/dev/null && incus info &>/dev/null 2>&1; then
# Honour the dispatcher's resolver and a local checkout before reaching for
# the network, so a fork or branch of core is picked up here too.
if declare -f _cs_source_func >/dev/null 2>&1; then
_cs_source_func "incus/vm-core.func"
elif [[ -n "${COMMUNITY_SCRIPTS_CORE_DIR:-}" && -f "${COMMUNITY_SCRIPTS_CORE_DIR}/incus/vm-core.func" ]]; then
# shellcheck disable=SC1090
source "${COMMUNITY_SCRIPTS_CORE_DIR}/incus/vm-core.func"
else
_vm_core_base="${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}"
# shellcheck disable=SC1090
source <(curl -fsSL "${_CS_CURL_RETRY[@]}" "${_vm_core_base}/incus/vm-core.func" 2>/dev/null ||
wget -qO- "${_vm_core_base}/incus/vm-core.func")
fi
return 0
fi
set -euo pipefail
# Value checks shared with the Incus VM path; the formatting below stays
# Proxmox-specific (",mtu=" / ",tag=" are pve-net syntax).
if declare -f _cs_source_func >/dev/null 2>&1; then
_cs_source_func "host/validate.func"
elif [[ -n "${COMMUNITY_SCRIPTS_CORE_DIR:-}" && -f "${COMMUNITY_SCRIPTS_CORE_DIR}/host/validate.func" ]]; then
# shellcheck disable=SC1090
source "${COMMUNITY_SCRIPTS_CORE_DIR}/host/validate.func"
else
_vm_validate_base="${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}"
# shellcheck disable=SC1090
source <(curl -fsSL "${_CS_CURL_RETRY[@]}" "${_vm_validate_base}/host/validate.func" 2>/dev/null ||
wget -qO- "${_vm_validate_base}/host/validate.func")
fi
SPINNER_PID=""
SPINNER_ACTIVE=0
SPINNER_MSG=""
# ------------------------------------------------------------------------------
# Loads core utility groups once (colors, formatting, icons, defaults).
# ------------------------------------------------------------------------------
[[ -n "${_CORE_FUNC_LOADED:-}" ]] && return
_CORE_FUNC_LOADED=1
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
load_api_functions() {
if ! declare -f post_to_api_vm >/dev/null 2>&1; then
source /dev/stdin <<<$(curl -fsSL "${_CS_CURL_RETRY[@]}" "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/api/api.func")
fi
}
# Only the groups that set variables. The six names that used to follow
# shell_check were left behind when their definitions moved out of this
# function, so loading the engine ran them: get_valid_nextid printed a bare VM
# ID, cleanup tore down the temp dir before there was one, and check_root and
# friends ran before the script had said what it wanted. Every script calls
# those itself, in its own order.
load_functions() {
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
__FUNCTIONS_LOADED=1
load_api_functions
color
formatting
icons
default_vars
set_std_mode
shell_check
}
# core/core.func owns this, but a VM script never loads that file -- it carries
# its own colours and icons. `clear` is part of ncurses and missing from minimal
# images, where a bare call returns 127 and errexit ends the run.
_cs_clear() {
if command -v clear >/dev/null 2>&1; then
clear
else
printf '\033[H\033[2J\033[3J'
fi
}
# Returns non-zero when the helpers are unavailable. A swallowed failure here
# once let the script run on and die on "setup_cloud_init: command not found".
load_cloud_init_functions() {
declare -f setup_cloud_init >/dev/null 2>&1 && return 0
local rel="vm/cloud-init.func" url
local base="${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}"
if [[ -n "${COMMUNITY_SCRIPTS_CORE_DIR:-}" && -f "${COMMUNITY_SCRIPTS_CORE_DIR}/${rel}" ]]; then
# shellcheck disable=SC1090
source "${COMMUNITY_SCRIPTS_CORE_DIR}/${rel}"
declare -f setup_cloud_init >/dev/null 2>&1 && return 0
fi
if declare -f _cs_source_func >/dev/null 2>&1; then
_cs_source_func "$rel" 2>/dev/null || true
declare -f setup_cloud_init >/dev/null 2>&1 && return 0
fi
local -a urls=("${base}/${rel}")
# raw.githubusercontent has been seen answering 400 for .../main/<path> while
# serving the identical blob under .../refs/heads/main/<path>. One helper
# being unreachable should not cost a finished VM, so try the long ref form
# and a mirror before giving up.
[[ "$base" == */main ]] && urls+=("${base%/main}/refs/heads/main/${rel}")
urls+=("https://cdn.jsdelivr.net/gh/community-scripts/core@main/${rel}")
for url in "${urls[@]}"; do
# shellcheck disable=SC1090
source <(curl -fsSL "${_CS_CURL_RETRY[@]}" "$url" 2>/dev/null) 2>/dev/null || true
declare -f setup_cloud_init >/dev/null 2>&1 && return 0
done
return 1
}
# Banner slugs to try, longest first. The generator keys on the first APP= line,
# but a few scripts append the release and type before drawing ("AlmaLinux 10
# VM") and so ask for a banner that was never generated. Only a trailing version
# or VM/LXC is dropped and retried; any other word ends the walk, so "Home
# Assistant OS" cannot fall back onto a "homeassistant" banner.
_cs_header_slugs() {
local -a words
read -ra words <<<"$1"
local i last
for ((i = ${#words[@]}; i > 0; i--)); do
echo "${words[*]:0:i}" | tr '[:upper:]' '[:lower:]' | tr -d ' '
last="${words[i - 1],,}"
[[ "$last" =~ ^v?[0-9][0-9.]*$ || "$last" == "vm" || "$last" == "lxc" ]] || break
done
}
# Function to download & save header files
get_header() {
local app_type=${APP_TYPE:-vm}
local core_base="${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}"
local slug cache
while read -r slug; do
[[ -z "$slug" ]] && continue
cache="/usr/local/community-scripts/headers/${app_type}/${slug}"
mkdir -p "$(dirname "$cache")"
if [ -s "$cache" ]; then
cat "$cache"
return 0
fi
# A banner the generator has not produced yet 404s here, which is expected
# and not worth printing at the user. header_info simply draws nothing.
if curl -fsSL "${core_base}/headers/${app_type}/${slug}" -o "$cache" 2>/dev/null && [ -s "$cache" ]; then
cat "$cache"
return 0
fi
rm -f "$cache"
done < <(_cs_header_slugs "${1:-$APP}")
return 1
}
header_info() {
# Hand the name over unchanged. Hyphenating it here asked for
# home-assistant-os instead of homeassistantos, and hid the word boundaries
# get_header needs to drop a trailing version.
local header_content
header_content=$(get_header "${1:-$APP}") || header_content=""
_cs_clear
local term_width
term_width=$(tput cols 2>/dev/null || echo 120)
if [ -n "$header_content" ]; then
echo "$header_content"
fi
}
# ------------------------------------------------------------------------------
# Sets ANSI color codes used for styled terminal output.
# ------------------------------------------------------------------------------
color() {
YW=$(echo "\033[33m")
YWB=$(echo "\033[93m")
BL=$(echo "\033[36m")
RD=$(echo "\033[01;31m")
BGN=$(echo "\033[4;92m")
GN=$(echo "\033[1;92m")
DGN=$(echo "\033[32m")
CL=$(echo "\033[m")
}
# ------------------------------------------------------------------------------
# Defines formatting helpers like tab, bold, and line reset sequences.
# ------------------------------------------------------------------------------
formatting() {
BFR="\\r\\033[K"
BOLD=$(echo "\033[1m")
HOLD=" "
TAB=" "
TAB3=" "
}
# ------------------------------------------------------------------------------
# Sets symbolic icons used throughout user feedback and prompts.
# ------------------------------------------------------------------------------
icons() {
CM="${TAB}✔️${TAB}"
CROSS="${TAB}✖️${TAB}"
DNSOK="✔️ "
DNSFAIL="${TAB}✖️${TAB}"
INFO="${TAB}💡${TAB}${CL}"
OVERRIDE="${TAB}🔀${TAB}${CL}"
CLOUD="${TAB}☁️${TAB}${CL}"
OS="${TAB}🖥️${TAB}${CL}"
OSVERSION="${TAB}🌟${TAB}${CL}"
CONTAINERTYPE="${TAB}📦${TAB}${CL}"
DISKSIZE="${TAB}💾${TAB}${CL}"
CPUCORE="${TAB}🧠${TAB}${CL}"
RAMSIZE="${TAB}🛠️${TAB}${CL}"
SEARCH="${TAB}🔍${TAB}${CL}"
VERBOSE_CROPPED="🔍${TAB}"
VERIFYPW="${TAB}🔐${TAB}${CL}"
CONTAINERID="${TAB}🆔${TAB}${CL}"
HOSTNAME="${TAB}🏠${TAB}${CL}"
BRIDGE="${TAB}🌉${TAB}${CL}"
NETWORK="${TAB}📡${TAB}${CL}"
GATEWAY="${TAB}🌐${TAB}${CL}"
DISABLEIPV6="${TAB}🚫${TAB}${CL}"
ICON_DISABLEIPV6="${TAB}🚫${TAB}${CL}"
DEFAULT="${TAB}⚙️${TAB}${CL}"
MACADDRESS="${TAB}🔗${TAB}${CL}"
VLANTAG="${TAB}🏷️${TAB}${CL}"
ROOTSSH="${TAB}🔑${TAB}${CL}"
CREATING="${TAB}🚀${TAB}${CL}"
ADVANCED="${TAB}🧩${TAB}${CL}"
FUSE="${TAB}🗂️${TAB}${CL}"
GPU="${TAB}🎮${TAB}${CL}"
HOURGLASS="${TAB}⏳${TAB}"
}
# ------------------------------------------------------------------------------
# Sets default verbose mode for script and os execution.
# ------------------------------------------------------------------------------
set_std_mode() {
if [ "${VERBOSE:-no}" = "yes" ]; then
STD=""
else
STD="silent"
fi
}
# ------------------------------------------------------------------------------
# default_vars()
#
# - Sets default retry and wait variables used for system actions
# - RETRY_NUM: Maximum number of retry attempts (default: 10)
# - RETRY_EVERY: Seconds to wait between retries (default: 3)
# ------------------------------------------------------------------------------
default_vars() {
RETRY_NUM=10
RETRY_EVERY=3
i=$RETRY_NUM
}
# ------------------------------------------------------------------------------
# get_active_logfile()
#
# - Returns the appropriate log file based on execution context
# - BUILD_LOG: Host operations (VM creation)
# - Fallback to /tmp/build-<timestamp>.log if not set
# ------------------------------------------------------------------------------
get_active_logfile() {
if [[ -n "${BUILD_LOG:-}" ]]; then
echo "$BUILD_LOG"
else
# Fallback for legacy scripts
echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log"
fi
}
# ------------------------------------------------------------------------------
# silent()
#
# - Executes command with output redirected to active log file
# - On error: displays last 20 lines of log and exits with original exit code
# - Temporarily disables error trap to capture exit code correctly
# - Sources explain_exit_code() for detailed error messages
# ------------------------------------------------------------------------------
silent() {
local cmd="$*"
local caller_line="${BASH_LINENO[0]:-unknown}"
local logfile="$(get_active_logfile)"
set +Eeuo pipefail
trap - ERR
"$@" >>"$logfile" 2>&1
local rc=$?
set -Eeuo pipefail
trap 'error_handler' ERR
if [[ $rc -ne 0 ]]; then
# Source explain_exit_code if needed
if ! declare -f explain_exit_code >/dev/null 2>&1; then
if declare -f _cs_source_func >/dev/null 2>&1; then
_cs_source_func "core/error_handler.func" 2>/dev/null || true
else
# shellcheck disable=SC1090
source <(curl -fsSL "${_CS_CURL_RETRY[@]}" "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/error_handler.func") 2>/dev/null || true
fi
fi
local explanation=""
if declare -f explain_exit_code >/dev/null 2>&1; then
explanation="$(explain_exit_code "$rc")"
fi
printf "\e[?25h"
if [[ -n "$explanation" ]]; then
msg_error "in line ${caller_line}: exit code ${rc} (${explanation})"
else
msg_error "in line ${caller_line}: exit code ${rc}"
fi
msg_custom "→" "${YWB}" "${cmd}"
if [[ -s "$logfile" ]]; then
echo -e "\n${TAB}--- Last 20 lines of log ---"
tail -n 20 "$logfile"
echo -e "${TAB}----------------------------\n"
fi
exit "$rc"
fi
}
# ------------------------------------------------------------------------------
# Performs a curl request with retry logic and inline feedback.
# ------------------------------------------------------------------------------
run_curl() {
if [ "$VERB" = "no" ]; then
curl "$@" >/dev/null 2>>/tmp/curl_error.log
else
curl "$@" 2>>/tmp/curl_error.log
fi
}
curl_handler() {
local args=()
local url=""
local max_retries=0 delay=2 attempt=1
local exit_code has_output_file=false
for arg in "$@"; do
if [[ "$arg" != -* && -z "$url" ]]; then
url="$arg"
fi
[[ "$arg" == "-o" || "$arg" == --output ]] && has_output_file=true
args+=("$arg")
done
if [[ -z "$url" ]]; then
msg_error "no valid url or option entered for curl_handler"
exit 1
fi
$STD msg_info "Fetching: $url"
while :; do
if $has_output_file; then
$STD run_curl "${args[@]}"
exit_code=$?
else
$STD result=$(run_curl "${args[@]}")
exit_code=$?
fi
if [[ $exit_code -eq 0 ]]; then
stop_spinner
msg_ok "Fetched: $url"
$has_output_file || printf '%s' "$result"
return 0
fi
if ((attempt >= max_retries)); then
stop_spinner
if [ -s /tmp/curl_error.log ]; then
local curl_stderr
curl_stderr=$(</tmp/curl_error.log)
rm -f /tmp/curl_error.log
fi
__curl_err_handler "$exit_code" "$url" "$curl_stderr"
exit 1 # hard exit if exit_code is not 0
fi
$STD printf "\r\033[K${INFO}${YW}Retry $attempt/$max_retries in ${delay}s...${CL}" >&2
sleep "$delay"
((attempt++))
done
}
# ------------------------------------------------------------------------------
# Handles specific curl error codes and displays descriptive messages.
# ------------------------------------------------------------------------------
__curl_err_handler() {
local exit_code="$1"
local target="$2"
local curl_msg="$3"
case $exit_code in
1) msg_error "Unsupported protocol: $target" ;;
2) msg_error "Curl init failed: $target" ;;
3) msg_error "Malformed URL: $target" ;;
5) msg_error "Proxy resolution failed: $target" ;;
6) msg_error "Host resolution failed: $target" ;;
7) msg_error "Connection failed: $target" ;;
9) msg_error "Access denied: $target" ;;
18) msg_error "Partial file transfer: $target" ;;
22) msg_error "HTTP error (e.g. 400/404): $target" ;;
23) msg_error "Write error on local system: $target" ;;
26) msg_error "Read error from local file: $target" ;;
28) msg_error "Timeout: $target" ;;
35) msg_error "SSL connect error: $target" ;;
47) msg_error "Too many redirects: $target" ;;
51) msg_error "SSL cert verify failed: $target" ;;
52) msg_error "Empty server response: $target" ;;
55) msg_error "Send error: $target" ;;
56) msg_error "Receive error: $target" ;;
60) msg_error "SSL CA not trusted: $target" ;;
67) msg_error "Login denied by server: $target" ;;
78) msg_error "Remote file not found (404): $target" ;;
*) msg_error "Curl failed with code $exit_code: $target" ;;
esac
[[ -n "$curl_msg" ]] && printf "%s\n" "$curl_msg" >&2
exit 1
}
# ------------------------------------------------------------------------------
# shell_check()
#
# - Verifies that the script is running under Bash shell
# - Exits with error message if different shell is detected
# ------------------------------------------------------------------------------
shell_check() {
if [[ "$(ps -p $$ -o comm=)" != "bash" ]]; then
_cs_clear
msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell."
echo -e "\nExiting..."
sleep 2
exit 103 # shell is not Bash
fi
}
# ------------------------------------------------------------------------------
# clear_line()
#
# - Clears current terminal line using tput or ANSI escape codes
# - Moves cursor to beginning of line (carriage return)
# - Fallback to ANSI codes if tput not available
# ------------------------------------------------------------------------------
clear_line() {
tput cr 2>/dev/null || echo -en "\r"
tput el 2>/dev/null || echo -en "\033[K"
}
# ------------------------------------------------------------------------------
# is_verbose_mode()
#
# - Determines if script should run in verbose mode
# - Checks VERBOSE and var_verbose variables
# - Also returns true if not running in TTY (pipe/redirect scenario)
# ------------------------------------------------------------------------------
is_verbose_mode() {
local verbose="${VERBOSE:-${var_verbose:-no}}"
[[ "$verbose" != "no" || ! -t 2 ]]
}
### dev spinner ###
SPINNER_ACTIVE=0
SPINNER_PID=""
SPINNER_MSG=""
# -gA rather than a file-scope declare: this file is also reached through
# _cs_source_func, where a file-scope declare is local to the loader and gone
# once it returns, leaving an indexed array whose subscript bash evaluates as
# arithmetic. A message key would then be read as a division.
declare -gA MSG_INFO_SHOWN=()
# Trap cleanup on various signals
trap 'cleanup_spinner' EXIT INT TERM HUP
# Cleans up spinner process on exit
cleanup_spinner() {
stop_spinner
# Additional cleanup if needed
}
if ! declare -f _msg_fit >/dev/null 2>&1; then
_msg_fit() {
local msg="$1" reserved="${2:-4}" size cols max
size="$(stty size 2>/dev/null </dev/tty)" || size=""
cols="${size##* }"
[[ "$cols" =~ ^[1-9][0-9]*$ ]] || cols="${COLUMNS:-}"
if [[ ! "$cols" =~ ^[1-9][0-9]*$ ]]; then
printf '%s' "$msg"
return 0
fi
max=$((cols - reserved))
((max < 10)) && max=10
if ((${#msg} > max)); then
printf '%s…' "${msg:0:max-1}"
else
printf '%s' "$msg"
fi
}
fi
start_spinner() {
local msg="${1:-Processing...}"
local frames=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
local spin_i=0
local interval=0.1
# Set message and clear current line
SPINNER_MSG="$msg"
printf "\r\e[2K" >&2
# Stop any existing spinner
stop_spinner
# Set active flag
SPINNER_ACTIVE=1
local display
display="$(_msg_fit "$msg" 8)"
# Start spinner in background
{
while [[ "$SPINNER_ACTIVE" -eq 1 ]]; do
printf "\r\e[2K%s %b" "${TAB}${frames[spin_i]}${TAB}" "${YW}${display}${CL}" >&2
spin_i=$(((spin_i + 1) % ${#frames[@]}))
sleep "$interval"
done
} &
SPINNER_PID=$!
# Disown to prevent getting "Terminated" messages
disown "$SPINNER_PID" 2>/dev/null || true
}
stop_spinner() {
# Check if spinner is active and PID exists
if [[ "$SPINNER_ACTIVE" -eq 1 ]] && [[ -n "${SPINNER_PID}" ]]; then
SPINNER_ACTIVE=0
if kill -0 "$SPINNER_PID" 2>/dev/null; then
kill "$SPINNER_PID" 2>/dev/null
# Give it a moment to terminate
sleep 0.1
# Force kill if still running
if kill -0 "$SPINNER_PID" 2>/dev/null; then
kill -9 "$SPINNER_PID" 2>/dev/null
fi
# Wait for process but ignore errors
wait "$SPINNER_PID" 2>/dev/null || true
fi
# Clear spinner line
printf "\r\e[2K" >&2
SPINNER_PID=""
fi
}
spinner_guard() {
# Safely stop spinner if it's running
if [[ "$SPINNER_ACTIVE" -eq 1 ]] && [[ -n "${SPINNER_PID}" ]]; then
stop_spinner
fi
}
msg_info() {
local msg="${1:-Information message}"
# Only show each message once unless reset
if [[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]]; then
return
fi
MSG_INFO_SHOWN["$msg"]=1
spinner_guard
start_spinner "$msg"
}
msg_ok() {
local msg="${1:-Operation completed successfully}"
stop_spinner
printf "\r\e[2K%s %b\n" "${CM}" "${GN}${msg}${CL}" >&2
# Remove from shown messages to allow it to be shown again.
#
# The key has to be the one msg_info stored. It was sanitised here and raw
# there, so the two never matched: nothing was ever removed, and a message
# was suppressed for the rest of the run after its first appearance.
unset 'MSG_INFO_SHOWN['"$msg"']' 2>/dev/null || true
}
msg_error() {
local msg="${1:-An error occurred}"
stop_spinner
printf "\r\e[2K%s %b\n" "${CROSS}" "${RD}${msg}${CL}" >&2
}
msg_warn() {
stop_spinner
local msg="$1"
echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2
}
# Helper function to display a message with custom symbol and color
msg_custom() {
local symbol="${1:-*}"
local color="${2:-$CL}"
local msg="${3:-Custom message}"
[[ -z "$msg" ]] && return
stop_spinner
printf "\r\e[2K%s %b\n" "$symbol" "${color}${msg}${CL}" >&2
}
# ------------------------------------------------------------------------------
# msg_debug()
#
# - Displays debug message with timestamp when var_full_verbose=1
# - Automatically enables var_verbose if not already set
# - Uses bright yellow color for debug output
# ------------------------------------------------------------------------------
msg_debug() {
if [[ "${var_full_verbose:-0}" == "1" ]]; then
[[ "${var_verbose:-0}" != "1" ]] && var_verbose=1
echo -e "${YWB}[$(date '+%F %T')] [DEBUG]${CL} $*"
fi
}
error_handler() {
local exit_code="$?"
local line_number="${1:-unknown}"
local command="${2:-unknown}"
if declare -f post_update_to_api >/dev/null 2>&1; then
post_update_to_api "failed" "$exit_code"
fi
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
echo -e "\n$error_message\n"
cleanup_vmid
}
# Displays error message and immediately terminates script
fatal() {
msg_error "$1"
kill -INT $$
}
get_valid_nextid() {
local try_id
try_id=$(pvesh get /cluster/nextid)
while true; do
if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
try_id=$((try_id + 1))
continue
fi
if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
try_id=$((try_id + 1))
continue
fi
break
done
echo "$try_id"
}
cleanup_vmid() {
if [[ -z "${VMID:-}" ]]; then
return
fi
if qm status "$VMID" &>/dev/null; then
qm stop "$VMID" &>/dev/null
qm destroy "$VMID" &>/dev/null
fi
}
cleanup() {
local exit_code=$?
stop_spinner
if [[ "$(dirs -p | wc -l)" -gt 1 ]]; then
popd >/dev/null || true
fi
if [[ -n "${TEMP_DIR:-}" && -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
# Report final telemetry status if post_to_api_vm was called but no update was sent
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
if declare -f post_update_to_api >/dev/null 2>&1; then
if [[ $exit_code -ne 0 ]]; then
post_update_to_api "failed" "$exit_code"
elif [[ -n "${VMID:-}" ]] && qm config "$VMID" &>/dev/null; then
# Clean exit with the VM in place: a script that never said so itself.
post_update_to_api "done" "none"
else
# Clean exit, no VM: the user backed out of a dialog.
post_update_to_api "aborted" "0"
fi
fi
fi
}
check_root() {
if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
_cs_clear
msg_error "Please run this script as root."
echo -e "\nExiting..."
sleep 2
exit 104 # not running as root
fi
}
pve_check() {
local pve_ver
pve_ver="$(declare -f _pve_version >/dev/null 2>&1 && _pve_version || pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
if [[ "$pve_ver" =~ ^8\.([0-9]+) ]]; then
local minor="${BASH_REMATCH[1]}"
if ((minor < 0 || minor > 9)); then
msg_error "This version of Proxmox VE is not supported."
msg_error "Supported: Proxmox VE version 8.0 – 8.9"
exit 105
fi
return 0
fi
if [[ "$pve_ver" =~ ^9\.([0-9]+) ]]; then
local minor="${BASH_REMATCH[1]}"
if ((minor < 0 || minor > 2)); then
msg_error "This version of Proxmox VE is not supported."
msg_error "Supported: Proxmox VE version 9.0 – 9.2"
exit 105
fi
return 0
fi
msg_error "This version of Proxmox VE is not supported."
msg_error "Supported versions: Proxmox VE 8.0 – 8.9 or 9.0 – 9.2"
exit 105
}
arch_check() {
if [ "$(dpkg --print-architecture)" != "amd64" ]; then
echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
echo -e "Exiting..."
sleep 2
exit
fi
}
ssh_check() {
# Purely advisory, and there is nobody to advise on an unattended run.
[[ "${VM_UNATTENDED:-0}" == "1" ]] && return 0
if command -v pveversion >/dev/null 2>&1 && [ -n "${SSH_CLIENT:-}" ]; then
if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
:
else
_cs_clear
exit
fi
fi
}
exit_script() {
# No screen clear: it wiped the line explaining why we stopped. Most callers
# are validation failures, so they can pass their own reason.
echo -e "\n${CROSS}${RD}${1:-User exited script}${CL}\n"
exit
}
sanitize_vm_hostname() {
local hostname="${1,,}"
hostname=$(echo "$hostname" | tr -cs 'a-z0-9-' '-' | sed 's/^-//;s/-$//')
echo "${hostname:0:63}"
}
vm_confirm_new_vm() {
local title="$1"
local message="$2"
local height="${3:-10}"
local width="${4:-58}"
# Unattended runs have nobody to answer this, and vm_start_script already
# skips the wizard for them -- without this the run hangs on the prompt.
[[ "${VM_UNATTENDED:-0}" == "1" ]] && return 0
vm_dialog yesno "$title" "$message" "$height" "$width"
}
vm_choose_settings_mode() {
local message="${1:-Use Default Settings?}"
local height="${2:-10}"
local width="${3:-58}"
local rc=0
# 0 is Default, 1 is the Advanced button. vm_dialog turns anything else --
# ESC, or a dialog that could not be drawn -- into a fatal error that says
# why, instead of falling through to Advanced and letting the next prompt
# "exit" the operator from a wizard they never asked for.
vm_dialog yesno "SETTINGS" "$message" --no-button Advanced "$height" "$width" || rc=$?
if ((rc == 0)); then
VM_SETTINGS_MODE="default"
else
VM_SETTINGS_MODE="advanced"
fi
return $rc
}
# ------------------------------------------------------------------------------
# vm_echo_default_settings()
#
# Prints the settings summary from the variables default_settings has just set.
# Every VM script wrote this block itself and they agreed on thirteen of the
# fifteen lines; the two that differed were the CPU model label and the app
# name, both derivable. The labels are computed rather than passed, so a script
# cannot print "Host" while CPU_TYPE says otherwise.
# ------------------------------------------------------------------------------
vm_echo_default_settings() {
local cpu_label
cpu_label="$(vm_cpu_model_label)"
local cache_label="None"
[[ -n "${DISK_CACHE:-}" ]] && cache_label="Write Through"
# vm_machine_type_label takes the type as an argument, not from the
# environment, so MACHINE_TYPE has to be handed over explicitly -- calling it
# bare silently reported i440fx for every machine.
local machine_label
machine_label="$(vm_machine_type_label "${MACHINE_TYPE:-i440fx}")"
echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}${machine_label}${CL}"
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}${cache_label}${CL}"
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}${cpu_label}${CL}"
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}${VLAN:-Default}${CL}"
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}${MTU:-Default}${CL}"
echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}"
# Shown on the default path too, so VERBOSE=yes is visible.
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}${VERBOSE:-no}${CL}"
echo -e "${CREATING}${BOLD}${DGN}Creating a ${APP:-${NSAPP}} VM using the above default settings${CL}"
}
# ------------------------------------------------------------------------------
# vm_start_script()
#
# The default-or-advanced fork, which was byte for byte the same in every VM
# script that had been migrated. default_settings and advanced_settings are
# resolved when this runs rather than when it is defined, so each script still
# supplies its own.
#
# Arguments: the dialog message, and optionally its height and width -- a script
# that lists what its defaults are needs a taller box than one asking a bare
# question.
# ------------------------------------------------------------------------------
# A terminal too small for a dialog used to surface as "User exited script".
# Check once, up front, and name the real problem.
vm_check_dialog_env() {
local cols rows
# The storage picker is the tallest dialog at 16 rows; the rest need 10.
local need_rows=18 need_cols=60
if ! command -v whiptail >/dev/null 2>&1; then
msg_error "whiptail is not installed, so the settings wizard cannot run"
msg_error "Install it with: apt-get install -y whiptail"
exit 1
fi
if [[ ! -t 2 ]] && [[ ! -r /dev/tty ]]; then
msg_error "No terminal is available for the settings wizard"
msg_error "Run the script directly rather than piping it into a shell:"
msg_error " bash -c \"\$(curl -fsSL <script-url>)\""
exit 1
fi
cols=$(tput cols 2>/dev/null || echo 0)
rows=$(tput lines 2>/dev/null || echo 0)
if ((cols > 0 && rows > 0)) && ((cols < need_cols || rows < need_rows)); then
msg_error "The terminal is ${cols}x${rows}; the wizard needs at least ${need_cols}x${need_rows}"
msg_error "Enlarge the window, or use a smaller console font, and run it again."
exit 1
fi
}
# VM_UNATTENDED=1 takes the script's defaults and lets the environment override
# them, so a run needs no tty. Names are prefixed because VMID, HN and friends
# are the script's own working variables.
vm_apply_env_overrides() {
[[ -n "${VM_VMID:-}" ]] && VMID="$VM_VMID"
[[ -n "${VM_DISK_SIZE:-}" ]] && DISK_SIZE="$VM_DISK_SIZE"
[[ -n "${VM_DISK_CACHE:-}" ]] && DISK_CACHE="$VM_DISK_CACHE"
[[ -n "${VM_HOSTNAME:-}" ]] && HN="$VM_HOSTNAME"
[[ -n "${VM_CORES:-}" ]] && CORE_COUNT="$VM_CORES"
[[ -n "${VM_RAM:-}" ]] && RAM_SIZE="$VM_RAM"
[[ -n "${VM_BRIDGE:-}" ]] && BRG="$VM_BRIDGE"
[[ -n "${VM_MAC:-}" ]] && MAC="$VM_MAC"
[[ -n "${VM_VLAN:-}" ]] && VLAN=",tag=${VM_VLAN}"
[[ -n "${VM_MTU:-}" ]] && MTU=",mtu=${VM_MTU}"
[[ -n "${VM_START:-}" ]] && START_VM="$VM_START"
[[ -n "${VM_STORAGE:-}" ]] && STORAGE="$VM_STORAGE"
[[ -n "${VM_MACHINE:-}" ]] && vm_apply_machine_type "$VM_MACHINE"
if [[ -n "${VM_CPU:-}" ]]; then
[[ "$VM_CPU" == "host" ]] && CPU_TYPE=" -cpu host" || CPU_TYPE=""
fi
return 0
}
vm_preflight() {
[[ -n "${_CS_VM_PREFLIGHT_DONE:-}" ]] && return 0
_CS_VM_PREFLIGHT_DONE=1
check_root
[[ "${var_arm64:-no}" == "yes" ]] || arch_check
pve_check
ssh_check
}
vm_start_script() {
vm_preflight
if [[ "${VM_UNATTENDED:-0}" == "1" ]]; then
METHOD="unattended"
default_settings
vm_apply_env_overrides
echo -e "${INFO}${BOLD}${DGN}Unattended: script defaults with environment overrides${CL}"
return 0
fi
vm_check_dialog_env
if vm_choose_settings_mode "$@"; then
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
default_settings
else
header_info
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
# Scripts that ask the Cloud-Init question before this fork -- it picks the
# disk image, so a default run has to answer it too -- never get to offer
# the rest of it. Offer it here. A no-op when Cloud-Init is off or when
# advanced_settings asks the question itself, which chains its own.
vm_prompt_cloud_init_advanced
advanced_settings
fi
}
vm_confirm_advanced_settings() {
local message="$1"
local height="${2:-10}"
local width="${3:-58}"
vm_dialog yesno "ADVANCED SETTINGS COMPLETE" "$message" --no-button Do-Over "$height" "$width"
}
# One entry point for every dialog. Result lands in VM_DIALOG_RESULT, since the
# return code carries the distinction: 0 = OK, 1 = cancel, anything else means
# whiptail could not draw and is fatal with a reason.
VM_DIALOG_RESULT=""
vm_dialog() {
local box="${1:?box type}" title="${2:-}"
shift 2
local rc=0 out="" cols rows
# A spinner writes asynchronously and would be left stranded on the dialog.
declare -f stop_spinner >/dev/null 2>&1 && stop_spinner
VM_DIALOG_RESULT=""