Repository navigation
Expand file tree
/
Copy pathbackend.func
More file actions
3857 lines (3444 loc) · 154 KB
/
Copy pathbackend.func
File metadata and controls
3857 lines (3444 loc) · 154 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://github.com/community-scripts/DevScripts/raw/main/LICENSE
# ==============================================================================
# PVE-BACKEND.FUNC - PROXMOX VE LXC CREATE / DEPLOY BACKEND
# ==============================================================================
# build_container, create_lxc_container, storage helpers, description, traps.
# Loaded by core/build.func after ui/build-ui.func on Proxmox hosts only.
# ==============================================================================
[[ -n "${_PVE_BACKEND_FUNC_LOADED:-}" ]] && return
_PVE_BACKEND_FUNC_LOADED=1
# ==============================================================================
# SECTION 8: CONTAINER CREATION & DEPLOYMENT
# ==============================================================================
# ------------------------------------------------------------------------------
# _apply_http_proxy_in_container()
# ------------------------------------------------------------------------------
_apply_http_proxy_in_container() {
local proxy="${HTTP_PROXY:-}"
local noproxy="${HTTP_NO_PROXY:-}"
[[ -z "$proxy" || -z "${CTID:-}" ]] && return 0
[[ -z "$noproxy" ]] && noproxy="localhost,127.0.0.1,.local"
msg_info "Applying HTTP proxy in container"
if ! pct exec "$CTID" -- sh -c 'proxy="$1"; noproxy="$2"; cat > /etc/profile.d/90-http-proxy.sh <<EOF
export http_proxy="$proxy"
export https_proxy="$proxy"
export HTTP_PROXY="$proxy"
export HTTPS_PROXY="$proxy"
export no_proxy="$noproxy"
export NO_PROXY="$noproxy"
EOF
chmod 644 /etc/profile.d/90-http-proxy.sh' sh "$proxy" "$noproxy" >/dev/null 2>&1; then
msg_warn "Failed to write HTTP proxy profile into container"
return 0
fi
if [[ "$var_os" != "alpine" && "${APT_CACHER:-}" != "yes" ]]; then
pct exec "$CTID" -- sh -c 'proxy="$1"; cat > /etc/apt/apt.conf.d/95http-proxy <<EOF
Acquire::http::Proxy "$proxy";
Acquire::https::Proxy "$proxy";
EOF' sh "$proxy" >/dev/null 2>&1 || true
fi
msg_ok "Applied HTTP proxy in container"
}
_apply_host_ca_certs_in_container() {
local host_ca_dir="/usr/local/share/ca-certificates"
[[ -z "${CTID:-}" ]] && return 0
local inherit_host_ca="${INHERIT_HOST_CA:-${var_inherit_host_ca:-no}}"
local -a host_certs=()
local cert
shopt -s nullglob
for cert in "$host_ca_dir"/*.crt; do
host_certs+=("$cert")
done
shopt -u nullglob
[[ ${#host_certs[@]} -eq 0 ]] && return 0
case "${inherit_host_ca,,}" in
yes | true | 1 | on) ;;
*)
return 0
;;
esac
msg_info "Inheriting host CA certificates into container"
local found=${#host_certs[@]}
local copied=0
local skipped=0
local cert_name
pct exec "$CTID" -- mkdir -p /usr/local/share/ca-certificates >/dev/null 2>&1 || {
msg_warn "Failed to create CA certificate directory in container"
return 0
}
for cert in "${host_certs[@]}"; do
cert_name="$(basename "$cert")"
if [[ ! -r "$cert" || "$cert_name" != *.crt ]]; then
msg_warn "Skipping invalid or unreadable host CA certificate: ${cert_name}"
skipped=$((skipped + 1))
continue
fi
if pct push "$CTID" "$cert" "/usr/local/share/ca-certificates/${cert_name}" >/dev/null 2>&1; then
pct exec "$CTID" -- chmod 644 "/usr/local/share/ca-certificates/${cert_name}" >/dev/null 2>&1 || true
copied=$((copied + 1))
else
msg_warn "Failed to push host CA certificate: ${cert_name}"
skipped=$((skipped + 1))
fi
done
if [[ $copied -eq 0 ]]; then
msg_warn "No host CA certificates were copied (${found} found, ${skipped} skipped)"
return 0
fi
local refresh_shell="bash"
[[ "$var_os" == "alpine" ]] && refresh_shell="ash"
if pct exec "$CTID" -- "$refresh_shell" -c 'command -v update-ca-certificates >/dev/null 2>&1 && update-ca-certificates' >/dev/null 2>&1; then
msg_ok "Inherited ${copied} host CA certificate(s) and updated trust store (${skipped} skipped)"
else
msg_warn "Copied ${copied} host CA certificate(s), but trust store update failed or update-ca-certificates is unavailable (${skipped} skipped)"
fi
}
reclaim_tty() {
command -v perl >/dev/null 2>&1 || return 0
[[ -e /dev/tty ]] || return 0
local pgid
pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ') || true
perl -e '
use POSIX;
$SIG{TTOU} = "IGNORE";
my $pgid = $ARGV[0] || POSIX::getpgrp();
open(my $t, "+<", "/dev/tty") or exit 1;
POSIX::tcsetpgrp(fileno($t), $pgid) or exit 1;
' "${pgid:-0}" 2>/dev/null || true
}
# pct create extracts the template under the caller's umask. The GUI calls it
# from pvedaemon, which systemd starts with 022; we run from the operator's
# shell, so a hardened 027 there gave containers /etc at 750 -- and apt cannot
# resolve then, because it fetches as _apt. Same template, same pct, different
# umask. Scoped to the call, so the operator's umask still governs everything
# else we write on the host.
_pct_create() {
local prev rc
prev="$(umask)"
umask 022
pct create "$@"
rc=$?
umask "$prev"
return $rc
}
# ------------------------------------------------------------------------------
# _pct_start_failed <ctid> [pct start output]
#
# pct says only that the startup failed; lxc writes why to the container's
# journal. Shows those lines and appends them to the build log, whose tail is
# what the telemetry reports: 80 failed starts a week arrived there as nothing
# more than "failed to start".
# ------------------------------------------------------------------------------
_pct_start_failed() {
local ctid="$1" out="${2:-}" reason line
# The last start attempt only, without systemd's own bookkeeping.
reason="$(journalctl -u "pve-container@${ctid}" -n 80 --no-pager -o cat 2>/dev/null |
awk '/^Starting pve-container@/ { n = 0; next } { l[++n] = $0 } END { for (i = 1; i <= n; i++) print l[i] }' |
grep -vE '^pve-container@[0-9]+\.service: |^Failed to start pve-container@|To get more details, run the container in foreground mode' |
tail -n 10 || true)"
{
echo "--- pct start ${ctid} ---"
[[ -n "$out" ]] && echo "$out"
[[ -n "$reason" ]] && echo "$reason"
} >>"${BUILD_LOG:-/dev/null}" 2>/dev/null
while IFS= read -r line; do
[[ -n "$line" ]] && echo -e "${TAB}${line}"
done <<<"${reason:-$out}"
msg_custom "🔧" "${YW}" "More: journalctl -u pve-container@${ctid}, or in the foreground: lxc-start -n ${ctid} -F -l DEBUG -o /tmp/lxc-${ctid}.log"
}
# Restart after a config change, and say something useful when it fails. The
# two call sites used to be `pct start "$CTID" >/dev/null 2>&1`, so a failure
# aborted the run under errexit with a bare 255 and the output discarded - six
# runs across four apps in the telemetry, none of them diagnosable. pct can
# also return 0 and leave the container stopped, so ask for the real state.
_pct_restart() {
local ctid="$1" what="$2" err
err="$(pct start "$ctid" 2>&1)" || true
[[ "$(pct status "$ctid" 2>/dev/null)" == "status: running" ]] && return 0
msg_error "Container ${ctid} did not start after ${what}"
_pct_start_failed "$ctid" "$err"
return 1
}
# Where a template lands in $TEMPLATE_STORAGE, or the default dir layout when
# pvesm cannot say.
_template_path() {
local p
p="$(pvesm path "${TEMPLATE_STORAGE}:vztmpl/$1" 2>/dev/null)" || p=""
echo "${p:-/var/lib/vz/template/cache/$1}"
}
# ------------------------------------------------------------------------------
# _template_download_failed <pveam output>
#
# Says why the template download failed, as far as the host can tell, and
# files a network or storage problem as such for the telemetry instead of as
# a Proxmox one.
# ------------------------------------------------------------------------------
_template_download_failed() {
local out="$1" host="download.proxmox.com" last
last="$(grep -iE 'failed|error|no such|denied|no space' <<<"$out" | tail -n1)"
msg_error "Template download failed after 3 attempts: ${TEMPLATE}"
[[ -n "$last" ]] && msg_custom "ℹ️" "${YW}" "pveam: ${last}"
if grep -qi 'no space left' <<<"$out"; then
msg_custom "🔧" "${YW}" "Free up space on storage '${TEMPLATE_STORAGE}', then retry."
export ERROR_CATEGORY_OVERRIDE="storage"
elif grep -q '^http_proxy:' /etc/pve/datacenter.cfg 2>/dev/null; then
msg_custom "🔧" "${YW}" "Downloads go through the HTTP proxy set in Datacenter → Options; check that it is reachable."
export ERROR_CATEGORY_OVERRIDE="network"
elif ! getent hosts "$host" &>/dev/null; then
msg_custom "🔧" "${YW}" "This host cannot resolve ${host}: check its DNS servers (node → System → DNS)."
export ERROR_CATEGORY_OVERRIDE="network"
elif grep -qiE 'unreachable|timed out|connection refused|name resolution|unable to resolve|interrupted by signal' <<<"$out" ||
! curl -fsI --connect-timeout 10 --max-time 20 -o /dev/null "http://${host}/images/system/"; then
msg_custom "🔧" "${YW}" "This host cannot reliably reach ${host}: check its gateway, firewall and connection."
export ERROR_CATEGORY_OVERRIDE="network"
else
msg_custom "🔧" "${YW}" "Try it by hand: pveam download ${TEMPLATE_STORAGE} ${TEMPLATE}"
fi
}
# ------------------------------------------------------------------------------
# lxc_os_supported <os> <version>
#
# Asks this host's pve-container whether it can set up an OS release, through
# the same constructor pct create runs, so old and new pve-container answer
# exactly as they would later. Returns 0 supported, 1 not supported, 2 unknown
# (other OS types, or the check itself could not run).
# ------------------------------------------------------------------------------
lxc_os_supported() {
local os="$1" ver="$2" module dir rc
case "$os" in
debian) module="Debian" ;;
ubuntu) module="Ubuntu" ;;
*) return 2 ;;
esac
# Debian templates always carry a point release, and older pve-container
# compared it as a number: 13.6 fails "<= 13" where a bare 13 passes.
[[ "$os" == "debian" && "$ver" != *.* ]] && ver="${ver}.1"
dir="$(mktemp -d 2>/dev/null)" || return 2
mkdir -p "$dir/etc"
if [[ "$os" == "debian" ]]; then
echo "$ver" >"$dir/etc/debian_version"
else
printf 'DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=%s\n' "$ver" >"$dir/etc/lsb-release"
fi
rc=0
perl -e '
my ($module, $root) = @ARGV;
eval "require PVE::LXC::Setup::$module; 1" or exit 2;
exit 0 if eval { "PVE::LXC::Setup::$module"->new({}, $root, undef, sub {}); 1 };
exit($@ =~ /unsupported|too old/i ? 1 : 2);
' "$module" "$dir" 2>/dev/null || rc=$?
rm -rf "$dir"
return "$rc"
}
# ------------------------------------------------------------------------------
# lxc_os_support_check()
#
# Runs as soon as the OS release is settled, before storage selection and the
# template download. A pve-container too old for the release used to surface
# only after both, as a failed pct create.
# Offers what can actually help on this host: updating the LXC stack when a
# newer pve-container is available, or the newest older release it supports.
# ------------------------------------------------------------------------------
lxc_os_support_check() {
local os="${var_os:-}" ver="${var_version:-}" rc=0
[[ -n "$os" && -n "$ver" ]] || return 0
lxc_os_supported "$os" "$ver" || rc=$?
[[ $rc -eq 1 ]] || return 0
local installed candidate="" older="" v
installed="$(dpkg-query -W -f='${Version}' pve-container 2>/dev/null)"
v="$(apt-cache policy pve-container 2>/dev/null | awk '/Candidate:/ {print $2}')"
if [[ -n "$v" && "$v" != "(none)" ]] && dpkg --compare-versions "$v" gt "${installed:-0}"; then
candidate="$v"
fi
while read -r v; do
dpkg --compare-versions "$v" lt "$ver" || continue
if lxc_os_supported "$os" "$v"; then
older="$v"
break
fi
done < <(_pveam_available | awk '{print $2}' | sed -nE "s/^${os}-([0-9]+(\.[0-9]+)?)-standard_.*/\1/p" | sort -u -rV)
msg_error "pve-container ${installed:-?} on this host cannot create ${os^} ${ver} containers."
local interactive=0
[[ "${PHS_SILENT:-0}" != "1" && -t 0 ]] && command -v whiptail &>/dev/null && interactive=1
if [[ $interactive -eq 0 || (-z "$candidate" && -z "$older") ]]; then
if [[ -n "$candidate" ]]; then
msg_custom "🔧" "${YW}" "pve-container ${candidate} is available: run 'apt update && apt dist-upgrade' on the host, then retry."
else
msg_custom "🔧" "${YW}" "No newer pve-container in the configured repositories. Enable a Proxmox VE repository"
msg_custom " " "${YW}" "(the post-pve-install tool can set one up), run 'apt update && apt dist-upgrade', then retry."
fi
[[ -n "$older" ]] && msg_custom "💡" "${YW}" "Or install with ${os^} ${older}: var_version=${older}"
exit 231
fi
local -a menu=()
[[ -n "$candidate" ]] && menu+=("upgrade" "Update the LXC stack to pve-container ${candidate} (apt dist-upgrade)")
[[ -n "$older" ]] && menu+=("older" "Use ${os^} ${older} instead (not every app supports it)")
menu+=("cancel" "Cancel")
local choice
choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${os^} ${ver} not supported" --menu \
"\npve-container ${installed:-?} on this host cannot create ${os^} ${ver} containers.\n\nHow do you want to continue?" \
16 78 3 "${menu[@]}" 3>&1 1>&2 2>&3) || choice="cancel"
case "$choice" in
upgrade)
msg_info "Updating the Proxmox LXC stack"
apt_update_safe
if ! DEBIAN_FRONTEND=noninteractive $STD apt-get -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold dist-upgrade -y; then
msg_error "The upgrade failed. Check the APT output above, then retry."
exit 231
fi
if ! pct list &>/dev/null; then
msg_error "The upgrade left the Proxmox tools unusable. Run 'apt update && apt dist-upgrade' and reboot, then retry."
exit 231
fi
msg_ok "Updated the Proxmox LXC stack"
rc=0
lxc_os_supported "$os" "$ver" || rc=$?
if [[ $rc -eq 1 ]]; then
msg_error "pve-container $(dpkg-query -W -f='${Version}' pve-container 2>/dev/null) still cannot create ${os^} ${ver} containers."
exit 231
fi
;;
older)
var_version="$older"
msg_ok "Using ${os^} ${older}"
;;
*)
msg_error "Cancelled by user."
exit 231
;;
esac
}
# ------------------------------------------------------------------------------
# build_container()
#
# - Main function for creating and configuring LXC container
# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6)
# - Creates container via pct create with all specified settings
# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough
# - Starts container and waits for network connectivity
# - Installs base packages (curl, sudo, etc.)
# - Injects SSH keys if configured
# - Executes <app>-install.sh inside container
# - Posts installation telemetry to API if diagnostics enabled
# ------------------------------------------------------------------------------
build_container() {
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
lxc_os_support_check
NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
if [[ -n "${var_sdn_vnet:-${SDN_VNET:-}}" ]]; then
NET_STRING="-net0 name=eth0,bridge=${var_sdn_vnet:-$SDN_VNET}"
fi
# MAC
if [[ -n "$MAC" ]]; then
case "$MAC" in
,hwaddr=*) NET_STRING+="$MAC" ;;
*) NET_STRING+=",hwaddr=$MAC" ;;
esac
fi
# IP (always required, default dhcp)
NET_STRING+=",ip=${NET:-dhcp}"
# Gateway
if [[ -n "$GATE" ]]; then
case "$GATE" in
,gw=) ;;
,gw=*) NET_STRING+="$GATE" ;;
*) NET_STRING+=",gw=$GATE" ;;
esac
fi
# VLAN
if [[ -n "$VLAN" ]]; then
case "$VLAN" in
,tag=*) NET_STRING+="$VLAN" ;;
*) NET_STRING+=",tag=$VLAN" ;;
esac
fi
# MTU
if [[ -n "$MTU" ]]; then
case "$MTU" in
,mtu=*) NET_STRING+="$MTU" ;;
*) NET_STRING+=",mtu=$MTU" ;;
esac
fi
# IPv6 Handling
case "$IPV6_METHOD" in
auto) NET_STRING="$NET_STRING,ip6=auto" ;;
dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
static)
if [[ -n "$IPV6_ADDR" ]]; then
NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
[ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
fi
;;
none) ;;
esac
# Build FEATURES string based on container type and user choices
FEATURES=""
# Nesting support (user configurable, default enabled)
if [ "${ENABLE_NESTING:-1}" == "1" ]; then
FEATURES="nesting=1"
fi
# Keyctl for unprivileged containers (needed for Docker)
if [ "$CT_TYPE" == "1" ]; then
[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
FEATURES="${FEATURES}keyctl=1"
fi
if [ "$ENABLE_FUSE" == "yes" ]; then
[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
FEATURES="${FEATURES}fuse=1"
fi
if [ "${ENABLE_MKNOD:-0}" == "1" ]; then
[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
FEATURES="${FEATURES}mknod=1"
fi
if [ -n "${ALLOW_MOUNT_FS:-}" ]; then
local _mount_clean="${ALLOW_MOUNT_FS// /}"
_mount_clean="${_mount_clean%%,}"
_mount_clean="${_mount_clean##,}"
_mount_clean="${_mount_clean%%;}"
_mount_clean="${_mount_clean//,/;}"
if [ -n "$_mount_clean" ]; then
[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
FEATURES="${FEATURES}mount=${_mount_clean}"
fi
fi
# Build PCT_OPTIONS as string for export
TEMP_DIR=$(mktemp -d)
pushd "$TEMP_DIR" >/dev/null
# One bootstrap for every OS. install.func detects Alpine itself and has
# carried apk, OpenRC and BusyBox branches all along; the split only meant
# those branches never ran and Alpine got a second, thinner copy of the same
# eight functions that had to be fixed separately every time.
export FUNCTIONS_FILE_PATH="$(_cs_fetch_text "lxc/install.func")"
# Core exports for install.func
export DIAGNOSTICS="$DIAGNOSTICS"
export RANDOM_UUID="$RANDOM_UUID"
export EXECUTION_ID="$EXECUTION_ID"
export SESSION_ID="$SESSION_ID"
# Repo attribution for container-side progress pings (container inherits the
# host's detection instead of re-detecting from its own cmdline)
export REPO_SOURCE="${REPO_SOURCE:-}"
export REPO_SLUG="${REPO_SLUG:-}"
# Virtualization platform for telemetry (container has no pveversion)
export TELEMETRY_PLATFORM="pve"
export CACHER="$APT_CACHER"
export CACHER_IP="$APT_CACHER_IP"
if [[ -n "${HTTP_PROXY:-}" ]]; then
export HTTP_PROXY HTTPS_PROXY="$HTTP_PROXY" http_proxy="$HTTP_PROXY" https_proxy="$HTTP_PROXY"
export NO_PROXY="${HTTP_NO_PROXY:-}" no_proxy="${HTTP_NO_PROXY:-}"
export var_http_proxy="$HTTP_PROXY"
export var_http_no_proxy="${HTTP_NO_PROXY:-}"
fi
export tz="$timezone"
export APPLICATION="$APP"
export app="$NSAPP"
export PASSWORD="$PW"
export VERBOSE="$VERBOSE"
export SSH_ROOT="${SSH}"
export SSH_AUTHORIZED_KEY
export CTID="$CT_ID"
export CTTYPE="$CT_TYPE"
export ENABLE_FUSE="$ENABLE_FUSE"
export ENABLE_TUN="$ENABLE_TUN"
export PCT_OSTYPE="$var_os"
export PCT_OSVERSION="$var_version"
export PCT_ARCH="$(dpkg --print-architecture 2>/dev/null || uname -m)"
export PCT_DISK_SIZE="$DISK_SIZE"
# load_vars_file exports var_ram from default.vars; hand over what the container really got.
export var_ram="$RAM_SIZE"
export IPV6_METHOD="$IPV6_METHOD"
export ENABLE_GPU="$ENABLE_GPU"
export APPLICATION_VERSION="${var_appversion:-}"
# The container writes its own MOTD, so it has to know whether this is a
# testing build. lxc-attach carries this across.
declare -f cs_testing_state >/dev/null 2>&1 && cs_testing_state
# DEV_MODE exports (optional, for debugging)
export BUILD_LOG="$BUILD_LOG"
export INSTALL_LOG="/root/.install-${SESSION_ID}.log"
export COMMUNITY_SCRIPTS_URL="$COMMUNITY_SCRIPTS_URL"
# The engine lives in its own repo — the container needs that base too,
# otherwise its misc/*.func loads fall back to the hardcoded default.
export COMMUNITY_SCRIPTS_CORE_URL="$COMMUNITY_SCRIPTS_CORE_URL"
# Keep host-side logging on BUILD_LOG (not exported — invisible to container)
# Without this, get_active_logfile() would return INSTALL_LOG (a container path)
# and all host msg_info/msg_ok/msg_error would write to /root/.install-SESSION.log
# on the HOST instead of BUILD_LOG, causing incomplete telemetry logs.
_HOST_LOGFILE="$BUILD_LOG"
export dev_mode="${dev_mode:-}"
export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}"
export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}"
export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}"
export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}"
export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}"
export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}"
export DEV_MODE_NET="${DEV_MODE_NET:-false}"
export DEV_MODE_TIMING="${DEV_MODE_TIMING:-false}"
# MODE export for unattended detection in install scripts
# This tells install scripts whether to prompt for input or use defaults
export MODE="${METHOD:-default}"
validate_ct_settings
# Build PCT_OPTIONS as multi-line string
PCT_OPTIONS_STRING=" -hostname $HN"
# Only add -tags if TAGS is not empty
if [ -n "$TAGS" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
-tags $TAGS"
fi
# Only add -features if FEATURES is not empty
if [ -n "$FEATURES" ]; then
PCT_OPTIONS_STRING=" -features $FEATURES
$PCT_OPTIONS_STRING"
fi
# Add storage if specified
if [ -n "$SD" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
$SD"
fi
# Add nameserver if specified
if [ -n "$NS" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
$NS"
fi
# Network configuration
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
$NET_STRING
-onboot 1
-cores $CORE_COUNT
-memory $RAM_SIZE
-unprivileged $CT_TYPE"
# Protection flag (if var_protection was set)
if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
-protection 1"
fi
# pct takes UTC or a zone.tab name only; aliases resolve, "host" is read here
if [ -n "${CT_TIMEZONE:-}" ]; then
local _pct_timezone
_pct_timezone="$(pct_timezone "$CT_TIMEZONE")"
if [[ -n "$_pct_timezone" ]]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
-timezone $_pct_timezone"
else
msg_warn "Timezone '${CT_TIMEZONE}' cannot be set by pct; the container keeps the template's (UTC)."
fi
fi
# PVE derives ostype from the template name, then rejects its own value when
# reading the config back - which breaks every later pct call.
case "${var_os:-}" in
openeuler | rockylinux | almalinux)
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
-ostype centos"
;;
esac
# The password stays out of PCT_OPTIONS: that string is expanded unquoted, so
# word splitting and globbing would reach it -- a space made two arguments, a
# * matched a filename. It goes in as its own quoted argument.
_PW_ARGS=()
[[ -n "${PW:-}" ]] && _PW_ARGS=(--password "$PW")
# Export as string (this works, unlike arrays!)
export PCT_OPTIONS="$PCT_OPTIONS_STRING"
export TEMPLATE_STORAGE="${var_template_storage:-}"
export CONTAINER_STORAGE="${var_container_storage:-}"
[[ -n "${HTTP_PROXY:-}" ]] && export HTTP_PROXY HTTPS_PROXY="$HTTP_PROXY" http_proxy="$HTTP_PROXY" https_proxy="$HTTP_PROXY"
[[ -n "${HTTP_NO_PROXY:-}" ]] && export NO_PROXY="$HTTP_NO_PROXY" no_proxy="$HTTP_NO_PROXY"
# Every storage path below shells out to pvesm. Where that binary is missing
# this is not a Proxmox host at all, and the `pvesm ... 2>/dev/null | awk`
# pipelines just returned a bare 127 with the diagnostic thrown away - the
# single largest group in our telemetry. Say what actually happened instead.
if ! command -v pvesm >/dev/null 2>&1; then
msg_error "pvesm is not available - this backend expects a Proxmox VE host"
if declare -f detect_lxc_platform >/dev/null 2>&1; then
msg_custom "ℹ️" "${YW}" "Detected platform: $(detect_lxc_platform). Override with LXC_PLATFORM=pve|incus if that is wrong"
fi
exit 1
fi
# Validate storage space only if CONTAINER_STORAGE is already set
# (Storage selection happens in create_lxc_container for some modes)
if [[ -n "$CONTAINER_STORAGE" ]]; then
msg_info "Validating storage space"
if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then
local free_space
free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
local free_fmt
free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB")
msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}"
exit 214
fi
msg_ok "Storage space validated"
check_storage_health "$CONTAINER_STORAGE" "$DISK_SIZE"
fi
create_lxc_container || exit $?
# Transition to 'configuring' — container created, now setting up OS/userland
post_progress_to_api "configuring"
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
# ============================================================================
# GPU/USB PASSTHROUGH CONFIGURATION
# ============================================================================
# Check if GPU passthrough is enabled
# Returns true only if var_gpu is explicitly set to "yes"
# Can be set via:
# - Environment variable: var_gpu=yes bash -c "..."
# - CT script default: var_gpu="${var_gpu:-no}"
# - Advanced settings wizard
# - App defaults file: /usr/local/community-scripts/defaults/<app>.vars
is_gpu_app() {
[[ "${var_gpu:-no}" == "yes" ]] && return 0
return 1
}
# Detect all available GPU devices
detect_gpu_devices() {
INTEL_DEVICES=()
AMD_DEVICES=()
NVIDIA_DEVICES=()
# Store PCI info to avoid multiple calls
local pci_vga_info
pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D" || true)
if [[ -z "$pci_vga_info" ]]; then
msg_debug "No VGA/Display/3D PCI devices found"
return 0
fi
if [[ -d /dev/dri ]]; then
for d in /dev/dri/renderD* /dev/dri/card*; do
[[ -e "$d" ]] || continue
local node vendor
node=$(basename "$d")
vendor=$(cat "/sys/class/drm/${node}/device/vendor" 2>/dev/null || true)
case "$vendor" in
0x8086) INTEL_DEVICES+=("$d") ;;
0x1002 | 0x1022) AMD_DEVICES+=("$d") ;;
0x10de) ;;
*) msg_debug "Unmapped GPU vendor '${vendor:-unknown}' for $d" ;;
esac
done
fi
[[ ${#INTEL_DEVICES[@]} -gt 0 ]] && msg_custom "🎮" "${BL}" "Detected Intel GPU"
[[ ${#AMD_DEVICES[@]} -gt 0 ]] && msg_custom "🎮" "${RD}" "Detected AMD GPU"
if [[ ${#AMD_DEVICES[@]} -gt 0 && -e /dev/kfd ]]; then
AMD_DEVICES+=("/dev/kfd")
fi
if grep -q "\[10de:" <<<"$pci_vga_info"; then
msg_custom "🎮" "${GN}" "Detected NVIDIA GPU"
for d in /dev/nvidia*; do
[[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
done
if [[ -d /dev/nvidia-caps ]]; then
for d in /dev/nvidia-caps/*; do
[[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
done
fi
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough"
else
msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found"
msg_custom "ℹ️" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)"
fi
fi
# Debug output
msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
msg_debug "AMD devices: ${AMD_DEVICES[*]}"
msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
}
# Configure USB passthrough for privileged containers
configure_usb_passthrough() {
if [[ "$CT_TYPE" != "0" ]]; then
return 0
fi
msg_info "Configuring automatic USB passthrough (privileged container)"
cat <<EOF >>"$LXC_CONFIG"
# Automatic USB passthrough (privileged container)
lxc.cgroup2.devices.allow: a
lxc.cap.drop:
lxc.cgroup2.devices.allow: c 188:* rwm
lxc.cgroup2.devices.allow: c 189:* rwm
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
EOF
msg_ok "USB passthrough configured"
}
# Configure GPU passthrough
configure_gpu_passthrough() {
# Skip if:
# GPU passthrough is enabled when var_gpu="yes":
# - Set via environment variable: var_gpu=yes bash -c "..."
# - Set in CT script: var_gpu="${var_gpu:-no}"
# - Enabled in advanced_settings wizard
# - Configured in app defaults file
if ! is_gpu_app "$APP"; then
return 0
fi
detect_gpu_devices
# Count available GPU types
local gpu_count=0
local available_gpus=()
if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
available_gpus+=("INTEL")
gpu_count=$((gpu_count + 1))
fi
if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
available_gpus+=("AMD")
gpu_count=$((gpu_count + 1))
fi
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
available_gpus+=("NVIDIA")
gpu_count=$((gpu_count + 1))
fi
if [[ $gpu_count -eq 0 ]]; then
msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough"
return 0
fi
# Several types can be passed through at once: the device nodes are disjoint,
# and an iGPU next to a discrete card is the normal case rather than an
# either/or. This used to pick exactly one, defaulting to the first entry --
# always INTEL, since that is the order they are collected in. On a host with
# an Intel iGPU and an NVIDIA card that silently passed through the iGPU and
# left /dev/nvidia* out, so CUDA never saw a device.
local -a selected_types=()
if [[ $gpu_count -eq 1 ]]; then
selected_types=("${available_gpus[0]}")
msg_ok "Automatically configuring ${selected_types[0]} GPU passthrough"
else
echo -e "\n${INFO} Multiple GPU types detected:"
local i
for i in "${!available_gpus[@]}"; do
echo " $((i + 1))) ${available_gpus[$i]}"
done
echo " A) Pass through ALL of them"
local type_selection=""
read -r -t 60 -p "Select GPU type(s) (1-${gpu_count}, comma-separated, A=all) [timeout 60s, default=all]: " type_selection </dev/tty || type_selection=""
type_selection="${type_selection^^}"
if [[ -z "$type_selection" || "$type_selection" == "A" ]]; then
selected_types=("${available_gpus[@]}")
else
local -a nums=()
local num
IFS=',' read -ra nums <<<"$type_selection"
for num in "${nums[@]}"; do
num="${num// /}"
if [[ "$num" =~ ^[0-9]+$ ]] && ((num >= 1 && num <= gpu_count)); then
selected_types+=("${available_gpus[$((num - 1))]}")
fi
done
# Nothing usable typed. Passing everything through beats passing nothing:
# a spare device node is harmless, a missing one is the bug being fixed.
if [[ ${#selected_types[@]} -eq 0 ]]; then
msg_warn "No valid selection - passing through all detected GPUs"
selected_types=("${available_gpus[@]}")
fi
fi
fi
# One counter across every selected type: each branch used to start at dev0,
# which was harmless while only one branch could run and would overwrite
# entries now that several can.
local dev_index=0
local -a configured_types=()
local selected_gpu
for selected_gpu in "${selected_types[@]}"; do
case "$selected_gpu" in
INTEL | AMD)
local devices=()
[[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
[[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
# dev0/dev1 format so the entries show up in the GUI.
# GIDs are detected and corrected once the container has started.
for dev in "${devices[@]}"; do
echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
dev_index=$((dev_index + 1))
done
configured_types+=("$selected_gpu")
msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)"
;;
NVIDIA)
if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
msg_warn "No NVIDIA devices available for passthrough"
continue
fi
# Separate per-card device nodes (/dev/nvidia0, /dev/nvidia1, ...) from
# the shared control nodes (nvidiactl, nvidia-uvm*, nvidia-caps/*,
# nvidia-modeset), so a host with several NVIDIA cards can pass through
# only some of them instead of always all-or-nothing.
local -a nvidia_cards=() nvidia_shared=()
for dev in "${NVIDIA_DEVICES[@]}"; do
if [[ "$dev" =~ ^/dev/nvidia[0-9]+$ ]]; then
nvidia_cards+=("$dev")
else
nvidia_shared+=("$dev")
fi
done
local -a selected_cards=("${nvidia_cards[@]}")
if [[ ${#nvidia_cards[@]} -gt 1 ]]; then
echo -e "\n${INFO} Multiple NVIDIA GPUs detected:"
local i
for i in "${!nvidia_cards[@]}"; do
echo " $((i + 1))) ${nvidia_cards[$i]}"
done
echo " A) Configure ALL GPUs"
local gpu_selection=""
read -r -t 60 -p "Select GPU(s) to pass through (1-${#nvidia_cards[@]}, A=all) [timeout 60s, default=all]: " gpu_selection </dev/tty || gpu_selection=""
gpu_selection="${gpu_selection^^}"
if [[ -n "$gpu_selection" && "$gpu_selection" != "A" ]]; then
selected_cards=()
local -a nums
IFS=',' read -ra nums <<<"$gpu_selection"
local num
for num in "${nums[@]}"; do
num=$(echo "$num" | tr -d ' ')
if [[ "$num" =~ ^[0-9]+$ ]] && ((num >= 1 && num <= ${#nvidia_cards[@]})); then
selected_cards+=("${nvidia_cards[$((num - 1))]}")
fi
done
[[ ${#selected_cards[@]} -eq 0 ]] && selected_cards=("${nvidia_cards[@]}")
fi
fi
local -a devices_to_add=("${selected_cards[@]}" "${nvidia_shared[@]}")
for dev in "${devices_to_add[@]}"; do
echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
dev_index=$((dev_index + 1))
done
configured_types+=("NVIDIA")
msg_ok "NVIDIA GPU passthrough configured (${#selected_cards[@]}/${#nvidia_cards[@]} GPU(s), ${#devices_to_add[@]} devices) - install drivers in container if needed"
;;
esac
done
# Space separated, so the readers below can test for one type without
# caring how many others came with it.
export GPU_TYPE="${configured_types[*]}"
}
# Additional device passthrough
configure_additional_devices() {
# TUN device passthrough
if [ "$ENABLE_TUN" == "yes" ]; then
cat <<EOF >>"$LXC_CONFIG"
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
EOF
fi
# Coral TPU passthrough
if [[ -e /dev/apex_0 ]]; then
msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough"
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
fi
}
# Execute pre-start configurations
configure_usb_passthrough
configure_gpu_passthrough
configure_additional_devices
# GPU_TYPE can now name several types at once, so this asks whether AMD is
# among them rather than whether it is the only one.
if [[ " ${GPU_TYPE:-} " == *" AMD "* ]]; then
local rocm_extra=4
local new_disk_size=$((PCT_DISK_SIZE + rocm_extra))
if pct resize "$CTID" rootfs "${new_disk_size}G" >/dev/null 2>&1; then
msg_ok "Disk resized ${PCT_DISK_SIZE}GB → ${new_disk_size}GB for ROCm"
else
msg_warn "Failed to resize disk for ROCm — installation may fail if space is insufficient"
fi
fi
# ============================================================================
# START CONTAINER AND INSTALL USERLAND
# ============================================================================
msg_info "Starting LXC Container"
# Checked, and with its own code. Unchecked, a failure here surfaced as pct's
# generic 255 -- which the exit table reads as DPKG's fatal error, so 197 runs
# across 70 apps in a fortnight were filed as a package manager problem when
# the container simply would not start.
local _start_out
if ! _start_out="$(pct start "$CTID" 2>&1)"; then
msg_error "Container ${CTID} was created but would not start"
_pct_start_failed "$CTID" "$_start_out"
exit 227
fi
# Wait for container to be running
for i in {1..10}; do
if pct status "$CTID" | grep -q "status: running"; then
msg_ok "Started LXC Container"
break
fi
sleep 1
if [ "$i" -eq 10 ]; then
local ct_status
ct_status=$(pct status "$CTID" 2>/dev/null || echo "unknown")
msg_error "LXC Container did not reach running state (status: ${ct_status})"
_pct_start_failed "$CTID" "$_start_out"
exit 117
fi
done
# Wait for network (skip for Alpine initially)
if [ "$var_os" != "alpine" ]; then
msg_info "Waiting for network in LXC container"
# Wait for IP assignment (IPv4 or IPv6)
local ip_in_lxc=""
for i in {1..60}; do
# Try IPv4 first
ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
# Fallback to IPv6 if IPv4 not available
if [ -z "$ip_in_lxc" ]; then
ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
fi
[ -n "$ip_in_lxc" ] && break
if [ "$i" -le 20 ]; then
sleep 1
elif [ "$i" -le 40 ]; then
sleep 2
else
sleep 3
fi
done