mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
Starting to add UISP data module
This commit is contained in:
parent
a5c8faa0eb
commit
d73e12c8e2
10
src/rust/Cargo.lock
generated
10
src/rust/Cargo.lock
generated
@ -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"
|
||||
|
@ -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
10
src/rust/uisp/Cargo.toml
Normal 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"
|
42
src/rust/uisp/src/data_link.rs
Normal file
42
src/rust/uisp/src/data_link.rs
Normal 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
178
src/rust/uisp/src/device.rs
Normal 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
36
src/rust/uisp/src/lib.rs
Normal 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
136
src/rust/uisp/src/rest.rs
Normal 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
159
src/rust/uisp/src/site.rs
Normal 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,
|
||||
}
|
Loading…
Reference in New Issue
Block a user