Skip to content

Commit 067a01b

Browse files
feat: gpu passthrough
1 parent 8ab31e8 commit 067a01b

File tree

7 files changed

+357
-6
lines changed

7 files changed

+357
-6
lines changed

.stats.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
configured_endpoints: 24
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-8fded10e90df28c07b64a92d12d665d54749b9fc13c35520667637fc596957d9.yml
3-
openapi_spec_hash: 7374a732372bddf7f2c0b532b56ae3fb
4-
config_hash: 510018ffa6ad6a17875954f66fe69598
1+
configured_endpoints: 29
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-95df8b193133def744aa61dc372f286663ffc20d833488d242fa288af65adc39.yml
3+
openapi_spec_hash: 833120a235ecb298688c2fb1122b3574
4+
config_hash: d34daaeca7d2ee972fa0b30a6a292465

api.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ Methods:
6565
- <code title="delete /volumes/{id}">client.Volumes.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#VolumeService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) <a href="https://pkg.go.dev/builtin#error">error</a></code>
6666
- <code title="get /volumes/{id}">client.Volumes.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#VolumeService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#Volume">Volume</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
6767

68+
# Devices
69+
70+
Response Types:
71+
72+
- <a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#AvailableDevice">AvailableDevice</a>
73+
- <a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#Device">Device</a>
74+
- <a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#DeviceType">DeviceType</a>
75+
76+
Methods:
77+
78+
- <code title="post /devices">client.Devices.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#DeviceService.New">New</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#DeviceNewParams">DeviceNewParams</a>) (<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#Device">Device</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
79+
- <code title="get /devices/{id}">client.Devices.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#DeviceService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#Device">Device</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
80+
- <code title="get /devices">client.Devices.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#DeviceService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#Device">Device</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
81+
- <code title="delete /devices/{id}">client.Devices.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#DeviceService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) <a href="https://pkg.go.dev/builtin#error">error</a></code>
82+
- <code title="get /devices/available">client.Devices.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#DeviceService.ListAvailable">ListAvailable</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/onkernel/hypeman-go#AvailableDevice">AvailableDevice</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
83+
6884
# Ingresses
6985

7086
Params Types:

client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Client struct {
2121
Images ImageService
2222
Instances InstanceService
2323
Volumes VolumeService
24+
Devices DeviceService
2425
Ingresses IngressService
2526
}
2627

