Skip to content

Commit 4990e47

Browse files
authored
Cloud Hypervisor client (#5)
* Cloud hypervisor client * Update README * Use self hosted runner * Download binaries before test and build * Include generated code
1 parent 1d06a97 commit 4990e47

File tree

13 files changed

+5836
-9
lines changed

13 files changed

+5836
-9
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55

66
jobs:
77
test:
8-
runs-on: ubuntu-latest
8+
runs-on: [self-hosted, linux, x64, kvm]
99
steps:
1010
- uses: actions/checkout@v4
1111

@@ -17,7 +17,10 @@ jobs:
1717
- name: Install dependencies
1818
run: |
1919
set -xe
20-
sudo apt-get install -y erofs-utils
20+
if ! command -v mkfs.erofs &> /dev/null; then
21+
sudo apt-get update
22+
sudo apt-get install -y erofs-utils
23+
fi
2124
go mod download
2225
2326
# Avoids rate limits when running the tests
@@ -33,4 +36,3 @@ jobs:
3336

3437
- name: Build
3538
run: make build
36-

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ bin/**
1414
tmp
1515
tmp/**
1616
.datadir
17+
18+
# Cloud Hypervisor binaries (embedded at build time)
19+
lib/vmm/binaries/cloud-hypervisor/*/*/cloud-hypervisor

Makefile

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
SHELL := /bin/bash
2-
.PHONY: oapi-generate generate-wire generate-all dev build test install-tools gen-jwt
2+
.PHONY: oapi-generate generate-vmm-client generate-wire generate-all dev build test install-tools gen-jwt download-ch-binaries download-ch-spec ensure-ch-binaries
33

44
# Directory where local binaries will be installed
55
BIN_DIR ?= $(CURDIR)/bin
@@ -31,31 +31,72 @@ $(GODOTENV): | $(BIN_DIR)
3131

3232
install-tools: $(OAPI_CODEGEN) $(AIR) $(WIRE) $(GODOTENV)
3333

34+
# Download Cloud Hypervisor binaries
35+
download-ch-binaries:
36+
@echo "Downloading Cloud Hypervisor binaries..."
37+
@mkdir -p lib/vmm/binaries/cloud-hypervisor/v48.0/{x86_64,aarch64}
38+
@mkdir -p lib/vmm/binaries/cloud-hypervisor/v49.0/{x86_64,aarch64}
39+
@echo "Downloading v48.0..."
40+
@curl -L -o lib/vmm/binaries/cloud-hypervisor/v48.0/x86_64/cloud-hypervisor \
41+
https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v48.0/cloud-hypervisor-static
42+
@curl -L -o lib/vmm/binaries/cloud-hypervisor/v48.0/aarch64/cloud-hypervisor \
43+
https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v48.0/cloud-hypervisor-static-aarch64
44+
@echo "Downloading v49.0..."
45+
@curl -L -o lib/vmm/binaries/cloud-hypervisor/v49.0/x86_64/cloud-hypervisor \
46+
https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v49.0/cloud-hypervisor-static
47+
@curl -L -o lib/vmm/binaries/cloud-hypervisor/v49.0/aarch64/cloud-hypervisor \
48+
https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v49.0/cloud-hypervisor-static-aarch64
49+
@chmod +x lib/vmm/binaries/cloud-hypervisor/v*/*/cloud-hypervisor
50+
@echo "Binaries downloaded successfully"
51+
52+
# Download Cloud Hypervisor API spec
53+
download-ch-spec:
54+
@echo "Downloading Cloud Hypervisor API spec..."
55+
@mkdir -p specs/cloud-hypervisor/api-v0.3.0
56+
@curl -L -o specs/cloud-hypervisor/api-v0.3.0/cloud-hypervisor.yaml \
57+
https://raw.githubusercontent.com/cloud-hypervisor/cloud-hypervisor/refs/tags/v48.0/vmm/src/api/openapi/cloud-hypervisor.yaml
58+
@echo "API spec downloaded"
59+
3460
# Generate Go code from OpenAPI spec
3561
oapi-generate: $(OAPI_CODEGEN)
3662
@echo "Generating Go code from OpenAPI spec..."
3763
$(OAPI_CODEGEN) -config ./oapi-codegen.yaml ./openapi.yaml
3864
@echo "Formatting generated code..."
3965
go fmt ./lib/oapi/oapi.go
4066

67+
# Generate Cloud Hypervisor client from their OpenAPI spec
68+
generate-vmm-client: $(OAPI_CODEGEN)
69+
@echo "Generating Cloud Hypervisor client from spec..."
70+
$(OAPI_CODEGEN) -config ./oapi-codegen-vmm.yaml ./specs/cloud-hypervisor/api-v0.3.0/cloud-hypervisor.yaml
71+
@echo "Formatting generated code..."
72+
go fmt ./lib/vmm/vmm.go
73+
4174
# Generate wire dependency injection code
4275
generate-wire: $(WIRE)
4376
@echo "Generating wire code..."
4477
cd ./cmd/api && $(WIRE)
4578

4679
# Generate all code
47-
generate-all: oapi-generate generate-wire
80+
generate-all: oapi-generate generate-vmm-client generate-wire
81+
82+
# Check if binaries exist, download if missing
83+
.PHONY: ensure-ch-binaries
84+
ensure-ch-binaries:
85+
@if [ ! -f lib/vmm/binaries/cloud-hypervisor/v48.0/x86_64/cloud-hypervisor ]; then \
86+
echo "Cloud Hypervisor binaries not found, downloading..."; \
87+
$(MAKE) download-ch-binaries; \
88+
fi
4889

4990
# Build the binary
50-
build: | $(BIN_DIR)
91+
build: ensure-ch-binaries | $(BIN_DIR)
5192
go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api
5293

5394
# Run in development mode with hot reload
5495
dev: $(AIR)
5596
$(AIR) -c .air.toml
5697

5798
# Run tests
58-
test:
99+
test: ensure-ch-binaries
59100
go test -tags containers_image_openpgp -v -timeout 30s ./...
60101

61102
# Generate JWT token for testing
@@ -67,4 +108,5 @@ gen-jwt: $(GODOTENV)
67108
clean:
68109
rm -rf $(BIN_DIR)
69110
rm -f lib/oapi/oapi.go
111+
rm -f lib/vmm/vmm.go
70112

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ Run containerized workloads in VMs, powered by [Cloud Hypervisor](https://github
88

99
### Prerequisites
1010

11-
**Go 1.25.4+**, **Cloud Hypervisor**, **KVM**, **erofs-utils**
11+
**Go 1.25.4+**, **KVM**, **erofs-utils**
1212

1313
```bash
14-
cloud-hypervisor --version
1514
mkfs.erofs --version
1615
```
1716

17+
**KVM Access:** User must be in `kvm` group for VM access:
18+
```bash
19+
sudo usermod -aG kvm $USER
20+
# Log out and back in, or use: newgrp kvm
21+
```
22+
1823
### Configuration
1924

2025
#### Environment variables

lib/vmm/README.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Cloud Hypervisor VMM Client
2+
3+
Thin Go wrapper around Cloud Hypervisor's HTTP API with embedded binaries.
4+
5+
Supports multiple versions of the Cloud Hypervisor VMM because instances are version-locked to cloud hypervisor if they are in standby. We can switch them to the latest version if we shutdown / reboot the VM.
6+
7+
Embeds the binaries to make it easier to install, instead of managing multiple versions of the Cloud Hypervisor CLI externally + configuring it.
8+
9+
## Features
10+
11+
- Embedded Cloud Hypervisor binaries of multiple versions
12+
- Generated HTTP client from official OpenAPI spec
13+
- Automatic binary extraction to data directory
14+
- Unix socket communication
15+
- Version detection and validation
16+
17+
## Requirements
18+
19+
**System:**
20+
- Linux with KVM support (`/dev/kvm`)
21+
- User must be in `kvm` group or run as root
22+
23+
**Add user to kvm group:**
24+
```bash
25+
sudo usermod -aG kvm $USER
26+
# Then log out and back in, or use: sg kvm -c "your-command"
27+
```
28+
29+
## Usage
30+
31+
### Start a VMM Process
32+
33+
```go
34+
import "github.com/onkernel/hypeman/lib/vmm"
35+
36+
ctx := context.Background()
37+
dataDir := "/var/lib/hypeman"
38+
socketPath := "/tmp/vmm.sock"
39+
40+
// Start Cloud Hypervisor VMM (extracts binary if needed)
41+
err := vmm.StartProcess(ctx, dataDir, vmm.V48_0, socketPath)
42+
if err != nil {
43+
log.Fatal(err)
44+
}
45+
```
46+
47+
### Connect to Existing VMM
48+
49+
```go
50+
// Create client for existing VMM socket
51+
client, err := vmm.NewVMM(socketPath)
52+
if err != nil {
53+
log.Fatal(err)
54+
}
55+
56+
// All generated API methods available directly
57+
resp, err := client.GetVmmPingWithResponse(ctx)
58+
```
59+
60+
### Check Binary Version
61+
62+
```go
63+
binaryPath, _ := vmm.GetBinaryPath(dataDir, vmm.V48_0)
64+
version, err := vmm.ParseVersion(binaryPath)
65+
fmt.Println(version) // "v48.0"
66+
```
67+
68+
## Architecture
69+
70+
```
71+
lib/vmm/
72+
├── vmm.go # Generated from OpenAPI spec (DO NOT EDIT)
73+
├── client.go # Thin wrapper: NewVMM, StartProcess
74+
├── binaries.go # Binary embedding and extraction
75+
├── version.go # Version parsing utilities
76+
├── binaries/ # Embedded Cloud Hypervisor binaries
77+
│ └── cloud-hypervisor/
78+
│ ├── v48.0/
79+
│ │ ├── x86_64/cloud-hypervisor (4.5MB)
80+
│ │ └── aarch64/cloud-hypervisor (3.3MB)
81+
│ └── v49.0/
82+
│ ├── x86_64/cloud-hypervisor (4.5MB)
83+
│ └── aarch64/cloud-hypervisor (3.3MB)
84+
| # There will be additional versions in the future...
85+
└── client_test.go # Tests with real Cloud Hypervisor
86+
```
87+
88+
## Supported Versions
89+
90+
- Cloud Hypervisor v48.0 (API v0.3.0)
91+
- Cloud Hypervisor v49.0 (API v0.3.0)
92+
93+
There may be additional versions in the future. Cloud hypervisor versions may update frequently, while the API updates less frequently.
94+
95+
## Regenerating Client
96+
97+
```bash
98+
# Download latest spec (if needed)
99+
make download-ch-spec
100+
101+
# Regenerate client from spec
102+
make generate-vmm-client
103+
```
104+
105+
## Testing
106+
107+
Tests run against real Cloud Hypervisor binaries (not mocked).
108+
109+
```bash
110+
# Must be in kvm group
111+
sg kvm -c "go test ./lib/vmm/..."
112+
```
113+
114+
## Binary Extraction
115+
116+
Binaries are automatically extracted from embedded FS to:
117+
```
118+
{dataDir}/system/binaries/{version}/{arch}/cloud-hypervisor
119+
```
120+
121+
Extraction happens once per version. Subsequent calls reuse the extracted binary.
122+
123+
## API Methods
124+
125+
All Cloud Hypervisor API methods available via embedded `*ClientWithResponses`:
126+
127+
- `GetVmmPingWithResponse()` - Check VMM health
128+
- `ShutdownVMMWithResponse()` - Shutdown VMM
129+
- `CreateVMWithResponse()` - Create VM configuration
130+
- `BootVMWithResponse()` - Boot configured VM
131+
- `PauseVMWithResponse()` - Pause running VM
132+
- `ResumeVMWithResponse()` - Resume paused VM
133+
- `VmSnapshotPutWithResponse()` - Create VM snapshot
134+
- `VmRestorePutWithResponse()` - Restore from snapshot
135+
- `VmInfoGetWithResponse()` - Get VM info
136+
- And many more...
137+
138+
See generated `vmm.go` for full API surface.

lib/vmm/binaries.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package vmm
2+
3+
import (
4+
"embed"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
)
10+
11+
//go:embed binaries/cloud-hypervisor/v48.0/x86_64/cloud-hypervisor
12+
//go:embed binaries/cloud-hypervisor/v48.0/aarch64/cloud-hypervisor
13+
//go:embed binaries/cloud-hypervisor/v49.0/x86_64/cloud-hypervisor
14+
//go:embed binaries/cloud-hypervisor/v49.0/aarch64/cloud-hypervisor
15+
var binaryFS embed.FS
16+
17+
type CHVersion string
18+
19+
const (
20+
V48_0 CHVersion = "v48.0"
21+
V49_0 CHVersion = "v49.0"
22+
)
23+
24+
var SupportedVersions = []CHVersion{V48_0, V49_0}
25+
26+
// ExtractBinary extracts the embedded Cloud Hypervisor binary to the data directory
27+
func ExtractBinary(dataDir string, version CHVersion) (string, error) {
28+
arch := runtime.GOARCH
29+
if arch == "amd64" {
30+
arch = "x86_64"
31+
}
32+
33+
embeddedPath := fmt.Sprintf("binaries/cloud-hypervisor/%s/%s/cloud-hypervisor", version, arch)
34+
extractPath := filepath.Join(dataDir, "system", "binaries", string(version), arch, "cloud-hypervisor")
35+
36+
// Check if already extracted
37+
if _, err := os.Stat(extractPath); err == nil {
38+
return extractPath, nil
39+
}
40+
41+
// Create directory
42+
if err := os.MkdirAll(filepath.Dir(extractPath), 0755); err != nil {
43+
return "", fmt.Errorf("create binaries dir: %w", err)
44+
}
45+
46+
// Read embedded binary
47+
data, err := binaryFS.ReadFile(embeddedPath)
48+
if err != nil {
49+
return "", fmt.Errorf("read embedded binary: %w", err)
50+
}
51+
52+
// Write to filesystem
53+
if err := os.WriteFile(extractPath, data, 0755); err != nil {
54+
return "", fmt.Errorf("write binary: %w", err)
55+
}
56+
57+
return extractPath, nil
58+
}
59+
60+
// GetBinaryPath returns path to extracted binary, extracting if needed
61+
func GetBinaryPath(dataDir string, version CHVersion) (string, error) {
62+
return ExtractBinary(dataDir, version)
63+
}

lib/vmm/binaries/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore all binaries
2+
cloud-hypervisor
3+

0 commit comments

Comments
 (0)