PVE制作Ubuntu24镜像模板

这篇文章讲清楚:如何在 Proxmox VE(PVE)上把 Ubuntu 24.04 做成可克隆、可注入配置、可被宿主管理的基础通用模板。适用对象是准备批量交付 VM 的运维/平台/研发同学;你会得到一套可复现流程:预装最小通用工具、安装并保证 qemu-guest-agent 开机可用、补齐 Cloud-Init 驱动器并让 PVE 面板可注入用户名/SSH Key/IP、最后做模板泛化清理避免克隆冲突。

1)环境与版本(含适用边界)

1.1 环境信息(示例)

项目
虚拟化平台Proxmox VE(PVE)
Guest OSUbuntu 24.04 LTS(noble)
网络管理netplan(默认)
镜像源mirrors.aliyun.com(可替换为内网源)

1.2 目标(模板交付标准)

  • PVE 能读取 VM IP、能优雅关机:qemu-guest-agent 可用
  • PVE Cloud-Init 页面可用:VM 已挂 CloudInit Drive,Guest 已装 cloud-init
  • 克隆后不冲突:machine-id/var/lib/cloud、SSH host key 已泛化清理

2)方案概述(技术→落地:一句话结论 + 关键点)

**结论:**模板采用 “PVE Cloud-Init + QEMU Guest Agent” 组合:Cloud-Init 负责注入(user/SSH key/network/hostname),Guest Agent 负责宿主管理能力(IP/关机/执行 guest 命令等)。

关键点:

  • PVE 提示“未找到 CloudInit 驱动器”= VM 没挂 CloudInit Drive,不是 Ubuntu 内部问题。
  • Cloud-Init 注入生效需要两端都满足:PVE 有 CloudInit Drive + Guest 安装 cloud-init
  • Ubuntu 24.04 上 qemu-guest-agent 常见为 static unit:可运行,但 systemctl enable 会提示“no installation config”;用启动器服务保证开机拉起。
  • 自动扩容建议用 cloud-guest-utils 提供的 growpart;LVM 场景走 growpart -> pvresize -> lvextend -r
  • 扩盘后经常会遇到 GPT 报警(backup GPT table 不在盘尾);首次开机脚本里用 sgdisk -e 进行修复。
  • 只跑一次靠 marker(如 /var/lib/firstboot-growroot.done),封模板前必须删除 marker。

3)对比与选型(为什么这样搭)

组件主要用途优势注意事项
PVE Cloud-Init Drive注入 user/ssh/ip/hostnamePVE 面板可控、克隆即用必须给 VM 添加 CloudInit Drive
cloud-init(Guest)读取注入配置免手工配置网络与账号需要清理 /var/lib/cloud 避免克隆复用
qemu-guest-agentIP 上报、优雅关机等宿主管理更完整Ubuntu 24.04 常见无法 enable(static unit)
firstboot-growroot(脚本)首次开机自动扩容克隆后无需手工扩容需要 marker;封模板前要删 marker

小结:

  • 要“PVE 面板注入配置”→ CloudInit Drive + cloud-init
  • 要“宿主机读 IP / 优雅关机”→ qemu-guest-agent
  • 要“克隆后根分区自动吃满扩盘”→ firstboot-growroot

4)落地步骤(可复现:安装→配置→验证→封模板)

4.1 Guest 内:基础通用包(可选但推荐)

Bash:安装最小通用工具集(运维/排障/脚本常用)。

Bash
sudo apt update && sudo apt -y full-upgrade

sudo apt install -y \
  openssh-server sudo \
  curl wget vim git rsync \
  ca-certificates gnupg \
  jq lsof htop tmux bash-completion \
  iputils-ping dnsutils traceroute net-tools \
  7zip zip unzip zstd xz-utils pigz \
  tree ncdu pv parted gdisk \
  mtr-tiny tcpdump netcat-openbsd socat ethtool \
  iotop sysstat psmisc \
  ripgrep fd-find \
  chrony

sudo systemctl enable --now chrony

允许SSH使用密码登录

Bash
#先设置密码
sudo passwd root

#放行密码登录
sudo tee /etc/ssh/sshd_config.d/99-root-login.conf >/dev/null <<'EOF'
PermitRootLogin yes
PasswordAuthentication yes
EOF
sudo systemctl restart ssh

验证:确认 SSH 服务可用、基础命令存在。

Bash
systemctl status ssh --no-pager
curl --version && git --version && jq --version
chronyc tracking || true
ncdu --version
7zz --help | head -n 1 || true

4.2 Guest 内:安装并运行 QEMU Guest Agent

Bash:安装并启动(注意:Ubuntu 24.04 可能无法 enable 属于正常现象)。

Bash
sudo apt install -y qemu-guest-agent
sudo systemctl start qemu-guest-agent
sudo systemctl status qemu-guest-agent --no-pager

验证:确认 virtio 通道存在(有 org.qemu.guest_agent.0 才说明 PVE 通道已挂进来)。

Bash
ls -l /dev/virtio-ports/

4.3 解决“enable 不了”的开机启动(推荐:加启动器服务)

Bash:新增一个 oneshot 启动器,开机检测到 virtio 设备后启动 GA。

Bash
sudo tee /etc/systemd/system/pve-qemu-ga-start.service >/dev/null <<'EOF'
[Unit]
Description=Start qemu-guest-agent for Proxmox VE
After=dev-virtio\x2dports-org.qemu.guest_agent.0.device
Wants=dev-virtio\x2dports-org.qemu.guest_agent.0.device
ConditionPathExists=/dev/virtio-ports/org.qemu.guest_agent.0

[Service]
Type=oneshot
ExecStart=/bin/systemctl start qemu-guest-agent.service
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now pve-qemu-ga-start.service

验证:启动器 active (exited) 属于正常;GA 需为 active (running)

Bash
systemctl status pve-qemu-ga-start --no-pager
systemctl status qemu-guest-agent --no-pager

4.4 PVE 侧:修复“未找到 CloudInit 驱动器”(核心)

路径 A:WebUI 添加(推荐)

  1. 关机 VM(Shutdown)
  2. VM → HardwareAddCloudInit Drive
  3. Storage 选择 local-lvm(或你的系统盘存储)
  4. Hardware 出现类似 ide2: <storage>:cloudinit
  5. VM → Cloud-Init 页面填写:ciuser / SSH Keys / ipconfig0 / DNS
  6. Regenerate Image(重新生成)→ 开机

路径 B:宿主机命令行添加(更可控)

Bash:假设 VMID=101,存储=local-lvm。

Bash
qm set 101 --ide2 local-lvm:cloudinit
qm set 101 --ciuser procoding
qm set 101 --ipconfig0 ip=dhcp
qm set 101 --sshkeys ~/.ssh/id_rsa.pub

qm config 101 | grep -i cloudinit

4.5 Guest 内:安装 cloud-init(让注入生效)

Bash:安装 cloud-init 与扩容工具。

Bash
sudo apt install -y cloud-init cloud-guest-utils

(可选)限制 datasource,减少无关探测:

Bash
cat <<'EOF' | sudo tee /etc/cloud/cloud.cfg.d/99-pve.cfg
datasource_list: [ ConfigDrive, NoCloud ]
EOF

验证:查看 cloud-init 是否能识别 datasource(首次克隆启动后更明显)。

Bash
cloud-init status --long || true

4.6 首次开机自动扩容(LVM 根盘:growpart + pvresize + lvextend -r)

4.6.1 创建扩容脚本(只跑一次)

脚本做四件事:
1)修 GPT 备份表到盘尾(sgdisk -e
2)扩 PV 所在分区(growpart
3)pvresize 吃掉新增空间
4)lvextend -r 扩 LV 并在线扩文件系统

Bash:写入 /usr/local/sbin/firstboot-growroot.sh

Bash
sudo tee /usr/local/sbin/firstboot-growroot.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

MARKER="/var/lib/firstboot-growroot.done"
[ -f "$MARKER" ] && exit 0

log() { echo "[firstboot-growroot] $*"; }

root_src="$(findmnt -no SOURCE /)"
fstype="$(findmnt -no FSTYPE /)"

partnum_of() {
  cat "/sys/class/block/$(basename "$1")/partition" 2>/dev/null || true
}

grow_partition_to_end() {
  local part="$1"
  local pkname partnum disk

  pkname="$(lsblk -no PKNAME "$part" 2>/dev/null | head -n1 || true)"
  partnum="$(partnum_of "$part")"
  [ -n "$pkname" ] && [ -n "$partnum" ] || return 0

  disk="/dev/$pkname"
  log "fix GPT (best effort): sgdisk -e ${disk}"
  sgdisk -e "$disk" >/dev/null 2>&1 || true
  partprobe "$disk" >/dev/null 2>&1 || true

  log "growpart ${disk} ${partnum} (for ${part})"
  growpart "$disk" "$partnum" || true
  partprobe "$disk" >/dev/null 2>&1 || true
}

# LVM root
if [[ "$root_src" == /dev/mapper/* ]]; then
  log "detected LVM root: ${root_src}"
  vg="$(lvs --noheadings -o vg_name "$root_src" 2>/dev/null | xargs || true)"
  [ -n "$vg" ] || { log "cannot detect VG for ${root_src}"; exit 1; }

  mapfile -t pvs_list < <(pvs --noheadings -o pv_name,vg_name | awk -v vg="$vg" '$2==vg{print $1}')
  for pv in "${pvs_list[@]}"; do
    pv="$(echo "$pv" | xargs)"
    [ -n "$pv" ] || continue
    log "PV: ${pv}"

    if lsblk -no TYPE "$pv" 2>/dev/null | grep -qx "part"; then
      grow_partition_to_end "$pv"
    fi

    log "pvresize ${pv}"
    pvresize "$pv" || true
  done

  log "lvextend -l +100%FREE -r ${root_src}"
  lvextend -l +100%FREE -r "$root_src" || true

else
  # Non-LVM root partition (fallback)
  log "detected partition root: ${root_src}"
  if lsblk -no TYPE "$root_src" 2>/dev/null | grep -qx "part"; then
    grow_partition_to_end "$root_src"
  fi

  case "$fstype" in
    ext4|ext3|ext2) resize2fs "$root_src" ;;
    xfs) xfs_growfs / ;;
    btrfs) btrfs filesystem resize max / ;;
    *) log "skip resize: unsupported fstype=${fstype}" ;;
  esac
fi

date -Is > "$MARKER"
log "done. marker=${MARKER}"
EOF

sudo chmod +x /usr/local/sbin/firstboot-growroot.sh

4.6.2 创建 systemd 服务(首次开机触发)

INI:写入 /etc/systemd/system/firstboot-growroot.service

Bash
[Unit]
Description=First boot: grow root partition and filesystem
After=local-fs.target
ConditionPathExists=!/var/lib/firstboot-growroot.done

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/firstboot-growroot.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

启用:

Bash
sudo systemctl daemon-reload
sudo systemctl enable firstboot-growroot.service

验证(在“克隆机首次开机”后):

Bash
systemctl status firstboot-growroot --no-pager || true
lsblk
df -h /

4.7(可选)网络策略:避免 netplan 冲突

如果你决定让 PVE Cloud-Init 管网络,建议删除安装器生成的 netplan,避免与 cloud-init 生成的 50-cloud-init.yaml 叠加冲突。

Bash

Bash
sudo rm -f /etc/netplan/00-installer-config.yaml
sudo netplan generate

验证:netplan 能正常生成配置(不要在远程断网环境盲 apply)。

Bash
sudo netplan generate

4.8 封模板前:泛化清理(必须做)

A)清 cloud-init 状态(装了 cloud-init 才做)
B) firstboot 扩容 marker(必须删除,否则克隆不扩容)
C)清 machine-id(避免克隆后同源冲突)
D)清 SSH host keys(让每台克隆机生成自己的)
E)清理缓存(可选)

Bash
# cloud-init(装了才执行)
sudo cloud-init clean --logs || true
sudo rm -rf /var/lib/cloud/* || true

# firstboot 扩容 marker(必须删除,否则克隆不扩容)
sudo rm -f /var/lib/firstboot-growroot.done

# machine-id(避免克隆同源)
sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo ln -sf /etc/machine-id /var/lib/dbus/machine-id

# SSH host keys(每台克隆机生成自己的)
sudo rm -f /etc/ssh/ssh_host_*

# 清理缓存
sudo apt clean

# 关机转模板
sudo poweroff

关机:

Bash
sudo poweroff

4.9 PVE 侧:转模板 + 首次克隆验证

PVE 转模板前检查:

  • VM → Options → QEMU Guest Agent:Enabled
  • VM → Hardware → 有 CloudInit Drive
  • Convert to template

克隆一台后在 Guest 内验证:

Bash
systemctl status qemu-guest-agent --no-pager
systemctl status pve-qemu-ga-start --no-pager
cloud-init status --long || true
ls -l /etc/machine-id
ls -l /etc/ssh/ssh_host_*

期望结果:

  • GA:active (running)
  • 启动器:active (exited)
  • cloud-init:状态正常(首次启动会跑一轮)
  • machine-id / ssh host keys:已重新生成(不再复用模板)

5)运维与故障定位(常见问题 4 条)

5.1 扩盘后 fdisk 提示 GPT backup table 不在盘尾

现象:

  • GPT PMBR size mismatch ...
  • The backup GPT table is not on the end of the device.

处理:修一次即可(脚本里已包含 best effort)。

Bash
sudo sgdisk -e /dev/sda
sudo partprobe /dev/sda

5.2 firstboot-growroot 失败:lsblk: unknown column: PARTNUM

原因:不同系统/版本的 lsblk 列名不一致,PARTNUM 不是通用列。
处理:改用 sysfs 读分区号(本文脚本已修复:/sys/class/block/<dev>/partition)。

5.3 服务被跳过:ConditionPathExists 不满足

现象:was skipped because of an unmet condition check
原因:marker 文件存在,服务认为已经跑过。
处理:删 marker 后手动重跑一次:

Bash
sudo rm -f /var/lib/firstboot-growroot.done
sudo systemctl restart firstboot-growroot.service

5.4 Cloud-Init 注入不生效

排查顺序:

  • PVE:Hardware 是否有 CloudInit Drive
  • Guest:是否安装 cloud-init
  • 模板泛化:是否清过 /var/lib/cloud/*

6)结尾(总结 / 边界 / 下一步)

  • 总结(可执行要点):
    • 模板基线:基础工具 + qemu-guest-agent + cloud-init
    • Ubuntu 24.04 的 GA:不纠结 enable,用启动器保证开机拉起
    • PVE Cloud-Init 页面可用的前提:必须挂 CloudInit Drive
    • 封模板前必做泛化:cloud-init 状态、machine-id、SSH host keys
  • 边界提醒:如果你不打算用 Cloud-Init 管网络,避免删除现有 netplan;否则可能导致克隆后网络配置不符合预期。
RabbitMQ 快速入门实践 使用TC(Traffic Control)对内外网分流限速 跨语言任务队列代码实战:Spring Boot + RabbitMQ + Celery 全链路打通
View Comments
There are currently no comments.