@@ -50,6 +51,7 @@ func NewClient(opts ...option.RequestOption) (r Client) {
5051
r.Images = NewImageService(opts...)
5152
r.Instances = NewInstanceService(opts...)
5253
r.Volumes = NewVolumeService(opts...)
54+
r.Devices = NewDeviceService(opts...)
5355
r.Ingresses = NewIngressService(opts...)
5456

5557
return

device.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
package hypeman
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"slices"
11+
"time"
12+
13+
"github.com/onkernel/hypeman-go/internal/apijson"
14+
"github.com/onkernel/hypeman-go/internal/requestconfig"
15+
"github.com/onkernel/hypeman-go/option"
16+
"github.com/onkernel/hypeman-go/packages/param"
17+
"github.com/onkernel/hypeman-go/packages/respjson"
18+
)
19+
20+
// DeviceService contains methods and other services that help with interacting
21+
// with the hypeman API.
22+
//
23+
// Note, unlike clients, this service does not read variables from the environment
24+
// automatically. You should not instantiate this service directly, and instead use
25+
// the [NewDeviceService] method instead.
26+
type DeviceService struct {
27+
Options []option.RequestOption
28+
}
29+
30+
// NewDeviceService generates a new service that applies the given options to each
31+
// request. These options are applied after the parent client's options (if there
32+
// is one), and before any request-specific options.
33+
func NewDeviceService(opts ...option.RequestOption) (r DeviceService) {
34+
r = DeviceService{}
35+
r.Options = opts
36+
return
37+
}
38+
39+
// Register a device for passthrough
40+
func (r *DeviceService) New(ctx context.Context, body DeviceNewParams, opts ...option.RequestOption) (res *Device, err error) {
41+
opts = slices.Concat(r.Options, opts)
42+
path := "devices"
43+
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
44+
return
45+
}
46+
47+
// Get device details
48+
func (r *DeviceService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Device, err error) {
49+
opts = slices.Concat(r.Options, opts)
50+
if id == "" {
51+
err = errors.New("missing required id parameter")
52+
return
53+
}
54+
path := fmt.Sprintf("devices/%s", id)
55+
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
56+
return
57+
}
58+
59+
// List registered devices
60+
func (r *DeviceService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Device, err error) {
61+
opts = slices.Concat(r.Options, opts)
62+
path := "devices"
63+
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
64+
return
65+
}
66+
67+
// Unregister device
68+
func (r *DeviceService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) {
69+
opts = slices.Concat(r.Options, opts)
70+
opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...)
71+
if id == "" {
72+
err = errors.New("missing required id parameter")
73+
return
74+
}
75+
path := fmt.Sprintf("devices/%s", id)
76+
err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...)
77+
return
78+
}
79+
80+
// Discover passthrough-capable devices on host
81+
func (r *DeviceService) ListAvailable(ctx context.Context, opts ...option.RequestOption) (res *[]AvailableDevice, err error) {
82+
opts = slices.Concat(r.Options, opts)
83+
path := "devices/available"
84+
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
85+
return
86+
}
87+
88+
type AvailableDevice struct {
89+
// PCI device ID (hex)
90+
DeviceID string `json:"device_id,required"`
91+
// IOMMU group number
92+
IommuGroup int64 `json:"iommu_group,required"`
93+
// PCI address
94+
PciAddress string `json:"pci_address,required"`
95+
// PCI vendor ID (hex)
96+
VendorID string `json:"vendor_id,required"`
97+
// Currently bound driver (null if none)
98+
CurrentDriver string `json:"current_driver,nullable"`
99+
// Human-readable device name
100+
DeviceName string `json:"device_name"`
101+
// Human-readable vendor name
102+
VendorName string `json:"vendor_name"`
103+
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
104+
JSON struct {
105+
DeviceID respjson.Field
106+
IommuGroup respjson.Field
107+
PciAddress respjson.Field
108+
VendorID respjson.Field
109+
CurrentDriver respjson.Field
110+
DeviceName respjson.Field
111+
VendorName respjson.Field
112+
ExtraFields map[string]respjson.Field
113+
raw string
114+
} `json:"-"`
115+
}
116+
117+
// Returns the unmodified JSON received from the API
118+
func (r AvailableDevice) RawJSON() string { return r.JSON.raw }
119+
func (r *AvailableDevice) UnmarshalJSON(data []byte) error {
120+
return apijson.UnmarshalRoot(data, r)
121+
}
122+
123+
type Device struct {
124+
// Auto-generated unique identifier (CUID2 format)
125+
ID string `json:"id,required"`
126+
// Whether the device is currently bound to the vfio-pci driver, which is required
127+
// for VM passthrough.
128+
//
129+
// - true: Device is bound to vfio-pci and ready for (or currently in use by) a VM.
130+
// The device's native driver has been unloaded.
131+
// - false: Device is using its native driver (e.g., nvidia) or no driver. Hypeman
132+
// will automatically bind to vfio-pci when attaching to an instance.
133+
BoundToVfio bool `json:"bound_to_vfio,required"`
134+
// Registration timestamp (RFC3339)
135+
CreatedAt time.Time `json:"created_at,required" format:"date-time"`
136+
// PCI device ID (hex)
137+
DeviceID string `json:"device_id,required"`
138+
// IOMMU group number
139+
IommuGroup int64 `json:"iommu_group,required"`
140+
// PCI address
141+
PciAddress string `json:"pci_address,required"`
142+
// Type of PCI device
143+
//
144+
// Any of "gpu", "pci".
145+
Type DeviceType `json:"type,required"`
146+
// PCI vendor ID (hex)
147+
VendorID string `json:"vendor_id,required"`
148+
// Instance ID if attached
149+
AttachedTo string `json:"attached_to,nullable"`
150+
// Device name (user-provided or auto-generated from PCI address)
151+
Name string `json:"name"`
152+
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
153+
JSON struct {
154+
ID respjson.Field
155+
BoundToVfio respjson.Field
156+
CreatedAt respjson.Field
157+
DeviceID respjson.Field
158+
IommuGroup respjson.Field
159+
PciAddress respjson.Field
160+
Type respjson.Field
161+
VendorID respjson.Field
162+
AttachedTo respjson.Field
163+
Name respjson.Field
164+
ExtraFields map[string]respjson.Field
165+
raw string
166+
} `json:"-"`
167+
}
168+
169+
// Returns the unmodified JSON received from the API
170+
func (r Device) RawJSON() string { return r.JSON.raw }
171+
func (r *Device) UnmarshalJSON(data []byte) error {
172+
return apijson.UnmarshalRoot(data, r)
173+
}
174+
175+
// Type of PCI device
176+
type DeviceType string
177+
178+
const (
179+
DeviceTypeGPU DeviceType = "gpu"
180+
DeviceTypePci DeviceType = "pci"
181+
)
182+
183+
type DeviceNewParams struct {
184+
// PCI address of the device (required, e.g., "0000:a2:00.0")
185+
PciAddress string `json:"pci_address,required"`
186+
// Optional globally unique device name. If not provided, a name is auto-generated
187+
// from the PCI address (e.g., "pci-0000-a2-00-0")
188+
Name param.Opt[string] `json:"name,omitzero"`
189+
paramObj
190+
}
191+
192+
func (r DeviceNewParams) MarshalJSON() (data []byte, err error) {
193+
type shadow DeviceNewParams
194+
return param.MarshalObject(r, (*shadow)(&r))
195+
}
196+
func (r *DeviceNewParams) UnmarshalJSON(data []byte) error {
197+
return apijson.UnmarshalRoot(data, r)
198+
}

