EVE-NG镜像导出脚本

作者:waMoYu 发布时间: 2026-01-07 阅读量:18 评论数:0

用豆包写的能将EVE-NG中QEMU、IOL镜像打包后导出的shell脚本。

脚本能自动识别对应镜像的yml、py、图标,并且一起打包到tar.gz压缩包中。

#!/bin/bash
set -euo pipefail

# ===================== 固定路径定义 =====================
QEMU_IMG_DIR="/opt/unetlab/addons/qemu"
IOL_IMG_DIR="/opt/unetlab/addons/iol/bin"
TEMPLATE_ROOT="/opt/unetlab/html/templates"
ICON_DIR="/opt/unetlab/html/images/icons"
SCRIPT_DIR="/opt/unetlab/config_scripts"

# 全局数组(存储镜像信息:格式 "类型:镜像名称:路径")
IOL_IMAGES=()
QEMU_IMAGES=()
ALL_IMAGES=()
# 全局变量(存储CPU对应的模板目录)
TEMPLATE_DIR=""

# ===================== 函数:获取CPU类型并确定模板目录 =====================
get_cpu_type_template() {
    echo "正在检测CPU类型..."
    # 检测CPU厂商(兼容不同lscpu输出格式)
    local cpu_vendor
    if cpu_vendor=$(lscpu | grep -iE 'Vendor ID|制造商' | awk '{print $NF}' | tr '[:lower:]' '[:upper:]'); then
        if [[ $cpu_vendor == *"INTEL"* ]]; then
            TEMPLATE_DIR="${TEMPLATE_ROOT}/intel"
            echo "CPU类型:Intel,对应模板目录:$TEMPLATE_DIR"
        elif [[ $cpu_vendor == *"AMD"* ]]; then
            TEMPLATE_DIR="${TEMPLATE_ROOT}/amd"
            echo "CPU类型:AMD,对应模板目录:$TEMPLATE_DIR"
        else
            echo "错误:无法识别CPU类型(非Intel/AMD架构)"
            exit 1
        fi
    else
        echo "错误:无法获取CPU信息,请确保lscpu工具已安装"
        exit 1
    fi

    # 验证模板目录是否存在
    if [ ! -d "$TEMPLATE_DIR" ]; then
        echo "错误:模板目录 $TEMPLATE_DIR 不存在,请检查EVE-NG安装是否完整"
        exit 1
    fi
}

