From 82bed8ca347e22b204bfa11ad0a5ec7b43f2c4a2 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 22:31:39 +0000 Subject: [PATCH] Update Hypeman Go SDK to v0.29.0 and CLI coverage --- go.mod | 6 ++---- go.sum | 8 ++++---- pkg/cmd/build.go | 1 + pkg/cmd/cp.go | 3 +++ pkg/cmd/ingresscmd.go | 7 ++++--- pkg/cmd/lifecycle.go | 1 + pkg/cmd/logs.go | 9 +++++---- pkg/cmd/ps.go | 5 +++-- pkg/cmd/ps_test.go | 7 +++++++ pkg/cmd/resourcecmd.go | 31 +++++++++++++++++++++++++++++-- pkg/cmd/resourcecmd_test.go | 34 ++++++++++++++++++++++++++++++++++ pkg/cmd/run.go | 30 ++++++++++++++++++------------ pkg/cmd/stats.go | 1 + 13 files changed, 112 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index 43b8ee2..88a2305 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,11 @@ require ( github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/term v0.2.1 - github.com/docker/docker v28.5.2+incompatible + github.com/docker/docker v28.5.3-0.20260325120914-0afb41ce194c+incompatible github.com/google/go-containerregistry v0.20.7 github.com/gorilla/websocket v1.5.3 github.com/itchyny/json2yaml v0.1.4 - github.com/kernel/hypeman-go v0.28.0 + github.com/kernel/hypeman-go v0.29.0 github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/providers/env v1.1.0 github.com/knadh/koanf/providers/file v1.2.1 @@ -93,5 +93,3 @@ require ( ) replace github.com/docker/cli v29.0.3+incompatible => github.com/docker/cli v29.2.0+incompatible - -replace github.com/docker/docker v28.5.2+incompatible => github.com/docker/docker v28.5.3-0.20260325120914-0afb41ce194c+incompatible diff --git a/go.sum b/go.sum index aabd839..cb7e5e4 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8= github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI= -github.com/kernel/hypeman-go v0.28.0 h1:1flQG6NfifaqrZsoU5LHydnVEUzaVdWt64/9JvEaOk8= -github.com/kernel/hypeman-go v0.28.0/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= +github.com/kernel/hypeman-go v0.29.0 h1:Q2Gci9hmBOrEd/6d3vY4x4//RbbzKNOJScbTMT3otqE= +github.com/kernel/hypeman-go v0.29.0/go.mod h1:UnkEgayRoT5fuGeo/lbpCSDIU21bIJH1UE2M7av9CIw= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -197,8 +197,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index d6d5722..3b6599e 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -25,6 +25,7 @@ var buildCmd = cli.Command{ The path argument specifies the build context directory containing the source code and Dockerfile. If not specified, the current directory is used. +Builds are unavailable when the server runs in macOS-only mode. Subcommands are available for managing builds: hypeman build list List builds diff --git a/pkg/cmd/cp.go b/pkg/cmd/cp.go index c8f18a6..a7032d3 100644 --- a/pkg/cmd/cp.go +++ b/pkg/cmd/cp.go @@ -77,6 +77,9 @@ The path format is: - Local path: /path/to/file or ./relative/path - Instance path: :/path/in/instance +Copying requires guest filesystem inspection, which is unavailable for some +guest types. + Examples: # Copy file to instance hypeman cp ./local-file.txt myinstance:/app/file.txt diff --git a/pkg/cmd/ingresscmd.go b/pkg/cmd/ingresscmd.go index 50ef004..532e1fc 100644 --- a/pkg/cmd/ingresscmd.go +++ b/pkg/cmd/ingresscmd.go @@ -27,9 +27,10 @@ var ingressCmd = cli.Command{ } var ingressCreateCmd = cli.Command{ - Name: "create", - Usage: "Create an ingress for an instance", - ArgsUsage: "", + Name: "create", + Usage: "Create an ingress for an instance", + ArgsUsage: "", + Description: "Ingress creation is unavailable when the server runs in macOS-only mode.", Flags: []cli.Flag{ &cli.StringFlag{ Name: "hostname", diff --git a/pkg/cmd/lifecycle.go b/pkg/cmd/lifecycle.go index bace063..48f8d9c 100644 --- a/pkg/cmd/lifecycle.go +++ b/pkg/cmd/lifecycle.go @@ -67,6 +67,7 @@ var restoreCmd = cli.Command{ Name: "restore", Usage: "Restore an instance from standby", ArgsUsage: "", + Description: "Standby restore is not supported for every guest type; the command returns an error when unavailable.", Action: handleRestore, HideHelpCommand: true, } diff --git a/pkg/cmd/logs.go b/pkg/cmd/logs.go index d461aae..3dd6f88 100644 --- a/pkg/cmd/logs.go +++ b/pkg/cmd/logs.go @@ -10,9 +10,10 @@ import ( ) var logsCmd = cli.Command{ - Name: "logs", - Usage: "Fetch the logs of an instance", - ArgsUsage: "", + Name: "logs", + Usage: "Fetch the logs of an instance", + ArgsUsage: "", + Description: "Available log sources depend on the guest type; requesting an unsupported source returns an error.", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "follow", @@ -27,7 +28,7 @@ var logsCmd = cli.Command{ &cli.StringFlag{ Name: "source", Aliases: []string{"s"}, - Usage: "Log source: app (default), vmm (Cloud Hypervisor), or hypeman (operations log)", + Usage: "Log source: app (default), vmm (Cloud Hypervisor), hypeman (operations log), or swtpm", }, }, Action: handleLogs, diff --git a/pkg/cmd/ps.go b/pkg/cmd/ps.go index 558f02c..e35993b 100644 --- a/pkg/cmd/ps.go +++ b/pkg/cmd/ps.go @@ -121,8 +121,9 @@ func formatGPU(gpu hypeman.InstanceGPU) string { if gpu.Profile != "" { return gpu.Profile } - // Check if mdev UUID is set (indicates vGPU without profile name shown) - if gpu.MdevUuid != "" { + // mdev hosts report a UUID, while SR-IOV hosts report the assigned + // device's sysfs path. Either indicates a vGPU without a profile name. + if gpu.MdevUuid != "" || gpu.DevicePath != "" { return "vgpu" } return "-" diff --git a/pkg/cmd/ps_test.go b/pkg/cmd/ps_test.go index ffa94d0..0f12951 100644 --- a/pkg/cmd/ps_test.go +++ b/pkg/cmd/ps_test.go @@ -34,6 +34,13 @@ func TestFormatGPU(t *testing.T) { }, expected: "vgpu", }, + { + name: "vGPU without profile but with device path", + gpu: hypeman.InstanceGPU{ + DevicePath: "/sys/bus/pci/devices/0000:82:00.4", + }, + expected: "vgpu", + }, } for _, tt := range tests { diff --git a/pkg/cmd/resourcecmd.go b/pkg/cmd/resourcecmd.go index 4aea45c..2094501 100644 --- a/pkg/cmd/resourcecmd.go +++ b/pkg/cmd/resourcecmd.go @@ -21,7 +21,10 @@ var resourcesCmd = cli.Command{ Description: `Display current host resource capacity, allocation status, and per-instance breakdown. Resources include CPU, memory, disk, network, and GPU (if available). -Oversubscription ratios are applied to calculate effective limits. +Oversubscription ratios are applied to calculate effective limits. GPU profile +availability is calculated per profile; counts across profiles are not additive. +For vGPU hosts, allocatable slots exclude quarantined VFs, and used and +quarantined counts can overlap while an affected instance still holds its VF. Examples: # Show all resources (default table format) @@ -285,8 +288,31 @@ func printGPUInfo(gpu gjson.Result) { mode := gpu.Get("mode").String() totalSlots := gpu.Get("total_slots").Int() usedSlots := gpu.Get("used_slots").Int() + allocatableSlots := gpu.Get("allocatable_slots").Int() + quarantinedSlots := gpu.Get("quarantined_slots").Int() - fmt.Printf("GPU: %s mode (%d/%d slots used)\n", mode, usedSlots, totalSlots) + if mode == "vgpu" { + fmt.Printf( + "GPU: %s mode (%d/%d slots used, %d allocatable, %d quarantined)\n", + mode, + usedSlots, + totalSlots, + allocatableSlots, + quarantinedSlots, + ) + } else { + fmt.Printf( + "GPU: %s mode (%d/%d slots used, %d allocatable)\n", + mode, + usedSlots, + totalSlots, + allocatableSlots, + ) + } + + if reason := gpu.Get("placement_disabled_reason").String(); reason != "" { + fmt.Printf("Placement disabled: %s\n", reason) + } if mode == "vgpu" { profiles := gpu.Get("profiles") @@ -301,6 +327,7 @@ func printGPUInfo(gpu gjson.Result) { fmt.Printf("%-14s %-10s %d\n", name, vram, available) return true }) + fmt.Println("Availability is per profile; counts are not additive.") } } else if mode == "passthrough" { devices := gpu.Get("devices") diff --git a/pkg/cmd/resourcecmd_test.go b/pkg/cmd/resourcecmd_test.go index 032a045..90b6070 100644 --- a/pkg/cmd/resourcecmd_test.go +++ b/pkg/cmd/resourcecmd_test.go @@ -1,10 +1,13 @@ package cmd import ( + "io" + "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" ) func TestParseReclaimBytes(t *testing.T) { @@ -37,6 +40,37 @@ func TestParseReclaimBytes(t *testing.T) { } } +func TestPrintGPUInfoShowsPlacementCapacity(t *testing.T) { + originalStdout := os.Stdout + reader, writer, err := os.Pipe() + require.NoError(t, err) + os.Stdout = writer + t.Cleanup(func() { + os.Stdout = originalStdout + _ = reader.Close() + }) + + printGPUInfo(gjson.Parse(`{ + "mode":"vgpu", + "total_slots":64, + "used_slots":5, + "allocatable_slots":57, + "quarantined_slots":2, + "placement_disabled_reason":"VF health state unavailable", + "profiles":[{"name":"L40S-1Q","framebuffer_mb":1024,"available":57}] + }`)) + + require.NoError(t, writer.Close()) + os.Stdout = originalStdout + output, err := io.ReadAll(reader) + require.NoError(t, err) + + text := string(output) + assert.Contains(t, text, "5/64 slots used, 57 allocatable, 2 quarantined") + assert.Contains(t, text, "Placement disabled: VF health state unavailable") + assert.Contains(t, text, "Availability is per profile; counts are not additive.") +} + func TestFormatBytes(t *testing.T) { tests := []struct { bytes int64 diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 38dd2a3..2940c2c 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -66,22 +66,20 @@ Examples: }, &cli.StringFlag{ Name: "platform", - Usage: `Target platform as os/arch[/variant] (e.g., "linux/amd64"). Defaults to the host platform`, + Usage: `Target platform as os/arch[/variant]. Linux supports amd64 and arm64; experimental darwin/arm64 requires an offline-imported macOS disk image and vz on Apple silicon. Omit to use the cached native image or host platform`, }, &cli.StringFlag{ Name: "memory", - Usage: `Base memory size (e.g., "1GB", "512MB")`, - Value: "1GB", + Usage: `Base memory size (e.g., "1GB", "512MB"). Linux defaults to 1GB. macOS defaults to the image memory; explicit values must be at least 4 GiB in whole MiB and within VZ host limits`, }, &cli.IntFlag{ - Name: "cpus", - Usage: "Number of virtual CPUs", - Value: 2, + Name: "cpus", + Usage: "Number of virtual CPUs. Linux defaults to 2. macOS defaults to the image vCPU count; explicit values must be at least 2 and within VZ host limits", + HideDefault: true, }, &cli.StringFlag{ Name: "overlay-size", - Usage: `Writable overlay disk size (e.g., "10GB")`, - Value: "10GB", + Usage: `Writable overlay disk size (e.g., "10GB"). Linux defaults to 10GB. Omit for macOS guests, which clone the image's complete boot disk`, }, &cli.StringFlag{ Name: "hotplug-size", @@ -254,18 +252,26 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { } } - // Build instance params + // Build instance params. Leave server-defaulted fields unset unless the user + // supplied them so macOS guests can inherit image-specific CPU and memory + // settings and clone the complete image disk without a Linux overlay. params := hypeman.InstanceNewParams{ Image: image, Name: name, - Vcpus: hypeman.Opt(int64(cmd.Int("cpus"))), - Size: hypeman.Opt(cmd.String("memory")), - OverlaySize: hypeman.Opt(cmd.String("overlay-size")), HotplugSize: hypeman.Opt(cmd.String("hotplug-size")), } if platform != "" { params.Platform = hypeman.Opt(platform) } + if cmd.IsSet("cpus") { + params.Vcpus = hypeman.Opt(int64(cmd.Int("cpus"))) + } + if cmd.IsSet("memory") { + params.Size = hypeman.Opt(cmd.String("memory")) + } + if cmd.IsSet("overlay-size") { + params.OverlaySize = hypeman.Opt(cmd.String("overlay-size")) + } if len(env) > 0 { params.Env = env diff --git a/pkg/cmd/stats.go b/pkg/cmd/stats.go index fd3fafc..b7a8d8c 100644 --- a/pkg/cmd/stats.go +++ b/pkg/cmd/stats.go @@ -15,6 +15,7 @@ var statsCmd = cli.Command{ Name: "stats", Usage: "Show live resource stats for an instance", ArgsUsage: "", + Description: "For macOS guests, received network bytes are always 0 because NAT provides no per-instance interface.", Action: handleStats, HideHelpCommand: true, }