- CableRecord type added to types.go (ID, HWID, Label, TestData, CatalogStatus) - CreateCable(ctx, label, assetTag, testDataJSON) uses DcimCablesCreate - Sets test_data and catalog_status custom fields; hw_id if assetTag non-empty - Rejects empty label with sentinel error message - Unit tests use httptest.NewUnstartedServer (201 success, 422 error, empty label)
147 lines
4.8 KiB
Go
147 lines
4.8 KiB
Go
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
|
|
}
|