Skip to content

03 · 部署实战 A:ISO 一体机无人值守装机

形态:Harvester OS 一体机。用 KVM/libvirt 起 3 台 UEFI 虚拟机,通过 netboot(vmlinuz + initrd + rootfs squashfs)+ HTTP 托管 config.yaml 完成 v1.8.2 无人值守安装(等效官方 PXE/iPXE 自动安装流程),节点自带 RKE2 + Longhorn + KubeVirt。

实测结果:3 节点全部 Ready control-plane,etcd,etcd 成员 3/3,异常 Pod 0总编排 42 分钟(不含 9.5 GB 介质下载约 44 分钟)。 裸金属部署同理,只需把「libvirt 域定义」换成「PXE/iPXE 服务器 + 物理机引导」。


1. 方案与架构

1.1 为什么用 netboot 而不是挂载 ISO

方式交互性可否全自动说明
挂载 ISO 引导需人工走安装向导即使配 harvester.install.automatic,仍要处理引导菜单
libvirt 直接内核引导(本方案)<kernel> + <initrd> + <cmdline> 直接注入,等效 iPXE 的 kernel/initrd 加载

关键收益:引导参数与装机配置全部由宿主机脚本控制,虚拟机开机即进入自动安装, 装完 poweroff,宿主机再把域定义切成硬盘启动并 autostart——全程零人工

1.2 引导链路(实测)

