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