diff --git a/.planning/phases/01-foundation/01-03-SUMMARY.md b/.planning/phases/01-foundation/01-03-SUMMARY.md new file mode 100644 index 0000000..3ac8978 --- /dev/null +++ b/.planning/phases/01-foundation/01-03-SUMMARY.md @@ -0,0 +1,111 @@ +--- +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