用豆包写的能将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