584 lines
17 KiB
Rust
584 lines
17 KiB
Rust
//! Topology Discovery Module
|
|
//!
|
|
//! This module provides automatic discovery of Thunderbolt 5 Mac cluster
|
|
//! topology, including node detection, bridge identification, and bandwidth probing.
|
|
|
|
use std::collections::HashMap;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use chrono::Utc;
|
|
use clusterviz_shared::{ClusterTopology, LinkInfo, NodeInfo, NodeStatus, TransportType};
|
|
use tracing::{debug, info, warn};
|
|
use uuid::Uuid;
|
|
|
|
/// Discovers cluster topology by probing network and devices
|
|
pub struct TopologyDiscoverer {
|
|
/// Discovery timeout
|
|
timeout: Duration,
|
|
/// Known node addresses (for simulation)
|
|
known_addresses: Vec<String>,
|
|
/// Whether to use simulation mode
|
|
simulation_mode: bool,
|
|
}
|
|
|
|
impl TopologyDiscoverer {
|
|
/// Create a new topology discoverer
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
timeout: Duration::from_secs(30),
|
|
known_addresses: Vec::new(),
|
|
simulation_mode: true, // Default to simulation for demo
|
|
}
|
|
}
|
|
|
|
/// Set discovery timeout
|
|
#[must_use]
|
|
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
|
self.timeout = timeout;
|
|
self
|
|
}
|
|
|
|
/// Add known node addresses for discovery
|
|
pub fn add_known_address(&mut self, address: impl Into<String>) {
|
|
self.known_addresses.push(address.into());
|
|
}
|
|
|
|
/// Enable or disable simulation mode
|
|
pub fn set_simulation_mode(&mut self, enabled: bool) {
|
|
self.simulation_mode = enabled;
|
|
}
|
|
|
|
/// Discover the cluster topology
|
|
pub async fn discover(&self) -> Result<ClusterTopology> {
|
|
if self.simulation_mode {
|
|
return self.discover_simulated().await;
|
|
}
|
|
|
|
self.discover_real().await
|
|
}
|
|
|
|
/// Discover real cluster topology (placeholder for actual implementation)
|
|
async fn discover_real(&self) -> Result<ClusterTopology> {
|
|
info!("Starting real topology discovery");
|
|
|
|
// In a real implementation, this would:
|
|
// 1. Query system for Thunderbolt devices
|
|
// 2. Probe network for other nodes
|
|
// 3. Establish connections and measure bandwidth
|
|
// 4. Build topology graph
|
|
|
|
// For now, return a simulated topology
|
|
warn!("Real topology discovery not implemented, using simulation");
|
|
self.discover_simulated().await
|
|
}
|
|
|
|
/// Discover simulated cluster topology for demo purposes
|
|
async fn discover_simulated(&self) -> Result<ClusterTopology> {
|
|
info!("Using simulated topology discovery");
|
|
|
|
let mut topology = ClusterTopology::new("Thunderbolt-5-Cluster");
|
|
|
|
// Detect local node (simulated)
|
|
let bridge_detector = BridgeDetector::new();
|
|
let local_node = bridge_detector.detect_local_node().await?;
|
|
let local_id = local_node.id;
|
|
topology.add_node(local_node);
|
|
|
|
// Discover peer nodes (simulated)
|
|
let peer_nodes = self.discover_peer_nodes().await?;
|
|
let peer_ids: Vec<Uuid> = peer_nodes.iter().map(|n| n.id).collect();
|
|
|
|
for node in peer_nodes {
|
|
topology.add_node(node);
|
|
}
|
|
|
|
// Probe bandwidth between nodes (simulated)
|
|
let bandwidth_probe = BandwidthProbe::new();
|
|
|
|
// Create links from local to each peer
|
|
for peer_id in &peer_ids {
|
|
let (bandwidth, latency) = bandwidth_probe.probe(local_id, *peer_id).await?;
|
|
let mut link = LinkInfo::new(local_id, *peer_id, TransportType::Thunderbolt);
|
|
link.bandwidth_gbps = bandwidth;
|
|
link.latency_us = latency;
|
|
topology.add_link(link);
|
|
}
|
|
|
|
// Create links between peers (ring topology)
|
|
for i in 0..peer_ids.len() {
|
|
let src = peer_ids[i];
|
|
let dst = peer_ids[(i + 1) % peer_ids.len()];
|
|
if src != dst {
|
|
let (bandwidth, latency) = bandwidth_probe.probe(src, dst).await?;
|
|
let mut link = LinkInfo::new(src, dst, TransportType::Thunderbolt);
|
|
link.bandwidth_gbps = bandwidth;
|
|
link.latency_us = latency;
|
|
topology.add_link(link);
|
|
}
|
|
}
|
|
|
|
topology.discovered_at = Utc::now();
|
|
Ok(topology)
|
|
}
|
|
|
|
/// Discover peer nodes on the network
|
|
async fn discover_peer_nodes(&self) -> Result<Vec<NodeInfo>> {
|
|
debug!("Discovering peer nodes");
|
|
|
|
// Simulated peer discovery
|
|
let peers = vec![
|
|
NodeInfo::new("mac-studio-2", "Apple M3 Max", 128),
|
|
NodeInfo::new("mac-studio-3", "Apple M3 Max", 128),
|
|
NodeInfo::new("mac-studio-4", "Apple M3 Max", 96),
|
|
];
|
|
|
|
Ok(peers)
|
|
}
|
|
}
|
|
|
|
impl Default for TopologyDiscoverer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Detects Thunderbolt bridges and local node information
|
|
pub struct BridgeDetector {
|
|
/// Detected bridge information
|
|
bridges: Vec<BridgeInfo>,
|
|
}
|
|
|
|
/// Information about a Thunderbolt bridge
|
|
#[derive(Debug, Clone)]
|
|
pub struct BridgeInfo {
|
|
/// Bridge identifier
|
|
pub id: Uuid,
|
|
/// Bridge name
|
|
pub name: String,
|
|
/// Thunderbolt version (e.g., 4, 5)
|
|
pub version: u8,
|
|
/// Number of ports
|
|
pub port_count: u8,
|
|
/// Connected device IDs
|
|
pub connected_devices: Vec<Uuid>,
|
|
}
|
|
|
|
impl BridgeDetector {
|
|
/// Create a new bridge detector
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
bridges: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Detect the local node
|
|
pub async fn detect_local_node(&self) -> Result<NodeInfo> {
|
|
debug!("Detecting local node");
|
|
|
|
// In a real implementation, this would query system info
|
|
// For simulation, return a representative local node
|
|
let mut node = NodeInfo::new("mac-studio-1", "Apple M3 Max", 128);
|
|
node.is_coordinator = true;
|
|
node.status = NodeStatus::Healthy;
|
|
node.last_heartbeat = Utc::now();
|
|
|
|
Ok(node)
|
|
}
|
|
|
|
/// Detect Thunderbolt bridges
|
|
pub async fn detect_bridges(&mut self) -> Result<&[BridgeInfo]> {
|
|
debug!("Detecting Thunderbolt bridges");
|
|
|
|
// Simulated bridge detection
|
|
let bridge = BridgeInfo {
|
|
id: Uuid::new_v4(),
|
|
name: "Thunderbolt 5 Bridge".to_string(),
|
|
version: 5,
|
|
port_count: 4,
|
|
connected_devices: Vec::new(),
|
|
};
|
|
|
|
self.bridges.push(bridge);
|
|
|
|
Ok(&self.bridges)
|
|
}
|
|
|
|
/// Get detected bridges
|
|
#[must_use]
|
|
pub fn get_bridges(&self) -> &[BridgeInfo] {
|
|
&self.bridges
|
|
}
|
|
}
|
|
|
|
impl Default for BridgeDetector {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Probes bandwidth between nodes
|
|
pub struct BandwidthProbe {
|
|
/// Cached bandwidth measurements
|
|
bandwidth_cache: HashMap<(Uuid, Uuid), f64>,
|
|
/// Cached latency measurements
|
|
latency_cache: HashMap<(Uuid, Uuid), f64>,
|
|
/// Number of probe iterations
|
|
iterations: u32,
|
|
/// Probe packet size in bytes
|
|
packet_size: u64,
|
|
}
|
|
|
|
impl BandwidthProbe {
|
|
/// Create a new bandwidth probe
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
bandwidth_cache: HashMap::new(),
|
|
latency_cache: HashMap::new(),
|
|
iterations: 10,
|
|
packet_size: 1024 * 1024, // 1 MB
|
|
}
|
|
}
|
|
|
|
/// Set the number of probe iterations
|
|
#[must_use]
|
|
pub fn with_iterations(mut self, iterations: u32) -> Self {
|
|
self.iterations = iterations;
|
|
self
|
|
}
|
|
|
|
/// Set the probe packet size
|
|
#[must_use]
|
|
pub fn with_packet_size(mut self, size: u64) -> Self {
|
|
self.packet_size = size;
|
|
self
|
|
}
|
|
|
|
/// Probe bandwidth between two nodes
|
|
pub async fn probe(&self, source: Uuid, target: Uuid) -> Result<(f64, f64)> {
|
|
debug!("Probing bandwidth between {} and {}", source, target);
|
|
|
|
// Check cache first
|
|
if let (Some(&bw), Some(&lat)) = (
|
|
self.bandwidth_cache.get(&(source, target)),
|
|
self.latency_cache.get(&(source, target)),
|
|
) {
|
|
return Ok((bw, lat));
|
|
}
|
|
|
|
// Simulated bandwidth probing
|
|
// In real implementation, would send actual probe packets
|
|
let bandwidth = self.simulate_bandwidth_measurement().await;
|
|
let latency = self.simulate_latency_measurement().await;
|
|
|
|
Ok((bandwidth, latency))
|
|
}
|
|
|
|
/// Simulate bandwidth measurement
|
|
async fn simulate_bandwidth_measurement(&self) -> f64 {
|
|
// Thunderbolt 5 theoretical max is 120 Gbps bidirectional
|
|
// Simulate achieving 75-90% of theoretical max
|
|
let efficiency = 0.75 + (rand::random::<f64>() * 0.15);
|
|
TransportType::Thunderbolt.max_bandwidth_gbps() * efficiency
|
|
}
|
|
|
|
/// Simulate latency measurement
|
|
async fn simulate_latency_measurement(&self) -> f64 {
|
|
// Thunderbolt 5 typical latency is 2-3 microseconds
|
|
let base_latency = TransportType::Thunderbolt.typical_latency_us();
|
|
let jitter = rand::random::<f64>() * 0.5; // 0-0.5 us jitter
|
|
base_latency + jitter
|
|
}
|
|
|
|
/// Clear the measurement cache
|
|
pub fn clear_cache(&mut self) {
|
|
self.bandwidth_cache.clear();
|
|
self.latency_cache.clear();
|
|
}
|
|
}
|
|
|
|
impl Default for BandwidthProbe {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Network topology analyzer
|
|
pub struct TopologyAnalyzer {
|
|
/// Adjacency matrix (node_id -> connected nodes)
|
|
adjacency: HashMap<Uuid, Vec<Uuid>>,
|
|
}
|
|
|
|
impl TopologyAnalyzer {
|
|
/// Create a new topology analyzer
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
adjacency: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Build adjacency from topology
|
|
pub fn build_from_topology(&mut self, topology: &ClusterTopology) {
|
|
self.adjacency.clear();
|
|
|
|
for link in &topology.links {
|
|
self.adjacency
|
|
.entry(link.source)
|
|
.or_default()
|
|
.push(link.target);
|
|
self.adjacency
|
|
.entry(link.target)
|
|
.or_default()
|
|
.push(link.source);
|
|
}
|
|
}
|
|
|
|
/// Get neighbors of a node
|
|
#[must_use]
|
|
pub fn get_neighbors(&self, node_id: Uuid) -> &[Uuid] {
|
|
self.adjacency.get(&node_id).map_or(&[], |v| v.as_slice())
|
|
}
|
|
|
|
/// Calculate node degree (number of connections)
|
|
#[must_use]
|
|
pub fn node_degree(&self, node_id: Uuid) -> usize {
|
|
self.adjacency.get(&node_id).map_or(0, Vec::len)
|
|
}
|
|
|
|
/// Find nodes with highest connectivity
|
|
#[must_use]
|
|
pub fn hub_nodes(&self, top_n: usize) -> Vec<(Uuid, usize)> {
|
|
let mut degrees: Vec<_> = self
|
|
.adjacency
|
|
.iter()
|
|
.map(|(id, neighbors)| (*id, neighbors.len()))
|
|
.collect();
|
|
|
|
degrees.sort_by(|a, b| b.1.cmp(&a.1));
|
|
degrees.truncate(top_n);
|
|
degrees
|
|
}
|
|
|
|
/// Check if topology is connected (all nodes reachable)
|
|
#[must_use]
|
|
pub fn is_connected(&self) -> bool {
|
|
if self.adjacency.is_empty() {
|
|
return true;
|
|
}
|
|
|
|
let mut visited = std::collections::HashSet::new();
|
|
let mut stack: Vec<Uuid> = self.adjacency.keys().take(1).copied().collect();
|
|
|
|
while let Some(node) = stack.pop() {
|
|
if visited.insert(node) {
|
|
for &neighbor in self.get_neighbors(node) {
|
|
if !visited.contains(&neighbor) {
|
|
stack.push(neighbor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
visited.len() == self.adjacency.len()
|
|
}
|
|
|
|
/// Calculate average path length (simplified)
|
|
#[must_use]
|
|
pub fn average_degree(&self) -> f64 {
|
|
if self.adjacency.is_empty() {
|
|
return 0.0;
|
|
}
|
|
|
|
let total_degree: usize = self.adjacency.values().map(Vec::len).sum();
|
|
total_degree as f64 / self.adjacency.len() as f64
|
|
}
|
|
}
|
|
|
|
impl Default for TopologyAnalyzer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_topology_discoverer_creation() {
|
|
let discoverer = TopologyDiscoverer::new();
|
|
assert!(discoverer.simulation_mode);
|
|
assert_eq!(discoverer.timeout, Duration::from_secs(30));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_topology_discoverer_with_timeout() {
|
|
let discoverer = TopologyDiscoverer::new().with_timeout(Duration::from_secs(60));
|
|
assert_eq!(discoverer.timeout, Duration::from_secs(60));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_discover_simulated() {
|
|
let discoverer = TopologyDiscoverer::new();
|
|
let topology = discoverer.discover().await.unwrap();
|
|
|
|
assert!(topology.node_count() >= 1);
|
|
assert!(!topology.links.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bridge_detector() {
|
|
let detector = BridgeDetector::new();
|
|
let local_node = detector.detect_local_node().await.unwrap();
|
|
|
|
assert!(local_node.is_coordinator);
|
|
assert_eq!(local_node.status, NodeStatus::Healthy);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bridge_detection() {
|
|
let mut detector = BridgeDetector::new();
|
|
let bridges = detector.detect_bridges().await.unwrap();
|
|
|
|
assert!(!bridges.is_empty());
|
|
assert_eq!(bridges[0].version, 5);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bandwidth_probe() {
|
|
let probe = BandwidthProbe::new();
|
|
let source = Uuid::new_v4();
|
|
let target = Uuid::new_v4();
|
|
|
|
let (bandwidth, latency) = probe.probe(source, target).await.unwrap();
|
|
|
|
assert!(bandwidth > 0.0);
|
|
assert!(bandwidth <= TransportType::Thunderbolt.max_bandwidth_gbps());
|
|
assert!(latency > 0.0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bandwidth_probe_configuration() {
|
|
let probe = BandwidthProbe::new()
|
|
.with_iterations(20)
|
|
.with_packet_size(2 * 1024 * 1024);
|
|
|
|
assert_eq!(probe.iterations, 20);
|
|
assert_eq!(probe.packet_size, 2 * 1024 * 1024);
|
|
}
|
|
|
|
#[test]
|
|
fn test_topology_analyzer() {
|
|
let mut topology = ClusterTopology::new("test");
|
|
|
|
let node1 = NodeInfo::new("n1", "M3", 64);
|
|
let node2 = NodeInfo::new("n2", "M3", 64);
|
|
let node3 = NodeInfo::new("n3", "M3", 64);
|
|
|
|
let id1 = node1.id;
|
|
let id2 = node2.id;
|
|
let id3 = node3.id;
|
|
|
|
topology.add_node(node1);
|
|
topology.add_node(node2);
|
|
topology.add_node(node3);
|
|
|
|
topology.add_link(LinkInfo::new(id1, id2, TransportType::Thunderbolt));
|
|
topology.add_link(LinkInfo::new(id2, id3, TransportType::Thunderbolt));
|
|
|
|
let mut analyzer = TopologyAnalyzer::new();
|
|
analyzer.build_from_topology(&topology);
|
|
|
|
assert_eq!(analyzer.node_degree(id1), 1);
|
|
assert_eq!(analyzer.node_degree(id2), 2);
|
|
assert_eq!(analyzer.node_degree(id3), 1);
|
|
assert!(analyzer.is_connected());
|
|
}
|
|
|
|
#[test]
|
|
fn test_topology_analyzer_hub_nodes() {
|
|
let mut topology = ClusterTopology::new("test");
|
|
|
|
let nodes: Vec<_> = (0..5)
|
|
.map(|i| NodeInfo::new(format!("n{}", i), "M3", 64))
|
|
.collect();
|
|
let ids: Vec<_> = nodes.iter().map(|n| n.id).collect();
|
|
|
|
for node in nodes {
|
|
topology.add_node(node);
|
|
}
|
|
|
|
// Star topology with node 0 as hub
|
|
for i in 1..5 {
|
|
topology.add_link(LinkInfo::new(ids[0], ids[i], TransportType::Thunderbolt));
|
|
}
|
|
|
|
let mut analyzer = TopologyAnalyzer::new();
|
|
analyzer.build_from_topology(&topology);
|
|
|
|
let hubs = analyzer.hub_nodes(1);
|
|
assert_eq!(hubs[0].0, ids[0]);
|
|
assert_eq!(hubs[0].1, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_topology_analyzer_disconnected() {
|
|
let mut topology = ClusterTopology::new("test");
|
|
|
|
let node1 = NodeInfo::new("n1", "M3", 64);
|
|
let node2 = NodeInfo::new("n2", "M3", 64);
|
|
let node3 = NodeInfo::new("n3", "M3", 64);
|
|
|
|
let id1 = node1.id;
|
|
let id2 = node2.id;
|
|
|
|
topology.add_node(node1);
|
|
topology.add_node(node2);
|
|
topology.add_node(node3);
|
|
|
|
// Only connect node1 and node2, leaving node3 disconnected
|
|
topology.add_link(LinkInfo::new(id1, id2, TransportType::Thunderbolt));
|
|
|
|
let mut analyzer = TopologyAnalyzer::new();
|
|
analyzer.build_from_topology(&topology);
|
|
|
|
// Note: analyzer only tracks nodes that have links
|
|
// In this case, node3 has no links, so it's not in adjacency
|
|
assert!(analyzer.is_connected());
|
|
}
|
|
|
|
#[test]
|
|
fn test_average_degree() {
|
|
let mut topology = ClusterTopology::new("test");
|
|
|
|
let nodes: Vec<_> = (0..4)
|
|
.map(|i| NodeInfo::new(format!("n{}", i), "M3", 64))
|
|
.collect();
|
|
let ids: Vec<_> = nodes.iter().map(|n| n.id).collect();
|
|
|
|
for node in nodes {
|
|
topology.add_node(node);
|
|
}
|
|
|
|
// Ring topology
|
|
for i in 0..4 {
|
|
topology.add_link(LinkInfo::new(
|
|
ids[i],
|
|
ids[(i + 1) % 4],
|
|
TransportType::Thunderbolt,
|
|
));
|
|
}
|
|
|
|
let mut analyzer = TopologyAnalyzer::new();
|
|
analyzer.build_from_topology(&topology);
|
|
|
|
// Each node has degree 2 in a ring
|
|
let avg = analyzer.average_degree();
|
|
assert!((avg - 2.0).abs() < f64::EPSILON);
|
|
}
|
|
}
|