homelabby/.planning/phases/01-foundation/01-03-SUMMARY.md

5.5 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
01-foundation 03 netbox
go
netbox
provisioning
custom-fields
dcim
tdd
requires provides affects
internal/netbox.Client
internal/netbox.Provision
internal/netbox.ProvisionCustomFields
internal/netbox.ProvisionLocationHierarchy
internal/quality
internal/hwid
scripts/provision-netbox.go
added patterns
idempotent-provisioning
check-before-create
tdd-red-green
created modified
internal/netbox/provision.go
internal/netbox/provision_test.go
scripts/provision-netbox.go
id summary
NB-PROV-01 Use PatchedWritableCustomFieldRequestType(spec.Type) to convert string type to go-netbox enum — no separate mapping table needed
id summary
NB-PROV-02 Use Int32AsDeviceWithConfigContextRequestSite/Location helpers to pass IDs in oneOf union types for location/rack creation
id summary
NB-PROV-03 photo_urls stored as text (comma-separated) not multiselect — avoids NetBox multi-object custom field complexity
duration completed tasks_completed files_created files_modified
~20 minutes 2026-04-10T06:15:00Z 2 3 0

Phase 1 Plan 03: NetBox Provisioning Summary

One-liner: Idempotent NetBox provisioner for 8 HWLab custom fields and Site→Location→Rack hierarchy using go-netbox v4 typed API.

What Was Built

internal/netbox/provision.go

Provisioning functions implementing check-before-create idempotency:

  • CustomFieldSpec — typed struct describing a NetBox custom field to provision
  • hwlabCustomFields — canonical slice of all 8 HWLab custom field specs
  • customFieldSpec(name) — lookup by name (used in tests)
  • ProvisionCustomFields(ctx) — creates missing fields, skips existing; returns count created
  • customFieldExists(ctx, name) — checks via ExtrasCustomFieldsList().Name([]string{name})
  • createCustomField(ctx, spec) — uses WritableCustomFieldRequest with PatchedWritableCustomFieldRequestType enum
  • ProvisionLocationHierarchy(ctx) — creates Site "Homelab" → Location "Lab Bench" → Rack "Primary Rack"
  • ensureSite / ensureLocation / ensureRack — check-then-create with proper go-netbox v4 types
  • CheckNetBoxInventoryPlugin(ctx) — GET /api/plugins/ to detect netbox-inventory plugin
  • Provision(ctx) — top-level entry point calling both custom fields and location hierarchy

internal/netbox/provision_test.go

4 unit tests, all passing:

  • TestCustomFieldSpec — hw_id spec has type=text and ObjectTypes contains dcim.device
  • TestCustomFieldSpecCatalogStatus — catalog_status is type=text (not selection)
  • TestCustomFieldSpecPhotoURLs — photo_urls has a non-empty description
  • TestAllEightFieldsDefined — all 8 expected field names present, len=8

scripts/provision-netbox.go

Standalone CLI with //go:build ignore tag:

  • Loads .env via godotenv.Load()
  • Reads HWLAB_NETBOX_URL and HWLAB_NETBOX_TOKEN
  • Calls client.Provision(ctx) then client.CheckNetBoxInventoryPlugin(ctx)
  • Usage: go run scripts/provision-netbox.go

Provisioning Status

Ran against live NetBox: No — HWLAB_NETBOX_TOKEN=homelab-netbox-api-token-2024 is a placeholder (22 chars, not a real NetBox token). Integration tests correctly skip.

To provision the live NetBox instance (LXC 130):

  1. Obtain a real API token from NetBox UI at http://10.5.0.130:8000/
  2. Set HWLAB_NETBOX_TOKEN=<40-char-token> in .env
  3. Run: go run scripts/provision-netbox.go

go-netbox v4 API Notes

  • Custom field type enum: PatchedWritableCustomFieldRequestType (shared between WritableCustomFieldRequest and PatchedWritableCustomFieldRequest) — casting string directly works: nb.PatchedWritableCustomFieldRequestType("text")
  • Site/Location/Rack oneOf site field: DeviceWithConfigContextRequestSite is a oneOf union type; use nb.Int32AsDeviceWithConfigContextRequestSite(&siteID) to pass an integer ID
  • Location oneOf site field: Same pattern — DeviceWithConfigContextRequestSite used by WritableLocationRequest
  • Rack location field: NullableDeviceWithConfigContextRequestLocation — use nb.Int32AsDeviceWithConfigContextRequestLocation(&locationID) then req.SetLocation(loc)
  • DcimRacksList Name filter: DcimRacksList(ctx).Name([]string{name}) works correctly (same pattern as custom fields)
  • GetId() returns int32 on Site, Location, and Rack models — consistent

Deviations from Plan

None — plan executed exactly as written. All stub pseudocode was replaced with real go-netbox v4 API calls. The CheckNetBoxInventoryPlugin function uses the HTTP client from c.api.GetConfig().HTTPClient to avoid importing net/http separately, with the base URL derived by stripping the /api suffix from c.url.

Known Stubs

None — all functions are fully implemented. Live provisioning requires a real NetBox token (currently placeholder in .env).

Self-Check

Files created:

  • internal/netbox/provision.go: FOUND
  • internal/netbox/provision_test.go: FOUND
  • scripts/provision-netbox.go: FOUND

Commits:

  • 07130d2 — test(01-03): add failing tests for custom field spec and provisioning
  • 9b4cc9a — feat(01-03): implement custom field provisioning with go-netbox v4
  • 49a729a — feat(01-03): add location hierarchy provisioning and provision CLI script

go build ./...: PASS go vet ./...: PASS go test ./internal/netbox/... -run "TestCustomFieldSpec|TestAllEight": PASS (4 tests) Integration tests: SKIP (placeholder token — correct behavior)

Self-Check: PASSED