Files
PVE-Tools-9/lib/network.sh
Maple 886e82f0cb refactor: 模块化重构 — 拆分 lib/ 基础设施层与 src/modules/ 功能模块
PVE-Tools.sh 从 ~14,700 行单文件重构为 150 行入口点,
所有函数按职责拆分到独立文件,通过 source 机制加载。

lib/ — 基础设施层(零业务逻辑)
  config.sh: 全局变量与常量
  core.sh: 日志、UI、确认、备份、GRUB、进度条
  network.sh: 网络检测、镜像选择
  runtime.sh: 运行时守卫、main() 入口

src/modules/ — 功能模块(10 个目录)
  01-optimization  02-sources  03-boot-kernel
  04-gpu-passthrough  05-vm-container  06-networking
  07-storage-disk  08-tools-about  09-security  10-third-party

新增 build.sh(合并编译)和 dev.sh(开发 source 入口),
release.yml 新增构建步骤,CI 适配模块化结构。
2026-07-08 11:15:59 +08:00

445 lines
17 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# SPDX-License-Identifier: GPL-3.0-only
# Copyright (C) 2026 Ciriu Networks
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
}
fetch_session_tip() {
if [[ -n "$SESSION_TIP" ]]; then
return 0
fi
if [[ "$IS_OFFLINE_MODE" -eq 1 ]]; then
SESSION_TIP="离线模式已启用,本次会话不获取在线 Tips。"
return 0
fi
local timeout=5
local response=""
if command -v curl >/dev/null 2>&1; then
response=$(curl -s --connect-timeout "$timeout" --max-time "$timeout" "$HITOKOTO_API_URL" 2>/dev/null)
elif command -v wget >/dev/null 2>&1; then
response=$(wget -q -T "$timeout" -O - "$HITOKOTO_API_URL" 2>/dev/null)
else
SESSION_TIP="当前环境缺少 curl 或 wget无法获取在线 Tips。"
return 0
fi
if [[ -z "$response" ]]; then
SESSION_TIP="一言获取失败,本次会话不再重试。"
return 0
fi
local hitokoto from from_who
hitokoto=$(printf '%s' "$response" | sed -n 's/.*"hitokoto":"\([^"]*\)".*/\1/p' | head -n 1)
from=$(printf '%s' "$response" | sed -n 's/.*"from":"\([^"]*\)".*/\1/p' | head -n 1)
from_who=$(printf '%s' "$response" | sed -n 's/.*"from_who":\("[^"]*"\|null\).*/\1/p' | head -n 1 | sed 's/^"//; s/"$//')
hitokoto=$(printf '%s' "$hitokoto" | sed 's/\\"/"/g; s/\\\\/\\/g')
from=$(printf '%s' "$from" | sed 's/\\"/"/g; s/\\\\/\\/g')
from_who=$(printf '%s' "$from_who" | sed 's/\\"/"/g; s/\\\\/\\/g')
if [[ -z "$hitokoto" ]]; then
SESSION_TIP="一言解析失败,本次会话不再重试。"
return 0
fi
SESSION_TIP="$hitokoto"
if [[ -n "$from" ]]; then
SESSION_TIP="${SESSION_TIP} —— ${from}"
if [[ -n "$from_who" && "$from_who" != "null" ]]; then
SESSION_TIP="${SESSION_TIP} / ${from_who}"
fi
fi
}
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="https://www.tencent.com/"
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() {
local managed_any=false
local service
local services=("nut-monitor.service" "nut-server.service")
if ! command -v systemctl >/dev/null 2>&1; then
log_warn "系统不支持 systemctl无法自动管理 UPS 服务"
return 1
fi
for service in "${services[@]}"; do
if systemctl list-unit-files 2>/dev/null | grep -q "^${service}"; then
systemctl stop "${service%.service}" >/dev/null 2>&1 || true
systemctl disable "${service%.service}" >/dev/null 2>&1 || true
managed_any=true
fi
done
if [[ "$managed_any" != true ]]; then
log_info "未检测到可管理的 NUT 服务,跳过 UPS 服务管理"
return 0
fi
log_success "已执行 UPS 服务关闭: systemctl stop/disable nut-monitor nut-server"
return 0
}
enable_ups_service() {
local managed_any=false
local service
local services=("nut-server.service" "nut-monitor.service")
if ! command -v systemctl >/dev/null 2>&1; then
return 1
fi
for service in "${services[@]}"; do
if systemctl list-unit-files 2>/dev/null | grep -q "^${service}"; then
systemctl enable "${service%.service}" >/dev/null 2>&1 || true
systemctl start "${service%.service}" >/dev/null 2>&1 || true
managed_any=true
fi
done
[[ "$managed_any" == true ]]
}
show_ups_diagnostics() {
local service active_state enabled_state has_nut_service=false
local upsc_path ups_list
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "UPS / NUT 诊断信息"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if upsc_path=$(command -v upsc 2>/dev/null); then
log_success "检测到 upsc: $upsc_path"
if command -v timeout >/dev/null 2>&1; then
ups_list=$(timeout --signal=TERM 3s upsc -l 2>/dev/null || true)
if [[ -n "$ups_list" ]]; then
echo "已发现 UPS 设备名:"
printf '%s\n' "$ups_list"
else
log_info "未列出 UPS 设备名;请确认 NUT 已由系统正确配置"
fi
else
log_warn "未检测到 timeout脚本不会在 Web UI 热路径里直接调用 upsc"
fi
else
log_warn "未检测到 upscnut-client 未安装),无法读取 UPS 数据"
fi
if command -v systemctl >/dev/null 2>&1; then
for service in nut-server.service nut-monitor.service; do
if systemctl list-unit-files 2>/dev/null | grep -q "^${service}"; then
active_state=$(systemctl is-active "${service%.service}" 2>/dev/null || echo unknown)
enabled_state=$(systemctl is-enabled "${service%.service}" 2>/dev/null || echo unknown)
echo "${service%.service} 状态: active=${active_state}, enabled=${enabled_state}"
has_nut_service=true
fi
done
if [[ "$has_nut_service" != true ]]; then
log_info "未检测到可管理的 NUT systemd 服务"
fi
else
log_info "系统不支持 systemctl跳过 NUT 服务状态检查"
fi
echo "说明:温度监控中的 UPS 展示仅做安全读取,不会自动启停 NUT 服务。"
}
# 显示横幅
show_banner() {
clear
echo -ne "${NC}"
cat << 'EOF'
██████╗ ██╗ ██╗███████╗ ████████╗ ██████╗ ██████╗ ██╗ ███████╗ ██████╗ ██████╗ ██████╗
██╔══██╗██║ ██║██╔════╝ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██╔════╝ ██╔══██╗██╔══██╗██╔═══██╗
██████╔╝██║ ██║█████╗ ██║ ██║ ██║██║ ██║██║ ███████╗ ██████╔╝██████╔╝██║ ██║
██╔═══╝ ╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██║ ██║██║ ╚════██║ ██╔═══╝ ██╔══██╗██║ ██║
██║ ╚████╔╝ ███████╗ ██║ ╚██████╔╝╚██████╔╝███████╗███████║ ██║ ██║ ██║╚██████╔╝
╚═╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝
EOF
echo -ne "${NC}"
echo "$UI_BORDER"
echo -e " ${H1}PVE-Tools-Pro | ${BUILD_NICKNAME} Build | Support PVE 9.x.x${NC}"
echo " 让每个人都能体验虚拟化技术的的便利。"
echo -e " 作者: ${PINK}Maple${NC} | 交流Q群: ${CYAN}1031976463${NC}"
echo -e " 当前版本: ${GREEN}$CURRENT_VERSION${NC} | 最新版本: ${remote_version:-"Not Found"}"
echo "$UI_BORDER"
}
# 检查是否为 root 用户
mirror_uri_by_type() {
local source_type="$1"
local idx="$2"
case "$source_type" in
debian) echo "${MIRROR_DEBIAN_URIS[$idx]}" ;;
security) echo "${MIRROR_SECURITY_URIS[$idx]}" ;;
pve) echo "${MIRROR_PVE_URIS[$idx]}" ;;
ceph) echo "${MIRROR_CEPH_URIS[$idx]}" ;;
ct) echo "${MIRROR_CT_URIS[$idx]}" ;;
*) echo "" ;;
esac
}
mirror_set_selected() {
local source_type="$1"
local idx="$2"
case "$source_type" in
debian) MIRROR_SELECTED_DEBIAN="$idx" ;;
security) MIRROR_SELECTED_SECURITY="$idx" ;;
pve) MIRROR_SELECTED_PVE="$idx" ;;
ceph) MIRROR_SELECTED_CEPH="$idx" ;;
ct) MIRROR_SELECTED_CT="$idx" ;;
*) return 1 ;;
esac
}
mirror_reset_selection() {
MIRROR_SELECTED_DEBIAN=-1
MIRROR_SELECTED_SECURITY=-1
MIRROR_SELECTED_PVE=-1
MIRROR_SELECTED_CEPH=-1
MIRROR_SELECTED_CT=-1
}
mirror_selection_complete() {
[[ "$MIRROR_SELECTED_DEBIAN" =~ ^[0-9]+$ && "$MIRROR_SELECTED_DEBIAN" -ge 0 ]] || return 1
[[ "$MIRROR_SELECTED_SECURITY" =~ ^[0-9]+$ && "$MIRROR_SELECTED_SECURITY" -ge 0 ]] || return 1
[[ "$MIRROR_SELECTED_PVE" =~ ^[0-9]+$ && "$MIRROR_SELECTED_PVE" -ge 0 ]] || return 1
[[ "$MIRROR_SELECTED_CEPH" =~ ^[0-9]+$ && "$MIRROR_SELECTED_CEPH" -ge 0 ]] || return 1
[[ "$MIRROR_SELECTED_CT" =~ ^[0-9]+$ && "$MIRROR_SELECTED_CT" -ge 0 ]] || return 1
}
mirror_selected_index_by_type() {
local source_type="$1"
case "$source_type" in
debian) echo "$MIRROR_SELECTED_DEBIAN" ;;
security) echo "$MIRROR_SELECTED_SECURITY" ;;
pve) echo "$MIRROR_SELECTED_PVE" ;;
ceph) echo "$MIRROR_SELECTED_CEPH" ;;
ct) echo "$MIRROR_SELECTED_CT" ;;
*) echo "-1" ;;
esac
}
mirror_source_label() {
local source_type="$1"
case "$source_type" in
debian) echo "Debian 软件源" ;;
security) echo "Debian Security 安全源" ;;
pve) echo "Proxmox VE no-subscription 源" ;;
ceph) echo "Ceph Squid 源" ;;
ct) echo "CT 模板源" ;;
*) echo "$source_type" ;;
esac
}
mirror_print_selection_summary() {
echo "$UI_DIVIDER"
echo -e "${CYAN}当前镜像源配置:${NC}"
local source_type idx uri
for source_type in debian security pve ceph ct; do
idx="$(mirror_selected_index_by_type "$source_type")"
if [[ "$idx" =~ ^[0-9]+$ && "$idx" -ge 0 ]]; then
uri="$(mirror_uri_by_type "$source_type" "$idx")"
[[ -n "$uri" ]] || uri="官方源兜底"
printf " %-12s %-24s %s\n" "$(mirror_source_label "$source_type"):" "${MIRROR_NAMES[$idx]}" "$uri"
else
printf " %-12s %s\n" "$(mirror_source_label "$source_type"):" "未选择"
fi
done
echo "$UI_DIVIDER"
}
mirror_print_recommendation_notice() {
echo -e " ${YELLOW}推荐优先选择靠前的镜像源。地区性高校源可能会起到反向作用,例如:同步慢、高峰期丢包、随时不可用等。${NC}"
echo -e " ${YELLOW}如使用生产环境,请优先使用官方源或商业源(阿里云/腾讯云)。${NC}"
echo "$UI_DIVIDER"
}
select_mirror_for_source() {
local source_type="$1"
local label="$2"
local selected_var="$3"
local candidates=()
local idx uri pick selected_idx
while true; do
clear
show_menu_header "$label"
mirror_print_recommendation_notice
candidates=()
for idx in "${!MIRROR_NAMES[@]}"; do
uri="$(mirror_uri_by_type "$source_type" "$idx")"
[[ -n "$uri" ]] || continue
candidates+=("$idx")
printf " ${PRIMARY}%-3s${NC}. %-28s %s\n" "${#candidates[@]}" "${MIRROR_NAMES[$idx]}" "$uri"
done
echo "$UI_DIVIDER"
show_menu_option "0" "返回"
show_menu_footer
read -p "请选择 $label [0-${#candidates[@]}]: " pick
pick="${pick:-0}"
[[ "$pick" == "0" ]] && return 1
if [[ ! "$pick" =~ ^[0-9]+$ ]] || (( pick < 1 || pick > ${#candidates[@]} )); then
log_error "无效选择,请重新输入"
pause_function
continue
fi
selected_idx="${candidates[$((pick - 1))]}"
mirror_set_selected "$source_type" "$selected_idx" || return 1
[[ -n "$selected_var" ]] && printf -v "$selected_var" '%s' "$selected_idx"
log_success "$label 已选择: ${MIRROR_NAMES[$selected_idx]}"
return 0
done
}
select_mirror_unified() {
local candidates=()
local idx pick selected_idx
while true; do
clear
show_menu_header "全部使用同一镜像"
mirror_print_recommendation_notice
echo " 仅展示同时支持 Debian / Security / PVE / Ceph / CT 的镜像。"
echo "$UI_DIVIDER"
candidates=()
for idx in "${!MIRROR_NAMES[@]}"; do
[[ -n "${MIRROR_DEBIAN_URIS[$idx]}" ]] || continue
[[ -n "${MIRROR_SECURITY_URIS[$idx]}" ]] || continue
[[ -n "${MIRROR_PVE_URIS[$idx]}" ]] || continue
[[ -n "${MIRROR_CEPH_URIS[$idx]}" ]] || continue
[[ -n "${MIRROR_CT_URIS[$idx]}" ]] || continue
candidates+=("$idx")
printf " ${PRIMARY}%-3s${NC}. %s\n" "${#candidates[@]}" "${MIRROR_NAMES[$idx]}"
done
echo "$UI_DIVIDER"
show_menu_option "0" "返回"
show_menu_footer
read -p "请选择统一镜像 [0-${#candidates[@]}]: " pick
pick="${pick:-0}"
[[ "$pick" == "0" ]] && return 1
if [[ ! "$pick" =~ ^[0-9]+$ ]] || (( pick < 1 || pick > ${#candidates[@]} )); then
log_error "无效选择,请重新输入"
pause_function
continue
fi
selected_idx="${candidates[$((pick - 1))]}"
MIRROR_SELECTED_DEBIAN="$selected_idx"
MIRROR_SELECTED_SECURITY="$selected_idx"
MIRROR_SELECTED_PVE="$selected_idx"
MIRROR_SELECTED_CEPH="$selected_idx"
MIRROR_SELECTED_CT="$selected_idx"
mirror_print_selection_summary
return 0
done
}
select_mirror_per_source() {
mirror_reset_selection
select_mirror_for_source debian "Debian 软件源" || { mirror_reset_selection; return 1; }
select_mirror_for_source security "Debian Security 安全源" || { mirror_reset_selection; return 1; }
select_mirror_for_source pve "Proxmox VE no-subscription 源" || { mirror_reset_selection; return 1; }
select_mirror_for_source ceph "Ceph Squid 源" || { mirror_reset_selection; return 1; }
select_mirror_for_source ct "CT 模板源" || { mirror_reset_selection; return 1; }
clear
show_menu_header "镜像源选择摘要"
mirror_print_selection_summary
read -p "确认使用以上配置?输入 yes 确认,其他任意键返回: " confirm
[[ "$confirm" == "yes" || "$confirm" == "YES" ]] || { mirror_reset_selection; return 1; }
}
select_mirror() {
while true; do
clear
show_menu_header "镜像源配置"
mirror_print_recommendation_notice
show_menu_option "1" "全部使用同一镜像(快速)"
show_menu_option "2" "按源类型分别选择(推荐)"
show_menu_option "3" "跳过(稍后在菜单中配置)"
echo "$UI_DIVIDER"
echo " 跳过时不会写入软件源;再次执行更换软件源会重新进入本配置。"
show_menu_footer
local choice
read -p "请选择 [1-3]: " choice
case "$choice" in
1) select_mirror_unified && return 0 ;;
2) select_mirror_per_source && return 0 ;;
3) mirror_reset_selection; log_info "已跳过镜像源配置"; return 1 ;;
*) log_error "无效选择,请重新输入"; pause_function ;;
esac
done
}
# 版本检查函数