mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
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:
parent
ba8b2c81a9
commit
d13a17821a
@ -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
|
||||
]
|
||||
|
@ -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)]
|
||||
|
@ -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>> {
|
||||
|
@ -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)]
|
||||
|
16
src/rust/uisp_integration/Cargo.toml
Normal file
16
src/rust/uisp_integration/Cargo.toml
Normal 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"
|
17
src/rust/uisp_integration/src/errors.rs
Normal file
17
src/rust/uisp_integration/src/errors.rs
Normal 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,
|
||||
}
|
87
src/rust/uisp_integration/src/main.rs
Normal file
87
src/rust/uisp_integration/src/main.rs
Normal 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());
|
||||
}
|
||||
}
|
8
src/rust/uisp_integration/src/strategies/flat.rs
Normal file
8
src/rust/uisp_integration/src/strategies/flat.rs
Normal 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(())
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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(())
|
||||
}
|
69
src/rust/uisp_integration/src/strategies/full/mod.rs
Normal file
69
src/rust/uisp_integration/src/strategies/full/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
59
src/rust/uisp_integration/src/strategies/full/parse.rs
Normal file
59
src/rust/uisp_integration/src/strategies/full/parse.rs
Normal 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
|
||||
}
|
201
src/rust/uisp_integration/src/strategies/full/root_site.rs
Normal file
201
src/rust/uisp_integration/src/strategies/full/root_site.rs
Normal 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());
|
||||
}
|
||||
}
|
@ -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(())
|
||||
}
|
27
src/rust/uisp_integration/src/strategies/full/tree_walk.rs
Normal file
27
src/rust/uisp_integration/src/strategies/full/tree_walk.rs
Normal 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(())
|
||||
}
|
48
src/rust/uisp_integration/src/strategies/full/uisp_fetch.rs
Normal file
48
src/rust/uisp_integration/src/strategies/full/uisp_fetch.rs
Normal 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))
|
||||
}
|
57
src/rust/uisp_integration/src/strategies/full/utils.rs
Normal file
57
src/rust/uisp_integration/src/strategies/full/utils.rs
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
29
src/rust/uisp_integration/src/strategies/mod.rs
Normal file
29
src/rust/uisp_integration/src/strategies/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
7
src/rust/uisp_integration/src/uisp_types/detected_ap.rs
Normal file
7
src/rust/uisp_integration/src/uisp_types/detected_ap.rs
Normal 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>,
|
||||
}
|
11
src/rust/uisp_integration/src/uisp_types/mod.rs
Normal file
11
src/rust/uisp_integration/src/uisp_types/mod.rs
Normal 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::*;
|
49
src/rust/uisp_integration/src/uisp_types/uisp_data_link.rs
Normal file
49
src/rust/uisp_integration/src/uisp_types/uisp_data_link.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
24
src/rust/uisp_integration/src/uisp_types/uisp_device.rs
Normal file
24
src/rust/uisp_integration/src/uisp_types/uisp_device.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
150
src/rust/uisp_integration/src/uisp_types/uisp_site.rs
Normal file
150
src/rust/uisp_integration/src/uisp_types/uisp_site.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
46
src/rust/uisp_integration/src/uisp_types/uisp_site_type.rs
Normal file
46
src/rust/uisp_integration/src/uisp_types/uisp_site_type.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user