随机
Enter 搜索 ↑↓ 切换 Esc 清空

pve_snapshot_recover

脚本

PVE 实例快照回滚工具

pve_snapshot_recover

PVE 实例快照回滚工具

一键脚本

bash <(curl -sL gitee.com/meimolihan/linux-command_sh/raw/master/pve_snapshot_recover.sh)

效果预览

执行脚本效果预览

补充说明

该脚本用于在 Proxmox VE(PVE)平台上将 QEMU 虚拟机和 LXC 容器回滚到指定快照状态,基于 qm rollbackpct rollback 命令实现,适合在系统故障、配置回退等需要恢复至历史状态的运维场景。

功能特点

输出说明

脚本交互过程包含以下信息:

阶段 说明
实例列表 按序号列出所有有快照的 VM 和 CT 及其名称
快照列表 选中实例后展示其所有快照(含描述和当前标识)
双重确认 输入 yes 确认回滚,取消则输入 no
回滚结果 回滚成功或失败的状态提示

参数示例

# 交互式模式(列出有快照的实例 → 选择 → 选快照 → 确认 → 回滚)
bash pve_snapshot_recover.sh

关联工具

工具 说明
pve_snapshot_new.sh 创建 VM/CT 快照
pve_snapshot_list.sh 查看所有实例快照一览
pve_vm_start.sh 批量启动 VM/CT 实例
pve_vm_stop.sh 批量停止 VM/CT 实例
pve_vm_reboot.sh 批量重启 VM/CT 实例
pve_vm_status.sh 查看 VM/CT 运行状态

注意事项

脚本源码

#!/bin/bash

list_color_init() {
    export gl_hui=$'\033[38;5;59m'
    export gl_hong=$'\033[38;5;9m'
    export gl_lv=$'\033[38;5;10m'
    export gl_huang=$'\033[38;5;11m'
    export gl_lan=$'\033[38;5;32m'
    export gl_bai=$'\033[38;5;15m'
    export gl_zi=$'\033[38;5;13m'
    export gl_bufan=$'\033[38;5;14m'
    export reset=$'\033[0m'
}
list_color_init

