主题
02 · 快速上手:从零到第一台虚拟机
目标:30~45 分钟内,从一台空机器到「Harvester Dashboard 可登录 + 一台能 SSH 的 VM」。 本章给出三条上手路径,并把「首次登录认证」这个最容易卡住的环节讲透。 生产级部署请直接看 03(ISO 一体机)或 04(容器化)。
1. 三条上手路径怎么选
| 路径 | 你需要 | 耗时 | 产出 | 适合 |
|---|---|---|---|---|
| A. ISO 单节点体验 | 1 台裸机/VM(8C32G+250G)、UEFI 引导 | 15~25 分钟 | 单节点 Harvester + Dashboard | 第一次接触、做 POC |
| B. ISO 三节点无人值守 | KVM 宿主机(24 vCPU/96 GiB/NVMe+机械盘) | 42 分钟(介质另需 44 分钟下载) | 3 节点 HA(etcd 3/3)+ VIP | 交付、演练 HA |
| C. Helm 装进既有 K8s | 已运行的 RKE2/K8s(≥3 节点) | 10~20 分钟 | 既有集群 + 虚拟化能力 | 已有平台团队 |
三条路径的共同前置:CPU 支持 VT-x/AMD-V,且(若在 VM 里跑)嵌套虚拟化必须透传
vmx。
1.1 硬件门槛:生产 vs 实验
| 项 | 官方生产门槛 | 实验环境实测 | 说明 |
|---|---|---|---|
| CPU | 支持硬件虚拟化,建议 ≥8 核/节点 | 8 vCPU | guest 内还要跑 VM,嵌套虚拟化场景需更多 |
| 内存 | 建议 ≥32 GiB/节点 | 32 GiB | Harvester 单节点门槛偏高 |
| 系统盘 | SSD/NVMe,≥250 GB | 250 GiB qcow2(实体在 NVMe) | ★ 机械盘会触发装机卡死(见 03 §7) |
| 数据盘 | 独立磁盘,按容量规划 | 400 GiB qcow2(机械盘可接受) | Longhorn 默认盘 |
| 引导 | v1.8 起强制 UEFI | OVMF + 每节点独立 NVRAM | Legacy BIOS 已不支持 |
| 检查跳过 | — | harvester.install.skipchecks=true | 虚拟机不满足生产门槛时必须加 |
1.2 嵌套虚拟化(在 VM 里跑 Harvester 的关键)
bash
# 宿主机确认
grep -c vmx /proc/cpuinfo # Intel:>0
grep -c svm /proc/cpuinfo # AMD:>0
cat /sys/module/kvm_intel/parameters/nested # 期望 Y(AMD 为 kvm_amd)
# libvirt 域定义:★ migratable='off' 是重点
# <cpu mode='host-passthrough' check='none' migratable='off'/>
# 原因:migratable 默认 on,libvirt 为保证迁移兼容性会【过滤掉 vmx】
# → guest 内 grep -c vmx /proc/cpuinfo = 0 → KubeVirt 报无硬件虚拟化支持
# guest 内确认
grep -c vmx /proc/cpuinfo # >0 ✓2. 路径 A:ISO 单节点 15 分钟体验
2.1 下载介质
| 文件 | 大小(实测字节) | 用途 |
|---|---|---|
harvester-v1.8.2-amd64.iso | 8,232,370,176(约 7.7 GiB) | 交互式/PXE 安装镜像 |
harvester-v1.8.2-vmlinuz-amd64 | 15,227,376 | netboot 内核 |
harvester-v1.8.2-initrd-amd64 | 108,951,900 | initramfs(dracut) |
harvester-v1.8.2-rootfs-amd64.squashfs | 1,176,158,208 | live 根文件系统 |
bash
BASE=https://releases.rancher.com/harvester/v1.8.2
curl -fLO $BASE/harvester-v1.8.2-amd64.iso # 交互式安装只需这一个ISO 完整性核对(可信判据 vs 不可信判据):
bash
stat -c %s harvester-v1.8.2-amd64.iso # 应为 2048 的整数倍(ISO9660 扇区对齐)
isoinfo -d -i harvester-v1.8.2-amd64.iso # PVD 魔数 CD001、卷标 COS_LIVE⚠️ 不要用 HEAD 请求返回的
x-goog-stored-content-length判断大小—— GCS 对复合对象返回的是分片信息,并非真实对象大小,实测曾据此误判文件损坏。
2.2 安装向导里的关键字段
| 字段 | 建议值 | 说明 |
|---|---|---|
| Install Mode | Create(首节点)/ Join(后续) | Join 需填 server URL + token |
| 管理网卡 | 实测名字(如 enp1s0) | ★ 不要猜:可预测网卡名由 PCI 拓扑推导 |
| VIP | 网段内DHCP 池外的空闲地址 | 由 kube-vip 承载,Dashboard 入口 |
| 系统盘 | /dev/vda / /dev/sda | 会分出 ESP/COS_STATE/COS_RECOVERY/COS_PERSISTENT |
| 数据盘 | 独立盘 | 交给 Longhorn;auto-disk-provision-paths 留空可避免误纳管系统盘 |
| 服务器密码 | xxx | ★ 这是 OS/SSH(rancher 用户)密码,不是 Dashboard 密码 |
| NTP | 内网 NTP 优先 | Longhorn 与 TLS 对时钟敏感 |
| DNS | 只写一个内网 DNS | 并列公网 DNS 会触发 CoreDNS policy=random 陷阱(见 07) |
2.3 装完第一件事
登录与改密——见本章 §4,这是最多人卡住的地方。
3. 路径 B / C:一句话入口
bash
# 路径 B:ISO 三节点无人值守(详见 03 章)
bash scripts/run-all.sh --preflight # 8 项前置检查,不改状态
setsid bash scripts/run-all.sh </dev/null >/tmp/run-all.log 2>&1 & # 42 分钟
tail -f /tmp/run-all.log
# 路径 C:Helm 装进既有 RKE2(详见 04 章)
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
bash scripts/deploy-harvester.sh --nodeport 32735
bash scripts/post-install.sh # ★ 必做4. ★ 首次登录与认证(ISO 形态完整根因链)
4.1 初始凭据从哪来
| 问题 | 实测答案 |
|---|---|
| 装机配置能设 Dashboard 密码吗? | 不能。对 rootfs.squashfs 内的 /usr/bin/harvester-installer 做字符串统计:admin_password 0 次、admin_token 0 次(对照组 scheme_version 1 次,证明统计方法有效) |
| 全新集群的初始凭据 | 用户 admin / 密码 xxx,来自 cattle-system/bootstrap-secret 的 bootstrapPassword |
| 初始状态能直接调 API 吗 | 不能。mustChangePassword=xxx 时,登录虽返回 **201** 并发 token,但该 token 对 /v1、/v3、/apis` 全部 401 |
| 怎么破 | 先 POST /v3/users?action=changepassword 改密,之后一切正常 |
配置里的 os.password | OS/SSH(rancher 用户)密码,与 Dashboard 无关 |
配置里的 token | RKE2 集群加入令牌(join 节点用),不是 Dashboard API token |
bash
# 查引导密码与 first-login 状态(Harvester OS 节点上,★ 绝对路径)
K='sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml'
$K -n cattle-system get secret bootstrap-secret -o jsonpath='{.data.bootstrapPassword}' | base64 -d; echo
$K get settings.management.cattle.io first-login -o jsonpath='{.value}'; echo4.2 三个 API 细节坑
bash
# 坑 1:登录响应里 .id 不是 Bearer 值
# .id = "token-9p5vr" ← 只是 token 名,用它必然 401
# .token = "xxx" ← ★ 真正的凭据,格式 <名>:<密钥>
T=$(curl -sk -X POST https://<VIP>/v3-public/localProviders/local?action=login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin","ttl":3600000}' | jq -r '.token // empty')
# 坑 2:changepassword 必须 POST(PUT → 405 MethodNotAllow)
curl -sk -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
--data '{"currentPassword":"admin","newPassword":"<ADMIN_PW>"}' \
"https://<VIP>/v3/users?action=changepassword" # → 200
# 坑 3:改密后必须复核三项
curl -sk -H "Authorization: Bearer $NEW_T" https://<VIP>/v3/users?me=true \
| jq '{username,mustChangePassword,enabled}'
# 期望:mustChangePassword=xxx 401 失效改密脚本的逻辑(幂等,可反复跑):
① curl $API/ping 探活 → 不可达则 exit 1
② login admin <新密码> → 成功即"已是目标值",短路 exit 0(★ 幂等)
③ login admin <引导密码> → 失败则报错,并打印"如何手工核对 bootstrap-secret"
④ POST /v3/users?action=changepassword → 非 200 打印响应前 300 字节并 exit 1
⑤ 复核三项:新密码可登录 / mustChangePassword / 旧密码是否失效
⑥ 打印最终凭据行4.3 「浏览器 401,但 curl 用同一密码 201」= 客户端问题
用三态判定一刀切开:把所有候选密码逐一用 xxx 打登录接口,看 HTTP 码分布。
bash
for pw in '<ADMIN_PW>' '<SSH_PW>' 'admin'; do
code=$(curl -sk -o /dev/null -w '%{http_code}' -X POST \
https://<VIP>/v3-public/localProviders/local?action=login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"admin\",\"password\":\"$pw\",\"ttl\":3600000}")
echo "$code $pw"
done
# 实测:201 / 401 / 401 → 服务端某个密码有效,浏览器仍失败必是客户端问题客户端三大元凶:输入法把 @ 打成全角 @、浏览器自动填充了旧密码、Cookie 残留。 → 先用无痕窗口重试,别急着改服务端密码(改了反而制造第二个问题)。
4.4 凭据管理铁律
★ Dashboard 密码与 SSH 密码故意设成不同值。 两者同值时,「登录失败」这个信号失去信息量——你无法判断是密码错了,还是用错了哪一类密码。 拆成不同值后,失败本身就能指认错误类型。配套做法:在凭据文件里显式写明「这两个不是同一个」。
4.5 容器化形态的入口:必须带 SNI
Harvester 入口会校验 TLS SNI/Host,直接用 IP 访问会失败:
bash
NODE_IP=$(kubectl get node -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
PORT=$(kubectl -n harvester-system get svc harvester \
-o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}')
curl -k --resolve harvester.local:${PORT}:${NODE_IP} \
https://harvester.local:${PORT}/dashboard/ # → 200 ✓
curl -k --resolve harvester.local:${PORT}:${NODE_IP} \
https://harvester.local:${PORT}/v1/harvester # → schema 列表 ✓浏览器访问:本机 hosts 加 <NODE_IP> harvester.local,再打开 https://harvester.local:<PORT>/。
5. 创建第一台虚拟机
5.1 方式一:Dashboard(推荐给业务方)
https://<VIP>/ (ISO 形态)
https://<SNI_HOST>:<PORT>/ (容器化形态)
→ Virtual Machines → Create依次填:CPU/内存 → 镜像(来自 Images 页,先上传/填 URL)→ 磁盘大小 → 网络(默认 Management Network = masquerade)→ cloud-init(用户名/SSH 公钥/密码)。
UI 会自动补齐 Harvester 需要的标签/注解(如
harvesterhci.io/creator)与网络配置, 比手写 YAML 更少踩坑(例如缺masquerade接口会被 webhook 直接拒绝)。
5.2 方式二:kubectl + DataVolume(完整可跑清单)
yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: vm-demo
namespace: default
labels:
harvesterhci.io/creator: harvester # Harvester 生态识别用
spec:
runStrategy: Always # ★ 与 spec.running 互斥,二选一
template:
metadata:
labels: {harvesterhci.io/creator: harvester}
spec:
terminationGracePeriodSeconds: 30
domain:
machine: {type: q35}
cpu: {cores: 1, sockets: 1, threads: 1} # ★ 必须写,否则 webhook patch 路径缺失
memory: {guest: 1Gi}
resources:
requests: {cpu: "1", memory: 1Gi}
limits: {cpu: "1", memory: 1Gi}
devices:
interfaces:
- name: default
masquerade: {} # ★ 必需,否则被 validator.harvesterhci.io 拒绝
disks:
- {name: disk-0, disk: {bus: virtio}}
- {name: cloudinitdisk, disk: {bus: virtio}}
networks:
- {name: default, pod: {}} # 使用 Pod 网络(本环境 Calico VXLAN)
volumes: # ★ 漏写这一段会导致 VM 引用不到磁盘
- name: disk-0
dataVolume: {name: vm-demo-disk-0}
- name: cloudinitdisk
cloudInitNoCloud: {secretRef: {name: cirros-cloudinit}}
dataVolumeTemplates:
- metadata: {name: vm-demo-disk-0, namespace: default}
spec:
pvc:
accessModes: [ReadWriteOnce] # ★ 唯一可靠组合之一
volumeMode: Filesystem
resources: {requests: {storage: 2Gi}}
storageClassName: harvester-longhorn
source:
http: {url: "https://<内网镜像源>/cirros-0.6.2-x86_64-disk.img"}
---
apiVersion: v1
kind: Secret
metadata: {name: cirros-cloudinit, namespace: default}
stringData:
userData: |
#cloud-config
password: <GUEST_PW>
chpasswd: {expire: false}
ssh_pwauth: true
users:
- name: cirros
ssh_authorized_keys: ["<YOUR_SSH_PUBKEY>"]bash
kubectl apply -f vm-demo.yaml
kubectl get dv,pvc,vm,vmi -n default -w # 观察导入 → 调度 → 运行实测状态流转(HTTP 源,镜像约 60 MB):
dv : ImportScheduled → ImportInProgress(2.5%→99.6%) → Succeeded
pvc: Pending → Bound
vm : Provisioning → Starting → Running
vmi: Scheduling → Scheduled → Running node=<node> ip=10.42.6.31DV 22 秒即完成、VM 进 Running(实测 e2e 基线)。
★
accessModes怎么选(实测结论,选错会被 webhook 直接拒绝):
目标 accessModesvolumeMode说明 只跑起来、不迁移 ReadWriteOnceFilesystem最省资源,Longhorn 本地三副本 要热迁移 ReadWriteManyFilesystem否则 KubeVirt 迁移 webhook 拒绝: DisksNotLiveMigratable——"PVC xxx is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)"任何情况都不要用 — BlockLonghorn 的 RWX 依赖 share-manager(NFS),不支持块设备:CDI importer 报 blockdev: cannot open /dev/cdi-block-volume: Permission denied另:停机用
runStrategy: Halted(不是Stopped,Stopped非法);runStrategy与spec.running互斥,同时写会被validator.harvesterhci.io拒绝。⚠️ 容器化形态实测:RWX 的数据面存在阻塞(
shareEndpoint/shareState为空、无share-managerPod), 因此该环境用 RWO + Filesystem + 冷迁移;详见 04 §8。
5.3 方式三:空盘(秒级验证平台链路)
yaml
source:
blank: {} # 不下载任何镜像实测:DV Succeeded 100%(75s 含调度),VM/VMI Running ✓ 用途:验证平台链路、压测调度、演练迁移;不能当业务 VM(无 OS,引导停在 SeaBIOS)。
5.4 创建后必查清单
bash
kubectl get vm,vmi -n default -o wide # 状态与落点节点
kubectl get pvc -n default <disk> -o jsonpath='{.spec.accessModes} {.spec.volumeMode} {.status.phase}'
PV=$(kubectl get pvc -n default <disk> -o jsonpath='{.spec.volumeName}')
kubectl get volumes.longhorn.io -n longhorn-system $PV \
-o jsonpath='{.status.state} {.status.robustness} {.status.currentNodeID}{"\n"}'
kubectl get pod -n default -l kubevirt.io/domain=vm-demo -o wide # virt-launcher6. 访问你的第一台 VM
| 通道 | 命令/入口 | 说明 |
|---|---|---|
| SSH(masquerade) | ssh cirros@<VMI IP> | IP 是 Pod IP(kubectl get vmi -o wide);跨节点经 CNI VXLAN 可达 |
| Web Console | Dashboard → VM → Console | 走 virt-launcher 的 VNC/websocket,无需 guest 网络 |
| VNC(宿主侧) | virsh vncdisplay <domain> | 仅「在 VM 里跑 Harvester」的嵌套场景 |
| 串口 | Dashboard → VM → Serial Console | guest 需开 ttyS0 |
跨节点连通性验证(从另一个节点发起,才能真正验证 CNI):
bash
ping -c2 <VMI_IP>
nc -vz -w3 <VMI_IP> 22 # 实测 ICMP 与 SSH 22 均通 ✓7. 首跑最容易失败的 6 件事
| 症状 | 根因 | 一步修复 |
|---|---|---|
admission webhook "validator.harvesterhci.io" denied(网络) | 缺 masquerade 接口 | 加 interfaces[].masquerade: {} + networks[].pod: {} |
snapshot feature gate not enabled | gate 未开 / 名字写成复数 / virt-api 未重启 | Snapshot(单数)写进 values,滚动重启 virt-api(见 04 §5) |
PVC 永远 Pending | 节点数 < numberOfReplicas(默认 3) | 下调副本数,或补齐节点 |
VM 卡 Scheduling | 卷无法 attach / nodeSelector 指向不可调度节点 | 查 Longhorn 卷 state/robustness/cloneStatus |
| 镜像导入进度回退(99.62% → 2.5%) | 公网镜像源不稳定,CDI 重试并从头计时 | 换内网源;先用 blank/pvc 验证平台 |
Dashboard 401 / API must authenticate | `mustChangePassword=xxx 期间 token 全部 401 | 先 POST /v3/users?action=changepassword(§4.2) |
doc is missing path: /spec/template/spec/domain/cpu/maxSockets | VM spec 缺 domain.cpu 段,mutating webhook 的 replace 无路径可打 | 显式写出 domain.cpu 拓扑(sockets/cores/threads) |
最后一条在 ISO 形态实测中尚未彻底解决(smoke VM 被拒),候选修法已给出, 完整取证见 13 故障排查手册 案例 23。
8. 下一步
| 目标 | 去 |
|---|---|
| VM 的日常操作(启停/镜像/快照/清理) | 05 虚拟机生命周期实战 |
| 搞清磁盘规格怎么选 | 06 存储实战 |
| 让 VM 接入业务网段(VLAN) | 07 网络实战 |
| 交付生产集群 | 11 生产落地 |