Starting to add UISP data module

This commit is contained in:
Herbert Wolverson 2023-06-07 13:34:17 +00:00
parent a5c8faa0eb
commit d73e12c8e2
8 changed files with 572 additions and 0 deletions

10
src/rust/Cargo.lock generated
View File

@ -4158,6 +4158,16 @@ dependencies = [
"serde",
]
[[package]]
name = "uisp"
version = "0.1.0"
dependencies = [
"anyhow",
"lqos_config",
"reqwest",
"serde",
]
[[package]]
name = "uncased"
version = "0.9.9"

View File

@ -37,4 +37,5 @@ members = [
"long_term_stats/wasm_pipe", # Provides a WebAssembly tight/compressed data pipeline
"long_term_stats/wasm_pipe_types", # Common types between the WASM conduit and the WASM server
"lqos_map_perf", # A CLI tool for testing eBPF map performance
"uisp", # REST support for the UISP API
]

10
src/rust/uisp/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "uisp"
version = "0.1.0"
edition = "2021"
[dependencies]
lqos_config = { path = "../lqos_config" }
serde = { version = "1.0", features = [ "derive" ] }
reqwest = { version = "0.11", features = [ "json" ] }
anyhow = "1"

View File

@ -0,0 +1,42 @@
use serde::Deserialize;
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DataLink {
pub id: String,
pub from: DataLinkFrom,
pub to: DataLinkTo,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DataLinkFrom {
pub device: Option<DataLinkDevice>,
pub site: Option<DataLinkSite>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DataLinkDevice {
pub identification: DataLinkDeviceIdentification,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DataLinkDeviceIdentification {
pub id: String,
pub name: String,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DataLinkTo {
pub device: Option<DataLinkDevice>,
pub site: Option<DataLinkSite>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DataLinkSite {
pub identification: DataLinkDeviceIdentification,
}

178
src/rust/uisp/src/device.rs Normal file
View File

@ -0,0 +1,178 @@
use std::collections::HashSet;
use serde::Deserialize;
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct Device {
pub identification: DeviceIdentification,
pub ipAddress: Option<String>,
pub attributes: Option<DeviceAttributes>,
pub mode: Option<String>,
pub interfaces: Option<Vec<DeviceInterface>>,
pub overview: Option<DeviceOverview>,
}
impl Device {
pub fn get_name(&self) -> Option<String> {
if let Some(hostname) = &self.identification.hostname {
return Some(hostname.clone());
}
None
}
pub fn get_model(&self) -> Option<String> {
if let Some(model) = &self.identification.model {
return Some(model.clone());
}
None
}
pub fn get_model_name(&self) -> Option<String> {
if let Some(model) = &self.identification.modelName {
return Some(model.clone());
}
None
}
pub fn get_id(&self) -> String {
self.identification.id.clone()
}
pub fn get_site_id(&self) -> Option<String> {
if let Some(site) = &self.identification.site {
return Some(site.id.clone());
}
None
}
fn strip_ip(ip: &String) -> String {
if !ip.contains("/") {
ip.clone()
} else {
ip[0..ip.find("/").unwrap()].to_string()
}
}
pub fn get_addresses(&self) -> HashSet<String> {
let mut result = HashSet::new();
if let Some(ip) = &self.ipAddress {
result.insert(Device::strip_ip(ip));
}
if let Some(interfaces) = &self.interfaces {
for interface in interfaces {
if let Some(addresses) = &interface.addresses {
for addy in addresses {
if let Some(cidr) = &addy.cidr {
result.insert(Device::strip_ip(cidr));
}
}
}
}
}
result
}
pub fn get_noise_floor(&self) -> Option<i32> {
if let Some(interfaces) = &self.interfaces {
for intf in interfaces.iter() {
if let Some(w) = &intf.wireless {
if let Some(nf) = &w.noiseFloor {
return Some(*nf);
}
}
}
}
None
}
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceIdentification {
pub id: String,
pub hostname: Option<String>,
pub mac: Option<String>,
pub model: Option<String>,
pub modelName: Option<String>,
pub role: Option<String>,
pub site: Option<DeviceSite>,
pub firmwareVersion: Option<String>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceSite {
pub id: String,
pub parent: Option<DeviceParent>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceParent {
pub id: String,
pub name: String,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceAttributes {
pub ssid: Option<String>,
pub apDevice: Option<DeviceAccessPoint>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceAccessPoint {
pub id: Option<String>,
pub name: Option<String>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceInterface {
pub identification: Option<InterfaceIdentification>,
pub addresses: Option<Vec<DeviceAddress>>,
pub status: Option<InterfaceStatus>,
pub wireless: Option<DeviceWireless>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct InterfaceIdentification {
pub name: Option<String>,
pub mac: Option<String>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceAddress {
pub cidr: Option<String>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct InterfaceStatus {
pub status: Option<String>,
pub speed: Option<String>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceOverview {
pub status: Option<String>,
pub frequency: Option<f64>,
pub outageScore: Option<f64>,
pub stationsCount: Option<i32>,
pub downlinkCapacity: Option<i32>,
pub uplinkCapacity: Option<i32>,
pub channelWidth: Option<i32>,
pub transmitPower: Option<i32>,
pub signal: Option<i32>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct DeviceWireless {
pub noiseFloor: Option<i32>,
}

36
src/rust/uisp/src/lib.rs Normal file
View File

@ -0,0 +1,36 @@
/// UISP Data Structures
///
/// Strong-typed implementation of the UISP API system. Used by long-term
/// stats to attach device information, possibly in the future used to
/// accelerate the UISP integration.
mod rest; // REST HTTP services
mod site; // UISP data definition for a site, pulled from the JSON
mod device; // UISP data definition for a device, including interfaces
mod data_link; // UISP data link definitions
use lqos_config::LibreQoSConfig;
pub use site::Site;
pub use device::Device;
pub use data_link::DataLink;
use self::rest::nms_request_get_vec;
use anyhow::Result;
/// Loads a complete list of all sites from UISP
pub async fn load_all_sites(config: LibreQoSConfig) -> Result<Vec<Site>> {
Ok(nms_request_get_vec("sites", &config.uisp_auth_token, &config.uisp_base_url).await?)
}
/// Load all devices from UISP that are authorized, and include their full interface definitions
pub async fn load_all_devices_with_interfaces(config: LibreQoSConfig) -> Result<Vec<Device>> {
Ok(nms_request_get_vec(
"devices?withInterfaces=true&authorized=true",
&config.uisp_auth_token,
&config.uisp_base_url,
)
.await?)
}
/// Loads all data links from UISP (including links in client sites)
pub async fn load_all_data_links(config: LibreQoSConfig) -> Result<Vec<DataLink>> {
Ok(nms_request_get_vec("data-links", &config.uisp_auth_token, &config.uisp_base_url).await?)
}

136
src/rust/uisp/src/rest.rs Normal file
View File

@ -0,0 +1,136 @@
use anyhow::Result;
use serde::de::DeserializeOwned;
fn url_fixup(base: &str) -> String {
if base.contains("/nms/api/v2.1") {
base.to_string()
} else {
format!("{base}/nms/api/v2.1")
}
}
/// Submits a request to the UNMS API and returns the result as unprocessed text.
/// This is a debug function: it doesn't do any parsing
#[allow(dead_code)]
pub async fn nms_request_get_text(
url: &str,
key: &str,
api: &str,
) -> Result<String, reqwest::Error> {
let full_url = format!("{}/{}", url_fixup(api), url);
//println!("{full_url}");
let client = reqwest::Client::new();
let res = client
.get(&full_url)
.header("'Content-Type", "application/json")
.header("X-Auth-Token", key)
.send()
.await
.unwrap();
res.text().await
}
/// Submits a request to the UNMS API, returning a deserialized vector of type T.
#[allow(dead_code)]
pub async fn nms_request_get_vec<T>(
url: &str,
key: &str,
api: &str,
) -> Result<Vec<T>, reqwest::Error>
where
T: DeserializeOwned,
{
let full_url = format!("{}/{}", url_fixup(api), url);
//println!("{full_url}");
let client = reqwest::Client::new();
let res = client
.get(&full_url)
.header("'Content-Type", "application/json")
.header("X-Auth-Token", key)
.send()
.await?;
res.json::<Vec<T>>().await
}
#[allow(dead_code)]
pub async fn nms_request_get_one<T>(url: &str, key: &str, api: &str) -> Result<T, reqwest::Error>
where
T: DeserializeOwned,
{
let full_url = format!("{}/{}", url_fixup(api), url);
//println!("{full_url}");
let client = reqwest::Client::new();
let res = client
.get(&full_url)
.header("'Content-Type", "application/json")
.header("X-Auth-Token", key)
.send()
.await?;
res.json::<T>().await
}
/// This is a debug function: it doesn't do any parsing
#[allow(dead_code)]
pub async fn crm_request_get_text(
api: &str,
key: &str,
url: &str,
) -> Result<String, reqwest::Error> {
let full_url = format!("{}/{}", url_fixup(api), url);
let client = reqwest::Client::new();
let res = client
.get(&full_url)
.header("'Content-Type", "application/json")
.header("X-Auth-App-Key", key)
.send()
.await?;
res.text().await
}
#[allow(dead_code)]
pub async fn crm_request_get_vec<T>(
api: &str,
key: &str,
url: &str,
) -> Result<Vec<T>, reqwest::Error>
where
T: DeserializeOwned,
{
let full_url = format!("{}/{}", api, url);
let client = reqwest::Client::new();
let res = client
.get(&full_url)
.header("'Content-Type", "application/json")
.header("X-Auth-App-Key", key)
.send()
.await?;
res.json::<Vec<T>>().await
}
#[allow(dead_code)]
pub async fn crm_request_get_one<T>(api: &str, key: &str, url: &str) -> Result<T, reqwest::Error>
where
T: DeserializeOwned,
{
let full_url = format!("{}/{}", api, url);
let client = reqwest::Client::new();
let res = client
.get(&full_url)
.header("'Content-Type", "application/json")
.header("X-Auth-App-Key", key)
.send()
.await?;
res.json::<T>().await
}

159
src/rust/uisp/src/site.rs Normal file
View File

@ -0,0 +1,159 @@
use serde::{Deserialize, Serialize};
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct Site {
pub id: String,
pub identification: Option<SiteId>,
pub description: Option<Description>,
pub qos: Option<Qos>,
pub ucrm: Option<Ucrm>,
}
impl Site {
pub fn name(&self) -> Option<String> {
if let Some(id) = &self.identification {
if let Some(name) = &id.name {
return Some(name.clone());
}
}
None
}
pub fn address(&self) -> Option<String> {
if let Some(desc) = &self.description {
if let Some(address) = &desc.address {
return Some(address.to_string());
}
}
None
}
pub fn is_tower(&self) -> bool {
if let Some(id) = &self.identification {
if let Some(site_type) = &id.site_type {
if site_type == "site" {
return true;
}
}
}
false
}
pub fn is_client_site(&self) -> bool {
if let Some(id) = &self.identification {
if let Some(site_type) = &id.site_type {
if site_type == "endpoint" {
return true;
}
}
}
false
}
pub fn is_child_of(&self, parent_id: &str) -> bool {
if let Some(id) = &self.identification {
if let Some(parent) = &id.parent {
if let Some(pid) = &parent.id {
if pid == parent_id {
return true;
}
}
}
}
false
}
pub fn qos(&self, default_download_mbps: u32, default_upload_mbps: u32) -> (u32, u32) {
let mut down = default_download_mbps;
let mut up = default_upload_mbps;
if let Some(qos) = &self.qos {
if let Some(d) = &qos.downloadSpeed {
down = *d as u32 / 1_000_000;
}
if let Some(u) = &qos.uploadSpeed {
up = *u as u32 / 1_000_000;
}
}
if down == 0 {
down = default_download_mbps;
}
if up == 0 {
up = default_upload_mbps;
}
(down, up)
}
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct SiteParent {
pub id: Option<String>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct SiteId {
pub name: Option<String>,
#[serde(rename = "type")]
pub site_type: Option<String>,
pub parent: Option<SiteParent>,
pub status: Option<String>,
pub suspended: bool,
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Endpoint {
pub id: Option<String>,
pub name: Option<String>,
pub parentId: Option<String>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct Description {
pub address: Option<String>,
pub location: Option<Location>,
pub height: Option<f64>,
pub endpoints: Option<Vec<Endpoint>>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct Location {
pub longitude: f64,
pub latitude: f64,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct Qos {
pub enabled: bool,
pub downloadSpeed: Option<usize>,
pub uploadSpeed: Option<usize>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct Ucrm {
pub client: Option<UcrmClient>,
pub service: Option<UcrmService>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct UcrmClient {
pub id: String,
pub name: String,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct UcrmService {
pub id: String,
pub name: String,
pub status: i32,
pub tariffId: String,
pub trafficShapingOverrideEnabled: bool,
}