Files
harmony/opnsense-config
johnride 27accb399e
Some checks failed
Run Check Script / check (push) Failing after 2m9s
Build and push harmony-fleet-operator image / build_and_push (push) Successful in 2m2s
Compile and package harmony_composer / package_harmony_composer (push) Failing after 2m0s
Merge pull request 'feat(fleet): IoT walking skeleton + deploy-architecture refactor' (#264) from feat/iot-walking-skeleton into master
Large merge: 209 commits, +47k/-5k across 428 files. Five clusters
of work, plus a deliberate behavior change and a known-deferred
backlog spelled out below.
1. IoT walking skeleton — harmony-reconciler-contracts crate,
   harmony-fleet-operator (Deployment CRD → NATS KV), harmony-fleet-
   agent reconciling PodmanV0Score into containers, K8sBareTopology,
   maud+htmx operator frontend, request/reply commands over NATS.
2. NATS auth callout + from-scratch nats/jwt crate (algorithm,
   claims, builders, xkey). nats/callout maps Zitadel roles to
   NATS permissions; harmony-fleet-auth holds the shared credential
   plumbing.
3. Zitadel deepening — setup.rs rewritten (cross-org admin,
   persisted admin password, device-code OIDC, OKD-compat values).
   New harmony_zitadel_auth crate (axum login flow, JWKS, sessions).
   Agent gets a JWT-bearer credential source.
4. Deploy-architecture refactor per ADR-023 — Scores everywhere,
   new harmony-fleet-deploy crate, harmony-fleet-e2e harness
   covering Pod target plus VM target (aarch64 production +
   x86_64 fast iteration). First Score companion lands:
   AgentObservation (ADR-023 P7).
5. Device enrollment via Zitadel SSO + aarch64 KVM support (AAVMF
   firmware, per-VM NVRAM, TCG perf overrides), IotDeviceSetupScore
   /FleetDeviceSetupScore SSH apply, fleet-sso-login example.
Behavior change worth flagging in this merge:
  harmony_secret now panics on an unknown SECRET_STORE value
  instead of silently defaulting to Infisical. No in-tree caller
  hits this; a typo in the env var is now loud rather than
  mysterious.
Deferred (not merge blockers — file as issues post-merge):
- CI is red on master because hub.nationtech.io/harmony/
  harmony_composer:latest is missing libvirt-dev + pkg-config,
  which `cargo check --all-features` requires once harmony's `kvm`
  feature is unified. Local `build/check.sh` is green. Fix is a
  two-line Dockerfile edit + rebuild of the composer image; top
  priority post-merge so master goes green again.
- Smoke-test contract (ADR-023 P4) — the principle is locked but
  the trait/struct shape is open. Each Score implements its own
  readiness today; harmony-fleet-e2e has `wait_until_ready` as a
  per-test stand-in.
- Rust equivalents for smoke-a1.sh / smoke-a4.sh (smoke-a3* are
  superseded by the new VM e2e tests).
- Five #[ignore]'d tests in examples/fleet_e2e_demo and nats/
  integration-test-callout — waiting on a CI runner with libvirt
  + k3d + podman.
- ADR-024 (capability decomposition) — kept as draft under
  docs/adr/drafts/024- pending more conviction.
2026-05-22 22:16:16 +00:00
..

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
            )
            ...