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>
Supporting a new field in OPNSense config.xml
Two steps:
- Supporting the field in
opnsense-config-xml - Enabling Harmony to control the field
We'll use the filename field in the dhcpcd section of the file as an example.
Supporting the field
As type checking if enforced, every field from config.xml must be known by the code. Each subsection of config.xml has its .rs file. For the dhcpcd section, we'll modify opnsense-config-xml/src/data/dhcpd.rs.
When a new field appears in the xml file, an error like this will be thrown and Harmony will panic :
Running `/home/stremblay/nt/dir/harmony/target/debug/example-nanodc`
Found unauthorized element filename
thread 'main' panicked at opnsense-config-xml/src/data/opnsense.rs:54:14:
OPNSense received invalid string, should be full XML: ()
Define the missing field (filename) in the DhcpInterface struct of opnsense-config-xml/src/data/dhcpd.rs:
pub struct DhcpInterface {
...
pub filename: Option<String>,
Harmony should now be fixed, build and run.
Controlling the field
Define the xml field setter in opnsense-config/src/modules/dhcpd.rs.
impl<'a> DhcpConfig<'a> {
...
pub fn set_filename(&mut self, filename: &str) {
self.enable_netboot();
self.get_lan_dhcpd().filename = Some(filename.to_string());
}
...
Define the value setter in the DhcpServer trait in domain/topology/network.rs
#[async_trait]
pub trait DhcpServer: Send + Sync {
...
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>;
...
Implement the value setter in each DhcpServer implementation.
infra/opnsense/dhcp.rs:
#[async_trait]
impl DhcpServer for OPNSenseFirewall {
...
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_filename(filename);
debug!("OPNsense dhcp server set filename {filename}");
}
Ok(())
}
...
domain/topology/ha_cluster.rs
#[async_trait]
impl DhcpServer for DummyInfra {
...
async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
...
Add the new field to the DhcpScore in modules/dhcp.rs
pub struct DhcpScore {
...
pub filename: Option<String>,
Define it in its implementation in modules/okd/dhcp.rs
impl OKDDhcpScore {
...
Self {
dhcp_score: DhcpScore {
...
filename: Some("undionly.kpxe".to_string()),
Define it in its implementation in modules/okd/bootstrap_dhcp.rs
impl OKDDhcpScore {
...
Self {
dhcp_score: DhcpScore::new(
...
Some("undionly.kpxe".to_string()),
Update the interpret (function called by the execute fn of the interpret) so it now updates the filename field value in modules/dhcp.rs
impl DhcpInterpret {
...
let filename_outcome = match &self.score.filename {
Some(filename) => {
let dhcp_server = Arc::new(topology.dhcp_server.clone());
dhcp_server.set_filename(&filename).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filename to {filename}"),
)
}
None => Outcome::noop(),
};
if next_server_outcome.status == InterpretStatus::NOOP
&& boot_filename_outcome.status == InterpretStatus::NOOP
&& filename_outcome.status == InterpretStatus::NOOP
...
Ok(Outcome::new(
InterpretStatus::SUCCESS,
format!(
"Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}]",
self.score.boot_filename, self.score.boot_filename, self.score.filename
)
...