Skip to content

Instantly share code, notes, and snippets.

@xjoker
Created September 6, 2025 13:11
Show Gist options
  • Select an option

  • Save xjoker/5b90895036596c532ceef2c89d4f483a to your computer and use it in GitHub Desktop.

Select an option

Save xjoker/5b90895036596c532ceef2c89d4f483a to your computer and use it in GitHub Desktop.
用于VPS.Town主机商的A3区域Debian13的Proxmox安装脚本。系统要求安装主机商的Debian13
#!/bin/bash
#
# Proxmox VE on Debian 13 - 优化版单脚本安装
# 架构重构:大幅精简代码,提升可维护性
#
set -eo pipefail
# =============================================================================
# 核心配置和工具函数
# =============================================================================
# 调试模式开关(通过环境变量或参数控制)
DEBUG_MODE=${DEBUG:-false}
if [ "$1" = "--debug" ] || [ "$1" = "-d" ]; then
DEBUG_MODE=true
shift # 移除调试参数
fi
# 输出重定向函数
run_cmd() {
if [ "$DEBUG_MODE" = "true" ]; then
# 调试模式:显示命令和所有输出
echo -e "\033[0;36m[DEBUG] 执行: $@\033[0m" >&2
"$@"
else
# 静默模式:隐藏输出
"$@" >/dev/null 2>&1
fi
}
# 颜色输出
log() { echo -e "\033[0;32m[INFO]\033[0m $1"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $1"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $1"; exit 1; }
# 默认选择 Yes 的确认函数
confirm_yes() {
echo -n -e "\033[1;33m[INPUT]\033[0m $1 (Y/n) "
read -n 1 -r
echo
# 默认为 yes(如果用户直接按回车或输入Y/y)
[[ -z "$REPLY" || "$REPLY" =~ ^[Yy]$ ]]
}
# 默认选择 No 的确认函数
confirm_no() {
echo -n -e "\033[1;33m[INPUT]\033[0m $1 (y/N) "
read -n 1 -r
echo
# 默认为 no(只有明确输入y/Y才返回true)
[[ "$REPLY" =~ ^[Yy]$ ]]
}
# 保留原来的confirm作为默认Yes的别名
confirm() { confirm_yes "$@"; }
debug() { [ "$DEBUG_MODE" = "true" ] && echo -e "\033[0;36m[DEBUG]\033[0m $1"; }
# 工具可用性检查函数
check_tool() {
local tool=$1
local package=${2:-$1}
if ! command -v "$tool" >/dev/null 2>&1; then
warn "工具 $tool 不可用,尝试安装 $package..."
run_cmd apt install -y "$package" || {
error "无法安装 $package,脚本无法继续执行"
}
fi
}
# 系统检查
check_system() {
[ "$EUID" -eq 0 ] || error "请使用 root 用户运行此脚本"
grep -q "13" /etc/debian_version || { warn "非 Debian 13 系统"; confirm_no "是否继续?" || exit 1; }
}
# 配置收集函数
collect_config() {
log "收集安装配置..."
# 主机名配置
CURRENT_HOSTNAME=$(hostname -s)
read -p "新主机名 (留空保持 $CURRENT_HOSTNAME): " NEW_HOSTNAME
NEW_HOSTNAME=${NEW_HOSTNAME:-$CURRENT_HOSTNAME}
# 修复:允许大写字母和更灵活的主机名格式
if ! [[ "$NEW_HOSTNAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$ ]]; then
error "主机名格式无效(只能包含字母、数字和连字符,不能以连字符开头或结尾)"
fi
# GRUB磁盘检测 - 智能检测当前GRUB安装位置
GRUB_DISKS=""
# 方法1: 通过grub-probe检测
# 确保 grub-probe 可用
if command -v grub-probe >/dev/null 2>&1 || check_tool grub-probe grub2-common 2>/dev/null; then
BOOT_DEVICE=$(grub-probe --target=device /boot 2>/dev/null || true)
if [ ! -z "$BOOT_DEVICE" ]; then
# 提取磁盘设备(去除分区号)
if [[ "$BOOT_DEVICE" =~ ^/dev/[sv]d[a-z][0-9]*$ ]]; then
GRUB_DISKS=$(echo "$BOOT_DEVICE" | sed 's/[0-9]*$//')
elif [[ "$BOOT_DEVICE" =~ ^/dev/nvme[0-9]+n[0-9]+p[0-9]+$ ]]; then
GRUB_DISKS=$(echo "$BOOT_DEVICE" | sed 's/p[0-9]*$//')
fi
fi
fi
# 方法2: 如果方法1失败,检查MBR签名
if [ -z "$GRUB_DISKS" ]; then
check_tool strings binutils
for disk in /dev/sda /dev/vda /dev/nvme0n1; do
if [ -b "$disk" ] && dd if="$disk" bs=512 count=1 2>/dev/null | strings | grep -q "GRUB"; then
GRUB_DISKS="$disk"
break
fi
done
fi
# 方法3: 如果仍未找到,使用第一个可用磁盘
if [ -z "$GRUB_DISKS" ]; then
check_tool lsblk util-linux
GRUB_DISKS=$(lsblk -d -n -o NAME,TYPE | grep disk | head -1 | awk '{print "/dev/"$1}')
fi
# 确认GRUB安装位置
if [ -b "$GRUB_DISKS" ]; then
log "检测到GRUB当前安装位置: $GRUB_DISKS"
if ! confirm_yes "确认将GRUB安装到 $GRUB_DISKS?"; then
read -p "输入磁盘路径: " GRUB_DISKS || true
fi
else
warn "无法自动检测GRUB位置"
read -p "请输入GRUB安装磁盘路径 (如 /dev/sda): " GRUB_DISKS || true
fi
# 验证GRUB磁盘路径
if [ -z "${GRUB_DISKS:-}" ]; then
error "GRUB磁盘路径不能为空"
elif [ ! -b "$GRUB_DISKS" ]; then
error "无效的GRUB磁盘路径: $GRUB_DISKS"
fi
# 8006端口安全配置
warn "8006端口默认仅允许已建立连接访问"
OPEN_8006_PUBLIC="no"
if confirm_no "是否开放8006到公网?(不推荐)"; then
OPEN_8006_PUBLIC="yes"
warn "请确保使用强密码!"
fi
log "配置收集完成"
}
# 保存/加载配置
save_config() {
cat > /root/.pve_install_config << EOF
NEW_HOSTNAME="$NEW_HOSTNAME"
GRUB_DISKS="$GRUB_DISKS"
OPEN_8006_PUBLIC="$OPEN_8006_PUBLIC"
STAGE_ONE_COMPLETED="yes"
EOF
}
load_config() {
[ -f /root/.pve_install_config ] && source /root/.pve_install_config
}
# APT操作函数
setup_pve_repo() {
log "配置 Proxmox VE 仓库..."
# 添加仓库
cat > /etc/apt/sources.list.d/pve-install-repo.sources << EOF
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
Architectures: amd64
EOF
# 下载密钥
check_tool wget wget
if [ "$DEBUG_MODE" = "true" ]; then
wget -O /usr/share/keyrings/proxmox-archive-keyring.gpg \
https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg || error "无法下载仓库密钥"
else
wget -qO /usr/share/keyrings/proxmox-archive-keyring.gpg \
https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg || error "无法下载仓库密钥"
fi
debug "更新软件包列表..."
run_cmd apt update
debug "升级软件包..."
run_cmd apt full-upgrade -y
}
# 配置主机名和hosts
setup_hostname() {
[ "$NEW_HOSTNAME" != "$CURRENT_HOSTNAME" ] || return 0
log "配置主机名: $NEW_HOSTNAME"
hostnamectl set-hostname "$NEW_HOSTNAME"
# 禁用cloud-init hosts管理 & 配置hosts文件(统一处理)
[ -f /etc/cloud/cloud.cfg ] && {
mkdir -p /etc/cloud/cloud.cfg.d
echo "manage_etc_hosts: false" > /etc/cloud/cloud.cfg.d/99-disable-hosts-management.cfg
}
PRIMARY_IP=$(ip route get 1 | awk '{print $7; exit}')
cp /etc/hosts /etc/hosts.bak 2>/dev/null || true
cat > /etc/hosts << EOF
127.0.0.1 localhost
$PRIMARY_IP $NEW_HOSTNAME
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
log "主机名配置完成: $PRIMARY_IP -> $NEW_HOSTNAME"
}
# 安装PVE内核
install_pve_kernel() {
log "安装 Proxmox VE 内核..."
# 预配置GRUB
check_tool debconf-set-selections debconf-utils
echo "grub-pc grub-pc/install_devices multiselect $GRUB_DISKS" | debconf-set-selections
echo "grub-pc grub-pc/install_devices_empty boolean false" | debconf-set-selections
# 安装内核
debug "安装PVE内核包..."
if [ "$DEBUG_MODE" = "true" ]; then
DEBIAN_FRONTEND=noninteractive apt install -y proxmox-default-kernel
else
DEBIAN_FRONTEND=noninteractive apt install -y proxmox-default-kernel >/dev/null 2>&1
fi || {
warn "内核安装失败,尝试修复..."
debug "运行dpkg --configure -a修复..."
run_cmd dpkg --configure -a
if [ "$DEBUG_MODE" = "true" ]; then
DEBIAN_FRONTEND=noninteractive apt install -y proxmox-default-kernel
else
DEBIAN_FRONTEND=noninteractive apt install -y proxmox-default-kernel >/dev/null 2>&1
fi || error "PVE内核安装失败"
}
}
# 配置网桥
setup_network_bridge() {
# 检查vmbr0是否已存在
if ip addr show vmbr0 >/dev/null 2>&1; then
log "网桥 vmbr0 已存在,跳过网桥配置"
debug "当前vmbr0配置: $(ip addr show vmbr0 | grep 'inet ' | awk '{print $2}' | head -n1)"
# 如果已存在vmbr0,可能之前创建过,仍需检查DNS
BRIDGE_CREATED=true
return 0
fi
IFACE=$(ip route | grep default | awk '{print $5}' | head -n1)
IP_CIDR=$(ip -4 addr show $IFACE | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+/\d+' | head -n1)
GATEWAY=$(ip route | grep default | awk '{print $3}' | head -n1)
[ -z "$IFACE" ] || [ -z "$IP_CIDR" ] || [ -z "$GATEWAY" ] && {
warn "无法自动检测网络配置,跳过网桥设置"
return 1
}
log "配置网络桥接: $IFACE -> vmbr0"
confirm_yes "确认创建网桥?" || return 0
cp /etc/network/interfaces /etc/network/interfaces.bak
cat > /etc/network/interfaces << EOF
auto lo
iface lo inet loopback
auto $IFACE
iface $IFACE inet manual
auto vmbr0
iface vmbr0 inet static
address $IP_CIDR
gateway $GATEWAY
bridge-ports $IFACE
bridge-stp off
bridge-fd 0
bridge-vlan-aware yes
bridge-vids 2-4094
EOF
debug "重启网络服务..."
run_cmd systemctl restart networking
sleep 2
if ip addr show vmbr0 | grep -q "$IP_CIDR"; then
log "网桥创建成功"
# 设置标记:创建了网桥,稍后必须修复DNS
BRIDGE_CREATED=true
else
warn "网桥创建可能失败"
BRIDGE_CREATED=false
fi
}
# 防火墙配置模板
create_firewall_rules() {
log "配置防火墙规则..."
mkdir -p /etc/pve/firewall
# 基础防火墙模板(默认不包含8006规则)
create_fw_file() {
cat << EOF
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT
log_level_in: info
[RULES]
IN ACCEPT -m conntrack --ctstate RELATED,ESTABLISHED
IN ACCEPT -i lo
IN SSH(ACCEPT)
IN ACCEPT -p icmp
EOF
}
# 仅当用户明确选择时才添加8006规则
if [ "$OPEN_8006_PUBLIC" = "yes" ]; then
create_fw_file_with_8006() {
cat << EOF
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT
log_level_in: info
[RULES]
IN ACCEPT -m conntrack --ctstate RELATED,ESTABLISHED
IN ACCEPT -i lo
IN SSH(ACCEPT)
IN ACCEPT -p tcp --dport 8006
IN ACCEPT -p icmp
EOF
}
create_fw_file_with_8006 > /etc/pve/firewall/cluster.fw
create_fw_file_with_8006 > /etc/pve/host.fw
log "8006端口: 公网开放模式"
else
create_fw_file > /etc/pve/firewall/cluster.fw
create_fw_file > /etc/pve/host.fw
log "8006端口: 安全模式(无公网访问规则)"
fi
}
# PVE集群服务管理
manage_pve_services() {
log "管理 Proxmox VE 服务..."
# 关键修复:清理FUSE挂载点冲突
if [ -d /etc/pve ] && [ "$(ls -A /etc/pve 2>/dev/null)" ]; then
warn "修复FUSE挂载点冲突..."
debug "停止PVE服务..."
run_cmd systemctl stop pve-cluster pvedaemon pvestatd pveproxy || true
debug "卸载/etc/pve..."
run_cmd umount /etc/pve || true
mv /etc/pve /etc/pve.backup.$(date +%s)
mkdir -p /etc/pve
fi
# 启动pve-cluster
systemctl start pve-cluster || {
warn "pve-cluster启动失败,执行修复..."
rm -f /var/lib/pve-cluster/.pmxcfs.lockfile
rm -rf /var/lib/pve-cluster/config.db*
chown -R root:www-data /var/lib/pve-cluster/
sleep 2
systemctl start pve-cluster || error "pve-cluster无法启动"
}
sleep 3
# 生成SSL证书
systemctl is-active --quiet pve-cluster && {
debug "生成SSL证书..."
run_cmd pvecm updatecerts --force
create_firewall_rules
log "SSL证书和防火墙配置完成"
}
# 启动其他服务
for service in pvedaemon pvestatd pveproxy pve-firewall; do
debug "启用并启动 $service..."
run_cmd systemctl enable $service
run_cmd systemctl restart $service && log "$service 启动成功" || warn "$service 启动失败"
done
}
# DNS修复函数(独立函数,在所有任务最后执行)
fix_dns() {
log "检查DNS解析状态..."
check_tool nslookup dnsutils
# 如果创建了网桥,强制进行DNS修复(网桥创建会导致DNS丢失)
if [ "${BRIDGE_CREATED:-false}" = "true" ]; then
warn "检测到网桥已创建,强制执行DNS修复(网桥创建通常导致DNS失效)"
else
# 只有未创建网桥时才进行DNS检测
debug "未创建网桥,检测DNS状态..."
# 多重测试DNS解析
dns_working=false
# 测试1: 使用nslookup
if nslookup google.com >/dev/null 2>&1; then
debug "nslookup测试通过"
# 测试2: 使用ping确认实际连通性
if ping -c 1 -W 3 google.com >/dev/null 2>&1; then
debug "ping测试通过"
dns_working=true
else
debug "ping测试失败,虽然nslookup成功"
fi
else
debug "nslookup测试失败"
fi
if [ "$dns_working" = "true" ]; then
log "DNS解析正常,无需修复"
return 0
fi
fi
warn "检测到DNS解析失败,开始修复..."
debug "备份原始DNS配置..."
cp /etc/systemd/resolved.conf /etc/systemd/resolved.conf.bak 2>/dev/null || true
debug "配置systemd-resolved DNS服务器..."
cat > /etc/systemd/resolved.conf << 'EOF'
[Resolve]
DNS=1.1.1.1 104.234.20.6
FallbackDNS=8.8.8.8 8.8.4.4
Domains=~.
DNSSEC=no
DNSOverTLS=no
EOF
debug "重启systemd-resolved服务..."
run_cmd systemctl restart systemd-resolved
# 等待服务重启完成
sleep 3
# 验证DNS修复效果(使用相同的多重测试)
dns_fixed=false
# 重新测试DNS解析
if nslookup google.com >/dev/null 2>&1; then
debug "修复后nslookup测试通过"
# 验证ping连通性
if ping -c 1 -W 3 google.com >/dev/null 2>&1; then
debug "修复后ping测试通过"
dns_fixed=true
else
debug "修复后ping仍然失败"
fi
else
debug "修复后nslookup仍然失败"
fi
if [ "$dns_fixed" = "true" ]; then
log "DNS配置修复成功"
return 0
else
warn "DNS修复未完全生效,可能需要重启系统"
log "您可以尝试手动运行: systemctl restart systemd-resolved"
return 1
fi
}
# 系统优化
optimize_system() {
log "系统优化..."
# 清理旧内核
kernels_to_remove=$(dpkg -l | grep -E "linux-image-[0-9]" | grep -v "$(uname -r)" | grep -v "pve" | awk '{print $2}')
[ -n "$kernels_to_remove" ] && {
log "清理旧内核: $kernels_to_remove"
debug "移除内核包..."
run_cmd apt remove -y linux-image-amd64 $kernels_to_remove
}
# 更新GRUB并移除os-prober
if [ -d /boot/grub ]; then
debug "更新GRUB配置..."
check_tool update-grub grub2-common
run_cmd update-grub
fi
if dpkg -l | grep -q os-prober; then
debug "移除os-prober..."
run_cmd apt remove -y os-prober
fi
# 恢复IPv6并刷新软件源
debug "恢复IPv6..."
run_cmd sysctl -w net.ipv6.conf.all.disable_ipv6=0
run_cmd sysctl -w net.ipv6.conf.default.disable_ipv6=0
debug "刷新软件源..."
run_cmd apt update || true
}
# 临时文件清理函数
cleanup_temp_files() {
debug "清理临时文件和配置..."
# 清理脚本创建的临时文件
local temp_files=(
"/root/.pve_install_config" # 脚本配置文件
"/etc/systemd/resolved.conf.bak" # DNS配置备份
"/etc/hosts.bak" # hosts文件备份
"/etc/network/interfaces.bak" # 网络配置备份
"/etc/pve.backup.*" # PVE挂载点备份
"/tmp/pve-install-*" # 可能的临时目录
)
for file_pattern in "${temp_files[@]}"; do
if [[ "$file_pattern" == *"*"* ]]; then
# 处理通配符模式
for file in $file_pattern; do
[ -e "$file" ] && {
debug "删除临时文件: $file"
rm -rf "$file" 2>/dev/null || true
}
done
else
# 处理普通文件
[ -e "$file_pattern" ] && {
debug "删除临时文件: $file_pattern"
rm -f "$file_pattern" 2>/dev/null || true
}
fi
done
# 清理APT缓存和临时包
debug "清理APT缓存..."
run_cmd apt autoremove -y || true
run_cmd apt autoclean || true
log "临时文件清理完成"
}
# 安装完成报告
installation_complete() {
IP_ADDR=$(hostname -I | awk '{print $1}')
log "========================================"
log "Proxmox VE 安装完成!"
log "========================================"
log "Web界面: https://$IP_ADDR:8006/"
log "用户名: root"
log "密码: 您的root密码"
if [ "$OPEN_8006_PUBLIC" = "yes" ]; then
warn "8006端口已开放到公网,请确保使用强密码!"
else
log "8006端口仅允许已建立连接访问(推荐)"
log "如需公网访问,请配置SSH隧道或修改防火墙规则"
fi
log "========================================"
# 清理所有临时文件和配置
cleanup_temp_files
}
# =============================================================================
# 主程序流程
# =============================================================================
main() {
# 初始化全局变量
BRIDGE_CREATED=false
# 显示调试模式状态
if [ "$DEBUG_MODE" = "true" ]; then
log "调试模式已启用 - 将显示所有命令输出"
else
log "静默模式运行 - 使用 --debug 或 -d 参数查看详细输出"
fi
check_system
# 检测执行阶段
current_kernel=$(uname -r)
if echo "$current_kernel" | grep -q "pve"; then
# 第二阶段:PVE软件包安装
log "第二阶段:PVE软件包安装"
load_config
# 临时禁用IPv6
debug "临时禁用IPv6..."
run_cmd sysctl -w net.ipv6.conf.all.disable_ipv6=1
# 安装PVE软件包
echo "postfix postfix/main_mailer_type select Local only" | debconf-set-selections
debug "安装核心PVE软件包..."
if [ "$DEBUG_MODE" = "true" ]; then
DEBIAN_FRONTEND=noninteractive apt install -y proxmox-ve postfix open-iscsi chrony bridge-utils ifupdown2
else
DEBIAN_FRONTEND=noninteractive apt install -y proxmox-ve postfix open-iscsi chrony bridge-utils ifupdown2 >/dev/null 2>&1
fi
debug "安装额外网络组件..."
run_cmd apt install -y libpve-network-perl frr-pythontools dnsmasq
optimize_system
setup_hostname # 第二阶段处理hosts
setup_network_bridge
manage_pve_services
# 所有任务完成后最后执行DNS修复
fix_dns
installation_complete
else
# 第一阶段:PVE内核安装
log "第一阶段:PVE内核安装"
collect_config
save_config
debug "安装基础依赖包..."
run_cmd apt update || error "apt update 失败"
# 安装脚本必需的工具包
REQUIRED_PACKAGES="bc gpg gnupg vim curl wget sudo systemd-resolved ifupdown2 dnsutils grub2-common binutils debconf-utils bridge-utils lsb-release"
debug "安装必需工具包: $REQUIRED_PACKAGES"
run_cmd apt install -y $REQUIRED_PACKAGES || {
warn "部分依赖包安装失败,尝试分批安装..."
# 核心包优先安装
run_cmd apt install -y bc gpg gnupg curl wget sudo systemd-resolved || error "核心依赖包安装失败"
# 网络工具
run_cmd apt install -y dnsutils || warn "DNS工具安装失败,可能影响DNS验证"
# GRUB工具
run_cmd apt install -y grub2-common || warn "GRUB工具安装失败,可能影响内核检测"
# 其他工具
run_cmd apt install -y binutils debconf-utils bridge-utils lsb-release ifupdown2 vim || warn "部分工具安装失败,继续执行..."
}
setup_pve_repo || error "PVE仓库配置失败"
install_pve_kernel || error "PVE内核安装失败"
log "========================================"
log "第一阶段完成!"
log "========================================"
log "请重启系统到PVE内核,然后再次运行此脚本完成安装"
log "重启命令: systemctl reboot"
log "重启后运行: $0"
log "========================================"
fi
}
# 执行主程序
main "$@" || {
error_code=$?
error "脚本执行失败,错误代码: $error_code"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment