这篇文章讲清楚:如何在 Proxmox VE(PVE)上把 Ubuntu 24.04 做成可克隆、可注入配置、可被宿主管理的基础通用模板。适用对象是准备批量交付 VM 的运维/平台/研发同学;你会得到一套可复现流程:预装最小通用工具、安装并保证 qemu-guest-agent 开机可用、补齐 Cloud-Init 驱动器并让 PVE 面板可注入用户名/SSH Key/IP、最后做模板泛化清理避免克隆冲突。
1)环境与版本(含适用边界)
1.1 环境信息(示例)
| 项目 | 值 |
|---|---|
| 虚拟化平台 | Proxmox VE(PVE) |
| Guest OS | Ubuntu 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/hostname | PVE 面板可控、克隆即用 | 必须给 VM 添加 CloudInit Drive |
cloud-init(Guest) | 读取注入配置 | 免手工配置网络与账号 | 需要清理 /var/lib/cloud 避免克隆复用 |
qemu-guest-agent | IP 上报、优雅关机等 | 宿主管理更完整 | 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:安装最小通用工具集(运维/排障/脚本常用)。
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使用密码登录
#先设置密码
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 服务可用、基础命令存在。
systemctl status ssh --no-pager
curl --version && git --version && jq --version
chronyc tracking || true
ncdu --version
7zz --help | head -n 1 || true4.2 Guest 内:安装并运行 QEMU Guest Agent
Bash:安装并启动(注意:Ubuntu 24.04 可能无法 enable 属于正常现象)。
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 通道已挂进来)。
ls -l /dev/virtio-ports/4.3 解决“enable 不了”的开机启动(推荐:加启动器服务)
Bash:新增一个 oneshot 启动器,开机检测到 virtio 设备后启动 GA。
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)。
systemctl status pve-qemu-ga-start --no-pager
systemctl status qemu-guest-agent --no-pager4.4 PVE 侧:修复“未找到 CloudInit 驱动器”(核心)
路径 A:WebUI 添加(推荐)
- 关机 VM(Shutdown)
- VM → Hardware → Add → CloudInit Drive
- Storage 选择
local-lvm(或你的系统盘存储) - Hardware 出现类似
ide2: <storage>:cloudinit - VM → Cloud-Init 页面填写:
ciuser/SSH Keys/ipconfig0/DNS - 点 Regenerate Image(重新生成)→ 开机
路径 B:宿主机命令行添加(更可控)
Bash:假设 VMID=101,存储=local-lvm。
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 与扩容工具。
sudo apt install -y cloud-init cloud-guest-utils(可选)限制 datasource,减少无关探测:
cat <<'EOF' | sudo tee /etc/cloud/cloud.cfg.d/99-pve.cfg
datasource_list: [ ConfigDrive, NoCloud ]
EOF验证:查看 cloud-init 是否能识别 datasource(首次克隆启动后更明显)。
cloud-init status --long || true4.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
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.sh4.6.2 创建 systemd 服务(首次开机触发)
INI:写入 /etc/systemd/system/firstboot-growroot.service
[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
启用:
sudo systemctl daemon-reload
sudo systemctl enable firstboot-growroot.service验证(在“克隆机首次开机”后):
systemctl status firstboot-growroot --no-pager || true
lsblk
df -h /4.7(可选)网络策略:避免 netplan 冲突
如果你决定让 PVE Cloud-Init 管网络,建议删除安装器生成的 netplan,避免与 cloud-init 生成的 50-cloud-init.yaml 叠加冲突。
Bash:
sudo rm -f /etc/netplan/00-installer-config.yaml
sudo netplan generate验证:netplan 能正常生成配置(不要在远程断网环境盲 apply)。
sudo netplan generate4.8 封模板前:泛化清理(必须做)
A)清 cloud-init 状态(装了 cloud-init 才做)
B) firstboot 扩容 marker(必须删除,否则克隆不扩容)
C)清 machine-id(避免克隆后同源冲突)
D)清 SSH host keys(让每台克隆机生成自己的)
E)清理缓存(可选)
# 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
关机:
sudo poweroff4.9 PVE 侧:转模板 + 首次克隆验证
PVE 转模板前检查:
- VM → Options → QEMU Guest Agent:Enabled
- VM → Hardware → 有 CloudInit Drive
- Convert to template
克隆一台后在 Guest 内验证:
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)。
sudo sgdisk -e /dev/sda
sudo partprobe /dev/sda5.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 后手动重跑一次:
sudo rm -f /var/lib/firstboot-growroot.done
sudo systemctl restart firstboot-growroot.service5.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;否则可能导致克隆后网络配置不符合预期。