package netbox import ( "context" "errors" "fmt" nb "github.com/netbox-community/go-netbox/v4" ) // Client wraps go-netbox v4 APIClient with typed HWLab methods. // All NetBox calls MUST go through this Client — no direct go-netbox calls in other packages. type Client struct { api *nb.APIClient url string } // NewClient creates a configured NetBox client. Returns error if url or token is empty. func NewClient(url, token string) (*Client, error) { if url == "" { return nil, errors.New("netbox url is required") } if token == "" { return nil, errors.New("netbox token is required") } // Note: NewAPIClientFor accepts the base URL WITHOUT /api suffix. // Strip trailing /api if present to avoid double-appending. baseURL := url if len(baseURL) > 4 && baseURL[len(baseURL)-4:] == "/api" { baseURL = baseURL[:len(baseURL)-4] } api := nb.NewAPIClientFor(baseURL, token) return &Client{api: api, url: url}, nil } // Ping verifies the NetBox API is reachable by making a lightweight list call. // Returns nil on success. func (c *Client) Ping(ctx context.Context) error { _, resp, err := c.api.DcimAPI.DcimDevicesList(ctx).Limit(1).Execute() if err != nil { return fmt.Errorf("netbox ping: %w", err) } if resp.StatusCode >= 500 { return fmt.Errorf("netbox ping: server error %d", resp.StatusCode) } return nil } // ListDevices returns up to limit devices from NetBox. func (c *Client) ListDevices(ctx context.Context, limit int) ([]Device, error) { if limit <= 0 { limit = 50 } res, _, err := c.api.DcimAPI.DcimDevicesList(ctx).Limit(int32(limit)).Execute() if err != nil { return nil, fmt.Errorf("list devices: %w", err) } devices := make([]Device, 0, len(res.Results)) for _, d := range res.Results { devices = append(devices, deviceFromNetBox(d)) } return devices, nil } // GetDevice retrieves a single device by its NetBox internal ID. func (c *Client) GetDevice(ctx context.Context, id int) (*Device, error) { d, _, err := c.api.DcimAPI.DcimDevicesRetrieve(ctx, int32(id)).Execute() if err != nil { return nil, fmt.Errorf("get device %d: %w", id, err) } dev := deviceFromNetBox(*d) 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 } // CreateCable creates a new cable record in NetBox with the given label, optional asset tag, // and test data JSON. Returns the new cable's NetBox ID or an error. // The test_data and catalog_status custom fields are set from testDataJSON and "complete". func (c *Client) CreateCable(ctx context.Context, label, assetTag, testDataJSON string) (int64, error) { if label == "" { return 0, fmt.Errorf("cable label must not be empty") } req := nb.NewWritableCableRequest() req.SetLabel(label) customFields := map[string]interface{}{ "test_data": testDataJSON, "catalog_status": "complete", } if assetTag != "" { customFields["hw_id"] = assetTag } req.SetCustomFields(customFields) result, _, err := c.api.DcimAPI.DcimCablesCreate(ctx). WritableCableRequest(*req).Execute() if err != nil { return 0, fmt.Errorf("CreateCable %q: %w", label, 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 { dev := Device{ ID: int(d.GetId()), Name: d.GetName(), } if tag := d.GetAssetTag(); tag != "" { dev.AssetTag = tag } dev.CustomFields = ParseCustomFields(d.GetCustomFields()) return dev }