README
¶
Harvester CLI
A fast, kubectl-style command-line tool for managing Harvester HCI clusters — create and control VMs, images, volumes, networks, and hosts without leaving your terminal.
Built and tested against Harvester v1.9.0 (KubeVirt 1.8.x). Earlier 1.x clusters keep working, minus the two 1.9.0-only features: image create --backend cdi and import --source-cluster-type ova.
Table of Contents
Features
| Area | Capabilities |
|---|---|
| Virtual Machines | List, create, delete, start, stop, restart, live-migrate |
| VM Images | List (with StorageClass and backend), upload from URL or file, delete, Longhorn backing-image or CDI backend |
| Image Catalog | Curated list of cloud-init-enabled Linux images (Fedora, CentOS Stream, Debian, AlmaLinux, Rocky, Ubuntu, openSUSE); interactive picker, scriptable create, works offline via embedded JSON + catalog init cache |
| Networks | List NADs with VLAN info, including trunk ranges |
| Volumes | List PVCs with live Longhorn usage and StorageClass; create and delete PVCs |
| Hosts | List nodes with real-time CPU % and memory usage from the metrics API |
| Templates | List and inspect VM templates |
| SSH Keypairs | List registered public keys |
| Shell | Direct SSH into a running VM |
| VM Import | Enable the vm-import-controller and import guests from VMware, OpenStack or an OVA server |
| Config | Login to Rancher and auto-download the Harvester kubeconfig |
| Dry-run | Print the Kubernetes YAML for any create command without applying it — ideal for GitOps workflows |
Installation
Homebrew (macOS and Linux — recommended)
brew install abonillabeeche/tap/harvester
Homebrew handles the macOS quarantine flag automatically, so Gatekeeper will not block the binary.
First-time setup: add the tap once with
brew tap abonillabeeche/tap, then install as above.
Install script (macOS and Linux)
curl -fsSL https://raw.githubusercontent.com/abonillabeeche/harvester-cli/main/install.sh | sh
The script auto-detects your OS and architecture, downloads the right binary from the latest release, and removes the macOS quarantine flag. To install to a custom location:
INSTALL_DIR=~/.local/bin sh <(curl -fsSL https://raw.githubusercontent.com/abonillabeeche/harvester-cli/main/install.sh)
Windows
Download the .exe for your architecture from the latest release and place it somewhere on your %PATH%.
Build from source
Requirements: Go 1.21+
git clone https://github.com/abonillabeeche/harvester-cli.git
cd harvester-cli
go build -o harvester .
sudo mv harvester /usr/local/bin/
harvester --version
Configuration
harvester needs a kubeconfig that points to your Harvester cluster's Kubernetes API. There are three ways to provide it, in order of precedence:
1. Default file location (recommended)
Place the kubeconfig at ~/.harvester/config. This is the path the CLI uses automatically with no extra flags.
mkdir -p ~/.harvester
cp /path/to/your/harvester-kubeconfig ~/.harvester/config
2. Environment variable
export HARVESTER_CONFIG=/path/to/kubeconfig
harvester vm list
3. Inline flag
harvester --harvester-config /path/to/kubeconfig vm list
Automatic download via Rancher
If your Harvester cluster is imported into Rancher, you can download the kubeconfig automatically using a Rancher API token:
# Log in to your Rancher server
harvester login https://<RANCHER_URL> -t <RANCHER_API_TOKEN>
# Download the kubeconfig for the target Harvester cluster
harvester get-config <HARVESTER_CLUSTER_NAME>
# Saved to ~/.harvester/config automatically
Commands
All commands accept -n <namespace> (or --namespace) to target a specific namespace, mirroring kubectl behavior. The default namespace is default.
VM Management
harvester vm list [-n NAMESPACE]
Lists all VMs in the cluster with their state, CPU, memory, and IP address.
harvester vm create [FLAGS...] VM_NAME
Creates a VM. Important: all flags must come before the VM name.
| Flag | Short | Description |
|---|---|---|
--namespace |
-n |
Target namespace (default: default) |
--cpus |
-c |
Number of vCPUs (default: 1) |
--memory |
-m |
RAM size, e.g. 4Gi (default: 1Gi) |
--disk-size |
-d |
Root disk size, e.g. 20Gi (default: 10Gi) |
--vm-image-id |
Harvester image ID to boot from | |
--ssh-keyname |
-i |
SSH keypair name (use namespace/name for cross-namespace) |
--network |
Network attachment, e.g. default/vlan1 |
|
--template |
VM template in name:version format |
|
--count |
Create N identical VMs named basename-1…basename-N |
|
--user-data-filepath |
Path to a cloud-init user-data YAML file | |
--dry-run |
Print the KubeVirt YAML without creating the VM |
Examples:
# Create a VM using a specific image and network
harvester vm create --cpus 2 --memory 4Gi --vm-image-id default/ubuntu-noble --network default/vlan1 --ssh-keyname default/mykey my-vm
# Create 3 VMs from a template
harvester vm create --template ubuntu-base:1 --count 3 test-vm
# Create a VM in a non-default namespace
harvester vm create -n dev --cpus 4 --memory 8Gi dev-vm
# Preview the manifest without applying it
harvester vm create --dry-run --vm-image-id default/ubuntu-noble --network default/vlan1 my-vm
harvester vm stop VM_NAME [-n NAMESPACE]
harvester vm start VM_NAME [-n NAMESPACE]
harvester vm restart VM_NAME [-n NAMESPACE]
harvester vm delete VM_NAME [-n NAMESPACE]
harvester vm migrate [--node TARGET_NODE] VM_NAME [-n NAMESPACE]
Live-migrates a running VM to another host without downtime. Omit --node to let the scheduler choose the best target automatically.
# Migrate to a specific host
harvester vm migrate --namespace default --node gr6-2 my-vm
# Let the scheduler pick the target
harvester vm migrate -n default my-vm
Images
harvester image list [-n NAMESPACE]
Lists VM images with their source type, storage backend, StorageClass, and URL.
NAME ID SOURCE TYPE BACKEND STORAGE CLASS URL
ubuntu-noble default/ubuntu-noble download backingimage tworeplicas https://cloud-images.ubuntu.com/...
leap-16 default/leap-16 download cdi ceph-rbd https://download.opensuse.org/...
harvester image create --source URL [--storage-class CLASS] [--backend BACKEND] [-n NAMESPACE] IMAGE_NAME
Uploads a VM image from an HTTP/HTTPS URL (or local file path). Prints the image ID on success.
--backend picks how Harvester stores the image:
| Backend | Meaning |
|---|---|
backingimage |
Longhorn v1 backing image — the Harvester default |
cdi |
Imported through CDI, the only backend that can place an image on a third-party CSI StorageClass (Ceph RBD, LINSTOR, Longhorn v2, …) |
Leave it out and the CLI infers it from --storage-class: anything that is not Longhorn v1 gets cdi, because a backing image cannot live on a third-party StorageClass and Harvester would silently move the image back to a Longhorn class.
CDI caveat: the CDI importer needs the source server to return a
Content-Lengthheader. Some mirrors (notablycloud-images.ubuntu.comwith chunked transfer encoding) do not, and the import fails withfailed to fetch image size. Use a mirror that sends the header, or thebackingimagebackend.
# Upload Ubuntu Noble using the tworeplicas storage class
harvester image create \
--source https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img \
--storage-class tworeplicas \
ubuntu-noble
# Image created: default/ubuntu-noble
# Put an image on Ceph RBD -- the cdi backend is inferred from the StorageClass
harvester image create \
--source https://download.opensuse.org/distribution/leap/16.0/appliances/Leap-16.0-Minimal-VM.x86_64-Cloud.qcow2 \
--storage-class ceph-rbd \
leap-16
# ...or ask for it explicitly
harvester image create --backend cdi \
--source https://download.opensuse.org/distribution/leap/16.0/appliances/Leap-16.0-Minimal-VM.x86_64-Cloud.qcow2 \
--storage-class ceph-rbd \
leap-16
# Preview the manifest without applying it
harvester image create --dry-run \
--source https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img \
ubuntu-noble
--storage-classis also written to theharvesterhci.io/storageClassNameannotation, which is where Harvester's backing-image mutator actually reads it from. Without the annotation the flag is ignored on thebackingimagepath, and on a cluster whose default StorageClass is marked only with the deprecatedstorageclass.beta.kubernetes.io/is-default-classannotation the create is rejected outright withno default storageClass found for backingImage.
harvester image delete [-n NAMESPACE] IMAGE_NAME [IMAGE_NAME...]
Deletes one or more images. An image can be named either by the display name image list shows or
by its resource name (the part after the / in the ID column). Unlike vm create --vm-image-id,
the lookup never leaves the namespace — a delete should not reach across the cluster for something
that merely looks close enough.
harvester image delete ubuntu-noble
# VM image ubuntu-noble (default/image-zzkzm) deleted successfully
harvester image delete -n staging leap-16 image-ccxcx
Harvester refuses the delete while a VM or volume still depends on the image, and the webhook's error is passed through unchanged.
Image Catalog
The catalog is a curated list of publicly-available, cloud-init-enabled Linux images (Fedora, CentOS Stream, Debian, AlmaLinux, Rocky Linux, Ubuntu, openSUSE — see image-metadata.json). Instead of hunting for the right download URL for every cluster, you pick from the catalog and the CLI submits the image to Harvester.
The catalog metadata lives in image-metadata.json at the root of this repo. That same file is bundled into every CLI binary via //go:embed, and it can be cached to disk with catalog init — so the catalog works even when your workstation has no internet, as long as the Harvester servers themselves can reach the image URLs.
Where the catalog comes from
Every catalog subcommand resolves its metadata source in this order:
--metadata-urlflag orHARVESTER_CATALOG_METADATAenv var, if explicitly set (acceptshttps://…,file://…, or a plain filesystem path).~/.harvester/image-metadata.jsonif it exists (the local cache written bycatalog init).- Default remote URL (
https://raw.githubusercontent.com/abonillabeeche/harvester-cli/main/image-metadata.json). If the HTTP fetch fails, the CLI transparently falls back to the copy embedded in the binary (with a warning), socatalogstill works offline out of the box.
The interactive catalog command prints the resolved source at the top of its output so you can always tell where the list came from:
$ harvester image catalog
Image catalog source: /Users/you/.harvester/image-metadata.json
catalog init — cache the catalog for offline use
harvester image catalog init [--metadata-url URL] [--force]
Downloads the metadata JSON and writes it to ~/.harvester/image-metadata.json (alongside the harvester CLI config). Subsequent catalog commands prefer this file automatically.
# First-time setup while online:
harvester image catalog init
# Downloading catalog from: https://raw.githubusercontent.com/abonillabeeche/harvester-cli/main/image-metadata.json
# Saving to: /Users/you/.harvester/image-metadata.json
# Cached catalog with 10 OS group(s). Future 'image catalog' runs will use this file automatically.
# Refresh when new distros are added upstream:
harvester image catalog init --force
# Cache from a private mirror instead of GitHub:
harvester image catalog init --metadata-url https://mirror.internal/harvester/image-metadata.json --force
catalog — interactive picker
harvester image catalog [--namespace NS] [--storage-class SC] [--metadata-url URL]
Walks you through picking an OS group, then an image, then the namespace, then the StorageClass. If the caller has cluster-list permissions, namespace and StorageClass are shown as numbered pickers; otherwise the CLI falls back to a free-form text prompt (useful for Rancher-proxied Harvester kubeconfigs, which typically only grant namespaced reads).
$ harvester image catalog
Image catalog source: /Users/you/.harvester/image-metadata.json
NUMBER NAME KEY NUMBER OF IMAGES
1 AlmaLinux almalinux 3
2 CentOS Stream centos-stream 2
3 Debian debian 2
4 Fedora fedora 3
5 openSUSE Leap opensuse-leap 4
6 openSUSE MicroOS opensuse-microos 1
7 openSUSE Tumbleweed opensuse-tumbleweed 1
8 Rocky Linux rocky-linux 5
9 Ubuntu ubuntu 6
Insert a number to select the image OS:
4
Here are the images available for Fedora
NUMBER NAME VERSION BUILD URL
1 Fedora Cloud 44 44 1.7 https://download.fedoraproject.org/.../Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2
2 Fedora Cloud 43 43 1.6 https://download.fedoraproject.org/.../Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2
3 Fedora Cloud 42 42 1.1 https://download.fedoraproject.org/.../Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2
Insert a number to select an image to download:
1
Your image URL is : https://download.fedoraproject.org/.../Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2
Enter the namespace to create the image in [default]: my-project
Enter the StorageClass name (empty = cluster default): tworeplicas
INFO Creating image "Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2" in namespace "my-project" with StorageClass "tworeplicas"
INFO Image was created in Harvester with display name Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2 and id image-a9b2f
Pass --namespace or --storage-class to skip the matching prompt. The KEY column shows the shell-friendly identifier to use with catalog list <key> and catalog create <key>/<version>.
catalog list — non-interactive listing
harvester image catalog list [OS] [--metadata-url URL]
Prints catalog entries without prompting. The full listing shows both the pretty NAME (label) and the OS key (used by scripts). Pass an OS key to filter to a single group.
# Full catalog
harvester image catalog list
# NAME OS VERSION BUILD SHORT NAME URL
# AlmaLinux almalinux 10 latest AlmaLinux 10 https://...
# ...
# openSUSE Leap opensuse-leap 16.0 latest openSUSE Leap 16.0 https://...
# ...
# Just Fedora
harvester image catalog list fedora
# Images for Fedora (fedora):
#
# VERSION BUILD SHORT NAME URL
# 44 1.7 Fedora Cloud 44 https://...
# 43 1.6 Fedora Cloud 43 https://...
# 42 1.1 Fedora Cloud 42 https://...
catalog create — scripted image creation
harvester image catalog create <OS>/<VERSION> \
[--namespace NS] [--storage-class SC] [--display-name NAME] \
[--description TEXT] [--dry-run] [--metadata-url URL]
Creates a VM image from a catalog entry by <os>/<version> selector (e.g. fedora/44, opensuse-leap/16.0, rocky-linux/9.5). No prompts — flag values are used as-is. Empty --storage-class means "use the cluster default StorageClass". When multiple builds share the same version, the last entry in the catalog array wins (arrays are ordered oldest → newest) and the choice is logged.
# Create with cluster default StorageClass
harvester image catalog create fedora/44
# Explicit namespace + StorageClass + friendly display name
harvester image catalog create --namespace prod --storage-class tworeplicas \
--display-name ubuntu-noble-base ubuntu/24.04
# Preview the YAML manifest without creating (GitOps-friendly)
harvester image catalog create --dry-run --storage-class tworeplicas debian/12
# openSUSE Leap 16 / Rocky 10 examples
harvester image catalog create --storage-class tworeplicas opensuse-leap/16.0
harvester image catalog create --storage-class tworeplicas rocky-linux/10
# Use a private mirror as the source
harvester image catalog create --metadata-url https://mirror.internal/catalog.json centos-stream/9
Error messages point you to what's available if you get the selector wrong:
$ harvester image catalog create ubuntu/99
FATA no image with version "99" for ubuntu. Available versions: 25.10, 25.04, 24.10, 24.04, 22.04, 20.04
$ harvester image catalog create nonexistent/1.0
FATA unknown OS "nonexistent". Available: almalinux, centos-stream, debian, fedora, opensuse-leap, opensuse-microos, opensuse-tumbleweed, rocky-linux, ubuntu
Flag ordering: because of a urfave/cli v2 quirk, flags must appear before the positional selector:
create --dry-run ubuntu/24.04— notcreate ubuntu/24.04 --dry-run.
Networks
harvester network list [-n NAMESPACE]
Lists NetworkAttachmentDefinitions with their CNI type and VLAN ID. A trunk network carries a set of
ranges rather than a single ID, and is shown as trunk <ranges>.
NAME NAMESPACE TYPE VLAN ID
vlan1 default bridge 1
vlan10 default bridge 10
multi-vlan default bridge trunk 1000-1010,2001
public-nat default kube-ovn
Volumes
harvester volume list [-n NAMESPACE]
Lists PersistentVolumeClaims cross-referenced with Longhorn to show actual used space.
NAME NAMESPACE STATE CAPACITY USED STORAGE CLASS
my-vm-disk default Healthy 20.0 GiB 3.2 GiB tworeplicas
data-vol default Healthy 100.0 GiB 45.1 GiB harvester-longhorn
harvester volume create --storage-class CLASS --size SIZE [-n NAMESPACE] [--dry-run] VOLUME_NAME
Creates a PersistentVolumeClaim backed by the specified StorageClass. Use binary suffixes for size (Gi, Mi).
| Flag | Short | Description |
|---|---|---|
--storage-class |
--sc |
StorageClass for the volume (see harvester volume list-storageclass) |
--size |
-s |
Volume size, e.g. 10Gi, 500Mi |
--namespace |
-n |
Target namespace (default: default) |
--dry-run |
Print the PVC YAML without creating it |
harvester volume create --sc tworeplicas --size 20Gi my-data-vol
# Volume created: default/my-data-vol
harvester volume delete [-n NAMESPACE] [--force] VOLUME_NAME [VOLUME_NAME...]
Deletes PersistentVolumeClaims. A volume that a VM still lists as a disk is refused by name, because
deleting it would leave the PVC in Terminating until the VM itself is gone — which looks like a
hang. Pass --force to skip the check.
harvester volume delete my-data-vol
# Volume deleted: default/my-data-vol
harvester volume delete my-vm-disk
# FATA volume default/my-vm-disk is still used by VM my-vm, delete the VM first or pass --force
harvester volume list-storageclass
Lists all StorageClasses in the cluster — equivalent to kubectl get sc, plus a DEFAULT column.
NAME DEFAULT PROVISIONER RECLAIM POLICY BINDING MODE ALLOW EXPANSION
harvester-longhorn * driver.longhorn.io Delete Immediate true
tworeplicas driver.longhorn.io Delete Immediate true
Harvester honours only storageclass.kubernetes.io/is-default-class. A class marked default with
nothing but the deprecated storageclass.beta.kubernetes.io/is-default-class looks default to
kubectl while Harvester acts as if the cluster has none — which shows up much later as
no default storageClass found for backingImage. That case is named rather than left blank:
NAME DEFAULT PROVISIONER RECLAIM POLICY BINDING MODE ALLOW EXPANSION
harvester-longhorn (beta only, Harvester ignores it) driver.longhorn.io Delete Immediate true
Hosts
harvester host list
Lists all cluster nodes with real-time CPU and memory usage pulled from the Kubernetes Metrics API. Falls back gracefully to <unknown> if the metrics server is unavailable.
NAME STATUS ROLES AGE CPU(cores) CPU% MEM USE MEM% MEM TOTAL
gr6-1 Ready etcd,control-plane,master 769d 1918m 12% 26.0 GiB 41% 62.2 GiB
gr6-2 Ready etcd 769d 1126m 10% 3.0 GiB 10% 28.3 GiB
gr6-3 Ready etcd,master,control-plane 769d 2480m 16% 26.5 GiB 42% 62.2 GiB
- CPU(cores): live usage in millicores (e.g.
1918m= 1.918 cores) - CPU%: usage relative to allocatable CPU on that node
- MEM USE: live memory usage from the metrics API
- MEM%: usage relative to allocatable memory on that node
- MEM TOTAL: total installed RAM (
node.Status.Capacity)
Templates
harvester template list [-n NAMESPACE]
# Show a specific template version
harvester template show NAME:VERSION [-n NAMESPACE]
harvester template show ubuntu-base:1
SSH Keypairs
harvester keypair list [-n NAMESPACE]
Lists SSH public keys registered in Harvester, which can be referenced by name when creating VMs.
Shell Access
harvester shell [--ssh-user USER] [--ssh-key PATH] VM_NAME
Opens an interactive SSH session directly into a running VM. Requires ssh to be available on the local system.
| Flag | Default | Description |
|---|---|---|
--ssh-user |
ubuntu |
Username to connect as |
--ssh-key |
~/.ssh/id_rsa |
Path to private key |
--ssh-port |
22 |
SSH port |
harvester shell --ssh-user ubuntu --ssh-key ~/.ssh/mykey my-vm
VM Import
Drives Harvester's vm-import-controller addon, which migrates guests from another platform into
Harvester. Three source types are supported: vmware, openstack, and — new in Harvester v1.9.0 —
ova, which pulls OVA files straight off an HTTP server.
harvester import enable
Enables the vm-import-controller addon. Safe to re-run; it is a no-op when already enabled.
Until it is enabled the migration CRDs do not exist, and every other import subcommand fails with
a 404 from the API server. The CLI adds the missing half of that message:
FATA failed to list VM imports: the server could not find the requested resource
(get virtualmachineimports.migration.harvesterhci.io). The vm-import-controller addon looks
disabled, run 'harvester import enable' first
harvester import source-add \
--source-cluster-type TYPE \
--source-cluster-namespace NAMESPACE \
--endpoint ENDPOINT \
[--credentials-secret NS/SECRET] \
[--dc DATACENTER] [--region REGION] [--http-timeout SECONDS] \
SOURCE_NAME
Registers a source to import from. Which flags apply depends on the type:
| Type | --endpoint |
--dc |
--region |
--credentials-secret |
--http-timeout |
|---|---|---|---|---|---|
vmware |
vCenter URL | required | — | required | — |
openstack |
Keystone URL | — | required | required | — |
ova |
URL the OVA files are served from | — | — | optional | optional, defaults to 600 |
For ova, the referenced secret may hold username, password and ca.crt keys; omit it entirely
for an unauthenticated server.
# VMware vCenter
harvester import source-add \
--source-cluster-type vmware \
--source-cluster-namespace harvester-system \
--endpoint https://vcenter.example.com/sdk \
--dc DC1 \
--credentials-secret harvester-system/vcenter-creds \
my-vcenter
# OVA files on a plain HTTP server, 2-minute download timeout
harvester import source-add \
--source-cluster-type ova \
--source-cluster-namespace harvester-system \
--endpoint http://images.example.com/ovas/ \
--http-timeout 120 \
my-ovas
harvester import create \
--vm-name GUEST \
--source-cluster SOURCE_NAME \
--source-cluster-type TYPE \
--source-cluster-namespace NAMESPACE \
[--network-mapping SRC:DST ...] \
VM_IMPORT_NAME
Starts an import. --network-mapping (alias --net-map) may be repeated to map each source network
onto a Harvester network; for ova, --vm-name is the OVA file name relative to --endpoint.
harvester import create \
--vm-name my-guest.ova \
--source-cluster my-ovas \
--source-cluster-type ova \
--source-cluster-namespace harvester-system \
--net-map "VM Network:default/vlan1" \
import-my-guest
harvester import list [-n NAMESPACE]
harvester import delete [-n NAMESPACE] VM_IMPORT_NAME
harvester import source-delete [-n NAMESPACE] [--type TYPE] SOURCE_NAME
An import lives in the same namespace as its source, which is not necessarily harvester-system, so
import list covers every namespace unless -n narrows it.
NAME NAMESPACE VM NAME STATUS SOURCE_CLUSTER CLUSTER_TYPE
import-my-guest harvester-system my-guest.ova Running my-ovas OvaSource
--source-cluster-namespace and -n/--namespace are accepted interchangeably across every
import subcommand.
Dry-run and GitOps
Every create subcommand accepts a --dry-run flag. Instead of calling the Kubernetes API, the CLI prints the fully-rendered YAML manifest to stdout and exits. This is useful for:
- GitOps workflows — generate manifests locally, commit them to a Git repo, and let Fleet, Flux or ArgoCD apply them to the cluster.
- Reviewing changes before applying — inspect the exact object the CLI would create before committing to it.
- Piping into
kubectl apply— runharvester vm create --dry-run ... | kubectl apply -f -for one-shot creation using the same flags as your normal workflow.
Volume
harvester volume create --dry-run --sc tworeplicas --size 5Gi testvol1
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
name: testvol1
namespace: default
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: tworeplicas
volumeMode: Block
status: {}
Image
harvester image create --dry-run \
--source https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img \
--storage-class tworeplicas \
ubuntu-noble
---
apiVersion: harvesterhci.io/v1beta1
kind: VirtualMachineImage
metadata:
creationTimestamp: null
generateName: image-
namespace: default
spec:
displayName: ubuntu-noble
sourceType: download
storageClassParameters: {}
targetStorageClassName: tworeplicas
url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
status:
progress: 0
VM
harvester vm create --dry-run \
--vm-image-id default/image-abc12 \
--network default/vlan1 \
--cpus 2 --memory 4Gi --disk-size 20Gi \
my-vm
---
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
annotations:
harvesterhci.io/volumeClaimTemplates: '[{"metadata":{"name":"my-vm-disk-0-...","annotations":{"harvesterhci.io/imageId":"default/image-abc12"}},"spec":{"accessModes":["ReadWriteMany"],"resources":{"requests":{"storage":"20Gi"}},"volumeMode":"Block","storageClassName":"longhorn-image-abc12"}}]'
networks.harvesterhci.io/ips: '[]'
labels:
harvesterhci.io/creator: harvester
name: my-vm
namespace: default
spec:
runStrategy: Always
template:
spec:
domain:
cpu:
cores: 2
sockets: 1
threads: 1
devices:
disks:
- disk:
bus: virtio
name: disk-0
- disk:
bus: virtio
name: cloudinitdisk
interfaces:
- bridge: {}
model: virtio
name: nic-1
resources:
limits:
cpu: "2"
memory: 4Gi
networks:
- multus:
networkName: default/vlan1
name: nic-1
volumes:
- name: disk-0
persistentVolumeClaim:
claimName: my-vm-disk-0-...
- cloudInitNoCloud:
networkData: "..."
userData: "#cloud-config\n..."
name: cloudinitdisk
GitOps example
# Generate manifests for a new environment
harvester volume create --dry-run --sc tworeplicas --size 50Gi -n prod db-vol > manifests/db-vol.yaml
harvester image create --dry-run --source https://example.com/os.qcow2 prod-os >> manifests/images.yaml
harvester vm create --dry-run --vm-image-id default/prod-os \
--network default/vlan10 --cpus 4 --memory 8Gi -n prod \
db-server > manifests/db-server.yaml
# Commit and push — let your GitOps controller apply them
git add manifests/ && git commit -m "add prod db-server" && git push
Tips and Gotchas
Flag ordering
Due to how Go's flag parser works with positional arguments, all flags must appear before the positional argument, on every command:
# Correct
harvester vm create --cpus 2 --memory 4Gi my-vm
# Rejected
harvester vm create my-vm --cpus 2 --memory 4Gi
# FATA flag "--cpus" was given after a positional argument and would be ignored,
# put every flag before the argument
The CLI refuses to run rather than dropping the flags, which used to mean a stray
harvester vm create my-vm --dry-run really created the VM.
Misspelled subcommands
Every top-level command lists when it is called with no subcommand, and urfave/cli falls through to
that action when the subcommand does not exist — so harvester image delet foo used to print the
image list and exit 0. It now names the mistake:
harvester image delet my-image
# FATA unknown subcommand "delet" for "image", expected one of: list, create, delete, catalog, help
Sizes need a unit
--memory 4 is 4 bytes, not 4 GiB, and Harvester rejects it much later with a confusing
guest memory is under the minimum requirement (10 Mi). --memory and --disk-size are now checked
before anything is sent:
harvester vm create -m 4 my-vm
# FATA invalid --memory "4", a bare number is read as bytes -- use a unit, such as 4Gi
Cross-namespace references
When your VM lives in namespace dev but the SSH key or network is in default, use the namespace/name format:
harvester vm create -n dev \
--ssh-keyname default/mykey \
--network default/vlan1 \
dev-vm
Template version is required for template show
The show subcommand requires both name and version separated by a colon:
harvester template show ubuntu-base:1
Debug mode
Add --debug before any command to enable verbose logging:
harvester --debug vm list
Documentation
¶
There is no documentation for this package.