mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2026-09-03 20:52:53 -05:00
Merge pull request #1135 from LibreQoE/fix_eth_1
Show infrastructure Ethernet caps and remove the lone Tree Overview tab
This commit is contained in:
@@ -105,9 +105,10 @@ Esta página documenta las vistas clave de la WebUI (Node Manager) y su comporta
|
||||
- `Flow Sankey` enfatiza los flujos recientes más activos en lugar de todos los flujos retenidos más antiguos.
|
||||
|
||||
### Ethernet Caps
|
||||
- La página de revisión Ethernet es una tabla ligera para operadores con los circuitos reducidos automáticamente porque la velocidad Ethernet detectada quedó por debajo del plan solicitado.
|
||||
- La página de revisión Ethernet es una tabla ligera para operadores con los circuitos y nodos de topología reducidos automáticamente porque la velocidad Ethernet detectada quedó por debajo de la velocidad solicitada.
|
||||
- Intencionalmente no aparece en la navegación principal; los operadores llegan a ella haciendo clic en las insignias de advertencia Ethernet de la página de circuito o de la tabla de circuitos adjuntos del árbol.
|
||||
- La página soporta búsqueda, filtro por tier (`10M`, `100M`, `1G+`) y paginación sobre los circuitos auto-capped.
|
||||
- Las filas de circuito abren la página Circuit, y las filas de nodo de topología abren el nodo correspondiente en el árbol.
|
||||
- La página soporta búsqueda, filtro por tier (`10M`, `100M`, `1G+`) y paginación sobre los objetivos con reducción automática.
|
||||
|
||||
### Árbol/ponderación de CPU
|
||||
- Muestra distribución de colas/circuitos por núcleo de CPU.
|
||||
|
||||
@@ -156,9 +156,10 @@ Practical meaning:
|
||||
- `Flow Sankey` emphasizes the hottest recent flows rather than every older retained flow.
|
||||
|
||||
### Ethernet Caps
|
||||
- The Ethernet review page is a lightweight operator table of circuits automatically down-rated because detected Ethernet speed was below the requested plan.
|
||||
- The Ethernet review page is a lightweight operator table of circuits and topology nodes automatically down-rated because detected Ethernet speed was below the requested rate.
|
||||
- It is intentionally not in the main navigation; operators reach it by clicking Ethernet warning badges on the Circuit page or Tree attached-circuits table.
|
||||
- The page supports search, tier filtering (`10M`, `100M`, `1G+`), and paging across auto-capped circuits.
|
||||
- Circuit rows open the Circuit page, while topology-node rows open the matching Tree node.
|
||||
- The page supports search, tier filtering (`10M`, `100M`, `1G+`), and paging across auto-capped targets.
|
||||
|
||||
### CPU Tree / CPU Weights
|
||||
- Shows queue/circuit distribution by CPU core.
|
||||
|
||||
@@ -17,12 +17,35 @@ pub struct CircuitEthernetMetadataFile {
|
||||
pub circuits: Vec<CircuitEthernetMetadata>,
|
||||
}
|
||||
|
||||
/// Describes a detected negotiated Ethernet speed and any automatic shaping cap applied to a circuit.
|
||||
/// Identifies the topology object whose configured rate was reduced by an Ethernet limit.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum EthernetCapTargetKind {
|
||||
/// A subscriber circuit represented by `ShapedDevices.csv`.
|
||||
#[default]
|
||||
Circuit,
|
||||
/// A topology node such as an access point or infrastructure device.
|
||||
Node,
|
||||
}
|
||||
|
||||
/// Describes a detected negotiated Ethernet speed and any automatic rate cap applied to a circuit or topology node.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct CircuitEthernetMetadata {
|
||||
/// Circuit identifier as emitted to `ShapedDevices.csv`.
|
||||
/// The kind of topology object affected by the Ethernet rate cap.
|
||||
#[serde(default)]
|
||||
pub target_kind: EthernetCapTargetKind,
|
||||
/// Stable target identity. Empty legacy values fall back to `circuit_id`.
|
||||
#[serde(default)]
|
||||
pub target_id: String,
|
||||
/// Human-facing target name. Empty legacy values fall back to `circuit_name`.
|
||||
#[serde(default)]
|
||||
pub target_name: String,
|
||||
/// Circuit identifier as emitted to `ShapedDevices.csv`, retained for compatibility.
|
||||
///
|
||||
/// For topology-node advisories, this matches `target_id`.
|
||||
pub circuit_id: String,
|
||||
/// Human-readable circuit name for UI display.
|
||||
/// Human-readable circuit name for UI display, retained for compatibility.
|
||||
///
|
||||
/// For topology-node advisories, this matches `target_name`.
|
||||
pub circuit_name: String,
|
||||
/// Device IDs considered when determining the circuit Ethernet limit.
|
||||
pub device_ids: Vec<String>,
|
||||
@@ -47,3 +70,73 @@ pub struct CircuitEthernetMetadata {
|
||||
/// Interface name that reported the limiting Ethernet speed when known.
|
||||
pub limiting_interface_name: Option<String>,
|
||||
}
|
||||
|
||||
impl CircuitEthernetMetadata {
|
||||
/// Creates empty advisory metadata for a circuit or topology-node target.
|
||||
///
|
||||
/// The legacy circuit fields mirror the target identity so existing readers can safely
|
||||
/// consume topology-node advisories.
|
||||
pub fn for_target(
|
||||
target_kind: EthernetCapTargetKind,
|
||||
target_id: String,
|
||||
target_name: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
target_kind,
|
||||
circuit_id: target_id.clone(),
|
||||
circuit_name: target_name.clone(),
|
||||
target_id,
|
||||
target_name,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the stable identity of the capped circuit or topology node.
|
||||
pub fn target_id_or_circuit_id(&self) -> &str {
|
||||
if self.target_id.trim().is_empty() {
|
||||
&self.circuit_id
|
||||
} else {
|
||||
&self.target_id
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the display name of the capped circuit or topology node.
|
||||
pub fn target_name_or_circuit_name(&self) -> &str {
|
||||
if self.target_name.trim().is_empty() {
|
||||
&self.circuit_name
|
||||
} else {
|
||||
&self.target_name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CircuitEthernetMetadata, EthernetCapTargetKind};
|
||||
|
||||
#[test]
|
||||
fn legacy_circuit_metadata_uses_circuit_identity_as_the_target() {
|
||||
let metadata = CircuitEthernetMetadata {
|
||||
circuit_id: "circuit-1".to_string(),
|
||||
circuit_name: "Circuit One".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(metadata.target_kind, EthernetCapTargetKind::Circuit);
|
||||
assert_eq!(metadata.target_id_or_circuit_id(), "circuit-1");
|
||||
assert_eq!(metadata.target_name_or_circuit_name(), "Circuit One");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_constructor_keeps_legacy_identity_in_sync() {
|
||||
let metadata = CircuitEthernetMetadata::for_target(
|
||||
EthernetCapTargetKind::Node,
|
||||
"uisp:device:ap-1".to_string(),
|
||||
"AP One".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(metadata.target_kind, EthernetCapTargetKind::Node);
|
||||
assert_eq!(metadata.circuit_id, metadata.target_id);
|
||||
assert_eq!(metadata.circuit_name, metadata.target_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,6 @@ pub fn apply_ethernet_rate_cap<'a>(
|
||||
download_max: applied_download_max,
|
||||
upload_max: applied_upload_max,
|
||||
advisory: Some(CircuitEthernetMetadata {
|
||||
circuit_id: circuit_id.to_string(),
|
||||
circuit_name: circuit_name.to_string(),
|
||||
device_ids: unique_device_ids(&observations),
|
||||
source: limiting_observation.source.clone(),
|
||||
negotiated_ethernet_mbps: limiting_observation.negotiated_ethernet_mbps,
|
||||
@@ -175,6 +173,11 @@ pub fn apply_ethernet_rate_cap<'a>(
|
||||
limiting_device_id: limiting_observation.device_id.clone(),
|
||||
limiting_device_name: limiting_observation.device_name.clone(),
|
||||
limiting_interface_name: limiting_observation.interface_name.clone(),
|
||||
..CircuitEthernetMetadata::for_target(
|
||||
Default::default(),
|
||||
circuit_id.to_string(),
|
||||
circuit_name.to_string(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ pub use circuit_anchors::{
|
||||
};
|
||||
pub use circuit_ethernet_metadata::{
|
||||
CIRCUIT_ETHERNET_METADATA_FILENAME, CircuitEthernetMetadata, CircuitEthernetMetadataFile,
|
||||
circuit_ethernet_metadata_path,
|
||||
EthernetCapTargetKind, circuit_ethernet_metadata_path,
|
||||
};
|
||||
pub use cpu_topology::{
|
||||
CpuListParseError, ShapingCpuDetection, ShapingCpuSource, detect_shaping_cpus,
|
||||
|
||||
@@ -53,18 +53,37 @@ function buildTierBadge(row) {
|
||||
return badge;
|
||||
}
|
||||
|
||||
function targetKindLabel(row) {
|
||||
return row?.target_kind === "Node" ? "Node" : "Circuit";
|
||||
}
|
||||
|
||||
function targetHref(row) {
|
||||
if (row?.target_kind === "Node") {
|
||||
return `/tree.html?nodeId=${encodeURIComponent(row.target_id)}`;
|
||||
}
|
||||
return `/circuit.html?id=${encodeURIComponent(row.target_id)}`;
|
||||
}
|
||||
|
||||
function setRequestError(message = "") {
|
||||
const error = document.getElementById("ethernetCapsError");
|
||||
if (!error) return;
|
||||
error.hidden = !message;
|
||||
error.textContent = message;
|
||||
}
|
||||
|
||||
function tableRow(row) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.classList.add("small");
|
||||
|
||||
const circuitCell = document.createElement("td");
|
||||
const link = document.createElement("a");
|
||||
link.href = `/circuit.html?id=${encodeURIComponent(row.circuit_id)}`;
|
||||
link.href = targetHref(row);
|
||||
link.classList.add("redactable");
|
||||
link.textContent = row.circuit_name || row.circuit_id;
|
||||
link.textContent = row.target_name || row.target_id;
|
||||
circuitCell.appendChild(link);
|
||||
tr.appendChild(circuitCell);
|
||||
|
||||
tr.appendChild(simpleRow(targetKindLabel(row)));
|
||||
tr.appendChild(simpleRow(row.parent_node || "-", true));
|
||||
|
||||
const ethernetCell = document.createElement("td");
|
||||
@@ -87,7 +106,7 @@ function renderTable(rows) {
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "text-muted small";
|
||||
empty.textContent = "No Ethernet-limited circuits matched this filter.";
|
||||
empty.textContent = "No Ethernet-limited circuits or nodes matched this filter.";
|
||||
target.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
@@ -98,7 +117,8 @@ function renderTable(rows) {
|
||||
table.classList.add("lqos-table", "lqos-table-tight");
|
||||
|
||||
const thead = document.createElement("thead");
|
||||
thead.appendChild(theading("Circuit"));
|
||||
thead.appendChild(theading("Target"));
|
||||
thead.appendChild(theading("Type"));
|
||||
thead.appendChild(theading("Parent"));
|
||||
thead.appendChild(theading("Ethernet"));
|
||||
thead.appendChild(theading("Requested"));
|
||||
@@ -114,7 +134,7 @@ function renderTable(rows) {
|
||||
target.appendChild(wrap);
|
||||
}
|
||||
|
||||
function updateSummary(query, rows) {
|
||||
function updateSummary(query, rows, requestFailed = false) {
|
||||
const summary = document.getElementById("ethernetCapsSummary");
|
||||
const pager = document.getElementById("ethernetCapsPager");
|
||||
const prev = document.getElementById("ethernetCapsPrev");
|
||||
@@ -126,9 +146,17 @@ function updateSummary(query, rows) {
|
||||
const start = totalRows === 0 ? 0 : (currentPage - 1) * pageSize + 1;
|
||||
const end = totalRows === 0 ? 0 : start + Math.max(0, rows.length - 1);
|
||||
|
||||
if (requestFailed) {
|
||||
summary.textContent = "Ethernet cap data unavailable";
|
||||
pager.textContent = "Retry the page after the connection is restored.";
|
||||
prev.disabled = true;
|
||||
next.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
summary.textContent = totalRows === 0
|
||||
? "No active Ethernet caps"
|
||||
: `${totalRows} Ethernet-limited circuits`;
|
||||
: `${totalRows} Ethernet-limited targets`;
|
||||
pager.textContent = totalRows === 0
|
||||
? "Page 1 / 1"
|
||||
: `Showing ${start}–${end} of ${totalRows} • Page ${currentPage} / ${totalPages}`;
|
||||
@@ -153,12 +181,14 @@ async function requestPage() {
|
||||
});
|
||||
const data = msg?.data || { rows: [], total_rows: 0, query };
|
||||
totalRows = Number.isFinite(Number(data.total_rows)) ? Number(data.total_rows) : 0;
|
||||
setRequestError();
|
||||
renderTable(data.rows || []);
|
||||
updateSummary(data.query, data.rows || []);
|
||||
} catch (_error) {
|
||||
totalRows = 0;
|
||||
setRequestError("Ethernet cap data could not be loaded. Check the node-manager connection and retry.");
|
||||
renderTable([]);
|
||||
updateSummary({ page: 0 }, []);
|
||||
updateSummary({ page: 0 }, [], true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
stormguardNodeContext,
|
||||
summarizeStormguardHistory,
|
||||
} from "./tree_stormguard.mjs";
|
||||
import {setTreeDetailTabsVisibility} from "./tree_detail_tabs.mjs";
|
||||
|
||||
var tree = null;
|
||||
var parent = 0;
|
||||
@@ -335,19 +336,9 @@ function ensureTreeStormguardGraph() {
|
||||
}
|
||||
|
||||
function renderTreeStormguard() {
|
||||
const tabItem = document.getElementById("treeStormguardTabItem");
|
||||
const pane = document.getElementById("treeStormguardPane");
|
||||
const visible = shouldShowStormguardTab(stormguardRuntime);
|
||||
const focusWillBeHidden = !visible
|
||||
&& (tabItem?.contains(document.activeElement) || pane?.contains(document.activeElement));
|
||||
tabItem?.classList.toggle("d-none", !visible);
|
||||
setTreeDetailTabsVisibility(document, window.bootstrap, visible);
|
||||
if (!visible) {
|
||||
const stormguardTab = document.getElementById("tree-stormguard-tab");
|
||||
const overviewTab = document.getElementById("tree-overview-tab");
|
||||
if (stormguardTab?.classList.contains("active")) {
|
||||
window.bootstrap?.Tab.getOrCreateInstance(overviewTab)?.show();
|
||||
}
|
||||
if (focusWillBeHidden) overviewTab?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Shows StormGuard detail tabs only when StormGuard is available and prevents focus from
|
||||
* remaining in a tab strip that has just been hidden.
|
||||
*/
|
||||
export function setTreeDetailTabsVisibility(document, bootstrap, visible) {
|
||||
const tabsContainer = document.getElementById("treeDetailTabsContainer");
|
||||
const stormguardTabItem = document.getElementById("treeStormguardTabItem");
|
||||
const stormguardTab = document.getElementById("tree-stormguard-tab");
|
||||
const overviewTab = document.getElementById("tree-overview-tab");
|
||||
const overviewPane = document.getElementById("treeOverviewPane");
|
||||
const stormguardPane = document.getElementById("treeStormguardPane");
|
||||
const focusWillBeHidden = !visible
|
||||
&& (tabsContainer?.contains(document.activeElement) || stormguardPane?.contains(document.activeElement));
|
||||
|
||||
stormguardTabItem?.classList.toggle("d-none", !visible);
|
||||
if (!visible) {
|
||||
if (stormguardTab?.classList.contains("active")) {
|
||||
bootstrap?.Tab.getOrCreateInstance(overviewTab)?.show();
|
||||
}
|
||||
tabsContainer?.classList.add("d-none");
|
||||
if (focusWillBeHidden) overviewPane?.focus();
|
||||
return;
|
||||
}
|
||||
tabsContainer?.classList.remove("d-none");
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {setTreeDetailTabsVisibility} from "./tree_detail_tabs.mjs";
|
||||
|
||||
function fakeElement() {
|
||||
const classes = new Set();
|
||||
const element = {
|
||||
classList: {
|
||||
add: (name) => classes.add(name),
|
||||
remove: (name) => classes.delete(name),
|
||||
contains: (name) => classes.has(name),
|
||||
toggle: (name, force) => {
|
||||
if (force) classes.add(name);
|
||||
else classes.delete(name);
|
||||
},
|
||||
},
|
||||
contains: (candidate) => candidate?.parent === element,
|
||||
focusCount: 0,
|
||||
focus() {
|
||||
this.focusCount += 1;
|
||||
},
|
||||
};
|
||||
return element;
|
||||
}
|
||||
|
||||
function treeDocument(activeElement, stormguardTabActive = false) {
|
||||
const tabsContainer = fakeElement();
|
||||
const stormguardTabItem = fakeElement();
|
||||
const stormguardTab = fakeElement();
|
||||
const overviewTab = fakeElement();
|
||||
const overviewPane = fakeElement();
|
||||
const stormguardPane = fakeElement();
|
||||
if (stormguardTabActive) stormguardTab.classList.add("active");
|
||||
const elements = {
|
||||
treeDetailTabsContainer: tabsContainer,
|
||||
treeStormguardTabItem: stormguardTabItem,
|
||||
"tree-stormguard-tab": stormguardTab,
|
||||
"tree-overview-tab": overviewTab,
|
||||
treeOverviewPane: overviewPane,
|
||||
treeStormguardPane: stormguardPane,
|
||||
};
|
||||
return {
|
||||
document: {
|
||||
activeElement,
|
||||
getElementById: (id) => elements[id] || null,
|
||||
},
|
||||
elements,
|
||||
};
|
||||
}
|
||||
|
||||
test("hiding StormGuard tabs moves tab focus to the visible overview pane", () => {
|
||||
const focusedTab = {parent: null};
|
||||
const {document, elements} = treeDocument(focusedTab, true);
|
||||
focusedTab.parent = elements.treeDetailTabsContainer;
|
||||
let overviewWasActivated = false;
|
||||
|
||||
setTreeDetailTabsVisibility(document, {
|
||||
Tab: {getOrCreateInstance: () => ({show: () => { overviewWasActivated = true; }})},
|
||||
}, false);
|
||||
|
||||
assert.equal(elements.treeDetailTabsContainer.classList.contains("d-none"), true);
|
||||
assert.equal(elements.treeStormguardTabItem.classList.contains("d-none"), true);
|
||||
assert.equal(elements.treeOverviewPane.focusCount, 1);
|
||||
assert.equal(overviewWasActivated, true);
|
||||
});
|
||||
|
||||
test("hiding StormGuard tabs does not steal focus from visible content", () => {
|
||||
const {document, elements} = treeDocument({parent: null});
|
||||
|
||||
setTreeDetailTabsVisibility(document, null, false);
|
||||
|
||||
assert.equal(elements.treeDetailTabsContainer.classList.contains("d-none"), true);
|
||||
assert.equal(elements.treeOverviewPane.focusCount, 0);
|
||||
});
|
||||
|
||||
test("hiding StormGuard tabs moves StormGuard pane focus to the overview pane", () => {
|
||||
const focusedContent = {parent: null};
|
||||
const {document, elements} = treeDocument(focusedContent);
|
||||
focusedContent.parent = elements.treeStormguardPane;
|
||||
|
||||
setTreeDetailTabsVisibility(document, null, false);
|
||||
|
||||
assert.equal(elements.treeOverviewPane.focusCount, 1);
|
||||
});
|
||||
|
||||
test("showing StormGuard reveals the full tab strip", () => {
|
||||
const {document, elements} = treeDocument({parent: null});
|
||||
elements.treeDetailTabsContainer.classList.add("d-none");
|
||||
elements.treeStormguardTabItem.classList.add("d-none");
|
||||
|
||||
setTreeDetailTabsVisibility(document, null, true);
|
||||
|
||||
assert.equal(elements.treeDetailTabsContainer.classList.contains("d-none"), false);
|
||||
assert.equal(elements.treeStormguardTabItem.classList.contains("d-none"), false);
|
||||
});
|
||||
@@ -15,6 +15,7 @@ node --test \
|
||||
"${SCRIPT_DIR}/src/circuit_packet_capture_dom.test.mjs" \
|
||||
"${SCRIPT_DIR}/src/tree_limit_reason.test.mjs" \
|
||||
"${SCRIPT_DIR}/src/tree_stormguard.test.mjs" \
|
||||
"${SCRIPT_DIR}/src/tree_detail_tabs.test.mjs" \
|
||||
"${SCRIPT_DIR}/src/config/shaped_device_identity.test.mjs" \
|
||||
"${SCRIPT_DIR}/src/config/shaped_device_wire.test.mjs" \
|
||||
"${SCRIPT_DIR}/src/config_radius_accounting_contract.test.mjs" \
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use lqos_config::{CircuitEthernetMetadata, CircuitEthernetMetadataFile, load_config};
|
||||
use lqos_config::{
|
||||
CircuitEthernetMetadata, CircuitEthernetMetadataFile, EthernetCapTargetKind, load_config,
|
||||
};
|
||||
use lqos_utils::normalize_circuit_id_key;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -71,13 +73,19 @@ pub struct EthernetCapsPageQuery {
|
||||
pub tier: Option<EthernetCapTier>,
|
||||
}
|
||||
|
||||
/// One Ethernet-limited circuit row for the review page.
|
||||
/// One Ethernet-limited circuit or topology-node row for the review page.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EthernetCapsPageRow {
|
||||
/// Stable circuit identifier used for deep-linking to the circuit page.
|
||||
/// Stable circuit identifier retained for clients that only understand circuit rows.
|
||||
pub circuit_id: String,
|
||||
/// Human-facing circuit name.
|
||||
/// Human-facing circuit name retained for clients that only understand circuit rows.
|
||||
pub circuit_name: String,
|
||||
/// Whether this row represents a circuit or topology node.
|
||||
pub target_kind: EthernetCapTargetKind,
|
||||
/// Stable target identifier used for deep-linking.
|
||||
pub target_id: String,
|
||||
/// Human-facing target name.
|
||||
pub target_name: String,
|
||||
/// Parent node from shaped devices when available.
|
||||
pub parent_node: String,
|
||||
/// Compact warning badge metadata.
|
||||
@@ -161,6 +169,35 @@ fn sort_rank(tier: &EthernetCapTier) -> u8 {
|
||||
}
|
||||
}
|
||||
|
||||
fn advisory_to_page_row(
|
||||
advisory: &CircuitEthernetMetadata,
|
||||
parent_nodes: &HashMap<String, String>,
|
||||
) -> Option<EthernetCapsPageRow> {
|
||||
let badge = advisory_to_badge(advisory)?;
|
||||
let target_id = advisory.target_id_or_circuit_id().to_string();
|
||||
let target_name = advisory.target_name_or_circuit_name().to_string();
|
||||
let parent_node = if advisory.target_kind == EthernetCapTargetKind::Circuit {
|
||||
parent_nodes
|
||||
.get(&advisory.circuit_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
Some(EthernetCapsPageRow {
|
||||
circuit_id: advisory.circuit_id.clone(),
|
||||
circuit_name: advisory.circuit_name.clone(),
|
||||
target_kind: advisory.target_kind.clone(),
|
||||
target_id,
|
||||
target_name,
|
||||
parent_node,
|
||||
badge,
|
||||
limiting_device_name: advisory.limiting_device_name.clone(),
|
||||
limiting_interface_name: advisory.limiting_interface_name.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a compact badge map keyed by circuit ID for Ethernet auto-capped circuits.
|
||||
pub(crate) fn ethernet_cap_badge_map() -> HashMap<String, EthernetCapBadge> {
|
||||
let mut badges = HashMap::new();
|
||||
@@ -168,6 +205,9 @@ pub(crate) fn ethernet_cap_badge_map() -> HashMap<String, EthernetCapBadge> {
|
||||
return badges;
|
||||
};
|
||||
for advisory in file.circuits {
|
||||
if advisory.target_kind != EthernetCapTargetKind::Circuit {
|
||||
continue;
|
||||
}
|
||||
let Some(badge) = advisory_to_badge(&advisory) else {
|
||||
continue;
|
||||
};
|
||||
@@ -183,7 +223,8 @@ pub(crate) fn ethernet_advisory_for_circuit(
|
||||
) -> Option<CircuitEthernetMetadata> {
|
||||
let file = load_advisory_file()?;
|
||||
file.circuits.into_iter().find(|entry| {
|
||||
entry.auto_capped
|
||||
entry.target_kind == EthernetCapTargetKind::Circuit
|
||||
&& entry.auto_capped
|
||||
&& entry.circuit_id.eq_ignore_ascii_case(circuit_id)
|
||||
&& entry
|
||||
.device_ids
|
||||
@@ -202,31 +243,19 @@ pub fn ethernet_caps_page(query: EthernetCapsPageQuery) -> EthernetCapsPage {
|
||||
let mut rows = Vec::new();
|
||||
if let Some(file) = load_advisory_file() {
|
||||
for advisory in file.circuits {
|
||||
let Some(badge) = advisory_to_badge(&advisory) else {
|
||||
let Some(row) = advisory_to_page_row(&advisory, &parent_nodes) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(filter_tier) = query.tier.as_ref()
|
||||
&& &badge.tier != filter_tier
|
||||
&& &row.badge.tier != filter_tier
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let parent_node = parent_nodes
|
||||
.get(&advisory.circuit_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let row = EthernetCapsPageRow {
|
||||
circuit_id: advisory.circuit_id,
|
||||
circuit_name: advisory.circuit_name,
|
||||
parent_node,
|
||||
badge,
|
||||
limiting_device_name: advisory.limiting_device_name,
|
||||
limiting_interface_name: advisory.limiting_interface_name,
|
||||
};
|
||||
if !search.is_empty() {
|
||||
let limiting_device = row.limiting_device_name.as_deref().unwrap_or("");
|
||||
let limiting_interface = row.limiting_interface_name.as_deref().unwrap_or("");
|
||||
if !row.circuit_id.to_lowercase().contains(&search)
|
||||
&& !row.circuit_name.to_lowercase().contains(&search)
|
||||
if !row.target_id.to_lowercase().contains(&search)
|
||||
&& !row.target_name.to_lowercase().contains(&search)
|
||||
&& !row.parent_node.to_lowercase().contains(&search)
|
||||
&& !row.badge.tier_label.to_lowercase().contains(&search)
|
||||
&& !limiting_device.to_lowercase().contains(&search)
|
||||
@@ -242,8 +271,8 @@ pub fn ethernet_caps_page(query: EthernetCapsPageQuery) -> EthernetCapsPage {
|
||||
rows.sort_by(|left, right| {
|
||||
sort_rank(&left.badge.tier)
|
||||
.cmp(&sort_rank(&right.badge.tier))
|
||||
.then_with(|| left.circuit_name.cmp(&right.circuit_name))
|
||||
.then_with(|| left.circuit_id.cmp(&right.circuit_id))
|
||||
.then_with(|| left.target_name.cmp(&right.target_name))
|
||||
.then_with(|| left.target_id.cmp(&right.target_id))
|
||||
});
|
||||
|
||||
let total_rows = rows.len();
|
||||
@@ -273,7 +302,9 @@ pub fn ethernet_caps_page(query: EthernetCapsPageQuery) -> EthernetCapsPage {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{EthernetCapTier, sort_rank, tier_for_speed};
|
||||
use super::{EthernetCapTargetKind, EthernetCapTier, advisory_to_page_row, sort_rank, tier_for_speed};
|
||||
use lqos_config::CircuitEthernetMetadata;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn ethernet_cap_tier_classifies_expected_speeds() {
|
||||
@@ -287,4 +318,30 @@ mod tests {
|
||||
assert!(sort_rank(&EthernetCapTier::TenM) < sort_rank(&EthernetCapTier::HundredM));
|
||||
assert!(sort_rank(&EthernetCapTier::HundredM) < sort_rank(&EthernetCapTier::GigPlus));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topology_node_advisory_becomes_a_node_page_row() {
|
||||
let advisory = CircuitEthernetMetadata {
|
||||
target_kind: EthernetCapTargetKind::Node,
|
||||
target_id: "uisp:device:ap-1".to_string(),
|
||||
target_name: "AP One".to_string(),
|
||||
circuit_id: "uisp:device:ap-1".to_string(),
|
||||
circuit_name: "AP One".to_string(),
|
||||
negotiated_ethernet_mbps: 100,
|
||||
requested_download_mbps: 500.0,
|
||||
requested_upload_mbps: 200.0,
|
||||
applied_download_mbps: 94.0,
|
||||
applied_upload_mbps: 94.0,
|
||||
auto_capped: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let row = advisory_to_page_row(&advisory, &HashMap::new())
|
||||
.expect("auto-capped topology node should appear on the page");
|
||||
|
||||
assert_eq!(row.target_kind, EthernetCapTargetKind::Node);
|
||||
assert_eq!(row.target_id, "uisp:device:ap-1");
|
||||
assert_eq!(row.target_name, "AP One");
|
||||
assert!(row.parent_node.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
<div class="lqos-config-panel-header">
|
||||
<div>
|
||||
<h5 class="lqos-config-panel-title"><i class="fa fa-ethernet me-2"></i>Ethernet Caps</h5>
|
||||
<div class="lqos-config-panel-subtitle">Circuits automatically down-rated because detected Ethernet speed was below the requested plan.</div>
|
||||
<div class="lqos-config-panel-subtitle">Circuits and topology nodes automatically down-rated because detected Ethernet speed was below the requested rate.</div>
|
||||
</div>
|
||||
<div id="ethernetCapsSummary" class="small text-body-secondary"></div>
|
||||
</div>
|
||||
|
||||
<div class="lqos-config-section">
|
||||
<div id="ethernetCapsError" class="alert alert-warning small" role="alert" aria-live="assertive" hidden></div>
|
||||
<div class="row g-2 align-items-end mb-3">
|
||||
<div class="col-12 col-lg-5">
|
||||
<label class="form-label small text-body-secondary" for="ethernetCapsSearch">Search</label>
|
||||
<input id="ethernetCapsSearch" class="form-control" type="text" placeholder="Search circuits, parents, device names…">
|
||||
<input id="ethernetCapsSearch" class="form-control" type="text" placeholder="Search targets, parents, device names…">
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label class="form-label small text-body-secondary" for="ethernetCapsTier">Ethernet</label>
|
||||
@@ -32,11 +33,11 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 d-flex justify-content-lg-end gap-2">
|
||||
<button id="ethernetCapsPrev" type="button" class="btn btn-outline-primary">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
<button id="ethernetCapsPrev" type="button" class="btn btn-outline-primary" aria-label="Previous page">
|
||||
<i class="fa fa-arrow-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button id="ethernetCapsNext" type="button" class="btn btn-outline-primary">
|
||||
<i class="fa fa-arrow-right"></i>
|
||||
<button id="ethernetCapsNext" type="button" class="btn btn-outline-primary" aria-label="Next page">
|
||||
<i class="fa fa-arrow-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="col-12 d-none" id="treeDetailTabsContainer">
|
||||
<ul class="nav nav-tabs lqos-tree-tabs" id="treeDetailTabs" role="tablist" aria-label="Network tree detail views">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tree-overview-tab" data-bs-toggle="tab" data-bs-target="#treeOverviewPane" type="button" role="tab" aria-controls="treeOverviewPane" aria-selected="true">
|
||||
|
||||
@@ -127,6 +127,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: speed,
|
||||
negotiated_ethernet_interface: iface.map(str::to_string),
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: crate::uisp_types::UispAttachmentRateSource::Static,
|
||||
|
||||
@@ -326,6 +326,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
@@ -386,6 +388,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
@@ -407,6 +411,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
@@ -428,6 +434,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
@@ -492,6 +500,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
@@ -513,6 +523,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
|
||||
@@ -549,6 +549,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: crate::uisp_types::UispAttachmentRateSource::Static,
|
||||
@@ -570,6 +572,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: crate::uisp_types::UispAttachmentRateSource::Static,
|
||||
@@ -647,6 +651,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
|
||||
@@ -525,6 +525,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: crate::uisp_types::UispAttachmentRateSource::Static,
|
||||
|
||||
@@ -19,12 +19,12 @@ use crate::strategies::legacy_routes_override::RouteOverride;
|
||||
use crate::uisp_types::{UispAttachmentRateSource, UispDevice};
|
||||
use lqos_config::{
|
||||
CircuitAnchor, CircuitAnchorsFile, CircuitEthernetMetadata, Config, ConfigShapedDevices,
|
||||
EthernetPortLimitPolicy, RequestedCircuitRates, ShapedDevice as ConfigShapedDevice,
|
||||
TOPOLOGY_ATTACHMENT_AUTO_ID, TopologyAllowedParent, TopologyAttachmentOption,
|
||||
TopologyAttachmentRateSource, TopologyAttachmentRole, TopologyCanonicalIngressKind,
|
||||
TopologyCanonicalStateFile, TopologyEditorNode, TopologyEditorStateFile,
|
||||
TopologyParentCandidate, TopologyParentCandidatesFile, TopologyParentCandidatesNode,
|
||||
TopologyQueueVisibilityPolicy, topology_auto_attachment_option,
|
||||
EthernetCapTargetKind, EthernetPortLimitPolicy, RequestedCircuitRates,
|
||||
ShapedDevice as ConfigShapedDevice, TOPOLOGY_ATTACHMENT_AUTO_ID, TopologyAllowedParent,
|
||||
TopologyAttachmentOption, TopologyAttachmentRateSource, TopologyAttachmentRole,
|
||||
TopologyCanonicalIngressKind, TopologyCanonicalStateFile, TopologyEditorNode,
|
||||
TopologyEditorStateFile, TopologyParentCandidate, TopologyParentCandidatesFile,
|
||||
TopologyParentCandidatesNode, TopologyQueueVisibilityPolicy, topology_auto_attachment_option,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use lqos_overrides::{TopologyAttachmentMode, TopologyParentOverrideMode};
|
||||
@@ -43,6 +43,41 @@ type GraphType = petgraph::Graph<GraphMapping, LinkMapping, Directed>;
|
||||
const GENERATED_INTERNET_ROOT_NAME: &str = "INSERTED_INTERNET";
|
||||
const GENERATED_INTERNET_ROOT_ID: &str = "ROOT-001";
|
||||
|
||||
/// Builds advisory metadata for a topology node whose infrastructure transport cap reduced it.
|
||||
fn topology_node_ethernet_advisory(device: &UispDevice) -> Option<CircuitEthernetMetadata> {
|
||||
let applied_ethernet_cap = device.transport_cap_mbps?;
|
||||
let auto_capped = device.download < device.raw_download || device.upload < device.raw_upload;
|
||||
if !auto_capped {
|
||||
return None;
|
||||
}
|
||||
|
||||
let target_id = format!("uisp:device:{}", device.id);
|
||||
Some(CircuitEthernetMetadata {
|
||||
device_ids: vec![device.id.clone()],
|
||||
source: "uisp/infrastructure_transport".to_string(),
|
||||
negotiated_ethernet_mbps: device
|
||||
.transport_cap_line_rate_mbps
|
||||
.or(device.negotiated_ethernet_mbps)
|
||||
.unwrap_or(applied_ethernet_cap),
|
||||
requested_download_mbps: device.raw_download as f32,
|
||||
requested_upload_mbps: device.raw_upload as f32,
|
||||
applied_download_mbps: device.download as f32,
|
||||
applied_upload_mbps: device.upload as f32,
|
||||
auto_capped,
|
||||
limiting_device_id: Some(device.id.clone()),
|
||||
limiting_device_name: Some(device.name.clone()),
|
||||
limiting_interface_name: device
|
||||
.transport_cap_interface
|
||||
.clone()
|
||||
.or_else(|| device.negotiated_ethernet_interface.clone()),
|
||||
..CircuitEthernetMetadata::for_target(
|
||||
EthernetCapTargetKind::Node,
|
||||
target_id,
|
||||
device.name.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct LegacyShapedDevice {
|
||||
circuit_id: String,
|
||||
@@ -771,6 +806,11 @@ async fn build_imported_full2_bundle_from_data(
|
||||
ingress_identity: None,
|
||||
nodes: topology_editor_nodes,
|
||||
};
|
||||
let exported_uisp_device_ids: HashSet<&str> = topology_editor_state
|
||||
.nodes
|
||||
.iter()
|
||||
.filter_map(|node| node.node_id.strip_prefix("uisp:device:"))
|
||||
.collect();
|
||||
let canonical_state = TopologyCanonicalStateFile::from_editor_and_network(
|
||||
&topology_editor_state,
|
||||
&Value::Object(network_json.clone()),
|
||||
@@ -927,6 +967,14 @@ async fn build_imported_full2_bundle_from_data(
|
||||
"Completed UISP full2 import bundle"
|
||||
);
|
||||
|
||||
ethernet_advisories.extend(
|
||||
uisp_data
|
||||
.devices
|
||||
.iter()
|
||||
.filter(|device| exported_uisp_device_ids.contains(device.id.as_str()))
|
||||
.filter_map(topology_node_ethernet_advisory),
|
||||
);
|
||||
|
||||
Ok(ImportedTopologyBundle {
|
||||
source: "uisp/full2".to_string(),
|
||||
generated_unix: topology_editor_state.generated_unix,
|
||||
@@ -2325,6 +2373,8 @@ mod topology_override_tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
@@ -3142,6 +3192,8 @@ mod topology_override_tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
@@ -3472,12 +3524,38 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: UispAttachmentRateSource::Static,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topology_node_ethernet_advisory_preserves_the_port_cap_details() {
|
||||
let mut device = test_uisp_device("ap-1", "AP One");
|
||||
device.raw_download = 500;
|
||||
device.raw_upload = 200;
|
||||
device.download = 94;
|
||||
device.upload = 94;
|
||||
device.negotiated_ethernet_mbps = Some(100);
|
||||
device.negotiated_ethernet_interface = Some("eth0".to_string());
|
||||
device.transport_cap_line_rate_mbps = Some(100);
|
||||
device.transport_cap_interface = Some("eth0".to_string());
|
||||
device.transport_cap_mbps = Some(94);
|
||||
|
||||
let advisory = topology_node_ethernet_advisory(&device)
|
||||
.expect("reduced infrastructure node should produce an Ethernet advisory");
|
||||
|
||||
assert_eq!(advisory.target_kind, EthernetCapTargetKind::Node);
|
||||
assert_eq!(advisory.target_id, "uisp:device:ap-1");
|
||||
assert_eq!(advisory.negotiated_ethernet_mbps, 100);
|
||||
assert_eq!(advisory.requested_download_mbps, 500.0);
|
||||
assert_eq!(advisory.applied_upload_mbps, 94.0);
|
||||
assert_eq!(advisory.limiting_interface_name.as_deref(), Some("eth0"));
|
||||
}
|
||||
|
||||
fn add_site(graph: &mut GraphType, name: &str, id: &str) -> NodeIndex {
|
||||
graph.add_node(GraphMapping::Site {
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -34,6 +34,10 @@ pub struct UispDevice {
|
||||
pub probe_ipv6: HashSet<String>,
|
||||
pub negotiated_ethernet_mbps: Option<u64>,
|
||||
pub negotiated_ethernet_interface: Option<String>,
|
||||
/// Physical line rate selected when applying an infrastructure transport cap.
|
||||
pub transport_cap_line_rate_mbps: Option<u64>,
|
||||
/// Interface selected when applying an observed infrastructure transport cap.
|
||||
pub transport_cap_interface: Option<String>,
|
||||
pub transport_cap_mbps: Option<u64>,
|
||||
pub transport_cap_reason: Option<String>,
|
||||
pub attachment_rate_source: UispAttachmentRateSource,
|
||||
@@ -594,6 +598,8 @@ impl UispDevice {
|
||||
}
|
||||
let raw_download = download;
|
||||
let raw_upload = upload;
|
||||
let mut transport_cap_line_rate_mbps = None;
|
||||
let mut transport_cap_interface = None;
|
||||
let mut transport_cap_mbps = None;
|
||||
let mut transport_cap_reason = None;
|
||||
|
||||
@@ -714,19 +720,24 @@ impl UispDevice {
|
||||
.infrastructure_transport_caps_enabled
|
||||
{
|
||||
let policy = EthernetPortLimitPolicy::from(&config.integration_common);
|
||||
let observed_transport = Self::infrastructure_transport_observation(device);
|
||||
let (observed_line_rate, observed_interface, observed_reason) =
|
||||
Self::infrastructure_transport_observation(device);
|
||||
let model_transport = Self::model_transport_port_ceiling(device);
|
||||
let selected_transport = observed_transport
|
||||
.0
|
||||
.zip(observed_transport.2)
|
||||
.or(model_transport);
|
||||
let selected_transport = observed_line_rate
|
||||
.zip(observed_reason)
|
||||
.map(|(line_rate_mbps, reason)| (line_rate_mbps, observed_interface, reason))
|
||||
.or_else(|| {
|
||||
model_transport.map(|(line_rate_mbps, reason)| (line_rate_mbps, None, reason))
|
||||
});
|
||||
|
||||
if let Some((line_rate_mbps, reason)) = selected_transport
|
||||
if let Some((line_rate_mbps, interface, reason)) = selected_transport
|
||||
&& let Some(usable_cap_mbps) = usable_ethernet_cap_mbps(policy, line_rate_mbps)
|
||||
&& (download > usable_cap_mbps || upload > usable_cap_mbps)
|
||||
{
|
||||
download = download.min(usable_cap_mbps);
|
||||
upload = upload.min(usable_cap_mbps);
|
||||
transport_cap_line_rate_mbps = Some(line_rate_mbps);
|
||||
transport_cap_interface = interface;
|
||||
transport_cap_mbps = Some(usable_cap_mbps);
|
||||
transport_cap_reason = Some(reason);
|
||||
}
|
||||
@@ -752,6 +763,8 @@ impl UispDevice {
|
||||
probe_ipv6,
|
||||
negotiated_ethernet_mbps,
|
||||
negotiated_ethernet_interface,
|
||||
transport_cap_line_rate_mbps,
|
||||
transport_cap_interface,
|
||||
transport_cap_mbps,
|
||||
transport_cap_reason,
|
||||
attachment_rate_source,
|
||||
@@ -1117,6 +1130,8 @@ mod tests {
|
||||
assert_eq!(trimmed.raw_upload, 2000);
|
||||
assert_eq!(trimmed.download, 940);
|
||||
assert_eq!(trimmed.upload, 940);
|
||||
assert_eq!(trimmed.transport_cap_line_rate_mbps, Some(1000));
|
||||
assert_eq!(trimmed.transport_cap_interface, None);
|
||||
assert_eq!(trimmed.transport_cap_mbps, Some(940));
|
||||
assert!(
|
||||
trimmed
|
||||
@@ -1156,6 +1171,8 @@ mod tests {
|
||||
assert_eq!(trimmed.raw_upload, 2700);
|
||||
assert_eq!(trimmed.download, 2350);
|
||||
assert_eq!(trimmed.upload, 2350);
|
||||
assert_eq!(trimmed.transport_cap_line_rate_mbps, Some(2500));
|
||||
assert_eq!(trimmed.transport_cap_interface.as_deref(), Some("eth0"));
|
||||
assert_eq!(trimmed.transport_cap_mbps, Some(2350));
|
||||
assert!(
|
||||
trimmed
|
||||
@@ -1289,6 +1306,8 @@ mod tests {
|
||||
probe_ipv6: HashSet::new(),
|
||||
negotiated_ethernet_mbps: None,
|
||||
negotiated_ethernet_interface: None,
|
||||
transport_cap_line_rate_mbps: None,
|
||||
transport_cap_interface: None,
|
||||
transport_cap_mbps: None,
|
||||
transport_cap_reason: None,
|
||||
attachment_rate_source: super::UispAttachmentRateSource::Static,
|
||||
|
||||
Reference in New Issue
Block a user