宿主机 <BASE>/
  boot/    vmlinuz + initrd + rootfs.squashfs + ISO        ← 00-download.sh 拉取
    │ (符号链接)
  http/    python3 -m http.server 绑定 <HTTP_IP>:<PORT>     ← 20-http-server.sh
    │        ├── harvester-v1.8.2-vmlinuz-amd64
    │        ├── harvester-v1.8.2-initrd-amd64
    │        ├── harvester-v1.8.2-rootfs-amd64.squashfs    (root=live:http://…)
    │        ├── harvester-v1.8.2-amd64.iso                (install.iso_url)
    │        └── configs/config-nodeNN.yaml                (install.config_url)

 libvirt 域 harvester-NN(UEFI/OVMF)
    │ ① 直接内核引导 → dracut 取 rootfs → 进入 live 安装环境
    │ ② harvester-installer 读 config_url → 应用网络/分区/装机
    │ ③ elemental install → 格式化 data 盘 → 取 ISO → 预加载镜像 → 写 GRUB
    ▼ poweroff(install.poweroff=true)
 宿主机 60-deploy.sh:切 disk 模式重定义 + virsh autostart + 开机

 Harvester OS 启动 → RKE2 起 → node01 create 出集群;node02/03 用 server_url+token join

 harvester-promote-node-controller 自动把 join 节点提升为 control-plane,etcd

 kube-vip 承载 VIP → Dashboard 可用

1.3 目录结构

<BASE>/                      # 实测为 /mnt/xfs/harvester
├── boot/        # 安装介质实体:vmlinuz / initrd / rootfs squashfs / ISO
├── disks/       # data 盘实体(400GiB qcow2,机械盘)
│                # + os 盘【符号链接】→ NVMe 目录(★ 见 §7 磁盘布局)
├── nvram/       # 每节点 UEFI 变量存储(*_VARS.fd + .bak),保留安装后的 EFI 启动项
├── http/        # HTTP 服务根目录:介质符号链接 + configs/ + images/
│   └── configs/ # config-node01.yaml(create) / config-node02,03.yaml(join)
├── configs/     # 虚拟机 XML(probe/install/disk 三态)、token.txt、credentials.txt(600)
├── logs/        # deploy.log / deploy-all.log / deploy.status / progress.log
│                # / harvester-NN-console.log(+.prev) / download/
├── import/      # 独立的镜像导入工作流(未纳入部署链路)
└── scripts/     # 19 个自动化脚本(见 §5)

http/ 下的介质都是符号链接指向 boot/,因此重下介质不需要重建 HTTP 目录。


2. 内核引导参数(cmdline 逐项)

libvirt 域 XML 的 <os> 段直接指定内核引导(实测原文,单行):

xml
<kernel><BASE>/boot/harvester-v1.8.2-vmlinuz-amd64</kernel>
<initrd><BASE>/boot/harvester-v1.8.2-initrd-amd64</initrd>
<cmdline>ip=dhcp rd.neednet=1 net.ifnames=1 rd.cos.disable rd.noverifyssl
  console=ttyS0,115200n8
  root=live:http://<HTTP_IP>:<PORT>/harvester-v1.8.2-rootfs-amd64.squashfs
  harvester.install.automatic=true
  harvester.install.config_url=http://<HTTP_IP>:<PORT>/configs/config-node01.yaml
  harvester.install.iso_url=http://<HTTP_IP>:<PORT>/harvester-v1.8.2-amd64.iso
  harvester.install.skipchecks=true
  harvester.install.tty=ttyS0,115200n8</cmdline>
<boot dev='hd'/>
参数作用不写会怎样
ip=dhcpdracut 早期网络用 DHCP 取地址initramfs 无网络,拉不到 rootfs
rd.neednet=1强制 initramfs 阶段建网同上(root=live:http:// 依赖它)
net.ifnames=1启用可预测网卡名引导期叫 eth0、安装后叫 enp1s0,配置里的接口名对不上
rd.cos.disable官方 PXE 安装要求的 dracut 参数COS 的 dracut 模块接管,引导流程与官方不一致
rd.noverifyssl不校验 SSLHTTPS 源会失败(本例是 HTTP,照抄官方要求)
console=ttyS0,115200n8内核与 systemd 输出到串口无法录制安装全过程,失败不可复盘
root=live:http://…squashfslive 根文件系统来自 HTTP无根文件系统
harvester.install.automatic=true进入自动安装停在交互式安装向导
harvester.install.config_url=…指定装机配置自动安装无参数可用
harvester.install.iso_url=…装机镜像源(get_iso() 下载 8 GB ISO 到目标盘)无镜像源,装机失败;★ 该 URL 走 cmdline,不写在 config.yaml
harvester.install.skipchecks=true跳过硬件最小要求检查虚拟机(8C/32G/250G)不满足生产门槛,安装被拒
harvester.install.tty=ttyS0,115200n8安装器自身 TTY安装器输出跑到 VNC,串口日志缺失

install.poweroff: true 写在 config.yaml 里而非 cmdline:安装完成后关机而非重启, 避免再次进入网络安装循环;随后由编排器用 disk 模式重定义域并设 autostart


3. 虚拟机定义要点(裸金属可跳到 §3.4)

配置项取值为什么必须这样
<memory>33554432 KiB(32 GiB)Harvester 生产门槛;配合 skipchecks 才能过
<vcpu> / topology8,sockets=1 dies=1 cores=8 threads=1固定拓扑,避免 guest 内 CPU 计数漂移
<cpu mode>host-passthrough + check='none' + migratable='off'migratable='on'过滤掉 vmx → guest 内 KubeVirt 跑不了虚机
<machine>pc-q35-6.2与 libvirt 8.0 兼容的固定机型
<loader> + <nvram>OVMF_CODE_4M.fd(readonly/pflash)+ 每节点独立 nvram/harvester-NN_VARS.fdv1.8 强制 UEFI;NVRAM 必须独立,否则三节点共享 EFI 变量会互相覆盖启动项
<on_poweroff>destroy装机 poweroff 后域进入 shut off,编排器据此判断安装结束
<on_reboot>restart★ 自愈 L3 靠 virsh reboot 重跑装机;netboot 参数不变,重启即重新自动安装
<disk> cachewriteback装机期大量写入(18 GB 镜像预加载),默认 cache 太慢
PCI 地址固定网卡 bus=0x01、os 盘 0x02、data 盘 0x03★ 保证 guest 内命名确定为 enp1s0/vda/vdb,装机配置才能写死设备名
<serial> / <console>type='pty'type='file' 会被 libvirt 置为 root:root 600,普通用户读不到日志
<rng model='virtio'>backend /dev/urandom装机期大量加密运算,缺熵会显著拖慢
<graphics type='vnc'>port='-1' autoport='yes' listen='0.0.0.0'兜底人工介入:virsh vncdisplay harvester-NN

3.1 三态域定义

模式用途特征
probe探测引导:确认 guest 内真实网卡名/盘名rd.debug不带 root=(不落盘、不装机)
installnetboot 自动装机注入 kernel/initrd/cmdline
disk装完后从硬盘正常启动移除引导注入,只留 <boot dev='hd'/>
bash
# 网卡名不要猜:先跑一次探测引导,从串口日志实测得到设备名再写进装机配置
bash scripts/40-define-vm.sh 01 probe
bash scripts/30-console-capture.sh 01 start && virsh start harvester-01
grep -oE 'enp[0-9a-z]+|vd[ab]' logs/harvester-01-console.log | sort -u   # → enp1s0 / vda / vdb
virsh destroy harvester-01

3.2 libvirt 8.0 兼容性三连(实测)

现象根因修复
virsh define 报 XML 不合法libvirt 8.0 不支持 driver discard='unpin'<disk><driver> 移除该属性
cannot undefine domain with nvramUEFI 域有独立 NVRAM 文件virsh undefine harvester-NN --keep-nvram
domain 'harvester-01' already exists with uuid …未先 undefine 就重复 define脚本内先 undefine --keep-nvramdefine

3.3 串口日志录制(可复盘的前提)

bash
bash scripts/30-console-capture.sh 01 start    # virsh console + pty + script 后台录制
tail -f logs/harvester-01-console.log          # 实测装机 8 万+ 行完整留存
bash scripts/30-console-capture.sh 01 status

两个坑(均已修):① 只杀 virsh console 子进程没杀重连循环; ② : > log覆盖失败现场 → 改为轮转成 .prev

3.4 裸金属怎么做(对照表)

本方案的 VM 手段裸金属等价物
libvirt <kernel>/<initrd>/<cmdline>PXE/iPXE 菜单项(kernel/initrd/append 同参数)
python3 -m http.server 托管介质内网 HTTP/Nginx(或 iPXE 脚本 + TFTP)
DHCP 静态保留(MAC→IP→主机名)机房 DHCP 保留 + 交换机端口规划
virsh destroy/startIPMI/BMC 电源控制(ipmitool power cycle
virsh console 录制BMC SOL(Serial-over-LAN)录制
自愈 L3 virsh rebootipmitool chassis power cycle
PCI 地址固定保证设备名hwAddr(MAC)匹配网卡,不要写死内核设备名

4. 装机配置(config-nodeNN.yaml)逐字段说明

50-gen-configs.sh 生成,通过 HTTP 提供给安装器(cmdline 的 config_url)。 下面是实测可用的 config-node01.yaml(create)全文骨架:

yaml
scheme_version: 1                # v1.8.2 为 1;★ 不写会被安装器拒绝

os:
  hostname: harvester-01
  password: "xxx"           # ★ OS/SSH(rancher 用户)密码,【不是】dashboard 密码
  ssh_authorized_keys:
    - "<宿主机公钥>"              # 注入后 rancher 用户免密 SSH,编排器全靠它
  ntp_servers:
    - ntp.aliyun.com             # 内网优先,其次公共池;时钟漂移会影响 etcd/TLS
    - ntp1.aliyun.com
  modules:
    - kvm                        # ★ guest 内跑 VM 必需;配合域定义 migratable='off'
  dns_nameservers:
    - 192.168.150.1              # ★ 只写宿主 dnsmasq 一个(详见 4.5 与第 07 章)

install:
  device: /dev/vda               # 系统盘:ESP / COS_STATE / COS_RECOVERY / COS_PERSISTENT
  mode: create                   # 首节点 create;node02/03 为 join
  automatic: true                # 无人值守
  management_interface:
    interfaces:
      - name: enp1s0             # ★ 实测名,由 PCI 地址 0x01 固定推导,不要猜
        hwAddr: "52:54:00:xx:xx:11"   # ★ 必须与域 XML/物理网卡 MAC 完全一致
    method: dhcp
  vip: 192.168.150.200           # ★ 仅 create 节点
  vip_mode: static               # ★ 仅 create 节点
  poweroff: true                 # 装完关机,避免再次进入网络安装循环
  skipchecks: true               # 虚拟机不满足生产硬件门槛时必须
  tty: ttyS0,115200n8            # 安装器自身 TTY,串口可观测
  # iso_url: http://192.168.150.1:8080/harvester-v1.8.2-amd64.iso   ← 已在 cmdline 传入

system_settings:
  auto-disk-provision-paths: ""  # ★ 关闭自动纳管,避免误把 /dev/vda 交给 Longhorn
  overcommit-config: ""          # 资源超分(CPU/内存/存储),留空=默认
  cluster-dns: ""                # 集群 DNS 域
  vm-force-reset-policy: ""      # VM 强制重置策略
  harvester-log-level: info      # ★ 排障期可临时调 debug
  ssl-certificates: {}           # 自定义证书
  ssl-parameters: {}             # TLS 参数(如最小版本/套件)
  additional-ca: ""              # 追加 CA(私仓/内网 CA 常用)
  ui-source: auto                # UI 资源来源:auto / bundled / external
  ui-index: ""                   # 外部 UI 索引
  containerd-registry: {}        # ★ 私有镜像仓库配置(声明源,见 4.6)
  support-bundle-timeout: ""     # support bundle 收集超时
  csi-driver-config: {}          # CSI 驱动参数
  default-storage-class: ""      # 默认 StorageClass(留空=harvester-longhorn)
  harvester-csi-ccm-versions: "" # CSI/CCM 镜像版本

token: <RKE2 加入令牌>            # ★ 顶层键;三节点必须一致;【不是】Dashboard API token
server_url: https://192.168.150.200:6443   # ★ 顶层键,仅 join 节点需要

4.1 create 与 join 的差异(只有 4 处)

字段node01(create)node02/03(join)
install.modecreatejoin
server_url无(自己就是 server)https://<VIP>:6443(★ 端口是 6443,不是 443)
install.vip / install.vip_mode<VIP> / static不写
os.hostname / hwAddr各自的值各自的值
token三节点完全一致三节点完全一致

4.2 字段位置的五个易错点

  1. server_url 是顶层键,写成 install.server_url 会被静默忽略 → join 节点找不到集群; 且端口必须是 6443(RKE2 API),写 443 会连到 ingress 而非 API Server。
  2. token 也是顶层键,不是 install.token
  3. hwAddr 必须与 MAC 完全一致(大小写/冒号格式),Harvester 靠它挑管理网卡; 写错会导致 bond mgmt-bo / bridge mgmt-br 建在错误接口上。
  4. interfaces[].name 必须是实测名,可预测网卡名由 PCI 拓扑推导(见 §3 的 PCI 固定)。
  5. iso_url 走 cmdline,不必写进 config.yaml(两处都写不冲突,但 cmdline 优先)。

4.3 ★ 装机配置不能设定 Dashboard 密码(实测证明)

rootfs.squashfs 内的 /usr/bin/harvester-installer 做字符串统计:

字符串出现次数结论
scheme_version1对照组:证明该二进制确实解析装机配置,统计方法有效
admin_password0无此配置能力
admin_token0无此配置能力
server_url0由其他组件处理,不在 installer 二进制内

所以全新集群的 Dashboard 初始状态只能是引导密码 xxx + mustChangePassword=xxx 必须由脚本在装完后补一次首登改密(75-set-admin-password.sh`,幂等)。 完整根因链见 02 快速上手 §4。

💡 方法要点:做「不存在」的证明时必须设对照组。 若只统计 admin_password=xxx scheme_version=1` 恰好排除了这种可能。

4.4 VIP 选址规则

规则原因
必须落在 DHCP 动态池之外池(实测 .100-.199)内的地址会被动态分配,导致 VIP 冲突、kube-vip 抢地址
必须与管理网同网段vip_mode: static 走二层 ARP 通告
前置检查用 arpingarping -I virbr10 <VIP> 无应答才算空闲;curl -sk https://<VIP>/ping 返回 pong 则说明已有集群在跑(本次为重跑/续跑,不是冲突)

4.5 ★ DNS 只写一个上游(否则集群会「间歇性抽风」)

dns_nameservers不要并列公网 DNS。根因链(实测,最隐蔽的一个故障):

节点 /etc/resolv.conf 并列 192.168.150.1(宿主 dnsmasq)+ 223.5.5.5(公网)
  → Harvester 的 CoreDNS 是 forward . /etc/resolv.conf,默认 policy=random
  → 每次查询【随机】选一个上游
  → 直查 CoreDNS 20 次:10 次返回内网 IP,10 次返回公网 IP
  → cattle-cluster-agent 连 wss://<域名>/v3/connect/register 时拿到公网文档站首页 HTML
  → websocket: bad handshake,Rancher 侧集群 state=error
  → CoreDNS cache 30 + 上游 TTL 过期后自愈 → 表现为【反复抖动】而非持续失败

定位手段(把「随机」变成可统计的分布,而不是多试几次碰运气):

bash
for i in $(seq 1 20); do dig +short <内网域> @10.53.0.10 | tr '\n' ' '; echo; done \
  | sort | uniq -c
# 期望:20 行全部是内网 IP;出现公网 IP 即命中

修复三处缺一不可

#动作命令
1节点 DNS 收敛为只用宿主 dnsmasqnmcli con mod bridge-mgmt ipv4.dns 192.168.150.1 + nmcli device reapply mgmt-br(★ 用 device reapply不是 con up,不断链)
2重启 CoreDNSkubectl -n kube-system rollout restart deploy/rke2-coredns-rke2-coredns(★ forward 插件只在启动时/etc/resolv.conf
3装机配置同步收敛50-gen-configs.shconfig-node0*.yamldns_nameservers 只留内网 DNS(原文件备份 *.bak-dns),否则重装后又引入随机上游

去掉公网 DNS 无副作用:宿主 dnsmasq 自身会转发公网域名(实测 www.baidu.com 正常解析)。 完整网络专题见 07 网络实战。

4.6 ★ containerd-registry:改声明源,不要改被渲染的产物

Harvester OS 是不可变系统/etc/rancher/rke2/registries.yaml 由控制器按 settings.harvesterhci.io/containerd-registry 渲染。实测:手工写入 167 B, 约 1 分钟后被清成 0 字节

bash
# ❌ 手写文件(会被回收)
# ✅ 改设置(value 是一段 JSON 字符串)
kubectl patch settings.harvesterhci.io containerd-registry --type merge \
  --patch-file /tmp/containerd-registry.patch.json
json
{"mirrors":{"<reg>":{"endpoints":["http://<reg>"]}},
 "configs":{"<reg>":{"tls":{"insecureSkipVerify":true}}}}
要点说明
字段名怎么确定的无官方文档时,从 /usr/bin/rancherd 二进制的 struct tag 反查mirrors.<reg>.endpoints[] + configs.<reg>.tls.insecureSkipVerify;渲染到 rke2 时转成 endpoint / insecure_skip_verify
为什么必须显式声明 httpcontainerd 默认按 HTTPS 访问 registry;纯 HTTP 私仓会报 http: server gave HTTP response to HTTPS client
生效验证设置生效后 3 节点 registries.yaml 均为 266 B;rke2 随即重写 certs.d,文件头变成 # File generated by rke2. DO NOT EDIT.crictl pull 成功
通用铁律在有控制器管文件的系统里,永远改声明源(CRD/设置),不要改被渲染的产物;产物文件头的 DO NOT EDIT 就是明确提示

5. 脚本资产清单与编排状态机

scripts/19 个脚本 + 1 个一键入口(编号 = 阶段序,9x 为自愈/运维层):

编号脚本作用幂等
run-all.sh★ 一键入口:前置检查 + 按序调用下列脚本,自身不含任何新部署逻辑
0000-download.sh拉取 4 个介质到 boot/(9.5 GB,断点续传),建 http/ 符号链接
1010-prepare.sh磁盘/NVRAM/HTTP 符号链接/DHCP 静态保留;建目录树,credentials.txt = 600
2020-http-server.sh启停查 python3 -m http.server192.168.150.1:8080start/stop/status
3030-console-capture.sh串口录制(pty + script),日志轮转 .prevstart/stop/status
4040-define-vm.sh生成并 define 三态域定义 probe/install/disk
5050-gen-configs.sh渲染 config-nodeNN.yaml(create/join 差异)+ token.txt + credentials.txt
6060-deploy.sh单节点编排:装机 → 等 poweroff → 切盘 → autostart → 等 Ready
6161-autostart-after-iso.sh等 ISO 下完自动接力 node01(长下载期间的自动衔接)
6565-deploy-all.sh总编排:node01 + node02/03 + 改密 + 验证 + 自愈挂载
7070-verify.sh9 段验证(见 §10),输出可直接粘贴的结论
7575-set-admin-password.sh★ Dashboard 首登改密(幂等短路,见 02 §4.2)
8080-smoke-vm.sh端到端测试 VM(create/clean);⚠️ 当前受已知问题阻塞⚠️
9090-progress.sh30 s 粒度进度记录到 logs/progress.log
9595-rescue-image-preload.sh自愈 L2 宿主侧:探测并推送救援体
9595-rescue-image-preload-guest.sh自愈 L2 救援体(guest 内执行)
9696-install-retry.sh自愈 L3:安装器卡死自动 virsh reboot 重试✅(限流)
9797-installer-netfix.sh自愈 L1:抢注入 nm-online 兜底包装
9898-nm-online-wrapper.sh自愈 L1:被注入的包装体
9999-rancher-internal-net.sh可选:接入内网 Rancher(A~F 六步,见 09)

一键入口与参数:

bash
bash scripts/run-all.sh --preflight                 # 只做前置检查后退出(不改状态)
setsid bash scripts/run-all.sh &lt;/dev/null &gt;/tmp/run-all.log 2>&1 &
bash scripts/run-all.sh --skip-download             # 跳过 9.5 GB 介质下载
bash scripts/run-all.sh --from configs              # 中断后从指定阶段续跑
# 退出码:0 成功 | 1 前置检查/参数错误 | 2 某阶段失败

setsid + </dev/null 是必需的:编排耗时 42 分钟,SSH 断线会把它带走; 且 60-deploy.sh65-deploy-all.sh 各只能有一个实例pgrep -af '60-deplo[y]|65-deplo[y]' 核对)。

5.1 编排阶段与前置检查

阶段顺序(固定)

preflight → download → prepare → http → configs → node01 → all → verify
                                                   │        └─ 65-deploy-all.sh:node02/03 + 改密 + 验证 + 自愈
                                                   └─ 60-deploy.sh:create 节点单独跑

每步写 logs/progress.log(30 s 粒度)与 logs/deploy.status(当前阶段),便于复盘「卡在哪一段」。

--preflight 的 8 项检查(全部来自实测踩坑,只读不改状态)

#检查不满足的后果
1os 盘实体在 NVMeOS_DISK_DIR/mnt/xfs 不同设备)镜像预加载 > 30 min → 装机永久卡死(§7)
2VIP 在 DHCP 池之外地址冲突,Dashboard 时通时断
3/mnt/xfs 剩余空间 > 2 TBdata 盘 qcow2 写满,Longhorn 卷损坏
4NVMe 剩余空间 > 100 GiBos 盘 qcow2 增长失败
5virbr10 存在且 activeVM 无网络
6HTTP 端口未被占用引导服务起不来
7OVMF_CODE_4M.fd 存在UEFI 无法引导(v1.8 强制 UEFI)
8nc / sshpass 已安装编排器探活与自动登录失败

5.2 ★ ensure_booted_from_disk:装机后切盘开机的分支判定

装机 poweroff 后,域可能停在多种状态。65-deploy-all.sh 每约 30 s 调一次 ensure_booted_from_disk,判定失败就 virsh reboot。分支必须覆盖全:

域状态域定义判定动作
shut off仍含 <kernel>(install 模式)装机刚完成重定义为 diskautostartvirsh start
shut off已是 disk 模式曾被打断仍要 virsh start(★ 见下方缺陷 B)
running仍含 <kernel>还在网络装机不干预,继续等;超时才 destroy 重来
running<kernel>已从硬盘运行return 0,进入 wait_node_ready

实测修掉的两个缺陷(都让编排器「以为成功了」):

缺陷旧代码事故修复
A · dumpxml 偶发返回空if ! virsh dumpxml … | grep -q '<kernel>'空输出也命中「无 kernel」→ 误报 running from disk跳过全部恢复动作,后续节点再没被推进先把 XML 存变量并判空,空则记日志继续等,绝不据此下结论
B · shut off + 已是 disk 定义时不开机只打印 already defined for disk bootreturn 0节点永远不会开机,而调用方以为成功补上 virsh start,并保留失败重试 || { log …; sleep 10; continue; }

💡 通用教训:任何「返回成功」的分支都必须对应一个真实发生的动作。 「什么都不做」不能算成功。

5.3 ★ 节点就绪判定:既不能用子串,也不能过度精确

bash
# ❌ 子串匹配:NotReady 里也含 "Ready"
kctl get nodes --no-headers | grep "harvester-${n}.*Ready"

# ❌ 过度精确:漏掉合法就绪态 Ready,SchedulingDisabled(角色自动提升期)
awk '$2=="Ready"'

# ✅ 精确节点名 + 锚定前缀
kctl get nodes --no-headers 2>/dev/null \
  | awk -v n="harvester-${n}" '$1==n && $2 ~ /^Ready(,|$)/ {f=1} END{exit !f}'
节点状态应判定grep Ready$2=="Ready"^Ready(,|$)
Ready✅ 就绪
Ready,SchedulingDisabled✅ 就绪(提升期)❌ 漏判
NotReady❌ 未就绪误判
NotReady,SchedulingDisabled❌ 未就绪误判

Ready,SchedulingDisabled + 角色 <none>Harvester 自动提升节点为 control-plane,etcd 期间的正常中间态harvester-promote-node-controller 行为),不要手工 uncordon,会干扰控制器。 也因此装机配置里 install.role 留空即可——3 节点场景全靠自动提升拿到正确角色。 曾因这个误判,70-verify.sh 在 node03 还没入集群时就跑完了。


6. 完整实施流程(阶段 0 → 6,含验收点)

阶段 0 · 环境准备(一次性,约 10 分钟)

bash
# 宿主机(Ubuntu 22.04 / libvirt 8.0)
sudo apt install -y qemu-kvm libvirt-daemon-system virtinst dnsmasq \
                    python3 curl jq ncurses-bin sshpass netcat-openbsd isoinfo
sudo usermod -aG libvirt,kvm $USER && newgrp libvirt

# libvirt 网络:独立的 virbr10 + NAT + DHCP 池(.100-.199)
sudo virsh net-define configs/harvester-net.xml && sudo virsh net-start virbr10
sudo virsh net-autostart virbr10
virsh net-dhcp-leases virbr10                # 验收:能列出三个静态保留

# UEFI 固件
ls /usr/share/OVMF/OVMF_CODE_4M.fd           # 验收:存在

验收点virsh net-info virbr10active: yes / autostart: yes;OVMF 固件存在。

阶段 1 · 介质与磁盘(44 分钟下载 / 建盘秒级)

bash
bash scripts/00-download.sh                              # 9.5 GB,断点续传
NVME_DISK_DIR=/home/$USER/harvester-disks bash scripts/10-prepare.sh
bash scripts/20-http-server.sh && bash scripts/20-http-server.sh status

# 验收
ls -l boot/                                              # 4 个文件,字节数见 02 章 §2.1
readlink -f disks/harvester-01-os.qcow2                  # ★ 必须指向 NVMe 目录
curl -sI http://192.168.150.1:8080/configs/config-node01.yaml | head -1   # 200

阶段 2 · 装机配置与探测引导(5 分钟)

bash
bash scripts/50-gen-configs.sh                           # config-nodeNN.yaml + token.txt + credentials.txt
grep -h 'mode:' http/configs/*.yaml                      # 验收:create ×1,join ×2
diff <(grep -v 'hostname\|hwAddr\|mode\|server_url\|vip' http/configs/config-node01.yaml) \
     <(grep -v 'hostname\|hwAddr\|mode\|server_url\|vip' http/configs/config-node02.yaml)   # 应只差这几行

# 探测引导:实测设备名,不要猜
bash scripts/40-define-vm.sh 01 probe
bash scripts/30-console-capture.sh 01 start && virsh start harvester-01
grep -oE 'enp[0-9a-z]+|vd[ab]' logs/harvester-01-console.log | sort -u    # → enp1s0 / vda / vdb
virsh destroy harvester-01

阶段 3 · 部署 node01(create,约 14 分钟)

bash
setsid bash scripts/60-deploy.sh node01 </dev/null >/dev/null 2>&1 &
tail -f logs/harvester-01-console.log

关键里程碑(串口日志里能逐条对上):

里程碑关键字说明
dracut 取 rootfsroot=live:http://…initramfs 阶段建网成功
安装器读配置config_url拿到 config-node01.yaml
elemental 落盘elemental install写 COS_STATE / COS_RECOVERY
数据盘格式化do_data_disk_format/dev/vdb → Longhorn
取 ISOget_iso8 GB 下载到目标盘(★ 磁盘性能敏感,见 §7)
镜像预加载ctr-check-images.sh180+ 个镜像导入(★ 30 分钟死线
GRUB 收尾update_grub_settings/oem/grubenvgrubcustomthird_party_kernel_args
保存日志save_installation_log装机日志落盘
关机powered off after install (~1006s)install.poweroff=true 生效

⚠️ 若在 do_preload 处失败,GRUB 补丁与安装日志都不会做, 即使 /oem 里有配置也只能重装(缺 update_grub_settings 会在启动时卡约 30 分钟找 grub 文件)。

阶段 4 · 部署 node02 / node03(join,串行,约 14 分钟/节点)

bash
setsid bash scripts/65-deploy-all.sh </dev/null >/dev/null 2>&1 &
pgrep -af '60-deplo[y]|65-deplo[y]'                      # 验收:各只有一个实例
tail -f logs/deploy-all.log

必须串行:并行 join 会同时抢 etcd 成员加入,导致 quorum 抖动。

阶段 5 · 集群收敛与首登改密(约 5 分钟)

bash
K='sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml'
H='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null rancher@192.168.150.11'
$H "$K get nodes -o wide"                                # 验收:3 行 Ready control-plane,etcd
$H "$K -n kube-system get pods | grep '^etcd-'"          # 验收:3 个 etcd-harvester-0N Running
bash scripts/75-set-admin-password.sh; echo rc=$?        # 验收:rc=0

阶段 6 · 验证与收尾

bash
bash scripts/70-verify.sh                                # 9 段体检,见 §10
bash scripts/80-smoke-vm.sh create                       # 端到端 VM(⚠️ 见 §11.3 已知问题)

7. ★ 磁盘布局:机械盘会让装机永久卡死(本方案最大的坑)

7.1 为什么 OS 盘必须放 NVMe

harv-installget_iso()mktemp -p ${TARGET}/usr/local8 GB ISO 下载到目标盘, 随后又从同一块盘读回 tar 包写入 containerd 内容库——读写同轴争抢

而 chroot 内那个「引导用 rke2 server」(只为借它内嵌的 containerd 导入镜像)有一条 约 30 分钟的自杀死线

chroot 内没有 systemd
  → kubelet: "Failed to create cgroup: systemd not running on this host" 每 5s 失败
  → static pods 永远同步不了
  → 约 30 分钟后 rke2 自杀:
     level=fatal "Failed waiting for static pods to sync: context deadline exceeded"
  → containerd 随之消失
  → /usr/sbin/ctr-check-images.sh 是 while true 无上限重试 → 永久卡死

实测时间线(OS 盘在 5400 rpm 机械盘上)

时刻事件
12:57引导用 rke2 启动(30 分钟死线开始计时)
13:25最后一个列表 rke2-images.linux-amd64-v1.35.7-rke2r1.txt 才开始核对(前 4 个列表共 180 个镜像已导入
13:27:59rke2 自杀 → containerd 消失 → 永久卡死

预加载耗时约 34 分钟,刚好越过死线 → node01 装机 TIMEOUT 40 分钟

⚠️ 易误判点:串口每 2 秒刷两行 ctr: failed to list images: … containerd.sock: connect: connection refused + level=warning msg="Failed to check deprecations"。 后者只是症状,真因是 containerd/socket 没了;只盯这行 warning 会一路查错方向。

7.2 修复:实体迁 NVMe + 符号链接兼容既有路径

/home/<user>/harvester-disks/harvester-NN-os.qcow2      ← 实体(NVMe,宿主根文件系统)
/mnt/xfs/harvester/disks/harvester-NN-os.qcow2          ← 符号链接(★ 兼容所有既有脚本路径)
/mnt/xfs/harvester/disks/harvester-NN-data.qcow2        ← 实体仍留机械盘
设计选择理由
OS 盘实体在 NVMe消除读写同轴争抢
符号链接而非改脚本路径所有既有脚本/XML/日志路径无需改动,回滚容易
data 盘故意留机械盘400 GiB × 3 节点 × Longhorn 三副本,放 NVMe 会撑爆宿主根分区
SELinux 未启用无需 semanage/chcon;若启用则须处理镜像标签

效果:ISO 下载 ~95 MB/s(原 55–70 MB/s),预加载从 ~34 分钟降到数分钟; 修复后 node02 实测 powered off after install (~1006s),node01/03 同级。

7.3 兜底:镜像预加载救援(自愈 L2)

95-rescue-image-preload.sh 已挂进 60-deploy.sh: wait_poweroff65-deploy-all.sh: ensure_booted_from_disk,每 3 轮(约 30 s)探测一次,命中即推送救援体, 并写 logs/rescue-NN.done 防重复。救援体(guest 内)做四件事:

  1. 用与 harv-install 完全相同的参数重启 containerd (-c …/config.toml -a /run/k3s/containerd/containerd.sock --state /run/k3s/containerd --root /var/lib/rancher/rke2/agent/containerd) → 已导入的 18 GB 内容库立即可见(参数不一致会看到空库,前功尽弃);
  2. 逐列表比对,缺哪个就 zstd -d + ctr -n k8s.io images import --no-unpack 补哪个;
  3. 起一个名为 rke2 的诱饵进程cp /bin/sleep /tmp/rke2 后运行),让收尾的 pkill rke2 返回 0 ——★ 该 heredoc 在 set -e 区块内,返回 1 会直接中止安装, 且 do_preload 之后的 update_grub_settings / save_installation_log 都不会执行,只能重装;
  4. 核对一结束就停 containerd(连续 0.6 s 看不到核对进程),否则后续 umount proc / umount ${TARGET}/${RKE2_IMAGES_DIR} 会因 EBUSYset -e 下失败。
bash
bash scripts/95-rescue-image-preload.sh 01 run    # 推送并执行救援
bash scripts/95-rescue-image-preload.sh 01 log    # 看 guest 内 /tmp/rescue-image-preload.log

💡 通用教训:set -e 的脚本里,任何「清理类」命令(pkill/umount/rm)都必须显式容错。 上游把 pkill rke2 写在 set -e 区块里,是只在「rke2 已死」这条罕见路径上才暴露的缺陷。


8. 耗时基线与判卡阈值

健康一轮实测(08:03:50 → 08:45:08):

阶段实测耗时日志依据
node01(已装完)从硬盘运行 → ssh up~10 sharvester-01: ssh up (~10s)
VIP 响应~10 sVIP https://<VIP> responding (~10s)
node01 Ready~10 sharvester-01: Ready (~10s)
node02 netboot 装机 → poweroff~1006 s(16.8 分钟)powered off after install (~1006s)
node02 硬盘启动 → ssh up~50 sharvester-02 (…12): ssh is up (~50s)
node02 → Ready~50 sharvester-02 is Ready (~50s)
node03 装机 → Ready 全程~22 分钟08:22:33 → 08:44:57
三节点总编排(node01 已就绪起算)42 分钟=== ALL DONE === 08:45:08
介质下载 9.5 GB~44 分钟20:03 → 20:47

判卡阈值(可直接抄进监控)

现象阈值动作
单节点装机未 powered off> 25 分钟介入看串口日志
同上> 30 分钟基本可断定踩中预加载死线(§7)→ 跑 L2 救援
串口日志静默≥ 60 s 且干活进程全不在L3 自动 virsh reboot
节点 NotReady> 10 分钟查 kubelet / containerd / CNI

失败样本对照(均为修复前的真实记录)

时间事件耗时根因
09-05 20:49 → 21:29node01 装机 TIMEOUT40 分钟OS 盘在机械盘 → 预加载 ~34 分钟越线(§7)
09-05 23:00 → 09-06 00:31node02 装机 TIMEOUT90 分钟nm-online 亚秒竞态致安装器报错卡死(§9 L1);期间还被误触发一次救援
09-06 08:03 → 08:45三节点全部成功42 分钟OS 盘迁 NVMe + L1 注入 + L3 自动重试后

9. ★ 三层自愈机制(无人值守的关键)

无人值守不是「写个 while 循环等」,而是给每一类已知故障配一个判据明确、防误伤的自动修复。 本方案落地了三层,全部由编排器每轮调用:

脚本触发判据(全部满足动作防误伤设计
L1 网络竞态97-installer-netfix.sh + 98-nm-online-wrapper.shSSH 一可用就开始(开机 +47~55 s),从 virsh start 起每 0.5 s 轮询把兜底 nm-online 包装注入 live 环境的 /usr/local/bin//usr/sbin/三次 SSH 往返压成一次(实测 1.4 s 完成注入);没赶上由 L3 兜底
L2 预加载卡死95-rescue-image-preload.sh + -guest.sh/run/cos/target 已挂载 + images-lists 存在 + ctr-check-images.sh 已跑 >300 s + containerd 不在 + 真正的引导用 rke2 不在①用与 harv-install 完全相同的参数重启 containerd ②逐列表比对,缺哪个就 zstd -d + ctr images import --no-unpack ③起名为 rke2诱饵进程让收尾的 pkill rke2 返回 0 ④核对结束就停 containerd模式串一律用 [x] 括号写法避免 pgrep -f 自匹配mkdir 原子锁防两个看门狗同秒重复救援;guest 侧前置校验不满足就什么都不做直接退出
L3 安装器卡死96-install-retry.shlevel=error 出现在最后 5 行非空行内 ②harv-install/elemental/ctr-check-images/rsync/mkfs/qemu-img/parted 等干活进程全都不在 ③日志静默 ≥60 svirsh reboot(域定义 <on_reboot>restart</on_reboot> + netboot 参数不变 → 自动安装重跑)每节点 ≤3 次、两次间隔 ≥120 s;重启后重新调 L1 注入(live 环境是内存 overlay,重启即失效)

9.1 L1 为什么必须「抢时间」:一条亚秒级竞态

node02 曾卡死 8 小时,真因是 nm-online 的亚秒级竞态。guest 内 /var/log/console.log 时间戳:

15:02:04.938  manager: disable requested -> NetworkManager state is now ASLEEP
15:02:05      level=error msg="exit status 1\rConnecting...............   30s [offline]\n"
15:02:05.31   dhcp4 (mgmt-br): activation: beginning transaction
15:02:05.44   device (mgmt-br): Activation: successful, device activated
15:02:06.90   manager: NetworkManager state is now CONNECTED_GLOBAL

网络其实 0.5 秒后就完全就绪,安装器却已报错并永久停在错误界面harvester-installer / start-installer.sh 两个进程会一直挂着,磁盘一个字节都没写)。

💡 找竞态只能靠时间戳对齐到毫秒。两个曾经把排查带偏的地方: ① 看到 Connecting... 30s [offline] 就以为网络真的不通(其实 0.5 s 后就通了); ② 看到进程还在就以为安装器还活着(其实是僵尸等待)。

9.2 ★ L3 的判据绝不能用「日志里存在 level=error

健康安装也有无害错误。实测 node02 就有:

unable to determine NIC speed from /sys/class/net/enp1s0/speed (got -1)

所以判据必须是三条件同时满足(错误在末尾 + 干活进程全无 + 静默超时)。 该判据已做双向验证:对健康安装中的节点返回 1(不触发);对真失败的归档日志能命中。

💡 判据必须双向验证:既要证明「该触发时触发」,也要证明「不该触发时不触发」。 只验证前者,等于给自己埋一颗误杀健康节点的雷。

9.3 pgrep -f 经 SSH 执行会自匹配(误救援事故)

bash
pgrep -f 'ctr-check-images.sh'        # ❌ 这条命令自己的命令行里含该串 → 恒命中
pgrep -f '[c]tr-check-images.sh'      # ✅ 括号写法,正则匹配但命令行字面量不含原串

实测曾因此误触发一次救援,把健康安装搞坏。所有自愈脚本的模式串统一用 [x] 括号写法。

9.4 编排器的静默 || exit 1

bash
ensure_booted_from_disk "$n" || exit 1     # ❌ 一超时整个脚本无声退出,日志里看不到原因
if ! ensure_booted_from_disk "$n"; then    # ✅ 显式分支 + 记日志 + 继续/告警
  log "harvester-$n: boot-from-disk failed"; 
fi

💡 远程执行不要吞 stderrset -e 环境下的清理命令(pkill/umount/rm)必须显式容错。


10. 部署验证

10.1 70-verify.sh 的 9 个检查段

#检查段判据
1虚拟机状态(libvirt)3 个域 running,且 Autostart: enable
2VIP 连通性 / Dashboardarp 有条目;/ping200(响应体 pong);/dashboard/200;能取到 <title>
3节点kubectl get nodes -o wide 三行且 STATUS 匹配 ^Ready(,|$)
4Harvester 版本 / 节点角色harvesterhci.io/node-role 标签、osImagekubeletVersion
5系统 Pod 异常统计Running/Completed/Succeeded 的行应为
6Longhorn 存储nodes.longhorn.ioReadydisksget sc 含默认 harvester-longhorn
7Harvester 管理网络vlanconfigsnetworks.harvesterhci.io
8各节点登录验证三个 IP 均 OK <hostname> <kernel>(密钥认证)
9凭据打印 configs/credentials.txt

10.2 手工深度核对(实测通过的命令)

bash
K='sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml'
H='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null rancher@192.168.150.11'

# ① 三节点角色与 etcd quorum(HA 的硬指标)
$H "$K get nodes -o wide"
$H "$K -n kube-system get pods | grep '^etcd-'"          # 期望 3 个 etcd-harvester-0N Running

# ② kube-vip 是否三节点都有
$H "$K -n kube-system get pods -o wide | grep kube-vip"

# ③ Longhorn 默认盘是否可调度(虚机存储的地基)
$H "$K -n longhorn-system get nodes.longhorn.io -o json | \
    jq -r '.items[] | .metadata.name as \$n | .spec.disks | to_entries[] |
           \"\(\$n) \(.key) schedulable=\(.value.schedulable)\"'"

# ④ Dashboard 凭据是否【真的】可用(不能只看 HTTP 200)
curl -sk -o /dev/null -w '%{http_code}\n' https://192.168.150.200/ping        # 200 + pong
bash scripts/75-set-admin-password.sh                                          # 幂等,短路即 OK

实测健康基线

harvester-01 Ready control-plane,etcd
harvester-02 Ready control-plane,etcd
harvester-03 Ready control-plane,etcd
etcd 成员: 3      异常 Pod: 0

⚠️ HTTP 200 ≠ 能登录。验收必须包含一次真实的登录调用(75 号脚本短路即代表凭据可用)。 详见 02 快速上手 §4。

10.3 验收清单(可直接抄)

  • [ ] virsh list --all → 3 个 runningvirsh dominfo harvester-0N | grep Autostartenable
  • [ ] curl -sk https://<VIP>/ping200 且响应体 pong
  • [ ] kubectl get nodes → 3 行 Ready control-plane,etcd
  • [ ] kubectl -n kube-system get pods | grep '^etcd-' → 3 个 Running(etcd quorum
  • [ ] kubectl -n kube-system get pods -o wide | grep kube-vip → 3 节点各 1 个
  • [ ] kubectl get sc → 含默认 harvester-longhorn
  • [ ] kubectl -A get pods | grep -vE 'Running|Completed|Succeeded'
  • [ ] bash scripts/75-set-admin-password.sh; echo rc=$?rc=0(Dashboard 凭据可用)
  • [ ] 三节点 ssh rancher@… 密钥免密可登录

11. 参数定制与已知问题

11.1 可参数化项

环境变量默认说明
BASE/mnt/xfs/harvester项目根目录
VIP192.168.150.200管理 VIP(★ 必须在 DHCP 池之外)
HTTP_IP / HTTP_PORT192.168.150.1 / 8080引导服务监听
OS_DISK_DIR$HOME/harvester-disks★ os 盘实体目录,必须在 NVMe

脚本内还有 5 处硬编码点(BASE / 网段 / VIP / HTTP / 节点名),迁移时批量替换后必须 bash -n 语法检查:

bash
find scripts/ -name '*.sh' -exec sed -i \
  -e 's#/mnt/xfs/harvester#/data/harvester#g' \
  -e 's/192\.168\.150\./10.0.99./g' {} +
grep -rn '192\.168\.150\|/mnt/xfs/harvester' scripts/ | head    # 验收:应为空
for f in scripts/*.sh; do bash -n "$f" || echo "语法错误: $f"; done   # ★ 改完必须语法检查

11.2 换网段的连带影响

改了网段还必须改
节点 IP .11/.12/.13dnsmasq 静态保留(virsh net-update … dhcp-host
VIPconfig-node01.yamlinstall.vip;node02/03 的 server_url
HTTP_IPcmdline 的 config_url / iso_url20-http-server.sh 的监听地址
DHCP 池VIP 必须仍在池外(池实测 .100-.199),否则地址冲突

11.3 ⚠️ 已知问题(部署前请知悉)

问题影响状态
80-smoke-vm.sh 创建 VM 被 KubeVirt 准入拒绝:doc is missing path: /spec/template/spec/domain/cpu/maxSockets端到端 VM 验证未完成;集群本身健康不受影响❌ 未解决(已定位到 VM spec 缺 domain.cpu 段)
残留 PVC test-vm-disk0default,20 Gi,Bound,空卷)占一份 Longhorn 卷,无功能影响⚠️ 待清理
logs/deploy-all.log 末尾是旧版凭据块看日志尾部会拿到过期凭据说明⚠️ configs/credentials.txt 为准
smoke VM 镜像走公网 URL生产不可依赖⚠️ 建议改为内网镜像源

11.4 重置与重跑

场景做法
某节点装机失败要重来virsh destroy harvester-0N → 重建盘 → bash scripts/60-deploy.sh node0N(域定义仍是 install 模式)
想复用已有集群只补节点确认 curl -sk https://<VIP>/pingpong,再单独跑该节点的 60-deploy.sh
全量重装清 NVRAM(nvram/*_VARS.fd)+ 清盘 + run-all.sh --from prepare凭据文件会重新生成
只想改 Dashboard 密码xxx(幂等,已改过则短路)

⚠️ 重置后节点 SSH host key 会变:脚本统一加 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no,否则报 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!


12. 排障分层与方法论

12.1 分层可观测点(排障前先定位在哪一层)

可观测点本章对应的坑
宿主(libvirt/磁盘/网桥)virsh list/domstate/dumpxmldfls -l disks/iptables -L FORWARD§7(机械盘同轴争抢)、§5.2(dumpxml 返回空)
引导(HTTP/dracut)logs/harvester-NN-console.log、HTTP 状态码§3(探测引导定网卡名)、§2(rd.neednet
安装器(harv-installguest 内 /var/log/console.log/rke2.log、进程表§7(预加载死线)、§9.1(nm-online 竞态)
集群(RKE2/K8s)kubectl get nodes/pods、etcd 成员数§5.3(NotReady 子串)、§10(quorum)
应用(Harvester/KubeVirt)API 响应码、webhook 报错§11.3(maxSockets 准入拒绝)
DNS / 镜像分发dig @<coredns> 跑 N 次看分布、crictl pullregistries.yaml 字节数与文件头§4.5(随机上游)、§4.6(声明源)

12.2 十条可直接复用的方法论

#原则一句话理由
1先分层,每层都要有可观测点不分层就只能靠猜,猜会一路带偏
2判据必须双向验证既要「该触发时触发」,也要「不该触发时不触发」
3状态匹配永远不要用子串NotReady 里含 Ready;用锚定正则或精确字段比较
4找竞态只能靠时间戳对齐到毫秒只看「卡住了」永远找不到竞态
5远程执行不要吞 stderr2>/dev/null 曾让 command not found 隐形,白等 40 分钟
6自愈必须限流 + 幂等 + 加锁没有限流的自愈比不自愈更危险
7先预演,再正式ISO 没下完就先跑一次真实引导,一次排除网络/引导/配置三类问题
8修复要落在「稳定的层」放在被依赖最少的独立文件里,目录回滚也毁不掉
9set -e 区块里的清理命令是雷区pkill/umount/rm 无匹配即中止全流程
10改声明源,不改被渲染的产物产物文件头 DO NOT EDIT 就是提示;「先成功后失效」最迷惑人

补充两条凭据类原则(详见 02 快速上手 §4): 同类不同用途的凭据不要同值(同值会让「登录失败」这个信号失去信息量); 用「三态判定」切开客户端与服务端问题(把所有候选密码逐一 xxx 打登录接口,看 HTTP 码分布)。


13. 快速参考卡

bash
# ── 部署 ────────────────────────────────────────────────
bash scripts/run-all.sh --preflight                # 前置检查(安全,不改状态)
setsid bash scripts/run-all.sh </dev/null >/tmp/run-all.log 2>&1 &   # 一键部署(~42 分钟)
bash scripts/run-all.sh --skip-download            # 介质已下载
bash scripts/run-all.sh --from configs             # 断点续跑

# ── 观察 ────────────────────────────────────────────────
tail -f logs/deploy-all.log                        # 总编排
tail -f logs/harvester-02-console.log              # 装机串口
cat logs/deploy.status                             # 当前阶段
tail -f logs/progress.log                          # 30s 粒度进度

# ── 验收 ────────────────────────────────────────────────
bash scripts/70-verify.sh                          # 集群 9 项体检
bash scripts/75-set-admin-password.sh; echo rc=$?  # rc=0 → dashboard 凭据可用

# ── 访问 ────────────────────────────────────────────────
Dashboard : https://<VIP>/          admin / <dashboard>
SSH       : rancher@<节点IP>         (密钥免密;OS 密码 dashboard 密码)
kubectl   : sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml

# ── 自愈(自动,无需干预;手工触发时用这些)────────────────
bash scripts/97-installer-netfix.sh 01             # L1 nm-online 注入
bash scripts/95-rescue-image-preload.sh 01 run     # L2 预加载卡死救援
bash scripts/96-install-retry.sh 01                # L3 安装器卡死重试

判卡阈值速记:装机 >25 min 介入,>30 min 基本是预加载死线;日志静默 ≥60 s 且进程全无 → L3。


14. 小结与下一步

本章要点一句话
引导方式libvirt 直接内核引导<kernel>+<initrd>+<cmdline>),不挂 ISO、不走交互向导
设备名固定 PCI 地址换稳定网卡名;migratable='off' 让嵌套虚拟化可用
配置server_url/token顶层键vip 仅 create;iso_url 走 cmdline
磁盘OS 盘实体必须在 NVMe,否则 30 分钟预加载死线必然踩中
编排阶段 preflight→download→prepare→http→configs→node01→all→verifysetsid 脱离会话
自愈L1 网络竞态 / L2 预加载救援 / L3 安装器重试,全部限流 + 幂等 + 加锁
凭据装机配置无法设 Dashboard 密码 → 装完必须跑一次首登改密

接下来

  • 已有 K8s 集群、想把虚拟化「装进去」而不是「换底座」→ 04 部署实战 B:容器化 KubeVirt
  • 装完要接入 Rancher / 内外网分离解析 → 09 接入 Rancher 与生态集成
  • 日常巡检、重置、网络打通 → 07 网络实战、08 高可用与迁移、14 运维 SOP
  • 全部踩坑原始记录 → worker/harvester-iso/02-故障排查与修复记录.md(28 个案例)