Skip to content

Commit f8d3b0f

Browse files
author
rchac
committed
changes
1 parent a2365ca commit f8d3b0f

7 files changed

Lines changed: 166 additions & 56 deletions

File tree

src/rust/Cargo.lock

Lines changed: 10 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ sysinfo = { version = "0", default-features = false, features = [ "system" ] }
7979
default-net = "0"
8080
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls-no-provider", "charset", "http2", "system-proxy"] }
8181
rustls = "0.23"
82-
pyo3 = "0.25.1"
82+
pyo3 = "0.29.0"
8383
colored = "2"
8484
miniz_oxide = "0.8"
8585
byteorder = "1"

src/rust/lqos_python/src/device_weights.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub struct DeviceWeightRequest {
3737
/// This struct is used to receive a response from the Long Term Stats API
3838
/// It contains the circuit_id and the weight of the device
3939
#[derive(Serialize, Deserialize, Debug, Clone)]
40-
#[pyclass]
40+
#[pyclass(skip_from_py_object)]
4141
pub struct DeviceWeightResponse {
4242
#[pyo3(get)]
4343
pub circuit_id: String,

src/rust/lqos_python/src/lib.rs

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ use serde::{Deserialize, Serialize};
3636
use std::collections::BTreeMap;
3737
use std::time::{Duration, SystemTime, UNIX_EPOCH};
3838

39+
type PyObject = Py<PyAny>;
40+
3941
// ===== Planner CBOR I/O =====
4042

4143
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
@@ -166,11 +168,11 @@ fn plan_top_level_cpu_bins(
166168
hysteresis_threshold: Option<f64>,
167169
) -> PyResult<PyObject> {
168170
let items_any = items.bind(py);
169-
let items_list = items_any.downcast::<PyList>()?;
171+
let items_list = items_any.cast::<PyList>()?;
170172
let planner_items: Vec<lqos_config::TopLevelPlannerItem> = items_list
171173
.iter()
172174
.map(|item| -> PyResult<lqos_config::TopLevelPlannerItem> {
173-
let dict = item.downcast::<PyDict>()?;
175+
let dict = item.cast::<PyDict>()?;
174176
let id = get_string(dict, "id", String::new());
175177
let weight = get_f64(dict, "weight", 1.0);
176178
Ok(lqos_config::TopLevelPlannerItem { id, weight })
@@ -180,7 +182,7 @@ fn plan_top_level_cpu_bins(
180182
let prev_assign_map = match prev_assign {
181183
Some(obj) => {
182184
let any = obj.bind(py);
183-
let dict = any.downcast::<PyDict>()?;
185+
let dict = any.cast::<PyDict>()?;
184186
dict.iter()
185187
.filter_map(|(k, v)| {
186188
Some((k.extract::<String>().ok()?, v.extract::<String>().ok()?))
@@ -192,7 +194,7 @@ fn plan_top_level_cpu_bins(
192194
let last_change_map = match last_change_ts {
193195
Some(obj) => {
194196
let any = obj.bind(py);
195-
let dict = any.downcast::<PyDict>()?;
197+
let dict = any.cast::<PyDict>()?;
196198
dict.iter()
197199
.filter_map(|(k, v)| Some((k.extract::<String>().ok()?, v.extract::<f64>().ok()?)))
198200
.collect::<BTreeMap<_, _>>()
@@ -258,11 +260,11 @@ fn plan_class_identities(
258260
stick_offset: u16,
259261
circuit_padding: u32,
260262
) -> PyResult<PyObject> {
261-
let sites_list = sites.bind(py).downcast::<PyList>()?;
263+
let sites_list = sites.bind(py).cast::<PyList>()?;
262264
let site_inputs: Vec<lqos_config::SiteIdentityInput> = sites_list
263265
.iter()
264266
.map(|item| -> PyResult<lqos_config::SiteIdentityInput> {
265-
let dict = item.downcast::<PyDict>()?;
267+
let dict = item.cast::<PyDict>()?;
266268
Ok(lqos_config::SiteIdentityInput {
267269
site_key: get_string(dict, "site_key", String::new()),
268270
parent_path: get_string(dict, "parent_path", String::new()),
@@ -272,14 +274,14 @@ fn plan_class_identities(
272274
})
273275
.collect::<PyResult<Vec<_>>>()?;
274276

275-
let groups_list = circuit_groups.bind(py).downcast::<PyList>()?;
277+
let groups_list = circuit_groups.bind(py).cast::<PyList>()?;
276278
let circuit_group_inputs: Vec<lqos_config::CircuitIdentityGroupInput> = groups_list
277279
.iter()
278280
.map(|item| -> PyResult<lqos_config::CircuitIdentityGroupInput> {
279-
let dict = item.downcast::<PyDict>()?;
281+
let dict = item.cast::<PyDict>()?;
280282
let ids = match dict.get_item("circuit_ids")? {
281283
Some(value) => value
282-
.downcast::<PyList>()?
284+
.cast::<PyList>()?
283285
.iter()
284286
.filter_map(|entry| entry.extract::<String>().ok())
285287
.collect::<Vec<_>>(),
@@ -295,11 +297,11 @@ fn plan_class_identities(
295297

296298
let previous_sites = match site_state {
297299
Some(obj) => {
298-
let dict = obj.bind(py).downcast::<PyDict>()?;
300+
let dict = obj.bind(py).cast::<PyDict>()?;
299301
dict.iter()
300302
.filter_map(|(k, v)| {
301303
let key = k.extract::<String>().ok()?;
302-
let entry = v.downcast::<PyDict>().ok()?;
304+
let entry = v.cast::<PyDict>().ok()?;
303305
Some((
304306
key,
305307
lqos_config::PlannerSiteIdentityState {
@@ -318,11 +320,11 @@ fn plan_class_identities(
318320

319321
let previous_circuits = match circuit_state {
320322
Some(obj) => {
321-
let dict = obj.bind(py).downcast::<PyDict>()?;
323+
let dict = obj.bind(py).cast::<PyDict>()?;
322324
dict.iter()
323325
.filter_map(|(k, v)| {
324326
let key = k.extract::<String>().ok()?;
325-
let entry = v.downcast::<PyDict>().ok()?;
327+
let entry = v.cast::<PyDict>().ok()?;
326328
Some((
327329
key,
328330
lqos_config::PlannerCircuitIdentityState {
@@ -411,7 +413,7 @@ fn plan_class_identities(
411413
fn write_planner_cbor(py: Python, path: String, state: PyObject) -> PyResult<bool> {
412414
use std::fs;
413415
use std::io::Write;
414-
let dict = state.downcast_bound::<pyo3::types::PyDict>(py)?;
416+
let dict = state.cast_bound::<pyo3::types::PyDict>(py)?;
415417
// Build strongly typed struct, preserving integer keys
416418
let algo_version = get_string(dict, "algo_version", default_algo_version());
417419
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
420422
let site_count = get_i64(dict, "site_count", 0);
421423
let mut site_names: Vec<i64> = Vec::new();
422424
if let Ok(Some(sn)) = dict.get_item("site_names")
423-
&& let Ok(list) = sn.downcast::<pyo3::types::PyList>()
425+
&& let Ok(list) = sn.cast::<pyo3::types::PyList>()
424426
{
425427
for item in list.iter() {
426428
if let Some(n) = to_i64_any(&item) {
@@ -431,11 +433,11 @@ fn write_planner_cbor(py: Python, path: String, state: PyObject) -> PyResult<boo
431433
// site_map
432434
let mut site_map: BTreeMap<i64, PlannerSiteEntry> = BTreeMap::new();
433435
if let Ok(Some(sm_any)) = dict.get_item("site_map")
434-
&& let Ok(sm_dict) = sm_any.downcast::<pyo3::types::PyDict>()
436+
&& let Ok(sm_dict) = sm_any.cast::<pyo3::types::PyDict>()
435437
{
436438
for (k, v) in sm_dict.iter() {
437439
if let Some(key) = to_i64_any(&k)
438-
&& let Ok(entry) = v.downcast::<pyo3::types::PyDict>()
440+
&& let Ok(entry) = v.cast::<pyo3::types::PyDict>()
439441
{
440442
let cpu = get_i64(entry, "cpu", 0);
441443
let major = get_i64(entry, "major", 0);
@@ -459,11 +461,11 @@ fn write_planner_cbor(py: Python, path: String, state: PyObject) -> PyResult<boo
459461
// circuit_map
460462
let mut circuit_map: BTreeMap<i64, PlannerCircuitEntry> = BTreeMap::new();
461463
if let Ok(Some(cm_any)) = dict.get_item("circuit_map")
462-
&& let Ok(cm_dict) = cm_any.downcast::<pyo3::types::PyDict>()
464+
&& let Ok(cm_dict) = cm_any.cast::<pyo3::types::PyDict>()
463465
{
464466
for (k, v) in cm_dict.iter() {
465467
if let Some(key) = to_i64_any(&k)
466-
&& let Ok(entry) = v.downcast::<pyo3::types::PyDict>()
468+
&& let Ok(entry) = v.cast::<pyo3::types::PyDict>()
467469
{
468470
let cpu = get_i64(entry, "cpu", 0);
469471
let major = get_i64(entry, "major", 0);
@@ -740,7 +742,7 @@ fn fetch_planner_remote(
740742
#[pyfunction]
741743
fn store_planner_remote(py: Python, state: PyObject) -> PyResult<bool> {
742744
// Extract needed values and serialize as compressed CBOR
743-
let dict = state.downcast_bound::<pyo3::types::PyDict>(py)?;
745+
let dict = state.cast_bound::<pyo3::types::PyDict>(py)?;
744746
let algo_version = get_string(dict, "algo_version", default_algo_version());
745747
let updated_at = get_f64(dict, "updated_at", 0.0);
746748
let queues_available = get_i64(dict, "queuesAvailable", 0);
@@ -749,7 +751,7 @@ fn store_planner_remote(py: Python, state: PyObject) -> PyResult<bool> {
749751
// site_names
750752
let mut site_names: Vec<i64> = Vec::new();
751753
if let Ok(Some(sn)) = dict.get_item("site_names")
752-
&& let Ok(list) = sn.downcast::<pyo3::types::PyList>()
754+
&& let Ok(list) = sn.cast::<pyo3::types::PyList>()
753755
{
754756
for item in list.iter() {
755757
if let Some(n) = to_i64_any(&item) {
@@ -760,11 +762,11 @@ fn store_planner_remote(py: Python, state: PyObject) -> PyResult<bool> {
760762
// site_map
761763
let mut site_map: BTreeMap<i64, PlannerSiteEntry> = BTreeMap::new();
762764
if let Ok(Some(sm_any)) = dict.get_item("site_map")
763-
&& let Ok(sm_dict) = sm_any.downcast::<pyo3::types::PyDict>()
765+
&& let Ok(sm_dict) = sm_any.cast::<pyo3::types::PyDict>()
764766
{
765767
for (k, v) in sm_dict.iter() {
766768
if let Some(key) = to_i64_any(&k)
767-
&& let Ok(entry) = v.downcast::<pyo3::types::PyDict>()
769+
&& let Ok(entry) = v.cast::<pyo3::types::PyDict>()
768770
{
769771
let cpu = get_i64(entry, "cpu", 0);
770772
let major = get_i64(entry, "major", 0);
@@ -788,11 +790,11 @@ fn store_planner_remote(py: Python, state: PyObject) -> PyResult<bool> {
788790
// circuit_map
789791
let mut circuit_map: BTreeMap<i64, PlannerCircuitEntry> = BTreeMap::new();
790792
if let Ok(Some(cm_any)) = dict.get_item("circuit_map")
791-
&& let Ok(cm_dict) = cm_any.downcast::<pyo3::types::PyDict>()
793+
&& let Ok(cm_dict) = cm_any.cast::<pyo3::types::PyDict>()
792794
{
793795
for (k, v) in cm_dict.iter() {
794796
if let Some(key) = to_i64_any(&k)
795-
&& let Ok(entry) = v.downcast::<pyo3::types::PyDict>()
797+
&& let Ok(entry) = v.cast::<pyo3::types::PyDict>()
796798
{
797799
let cpu = get_i64(entry, "cpu", 0);
798800
let major = get_i64(entry, "major", 0);

src/rust/lqos_topology/src/lib.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2757,12 +2757,16 @@ fn resolved_auto_queue_visibility_policy(
27572757
if !queue_auto_node_kind_can_hide(tree_node) {
27582758
return TopologyQueueVisibilityPolicy::QueueVisible;
27592759
}
2760-
if child_branch_counts
2760+
let logical_child_count = child_branch_counts
27612761
.get(ui_node.node_id.as_str())
27622762
.copied()
2763-
.unwrap_or_default()
2764-
== 0
2765-
{
2763+
.unwrap_or_default();
2764+
let effective_child_count = tree_node
2765+
.get("children")
2766+
.and_then(Value::as_object)
2767+
.map(Map::len)
2768+
.unwrap_or_default();
2769+
if logical_child_count == 0 && effective_child_count == 0 {
27662770
return TopologyQueueVisibilityPolicy::QueueVisible;
27672771
}
27682772
let threshold = config.topology.queue_auto_virtualize_threshold_mbps;
@@ -7036,6 +7040,36 @@ mod tests {
70367040
assert!(aggregation_children.get("Access AP").is_some());
70377041
}
70387042

7043+
#[test]
7044+
fn queue_auto_marks_large_ap_branch_virtual_with_effective_tree_children() {
7045+
let (config, canonical, mut editor_state, effective) = ap_branch_fixture();
7046+
let access_ap = editor_state
7047+
.nodes
7048+
.iter_mut()
7049+
.find(|node| node.node_id == "ap-child")
7050+
.expect("fixture should include Access AP");
7051+
access_ap.current_parent_node_id = Some("site-root".to_string());
7052+
access_ap.current_parent_node_name = Some("Core".to_string());
7053+
7054+
let effective_network = apply_effective_topology_to_network_json(
7055+
&config,
7056+
&canonical,
7057+
&editor_state,
7058+
&effective,
7059+
);
7060+
let root = effective_network
7061+
.as_object()
7062+
.expect("effective export should remain an object tree");
7063+
let aggregation = root["Core"]["children"]["Aggregation Switch"]
7064+
.as_object()
7065+
.expect("Aggregation Switch should remain visible as a virtual node");
7066+
7067+
assert_eq!(
7068+
aggregation.get("virtual").and_then(Value::as_bool),
7069+
Some(true)
7070+
);
7071+
}
7072+
70397073
#[test]
70407074
fn queue_auto_keeps_large_ap_branch_visible_with_direct_circuit() {
70417075
let (config, canonical, editor_state, effective) = ap_branch_fixture();

0 commit comments

Comments
 (0)