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 }