-
-
Notifications
You must be signed in to change notification settings - Fork 198
Expand file tree
/
Copy pathPVE-Tools.sh
More file actions
7539 lines (6550 loc) · 266 KB
/
PVE-Tools.sh
File metadata and controls
7539 lines (6550 loc) · 266 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
#!/bin/bash
# SPDX-License-Identifier: GPL-3.0-only
# Copyright (C) 2026 Ciriu Networks
# Auther:Maple
# 二次修改使用请不要删除此段注释
# PVE 9.0 配置工具脚本
# 支持换源、删除订阅弹窗、硬盘管理等功能
# 适用于 Proxmox VE 9.0 (基于 Debian 13)
# 版本信息
CURRENT_VERSION="6.8.0"
VERSION_FILE_URL="https://raw.githubusercontent.com/Mapleawaa/PVE-Tools-9/main/VERSION"
UPDATE_FILE_URL="https://raw.githubusercontent.com/Mapleawaa/PVE-Tools-9/main/UPDATE"
PVE_VERSION_DETECTED=""
PVE_MAJOR_VERSION=""
RISK_ACK_BYPASS=false
# ============ 颜色系统 ============
# 终端颜色初始化
setup_colors() {
if [[ -t 1 && -z "${NO_COLOR}" ]]; then
# 使用 printf 确保变量包含真实的转义字符,提高不同 shell 间的兼容性
RED=$(printf '\033[0;31m')
GREEN=$(printf '\033[0;32m')
YELLOW=$(printf '\033[1;33m')
BLUE=$(printf '\033[0;34m')
PINK=$(printf '\033[0;35m')
CYAN=$(printf '\033[0;36m')
MAGENTA=$(printf '\033[0;35m')
WHITE=$(printf '\033[1;37m')
ORANGE=$(printf '\033[0;33m')
NC=$(printf '\033[0m')
# UI 辅助色映射
PRIMARY="${CYAN}"
H1=$(printf '\033[1;36m')
H2=$(printf '\033[1;37m')
else
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' WHITE='' ORANGE='' NC=''
PRIMARY='' H1='' H2=''
fi
# UI 界面一致性常量
UI_BORDER="${NC}═════════════════════════════════════════════════${NC}"
UI_DIVIDER="${NC}═════════════════════════════════════════════════${NC}"
UI_FOOTER="${NC}═════════════════════════════════════════════════${NC}"
UI_HEADER="${NC}═════════════════════════════════════════════════${NC}"
}
# 初始化颜色
setup_colors
# 镜像源配置
MIRROR_USTC="https://mirrors.ustc.edu.cn/proxmox/debian/pve"
MIRROR_TUNA="https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/pve"
MIRROR_DEBIAN="https://deb.debian.org/debian"
SELECTED_MIRROR=""
# ceph 模板源配置
CEPH_MIRROR_USTC="https://mirrors.ustc.edu.cn/proxmox/debian/ceph-squid"
CEPH_MIRROR_TUNA="https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/ceph-squid"
CEPH_MIRROR_OFFICIAL="http://download.proxmox.com/debian/ceph-squid"
# CT 模板源配置
CT_MIRROR_USTC="https://mirrors.ustc.edu.cn/proxmox"
CT_MIRROR_TUNA="https://mirrors.tuna.tsinghua.edu.cn/proxmox"
CT_MIRROR_OFFICIAL="http://download.proxmox.com"
# 自动更新网络检测配置
CF_TRACE_URL="https://www.cloudflare.com/cdn-cgi/trace"
GITHUB_MIRROR_PREFIX="https://ghfast.top/"
USE_MIRROR_FOR_UPDATE=0
USER_COUNTRY_CODE=""
NETWORK_MODE="auto"
IS_OFFLINE_MODE=0
# 快速虚拟机下载脚本配置
FASTPVE_INSTALLER_URL="https://raw.githubusercontent.com/kspeeder/fastpve/main/fastpve-install.sh"
FASTPVE_PROJECT_URL="https://github.com/kspeeder/fastpve"
THIRD_PARTY_MODULES_TREE_API_MAIN_URL="https://api.github.com/repos/Mapleawaa/PVE-Tools-9/git/trees/main?recursive=1"
THIRD_PARTY_MODULES_TREE_API_MASTER_URL="https://api.github.com/repos/Mapleawaa/PVE-Tools-9/git/trees/master?recursive=1"
THIRD_PARTY_MODULES_RAW_BASE_URL="https://raw.githubusercontent.com/Mapleawaa/PVE-Tools-9/main/Modules"
NVIDIA_ASSETS_BASE_URL="https://raw.githubusercontent.com/Mapleawaa/PVE-Tools-9/main/Modules/NVIDIA"
NVIDIA_VGPU_UNLOCK_SO_URL="${NVIDIA_ASSETS_BASE_URL}/libvgpu_unlock_rs.so"
# 日志函数
log_info() {
local timestamp=$(date +'%H:%M:%S')
echo -e "${GREEN}[$timestamp]${NC} ${CYAN}INFO${NC} $1"
echo "[$timestamp] INFO $1" >> /var/log/pve-tools.log
}
log_warn() {
local timestamp=$(date +'%H:%M:%S')
echo -e "${YELLOW}[$timestamp]${NC} ${ORANGE}WARN${NC} $1"
echo "[$timestamp] WARN $1" >> /var/log/pve-tools.log
}
log_error() {
local timestamp=$(date +'%H:%M:%S')
echo -e "${RED}[$timestamp]${NC} ${RED}ERROR${NC} $1" >&2
echo "[$timestamp] ERROR $1" >> /var/log/pve-tools.log
}
log_step() {
local timestamp=$(date +'%H:%M:%S')
echo -e "${BLUE}[$timestamp]${NC} ${MAGENTA}STEP${NC} $1"
echo "[$timestamp] STEP $1" >> /var/log/pve-tools.log
}
log_success() {
local timestamp=$(date +'%H:%M:%S')
echo -e "${GREEN}[$timestamp]${NC} ${GREEN}OK${NC} $1"
echo "[$timestamp] OK $1" >> /var/log/pve-tools.log
}
log_tips(){
local timestamp=$(date +'%H:%M:%S')
echo -e "${CYAN}[$timestamp]${NC} ${MAGENTA}TIPS${NC} $1"
echo "[$timestamp] TIPS $1" >> /var/log/pve-tools.log
}
# Enhanced error handling function with consistent messaging
display_error() {
local error_msg="$1"
local suggestion="${2:-请检查输入或联系作者寻求帮助。}"
log_error "$error_msg"
echo -e "${YELLOW}提示: $suggestion${NC}"
pause_function
}
# Enhanced success feedback
display_success() {
local success_msg="$1"
local next_step="${2:-}"
log_success "$success_msg"
if [[ -n "$next_step" ]]; then
echo -e "${GREEN}下一步: $next_step${NC}"
fi
}
# Confirmation prompt with consistent UI
confirm_action() {
local action_desc="$1"
local default_choice="${2:-N}"
echo -e "${YELLOW}确认操作: $action_desc${NC}"
read -p "请输入 'yes' 确认继续,其他任意键取消 [$default_choice]: " -r confirm
if [[ "$confirm" == "yes" || "$confirm" == "YES" ]]; then
return 0
else
log_info "操作已取消"
return 1
fi
}
LEGAL_VERSION="1.0"
LEGAL_EFFECTIVE_DATE="2026-__-__"
ensure_legal_acceptance() {
local dir="/var/lib/pve-tools"
local marker="${dir}/legal_acceptance_${LEGAL_VERSION}"
mkdir -p "$dir" >/dev/null 2>&1 || true
if [[ -f "$marker" ]]; then
return 0
fi
clear
show_menu_header "许可与服务条款"
echo -e "${CYAN}继续使用本脚本前,请阅读并同意以下条款:${NC}"
echo -e " - ULA(最终用户许可与使用协议): https://pve.u3u.icu/ula"
echo -e " - TOS(服务条款): https://pve.u3u.icu/tos"
echo -e "${RED} 您可以随时撤回同意,只需删除 ${marker} 文件即可。${NC}"
echo -e "${UI_DIVIDER}"
echo -n "是否同意并继续?(Y/N): "
local ans
read -n 1 -r ans
echo
if [[ "$ans" == "Y" || "$ans" == "y" ]]; then
printf '%s\n' "accepted_version=${LEGAL_VERSION}" "accepted_time=$(date +%F\ %T)" > "$marker" 2>/dev/null || true
log_success "已记录同意条款,后续将跳过许可检查。"
return 0
fi
log_info "未同意条款,退出脚本"
exit 0
}
# ============ 配置文件安全管理函数 ============
# 备份文件到 /var/backups/pve-tools/
backup_file() {
local file_path="$1"
local backup_dir="/var/backups/pve-tools"
if [[ ! -f "$file_path" ]]; then
log_warn "文件不存在,跳过备份: $file_path"
return 1
fi
# 创建备份目录
mkdir -p "$backup_dir"
# 生成带时间戳的备份文件名
local filename=$(basename "$file_path")
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_path="${backup_dir}/${filename}.${timestamp}.bak"
# 执行备份
if cp -a "$file_path" "$backup_path"; then
log_success "文件已备份: $backup_path"
return 0
else
log_error "备份失败: $file_path"
return 1
fi
}
# 写入配置块(带标记)
# 用法: apply_block <file> <marker> <content>
apply_block() {
local file_path="$1"
local marker="$2"
local content="$3"
if [[ -z "$file_path" || -z "$marker" ]]; then
log_error "apply_block: 缺少必需参数"
return 1
fi
# 先备份文件
backup_file "$file_path"
# 移除旧的配置块(如果存在)
remove_block "$file_path" "$marker"
# 写入新的配置块
{
echo "# PVE-TOOLS BEGIN $marker"
echo "$content"
echo "# PVE-TOOLS END $marker"
} >> "$file_path"
log_success "配置块已写入: $file_path [$marker]"
}
# 删除配置块(精确匹配标记)
# 用法: remove_block <file> <marker>
remove_block() {
local file_path="$1"
local marker="$2"
if [[ -z "$file_path" || -z "$marker" ]]; then
log_error "remove_block: 缺少必需参数"
return 1
fi
if [[ ! -f "$file_path" ]]; then
log_warn "文件不存在,跳过删除: $file_path"
return 0
fi
# 使用 sed 删除标记之间的所有内容(包括标记行)
sed -i "/# PVE-TOOLS BEGIN $marker/,/# PVE-TOOLS END $marker/d" "$file_path"
log_info "配置块已删除: $file_path [$marker]"
}
# ============ 配置文件安全管理函数结束 ============
# ============ GRUB 参数幂等管理函数 ============
# 添加 GRUB 参数(幂等操作,不会重复添加)
# 用法: grub_add_param "intel_iommu=on"
grub_add_param() {
local param="$1"
if [[ -z "$param" ]]; then
log_error "grub_add_param: 缺少参数"
return 1
fi
# 备份 GRUB 配置
backup_file "/etc/default/grub"
# 读取当前的 GRUB_CMDLINE_LINUX_DEFAULT 值
local current_line=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub)
if [[ -z "$current_line" ]]; then
log_error "未找到 GRUB_CMDLINE_LINUX_DEFAULT 配置"
return 1
fi
# 提取引号内的参数
local current_params=$(echo "$current_line" | sed 's/^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"$/\1/')
# 检查参数是否已存在(支持 key=value 和 key 两种格式)
local param_key=$(echo "$param" | cut -d'=' -f1)
if echo "$current_params" | grep -qw "$param_key"; then
# 参数已存在,先删除旧值
current_params=$(echo "$current_params" | sed "s/\b${param_key}[^ ]*\b//g")
fi
# 添加新参数(去除多余空格)
local new_params=$(echo "$current_params $param" | sed 's/ */ /g' | sed 's/^ //;s/ $//')
# 写回配置文件
sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"$new_params\"|" /etc/default/grub
log_success "GRUB 参数已添加: $param"
}
# 删除 GRUB 参数(精确删除,不影响其他参数)
# 用法: grub_remove_param "intel_iommu=on"
grub_remove_param() {
local param="$1"
if [[ -z "$param" ]]; then
log_error "grub_remove_param: 缺少参数"
return 1
fi
# 备份 GRUB 配置
backup_file "/etc/default/grub"
# 读取当前的 GRUB_CMDLINE_LINUX_DEFAULT 值
local current_line=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub)
if [[ -z "$current_line" ]]; then
log_error "未找到 GRUB_CMDLINE_LINUX_DEFAULT 配置"
return 1
fi
# 提取引号内的参数
local current_params=$(echo "$current_line" | sed 's/^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"$/\1/')
# 删除指定参数(支持精确匹配和前缀匹配)
local param_key=$(echo "$param" | cut -d'=' -f1)
local new_params=$(echo "$current_params" | sed "s/\b${param_key}[^ ]*\b//g" | sed 's/ */ /g' | sed 's/^ //;s/ $//')
# 写回配置文件
sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"$new_params\"|" /etc/default/grub
log_success "GRUB 参数已删除: $param"
}
# ============ GRUB 参数幂等管理函数结束 ============
# 进度指示函数
show_progress() {
local message="$1"
local spinner="|/-\\"
local i=0
# Print initial message
echo -ne "${CYAN}[ ]${NC} $message\033[0K\r"
# Update the spinner position in the box
while true; do
i=$(( (i + 1) % 4 ))
echo -ne "\b\b\b\b\b${CYAN}[${spinner:$i:1}]${NC}\033[0K\r"
sleep 0.1
done &
# Store the background job ID to be killed later
SPINNER_PID=$!
}
update_progress() {
local message="$1"
# Kill the spinner if running
if [[ -n "$SPINNER_PID" ]]; then
kill $SPINNER_PID 2>/dev/null
fi
echo -ne "${GREEN}[ OK ]${NC} $message\033[0K\r"
echo
}
# Enhanced visual feedback function
show_status() {
local status="$1"
local message="$2"
local color="$3"
case $status in
"info")
echo -e "${CYAN}[INFO]${NC} $message"
;;
"success")
echo -e "${GREEN}[ OK ]${NC} $message"
;;
"warning")
echo -e "${YELLOW}[WARN]${NC} $message"
;;
"error")
echo -e "${RED}[FAIL]${NC} $message"
;;
"step")
echo -e "${MAGENTA}[STEP]${NC} $message"
;;
*)
echo -e "${WHITE}[$status]${NC} $message"
;;
esac
}
# Progress bar function
show_progress_bar() {
local current="$1"
local total="$2"
local message="$3"
local width=40
local percentage=$(( current * 100 / total ))
local filled=$(( width * current / total ))
printf "${CYAN}[${NC}"
for ((i=0; i<filled; i++)); do
printf "█"
done
for ((i=filled; i<width; i++)); do
printf " "
done
printf "${CYAN}]${NC} ${percentage}%% $message\r"
}
# 通过 Cloudflare Trace 检测地区,决定是否启用镜像源
detect_network_region() {
local timeout=5
USER_COUNTRY_CODE=""
USE_MIRROR_FOR_UPDATE=0
if ! command -v curl &> /dev/null; then
return 1
fi
local trace_output
trace_output=$(curl -s --connect-timeout $timeout --max-time $timeout "$CF_TRACE_URL" 2>/dev/null)
if [[ -z "$trace_output" ]]; then
return 1
fi
local loc
loc=$(echo "$trace_output" | awk -F= '/^loc=/{print $2}' | tr -d '\r')
if [[ -z "$loc" ]]; then
return 1
fi
USER_COUNTRY_CODE="$loc"
if [[ "$USER_COUNTRY_CODE" == "CN" ]]; then
USE_MIRROR_FOR_UPDATE=1
fi
return 0
}
network_show_diagnostics() {
echo "${UI_DIVIDER}"
echo -e "${CYAN}当前网络诊断信息:${NC}"
echo -e "${CYAN}IPv4 地址:${NC}"
ip -4 -o addr show scope global 2>/dev/null | awk '{print " "$2": "$4}' || true
echo -e "${CYAN}默认路由:${NC}"
ip route 2>/dev/null | sed -n '1,3p' | sed 's/^/ /' || true
echo -e "${CYAN}DNS 配置:${NC}"
grep -E '^\s*nameserver\s+' /etc/resolv.conf 2>/dev/null | sed 's/^/ /' || true
echo "${UI_DIVIDER}"
}
network_can_access_internet() {
local test_url="$VERSION_FILE_URL"
if command -v curl >/dev/null 2>&1; then
curl -fsSL --connect-timeout 5 --max-time 8 "$test_url" >/dev/null 2>&1
return $?
fi
if command -v wget >/dev/null 2>&1; then
wget -q --timeout=8 -O - "$test_url" >/dev/null 2>&1
return $?
fi
return 1
}
network_offline_guard() {
IS_OFFLINE_MODE=0
if [[ "$NETWORK_MODE" == "offline" ]]; then
IS_OFFLINE_MODE=1
log_warn "已配置为离线模式:将跳过在线更新检查与在线资源拉取。"
return 0
fi
if network_can_access_internet; then
log_success "网络连通性检测通过。"
return 0
fi
IS_OFFLINE_MODE=1
log_warn "检测到当前主机无法访问互联网,在线资源可能不可用。"
network_show_diagnostics
echo -e "${YELLOW}请先确认是否为本机网络问题(网关、DNS、NAT、防火墙)再继续。${NC}"
echo -e "${YELLOW}如果你确定当前环境需要离线使用,可继续,但涉及在线下载/更新的功能会失败。${NC}"
read -p "输入 'offline' 继续离线模式,其他任意键退出排查网络: " offline_confirm
if [[ "$offline_confirm" != "offline" ]]; then
log_info "已取消执行,请先修复网络后重试。"
exit 0
fi
return 0
}
disable_ups_service() {
if ! command -v systemctl >/dev/null 2>&1; then
log_warn "系统不支持 systemctl,无法自动管理 UPS 服务"
return 1
fi
if ! systemctl list-unit-files 2>/dev/null | grep -q '^apcupsd\.service'; then
log_info "未检测到 apcupsd.service,跳过 UPS 服务管理"
return 0
fi
systemctl stop apcupsd >/dev/null 2>&1 || true
systemctl disable apcupsd >/dev/null 2>&1 || true
log_success "已执行 UPS 服务关闭: systemctl stop/disable apcupsd"
return 0
}
enable_ups_service() {
if ! command -v systemctl >/dev/null 2>&1; then
return 1
fi
if ! systemctl list-unit-files 2>/dev/null | grep -q '^apcupsd\.service'; then
return 1
fi
systemctl enable apcupsd >/dev/null 2>&1 || true
systemctl start apcupsd >/dev/null 2>&1 || true
return 0
}
# 显示横幅
show_banner() {
clear
echo -ne "${NC}"
cat << 'EOF'
██████╗ ██╗ ██╗███████╗ ████████╗ ██████╗ ██████╗ ██╗ ███████╗ █████╗
██╔══██╗██║ ██║██╔════╝ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██╔════╝ ██╔══██╗
██████╔╝██║ ██║█████╗ ██║ ██║ ██║██║ ██║██║ ███████╗ ╚██████║
██╔═══╝ ╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██║ ██║██║ ╚════██║ ╚═══██║
██║ ╚████╔╝ ███████╗ ██║ ╚██████╔╝╚██████╔╝███████╗███████║ █████╔╝
╚═╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚════╝
EOF
echo -ne "${NC}"
echo "$UI_BORDER"
echo -e " ${H1}PVE-Tools-9 一键脚本${NC}"
echo " 让每个人都能体验虚拟化技术的的便利。"
echo -e " 作者: ${PINK}Maple${NC} | 交流群: ${CYAN}1031976463${NC}"
echo -e " 当前版本: ${GREEN}$CURRENT_VERSION${NC} | 最新版本: ${remote_version:-"未检测"}"
echo "$UI_BORDER"
}
# 检查是否为 root 用户
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "哎呀!需要超级管理员权限才能运行哦"
echo "请使用以下命令重新运行:"
echo "sudo $0"
exit 1
fi
}
# 检查调试模式
check_debug_mode() {
for arg in "$@"; do
if [[ "$arg" == "--i-know-what-i-do" ]]; then
RISK_ACK_BYPASS=true
fi
done
for arg in "$@"; do
if [[ "$arg" == "--debug" ]]; then
log_warn "警告:您正在使用调试模式!"
echo "此模式将跳过 PVE 系统版本检测"
echo "仅在开发和测试环境中使用"
echo "在非 PVE (Debian 系) 系统上使用可能导致系统损坏"
echo "您确定要继续吗?输入 'yes' 确认,其他任意键退出: "
read -r confirm
if [[ "$confirm" != "yes" ]]; then
log_info "已取消操作,退出脚本"
exit 0
fi
DEBUG_MODE=true
log_success "已启用调试模式"
return
fi
done
DEBUG_MODE=false
}
# 检查是否安装依赖软件包
check_packages() {
# 程序依赖的软件包: `sudo` `curl`
local packages=("sudo" "curl")
for pkg in "${packages[@]}"; do
if ! command -v "$pkg" &> /dev/null; then
log_error "哎呀!需要安装 $pkg 软件包才能运行哦"
echo "请使用以下命令安装:apt install -y $pkg"
exit 1
fi
done
}
# 检查 PVE 版本
check_pve_version() {
# 如果在调试模式下,跳过 PVE 版本检测
if [[ "$DEBUG_MODE" == "true" ]]; then
log_warn "调试模式:跳过 PVE 版本检测"
echo "请注意:您正在非 PVE 系统上运行此脚本,某些功能可能无法正常工作"
PVE_VERSION_DETECTED="debug"
PVE_MAJOR_VERSION="debug"
return
fi
if ! command -v pveversion &> /dev/null; then
log_error "咦?这里好像不是 PVE 环境呢"
echo "请在 Proxmox VE 系统上运行此脚本"
exit 1
fi
local pve_version pkg_ver out
out="$(pveversion 2>/dev/null || true)"
if [[ "$out" =~ pve-manager/([0-9]+(\.[0-9]+)*) ]]; then
pve_version="${BASH_REMATCH[1]}"
else
pve_version=""
fi
if [[ -z "$pve_version" ]] && command -v dpkg-query >/dev/null 2>&1; then
pkg_ver="$(dpkg-query -W -f='${Version}' pve-manager 2>/dev/null || true)"
pve_version="$(echo "$pkg_ver" | grep -oE '^[0-9]+(\.[0-9]+)*' | head -n 1)"
fi
if [[ -z "$pve_version" ]]; then
pve_version="unknown"
fi
PVE_VERSION_DETECTED="$pve_version"
if [[ "$pve_version" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then
PVE_MAJOR_VERSION="$(echo "$pve_version" | cut -d'.' -f1)"
else
PVE_MAJOR_VERSION="unknown"
fi
log_info "太好了!检测到 PVE 版本: $pve_version"
if [[ "$PVE_MAJOR_VERSION" != "9" && "$RISK_ACK_BYPASS" != "true" ]]; then
clear
show_menu_header "高风险提示:非 PVE9 环境"
echo -e "${RED}警告:检测到当前不是 PVE 9.x(当前:${PVE_VERSION_DETECTED})。${NC}"
echo -e "${RED}本脚本面向 PVE 9.x(Debian 13 / trixie)编写。${NC}"
echo -e "${RED}在 PVE 7/8 等系统上执行“换源/升级/一键优化”等自动化修改,可能是毁灭性的:${NC}"
echo -e "${RED}可能导致软件源错配、系统升级路径错误、依赖冲突、宿主机不可用。${NC}"
echo -e "${UI_DIVIDER}"
echo -e "${YELLOW}严禁在非 PVE9 上使用的选项(脚本将强制拦截):${NC}"
echo -e " - 一键优化(换源+删弹窗+更新)"
echo -e " - 软件源与更新(更换软件源/更新系统软件包/PVE 8 升级到 9)"
echo -e "${UI_DIVIDER}"
echo -e "${CYAN}如你仍要继续使用脚本的其它功能,请手动输入以下任意一项以确认风险:${NC}"
echo -e " - 确认"
echo -e " - Confirm with Risks"
echo -e "${UI_DIVIDER}"
local ack ack_lc
read -r -p "请输入确认文本以继续(回车退出): " ack
if [[ -z "$ack" ]]; then
log_info "未确认风险,退出脚本"
exit 0
fi
ack_lc="$(echo "$ack" | tr 'A-Z' 'a-z' | sed -E 's/[[:space:]]+/ /g' | sed -E 's/^ +| +$//g')"
if [[ "$ack" != "确认" && "$ack_lc" != "confirm with risks" ]]; then
log_error "确认文本不匹配,已退出"
exit 1
fi
log_warn "已确认风险:当前为非 PVE9 环境,将拦截毁灭性自动化修改功能"
fi
}
block_non_pve9_destructive() {
local feature="$1"
if [[ "$DEBUG_MODE" == "true" ]]; then
return 0
fi
if [[ "$RISK_ACK_BYPASS" == "true" ]]; then
return 0
fi
if [[ "${PVE_MAJOR_VERSION:-}" != "9" ]]; then
display_error "已拦截:非 PVE9 环境禁止执行该自动化操作" "功能:${feature}。请在 PVE9 上使用,或手动参考文档/自行处理。如需强制执行,请加启动参数 --i-know-what-i-do"
return 1
fi
return 0
}
pve_mail_send_test() {
local from_addr="$1"
local to_addr="$2"
local subject="$3"
local body="$4"
if ! command -v sendmail >/dev/null 2>&1; then
display_error "未找到 sendmail" "请确认 postfix 已安装并提供 sendmail。"
return 1
fi
{
echo "From: ${from_addr}"
echo "To: ${to_addr}"
echo "Subject: ${subject}"
echo
echo "${body}"
} | sendmail -f "${from_addr}" -t >/dev/null 2>&1
}
pve_mail_configure_postfix_smtp() {
local relay_host="$1"
local relay_port="$2"
local tls_mode="$3"
local sasl_user="$4"
local sasl_pass="$5"
if ! command -v postconf >/dev/null 2>&1; then
display_error "未找到 postconf" "请先安装 postfix 并确保其命令可用。"
return 1
fi
local relay
relay="[${relay_host}]:${relay_port}"
backup_file "/etc/postfix/main.cf" >/dev/null 2>&1 || true
postconf -e "relayhost = ${relay}"
postconf -e "smtp_use_tls = yes"
postconf -e "smtp_tls_security_level = encrypt"
postconf -e "smtp_sasl_auth_enable = yes"
postconf -e "smtp_sasl_security_options ="
postconf -e "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd"
postconf -e "smtp_tls_CApath = /etc/ssl/certs"
postconf -e "smtp_tls_session_cache_database = btree:/var/lib/postfix/smtp_tls_session_cache"
postconf -e "smtp_tls_session_cache_timeout = 3600s"
if [[ "$tls_mode" == "wrapper" ]]; then
postconf -e "smtp_tls_wrappermode = yes"
else
postconf -e "smtp_tls_wrappermode = no"
fi
local sasl_file="/etc/postfix/sasl_passwd"
backup_file "$sasl_file" >/dev/null 2>&1 || true
umask 077
printf '%s %s:%s\n' "${relay}" "${sasl_user}" "${sasl_pass}" > "$sasl_file"
chmod 600 "$sasl_file" >/dev/null 2>&1 || true
if ! command -v postmap >/dev/null 2>&1; then
display_error "未找到 postmap" "请确认 postfix 已安装完整。"
return 1
fi
postmap "hash:${sasl_file}" >/dev/null 2>&1 || {
display_error "postmap 执行失败" "请检查 /etc/postfix/sasl_passwd 格式与权限。"
return 1
}
postfix reload >/dev/null 2>&1 || {
systemctl reload postfix >/dev/null 2>&1 || systemctl restart postfix >/dev/null 2>&1 || true
}
return 0
}
pve_mail_configure_datacenter_emails() {
local from_addr="$1"
local root_addr="$2"
if ! command -v pvesh >/dev/null 2>&1; then
display_error "未找到 pvesh" "请确认当前环境为 PVE 宿主机。"
return 1
fi
pvesh set /cluster/options --email-from "$from_addr" >/dev/null 2>&1 || {
display_error "设置“来自…邮件”失败" "请在 WebUI:数据中心 -> 选项 -> 电子邮件(From)中手动设置。"
return 1
}
pvesh set /access/users/root@pam --email "$root_addr" >/dev/null 2>&1 || {
display_error "设置 root 邮箱失败" "请在 WebUI:数据中心 -> 权限 -> 用户 -> root@pam 中手动设置邮箱。"
return 1
}
return 0
}
pve_mail_configure_zed_mail() {
local from_addr="$1"
local to_addr="$2"
local zed_rc="/etc/zfs/zed.d/zed.rc"
if [[ ! -f "$zed_rc" ]]; then
log_warn "未找到 zed.rc(跳过 ZFS ZED 邮件配置)"
return 0
fi
backup_file "$zed_rc" >/dev/null 2>&1 || true
if grep -qE '^ZED_EMAIL_ADDR=' "$zed_rc"; then
sed -i "s|^ZED_EMAIL_ADDR=.*|ZED_EMAIL_ADDR=\"${to_addr}\"|g" "$zed_rc"
else
printf '\nZED_EMAIL_ADDR="%s"\n' "$to_addr" >> "$zed_rc"
fi
if grep -qE '^ZED_EMAIL_OPTS=' "$zed_rc"; then
sed -i "s|^ZED_EMAIL_OPTS=.*|ZED_EMAIL_OPTS=\"-r ${from_addr}\"|g" "$zed_rc"
else
printf 'ZED_EMAIL_OPTS="-r %s"\n' "$from_addr" >> "$zed_rc"
fi
systemctl restart zfs-zed >/dev/null 2>&1 || true
return 0
}
pve_mail_notification_setup() {
block_non_pve9_destructive "配置邮件通知(SMTP)" || return 1
log_step "配置 PVE 邮件通知(商业邮箱 SMTP)"
if ! command -v postfix >/dev/null 2>&1 && ! command -v postconf >/dev/null 2>&1; then
display_error "未检测到 postfix" "请先安装 postfix 后再配置(安装过程可能需要交互)。"
return 1
fi
local from_addr root_addr
read -p "请输入“来自…邮件”(发件人邮箱): " from_addr
if [[ -z "$from_addr" ]]; then
display_error "发件人邮箱不能为空"
return 1
fi
read -p "请输入 root 通知邮箱(收件人邮箱): " root_addr
if [[ -z "$root_addr" ]]; then
display_error "收件人邮箱不能为空"
return 1
fi
local preset
echo -e "${CYAN}请选择 SMTP 预设:${NC}"
echo " 1) QQ 邮箱(smtp.qq.com:465 SSL)"
echo " 2) 163 邮箱(smtp.163.com:465 SSL)"
echo " 3) Gmail(smtp.gmail.com:587 STARTTLS)"
echo " 4) 自定义(SMTP 兼容)"
read -p "请选择 [1-4] (默认: 1): " preset
preset="${preset:-1}"
local smtp_host smtp_port tls_mode
case "$preset" in
1) smtp_host="smtp.qq.com"; smtp_port="465"; tls_mode="wrapper" ;;
2) smtp_host="smtp.163.com"; smtp_port="465"; tls_mode="wrapper" ;;
3) smtp_host="smtp.gmail.com"; smtp_port="587"; tls_mode="starttls" ;;
4)
read -p "请输入 SMTP 服务器地址(如 smtp.xxx.com): " smtp_host
read -p "请输入 SMTP 端口(如 465/587): " smtp_port
read -p "TLS 模式(wrapper/starttls)[wrapper]: " tls_mode
tls_mode="${tls_mode:-wrapper}"
;;
*) smtp_host="smtp.qq.com"; smtp_port="465"; tls_mode="wrapper" ;;
esac
if [[ -z "$smtp_host" || -z "$smtp_port" ]]; then
display_error "SMTP 参数不完整"
return 1
fi
if [[ "$tls_mode" != "wrapper" && "$tls_mode" != "starttls" ]]; then
display_error "TLS 模式无效" "仅支持 wrapper 或 starttls"
return 1
fi
local smtp_user smtp_pass
read -p "请输入 SMTP 登录账号(通常为邮箱地址)[${from_addr}]: " smtp_user
smtp_user="${smtp_user:-$from_addr}"
if [[ -z "$smtp_user" ]]; then
display_error "SMTP 账号不能为空"
return 1
fi
echo -n "请输入 SMTP 密码/授权码(输入不回显): "
read -r -s smtp_pass
echo
if [[ -z "$smtp_pass" ]]; then
display_error "SMTP 密码/授权码不能为空"
return 1
fi
clear
show_menu_header "邮件通知配置确认"
echo -e "${YELLOW}发件人(From):${NC} $from_addr"
echo -e "${YELLOW}收件人(root 邮箱):${NC} $root_addr"
echo -e "${YELLOW}SMTP 服务器:${NC} ${smtp_host}:${smtp_port}"
echo -e "${YELLOW}TLS 模式:${NC} ${tls_mode}"
echo -e "${YELLOW}SMTP 账号:${NC} ${smtp_user}"
echo -e "${UI_DIVIDER}"
echo -e "${RED}提醒:此功能会修改 postfix 配置并写入 SMTP 凭据文件。${NC}"
echo -e "${RED}请确保你使用的是邮箱提供商的 SMTP 授权码/应用专用密码,而非登录密码。${NC}"
echo -e "${UI_DIVIDER}"
if ! confirm_action "开始应用配置并重载 postfix?"; then
return 0
fi
log_step "配置 PVE 数据中心邮件选项"
pve_mail_configure_datacenter_emails "$from_addr" "$root_addr" || return 1
log_step "安装 SASL 模块(libsasl2-modules)"
apt-get update >/dev/null 2>&1 || true
if ! apt-get install -y libsasl2-modules >/dev/null 2>&1; then
display_error "安装 libsasl2-modules 失败" "请检查网络与软件源。"
return 1
fi
log_step "配置 postfix 通过 SMTP 中继发信"
pve_mail_configure_postfix_smtp "$smtp_host" "$smtp_port" "$tls_mode" "$smtp_user" "$smtp_pass" || return 1
local test_choice="yes"
read -p "是否发送测试邮件?(yes/no) [yes]: " test_choice
test_choice="${test_choice:-yes}"
if [[ "$test_choice" == "yes" || "$test_choice" == "YES" ]]; then
log_step "发送测试邮件"
if pve_mail_send_test "$from_addr" "$root_addr" "PVE-Tools 邮件测试" "这是一封测试邮件:如果你收到,说明 SMTP 中继已可用。"; then
log_success "测试邮件已提交发送队列(请检查收件箱与垃圾箱)"
else
log_warn "测试邮件发送失败,请检查 postfix 日志与 SMTP 配置"
log_tips "可查看:journalctl -u postfix -n 200 或 tail -n 200 /var/log/mail.log"
fi
fi
local zed_choice="no"
read -p "是否额外配置 ZFS ZED 邮件(ZFS 阵列事件通知)?(yes/no) [no]: " zed_choice
zed_choice="${zed_choice:-no}"
if [[ "$zed_choice" == "yes" || "$zed_choice" == "YES" ]]; then
log_step "配置 ZFS ZED 邮件参数"
pve_mail_configure_zed_mail "$from_addr" "$root_addr" || true
log_success "ZED 配置已处理(建议手动制造一次 ZFS 事件验证)"
fi
display_success "邮件通知配置完成" "建议在 WebUI 里触发一次通知或检查系统事件确认生效。"
return 0
}
# 获取已安装的 PVE 内核包(兼容 pve-kernel / proxmox-kernel 以及 -signed 后缀)
get_installed_kernel_packages() {
local status_regex="${1:-ii|hi}"
dpkg -l 2>/dev/null | awk -v sr="$status_regex" '
$1 ~ ("^(" sr ")$") &&
$2 ~ /^(pve-kernel|proxmox-kernel)-[0-9].*-pve(-signed)?$/ {
print $2
}
' | sort -Vu
}
# 检测当前内核版本
check_kernel_version() {
log_info "检测当前内核信息..."
local current_kernel=$(uname -r)
local kernel_arch=$(uname -m)
local kernel_variant=""
# 检测内核变体(普通/企业版/测试版)
if [[ $current_kernel == *"pve"* ]]; then
kernel_variant="PVE标准内核"
elif [[ $current_kernel == *"edge"* ]]; then
kernel_variant="PVE边缘内核"
elif [[ $current_kernel == *"test"* ]]; then
kernel_variant="测试内核"
else
kernel_variant="未知类型"
fi
echo -e "${CYAN}当前内核信息:${NC}"
echo -e " 版本: ${GREEN}$current_kernel${NC}"
echo -e " 架构: ${GREEN}$kernel_arch${NC}"
echo -e " 类型: ${GREEN}$kernel_variant${NC}"
# 检测可用的内核版本
local installed_kernels=$(get_installed_kernel_packages)
if [[ -n "$installed_kernels" ]]; then
echo -e "${CYAN}已安装的内核版本:${NC}"
while IFS= read -r kernel; do
echo -e " ${GREEN}•${NC} $kernel"
done <<< "$installed_kernels"
fi
return 0
}
# 获取可用内核列表