device_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
package hypeman_test
4+
5+
import (
6+
"context"
7+
"errors"
8+
"os"
9+
"testing"
10+
11+
"github.com/onkernel/hypeman-go"
12+
"github.com/onkernel/hypeman-go/internal/testutil"
13+
"github.com/onkernel/hypeman-go/option"
14+
)
15+
16+
func TestDeviceNewWithOptionalParams(t *testing.T) {
17+
t.Skip("Prism tests are disabled")
18+
baseURL := "http://localhost:4010"
19+
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
20+
baseURL = envURL
21+
}
22+
if !testutil.CheckTestServer(t, baseURL) {
23+
return
24+
}
25+
client := hypeman.NewClient(
26+
option.WithBaseURL(baseURL),
27+
option.WithAPIKey("My API Key"),
28+
)
29+
_, err := client.Devices.New(context.TODO(), hypeman.DeviceNewParams{
30+
PciAddress: "0000:a2:00.0",
31+
Name: hypeman.String("l4-gpu"),
32+
})
33+
if err != nil {
34+
var apierr *hypeman.Error
35+
if errors.As(err, &apierr) {
36+
t.Log(string(apierr.DumpRequest(true)))
37+
}
38+
t.Fatalf("err should be nil: %s", err.Error())
39+
}
40+
}
41+
42+
func TestDeviceGet(t *testing.T) {
43+
t.Skip("Prism tests are disabled")
44+
baseURL := "http://localhost:4010"
45+
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
46+
baseURL = envURL
47+
}
48+
if !testutil.CheckTestServer(t, baseURL) {
49+
return
50+
}
51+
client := hypeman.NewClient(
52+
option.WithBaseURL(baseURL),
53+
option.WithAPIKey("My API Key"),
54+
)
55+
_, err := client.Devices.Get(context.TODO(), "id")
56+
if err != nil {
57+
var apierr *hypeman.Error
58+
if errors.As(err, &apierr) {
59+
t.Log(string(apierr.DumpRequest(true)))
60+
}
61+
t.Fatalf("err should be nil: %s", err.Error())
62+
}
63+
}
64+
65+
func TestDeviceList(t *testing.T) {
66+
t.Skip("Prism tests are disabled")
67+
baseURL := "http://localhost:4010"
68+
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
69+
baseURL = envURL
70+
}
71+
if !testutil.CheckTestServer(t, baseURL) {
72+
return
73+
}
74+
client := hypeman.NewClient(
75+
option.WithBaseURL(baseURL),
76+
option.WithAPIKey("My API Key"),
77+
)
78+
_, err := client.Devices.List(context.TODO())
79+
if err != nil {
80+
var apierr *hypeman.Error
81+
if errors.As(err, &apierr) {
82+
t.Log(string(apierr.DumpRequest(true)))
83+
}
84+
t.Fatalf("err should be nil: %s", err.Error())
85+
}
86+
}
87+
88+
func TestDeviceDelete(t *testing.T) {
89+
t.Skip("Prism tests are disabled")
90+
baseURL := "http://localhost:4010"
91+
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
92+
baseURL = envURL
93+
}
94+
if !testutil.CheckTestServer(t, baseURL) {
95+
return
96+
}
97+
client := hypeman.NewClient(
98+
option.WithBaseURL(baseURL),
99+
option.WithAPIKey("My API Key"),
100+
)
101+
err := client.Devices.Delete(context.TODO(), "id")
102+
if err != nil {
103+
var apierr *hypeman.Error
104+
if errors.As(err, &apierr) {
105+
t.Log(string(apierr.DumpRequest(true)))
106+
}
107+
t.Fatalf("err should be nil: %s", err.Error())
108+
}
109+
}
110+
111+
func TestDeviceListAvailable(t *testing.T) {
112+
t.Skip("Prism tests are disabled")
113+
baseURL := "http://localhost:4010"
114+
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
115+
baseURL = envURL
116+
}
117+
if !testutil.CheckTestServer(t, baseURL) {
118+
return
119+
}
120+
client := hypeman.NewClient(
121+
option.WithBaseURL(baseURL),
122+
option.WithAPIKey("My API Key"),
123+
)
124+
_, err := client.Devices.ListAvailable(context.TODO())
125+
if err != nil {
126+
var apierr *hypeman.Error
127+
if errors.As(err, &apierr) {
128+
t.Log(string(apierr.DumpRequest(true)))
129+
}
130+
t.Fatalf("err should be nil: %s", err.Error())
131+
}
132+
}

instance.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ type InstanceNewParams struct {
354354
Size param.Opt[string] `json:"size,omitzero"`
355355
// Number of virtual CPUs
356356
Vcpus param.Opt[int64] `json:"vcpus,omitzero"`
357+
// Device IDs or names to attach for GPU/PCI passthrough
358+
Devices []string `json:"devices,omitzero"`
357359
// Environment variables
358360
Env map[string]string `json:"env,omitzero"`
359361
// Network configuration for the instance

0 commit comments

Comments
 (0)