Skip to content

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 vCPUguest 内还要跑 VM,嵌套虚拟化场景需更多
内存建议 ≥32 GiB/节点32 GiBHarvester 单节点门槛偏高
系统盘SSD/NVMe,≥250 GB250 GiB qcow2(实体在 NVMe★ 机械盘会触发装机卡死(见 03 §7)
数据盘独立磁盘,按容量规划400 GiB qcow2(机械盘可接受)Longhorn 默认盘
引导v1.8 起强制 UEFIOVMF + 每节点独立 NVRAMLegacy 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.iso8,232,370,176(约 7.7 GiB)交互式/PXE 安装镜像
harvester-v1.8.2-vmlinuz-amd6415,227,376netboot 内核
harvester-v1.8.2-initrd-amd64108,951,900initramfs(dracut)
harvester-v1.8.2-rootfs-amd64.squashfs1,176,158,208live 根文件系统
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 ModeCreate(首节点)/ 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-secretbootstrapPassword
初始状态能直接调 API 吗不能mustChangePassword=xxx 时,登录虽返回 **201** 并发 token,但该 token 对 /v1/v3/apis` 全部 401
怎么破POST /v3/users?action=changepassword 改密,之后一切正常
配置里的 os.passwordOS/SSH(rancher 用户)密码,与 Dashboard 无关
配置里的 tokenRKE2 集群加入令牌(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}'; echo

4.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.31

DV 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不是 StoppedStopped 非法); runStrategyspec.running 互斥,同时写会被 validator.harvesterhci.io 拒绝。

⚠️ 容器化形态实测:RWX 的数据面存在阻塞shareEndpoint/shareState 为空、无 share-manager Pod), 因此该环境用 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-launcher

6. 访问你的第一台 VM

通道命令/入口说明
SSH(masquerade)ssh cirros@<VMI IP>IP 是 Pod IPkubectl get vmi -o wide);跨节点经 CNI VXLAN 可达
Web ConsoleDashboard → VM → Consolevirt-launcher 的 VNC/websocket,无需 guest 网络
VNC(宿主侧)virsh vncdisplay <domain>仅「在 VM 里跑 Harvester」的嵌套场景
串口Dashboard → VM → Serial Consoleguest 需开 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 enabledgate 未开 / 名字写成复数 / 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 全部 401POST /v3/users?action=changepassword(§4.2)
doc is missing path: /spec/template/spec/domain/cpu/maxSocketsVM spec 缺 domain.cpu 段,mutating webhook 的 replace 无路径可打显式写出 domain.cpu 拓扑(sockets/cores/threads)

最后一条在 ISO 形态实测中尚未彻底解决(smoke VM 被拒),候选修法已给出, 完整取证见 13 故障排查手册 案例 23。


8. 下一步

目标
VM 的日常操作(启停/镜像/快照/清理)05 虚拟机生命周期实战
搞清磁盘规格怎么选06 存储实战
让 VM 接入业务网段(VLAN)07 网络实战
交付生产集群11 生产落地