mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
Merge pull request #533 from LibreQoE/user_interface_2
User interface 2
This commit is contained in:
commit
35b436f48b
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: sudo /bin/apt-get update; sudo /bin/apt-get install --no-install-recommends python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`/bin/uname r` libbpf-dev
|
||||
run: sudo /bin/apt-get update; sudo /bin/apt-get install --no-install-recommends python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`/bin/uname r` libbpf-dev mold
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- name: Build
|
||||
run: pushd src/rust; cargo build --verbose --all; popd
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -121,3 +121,5 @@ src/network.pdf
|
||||
src/ShapedDevices.csv.good
|
||||
.gitignore
|
||||
src/rust/lqosd/lts_keys.bin
|
||||
src/rust/lqosd/src/node_manager/js_build/out
|
||||
src/bin/dashboards
|
||||
|
@ -27,17 +27,34 @@ from liblqos_python import is_lqosd_alive, clear_ip_mappings, delete_ip_mapping,
|
||||
run_shell_commands_as_sudo, generated_pn_download_mbps, generated_pn_upload_mbps, queues_available_override, \
|
||||
on_a_stick, get_tree_weights, get_weights
|
||||
|
||||
R2Q = 10
|
||||
#MAX_R2Q = 200_000
|
||||
MAX_R2Q = 60_000 # See https://lartc.vger.kernel.narkive.com/NKaH1ZNG/htb-quantum-of-class-100001-is-small-consider-r2q-change
|
||||
MIN_QUANTUM = 1522
|
||||
|
||||
def calculateR2q(maxRateInMbps):
|
||||
# So we've learned that r2q defaults to 10, and is used to calculate quantum. Quantum is rateInBytes/r2q by
|
||||
# default. This default gives errors at high rates, and tc clamps the quantum to 200000. Setting a high quantum
|
||||
# directly gives no errors. So we want to calculate r2q to default to 10, but not exceed 200000 for the highest
|
||||
# specified rate (which will be the available bandwidth rate).
|
||||
maxRateInBytesPerSecond = maxRateInMbps * 125000
|
||||
r2q = 10
|
||||
quantum = maxRateInBytesPerSecond / r2q
|
||||
while quantum > MAX_R2Q:
|
||||
r2q += 1
|
||||
quantum = maxRateInBytesPerSecond / r2q
|
||||
global R2Q
|
||||
R2Q = r2q
|
||||
|
||||
def quantum(rateInMbps):
|
||||
# Attempt to calculate an appropriate quantum for an HTB queue, given
|
||||
# that `mq` does not appear to carry a valid `r2q` value to individual
|
||||
# root nodes.
|
||||
R2Q = 10
|
||||
rateInBytesPerSecond = rateInMbps * 125000
|
||||
quantum = int(rateInBytesPerSecond / R2Q)
|
||||
quantum = max(MIN_QUANTUM, int(rateInBytesPerSecond / R2Q))
|
||||
#print("R2Q=" + str(R2Q) + ", quantum: " + str(quantum))
|
||||
quantrumString = " quantum " + str(quantum)
|
||||
#print("Calculated quantum for " + str(rateInMbps) + " Mbps: " + str(quantum))
|
||||
return quantrumString
|
||||
#return " quantum 1522"
|
||||
|
||||
# Automatically account for TCP overhead of plans. For example a 100Mbps plan needs to be set to 109Mbps for the user to ever see that result on a speed test
|
||||
# Does not apply to nodes of any sort, just endpoint devices
|
||||
@ -793,6 +810,8 @@ def refreshShapers():
|
||||
logging.info("# MQ Setup for " + thisInterface)
|
||||
command = 'qdisc replace dev ' + thisInterface + ' root handle 7FFF: mq'
|
||||
linuxTCcommands.append(command)
|
||||
maxBandwidth = max(upstream_bandwidth_capacity_upload_mbps(), upstream_bandwidth_capacity_download_mbps())
|
||||
calculateR2q(maxBandwidth)
|
||||
for queue in range(queuesAvailable):
|
||||
command = 'qdisc add dev ' + thisInterface + ' parent 7FFF:' + hex(queue+1) + ' handle ' + hex(queue+1) + ': htb default 2'
|
||||
linuxTCcommands.append(command)
|
||||
|
@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
After=network.service lqosd.service
|
||||
Requires=lqosd.service
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/opt/libreqos/src/bin
|
||||
ExecStart=/opt/libreqos/src/bin/lqos_node_manager
|
||||
Restart=always
|
||||
#Turn on debuging for service
|
||||
#Environment=RUST_LOG=info
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
@ -21,8 +21,8 @@ LQOS_DIR=$DPKG_DIR/opt/libreqos/src
|
||||
ETC_DIR=$DPKG_DIR/etc
|
||||
MOTD_DIR=$DPKG_DIR/etc/update-motd.d
|
||||
LQOS_FILES="graphInfluxDB.py influxDBdashboardTemplate.json integrationCommon.py integrationPowercode.py integrationRestHttp.py integrationSonar.py integrationSplynx.py integrationUISP.py integrationSonar.py LibreQoS.py lqos.example lqTools.py mikrotikFindIPv6.py network.example.json pythonCheck.py README.md scheduler.py ShapedDevices.example.csv lqos.example ../requirements.txt"
|
||||
LQOS_BIN_FILES="lqos_scheduler.service.example lqosd.service.example lqos_node_manager.service.example"
|
||||
RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_setup lqos_map_perf uisp_integration lqos_support_tool"
|
||||
LQOS_BIN_FILES="lqos_scheduler.service.example lqosd.service.example"
|
||||
RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqusers lqos_setup lqos_map_perf uisp_integration lqos_support_tool"
|
||||
|
||||
####################################################
|
||||
# Clean any previous dist build
|
||||
@ -39,12 +39,12 @@ mkdir -p $DEBIAN_DIR
|
||||
|
||||
# Build the chroot directory structure
|
||||
mkdir -p $LQOS_DIR
|
||||
mkdir -p $LQOS_DIR/bin/static
|
||||
mkdir -p $LQOS_DIR/bin/static2
|
||||
mkdir -p $ETC_DIR
|
||||
mkdir -p $MOTD_DIR
|
||||
|
||||
# Create the Debian control file
|
||||
pushd $DEBIAN_DIR > /dev/null
|
||||
pushd $DEBIAN_DIR > /dev/null || exit
|
||||
touch control
|
||||
echo "Package: $PACKAGE" >> control
|
||||
echo "Version: $VERSION" >> control
|
||||
@ -52,10 +52,10 @@ echo "Architecture: amd64" >> control
|
||||
echo "Maintainer: Herbert Wolverson <herberticus@gmail.com>" >> control
|
||||
echo "Description: CAKE-based traffic shaping for ISPs" >> control
|
||||
echo "Depends: $APT_DEPENDENCIES" >> control
|
||||
popd > /dev/null
|
||||
popd > /dev/null || exit
|
||||
|
||||
# Create the post-installation file
|
||||
pushd $DEBIAN_DIR > /dev/null
|
||||
pushd $DEBIAN_DIR > /dev/null || exit
|
||||
touch postinst
|
||||
echo "#!/bin/bash" >> postinst
|
||||
echo "# Install Python Dependencies" >> postinst
|
||||
@ -66,13 +66,13 @@ echo "sudo python3 -m pip install --break-system-packages -r src/requirements.tx
|
||||
# - Run lqsetup
|
||||
echo "/opt/libreqos/src/bin/lqos_setup" >> postinst
|
||||
# - Setup the services
|
||||
echo "cp /opt/libreqos/src/bin/lqos_node_manager.service.example /etc/systemd/system/lqos_node_manager.service" >> postinst
|
||||
echo "cp /opt/libreqos/src/bin/lqosd.service.example /etc/systemd/system/lqosd.service" >> postinst
|
||||
echo "cp /opt/libreqos/src/bin/lqos_scheduler.service.example /etc/systemd/system/lqos_scheduler.service" >> postinst
|
||||
echo "/bin/systemctl daemon-reload" >> postinst
|
||||
echo "/bin/systemctl enable lqosd lqos_node_manager lqos_scheduler" >> postinst
|
||||
echo "/bin/systemctl stop lqos_node_manager" >> postinst # In case it's running from a previous release
|
||||
echo "/bin/systemctl disable lqos_node_manager" >> postinst # In case it's running from a previous release
|
||||
echo "/bin/systemctl enable lqosd lqos_scheduler" >> postinst
|
||||
echo "/bin/systemctl start lqosd" >> postinst
|
||||
echo "/bin/systemctl start lqos_node_manager" >> postinst
|
||||
echo "/bin/systemctl start lqos_scheduler" >> postinst
|
||||
echo "popd" >> postinst
|
||||
# Attempting to fixup versioning issues with libpython.
|
||||
@ -92,19 +92,18 @@ chmod a+x postinst
|
||||
# Uninstall Script
|
||||
touch postrm
|
||||
echo "#!/bin/bash" >> postrm
|
||||
echo "/bin/systemctl disable lqosd lqos_node_manager lqos_scheduler" >> postrm
|
||||
echo "/bin/systemctl stop lqosd" >> postrm
|
||||
echo "/bin/systemctl stop lqos_node_manager" >> postrm
|
||||
echo "/bin/systemctl stop lqos_scheduler" >> postrm
|
||||
echo "/bin/systemctl disable lqosd lqos_scheduler" >> postrm
|
||||
chmod a+x postrm
|
||||
popd > /dev/null
|
||||
popd > /dev/null || exit
|
||||
|
||||
# Create the cleanup file
|
||||
pushd $DEBIAN_DIR > /dev/null
|
||||
pushd $DEBIAN_DIR > /dev/null || exit
|
||||
touch postrm
|
||||
echo "#!/bin/bash" >> postrm
|
||||
chmod a+x postrm
|
||||
popd > /dev/null
|
||||
popd > /dev/null || exit
|
||||
|
||||
# Copy files into the LibreQoS directory
|
||||
for file in $LQOS_FILES
|
||||
@ -120,10 +119,10 @@ done
|
||||
|
||||
####################################################
|
||||
# Build the Rust programs
|
||||
pushd rust > /dev/null
|
||||
pushd rust > /dev/null || exit
|
||||
cargo clean
|
||||
cargo build --all --release
|
||||
popd > /dev/null
|
||||
popd > /dev/null || exit
|
||||
|
||||
# Copy newly built Rust files
|
||||
# - The Python integration Library
|
||||
@ -133,13 +132,14 @@ for prog in $RUSTPROGS
|
||||
do
|
||||
cp rust/target/release/$prog $LQOS_DIR/bin
|
||||
done
|
||||
# - The webserver skeleton files
|
||||
cp rust/lqos_node_manager/Rocket.toml $LQOS_DIR/bin
|
||||
cp -R rust/lqos_node_manager/static/* $LQOS_DIR/bin/static
|
||||
# Compile the website
|
||||
pushd rust/lqosd > /dev/null || exit
|
||||
./copy_files.sh
|
||||
popd || exit
|
||||
|
||||
####################################################
|
||||
# Add Message of the Day
|
||||
pushd $MOTD_DIR > /dev/null
|
||||
pushd $MOTD_DIR > /dev/null || exit
|
||||
echo "#!/bin/bash" > 99-libreqos
|
||||
echo "MY_IP=\'hostname -I | cut -d' ' -f1\'" >> 99-libreqos
|
||||
echo "echo \"\"" >> 99-libreqos
|
||||
@ -147,7 +147,7 @@ echo "echo \"LibreQoS Traffic Shaper is installed on this machine.\"" >> 99-libr
|
||||
echo "echo \"Point a browser at http://\$MY_IP:9123/ to manage it.\"" >> 99-libreqos
|
||||
echo "echo \"\"" >> 99-libreqos
|
||||
chmod a+x 99-libreqos
|
||||
popd
|
||||
popd || exit
|
||||
|
||||
####################################################
|
||||
# Assemble the package
|
||||
|
@ -3,13 +3,12 @@
|
||||
# This script builds the Rust sub-system and places the results in the
|
||||
# `src/bin` directory.
|
||||
#
|
||||
# You still need to setup services to run `lqosd` and `lqos_node_manager`
|
||||
# automatically.
|
||||
# You still need to setup services to run `lqosd` and possibly `lqos_scheduler` automatically.
|
||||
#
|
||||
# Don't forget to setup `/etc/lqos.conf`
|
||||
|
||||
# Check Pre-Requisites
|
||||
sudo apt install python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`uname -r` libbpf-dev libssl-dev
|
||||
sudo apt install python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`uname -r` libbpf-dev libssl-dev esbuild mold
|
||||
|
||||
if ! rustup -V &> /dev/null
|
||||
then
|
||||
@ -39,7 +38,7 @@ rustup update
|
||||
|
||||
# Start building
|
||||
echo "Please wait while the system is compiled. Service will not be interrupted during this stage."
|
||||
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_map_perf uisp_integration lqos_support_tool"
|
||||
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqusers lqos_map_perf uisp_integration lqos_support_tool"
|
||||
mkdir -p bin/static
|
||||
pushd rust > /dev/null
|
||||
#cargo clean
|
||||
@ -65,10 +64,10 @@ done
|
||||
popd > /dev/null
|
||||
|
||||
# Copy the node manager's static web content
|
||||
cp -R rust/lqos_node_manager/static/* bin/static
|
||||
|
||||
# Copy Rocket.toml to tell the node manager where to listen
|
||||
cp rust/lqos_node_manager/Rocket.toml bin/
|
||||
mkdir -p bin/static2/vendor
|
||||
pushd rust/lqosd > /dev/null
|
||||
./copy_files.sh
|
||||
popd > /dev/null
|
||||
|
||||
# Copy the Python library for LibreQoS.py et al.
|
||||
pushd rust/lqos_python > /dev/null
|
||||
@ -87,6 +86,11 @@ service_exists() {
|
||||
fi
|
||||
}
|
||||
|
||||
if service_exists lqos_node_manager; then
|
||||
echo "lqos_node_manager is running as a service. It's not needed anymore. Killing it."
|
||||
sudo systemctl stop lqos_node_manager
|
||||
sudo systemctl disable lqos_node_manager
|
||||
fi
|
||||
if service_exists lqosd; then
|
||||
echo "lqosd is running as a service. Restarting it. You may need to enter your sudo password."
|
||||
sudo systemctl restart lqosd
|
||||
@ -95,10 +99,6 @@ if service_exists lqos_scheduler; then
|
||||
echo "lqos_scheduler is running as a service. Restarting it. You may need to enter your sudo password."
|
||||
sudo systemctl restart lqos_scheduler
|
||||
fi
|
||||
if service_exists lqos_node_manager; then
|
||||
echo "lqos_node_manager is running as a service. Restarting it. You may need to enter your sudo password."
|
||||
sudo systemctl restart lqos_node_manager
|
||||
fi
|
||||
|
||||
echo "-----------------------------------------------------------------"
|
||||
echo "Don't forget to setup /etc/lqos.conf!"
|
||||
|
@ -88,6 +88,7 @@ bandwidth_overhead_factor = 1.0
|
||||
commit_bandwidth_multiplier = 0.98
|
||||
exception_cpes = []
|
||||
use_ptmp_as_parent = false
|
||||
ignore_calculated_capacity = false
|
||||
|
||||
[powercode_integration]
|
||||
enable_powercode = false
|
||||
|
3
src/rust/.cargo/config.toml
Normal file
3
src/rust/.cargo/config.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "/usr/bin/clang"
|
||||
rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold"]
|
1312
src/rust/Cargo.lock
generated
1312
src/rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,11 +4,14 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-2.0-only"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[profile.release]
|
||||
strip = "debuginfo"
|
||||
lto = "fat"
|
||||
lto = "thin"
|
||||
incremental = true
|
||||
|
||||
[profile.release.build-override]
|
||||
opt-level = 3
|
||||
codegen-units = 16
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
@ -19,7 +22,6 @@ members = [
|
||||
"lqtop", # A command line utility to show current activity
|
||||
"xdp_iphash_to_cpu_cmdline", # Rust port of the C xdp_iphash_to_cpu_cmdline tool, for compatibility
|
||||
"xdp_pping", # Rust port of cpumap's `xdp_pping` tool, for compatibility
|
||||
"lqos_node_manager", # A lightweight web interface for management and local monitoring
|
||||
"lqos_python", # Python bindings for using the Rust bus directly
|
||||
"lqusers", # CLI control for managing the web user list
|
||||
"lqos_utils", # A collection of macros and helpers we find useful
|
||||
@ -34,3 +36,37 @@ members = [
|
||||
"uisp_integration", # UISP Integration in Rust
|
||||
"lqos_support_tool", # A Helper tool to make it easier to request/receive support
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
log = "0"
|
||||
bincode = "1"
|
||||
once_cell = "1"
|
||||
nix = { version = "0", features = ["time"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_cbor = "0" # For RFC8949/7409 format C binary objects
|
||||
serde_json = "1"
|
||||
csv = "1"
|
||||
ip_network_table = "0"
|
||||
ip_network = "0"
|
||||
sha2 = "0"
|
||||
uuid = { version = "1", features = ["v4", "fast-rng" ] }
|
||||
dashmap = "5"
|
||||
toml = "0.8.8"
|
||||
zerocopy = {version = "0.6.1", features = [ "simd" ] }
|
||||
sysinfo = "0"
|
||||
default-net = "0"
|
||||
reqwest = { version = "0", features = ["json", "blocking"] }
|
||||
pyo3 = "0.20.3"
|
||||
colored = "2"
|
||||
miniz_oxide = "0.7"
|
||||
byteorder = "1"
|
||||
num-traits = "0.2.19"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# May have to change this one for ARM?
|
||||
jemallocator = "0.5"
|
||||
|
@ -6,11 +6,10 @@ license = "GPL-2.0-only"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.25.0", features = ["full"] }
|
||||
anyhow = "1"
|
||||
anyhow = { workspace = true }
|
||||
env_logger = "0"
|
||||
log = "0"
|
||||
log = { workspace = true }
|
||||
lqos_bus = { path = "../lqos_bus" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_cbor = "0" # For RFC8949/7409 format C binary objects
|
||||
serde_cbor = { workspace = true }
|
||||
sqlite = "0.30.4"
|
||||
axum = "0.6"
|
||||
|
@ -9,16 +9,16 @@ default = ["equinix_tests"]
|
||||
equinix_tests = []
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
bincode = "1"
|
||||
thiserror = "1"
|
||||
serde = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
lts_client = { path = "../lts_client" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
log = "0"
|
||||
nix = "0"
|
||||
serde_cbor = "0" # For RFC8949/7409 format C binary objects
|
||||
tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
serde_cbor = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0", features = [ "html_reports", "async_tokio"] }
|
||||
|
@ -5,6 +5,7 @@ use crate::{
|
||||
use lts_client::transport_data::{StatsTotals, StatsHost, StatsTreeNode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
use lqos_utils::units::DownUpOrder;
|
||||
|
||||
/// A `BusResponse` object represents a single
|
||||
/// reply generated from a `BusRequest`, and batched
|
||||
@ -24,18 +25,18 @@ pub enum BusResponse {
|
||||
/// Current throughput for the overall system.
|
||||
CurrentThroughput {
|
||||
/// In bps
|
||||
bits_per_second: (u64, u64),
|
||||
bits_per_second: DownUpOrder<u64>,
|
||||
|
||||
/// In pps
|
||||
packets_per_second: (u64, u64),
|
||||
packets_per_second: DownUpOrder<u64>,
|
||||
|
||||
/// How much of the response has been subject to the shaper?
|
||||
shaped_bits_per_second: (u64, u64),
|
||||
shaped_bits_per_second: DownUpOrder<u64>,
|
||||
},
|
||||
|
||||
/// Provides a list of ALL mapped hosts traffic counters,
|
||||
/// listing the IP Address and upload/download in a tuple.
|
||||
HostCounters(Vec<(IpAddr, u64, u64)>),
|
||||
HostCounters(Vec<(IpAddr, DownUpOrder<u64>)>),
|
||||
|
||||
/// Provides the Top N downloaders IP stats.
|
||||
TopDownloaders(Vec<IpStats>),
|
||||
@ -89,7 +90,7 @@ pub enum BusResponse {
|
||||
/// Us to poll hosts
|
||||
time_to_poll_hosts: u64,
|
||||
/// High traffic watermark
|
||||
high_watermark: (u64, u64),
|
||||
high_watermark: DownUpOrder<u64>,
|
||||
/// Number of flows tracked
|
||||
tracked_flows: u64,
|
||||
/// RTT events per second
|
||||
@ -132,7 +133,7 @@ pub enum BusResponse {
|
||||
FlowsByIp(Vec<FlowbeeSummaryData>),
|
||||
|
||||
/// Current endpoints by country
|
||||
CurrentEndpointsByCountry(Vec<(String, [u64; 2], [f32; 2])>),
|
||||
CurrentEndpointsByCountry(Vec<(String, DownUpOrder<u64>, [f32; 2], String)>),
|
||||
|
||||
/// Current Lat/Lon of endpoints
|
||||
CurrentLatLon(Vec<(f64, f64, String, u64, f32)>),
|
||||
@ -140,19 +141,19 @@ pub enum BusResponse {
|
||||
/// Summary of Ether Protocol
|
||||
EtherProtocols{
|
||||
/// Number of IPv4 Bytes
|
||||
v4_bytes: [u64; 2],
|
||||
v4_bytes: DownUpOrder<u64>,
|
||||
/// Number of IPv6 Bytes
|
||||
v6_bytes: [u64; 2],
|
||||
v6_bytes: DownUpOrder<u64>,
|
||||
/// Number of IPv4 Packets
|
||||
v4_packets: [u64; 2],
|
||||
v4_packets: DownUpOrder<u64>,
|
||||
/// Number of IPv6 Packets
|
||||
v6_packets: [u64; 2],
|
||||
v6_packets: DownUpOrder<u64>,
|
||||
/// Number of IPv4 Flows
|
||||
v4_rtt: [u64; 2],
|
||||
v4_rtt: DownUpOrder<u64>,
|
||||
/// Number of IPv6 Flows
|
||||
v6_rtt: [u64; 2],
|
||||
v6_rtt: DownUpOrder<u64>,
|
||||
},
|
||||
|
||||
/// Summary of IP Protocols
|
||||
IpProtocols(Vec<(String, (u64, u64))>),
|
||||
IpProtocols(Vec<(String, DownUpOrder<u64>)>),
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::TcHandle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use lqos_utils::units::DownUpOrder;
|
||||
|
||||
/// Transmission representation of IP statistics associated
|
||||
/// with a host.
|
||||
@ -11,13 +12,12 @@ pub struct IpStats {
|
||||
/// The host's mapped circuit ID
|
||||
pub circuit_id: String,
|
||||
|
||||
/// The current bits-per-second passing through this host. Tuple
|
||||
/// 0 is download, tuple 1 is upload.
|
||||
pub bits_per_second: (u64, u64),
|
||||
/// The current bits-per-second passing through this host.
|
||||
pub bits_per_second: DownUpOrder<u64>,
|
||||
|
||||
/// The current packets-per-second passing through this host. Tuple
|
||||
/// 0 is download, tuple 1 is upload.
|
||||
pub packets_per_second: (u64, u64),
|
||||
pub packets_per_second: DownUpOrder<u64>,
|
||||
|
||||
/// Median TCP round-trip-time for this host at the current time.
|
||||
pub median_tcp_rtt: f32,
|
||||
@ -26,7 +26,7 @@ pub struct IpStats {
|
||||
pub tc_handle: TcHandle,
|
||||
|
||||
/// TCP Retransmits for this host at the current time.
|
||||
pub tcp_retransmits: (u64, u64),
|
||||
pub tcp_retransmits: DownUpOrder<u64>,
|
||||
}
|
||||
|
||||
/// Represents an IP Mapping in the XDP IP to TC/CPU mapping system.
|
||||
@ -153,13 +153,13 @@ pub struct FlowbeeSummaryData {
|
||||
/// Time (nanos) when the connection was last seen
|
||||
pub last_seen: u64,
|
||||
/// Bytes transmitted
|
||||
pub bytes_sent: [u64; 2],
|
||||
pub bytes_sent: DownUpOrder<u64>,
|
||||
/// Packets transmitted
|
||||
pub packets_sent: [u64; 2],
|
||||
pub packets_sent: DownUpOrder<u64>,
|
||||
/// Rate estimate
|
||||
pub rate_estimate_bps: [u32; 2],
|
||||
pub rate_estimate_bps: DownUpOrder<u32>,
|
||||
/// TCP Retransmission count (also counts duplicates)
|
||||
pub tcp_retransmits: [u16; 2],
|
||||
pub tcp_retransmits: DownUpOrder<u16>,
|
||||
/// Has the connection ended?
|
||||
/// 0 = Alive, 1 = FIN, 2 = RST
|
||||
pub end_status: u8,
|
||||
@ -168,7 +168,7 @@ pub struct FlowbeeSummaryData {
|
||||
/// Raw TCP flags
|
||||
pub flags: u8,
|
||||
/// Recent RTT median
|
||||
pub rtt_nanos: [u64; 2],
|
||||
pub rtt_nanos: DownUpOrder<u64>,
|
||||
/// Remote ASN
|
||||
pub remote_asn: u32,
|
||||
/// Remote ASN Name
|
||||
@ -177,4 +177,8 @@ pub struct FlowbeeSummaryData {
|
||||
pub remote_asn_country: String,
|
||||
/// Analysis
|
||||
pub analysis: String,
|
||||
/// Circuit ID
|
||||
pub circuit_id: String,
|
||||
/// Circuit Name
|
||||
pub circuit_name: String,
|
||||
}
|
||||
|
@ -5,17 +5,17 @@ edition = "2021"
|
||||
license = "GPL-2.0-only"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
thiserror = { workspace = true }
|
||||
toml_edit = { version = "0", features = [ "serde" ] }
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
serde_json = "1"
|
||||
csv = "1"
|
||||
ip_network_table = "0"
|
||||
ip_network = "0"
|
||||
sha2 = "0"
|
||||
uuid = { version = "1", features = ["v4", "fast-rng" ] }
|
||||
log = "0"
|
||||
dashmap = "5"
|
||||
pyo3 = "0.20"
|
||||
toml = "0.8.8"
|
||||
once_cell = "1.19.0"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
csv = { workspace = true }
|
||||
ip_network_table = { workspace = true }
|
||||
ip_network = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
log = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
pyo3 = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
|
@ -66,6 +66,11 @@ impl WebUsers {
|
||||
Ok(filename)
|
||||
}
|
||||
|
||||
/// Is the list of users empty?
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.users.is_empty()
|
||||
}
|
||||
|
||||
fn save_to_disk(&self) -> Result<(), AuthenticationError> {
|
||||
let path = Self::path()?;
|
||||
let new_contents = toml_edit::ser::to_string(&self);
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Manages the `/etc/lqos.conf` file.
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml_edit::{Document, value};
|
||||
use toml_edit::{DocumentMut, value};
|
||||
use std::{fs, path::Path};
|
||||
use thiserror::Error;
|
||||
|
||||
@ -172,7 +172,7 @@ impl EtcLqos {
|
||||
|
||||
pub(crate) fn load_from_string(raw: &str) -> Result<Self, EtcLqosError> {
|
||||
log::info!("Trying to load old TOML version from /etc/lqos.conf");
|
||||
let document = raw.parse::<Document>();
|
||||
let document = raw.parse::<DocumentMut>();
|
||||
match document {
|
||||
Err(e) => {
|
||||
error!("Unable to parse TOML from /etc/lqos.conf");
|
||||
@ -199,7 +199,7 @@ impl EtcLqos {
|
||||
|
||||
/// Saves changes made to /etc/lqos.conf
|
||||
/// Copies current configuration into /etc/lqos.conf.backup first
|
||||
pub fn save(&self, document: &mut Document) -> Result<(), EtcLqosError> {
|
||||
pub fn save(&self, document: &mut DocumentMut) -> Result<(), EtcLqosError> {
|
||||
let cfg_path = Path::new("/etc/lqos.conf");
|
||||
let backup_path = Path::new("/etc/lqos.conf.backup");
|
||||
if let Err(e) = std::fs::copy(cfg_path, backup_path) {
|
||||
@ -223,7 +223,7 @@ impl EtcLqos {
|
||||
#[allow(dead_code)]
|
||||
pub fn enable_long_term_stats(license_key: String) {
|
||||
if let Ok(raw) = std::fs::read_to_string("/etc/lqos.conf") {
|
||||
let document = raw.parse::<Document>();
|
||||
let document = raw.parse::<DocumentMut>();
|
||||
match document {
|
||||
Err(e) => {
|
||||
error!("Unable to parse TOML from /etc/lqos.conf");
|
||||
@ -267,7 +267,7 @@ pub fn enable_long_term_stats(license_key: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_config(cfg_doc: &mut Document, cfg: &mut EtcLqos) {
|
||||
fn check_config(cfg_doc: &mut DocumentMut, cfg: &mut EtcLqos) {
|
||||
use sha2::digest::Update;
|
||||
use sha2::Digest;
|
||||
|
||||
@ -307,14 +307,14 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn round_trip_toml() {
|
||||
let doc = EXAMPLE_LQOS_CONF.parse::<toml_edit::Document>().unwrap();
|
||||
let doc = EXAMPLE_LQOS_CONF.parse::<toml_edit::DocumentMut>().unwrap();
|
||||
let reserialized = doc.to_string();
|
||||
assert_eq!(EXAMPLE_LQOS_CONF.trim(), reserialized.trim());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_node_id() {
|
||||
let mut doc = EXAMPLE_LQOS_CONF.parse::<toml_edit::Document>().unwrap();
|
||||
let mut doc = EXAMPLE_LQOS_CONF.parse::<toml_edit::DocumentMut>().unwrap();
|
||||
doc["node_id"] = toml_edit::value("test");
|
||||
let reserialized = doc.to_string();
|
||||
assert!(reserialized.contains("node_id = \"test\""));
|
||||
|
@ -6,7 +6,7 @@ use super::{
|
||||
EtcLqosError, EtcLqos,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use toml_edit::Document;
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MigrationError {
|
||||
@ -28,7 +28,7 @@ pub fn migrate_if_needed() -> Result<(), MigrationError> {
|
||||
std::fs::read_to_string("/etc/lqos.conf").map_err(|e| MigrationError::ReadError(e))?;
|
||||
|
||||
let doc = raw
|
||||
.parse::<Document>()
|
||||
.parse::<DocumentMut>()
|
||||
.map_err(|e| MigrationError::ParseError(e))?;
|
||||
if let Some((_key, version)) = doc.get_key_value("version") {
|
||||
log::info!("Configuration file is at version {}", version.as_str().unwrap());
|
||||
|
@ -16,6 +16,12 @@ pub struct UispIntegration {
|
||||
pub commit_bandwidth_multiplier: f32,
|
||||
pub exception_cpes: Vec<ExceptionCpe>,
|
||||
pub use_ptmp_as_parent: bool,
|
||||
#[serde(default = "default_ignore_calculated_capacity")]
|
||||
pub ignore_calculated_capacity: bool,
|
||||
}
|
||||
|
||||
fn default_ignore_calculated_capacity() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@ -41,6 +47,7 @@ impl Default for UispIntegration {
|
||||
commit_bandwidth_multiplier: 1.0,
|
||||
exception_cpes: vec![],
|
||||
use_ptmp_as_parent: false,
|
||||
ignore_calculated_capacity: false,
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ mod shaped_devices;
|
||||
|
||||
pub use authentication::{UserRole, WebUsers};
|
||||
pub use etc::{load_config, Config, enable_long_term_stats, Tunables, BridgeConfig, update_config, disable_xdp_bridge};
|
||||
pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport};
|
||||
pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport, NetworkJsonCounting};
|
||||
pub use program_control::load_libreqos;
|
||||
pub use shaped_devices::{ConfigShapedDevices, ShapedDevice};
|
||||
|
||||
|
226
src/rust/lqos_config/src/network_json.rs
Normal file
226
src/rust/lqos_config/src/network_json.rs
Normal file
@ -0,0 +1,226 @@
|
||||
mod network_json_node;
|
||||
mod network_json_transport;
|
||||
mod network_json_counting;
|
||||
|
||||
use dashmap::DashSet;
|
||||
use log::{error, info};
|
||||
use serde_json::{Map, Value};
|
||||
use std::{
|
||||
fs, path::{Path, PathBuf},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use lqos_utils::units::DownUpOrder;
|
||||
pub use network_json_node::NetworkJsonNode;
|
||||
pub use network_json_transport::NetworkJsonTransport;
|
||||
pub use network_json_counting::NetworkJsonCounting;
|
||||
|
||||
/// Holder for the network.json representation.
|
||||
/// This is condensed into a single level vector with index-based referencing
|
||||
/// for easy use in funnel calculations.
|
||||
#[derive(Debug)]
|
||||
pub struct NetworkJson {
|
||||
/// Nodes that make up the tree, flattened and referenced by index number.
|
||||
/// TODO: We should add a primary key to nodes in network.json.
|
||||
nodes: Vec<NetworkJsonNode>,
|
||||
}
|
||||
|
||||
impl Default for NetworkJson {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkJson {
|
||||
/// Generates an empty network.json
|
||||
pub fn new() -> Self {
|
||||
Self { nodes: Vec::new() }
|
||||
}
|
||||
|
||||
/// The path to the current `network.json` file, determined
|
||||
/// by acquiring the prefix from the `/etc/lqos.conf` configuration
|
||||
/// file.
|
||||
pub fn path() -> Result<PathBuf, NetworkJsonError> {
|
||||
let cfg =
|
||||
crate::load_config().map_err(|_| NetworkJsonError::ConfigLoadError)?;
|
||||
let base_path = Path::new(&cfg.lqos_directory);
|
||||
Ok(base_path.join("network.json"))
|
||||
}
|
||||
|
||||
/// Does network.json exist?
|
||||
pub fn exists() -> bool {
|
||||
if let Ok(path) = Self::path() {
|
||||
path.exists()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to load network.json from disk
|
||||
pub fn load() -> Result<Self, NetworkJsonError> {
|
||||
let mut nodes = vec![NetworkJsonNode {
|
||||
name: "Root".to_string(),
|
||||
max_throughput: (0, 0),
|
||||
current_throughput: DownUpOrder::zeroed(),
|
||||
current_tcp_retransmits: DownUpOrder::zeroed(),
|
||||
current_drops: DownUpOrder::zeroed(),
|
||||
current_marks: DownUpOrder::zeroed(),
|
||||
parents: Vec::new(),
|
||||
immediate_parent: None,
|
||||
rtts: DashSet::new(),
|
||||
node_type: None,
|
||||
}];
|
||||
if !Self::exists() {
|
||||
return Err(NetworkJsonError::FileNotFound);
|
||||
}
|
||||
let path = Self::path()?;
|
||||
let raw = fs::read_to_string(path)
|
||||
.map_err(|_| NetworkJsonError::ConfigLoadError)?;
|
||||
let json: Value = serde_json::from_str(&raw)
|
||||
.map_err(|_| NetworkJsonError::ConfigLoadError)?;
|
||||
|
||||
// Start reading from the top. We are at the root node.
|
||||
let parents = vec![0];
|
||||
if let Value::Object(map) = &json {
|
||||
for (key, value) in map.iter() {
|
||||
if let Value::Object(inner_map) = value {
|
||||
recurse_node(&mut nodes, key, inner_map, &parents, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { nodes })
|
||||
}
|
||||
|
||||
/// Find the index of a circuit_id
|
||||
pub fn get_index_for_name(&self, name: &str) -> Option<usize> {
|
||||
self.nodes.iter().position(|n| n.name == name)
|
||||
}
|
||||
|
||||
/// Retrieve a cloned copy of a NetworkJsonNode entry, or None if there isn't
|
||||
/// an entry at that index.
|
||||
pub fn get_cloned_entry_by_index(
|
||||
&self,
|
||||
index: usize,
|
||||
) -> Option<NetworkJsonTransport> {
|
||||
self.nodes.get(index).map(|n| n.clone_to_transit())
|
||||
}
|
||||
|
||||
/// Retrieve a cloned copy of all children with a parent containing a specific
|
||||
/// node index.
|
||||
pub fn get_cloned_children(
|
||||
&self,
|
||||
index: usize,
|
||||
) -> Vec<(usize, NetworkJsonTransport)> {
|
||||
self
|
||||
.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_i, n)| n.immediate_parent == Some(index))
|
||||
.map(|(i, n)| (i, n.clone_to_transit()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find a circuit_id, and if it exists return its list of parent nodes
|
||||
/// as indices within the network_json layout.
|
||||
pub fn get_parents_for_circuit_id(
|
||||
&self,
|
||||
circuit_id: &str,
|
||||
) -> Option<Vec<usize>> {
|
||||
//println!("Looking for parents of {circuit_id}");
|
||||
self
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.name == circuit_id)
|
||||
.map(|node| node.parents.clone())
|
||||
}
|
||||
|
||||
/// Obtains a reference to nodes once we're sure that
|
||||
/// doing so will provide valid data.
|
||||
pub fn get_nodes_when_ready(&self) -> &Vec<NetworkJsonNode> {
|
||||
//log::warn!("Awaiting the network tree");
|
||||
//atomic_wait::wait(&self.busy, 1);
|
||||
//log::warn!("Acquired");
|
||||
&self.nodes
|
||||
}
|
||||
|
||||
/// Starts an update cycle. This clones the nodes into
|
||||
/// another structure - work will be performed on the clone.
|
||||
pub fn begin_update_cycle(&self) -> NetworkJsonCounting {
|
||||
NetworkJsonCounting::begin_update_cycle(self.nodes.clone())
|
||||
}
|
||||
|
||||
/// Finishes an update cycle. This is called after all updates
|
||||
/// have been made to the clone, and the clone is then copied back
|
||||
/// into the main structure.
|
||||
pub fn finish_update_cycle(&mut self, counting: NetworkJsonCounting) {
|
||||
if !counting.nodes.is_empty() {
|
||||
self.nodes = counting.nodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn json_to_u32(val: Option<&Value>) -> u32 {
|
||||
if let Some(val) = val {
|
||||
if let Some(n) = val.as_u64() {
|
||||
n as u32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn recurse_node(
|
||||
nodes: &mut Vec<NetworkJsonNode>,
|
||||
name: &str,
|
||||
json: &Map<String, Value>,
|
||||
parents: &[usize],
|
||||
immediate_parent: usize,
|
||||
) {
|
||||
info!("Mapping {name} from network.json");
|
||||
let mut parents = parents.to_vec();
|
||||
let my_id = if name != "children" {
|
||||
parents.push(nodes.len());
|
||||
nodes.len()
|
||||
} else {
|
||||
nodes.len() - 1
|
||||
};
|
||||
let node = NetworkJsonNode {
|
||||
parents: parents.to_vec(),
|
||||
max_throughput: (
|
||||
json_to_u32(json.get("downloadBandwidthMbps")),
|
||||
json_to_u32(json.get("uploadBandwidthMbps")),
|
||||
),
|
||||
current_throughput: DownUpOrder::zeroed(),
|
||||
current_tcp_retransmits: DownUpOrder::zeroed(),
|
||||
current_drops: DownUpOrder::zeroed(),
|
||||
current_marks: DownUpOrder::zeroed(),
|
||||
name: name.to_string(),
|
||||
immediate_parent: Some(immediate_parent),
|
||||
rtts: DashSet::new(),
|
||||
node_type: json.get("type").map(|v| v.as_str().unwrap().to_string()),
|
||||
};
|
||||
|
||||
if node.name != "children" {
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
// Recurse children
|
||||
for (key, value) in json.iter() {
|
||||
let key_str = key.as_str();
|
||||
if key_str != "uploadBandwidthMbps" && key_str != "downloadBandwidthMbps" {
|
||||
if let Value::Object(value) = value {
|
||||
recurse_node(nodes, key, value, &parents, my_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NetworkJsonError {
|
||||
#[error("Unable to find or load network.json")]
|
||||
ConfigLoadError,
|
||||
#[error("network.json not found or does not exist")]
|
||||
FileNotFound,
|
||||
}
|
@ -1,302 +0,0 @@
|
||||
use dashmap::DashSet;
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf}, sync::atomic::AtomicU64,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Describes a node in the network map tree.
|
||||
#[derive(Debug)]
|
||||
pub struct NetworkJsonNode {
|
||||
/// The node name, as it appears in `network.json`
|
||||
pub name: String,
|
||||
|
||||
/// The maximum throughput allowed per `network.json` for this node
|
||||
pub max_throughput: (u32, u32), // In mbps
|
||||
|
||||
/// Current throughput (in bytes/second) at this node
|
||||
pub current_throughput: (AtomicU64, AtomicU64), // In bytes
|
||||
|
||||
/// Approximate RTTs reported for this level of the tree.
|
||||
/// It's never going to be as statistically accurate as the actual
|
||||
/// numbers, being based on medians.
|
||||
pub rtts: DashSet<u16>,
|
||||
|
||||
/// A list of indices in the `NetworkJson` vector of nodes
|
||||
/// linking to parent nodes
|
||||
pub parents: Vec<usize>,
|
||||
|
||||
/// The immediate parent node
|
||||
pub immediate_parent: Option<usize>,
|
||||
|
||||
/// The node type
|
||||
pub node_type: Option<String>,
|
||||
}
|
||||
|
||||
impl NetworkJsonNode {
|
||||
/// Make a deep copy of a `NetworkJsonNode`, converting atomics
|
||||
/// into concrete values.
|
||||
pub fn clone_to_transit(&self) -> NetworkJsonTransport {
|
||||
NetworkJsonTransport {
|
||||
name: self.name.clone(),
|
||||
max_throughput: self.max_throughput,
|
||||
current_throughput: (
|
||||
self.current_throughput.0.load(std::sync::atomic::Ordering::Relaxed),
|
||||
self.current_throughput.1.load(std::sync::atomic::Ordering::Relaxed),
|
||||
),
|
||||
rtts: self.rtts.iter().map(|n| *n as f32 / 100.0).collect(),
|
||||
parents: self.parents.clone(),
|
||||
immediate_parent: self.immediate_parent,
|
||||
node_type: self.node_type.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A "transport-friendly" version of `NetworkJsonNode`. Designed
|
||||
/// to be quickly cloned from original nodes and efficiently
|
||||
/// transmitted/received.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct NetworkJsonTransport {
|
||||
/// Display name
|
||||
pub name: String,
|
||||
/// Max throughput for node (not clamped)
|
||||
pub max_throughput: (u32, u32),
|
||||
/// Current node throughput
|
||||
pub current_throughput: (u64, u64),
|
||||
/// Set of RTT data
|
||||
pub rtts: Vec<f32>,
|
||||
/// Node indices of parents
|
||||
pub parents: Vec<usize>,
|
||||
/// The immediate parent node in the tree
|
||||
pub immediate_parent: Option<usize>,
|
||||
/// The type of node (site, ap, etc.)
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: Option<String>,
|
||||
}
|
||||
|
||||
/// Holder for the network.json representation.
|
||||
/// This is condensed into a single level vector with index-based referencing
|
||||
/// for easy use in funnel calculations.
|
||||
#[derive(Debug)]
|
||||
pub struct NetworkJson {
|
||||
/// Nodes that make up the tree, flattened and referenced by index number.
|
||||
/// TODO: We should add a primary key to nodes in network.json.
|
||||
pub nodes: Vec<NetworkJsonNode>,
|
||||
}
|
||||
|
||||
impl Default for NetworkJson {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkJson {
|
||||
/// Generates an empty network.json
|
||||
pub fn new() -> Self {
|
||||
Self { nodes: Vec::new() }
|
||||
}
|
||||
|
||||
/// The path to the current `network.json` file, determined
|
||||
/// by acquiring the prefix from the `/etc/lqos.conf` configuration
|
||||
/// file.
|
||||
pub fn path() -> Result<PathBuf, NetworkJsonError> {
|
||||
let cfg =
|
||||
crate::load_config().map_err(|_| NetworkJsonError::ConfigLoadError)?;
|
||||
let base_path = Path::new(&cfg.lqos_directory);
|
||||
Ok(base_path.join("network.json"))
|
||||
}
|
||||
|
||||
/// Does network.json exist?
|
||||
pub fn exists() -> bool {
|
||||
if let Ok(path) = Self::path() {
|
||||
path.exists()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to load network.json from disk
|
||||
pub fn load() -> Result<Self, NetworkJsonError> {
|
||||
let mut nodes = vec![NetworkJsonNode {
|
||||
name: "Root".to_string(),
|
||||
max_throughput: (0, 0),
|
||||
current_throughput: (AtomicU64::new(0), AtomicU64::new(0)),
|
||||
parents: Vec::new(),
|
||||
immediate_parent: None,
|
||||
rtts: DashSet::new(),
|
||||
node_type: None,
|
||||
}];
|
||||
if !Self::exists() {
|
||||
return Err(NetworkJsonError::FileNotFound);
|
||||
}
|
||||
let path = Self::path()?;
|
||||
let raw = fs::read_to_string(path)
|
||||
.map_err(|_| NetworkJsonError::ConfigLoadError)?;
|
||||
let json: Value = serde_json::from_str(&raw)
|
||||
.map_err(|_| NetworkJsonError::ConfigLoadError)?;
|
||||
|
||||
// Start reading from the top. We are at the root node.
|
||||
let parents = vec![0];
|
||||
if let Value::Object(map) = &json {
|
||||
for (key, value) in map.iter() {
|
||||
if let Value::Object(inner_map) = value {
|
||||
recurse_node(&mut nodes, key, inner_map, &parents, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { nodes })
|
||||
}
|
||||
|
||||
/// Find the index of a circuit_id
|
||||
pub fn get_index_for_name(&self, name: &str) -> Option<usize> {
|
||||
self.nodes.iter().position(|n| n.name == name)
|
||||
}
|
||||
|
||||
/// Retrieve a cloned copy of a NetworkJsonNode entry, or None if there isn't
|
||||
/// an entry at that index.
|
||||
pub fn get_cloned_entry_by_index(
|
||||
&self,
|
||||
index: usize,
|
||||
) -> Option<NetworkJsonTransport> {
|
||||
self.nodes.get(index).map(|n| n.clone_to_transit())
|
||||
}
|
||||
|
||||
/// Retrieve a cloned copy of all children with a parent containing a specific
|
||||
/// node index.
|
||||
pub fn get_cloned_children(
|
||||
&self,
|
||||
index: usize,
|
||||
) -> Vec<(usize, NetworkJsonTransport)> {
|
||||
self
|
||||
.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_i, n)| n.immediate_parent == Some(index))
|
||||
.map(|(i, n)| (i, n.clone_to_transit()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find a circuit_id, and if it exists return its list of parent nodes
|
||||
/// as indices within the network_json layout.
|
||||
pub fn get_parents_for_circuit_id(
|
||||
&self,
|
||||
circuit_id: &str,
|
||||
) -> Option<Vec<usize>> {
|
||||
//println!("Looking for parents of {circuit_id}");
|
||||
self
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.name == circuit_id)
|
||||
.map(|node| node.parents.clone())
|
||||
}
|
||||
|
||||
/// Sets all current throughput values to zero
|
||||
/// Note that due to interior mutability, this does not require mutable
|
||||
/// access.
|
||||
pub fn zero_throughput_and_rtt(&self) {
|
||||
self.nodes.iter().for_each(|n| {
|
||||
n.current_throughput.0.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
n.current_throughput.1.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
n.rtts.clear();
|
||||
});
|
||||
}
|
||||
|
||||
/// Add throughput numbers to node entries. Note that this does *not* require
|
||||
/// mutable access due to atomics and interior mutability - so it is safe to use
|
||||
/// a read lock.
|
||||
pub fn add_throughput_cycle(
|
||||
&self,
|
||||
targets: &[usize],
|
||||
bytes: (u64, u64),
|
||||
) {
|
||||
for idx in targets {
|
||||
// Safety first: use "get" to ensure that the node exists
|
||||
if let Some(node) = self.nodes.get(*idx) {
|
||||
node.current_throughput.0.fetch_add(bytes.0, std::sync::atomic::Ordering::Relaxed);
|
||||
node.current_throughput.1.fetch_add(bytes.1, std::sync::atomic::Ordering::Relaxed);
|
||||
} else {
|
||||
warn!("No network tree entry for index {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Record RTT time in the tree. Note that due to interior mutability,
|
||||
/// this does not require mutable access.
|
||||
pub fn add_rtt_cycle(&self, targets: &[usize], rtt: f32) {
|
||||
for idx in targets {
|
||||
// Safety first: use "get" to ensure that the node exists
|
||||
if let Some(node) = self.nodes.get(*idx) {
|
||||
node.rtts.insert((rtt * 100.0) as u16);
|
||||
} else {
|
||||
warn!("No network tree entry for index {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn json_to_u32(val: Option<&Value>) -> u32 {
|
||||
if let Some(val) = val {
|
||||
if let Some(n) = val.as_u64() {
|
||||
n as u32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn recurse_node(
|
||||
nodes: &mut Vec<NetworkJsonNode>,
|
||||
name: &str,
|
||||
json: &Map<String, Value>,
|
||||
parents: &[usize],
|
||||
immediate_parent: usize,
|
||||
) {
|
||||
info!("Mapping {name} from network.json");
|
||||
let mut parents = parents.to_vec();
|
||||
let my_id = if name != "children" {
|
||||
parents.push(nodes.len());
|
||||
nodes.len()
|
||||
} else {
|
||||
nodes.len() - 1
|
||||
};
|
||||
let node = NetworkJsonNode {
|
||||
parents: parents.to_vec(),
|
||||
max_throughput: (
|
||||
json_to_u32(json.get("downloadBandwidthMbps")),
|
||||
json_to_u32(json.get("uploadBandwidthMbps")),
|
||||
),
|
||||
current_throughput: (AtomicU64::new(0), AtomicU64::new(0)),
|
||||
name: name.to_string(),
|
||||
immediate_parent: Some(immediate_parent),
|
||||
rtts: DashSet::new(),
|
||||
node_type: json.get("type").map(|v| v.as_str().unwrap().to_string()),
|
||||
};
|
||||
|
||||
if node.name != "children" {
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
// Recurse children
|
||||
for (key, value) in json.iter() {
|
||||
let key_str = key.as_str();
|
||||
if key_str != "uploadBandwidthMbps" && key_str != "downloadBandwidthMbps" {
|
||||
if let Value::Object(value) = value {
|
||||
recurse_node(nodes, key, value, &parents, my_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NetworkJsonError {
|
||||
#[error("Unable to find or load network.json")]
|
||||
ConfigLoadError,
|
||||
#[error("network.json not found or does not exist")]
|
||||
FileNotFound,
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
use log::warn;
|
||||
use lqos_utils::units::DownUpOrder;
|
||||
use crate::NetworkJsonNode;
|
||||
|
||||
/// Type used while updating the network tree with new data.
|
||||
/// Rather than have a race condition while the updates are performed
|
||||
/// (and potentially new requests come in, and receive invalid data),
|
||||
/// we copy the network tree into this structure, and then update this
|
||||
/// structure. Once the updates are complete, we copy the data back
|
||||
/// into the main network tree.
|
||||
pub struct NetworkJsonCounting {
|
||||
pub(super) nodes: Vec<NetworkJsonNode>,
|
||||
}
|
||||
|
||||
impl NetworkJsonCounting {
|
||||
/// Starts an update cycle. This clones the nodes into
|
||||
/// the `NetworkJsonCounting` structure - work will be performed on the clone.
|
||||
pub fn begin_update_cycle(nodes: Vec<NetworkJsonNode>) -> Self {
|
||||
Self { nodes }
|
||||
}
|
||||
|
||||
/// Sets all current throughput values to zero
|
||||
/// Note that due to interior mutability, this does not require mutable
|
||||
/// access.
|
||||
pub fn zero_throughput_and_rtt(&mut self) {
|
||||
//log::warn!("Locking network tree for throughput cycle");
|
||||
self.nodes.iter_mut().for_each(|n| {
|
||||
n.current_throughput.set_to_zero();
|
||||
n.current_tcp_retransmits.set_to_zero();
|
||||
n.rtts.clear();
|
||||
n.current_drops.set_to_zero();
|
||||
n.current_marks.set_to_zero();
|
||||
});
|
||||
}
|
||||
|
||||
/// Add throughput numbers to node entries. Note that this does *not* require
|
||||
/// mutable access due to atomics and interior mutability - so it is safe to use
|
||||
/// a read lock.
|
||||
pub fn add_throughput_cycle(
|
||||
&mut self,
|
||||
targets: &[usize],
|
||||
bytes: (u64, u64),
|
||||
) {
|
||||
for idx in targets {
|
||||
// Safety first: use "get" to ensure that the node exists
|
||||
if let Some(node) = self.nodes.get_mut(*idx) {
|
||||
node.current_throughput.checked_add_tuple(bytes);
|
||||
} else {
|
||||
warn!("No network tree entry for index {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Record RTT time in the tree. Note that due to interior mutability,
|
||||
/// this does not require mutable access.
|
||||
pub fn add_rtt_cycle(&self, targets: &[usize], rtt: f32) {
|
||||
for idx in targets {
|
||||
// Safety first: use "get" to ensure that the node exists
|
||||
if let Some(node) = self.nodes.get(*idx) {
|
||||
node.rtts.insert((rtt * 100.0) as u16);
|
||||
} else {
|
||||
warn!("No network tree entry for index {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Record TCP Retransmits in the tree.
|
||||
pub fn add_retransmit_cycle(&mut self, targets: &[usize], tcp_retransmits: DownUpOrder<u64>) {
|
||||
for idx in targets {
|
||||
// Safety first; use "get" to ensure that the node exists
|
||||
if let Some(node) = self.nodes.get_mut(*idx) {
|
||||
node.current_tcp_retransmits.checked_add(tcp_retransmits);
|
||||
} else {
|
||||
warn!("No network tree entry for index {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a series of CAKE marks and drops to the tree structure.
|
||||
pub fn add_queue_cycle(&mut self, targets: &[usize], marks: &DownUpOrder<u64>, drops: &DownUpOrder<u64>) {
|
||||
for idx in targets {
|
||||
// Safety first; use "get" to ensure that the node exists
|
||||
if let Some(node) = self.nodes.get_mut(*idx) {
|
||||
node.current_marks.checked_add(*marks);
|
||||
node.current_drops.checked_add(*drops);
|
||||
} else {
|
||||
warn!("No network tree entry for index {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
71
src/rust/lqos_config/src/network_json/network_json_node.rs
Normal file
71
src/rust/lqos_config/src/network_json/network_json_node.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use dashmap::DashSet;
|
||||
use lqos_utils::units::DownUpOrder;
|
||||
use crate::NetworkJsonTransport;
|
||||
|
||||
/// Describes a node in the network map tree.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkJsonNode {
|
||||
/// The node name, as it appears in `network.json`
|
||||
pub name: String,
|
||||
|
||||
/// The maximum throughput allowed per `network.json` for this node
|
||||
pub max_throughput: (u32, u32), // In mbps
|
||||
|
||||
/// Current throughput (in bytes/second) at this node
|
||||
pub current_throughput: DownUpOrder<u64>, // In bytes
|
||||
|
||||
/// Current TCP Retransmits
|
||||
pub current_tcp_retransmits: DownUpOrder<u64>, // In retries
|
||||
|
||||
/// Current Cake Marks
|
||||
pub current_marks: DownUpOrder<u64>,
|
||||
|
||||
/// Current Cake Drops
|
||||
pub current_drops: DownUpOrder<u64>,
|
||||
|
||||
/// Approximate RTTs reported for this level of the tree.
|
||||
/// It's never going to be as statistically accurate as the actual
|
||||
/// numbers, being based on medians.
|
||||
pub rtts: DashSet<u16>,
|
||||
|
||||
/// A list of indices in the `NetworkJson` vector of nodes
|
||||
/// linking to parent nodes
|
||||
pub parents: Vec<usize>,
|
||||
|
||||
/// The immediate parent node
|
||||
pub immediate_parent: Option<usize>,
|
||||
|
||||
/// The node type
|
||||
pub node_type: Option<String>,
|
||||
}
|
||||
|
||||
impl NetworkJsonNode {
|
||||
/// Make a deep copy of a `NetworkJsonNode`, converting atomics
|
||||
/// into concrete values.
|
||||
pub fn clone_to_transit(&self) -> NetworkJsonTransport {
|
||||
NetworkJsonTransport {
|
||||
name: self.name.clone(),
|
||||
max_throughput: self.max_throughput,
|
||||
current_throughput: (
|
||||
self.current_throughput.get_down(),
|
||||
self.current_throughput.get_up(),
|
||||
),
|
||||
current_retransmits: (
|
||||
self.current_tcp_retransmits.get_down(),
|
||||
self.current_tcp_retransmits.get_up(),
|
||||
),
|
||||
current_marks: (
|
||||
self.current_marks.get_down(),
|
||||
self.current_marks.get_up(),
|
||||
),
|
||||
current_drops: (
|
||||
self.current_drops.get_down(),
|
||||
self.current_drops.get_up(),
|
||||
),
|
||||
rtts: self.rtts.iter().map(|n| *n as f32 / 100.0).collect(),
|
||||
parents: self.parents.clone(),
|
||||
immediate_parent: self.immediate_parent,
|
||||
node_type: self.node_type.clone(),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A "transport-friendly" version of `NetworkJsonNode`. Designed
|
||||
/// to be quickly cloned from original nodes and efficiently
|
||||
/// transmitted/received.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct NetworkJsonTransport {
|
||||
/// Display name
|
||||
pub name: String,
|
||||
/// Max throughput for node (not clamped)
|
||||
pub max_throughput: (u32, u32),
|
||||
/// Current node throughput
|
||||
pub current_throughput: (u64, u64),
|
||||
/// Current count of TCP retransmits
|
||||
pub current_retransmits: (u64, u64),
|
||||
/// Cake marks
|
||||
pub current_marks: (u64, u64),
|
||||
/// Cake drops
|
||||
pub current_drops: (u64, u64),
|
||||
/// Set of RTT data
|
||||
pub rtts: Vec<f32>,
|
||||
/// Node indices of parents
|
||||
pub parents: Vec<usize>,
|
||||
/// The immediate parent node in the tree
|
||||
pub immediate_parent: Option<usize>,
|
||||
/// The type of node (site, ap, etc.)
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: Option<String>,
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
mod serializable;
|
||||
mod shaped_device;
|
||||
|
||||
use std::net::IpAddr;
|
||||
use crate::SUPPORTED_CUSTOMERS;
|
||||
use csv::{QuoteStyle, ReaderBuilder, WriterBuilder};
|
||||
use log::error;
|
||||
@ -7,6 +9,7 @@ use serializable::SerializableShapedDevice;
|
||||
pub use shaped_device::ShapedDevice;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use lqos_utils::XdpIpAddress;
|
||||
|
||||
/// Provides handling of the `ShapedDevices.csv` file that maps
|
||||
/// circuits to traffic shaping.
|
||||
@ -166,6 +169,21 @@ impl ConfigShapedDevices {
|
||||
//println!("Would write to file: {}", csv);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to search for an XdpIpAddress and return a circuit id and name
|
||||
/// if they exist.
|
||||
pub fn get_circuit_id_and_name_from_ip(&self, ip: &XdpIpAddress) -> Option<(String, String)> {
|
||||
let lookup = match ip.as_ip() {
|
||||
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
|
||||
IpAddr::V6(ip) => ip,
|
||||
};
|
||||
if let Some(c) = self.trie.longest_match(lookup) {
|
||||
let device = &self.devices[*c.1];
|
||||
return Some((device.circuit_id.clone(), device.circuit_name.clone()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -9,8 +9,8 @@ lqos_utils = { path = "../lqos_utils" }
|
||||
lqos_bus = { path = "../lqos_bus" }
|
||||
lqos_sys = { path = "../lqos_sys" }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
log = "0"
|
||||
zerocopy = {version = "0.6.1", features = [ "simd" ] }
|
||||
once_cell = "1.17.1"
|
||||
dashmap = "5.4.0"
|
||||
anyhow = "1"
|
||||
log = { workspace = true }
|
||||
zerocopy = { workspace = true }
|
||||
once_cell = { workspace = true}
|
||||
dashmap = { workspace = true }
|
||||
anyhow = { workspace = true }
|
@ -1,29 +0,0 @@
|
||||
[package]
|
||||
name = "lqos_node_manager"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-2.0-only"
|
||||
|
||||
[features]
|
||||
default = ["equinix_tests"]
|
||||
equinix_tests = []
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5.1", features = [ "json", "msgpack", "uuid" ] }
|
||||
rocket_async_compression = "0.6.0"
|
||||
lqos_bus = { path = "../lqos_bus" }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
anyhow = "1"
|
||||
sysinfo = "0"
|
||||
default-net = "0"
|
||||
nix = "0"
|
||||
once_cell = "1"
|
||||
dns-lookup = "1"
|
||||
dashmap = "5"
|
||||
reqwest = { version = "0.11.20", features = ["json"] }
|
||||
lqos_support_tool = { path = "../lqos_support_tool" }
|
||||
|
||||
# Support JemAlloc on supported platforms
|
||||
[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies]
|
||||
jemallocator = "0.5"
|
@ -1,3 +0,0 @@
|
||||
[default]
|
||||
port = 9123
|
||||
address = "::"
|
@ -1,7 +0,0 @@
|
||||
use std::process::Command;
|
||||
fn main() {
|
||||
// Adds a git commit hash to the program
|
||||
let output = Command::new("git").args(["rev-parse", "HEAD"]).output().unwrap();
|
||||
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::Error;
|
||||
use lqos_config::{UserRole, WebUsers};
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::serde::{json::Json, Deserialize, Serialize};
|
||||
use rocket::{
|
||||
http::{Cookie, CookieJar, Status},
|
||||
request::{FromRequest, Outcome},
|
||||
Request,
|
||||
};
|
||||
|
||||
static WEB_USERS: Lazy<Mutex<Option<WebUsers>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AuthGuard {
|
||||
Admin,
|
||||
ReadOnly,
|
||||
FirstUse,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AuthGuard {
|
||||
type Error = anyhow::Error; // Decorated because Error=Error looks odd
|
||||
|
||||
async fn from_request(
|
||||
request: &'r Request<'_>,
|
||||
) -> Outcome<Self, Self::Error> {
|
||||
let mut lock = WEB_USERS.lock().unwrap();
|
||||
if lock.is_none() {
|
||||
if WebUsers::does_users_file_exist().unwrap() {
|
||||
*lock = Some(WebUsers::load_or_create().unwrap());
|
||||
} else {
|
||||
// There is no user list, so we're redirecting to the
|
||||
// new user page.
|
||||
return Outcome::Success(AuthGuard::FirstUse);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(users) = &*lock {
|
||||
if let Some(token) = request.cookies().get("User-Token") {
|
||||
match users.get_role_from_token(token.value()) {
|
||||
Ok(UserRole::Admin) => return Outcome::Success(AuthGuard::Admin),
|
||||
Ok(UserRole::ReadOnly) => {
|
||||
return Outcome::Success(AuthGuard::ReadOnly)
|
||||
}
|
||||
_ => {
|
||||
return Outcome::Error((
|
||||
Status::Unauthorized,
|
||||
Error::msg("Invalid token"),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no login, do we allow anonymous?
|
||||
if users.do_we_allow_anonymous() {
|
||||
return Outcome::Success(AuthGuard::ReadOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Outcome::Error((Status::Unauthorized, Error::msg("Access Denied")))
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthGuard {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct FirstUser {
|
||||
pub allow_anonymous: bool,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[post("/api/create_first_user", data = "<info>")]
|
||||
pub fn create_first_user(
|
||||
cookies: &CookieJar,
|
||||
info: Json<FirstUser>,
|
||||
) -> Json<String> {
|
||||
if WebUsers::does_users_file_exist().unwrap() {
|
||||
return Json("ERROR".to_string());
|
||||
}
|
||||
let mut lock = WEB_USERS.lock().unwrap();
|
||||
let mut users = WebUsers::load_or_create().unwrap();
|
||||
users.allow_anonymous(info.allow_anonymous).unwrap();
|
||||
let token = users
|
||||
.add_or_update_user(&info.username, &info.password, UserRole::Admin)
|
||||
.unwrap();
|
||||
cookies.add(Cookie::new("User-Token", token));
|
||||
*lock = Some(users);
|
||||
Json("OK".to_string())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct LoginAttempt {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[post("/api/login", data = "<info>")]
|
||||
pub fn login(cookies: &CookieJar, info: Json<LoginAttempt>) -> Json<String> {
|
||||
let mut lock = WEB_USERS.lock().unwrap();
|
||||
if lock.is_none() && WebUsers::does_users_file_exist().unwrap() {
|
||||
*lock = Some(WebUsers::load_or_create().unwrap());
|
||||
}
|
||||
if let Some(users) = &*lock {
|
||||
if let Ok(token) = users.login(&info.username, &info.password) {
|
||||
cookies.add(Cookie::new("User-Token", token));
|
||||
return Json("OK".to_string());
|
||||
}
|
||||
}
|
||||
Json("ERROR".to_string())
|
||||
}
|
||||
|
||||
#[get("/api/admin_check")]
|
||||
pub fn admin_check(auth: AuthGuard) -> Json<bool> {
|
||||
match auth {
|
||||
AuthGuard::Admin => Json(true),
|
||||
_ => Json(false),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/username")]
|
||||
pub fn username(_auth: AuthGuard, cookies: &CookieJar) -> Json<String> {
|
||||
if let Some(token) = cookies.get("User-Token") {
|
||||
let lock = WEB_USERS.lock().unwrap();
|
||||
if let Some(users) = &*lock {
|
||||
return Json(users.get_username(token.value()));
|
||||
}
|
||||
}
|
||||
Json("Anonymous".to_string())
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
use rocket::http::Header;
|
||||
use rocket::response::Responder;
|
||||
|
||||
/// Use to wrap a responder when you want to tell the user's
|
||||
/// browser to try and cache a response.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```
|
||||
/// pub async fn bootsrap_css<'a>() -> LongCache<Option<NamedFile>> {
|
||||
/// LongCache::new(NamedFile::open("static/vendor/bootstrap.min.css").await.ok())
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Responder)]
|
||||
pub struct LongCache<T> {
|
||||
inner: T,
|
||||
my_header: Header<'static>,
|
||||
}
|
||||
impl<'r, 'o: 'r, T: Responder<'r, 'o>> LongCache<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
my_header: Header::new("cache-control", "max-age=604800, public"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use to wrap a responder when you want to tell the user's
|
||||
/// browser to keep data private and never cahce it.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```
|
||||
/// pub async fn bootsrap_css<'a>() -> LongCache<Option<NamedFile>> {
|
||||
/// LongCache::new(NamedFile::open("static/vendor/bootstrap.min.css").await.ok())
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Responder)]
|
||||
pub struct NoCache<T> {
|
||||
inner: T,
|
||||
my_header: Header<'static>,
|
||||
}
|
||||
impl<'r, 'o: 'r, T: Responder<'r, 'o>> NoCache<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
my_header: Header::new("cache-control", "no-cache, private"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
use crate::{auth_guard::AuthGuard, cache_control::NoCache};
|
||||
use default_net::get_interfaces;
|
||||
use lqos_bus::{bus_request, BusRequest, BusResponse};
|
||||
use lqos_config::{Tunables, Config, ShapedDevice};
|
||||
use rocket::{fs::NamedFile, serde::{json::Json, Serialize, Deserialize}};
|
||||
use rocket::serde::json::Value;
|
||||
use crate::tracker::SHAPED_DEVICES;
|
||||
|
||||
#[get("/api/node_name")]
|
||||
pub async fn get_node_name() -> Json<String> {
|
||||
if let Ok(config) = lqos_config::load_config() {
|
||||
Json(config.node_name)
|
||||
} else {
|
||||
Json("No Name Provided".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/config")]
|
||||
pub async fn config_page<'a>(_auth: AuthGuard) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/config.html").await.ok())
|
||||
}
|
||||
|
||||
#[get("/api/list_nics")]
|
||||
pub async fn get_nic_list<'a>(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Json<Vec<(String, String, String)>>> {
|
||||
let result = get_interfaces()
|
||||
.iter()
|
||||
.map(|eth| {
|
||||
let mac = if let Some(mac) = ð.mac_addr {
|
||||
mac.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
(eth.name.clone(), format!("{:?}", eth.if_type), mac)
|
||||
})
|
||||
.collect();
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/config")]
|
||||
pub async fn get_current_lqosd_config(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Json<Config>> {
|
||||
let config = lqos_config::load_config().unwrap();
|
||||
println!("{config:#?}");
|
||||
NoCache::new(Json(config))
|
||||
}
|
||||
|
||||
#[post("/api/update_config", data = "<data>")]
|
||||
pub async fn update_lqosd_config(
|
||||
data: Json<Config>
|
||||
) -> String {
|
||||
let config: Config = (*data).clone();
|
||||
bus_request(vec![BusRequest::UpdateLqosdConfig(Box::new(config))])
|
||||
.await
|
||||
.unwrap();
|
||||
"Ok".to_string()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct NetworkAndDevices {
|
||||
shaped_devices: Vec<ShapedDevice>,
|
||||
network_json: Value,
|
||||
}
|
||||
|
||||
#[post("/api/update_network_and_devices", data = "<data>")]
|
||||
pub async fn update_network_and_devices(
|
||||
data: Json<NetworkAndDevices>
|
||||
) -> String {
|
||||
let config = lqos_config::load_config().unwrap();
|
||||
|
||||
// Save network.json
|
||||
let serialized_string = rocket::serde::json::to_pretty_string(&data.network_json).unwrap();
|
||||
let net_json_path = std::path::Path::new(&config.lqos_directory).join("network.json");
|
||||
let net_json_backup_path = std::path::Path::new(&config.lqos_directory).join("network.json.backup");
|
||||
if net_json_path.exists() {
|
||||
// Make a backup
|
||||
std::fs::copy(&net_json_path, net_json_backup_path).unwrap();
|
||||
}
|
||||
std::fs::write(net_json_path, serialized_string).unwrap();
|
||||
|
||||
// Save the Shaped Devices
|
||||
let sd_path = std::path::Path::new(&config.lqos_directory).join("ShapedDevices.csv");
|
||||
let sd_backup_path = std::path::Path::new(&config.lqos_directory).join("ShapedDevices.csv.backup");
|
||||
if sd_path.exists() {
|
||||
std::fs::copy(&sd_path, sd_backup_path).unwrap();
|
||||
}
|
||||
let mut lock = SHAPED_DEVICES.write().unwrap();
|
||||
lock.replace_with_new_data(data.shaped_devices.clone());
|
||||
println!("{:?}", lock.devices);
|
||||
lock.write_csv(&format!("{}/ShapedDevices.csv", config.lqos_directory)).unwrap();
|
||||
|
||||
"Ok".to_string()
|
||||
}
|
||||
|
||||
#[post("/api/lqos_tuning/<period>", data = "<tuning>")]
|
||||
pub async fn update_lqos_tuning(
|
||||
auth: AuthGuard,
|
||||
period: u64,
|
||||
tuning: Json<Tunables>,
|
||||
) -> Json<String> {
|
||||
if auth != AuthGuard::Admin {
|
||||
return Json("Error: Not authorized".to_string());
|
||||
}
|
||||
|
||||
// Send the update to the server
|
||||
bus_request(vec![BusRequest::UpdateLqosDTuning(period, (*tuning).clone())])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// For now, ignore the reply.
|
||||
|
||||
Json("OK".to_string())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Default)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct LqosStats {
|
||||
pub bus_requests_since_start: u64,
|
||||
pub time_to_poll_hosts_us: u64,
|
||||
pub high_watermark: (u64, u64),
|
||||
pub tracked_flows: u64,
|
||||
pub rtt_events_per_second: u64,
|
||||
}
|
||||
|
||||
#[get("/api/stats")]
|
||||
pub async fn stats() -> NoCache<Json<LqosStats>> {
|
||||
for msg in bus_request(vec![BusRequest::GetLqosStats]).await.unwrap() {
|
||||
if let BusResponse::LqosdStats { bus_requests, time_to_poll_hosts, high_watermark, tracked_flows, rtt_events_per_second } = msg {
|
||||
return NoCache::new(Json(LqosStats {
|
||||
bus_requests_since_start: bus_requests,
|
||||
time_to_poll_hosts_us: time_to_poll_hosts,
|
||||
high_watermark,
|
||||
tracked_flows,
|
||||
rtt_events_per_second,
|
||||
}));
|
||||
}
|
||||
}
|
||||
NoCache::new(Json(LqosStats::default()))
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
use lqos_bus::{bus_request, BusRequest, BusResponse, FlowbeeSummaryData};
|
||||
use rocket::serde::json::Json;
|
||||
use crate::cache_control::NoCache;
|
||||
|
||||
#[get("/api/flows/dump_all")]
|
||||
pub async fn all_flows_debug_dump() -> NoCache<Json<Vec<FlowbeeSummaryData>>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::DumpActiveFlows]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::AllActiveFlows(flowbee) => flowbee.to_owned(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/flows/count")]
|
||||
pub async fn count_flows() -> NoCache<Json<u64>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::CountActiveFlows]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::CountActiveFlows(count) => *count,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/flows/top/<top_n>/<flow_type>")]
|
||||
pub async fn top_5_flows(top_n: u32, flow_type: String) -> NoCache<Json<Vec<FlowbeeSummaryData>>> {
|
||||
let flow_type = match flow_type.as_str() {
|
||||
"rate" => lqos_bus::TopFlowType::RateEstimate,
|
||||
"bytes" => lqos_bus::TopFlowType::Bytes,
|
||||
"packets" => lqos_bus::TopFlowType::Packets,
|
||||
"drops" => lqos_bus::TopFlowType::Drops,
|
||||
"rtt" => lqos_bus::TopFlowType::RoundTripTime,
|
||||
_ => lqos_bus::TopFlowType::RateEstimate,
|
||||
};
|
||||
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::TopFlows { n: top_n, flow_type }]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::TopFlows(flowbee) => flowbee.to_owned(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/flows/by_country")]
|
||||
pub async fn flows_by_country() -> NoCache<Json<Vec<(String, [u64; 2], [f32; 2])>>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::CurrentEndpointsByCountry]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::CurrentEndpointsByCountry(country_summary) => country_summary.to_owned(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/flows/lat_lon")]
|
||||
pub async fn flows_lat_lon() -> NoCache<Json<Vec<(f64, f64, String, u64, f32)>>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::CurrentEndpointLatLon]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::CurrentLatLon(lat_lon) => lat_lon.to_owned(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/flows/ether_protocol")]
|
||||
pub async fn flows_ether_protocol() -> NoCache<Json<BusResponse>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::EtherProtocolSummary]).await.unwrap();
|
||||
let result = responses[0].to_owned();
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/flows/ip_protocol")]
|
||||
pub async fn flows_ip_protocol() -> NoCache<Json<Vec<(String, (u64, u64))>>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::IpProtocolSummary]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::IpProtocols(ip_protocols) => ip_protocols.to_owned(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
use rocket::fairing::AdHoc;
|
||||
mod cache_control;
|
||||
mod shaped_devices;
|
||||
mod static_pages;
|
||||
mod tracker;
|
||||
mod unknown_devices;
|
||||
use rocket_async_compression::Compression;
|
||||
mod auth_guard;
|
||||
mod config_control;
|
||||
mod network_tree;
|
||||
mod queue_info;
|
||||
mod toasts;
|
||||
mod flow_monitor;
|
||||
mod support;
|
||||
|
||||
// Use JemAllocator only on supported platforms
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
use jemallocator::Jemalloc;
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: Jemalloc = Jemalloc;
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
let server = rocket::build()
|
||||
.attach(AdHoc::on_liftoff("Poll lqosd", |_| {
|
||||
Box::pin(async move {
|
||||
rocket::tokio::spawn(tracker::update_tracking());
|
||||
})
|
||||
}))
|
||||
.attach(AdHoc::on_liftoff("Poll throughput", |_| {
|
||||
Box::pin(async move {
|
||||
rocket::tokio::spawn(tracker::update_total_throughput_buffer());
|
||||
})
|
||||
}))
|
||||
.register("/", catchers![static_pages::login])
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
static_pages::index,
|
||||
static_pages::shaped_devices_csv_page,
|
||||
static_pages::shaped_devices_add_page,
|
||||
static_pages::unknown_devices_page,
|
||||
static_pages::circuit_queue,
|
||||
static_pages::pretty_map_graph,
|
||||
static_pages::help_page,
|
||||
config_control::config_page,
|
||||
network_tree::tree_page,
|
||||
static_pages::ip_dump,
|
||||
// Our JS library
|
||||
static_pages::lqos_js,
|
||||
static_pages::lqos_css,
|
||||
static_pages::klingon,
|
||||
// API calls
|
||||
tracker::current_throughput,
|
||||
tracker::throughput_ring_buffer,
|
||||
tracker::cpu_usage,
|
||||
tracker::ram_usage,
|
||||
tracker::top_10_downloaders,
|
||||
tracker::worst_10_rtt,
|
||||
tracker::worst_10_tcp,
|
||||
tracker::rtt_histogram,
|
||||
tracker::host_counts,
|
||||
shaped_devices::all_shaped_devices,
|
||||
shaped_devices::shaped_devices_count,
|
||||
shaped_devices::shaped_devices_range,
|
||||
shaped_devices::shaped_devices_search,
|
||||
shaped_devices::reload_required,
|
||||
shaped_devices::reload_libreqos,
|
||||
unknown_devices::all_unknown_devices,
|
||||
unknown_devices::unknown_devices_count,
|
||||
unknown_devices::unknown_devices_range,
|
||||
unknown_devices::unknown_devices_csv,
|
||||
queue_info::raw_queue_by_circuit,
|
||||
queue_info::run_btest,
|
||||
queue_info::circuit_info,
|
||||
queue_info::current_circuit_throughput,
|
||||
queue_info::watch_circuit,
|
||||
queue_info::flow_stats,
|
||||
queue_info::packet_dump,
|
||||
queue_info::pcap,
|
||||
queue_info::request_analysis,
|
||||
queue_info::dns_query,
|
||||
config_control::get_nic_list,
|
||||
//config_control::get_current_python_config,
|
||||
config_control::get_current_lqosd_config,
|
||||
//config_control::update_python_config,
|
||||
config_control::update_network_and_devices,
|
||||
config_control::update_lqos_tuning,
|
||||
config_control::update_lqosd_config,
|
||||
config_control::get_node_name,
|
||||
auth_guard::create_first_user,
|
||||
auth_guard::login,
|
||||
auth_guard::admin_check,
|
||||
static_pages::login_page,
|
||||
auth_guard::username,
|
||||
network_tree::tree_entry,
|
||||
network_tree::tree_clients,
|
||||
network_tree::network_tree_summary,
|
||||
network_tree::node_names,
|
||||
network_tree::funnel_for_queue,
|
||||
network_tree::get_network_json,
|
||||
config_control::stats,
|
||||
// Supporting files
|
||||
static_pages::bootsrap_css,
|
||||
static_pages::plotly_js,
|
||||
static_pages::jquery_js,
|
||||
static_pages::msgpack_js,
|
||||
static_pages::bootsrap_js,
|
||||
static_pages::tinylogo,
|
||||
static_pages::favicon,
|
||||
static_pages::fontawesome_solid,
|
||||
static_pages::fontawesome_webfont,
|
||||
static_pages::fontawesome_woff,
|
||||
// Front page toast checks
|
||||
toasts::version_check,
|
||||
toasts::stats_check,
|
||||
// Flowbee System
|
||||
flow_monitor::all_flows_debug_dump,
|
||||
flow_monitor::count_flows,
|
||||
flow_monitor::top_5_flows,
|
||||
flow_monitor::flows_by_country,
|
||||
flow_monitor::flows_lat_lon,
|
||||
flow_monitor::flows_ether_protocol,
|
||||
flow_monitor::flows_ip_protocol,
|
||||
// Suport System
|
||||
support::run_sanity_check,
|
||||
support::gather_support_data,
|
||||
support::submit_support_data,
|
||||
],
|
||||
);
|
||||
|
||||
// Compression is slow in debug builds,
|
||||
// so only enable it on release builds.
|
||||
if cfg!(debug_assertions) {
|
||||
server
|
||||
} else {
|
||||
server.attach(Compression::fairing())
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use lqos_bus::{bus_request, BusRequest, BusResponse};
|
||||
use lqos_config::NetworkJsonTransport;
|
||||
use rocket::{
|
||||
fs::NamedFile,
|
||||
serde::{json::Json, Serialize, msgpack::MsgPack},
|
||||
};
|
||||
use rocket::serde::json::Value;
|
||||
|
||||
use crate::{cache_control::NoCache, tracker::SHAPED_DEVICES};
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/tree")]
|
||||
pub async fn tree_page<'a>() -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/tree.html").await.ok())
|
||||
}
|
||||
|
||||
#[get("/api/network_tree/<parent>")]
|
||||
pub async fn tree_entry(
|
||||
parent: usize,
|
||||
) -> NoCache<MsgPack<Vec<(usize, NetworkJsonTransport)>>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::GetNetworkMap { parent }]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::NetworkMap(nodes) => nodes.to_owned(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
|
||||
#[get("/api/network_tree_summary")]
|
||||
pub async fn network_tree_summary(
|
||||
) -> NoCache<MsgPack<Vec<(usize, NetworkJsonTransport)>>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::TopMapQueues(4)]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::NetworkMap(nodes) => nodes.to_owned(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct CircuitThroughput {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub traffic: (u64, u64),
|
||||
pub limit: (u64, u64),
|
||||
}
|
||||
|
||||
#[get("/api/tree_clients/<parent>")]
|
||||
pub async fn tree_clients(
|
||||
parent: String,
|
||||
) -> NoCache<MsgPack<Vec<CircuitThroughput>>> {
|
||||
let mut result = Vec::new();
|
||||
for msg in
|
||||
bus_request(vec![BusRequest::GetHostCounter]).await.unwrap().iter()
|
||||
{
|
||||
let devices = SHAPED_DEVICES.read().unwrap();
|
||||
if let BusResponse::HostCounters(hosts) = msg {
|
||||
for (ip, down, up) in hosts.iter() {
|
||||
let lookup = match ip {
|
||||
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
|
||||
IpAddr::V6(ip) => *ip,
|
||||
};
|
||||
if let Some(c) = devices.trie.longest_match(lookup) {
|
||||
if devices.devices[*c.1].parent_node == parent {
|
||||
result.push(CircuitThroughput {
|
||||
id: devices.devices[*c.1].circuit_id.clone(),
|
||||
name: devices.devices[*c.1].circuit_name.clone(),
|
||||
traffic: (*down, *up),
|
||||
limit: (
|
||||
devices.devices[*c.1].download_max_mbps as u64,
|
||||
devices.devices[*c.1].upload_max_mbps as u64,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
|
||||
#[post("/api/node_names", data = "<nodes>")]
|
||||
pub async fn node_names(
|
||||
nodes: Json<Vec<usize>>,
|
||||
) -> NoCache<Json<Vec<(usize, String)>>> {
|
||||
let mut result = Vec::new();
|
||||
for msg in bus_request(vec![BusRequest::GetNodeNamesFromIds(nodes.0)])
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
{
|
||||
if let BusResponse::NodeNames(map) = msg {
|
||||
result.extend_from_slice(map);
|
||||
}
|
||||
}
|
||||
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/funnel_for_queue/<circuit_id>")]
|
||||
pub async fn funnel_for_queue(
|
||||
circuit_id: String,
|
||||
) -> NoCache<MsgPack<Vec<(usize, NetworkJsonTransport)>>> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
let target = SHAPED_DEVICES
|
||||
.read()
|
||||
.unwrap()
|
||||
.devices
|
||||
.iter()
|
||||
.find(|d| d.circuit_id == circuit_id)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.parent_node
|
||||
.clone();
|
||||
|
||||
for msg in
|
||||
bus_request(vec![BusRequest::GetFunnel { target }]).await.unwrap().iter()
|
||||
{
|
||||
if let BusResponse::NetworkMap(map) = msg {
|
||||
result.extend_from_slice(map);
|
||||
}
|
||||
}
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
|
||||
#[get("/api/network_json")]
|
||||
pub async fn get_network_json() -> NoCache<Json<Value>> {
|
||||
if let Ok(config) = lqos_config::load_config() {
|
||||
let path = std::path::Path::new(&config.lqos_directory).join("network.json");
|
||||
if path.exists() {
|
||||
let raw = std::fs::read_to_string(path).unwrap();
|
||||
let json: Value = rocket::serde::json::from_str(&raw).unwrap();
|
||||
return NoCache::new(Json(json));
|
||||
}
|
||||
}
|
||||
|
||||
NoCache::new(Json(Value::String("Not done yet".to_string())))
|
||||
}
|
@ -1,202 +0,0 @@
|
||||
use crate::auth_guard::AuthGuard;
|
||||
use crate::cache_control::NoCache;
|
||||
use crate::tracker::{SHAPED_DEVICES, lookup_dns};
|
||||
use lqos_bus::{bus_request, BusRequest, BusResponse, FlowbeeSummaryData, PacketHeader, QueueStoreTransit};
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::http::Status;
|
||||
use rocket::response::content::RawJson;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::serde::Serialize;
|
||||
use rocket::serde::msgpack::MsgPack;
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct CircuitInfo {
|
||||
pub name: String,
|
||||
pub capacity: (u64, u64),
|
||||
}
|
||||
|
||||
#[get("/api/watch_circuit/<circuit_id>")]
|
||||
pub async fn watch_circuit(
|
||||
circuit_id: String,
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Json<String>> {
|
||||
bus_request(vec![BusRequest::WatchQueue(circuit_id)]).await.unwrap();
|
||||
|
||||
NoCache::new(Json("OK".to_string()))
|
||||
}
|
||||
|
||||
#[get("/api/circuit_info/<circuit_id>")]
|
||||
pub async fn circuit_info(
|
||||
circuit_id: String,
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<MsgPack<CircuitInfo>> {
|
||||
if let Some(device) = SHAPED_DEVICES
|
||||
.read()
|
||||
.unwrap()
|
||||
.devices
|
||||
.iter()
|
||||
.find(|d| d.circuit_id == circuit_id)
|
||||
{
|
||||
let result = CircuitInfo {
|
||||
name: device.circuit_name.clone(),
|
||||
capacity: (
|
||||
device.download_max_mbps as u64 * 1_000_000,
|
||||
device.upload_max_mbps as u64 * 1_000_000,
|
||||
),
|
||||
};
|
||||
NoCache::new(MsgPack(result))
|
||||
} else {
|
||||
let result = CircuitInfo {
|
||||
name: "Nameless".to_string(),
|
||||
capacity: (1_000_000, 1_000_000),
|
||||
};
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/circuit_throughput/<circuit_id>")]
|
||||
pub async fn current_circuit_throughput(
|
||||
circuit_id: String,
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<MsgPack<Vec<(String, u64, u64)>>> {
|
||||
let mut result = Vec::new();
|
||||
// Get a list of host counts
|
||||
// This is really inefficient, but I'm struggling to find a better way.
|
||||
// TODO: Fix me up
|
||||
|
||||
for msg in
|
||||
bus_request(vec![BusRequest::GetHostCounter]).await.unwrap().iter()
|
||||
{
|
||||
if let BusResponse::HostCounters(hosts) = msg {
|
||||
let devices = SHAPED_DEVICES.read().unwrap();
|
||||
for (ip, down, up) in hosts.iter() {
|
||||
let lookup = match ip {
|
||||
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
|
||||
IpAddr::V6(ip) => *ip,
|
||||
};
|
||||
if let Some(c) = devices.trie.longest_match(lookup) {
|
||||
if devices.devices[*c.1].circuit_id == circuit_id {
|
||||
result.push((ip.to_string(), *down, *up));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
|
||||
#[get("/api/raw_queue_by_circuit/<circuit_id>")]
|
||||
pub async fn raw_queue_by_circuit(
|
||||
circuit_id: String,
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<MsgPack<QueueStoreTransit>> {
|
||||
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::GetRawQueueData(circuit_id)]).await.unwrap();
|
||||
|
||||
let result = match &responses[0] {
|
||||
BusResponse::RawQueueData(Some(msg)) => {
|
||||
*msg.clone()
|
||||
}
|
||||
_ => QueueStoreTransit::default()
|
||||
};
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
|
||||
#[get("/api/flows/<ip_list>")]
|
||||
pub async fn flow_stats(ip_list: String, _auth: AuthGuard) -> NoCache<Json<Vec<FlowbeeSummaryData>>> {
|
||||
let mut result = Vec::new();
|
||||
let request: Vec<BusRequest> = ip_list.split(',').map(|ip| BusRequest::FlowsByIp(ip.to_string())).collect();
|
||||
let responses = bus_request(request).await.unwrap();
|
||||
for r in responses.iter() {
|
||||
if let BusResponse::FlowsByIp(flow) = r {
|
||||
result.extend_from_slice(flow);
|
||||
}
|
||||
}
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
/*#[get("/api/flows/<ip_list>")]
|
||||
pub async fn flow_stats(ip_list: String, _auth: AuthGuard) -> NoCache<MsgPack<Vec<(FlowTransport, Option<FlowTransport>)>>> {
|
||||
let mut result = Vec::new();
|
||||
let request: Vec<BusRequest> = ip_list.split(',').map(|ip| BusRequest::GetFlowStats(ip.to_string())).collect();
|
||||
let responses = bus_request(request).await.unwrap();
|
||||
for r in responses.iter() {
|
||||
if let BusResponse::FlowData(flow) = r {
|
||||
result.extend_from_slice(flow);
|
||||
}
|
||||
}
|
||||
NoCache::new(MsgPack(result))
|
||||
}*/
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub enum RequestAnalysisResult {
|
||||
Fail,
|
||||
Ok{ session_id: usize, countdown: usize }
|
||||
}
|
||||
|
||||
#[get("/api/request_analysis/<ip>")]
|
||||
pub async fn request_analysis(ip: String) -> NoCache<Json<RequestAnalysisResult>> {
|
||||
for r in bus_request(vec![BusRequest::GatherPacketData(ip)]).await.unwrap() {
|
||||
if let BusResponse::PacketCollectionSession{session_id, countdown} = r {
|
||||
return NoCache::new(Json(RequestAnalysisResult::Ok{session_id, countdown}));
|
||||
}
|
||||
}
|
||||
|
||||
NoCache::new(Json(RequestAnalysisResult::Fail))
|
||||
}
|
||||
|
||||
#[get("/api/packet_dump/<id>")]
|
||||
pub async fn packet_dump(id: usize, _auth: AuthGuard) -> NoCache<Json<Vec<PacketHeader>>> {
|
||||
let mut result = Vec::new();
|
||||
for r in bus_request(vec![BusRequest::GetPacketHeaderDump(id)]).await.unwrap() {
|
||||
if let BusResponse::PacketDump(Some(packets)) = r {
|
||||
result.extend(packets);
|
||||
}
|
||||
}
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[get("/api/pcap/<id>/<filename>")]
|
||||
pub async fn pcap(id: usize, filename: String) -> Result<NoCache<NamedFile>, Status> {
|
||||
// The unusued _filename parameter is there to allow the changing of the
|
||||
// filename on the client side. See Github issue 291.
|
||||
for r in bus_request(vec![BusRequest::GetPcapDump(id)]).await.unwrap() {
|
||||
if let BusResponse::PcapDump(Some(filename)) = r {
|
||||
return Ok(NoCache::new(NamedFile::open(filename).await.unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
Err(Status::NotFound)
|
||||
}
|
||||
|
||||
#[get("/api/dns/<ip>")]
|
||||
pub async fn dns_query(ip: String) -> NoCache<String> {
|
||||
if let Ok(ip) = ip.parse::<IpAddr>() {
|
||||
NoCache::new(lookup_dns(ip))
|
||||
} else {
|
||||
NoCache::new(ip)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "equinix_tests")]
|
||||
#[get("/api/run_btest")]
|
||||
pub async fn run_btest() -> NoCache<RawJson<String>> {
|
||||
let responses =
|
||||
bus_request(vec![BusRequest::RequestLqosEquinixTest]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::Ack => String::new(),
|
||||
_ => "Unable to request test".to_string(),
|
||||
};
|
||||
NoCache::new(RawJson(result))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "equinix_tests"))]
|
||||
pub async fn run_btest() -> NoCache<RawJson<String>> {
|
||||
NoCache::new(RawJson("No!"))
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use crate::auth_guard::AuthGuard;
|
||||
use crate::cache_control::NoCache;
|
||||
use crate::tracker::SHAPED_DEVICES;
|
||||
use lqos_bus::{bus_request, BusRequest, BusResponse};
|
||||
use lqos_config::ShapedDevice;
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
static RELOAD_REQUIRED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[get("/api/all_shaped_devices")]
|
||||
pub fn all_shaped_devices(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
NoCache::new(Json(SHAPED_DEVICES.read().unwrap().devices.clone()))
|
||||
}
|
||||
|
||||
#[get("/api/shaped_devices_count")]
|
||||
pub fn shaped_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
|
||||
NoCache::new(Json(SHAPED_DEVICES.read().unwrap().devices.len()))
|
||||
}
|
||||
|
||||
#[get("/api/shaped_devices_range/<start>/<end>")]
|
||||
pub fn shaped_devices_range(
|
||||
start: usize,
|
||||
end: usize,
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
let reader = SHAPED_DEVICES.read().unwrap();
|
||||
let result: Vec<ShapedDevice> =
|
||||
reader.devices.iter().skip(start).take(end).cloned().collect();
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/shaped_devices_search/<term>")]
|
||||
pub fn shaped_devices_search(
|
||||
term: String,
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
let term = term.trim().to_lowercase();
|
||||
let reader = SHAPED_DEVICES.read().unwrap();
|
||||
let result: Vec<ShapedDevice> = reader
|
||||
.devices
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
s.circuit_name.trim().to_lowercase().contains(&term)
|
||||
|| s.device_name.trim().to_lowercase().contains(&term)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/reload_required")]
|
||||
pub fn reload_required() -> NoCache<Json<bool>> {
|
||||
NoCache::new(Json(
|
||||
RELOAD_REQUIRED.load(std::sync::atomic::Ordering::Relaxed),
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/api/reload_libreqos")]
|
||||
pub async fn reload_libreqos(auth: AuthGuard) -> NoCache<Json<String>> {
|
||||
if auth != AuthGuard::Admin {
|
||||
return NoCache::new(Json("Not authorized".to_string()));
|
||||
}
|
||||
// Send request to lqosd
|
||||
let responses = bus_request(vec![BusRequest::ReloadLibreQoS]).await.unwrap();
|
||||
let result = match &responses[0] {
|
||||
BusResponse::ReloadLibreQoS(msg) => msg.clone(),
|
||||
_ => "Unable to reload LibreQoS".to_string(),
|
||||
};
|
||||
|
||||
RELOAD_REQUIRED.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
NoCache::new(Json(result))
|
||||
}
|
@ -1,168 +0,0 @@
|
||||
use crate::{
|
||||
auth_guard::AuthGuard,
|
||||
cache_control::{LongCache, NoCache},
|
||||
};
|
||||
use rocket::fs::NamedFile;
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/")]
|
||||
pub async fn index<'a>(auth: AuthGuard) -> NoCache<Option<NamedFile>> {
|
||||
match auth {
|
||||
AuthGuard::FirstUse => {
|
||||
NoCache::new(NamedFile::open("static/first_run.html").await.ok())
|
||||
}
|
||||
_ => NoCache::new(NamedFile::open("static/main.html").await.ok()),
|
||||
}
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[catch(401)]
|
||||
pub async fn login<'a>() -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/login.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/login")]
|
||||
pub async fn login_page<'a>() -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/login.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/shaped")]
|
||||
pub async fn shaped_devices_csv_page<'a>(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/shaped.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/circuit_queue")]
|
||||
pub async fn circuit_queue<'a>(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/circuit_queue.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/ip_dump")]
|
||||
pub async fn ip_dump<'a>(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/ip_dump.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/unknown")]
|
||||
pub async fn unknown_devices_page<'a>(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/unknown-ip.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/shaped-add")]
|
||||
pub async fn shaped_devices_add_page<'a>(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/shaped-add.html").await.ok())
|
||||
}
|
||||
|
||||
// Temporary for funsies
|
||||
#[get("/showoff")]
|
||||
pub async fn pretty_map_graph<'a>(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/showoff.html").await.ok())
|
||||
}
|
||||
|
||||
// Help me obi-wan, you're our only hope
|
||||
#[get("/help")]
|
||||
pub async fn help_page<'a>(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/help.html").await.ok())
|
||||
}
|
||||
|
||||
#[get("/vendor/bootstrap.min.css")]
|
||||
pub async fn bootsrap_css<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/vendor/bootstrap.min.css").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/lqos.js")]
|
||||
pub async fn lqos_js<'a>() -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/lqos.js").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/lqos.css")]
|
||||
pub async fn lqos_css<'a>() -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/lqos.css").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/vendor/klingon.ttf")]
|
||||
pub async fn klingon<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/vendor/klingon.ttf").await.ok())
|
||||
}
|
||||
|
||||
#[get("/vendor/plotly-2.16.1.min.js")]
|
||||
pub async fn plotly_js<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(
|
||||
NamedFile::open("static/vendor/plotly-2.16.1.min.js").await.ok(),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/vendor/jquery.min.js")]
|
||||
pub async fn jquery_js<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/vendor/jquery.min.js").await.ok())
|
||||
}
|
||||
|
||||
#[get("/vendor/msgpack.min.js")]
|
||||
pub async fn msgpack_js<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/vendor/msgpack.min.js").await.ok())
|
||||
}
|
||||
|
||||
#[get("/vendor/bootstrap.bundle.min.js")]
|
||||
pub async fn bootsrap_js<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(
|
||||
NamedFile::open("static/vendor/bootstrap.bundle.min.js").await.ok(),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/vendor/tinylogo.svg")]
|
||||
pub async fn tinylogo<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/tinylogo.svg").await.ok())
|
||||
}
|
||||
|
||||
#[get("/favicon.png")]
|
||||
pub async fn favicon<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/favicon.png").await.ok())
|
||||
}
|
||||
|
||||
/// FontAwesome icons
|
||||
#[get("/vendor/solid.min.css")]
|
||||
pub async fn fontawesome_solid<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/vendor/solid.min.css").await.ok())
|
||||
}
|
||||
|
||||
#[get("/fonts/fontawesome-webfont.ttf")]
|
||||
pub async fn fontawesome_webfont<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/vendor/fa-webfont.ttf").await.ok())
|
||||
}
|
||||
|
||||
#[get("/fonts/fontawesome-webfont.woff2")]
|
||||
pub async fn fontawesome_woff<'a>() -> LongCache<Option<NamedFile>> {
|
||||
LongCache::new(NamedFile::open("static/vendor/fa-webfont.ttf").await.ok())
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize};
|
||||
|
||||
const MAX_CPUS_COUNTED: usize = 128;
|
||||
|
||||
/// Stores overall CPU usage
|
||||
pub static CPU_USAGE: Lazy<[AtomicU32; MAX_CPUS_COUNTED]> =
|
||||
Lazy::new(build_empty_cpu_list);
|
||||
|
||||
/// Total number of CPUs detected
|
||||
pub static NUM_CPUS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Total RAM used (bytes)
|
||||
pub static RAM_USED: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Total RAM installed (bytes)
|
||||
pub static TOTAL_RAM: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn build_empty_cpu_list() -> [AtomicU32; MAX_CPUS_COUNTED] {
|
||||
let mut temp = Vec::with_capacity(MAX_CPUS_COUNTED);
|
||||
for _ in 0..MAX_CPUS_COUNTED {
|
||||
temp.push(AtomicU32::new(0));
|
||||
}
|
||||
temp.try_into().expect("This should never happen, sizes are constant.")
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
//! Implements a lock-free DNS least-recently-used DNS cache.
|
||||
|
||||
use std::net::IpAddr;
|
||||
use dashmap::DashMap;
|
||||
use dns_lookup::lookup_addr;
|
||||
use lqos_utils::unix_time::unix_now;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const CACHE_SIZE: usize = 1000;
|
||||
|
||||
struct DnsEntry {
|
||||
hostname: String,
|
||||
last_accessed: u64,
|
||||
}
|
||||
|
||||
static DNS_CACHE: Lazy<DashMap<IpAddr, DnsEntry>> = Lazy::new(|| DashMap::with_capacity(CACHE_SIZE));
|
||||
|
||||
pub fn lookup_dns(ip: IpAddr) -> String {
|
||||
// If the cached value exists, just return it
|
||||
if let Some(mut dns) = DNS_CACHE.get_mut(&ip) {
|
||||
if let Ok(now) = unix_now() {
|
||||
dns.last_accessed = now;
|
||||
}
|
||||
return dns.hostname.clone();
|
||||
}
|
||||
|
||||
// If it doesn't, we'll be adding it.
|
||||
if DNS_CACHE.len() >= CACHE_SIZE {
|
||||
let mut entries : Vec<(IpAddr, u64)> = DNS_CACHE.iter().map(|v| (*v.key(), v.last_accessed)).collect();
|
||||
entries.sort_by(|a,b| b.1.cmp(&a.1));
|
||||
DNS_CACHE.remove(&entries[0].0);
|
||||
}
|
||||
let hostname = lookup_addr(&ip).unwrap_or(ip.to_string());
|
||||
DNS_CACHE.insert(ip, DnsEntry { hostname, last_accessed: unix_now().unwrap_or(0) });
|
||||
|
||||
|
||||
String::new()
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
//! The cache module stores cached data, periodically
|
||||
//! obtained from the `lqosd` server and other parts
|
||||
//! of the system.
|
||||
|
||||
mod cpu_ram;
|
||||
mod shaped_devices;
|
||||
mod throughput;
|
||||
mod dns_cache;
|
||||
|
||||
pub use cpu_ram::*;
|
||||
pub use shaped_devices::*;
|
||||
pub use throughput::THROUGHPUT_BUFFER;
|
||||
pub use dns_cache::lookup_dns;
|
@ -1,9 +0,0 @@
|
||||
use lqos_config::ConfigShapedDevices;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// Global storage of the shaped devices csv data.
|
||||
/// Updated by the file system watcher whenever
|
||||
/// the underlying file changes.
|
||||
pub static SHAPED_DEVICES: Lazy<RwLock<ConfigShapedDevices>> =
|
||||
Lazy::new(|| RwLock::new(ConfigShapedDevices::default()));
|
@ -1,71 +0,0 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::tracker::ThroughputPerSecond;
|
||||
use lqos_bus::{bus_request, BusRequest, BusResponse};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub static THROUGHPUT_BUFFER: Lazy<TotalThroughput> =
|
||||
Lazy::new(|| TotalThroughput::new());
|
||||
|
||||
/// Maintains an in-memory ringbuffer of the last 5 minutes of
|
||||
/// throughput data.
|
||||
pub struct TotalThroughput {
|
||||
inner: Mutex<TotalThroughputInner>
|
||||
}
|
||||
|
||||
struct TotalThroughputInner {
|
||||
data: Vec<ThroughputPerSecond>,
|
||||
head: usize,
|
||||
prev_head: usize,
|
||||
}
|
||||
|
||||
impl TotalThroughput {
|
||||
/// Create a new throughput ringbuffer system
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Mutex::new(TotalThroughputInner {
|
||||
data: vec![ThroughputPerSecond::default(); 300],
|
||||
head: 0,
|
||||
prev_head: 0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run once per second to update the ringbuffer with current data
|
||||
pub async fn tick(&self) {
|
||||
if let Ok(messages) =
|
||||
bus_request(vec![BusRequest::GetCurrentThroughput]).await
|
||||
{
|
||||
for msg in messages {
|
||||
if let BusResponse::CurrentThroughput {
|
||||
bits_per_second,
|
||||
packets_per_second,
|
||||
shaped_bits_per_second,
|
||||
} = msg
|
||||
{
|
||||
let mut lock = self.inner.lock().unwrap();
|
||||
let head = lock.head;
|
||||
lock.data[head].bits_per_second = bits_per_second;
|
||||
lock.data[head].packets_per_second = packets_per_second;
|
||||
lock.data[head].shaped_bits_per_second = shaped_bits_per_second;
|
||||
lock.prev_head = lock.head;
|
||||
lock.head += 1;
|
||||
lock.head %= 300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve just the current throughput data (1 tick)
|
||||
pub fn current(&self) -> ThroughputPerSecond {
|
||||
let lock = self.inner.lock().unwrap();
|
||||
lock.data[lock.prev_head]
|
||||
}
|
||||
|
||||
/// Retrieve the head (0-299) and the full current throughput
|
||||
/// buffer. Used to populate the dashboard the first time.
|
||||
pub fn copy(&self) -> (usize, Vec<ThroughputPerSecond>) {
|
||||
let lock = self.inner.lock().unwrap();
|
||||
(lock.head, lock.data.clone())
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
//! The Cache mod stores data that is periodically updated
|
||||
//! on the server-side, to avoid re-requesting repeatedly
|
||||
//! when there are multiple clients.
|
||||
use super::cache::*;
|
||||
use anyhow::Result;
|
||||
use lqos_config::ConfigShapedDevices;
|
||||
use lqos_utils::file_watcher::FileWatcher;
|
||||
use nix::sys::{
|
||||
time::{TimeSpec, TimeValLike},
|
||||
timerfd::{ClockId, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags},
|
||||
};
|
||||
use rocket::tokio::{task::spawn_blocking, time::Instant};
|
||||
use std::{sync::atomic::AtomicBool, time::Duration};
|
||||
|
||||
/// Once per second, update CPU and RAM usage and ask
|
||||
/// `lqosd` for updated system statistics.
|
||||
/// Called from the main program as a "fairing", meaning
|
||||
/// it runs as part of start-up - and keeps running.
|
||||
/// Designed to never return or fail on error.
|
||||
pub async fn update_tracking() {
|
||||
use sysinfo::System;
|
||||
let mut sys = System::new_all();
|
||||
|
||||
spawn_blocking(|| {
|
||||
info!("Watching for ShapedDevices.csv changes");
|
||||
let _ = watch_for_shaped_devices_changing();
|
||||
});
|
||||
let interval_ms = 1000;
|
||||
info!("Updating throughput ring buffer at {interval_ms} ms cadence.");
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
let monitor_busy = AtomicBool::new(false);
|
||||
if let Ok(timer) =
|
||||
TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty())
|
||||
{
|
||||
if timer
|
||||
.set(
|
||||
Expiration::Interval(TimeSpec::milliseconds(interval_ms as i64)),
|
||||
TimerSetTimeFlags::TFD_TIMER_ABSTIME,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
loop {
|
||||
if timer.wait().is_ok() {
|
||||
if monitor_busy.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
warn!("Ring buffer tick fired while another queue read is ongoing. Skipping this cycle.");
|
||||
} else {
|
||||
monitor_busy.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
//info!("Queue tracking timer fired.");
|
||||
|
||||
sys.refresh_cpu();
|
||||
sys.refresh_memory();
|
||||
|
||||
sys
|
||||
.cpus()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, cpu)| (i, cpu.cpu_usage() as u32)) // Always rounds down
|
||||
.for_each(|(i, cpu)| {
|
||||
CPU_USAGE[i].store(cpu, std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
|
||||
NUM_CPUS
|
||||
.store(sys.cpus().len(), std::sync::atomic::Ordering::Relaxed);
|
||||
RAM_USED
|
||||
.store(sys.used_memory(), std::sync::atomic::Ordering::Relaxed);
|
||||
TOTAL_RAM
|
||||
.store(sys.total_memory(), std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
monitor_busy.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
"Error in timer wait (Linux fdtimer). This should never happen."
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Unable to set the Linux fdtimer timer interval. Queues will not be monitored.");
|
||||
}
|
||||
} else {
|
||||
error!("Unable to acquire Linux fdtimer. Queues will not be monitored.");
|
||||
}
|
||||
}
|
||||
|
||||
fn load_shaped_devices() {
|
||||
info!("ShapedDevices.csv has changed. Attempting to load it.");
|
||||
let shaped_devices = ConfigShapedDevices::load();
|
||||
if let Ok(new_file) = shaped_devices {
|
||||
info!("ShapedDevices.csv loaded");
|
||||
*SHAPED_DEVICES.write().unwrap() = new_file;
|
||||
} else {
|
||||
warn!("ShapedDevices.csv failed to load, see previous error messages. Reverting to empty set.");
|
||||
*SHAPED_DEVICES.write().unwrap() = ConfigShapedDevices::default();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fires up a Linux file system watcher than notifies
|
||||
/// when `ShapedDevices.csv` changes, and triggers a reload.
|
||||
fn watch_for_shaped_devices_changing() -> Result<()> {
|
||||
let watch_path = ConfigShapedDevices::path();
|
||||
if watch_path.is_err() {
|
||||
error!("Unable to generate path for ShapedDevices.csv");
|
||||
return Err(anyhow::Error::msg(
|
||||
"Unable to create path for ShapedDevices.csv",
|
||||
));
|
||||
}
|
||||
let watch_path = watch_path.unwrap();
|
||||
|
||||
let mut watcher = FileWatcher::new("ShapedDevices.csv", watch_path);
|
||||
watcher.set_file_exists_callback(load_shaped_devices);
|
||||
watcher.set_file_created_callback(load_shaped_devices);
|
||||
watcher.set_file_changed_callback(load_shaped_devices);
|
||||
loop {
|
||||
let result = watcher.watch();
|
||||
info!("ShapedDevices watcher returned: {result:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Fires once per second and updates the global traffic ringbuffer.
|
||||
pub async fn update_total_throughput_buffer() {
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
THROUGHPUT_BUFFER.tick().await;
|
||||
let elapsed = now.elapsed();
|
||||
if elapsed < Duration::from_secs(1) {
|
||||
rocket::tokio::time::sleep(Duration::from_secs(1) - elapsed).await;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
mod cache;
|
||||
mod cache_manager;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use self::cache::{
|
||||
CPU_USAGE, NUM_CPUS, RAM_USED, TOTAL_RAM, THROUGHPUT_BUFFER,
|
||||
};
|
||||
use crate::{auth_guard::AuthGuard, cache_control::NoCache};
|
||||
pub use cache::SHAPED_DEVICES;
|
||||
pub use cache_manager::{update_tracking, update_total_throughput_buffer};
|
||||
use lqos_bus::{bus_request, BusRequest, BusResponse, IpStats, TcHandle};
|
||||
use rocket::serde::{Deserialize, Serialize, msgpack::MsgPack};
|
||||
pub use cache::lookup_dns;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct IpStatsWithPlan {
|
||||
pub ip_address: String,
|
||||
pub bits_per_second: (u64, u64),
|
||||
pub packets_per_second: (u64, u64),
|
||||
pub median_tcp_rtt: f32,
|
||||
pub tc_handle: TcHandle,
|
||||
pub circuit_id: String,
|
||||
pub plan: (u32, u32),
|
||||
pub tcp_retransmits: (u64, u64),
|
||||
}
|
||||
|
||||
impl From<&IpStats> for IpStatsWithPlan {
|
||||
fn from(i: &IpStats) -> Self {
|
||||
let mut result = Self {
|
||||
ip_address: i.ip_address.clone(),
|
||||
bits_per_second: i.bits_per_second,
|
||||
packets_per_second: i.packets_per_second,
|
||||
median_tcp_rtt: i.median_tcp_rtt,
|
||||
tc_handle: i.tc_handle,
|
||||
circuit_id: i.circuit_id.clone(),
|
||||
plan: (0, 0),
|
||||
tcp_retransmits: i.tcp_retransmits,
|
||||
};
|
||||
|
||||
if !result.circuit_id.is_empty() {
|
||||
if let Some(circuit) = SHAPED_DEVICES
|
||||
.read()
|
||||
.unwrap()
|
||||
.devices
|
||||
.iter()
|
||||
.find(|sd| sd.circuit_id == result.circuit_id)
|
||||
{
|
||||
let name = if circuit.circuit_name.len() > 20 {
|
||||
&circuit.circuit_name[0..20]
|
||||
} else {
|
||||
&circuit.circuit_name
|
||||
};
|
||||
result.ip_address = format!("{} ({})", name, result.ip_address);
|
||||
result.plan = (circuit.download_max_mbps, circuit.upload_max_mbps);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores total system throughput per second.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Default)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct ThroughputPerSecond {
|
||||
pub bits_per_second: (u64, u64),
|
||||
pub packets_per_second: (u64, u64),
|
||||
pub shaped_bits_per_second: (u64, u64),
|
||||
}
|
||||
|
||||
#[get("/api/current_throughput")]
|
||||
pub async fn current_throughput(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<MsgPack<ThroughputPerSecond>> {
|
||||
let result = THROUGHPUT_BUFFER.current();
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
|
||||
#[get("/api/throughput_ring_buffer")]
|
||||
pub async fn throughput_ring_buffer(
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<MsgPack<(usize, Vec<ThroughputPerSecond>)>> {
|
||||
let result = THROUGHPUT_BUFFER.copy();
|
||||
NoCache::new(MsgPack(result))
|
||||
}
|
||||
|
||||
#[get("/api/cpu")]
|
||||
pub fn cpu_usage(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u32>>> {
|
||||
let usage: Vec<u32> = CPU_USAGE
|
||||
.iter()
|
||||
.take(NUM_CPUS.load(std::sync::atomic::Ordering::Relaxed))
|
||||
.map(|cpu| cpu.load(std::sync::atomic::Ordering::Relaxed))
|
||||
.collect();
|
||||
|
||||
NoCache::new(MsgPack(usage))
|
||||
}
|
||||
|
||||
#[get("/api/ram")]
|
||||
pub fn ram_usage(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u64>>> {
|
||||
let ram_usage = RAM_USED.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let total_ram = TOTAL_RAM.load(std::sync::atomic::Ordering::Relaxed);
|
||||
NoCache::new(MsgPack(vec![ram_usage, total_ram]))
|
||||
}
|
||||
|
||||
#[get("/api/top_10_downloaders")]
|
||||
pub async fn top_10_downloaders(_auth: AuthGuard) -> NoCache<MsgPack<Vec<IpStatsWithPlan>>> {
|
||||
if let Ok(messages) = bus_request(vec![BusRequest::GetTopNDownloaders { start: 0, end: 10 }]).await
|
||||
{
|
||||
for msg in messages {
|
||||
if let BusResponse::TopDownloaders(stats) = msg {
|
||||
let result = stats.iter().map(|tt| tt.into()).collect();
|
||||
return NoCache::new(MsgPack(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NoCache::new(MsgPack(Vec::new()))
|
||||
}
|
||||
|
||||
#[get("/api/worst_10_rtt")]
|
||||
pub async fn worst_10_rtt(_auth: AuthGuard) -> NoCache<MsgPack<Vec<IpStatsWithPlan>>> {
|
||||
if let Ok(messages) = bus_request(vec![BusRequest::GetWorstRtt { start: 0, end: 10 }]).await
|
||||
{
|
||||
for msg in messages {
|
||||
if let BusResponse::WorstRtt(stats) = msg {
|
||||
let result = stats.iter().map(|tt| tt.into()).collect();
|
||||
return NoCache::new(MsgPack(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NoCache::new(MsgPack(Vec::new()))
|
||||
}
|
||||
|
||||
#[get("/api/worst_10_tcp")]
|
||||
pub async fn worst_10_tcp(_auth: AuthGuard) -> NoCache<MsgPack<Vec<IpStatsWithPlan>>> {
|
||||
if let Ok(messages) = bus_request(vec![BusRequest::GetWorstRetransmits { start: 0, end: 10 }]).await
|
||||
{
|
||||
for msg in messages {
|
||||
if let BusResponse::WorstRetransmits(stats) = msg {
|
||||
let result = stats.iter().map(|tt| tt.into()).collect();
|
||||
return NoCache::new(MsgPack(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NoCache::new(MsgPack(Vec::new()))
|
||||
}
|
||||
|
||||
#[get("/api/rtt_histogram")]
|
||||
pub async fn rtt_histogram(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u32>>> {
|
||||
if let Ok(messages) = bus_request(vec![BusRequest::RttHistogram]).await
|
||||
{
|
||||
for msg in messages {
|
||||
if let BusResponse::RttHistogram(stats) = msg {
|
||||
let result = stats;
|
||||
return NoCache::new(MsgPack(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NoCache::new(MsgPack(Vec::new()))
|
||||
}
|
||||
|
||||
#[get("/api/host_counts")]
|
||||
pub async fn host_counts(_auth: AuthGuard) -> NoCache<MsgPack<(u32, u32)>> {
|
||||
let mut host_counts = (0, 0);
|
||||
if let Ok(messages) = bus_request(vec![BusRequest::AllUnknownIps]).await {
|
||||
for msg in messages {
|
||||
if let BusResponse::AllUnknownIps(unknowns) = msg {
|
||||
let really_unknown: Vec<IpStats> = unknowns
|
||||
.iter()
|
||||
.filter(|ip| {
|
||||
if let Ok(ip) = ip.ip_address.parse::<IpAddr>() {
|
||||
let lookup = match ip {
|
||||
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
|
||||
IpAddr::V6(ip) => ip,
|
||||
};
|
||||
SHAPED_DEVICES.read().unwrap().trie.longest_match(lookup).is_none()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
host_counts = (really_unknown.len() as u32, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let n_devices = SHAPED_DEVICES.read().unwrap().devices.len();
|
||||
let unknown = host_counts.0 - host_counts.1;
|
||||
NoCache::new(MsgPack((n_devices as u32, unknown)))
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::{
|
||||
auth_guard::AuthGuard, cache_control::NoCache, tracker::SHAPED_DEVICES
|
||||
};
|
||||
use lqos_bus::{IpStats, bus_request, BusRequest, BusResponse};
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
async fn unknown_devices() -> Vec<IpStats> {
|
||||
if let Ok(messages) = bus_request(vec![BusRequest::AllUnknownIps]).await {
|
||||
for msg in messages {
|
||||
if let BusResponse::AllUnknownIps(unknowns) = msg {
|
||||
let cfg = SHAPED_DEVICES.read().unwrap();
|
||||
let really_unknown: Vec<IpStats> = unknowns
|
||||
.iter()
|
||||
.filter(|ip| {
|
||||
if let Ok(ip) = ip.ip_address.parse::<IpAddr>() {
|
||||
let lookup = match ip {
|
||||
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
|
||||
IpAddr::V6(ip) => ip,
|
||||
};
|
||||
cfg.trie.longest_match(lookup).is_none()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
return really_unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[get("/api/all_unknown_devices")]
|
||||
pub async fn all_unknown_devices(_auth: AuthGuard) -> NoCache<Json<Vec<IpStats>>> {
|
||||
NoCache::new(Json(unknown_devices().await))
|
||||
}
|
||||
|
||||
#[get("/api/unknown_devices_count")]
|
||||
pub async fn unknown_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
|
||||
NoCache::new(Json(unknown_devices().await.len()))
|
||||
}
|
||||
|
||||
#[get("/api/unknown_devices_range/<start>/<end>")]
|
||||
pub async fn unknown_devices_range(
|
||||
start: usize,
|
||||
end: usize,
|
||||
_auth: AuthGuard,
|
||||
) -> NoCache<Json<Vec<IpStats>>> {
|
||||
let reader = unknown_devices().await;
|
||||
let result: Vec<IpStats> =
|
||||
reader.iter().skip(start).take(end).cloned().collect();
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/unknown_devices_csv")]
|
||||
pub async fn unknown_devices_csv(_auth: AuthGuard) -> NoCache<String> {
|
||||
let mut result = "IP Address,Download,Upload\n".to_string();
|
||||
let reader = unknown_devices().await;
|
||||
|
||||
for unknown in reader.iter() {
|
||||
result += &format!("{},{},{}\n", unknown.ip_address, unknown.bits_per_second.0, unknown.bits_per_second.1);
|
||||
}
|
||||
NoCache::new(result)
|
||||
}
|
@ -1,997 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script>
|
||||
<script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-secondary">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
|
||||
height="25" /> LibreQoS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped
|
||||
Devices <span id="shapedCount"
|
||||
class="badge badge-pill badge-success green-badge">?</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
|
||||
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item" id="statsLink"></li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i>
|
||||
Reload LibreQoS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="container" class="pad4">
|
||||
|
||||
<div class="row top-shunt">
|
||||
<div class="col-sm-12 bg-light center-txt">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<span id="circuitName" class="bold redact"></span>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="pills-home-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#pills-home" type="button" role="tab" aria-controls="pills-home"
|
||||
aria-selected="true">Overview</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="pills-tins-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#pills-tins" type="button" role="tab" aria-controls="pills-profile"
|
||||
aria-selected="false">All Tins</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="pills-funnel-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#pills-funnel" type="button" role="tab" aria-controls="pills-funnel"
|
||||
aria-selected="false">Queue Tree</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="pills-flows-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#pills-flows" type="button" role="tab" aria-controls="pills-flows"
|
||||
aria-selected="false">Flows</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<a href="#" class="btn btn-small btn-info" id="btnPause"><i class="fa fa-pause"></i> Pause</a>
|
||||
<a href="#" class="btn btn-small btn-info" id="btnSlow"><i class="fa fa-hourglass"></i> Slow
|
||||
Mode</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
<div class="tab-pane fade show active" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab"
|
||||
tabindex="0">
|
||||
|
||||
<!-- Total Throughput and Backlog -->
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-dashboard"></i> Throughput</h5>
|
||||
<div id="throughputGraph" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-car"></i> Backlog</h5>
|
||||
<div id="backlogGraph" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-bar-chart"></i> Capacity Quantile (Last 10s)</h5>
|
||||
<div id="capacityQuantile" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delay and Queue Length -->
|
||||
<div class="row mtop4">
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-hourglass"></i> Delays</h5>
|
||||
<div id="delayGraph" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-fast-forward"></i> Queue Length</h5>
|
||||
<div id="qlenGraph" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mtop4">
|
||||
<div class="col-sm-2">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
Queue Memory: <span id="memory"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="tab-pane fade" id="pills-tins" role="tabpanel" aria-labelledby="pills-tins-tab" tabindex="1">
|
||||
<div class="row" class="mtop4">
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-truck"></i> Tin 1 (Bulk)</h5>
|
||||
<div id="tinTp_0" class="graph150"></div>
|
||||
<div id="tinMd_0" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-balance-scale"></i> Tin 2 (Best Effort)</h5>
|
||||
<div id="tinTp_1" class="graph150"></div>
|
||||
<div id="tinMd_1" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mtop4">
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-television"></i> Tin 3 (Video)</h5>
|
||||
<div id="tinTp_2" class="graph150"></div>
|
||||
<div id="tinMd_2" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-phone"></i> Tin 4 (Voice)</h5>
|
||||
<div id="tinTp_3" class="graph150"></div>
|
||||
<div id="tinMd_3" class="graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-funnel" role="tabpanel" aria-labelledby="pills-funnel-tab"
|
||||
tabindex="2">
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-flows" role="tabpanel" aria-labelledby="pills-flows-tab" tabindex="3">
|
||||
<div class="row">
|
||||
<div class="col-sm12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-bar-chart"></i> Flows (Last 30 Seconds)</h5>
|
||||
<div id="packetButtons"></div>
|
||||
<div id="flowList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
let throughput = new Object();
|
||||
let throughput_head = 0;
|
||||
let circuit_info = null;
|
||||
|
||||
function nameCircuit() {
|
||||
if (circuit_info == null) {
|
||||
msgPackGet("/api/circuit_info/" + encodeURI(id), (data) => {
|
||||
circuit_info = data;
|
||||
let capacity = scaleNumber(circuit_info[CircuitInfo.capacity][0]) + " / " + scaleNumber(circuit_info[CircuitInfo.capacity][1]);
|
||||
$("#circuitName").text(redactText(circuit_info[CircuitInfo.name]) + " " + capacity);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function displayMemory(data) {
|
||||
// Fill Base Information
|
||||
let total_memory = data[QD.current_download][CT.memory_used] + data[QD.current_upload][CT.memory_used];
|
||||
$("#memory").text(scaleNumber(total_memory));
|
||||
}
|
||||
|
||||
class CombinedPlot {
|
||||
constructor(capacity) {
|
||||
this.y = []
|
||||
for (let i = 0; i < capacity * 2; ++i) {
|
||||
this.y.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
store(x, y, value) {
|
||||
if (value == 0) value = null;
|
||||
this.y[(x * 2) + y] = value;
|
||||
}
|
||||
}
|
||||
|
||||
class TinsPlot {
|
||||
constructor(capacity) {
|
||||
this.tins = [
|
||||
new CombinedPlot(capacity),
|
||||
new CombinedPlot(capacity),
|
||||
new CombinedPlot(capacity),
|
||||
new CombinedPlot(capacity)
|
||||
];
|
||||
}
|
||||
|
||||
store(tin, x, y, value) {
|
||||
this.tins[tin].store(x, y, value);
|
||||
}
|
||||
}
|
||||
|
||||
class QueuePlotter {
|
||||
constructor(capacity) {
|
||||
this.capacity = capacity;
|
||||
this.x_axis = [];
|
||||
this.backlog = new TinsPlot(capacity);
|
||||
this.delays = new TinsPlot(capacity);
|
||||
this.queueLen = new CombinedPlot(capacity);
|
||||
this.throughput = new TinsPlot(capacity);
|
||||
this.drops = new TinsPlot(capacity);
|
||||
this.marks = new TinsPlot(capacity);
|
||||
for (let i = 0; i < capacity; ++i) {
|
||||
this.x_axis.push(i);
|
||||
this.x_axis.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
ingestBacklog(subData, currentX, tin) {
|
||||
this.backlog.store(tin, currentX, 0, subData[0][CDT.tins][tin][CDTT.backlog_bytes] * 8);
|
||||
this.backlog.store(tin, currentX, 1, 0.0 - subData[1][CDT.tins][tin][CDTT.backlog_bytes] * 8);
|
||||
}
|
||||
|
||||
ingestDelays(subData, currentX, tin) {
|
||||
let down = subData[0][CDT.tins][tin][CDTT.avg_delay_us] * 0.001;
|
||||
let up = subData[1][CDT.tins][tin][CDTT.avg_delay_us] * 0.001;
|
||||
if (down == 0.0) {
|
||||
down = null;
|
||||
} else {
|
||||
down = Math.log10(down);
|
||||
}
|
||||
if (up == 0.0) {
|
||||
up = null;
|
||||
} else {
|
||||
//console.log(up);
|
||||
up = 0.0 - Math.log10(up);
|
||||
}
|
||||
this.delays.store(tin, currentX, 0, down);
|
||||
this.delays.store(tin, currentX, 1, up);
|
||||
}
|
||||
|
||||
ingestQueueLen(subData, currentX) {
|
||||
this.queueLen.store(currentX, 0, subData[0][CDT.qlen]);
|
||||
this.queueLen.store(currentX, 1, 0.0 - subData[1][CDT.qlen]);
|
||||
}
|
||||
|
||||
ingestThroughput(subData, currentX, tin) {
|
||||
this.throughput.store(tin, currentX, 0, subData[0][CDT.tins][tin][CDTT.sent_bytes] * 8);
|
||||
this.throughput.store(tin, currentX, 1, 0.0 - (subData[1][CDT.tins][tin][CDTT.sent_bytes] * 8));
|
||||
}
|
||||
|
||||
ingestDrops(subData, currentX, tin) {
|
||||
this.drops.store(tin, currentX, 0, subData[0][CDT.tins][tin][CDTT.drops]);
|
||||
this.drops.store(tin, currentX, 1, 0.0 - subData[1][CDT.tins][tin][CDTT.drops]);
|
||||
}
|
||||
|
||||
ingestMarks(subData, currentX, tin) {
|
||||
this.marks.store(tin, currentX, 0, subData[0][CDT.tins][tin][CDTT.marks]);
|
||||
this.marks.store(tin, currentX, 1, 0.0 - subData[1][CDT.tins][tin][CDTT.marks]);
|
||||
}
|
||||
|
||||
ingest(data, currentX, hi) {
|
||||
// We're inserting at currentX, from the history entry indexed
|
||||
// by hi
|
||||
if (activeTab == "pills-home-tab") {
|
||||
this.ingestQueueLen(data[QD.history][hi], currentX);
|
||||
}
|
||||
for (let tin = 0; tin < 4; ++tin) {
|
||||
if (data[QD.history][hi][0][3].length == 4 && data[QD.history][hi][1][3].length == 4) {
|
||||
if (activeTab == "pills-home-tab") {
|
||||
this.ingestBacklog(data[QD.history][hi], currentX, tin);
|
||||
this.ingestDelays(data[QD.history][hi], currentX, tin);
|
||||
} else if (activeTab == "pills-tins-tab") {
|
||||
this.ingestThroughput(data[QD.history][hi], currentX, tin);
|
||||
this.ingestDrops(data[QD.history][hi], currentX, tin);
|
||||
this.ingestMarks(data[QD.history][hi], currentX, tin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(data) {
|
||||
// Iterate the whole history ringbuffer
|
||||
// Note that we're going backwards. reverse() turned out
|
||||
// to be surprisingly expensive in JS.
|
||||
let currentX = this.capacity;
|
||||
for (let hi = data[QD.history_head]; hi < 600; ++hi) {
|
||||
this.ingest(data, currentX, hi);
|
||||
currentX--;
|
||||
}
|
||||
for (let hi = 0; hi < data[QD.history_head]; ++hi) {
|
||||
this.ingest(data, currentX, hi);
|
||||
currentX--;
|
||||
}
|
||||
}
|
||||
|
||||
plotBacklog() {
|
||||
let graph = document.getElementById("backlogGraph");
|
||||
let graphData = [
|
||||
{ x: this.x_axis, y: this.backlog.tins[0].y, type: 'scattergl', mode: 'markers', name: 'Bulk', marker: { size: 4 } },
|
||||
{ x: this.x_axis, y: this.backlog.tins[1].y, type: 'scattergl', mode: 'markers', name: 'Best Effort', marker: { size: 4 } },
|
||||
{ x: this.x_axis, y: this.backlog.tins[2].y, type: 'scattergl', mode: 'markers', name: 'Video', marker: { size: 4 } },
|
||||
{ x: this.x_axis, y: this.backlog.tins[3].y, type: 'scattergl', mode: 'markers', name: 'Voice', marker: { size: 4 } },
|
||||
];
|
||||
|
||||
if (this.backlogPlotted == null) {
|
||||
this.backlogPlotted = true;
|
||||
Plotly.newPlot(
|
||||
graph,
|
||||
graphData,
|
||||
{
|
||||
margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 },
|
||||
yaxis: { automargin: true, title: "Bytes" },
|
||||
xaxis: { automargin: true, title: "Time since now" }
|
||||
});
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
}
|
||||
|
||||
plotDelays() {
|
||||
let graph = document.getElementById("delayGraph");
|
||||
let graphData = [
|
||||
{ x: this.x_axis, y: this.delays.tins[0].y, type: 'scattergl', mode: 'markers', name: 'Bulk', marker: { size: 4 } },
|
||||
{ x: this.x_axis, y: this.delays.tins[1].y, type: 'scattergl', mode: 'markers', name: 'Best Effort', marker: { size: 4 } },
|
||||
{ x: this.x_axis, y: this.delays.tins[2].y, type: 'scattergl', mode: 'markers', name: 'Video', marker: { size: 4 } },
|
||||
{ x: this.x_axis, y: this.delays.tins[3].y, type: 'scattergl', mode: 'markers', name: 'Voice', marker: { size: 4 } },
|
||||
];
|
||||
|
||||
if (this.delaysPlotted == null) {
|
||||
Plotly.newPlot(
|
||||
graph,
|
||||
graphData,
|
||||
{
|
||||
margin: { l: 8, r: 0, b: 0, t: 0, pad: 4 },
|
||||
yaxis: { automargin: true, title: "log10(ms)", range: [-1.0, 1.0] },
|
||||
xaxis: { automargin: true, title: "Time since now" }
|
||||
});
|
||||
this.delaysPlotted = true;
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
plotQueueLen() {
|
||||
let graph = document.getElementById("qlenGraph");
|
||||
let graphData = [
|
||||
{ x: this.x_axis, y: this.queueLen.y, type: 'scattergl', mode: 'markers', name: 'Queue Length' },
|
||||
];
|
||||
if (this.queueLenPlotted == null) {
|
||||
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Packets" }, xaxis: { automargin: true, title: "Time since now" } });
|
||||
this.queueLenPlotted = true;
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
}
|
||||
|
||||
plotTinThroughput(tin) {
|
||||
let graph = document.getElementById("tinTp_" + tin);
|
||||
let graphData = [
|
||||
{ x: this.x_axis, y: this.throughput.tins[tin].y, type: 'scatter', mode: 'markers' }
|
||||
];
|
||||
if (this.tinsPlotted == null) {
|
||||
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Bits" }, xaxis: { automargin: true, title: "Time since now" } });
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
}
|
||||
|
||||
plotMarksDrops(tin) {
|
||||
let graph = document.getElementById("tinMd_" + tin);
|
||||
let graphData = [
|
||||
{ x: this.x_axis, y: this.drops.tins[tin].y, name: 'Drops', type: 'scatter', mode: 'markers', marker: { size: 4 } },
|
||||
{ x: this.x_axis, y: this.marks.tins[tin].y, name: 'Marks', type: 'scatter', mode: 'markers', marker: { size: 4 } },
|
||||
];
|
||||
if (this.tinsPlotted == null) {
|
||||
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Packets" }, xaxis: { automargin: true, title: "Time since now" } });
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
}
|
||||
|
||||
plot() {
|
||||
if (activeTab == "pills-home-tab") {
|
||||
this.plotBacklog();
|
||||
this.plotDelays();
|
||||
this.plotQueueLen();
|
||||
} else if (activeTab == "pills-tins-tab") {
|
||||
for (let tin = 0; tin < 4; ++tin) {
|
||||
this.plotTinThroughput(tin);
|
||||
this.plotMarksDrops(tin);
|
||||
}
|
||||
this.tinsPlotted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let qp = null;
|
||||
|
||||
function pollQueue() {
|
||||
if (id != null) {
|
||||
// Name the circuit
|
||||
nameCircuit();
|
||||
|
||||
// Graphs
|
||||
msgPackGet("/api/raw_queue_by_circuit/" + encodeURI(id), (data) => {
|
||||
if (qp == null) qp = new QueuePlotter(600);
|
||||
qp.update(data);
|
||||
qp.plot();
|
||||
displayMemory(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let ips = [];
|
||||
|
||||
class ThroughputMonitor {
|
||||
constructor(capacity) {
|
||||
this.capacity = capacity;
|
||||
this.head = 0;
|
||||
this.per_ip = {};
|
||||
this.y = {};
|
||||
this.x_axis = [];
|
||||
for (let i = 0; i < capacity; ++i) {
|
||||
this.x_axis.push(i);
|
||||
this.x_axis.push(i);
|
||||
}
|
||||
this.quantiles = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
||||
];
|
||||
}
|
||||
|
||||
clearQuantiles() {
|
||||
for (let i = 0; i < 12; ++i) {
|
||||
this.quantiles[0][i] = 0;
|
||||
this.quantiles[1][i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ingest(ip, down, up) {
|
||||
down = down * 8;
|
||||
up = up * 8;
|
||||
if (!this.per_ip.hasOwnProperty(ip)) {
|
||||
this.per_ip[ip] = [];
|
||||
this.y[ip] = [];
|
||||
for (let i = 0; i < this.capacity; ++i) {
|
||||
this.per_ip[ip].push(0);
|
||||
this.per_ip[ip].push(0);
|
||||
this.y[ip].push(0);
|
||||
this.y[ip].push(0);
|
||||
}
|
||||
}
|
||||
this.per_ip[ip][this.head] = down;
|
||||
this.per_ip[ip][this.head + 1] = 0.0 - up;
|
||||
this.head += 2;
|
||||
if (this.head > this.capacity * 2) {
|
||||
this.head = 0;
|
||||
}
|
||||
}
|
||||
|
||||
addQuantile(down, up) {
|
||||
up = 0 - up;
|
||||
let down_slot = Math.floor((down / circuit_info[CircuitInfo.capacity][0]) * 10.0);
|
||||
let up_slot = Math.floor((up / circuit_info[CircuitInfo.capacity][1]) * 10.0);
|
||||
if (down_slot < 0) down_slot = 0;
|
||||
if (up_slot < 0) up_slot = 0;
|
||||
if (down_slot > 10) down_slot = 10;
|
||||
if (up_slot > 10) up_slot = 10;
|
||||
this.quantiles[0][down_slot] += 1;
|
||||
this.quantiles[1][up_slot] += 1;
|
||||
//console.log(down_slot, up_slot);
|
||||
}
|
||||
|
||||
prepare() {
|
||||
this.clearQuantiles();
|
||||
for (const ip in this.per_ip) {
|
||||
let counter = this.capacity * 2;
|
||||
for (let i = this.head; i < this.capacity * 2; i++) {
|
||||
this.y[ip][counter] = this.per_ip[ip][i];
|
||||
counter--;
|
||||
}
|
||||
for (let i = 0; i < this.head; i++) {
|
||||
this.y[ip][counter] = this.per_ip[ip][i];
|
||||
counter--;
|
||||
}
|
||||
for (let i = 2; i < 22; i += 2) {
|
||||
this.addQuantile(this.y[ip][i], this.y[ip][i + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plot(target) {
|
||||
let graph = document.getElementById(target);
|
||||
let graphData = [];
|
||||
for (const ip in this.per_ip) {
|
||||
graphData.push({ x: this.x_axis, y: this.y[ip], name: ip, mode: 'markers', type: 'scattergl', marker: { size: 3 } });
|
||||
}
|
||||
if (!this.hasOwnProperty("plotted" + target)) {
|
||||
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Traffic (bits)" }, xaxis: { automargin: true, title: "Time since now" } });
|
||||
this["plotted" + target] = true;
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
}
|
||||
|
||||
plotQuantiles() {
|
||||
let graph = document.getElementById("capacityQuantile");
|
||||
let graphData = [
|
||||
{ x: this.quantiles[2], y: this.quantiles[0], name: 'Download', type: 'bar' },
|
||||
{ x: this.quantiles[2], y: this.quantiles[1], name: 'Upload', type: 'bar' },
|
||||
];
|
||||
if (this.plottedQ == null) {
|
||||
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: '# Samples' }, xaxis: { automargin: true, title: '% Utilization' } });
|
||||
this.plottedQ = true;
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tpData = null;
|
||||
|
||||
function getThroughput() {
|
||||
if (id != null) {
|
||||
msgPackGet("/api/circuit_throughput/" + encodeURI(id), (data) => {
|
||||
if (tpData == null) tpData = new ThroughputMonitor(300);
|
||||
ips = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let ip = data[i][0];
|
||||
ips.push(ip);
|
||||
let down = data[i][1];
|
||||
let up = data[i][2];
|
||||
tpData.ingest(ip, down, up);
|
||||
}
|
||||
tpData.prepare();
|
||||
tpData.plot("throughputGraph");
|
||||
tpData.plotQuantiles();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let funnels = new ThroughputMonitor(300);
|
||||
let rtts = {};
|
||||
let circuitId = "";
|
||||
let builtFunnelDivs = false;
|
||||
|
||||
function getFunnel() {
|
||||
if (builtFunnelDivs) {
|
||||
plotFunnels();
|
||||
return;
|
||||
}
|
||||
circuitId = encodeURI(id);
|
||||
msgPackGet("/api/funnel_for_queue/" + circuitId, (data) => {
|
||||
let html = "";
|
||||
|
||||
// Add the client on top
|
||||
let row = "<div class='row row220'>";
|
||||
|
||||
row += "<div class='col-sm-12'>";
|
||||
row += "<div class='card bg-light'>";
|
||||
row += "<h5 class='card-title'><i class='fa fa-hourglass'></i> Client Throughput</h5>";
|
||||
row += "<div id='tp_client' class='graph98 graph150'></div>";
|
||||
row += "</div>";
|
||||
row += "</div>";
|
||||
|
||||
row += "</div>";
|
||||
html += row;
|
||||
|
||||
// Funnels
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
//funnels.push(data[i][0], data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
|
||||
funnels.ingest(data[i][0], data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
|
||||
rtts[data[i][0]] = new RttHistogram();
|
||||
|
||||
let row = "<div class='row row220'>";
|
||||
|
||||
row += "<div class='col-sm-6'>";
|
||||
row += "<div class='card bg-light'>";
|
||||
row += "<h5 class='card-title'><i class='fa fa-hourglass'></i> <a class='redact' href='/tree?parent=" + data[i][0] + "'>" + redactText(data[i][1][NetTrans.name]) + " Throughput</a></h5>";
|
||||
row += "<div id='tp" + data[i][0] + "' class='graph98 graph150'></div>";
|
||||
row += "</div>";
|
||||
row += "</div>";
|
||||
|
||||
row += "<div class='col-sm-6'>";
|
||||
row += "<div class='card bg-light'>";
|
||||
row += "<h5 class='card-title redact'><i class='fa fa-bar-chart'></i> " + redactText(data[i][1][NetTrans.name]) + " TCP RTT</h5>";
|
||||
row += "<div id='rtt" + data[i][0] + "' class='graph98 graph150'></div>";
|
||||
row += "</div>";
|
||||
row += "</div>";
|
||||
|
||||
row += "</div>";
|
||||
html += row;
|
||||
}
|
||||
$("#pills-funnel").html(html);
|
||||
builtFunnelDivs = true;
|
||||
});
|
||||
}
|
||||
|
||||
let plottedFunnels = {};
|
||||
|
||||
function plotFunnels() {
|
||||
if (tpData != null) tpData.plot("tp_client");
|
||||
funnels.prepare();
|
||||
msgPackGet("/api/funnel_for_queue/" + encodeURI(circuitId), (data) => {
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
rtts[data[i][0]].clear();
|
||||
funnels.ingest(data[i][0], data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
|
||||
for (let j = 0; j < data[i][1][NetTrans.rtts].length; j++) {
|
||||
rtts[data[i][0]].push(data[i][1][NetTrans.rtts][j]);
|
||||
}
|
||||
|
||||
rtts[data[i][0]].plot("rtt" + data[i][0]);
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(funnels.y)) {
|
||||
let target_div = "tp" + k;
|
||||
let graphData = [
|
||||
{ x: funnels.x_axis, y: v, type: 'scatter', mode: 'markers', marker: { size: 3 } }
|
||||
];
|
||||
let graph = document.getElementById(target_div);
|
||||
if (!plotFunnels.hasOwnProperty(target_div)) {
|
||||
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Traffic (bits)" }, xaxis: { automargin: true, title: "Time since now" } });
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function icmpType(n) {
|
||||
switch (n) {
|
||||
case 0: return "ECHO REPLY";
|
||||
case 3: return "DESTINATION UNREACHABLE";
|
||||
case 4: return "SOURCE QUENCH";
|
||||
case 8: return "ECHO REQUEST";
|
||||
case 11: return "TIME EXCEEDED";
|
||||
case 12: return "PARAMETER PROBLEM";
|
||||
case 13: return "TIMESTAMP REQUEST";
|
||||
case 14: return "TIMESTAMP REPLY";
|
||||
case 15: return "INFO REQUEST";
|
||||
case 16: return "INFO REPLY";
|
||||
case 17: return "ADDRESS REQUEST";
|
||||
case 18: return "ADDRESS REPLY";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
var madeButtons = false;
|
||||
var analysisId = null;
|
||||
var analysisTimer = null;
|
||||
var analysisBtn = null;
|
||||
|
||||
function analyze(id) {
|
||||
if (analysisId != null) {
|
||||
alert("Heimdall says: 'STOP CLICKING ME'");
|
||||
return;
|
||||
}
|
||||
let ip = ips[id];
|
||||
$.get("/api/request_analysis/" + encodeURI(ip), (data) => {
|
||||
if (data == "Fail") {
|
||||
alert("Heimdall is busy serving other customers. Your desire is important to him, please try again later.")
|
||||
return;
|
||||
}
|
||||
analysisId = data.Ok.session_id;
|
||||
analysisBtn = "#dumpBtn_" + id;
|
||||
analysisTimer = data.Ok.countdown;
|
||||
analyzeTick();
|
||||
});
|
||||
}
|
||||
|
||||
function analyzeTick() {
|
||||
$(analysisBtn).text("Gathering Data for " + analysisTimer + " more seconds");
|
||||
analysisTimer--;
|
||||
if (analysisTimer > -1) {
|
||||
setTimeout(analyzeTick, 1000);
|
||||
} else {
|
||||
window.location.href = "/ip_dump?id=" + analysisId + "&circuit_id=" + encodeURI(id);
|
||||
}
|
||||
}
|
||||
|
||||
function parse_rtts(data, idx) {
|
||||
let n = [];
|
||||
for (let i=0; i<data.rtt_ringbuffer[idx].length; i++) {
|
||||
n.push(data.rtt_ringbuffer[idx][i]);
|
||||
}
|
||||
if (n.length == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
n.sort();
|
||||
// Median
|
||||
return n[Math.floor(n.length / 2)];
|
||||
}
|
||||
|
||||
function getFlows() {
|
||||
let ip_list = "";
|
||||
let ip_btns = "";
|
||||
for (let i = 0; i < ips.length; ++i) {
|
||||
ip_list += ips[i] + ",";
|
||||
if (circuit_info != null) {
|
||||
ip_btns += "<a id='dumpBtn_" + i + "' href='#' onclick='analyze(\"" + i + "\")' class='btn btn-info'><i class='fa fa-search'></i> Analyze: " + ips[i] + "</a> "
|
||||
}
|
||||
}
|
||||
if (!madeButtons && ips.length > 0 && circuit_info != null) {
|
||||
ip_btns += "<br />";
|
||||
madeButtons = true;
|
||||
$("#packetButtons").html(ip_btns);
|
||||
}
|
||||
ip_list = ip_list.substring(0, ip_list.length - 1);
|
||||
if (ip_list == "") return;
|
||||
$.get("/api/flows/" + ip_list, (data) => {
|
||||
//msgPackGet("/api/flows/" + ip_list, (data) => {
|
||||
//console.log(data);
|
||||
let html = "<table class='table table-striped'>";
|
||||
|
||||
html += "<thead>";
|
||||
html += "<th>Connection</th>";
|
||||
html += "<th>Bytes</th>";
|
||||
html += "<th>Packets</th>";
|
||||
html += "<th>TCP Retransmits</th>";
|
||||
html += "<th>TCP RTT</th>";
|
||||
html += "<th>ASN</th>";
|
||||
html += "<th>ASN Country</th>";
|
||||
html += "</thead>";
|
||||
html += "<tbody>";
|
||||
for (var i=0; i<data.length; i++) {
|
||||
console.log(data[i]);
|
||||
html += "<tr>";
|
||||
html += "<td>" + data[i].analysis + "</td>";
|
||||
html += "<td>" + scaleNumber(data[i].bytes_sent[0]) + " / " + scaleNumber(data[i].bytes_sent[1]) + "</td>";
|
||||
html += "<td>" + scaleNumber(data[i].packets_sent[0]) + " / " + scaleNumber(data[i].packets_sent[1]) + "</td>";
|
||||
html += "<td>" + data[i].tcp_retransmits[0] + " / " + data[i].tcp_retransmits[1] + "</td>";
|
||||
html += "<td>" + scaleNanos(data[i].rtt_nanos[0]) + " / " + scaleNanos(data[i].rtt_nanos[1]) + "</td>";
|
||||
html += "<td>(" + data[i].remote_asn + ") " + data[i].remote_asn_name + "</td>";
|
||||
html += "<td>" + data[i].remote_asn_country + "</td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</tbody>";
|
||||
|
||||
/*html += "<thead>";
|
||||
html += "<th>Protocol</th>";
|
||||
html += "<th>Src</th>";
|
||||
html += "<th>Src Port</th>";
|
||||
html += "<th>Dst</th>";
|
||||
html += "<th>Dst Port</th>";
|
||||
html += "<th>Pkt In</th>";
|
||||
html += "<th>Pkt Out</th>";
|
||||
html += "<th>Bytes In</th>";
|
||||
html += "<th>Bytes Out</th>";
|
||||
html += "<th>DSCP In</th>";
|
||||
html += "<th>DSCP Out</th>";
|
||||
html += "<th>ECN In</th>";
|
||||
html += "<th>ECN Out</th>";
|
||||
html += "</thead>";
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let rpackets = "-";
|
||||
let rbytes = "-";
|
||||
let rdscp = "-";
|
||||
let rcongestion = "-";
|
||||
if (data[i][1] != null) {
|
||||
rpackets = data[i][1][FlowTrans.packets];
|
||||
rbytes = scaleNumber(data[i][1][FlowTrans.bytes]);
|
||||
rdscp = "0x" + data[i][1][FlowTrans.dscp].toString(16);
|
||||
rcongestion = ecn(data[i][1][FlowTrans.ecn]);
|
||||
}
|
||||
html += "<tr>";
|
||||
html += "<td>" + data[i][0][FlowTrans.proto] + "</td>";
|
||||
html += "<td>" + ipToHostname(data[i][0][FlowTrans.src]) + "</td>";
|
||||
if (data[i][0].proto == "ICMP") {
|
||||
html += "<td>" + icmpType(data[i][0][FlowTrans.src_port]) + "</td>";
|
||||
} else {
|
||||
html += "<td>" + data[i][0][FlowTrans.src_port] + "</td>";
|
||||
}
|
||||
html += "<td>" + ipToHostname(data[i][0][FlowTrans.dst]) + "</td>";
|
||||
if (data[i][0][FlowTrans.proto] == "ICMP") {
|
||||
if (data[i][1] != null) {
|
||||
html += "<td>" + icmpType(data[i][1][FlowTrans.src_port]) + "</td>";
|
||||
} else {
|
||||
html += "<td></td>";
|
||||
}
|
||||
} else {
|
||||
html += "<td>" + data[i][0][FlowTrans.dst_port] + "</td>";
|
||||
}
|
||||
html += "<td>" + data[i][0][FlowTrans.packets] + "</td>";
|
||||
html += "<td>" + rpackets + "</td>";
|
||||
html += "<td>" + scaleNumber(data[i][0][FlowTrans.bytes]) + "</td>";
|
||||
html += "<td>" + rbytes + "</td>";
|
||||
html += "<td>0x" + data[i][0][FlowTrans.dscp].toString(16) + "</td>";
|
||||
html += "<td>" + rdscp + "</td>";
|
||||
html += "<td>" + ecn(data[i][0][FlowTrans.ecn]) + "</td>";
|
||||
html += "<td>" + rcongestion + "</td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</tbody></table>";
|
||||
*/
|
||||
$("#flowList").html(html);
|
||||
})
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
let activeTab = "pills-home-tab";
|
||||
var lastCalledTime;
|
||||
var fps;
|
||||
var worstDelta = 0;
|
||||
var paused = false;
|
||||
var slowMode = false;
|
||||
|
||||
function showFps() {
|
||||
if (!lastCalledTime) {
|
||||
lastCalledTime = Date.now();
|
||||
fps = 0;
|
||||
return;
|
||||
}
|
||||
delta = (Date.now() - lastCalledTime) / 1000;
|
||||
lastCalledTime = Date.now();
|
||||
fps = 1 / delta;
|
||||
//$("#fps").text(fps.toFixed(0));
|
||||
worstDelta = Math.max(delta, worstDelta);
|
||||
}
|
||||
|
||||
function updateFrame() {
|
||||
showFps();
|
||||
if (!paused) {
|
||||
switch (activeTab) {
|
||||
case "pills-funnel-tab": {
|
||||
getFunnel();
|
||||
getThroughput();
|
||||
} break;
|
||||
case "pills-flows-tab": {
|
||||
getFlows();
|
||||
} break;
|
||||
default: {
|
||||
pollQueue();
|
||||
getThroughput();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Doing this to balance out the FPS
|
||||
// It will tend towards the slowest
|
||||
if (slowMode) {
|
||||
setTimeout(updateFrame, 1000);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(updateFrame);
|
||||
}, worstDelta * 200);
|
||||
}
|
||||
}
|
||||
|
||||
function wireUpTabEvents() {
|
||||
// Fire events when the active tab changes
|
||||
$(document).on('shown.bs.tab', 'button[data-bs-toggle="pill"]', function (e) {
|
||||
activeTab = e.target.id;
|
||||
//console.log(activeTab);
|
||||
});
|
||||
}
|
||||
|
||||
function isSlowMode() {
|
||||
let slow = localStorage.getItem("slowMode");
|
||||
if (slow == null) {
|
||||
localStorage.setItem("slowMode", false);
|
||||
slow = false;
|
||||
}
|
||||
if (slow == "false") {
|
||||
slow = false;
|
||||
} else if (slow == "true") {
|
||||
slow = true;
|
||||
}
|
||||
return slow;
|
||||
}
|
||||
|
||||
function start() {
|
||||
setTitle();
|
||||
wireUpTabEvents();
|
||||
$("#btnPause").on('click', () => {
|
||||
paused = !paused;
|
||||
if (paused) {
|
||||
$("#btnPause").html("<i class='fa fa-play'></i> Resume");
|
||||
} else {
|
||||
$("#btnPause").html("<i class='fa fa-pause'></i> Pause");
|
||||
}
|
||||
});
|
||||
slowMode = isSlowMode();
|
||||
if (slowMode) {
|
||||
$("#btnSlow").html("<i class='fa fa-fast-forward'></i> Fast Mode");
|
||||
} else {
|
||||
$("#btnSlow").html("<i class='fa fa-hourglass'></i> Slow Mode");
|
||||
}
|
||||
$("#btnSlow").on('click', () => {
|
||||
slowMode = !slowMode;
|
||||
localStorage.setItem("slowMode", slowMode);
|
||||
if (slowMode) {
|
||||
$("#btnSlow").html("<i class='fa fa-fast-forward'></i> Fast Mode");
|
||||
} else {
|
||||
$("#btnSlow").html("<i class='fa fa-hourglass'></i> Slow Mode");
|
||||
}
|
||||
});
|
||||
colorReloadButton();
|
||||
updateHostCounts();
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
});
|
||||
id = params.id;
|
||||
$.get("/api/watch_circuit/" + params.id, () => {
|
||||
//updateFrame();
|
||||
requestAnimationFrame(updateFrame);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
@ -1,93 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-secondary">
|
||||
|
||||
<div id="container" class="pad4">
|
||||
<div class="row">
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">First Login</h5>
|
||||
<p>
|
||||
No <em>lqusers.toml</em> file was found. This is probably the first time you've run
|
||||
the LibreQoS web system. If it isn't, then please check permissions on that file and
|
||||
use the "bin/lqusers" command to verify that your system is working.
|
||||
</p>
|
||||
<p class="alert alert-warning" role="alert">
|
||||
This site will use a cookie to store your identification. If that's not ok,
|
||||
please don't use the site.
|
||||
</p>
|
||||
<p>Let's create a new user, and set some parameters:</p>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input class="form-check-input" type="checkbox" value="" id="allowAnonymous">
|
||||
<label class="form-check-label" for="allowAnonymous">
|
||||
Allow anonymous users to view (but not change) settings.
|
||||
</label>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td>
|
||||
Your Username
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="username" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Your password</td>
|
||||
<td><input type="password" id="password" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<a class="btn btn-primary" id="btnCreateUser">Create User Account</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
function start() {
|
||||
$("#btnCreateUser").on('click', (data) => {
|
||||
let newUser = {
|
||||
allow_anonymous: $("#allowAnonymous").prop('checked'),
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/create_first_user",
|
||||
data: JSON.stringify(newUser),
|
||||
success: (data) => {
|
||||
if (data == "ERROR") {
|
||||
alert("Unable to create a first user.")
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,322 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-secondary">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
|
||||
height="25" /> LibreQoS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
|
||||
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
|
||||
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item" id="statsLink"></li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
|
||||
LibreQoS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="container" class="pad4">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-question-circle"></i> Help & Support</h5>
|
||||
|
||||
<p class="alert-warning">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
Priority support is given to Long-Term Stats subscribers and donors. Other support is
|
||||
best effort, with volunteers trying to help as best we can.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/sponsors/LibreQoE/" class="btn btn-primary">
|
||||
<i class="fa fa-money"></i> Support LibreQoS Today
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-book"></i> Documentation</h5>
|
||||
<p>The documentation is a great place to start!</p>
|
||||
<a href="https://libreqos.readthedocs.io/en/latest/" class="btn btn-primary"><i class="fa fa-book"></i> Read The LibreQoS Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-user-circle"></i> Chat</h5>
|
||||
<p>
|
||||
Connect with other LibreQoS users, and the LibreQoS team on our Zulip Chat System. <br />
|
||||
<a class="btn btn-primary" href="https://chat.libreqos.io/"><i class="fa fa-user-circle"></i> Zulip chat system</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 10px;">
|
||||
<div class="col-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-wrench"></i> Tools</h5>
|
||||
<p style="border: 1px solid #eee">
|
||||
<strong>Sanity check</strong> reads your configuration and looks for common issues. This should be
|
||||
your first step when troubleshooting the system.<br />
|
||||
<button id="btnSanity" class="btn btn-primary" onclick="sanity()"><i class="fa fa-info"></i> Sanity Check Your Installation</button>
|
||||
</p>
|
||||
<p style="border: 1px solid #eee">
|
||||
<strong>Download</strong> support data to a local file. You can send this to LibreQoS via the
|
||||
chat or email (as part of an ongoing support discussion).<br />
|
||||
<button id="btnDownload" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#downloadDump"><i class="fa fa-download"></i> Download Support Dump</button>
|
||||
</p>
|
||||
<p style="border: 1px solid #eee">
|
||||
<strong>Submit</strong> support data directly to LibreQoS. Please contact us when you do this,
|
||||
with detailed information about the problems you are experiencing. Repeatedly hitting this
|
||||
button will get you slower - or no - service!<br />
|
||||
<button id="btnSubmit" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#submitDump"><i class="fa fa-send"></i> Send Support Dump</button>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<div class="modal" tabindex="-1" id="sanityModal">
|
||||
<div class="modal-dialog modal-fullscreen">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Configuration Check Results</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow: auto;">
|
||||
<p id="configCheck"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" id="downloadDump">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Information To Add to the Support Dump</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow: auto;">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="gatherName" class="form-label">Your Name</label>
|
||||
<input type="text" id="gatherName" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="gatherEmail" class="form-label">Your Email Address</label>
|
||||
<input type="text" id="gatherEmail" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="gatherComments" class="form-label">Comments</label>
|
||||
<input type="text" id="gatherComments" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="gather()"><i class="fa fa-download"></i> Generate and Download</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" id="submitDump">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Information To Add to the Support Dump</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow: auto;">
|
||||
<p class="alert-warning"><i class="fa fa-warning"></i> Your support dump will contain unredated data about your customers, and lots
|
||||
of information about your server. By submitting, you are acknowledging that LibreQoS bear no liability for this data.
|
||||
LibreQoS will take all reasonable measures to protect your data, and will not share it.</p>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="submitName" class="form-label">Your Name</label>
|
||||
<input type="text" id="submitName" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="submitEmail" class="form-label">Your Email Address</label>
|
||||
<input type="text" id="submitEmail" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="submitComments" class="form-label">Comments</label>
|
||||
<input type="text" id="submitComments" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="submit()"><i class="fa fa-send"></i> Submit Support Data to LibreQoS</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function start() {
|
||||
setTitle();
|
||||
colorReloadButton();
|
||||
updateHostCounts();
|
||||
}
|
||||
|
||||
function gather() {
|
||||
let details = {
|
||||
name: $("#gatherName").val() + ", " + $("#gatherEmail").val(),
|
||||
comment: $("#gatherComments").val()
|
||||
};
|
||||
console.log(details);
|
||||
|
||||
if (details.name === ", " || details.name.indexOf("@")<1) {
|
||||
alert("Please enter a name and email address. If you share this, it'll be handy to know who sent it!");
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/gatherSupport",
|
||||
data: JSON.stringify(details),
|
||||
xhrFields: {
|
||||
responseType: 'blob' // to avoid binary data being mangled on charset conversion
|
||||
},
|
||||
success: function(blob, result, xhr) {
|
||||
var filename = "libreqos.support";
|
||||
|
||||
// use HTML5 a[download] attribute to specify filename
|
||||
var downloadUrl = URL.createObjectURL(blob);
|
||||
var a = document.createElement("a");
|
||||
// safari doesn't support this yet
|
||||
if (typeof a.download === 'undefined') {
|
||||
window.location.href = downloadUrl;
|
||||
} else {
|
||||
a.href = downloadUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function submit() {
|
||||
let details = {
|
||||
name: $("#submitName").val() + ", " + $("#submitEmail").val(),
|
||||
comment: $("#submitComments").val()
|
||||
};
|
||||
console.log(details);
|
||||
|
||||
if (details.name === ", " || details.name.indexOf("@")<1) {
|
||||
alert("Please enter a name and email address. If you share this, it'll be handy to know who sent it!");
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/submitSupport",
|
||||
data: JSON.stringify(details),
|
||||
success: function(result) {
|
||||
console.log(result);
|
||||
alert(result);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function sanity() {
|
||||
$.get("/api/sanity", (data) => {
|
||||
console.log(data);
|
||||
let html = "<table class='table'><thead><th>Check</th><th>Success?</th><th>Comment</th></thead><tbody>";
|
||||
for (let i=0; i<data.results.length; i++) {
|
||||
let row = data.results[i];
|
||||
html += "<tr>";
|
||||
html += "<td>" + row.name + "</td>";
|
||||
if (row.success) {
|
||||
html += "<td style='color: green'><i class='fa fa-check'></i>";
|
||||
} else {
|
||||
html += "<td style='color: red'><i class='fa fa-warning'></i>";
|
||||
}
|
||||
html += "<td>" + row.comments + "</td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</tbody>";
|
||||
$("#configCheck").html(html);
|
||||
|
||||
// Show the modal
|
||||
const myModal = new bootstrap.Modal(document.getElementById('sanityModal'), { focus: true });
|
||||
myModal.show();
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,376 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-secondary">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" /> LibreQoS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item" id="statsLink"></li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="container" class="pad4">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-users"></i> Packet Dump</h5>
|
||||
|
||||
<div id="pages"></div>
|
||||
<div id="graph"></div>
|
||||
<div id="dump">Please Wait... this may take a second.</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
var packets = [];
|
||||
var flows = {};
|
||||
var pages = 0;
|
||||
var PAGE_SIZE = 1000;
|
||||
var target = "";
|
||||
var capacity = [];
|
||||
var activeFilter = null;
|
||||
var activeSet = null;
|
||||
var activeChart = 0;
|
||||
var activePage = 0;
|
||||
|
||||
function filter(newFilter) {
|
||||
activeFilter = newFilter;
|
||||
if (newFilter == null) {
|
||||
activeSet = packets;
|
||||
} else {
|
||||
activeSet = packets.filter(packet => packet.flow_id == activeFilter);
|
||||
}
|
||||
pages = Math.ceil((activeSet.length / PAGE_SIZE));
|
||||
paginator(0);
|
||||
viewPage(0);
|
||||
}
|
||||
|
||||
function setChart(n) {
|
||||
activeChart = n;
|
||||
paginator(activePage);
|
||||
viewPage(activePage);
|
||||
}
|
||||
|
||||
function proto(n) {
|
||||
switch (n) {
|
||||
case 6: return "TCP"
|
||||
case 17: return "UDP"
|
||||
default: return "ICMP"
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Snippet for tcp_flags decoding
|
||||
if (hdr->fin) flags |= 1;
|
||||
if (hdr->syn) flags |= 2;
|
||||
if (hdr->rst) flags |= 4;
|
||||
if (hdr->psh) flags |= 8;
|
||||
if (hdr->ack) flags |= 16;
|
||||
if (hdr->urg) flags |= 32;
|
||||
if (hdr->ece) flags |= 64;
|
||||
if (hdr->cwr) flags |= 128;
|
||||
*/
|
||||
|
||||
function tcp_flags(n) {
|
||||
let result = "";
|
||||
if (n & 1) result += "FIN-";
|
||||
if (n & 2) result += "SYN-";
|
||||
if (n & 4) result += "RST-";
|
||||
if (n & 8) result += "PSH-";
|
||||
if (n & 16) result += "ACK-";
|
||||
if (n & 32) result += "URG-";
|
||||
if (n & 64) result += "ECE-";
|
||||
if (n & 128) result += "CWR-";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
PAGE_SIZE /= 2;
|
||||
activePage /= 2;
|
||||
pages = packets.length / PAGE_SIZE;
|
||||
viewPage(activePage);
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
PAGE_SIZE *= 2;
|
||||
activePage *= 2;
|
||||
pages = packets.length / PAGE_SIZE;
|
||||
viewPage(activePage);
|
||||
}
|
||||
|
||||
function paginator(active) {
|
||||
activePage = active;
|
||||
let paginator = "<a href='/api/pcap/" + target + "/capture-" + circuit_id + "-" + starting_timestamp + ".pcap' class='btn btn-warning'>Download PCAP Dump</a> ";
|
||||
paginator += "<a href='#' class='btn btn-info' onClick='zoomIn();'>Zoom In</a> ";
|
||||
paginator += "<a href='#' class='btn btn-info' onClick='zoomOut();'>Zoom Out</a> (ℹ️ Or drag an area of the graph) <br />";
|
||||
|
||||
paginator += "<div style='margin: 4px; padding: 6px; background-color: #ddd; border: solid 1px black;'>";
|
||||
paginator += "<strong>Jump to page</strong>: ";
|
||||
paginator += "<select>"
|
||||
for (let i=0; i<pages; i++) {
|
||||
if (i == active) {
|
||||
paginator += "<option selected>" + i + "</option>";
|
||||
} else {
|
||||
paginator += "<option onclick='viewPage(" + i + ");'>" + i + "</option> ";
|
||||
}
|
||||
}
|
||||
paginator += "</select> | ";
|
||||
|
||||
// Offer flow filtering
|
||||
paginator += "<strong>Filter Flows</strong>: ";
|
||||
paginator += "<select>";
|
||||
if (activeFilter == null) {
|
||||
paginator += "<option selected onclick='filter(null);'>View All</option>";
|
||||
} else {
|
||||
paginator += "<option onclick='filter(null);'>View All</option>";
|
||||
}
|
||||
Object.keys(flows).forEach(key => {
|
||||
if (activeFilter == key) {
|
||||
paginator += "<option selected onclick='filter(\"" + key + "\");'>" + key + "</option>";
|
||||
} else {
|
||||
paginator += "<option onclick='filter(\"" + key + "\");'>" + key + "</option>";
|
||||
}
|
||||
});
|
||||
paginator += "</select> | ";
|
||||
|
||||
// Offer graph choices
|
||||
paginator += "<strong>Graph</strong>: ";
|
||||
paginator += "<select>";
|
||||
if (activeChart == 0) {
|
||||
paginator += "<option selected>Packet-Size Chart</option>";
|
||||
} else {
|
||||
paginator += "<option onclick='setChart(0);'>Packet-Size Chart</option>";
|
||||
}
|
||||
if (activeChart == 1) {
|
||||
paginator += "<option selected>Piano Roll Flow Chart</option>";
|
||||
} else {
|
||||
paginator += "<option onclick='setChart(1);'>Piano Roll Flow Chart</option>";
|
||||
}
|
||||
if (activeChart == 2) {
|
||||
paginator += "<option selected>TCP Window Chart</option>";
|
||||
} else {
|
||||
paginator += "<option onclick='setChart(2);'>TCP Window Chart</option>";
|
||||
}
|
||||
paginator += "</select>";
|
||||
paginator += "</div>";
|
||||
|
||||
$("#pages").html(paginator);
|
||||
}
|
||||
|
||||
function viewPage(n) {
|
||||
let start = n * PAGE_SIZE;
|
||||
let end = Math.min(start + PAGE_SIZE, activeSet.length);
|
||||
if (start > packets.length) {
|
||||
console.log("OOps");
|
||||
}
|
||||
let html = "<table class='table table-striped'>";
|
||||
html += "<thead><th>Time (nanos)</th><th>Proto</th><th>TCP Flags</th><th>Sequence</th><th>Window</th><th>Flow</th><th>Bytes</th><th>ECN</th><th>DSCP</th></thead>";
|
||||
let x_axis = [];
|
||||
let y1_axis = [];
|
||||
let y2_axis = [];
|
||||
for (let i=start; i<end; ++i) {
|
||||
html += "<tr>";
|
||||
html += "<td>" + activeSet[i].timestamp + "</td>";
|
||||
html += "<td>" + proto(activeSet[i].ip_protocol) + "</td>";
|
||||
|
||||
if (activeSet[i].ip_protocol == 6) {
|
||||
html += "<td>" + tcp_flags(activeSet[i].tcp_flags) + "</td>";
|
||||
html += "<td>" + activeSet[i].tcp_tsval + "/" + activeSet[i].tcp_tsecr + "</td>";
|
||||
html += "<td>" + activeSet[i].tcp_window + "</td>";
|
||||
} else {
|
||||
html += "<td></td><td></td><td></td>";
|
||||
}
|
||||
|
||||
if (activeSet[i].ip_protocol != 1) {
|
||||
html += "<td>" + activeSet[i].src + ":" + activeSet[i].src_port + " -> " + activeSet[i].dst + ":" + activeSet[i].dst_port + "</td>";
|
||||
} else {
|
||||
html += "<td>" + activeSet[i].src + " -> " + activeSet[i].dst + "</td>";
|
||||
}
|
||||
html += "<td>" + activeSet[i].size + "</td>";
|
||||
html += "<td>" + ecn(activeSet[i].ecn) + "</td>";
|
||||
html += "<td>0x" + activeSet[i].dscp.toString(16) + "</td>";
|
||||
html += "</tr>";
|
||||
x_axis.push(activeSet[i].timestamp);
|
||||
if (activeSet[i].src == target) {
|
||||
y1_axis.push(activeSet[i].size);
|
||||
y2_axis.push(0);
|
||||
} else {
|
||||
y1_axis.push(0);
|
||||
y2_axis.push(0.0 - activeSet[i].size);
|
||||
}
|
||||
}
|
||||
html += "</table>";
|
||||
$("#dump").html(html);
|
||||
paginator(n);
|
||||
|
||||
// Make the graph
|
||||
let graph = document.getElementById("graph");
|
||||
if (activeChart == 0) {
|
||||
// Render the timeline chart
|
||||
let data = [
|
||||
{x: x_axis, y:y1_axis, name: 'Download', type: 'scatter', mode: 'markers', error_x: { type: 'percent', value: capacity[0], symetric: false, valueminus: 0 }},
|
||||
{x: x_axis, y:y2_axis, name: 'Upload', type: 'scatter', mode: 'markers', error_x: { type: 'percent', value: capacity[1], symetric: false, valueminus: 0 }},
|
||||
];
|
||||
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: 'Bytes' }, xaxis: {automargin: true, title: "Nanoseconds"} }, { responsive: true });
|
||||
} else if (activeChart == 1) {
|
||||
// Render the piano roll chart
|
||||
let flowGraphY = {};
|
||||
for (var i=start; i<end; ++i) {
|
||||
let flow_id = activeSet[i].flow_id;
|
||||
if (flowGraphY.hasOwnProperty(flow_id)) {
|
||||
flowGraphY[flow_id].x.push(activeSet[i].timestamp);
|
||||
flowGraphY[flow_id].y.push(flows[flow_id].flowCounter);
|
||||
} else {
|
||||
flowGraphY[flow_id] = {
|
||||
"x": [ activeSet[i].timestamp ],
|
||||
"y": [ flows[flow_id].flowCounter ],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data = [];
|
||||
for (flow in flowGraphY) {
|
||||
//console.log(flowGraphY[flow]);
|
||||
data.push({
|
||||
x: flowGraphY[flow].x, y: flowGraphY[flow].y, name: flow, type: 'scatter', mode: 'markers',
|
||||
});
|
||||
}
|
||||
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: 'Flow' }, xaxis: {automargin: true, title: "Nanoseconds"} }, { responsive: true });
|
||||
} else if (activeChart == 2) {
|
||||
// Render the window chart
|
||||
let flowGraphY = {};
|
||||
for (var i=start; i<end; ++i) {
|
||||
let flow_id = activeSet[i].flow_id;
|
||||
if (flow_id.includes("TCP")) {
|
||||
if (flowGraphY.hasOwnProperty(flow_id)) {
|
||||
flowGraphY[flow_id].x.push(activeSet[i].timestamp);
|
||||
flowGraphY[flow_id].y.push(activeSet[i].tcp_window);
|
||||
} else {
|
||||
flowGraphY[flow_id] = {
|
||||
"x": [ activeSet[i].timestamp ],
|
||||
"y": [ activeSet[i].tcp_window ],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data = [];
|
||||
for (flow in flowGraphY) {
|
||||
//console.log(flowGraphY[flow]);
|
||||
data.push({
|
||||
x: flowGraphY[flow].x, y: flowGraphY[flow].y, name: flow, type: 'scatter', mode: 'markers',
|
||||
});
|
||||
}
|
||||
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: 'Window Size' }, xaxis: {automargin: true, title: "Nanoseconds"} }, { responsive: true });
|
||||
}
|
||||
}
|
||||
|
||||
let circuit_id = null;
|
||||
let starting_timestamp = null;
|
||||
|
||||
function start() {
|
||||
colorReloadButton();
|
||||
updateHostCounts();
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
});
|
||||
circuit_id = params.circuit_id;
|
||||
|
||||
capacity = [ params.dn, params.up ]; // Bits per second
|
||||
capacity = [ capacity[0] / 8, capacity[1] / 8 ]; // Bytes per second
|
||||
capacity = [ capacity[0] / 1e9, capacity[1] / 1e9 ]; // Bytes per nanosecond
|
||||
|
||||
|
||||
target = params.id;
|
||||
$.get("/api/packet_dump/" + params.id, (data) => {
|
||||
console.log(data);
|
||||
data.sort((a,b) => a.timestamp - b.timestamp);
|
||||
|
||||
// Find the minimum timestamp
|
||||
let min_ts = data.reduce((prev, curr) => prev.timestamp < curr.timestamp ? prev : curr).timestamp;
|
||||
|
||||
// Set the displayed timestamp to be (ts - min)
|
||||
data.forEach(packet => packet.timestamp -= min_ts);
|
||||
|
||||
// Divide the packets into flows and append the flow_id
|
||||
let flowCounter = 0;
|
||||
data.forEach(packet => {
|
||||
let flow_id = proto(packet.ip_protocol) + " " + packet.src + ":" + packet.src_port + " <-> " + packet.dst + ":" + packet.dst_port;
|
||||
let reverse_flow_id = proto(packet.ip_protocol) + " " + packet.dst + ":" + packet.dst_port + " <-> " + packet.src + ":" + packet.src_port;
|
||||
if (flows.hasOwnProperty(flow_id)) {
|
||||
packet.flow_id = flow_id;
|
||||
} else if (flows.hasOwnProperty(reverse_flow_id)) {
|
||||
packet.flow_id = reverse_flow_id;
|
||||
} else {
|
||||
flows[flow_id] = { flowCounter };
|
||||
packet.flow_id = flow_id;
|
||||
flowCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
packets = data;
|
||||
activeSet = packets;
|
||||
pages = Math.ceil((activeSet.length / PAGE_SIZE));
|
||||
starting_timestamp = min_ts;
|
||||
paginator(0);
|
||||
viewPage(0);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,83 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-secondary">
|
||||
|
||||
<div id="container" class="pad4">
|
||||
<div class="row">
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Login</h5>
|
||||
<p>Please enter a username and password to access LibreQoS.</p>
|
||||
<p>You can control access locally with <em>bin/lqusers</em> from the console.</p>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>Username</td>
|
||||
<td><input type="text" id="username" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password</td>
|
||||
<td><input type="password" id="password" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<a class="btn btn-primary" id="btnLogin">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a href="https://libreqos.io/credits/">© 2022 - 2023, LibreQoE LLC</a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
|
||||
function try_login() {
|
||||
let newUser = {
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/login",
|
||||
data: JSON.stringify(newUser),
|
||||
success: (data) => {
|
||||
if (data == "ERROR") {
|
||||
alert("Invalid login")
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function start() {
|
||||
$("#btnLogin").on('click', try_login)
|
||||
$(document).on('keydown', (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
try_login()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,22 +0,0 @@
|
||||
@font-face {
|
||||
font-family: Klingon;
|
||||
src: url(/vendor/klingon.ttf);
|
||||
}
|
||||
.green-badge { background-color: green; }
|
||||
.orange-badge { background-color: darkgoldenrod; }
|
||||
.black-txt { color: black; }
|
||||
.pad4 { padding: 4px; }
|
||||
.top-shunt { margin-top: -4px; margin-bottom: 4px; }
|
||||
.center-txt { text-align: center; }
|
||||
.bold { font-weight: bold; }
|
||||
.graph200 { height: 200px; }
|
||||
.graph150 { height: 150px; }
|
||||
.graph98 { min-height: 97px; width: 100%; }
|
||||
.mtop4 { margin-top: 4px; }
|
||||
.mbot4 { margin-bottom: 4px; }
|
||||
.mbot8 { margin-bottom: 8px; }
|
||||
.row220 { height: 220px; }
|
||||
.redact { font-display: unset; }
|
||||
footer > a { color: white; }
|
||||
footer { color: white; font-style: italic; }
|
||||
.invalid { background-color: #ffdddd }
|
@ -1,513 +0,0 @@
|
||||
function msgPackGet(url, success) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, true);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onload = () => {
|
||||
var data = xhr.response;
|
||||
let decoded = msgpack.decode(new Uint8Array(data));
|
||||
success(decoded);
|
||||
};
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
const NetTrans = {
|
||||
"name": 0,
|
||||
"max_throughput": 1,
|
||||
"current_throughput": 2,
|
||||
"rtts": 3,
|
||||
"parents": 4,
|
||||
"immediate_parent": 5
|
||||
}
|
||||
|
||||
const Circuit = {
|
||||
"id" : 0,
|
||||
"name" : 1,
|
||||
"traffic": 2,
|
||||
"limit": 3,
|
||||
}
|
||||
|
||||
const IpStats = {
|
||||
"ip_address": 0,
|
||||
"bits_per_second": 1,
|
||||
"packets_per_second": 2,
|
||||
"median_tcp_rtt": 3,
|
||||
"tc_handle": 4,
|
||||
"circuit_id": 5,
|
||||
"plan": 6,
|
||||
"tcp_retransmits": 7,
|
||||
}
|
||||
|
||||
const FlowTrans = {
|
||||
"src": 0,
|
||||
"dst": 1,
|
||||
"proto": 2,
|
||||
"src_port": 3,
|
||||
"dst_port": 4,
|
||||
"bytes": 5,
|
||||
"packets": 6,
|
||||
"dscp": 7,
|
||||
"ecn": 8
|
||||
}
|
||||
|
||||
const CircuitInfo = {
|
||||
"name" : 0,
|
||||
"capacity" : 1,
|
||||
}
|
||||
|
||||
const QD = { // Queue data
|
||||
"history": 0,
|
||||
"history_head": 1,
|
||||
"current_download": 2,
|
||||
"current_upload": 3,
|
||||
}
|
||||
|
||||
const CT = { // Cake transit
|
||||
"memory_used": 0,
|
||||
}
|
||||
|
||||
const CDT = { // Cake Diff Transit
|
||||
"bytes": 0,
|
||||
"packets": 1,
|
||||
"qlen": 2,
|
||||
"tins": 3,
|
||||
}
|
||||
|
||||
const CDTT = { // Cake Diff Tin Transit
|
||||
"sent_bytes": 0,
|
||||
"backlog_bytes": 1,
|
||||
"drops": 2,
|
||||
"marks": 3,
|
||||
"avg_delay_us": 4,
|
||||
}
|
||||
|
||||
function metaverse_color_ramp(n) {
|
||||
if (n <= 9) {
|
||||
return "#32b08c";
|
||||
} else if (n <= 20) {
|
||||
return "#ffb94a";
|
||||
} else if (n <= 50) {
|
||||
return "#f95f53";
|
||||
} else if (n <= 70) {
|
||||
return "#bf3d5e";
|
||||
} else {
|
||||
return "#dc4e58";
|
||||
}
|
||||
}
|
||||
|
||||
function regular_color_ramp(n) {
|
||||
if (n <= 100) {
|
||||
return "#aaffaa";
|
||||
} else if (n <= 150) {
|
||||
return "goldenrod";
|
||||
} else {
|
||||
return "#ffaaaa";
|
||||
}
|
||||
}
|
||||
|
||||
function color_ramp(n) {
|
||||
let colorPreference = window.localStorage.getItem("colorPreference");
|
||||
if (colorPreference == null) {
|
||||
window.localStorage.setItem("colorPreference", 0);
|
||||
colorPreference = 0;
|
||||
}
|
||||
if (colorPreference == 0) {
|
||||
return regular_color_ramp(n);
|
||||
} else {
|
||||
return metaverse_color_ramp(n);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteAllCookies() {
|
||||
const cookies = document.cookie.split(";");
|
||||
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i];
|
||||
const eqPos = cookie.indexOf("=");
|
||||
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function cssrules() {
|
||||
var rules = {};
|
||||
for (var i = 0; i < document.styleSheets.length; ++i) {
|
||||
var cssRules = document.styleSheets[i].cssRules;
|
||||
for (var j = 0; j < cssRules.length; ++j)
|
||||
rules[cssRules[j].selectorText] = cssRules[j];
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
function css_getclass(name) {
|
||||
var rules = cssrules();
|
||||
if (!rules.hasOwnProperty(name))
|
||||
throw 'TODO: deal_with_notfound_case';
|
||||
return rules[name];
|
||||
}
|
||||
|
||||
function updateHostCounts() {
|
||||
msgPackGet("/api/host_counts", (hc) => {
|
||||
$("#shapedCount").text(hc[0]);
|
||||
$("#unshapedCount").text(hc[1]);
|
||||
setTimeout(updateHostCounts, 5000);
|
||||
});
|
||||
$.get("/api/username", (un) => {
|
||||
let html = "";
|
||||
if (un == "Anonymous") {
|
||||
html = "<a class='nav-link' href='/login'><i class='fa fa-user'></i> Login</a>";
|
||||
} else {
|
||||
html = "<a class='nav-link' onclick='deleteAllCookies();'><i class='fa fa-user'></i> Logout " + un + "</a>";
|
||||
}
|
||||
$("#currentLogin").html(html);
|
||||
});
|
||||
/*$("#startTest").on('click', () => {
|
||||
$.get("/api/run_btest", () => { });
|
||||
});*/
|
||||
// LTS Check
|
||||
$.get("/api/stats_check", (data) => {
|
||||
//console.log(data);
|
||||
let template = "<a class='nav-link' href='$URL$'><i class='fa fa-dashboard'></i> $TEXT$</a>";
|
||||
switch (data.action) {
|
||||
case "Disabled": {
|
||||
template = template.replace("$URL$", "#")
|
||||
.replace("$TEXT$", "<span style='color: red'>Stats Disabled</span>");
|
||||
}
|
||||
case "NotSetup": {
|
||||
template = template.replace("$URL$", "https://stats.libreqos.io/trial1/" + encodeURI(data.node_id))
|
||||
.replace("$TEXT$", "<span class='badge badge-pill badge-success green-badge'>Statistics Free Trial</span>");
|
||||
} break;
|
||||
default: {
|
||||
template = template.replace("$URL$", "https://stats.libreqos.io/")
|
||||
.replace("$TEXT$", "Statistics");
|
||||
}
|
||||
}
|
||||
$("#statsLink").html(template);
|
||||
});
|
||||
}
|
||||
|
||||
function colorReloadButton() {
|
||||
$("body").append(reloadModal);
|
||||
$("#btnReload").on('click', () => {
|
||||
$.get("/api/reload_libreqos", (result) => {
|
||||
const myModal = new bootstrap.Modal(document.getElementById('reloadModal'), { focus: true });
|
||||
$("#reloadLibreResult").text(result);
|
||||
myModal.show();
|
||||
});
|
||||
});
|
||||
$.get("/api/reload_required", (req) => {
|
||||
if (req) {
|
||||
$("#btnReload").addClass('btn-warning');
|
||||
$("#btnReload").css('color', 'darkred');
|
||||
} else {
|
||||
$("#btnReload").addClass('btn-secondary');
|
||||
}
|
||||
});
|
||||
|
||||
// Redaction
|
||||
if (isRedacted()) {
|
||||
console.log("Redacting");
|
||||
//css_getclass(".redact").style.filter = "blur(4px)";
|
||||
css_getclass(".redact").style.fontFamily = "klingon";
|
||||
}
|
||||
}
|
||||
|
||||
function isRedacted() {
|
||||
let redact = localStorage.getItem("redact");
|
||||
if (redact == null) {
|
||||
localStorage.setItem("redact", false);
|
||||
redact = false;
|
||||
}
|
||||
if (redact == "false") {
|
||||
redact = false;
|
||||
} else if (redact == "true") {
|
||||
redact = true;
|
||||
}
|
||||
return redact;
|
||||
}
|
||||
|
||||
const phrases = [
|
||||
"quSDaq ba’lu’’a’", // Is this seat taken?
|
||||
"vjIjatlh", // speak
|
||||
"pe’vIl mu’qaDmey", // curse well
|
||||
"nuqDaq ‘oH puchpa’’e’", // where's the bathroom?
|
||||
"nuqDaq ‘oH tach’e’", // Where's the bar?
|
||||
"tera’ngan Soj lujab’a’", // Do they serve Earth food?
|
||||
"qut na’ HInob", // Give me the salty crystals
|
||||
"qagh Sopbe’", // He doesn't eat gagh
|
||||
"HIja", // Yes
|
||||
"ghobe’", // No
|
||||
"Dochvetlh vIneH", // I want that thing
|
||||
"Hab SoSlI’ Quch", // Your mother has a smooth forehead
|
||||
"nuqjatlh", // What did you say?
|
||||
"jagh yIbuStaH", // Concentrate on the enemy
|
||||
"Heghlu’meH QaQ jajvam", // Today is a good day to die
|
||||
"qaStaH nuq jay’", // WTF is happening?
|
||||
"wo’ batlhvaD", // For the honor of the empire
|
||||
"tlhIngan maH", // We are Klingon!
|
||||
"Qapla’", // Success!
|
||||
]
|
||||
|
||||
function redactText(text) {
|
||||
if (!isRedacted()) return text;
|
||||
let redacted = "";
|
||||
let sum = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
let code = text.charCodeAt(i);
|
||||
sum += code;
|
||||
}
|
||||
sum = sum % phrases.length;
|
||||
return phrases[sum];
|
||||
}
|
||||
|
||||
function scaleNumber(n) {
|
||||
if (n > 1000000000000) {
|
||||
return (n / 1000000000000).toFixed(2) + "T";
|
||||
} else if (n > 1000000000) {
|
||||
return (n / 1000000000).toFixed(2) + "G";
|
||||
} else if (n > 1000000) {
|
||||
return (n / 1000000).toFixed(2) + "M";
|
||||
} else if (n > 1000) {
|
||||
return (n / 1000).toFixed(2) + "K";
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function scaleNanos(n) {
|
||||
if (n == 0) return "";
|
||||
if (n > 1000000000) {
|
||||
return (n / 1000000000).toFixed(2) + "s";
|
||||
} else if (n > 1000000) {
|
||||
return (n / 1000000).toFixed(2) + "ms";
|
||||
} else if (n > 1000) {
|
||||
return (n / 1000).toFixed(2) + "µs";
|
||||
}
|
||||
return n + "ns";
|
||||
}
|
||||
|
||||
const reloadModal = `
|
||||
<div class='modal fade' id='reloadModal' tabindex='-1' aria-labelledby='reloadModalLabel' aria-hidden='true'>
|
||||
<div class='modal-dialog modal-fullscreen'>
|
||||
<div class='modal-content'>
|
||||
<div class='modal-header'>
|
||||
<h1 class='modal-title fs-5' id='reloadModalLabel'>LibreQoS Reload Result</h1>
|
||||
<button type='button' class='btn-close' data-bs-dismiss='modal' aria-label='Close'></button>
|
||||
</div>
|
||||
<div class='modal-body'>
|
||||
<pre id='reloadLibreResult' style='overflow: vertical; height: 100%; width: 100%;'>
|
||||
</pre>
|
||||
</div>
|
||||
<div class='modal-footer'>
|
||||
<button type='button' class='btn btn-secondary' data-bs-dismiss='modal'>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// MultiRingBuffer provides an interface for storing multiple ring-buffers
|
||||
// of performance data, with a view to them ending up on the same graph.
|
||||
class MultiRingBuffer {
|
||||
constructor(capacity) {
|
||||
this.capacity = capacity;
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
push(id, download, upload) {
|
||||
if (!this.data.hasOwnProperty(id)) {
|
||||
this.data[id] = new RingBuffer(this.capacity);
|
||||
}
|
||||
this.data[id].push(download, upload);
|
||||
}
|
||||
|
||||
plotStackedBars(target_div, rootName) {
|
||||
let graphData = [];
|
||||
for (const [k, v] of Object.entries(this.data)) {
|
||||
if (k != rootName) {
|
||||
let y = v.sortedY;
|
||||
let dn = { x: v.x_axis, y: y.down, name: k + "_DL", type: 'scatter', stackgroup: 'dn' };
|
||||
let up = { x: v.x_axis, y: y.up, name: k + "_UL", type: 'scatter', stackgroup: 'up' };
|
||||
graphData.push(dn);
|
||||
graphData.push(up);
|
||||
}
|
||||
}
|
||||
|
||||
let graph = document.getElementById(target_div);
|
||||
Plotly.newPlot(
|
||||
graph,
|
||||
graphData,
|
||||
{
|
||||
margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 },
|
||||
yaxis: { automargin: true },
|
||||
xaxis: { automargin: true, title: "Time since now (seconds)" },
|
||||
showlegend: false,
|
||||
},
|
||||
{ responsive: true, displayModeBar: false });
|
||||
}
|
||||
|
||||
plotTotalThroughput(target_div) {
|
||||
let graph = document.getElementById(target_div);
|
||||
|
||||
this.data['total'].prepare();
|
||||
this.data['shaped'].prepare();
|
||||
|
||||
let x = this.data['total'].x_axis;
|
||||
|
||||
let graphData = [
|
||||
{x: x, y:this.data['total'].sortedY[0], name: 'Download', type: 'scatter', marker: {color: 'rgb(255,160,122)'}},
|
||||
{x: x, y:this.data['total'].sortedY[1], name: 'Upload', type: 'scatter', marker: {color: 'rgb(255,160,122)'}},
|
||||
{x: x, y:this.data['shaped'].sortedY[0], name: 'Shaped Download', type: 'scatter', fill: 'tozeroy', marker: {color: 'rgb(124,252,0)'}},
|
||||
{x: x, y:this.data['shaped'].sortedY[1], name: 'Shaped Upload', type: 'scatter', fill: 'tozeroy', marker: {color: 'rgb(124,252,0)'}},
|
||||
];
|
||||
if (this.plotted == null) {
|
||||
Plotly.newPlot(
|
||||
graph,
|
||||
graphData,
|
||||
{
|
||||
margin: { l:0,r:0,b:0,t:0,pad:4 },
|
||||
yaxis: { automargin: true, title: "Traffic (bits)", exponentformat: "SI" },
|
||||
xaxis: {automargin: true, title: "Time since now (seconds)"}
|
||||
}, { responsive: true });
|
||||
this.plotted = true;
|
||||
} else {
|
||||
Plotly.redraw(graph, graphData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RingBuffer {
|
||||
constructor(capacity) {
|
||||
this.capacity = capacity;
|
||||
this.head = capacity - 1;
|
||||
this.download = [];
|
||||
this.upload = [];
|
||||
this.x_axis = [];
|
||||
this.sortedY = [ [], [] ];
|
||||
for (var i = 0; i < capacity; ++i) {
|
||||
this.download.push(0.0);
|
||||
this.upload.push(0.0);
|
||||
this.x_axis.push(capacity - i);
|
||||
this.sortedY[0].push(0);
|
||||
this.sortedY[1].push(0);
|
||||
}
|
||||
}
|
||||
|
||||
push(download, upload) {
|
||||
this.download[this.head] = download;
|
||||
this.upload[this.head] = 0.0 - upload;
|
||||
this.head += 1;
|
||||
this.head %= this.capacity;
|
||||
}
|
||||
|
||||
prepare() {
|
||||
let counter = 0;
|
||||
for (let i=this.head; i<this.capacity; i++) {
|
||||
this.sortedY[0][counter] = this.download[i];
|
||||
this.sortedY[1][counter] = this.upload[i];
|
||||
counter++;
|
||||
}
|
||||
for (let i=0; i < this.head; i++) {
|
||||
this.sortedY[0][counter] = this.download[i];
|
||||
this.sortedY[1][counter] = this.upload[i];
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
toScatterGraphData() {
|
||||
this.prepare();
|
||||
let GraphData = [
|
||||
{ x: this.x_axis, y: this.sortedY[0], name: 'Download', type: 'scatter' },
|
||||
{ x: this.x_axis, y: this.sortedY[1], name: 'Upload', type: 'scatter' },
|
||||
];
|
||||
return GraphData;
|
||||
}
|
||||
}
|
||||
|
||||
class RttHistogram {
|
||||
constructor() {
|
||||
this.entries = []
|
||||
this.x = [];
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
this.entries.push(i);
|
||||
this.x.push(i * 10);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
this.entries[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
push(rtt) {
|
||||
let band = Math.floor(rtt / 10.0);
|
||||
if (band > 19) {
|
||||
band = 19;
|
||||
}
|
||||
this.entries[band] += 1;
|
||||
}
|
||||
|
||||
pushBand(band, n) {
|
||||
this.entries[band] += n;
|
||||
}
|
||||
|
||||
plot(target_div) {
|
||||
let gData = [
|
||||
{ x: this.x, y: this.entries, type: 'bar', marker: { color: this.x, colorscale: 'RdBu' } }
|
||||
]
|
||||
let graph = document.getElementById(target_div);
|
||||
if (this.plotted == null) {
|
||||
Plotly.newPlot(graph, gData, { margin: { l: 40, r: 0, b: 35, t: 0 }, yaxis: { title: "# Hosts" }, xaxis: { title: 'TCP Round-Trip Time (ms)' } }, { responsive: true });
|
||||
this.plotted = true;
|
||||
} else {
|
||||
Plotly.redraw(graph, gData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ecn(n) {
|
||||
switch (n) {
|
||||
case 0: return "-";
|
||||
case 1: return "L4S";
|
||||
case 2: return "ECT0";
|
||||
case 3: return "CE";
|
||||
default: return "???";
|
||||
}
|
||||
}
|
||||
|
||||
function zip(a, b) {
|
||||
let zipped = [];
|
||||
for (let i=0; i<a.length; ++i) {
|
||||
zipped.push(a[i]);
|
||||
zipped.push(b[i]);
|
||||
}
|
||||
return zipped;
|
||||
}
|
||||
|
||||
function zero_to_null(array) {
|
||||
for (let i=0; i<array.length; ++i) {
|
||||
if (array[i] == 0) array[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
var dnsCache = {};
|
||||
|
||||
function ipToHostname(ip) {
|
||||
if (dnsCache.hasOwnProperty(ip)) {
|
||||
if (dnsCache[ip] != ip) {
|
||||
return ip + "<br /><span style='font-size: 6pt'>" + dnsCache[ip] + "</span>";
|
||||
} else {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
$.get("/api/dns/" + encodeURI(ip), (hostname) => {
|
||||
dnsCache[ip] = hostname;
|
||||
})
|
||||
return ip;
|
||||
}
|
||||
|
||||
function setTitle() {
|
||||
$.get("/api/node_name", (name) => {
|
||||
// Set the window title
|
||||
document.title = name + " - LibreQoS Node Manager";
|
||||
})
|
||||
}
|
@ -1,600 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-secondary">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
|
||||
height="25" /> LibreQoS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
|
||||
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
|
||||
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item" id="statsLink"></li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
|
||||
LibreQoS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="container" class="pad4">
|
||||
|
||||
<div id="toasts"></div>
|
||||
|
||||
<!-- Dashboard Row 1 -->
|
||||
<div class="row mbot8">
|
||||
<!-- THROUGHPUT -->
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-bolt"></i> Current Throughput <span class="badge badge-pill green-badge" id="flowCount">?</span></h5>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="bold">Packets/Second</td>
|
||||
<td id="ppsDown"></td>
|
||||
<td id="ppsUp"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="bold">Bits/Second</td>
|
||||
<td id="bpsDown"></td>
|
||||
<td id="bpsUp"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RAM INFO -->
|
||||
<div class="col-sm-2">
|
||||
<div class="card bg-light d-none d-lg-block">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-database"></i> Memory Status</h5>
|
||||
<div id="ram" class="graph98"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU INFO -->
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-microchip"></i> CPU Status</h5>
|
||||
<div id="cpu" class="graph98"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Row 2 -->
|
||||
<div class="row mbot8 row220">
|
||||
<!-- 5 minutes of throughput -->
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-dashboard"></i> Last 5 Minutes</h5>
|
||||
<div id="tpGraph" class="graph98 graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RTT Histogram -->
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-bar-chart"></i> TCP Round-Trip Time Histogram</h5>
|
||||
<div id="rttHistogram" class="graph98 graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Site Funnel -->
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-tree"></i> Network Tree</h5>
|
||||
<div id="siteFunnel" class="graph98 graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Row 3 -->
|
||||
<div class="row">
|
||||
<!-- Top 10 downloaders -->
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class='fa fa-arrow-down'></i> Top 10 Downloaders
|
||||
<button id="btntop10dl" class="btn btn-small btn-success" href="/top10" onclick="showCircuits()">Circuits</button>
|
||||
<button id="btntop10flows" class="btn btn-small btn-primary" href="/top10" onclick="showFlows()">Flows</button>
|
||||
<button id="btntop10ep" class="btn btn-small btn-primary" href="/top10" onclick="showEndpoints()">Geo Endpoints</button>
|
||||
<button id="btntop10pro" class="btn btn-small btn-primary" href="/top10" onclick="showProtocols()">Protocols</button>
|
||||
<button id="btntop10eth" class="btn btn-small btn-primary" href="/top10" onclick="showEthertypes()">Ethertypes</button>
|
||||
<a href="/showoff" class="btn btn-small btn-info"><i class="fa-solid fa-map"></i> Flow Map</a>
|
||||
</h5>
|
||||
<div id="top10dl" style="display:block;"></div>
|
||||
<div id="top10flows" style="display: none;"></div>
|
||||
<div id="top10ep" style="display: none;"></div>
|
||||
<div id="top10eth" style="display: none;"></div>
|
||||
<div id="top10pro" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Worst 10 RTT -->
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class='fa fa-exclamation'></i> Worst 10
|
||||
<button id="btnworstRtt" class="btn btn-small btn-success" href="/top10" onclick="showWorstRtt()">RTT</button>
|
||||
<button id="btnworstTcp" class="btn btn-small btn-primary" href="/top10" onclick="showWorstTcp()">TCP Retransmits</button>
|
||||
</h5>
|
||||
<div id="worstRtt"></div>
|
||||
<div id="worstTcp" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
var throughput = new MultiRingBuffer(300);
|
||||
|
||||
// Loads the complete ringbuffer for initial display
|
||||
function fillCurrentThroughput() {
|
||||
msgPackGet("/api/throughput_ring_buffer", (tp) => {
|
||||
//console.log(tp);
|
||||
const bits = 0;
|
||||
const packets = 1;
|
||||
const shaped = 2;
|
||||
|
||||
let head = tp[0];
|
||||
for (let i=head; i<300; ++i) {
|
||||
throughput.push("pps", tp[1][i][packets][0], tp[1][i][packets][1]);
|
||||
throughput.push("total", tp[1][i][bits][0], tp[1][i][bits][1]);
|
||||
throughput.push("shaped", tp[1][i][shaped][0], tp[1][i][shaped][1]);
|
||||
}
|
||||
for (let i=0; i<head; ++i) {
|
||||
throughput.push("pps", tp[1][i][packets][0], tp[1][i][packets][1]);
|
||||
throughput.push("total", tp[1][i][bits][0], tp[1][i][bits][1]);
|
||||
throughput.push("shaped", tp[1][i][shaped][0], tp[1][i][shaped][1]);
|
||||
}
|
||||
throughput.plotTotalThroughput("tpGraph");
|
||||
});
|
||||
}
|
||||
|
||||
function updateFlowCounter() {
|
||||
$.get("/api/flows/count", (data) => {
|
||||
$("#flowCount").text(data + " flows");
|
||||
});
|
||||
}
|
||||
|
||||
function updateCurrentThroughput() {
|
||||
msgPackGet("/api/current_throughput", (tp) => {
|
||||
const bits = 0;
|
||||
const packets = 1;
|
||||
const shaped = 2;
|
||||
$("#ppsDown").text(scaleNumber(tp[packets][0]));
|
||||
$("#ppsUp").text(scaleNumber(tp[packets][1]));
|
||||
$("#bpsDown").text(scaleNumber(tp[bits][0]));
|
||||
$("#bpsUp").text(scaleNumber(tp[bits][1]));
|
||||
|
||||
throughput.push("pps", tp[packets][0], tp[packets][1]);
|
||||
throughput.push("total", tp[bits][0], tp[bits][1]);
|
||||
throughput.push("shaped", tp[shaped][0], tp[shaped][1]);
|
||||
throughput.plotTotalThroughput("tpGraph");
|
||||
});
|
||||
}
|
||||
|
||||
var funnelData = new MultiRingBuffer(300);
|
||||
|
||||
function updateSiteFunnel() {
|
||||
msgPackGet("/api/network_tree_summary/", (data) => {
|
||||
let table = "<table class='table table-striped' style='font-size: 8pt;'>";
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
let id = data[i][0];
|
||||
let name = data[i][1][NetTrans.name];
|
||||
if (name.length > 20) {
|
||||
name = name.substring(0, 20) + "...";
|
||||
}
|
||||
table += "<tr>";
|
||||
table += "<td class='redact'><a href='/tree?parent=" + id + "'>" + redactText(name) + "</a></td>";
|
||||
table += "<td>" + scaleNumber(data[i][1][NetTrans.current_throughput][0] * 8) + "</td>";
|
||||
table += "<td>" + scaleNumber(data[i][1][NetTrans.current_throughput][1] * 8) + "</td>";
|
||||
table += "</tr>";
|
||||
}
|
||||
table += "</table>";
|
||||
$("#siteFunnel").html(table);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCpu() {
|
||||
msgPackGet("/api/cpu", (cpu) => {
|
||||
let graph = document.getElementById("cpu");
|
||||
let x = [];
|
||||
let y = [];
|
||||
let colors = [];
|
||||
for (i = 0; i < cpu.length; i++) {
|
||||
x.push(i);
|
||||
y.push(cpu[i]);
|
||||
colors.push(cpu[i]);
|
||||
}
|
||||
colors.push(100); // 1 extra colors entry to force color scaling
|
||||
let data = [{ x: x, y: y, type: 'bar', marker: { color: colors, colorscale: 'Jet' } }];
|
||||
Plotly.newPlot(graph, data, {
|
||||
margin: { l: 0, r: 0, b: 15, t: 0 },
|
||||
yaxis: { automargin: true, autorange: false, range: [0.0, 100.0] },
|
||||
},
|
||||
{ responsive: true });
|
||||
});
|
||||
}
|
||||
|
||||
function updateRam() {
|
||||
msgPackGet("/api/ram", (ram) => {
|
||||
let graph = document.getElementById("ram");
|
||||
let data = [{
|
||||
values: [Math.round(ram[0]), Math.round(ram[1] - ram[0])],
|
||||
labels: ['Used', 'Available'],
|
||||
type: 'pie'
|
||||
}];
|
||||
Plotly.newPlot(graph, data, { margin: { l: 0, r: 0, b: 0, t: 12 }, showlegend: false }, { responsive: true });
|
||||
});
|
||||
}
|
||||
|
||||
function updateNTable(target, tt) {
|
||||
let html = "<table class='table table-striped' style='font-size: 8pt'>";
|
||||
html += "<thead><th></th><th>IP Address</th><th>DL ⬇️</th><th>UL ⬆️</th><th>RTT (ms)</th><th>TCP Retransmits</th><th>Shaped</th></thead>";
|
||||
for (let i = 0; i < tt.length; i++) {
|
||||
let color = color_ramp(tt[i][IpStats.median_tcp_rtt]);
|
||||
html += "<tr>";
|
||||
html += "<td style='color: " + color + "'>⬤</td>";
|
||||
if (tt[i][IpStats.circuit_id] != "") {
|
||||
html += "<td><a class='redact' href='/circuit_queue?id=" + encodeURI(tt[i][IpStats.circuit_id]) + "'>" + redactText(tt[i][IpStats.ip_address]) + "</td>";
|
||||
} else {
|
||||
html += "<td><span class='redact'>" + redactText(tt[i][IpStats.ip_address]) + "</span></td>";
|
||||
}
|
||||
html += "<td>" + scaleNumber(tt[i][IpStats.bits_per_second][0]) + "</td>";
|
||||
html += "<td>" + scaleNumber(tt[i][IpStats.bits_per_second][1]) + "</td>";
|
||||
html += "<td>" + tt[i][IpStats.median_tcp_rtt].toFixed(2) + "</td>";
|
||||
html += "<td>" + tt[i][IpStats.tcp_retransmits][0] + "/" + tt[i][IpStats.tcp_retransmits][1] + "</td>";
|
||||
if (tt[i].tc_handle != 0) {
|
||||
html += "<td><i class='fa fa-check-circle'></i> (" + tt[i][IpStats.plan][0] + "/" + tt[i][IpStats.plan][1] + ")</td>";
|
||||
} else {
|
||||
//html += "<td><a class='btn btn-small btn-success' href='/shaped-add?ip=" + tt[i].ip_address + "'>Add Shaper</a></td>";
|
||||
html += "<td>Not Shaped</td>"
|
||||
}
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</table>";
|
||||
$(target).html(html);
|
||||
}
|
||||
|
||||
function updateTop10() {
|
||||
msgPackGet("/api/top_10_downloaders", (tt) => {
|
||||
updateNTable('#top10dl', tt);
|
||||
});
|
||||
}
|
||||
|
||||
function updateWorst10() {
|
||||
msgPackGet("/api/worst_10_rtt", (tt) => {
|
||||
updateNTable('#worstRtt', tt);
|
||||
});
|
||||
}
|
||||
|
||||
function updateWorstTcp() {
|
||||
msgPackGet("/api/worst_10_tcp", (tt) => {
|
||||
//console.log(tt);
|
||||
updateNTable('#worstTcp', tt);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTop10Flows() {
|
||||
$.get("/api/flows/top/10/rate", data => {
|
||||
let html = "<table class='table table-striped' style='font-size: 8pt'>";
|
||||
html += "<thead>";
|
||||
html += "<th>Protocol</th>";
|
||||
html += "<th>Local IP</th>";
|
||||
html += "<th>Remote IP</th>";
|
||||
html += "<th>UL ⬆️</th>";
|
||||
html += "<th>DL ⬇️</th>";
|
||||
html += "<th>UL RTT</th>";
|
||||
html += "<th>DL RTT</th>";
|
||||
html += "<th>TCP Retransmits</th>";
|
||||
html += "<th>Remote ASN</th>";
|
||||
html += "<th>Country</th>";
|
||||
html += "</thead><tbody>";
|
||||
for (var i = 0; i<data.length; i++) {
|
||||
//console.log(data[i]);
|
||||
html += "<tr>";
|
||||
html += "<td>" + data[i].analysis + "</td>";
|
||||
html += "<td>" + data[i].local_ip + "</td>";
|
||||
html += "<td>" + data[i].remote_ip + "</td>";
|
||||
// TODO: Check scaling
|
||||
html += "<td>" + scaleNumber(data[i].rate_estimate_bps[0]) + "</td>";
|
||||
html += "<td>" + scaleNumber(data[i].rate_estimate_bps[1]) + "</td>";
|
||||
html += "<td>" + scaleNanos(data[i].rtt_nanos[0]) + "</td>";
|
||||
html += "<td>" + scaleNanos(data[i].rtt_nanos[1]) + "</td>";
|
||||
html += "<td>" + data[i].tcp_retransmits[0] + "/" + data[i].tcp_retransmits[1] + "</td>";
|
||||
html += "<td>" + data[i].remote_asn_name + "</td>";
|
||||
html += "<td>" + data[i].remote_asn_country + "</td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</tbody></table>";
|
||||
$("#top10flows").html(html);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTop10Endpoints() {
|
||||
$.get("/api/flows/by_country", data => {
|
||||
//console.log(data);
|
||||
let html = "<table class='table table-striped' style='font-size: 8pt'>";
|
||||
html += "<thead>";
|
||||
html += "<th>Country</th>";
|
||||
html += "<th>UL ⬆️</th>";
|
||||
html += "<th>DL ⬇️</th>";
|
||||
html += "<th>UL RTT</th>";
|
||||
html += "<th>DL RTT</th>";
|
||||
html += "</thead></tbody>";
|
||||
let i = 0;
|
||||
while (i < data.length && i < 10) {
|
||||
html += "<tr>";
|
||||
html += "<td>" + data[i][0] + "</td>";
|
||||
html += "<td>" + scaleNumber(data[i][1][0]) + "</td>";
|
||||
html += "<td>" + scaleNumber(data[i][1][1]) + "</td>";
|
||||
html += "<td>" + scaleNanos(data[i][2][0]) + "</td>";
|
||||
html += "<td>" + scaleNanos(data[i][2][1]) + "</td>";
|
||||
html += "</tr>";
|
||||
i += 1;
|
||||
}
|
||||
html += "</tbody></table>";
|
||||
$("#top10ep").html(html);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTop10Ethertypes() {
|
||||
$.get("/api/flows/ether_protocol", data => {
|
||||
let html = "<table class='table' style='font-size: 8pt'>";
|
||||
html += "<thead>";
|
||||
html += "<th>Protocol</th>";
|
||||
html += "<th>UL ⬆️</th>";
|
||||
html += "<th>DL ⬇️</th>";
|
||||
html += "<th>UL RTT</th>";
|
||||
html += "<th>DL RTT</th>";
|
||||
html += "</thead></tbody>";
|
||||
let row = data.EtherProtocols;
|
||||
html += "<tr>";
|
||||
html += "<td>IPv4</td>";
|
||||
html += "<td>" + scaleNumber(row.v4_bytes[0]) + "</td>";
|
||||
html += "<td>" + scaleNumber(row.v4_bytes[1]) + "</td>";
|
||||
html += "<td>" + scaleNanos(row.v4_rtt[0]) + "</td>";
|
||||
html += "<td>" + scaleNanos(row.v4_rtt[1]) + "</td>";
|
||||
html += "</tr>";
|
||||
|
||||
html += "<tr>";
|
||||
html += "<td>IPv6</td>";
|
||||
html += "<td>" + scaleNumber(row.v6_bytes[0]) + "</td>";
|
||||
html += "<td>" + scaleNumber(row.v6_bytes[1]) + "</td>";
|
||||
html += "<td>" + scaleNanos(row.v6_rtt[0]) + "</td>";
|
||||
html += "<td>" + scaleNanos(row.v6_rtt[1]) + "</td>";
|
||||
html += "</tr>";
|
||||
|
||||
html += "</tbody></table>";
|
||||
$("#top10eth").html(html);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTop10Protocols() {
|
||||
$.get("/api/flows/ip_protocol", data => {
|
||||
let html = "<table class='table' style='font-size: 8pt'>";
|
||||
html += "<thead>";
|
||||
html += "<th>Protocol</th>";
|
||||
html += "<th>UL ⬆️</th>";
|
||||
html += "<th>DL ⬇️</th>";
|
||||
html += "</thead></tbody>";
|
||||
|
||||
for (i=0; i<data.length; i++) {
|
||||
html += "<tr>";
|
||||
html += "<td>" + data[i][0] + "</td>";
|
||||
html += "<td>" + scaleNumber(data[i][1][0]) + "</td>";
|
||||
html += "<td>" + scaleNumber(data[i][1][1]) + "</td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</tbody></table>";
|
||||
$("#top10pro").html(html);
|
||||
});
|
||||
}
|
||||
|
||||
let top10view = "circuits";
|
||||
let worst10view = "rtt";
|
||||
|
||||
function changeBottom10(visible) {
|
||||
const bottom10 = ["worstRtt", "worstTcp"];
|
||||
for (let i=0; i<bottom10.length; i++) {
|
||||
$("#" + bottom10[i]).hide();
|
||||
$("#btn" + bottom10[i]).removeClass("btn-success");
|
||||
$("#btn" + bottom10[i]).addClass("btn-primary");
|
||||
}
|
||||
$("#" + visible).show();
|
||||
$("#btn" + visible).removeClass("btn-primary");
|
||||
$("#btn" + visible).addClass("btn-success");
|
||||
}
|
||||
|
||||
function showWorstRtt() {
|
||||
changeBottom10("worstRtt");
|
||||
worst10view = "rtt";
|
||||
}
|
||||
|
||||
function showWorstTcp() {
|
||||
changeBottom10("worstTcp");
|
||||
worst10view = "tcp";
|
||||
}
|
||||
|
||||
function changeTop10(visible) {
|
||||
const top10 = ["top10dl", "top10flows", "top10ep", "top10eth", "top10pro"];
|
||||
for (let i=0; i<top10.length; i++) {
|
||||
$("#" + top10[i]).hide();
|
||||
$("#btn" + top10[i]).removeClass("btn-success");
|
||||
$("#btn" + top10[i]).addClass("btn-primary");
|
||||
}
|
||||
$("#" + visible).show();
|
||||
$("#btn" + visible).removeClass("btn-primary");
|
||||
$("#btn" + visible).addClass("btn-success");
|
||||
}
|
||||
|
||||
function showCircuits() {
|
||||
changeTop10("top10dl");
|
||||
top10view = "circuits";
|
||||
}
|
||||
|
||||
function showFlows() {
|
||||
changeTop10("top10flows");
|
||||
top10view = "flows";
|
||||
}
|
||||
|
||||
function showEndpoints() {
|
||||
changeTop10("top10ep");
|
||||
top10view = "endpoints";
|
||||
}
|
||||
|
||||
function showProtocols() {
|
||||
changeTop10("top10pro");
|
||||
top10view = "protocols";
|
||||
}
|
||||
|
||||
function showEthertypes() {
|
||||
changeTop10("top10eth");
|
||||
top10view = "ethertypes";
|
||||
}
|
||||
|
||||
var rttGraph = new RttHistogram();
|
||||
|
||||
function updateHistogram() {
|
||||
msgPackGet("/api/rtt_histogram", (rtt) => {
|
||||
rttGraph.clear();
|
||||
for (let i = 0; i < rtt.length; i++) {
|
||||
rttGraph.pushBand(i, rtt[i]);
|
||||
}
|
||||
rttGraph.plot("rttHistogram");
|
||||
});
|
||||
}
|
||||
|
||||
var tickCount = 0;
|
||||
|
||||
function OneSecondCadence() {
|
||||
updateCurrentThroughput();
|
||||
updateFlowCounter();
|
||||
updateSiteFunnel();
|
||||
|
||||
if (tickCount % 5 == 0) {
|
||||
updateHistogram();
|
||||
if (worst10view == "rtt") {
|
||||
updateWorst10();
|
||||
} else if (worst10view == "tcp") {
|
||||
updateWorstTcp();
|
||||
}
|
||||
if (top10view == "circuits") {
|
||||
updateTop10();
|
||||
} else if (top10view == "flows") {
|
||||
updateTop10Flows();
|
||||
} else if (top10view == "endpoints") {
|
||||
updateTop10Endpoints();
|
||||
} else if (top10view == "protocols") {
|
||||
updateTop10Protocols();
|
||||
} else if (top10view == "ethertypes") {
|
||||
updateTop10Ethertypes();
|
||||
}
|
||||
}
|
||||
|
||||
if (tickCount % 10 == 0) {
|
||||
updateCpu();
|
||||
updateRam();
|
||||
}
|
||||
|
||||
tickCount++;
|
||||
setTimeout(OneSecondCadence, 1000);
|
||||
}
|
||||
|
||||
function start() {
|
||||
setTitle();
|
||||
if (isRedacted()) {
|
||||
//console.log("Redacting");
|
||||
//css_getclass(".redact").style.filter = "blur(4px)";
|
||||
css_getclass(".redact").style.fontFamily = "klingon";
|
||||
}
|
||||
|
||||
colorReloadButton();
|
||||
fillCurrentThroughput();
|
||||
updateFlowCounter();
|
||||
updateCpu();
|
||||
updateRam();
|
||||
updateTop10();
|
||||
updateWorst10();
|
||||
updateHistogram();
|
||||
updateHostCounts();
|
||||
updateSiteFunnel();
|
||||
OneSecondCadence();
|
||||
|
||||
// Version Check
|
||||
$.get("/api/version_check", (data) => {
|
||||
if (data != "All Good") {
|
||||
let html = "<div class='alert alert-info alert-dismissible fade show' role='alert'>";
|
||||
html += "<strong>LibreQoS Update Available!</strong>";
|
||||
html += "<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button>";
|
||||
html += "</div>";
|
||||
$("#toasts").append(html);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,167 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-secondary">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" /> LibreQoS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="container" class="pad4">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-users"></i> Add Shaped Circuit</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="circuitId" class="form-label">Circuit ID</label>
|
||||
<input type="text" id="circuitId" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="circuitName" class="form-label">Circuit Name</label>
|
||||
<input type="text" id="circuitName" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="deviceId" class="form-label">Device ID</label>
|
||||
<input type="text" id="deviceId" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="circuitName" class="form-label">Device Name</label>
|
||||
<input type="text" id="deviceName" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="parent" class="form-label">Parent</label>
|
||||
<input type="text" id="parent" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="mac" class="form-label">MAC Address</label>
|
||||
<input type="text" id="mac" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="dlMin" class="form-label">Download Minimum (Mbps)</label>
|
||||
<input type="number" id="dlMin" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="ulMin" class="form-label">Upload Minimum (Mbps)</label>
|
||||
<input type="number" id="ulMin" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="dlMax" class="form-label">Download Maximum (Mbps)</label>
|
||||
<input type="number" id="dlMax" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="ulMax" class="form-label">Upload Maximum (Mbps)</label>
|
||||
<input type="number" id="ulMax" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mbot8">
|
||||
<div class="col">
|
||||
<label for="comment" class="form-label">Comment</label>
|
||||
<input type="text" id="comment" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mbot8">
|
||||
<div class="col">
|
||||
<strong>IPv4 Addresses</strong> (You can use 1.2.3.4/X to match a CIDR subnet)<br />
|
||||
<label for="ipv4_1" class="form-label">Address 1</label>
|
||||
<input type="text" id="ipv4_1" class="form-control" />
|
||||
<label for="ipv4_2" class="form-label">Address 2</label>
|
||||
<input type="text" id="ipv4_2" class="form-control" />
|
||||
<label for="ipv4_3" class="form-label">Address 3</label>
|
||||
<input type="text" id="ipv4_3" class="form-control" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<strong>IPv6 Addresses</strong> (You can use /X to match a subnet)<br />
|
||||
<label for="ipv6_1" class="form-label">Address 1</label>
|
||||
<input type="text" id="ipv6_1" class="form-control" />
|
||||
<label for="ipv6_2" class="form-label">Address 2</label>
|
||||
<input type="text" id="ipv6_2" class="form-control" />
|
||||
<label for="ipv6_3" class="form-label">Address 3</label>
|
||||
<input type="text" id="ip64_3" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col" align="center">
|
||||
<a href="#" class="btn btn-success"><i class='fa fa-plus'></i> Add Record</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
function start() {
|
||||
colorReloadButton();
|
||||
updateHostCounts();
|
||||
|
||||
// Get the ? search params
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
});
|
||||
if (params.ip != null) {
|
||||
if (params.ip.includes(":")) {
|
||||
$("#ipv6_1").val(params.ip + "/128");
|
||||
} else {
|
||||
$("#ipv4_1").val(params.ip + "/32");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,168 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-secondary">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" /> LibreQoS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item" id="statsLink"></li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="container" class="pad4">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-users"></i> Shaped Devices</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input id="search" class="form-control" placeholder="Search" style="min-width: 150px">
|
||||
</div>
|
||||
<div class="col">
|
||||
<a href="#" class="btn btn-primary" id="btnSearch"><i class='fa fa-search'></i></a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--<a href="/shaped-add" class="btn btn-success"><i class='fa fa-plus'></i> Add</a>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<th>Circuit</th>
|
||||
<th>Device</th>
|
||||
<th>Plan</th>
|
||||
<th>IPs</th>
|
||||
<th><i class="fa fa-gear"></i></th>
|
||||
</thead>
|
||||
<tbody id="shapedList"></tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
Go to page: <span id="shapedPaginator"></span><br />
|
||||
Total Shaped Devices: <span id="shapedTotal"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
function fillDeviceTable(devices) {
|
||||
let html = "";
|
||||
for (let i=0; i<devices.length; i++) {
|
||||
html += "<tr>";
|
||||
html += "<td><a class='redact' href='/circuit_queue?id=" + encodeURI(devices[i].circuit_id) + "'>" + devices[i].circuit_id + ": " +redactText(devices[i].circuit_name) + "</a></td>";
|
||||
html += "<td class='redact'>" + devices[i].device_id + ": " + redactText(devices[i].device_name) + "</td>";
|
||||
html += "<td>" + devices[i].download_max_mbps + "/" + devices[i].upload_max_mbps + "</td>";
|
||||
html += "<td style='font-size: 8pt' class='redact'>";
|
||||
for (let j=0; j<devices[i].ipv4.length; j++) {
|
||||
html += devices[i].ipv4[j][0] + "/" + devices[i].ipv4[j][1] + "<br />";
|
||||
}
|
||||
for (let j=0; j<devices[i].ipv6.length; j++) {
|
||||
html += devices[i].ipv6[j][0] + "/" + devices[i].ipv6[j][1] + "<br />";
|
||||
}
|
||||
html += "</td>";
|
||||
html += "<td><a class='btn btn-primary btn-sm' href='#'><i class='fa fa-pencil'></i></a>";
|
||||
html +=" <a href='#' class='btn btn-danger btn-sm'><i class='fa fa-trash'></i></a></td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
$("#shapedList").html(html);
|
||||
}
|
||||
|
||||
function paginator(page) {
|
||||
$.get("/api/shaped_devices_range/" + page * 25 + "/" + (page+1)*25, (devices) => {
|
||||
fillDeviceTable(devices);
|
||||
});
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
let term = $("#search").val();
|
||||
if (term == "") {
|
||||
paginator(0);
|
||||
} else {
|
||||
// /api/shaped_devices_search/<term>
|
||||
let safe_term = encodeURIComponent(term);
|
||||
$.get("/api/shaped_devices_search/" + safe_term, (devices) => {
|
||||
fillDeviceTable(devices);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
setTitle();
|
||||
colorReloadButton();
|
||||
updateHostCounts();
|
||||
$.get("/api/shaped_devices_count", (count) => {
|
||||
let n_pages = count / 25;
|
||||
$("#shapedTotal").text(count);
|
||||
let paginator = "";
|
||||
for (let i=0; i<n_pages; i++) {
|
||||
paginator += "<a href='#' onclick='paginator(" + i + ")'>" + (i+1) + "</a> ";
|
||||
}
|
||||
$("#shapedPaginator").html(paginator);
|
||||
});
|
||||
$.get("/api/shaped_devices_range/0/25", (devices) => {
|
||||
fillDeviceTable(devices);
|
||||
});
|
||||
$("#btnSearch").on('click', () => {
|
||||
doSearch();
|
||||
});
|
||||
$("#search").on('keyup', (k) => {
|
||||
if (k.originalEvent.keyCode == 13) doSearch();
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,157 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
|
||||
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body style="background: black; font-family: Arial, Helvetica, sans-serif; height: 100%; margin: 0;">
|
||||
<p style="font-size: 20pt; text-align: center; color: #dddddd" id="heading"></p>
|
||||
<p style="font-size: 12pt; text-align: center; color: #dddddd">
|
||||
<a href="#" onclick="init()">Refresh</a>
|
||||
</p>
|
||||
<div id="chart" style="width:100vw; height: 100bh; background: black;">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var option;
|
||||
var routes = [];
|
||||
|
||||
function lerpColor(color1, color2, weight) {
|
||||
var r = Math.round(color1[0] + (color2[0] - color1[0]) * weight);
|
||||
var g = Math.round(color1[1] + (color2[1] - color1[1]) * weight);
|
||||
var b = Math.round(color1[2] + (color2[2] - color1[2]) * weight);
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
function getColorForWeight(weight) {
|
||||
// Define our colors as [R, G, B]
|
||||
const green = [0, 128, 0];
|
||||
const orange = [255, 165, 0];
|
||||
const red = [255, 0, 0];
|
||||
|
||||
if (weight <= 0.5) {
|
||||
// Scale weight to be from 0 to 1 for the green to orange transition
|
||||
const adjustedWeight = weight * 2;
|
||||
return lerpColor(green, orange, adjustedWeight);
|
||||
} else {
|
||||
// Scale weight to be from 0 to 1 for the orange to red transition
|
||||
const adjustedWeight = (weight - 0.5) * 2;
|
||||
return lerpColor(orange, red, adjustedWeight);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
$.get("/api/flows/lat_lon", (worldData) => {
|
||||
let condensed = {};
|
||||
let totalBytes = 0;
|
||||
for (let i = 0; i < worldData.length; i++) {
|
||||
let label = worldData[i][2];
|
||||
if (label in condensed) {
|
||||
condensed[label][3] += worldData[i][3]; // Bytes
|
||||
totalBytes += worldData[i][3];
|
||||
if (worldData[i][4] != 0) {
|
||||
condensed[label][4].push(worldData[i][4]); // RTT
|
||||
}
|
||||
} else {
|
||||
condensed[label] = worldData[i];
|
||||
worldData[i][4] = [worldData[i][4]];
|
||||
totalBytes += worldData[i][3];
|
||||
}
|
||||
}
|
||||
|
||||
let entries = [];
|
||||
for (const [key, value] of Object.entries(condensed)) {
|
||||
value[3] = value[3] / totalBytes;
|
||||
entries.push(value);
|
||||
}
|
||||
|
||||
$("#heading").text("World Data. Now tracking " + worldData.length + " flows and " + entries.length + " locations.");
|
||||
|
||||
var data = [{
|
||||
type: 'scattergeo',
|
||||
//locationmode: 'world',
|
||||
lat: [],
|
||||
lon: [],
|
||||
hoverinfo: 'text',
|
||||
text: [],
|
||||
marker: {
|
||||
size: [],
|
||||
color: [],
|
||||
line: {
|
||||
color: [],
|
||||
width: 2
|
||||
},
|
||||
}
|
||||
}];
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
var flow = entries[i];
|
||||
if (flow[4].length != 0) {
|
||||
var lat = flow[0];
|
||||
var lon = flow[1];
|
||||
var text = flow[2];
|
||||
|
||||
var bytes = flow[3] * 20;
|
||||
if (bytes < 5) bytes = 5;
|
||||
|
||||
let middle = flow[4].length -1;
|
||||
var rtt = flow[4][middle] / 1000000;
|
||||
rtt = rtt / 200;
|
||||
if (rtt > 1) rtt = 1;
|
||||
var color = getColorForWeight(rtt);
|
||||
data[0].lat.push(lat);
|
||||
data[0].lon.push(lon);
|
||||
data[0].text.push(text);
|
||||
data[0].marker.size.push(bytes);
|
||||
data[0].marker.line.color.push(color);
|
||||
data[0].marker.color.push(color);
|
||||
}
|
||||
}
|
||||
|
||||
var layout = {
|
||||
autosize: true,
|
||||
margin: {
|
||||
l: 0,
|
||||
r: 0,
|
||||
b: 0,
|
||||
t: 0,
|
||||
pad: 0
|
||||
},
|
||||
paper_bgcolor: 'black',
|
||||
geo: {
|
||||
scope: 'world',
|
||||
projection: {
|
||||
type: 'natural earth'
|
||||
},
|
||||
showland: true,
|
||||
showocean: true,
|
||||
showlakes: true,
|
||||
showrivers: true,
|
||||
showcountries: true,
|
||||
landcolor: 'rgb(217, 217, 217)',
|
||||
subunitwidth: 1,
|
||||
countrywidth: 1,
|
||||
subunitcolor: 'rgb(255,255,255)',
|
||||
countrycolor: 'rgb(255,255,255)',
|
||||
framecolor: 'black',
|
||||
bgcolor: 'black',
|
||||
},
|
||||
};
|
||||
|
||||
Plotly.newPlot('chart', data, layout, { responsive: true, displayModeBar: false });
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init()
|
||||
});
|
||||
</script>
|
||||
|
||||
</html>
|
@ -1,293 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-secondary">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
|
||||
height="25" /> LibreQoS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
|
||||
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
|
||||
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item" id="statsLink"></li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
|
||||
LibreQoS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="container" class="pad4">
|
||||
|
||||
<div class="row mbot8 row220">
|
||||
<!-- 5 minutes of throughput -->
|
||||
<!--
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-hourglass"></i> Last 5 Minutes</h5>
|
||||
<div id="tpGraph" class="graph98 graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<!-- RTT Histogram -->
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-bar-chart"></i> TCP Round-Trip Time Histogram</h5>
|
||||
<div id="rttHistogram" class="graph98 graph150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-tree"></i> <span id="nodeName"
|
||||
style="font-weight: bold;" class='redact'></span></h5>
|
||||
<strong>DL Limit</strong>: <span id="nodeDL"></span><br />
|
||||
<strong>UL Limit</strong>: <span id="nodeUL"></span><br />
|
||||
<div id="breadcrumbs"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 4px;">
|
||||
<!-- List of network circuits -->
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-tree"></i> Child Nodes</h5>
|
||||
<div id="treeList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List of client circuits -->
|
||||
<div class="col-sm-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-users"></i> Attached Clients</h5>
|
||||
<div id="clientList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
let node = 0;
|
||||
let buffers = new MultiRingBuffer(300);
|
||||
let rtt_histo = new RttHistogram();
|
||||
|
||||
function bgColor(traffic, limit) {
|
||||
if (limit == 0) {
|
||||
return "#ddffdd";
|
||||
}
|
||||
let usage = (traffic * 8) / (limit * 1000000);
|
||||
if (usage < 0.25) { return "#ddffdd" }
|
||||
else if (usage < 0.5) { return "#aaffaa" }
|
||||
else if (usage < 0.75) { return "#ffa500" }
|
||||
else { return "#ffdddd" }
|
||||
}
|
||||
|
||||
function getClients(rootName) {
|
||||
msgPackGet("/api/tree_clients/" + encodeURI(rootName), (data) => {
|
||||
let tbl = "<table class='table table-striped'>";
|
||||
tbl += "<thead><th>Circuit</th><th>Limit</th><th>⬇️ DL</th><th>⬆️ UL</th></thead>";
|
||||
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
let nodeDL = scaleNumber(data[i][Circuit.limit][0] * 1000000);
|
||||
let nodeUL = scaleNumber(data[i][Circuit.limit][1] * 1000000);
|
||||
if (nodeDL == "0") nodeDL = "Unlimited";
|
||||
if (nodeUL == "0") nodeUL = "Unlimited";
|
||||
tbl += "<tr>";
|
||||
let displayName = data[i][Circuit.name];
|
||||
if (displayName.length > 30) displayName = displayName.substring(0, 30) + "...";
|
||||
tbl += "<td class='redact'><a href='/circuit_queue?id=" + encodeURI(data[i][Circuit.id]) + "'>" + redactText(displayName) + "</a></td>";
|
||||
tbl += "<td>" + nodeDL + " / " + nodeUL + "</td>";
|
||||
let upbg = bgColor(data[i][Circuit.traffic][1], data[i][Circuit.limit][1]);
|
||||
let dnbg = bgColor(data[i][Circuit.traffic][0], data[0][Circuit.limit][1]);
|
||||
tbl += "<td style='background-color: " + dnbg + "'>" + scaleNumber(data[i][Circuit.traffic][0] * 8) + "</td>";
|
||||
tbl += "<td style='background-color: " + upbg + "'>" + scaleNumber(data[i][Circuit.traffic][1] * 8) + "</td>";
|
||||
|
||||
buffers.push(nodeName, data[i][Circuit.traffic][0] * 8, data[i][Circuit.traffic][1] * 8);
|
||||
}
|
||||
tbl += "</table>";
|
||||
$("#clientList").html(tbl);
|
||||
});
|
||||
}
|
||||
|
||||
let filled_root = false;
|
||||
|
||||
function getTree() {
|
||||
msgPackGet("/api/network_tree/" + node, (data) => {
|
||||
rtt_histo.clear();
|
||||
//console.log(data);
|
||||
// Setup "this node"
|
||||
let rootName = data[0][1][NetTrans.name];
|
||||
if (!filled_root) {
|
||||
$("#nodeName").text(redactText(rootName));
|
||||
let nodeDL = scaleNumber(data[0][1][NetTrans.max_throughput][0] * 1000000);
|
||||
let nodeUL = scaleNumber(data[0][1][NetTrans.max_throughput][1] * 1000000);
|
||||
if (nodeDL == "0") nodeDL = "Unlimited";
|
||||
if (nodeUL == "0") nodeUL = "Unlimited";
|
||||
$("#nodeDL").text(nodeDL);
|
||||
$("#nodeUL").text(nodeUL);
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/node_names",
|
||||
data: JSON.stringify(data[0][1][NetTrans.parents]),
|
||||
success: (nodeNames) => {
|
||||
let breadcrumbs = "<nav aria-label='breadcrumb'>";
|
||||
breadcrumbs += "<ol class='breadcrumb'>";
|
||||
for (let i=0; i<data[0][1][NetTrans.parents].length; ++i) {
|
||||
let bcid = data[0][1][NetTrans.parents][i];
|
||||
if (bcid != node) {
|
||||
let n = nodeNames.find(e => e[0] == data[0][1][NetTrans.parents][i])[1];
|
||||
breadcrumbs += "<li class='breadcrumb-item redact'>";
|
||||
breadcrumbs += "<a href='/tree?parent=" + data[0][1][NetTrans.parents][i] + "'>";
|
||||
breadcrumbs += redactText(n);
|
||||
breadcrumbs += "</a></li>";
|
||||
}
|
||||
}
|
||||
breadcrumbs += "<li class='breadcrumb-item active redact' aria-current='page'>";
|
||||
breadcrumbs += redactText(rootName);
|
||||
breadcrumbs += "</li>";
|
||||
breadcrumbs += "</ol>";
|
||||
breadcrumbs += "</nav>";
|
||||
$("#breadcrumbs").html(breadcrumbs);
|
||||
}
|
||||
});
|
||||
filled_root = true;
|
||||
}
|
||||
|
||||
getClients(rootName);
|
||||
|
||||
// Throughput graph
|
||||
buffers.push(rootName, data[0][1][NetTrans.current_throughput][0] * 8, data[0][1][NetTrans.current_throughput][1] * 8);
|
||||
|
||||
// Build the table & update node buffers
|
||||
let tbl = "<table class='table table-striped'>";
|
||||
tbl += "<thead><th>Site</th><th>Limit</th><th>⬇️ DL</th><th>⬆️ UL</th><th>RTT Latency</th></thead>";
|
||||
for (let i = 1; i < data.length; ++i) {
|
||||
let nodeName = data[i][1][NetTrans.name];
|
||||
|
||||
buffers.push(nodeName, data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
|
||||
|
||||
tbl += "<tr>";
|
||||
tbl += "<td class='redact'><a href='/tree?parent=" + encodeURI(data[i][0]) + "'>" + redactText(nodeName) + "</a></td>";
|
||||
if (data[i][1][NetTrans.max_throughput][0] == 0 && data[i][1][NetTrans.max_throughput][1] == 0) {
|
||||
tbl += "<td>No Limit</td>";
|
||||
} else {
|
||||
let down = scaleNumber(data[i][1][NetTrans.max_throughput][0] * 1000000);
|
||||
let up = scaleNumber(data[i][1][NetTrans.max_throughput][1] * 1000000);
|
||||
tbl += "<td>" + down + " / " + up + "</td>";
|
||||
}
|
||||
let down = scaleNumber(data[i][1][NetTrans.current_throughput][0] * 8);
|
||||
let up = scaleNumber(data[i][1][NetTrans.current_throughput][1] * 8);
|
||||
let dbg = bgColor(data[i][1][NetTrans.current_throughput][0], data[i][1][NetTrans.max_throughput][0]);
|
||||
let ubg = bgColor(data[i][1][NetTrans.current_throughput][0], data[i][1][NetTrans.max_throughput][0]);
|
||||
tbl += "<td style='background-color: " + dbg + "'>" + down + "</td>";
|
||||
tbl += "<td style='background-color: " + ubg + "'>" + up + "</td>";
|
||||
let rtt = "-";
|
||||
if (data[i][1][NetTrans.rtts].length > 0) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < data[i][1][NetTrans.rtts].length; ++j) {
|
||||
sum += data[i][1][NetTrans.rtts][j];
|
||||
}
|
||||
sum /= data[i][1][NetTrans.rtts].length;
|
||||
rtt = sum.toFixed(2) + " ms";
|
||||
rtt_histo.push(sum);
|
||||
}
|
||||
tbl += "<td>" + rtt + "</td>";
|
||||
tbl += "</tr>";
|
||||
}
|
||||
tbl += "</table>";
|
||||
$("#treeList").html(tbl);
|
||||
|
||||
// Build the stacked chart
|
||||
//buffers.plotStackedBars("tpGraph", rootName);
|
||||
|
||||
// Build the RTT histo
|
||||
rtt_histo.plot("rttHistogram");
|
||||
});
|
||||
|
||||
if (isRedacted()) {
|
||||
//console.log("Redacting");
|
||||
//css_getclass(".redact").style.filter = "blur(4px)";
|
||||
css_getclass(".redact").style.fontFamily = "klingon";
|
||||
}
|
||||
|
||||
setTimeout(getTree, 1000);
|
||||
}
|
||||
|
||||
function start() {
|
||||
setTitle();
|
||||
for (let i = 0; i < 20; ++i) rtt_histo.push(0);
|
||||
colorReloadButton();
|
||||
updateHostCounts();
|
||||
getTree();
|
||||
}
|
||||
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
});
|
||||
node = params.parent;
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,132 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<link rel="stylesheet" href="/lqos.css">
|
||||
<link rel="icon" href="/favicon.png">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
|
||||
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-secondary">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" /> LibreQoS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto">
|
||||
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="container" class="pad4">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-address-card"></i> Unmapped IP Addresses (Most recently seen first)</h5>
|
||||
|
||||
<a id="btnDownloadCsv" class="btn btn-info"><i class="fa fa-download"></i> Download Text File of Unknown IP addresses.</a>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<th>IP</th>
|
||||
<th>Total Bandwidth</th>
|
||||
<th>Total Packets</th>
|
||||
<th><i class='fa fa-gear'></i></th>
|
||||
</thead>
|
||||
<tbody id="unknownList"></tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
Go to page: <span id="unknownPaginator"></span><br />
|
||||
Total Shaped Devices: <span id="unknownTotal"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>© 2022-2023, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
function fillDeviceTable(devices) {
|
||||
let html = "";
|
||||
for (let i=0; i<devices.length; i++) {
|
||||
html += "<tr>";
|
||||
html += "<td>" + devices[i].ip_address + "</td>";
|
||||
html += "<td>" + scaleNumber(devices[i].bits_per_second[0]) + " / " + scaleNumber(devices[i].bits_per_second[1]) + "</td>";
|
||||
html += "<td>" + scaleNumber(devices[i].packets_per_second[0]) + " / " + scaleNumber(devices[i].packets_per_second[1]) + "</td>";
|
||||
//html += "<td><a class='btn btn-small btn-success' href='/shaped-add?ip=" + devices[i].ip_address + "'><i class='fa fa-plus'></i></a></td>";
|
||||
html += "<td></td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
$("#unknownList").html(html);
|
||||
}
|
||||
|
||||
function paginator(page) {
|
||||
$.get("/api/unknown_devices_range/" + page * 25 + "/" + (page+1)*25, (devices) => {
|
||||
fillDeviceTable(devices);
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
colorReloadButton();
|
||||
updateHostCounts();
|
||||
$.get("/api/unknown_devices_count", (count) => {
|
||||
let n_pages = count / 25;
|
||||
$("#unknownTotal").text(count);
|
||||
let paginator = "";
|
||||
for (let i=0; i<n_pages; i++) {
|
||||
paginator += "<a href='#' onclick='paginator(" + i + ")'>" + (i+1) + "</a> ";
|
||||
}
|
||||
$("#unknownPaginator").html(paginator);
|
||||
});
|
||||
$.get("/api/unknown_devices_range/0/25", (devices) => {
|
||||
console.log(devices);
|
||||
fillDeviceTable(devices);
|
||||
});
|
||||
$("#btnDownloadCsv").on('click', () => {
|
||||
window.location.href = "/api/unknown_devices_csv";
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
src/rust/lqos_node_manager/static/vendor/klingon.ttf
vendored
BIN
src/rust/lqos_node_manager/static/vendor/klingon.ttf
vendored
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -9,14 +9,14 @@ name = "lqos_python"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
pyo3 = "0"
|
||||
pyo3 = { workspace = true }
|
||||
lqos_bus = { path = "../lqos_bus" }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
anyhow = "1"
|
||||
sysinfo = "0"
|
||||
nix = "0"
|
||||
reqwest = { version = "0.11.24", features = ["blocking"] }
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
chrono = "0.4.33"
|
||||
|
@ -139,7 +139,7 @@ fn recurse_weights(
|
||||
node_index: usize,
|
||||
) -> Result<i64> {
|
||||
let mut weight = 0;
|
||||
let n = &network.nodes[node_index];
|
||||
let n = &network.get_nodes_when_ready()[node_index];
|
||||
//println!(" Tower: {}", n.name);
|
||||
|
||||
device_list
|
||||
@ -152,7 +152,7 @@ fn recurse_weights(
|
||||
});
|
||||
//println!(" Weight: {}", weight);
|
||||
|
||||
for (i, n) in network.nodes
|
||||
for (i, _n) in network.get_nodes_when_ready()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_i, n)| n.immediate_parent == Some(node_index))
|
||||
@ -177,13 +177,13 @@ pub(crate) fn calculate_tree_weights() -> Result<Vec<NetworkNodeWeight>> {
|
||||
let device_list = ConfigShapedDevices::load()?.devices;
|
||||
let device_weights = get_weights_rust()?;
|
||||
let network = lqos_config::NetworkJson::load()?;
|
||||
let root_index = network.nodes.iter().position(|n| n.immediate_parent.is_none()).unwrap();
|
||||
let root_index = network.get_nodes_when_ready().iter().position(|n| n.immediate_parent.is_none()).unwrap();
|
||||
let mut result = Vec::new();
|
||||
//println!("Root index is: {}", root_index);
|
||||
|
||||
// Find all network nodes one off the top
|
||||
network
|
||||
.nodes
|
||||
.get_nodes_when_ready()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_,n)| n.immediate_parent.is_some() && n.immediate_parent.unwrap() == root_index)
|
||||
|
@ -5,18 +5,18 @@ edition = "2021"
|
||||
license = "GPL-2.0-only"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
lqos_bus = { path = "../lqos_bus" }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
lqos_sys = { path = "../lqos_sys" }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
log = "0"
|
||||
log = { workspace = true }
|
||||
log-once = "0.4.0"
|
||||
tokio = { version = "1", features = [ "full", "parking_lot" ] }
|
||||
once_cell = "1"
|
||||
dashmap = "5"
|
||||
tokio = { workspace = true }
|
||||
once_cell = { workspace = true}
|
||||
dashmap = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0", features = [ "html_reports"] }
|
||||
|
@ -23,3 +23,4 @@ pub use queue_structure::spawn_queue_structure_monitor;
|
||||
pub use queue_types::deserialize_tc_tree; // Exported for the benchmarker
|
||||
pub use tracking::spawn_queue_monitor;
|
||||
pub use tracking::{add_watched_queue, still_watching};
|
||||
pub use tracking::{ALL_QUEUE_SUMMARY, TOTAL_QUEUE_STATS};
|
@ -5,7 +5,7 @@ use log::error;
|
||||
pub use queing_structure_json_monitor::spawn_queue_structure_monitor;
|
||||
pub(crate) use queing_structure_json_monitor::QUEUE_STRUCTURE;
|
||||
use queue_network::QueueNetwork;
|
||||
use queue_node::QueueNode;
|
||||
pub(crate) use queue_node::QueueNode;
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) fn read_queueing_structure(
|
||||
|
@ -8,6 +8,7 @@ use lqos_utils::file_watcher::FileWatcher;
|
||||
use once_cell::sync::Lazy;
|
||||
use thiserror::Error;
|
||||
use tokio::task::spawn_blocking;
|
||||
use crate::tracking::ALL_QUEUE_SUMMARY;
|
||||
|
||||
pub(crate) static QUEUE_STRUCTURE: Lazy<RwLock<QueueStructure>> =
|
||||
Lazy::new(|| RwLock::new(QueueStructure::new()));
|
||||
@ -27,6 +28,7 @@ impl QueueStructure {
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
ALL_QUEUE_SUMMARY.clear();
|
||||
if let Ok(queues) = read_queueing_structure() {
|
||||
self.maybe_queues = Some(queues);
|
||||
} else {
|
||||
@ -49,7 +51,7 @@ fn update_queue_structure() {
|
||||
}
|
||||
|
||||
/// Fires up a Linux file system watcher than notifies
|
||||
/// when `ShapedDevices.csv` changes, and triggers a reload.
|
||||
/// when `queuingStructure.json` changes, and triggers a reload.
|
||||
fn watch_for_queueing_structure_changing() -> Result<(), QueueWatcherError> {
|
||||
// Obtain the path to watch
|
||||
let watch_path = QueueNetwork::path();
|
||||
|
142
src/rust/lqos_queue_tracker/src/tracking/all_queue_data.rs
Normal file
142
src/rust/lqos_queue_tracker/src/tracking/all_queue_data.rs
Normal file
@ -0,0 +1,142 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use once_cell::sync::Lazy;
|
||||
use lqos_utils::units::{AtomicDownUp, DownUpOrder};
|
||||
use crate::tracking::TrackedQueue;
|
||||
|
||||
/// Holds all of the CAKE queue summaries being tracked by the system.
|
||||
pub static ALL_QUEUE_SUMMARY: Lazy<AllQueueData> = Lazy::new(|| AllQueueData::new());
|
||||
|
||||
/// Tracks the total number of drops and marks across all queues.
|
||||
pub static TOTAL_QUEUE_STATS: TotalQueueStats = TotalQueueStats::new();
|
||||
|
||||
pub struct TotalQueueStats {
|
||||
pub drops: AtomicDownUp,
|
||||
pub marks: AtomicDownUp,
|
||||
}
|
||||
|
||||
impl TotalQueueStats {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
drops: AtomicDownUp::zeroed(),
|
||||
marks: AtomicDownUp::zeroed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QueueData {
|
||||
pub drops: DownUpOrder<u64>,
|
||||
pub marks: DownUpOrder<u64>,
|
||||
pub prev_drops: Option<DownUpOrder<u64>>,
|
||||
pub prev_marks: Option<DownUpOrder<u64>>,
|
||||
}
|
||||
|
||||
fn zero_total_queue_stats() {
|
||||
TOTAL_QUEUE_STATS.drops.set_to_zero();
|
||||
TOTAL_QUEUE_STATS.marks.set_to_zero();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AllQueueData {
|
||||
data: Mutex<HashMap<String, QueueData>>,
|
||||
}
|
||||
|
||||
impl AllQueueData {
|
||||
pub fn new() -> Self {
|
||||
Self { data: Mutex::new(HashMap::new()) }
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
let mut lock = self.data.lock().unwrap();
|
||||
lock.clear();
|
||||
}
|
||||
|
||||
pub fn ingest_batch(&self, download: Vec<TrackedQueue>, upload: Vec<TrackedQueue>) {
|
||||
let mut lock = self.data.lock().unwrap();
|
||||
|
||||
// Roll through moving current to previous
|
||||
for (_, q) in lock.iter_mut() {
|
||||
q.prev_drops = Some(q.drops);
|
||||
q.prev_marks = Some(q.marks);
|
||||
q.drops = DownUpOrder::zeroed();
|
||||
q.marks = DownUpOrder::zeroed();
|
||||
}
|
||||
|
||||
// Make download markings
|
||||
for dl in download.into_iter() {
|
||||
if let Some(q) = lock.get_mut(&dl.circuit_id) {
|
||||
// We need to update it
|
||||
q.drops.down = dl.drops;
|
||||
q.marks.down = dl.marks;
|
||||
} else {
|
||||
// We need to add it
|
||||
let mut new_record = QueueData {
|
||||
drops: Default::default(),
|
||||
marks: Default::default(),
|
||||
prev_drops: None,
|
||||
prev_marks: None,
|
||||
};
|
||||
new_record.drops.down = dl.drops;
|
||||
new_record.marks.down = dl.marks;
|
||||
lock.insert(dl.circuit_id.clone(), new_record);
|
||||
}
|
||||
}
|
||||
|
||||
// Make upload markings
|
||||
for ul in upload.into_iter() {
|
||||
if let Some(q) = lock.get_mut(&ul.circuit_id) {
|
||||
// We need to update it
|
||||
q.drops.up = ul.drops;
|
||||
q.marks.up = ul.marks;
|
||||
} else {
|
||||
// We need to add it
|
||||
let mut new_record = QueueData {
|
||||
drops: Default::default(),
|
||||
marks: Default::default(),
|
||||
prev_drops: Default::default(),
|
||||
prev_marks: Default::default(),
|
||||
};
|
||||
new_record.drops.up = ul.drops;
|
||||
new_record.marks.up = ul.marks;
|
||||
lock.insert(ul.circuit_id.clone(), new_record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterate_queues(&self, mut f: impl FnMut(&str, &DownUpOrder<u64>, &DownUpOrder<u64>)) {
|
||||
let lock = self.data.lock().unwrap();
|
||||
for (circuit_id, q) in lock.iter() {
|
||||
if let Some(prev_drops) = q.prev_drops {
|
||||
if let Some(prev_marks) = q.prev_marks {
|
||||
if q.drops > prev_drops || q.marks > prev_marks {
|
||||
let drops = q.drops.checked_sub_or_zero(prev_drops);
|
||||
let marks = q.marks.checked_sub_or_zero(prev_marks);
|
||||
f(circuit_id, &drops, &marks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_total_queue_stats(&self) {
|
||||
zero_total_queue_stats();
|
||||
let lock = self.data.lock().unwrap();
|
||||
|
||||
let mut drops = DownUpOrder::zeroed();
|
||||
let mut marks = DownUpOrder::zeroed();
|
||||
|
||||
lock
|
||||
.iter()
|
||||
.filter(|(_, q)| q.prev_drops.is_some() && q.prev_marks.is_some())
|
||||
.for_each(|(_, q)| {
|
||||
drops += q.drops.checked_sub_or_zero(q.prev_drops.unwrap());
|
||||
marks += q.marks.checked_sub_or_zero(q.prev_marks.unwrap());
|
||||
});
|
||||
|
||||
TOTAL_QUEUE_STATS.drops.set_down(drops.down);
|
||||
TOTAL_QUEUE_STATS.drops.set_up(drops.up);
|
||||
TOTAL_QUEUE_STATS.marks.set_down(marks.down);
|
||||
TOTAL_QUEUE_STATS.marks.set_up(marks.up);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use std::time::Instant;
|
||||
use crate::{
|
||||
circuit_to_queue::CIRCUIT_TO_QUEUE, interval::QUEUE_MONITOR_INTERVAL,
|
||||
queue_store::QueueStore, tracking::reader::read_named_queue_from_interface,
|
||||
@ -6,9 +7,15 @@ use log::info;
|
||||
use lqos_utils::fdtimer::periodic;
|
||||
mod reader;
|
||||
mod watched_queues;
|
||||
mod all_queue_data;
|
||||
pub use all_queue_data::*;
|
||||
|
||||
use self::watched_queues::expire_watched_queues;
|
||||
use watched_queues::WATCHED_QUEUES;
|
||||
pub use watched_queues::{add_watched_queue, still_watching};
|
||||
use crate::queue_structure::{QUEUE_STRUCTURE, QueueNode};
|
||||
use crate::queue_types::QueueType;
|
||||
use crate::tracking::reader::read_all_queues_from_interface;
|
||||
|
||||
fn track_queues() {
|
||||
if WATCHED_QUEUES.is_empty() {
|
||||
@ -75,6 +82,106 @@ fn track_queues() {
|
||||
expire_watched_queues();
|
||||
}
|
||||
|
||||
/// Holds the CAKE marks/drops for a given queue/circuit.
|
||||
pub struct TrackedQueue {
|
||||
circuit_id: String,
|
||||
drops: u64,
|
||||
marks: u64,
|
||||
}
|
||||
|
||||
fn connect_queues_to_circuit(structure: &[QueueNode], queues: &[QueueType]) -> Vec<TrackedQueue> {
|
||||
queues
|
||||
.iter()
|
||||
.filter_map(|q| {
|
||||
if let QueueType::Cake(cake) = q {
|
||||
let (major, minor) = cake.parent.get_major_minor();
|
||||
if let Some (s) = structure.iter().find(|s| s.class_major == major as u32 && s.class_minor == minor as u32) {
|
||||
if let Some(circuit_id) = &s.circuit_id {
|
||||
let marks: u32 = cake.tins.iter().map(|tin| tin.ecn_marks).sum();
|
||||
if cake.drops > 0 || marks > 0 {
|
||||
return Some(TrackedQueue {
|
||||
circuit_id: circuit_id.clone(),
|
||||
drops: cake.drops as u64,
|
||||
marks: marks as u64,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn connect_queues_to_circuit_up(structure: &[QueueNode], queues: &[QueueType]) -> Vec<TrackedQueue> {
|
||||
queues
|
||||
.iter()
|
||||
.filter_map(|q| {
|
||||
if let QueueType::Cake(cake) = q {
|
||||
let (major, minor) = cake.parent.get_major_minor();
|
||||
if let Some (s) = structure.iter().find(|s| s.up_class_major == major as u32 && s.class_minor == minor as u32) {
|
||||
if let Some(circuit_id) = &s.circuit_id {
|
||||
let marks: u32 = cake.tins.iter().map(|tin| tin.ecn_marks).sum();
|
||||
if cake.drops > 0 || marks > 0 {
|
||||
return Some(TrackedQueue {
|
||||
circuit_id: circuit_id.clone(),
|
||||
drops: cake.drops as u64,
|
||||
marks: marks as u64,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_queue_reader() {
|
||||
let start = Instant::now();
|
||||
let structure = QUEUE_STRUCTURE.read().unwrap();
|
||||
if let Some(structure) = &structure.maybe_queues {
|
||||
if let Ok(config) = lqos_config::load_config() {
|
||||
// Get all the queues
|
||||
let (download, upload) = if config.on_a_stick_mode() {
|
||||
let all_queues = read_all_queues_from_interface(&config.internet_interface());
|
||||
let (download, upload) = if let Ok(q) = all_queues {
|
||||
let download = connect_queues_to_circuit(&structure, &q);
|
||||
let upload = connect_queues_to_circuit_up(&structure, &q);
|
||||
(download, upload)
|
||||
} else {
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
(download, upload)
|
||||
} else {
|
||||
let all_queues_down = read_all_queues_from_interface(&config.internet_interface());
|
||||
let all_queues_up = read_all_queues_from_interface(&config.isp_interface());
|
||||
|
||||
let download = if let Ok(q) = all_queues_down {
|
||||
connect_queues_to_circuit(&structure, &q)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let upload = if let Ok(q) = all_queues_up {
|
||||
connect_queues_to_circuit(&structure, &q)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
(download, upload)
|
||||
};
|
||||
|
||||
//println!("{}", download.len() + upload.len());
|
||||
ALL_QUEUE_SUMMARY.ingest_batch(download, upload);
|
||||
} else {
|
||||
log::warn!("(TC monitor) Unable to read configuration");
|
||||
}
|
||||
} else {
|
||||
log::warn!("(TC monitor) Not reading queues due to structure not yet ready");
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
log::debug!("(TC monitor) Completed in {:.5} seconds", elapsed.as_secs_f32());
|
||||
}
|
||||
|
||||
/// Spawns a thread that periodically reads the queue statistics from
|
||||
/// the Linux `tc` shaper, and stores them in a `QueueStore` for later
|
||||
/// retrieval.
|
||||
@ -96,4 +203,11 @@ pub fn spawn_queue_monitor() {
|
||||
track_queues();
|
||||
});
|
||||
});
|
||||
|
||||
// Set up a 2nd thread to periodically gather ALL the queue stats
|
||||
std::thread::spawn(|| {
|
||||
periodic(2000, "All Queues", &mut || {
|
||||
all_queue_reader();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,46 @@
|
||||
use crate::{deserialize_tc_tree, queue_types::QueueType};
|
||||
use log::error;
|
||||
use log::{error, info};
|
||||
use lqos_bus::TcHandle;
|
||||
use std::process::Command;
|
||||
use thiserror::Error;
|
||||
|
||||
const TC: &str = "/sbin/tc";
|
||||
|
||||
pub fn read_all_queues_from_interface(
|
||||
interface: &str
|
||||
) -> Result<Vec<QueueType>, QueueReaderError> {
|
||||
let command_output = Command::new(TC)
|
||||
.args([
|
||||
"-s",
|
||||
"-j",
|
||||
"qdisc",
|
||||
"show",
|
||||
"dev",
|
||||
interface,
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
info!("Failed to poll TC for queues: {interface}");
|
||||
info!("{:?}", e);
|
||||
QueueReaderError::CommandError
|
||||
})?;
|
||||
|
||||
let raw_json = String::from_utf8(command_output.stdout)
|
||||
.map_err(|e| {
|
||||
info!("Failed to convert byte stream to UTF-8 string");
|
||||
info!("{:?}", e);
|
||||
QueueReaderError::Utf8Error
|
||||
})?;
|
||||
let result = deserialize_tc_tree(&raw_json)
|
||||
.map_err(|e| {
|
||||
info!("Failed to deserialize TC tree result.");
|
||||
info!("{:?}", e);
|
||||
QueueReaderError::Deserialization
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn read_named_queue_from_interface(
|
||||
interface: &str,
|
||||
tc_handle: TcHandle,
|
||||
|
@ -5,8 +5,7 @@ edition = "2021"
|
||||
license = "GPL-2.0-only"
|
||||
|
||||
[dependencies]
|
||||
colored = "2"
|
||||
default-net = "0" # For obtaining an easy-to-use NIC list
|
||||
uuid = { version = "1", features = ["v4", "fast-rng" ] }
|
||||
colored = { workspace = true }
|
||||
default-net = { workspace = true }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
toml = "0.8.8"
|
||||
toml = { workspace = true }
|
||||
|
@ -1,6 +1,5 @@
|
||||
use colored::Colorize;
|
||||
use default_net::{get_interfaces, interface::InterfaceType, Interface};
|
||||
use uuid::Uuid;
|
||||
use std::{fs, path::Path, process::Command};
|
||||
|
||||
fn get_available_interfaces() -> Vec<Interface> {
|
||||
|
@ -4,14 +4,13 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
colored = "2.1.0"
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_cbor = "0" # For RFC8949/7409 format C binary objects
|
||||
miniz_oxide = "0.7.1"
|
||||
log = "0.4.21" # For compression
|
||||
serde = { workspace = true }
|
||||
serde_cbor = { workspace = true }
|
||||
miniz_oxide = { workspace = true }
|
||||
libc = "0.2.155"
|
||||
nix = "0.29.0"
|
||||
serde_json = "1.0.117"
|
||||
nix = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
@ -38,7 +38,7 @@ enum Commands {
|
||||
}
|
||||
|
||||
fn read_line() -> String {
|
||||
use std::io::{stdin,stdout,Write};
|
||||
use std::io::stdin;
|
||||
let mut s = String::new();
|
||||
stdin().read_line(&mut s).expect("Did not enter a correct string");
|
||||
s.trim().to_string()
|
||||
|
@ -90,7 +90,6 @@ pub fn check_interface_status(results: &mut Vec<SanityCheck>) {
|
||||
#[derive(Debug)]
|
||||
struct IpLinkInterface {
|
||||
pub name: String,
|
||||
pub index: u32,
|
||||
pub operational_state: String,
|
||||
pub link_type: String,
|
||||
pub master: Option<String>,
|
||||
@ -106,14 +105,12 @@ fn get_interfaces_from_ip_link() -> anyhow::Result<Vec<IpLinkInterface>> {
|
||||
let mut interfaces = Vec::new();
|
||||
for interface in output_json.as_array().unwrap() {
|
||||
let name = interface["ifname"].as_str().unwrap().to_string();
|
||||
let index = interface["ifindex"].as_u64().unwrap() as u32;
|
||||
let operstate = interface["operstate"].as_str().unwrap().to_string();
|
||||
let link_type = interface["link_type"].as_str().unwrap().to_string();
|
||||
let master = interface["master"].as_str().map(|s| s.to_string());
|
||||
|
||||
interfaces.push(IpLinkInterface {
|
||||
name,
|
||||
index,
|
||||
operational_state: operstate,
|
||||
link_type,
|
||||
master,
|
||||
|
@ -28,7 +28,7 @@ pub fn can_we_load_net_json(results: &mut Vec<SanityCheck>) {
|
||||
if path.exists() {
|
||||
if let Ok(str) = std::fs::read_to_string(path) {
|
||||
match serde_json::from_str::<Value>(&str) {
|
||||
Ok(json) => {
|
||||
Ok(_json) => {
|
||||
results.push(SanityCheck{
|
||||
name: "network.json is parseable JSON".to_string(),
|
||||
success: true,
|
||||
@ -50,7 +50,7 @@ pub fn can_we_load_net_json(results: &mut Vec<SanityCheck>) {
|
||||
|
||||
pub fn can_we_parse_net_json(results: &mut Vec<SanityCheck>) {
|
||||
match lqos_config::NetworkJson::load() {
|
||||
Ok(json) => {
|
||||
Ok(_json) => {
|
||||
results.push(SanityCheck{
|
||||
name: "network.json is valid JSON".to_string(),
|
||||
success: true,
|
||||
|
@ -42,7 +42,7 @@ pub fn can_we_read_shaped_devices(results: &mut Vec<SanityCheck>) {
|
||||
|
||||
pub fn parent_check(results: &mut Vec<SanityCheck>) {
|
||||
if let Ok(net_json) = lqos_config::NetworkJson::load() {
|
||||
if net_json.nodes.len() < 2 {
|
||||
if net_json.get_nodes_when_ready().len() < 2 {
|
||||
results.push(SanityCheck{
|
||||
name: "Flat Network - Skipping Parent Check".to_string(),
|
||||
success: true,
|
||||
@ -53,7 +53,7 @@ pub fn parent_check(results: &mut Vec<SanityCheck>) {
|
||||
|
||||
if let Ok(shaped_devices) = lqos_config::ConfigShapedDevices::load() {
|
||||
for sd in shaped_devices.devices.iter() {
|
||||
if !net_json.nodes.iter().any(|n| n.name == sd.parent_node) {
|
||||
if !net_json.get_nodes_when_ready().iter().any(|n| n.name == sd.parent_node) {
|
||||
results.push(SanityCheck{
|
||||
name: "Shaped Device Invalid Parent".to_string(),
|
||||
success: false,
|
||||
|
@ -5,18 +5,16 @@ edition = "2021"
|
||||
license = "GPL-2.0-only"
|
||||
|
||||
[dependencies]
|
||||
nix = "0"
|
||||
nix = { workspace = true }
|
||||
libbpf-sys = "1"
|
||||
anyhow = "1"
|
||||
byteorder = "1.4"
|
||||
anyhow = { workspace = true }
|
||||
lqos_bus = { path = "../lqos_bus" }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
log = "0"
|
||||
log = { workspace = true }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
once_cell = "1"
|
||||
dashmap = "5"
|
||||
thiserror = "1"
|
||||
zerocopy = { version = "0.6.1", features = ["simd"] }
|
||||
once_cell = { workspace = true}
|
||||
thiserror = { workspace = true }
|
||||
zerocopy = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0"
|
||||
|
@ -336,12 +336,13 @@ static __always_inline void track_flows(
|
||||
) {
|
||||
u_int8_t rate_index;
|
||||
u_int8_t other_rate_index;
|
||||
// Ensure that we get DownUp order in the lqosd map
|
||||
if (direction == TO_INTERNET) {
|
||||
rate_index = 0;
|
||||
other_rate_index = 1;
|
||||
} else {
|
||||
rate_index = 1;
|
||||
other_rate_index = 0;
|
||||
} else {
|
||||
rate_index = 0;
|
||||
other_rate_index = 1;
|
||||
}
|
||||
|
||||
// Pass to the appropriate protocol handler
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
use lqos_utils::XdpIpAddress;
|
||||
use zerocopy::FromBytes;
|
||||
use lqos_utils::units::DownUpOrder;
|
||||
|
||||
/// Representation of the eBPF `flow_key_t` type.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, FromBytes)]
|
||||
@ -32,29 +33,29 @@ pub struct FlowbeeData {
|
||||
/// Time (nanos) when the connection was last seen
|
||||
pub last_seen: u64,
|
||||
/// Bytes transmitted
|
||||
pub bytes_sent: [u64; 2],
|
||||
pub bytes_sent: DownUpOrder<u64>,
|
||||
/// Packets transmitted
|
||||
pub packets_sent: [u64; 2],
|
||||
pub packets_sent: DownUpOrder<u64>,
|
||||
/// Clock for the next rate estimate
|
||||
pub next_count_time: [u64; 2],
|
||||
pub next_count_time: DownUpOrder<u64>,
|
||||
/// Clock for the previous rate estimate
|
||||
pub last_count_time: [u64; 2],
|
||||
pub last_count_time: DownUpOrder<u64>,
|
||||
/// Bytes at the next rate estimate
|
||||
pub next_count_bytes: [u64; 2],
|
||||
pub next_count_bytes: DownUpOrder<u64>,
|
||||
/// Rate estimate
|
||||
pub rate_estimate_bps: [u32; 2],
|
||||
pub rate_estimate_bps: DownUpOrder<u32>,
|
||||
/// Sequence number of the last packet
|
||||
pub last_sequence: [u32; 2],
|
||||
pub last_sequence: DownUpOrder<u32>,
|
||||
/// Acknowledgement number of the last packet
|
||||
pub last_ack: [u32; 2],
|
||||
pub last_ack: DownUpOrder<u32>,
|
||||
/// TCP Retransmission count (also counts duplicates)
|
||||
pub tcp_retransmits: [u16; 2],
|
||||
pub tcp_retransmits: DownUpOrder<u16>,
|
||||
/// Timestamp values
|
||||
pub tsval: [u32; 2],
|
||||
pub tsval: DownUpOrder<u32>,
|
||||
/// Timestamp echo values
|
||||
pub tsecr: [u32; 2],
|
||||
pub tsecr: DownUpOrder<u32>,
|
||||
/// When did the timestamp change?
|
||||
pub ts_change_time: [u64; 2],
|
||||
pub ts_change_time: DownUpOrder<u64>,
|
||||
/// Has the connection ended?
|
||||
/// 0 = Alive, 1 = FIN, 2 = RST
|
||||
pub end_status: u8,
|
||||
|
@ -5,10 +5,11 @@ edition = "2021"
|
||||
license = "GPL-2.0-only"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
nix = { version = "0", features = ["time"] }
|
||||
log = "0"
|
||||
serde = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
log = { workspace = true }
|
||||
notify = { version = "5.0.0", default-features = false } # Not using crossbeam because of Tokio
|
||||
thiserror = "1"
|
||||
byteorder = "1.4"
|
||||
zerocopy = { version = "0.6.1", features = ["simd"] }
|
||||
thiserror = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
zerocopy = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
|
@ -19,6 +19,8 @@ mod string_table_enum;
|
||||
/// Utilities dealing with Unix Timestamps
|
||||
pub mod unix_time;
|
||||
mod xdp_ip_address;
|
||||
/// Helpers for units of measurement
|
||||
pub mod units;
|
||||
|
||||
/// XDP compatible IP Address
|
||||
pub use xdp_ip_address::XdpIpAddress;
|
||||
|
7
src/rust/lqos_utils/src/units.rs
Normal file
7
src/rust/lqos_utils/src/units.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod atomic_down_up;
|
||||
mod down_up;
|
||||
mod up_down;
|
||||
|
||||
pub use atomic_down_up::*;
|
||||
pub use down_up::*;
|
||||
pub use up_down::*;
|
120
src/rust/lqos_utils/src/units/atomic_down_up.rs
Normal file
120
src/rust/lqos_utils/src/units/atomic_down_up.rs
Normal file
@ -0,0 +1,120 @@
|
||||
//! AtomicDownUp is a struct that contains two atomic u64 values, one for down and one for up.
|
||||
//! We frequently order things down and then up in kernel maps, keeping the ordering explicit
|
||||
//! helps reduce directional confusion/bugs.
|
||||
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
use crate::units::DownUpOrder;
|
||||
|
||||
/// AtomicDownUp is a struct that contains two atomic u64 values, one for down and one for up.
|
||||
/// It's typically used for throughput, but can be used for any pairing that needs to keep track
|
||||
/// of values by direction.
|
||||
///
|
||||
/// Note that unlike the DownUpOrder struct, it is not intended for direct serialization, and
|
||||
/// is not generic.
|
||||
#[derive(Debug)]
|
||||
pub struct AtomicDownUp {
|
||||
down: AtomicU64,
|
||||
up: AtomicU64,
|
||||
}
|
||||
|
||||
impl AtomicDownUp {
|
||||
/// Create a new `AtomicDownUp` with both values set to zero.
|
||||
pub const fn zeroed() -> Self {
|
||||
Self {
|
||||
down: AtomicU64::new(0),
|
||||
up: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set both down and up to zero.
|
||||
pub fn set_to_zero(&self) {
|
||||
self.up.store(0, Relaxed);
|
||||
self.down.store(0, Relaxed);
|
||||
}
|
||||
|
||||
/// Add a tuple of u64 values to the down and up values. The addition
|
||||
/// is checked, and will not occur if it would result in an overflow.
|
||||
pub fn checked_add_tuple(&self, n: (u64, u64)) {
|
||||
let n0 = self.down.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(n) = n0.checked_add(n.0) {
|
||||
self.down.store(n, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
let n1 = self.up.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(n) = n1.checked_add(n.1) {
|
||||
self.up.store(n, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a DownUpOrder to the down and up values. The addition
|
||||
/// is checked, and will not occur if it would result in an overflow.
|
||||
pub fn checked_add(&self, n: DownUpOrder<u64>) {
|
||||
let n0 = self.down.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(n) = n0.checked_add(n.down) {
|
||||
self.down.store(n, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
let n1 = self.up.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(n) = n1.checked_add(n.up) {
|
||||
self.up.store(n, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the down value.
|
||||
pub fn get_down(&self) -> u64 {
|
||||
self.down.load(Relaxed)
|
||||
}
|
||||
|
||||
/// Get the up value.
|
||||
pub fn get_up(&self) -> u64 {
|
||||
self.up.load(Relaxed)
|
||||
}
|
||||
|
||||
/// Set the down value.
|
||||
pub fn set_down(&self, n: u64) {
|
||||
self.down.store(n, Relaxed);
|
||||
}
|
||||
|
||||
/// Set the up value.
|
||||
pub fn set_up(&self, n: u64) {
|
||||
self.up.store(n, Relaxed);
|
||||
}
|
||||
|
||||
/// Transform the AtomicDownUp into a `DownUpOrder<u64>`.
|
||||
pub fn as_down_up(&self) -> DownUpOrder<u64> {
|
||||
DownUpOrder::new(
|
||||
self.get_down(),
|
||||
self.get_up()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_atomic_down_up() {
|
||||
let adu = AtomicDownUp::zeroed();
|
||||
assert_eq!(adu.get_down(), 0);
|
||||
assert_eq!(adu.get_up(), 0);
|
||||
|
||||
adu.set_down(1);
|
||||
adu.set_up(2);
|
||||
assert_eq!(adu.get_down(), 1);
|
||||
assert_eq!(adu.get_up(), 2);
|
||||
|
||||
adu.checked_add(DownUpOrder::new(1, 2));
|
||||
assert_eq!(adu.get_down(), 2);
|
||||
assert_eq!(adu.get_up(), 4);
|
||||
|
||||
adu.checked_add_tuple((1, 2));
|
||||
assert_eq!(adu.get_down(), 3);
|
||||
assert_eq!(adu.get_up(), 6);
|
||||
|
||||
adu.set_to_zero();
|
||||
assert_eq!(adu.get_down(), 0);
|
||||
assert_eq!(adu.get_up(), 0);
|
||||
}
|
||||
}
|
191
src/rust/lqos_utils/src/units/down_up.rs
Normal file
191
src/rust/lqos_utils/src/units/down_up.rs
Normal file
@ -0,0 +1,191 @@
|
||||
//! AtomicDownUp is a struct that contains two atomic u64 values, one for down and one for up.
|
||||
//! We frequently order things down and then up in kernel maps, keeping the ordering explicit
|
||||
//! helps reduce directional confusion/bugs.
|
||||
|
||||
use std::ops::AddAssign;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zerocopy::FromBytes;
|
||||
use crate::units::UpDownOrder;
|
||||
|
||||
/// Provides strong download/upload separation for
|
||||
/// stored statistics to eliminate confusion. This is a generic
|
||||
/// type: you can control the type stored inside.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, FromBytes, Default, Ord, PartialOrd)]
|
||||
pub struct DownUpOrder<T> {
|
||||
/// The down value
|
||||
pub down: T,
|
||||
/// The up value
|
||||
pub up: T,
|
||||
}
|
||||
|
||||
impl <T> DownUpOrder<T>
|
||||
where T: std::cmp::Ord + num_traits::Zero + Copy + num_traits::CheckedSub
|
||||
+ num_traits::CheckedAdd + num_traits::SaturatingSub + num_traits::SaturatingMul
|
||||
+ num_traits::FromPrimitive
|
||||
{
|
||||
/// Create a new DownUpOrder with the given down and up values.
|
||||
pub fn new(down: T, up: T) -> Self {
|
||||
Self { down, up }
|
||||
}
|
||||
|
||||
/// In the C code, it's common to refer to a "direction" byte:
|
||||
///
|
||||
/// * 0: down
|
||||
/// * 1: up
|
||||
/// * >1: error
|
||||
///
|
||||
/// This is a helper function to translate that byte into the
|
||||
/// appropriate value.
|
||||
pub fn dir(&self, direction: usize) -> T {
|
||||
if direction == 0 {
|
||||
self.down
|
||||
} else {
|
||||
self.up
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a new DownUpOrder with both down and up set to zero.
|
||||
pub fn zeroed() -> Self {
|
||||
Self { down: T::zero(), up: T::zero() }
|
||||
}
|
||||
|
||||
/// Check if both down and up are less than the given limit.
|
||||
/// Returns `true` if they are both less than the limit, `false` otherwise.
|
||||
pub fn both_less_than(&self, limit: T) -> bool {
|
||||
self.down < limit && self.up < limit
|
||||
}
|
||||
|
||||
/// Check if the sum of down and up exceeds the given limit.
|
||||
pub fn sum_exceeds(&self, limit: T) -> bool {
|
||||
self.down + self.up > limit
|
||||
}
|
||||
|
||||
/// Subtract the given DownUpOrder from this one, returning a new DownUpOrder.
|
||||
/// If the result would be negative, it is clamped to zero.
|
||||
pub fn checked_sub_or_zero(&self, rhs: DownUpOrder<T>) -> DownUpOrder<T> {
|
||||
let down = T::checked_sub(&self.down, &rhs.down).unwrap_or(T::zero());
|
||||
let up = T::checked_sub(&self.up, &rhs.up).unwrap_or(T::zero());
|
||||
DownUpOrder { down, up }
|
||||
}
|
||||
|
||||
/// Add the given DownUpOrder to this one. If the result would overflow,
|
||||
/// it is set to zero.
|
||||
pub fn checked_add(&mut self, rhs: DownUpOrder<T>) {
|
||||
self.down = self.down.checked_add(&rhs.down).unwrap_or(T::zero());
|
||||
self.up = self.up.checked_add(&rhs.up).unwrap_or(T::zero());
|
||||
}
|
||||
|
||||
/// Add the given down and up values to this DownUpOrder. If the result would overflow,
|
||||
/// it is set to zero.
|
||||
pub fn checked_add_direct(&mut self, down: T, up: T) {
|
||||
self.down = self.down.checked_add(&down).unwrap_or(T::zero());
|
||||
self.up = self.up.checked_add(&up).unwrap_or(T::zero());
|
||||
}
|
||||
|
||||
/// Add the given tuple of down and up values to this DownUpOrder. If the result would overflow,
|
||||
/// it is set to zero.
|
||||
pub fn checked_add_tuple(&mut self, (down, up): (T, T)) {
|
||||
self.down = self.down.checked_add(&down).unwrap_or(T::zero());
|
||||
self.up = self.up.checked_add(&up).unwrap_or(T::zero());
|
||||
}
|
||||
|
||||
/// Add the `down` and `up` values, giving a total.
|
||||
pub fn sum(&self) -> T {
|
||||
self.down + self.up
|
||||
}
|
||||
|
||||
/// Multiply the `down` and `up` values by 8, giving the total number of bits, assuming
|
||||
/// that the previous value was bytes.
|
||||
pub fn to_bits_from_bytes(&self) -> DownUpOrder<T> {
|
||||
DownUpOrder {
|
||||
down: self.down.saturating_mul(&T::from_u32(8).unwrap()),
|
||||
up: self.up.saturating_mul(&T::from_u32(8).unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `down` value.
|
||||
pub fn get_down(&self) -> T {
|
||||
self.down
|
||||
}
|
||||
|
||||
/// Get the `up` value.
|
||||
pub fn get_up(&self) -> T {
|
||||
self.up
|
||||
}
|
||||
|
||||
/// Set both the `down` and `up` values to zero.
|
||||
pub fn set_to_zero(&mut self) {
|
||||
self.down = T::zero();
|
||||
self.up = T::zero();
|
||||
}
|
||||
}
|
||||
|
||||
impl <T> Into<UpDownOrder<T>> for DownUpOrder<T> {
|
||||
fn into(self) -> UpDownOrder<T> {
|
||||
UpDownOrder {
|
||||
up: self.down,
|
||||
down: self.up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl <T> AddAssign for DownUpOrder<T>
|
||||
where T: std::cmp::Ord + num_traits::Zero + Copy + num_traits::CheckedAdd
|
||||
{
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.down = self.down.checked_add(&rhs.down).unwrap_or(T::zero());
|
||||
self.up = self.up.checked_add(&rhs.up).unwrap_or(T::zero());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reverse() {
|
||||
let a = UpDownOrder::new(1, 2);
|
||||
let b: DownUpOrder<i32> = a.into();
|
||||
assert_eq!(a.down, b.up);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checked_sub() {
|
||||
let a = DownUpOrder::new(1u64, 1);
|
||||
let b= DownUpOrder::new(1, 1);
|
||||
let c = a.checked_sub_or_zero(b);
|
||||
assert_eq!(c.up, 0);
|
||||
assert_eq!(c.down, 0);
|
||||
|
||||
let b= DownUpOrder::new(2, 2);
|
||||
let c = a.checked_sub_or_zero(b);
|
||||
assert_eq!(c.up, 0);
|
||||
assert_eq!(c.down, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checked_add() {
|
||||
let mut a = DownUpOrder::new(u64::MAX, u64::MAX);
|
||||
let b = DownUpOrder::new(1, 1);
|
||||
a.checked_add(b);
|
||||
assert_eq!(a.down, 0);
|
||||
assert_eq!(a.up, 0);
|
||||
let mut a = DownUpOrder::new(1, 2);
|
||||
a.checked_add(b);
|
||||
assert_eq!(a.down, 2);
|
||||
assert_eq!(a.up, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checked_add_direct() {
|
||||
let mut a = DownUpOrder::new(u64::MAX, u64::MAX);
|
||||
a.checked_add_direct(1, 1);
|
||||
assert_eq!(a.down, 0);
|
||||
assert_eq!(a.up, 0);
|
||||
let mut a = DownUpOrder::new(1, 2);
|
||||
a.checked_add_direct(1, 1);
|
||||
assert_eq!(a.down, 2);
|
||||
assert_eq!(a.up, 3);
|
||||
}
|
||||
}
|
101
src/rust/lqos_utils/src/units/up_down.rs
Normal file
101
src/rust/lqos_utils/src/units/up_down.rs
Normal file
@ -0,0 +1,101 @@
|
||||
//! AtomicDownUp is a struct that contains two atomic u64 values, one for down and one for up.
|
||||
//! We frequently order things down and then up in kernel maps, keeping the ordering explicit
|
||||
//! helps reduce directional confusion/bugs.
|
||||
|
||||
use crate::units::DownUpOrder;
|
||||
|
||||
/// Provides strong download/upload separation for
|
||||
/// stored statistics to eliminate confusion.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct UpDownOrder<T> {
|
||||
/// The up value
|
||||
pub up: T,
|
||||
/// The down value
|
||||
pub down: T,
|
||||
}
|
||||
|
||||
impl<T> UpDownOrder<T>
|
||||
where T: std::cmp::Ord + num_traits::Zero + Copy + num_traits::CheckedSub
|
||||
+ num_traits::CheckedAdd
|
||||
{
|
||||
/// Create a new UpDownOrder with the given up and down values.
|
||||
pub fn new(up: T, down: T) -> Self {
|
||||
Self {
|
||||
up, down
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a new UpDownOrder with both up and down set to zero.
|
||||
pub fn zeroed() -> Self {
|
||||
Self { down: T::zero(), up: T::zero() }
|
||||
}
|
||||
|
||||
/// Check if both up and down are less than the given limit.
|
||||
pub fn both_less_than(&self, limit: T) -> bool {
|
||||
self.down < limit && self.up < limit
|
||||
}
|
||||
|
||||
/// Subtract the given UpDownOrder from this one, returning a new UpDownOrder.
|
||||
/// If the subtraction would result in a negative value, the result is set to zero.
|
||||
pub fn checked_sub_or_zero(&self, rhs: UpDownOrder<T>) -> UpDownOrder<T> {
|
||||
let down = T::checked_sub(&self.down, &rhs.down).unwrap_or(T::zero());
|
||||
let up = T::checked_sub(&self.up, &rhs.up).unwrap_or(T::zero());
|
||||
UpDownOrder { down, up }
|
||||
}
|
||||
|
||||
/// Add the given UpDownOrder to this one, updating the values in place.
|
||||
/// Overflowing values are set to zero.
|
||||
pub fn checked_add(&mut self, rhs: UpDownOrder<T>) {
|
||||
self.down = self.down.checked_add(&rhs.down).unwrap_or(T::zero());
|
||||
self.up = self.up.checked_add(&rhs.up).unwrap_or(T::zero());
|
||||
}
|
||||
}
|
||||
|
||||
impl <T> Into<DownUpOrder<T>> for UpDownOrder<T> {
|
||||
fn into(self) -> DownUpOrder<T> {
|
||||
DownUpOrder {
|
||||
up: self.down,
|
||||
down: self.up,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reverse() {
|
||||
let a = DownUpOrder::new(1, 2);
|
||||
let b: UpDownOrder<i32> = a.into();
|
||||
assert_eq!(a.down, b.up);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checked_sub() {
|
||||
let a = UpDownOrder::new(1u64, 1);
|
||||
let b= UpDownOrder::new(1, 1);
|
||||
let c = a.checked_sub_or_zero(b);
|
||||
assert_eq!(c.up, 0);
|
||||
assert_eq!(c.down, 0);
|
||||
|
||||
let b= UpDownOrder::new(2, 2);
|
||||
let c = a.checked_sub_or_zero(b);
|
||||
assert_eq!(c.up, 0);
|
||||
assert_eq!(c.down, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checked_add() {
|
||||
let mut a = UpDownOrder::new(u64::MAX, u64::MAX);
|
||||
let b = UpDownOrder::new(1, 1);
|
||||
a.checked_add(b);
|
||||
assert_eq!(a.down, 0);
|
||||
assert_eq!(a.up, 0);
|
||||
let mut a = UpDownOrder::new(1, 2);
|
||||
a.checked_add(b);
|
||||
assert_eq!(a.down, 3);
|
||||
assert_eq!(a.up, 2);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use std::fmt::Display;
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use zerocopy::FromBytes;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
@ -58,7 +59,7 @@ impl XdpIpAddress {
|
||||
&& self.0[11] == 0xFF
|
||||
}
|
||||
|
||||
/// Convers an `XdpIpAddress` type to a Rust `IpAddr` type, using
|
||||
/// Converts an `XdpIpAddress` type to a Rust `IpAddr` type, using
|
||||
/// the in-build mapped function for squishing IPv4 into IPv6
|
||||
pub fn as_ipv6(&self) -> Ipv6Addr {
|
||||
if self.is_v4() {
|
||||
@ -99,6 +100,13 @@ impl XdpIpAddress {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for XdpIpAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_ip())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl From<XdpIpAddress> for IpAddr {
|
||||
fn from(val: XdpIpAddress) -> Self {
|
||||
val.as_ip()
|
||||
|
@ -9,36 +9,43 @@ default = ["equinix_tests"]
|
||||
equinix_tests = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
anyhow = { workspace = true }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
lqos_sys = { path = "../lqos_sys" }
|
||||
lqos_queue_tracker = { path = "../lqos_queue_tracker" }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
lqos_heimdall = { path = "../lqos_heimdall" }
|
||||
lts_client = { path = "../lts_client" }
|
||||
lqos_support_tool = { path = "../lqos_support_tool" }
|
||||
tokio = { version = "1", features = [ "full", "parking_lot" ] }
|
||||
once_cell = "1.17.1"
|
||||
once_cell = { workspace = true}
|
||||
lqos_bus = { path = "../lqos_bus" }
|
||||
signal-hook = "0.3"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
env_logger = "0"
|
||||
log = "0"
|
||||
nix = "0"
|
||||
sysinfo = "0"
|
||||
dashmap = "5"
|
||||
num-traits = "0.2"
|
||||
thiserror = "1"
|
||||
log = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
itertools = "0.12.1"
|
||||
csv = "1"
|
||||
reqwest = { version = "0.11.24", features = ["blocking"] }
|
||||
csv = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
flate2 = "1.0"
|
||||
bincode = "1"
|
||||
ip_network_table = "0"
|
||||
ip_network = "0"
|
||||
zerocopy = {version = "0.6.1", features = [ "simd" ] }
|
||||
bincode = { workspace = true }
|
||||
ip_network_table = { workspace = true }
|
||||
ip_network = { workspace = true }
|
||||
zerocopy = { workspace = true }
|
||||
fxhash = "0.2.1"
|
||||
axum = { version = "0.7.5", features = ["ws", "http2"] }
|
||||
axum-extra = { version = "0.9.3", features = ["cookie", "cookie-private"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
default-net = { workspace = true }
|
||||
surge-ping = "0.8.1"
|
||||
rand = "0.8.5"
|
||||
mime_guess = "2.0.4"
|
||||
|
||||
# Support JemAlloc on supported platforms
|
||||
[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies]
|
||||
jemallocator = "0.5"
|
||||
jemallocator = { workspace = true }
|
||||
|
9
src/rust/lqosd/copy_files.sh
Executable file
9
src/rust/lqosd/copy_files.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo "Copying static"
|
||||
cp -v -R src/node_manager/static2/* ../../bin/static2/
|
||||
echo "Done"
|
||||
pushd src/node_manager/js_build || exit
|
||||
./esbuild.sh
|
||||
popd || exit
|
||||
cp -R src/node_manager/js_build/out/* ../../bin/static2/
|
@ -4,7 +4,7 @@ use std::{time::Duration, net::TcpStream, io::Write};
|
||||
use lqos_bus::anonymous::{AnonymousUsageV1, build_stats};
|
||||
use lqos_sys::num_possible_cpus;
|
||||
use sysinfo::System;
|
||||
use crate::{shaped_devices_tracker::{SHAPED_DEVICES, NETWORK_JSON}, stats::{HIGH_WATERMARK_DOWN, HIGH_WATERMARK_UP}};
|
||||
use crate::{shaped_devices_tracker::{SHAPED_DEVICES, NETWORK_JSON}, stats::HIGH_WATERMARK};
|
||||
|
||||
const SLOW_START_SECS: u64 = 1;
|
||||
const INTERVAL_SECS: u64 = 60 * 60 * 24;
|
||||
@ -71,11 +71,11 @@ fn anonymous_usage_dump() -> anyhow::Result<()> {
|
||||
|
||||
data.git_hash = env!("GIT_HASH").to_string();
|
||||
data.shaped_device_count = SHAPED_DEVICES.read().unwrap().devices.len();
|
||||
data.net_json_len = NETWORK_JSON.read().unwrap().nodes.len();
|
||||
data.net_json_len = NETWORK_JSON.read().unwrap().get_nodes_when_ready().len();
|
||||
|
||||
data.high_watermark_bps = (
|
||||
HIGH_WATERMARK_DOWN.load(std::sync::atomic::Ordering::Relaxed),
|
||||
HIGH_WATERMARK_UP.load(std::sync::atomic::Ordering::Relaxed),
|
||||
HIGH_WATERMARK.get_down(),
|
||||
HIGH_WATERMARK.get_up(),
|
||||
);
|
||||
|
||||
|
||||
|
@ -8,7 +8,7 @@ use lts_client::{
|
||||
pub(crate) fn get_network_tree() -> Vec<(usize, NetworkTreeEntry)> {
|
||||
if let Ok(reader) = NETWORK_JSON.read() {
|
||||
let result = reader
|
||||
.nodes
|
||||
.get_nodes_when_ready()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, n)| (idx, n.into()))
|
||||
|
@ -28,11 +28,12 @@ use signal_hook::{
|
||||
consts::{SIGHUP, SIGINT, SIGTERM},
|
||||
iterator::Signals,
|
||||
};
|
||||
use stats::{BUS_REQUESTS, TIME_TO_POLL_HOSTS, HIGH_WATERMARK_DOWN, HIGH_WATERMARK_UP, FLOWS_TRACKED};
|
||||
use stats::{BUS_REQUESTS, TIME_TO_POLL_HOSTS, HIGH_WATERMARK, FLOWS_TRACKED};
|
||||
use throughput_tracker::flow_data::get_rtt_events_per_second;
|
||||
use tokio::join;
|
||||
mod stats;
|
||||
mod preflight_checks;
|
||||
mod node_manager;
|
||||
|
||||
// Use JemAllocator only on supported platforms
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
@ -64,7 +65,6 @@ async fn main() -> Result<()> {
|
||||
|
||||
// Load config
|
||||
let config = lqos_config::load_config()?;
|
||||
|
||||
|
||||
// Apply Tunings
|
||||
tuning::tune_lqosd_from_config_file()?;
|
||||
@ -137,6 +137,13 @@ async fn main() -> Result<()> {
|
||||
// Create the socket server
|
||||
let server = UnixSocketServer::new().expect("Unable to spawn server");
|
||||
|
||||
// Webserver starting point
|
||||
tokio::spawn(async {
|
||||
if let Err(e) = node_manager::spawn_webserver().await {
|
||||
log::error!("Node Manager Failed: {e:?}");
|
||||
}
|
||||
});
|
||||
|
||||
// Main bus listen loop
|
||||
server.listen(handle_bus_requests).await?;
|
||||
Ok(())
|
||||
@ -177,7 +184,7 @@ fn handle_bus_requests(
|
||||
BusRequest::ClearIpFlow => clear_ip_flows(),
|
||||
BusRequest::ListIpFlow => list_mapped_ips(),
|
||||
BusRequest::XdpPping => throughput_tracker::xdp_pping_compat(),
|
||||
BusRequest::RttHistogram => throughput_tracker::rtt_histogram(),
|
||||
BusRequest::RttHistogram => throughput_tracker::rtt_histogram::<20>(),
|
||||
BusRequest::HostCounts => throughput_tracker::host_counts(),
|
||||
BusRequest::AllUnknownIps => throughput_tracker::all_unknown_ips(),
|
||||
BusRequest::ReloadLibreQoS => program_control::reload_libre_qos(),
|
||||
@ -217,10 +224,7 @@ fn handle_bus_requests(
|
||||
BusResponse::LqosdStats {
|
||||
bus_requests: BUS_REQUESTS.load(std::sync::atomic::Ordering::Relaxed),
|
||||
time_to_poll_hosts: TIME_TO_POLL_HOSTS.load(std::sync::atomic::Ordering::Relaxed),
|
||||
high_watermark: (
|
||||
HIGH_WATERMARK_DOWN.load(std::sync::atomic::Ordering::Relaxed),
|
||||
HIGH_WATERMARK_UP.load(std::sync::atomic::Ordering::Relaxed),
|
||||
),
|
||||
high_watermark: HIGH_WATERMARK.as_down_up(),
|
||||
tracked_flows: FLOWS_TRACKED.load(std::sync::atomic::Ordering::Relaxed),
|
||||
rtt_events_per_second: get_rtt_events_per_second(),
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user