Commit 746de92
authored
feat: CommonDevice → OpnSenseDocument reverse serializer (#9)
## Summary
opnConfigGenerator now implements the **reverse serializer** that
opnDossier doesn't ship. opnDossier parses `config.xml →
*model.CommonDevice`; this PR delivers the missing inverse direction:
`faker → *model.CommonDevice → *opnsense.OpnSenseDocument → config.xml`.
Zero-argument `opnConfigGenerator generate` emits a valid OPNsense
`config.xml` that round-trips through opnDossier's `ConvertDocument`
with zero warnings.
**Impact:** 57 files changed, +2,513 / −3,615 lines (net −1,102). One
new dependency (`gofakeit/v7`). Two new packages, one rewritten package,
two deleted packages.
**Risk level:** 🟡 Medium — architectural pivot with breaking CLI
changes; mitigated by round-trip acceptance test, byte-stability tests,
fuzz tests, and cross-platform CI green.
**Review time:** ~60–90 min (11 commits; recommend reading
commit-by-commit).
## Type of Change
- [x] **New feature** — the `CommonDevice → OpnSense` serializer is a
net-new deliverable
- [x] **Refactor** — CLI reshape + transport-only `opnsensegen`
- [x] **Breaking change** — CLI flag surface changed (see below)
- [x] **Documentation** — README, CONTRIBUTING, GOTCHAS, and new
`docs/solutions/` entry
## What Changed
### 🔧 New packages
- **`internal/faker/`** (7 files + tests) — produces
`*model.CommonDevice` via `NewCommonDevice(opts...)`. Functional
options: `WithSeed`, `WithVLANCount`, `WithFirewallRules`,
`WithHostname`, `WithDomain`, `WithDeviceType`. Target-neutral by
default. Uses `gofakeit/v7` + `math/rand/v2` PCG for seeded determinism.
- **`internal/serializer/opnsense/`** (7 files + tests) —
`Serialize(device) → *opnsense.OpnSenseDocument` and `Overlay(base,
device) → *opnsense.OpnSenseDocument` for the `--base-config` path. One
file per CommonDevice subsystem; package layout reserves an
`internal/serializer/pfsense/` sibling for a future plan.
### 🔧 Rewritten packages
- **`internal/opnsensegen/template.go`** — transport-only
(`LoadBaseConfig`, `ParseConfig`, `MarshalConfig`). `MarshalConfig`
post-processes XML to sort children of map-backed sections
(`<interfaces>`, `<dhcpd>`) alphabetically so output is byte-stable
under a fixed seed. Atomic single-write contract: encode errors leave
the destination untouched.
- **`internal/csvio/csvio.go`** — consumes `*model.CommonDevice` instead
of `[]generator.VlanConfig`. German headers preserved.
- **`cmd/generate.go`** — zero-argument emission, new flag surface,
overlay routing helper.
### 🗑️ Deleted packages
- `internal/generator/` (14 files) — dead code under the new pipeline.
- `internal/validate/` (2 files) — only validated
`generator.VlanConfig`.
### 📝 Documentation
- `README.md` reframes the project as the opnDossier reverse serializer.
- `CONTRIBUTING.md` adds an "Adding a New CommonDevice Subsystem"
playbook.
- `GOTCHAS.md` §7.1 documents the map-backed-section ordering
workaround; §7.2 documents the round-trip-must-assert-per-field lesson.
## Breaking Changes
### Removed CLI flags
`--count`, `--csv-file`, `--firewall-nr`, `--opt-counter`,
`--include-firewall-rules`, `--firewall-rule-complexity`,
`--vlan-range`, `--vpn-count`, `--nat-mappings`, `--wan-assignments`.
### New CLI surface
| Flag | Default | Purpose |
|------|---------|---------|
| `--format` | `xml` (was required) | `xml` or `csv` |
| `--vlan-count`/`-n` | `10` | Number of VLANs (0–4093) |
| `--base-config` | *(unset)* | Optional overlay source; xml-only |
| `--firewall-rules` | `false` | Emit default allow-per-interface rules
|
| `--seed` | `0` (random) | Deterministic PCG seed |
| `--hostname`, `--domain` | *(unset)* | Override faker-generated values
|
## Testing
### Round-trip contract
`TestRoundTrip` in `internal/serializer/opnsense/serializer_test.go` is
the primary acceptance gate. It exercises the full pipeline — `faker →
Serialize → MarshalConfig → ParseConfig → ConvertDocument` — and asserts
**per-field parity** (not just counts) on:
- System: Hostname, Domain, Timezone, Language
- Interfaces: Type, Virtual, Description, IPAddress, Subnet (keyed by
Name)
- VLANs: VLANIf, PhysicalIf, Description (keyed by Tag)
- DHCP: Range.From/To, Gateway, DNSServer (keyed by Interface)
- FirewallRules: Type, Description, IPProtocol, Direction, Protocol,
Log, Disabled, Tracker, Source.Address/Port/Negated,
Destination.Address/Port/Negated
opnDossier returns zero `ConversionWarning`s on every round-trip.
### Byte stability
`TestRoundTripByteStable` re-marshals the same input 20 times and
asserts byte-identical output — a direct guard for
`sortMapBackedSections` against Go's randomized map iteration.
### Determinism
`TestGenerateDeterministicSeed` compares two `--seed 42` runs
byte-for-byte. `TestNewRandZeroSeedIsRandom` samples 8 draws to drive
flake probability to ~2⁻⁵¹².
### Fuzz + boundaries
`TestNewCommonDeviceFuzzSeeds` iterates 200 distinct seeds at
`--vlan-count 8` to catch adversarial-seed regressions. CLI boundary
tests cover `--vlan-count 0`, `1`, `4094` (reject), malformed
`--base-config` XML, and overlay wholesale-replace.
### Cross-platform
CI green on `ubuntu-latest`, `macos-latest`, `windows-latest`, plus
CodeQL and DCO checks.
## Review Checklist
### Architecture
- [ ] `*model.CommonDevice` is the single intermediate representation;
no parallel types
- [ ] Faker is target-neutral (`WithDeviceType` option, not hardcoded)
- [ ] Serializer package layout (`internal/serializer/opnsense/`)
reserves pfSense sibling
- [ ] Transport (`internal/opnsensegen/`) has no generation or
serialization logic
### Correctness
- [ ] Round-trip test asserts per-field parity on every Phase 1
subsystem
- [ ] `sortMapBackedSections` covers every map-backed section in
`OpnSenseDocument` (currently `interfaces`, `dhcpd`)
- [ ] `MarshalConfig` buffers fully before first `Write` — no partial
output on encode failure
- [ ] `pickUnique*` loops return errors on exhaustion, not panic
### CLI
- [ ] Zero-argument `generate` emits valid XML to stdout
- [ ] `--base-config` only accepted with `--format xml`
- [ ] `--seed N` produces byte-identical output across invocations
- [ ] Flag help text matches actual bounds (0–4093)
### Tests
- [ ] Per-subsystem unit tests assert `require.True` on map key presence
before reading fields
- [ ] Overlay test seeds Dhcpd + Filter in base with sentinel values and
asserts they're dropped
- [ ] Consumer round-trip test enables firewall rules and asserts they
survive
### Docs
- [ ] README describes current behavior (CLI hardwires OPNsense;
DeviceType routing is planned)
- [ ] GOTCHAS captures the two non-obvious behaviors (map ordering,
silent round-trip drops)
- [ ] CONTRIBUTING has a concrete "Adding a New CommonDevice Subsystem"
procedure
## Risk Assessment
| Factor | Rating | Details |
|--------|--------|---------|
| Size | 🟡 Medium | 57 files / ~6K line-diff; offset by 1,500+ deleted
dead-code lines |
| Complexity | 🟡 Medium | New token-stream XML stabilizer +
functional-options faker |
| Test coverage | 🟢 Low | Round-trip + byte-stability + 200-seed fuzz +
boundary tests |
| Dependencies | 🟢 Low | One new pure-Go lib (`gofakeit/v7`), no CGO |
| Security | 🟢 Low | Tool generates synthetic data locally; no network
calls, no secrets |
| Breaking | 🟠 High | Removes 10 CLI flags; new project with no external
consumers — acceptable |
**Mitigations:**
1. Round-trip acceptance test fails loud on any field-level regression
(learned the hard way — see `GOTCHAS §7.2`).
2. Byte-stability test defeats Go's map-iteration randomization.
3. Fuzz test covers 200 distinct seeds at moderate VLAN count.
4. Cross-platform CI (Linux/macOS/Windows) validates stdlib behavior
differences.
5. Deferred subsystems (NAT, VPN, Users, Certs, …) are explicitly scoped
out via pending todos — see "Known residuals" below.
## Pipeline Architecture
```mermaid
flowchart LR
CLI[cmd/generate] --> F[faker.NewCommonDevice]
F --> CD[*model.CommonDevice]
CD --> S[serializer/opnsense.Serialize]
CD -- "--format csv" --> CSV[csvio.WriteVlanCSV]
S --> D[*opnsense.OpnSenseDocument]
BASE[--base-config file] --> LOAD[opnsensegen.LoadBaseConfig]
LOAD --> OV[serializer/opnsense.Overlay]
CD --> OV
OV --> D
D --> M[opnsensegen.MarshalConfig]
M --> XML[(config.xml)]
CSV --> OUT[(csv)]
```
## Known Residuals (Pending Todos)
Captured in `.context/compound-engineering/todos/` (gitignored,
local-only) for follow-up triage. **None block merge** — they were
surfaced by ce:review as `gated_auto` or `manual` and need a product
call before implementation:
| Priority | Area | What |
|----------|------|------|
| P1 | `cmd/root.go` | Orphan output file on serialize error — atomic
temp-file + rename pattern |
| P1 | `serializer/opnsense/overlay.go` | Overlay wholesale-replaces
`System.User/Group` (merge vs warn+document decision) |
| P2 | `serializer/opnsense/overlay.go` | Shallow-copy aliases caller's
base (document or deep-copy) |
| P2 | `cmd/validate.go` | `validate` subcommand still stub; wire to
opnDossier or hide |
| P2 | `cmd/generate.go` | Distinct exit codes per failure class |
| P2 | `internal/faker/rand.go` | `--seed 0` sentinel ambiguity |
| P3 | `internal/csvio/csvio.go` | Unconditional UTF-8 BOM breaks Go
`csv.Reader` |
| P3 | `cmd/root.go` | `--output -` stdout convention |
Each will be addressed in a focused follow-up PR.
## Deferred Subsystems (Follow-up Plans)
Phase 1 covers System, Interfaces, VLANs, DHCP, and default firewall
rules. One plan per subsystem, landing as separate PRs:
NAT · VPN (OpenVPN / WireGuard / IPsec) · Users/Groups ·
Certificates/CAs · IDS · HighAvailability · VirtualIPs · Bridges ·
GIF/GRE/LAGG · PPP · CaptivePortal · Kea DHCP · Monit · Netflow ·
TrafficShaper · Syslog forwarding · **pfSense target**.
## Test Plan (reviewer verification)
- [x] `just ci-check` passes locally
- [x] CI green on ubuntu/macos/windows + CodeQL + DCO + codecov
- [ ] Reviewer manually confirms `go run . generate | head` emits valid
XML
- [ ] Reviewer manually confirms `go run . generate --seed 42` twice
produces identical bytes
- [ ] Reviewer manually confirms `go run . generate --base-config
testdata/base-config.xml` preserves base fields outside Phase 1 scope
- [ ] Reviewer reads GOTCHAS §7.1 + §7.2 before approving — these are
load-bearing
## Commits (chronological)
| SHA | Subject |
|-----|---------|
| `53131f3` | chore(deps): add gofakeit/v7 for CommonDevice faker |
| `d5d8cae` | feat(faker): CommonDevice faker package |
| `0460e3d` | feat(serializer/opnsense): CommonDevice to
OpnSenseDocument serializer |
| `cd3b179` | refactor: transport-only opnsensegen; csvio consumes
CommonDevice |
| `dd0f27c` | feat(cmd/generate): zero-arg config.xml emission |
| `de4ffba` | refactor: delete generator and validate packages |
| `b100924` | docs: reframe README as CommonDevice reverse serializer |
| `54787a6` | fix: round-trip fidelity, stability hardening, and docs
from ce:review |
| `3756d96` | fix: surface faker exhaustion as error; harden XML
stabilizer and tests |
| `6b660fb` | Address PR review feedback (#9) — round 1 |
| `2f615de` | Address PR review feedback (#9) — round 2 |
---------
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>1 parent 0e8244b commit 746de92
57 files changed
Lines changed: 2515 additions & 3615 deletions
File tree
- cmd
- internal
- csvio
- faker
- generator
- opnsensegen
- serializer/opnsense
- validate
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
89 | 89 | | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
105 | 113 | | |
106 | | - | |
107 | | - | |
| 114 | + | |
| 115 | + | |
108 | 116 | | |
109 | 117 | | |
110 | 118 | | |
111 | 119 | | |
112 | | - | |
| 120 | + | |
113 | 121 | | |
114 | | - | |
| 122 | + | |
115 | 123 | | |
116 | | - | |
| 124 | + | |
117 | 125 | | |
118 | | - | |
| 126 | + | |
119 | 127 | | |
120 | | - | |
| 128 | + | |
121 | 129 | | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
127 | 141 | | |
128 | 142 | | |
129 | 143 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
21 | 35 | | |
22 | 36 | | |
23 | 37 | | |
| |||
0 commit comments