forked from NationTech/harmony
feat(opnsense-config): Public API now complete for dhcp add_static_mapping and remove_static_mapping, not perfect but good enough to move forward
This commit is contained in:
parent
b14d0ab686
commit
d30e909b83
151
harmony-rs/Cargo.lock
generated
151
harmony-rs/Cargo.lock
generated
@ -58,6 +58,19 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"const-random",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -203,6 +216,9 @@ name = "bitflags"
|
|||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitvec"
|
name = "bitvec"
|
||||||
@ -339,6 +355,26 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||||
|
dependencies = [
|
||||||
|
"const-random-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random-macro"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"tiny-keccak",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@ -373,6 +409,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-bigint"
|
name = "crypto-bigint"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@ -627,6 +669,18 @@ dependencies = [
|
|||||||
"miniz_oxide 0.8.0",
|
"miniz_oxide 0.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flurry"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf5efcf77a4da27927d3ab0509dec5b0954bb3bc59da5a1de9e52642ebd4cdf9"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"num_cpus",
|
||||||
|
"parking_lot",
|
||||||
|
"seize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -1099,6 +1153,16 @@ version = "0.4.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.22"
|
version = "0.4.22"
|
||||||
@ -1228,6 +1292,16 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.4"
|
version = "0.36.4"
|
||||||
@ -1305,7 +1379,9 @@ dependencies = [
|
|||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"russh",
|
"russh",
|
||||||
"russh-keys",
|
"russh-keys",
|
||||||
|
"russh-sftp",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@ -1364,6 +1440,29 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "password-hash"
|
name = "password-hash"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@ -1582,6 +1681,15 @@ dependencies = [
|
|||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.6"
|
version = "1.10.6"
|
||||||
@ -1786,6 +1894,24 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "russh-sftp"
|
||||||
|
version = "2.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a72c8afe2041c17435eecd85d0b7291841486fd3d1c4082e0b212e5437ca42"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"flurry",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ipmi"
|
name = "rust-ipmi"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -1862,6 +1988,12 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scrypt"
|
name = "scrypt"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -1910,6 +2042,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "seize"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "689224d06523904ebcc9b482c6a3f4f7fb396096645c4cd10c0d2ff7371a34d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
@ -1938,9 +2076,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.128"
|
version = "1.0.133"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -2193,6 +2331,15 @@ dependencies = [
|
|||||||
"syn 2.0.77",
|
"syn 2.0.77",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-keccak"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||||
|
dependencies = [
|
||||||
|
"crunchy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
mod xml_utils;
|
mod xml_utils;
|
||||||
mod data;
|
mod data;
|
||||||
pub use data::*;
|
pub use data::*;
|
||||||
|
pub use yaserde::MaybeString;
|
||||||
|
|||||||
@ -16,6 +16,8 @@ async-trait = { workspace = true }
|
|||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
opnsense-config-xml = { path = "../opnsense-config-xml" }
|
opnsense-config-xml = { path = "../opnsense-config-xml" }
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
|
russh-sftp = "2.0.6"
|
||||||
|
serde_json = "1.0.133"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{error::Error, modules::dhcp::DhcpConfig};
|
use crate::{error::Error, modules::dhcp::DhcpConfig};
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use opnsense_config_xml::OPNsense;
|
use opnsense_config_xml::OPNsense;
|
||||||
|
|
||||||
use super::ConfigManager;
|
use super::{ConfigManager, OPNsenseShell};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
opnsense: OPNsense,
|
opnsense: OPNsense,
|
||||||
repository: Box<dyn ConfigManager + Send + Sync>,
|
repository: Arc<dyn ConfigManager>,
|
||||||
|
shell: Arc<dyn OPNsenseShell>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub async fn new(repository: Box<dyn ConfigManager + Send + Sync>) -> Result<Self, Error> {
|
pub async fn new(
|
||||||
|
repository: Arc<dyn ConfigManager>,
|
||||||
|
shell: Arc<dyn OPNsenseShell>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
let xml = repository.load_as_str().await?;
|
let xml = repository.load_as_str().await?;
|
||||||
trace!("xml {}", xml);
|
trace!("xml {}", xml);
|
||||||
|
|
||||||
@ -20,21 +26,24 @@ impl Config {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
opnsense,
|
opnsense,
|
||||||
repository,
|
repository,
|
||||||
|
shell,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dhcp(&mut self) -> DhcpConfig {
|
pub fn dhcp(&mut self) -> DhcpConfig {
|
||||||
DhcpConfig::new(&mut self.opnsense)
|
DhcpConfig::new(&mut self.opnsense, self.shell.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn apply(&self) -> Result<(), Error> {
|
pub async fn apply(&self) -> Result<(), Error> {
|
||||||
self.repository.apply_new_config(&self.opnsense.to_xml()).await
|
self.repository
|
||||||
|
.apply_new_config(&self.opnsense.to_xml())
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::config::LocalFileConfigManager;
|
use crate::config::{DummyOPNSenseShell, LocalFileConfigManager};
|
||||||
use crate::modules::dhcp::DhcpConfig;
|
use crate::modules::dhcp::DhcpConfig;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
@ -55,9 +64,10 @@ mod tests {
|
|||||||
|
|
||||||
let config_file_path = test_file_path.to_str().unwrap().to_string();
|
let config_file_path = test_file_path.to_str().unwrap().to_string();
|
||||||
println!("File path {config_file_path}");
|
println!("File path {config_file_path}");
|
||||||
let repository = Box::new(LocalFileConfigManager::new(config_file_path));
|
let repository = Arc::new(LocalFileConfigManager::new(config_file_path));
|
||||||
|
let shell = Arc::new(DummyOPNSenseShell {});
|
||||||
let config_file_str = repository.load_as_str().await.unwrap();
|
let config_file_str = repository.load_as_str().await.unwrap();
|
||||||
let config = Config::new(repository)
|
let config = Config::new(repository, shell)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to load config");
|
.expect("Failed to load config");
|
||||||
|
|
||||||
@ -82,14 +92,15 @@ mod tests {
|
|||||||
|
|
||||||
let config_file_path = test_file_path.to_str().unwrap().to_string();
|
let config_file_path = test_file_path.to_str().unwrap().to_string();
|
||||||
println!("File path {config_file_path}");
|
println!("File path {config_file_path}");
|
||||||
let repository = Box::new(LocalFileConfigManager::new(config_file_path));
|
let repository = Arc::new(LocalFileConfigManager::new(config_file_path));
|
||||||
let mut config = Config::new(repository)
|
let shell = Arc::new(DummyOPNSenseShell {});
|
||||||
|
let mut config = Config::new(repository, shell.clone())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to load config");
|
.expect("Failed to load config");
|
||||||
|
|
||||||
println!("Config {:?}", config);
|
println!("Config {:?}", config);
|
||||||
|
|
||||||
let mut dhcp_config = DhcpConfig::new(&mut config.opnsense);
|
let mut dhcp_config = DhcpConfig::new(&mut config.opnsense, shell);
|
||||||
dhcp_config
|
dhcp_config
|
||||||
.add_static_mapping(
|
.add_static_mapping(
|
||||||
"00:00:00:00:00:00",
|
"00:00:00:00:00:00",
|
||||||
|
|||||||
@ -1,33 +1,9 @@
|
|||||||
use crate::config::manager::ConfigManager;
|
use crate::config::{manager::ConfigManager, OPNsenseShell};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::{debug, info};
|
use log::info;
|
||||||
use russh::{
|
use russh_keys::key::KeyPair;
|
||||||
client::{Config as SshConfig, Handler, Msg},
|
use std::sync::Arc;
|
||||||
Channel,
|
|
||||||
};
|
|
||||||
use russh_keys::key::{self, KeyPair};
|
|
||||||
use russh_sftp::client::SftpSession;
|
|
||||||
use std::{
|
|
||||||
net::Ipv4Addr,
|
|
||||||
sync::Arc,
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
|
|
||||||
struct Client {}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Handler for Client {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
async fn check_server_key(
|
|
||||||
&mut self,
|
|
||||||
_server_public_key: &key::PublicKey,
|
|
||||||
) -> Result<bool, Self::Error> {
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SshCredentials {
|
pub enum SshCredentials {
|
||||||
@ -37,170 +13,50 @@ pub enum SshCredentials {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SshConfigManager {
|
pub struct SshConfigManager {
|
||||||
ssh_config: Arc<SshConfig>,
|
opnsense_shell: Arc<dyn OPNsenseShell>,
|
||||||
credentials: SshCredentials,
|
|
||||||
host: (Ipv4Addr, u16),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshConfigManager {
|
impl SshConfigManager {
|
||||||
pub fn new(
|
pub fn new(opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
|
||||||
host: (Ipv4Addr, u16),
|
Self { opnsense_shell }
|
||||||
credentials: SshCredentials,
|
|
||||||
ssh_config: Arc<SshConfig>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
ssh_config,
|
|
||||||
credentials,
|
|
||||||
host,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshConfigManager {
|
impl SshConfigManager {
|
||||||
async fn get_ssh_channel(&self) -> Result<Channel<Msg>, Error> {
|
|
||||||
let mut ssh = russh::client::connect(self.ssh_config.clone(), self.host, Client {}).await?;
|
|
||||||
|
|
||||||
match &self.credentials {
|
|
||||||
SshCredentials::SshKey { username, key } => {
|
|
||||||
ssh.authenticate_publickey(username, key.clone()).await?;
|
|
||||||
}
|
|
||||||
SshCredentials::Password { username, password } => {
|
|
||||||
ssh.authenticate_password(username, password).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ssh.channel_open_session().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_content_to_temp_file(&self, content: &str) -> Result<String, Error> {
|
|
||||||
let temp_filename = format!(
|
|
||||||
"/tmp/opnsense-config-tmp-config_{}",
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis()
|
|
||||||
);
|
|
||||||
let channel = self.get_ssh_channel().await?;
|
|
||||||
channel
|
|
||||||
.request_subsystem(true, "sftp")
|
|
||||||
.await
|
|
||||||
.expect("Should request sftp subsystem");
|
|
||||||
let sftp = SftpSession::new(channel.into_stream())
|
|
||||||
.await
|
|
||||||
.expect("Should acquire sftp subsystem");
|
|
||||||
|
|
||||||
let mut file = sftp.create(&temp_filename).await.unwrap();
|
|
||||||
file.write_all(content.as_bytes()).await?;
|
|
||||||
|
|
||||||
Ok(temp_filename)
|
|
||||||
}
|
|
||||||
async fn backup_config_remote(&self) -> Result<String, Error> {
|
async fn backup_config_remote(&self) -> Result<String, Error> {
|
||||||
let backup_filename = format!("config_{}.xml", chrono::Local::now().format("%Y%m%d%H%M%S"));
|
let backup_filename = format!("config_{}.xml", chrono::Local::now().format("%Y%m%d%H%M%S"));
|
||||||
|
|
||||||
self.run_command(&format!("cp /conf/config.xml /tmp/{}", backup_filename))
|
self.opnsense_shell.exec(&format!("cp /conf/config.xml /tmp/{}", backup_filename))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn move_to_live_config(&self, new_config_path: &str) -> Result<String, Error> {
|
async fn move_to_live_config(&self, new_config_path: &str) -> Result<String, Error> {
|
||||||
info!("Overwriting OPNSense /conf/config.xml with {new_config_path}");
|
info!("Overwriting OPNSense /conf/config.xml with {new_config_path}");
|
||||||
self.run_command(&format!("mv {new_config_path} /conf/config.xml"))
|
self.opnsense_shell.exec(&format!("mv {new_config_path} /conf/config.xml"))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reload_all_services(&self) -> Result<String, Error> {
|
async fn reload_all_services(&self) -> Result<String, Error> {
|
||||||
info!("Reloading all opnsense services");
|
info!("Reloading all opnsense services");
|
||||||
self.run_command(&format!("configctl service reload all"))
|
self.opnsense_shell.exec(&format!("configctl service reload all"))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_command(&self, command: &str) -> Result<String, Error> {
|
|
||||||
debug!("Running ssh command {command}");
|
|
||||||
let mut channel = self.get_ssh_channel().await?;
|
|
||||||
channel.exec(true, command).await?;
|
|
||||||
wait_for_completion(&mut channel).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ConfigManager for SshConfigManager {
|
impl ConfigManager for SshConfigManager {
|
||||||
async fn load_as_str(&self) -> Result<String, Error> {
|
async fn load_as_str(&self) -> Result<String, Error> {
|
||||||
let mut channel = self.get_ssh_channel().await?;
|
self.opnsense_shell.exec("cat /conf/config.xml").await
|
||||||
|
|
||||||
channel.exec(true, "cat /conf/config.xml").await?;
|
|
||||||
let mut output: Vec<u8> = vec![];
|
|
||||||
loop {
|
|
||||||
let Some(msg) = channel.wait().await else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("got msg {:?}", msg);
|
|
||||||
match msg {
|
|
||||||
russh::ChannelMsg::Data { ref data } => {
|
|
||||||
output.append(&mut data.to_vec());
|
|
||||||
}
|
|
||||||
russh::ChannelMsg::ExitStatus { .. } => {}
|
|
||||||
russh::ChannelMsg::WindowAdjusted { .. } => {}
|
|
||||||
russh::ChannelMsg::Success { .. } => {}
|
|
||||||
russh::ChannelMsg::Eof { .. } => {}
|
|
||||||
_ => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(String::from_utf8(output).expect("Valid utf-8 bytes"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn apply_new_config(&self, content: &str) -> Result<(), Error> {
|
async fn apply_new_config(&self, content: &str) -> Result<(), Error> {
|
||||||
let temp_filename = self.write_content_to_temp_file(content).await?;
|
let temp_filename = self
|
||||||
|
.opnsense_shell
|
||||||
|
.write_content_to_temp_file(content)
|
||||||
|
.await?;
|
||||||
self.backup_config_remote().await?;
|
self.backup_config_remote().await?;
|
||||||
self.move_to_live_config(&temp_filename).await?;
|
self.move_to_live_config(&temp_filename).await?;
|
||||||
self.reload_all_services().await?;
|
self.reload_all_services().await?;
|
||||||
|
Ok(())
|
||||||
// TODO
|
|
||||||
// 1. use russh to copy the current opnsense /conf/config.xml to the backup folder with a
|
|
||||||
// proper name
|
|
||||||
//
|
|
||||||
// 2. use russh scp functionality to copy the content directly into a file
|
|
||||||
// if necessary, save it first to a local file with minimal permissions in
|
|
||||||
// /tmp/{randomname}
|
|
||||||
//
|
|
||||||
// 3. Use opnsense cli to validate the config file
|
|
||||||
//
|
|
||||||
// 4. Reload all opnsense services using opnsense cli (still through russh commands)
|
|
||||||
//
|
|
||||||
todo!("apply_new_config not fully implemented yet")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_completion(channel: &mut Channel<Msg>) -> Result<String, Error> {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let Some(msg) = channel.wait().await else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
match msg {
|
|
||||||
russh::ChannelMsg::ExtendedData { ref data, .. }
|
|
||||||
| russh::ChannelMsg::Data { ref data } => {
|
|
||||||
output.append(&mut data.to_vec());
|
|
||||||
}
|
|
||||||
russh::ChannelMsg::ExitStatus { exit_status } => {
|
|
||||||
if exit_status != 0 {
|
|
||||||
return Err(Error::Command(format!(
|
|
||||||
"Command failed with exit status {exit_status}, output {}",
|
|
||||||
String::from_utf8(output).unwrap_or_default()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
russh::ChannelMsg::Success { .. }
|
|
||||||
| russh::ChannelMsg::WindowAdjusted { .. }
|
|
||||||
| russh::ChannelMsg::Eof { .. } => {}
|
|
||||||
_ => {
|
|
||||||
return Err(Error::Unexpected(format!(
|
|
||||||
"Russh got unexpected msg {msg:?}"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(String::from_utf8(output).unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
mod config;
|
mod config;
|
||||||
mod manager;
|
mod manager;
|
||||||
|
mod shell;
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
|
pub use shell::*;
|
||||||
|
|||||||
27
harmony-rs/opnsense-config/src/config/shell/mod.rs
Normal file
27
harmony-rs/opnsense-config/src/config/shell/mod.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
mod ssh;
|
||||||
|
pub use ssh::*;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OPNsenseShell: std::fmt::Debug + Send + Sync {
|
||||||
|
async fn exec(&self, command: &str) -> Result<String, Error>;
|
||||||
|
async fn write_content_to_temp_file(&self, content: &str) -> Result<String, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DummyOPNSenseShell;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[async_trait]
|
||||||
|
impl OPNsenseShell for DummyOPNSenseShell {
|
||||||
|
async fn exec(&self, _command: &str) -> Result<String, Error> {
|
||||||
|
unimplemented!("This is a dummy implementation");
|
||||||
|
}
|
||||||
|
async fn write_content_to_temp_file(&self, _content: &str) -> Result<String, Error> {
|
||||||
|
unimplemented!("This is a dummy implementation");
|
||||||
|
}
|
||||||
|
}
|
||||||
141
harmony-rs/opnsense-config/src/config/shell/ssh.rs
Normal file
141
harmony-rs/opnsense-config/src/config/shell/ssh.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
use std::{
|
||||||
|
net::Ipv4Addr,
|
||||||
|
sync::Arc,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use log::debug;
|
||||||
|
use russh::{
|
||||||
|
client::{Config, Handler, Msg},
|
||||||
|
Channel,
|
||||||
|
};
|
||||||
|
use russh_keys::key;
|
||||||
|
use russh_sftp::client::SftpSession;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
use crate::{config::SshCredentials, Error};
|
||||||
|
|
||||||
|
use super::OPNsenseShell;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SshOPNSenseShell {
|
||||||
|
host: (Ipv4Addr, u16),
|
||||||
|
credentials: SshCredentials,
|
||||||
|
ssh_config: Arc<Config>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OPNsenseShell for SshOPNSenseShell {
|
||||||
|
async fn exec(&self, command: &str) -> Result<String, Error> {
|
||||||
|
self.run_command(command).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_content_to_temp_file(&self, content: &str) -> Result<String, Error> {
|
||||||
|
let temp_filename = format!(
|
||||||
|
"/tmp/opnsense-config-tmp-config_{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis()
|
||||||
|
);
|
||||||
|
let channel = self.get_ssh_channel().await?;
|
||||||
|
channel
|
||||||
|
.request_subsystem(true, "sftp")
|
||||||
|
.await
|
||||||
|
.expect("Should request sftp subsystem");
|
||||||
|
let sftp = SftpSession::new(channel.into_stream())
|
||||||
|
.await
|
||||||
|
.expect("Should acquire sftp subsystem");
|
||||||
|
|
||||||
|
let mut file = sftp.create(&temp_filename).await.unwrap();
|
||||||
|
file.write_all(content.as_bytes()).await?;
|
||||||
|
|
||||||
|
Ok(temp_filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshOPNSenseShell {
|
||||||
|
pub async fn get_ssh_channel(&self) -> Result<Channel<Msg>, Error> {
|
||||||
|
let mut ssh = russh::client::connect(self.ssh_config.clone(), self.host, Client {}).await?;
|
||||||
|
|
||||||
|
match &self.credentials {
|
||||||
|
SshCredentials::SshKey { username, key } => {
|
||||||
|
ssh.authenticate_publickey(username, key.clone()).await?;
|
||||||
|
}
|
||||||
|
SshCredentials::Password { username, password } => {
|
||||||
|
ssh.authenticate_password(username, password).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ssh.channel_open_session().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_command(&self, command: &str) -> Result<String, Error> {
|
||||||
|
debug!("Running ssh command {command}");
|
||||||
|
let mut channel = self.get_ssh_channel().await?;
|
||||||
|
channel.exec(true, command).await?;
|
||||||
|
wait_for_completion(&mut channel).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
host: (Ipv4Addr, u16),
|
||||||
|
credentials: SshCredentials,
|
||||||
|
ssh_config: Arc<Config>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
host,
|
||||||
|
credentials,
|
||||||
|
ssh_config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Client {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler for Client {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
async fn check_server_key(
|
||||||
|
&mut self,
|
||||||
|
_server_public_key: &key::PublicKey,
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_completion(channel: &mut Channel<Msg>) -> Result<String, Error> {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(msg) = channel.wait().await else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
russh::ChannelMsg::ExtendedData { ref data, .. }
|
||||||
|
| russh::ChannelMsg::Data { ref data } => {
|
||||||
|
output.append(&mut data.to_vec());
|
||||||
|
}
|
||||||
|
russh::ChannelMsg::ExitStatus { exit_status } => {
|
||||||
|
if exit_status != 0 {
|
||||||
|
return Err(Error::Command(format!(
|
||||||
|
"Command failed with exit status {exit_status}, output {}",
|
||||||
|
String::from_utf8(output).unwrap_or_default()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
russh::ChannelMsg::Success { .. }
|
||||||
|
| russh::ChannelMsg::WindowAdjusted { .. }
|
||||||
|
| russh::ChannelMsg::Eof { .. } => {}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::Unexpected(format!(
|
||||||
|
"Russh got unexpected msg {msg:?}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(String::from_utf8(output).unwrap_or_default())
|
||||||
|
}
|
||||||
@ -7,16 +7,37 @@ pub use error::Error;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use config::SshConfigManager;
|
use config::SshConfigManager;
|
||||||
|
use opnsense_config_xml::StaticMap;
|
||||||
use russh::client;
|
use russh::client;
|
||||||
use std::{net::Ipv4Addr, sync::Arc, time::Duration};
|
use std::{net::Ipv4Addr, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{self, SshCredentials},
|
config::{self, SshCredentials, SshOPNSenseShell},
|
||||||
Config,
|
Config,
|
||||||
};
|
};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_public_sdk() {
|
async fn test_public_sdk() {
|
||||||
|
let mac = "11:22:33:44:55:66";
|
||||||
|
let ip = Ipv4Addr::new(10, 100, 8, 200);
|
||||||
|
let hostname = "test_hostname";
|
||||||
|
|
||||||
|
remove_static_mapping(mac).await;
|
||||||
|
|
||||||
|
// Make sure static mapping does not exist anymore
|
||||||
|
let static_mapping_removed = get_static_mappings().await;
|
||||||
|
assert!(!static_mapping_removed.iter().any(|e| e.mac == mac));
|
||||||
|
|
||||||
|
add_static_mapping(mac, ip, hostname).await;
|
||||||
|
|
||||||
|
// Make sure static mapping has been added successfully
|
||||||
|
let static_mapping_added = get_static_mappings().await;
|
||||||
|
assert_eq!(static_mapping_added.len(), static_mapping_removed.len() + 1);
|
||||||
|
assert!(static_mapping_added.iter().any(|e| e.mac == mac));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize_config() -> Config {
|
||||||
let config = Arc::new(client::Config {
|
let config = Arc::new(client::Config {
|
||||||
inactivity_timeout: Some(Duration::from_secs(5)),
|
inactivity_timeout: Some(Duration::from_secs(5)),
|
||||||
..<_>::default()
|
..<_>::default()
|
||||||
@ -27,19 +48,29 @@ mod test {
|
|||||||
password: String::from("opnsense"),
|
password: String::from("opnsense"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let repo =
|
let shell = Arc::new(SshOPNSenseShell::new(
|
||||||
SshConfigManager::new((Ipv4Addr::new(192, 168, 5, 229), 22), credentials, config);
|
(Ipv4Addr::new(192, 168, 5, 229), 22),
|
||||||
|
credentials,
|
||||||
|
config,
|
||||||
|
));
|
||||||
|
let manager = Arc::new(SshConfigManager::new(shell.clone()));
|
||||||
|
Config::new(manager, shell).await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
let mut config = Config::new(Box::new(repo)).await.unwrap();
|
async fn get_static_mappings() -> Vec<StaticMap> {
|
||||||
config
|
let mut config = initialize_config().await;
|
||||||
.dhcp()
|
config.dhcp().get_static_mappings().await.unwrap()
|
||||||
.add_static_mapping(
|
}
|
||||||
"11:22:33:44:55:66",
|
|
||||||
Ipv4Addr::new(10, 100, 8, 200),
|
|
||||||
"test_hostname",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
|
async fn add_static_mapping(mac: &str, ip: Ipv4Addr, hostname: &str) {
|
||||||
|
let mut config = initialize_config().await;
|
||||||
|
config.dhcp().add_static_mapping(mac, ip, hostname).unwrap();
|
||||||
|
config.apply().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_static_mapping(mac: &str) {
|
||||||
|
let mut config = initialize_config().await;
|
||||||
|
config.dhcp().remove_static_mapping(mac);
|
||||||
config.apply().await.unwrap();
|
config.apply().await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
|
use opnsense_config_xml::MaybeString;
|
||||||
use opnsense_config_xml::Range;
|
use opnsense_config_xml::Range;
|
||||||
use opnsense_config_xml::StaticMap;
|
use opnsense_config_xml::StaticMap;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use opnsense_config_xml::OPNsense;
|
use opnsense_config_xml::OPNsense;
|
||||||
|
|
||||||
|
use crate::config::OPNsenseShell;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
pub struct DhcpConfig<'a> {
|
pub struct DhcpConfig<'a> {
|
||||||
opnsense: &'a mut OPNsense,
|
opnsense: &'a mut OPNsense,
|
||||||
|
opnsense_shell: Arc<dyn OPNsenseShell>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -39,8 +45,27 @@ impl std::fmt::Display for DhcpError {
|
|||||||
impl std::error::Error for DhcpError {}
|
impl std::error::Error for DhcpError {}
|
||||||
|
|
||||||
impl<'a> DhcpConfig<'a> {
|
impl<'a> DhcpConfig<'a> {
|
||||||
pub fn new(opnsense: &'a mut OPNsense) -> Self {
|
pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
|
||||||
Self { opnsense }
|
Self {
|
||||||
|
opnsense,
|
||||||
|
opnsense_shell,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_static_mapping(&mut self, mac: &str) {
|
||||||
|
let lan_dhcpd = self.get_lan_dhcpd();
|
||||||
|
lan_dhcpd.staticmaps.retain(|static_entry| static_entry.mac != mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface {
|
||||||
|
&mut self
|
||||||
|
.opnsense
|
||||||
|
.dhcpd
|
||||||
|
.elements
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(name, _config)| return name == "lan")
|
||||||
|
.expect("Interface lan should have dhcpd activated")
|
||||||
|
.1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_static_mapping(
|
pub fn add_static_mapping(
|
||||||
@ -51,16 +76,9 @@ impl<'a> DhcpConfig<'a> {
|
|||||||
) -> Result<(), DhcpError> {
|
) -> Result<(), DhcpError> {
|
||||||
let mac = mac.to_string();
|
let mac = mac.to_string();
|
||||||
let hostname = hostname.to_string();
|
let hostname = hostname.to_string();
|
||||||
let lan_dhcpd = &mut self
|
let lan_dhcpd = self.get_lan_dhcpd();
|
||||||
.opnsense
|
|
||||||
.dhcpd
|
|
||||||
.elements
|
|
||||||
.iter_mut()
|
|
||||||
.find(|(name, _config)| return name == "lan")
|
|
||||||
.expect("Interface lan should have dhcpd activated")
|
|
||||||
.1;
|
|
||||||
|
|
||||||
let range = &lan_dhcpd.range;
|
let range = &lan_dhcpd.range;
|
||||||
|
let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps;
|
||||||
|
|
||||||
if !Self::is_valid_mac(&mac) {
|
if !Self::is_valid_mac(&mac) {
|
||||||
return Err(DhcpError::InvalidMacAddress(mac));
|
return Err(DhcpError::InvalidMacAddress(mac));
|
||||||
@ -70,8 +88,6 @@ impl<'a> DhcpConfig<'a> {
|
|||||||
return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string()));
|
return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps;
|
|
||||||
|
|
||||||
if existing_mappings.iter().any(|m| {
|
if existing_mappings.iter().any(|m| {
|
||||||
m.ipaddr
|
m.ipaddr
|
||||||
.parse::<Ipv4Addr>()
|
.parse::<Ipv4Addr>()
|
||||||
@ -128,6 +144,35 @@ impl<'a> DhcpConfig<'a> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_static_mappings(&self) -> Result<Vec<StaticMap>, Error> {
|
||||||
|
let list_static_output = self
|
||||||
|
.opnsense_shell
|
||||||
|
.exec("configctl dhcpd list static")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let value: serde_json::Value = serde_json::from_str(&list_static_output).expect(&format!(
|
||||||
|
"Got invalid json from configctl {list_static_output}"
|
||||||
|
));
|
||||||
|
let static_maps = value["dhcpd"]
|
||||||
|
.as_array()
|
||||||
|
.ok_or(Error::Command(format!(
|
||||||
|
"Invalid DHCP data from configctl command, got {list_static_output}"
|
||||||
|
)))?
|
||||||
|
.iter()
|
||||||
|
.map(|entry| StaticMap {
|
||||||
|
mac: entry["mac"].as_str().unwrap_or_default().to_string(),
|
||||||
|
ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(),
|
||||||
|
hostname: entry["hostname"].as_str().unwrap_or_default().to_string(),
|
||||||
|
descr: entry["descr"].as_str().map(MaybeString::from),
|
||||||
|
winsserver: MaybeString::default(),
|
||||||
|
dnsserver: MaybeString::default(),
|
||||||
|
ntpserver: MaybeString::default(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(static_maps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user