419 lines
12 KiB
Rust
419 lines
12 KiB
Rust
use std::sync::Arc;
|
|
|
|
use derive_new::new;
|
|
use harmony_types::net::MacAddress;
|
|
use serde::{Serialize, Serializer, ser::SerializeStruct};
|
|
use serde_value::Value;
|
|
|
|
pub type HostGroup = Vec<PhysicalHost>;
|
|
pub type SwitchGroup = Vec<Switch>;
|
|
pub type FirewallGroup = Vec<PhysicalHost>;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct PhysicalHost {
|
|
pub category: HostCategory,
|
|
pub network: Vec<NetworkInterface>,
|
|
pub management: Arc<dyn ManagementInterface>,
|
|
pub storage: Vec<Storage>,
|
|
pub labels: Vec<Label>,
|
|
pub memory_size: Option<u64>,
|
|
pub cpu_count: Option<u64>,
|
|
}
|
|
|
|
impl PhysicalHost {
|
|
pub fn empty(category: HostCategory) -> Self {
|
|
Self {
|
|
category,
|
|
network: vec![],
|
|
storage: vec![],
|
|
labels: vec![],
|
|
management: Arc::new(ManualManagementInterface {}),
|
|
memory_size: None,
|
|
cpu_count: None,
|
|
}
|
|
}
|
|
|
|
pub fn cluster_mac(&self) -> MacAddress {
|
|
self.network
|
|
.get(0)
|
|
.expect("Cluster physical host should have a network interface")
|
|
.mac_address
|
|
.clone()
|
|
}
|
|
|
|
pub fn cpu(mut self, cpu_count: Option<u64>) -> Self {
|
|
self.cpu_count = cpu_count;
|
|
self
|
|
}
|
|
|
|
pub fn memory_size(mut self, memory_size: Option<u64>) -> Self {
|
|
self.memory_size = memory_size;
|
|
self
|
|
}
|
|
|
|
pub fn storage(
|
|
mut self,
|
|
connection: StorageConnectionType,
|
|
kind: StorageKind,
|
|
size: u64,
|
|
serial: String,
|
|
) -> Self {
|
|
self.storage.push(Storage {
|
|
connection,
|
|
kind,
|
|
size,
|
|
serial,
|
|
});
|
|
self
|
|
}
|
|
|
|
pub fn mac_address(mut self, mac_address: MacAddress) -> Self {
|
|
self.network.push(NetworkInterface {
|
|
name: None,
|
|
mac_address,
|
|
speed: None,
|
|
});
|
|
self
|
|
}
|
|
|
|
pub fn label(mut self, name: String, value: String) -> Self {
|
|
self.labels.push(Label { name, value });
|
|
self
|
|
}
|
|
|
|
pub fn management(mut self, management: Arc<dyn ManagementInterface>) -> Self {
|
|
self.management = management;
|
|
self
|
|
}
|
|
}
|
|
|
|
// Custom Serialize implementation for PhysicalHost
|
|
impl Serialize for PhysicalHost {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
// Determine the number of fields
|
|
let mut num_fields = 5; // category, network, storage, labels, management
|
|
if self.memory_size.is_some() {
|
|
num_fields += 1;
|
|
}
|
|
if self.cpu_count.is_some() {
|
|
num_fields += 1;
|
|
}
|
|
|
|
// Create a serialization structure
|
|
let mut state = serializer.serialize_struct("PhysicalHost", num_fields)?;
|
|
|
|
// Serialize the standard fields
|
|
state.serialize_field("category", &self.category)?;
|
|
state.serialize_field("network", &self.network)?;
|
|
state.serialize_field("storage", &self.storage)?;
|
|
state.serialize_field("labels", &self.labels)?;
|
|
|
|
// Serialize optional fields
|
|
if let Some(memory) = self.memory_size {
|
|
state.serialize_field("memory_size", &memory)?;
|
|
}
|
|
if let Some(cpu) = self.cpu_count {
|
|
state.serialize_field("cpu_count", &cpu)?;
|
|
}
|
|
|
|
let mgmt_data = self.management.serialize_management();
|
|
// pub management: Arc<dyn ManagementInterface>,
|
|
|
|
// Handle management interface - either as a field or flattened
|
|
state.serialize_field("management", &mgmt_data)?;
|
|
|
|
state.end()
|
|
}
|
|
}
|
|
|
|
#[derive(new, Serialize)]
|
|
pub struct ManualManagementInterface;
|
|
|
|
impl ManagementInterface for ManualManagementInterface {
|
|
fn boot_to_pxe(&self) {
|
|
todo!()
|
|
}
|
|
|
|
fn get_supported_protocol_names(&self) -> String {
|
|
// todo!()
|
|
"none".to_string()
|
|
}
|
|
}
|
|
|
|
pub trait ManagementInterface: Send + Sync + SerializableManagement {
|
|
fn boot_to_pxe(&self);
|
|
fn get_supported_protocol_names(&self) -> String;
|
|
}
|
|
|
|
impl std::fmt::Debug for dyn ManagementInterface {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_fmt(format_args!(
|
|
"ManagementInterface protocols : {}",
|
|
self.get_supported_protocol_names(),
|
|
))
|
|
}
|
|
}
|
|
|
|
// Define a trait for serializing management interfaces
|
|
pub trait SerializableManagement {
|
|
fn serialize_management(&self) -> Value;
|
|
}
|
|
|
|
// Provide a blanket implementation for all types that implement both ManagementInterface and Serialize
|
|
impl<T> SerializableManagement for T
|
|
where
|
|
T: ManagementInterface + Serialize,
|
|
{
|
|
fn serialize_management(&self) -> Value {
|
|
serde_value::to_value(self).expect("ManagementInterface should serialize successfully")
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub enum HostCategory {
|
|
Server,
|
|
Firewall,
|
|
Switch,
|
|
}
|
|
|
|
#[derive(Debug, new, Clone, Serialize)]
|
|
pub struct NetworkInterface {
|
|
pub name: Option<String>,
|
|
pub mac_address: MacAddress,
|
|
pub speed: Option<u64>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
use harmony_macros::mac_address;
|
|
#[cfg(test)]
|
|
impl NetworkInterface {
|
|
pub fn dummy() -> Self {
|
|
Self {
|
|
name: Some(String::new()),
|
|
mac_address: mac_address!("00:00:00:00:00:00"),
|
|
speed: Some(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, new, Clone, Serialize)]
|
|
pub enum StorageConnectionType {
|
|
Sata3g,
|
|
Sata6g,
|
|
Sas6g,
|
|
Sas12g,
|
|
PCIE,
|
|
}
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub enum StorageKind {
|
|
SSD,
|
|
NVME,
|
|
HDD,
|
|
}
|
|
#[derive(Debug, new, Clone, Serialize)]
|
|
pub struct Storage {
|
|
pub connection: StorageConnectionType,
|
|
pub kind: StorageKind,
|
|
pub size: u64,
|
|
pub serial: String,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl Storage {
|
|
pub fn dummy() -> Self {
|
|
Self {
|
|
connection: StorageConnectionType::Sata3g,
|
|
kind: StorageKind::SSD,
|
|
size: 0,
|
|
serial: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct Switch {
|
|
_interface: Vec<NetworkInterface>,
|
|
_management_interface: NetworkInterface,
|
|
}
|
|
|
|
#[derive(Debug, new, Clone, Serialize)]
|
|
pub struct Label {
|
|
pub name: String,
|
|
pub value: String,
|
|
}
|
|
|
|
pub type Address = String;
|
|
|
|
#[derive(new, Debug, Serialize)]
|
|
pub struct Location {
|
|
pub address: Address,
|
|
pub name: String,
|
|
}
|
|
|
|
impl Location {
|
|
pub fn test_building() -> Location {
|
|
Self {
|
|
address: String::new(),
|
|
name: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
|
|
// Mock implementation of ManagementInterface
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct MockHPIlo {
|
|
ip: String,
|
|
username: String,
|
|
password: String,
|
|
firmware_version: String,
|
|
}
|
|
|
|
impl ManagementInterface for MockHPIlo {
|
|
fn boot_to_pxe(&self) {}
|
|
|
|
fn get_supported_protocol_names(&self) -> String {
|
|
String::new()
|
|
}
|
|
}
|
|
|
|
// Another mock implementation
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct MockDellIdrac {
|
|
hostname: String,
|
|
port: u16,
|
|
api_token: String,
|
|
}
|
|
|
|
impl ManagementInterface for MockDellIdrac {
|
|
fn boot_to_pxe(&self) {}
|
|
|
|
fn get_supported_protocol_names(&self) -> String {
|
|
String::new()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialize_physical_host_with_hp_ilo() {
|
|
// Create a PhysicalHost with HP iLO management
|
|
let host = PhysicalHost {
|
|
category: HostCategory::Server,
|
|
network: vec![NetworkInterface::dummy()],
|
|
management: Arc::new(MockHPIlo {
|
|
ip: "192.168.1.100".to_string(),
|
|
username: "admin".to_string(),
|
|
password: "password123".to_string(),
|
|
firmware_version: "2.5.0".to_string(),
|
|
}),
|
|
storage: vec![Storage::dummy()],
|
|
labels: vec![Label::new("datacenter".to_string(), "us-east".to_string())],
|
|
memory_size: Some(64_000_000),
|
|
cpu_count: Some(16),
|
|
};
|
|
|
|
// Serialize to JSON
|
|
let json = serde_json::to_string(&host).expect("Failed to serialize host");
|
|
|
|
// Check that the serialized JSON contains the HP iLO details
|
|
assert!(json.contains("192.168.1.100"));
|
|
assert!(json.contains("admin"));
|
|
assert!(json.contains("password123"));
|
|
assert!(json.contains("firmware_version"));
|
|
assert!(json.contains("2.5.0"));
|
|
|
|
// Parse back to verify structure (not the exact management interface)
|
|
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON");
|
|
|
|
// Verify basic structure
|
|
assert_eq!(parsed["cpu_count"], 16);
|
|
assert_eq!(parsed["memory_size"], 64_000_000);
|
|
assert_eq!(parsed["network"][0]["name"], "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialize_physical_host_with_dell_idrac() {
|
|
// Create a PhysicalHost with Dell iDRAC management
|
|
let host = PhysicalHost {
|
|
category: HostCategory::Server,
|
|
network: vec![NetworkInterface::dummy()],
|
|
management: Arc::new(MockDellIdrac {
|
|
hostname: "idrac-server01".to_string(),
|
|
port: 443,
|
|
api_token: "abcdef123456".to_string(),
|
|
}),
|
|
storage: vec![Storage::dummy()],
|
|
labels: vec![Label::new("env".to_string(), "production".to_string())],
|
|
memory_size: Some(128_000_000),
|
|
cpu_count: Some(32),
|
|
};
|
|
|
|
// Serialize to JSON
|
|
let json = serde_json::to_string(&host).expect("Failed to serialize host");
|
|
|
|
// Check that the serialized JSON contains the Dell iDRAC details
|
|
assert!(json.contains("idrac-server01"));
|
|
assert!(json.contains("443"));
|
|
assert!(json.contains("abcdef123456"));
|
|
|
|
// Parse back to verify structure
|
|
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON");
|
|
|
|
// Verify basic structure
|
|
assert_eq!(parsed["cpu_count"], 32);
|
|
assert_eq!(parsed["memory_size"], 128_000_000);
|
|
assert_eq!(parsed["storage"][0]["path"], serde_json::Value::Null);
|
|
}
|
|
|
|
#[test]
|
|
fn test_different_management_implementations_produce_valid_json() {
|
|
// Create hosts with different management implementations
|
|
let host1 = PhysicalHost {
|
|
category: HostCategory::Server,
|
|
network: vec![],
|
|
management: Arc::new(MockHPIlo {
|
|
ip: "10.0.0.1".to_string(),
|
|
username: "root".to_string(),
|
|
password: "secret".to_string(),
|
|
firmware_version: "3.0.0".to_string(),
|
|
}),
|
|
storage: vec![],
|
|
labels: vec![],
|
|
memory_size: None,
|
|
cpu_count: None,
|
|
};
|
|
|
|
let host2 = PhysicalHost {
|
|
category: HostCategory::Server,
|
|
network: vec![],
|
|
management: Arc::new(MockDellIdrac {
|
|
hostname: "server02-idrac".to_string(),
|
|
port: 8443,
|
|
api_token: "token123".to_string(),
|
|
}),
|
|
storage: vec![],
|
|
labels: vec![],
|
|
memory_size: None,
|
|
cpu_count: None,
|
|
};
|
|
|
|
// Both should serialize successfully
|
|
let json1 = serde_json::to_string(&host1).expect("Failed to serialize host1");
|
|
let json2 = serde_json::to_string(&host2).expect("Failed to serialize host2");
|
|
|
|
// Both JSONs should be valid and parseable
|
|
let _: serde_json::Value = serde_json::from_str(&json1).expect("Invalid JSON for host1");
|
|
let _: serde_json::Value = serde_json::from_str(&json2).expect("Invalid JSON for host2");
|
|
|
|
// The JSONs should be different because they contain different management interfaces
|
|
assert_ne!(json1, json2);
|
|
}
|
|
}
|