This commit is contained in:
rchac committed 2026-06-12 16:04:18 -06:00
1 parent a2365ca78a
commit f8d3b0fc6a
7 files changed
+166 -56

No files matched your search

+10 -21
View File
@@ -3288,36 +3288,32 @@ dependencies = [
[[package]]
name = "pyo3"
version = "0.25.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a"
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
dependencies = [
"indoc",
"libc",
"memoffset",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.25.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598"
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.25.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c"
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
dependencies = [
"libc",
"pyo3-build-config",
@@ -3325,9 +3321,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.25.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50"
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -3337,13 +3333,12 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.25.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc"
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.117",
]
@@ -4742,12 +4737,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unindent"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]]
name = "universal-hash"
version = "0.5.1"
+1 -1
View File
@@ -79,7 +79,7 @@ sysinfo = { version = "0", default-features = false, features = [ "system" ] }
default-net = "0"
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls-no-provider", "charset", "http2", "system-proxy"] }
rustls = "0.23"
pyo3 = "0.25.1"
pyo3 = "0.29.0"
colored = "2"
miniz_oxide = "0.8"
byteorder = "1"
+1 -1
View File
@@ -37,7 +37,7 @@ pub struct DeviceWeightRequest {
/// This struct is used to receive a response from the Long Term Stats API
/// It contains the circuit_id and the weight of the device
#[derive(Serialize, Deserialize, Debug, Clone)]
#[pyclass]
#[pyclass(skip_from_py_object)]
pub struct DeviceWeightResponse {
#[pyo3(get)]
pub circuit_id: String,
+27 -25
View File
@@ -36,6 +36,8 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
type PyObject = Py<PyAny>;
// ===== Planner CBOR I/O =====
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
@@ -166,11 +168,11 @@ fn plan_top_level_cpu_bins(
hysteresis_threshold: Option<f64>,
) -> PyResult<PyObject> {
let items_any = items.bind(py);
let items_list = items_any.downcast::<PyList>()?;
let items_list = items_any.cast::<PyList>()?;
let planner_items: Vec<lqos_config::TopLevelPlannerItem> = items_list
.iter()
.map(|item| -> PyResult<lqos_config::TopLevelPlannerItem> {
let dict = item.downcast::<PyDict>()?;
let dict = item.cast::<PyDict>()?;
let id = get_string(dict, "id", String::new());
let weight = get_f64(dict, "weight", 1.0);
Ok(lqos_config::TopLevelPlannerItem { id, weight })
@@ -180,7 +182,7 @@ fn plan_top_level_cpu_bins(
let prev_assign_map = match prev_assign {
Some(obj) => {
let any = obj.bind(py);
let dict = any.downcast::<PyDict>()?;
let dict = any.cast::<PyDict>()?;
dict.iter()
.filter_map(|(k, v)| {
Some((k.extract::<String>().ok()?, v.extract::<String>().ok()?))
@@ -192,7 +194,7 @@ fn plan_top_level_cpu_bins(
let last_change_map = match last_change_ts {
Some(obj) => {
let any = obj.bind(py);
let dict = any.downcast::<PyDict>()?;
let dict = any.cast::<PyDict>()?;
dict.iter()
.filter_map(|(k, v)| Some((k.extract::<String>().ok()?, v.extract::<f64>().ok()?)))
.collect::<BTreeMap<_, _>>()
@@ -258,11 +260,11 @@ fn plan_class_identities(
stick_offset: u16,
circuit_padding: u32,
) -> PyResult<PyObject> {
let sites_list = sites.bind(py).downcast::<PyList>()?;
let sites_list = sites.bind(py).cast::<PyList>()?;
let site_inputs: Vec<lqos_config::SiteIdentityInput> = sites_list
.iter()
.map(|item| -> PyResult<lqos_config::SiteIdentityInput> {
let dict = item.downcast::<PyDict>()?;
let dict = item.cast::<PyDict>()?;
Ok(lqos_config::SiteIdentityInput {
site_key: get_string(dict, "site_key", String::new()),
parent_path: get_string(dict, "parent_path", String::new()),
@@ -272,14 +274,14 @@ fn plan_class_identities(
})
.collect::<PyResult<Vec<_>>>()?;
let groups_list = circuit_groups.bind(py).downcast::<PyList>()?;
let groups_list = circuit_groups.bind(py).cast::<PyList>()?;
let circuit_group_inputs: Vec<lqos_config::CircuitIdentityGroupInput> = groups_list
.iter()
.map(|item| -> PyResult<lqos_config::CircuitIdentityGroupInput> {
let dict = item.downcast::<PyDict>()?;
let dict = item.cast::<PyDict>()?;
let ids = match dict.get_item("circuit_ids")? {
Some(value) => value
.downcast::<PyList>()?
.cast::<PyList>()?
.iter()
.filter_map(|entry| entry.extract::<String>().ok())
.collect::<Vec<_>>(),
@@ -295,11 +297,11 @@ fn plan_class_identities(
let previous_sites = match site_state {
Some(obj) => {
let dict = obj.bind(py).downcast::<PyDict>()?;
let dict = obj.bind(py).cast::<PyDict>()?;
dict.iter()
.filter_map(|(k, v)| {
let key = k.extract::<String>().ok()?;
let entry = v.downcast::<PyDict>().ok()?;
let entry = v.cast::<PyDict>().ok()?;
Some((
key,
lqos_config::PlannerSiteIdentityState {
@@ -318,11 +320,11 @@ fn plan_class_identities(
let previous_circuits = match circuit_state {
Some(obj) => {
let dict = obj.bind(py).downcast::<PyDict>()?;
let dict = obj.bind(py).cast::<PyDict>()?;
dict.iter()
.filter_map(|(k, v)| {
let key = k.extract::<String>().ok()?;
let entry = v.downcast::<PyDict>().ok()?;
let entry = v.cast::<PyDict>().ok()?;
Some((
key,
lqos_config::PlannerCircuitIdentityState {
@@ -411,7 +413,7 @@ fn plan_class_identities(
fn write_planner_cbor(py: Python, path: String, state: PyObject) -> PyResult<bool> {
use std::fs;
use std::io::Write;
let dict = state.downcast_bound::<pyo3::types::PyDict>(py)?;
let dict = state.cast_bound::<pyo3::types::PyDict>(py)?;
// Build strongly typed struct, preserving integer keys
let algo_version = get_string(dict, "algo_version", default_algo_version());
let updated_at = get_f64(dict, "updated_at", 0.0);
@@ -420,7 +422,7 @@ fn write_planner_cbor(py: Python, path: String, state: PyObject) -> PyResult<boo
let site_count = get_i64(dict, "site_count", 0);
let mut site_names: Vec<i64> = Vec::new();
if let Ok(Some(sn)) = dict.get_item("site_names")
&& let Ok(list) = sn.downcast::<pyo3::types::PyList>()
&& let Ok(list) = sn.cast::<pyo3::types::PyList>()
{
for item in list.iter() {
if let Some(n) = to_i64_any(&item) {
@@ -431,11 +433,11 @@ fn write_planner_cbor(py: Python, path: String, state: PyObject) -> PyResult<boo
// site_map
let mut site_map: BTreeMap<i64, PlannerSiteEntry> = BTreeMap::new();
if let Ok(Some(sm_any)) = dict.get_item("site_map")
&& let Ok(sm_dict) = sm_any.downcast::<pyo3::types::PyDict>()
&& let Ok(sm_dict) = sm_any.cast::<pyo3::types::PyDict>()
{
for (k, v) in sm_dict.iter() {
if let Some(key) = to_i64_any(&k)
&& let Ok(entry) = v.downcast::<pyo3::types::PyDict>()
&& let Ok(entry) = v.cast::<pyo3::types::PyDict>()
{
let cpu = get_i64(entry, "cpu", 0);
let major = get_i64(entry, "major", 0);
@@ -459,11 +461,11 @@ fn write_planner_cbor(py: Python, path: String, state: PyObject) -> PyResult<boo
// circuit_map
let mut circuit_map: BTreeMap<i64, PlannerCircuitEntry> = BTreeMap::new();
if let Ok(Some(cm_any)) = dict.get_item("circuit_map")
&& let Ok(cm_dict) = cm_any.downcast::<pyo3::types::PyDict>()
&& let Ok(cm_dict) = cm_any.cast::<pyo3::types::PyDict>()
{
for (k, v) in cm_dict.iter() {
if let Some(key) = to_i64_any(&k)
&& let Ok(entry) = v.downcast::<pyo3::types::PyDict>()
&& let Ok(entry) = v.cast::<pyo3::types::PyDict>()
{
let cpu = get_i64(entry, "cpu", 0);
let major = get_i64(entry, "major", 0);
@@ -740,7 +742,7 @@ fn fetch_planner_remote(
#[pyfunction]
fn store_planner_remote(py: Python, state: PyObject) -> PyResult<bool> {
// Extract needed values and serialize as compressed CBOR
let dict = state.downcast_bound::<pyo3::types::PyDict>(py)?;
let dict = state.cast_bound::<pyo3::types::PyDict>(py)?;
let algo_version = get_string(dict, "algo_version", default_algo_version());
let updated_at = get_f64(dict, "updated_at", 0.0);
let queues_available = get_i64(dict, "queuesAvailable", 0);
@@ -749,7 +751,7 @@ fn store_planner_remote(py: Python, state: PyObject) -> PyResult<bool> {
// site_names
let mut site_names: Vec<i64> = Vec::new();
if let Ok(Some(sn)) = dict.get_item("site_names")
&& let Ok(list) = sn.downcast::<pyo3::types::PyList>()
&& let Ok(list) = sn.cast::<pyo3::types::PyList>()
{
for item in list.iter() {
if let Some(n) = to_i64_any(&item) {
@@ -760,11 +762,11 @@ fn store_planner_remote(py: Python, state: PyObject) -> PyResult<bool> {
// site_map
let mut site_map: BTreeMap<i64, PlannerSiteEntry> = BTreeMap::new();
if let Ok(Some(sm_any)) = dict.get_item("site_map")
&& let Ok(sm_dict) = sm_any.downcast::<pyo3::types::PyDict>()
&& let Ok(sm_dict) = sm_any.cast::<pyo3::types::PyDict>()
{
for (k, v) in sm_dict.iter() {
if let Some(key) = to_i64_any(&k)
&& let Ok(entry) = v.downcast::<pyo3::types::PyDict>()
&& let Ok(entry) = v.cast::<pyo3::types::PyDict>()
{
let cpu = get_i64(entry, "cpu", 0);
let major = get_i64(entry, "major", 0);
@@ -788,11 +790,11 @@ fn store_planner_remote(py: Python, state: PyObject) -> PyResult<bool> {
// circuit_map
let mut circuit_map: BTreeMap<i64, PlannerCircuitEntry> = BTreeMap::new();
if let Ok(Some(cm_any)) = dict.get_item("circuit_map")
&& let Ok(cm_dict) = cm_any.downcast::<pyo3::types::PyDict>()
&& let Ok(cm_dict) = cm_any.cast::<pyo3::types::PyDict>()
{
for (k, v) in cm_dict.iter() {
if let Some(key) = to_i64_any(&k)
&& let Ok(entry) = v.downcast::<pyo3::types::PyDict>()
&& let Ok(entry) = v.cast::<pyo3::types::PyDict>()
{
let cpu = get_i64(entry, "cpu", 0);
let major = get_i64(entry, "major", 0);
+38 -4
View File
@@ -2757,12 +2757,16 @@ fn resolved_auto_queue_visibility_policy(
if !queue_auto_node_kind_can_hide(tree_node) {
return TopologyQueueVisibilityPolicy::QueueVisible;
}
if child_branch_counts
let logical_child_count = child_branch_counts
.get(ui_node.node_id.as_str())
.copied()
.unwrap_or_default()
== 0
{
.unwrap_or_default();
let effective_child_count = tree_node
.get("children")
.and_then(Value::as_object)
.map(Map::len)
.unwrap_or_default();
if logical_child_count == 0 && effective_child_count == 0 {
return TopologyQueueVisibilityPolicy::QueueVisible;
}
let threshold = config.topology.queue_auto_virtualize_threshold_mbps;
@@ -7036,6 +7040,36 @@ mod tests {
assert!(aggregation_children.get("Access AP").is_some());
}
#[test]
fn queue_auto_marks_large_ap_branch_virtual_with_effective_tree_children() {
let (config, canonical, mut editor_state, effective) = ap_branch_fixture();
let access_ap = editor_state
.nodes
.iter_mut()
.find(|node| node.node_id == "ap-child")
.expect("fixture should include Access AP");
access_ap.current_parent_node_id = Some("site-root".to_string());
access_ap.current_parent_node_name = Some("Core".to_string());
let effective_network = apply_effective_topology_to_network_json(
&config,
&canonical,
&editor_state,
&effective,
);
let root = effective_network
.as_object()
.expect("effective export should remain an object tree");
let aggregation = root["Core"]["children"]["Aggregation Switch"]
.as_object()
.expect("Aggregation Switch should remain visible as a virtual node");
assert_eq!(
aggregation.get("virtual").and_then(Value::as_bool),
Some(true)
);
}
#[test]
fn queue_auto_keeps_large_ap_branch_visible_with_direct_circuit() {
let (config, canonical, editor_state, effective) = ap_branch_fixture();
+54 -2
View File
@@ -175,6 +175,15 @@ apt_candidate_has_hotfix_repo() {
-v version="$version" \
-v repo_url="$repo_url" \
-v dist_component="${HOTFIX_REPO_DIST}/${HOTFIX_REPO_COMPONENT}" '
function normalized_url(url) {
sub(/\/+$/, "", url)
return url
}
BEGIN {
repo_url = normalized_url(repo_url)
}
($1 == "***" && $2 == version) || ($1 == version && $2 ~ /^[0-9]+$/) {
in_version = 1
next
@@ -184,7 +193,7 @@ apt_candidate_has_hotfix_repo() {
in_version = 0
}
in_version && $1 ~ /^[0-9]+$/ && $2 == repo_url && $3 == dist_component {
in_version && $1 ~ /^[0-9]+$/ && normalized_url($2) == repo_url && $3 == dist_component {
found = 1
exit
}
@@ -193,6 +202,47 @@ apt_candidate_has_hotfix_repo() {
'
}
apt_madison_has_hotfix_repo() {
local package="$1"
local version="$2"
local repo_url="${HOTFIX_REPO_URL%/}"
LC_ALL=C apt-cache madison "$package" | awk \
-F '|' \
-v version="$version" \
-v repo_url="$repo_url" \
-v dist_component="${HOTFIX_REPO_DIST}/${HOTFIX_REPO_COMPONENT}" '
function trim(text) {
gsub(/^[ \t]+|[ \t]+$/, "", text)
return text
}
function normalized_url(url) {
sub(/\/+$/, "", url)
return url
}
BEGIN {
repo_url = normalized_url(repo_url)
}
{
candidate_version = trim($2)
source = trim($3)
split(source, source_parts, /[ \t]+/)
if (candidate_version == version &&
normalized_url(source_parts[1]) == repo_url &&
source_parts[2] == dist_component) {
found = 1
exit
}
}
END { if (!found) exit 1 }
'
}
resolve_hotfix_package_version() {
local package version priority resolved_version
@@ -211,7 +261,9 @@ resolve_hotfix_package_version() {
[[ "$priority" =~ ^[0-9]+$ ]] || fail "Unable to verify LibreQoS APT pin priority for $package=$version."
(( priority >= 1001 )) || fail "APT candidate for $package=$version is not pinned from the LibreQoS hotfix repo."
# apt-cache reports the pin on the version line; the matching source line can remain at archive priority 500.
apt_candidate_has_hotfix_repo "$package" "$version" || fail "APT candidate for $package=$version is not from $HOTFIX_REPO_URL $HOTFIX_REPO_DIST/$HOTFIX_REPO_COMPONENT."
apt_candidate_has_hotfix_repo "$package" "$version" || \
apt_madison_has_hotfix_repo "$package" "$version" || \
fail "APT candidate for $package=$version is not from $HOTFIX_REPO_URL $HOTFIX_REPO_DIST/$HOTFIX_REPO_COMPONENT."
if [[ -z "${resolved_version:-}" ]]; then
resolved_version="$version"
+35 -2
View File
@@ -77,10 +77,13 @@ class SystemdHotfixScriptTest(unittest.TestCase):
#!/bin/sh
[ "${HOTFIX_TEST_APT_CACHE_FAIL:-0}" = "1" ] && exit 12
command="$1"
package="$2"
version="${HOTFIX_TEST_CANDIDATE_VERSION:-255.4-1ubuntu9999+libreqos1}"
priority="${HOTFIX_TEST_CANDIDATE_PRIORITY:-1001}"
repo_line="${HOTFIX_TEST_REPO_LINE:- 500 https://repo.libreqos.com noble/main amd64 Packages}"
madison_repo_url="${HOTFIX_TEST_MADISON_REPO_URL:-https://repo.libreqos.com}"
madison_dist_component="${HOTFIX_TEST_MADISON_DIST_COMPONENT:-noble/main}"
case ",${HOTFIX_TEST_INCONSISTENT_PACKAGES:-}," in
*,"$package",*) version="${HOTFIX_TEST_OTHER_VERSION:-255.4-1ubuntu9999+libreqos2}" ;;
@@ -90,6 +93,11 @@ class SystemdHotfixScriptTest(unittest.TestCase):
version="(none)"
fi
if [ "$command" = "madison" ]; then
printf ' %s | %s | %s %s amd64 Packages\\n' "$package" "$version" "$madison_repo_url" "$madison_dist_component"
exit 0
fi
cat <<POLICY
$package:
Installed: 255.4-1ubuntu8.16
@@ -161,6 +169,29 @@ class SystemdHotfixScriptTest(unittest.TestCase):
self.assertNotIn("libnss-resolve=", apt_log)
self.assertIn("package_version=255.4-1ubuntu9999+libreqos1", marker)
def test_auto_accepts_policy_repo_url_with_trailing_slash(self):
result, apt_log, marker = self.run_install(
{
"HOTFIX_TEST_REPO_LINE": (
" 500 https://repo.libreqos.com/ noble/main amd64 Packages"
),
"HOTFIX_TEST_MADISON_REPO_URL": "https://example.invalid",
}
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("systemd=255.4-1ubuntu9999+libreqos1", apt_log)
self.assertIn("package_version=255.4-1ubuntu9999+libreqos1", marker)
def test_auto_uses_madison_when_policy_source_line_is_unrecognized(self):
result, apt_log, marker = self.run_install(
{"HOTFIX_TEST_REPO_LINE": " release o=LibreQoS,l=LibreQoS,n=noble"}
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("systemd=255.4-1ubuntu9999+libreqos1", apt_log)
self.assertIn("package_version=255.4-1ubuntu9999+libreqos1", marker)
def test_auto_rejects_ubuntu_candidate(self):
result, _, _ = self.run_install(
{"HOTFIX_TEST_CANDIDATE_VERSION": "255.4-1ubuntu8.16"}
@@ -198,7 +229,8 @@ class SystemdHotfixScriptTest(unittest.TestCase):
{
"HOTFIX_TEST_REPO_LINE": (
" 500 https://example.invalid noble/main amd64 Packages"
)
),
"HOTFIX_TEST_MADISON_REPO_URL": "https://example.invalid",
}
)
@@ -210,7 +242,8 @@ class SystemdHotfixScriptTest(unittest.TestCase):
{
"HOTFIX_TEST_REPO_LINE": (
" 500 https://repo.libreqos.com.evil noble/main amd64 Packages"
)
),
"HOTFIX_TEST_MADISON_REPO_URL": "https://repo.libreqos.com.evil",
}
)