wip(opnsense-config): Refactoring to improve usability and features

This commit is contained in:
Jean-Gabriel Gill-Couture
2024-11-14 23:45:13 -05:00
parent 85786cf648
commit 65c395aeae
20 changed files with 754 additions and 455 deletions

View File

@@ -1,7 +1,9 @@
[package]
name = "opnsense-config"
version = "0.1.0"
edition = "2021"
version.workspace = true
readme.workspace = true
license.workspace = true
[dependencies]
serde = { version = "1.0.123", features = [ "derive" ] }
@@ -9,14 +11,10 @@ log = { workspace = true }
env_logger = { workspace = true }
russh = { workspace = true }
russh-keys = { workspace = true }
#yaserde = { git = "https://git.nationtech.io/NationTech/yaserde" }
#yaserde_derive = { git = "https://git.nationtech.io/NationTech/yaserde" }
yaserde = { path = "../../../../github/yaserde/yaserde" }
yaserde_derive = { path = "../../../../github/yaserde/yaserde_derive" }
xml-rs = "0.8"
thiserror = "1.0"
async-trait = { workspace = true }
tokio = { workspace = true }
opnsense-config-xml = { path = "../opnsense-config-xml" }
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@@ -1,10 +1,10 @@
use crate::error::Error;
use crate::modules::opnsense::OPNsense;
use async_trait::async_trait;
use log::info;
use log::{info, trace};
use opnsense_config_xml::OPNsense;
use russh::client::{Config as SshConfig, Handler};
use russh_keys::key;
use std::{fs, net::Ipv4Addr, path::Path, sync::Arc};
use std::{fs, net::Ipv4Addr, sync::Arc};
#[async_trait]
pub trait ConfigRepository: std::fmt::Debug {
@@ -87,6 +87,7 @@ impl ConfigRepository for SshConfigRepository {
.await?;
let mut channel = ssh.channel_open_session().await?;
todo!("Backup, Validate, Reload config file");
let command = format!(
"echo '{}' > /conf/config.xml",
@@ -144,9 +145,9 @@ pub struct Config {
impl Config {
pub async fn new(repository: Box<dyn ConfigRepository + Send + Sync>) -> Result<Self, Error> {
let xml = repository.load().await?;
info!("xml {}", xml);
trace!("xml {}", xml);
let opnsense = yaserde::de::from_str(&xml).map_err(|e| Error::Xml(e.to_string()))?;
let opnsense = OPNsense::from(xml);
Ok(Self {
opnsense,
@@ -163,15 +164,12 @@ impl Config {
}
pub async fn save(&self) -> Result<(), Error> {
let xml = yaserde::ser::to_string(&self.opnsense).map_err(|e| Error::Xml(e.to_string()))?;
self.repository.save(&xml).await
self.repository.save(&self.opnsense.to_xml()).await
}
}
#[cfg(test)]
mod tests {
use crate::infra::yaserde::to_xml_str;
use super::*;
use std::path::PathBuf;
use pretty_assertions::assert_eq;
@@ -191,11 +189,33 @@ mod tests {
println!("Config {:?}", config);
let serialized = to_xml_str(&config.opnsense).unwrap();
let serialized = config.opnsense.to_xml();
fs::write("/tmp/serialized.xml", &serialized).unwrap();
assert_eq!(config_file_str, serialized);
}
#[tokio::test]
async fn test_add_dhcpd_static_entry() {
let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_file_path.push("src/tests/data/config-full-1.xml");
let config_file_path = test_file_path.to_str().unwrap().to_string();
println!("File path {config_file_path}");
let repository = Box::new(LocalFileConfigRepository::new(config_file_path));
let config_file_str = repository.load().await.unwrap();
let mut config = Config::new(repository)
.await
.expect("Failed to load config");
println!("Config {:?}", config);
let serialized = config.opnsense.to_xml();
fs::write("/tmp/serialized.xml", &serialized).unwrap();
assert_eq!(config_file_str, serialized);
todo!();
}
}

View File

@@ -1,254 +0,0 @@
use xml::reader::XmlEvent as ReadEvent;
use yaserde::{ser, YaDeserialize as YaDeserializeTrait, YaSerialize as YaSerializeTrait};
#[derive(Debug, PartialEq, Default)]
pub struct RawXml(String);
impl YaDeserializeTrait for RawXml {
fn deserialize<R: std::io::Read>(
reader: &mut yaserde::de::Deserializer<R>,
) -> Result<Self, String> {
let mut buffer = String::new();
let mut depth = 0;
let own_name = match reader.peek()? {
ReadEvent::StartElement { name, .. } => name.local_name.clone(),
_ => return Err("RawXml Should start deserializing with StartElement".to_string()),
};
println!("RawXml deserialize from root element name : {own_name}");
loop {
let current_event = reader.peek()?.to_owned();
match current_event.clone() {
ReadEvent::StartElement {
name, attributes, ..
} => {
println!("StartElement {name} depth {depth}");
depth += 1;
let mut attr_string = String::new();
attributes.iter().for_each(|a| {
attr_string.push_str(&format!(r#" {}="{}""#, &a.name, &a.value));
});
buffer.push_str(&format!("<{}{}>", name, attr_string));
let _event = reader.next_event()?;
}
ReadEvent::EndElement { name } => {
println!("EndElement {name} depth {depth}");
depth -= 1;
buffer.push_str(&format!("</{}>", name));
println!(
"Checking if name.local_name {} matches own_name {} at depth {depth}",
&name.local_name, &own_name
);
if name.local_name == own_name && depth == 0 {
println!(
"Found next EndElement is closing my struct, breaking out of loop"
);
break;
} else {
let _event = reader.next_event()?;
}
}
ReadEvent::Characters(content) => {
println!("Characters {content} depth {depth}");
buffer.push_str(&content);
let _event = reader.next_event()?;
}
ReadEvent::StartDocument {
version,
encoding,
standalone,
} => todo!(
"StartDocument {:?} {:?} {:?}",
version,
encoding,
standalone
),
ReadEvent::EndDocument => todo!(),
ReadEvent::ProcessingInstruction { name, data } => {
todo!("ProcessingInstruction {:?}, {:?}", name, data)
}
ReadEvent::CData(cdata) => todo!("CData, {:?}", cdata),
ReadEvent::Comment(comment) => todo!("Comment, {:?}", comment),
ReadEvent::Whitespace(whitespace) => todo!("Whitespace, {:?}", whitespace),
}
let next = reader.peek()?;
println!(
"Processing done on \ncurrent_event : {:?} \nnext : {:?}",
&current_event, &next
);
}
println!("buffered events {buffer}");
Ok(RawXml(buffer))
}
}
impl YaSerializeTrait for RawXml {
fn serialize<W: std::io::Write>(&self, writer: &mut ser::Serializer<W>) -> Result<(), String> {
let content = self.0.clone();
let content = xml::EventReader::from_str(content.as_str());
let mut reader = yaserde::de::Deserializer::new(content);
loop {
let e = reader.next_event()?;
if let ReadEvent::EndDocument = e {
break;
}
writer
.write(e.as_writer_event().unwrap())
.expect("Writer should write write event");
}
Ok(())
}
fn serialize_attributes(
&self,
attributes: Vec<xml::attribute::OwnedAttribute>,
namespace: xml::namespace::Namespace,
) -> Result<
(
Vec<xml::attribute::OwnedAttribute>,
xml::namespace::Namespace,
),
String,
> {
todo!()
}
}
#[cfg(test)]
mod test {
use crate::infra::yaserde::to_xml_str;
use super::*;
use pretty_assertions::assert_eq;
use yaserde_derive::YaDeserialize;
use yaserde_derive::YaSerialize;
#[derive(Debug, PartialEq, Default, YaDeserialize)]
pub struct Parent {
// pub rawxml_child: RawXml,
pub string_child: String,
pub child_child: Child,
}
#[derive(Debug, PartialEq, Default, YaDeserialize)]
pub struct Child {
pub child_val: String,
pub child_val2: String,
pub child_option: Option<String>,
}
#[test]
fn rawxml_should_buffer_empty_element() {
let rawxml: RawXml = yaserde::de::from_str("<something/>").unwrap();
assert_eq!(rawxml.0, String::from("<something></something>"));
}
#[test]
fn rawxml_should_buffer_elements_with_different_case_as_they_are() {
let xml = "<xml><Some_thing></Some_thing><something></something></xml>";
let rawxml: RawXml = yaserde::de::from_str(xml).unwrap();
assert_eq!(rawxml.0, String::from(xml));
}
#[test]
fn rawxml_should_buffer_elements_with_attributes() {
let xml = r#"<xml version="ababa"><Some_thing></Some_thing><something></something></xml>"#;
let rawxml: RawXml = yaserde::de::from_str(xml).unwrap();
assert_eq!(rawxml.0, String::from(xml));
}
#[test]
fn rawxml_should_handle_complex_documents() {
let xml = r#"<xml><OpenVPN version="1.0.0"><Overwrites></Overwrites><Instances></Instances><StaticKeys></StaticKeys></OpenVPN><Gateways version="0.0.1"></Gateways><HAProxy version="4.0.0"><general><enabled>1</enabled><gracefulStop>0</gracefulStop><hardStopAfter>60s</hardStopAfter><closeSpreadTime></closeSpreadTime><seamlessReload>0</seamlessReload><storeOcsp>0</storeOcsp><showIntro>1</showIntro><peers><enabled>0</enabled><name1></name1><listen1></listen1><port1>1024</port1><name2></name2><listen2></listen2><port2>1024</port2></peers><tuning><root>0</root><maxConnections></maxConnections><nbthread>1</nbthread><sslServerVerify>ignore</sslServerVerify><maxDHSize>2048</maxDHSize><bufferSize>16384</bufferSize></tuning></general></HAProxy></xml>"#;
let rawxml: RawXml = yaserde::de::from_str(xml).unwrap();
assert_eq!(rawxml.0, String::from(xml));
}
#[test]
fn rawxml_should_serialize_simple_documents() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?><xml />"#;
let rawxml: RawXml = yaserde::de::from_str(xml).unwrap();
assert_eq!(yaserde::ser::to_string(&rawxml).unwrap(), xml);
}
#[test]
fn rawxml_should_serialize_complex_documents() {
let xml = r#"<?xml version="1.0"?>
<xml>
<OpenVPN version="1.0.0">
<Overwrites/>
<Instances/>
<StaticKeys/>
</OpenVPN>
<Gateways version="0.0.1"/>
<HAProxy version="4.0.0">
<general>
<enabled>1</enabled>
<gracefulStop>0</gracefulStop>
<hardStopAfter>60s</hardStopAfter>
<closeSpreadTime/>
<seamlessReload>0</seamlessReload>
<storeOcsp>0</storeOcsp>
<showIntro>1</showIntro>
<peers>
<enabled>0</enabled>
<name1/>
<listen1/>
<port1>1024</port1>
<name2/>
<listen2/>
<port2>1024</port2>
</peers>
<tuning>
<root>0</root>
<maxConnections/>
<nbthread>1</nbthread>
<sslServerVerify>ignore</sslServerVerify>
<maxDHSize>2048</maxDHSize>
<bufferSize>16384</bufferSize>
</tuning>
</general>
</HAProxy>
</xml>
"#;
let rawxml: RawXml = yaserde::de::from_str(xml).unwrap();
assert_eq!(to_xml_str(&rawxml).unwrap(), xml);
}
#[test]
fn rawxml_should_allow_siblings_before() {
#[derive(YaDeserialize, YaSerialize)]
struct Config {
paul: Vec<String>,
raw: RawXml,
}
let xml = r#"<?xml version="1.0" encoding="utf-8"?><Config><paul>bobob</paul><paul>patate</paul><raw>allo something</raw></Config>"#;
let config: Config = yaserde::de::from_str(xml).unwrap();
assert_eq!(yaserde::ser::to_string(&config).unwrap(), xml);
}
#[test]
fn rawxml_should_allow_siblings_after() {
#[derive(YaDeserialize, YaSerialize)]
struct Config {
raw: RawXml,
paul: Vec<String>,
}
let xml = r#"<?xml version="1.0" encoding="utf-8"?><Config><raw>allo something</raw><paul>bobob</paul><paul>patate</paul></Config>"#;
let config: Config = yaserde::de::from_str(xml).unwrap();
assert_eq!(config.paul.get(0).unwrap(), "bobob");
assert_eq!(config.paul.get(1).unwrap(), "patate");
assert_eq!(config.paul.len(), 2);
assert_eq!(config.raw.0, "<raw>allo something</raw>");
assert_eq!(yaserde::ser::to_string(&config).unwrap(), xml);
}
#[test]
fn rawxml_should_allow_being_end_of_document() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?><Config><raw>allo something</raw><paul>bobob</paul><paul>patate</paul></Config>"#;
let config: RawXml = yaserde::de::from_str(xml).unwrap();
assert_eq!(yaserde::ser::to_string(&config).unwrap(), xml);
}
}

View File

@@ -1,173 +0,0 @@
use xml::reader::XmlEvent as ReadEvent;
use xml::writer::XmlEvent as WriteEvent;
use yaserde::{ser, YaDeserialize as YaDeserializeTrait, YaSerialize as YaSerializeTrait};
#[derive(Debug, PartialEq, Default)]
pub struct MaybeString {
field_name: String,
content: Option<String>,
}
impl YaDeserializeTrait for MaybeString {
fn deserialize<R: std::io::Read>(
reader: &mut yaserde::de::Deserializer<R>,
) -> Result<Self, String> {
let field_name = match reader.peek()? {
ReadEvent::StartElement {
name, attributes, ..
} => {
if attributes.len() > 0 {
return Err(String::from(
"Attributes not currently supported by MaybeString",
));
}
name.local_name.clone()
}
_ => return Err(String::from("Unsupporte ReadEvent type")),
};
reader.next_event()?;
let content = match reader.peek()? {
ReadEvent::Characters(content) => Some(content.clone()),
ReadEvent::EndElement { name } => {
if name.local_name != field_name {
return Err(format!(
"Invalid EndElement, expected {field_name} but got {}",
name.local_name
));
}
None
}
_ => return Err(String::from("Unsupporte ReadEvent type")),
};
Ok(Self {
field_name,
content,
})
}
}
impl YaSerializeTrait for MaybeString {
fn serialize<W: std::io::Write>(&self, writer: &mut ser::Serializer<W>) -> Result<(), String> {
let start_element_event = WriteEvent::start_element(self.field_name.as_str());
writer.write(start_element_event).expect("Writer failed");
match &self.content {
Some(content) => {
writer
.write(WriteEvent::characters(content))
.expect("Writer failed");
}
None => {}
};
writer
.write(WriteEvent::end_element())
.expect("Writer failed");
Ok(())
}
fn serialize_attributes(
&self,
_attributes: Vec<xml::attribute::OwnedAttribute>,
_namespace: xml::namespace::Namespace,
) -> Result<
(
Vec<xml::attribute::OwnedAttribute>,
xml::namespace::Namespace,
),
String,
> {
unimplemented!("MaybeString does not currently support attributes")
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use yaserde_derive::YaDeserialize;
use yaserde_derive::YaSerialize;
#[derive(Debug, PartialEq, Default, YaDeserialize, YaSerialize)]
struct TestStruct {
maybe: MaybeString,
}
#[test]
fn maybe_string_should_deserialize_empty_element() {
let initial_xml = "<struct><maybe/></struct>";
let test_struct: TestStruct =
yaserde::de::from_str(initial_xml).expect("Shoudl deserialize teststruct");
println!("Got test_struct {:?}", test_struct);
assert_eq!(
test_struct,
TestStruct {
maybe: MaybeString {
field_name: String::from("maybe"),
content: None
}
}
);
}
#[test]
fn maybe_string_should_deserialize_content() {
let initial_xml = "<struct><maybe>some content</maybe></struct>";
let test_struct: TestStruct =
yaserde::de::from_str(initial_xml).expect("Shoudl deserialize teststruct");
println!("Got test_struct {:?}", test_struct);
assert_eq!(
test_struct,
TestStruct {
maybe: MaybeString {
field_name: String::from("maybe"),
content: Some(String::from("some content"))
}
}
);
}
#[test]
fn maybe_string_should_deserialize_empty_long_format() {
let initial_xml = "<struct><maybe></maybe></struct>";
let test_struct: TestStruct =
yaserde::de::from_str(initial_xml).expect("Shoudl deserialize teststruct");
println!("Got test_struct {:?}", test_struct);
assert_eq!(
test_struct,
TestStruct {
maybe: MaybeString {
field_name: String::from("maybe"),
content: None
}
}
);
}
#[test]
fn maybe_string_should_serialize_to_empty_element() {
let initial_xml =
r#"<?xml version="1.0" encoding="utf-8"?><TestStruct><maybe /></TestStruct>"#;
let test_struct: TestStruct =
yaserde::de::from_str(initial_xml).expect("Shoudl deserialize teststruct");
println!("Got test_struct {:?}", test_struct);
assert_eq!(
yaserde::ser::to_string(&test_struct).expect("should serialize teststruct"),
initial_xml
);
}
#[test]
fn maybe_string_should_serialize_content() {
let initial_xml = r#"<?xml version="1.0" encoding="utf-8"?><TestStruct><maybe>some content</maybe></TestStruct>"#;
let test_struct: TestStruct =
yaserde::de::from_str(initial_xml).expect("Shoudl deserialize teststruct");
println!("Got test_struct {:?}", test_struct);
assert_eq!(
yaserde::ser::to_string(&test_struct).expect("should serialize teststruct"),
initial_xml
);
}
}

View File

@@ -1,4 +0,0 @@
pub mod generic_xml;
pub mod maybe_string;
pub mod yaserde;

View File

@@ -1,20 +0,0 @@
use yaserde::YaSerialize;
pub fn to_xml_str<T: YaSerialize>(model: &T) -> Result<String, String> {
let yaserde_cfg = yaserde::ser::Config {
perform_indent: true,
write_document_declaration: false,
pad_self_closing: false,
..Default::default()
};
let serialized = yaserde::ser::to_string_with_config::<T>(model, &yaserde_cfg)?;
// Opnsense does not specify encoding in the document declaration
//
// yaserde / xml-rs does not allow disabling the encoding attribute in the
// document declaration
//
// So here we just manually prefix the xml document with the exact document declaration
// that opnsense uses
Ok(format!("<?xml version=\"1.0\"?>\n{serialized}\n"))
}

View File

@@ -1,7 +1,23 @@
pub mod config;
pub mod modules;
pub mod error;
pub mod infra;
pub mod modules;
use std::net::Ipv4Addr;
pub use config::Config;
pub use error::Error;
use modules::dhcp::DhcpConfig;
use opnsense_config_xml::OPNsense;
#[tokio::test]
async fn test_public_sdk() {
let mut opnsense = OPNsense::default();
let mut dhcpd = DhcpConfig::new(&mut opnsense);
dhcpd.add_static_mapping(
"test_mac",
Ipv4Addr::new(192, 168, 168, 168),
"test_hostname",
);
todo!();
// opnsense.apply_changes().await;
}

View File

@@ -1,111 +0,0 @@
use xml::reader::XmlEvent;
use yaserde::{YaDeserialize, YaSerialize};
use crate::modules::opnsense::{Interface, Interfaces};
impl YaSerialize for Interfaces {
fn serialize<W: std::io::Write>(
&self,
writer: &mut yaserde::ser::Serializer<W>,
) -> Result<(), String> {
writer.write("Interfaces serializer TODO");
Ok(())
}
fn serialize_attributes(
&self,
attributes: Vec<xml::attribute::OwnedAttribute>,
namespace: xml::namespace::Namespace,
) -> Result<
(
Vec<xml::attribute::OwnedAttribute>,
xml::namespace::Namespace,
),
String,
> {
todo!()
}
}
impl YaDeserialize for Interfaces {
fn deserialize<R: std::io::Read>(
reader: &mut yaserde::de::Deserializer<R>,
) -> Result<Self, String> {
let mut interfaces = Interfaces::default();
let mut current_interface_name = String::new();
let mut current_interface = Interface::default();
let mut event = reader.next_event()?;
loop {
match event {
xml::reader::XmlEvent::EndElement { name } => {
println!(
"Handling EndElement {}, current_interface_name: {}",
name.local_name, current_interface_name
);
println!("Peeking after EndElement {:?}", reader.peek()?);
if name.local_name == "interfaces" {
break;
}
todo!("Should not hit here");
}
xml::reader::XmlEvent::StartElement { ref name, .. } => {
println!("Got start Element {:?}", name);
current_interface_name = name.local_name.clone();
println!("About to deserialize interface from name {:?}", name);
// TODO store names
'inner_iface: loop {
let peek = reader.peek()?;
println!("Peeking before forcing next event {:?}", peek);
if let XmlEvent::EndElement { name } = peek {
// TODO save the interface name and struct in the hashmap
println!("Forcing next_event");
reader.next_event()?;
let peek = reader.peek()?;
println!("Peeking after next event deserializing {:?}", peek);
if let XmlEvent::EndElement { name } = peek {
println!("Got two EndElement in a row, breaking out of loop");
break 'inner_iface;
}
}
println!("Peeking before deserializing {:?}", reader.peek()?);
let interface = <Interface as yaserde::YaDeserialize>::deserialize(reader)?;
println!("Interface deserialized {:?}", interface);
println!("Peeking after deserializing {:?}", reader.peek()?);
}
println!(
"Done with inner interface, loop completed {:?}",
reader.peek()?
);
}
xml::reader::XmlEvent::EndDocument => break,
_ => {
return Err(
"This Interfaces Deserializer does not support all XmlEvent types".into(),
)
}
}
let peek = reader.peek()?;
let read_next = true;
if let XmlEvent::EndElement { name } = peek {
if name.local_name == "interfaces" {
println!("Got endElement interfaces, not reading next event");
break;
}
}
if read_next {
event = reader.next_event()?;
}
println!("Outer loop got event {:?}", event);
println!("Outer loop got peek {:?}", reader.peek()?);
}
println!("Done with interfaces {:?}", interfaces);
println!("reader peeking shows {:?}", reader.peek());
Ok(interfaces)
}
}

View File

@@ -1,3 +0,0 @@
mod interfaces;

View File

@@ -1,12 +1,9 @@
use opnsense_config_xml::Range;
use opnsense_config_xml::StaticMap;
use std::cmp::Ordering;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use super::opnsense::{OPNsense, StaticMap};
use crate::infra::maybe_string::MaybeString;
use crate::modules::opnsense::NumberOption;
use crate::modules::opnsense::Range;
use yaserde_derive::{YaDeserialize, YaSerialize};
use opnsense_config_xml::OPNsense;
pub struct DhcpConfig<'a> {
opnsense: &'a mut OPNsense,
@@ -48,41 +45,48 @@ impl<'a> DhcpConfig<'a> {
pub fn add_static_mapping(
&mut self,
mac: String,
mac: &str,
ipaddr: Ipv4Addr,
hostname: String,
hostname: &str,
) -> Result<(), DhcpError> {
let range: Range = todo!();
let mac = mac.to_string();
let hostname = hostname.to_string();
let range = &self.opnsense.dhcpd.lan.range;
if !Self::is_valid_mac(&mac) {
return Err(DhcpError::InvalidMacAddress(mac));
}
// if !Self::is_ip_in_range(&ipaddr, range) {
// return Err(DhcpError::IpAddressOutOfRange(ipaddr));
// }
if !Self::is_ip_in_range(&ipaddr, range) {
return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string()));
}
let existing_mappings = &self.opnsense.dhcpd.lan.staticmaps;
// TODO
// if existing_mappings.iter().any(|m| m.ipaddr == ipaddr) {
// return Err(DhcpError::IpAddressAlreadyMapped(ipaddr));
// }
if existing_mappings.iter().any(|m| {
m.ipaddr
.parse::<Ipv4Addr>()
.expect("Mapping contains invalid ipv4")
== ipaddr
}) {
return Err(DhcpError::IpAddressAlreadyMapped(ipaddr.to_string()));
}
// if existing_mappings.iter().any(|m| m.mac == mac) {
// return Err(DhcpError::MacAddressAlreadyMapped(mac));
// }
if existing_mappings.iter().any(|m| m.mac == mac) {
return Err(DhcpError::MacAddressAlreadyMapped(mac));
}
// let static_map = StaticMap {
// mac,
// ipaddr,
// hostname,
// descr: Default::default(),
// winsserver: Default::default(),
// dnsserver: Default::default(),
// ntpserver: Default::default(),
// };
let static_map = StaticMap {
mac,
ipaddr: ipaddr.to_string(),
hostname,
descr: Default::default(),
winsserver: Default::default(),
dnsserver: Default::default(),
ntpserver: Default::default(),
};
// self.opnsense.dhcpd.lan.staticmaps.push(static_map);
self.opnsense.dhcpd.lan.staticmaps.push(static_map);
Ok(())
}
@@ -101,7 +105,7 @@ impl<'a> DhcpConfig<'a> {
.all(|part| part.len() == 2 && part.chars().all(|c| c.is_ascii_hexdigit()))
}
fn is_ip_in_range(ip: &Ipv4Addr, range: Range) -> bool {
fn is_ip_in_range(ip: &Ipv4Addr, range: &Range) -> bool {
let range_start = range
.from
.parse::<Ipv4Addr>()
@@ -121,114 +125,11 @@ impl<'a> DhcpConfig<'a> {
}
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
#[yaserde(rename = "dhcpd")]
pub struct Dhcpd {
#[yaserde(rename = "lan")]
pub lan: DhcpInterface,
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct DhcpInterface {
pub enable: i32,
pub gateway: String,
pub domain: String,
#[yaserde(rename = "ddnsdomainalgorithm")]
pub ddns_domain_algorithm: String,
#[yaserde(rename = "numberoptions")]
pub number_options: Vec<NumberOption>,
#[yaserde(rename = "range")]
pub range: Range,
pub winsserver: MaybeString,
pub dnsserver: MaybeString,
pub ntpserver: MaybeString,
#[yaserde(rename = "staticmap")]
pub staticmaps: Vec<StaticMap>,
pub pool: MaybeString,
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct DhcpRange {
#[yaserde(rename = "from")]
pub from: String,
#[yaserde(rename = "to")]
pub to: String,
}
#[cfg(test)]
mod test {
use std::net::Ipv4Addr;
use crate::infra::yaserde::to_xml_str;
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn dhcpd_should_deserialize_serialize_identical() {
let dhcpd: Dhcpd =
yaserde::de::from_str(SERIALIZED_DHCPD).expect("Deserialize Dhcpd failed");
assert_eq!(
to_xml_str(&dhcpd).expect("Serialize Dhcpd failed"),
SERIALIZED_DHCPD
);
}
const SERIALIZED_DHCPD: &str = "<?xml version=\"1.0\"?>
<dhcpd>
<lan>
<enable>1</enable>
<gateway>192.168.20.1</gateway>
<domain>somedomain.yourlocal.mcd</domain>
<ddnsdomainalgorithm>hmac-md5</ddnsdomainalgorithm>
<numberoptions>
<item/>
</numberoptions>
<range>
<from>192.168.20.50</from>
<to>192.168.20.200</to>
</range>
<winsserver/>
<dnsserver>192.168.20.1</dnsserver>
<ntpserver/>
<staticmap>
<mac>55:55:55:55:55:1c</mac>
<ipaddr>192.168.20.160</ipaddr>
<hostname>somehost983</hostname>
<descr>someservire8</descr>
<winsserver/>
<dnsserver/>
<ntpserver/>
</staticmap>
<staticmap>
<mac>55:55:55:55:55:1c</mac>
<ipaddr>192.168.20.155</ipaddr>
<hostname>somehost893</hostname>
<winsserver/>
<dnsserver/>
<ntpserver/>
</staticmap>
<staticmap>
<mac>55:55:55:55:55:1c</mac>
<ipaddr>192.168.20.165</ipaddr>
<hostname>somehost893</hostname>
<descr/>
<winsserver/>
<dnsserver/>
<ntpserver/>
</staticmap>
<staticmap>
<mac>55:55:55:55:55:1c</mac>
<ipaddr>192.168.20.50</ipaddr>
<hostname>hostswitch2</hostname>
<descr>switch-2 (bottom)</descr>
<winsserver/>
<dnsserver/>
<ntpserver/>
</staticmap>
<pool/>
</lan>
</dhcpd>\n";
use std::net::Ipv4Addr;
#[test]
fn test_ip_in_range() {
@@ -239,22 +140,23 @@ mod test {
// Test IP within range
let ip = "192.168.1.150".parse::<Ipv4Addr>().unwrap();
assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), true);
assert_eq!(DhcpConfig::is_ip_in_range(&ip, &range), true);
// Test IP at start of range
let ip = "192.168.1.100".parse::<Ipv4Addr>().unwrap();
assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), true);
assert_eq!(DhcpConfig::is_ip_in_range(&ip, &range), true);
// Test IP at end of range
let ip = "192.168.1.200".parse::<Ipv4Addr>().unwrap();
assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), true);
assert_eq!(DhcpConfig::is_ip_in_range(&ip, &range), true);
// Test IP before range
let ip = "192.168.1.99".parse::<Ipv4Addr>().unwrap();
assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), false);
assert_eq!(DhcpConfig::is_ip_in_range(&ip, &range), false);
// Test IP after range
let ip = "192.168.1.201".parse::<Ipv4Addr>().unwrap();
assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), false);
assert_eq!(DhcpConfig::is_ip_in_range(&ip, &range), false);
}
}

View File

@@ -1,3 +1 @@
pub mod opnsense;
pub mod dhcp;
mod deserializer;

File diff suppressed because it is too large Load Diff