# ===================== 函数:加载并显示所有镜像(按字母排序+统一全局编号) =====================
load_and_show_all_images() {
    # 初始化镜像数组
    IOL_IMAGES=()
    QEMU_IMAGES=()
    ALL_IMAGES=()

    echo -e "\033[34m===== 所有镜像(全局统一编号,按字母排序) =====\033[0m"

    # -------- 加载并排序IOL镜像 --------
    if [ -d "$IOL_IMG_DIR" ]; then
        local iol_bin_list=()
        while IFS= read -r -d '' bin_file; do
            iol_bin_list+=("$bin_file")
        done < <(find "$IOL_IMG_DIR" -maxdepth 1 -name "*.bin" -print0 2>/dev/null)

        for bin_file in "${iol_bin_list[@]}"; do
            local img_name=$(basename "$bin_file" .bin)
            IOL_IMAGES+=("IOL:${img_name}:${bin_file}")
        done
        IOL_IMAGES=($(printf "%s\n" "${IOL_IMAGES[@]}" | sort -f -t':' -k2))
    fi

    # -------- 加载并排序QEMU镜像 --------
    if [ -d "$QEMU_IMG_DIR" ]; then
        local qemu_dir_list=()
        while IFS= read -r -d '' img_dir; do
            qemu_dir_list+=("$img_dir")
        done < <(find "$QEMU_IMG_DIR" -maxdepth 1 -type d ! -path "$QEMU_IMG_DIR" -print0 2>/dev/null)

        for img_dir in "${qemu_dir_list[@]}"; do
            local img_name=$(basename "$img_dir" /)
            QEMU_IMAGES+=("QEMU:${img_name}:${img_dir}")
        done
        QEMU_IMAGES=($(printf "%s\n" "${QEMU_IMAGES[@]}" | sort -f -t':' -k2))
    fi

    # -------- 组装全局镜像数组 --------
    ALL_IMAGES=("${IOL_IMAGES[@]}" "${QEMU_IMAGES[@]}")

    # -------- 统一显示 --------
    local global_idx=1
    local iol_flag=0
    local qemu_flag=0

    echo -e "\033[33m--- IOL镜像(按字母排序) ---\033[0m"
    if [ ${#IOL_IMAGES[@]} -eq 0 ]; then
        echo "  无可用IOL镜像"
    else
        for iol_img in "${IOL_IMAGES[@]}"; do
            local img_name=$(echo "$iol_img" | cut -d':' -f2)
            echo "  ${global_idx}. ${img_name}(IOL类型)"
            ((global_idx++))
            iol_flag=1
        done
    fi

    echo -e "\n\033[33m--- QEMU镜像(按字母排序) ---\033[0m"
    if [ ${#QEMU_IMAGES[@]} -eq 0 ]; then
        echo "  无可用QEMU镜像"
    else
        for qemu_img in "${QEMU_IMAGES[@]}"; do
            local img_name=$(echo "$qemu_img" | cut -d':' -f2)
            local img_path=$(echo "$qemu_img" | cut -d':' -f3)
            local qcow2_names=""
            local qcow2_list=()
            while IFS= read -r -d '' qcow2_file; do
                qcow2_list+=("$(basename "$qcow2_file")")
            done < <(find "$img_path" -maxdepth 1 -name "*.qcow2" -print0 2>/dev/null)
            for idx in "${!qcow2_list[@]}"; do
                if [ $idx -eq 0 ]; then
                    qcow2_names="${qcow2_list[$idx]}"
                else
                    qcow2_names="${qcow2_names}、${qcow2_list[$idx]}"
                fi
            done
            echo "  ${global_idx}. ${img_name} [${qcow2_names}](QEMU类型)"
            ((global_idx++))
            qemu_flag=1
        done
    fi

    # 无镜像提示
    if [ $iol_flag -eq 0 ] && [ $qemu_flag -eq 0 ]; then
        echo -e "\033[31m  无任何可用镜像\033[0m"
        exit 1
    fi
}

# ===================== 函数:模糊搜索镜像 =====================
search_images() {
    local keyword=$1
    local search_results=()
    local result_idx=1

    echo -e "\n\033[34m===== 搜索关键字:${keyword}(忽略大小写) =====\033[0m"

    for img in "${ALL_IMAGES[@]}"; do
        local img_name=$(echo "$img" | cut -d':' -f2)
        if [[ "${img_name,,}" == *"${keyword,,}"* ]]; then
            search_results+=("$img")
            local img_type=$(echo "$img" | cut -d':' -f1)
            echo "  ${result_idx}. ${img_name}(${img_type}类型)"
            ((result_idx++))
        fi
    done

    if [ ${#search_results[@]} -eq 0 ]; then
        echo -e "\033[31m  未找到包含关键字「${keyword}」的镜像\033[0m"
        return 1
    fi

    export SEARCH_RESULTS=("${search_results[@]}")
    return 0
}

# ===================== 函数:导出单个镜像(显式指定导出内容) =====================
export_single_image() {
    local img_type=$1
    local img_name=$2
    local img_path=$3
    local export_file="${img_name}.tar.gz"
    local temp_dir=$(mktemp -d -t eve-img-export-XXXXXX)

    echo -e "\n\033[32m开始导出 ${img_type} 镜像:${img_name}\033[0m"
    echo "临时工作目录:$temp_dir"

    # -------- 复制镜像核心文件 --------
    if [ "$img_type" == "IOL" ]; then
        local bin_file="${IOL_IMG_DIR}/${img_name}.bin"
        if [ -f "$bin_file" ]; then
            cp "$bin_file" "$temp_dir/"
            echo "  ✅ 已复制IOL镜像文件:$(basename "$bin_file")"
        else
            echo "  ⚠️  警告:IOL镜像文件 $bin_file 不存在,跳过"
        fi
    elif [ "$img_type" == "QEMU" ]; then
        local qemu_img_dir="$img_path"
        if [ -d "$qemu_img_dir" ]; then
            mkdir -p "${temp_dir}/${img_name}"
            find "$qemu_img_dir" -maxdepth 1 -name "*.qcow2" -exec cp {} "${temp_dir}/${img_name}/" \; 2>/dev/null
            local qcow2_count=$(find "${temp_dir}/${img_name}" -maxdepth 1 -name "*.qcow2" | wc -l)
            if [ $qcow2_count -gt 0 ]; then
                echo "  ✅ 已复制QEMU镜像 ${img_name} 下 ${qcow2_count} 个qcow2文件"
            else
                echo "  ⚠️  警告:QEMU镜像目录 $qemu_img_dir 下无qcow2文件,跳过"
            fi
        else
            echo "  ⚠️  警告:QEMU镜像目录 $qemu_img_dir 不存在,跳过"
        fi
    fi

    # -------- 复制.yml模板文件 --------
    local template_name
    if [[ "$img_name" == *"-"* ]]; then
        template_name=$(echo "$img_name" | cut -d'-' -f1)
    else
        template_name="$img_name"
    fi
    local yml_file="${TEMPLATE_DIR}/${template_name}.yml"
    if [ -f "$yml_file" ]; then
        cp "$yml_file" "$temp_dir/"
        echo "  ✅ 已复制模板文件:$(basename "$yml_file")"

        # -------- 复制icon图标文件 --------
        local icon_name=$(grep -i '^icon:' "$yml_file" | awk '{print $2}' | sed -e 's/["'\'' ]//g' -e 's/^//' -e 's/$//')
        local icon_found=0
        if [ -n "$icon_name" ]; then
            local full_icon_path="${ICON_DIR}/${icon_name}"
            if [ -f "$full_icon_path" ]; then
                cp "$full_icon_path" "$temp_dir/"
                echo "  ✅ 已复制图标文件:$(basename "$full_icon_path")"
                icon_found=1
            else
                local icon_suffixes=("png" "svg" "jpg")
                for suffix in "${icon_suffixes[@]}"; do
                    local icon_file="${ICON_DIR}/${icon_name}.${suffix}"
                    if [ -f "$icon_file" ]; then
                        cp "$icon_file" "$temp_dir/"
                        echo "  ✅ 已复制图标文件:$(basename "$icon_file")"
                        icon_found=1
                        break
                    fi
                done
            fi
            if [ $icon_found -eq 0 ]; then
                echo "  ⚠️  警告:未找到图标文件(原始名称:${icon_name};或拼接后缀:${icon_name}.png/svg/jpg)"
            fi
        else
            echo "  ⚠️  警告:yml文件中未找到有效icon字段,跳过图标复制"
        fi

        # -------- 复制config_script脚本(QEMU专属) --------
        if [ "$img_type" == "QEMU" ]; then
            local script_name=$(grep -i '^config_script:' "$yml_file" | awk '{print $2}' | sed -e 's/["'\'' ]//g' -e 's/^//' -e 's/$//')
            if [ -n "$script_name" ]; then
                local script_file="${SCRIPT_DIR}/${script_name}"
                if [ -f "$script_file" ]; then
                    cp "$script_file" "$temp_dir/"
                    echo "  ✅ 已复制配置脚本:$(basename "$script_file")"
                else
                    echo "  ⚠️  警告:配置脚本 $script_file 不存在,跳过"
                fi
            else
                echo "  ℹ️  提示:yml文件中未配置config_script字段,跳过脚本复制"
            fi
        fi
    else
        echo "  ⚠️  警告:模板文件 $yml_file 不存在,跳过yml及相关图标/脚本复制"
    fi

    # -------- 收集待导出文件列表 --------
    echo "正在收集待导出文件列表..."
    cd "$temp_dir"
    local export_files=()
    while IFS= read -r -d '' file; do
        local rel_file="${file#./}"
        export_files+=("$rel_file")
    done < <(find . -maxdepth 1 -mindepth 1 -print0)
    cd - > /dev/null

    if [ ${#export_files[@]} -eq 0 ]; then
        echo -e "\033[31m  ❌ 错误:临时目录中无任何可导出的文件!\033[0m"
        rm -rf "$temp_dir"
        return 1
    fi

    echo "待导出文件列表:${export_files[*]}"

    # -------- 执行导出(显式指定文件列表) --------
    echo "正在生成导出文件:$export_file,临时目录:$temp_dir"
    tar -zcvf "$export_file" -C "$temp_dir" "${export_files[@]}"
    
    if [ $? -eq 0 ]; then
        echo -e "\033[32m  ✅ 导出成功!文件路径:$(pwd)/$export_file\033[0m"
    else
        echo -e "\033[31m  ❌ 导出失败!\033[0m"
        rm -rf "$temp_dir"
        return 1
    fi

    # -------- 清理临时目录 --------
    rm -rf "$temp_dir"
    echo "临时目录已清理"
}

# ===================== 函数:导出所有镜像 =====================
export_all_images() {
    echo -e "\033[33m===== 开始导出所有镜像 =====\033[0m"

    # 导出所有IOL镜像
    if [ ${#IOL_IMAGES[@]} -gt 0 ]; then
        echo -e "\n\033[36m--- 导出IOL镜像组 ---\033[0m"
        for iol_img in "${IOL_IMAGES[@]}"; do
            local img_type=$(echo "$iol_img" | cut -d':' -f1)
            local img_name=$(echo "$iol_img" | cut -d':' -f2)
            local img_path=$(echo "$iol_img" | cut -d':' -f3)
            export_single_image "$img_type" "$img_name" "$img_path"
            echo "-------------------------"
        done
    else
        echo -e "\n\033[36m--- 无IOL镜像可导出 ---\033[0m"
    fi

    # 导出所有QEMU镜像
    if [ ${#QEMU_IMAGES[@]} -gt 0 ]; then
        echo -e "\n\033[36m--- 导出QEMU镜像组 ---\033[0m"
        for qemu_img in "${QEMU_IMAGES[@]}"; do
            local img_type=$(echo "$qemu_img" | cut -d':' -f1)
            local img_name=$(echo "$qemu_img" | cut -d':' -f2)
            local img_path=$(echo "$qemu_img" | cut -d':' -f3)
            export_single_image "$img_type" "$img_name" "$img_path"
            echo "-------------------------"
        done
    else
        echo -e "\n\033[36m--- 无QEMU镜像可导出 ---\033[0m"
    fi

    echo -e "\033[33m===== 所有镜像导出流程结束 =====\033[0m"
}

# ===================== 主程序:极简交互(默认进入导出菜单) =====================
main() {
    # 第一步:检测CPU类型和模板目录(必须前置)
    get_cpu_type_template

    # 第二步:显示极简导出菜单
    while true; do
        echo -e "\n\033[35m-----------------EVE-NG镜像导出----------------\033[0m"
        echo "1. 导出单个镜像"
        echo "2. 导出所有镜像(qemu+iol)"
        read -p "请选择功能(1/2):" func_choice

        case $func_choice in
            1)
                # 功能1:导出单个镜像(先加载并显示所有镜像)
                load_and_show_all_images

                # 直接提示输入(全局编号或search+关键字)
                read -p "请输入全局编号或search+关键字:" user_input
                local selected_img=""
                
                # 分支1:输入是search+关键字(模糊搜索)
                if [[ "$user_input" =~ ^search\ +.*$ ]]; then
                    local keyword=$(echo "$user_input" | sed -e 's/^search[[:space:]]\+//' -e 's/[[:space:]]\+$//')
                    if [ -z "$keyword" ]; then
                        echo -e "\033[31m错误:搜索关键字不能为空!\033[0m"
                        continue
                    fi
                    if ! search_images "$keyword"; then
                        continue
                    fi
                    read -p "请输入搜索结果中的编号(选择要导出的镜像):" search_idx
                    if ! [[ "$search_idx" =~ ^[0-9]+$ ]] || [ "$search_idx" -lt 1 ] || [ "$search_idx" -gt ${#SEARCH_RESULTS[@]} ]; then
                        echo -e "\033[31m错误:无效的搜索结果编号(范围1-${#SEARCH_RESULTS[@]})\033[0m"
                        continue
                    fi
                    selected_img=${SEARCH_RESULTS[$((search_idx-1))]}
                
                # 分支2:输入是数字(全局编号)
                elif [[ "$user_input" =~ ^[0-9]+$ ]]; then
                    local img_idx="$user_input"
                    if [ "$img_idx" -lt 1 ] || [ "$img_idx" -gt ${#ALL_IMAGES[@]} ]; then
                        echo -e "\033[31m错误:无效的镜像全局编号(范围1-${#ALL_IMAGES[@]})\033[0m"
                        continue
                    fi
                    selected_img=${ALL_IMAGES[$((img_idx-1))]}
                
                # 分支3:输入格式错误
                else
                    echo -e "\033[31m错误:输入格式错误!\n  正确格式1(搜索):search 关键字(如 search linux)\n  正确格式2(选号):数字(如 5)\033[0m"
                    continue
                fi

                # 提取镜像信息并确认导出
                local img_type=$(echo "$selected_img" | cut -d':' -f1)
                local img_name=$(echo "$selected_img" | cut -d':' -f2)
                local img_path=$(echo "$selected_img" | cut -d':' -f3)

                echo -e "\n\033[33m--- 导出确认 ---\033[0m"
                echo "你选中的镜像信息如下:"
                echo "  镜像类型:$img_type"
                echo "  镜像名称:$img_name"
                read -p "是否确认导出该镜像?(y/n,默认n):" confirm_choice
                if [[ ! "$confirm_choice" =~ ^[Yy]$ ]]; then
                    echo -e "\033[36m已取消导出 ${img_name} 镜像\033[0m"
                    continue
                fi

                # 执行导出
                export_single_image "$img_type" "$img_name" "$img_path"
                ;;

            2)
                # 功能2:导出所有镜像(先加载所有镜像)
                load_and_show_all_images

                # 全量导出确认
                echo -e "\n\033[33m--- 全量导出确认 ---\033[0m"
                echo "即将导出所有IOL和QEMU镜像,可能耗时较长!"
                read -p "是否确认全量导出?(y/n,默认n):" batch_confirm
                if [[ ! "$batch_confirm" =~ ^[Yy]$ ]]; then
                    echo -e "\033[36m已取消全量导出\033[0m"
                    continue
                fi

                export_all_images
                ;;

            *)
                echo -e "\033[31m错误:无效的功能选择,请输入1/2\033[0m"
                ;;
        esac
    done
}

# 启动主程序
main

评论