log_info()  { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok()    { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn()  { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }

sleep_fractional() {
    local seconds=$1
    if sleep "$seconds" 2>/dev/null; then return 0; fi
    if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
    if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
    if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
    local int_seconds=$(echo "$seconds" | awk '{print int($1+0.999)}')
    sleep "$int_seconds"
}

break_end() {
    echo -e "${gl_lv}操作完成${gl_bai}"
    echo -e "${gl_bai}按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} \c"
    read -r -n 1 -s -r -p ""
    echo ""
    clear
}

root_use() {
    clear
    if [ "$EUID" -ne 0 ]; then
        echo -e "\n${gl_zi}>>> ROOT登录检查 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_huang}提示: ${gl_bai}该功能需要root用户才能运行!"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        break_end
        mobufan
        return 1
    fi
    return 0
}

exit_animation() {
    echo -ne "${gl_lv}即将退出 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.5
    echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    echo ""
    clear
}

exit_script() {
    echo ""
    echo -ne "${gl_hong}感谢使用,再见!${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.5
    echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    clear
    exit 0
}

list_vms_with_snapshots() {
    local vms=$(qm list 2>/dev/null | awk 'NR>1 {print $1}' | sort -n)
    local result=()
    for vmid in $vms; do
        local snap_count=$(qm listsnapshot "$vmid" 2>/dev/null | grep -vc "current\|You are here")
        if [ "$snap_count" -gt 0 ]; then
            local vm_name=$(qm config "$vmid" 2>/dev/null | grep "^name:" | awk '{print $2}')
            result+=("$vmid:$vm_name")
        fi
    done
    printf '%s\n' "${result[@]}"
}

list_cts_with_snapshots() {
    local cts=$(pct list 2>/dev/null | awk 'NR>1 {print $1}' | sort -n)
    local result=()
    for ctid in $cts; do
        local snap_count=$(pct listsnapshot "$ctid" 2>/dev/null | grep -vc "current\|You are here")
        if [ "$snap_count" -gt 0 ]; then
            local ct_name=$(pct config "$ctid" 2>/dev/null | grep "^hostname:" | awk '{print $2}')
            result+=("$ctid:$ct_name")
        fi
    done
    printf '%s\n' "${result[@]}"
}

get_snapshot_names() {
    local type=$1
    local id=$2
    if [ "$type" = "vm" ]; then
        qm listsnapshot "$id" 2>/dev/null | awk 'NR>1 && !/current/ {print $2}'
    else
        pct listsnapshot "$id" 2>/dev/null | awk 'NR>1 && !/current/ {print $2}'
    fi
}

get_snapshot_description() {
    local type=$1
    local id=$2
    local snapname=$3
    if [ "$type" = "vm" ]; then
        qm listsnapshot "$id" 2>/dev/null | awk -v snap="$snapname" '$2 == snap {desc=""; for(i=4;i<=NF;i++){if($i!="no-description"&&$i!="current"){desc=(desc?desc" ":"")$i}}; if(desc) print desc}'
    else
        pct listsnapshot "$id" 2>/dev/null | awk -v snap="$snapname" '$2 == snap {desc=""; for(i=4;i<=NF;i++){if($i!="no-description"&&$i!="current"){desc=(desc?desc" ":"")$i}}; if(desc) print desc}'
    fi
}

is_current_snapshot() {
    local type=$1
    local id=$2
    local snapname=$3
    if [ "$type" = "vm" ]; then
        qm listsnapshot "$id" 2>/dev/null | awk -v snap="$snapname" '$2 == snap {for(i=1;i<=NF;i++) if($i=="current") print "yes"}'
    else
        pct listsnapshot "$id" 2>/dev/null | awk -v snap="$snapname" '$2 == snap {for(i=1;i<=NF;i++) if($i=="current") print "yes"}'
    fi
}

rollback_vm() {
    local vmid=$1
    local snapname=$2
    local was_running=false
    
    local current_status=$(qm status "$vmid" 2>/dev/null | awk '{print $2}')
    if [ "$current_status" = "running" ]; then
        was_running=true
        log_warn "虚拟机 $vmid 正在运行,即将停止 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        if ! qm stop "$vmid" &>/dev/null; then
            log_error "无法停止虚拟机 $vmid"
            return 1
        fi
        log_ok "虚拟机 $vmid 已停止"
    fi
    
    log_info "正在回滚虚拟机 ${gl_bufan}$vmid${gl_bai} 到快照 ${gl_bufan}$snapname${gl_bai}"
    if qm rollback "$vmid" "$snapname" 2>&1; then
        log_ok "虚拟机 $vmid 回滚到快照 $snapname 成功"
        if [ "$was_running" = true ]; then
            log_info "正在重新启动虚拟机 $vmid ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
            qm start "$vmid" &>/dev/null && log_ok "虚拟机 $vmid 已启动" || log_warn "虚拟机 $vmid 启动失败,请手动启动"
        fi
        return 0
    else
        log_error "虚拟机 $vmid 回滚失败"
        if [ "$was_running" = true ]; then
            log_warn "虚拟机 $vmid 回滚前已停止,请手动启动"
        fi
        return 1
    fi
}

rollback_ct() {
    local ctid=$1
    local snapname=$2
    local was_running=false
    
    local current_status=$(pct status "$ctid" 2>/dev/null | awk '{print $2}')
    if [ "$current_status" = "running" ]; then
        was_running=true
        log_warn "容器 $ctid 正在运行,即将停止 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        if ! pct stop "$ctid" &>/dev/null; then
            log_error "无法停止容器 $ctid"
            return 1
        fi
        log_ok "容器 $ctid 已停止"
    fi
    
    log_info "正在回滚容器 ${gl_bufan}$ctid${gl_bai} 到快照 ${gl_bufan}$snapname${gl_bai}"
    if pct rollback "$ctid" "$snapname" 2>&1; then
        log_ok "容器 $ctid 回滚到快照 $snapname 成功"
        if [ "$was_running" = true ]; then
            log_info "正在重新启动容器 $ctid ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
            pct start "$ctid" &>/dev/null && log_ok "容器 $ctid 已启动" || log_warn "容器 $ctid 启动失败,请手动启动"
        fi
        return 0
    else
        log_error "容器 $ctid 回滚失败"
        if [ "$was_running" = true ]; then
            log_warn "容器 $ctid 回滚前已停止,请手动启动"
        fi
        return 1
    fi
}

handle_invalid_input() {
    echo -ne "\r${gl_huang}无效的输入,请重新输入! ${gl_zi} 1 ${gl_huang} 秒后返回"
    sleep 1
    echo -e "\r${gl_lv}无效的输入,请重新输入! ${gl_zi}0${gl_lv} 秒后返回"
    sleep 0.5
}

main() {
    while true; do
        clear
        root_use

        if ! command -v qm &> /dev/null; then
            echo -e ""
            echo -e "${gl_huang}>>> PVE 虚拟机/容器快照回滚${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            log_error "未检测到Proxmox VE环境,请确保脚本在PVE节点上运行"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            break_end
            return 1
        fi
    
        echo -e ""
        echo -e "${gl_zi}>>> PVE 虚拟机/容器快照回滚 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        
        local instance_list=()
        local index=1
        
        local vm_list=$(list_vms_with_snapshots)
        if [ -n "$vm_list" ]; then
            while IFS=: read -r vmid vmname; do
                instance_list+=("vm:$vmid")
                if [ -n "$vmname" ]; then
                    echo -e "${gl_bufan}$index. ${gl_bai}VM $vmid - $vmname"
                else
                    echo -e "${gl_bufan}$index. ${gl_bai}VM $vmid"
                fi
                ((index++))
            done <<< "$vm_list"
        fi
        
        local ct_list=$(list_cts_with_snapshots)
        if [ -n "$ct_list" ]; then
            while IFS=: read -r ctid ctname; do
                instance_list+=("ct:$ctid")
                if [ -n "$ctname" ]; then
                    echo -e "${gl_bufan}$index. ${gl_bai}CT $ctid - $ctname"
                else
                    echo -e "${gl_bufan}$index. ${gl_bai}CT $ctid"
                fi
                ((index++))
            done <<< "$ct_list"
        fi
        
        if [ ${#instance_list[@]} -eq 0 ]; then
            echo -e "${gl_huang}没有拥有快照的虚拟机或容器${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            break_end
            return 1
        fi
        
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        read -r -e -p "$(echo -e "${gl_bai}请选择要回滚快照的实例序号 (${gl_hong}0${gl_bai} 退出): ")" choice
        
        if [ "$choice" = "0" ]; then
            exit_script
        fi
        
        if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#instance_list[@]}" ]; then
            local selected="${instance_list[$((choice-1))]}"
            local type="${selected%%:*}"
            local id="${selected#*:}"
            
            clear
            echo -e ""
            echo -e "${gl_zi}>>> 快照列表 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            echo -e "${gl_bai}实例: ${gl_bufan}$id${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            
            local snap_names=()
            local snap_index=1
            while IFS= read -r snapname; do
                [ -z "$snapname" ] && continue
                snap_names+=("$snapname")
                local desc=$(get_snapshot_description "$type" "$id" "$snapname")
                local current_mark=$(is_current_snapshot "$type" "$id" "$snapname")
                
                if [ -n "$current_mark" ]; then
                    echo -e "${gl_bufan}$snap_index. ${gl_lv}$snapname ${gl_huang}(当前)${gl_bai}"
                elif [ -n "$desc" ]; then
                    echo -e "${gl_bufan}$snap_index. ${gl_bai}$snapname - $desc"
                else
                    echo -e "${gl_bufan}$snap_index. ${gl_bai}$snapname"
                fi
                ((snap_index++))
            done <<< "$(get_snapshot_names "$type" "$id")"
            
            if [ ${#snap_names[@]} -eq 0 ]; then
                log_info "该实例没有可回滚的快照"
                break_end
                continue
            fi
            
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            read -r -e -p "$(echo -e "${gl_bai}请选择要回滚的快照序号 (${gl_hong}0${gl_bai} 取消): ")" snap_choice
            
            if [ "$snap_choice" = "0" ]; then
                continue
            fi
            
            if [[ "$snap_choice" =~ ^[0-9]+$ ]] && [ "$snap_choice" -ge 1 ] && [ "$snap_choice" -le "${#snap_names[@]}" ]; then
                local snapname="${snap_names[$((snap_choice-1))]}"
                
                echo -e ""
                echo -e "${gl_hong}⚠ 危险操作!回滚将丢失所有后续更改,且不可逆!${gl_bai}"
                echo -e "${gl_huang}即将回滚实例 ${gl_bufan}$id${gl_huang} 到快照 ${gl_bufan}$snapname${gl_huang}${gl_bai}"
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                read -r -e -p "$(echo -e "${gl_bai}请输入 ${gl_hong}yes${gl_bai} 确认回滚 (输入 ${gl_lv}no${gl_bai} 取消): ")" confirm
                
                if [ "$confirm" != "yes" ]; then
                    log_info "已取消回滚操作"
                    break_end
                    continue
                fi
                
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                if [ "$type" = "ct" ]; then
                    rollback_ct "$id" "$snapname"
                else
                    rollback_vm "$id" "$snapname"
                fi
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                break_end
                return 1
            else
                handle_invalid_input
            fi
        else
            handle_invalid_input
        fi
    done
}

main

创建本地脚本

new_script="pve_snapshot_recover.sh"

cat > "$new_script" << 'EOF'
#!/bin/bash

# 粘贴脚本源码

EOF

chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"