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.
| Area | Capabilities |
|---|---|
| Virtual Machines | List, create, delete, start, stop, restart, live-migrate |
| VM Images | List (with StorageClass), upload from URL or file |
| 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 |
| Volumes | List PVCs with live Longhorn usage and StorageClass; create new 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 |
| 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 |
brew install abonillabeeche/tap/harvesterHomebrew 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.
curl -fsSL https://raw.githubusercontent.com/abonillabeeche/harvester-cli/main/install.sh | shThe 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)Download the .exe for your architecture from the latest release and place it somewhere on your %PATH%.
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 --versionharvester needs a kubeconfig that points to your Harvester cluster's Kubernetes API. There are three ways to provide it, in order of precedence:
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/configexport HARVESTER_CONFIG=/path/to/kubeconfig
harvester vm listharvester --harvester-config /path/to/kubeconfig vm listIf 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 automaticallyAll commands accept -n <namespace> (or --namespace) to target a specific namespace, mirroring kubectl behavior. The default namespace is default.
harvester vm list [-n NAMESPACE]Lists all VMs in the cluster with their state, CPU, memory, and IP address.
harvester vm create [FLAGS...] VM_NAMECreates 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-vmharvester 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-vmharvester image list [-n NAMESPACE]Lists VM images with their source type, StorageClass, and URL.
NAME ID SOURCE TYPE STORAGE CLASS URL
ubuntu-noble default/ubuntu-noble download tworeplicas https://cloud-images.ubuntu.com/...
harvester image create --source URL [--storage-class CLASS] [-n NAMESPACE] IMAGE_NAMEUploads a VM image from an HTTP/HTTPS URL (or local file path). Prints the image ID on success.
# 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
# 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-nobleThe 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.
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
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 --forceharvester 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>.
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://...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/9Error 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.
harvester network list [-n NAMESPACE]Lists NetworkAttachmentDefinitions with their CNI type and VLAN ID.
NAME NAMESPACE TYPE VLAN ID
vlan1 default bridge 1
vlan10 default bridge 10
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_NAMECreates 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-volharvester volume list-storageclassLists all StorageClasses in the cluster — equivalent to kubectl get sc.
NAME PROVISIONER RECLAIM POLICY BINDING MODE ALLOW EXPANSION
harvester-longhorn driver.longhorn.io Delete Immediate true
tworeplicas driver.longhorn.io Delete Immediate true
harvester host listLists 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)
harvester template list [-n NAMESPACE]
# Show a specific template version
harvester template show NAME:VERSION [-n NAMESPACE]
harvester template show ubuntu-base:1harvester keypair list [-n NAMESPACE]Lists SSH public keys registered in Harvester, which can be referenced by name when creating VMs.
harvester shell [--ssh-user USER] [--ssh-key PATH] VM_NAMEOpens 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-vmEvery 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.
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: {}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: 0harvester 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# 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 pushDue to how Go's flag parser works with positional arguments, all flags must appear before the VM name:
# Correct
harvester vm create --cpus 2 --memory 4Gi my-vm
# Wrong — flags after the name are silently ignored
harvester vm create my-vm --cpus 2 --memory 4GiWhen 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-vmThe show subcommand requires both name and version separated by a colon:
harvester template show ubuntu-base:1Add --debug before any command to enable verbose logging:
harvester --debug vm list