First attempt at a Rust-based UISP integration system. There's still a LOT to implement, but the basics are there and it isn't missing anything on my crazy layout.

This commit is contained in:
Herbert Wolverson 2024-04-19 13:16:21 -05:00
parent ba8b2c81a9
commit d13a17821a
24 changed files with 1062 additions and 2 deletions

View File

@ -31,4 +31,5 @@ members = [
"lts_client", # Shared data and client-side code for long-term stats
"lqos_map_perf", # A CLI tool for testing eBPF map performance
"uisp", # REST support for the UISP API
"uisp_integration", # UISP Integration in Rust
]

View File

@ -6,6 +6,8 @@ pub struct DataLink {
pub id: String,
pub from: DataLinkFrom,
pub to: DataLinkTo,
#[serde(rename = "canDelete")]
pub can_delete: bool,
}
#[allow(non_snake_case)]

View File

@ -11,9 +11,9 @@ use lqos_config::Config;
// UISP data link definitions
use self::rest::nms_request_get_vec;
use anyhow::Result;
pub use data_link::DataLink;
pub use data_link::*;
pub use device::Device;
pub use site::Site;
pub use site::{Site, SiteId, Description};
/// Loads a complete list of all sites from UISP
pub async fn load_all_sites(config: Config) -> Result<Vec<Site>> {

View File

@ -20,6 +20,14 @@ impl Site {
None
}
pub fn name_or_blank(&self) -> String {
if let Some(name) = self.name() {
name
} else {
"".to_string()
}
}
pub fn address(&self) -> Option<String> {
if let Some(desc) = &self.description {
if let Some(address) = &desc.address {
@ -83,6 +91,14 @@ impl Site {
}
(down, up)
}
pub fn is_suspended(&self) -> bool {
if let Some(site_id) = &self.identification {
site_id.suspended
} else {
false
}
}
}
#[allow(non_snake_case)]

View File

@ -0,0 +1,16 @@
[package]
name = "uisp_integration"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.82"
reqwest = { version = "0.12.3", features = ["json"] }
tokio = { version = "1.37.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
lqos_config = { path = "../lqos_config" }
uisp = { path = "../uisp" }
thiserror = "1.0.58"

View File

@ -0,0 +1,17 @@
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum UispIntegrationError {
#[error("Unable to load configuration")]
CannotLoadConfig,
#[error("UISP Integration is Disabled")]
IntegrationDisabled,
#[error("Unknown Integration Strategy")]
UnknownIntegrationStrategy,
#[error("Error contacting UISP")]
UispConnectError,
#[error("Root site not found")]
NoRootSite,
#[error("Unknown Site Type")]
UnknownSiteType,
}

View File

@ -0,0 +1,87 @@
//! Rust version of the UISP Integration from LibreQoS. This will probably
//! be ported back to Python, with Rust support structures - but I'll iterate
//! faster in Rust.
mod errors;
mod strategies;
pub mod uisp_types;
use crate::errors::UispIntegrationError;
use lqos_config::Config;
use tokio::time::Instant;
use tracing::{error, info};
/// Start the tracing/logging system
fn init_tracing() {
tracing_subscriber::fmt()
.with_file(true)
.with_line_number(true)
.compact()
.init();
}
fn check_enabled_status(config: &Config) -> Result<(), UispIntegrationError> {
if !config.uisp_integration.enable_uisp {
error!("UISP Integration is disabled in /etc/lqos.conf");
error!("Integration will not run.");
Err(UispIntegrationError::IntegrationDisabled)
} else {
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<(), UispIntegrationError> {
let now = Instant::now();
init_tracing();
info!("UISP Integration 2.0-rust");
// Load the configuration
info!("Loading Configuration");
let config = lqos_config::load_config().map_err(|e| {
error!("Unable to load configuration");
error!("{e:?}");
UispIntegrationError::CannotLoadConfig
})?;
// Check that we're allowed to run
check_enabled_status(&config)?;
// Select a strategy and go from there
strategies::build_with_strategy(config).await?;
// Print timings
let elapsed = now.elapsed();
info!(
"UISP Integration Run Completed in {:.3} seconds",
elapsed.as_secs_f32()
);
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use lqos_config::Config;
#[test]
fn test_uisp_disabled() {
let mut cfg = Config::default();
cfg.uisp_integration.enable_uisp = false;
let result = check_enabled_status(&cfg);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
UispIntegrationError::IntegrationDisabled
);
}
#[test]
fn test_uisp_enabled() {
let mut cfg = Config::default();
cfg.uisp_integration.enable_uisp = true;
let result = check_enabled_status(&cfg);
assert!(result.is_ok());
}
}

View File

@ -0,0 +1,8 @@
use crate::errors::UispIntegrationError;
use lqos_config::Config;
use tracing::error;
pub async fn build_flat_network(_config: Config) -> Result<(), UispIntegrationError> {
error!("Not implemented yet");
Ok(())
}

View File

@ -0,0 +1,52 @@
use crate::uisp_types::{UispSite, UispSiteType};
use std::collections::HashSet;
use tracing::info;
use uisp::{DataLink, Device, Site};
pub fn promote_access_points(
sites: &mut Vec<UispSite>,
devices_raw: &[Device],
data_links_raw: &[DataLink],
sites_raw: &[Site],
) {
let mut all_links = Vec::new();
sites.iter().for_each(|s| {
let links = s.find_aps(&devices_raw, &data_links_raw, &sites_raw);
if !links.is_empty() {
all_links.extend(links);
}
});
info!("Detected {} intra-site links", all_links.len());
// Insert AP entries
for link in all_links {
// Create the new AP site
let parent_site_id = sites.iter().position(|s| s.id == link.site_id).unwrap();
/*if sites[parent_site_id].site_type == UispSiteType::Client {
warn!(
"{} is a client, but has an AP pointing at other locations",
sites[parent_site_id].name
);
}*/
let mut new_site = UispSite {
id: link.device_id,
name: link.device_name,
site_type: UispSiteType::AccessPoint,
uisp_parent_id: None,
parent_indices: HashSet::new(),
max_up_mbps: 0, // TODO: I need to read this from the device capacity
max_down_mbps: 0,
..Default::default()
};
new_site.parent_indices.insert(parent_site_id);
// Add it
let new_id = sites.len();
sites.push(new_site);
sites.iter_mut().for_each(|s| {
if link.child_sites.contains(&s.id) {
s.parent_indices.insert(new_id);
}
});
}
}

View File

@ -0,0 +1,55 @@
use crate::errors::UispIntegrationError;
use crate::uisp_types::{UispSite, UispSiteType};
use std::collections::HashSet;
use tracing::info;
pub fn promote_clients_with_children(
sites: &mut Vec<UispSite>,
) -> Result<(), UispIntegrationError> {
info!("Scanning for client sites with child sites");
let mut client_sites_with_children = Vec::new();
sites
.iter()
.enumerate()
.filter(|(_, s)| s.site_type == UispSiteType::Client)
.for_each(|(idx, s)| {
let child_count = sites
.iter()
.filter(|c| c.parent_indices.contains(&idx))
.count();
if child_count > 0 {
client_sites_with_children.push(idx);
}
});
for child_site in client_sites_with_children {
//info!("Promoting {} to ClientWithChildren", sites[child_site].name);
sites[child_site].site_type = UispSiteType::ClientWithChildren;
let old_name = sites[child_site].name.clone();
sites[child_site].name = format!("(Generated Site) {}", sites[child_site].name);
let old_id = sites[child_site].id.clone();
sites[child_site].id = format!("GEN-{}", sites[child_site].id);
sites[child_site].suspended = false;
let new_id = sites.len();
let mut parent_indices = HashSet::new();
parent_indices.insert(child_site);
let mut new_site = UispSite {
id: old_id,
name: old_name,
site_type: UispSiteType::Client,
uisp_parent_id: None,
parent_indices,
max_down_mbps: sites[child_site].max_down_mbps,
max_up_mbps: sites[child_site].max_up_mbps,
suspended: sites[child_site].suspended,
..Default::default()
};
new_site.device_indices.extend_from_slice(&sites[child_site].device_indices);
sites[child_site].device_indices.clear();
sites.push(new_site);
}
Ok(())
}

View File

@ -0,0 +1,69 @@
mod ap_promotion;
mod client_site_promotion;
mod parse;
mod root_site;
mod uisp_fetch;
mod utils;
mod squash_single_entry_aps;
mod tree_walk;
use crate::errors::UispIntegrationError;
use crate::strategies::full::ap_promotion::promote_access_points;
use crate::strategies::full::client_site_promotion::promote_clients_with_children;
use crate::strategies::full::parse::parse_uisp_datasets;
use crate::strategies::full::root_site::{find_root_site, set_root_site};
use crate::strategies::full::uisp_fetch::load_uisp_data;
use crate::strategies::full::utils::{print_sites, warn_of_no_parents};
use lqos_config::Config;
use crate::strategies::full::squash_single_entry_aps::squash_single_aps;
use crate::strategies::full::tree_walk::walk_tree_for_routing;
use crate::uisp_types::UispSite;
/// Attempt to construct a full hierarchy topology for the UISP network.
pub async fn build_full_network(config: Config) -> Result<(), UispIntegrationError> {
// Obtain the UISP data and transform it into easier to work with types
let (sites_raw, devices_raw, data_links_raw) = load_uisp_data(config.clone()).await?;
let (mut sites, data_links, _devices) =
parse_uisp_datasets(&sites_raw, &data_links_raw, &devices_raw, &config);
// Check root sites
let root_site = find_root_site(&config, &mut sites, &data_links)?;
// Set the site root
set_root_site(&mut sites, &root_site)?;
// Search for devices that provide links elsewhere
promote_access_points(&mut sites, &devices_raw, &data_links_raw, &sites_raw);
// Sites that are clients but have children should be promoted
promote_clients_with_children(&mut sites)?;
// Do Link Squashing
squash_single_aps(&mut sites)?;
// Build Path Weights
walk_tree_for_routing(&mut sites, &root_site)?;
// Issue No Parent Warnings
warn_of_no_parents(&sites, &devices_raw);
// Print Sites
if let Some(root_idx) = sites.iter().position(|s| s.name == root_site) {
print_sites(&sites, root_idx);
}
Ok(())
}
fn walk_node(idx: usize, weight: u32, sites: &mut Vec<UispSite>, visited: &mut std::collections::HashSet<usize>) {
if visited.contains(&idx) {
return;
}
visited.insert(idx);
for i in 0 .. sites.len() {
if sites[i].parent_indices.contains(&idx) {
sites[i].route_weights.push((idx, weight));
walk_node(i, weight+10, sites, visited);
}
}
}

View File

@ -0,0 +1,59 @@
use crate::uisp_types::{UispDataLink, UispDevice, UispSite};
use lqos_config::Config;
use tracing::info;
use uisp::{DataLink, Device, Site};
pub fn parse_uisp_datasets(
sites_raw: &[Site],
data_links_raw: &[DataLink],
devices_raw: &[Device],
config: &Config,
) -> (Vec<UispSite>, Vec<UispDataLink>, Vec<UispDevice>) {
let (mut sites, data_links, devices) = (
parse_sites(sites_raw, config),
parse_data_links(data_links_raw, devices_raw),
parse_devices(devices_raw),
);
// Assign devices to sites
for site in sites.iter_mut() {
devices.iter().enumerate().filter(|(_, device)|
device.site_id == site.id
).for_each(|(idx, _)| {
site.device_indices.push(idx);
});
}
(sites, data_links, devices)
}
fn parse_sites(sites_raw: &[Site], config: &Config) -> Vec<UispSite> {
let mut sites: Vec<UispSite> = sites_raw
.iter()
.map(|s| UispSite::from_uisp(s, &config))
.collect();
info!("{} sites have been successfully parsed", sites.len());
sites
}
fn parse_data_links(data_links_raw: &[DataLink], devices_raw: &[Device]) -> Vec<UispDataLink> {
let mut data_links: Vec<UispDataLink> = data_links_raw
.iter()
.map(|l| UispDataLink::from_uisp(l, &devices_raw))
.flatten()
.collect();
info!(
"{} data-links have been successfully parsed",
data_links.len()
);
data_links
}
fn parse_devices(devices_raw: &[Device]) -> Vec<UispDevice> {
let mut devices: Vec<UispDevice> = devices_raw
.iter()
.map(|d| UispDevice::from_uisp(d))
.collect();
info!("{} devices have been sucessfully parsed", devices.len());
devices
}

View File

@ -0,0 +1,201 @@
use crate::errors::UispIntegrationError;
use crate::uisp_types::{UispDataLink, UispSite, UispSiteType};
use lqos_config::Config;
use tracing::{error, info, warn};
/// Looks to identify the root site for the site tree.
/// If the "site" is defined in the configuration, it will try to use it.
/// If the site is defined but does not exist, it will search for an Internet-connected site
/// and try to use that.
/// If it still hasn't found one, and there are multiple Internet connected sites - it will insert
/// a fake root and use that instead. I'm not sure that's a great idea.
pub fn find_root_site(
config: &Config,
sites: &mut Vec<UispSite>,
data_links: &[UispDataLink],
) -> Result<String, UispIntegrationError> {
let mut root_site_name = config.uisp_integration.site.clone();
if root_site_name.is_empty() {
warn!("Root site name isn't specified in /etc/lqos.conf - we'll try and figure it out");
root_site_name = handle_multiple_internet_connected_sites(sites, data_links)?;
} else {
info!("Using root UISP site from /etc/lqos.conf: {root_site_name}");
if sites.iter().find(|s| s.name == root_site_name).is_none() {
error!("Site {root_site_name} (from /etc/lqos.conf) not found in the UISP sites list");
return Err(UispIntegrationError::NoRootSite);
} else {
info!("{root_site_name} found in the sites list.");
}
}
Ok(root_site_name)
}
fn handle_multiple_internet_connected_sites(
sites: &mut Vec<UispSite>,
data_links: &[UispDataLink],
) -> Result<String, UispIntegrationError> {
let mut root_site_name = String::new();
let mut candidates = Vec::new();
data_links
.iter()
.filter(|l| l.can_delete == false)
.for_each(|l| {
candidates.push(l.from_site_name.clone());
});
if candidates.is_empty() {
error!("Unable to find a root site in the sites/data-links.");
return Err(UispIntegrationError::NoRootSite);
} else if candidates.len() == 1 {
info!(
"Found only one site with an Internet connection: {root_site_name}, using it as root"
);
root_site_name = candidates[0].clone();
} else {
warn!("Multiple Internet links detected. Will create an 'Internet' root node");
root_site_name = "INSERTED_INTERNET".to_string();
sites.push(UispSite {
id: "ROOT-001".to_string(),
name: "INSERTED_INTERNET".to_string(),
site_type: UispSiteType::Root,
..Default::default()
})
}
Ok(root_site_name)
}
pub fn set_root_site(sites: &mut [UispSite], root_site: &str) -> Result<(), UispIntegrationError> {
if let Some(root) = sites.iter_mut().find(|s| s.name == root_site) {
root.site_type = UispSiteType::Root;
}
let number_of_roots = sites
.iter()
.filter(|s| s.site_type == UispSiteType::Root)
.count();
if number_of_roots > 1 {
error!("More than one root present in the tree! That's not going to work. Bailing.");
return Err(UispIntegrationError::NoRootSite);
} else {
info!("Single root tagged in the tree");
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::init_tracing;
use uisp::{
DataLinkDevice, DataLinkDeviceIdentification, DataLinkFrom, DataLinkSite, DataLinkTo,
Description, SiteId,
};
#[test]
fn test_known_root() {
let mut cfg = Config::default();
cfg.uisp_integration.enable_uisp = true;
cfg.uisp_integration.site = "TEST".to_string();
let mut sites = vec![UispSite {
id: "TEST".to_string(),
name: "TEST".to_string(),
site_type: UispSiteType::Site,
..Default::default()
}];
let data_links = vec![];
let result = find_root_site(&cfg, &mut sites, &data_links);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "TEST");
}
#[test]
fn fail_find_a_known_root() {
let mut cfg = Config::default();
cfg.uisp_integration.enable_uisp = true;
cfg.uisp_integration.site = "DOES NOT EXIST".to_string();
let mut sites = vec![UispSite {
id: "TEST".to_string(),
name: "TEST".to_string(),
site_type: UispSiteType::Site,
..Default::default()
}];
let data_links = vec![];
let result = find_root_site(&cfg, &mut sites, &data_links);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), UispIntegrationError::NoRootSite);
}
#[test]
fn find_single_root_from_data_links() {
let mut cfg = Config::default();
cfg.uisp_integration.enable_uisp = true;
cfg.uisp_integration.site = String::new();
let mut sites = vec![UispSite {
id: "TEST".to_string(),
name: "TEST".to_string(),
site_type: UispSiteType::Site,
..Default::default()
}];
let mut data_links = vec![UispDataLink {
id: "".to_string(),
from_site_id: "TEST".to_string(),
from_site_name: "TEST".to_string(),
to_site_id: "".to_string(),
to_site_name: "".to_string(),
can_delete: false,
}];
let result = find_root_site(&cfg, &mut sites, &data_links);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "TEST");
}
#[test]
fn test_inserted_internet() {
let mut cfg = Config::default();
cfg.uisp_integration.enable_uisp = true;
cfg.uisp_integration.site = String::new();
let mut sites = vec![
UispSite {
id: "TEST".to_string(),
name: "TEST".to_string(),
site_type: UispSiteType::Site,
..Default::default()
},
UispSite {
id: "TEST2".to_string(),
name: "TEST2".to_string(),
site_type: UispSiteType::Site,
..Default::default()
},
];
let data_links = vec![
UispDataLink {
id: "".to_string(),
from_site_id: "".to_string(),
to_site_id: "TEST".to_string(),
from_site_name: "".to_string(),
to_site_name: "TEST".to_string(),
can_delete: false,
},
UispDataLink {
id: "".to_string(),
from_site_id: "".to_string(),
to_site_id: "TEST2".to_string(),
from_site_name: "".to_string(),
to_site_name: "TEST2".to_string(),
can_delete: false,
},
];
let result = find_root_site(&cfg, &mut sites, &data_links);
assert!(result.is_ok());
assert!(sites
.iter()
.find(|s| s.name == "INSERTED_INTERNET")
.is_some());
}
}

View File

@ -0,0 +1,29 @@
use crate::errors::UispIntegrationError;
use crate::uisp_types::{UispSite, UispSiteType};
pub fn squash_single_aps(sites: &mut Vec<UispSite>) -> Result<(), UispIntegrationError> {
let mut squashable = Vec::new();
for (idx, site) in sites.iter().enumerate() {
if site.site_type == UispSiteType::AccessPoint {
let target_count = sites.iter().filter(|s| s.parent_indices.contains(&idx)).count();
if target_count == 1 && site.parent_indices.len() == 1 {
//tracing::info!("Site {} has only one child and is therefore eligible for squashing.", site.name);
squashable.push(idx);
}
}
}
for squash_idx in squashable {
sites[squash_idx].site_type = UispSiteType::SquashDeleted;
sites[squash_idx].name += " (SQUASHED)";
let new_parent = *sites[squash_idx].parent_indices.iter().nth(0).unwrap();
sites.iter_mut().for_each(|s| {
if s.parent_indices.contains(&squash_idx) {
s.parent_indices.remove(&squash_idx);
s.parent_indices.insert(new_parent);
}
});
sites[squash_idx].parent_indices.clear();
}
Ok(())
}

View File

@ -0,0 +1,27 @@
use crate::errors::UispIntegrationError;
use crate::strategies::full::walk_node;
use crate::uisp_types::{UispSite, UispSiteType};
pub fn walk_tree_for_routing(sites: &mut Vec<UispSite>, root_site: &str) -> Result<(), UispIntegrationError> {
if let Some(root_idx) = sites.iter().position(|s| s.name == root_site) {
let mut visited = std::collections::HashSet::new();
let mut current_node = root_idx;
walk_node(current_node, 10, sites, &mut visited);
} else {
tracing::error!("Unable to build a path-weights graph because I can't find the root node");
return Err(UispIntegrationError::NoRootSite);
}
// Apply the lowest weight route
for site in sites.iter_mut() {
if site.site_type != UispSiteType::Root && !site.route_weights.is_empty() {
// Sort to find the lowest exit
site.route_weights.sort_by(|a,b| {
a.1.cmp(&b.1)
});
site.selected_parent = Some(site.route_weights[0].0);
}
}
Ok(())
}

View File

@ -0,0 +1,48 @@
use crate::errors::UispIntegrationError;
use lqos_config::Config;
use tokio::join;
use tracing::{error, info};
use uisp::{DataLink, Device, Site};
/// Load required data from UISP, using the API.
/// Requires a valid configuration with working token data.
pub async fn load_uisp_data(
config: Config,
) -> Result<(Vec<Site>, Vec<Device>, Vec<DataLink>), UispIntegrationError> {
info!("Loading Devices, Sites and Data-Links from UISP");
let (devices, sites, data_links) = join!(
uisp::load_all_devices_with_interfaces(config.clone()),
uisp::load_all_sites(config.clone()),
uisp::load_all_data_links(config.clone()),
);
// Error Handling
if devices.is_err() {
error!("Error downloading devices list from UISP");
error!("{:?}", devices);
return Err(UispIntegrationError::UispConnectError);
}
let devices = devices.unwrap();
if sites.is_err() {
error!("Error downloading sites list from UISP");
error!("{:?}", sites);
return Err(UispIntegrationError::UispConnectError);
}
let sites = sites.unwrap();
if data_links.is_err() {
error!("Error downloading data_links list from UISP");
error!("{:?}", data_links);
return Err(UispIntegrationError::UispConnectError);
}
let data_links = data_links.unwrap();
info!(
"Loaded backing data: {} sites, {} devices, {} links",
sites.len(),
devices.len(),
data_links.len()
);
Ok((sites, devices, data_links))
}

View File

@ -0,0 +1,57 @@
use crate::uisp_types::UispSite;
use tracing::warn;
use uisp::Device;
/// Counts how many devices are present at a siteId. It's a simple
/// iteration of the devices.
pub fn count_devices_in_site(site_id: &str, devices: &[Device]) -> usize {
devices
.iter()
.filter(|d| {
if let Some(site) = &d.identification.site {
if let Some(parent) = &site.parent {
if parent.id == site_id {
return true;
}
}
}
false
})
.count()
}
/// Utility function to dump the site tree to the console.
/// Useful for debugging.
pub fn print_sites(sites: &[UispSite], root_idx: usize) {
println!("{}", sites[root_idx].name);
iterate_child_sites(sites, root_idx, 2);
}
fn iterate_child_sites(sites: &[UispSite], parent: usize, indent: usize) {
sites
.iter()
.enumerate()
.filter(|(_, s)| s.selected_parent == Some(parent))
.for_each(|(i, s)| {
// Indent print
for _ in 0..indent {
print!("-");
}
s.print_tree_summary();
println!();
if indent < 20 {
iterate_child_sites(sites, i, indent + 2);
}
});
}
pub fn warn_of_no_parents(sites: &[UispSite], devices_raw: &[Device]) {
sites
.iter()
.filter(|s| s.parent_indices.is_empty())
.for_each(|s| {
if count_devices_in_site(&s.id, &devices_raw) > 0 {
warn!("Site: {} has no parents", s.name);
}
});
}

View File

@ -0,0 +1,29 @@
mod flat;
mod full;
use crate::errors::UispIntegrationError;
use lqos_config::Config;
use tracing::{error, info};
pub async fn build_with_strategy(config: Config) -> Result<(), UispIntegrationError> {
// Select a Strategy
match config.uisp_integration.strategy.to_lowercase().as_str() {
"flat" => {
info!("Strategy selected: flat");
flat::build_flat_network(config).await?;
Ok(())
}
"full" => {
info!("Strategy selected: full");
full::build_full_network(config).await?;
Ok(())
}
_ => {
error!(
"Unknown strategy: {}. Bailing.",
config.uisp_integration.strategy
);
Err(UispIntegrationError::UnknownIntegrationStrategy)
}
}
}

View File

@ -0,0 +1,7 @@
#[derive(Debug)]
pub struct DetectedAccessPoint {
pub site_id: String,
pub device_id: String,
pub device_name: String,
pub child_sites: Vec<String>,
}

View File

@ -0,0 +1,11 @@
mod detected_ap;
mod uisp_data_link;
mod uisp_device;
mod uisp_site;
mod uisp_site_type;
pub use detected_ap::*;
pub use uisp_data_link::*;
pub use uisp_device::*;
pub use uisp_site::*;
pub use uisp_site_type::*;

View File

@ -0,0 +1,49 @@
use uisp::{DataLink, Device};
/// Shortened/Flattened version of the UISP DataLink type.
pub struct UispDataLink {
pub id: String,
pub from_site_id: String,
pub to_site_id: String,
pub from_site_name: String,
pub to_site_name: String,
pub can_delete: bool,
}
impl UispDataLink {
pub fn from_uisp(value: &DataLink, devices: &[Device]) -> Option<Self> {
let mut from_site_id = String::new();
let mut to_site_id = String::new();
let mut to_site_name = String::new();
let mut from_site_name = String::new();
// Obvious Site Links
if let Some(from_site) = &value.from.site {
from_site_id = from_site.identification.id.clone();
to_site_id = from_site.identification.name.clone();
}
if let Some(to_site) = &value.to.site {
to_site_id = to_site.identification.id.clone();
to_site_name = to_site.identification.name.clone();
}
// Remove any links with no site targets
if from_site_id.is_empty() || to_site_id.is_empty() {
return None;
}
// Remove any links that go to themselves
if from_site_id == to_site_id {
return None;
}
Some(Self {
id: value.id.clone(),
from_site_id,
to_site_id,
from_site_name,
to_site_name,
can_delete: value.can_delete,
})
}
}

View File

@ -0,0 +1,24 @@
use uisp::Device;
/// Trimmed UISP device for easy use
pub struct UispDevice {
pub id: String,
pub mac: String,
pub site_id: String,
}
impl UispDevice {
pub fn from_uisp(device: &Device) -> Self {
let mac = if let Some(id) = &device.identification.mac {
id.clone()
} else {
"".to_string()
};
Self {
id: device.get_id(),
mac,
site_id: device.get_site_id().unwrap_or("".to_string())
}
}
}

View File

@ -0,0 +1,150 @@
use crate::uisp_types::uisp_site_type::UispSiteType;
use crate::uisp_types::DetectedAccessPoint;
use lqos_config::Config;
use std::collections::HashSet;
use tracing::warn;
use uisp::{DataLink, Device, Site};
/// Shortened/flattened version of the UISP Site type.
#[derive(Debug)]
pub struct UispSite {
pub id: String,
pub name: String,
pub site_type: UispSiteType,
pub uisp_parent_id: Option<String>,
pub parent_indices: HashSet<usize>,
pub max_down_mbps: u32,
pub max_up_mbps: u32,
pub suspended: bool,
pub device_indices: Vec<usize>,
pub route_weights: Vec<(usize, u32)>,
pub selected_parent: Option<usize>,
}
impl Default for UispSite {
fn default() -> Self {
Self {
id: "".to_string(),
name: "".to_string(),
site_type: UispSiteType::Site,
uisp_parent_id: None,
parent_indices: Default::default(),
max_down_mbps: 0,
max_up_mbps: 0,
suspended: false,
device_indices: Vec::new(),
route_weights: Vec::new(),
selected_parent: None,
}
}
}
impl UispSite {
pub fn from_uisp(value: &Site, config: &Config) -> Self {
let mut uisp_parent_id = None;
if let Some(id) = &value.identification {
if let Some(parent) = &id.parent {
if let Some(pid) = &parent.id {
uisp_parent_id = Some(pid.clone());
}
}
}
let (mut max_down_mbps, mut max_up_mbps) = value.qos(
config.queues.generated_pn_download_mbps,
config.queues.generated_pn_upload_mbps,
);
let suspended = value.is_suspended();
if suspended {
match config.uisp_integration.suspended_strategy.as_str() {
"slow" => {
warn!(
"{} is suspended. Setting a slow speed.",
value.name_or_blank()
);
max_down_mbps = 1;
max_up_mbps = 1;
}
_ => warn!(
"{} is suspended. No strategy is set, leaving at full speed.",
value.name_or_blank()
),
}
}
Self {
id: value.id.clone(),
name: value.name_or_blank(),
site_type: UispSiteType::from_uisp_record(value).unwrap(),
parent_indices: HashSet::new(),
uisp_parent_id,
max_down_mbps,
max_up_mbps,
suspended,
..Default::default()
}
}
pub fn find_aps(
&self,
devices: &[Device],
data_links: &[DataLink],
sites: &[Site],
) -> Vec<DetectedAccessPoint> {
let mut links = Vec::new();
for device in devices.iter() {
if let Some(device_site) = device.get_site_id() {
if device_site == self.id {
// We're in the correct site, now look for anything that
// links to/from this device
let potential_ap_id = device.get_id();
let mut potential_ap = DetectedAccessPoint {
site_id: self.id.clone(),
device_id: potential_ap_id.clone(),
device_name: device.get_name().unwrap_or(String::new()),
child_sites: vec![],
};
for dl in data_links.iter() {
// The "I'm the FROM device case"
if let Some(from_device) = &dl.from.device {
if from_device.identification.id == potential_ap_id {
if let Some(to_site) = &dl.to.site {
if to_site.identification.id != self.id {
// We have a data link from this device that goes to
// another site.
if let Some(remote_site) =
sites.iter().find(|s| s.id == to_site.identification.id)
{
potential_ap.child_sites.push(remote_site.id.clone());
}
}
}
}
}
}
if !potential_ap.child_sites.is_empty() {
links.push(potential_ap);
}
}
}
}
links
}
pub fn print_tree_summary(&self) {
print!(
"{} ({}) {}/{} Mbps",
self.name, self.site_type, self.max_down_mbps, self.max_up_mbps
);
if self.suspended {
print!(" (SUSPENDED)");
}
if !self.device_indices.is_empty() {
print!(" [{} devices]", self.device_indices.len());
}
}
}

View File

@ -0,0 +1,46 @@
use crate::errors::UispIntegrationError;
use std::fmt::{Display, Formatter};
use tracing::error;
use uisp::Site;
/// Defines the types of sites found in the UISP Tree
#[derive(Debug, PartialEq)]
pub enum UispSiteType {
Site,
Client,
ClientWithChildren,
AccessPoint,
Root,
SquashDeleted,
}
impl Display for UispSiteType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
Self::Site => write!(f, "Site"),
Self::Client => write!(f, "Client"),
Self::ClientWithChildren => write!(f, "GeneratedNode"),
Self::AccessPoint => write!(f, "AP"),
Self::Root => write!(f, "Root"),
Self::SquashDeleted => write!(f, "SquashDeleted"),
}
}
}
impl UispSiteType {
pub fn from_uisp_record(site: &Site) -> Result<Self, UispIntegrationError> {
if let Some(id) = &site.identification {
if let Some(t) = &id.site_type {
return match t.as_str() {
"site" => Ok(Self::Site),
"endpoint" => Ok(Self::Client),
_ => {
error!("Unknown site type: {t}");
Err(UispIntegrationError::UnknownSiteType)
}
};
}
}
Err(UispIntegrationError::UnknownSiteType)
}
}