mikronova/kyokkou
src/rack_topology.rs
use crate::common::{Scene, ValidationStatus, render_button, show_validation};
#[derive(Debug, Clone, PartialEq)]
pub enum UnitType {
Server1U,
Server2U,
Server4U,
Switch,
PatchPanel,
Pdu,
BlankingPanel,
}
#[derive(Debug, Clone)]
pub struct RackUnit {
pub slot: u8, // 1-based U position from bottom
pub size: u8, // how many U it occupies
pub kind: UnitType,
pub label: String,
pub cpu_load: f32, // 0.0–1.0
pub net_utilisation: f32,
pub power_draw_w: f32,
}
pub struct RackTopology {
pub rack_u_total: u8,
pub units: Vec<RackUnit>,
pub pdu_capacity_w: f32,
pub import_source: String,
}
impl Default for RackTopology {
fn default() -> Self {
Self {
rack_u_total: 42,
units: vec![
RackUnit { slot: 1, size: 1, kind: UnitType::Switch, label: "core-sw-01".into(), cpu_load: 0.2, net_utilisation: 0.6, power_draw_w: 80.0 },
RackUnit { slot: 2, size: 2, kind: UnitType::Server2U, label: "srv-01".into(), cpu_load: 0.7, net_utilisation: 0.4, power_draw_w: 350.0 },
RackUnit { slot: 4, size: 1, kind: UnitType::Server1U, label: "srv-02".into(), cpu_load: 0.3, net_utilisation: 0.2, power_draw_w: 180.0 },
],
pdu_capacity_w: 3200.0,
import_source: String::new(),
}
}
}
impl RackTopology {
fn total_power_draw(&self) -> f32 {
self.units.iter().map(|u| u.power_draw_w).sum()
}
fn slot_conflicts(&self) -> Vec<(u8, u8)> {
let mut occupied: Vec<(u8, u8)> = vec![];
let mut conflicts = vec![];
for unit in &self.units {
for u in unit.slot..unit.slot + unit.size {
if occupied.iter().any(|(s, _)| *s == u) {
conflicts.push((unit.slot, u));
}
occupied.push((u, unit.slot));
}
}
conflicts
}
}
impl Scene for RackTopology {
fn name(&self) -> &str {
"Rack topology"
}
fn validate(&self) -> Vec<ValidationStatus> {
let mut v = vec![];
let conflicts = self.slot_conflicts();
if !conflicts.is_empty() {
v.push(ValidationStatus::Error(format!(
"{} U slot conflict(s) — cannot render",
conflicts.len()
)));
}
let draw = self.total_power_draw();
if draw > self.pdu_capacity_w {
v.push(ValidationStatus::Error(format!(
"Power budget exceeded: {draw:.0} W > {:.0} W PDU capacity",
self.pdu_capacity_w
)));
} else if draw > self.pdu_capacity_w * 0.9 {
v.push(ValidationStatus::Warning(format!(
"Power draw at {:.0}% of PDU capacity",
draw / self.pdu_capacity_w * 100.0
)));
}
let cable_count = self.units.len(); // placeholder
if cable_count > 100 {
v.push(ValidationStatus::Warning(
"Cable count > 100 — runs will be batched into trunks".into(),
));
}
if self.units.is_empty() {
v.push(ValidationStatus::Error("Rack is empty".into()));
}
if v.is_empty() {
v.push(ValidationStatus::Ok);
}
v
}
fn show(&mut self, ui: &mut egui::Ui) -> Option<String> {
ui.heading("Rack topology");
ui.separator();
ui.horizontal(|ui| {
ui.label("Rack size (U):");
ui.add(egui::Slider::new(&mut self.rack_u_total, 4..=52));
});
ui.horizontal(|ui| {
ui.label("PDU capacity (W):");
ui.add(egui::Slider::new(&mut self.pdu_capacity_w, 1000.0..=20000.0).step_by(100.0));
});
ui.horizontal(|ui| {
ui.label("Import (Netbox JSON / CSV):");
ui.text_edit_singleline(&mut self.import_source);
if ui.button("Load").clicked() {
// TODO: parse import_source and populate units
}
});
ui.separator();
ui.label(format!(
"{} units | {:.0} W / {:.0} W ({:.0}%)",
self.units.len(),
self.total_power_draw(),
self.pdu_capacity_w,
self.total_power_draw() / self.pdu_capacity_w * 100.0,
));
// Rack diagram — side-on U view
egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| {
egui::Grid::new("rack_grid").striped(true).show(ui, |ui| {
ui.label("U");
ui.label("Type");
ui.label("Label");
ui.label("CPU");
ui.label("Net");
ui.label("Power W");
ui.end_row();
for unit in &self.units {
ui.label(format!("{}–{}", unit.slot, unit.slot + unit.size - 1));
ui.label(format!("{:?}", unit.kind));
ui.label(&unit.label);
ui.add(egui::ProgressBar::new(unit.cpu_load).desired_width(60.0));
ui.add(egui::ProgressBar::new(unit.net_utilisation).desired_width(60.0));
ui.label(format!("{:.0}", unit.power_draw_w));
ui.end_row();
}
});
});
ui.separator();
let statuses = self.validate();
show_validation(ui, &statuses);
ui.separator();
if render_button(ui, self.is_renderable()) {
// TODO: to_yaml() for rack topology
}
None
}
}