- formatHWID/parseHWID with HW-NNNNN regex validation - AllocateNextHWID with optimistic-lock retry (3 attempts) - getHighestHWIDNumber scans all devices for highest existing asset_tag - hwIDExists checks specific asset_tag via DcimDevicesList filter - Unit tests for format/parse covering valid and invalid cases
99 lines
2.7 KiB
Go
99 lines
2.7 KiB
Go
package netbox
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var hwIDPattern = regexp.MustCompile(`^HW-(\d{5})$`)
|
|
|
|
// formatHWID formats an integer as a HW-XXXXX string.
|
|
func formatHWID(n int) string {
|
|
return fmt.Sprintf("HW-%05d", n)
|
|
}
|
|
|
|
// parseHWID parses a HW-XXXXX string to an integer.
|
|
// Returns error if the format does not match.
|
|
func parseHWID(s string) (int, error) {
|
|
m := hwIDPattern.FindStringSubmatch(s)
|
|
if m == nil {
|
|
return 0, fmt.Errorf("invalid HW-ID format: %q (expected HW-NNNNN)", s)
|
|
}
|
|
n, err := strconv.Atoi(m[1])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// AllocateNextHWID allocates the next available HW-XXXXX identifier.
|
|
// Strategy: optimistic locking — query the highest existing asset_tag, increment by 1,
|
|
// attempt to reserve it. Retry up to 3 times on conflict.
|
|
//
|
|
// For Phase 1, AllocateNextHWID returns the ID string without creating a device.
|
|
// The caller is responsible for creating the device record and setting asset_tag.
|
|
func (c *Client) AllocateNextHWID(ctx context.Context) (string, error) {
|
|
const maxAttempts = 3
|
|
|
|
for attempt := 0; attempt < maxAttempts; attempt++ {
|
|
highest, err := c.getHighestHWIDNumber(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("get highest HW-ID: %w", err)
|
|
}
|
|
candidate := formatHWID(highest + 1)
|
|
// Check that this candidate is not already taken
|
|
// (handles concurrent allocation if ever needed)
|
|
taken, err := c.hwIDExists(ctx, candidate)
|
|
if err != nil {
|
|
return "", fmt.Errorf("check HW-ID %s: %w", candidate, err)
|
|
}
|
|
if !taken {
|
|
return candidate, nil
|
|
}
|
|
// Candidate is taken — loop and try highest+2, etc.
|
|
}
|
|
return "", errors.New("HW-ID allocation failed after 3 attempts — concurrent allocation conflict")
|
|
}
|
|
|
|
// getHighestHWIDNumber queries NetBox for the highest existing HW-XXXXX asset_tag number.
|
|
// Returns 0 if no HW-XXXXX asset_tags exist (first allocation will be HW-00001).
|
|
func (c *Client) getHighestHWIDNumber(ctx context.Context) (int, error) {
|
|
res, _, err := c.api.DcimAPI.DcimDevicesList(ctx).
|
|
Limit(1000).
|
|
Execute()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("list devices for HW-ID query: %w", err)
|
|
}
|
|
|
|
highest := 0
|
|
for _, d := range res.Results {
|
|
tag := d.GetAssetTag()
|
|
if !strings.HasPrefix(tag, "HW-") {
|
|
continue
|
|
}
|
|
n, err := parseHWID(tag)
|
|
if err != nil {
|
|
continue // non-HWLab asset tag — skip
|
|
}
|
|
if n > highest {
|
|
highest = n
|
|
}
|
|
}
|
|
return highest, nil
|
|
}
|
|
|
|
// hwIDExists checks if a given HW-XXXXX asset_tag is already used in NetBox.
|
|
func (c *Client) hwIDExists(ctx context.Context, hwid string) (bool, error) {
|
|
res, _, err := c.api.DcimAPI.DcimDevicesList(ctx).
|
|
AssetTag([]string{hwid}).
|
|
Limit(1).
|
|
Execute()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return res.GetCount() > 0, nil
|
|
}
|