Two regressions from fc16e9f that ./build/check.sh catches:
1. `opnsense-api`'s `test_haproxy_deser` example references
`resp.haproxy` on the response wrapper. The regen auto-derived the
field name as `op_nsenseha_proxy` from the struct name. Need to pass
`--api-key haproxy` to keep the wrapper key stable.
2. For enums whose wire values aren't all-lowercase (e.g. `"SSLv3"`,
`"CONNECT"`), the emitted `From<&str>` matched `s.to_lowercase()`
against the original-case wire value, which clippy flags as
unreachable ("match arm has differing case"). Lowercase the wire
value in the emitted match arm so case-insensitive matching actually
works; serialization still emits the original-case wire value
because the serde module is unaffected.
Regenerated `haproxy.rs` via
`cargo run -p opnsense-codegen -- generate --xml ... --module-name haproxy --api-key haproxy`.
`./build/check.sh` now passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses review feedback on the previous HAProxy field-default fixes:
the eight match blocks in `configure_service` that mapped loose strings
("get", "tcp", "roundrobin", ...) to generated OPNsense enum variants
were poor Rust — they duplicated the wire-value knowledge that the
codegen already has, and any new enum variant in OPNsense meant editing
every call site by hand.
- `opnsense-codegen/src/codegen.rs::generate_enum` now emits
`impl From<&str>` and `impl From<String>` for every generated enum,
right after the existing serde module. Lowercase-matches wire values;
unknown inputs fall through to the `Other(String)` variant the codegen
already emits for forward-compat round-tripping.
- `opnsense-api/src/generated/haproxy.rs` regenerated — 153 enums, 306
new impl blocks. No hand edits; re-run via
`cargo run -p opnsense-codegen -- generate --xml
opnsense-codegen/vendor/plugins/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml
--output-dir opnsense-api/src/generated --module-name haproxy`.
- `opnsense-config/src/modules/load_balancer.rs::configure_service`
replaces eight string-match blocks with one-liners:
`HealthcheckType::from(hc.check_type.as_str())` etc.
- Drive-by: fixed a pre-existing typo at
`harmony/src/infra/opnsense/load_balancer.rs:185` and the matching
reverse at `:149` — `SSL::SNI` was mapped to `"sslni"`, but the
OPNsense wire value is `"sslsni"`. Before this refactor the typo
silently hit `HealthcheckSsl::Other("sslni")`; the cleaner conversion
made the bug obvious so it's fixed here rather than left for a
follow-up.
Verification:
- `cargo check -p harmony -p opnsense-config -p opnsense-api` clean
- `cargo test -p harmony --lib okd::load_balancer` 6/6 pass
- `cargo test -p opnsense-codegen` 22/22 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverting 5e72777. The HAProxy startup failure that motivated the
bind-to-FW-IP change was environment-specific on the sttest basement
firewall: OPNsense's "HTTP → HTTPS redirect" service (lighttpd bound to
`[::]:80`, dual-stack) was holding IPv4 port 80 via v4-mapped addresses
— invisible in `sockstat -l4` but still enough to make `0.0.0.0:80`
return EADDRINUSE to HAProxy.
Disabling the HTTP redirect on that firewall resolves the conflict.
Other OPNsense deployments already ship with the redirect off (or
HAProxy on non-conflicting ports), so `0.0.0.0` remains the correct
default.
This reverts commit 5e72777.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Binding HAProxy on 0.0.0.0 collided with OPNsense's own listeners
(HTTP→HTTPS redirect on :80, WebUI, etc.), preventing the HAProxy
service from starting once the LoadBalancer score was applied.
Use `topology.load_balancer.get_ip()` to bind each frontend on the
firewall's LAN interface IP instead. The `LoadBalancer` capability was
already in scope, so no new trait imports are needed.
The previous `0.0.0.0` rationale (avoiding CARP VIP rebind races) is
noted in a comment: HA CARP setups still need OPNsense's
`net.inet.ip.nonlocal_bind` or HAProxy `transparent` bind — not
addressed here.
Test module: added an inline `DummyLoadBalancer` stub (mirrors the
existing `DummyRouter` pattern) so `OKDLoadBalancerScore::new` no longer
hits `DummyInfra::get_ip`'s `unimplemented!()` panic. Renamed
`test_all_services_bind_on_unspecified_address` →
`test_all_services_bind_on_firewall_ip`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`configure_service` was relying on `..Default::default()` for most fields
of the generated HAProxy structs. That leaked OPNsense's *model defaults*
into the wire payload for fields Harmony never meant to default:
- `http_host` → `localhost` (sent `Host: localhost` on every check)
- `http_method` → `options` (sent OPTIONS instead of the declared method)
- `http_version` → `http10` (wanted NONE)
- `sslVerify` on real servers → `1` (broke self-signed backends)
- Healthcheck `ssl` was never propagated, so SSL-required checks like
kube-apiserver `/readyz` on 6443 stayed plain HTTP and never succeeded
Set every field explicitly from `LbHealthCheck`/`LbServer`: map
`http_method` through `HealthcheckHttpMethod`, pass `None` for
`http_version` (serializes as `""` = NONE), clear `http_host` to an empty
string, propagate `hc.ssl` through `HealthcheckSsl`, and pin
`ssl`/`sslVerify` to `false` on the server struct so intent is declared
at the call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`LoadBalancerConfig::is_installed` previously collapsed every error from
the settings endpoint into `false`, so a timeout, DNS failure, or auth
rejection all looked identical to "os-haproxy not installed" — the
`LoadBalancer` score would then attempt to install the plugin on top of
an unreachable firewall and fail in cascade further down the pipeline.
Return `Result<bool, Error>` and treat only HTTP 404 (controller not
found) as "not installed". Every other error is propagated so
`ensure_initialized` fails the score immediately with a message pointing
at the real problem.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>