Files
harmony/opnsense-config
Sylvain Tremblay f2ab47de6e
Some checks failed
Run Check Script / check (pull_request) Failing after 32s
fix(opnsense-config): preserve symlinks in upload_folder
`tokio::fs::DirEntry::file_type()` does not follow symlinks, so the
existing is_file()/is_dir() branches both returned false for them and
the loop silently skipped every symlink.

Caller-visible consequence: uploading
./data/okd/installer_image/ (three versioned SCOS files + three
stable-name symlinks pointing at them) ended up with only the
versioned files on the firewall under /usr/local/http/scos/. The
byMAC iPXE files chainload via the stable names
(scos-live-kernel.x86_64 etc.), so PXE boots dangled on a 404 until
an operator created the symlinks by hand.

Adds a third is_symlink() branch that reads the link target with
tokio::fs::read_link and recreates it remotely via `ln -sfn` over
SSH. `ln` rather than SFTP SSH_FXP_SYMLINK because OpenSSH's server
inverts the (path, target) argument order versus the spec — `ln` is
unambiguous across shells (including the firewall's tcsh).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:03:10 -04: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
            )
            ...