From 6040ecc3cca17228da762975d2ee4b1622746c5b Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Fri, 10 Apr 2026 05:42:51 +0000 Subject: [PATCH] feat(02-01): install go-openai and add CreateDevice to NetBox client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - go get github.com/sashabaranov/go-openai v1.41.2 - Add CreateDevice(ctx, name, assetTag, deviceTypeID, roleID, siteID) → (int64, error) - Add DeleteDevice(ctx, id) for test cleanup - Use Int32As* oneOf helpers for go-netbox v4 FK fields - TestCreateDeviceValidation PASS; TestCreateDeviceLive SKIP (no live token) --- go.mod | 1 + go.sum | 2 ++ internal/netbox/client.go | 35 +++++++++++++++++++++ internal/netbox/client_test.go | 56 ++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+) diff --git a/go.mod b/go.mod index b2698ec..f9200b2 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/redis/go-redis/v9 v9.18.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sashabaranov/go-openai v1.41.2 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect diff --git a/go.sum b/go.sum index dd066ba..4de2033 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM= +github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= diff --git a/internal/netbox/client.go b/internal/netbox/client.go index d29fccb..b379dec 100644 --- a/internal/netbox/client.go +++ b/internal/netbox/client.go @@ -72,6 +72,41 @@ func (c *Client) GetDevice(ctx context.Context, id int) (*Device, error) { return &dev, nil } +// CreateDevice creates a new device in NetBox with the given name and asset tag. +// deviceTypeID, roleID, and siteID must be valid NetBox IDs (pre-existing objects). +// Returns the new device's NetBox ID or error. +func (c *Client) CreateDevice(ctx context.Context, name, assetTag string, deviceTypeID, roleID, siteID int32) (int64, error) { + if name == "" { + return 0, fmt.Errorf("device name must not be empty") + } + // go-netbox v4 uses oneOf wrapper types for FK fields — use the Int32As* helpers. + dtID := nb.Int32AsDeviceBayTemplateRequestDeviceType(&deviceTypeID) + role := nb.Int32AsDeviceWithConfigContextRequestRole(&roleID) + site := nb.Int32AsDeviceWithConfigContextRequestSite(&siteID) + + req := nb.NewWritableDeviceWithConfigContextRequest(dtID, role, site) + req.SetName(name) + if assetTag != "" { + req.SetAssetTag(assetTag) + } + result, _, err := c.api.DcimAPI.DcimDevicesCreate(ctx). + WritableDeviceWithConfigContextRequest(*req).Execute() + if err != nil { + return 0, fmt.Errorf("CreateDevice %q: %w", name, err) + } + return int64(result.GetId()), nil +} + +// DeleteDevice removes a device from NetBox by its internal ID. +// Used primarily for test cleanup after CreateDevice integration tests. +func (c *Client) DeleteDevice(ctx context.Context, id int64) error { + _, err := c.api.DcimAPI.DcimDevicesDestroy(ctx, int32(id)).Execute() + if err != nil { + return fmt.Errorf("DeleteDevice %d: %w", id, err) + } + return nil +} + // deviceFromNetBox maps a go-netbox DeviceWithConfigContext to our Device type. // Custom fields are mapped separately via ParseCustomFields. func deviceFromNetBox(d nb.DeviceWithConfigContext) Device { diff --git a/internal/netbox/client_test.go b/internal/netbox/client_test.go index e9d4693..e5d7363 100644 --- a/internal/netbox/client_test.go +++ b/internal/netbox/client_test.go @@ -2,6 +2,7 @@ package netbox_test import ( "context" + "fmt" "os" "testing" @@ -60,3 +61,58 @@ func TestListDevicesLive(t *testing.T) { t.Logf("found %d devices in NetBox", len(devices)) // Not asserting count — NetBox may be empty; just assert no error } + +// TestCreateDeviceValidation verifies that CreateDevice rejects an empty name +// without making any NetBox API call. +func TestCreateDeviceValidation(t *testing.T) { + c, err := netbox.NewClient("http://10.5.0.130:8000/api", "sometoken") + if err != nil { + t.Fatalf("NewClient: %v", err) + } + _, err = c.CreateDevice(context.Background(), "", "HW-00001", 1, 1, 1) + if err == nil { + t.Error("expected error for empty device name") + } +} + +// TestCreateDeviceLive creates a real device in NetBox and deletes it as cleanup. +// Requires HWLAB_NETBOX_TOKEN (40 chars) and HWLAB_TEST_SITE_ID to be set. +func TestCreateDeviceLive(t *testing.T) { + token := integrationToken(t) + siteIDStr := os.Getenv("HWLAB_TEST_SITE_ID") + if siteIDStr == "" { + t.Skip("HWLAB_TEST_SITE_ID not set — skipping live CreateDevice test") + } + + // Parse site ID + var siteID int32 + if _, err := fmt.Sscanf(siteIDStr, "%d", &siteID); err != nil { + t.Fatalf("HWLAB_TEST_SITE_ID is not a valid int: %v", err) + } + + // These must exist in the NetBox instance used for integration testing. + // Device type 1 and role 1 are typically pre-provisioned. + const ( + testDeviceTypeID int32 = 1 + testRoleID int32 = 1 + ) + + c, err := netbox.NewClient("http://10.5.0.130:8000/api", token) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + + id, err := c.CreateDevice(context.Background(), "hwlab-test-device", "HW-TEST-01", testDeviceTypeID, testRoleID, siteID) + if err != nil { + t.Fatalf("CreateDevice: %v", err) + } + if id <= 0 { + t.Errorf("expected positive device ID, got %d", id) + } + t.Logf("created device id=%d — will clean up", id) + + // Cleanup: delete the test device + if err := c.DeleteDevice(context.Background(), id); err != nil { + t.Logf("cleanup warning: DeleteDevice(%d): %v", id, err) + } +}