From f8d3b0fc6a2c413d19fffb578dcb297e58a5da90 Mon Sep 17 00:00:00 2001 From: rchac Date: Fri, 12 Jun 2026 16:04:18 -0600 Subject: [PATCH] changes --- src/rust/Cargo.lock | 31 ++++-------- src/rust/Cargo.toml | 2 +- src/rust/lqos_python/src/device_weights.rs | 2 +- src/rust/lqos_python/src/lib.rs | 52 ++++++++++---------- src/rust/lqos_topology/src/lib.rs | 42 ++++++++++++++-- src/systemd_hotfix.sh | 56 +++++++++++++++++++++- src/test_systemd_hotfix.py | 37 +++++++++++++- 7 files changed, 166 insertions(+), 56 deletions(-) diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 9d4352c8..e669ade8 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -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" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index f2a0f51d..9e068f37 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -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" diff --git a/src/rust/lqos_python/src/device_weights.rs b/src/rust/lqos_python/src/device_weights.rs index 7d0076ae..49e7d1f8 100644 --- a/src/rust/lqos_python/src/device_weights.rs +++ b/src/rust/lqos_python/src/device_weights.rs @@ -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, diff --git a/src/rust/lqos_python/src/lib.rs b/src/rust/lqos_python/src/lib.rs index ea46c9bc..0a966822 100644 --- a/src/rust/lqos_python/src/lib.rs +++ b/src/rust/lqos_python/src/lib.rs @@ -36,6 +36,8 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +type PyObject = Py; + // ===== Planner CBOR I/O ===== #[derive(Serialize, Deserialize, Clone, Debug, Default)] @@ -166,11 +168,11 @@ fn plan_top_level_cpu_bins( hysteresis_threshold: Option, ) -> PyResult { let items_any = items.bind(py); - let items_list = items_any.downcast::()?; + let items_list = items_any.cast::()?; let planner_items: Vec = items_list .iter() .map(|item| -> PyResult { - let dict = item.downcast::()?; + let dict = item.cast::()?; 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::()?; + let dict = any.cast::()?; dict.iter() .filter_map(|(k, v)| { Some((k.extract::().ok()?, v.extract::().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::()?; + let dict = any.cast::()?; dict.iter() .filter_map(|(k, v)| Some((k.extract::().ok()?, v.extract::().ok()?))) .collect::>() @@ -258,11 +260,11 @@ fn plan_class_identities( stick_offset: u16, circuit_padding: u32, ) -> PyResult { - let sites_list = sites.bind(py).downcast::()?; + let sites_list = sites.bind(py).cast::()?; let site_inputs: Vec = sites_list .iter() .map(|item| -> PyResult { - let dict = item.downcast::()?; + let dict = item.cast::()?; 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::>>()?; - let groups_list = circuit_groups.bind(py).downcast::()?; + let groups_list = circuit_groups.bind(py).cast::()?; let circuit_group_inputs: Vec = groups_list .iter() .map(|item| -> PyResult { - let dict = item.downcast::()?; + let dict = item.cast::()?; let ids = match dict.get_item("circuit_ids")? { Some(value) => value - .downcast::()? + .cast::()? .iter() .filter_map(|entry| entry.extract::().ok()) .collect::>(), @@ -295,11 +297,11 @@ fn plan_class_identities( let previous_sites = match site_state { Some(obj) => { - let dict = obj.bind(py).downcast::()?; + let dict = obj.bind(py).cast::()?; dict.iter() .filter_map(|(k, v)| { let key = k.extract::().ok()?; - let entry = v.downcast::().ok()?; + let entry = v.cast::().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::()?; + let dict = obj.bind(py).cast::()?; dict.iter() .filter_map(|(k, v)| { let key = k.extract::().ok()?; - let entry = v.downcast::().ok()?; + let entry = v.cast::().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 { use std::fs; use std::io::Write; - let dict = state.downcast_bound::(py)?; + let dict = state.cast_bound::(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 = Vec::new(); if let Ok(Some(sn)) = dict.get_item("site_names") - && let Ok(list) = sn.downcast::() + && let Ok(list) = sn.cast::() { 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 = BTreeMap::new(); if let Ok(Some(sm_any)) = dict.get_item("site_map") - && let Ok(sm_dict) = sm_any.downcast::() + && let Ok(sm_dict) = sm_any.cast::() { for (k, v) in sm_dict.iter() { if let Some(key) = to_i64_any(&k) - && let Ok(entry) = v.downcast::() + && let Ok(entry) = v.cast::() { 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 = BTreeMap::new(); if let Ok(Some(cm_any)) = dict.get_item("circuit_map") - && let Ok(cm_dict) = cm_any.downcast::() + && let Ok(cm_dict) = cm_any.cast::() { for (k, v) in cm_dict.iter() { if let Some(key) = to_i64_any(&k) - && let Ok(entry) = v.downcast::() + && let Ok(entry) = v.cast::() { 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 { // Extract needed values and serialize as compressed CBOR - let dict = state.downcast_bound::(py)?; + let dict = state.cast_bound::(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 { // site_names let mut site_names: Vec = Vec::new(); if let Ok(Some(sn)) = dict.get_item("site_names") - && let Ok(list) = sn.downcast::() + && let Ok(list) = sn.cast::() { 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 { // site_map let mut site_map: BTreeMap = BTreeMap::new(); if let Ok(Some(sm_any)) = dict.get_item("site_map") - && let Ok(sm_dict) = sm_any.downcast::() + && let Ok(sm_dict) = sm_any.cast::() { for (k, v) in sm_dict.iter() { if let Some(key) = to_i64_any(&k) - && let Ok(entry) = v.downcast::() + && let Ok(entry) = v.cast::() { 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 { // circuit_map let mut circuit_map: BTreeMap = BTreeMap::new(); if let Ok(Some(cm_any)) = dict.get_item("circuit_map") - && let Ok(cm_dict) = cm_any.downcast::() + && let Ok(cm_dict) = cm_any.cast::() { for (k, v) in cm_dict.iter() { if let Some(key) = to_i64_any(&k) - && let Ok(entry) = v.downcast::() + && let Ok(entry) = v.cast::() { let cpu = get_i64(entry, "cpu", 0); let major = get_i64(entry, "major", 0); diff --git a/src/rust/lqos_topology/src/lib.rs b/src/rust/lqos_topology/src/lib.rs index 75068a5d..884afc81 100644 --- a/src/rust/lqos_topology/src/lib.rs +++ b/src/rust/lqos_topology/src/lib.rs @@ -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(); diff --git a/src/systemd_hotfix.sh b/src/systemd_hotfix.sh index dbd09134..570a0682 100755 --- a/src/systemd_hotfix.sh +++ b/src/systemd_hotfix.sh @@ -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" diff --git a/src/test_systemd_hotfix.py b/src/test_systemd_hotfix.py index 433efc31..c65b1b8d 100644 --- a/src/test_systemd_hotfix.py +++ b/src/test_systemd_hotfix.py @@ -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 <