chore(harmony): Use Cargo workspaces for core harmony and client specific implementation

This commit is contained in:
jeangab
2024-09-06 12:17:23 -04:00
parent aa28ab37b8
commit cc01ec5fe5
41 changed files with 783 additions and 142 deletions

View File

@@ -0,0 +1,18 @@
[package]
name = "harmony"
version = "0.1.0"
edition = "2021"
[dependencies]
libredfish = "0.1.1"
reqwest = {version = "0.11", features = ["blocking", "json"] }
russh = "0.45.0"
rust-ipmi = "0.1.1"
semver = "1.0.23"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127"
tokio = { version = "1.40.0", features = ["io-std"] }
derive-new = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }
async-trait = { workspace = true }

View File

@@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Id {
value: String,
}
impl Id {
pub fn from_string(value: String) -> Self {
Self { value }
}
}

View File

@@ -0,0 +1,4 @@
mod id;
mod version;
pub use id::*;
pub use version::*;

View File

@@ -0,0 +1,76 @@
#[derive(Debug, Clone)]
pub struct Version {
value: semver::Version,
}
#[derive(Debug, Clone)]
pub struct VersionError {
msg: String,
}
impl From<semver::Error> for VersionError {
fn from(value: semver::Error) -> Self {
Self {
msg: value.to_string(),
}
}
}
impl Version {
pub fn from(val: &str) -> Result<Self, VersionError> {
Ok(Self {
value: semver::Version::parse(val)?,
})
}
}
impl<'de> serde::Deserialize<'de> for Version {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
semver::Version::parse(&s)
.map(|value| Version { value })
.map_err(serde::de::Error::custom)
}
}
impl serde::Serialize for Version {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.value.to_string().serialize(serializer)
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return self.value.fmt(f);
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn version_serialize_deserialize() {
let v = "10.0.1331-ababa+b123";
let version = Version {
value: semver::Version::parse(v).unwrap(),
};
let s = serde_json::to_string(&version).unwrap();
let version2: Version = serde_json::from_str(&s).unwrap();
assert_eq!(version2.value.major, 10);
assert_eq!(version2.value.minor, 0);
assert_eq!(version2.value.patch, 1331);
assert_eq!(version2.value.build.to_string(), "b123");
assert_eq!(version2.value.pre.to_string(), "ababa");
assert_eq!(version2.value.to_string(), v);
}
}

View File

@@ -0,0 +1,33 @@
use std::fmt;
use async_trait::async_trait;
pub struct ExecutorResult {
message: String,
}
#[derive(Debug)]
pub enum ExecutorError {
NetworkError(String),
AuthenticationError(String),
ConfigurationError(String),
UnexpectedError(String),
}
impl fmt::Display for ExecutorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExecutorError::NetworkError(msg) => write!(f, "Network error: {}", msg),
ExecutorError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
ExecutorError::ConfigurationError(msg) => write!(f, "Configuration error: {}", msg),
ExecutorError::UnexpectedError(msg) => write!(f, "Unexpected error: {}", msg),
}
}
}
impl std::error::Error for ExecutorError {}
#[async_trait]
pub trait SshClient {
async fn test_connection(&self, username: String, password: String) -> Result<(), ExecutorError>;
}

View File

@@ -0,0 +1,15 @@
use derive_new::new;
#[derive(Debug, Clone)]
pub enum FilterKind {
Label,
Kind,
}
pub type FilterValue = String;
#[derive(Debug, new, Clone)]
pub struct Filter {
kind: FilterKind,
value: FilterValue,
}

View File

@@ -0,0 +1,76 @@
use derive_new::new;
pub type HostGroup = Vec<Host>;
pub type SwitchGroup = Vec<Switch>;
pub type FirewallGroup = Vec<Firewall>;
#[derive(Debug)]
pub struct Host {
pub category: HostCategory,
pub network: Vec<NetworkInterface>,
pub storage: Vec<Storage>,
pub labels: Vec<Label>,
}
#[derive(Debug)]
pub enum HostCategory {
Server,
Firewall,
Switch,
}
#[derive(Debug)]
pub struct NetworkInterface {
speed: u64,
mac_address: MacAddress,
plugged_in: bool,
}
type MacAddress = String;
#[derive(Debug)]
pub enum StorageConnectionType {
Sata3g,
Sata6g,
Sas6g,
Sas12g,
PCIE,
}
#[derive(Debug)]
pub enum StorageKind {
SSD,
NVME,
HDD,
}
#[derive(Debug)]
pub struct Storage {
connection: StorageConnectionType,
kind: StorageKind,
size: u64,
serial: String,
}
#[derive(Debug)]
pub struct Switch {
interface: Vec<NetworkInterface>,
management_interface: NetworkInterface,
}
#[derive(Debug)]
pub struct Firewall {}
#[derive(Debug)]
pub struct Label;
pub type Address = String;
#[derive(new, Debug)]
pub struct Location {
pub address: Address,
pub name: String,
}
impl Location {
#[cfg(test)]
pub fn test_building() -> Location {
Self {
address: String::new(),
name: String::new(),
}
}
}

View File

@@ -0,0 +1,45 @@
use super::{
data::{Id, Version},
inventory::Inventory,
score::Score,
};
pub enum InterpretName {
OPNSenseDHCP,
}
impl std::fmt::Display for InterpretName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InterpretName::OPNSenseDHCP => f.write_str("OPNSenseDHCP"),
}
}
}
pub trait Interpret {
fn execute(&self, inventory: &Inventory) -> Result<Outcome, InterpretError>;
fn get_name(&self) -> InterpretName;
fn get_version(&self) -> Version;
fn get_status(&self) -> InterpretStatus;
fn get_children(&self) -> Vec<Id>;
}
#[derive(Debug)]
pub struct Outcome {
status: InterpretStatus,
message: String,
}
#[derive(Debug, Clone)]
pub enum InterpretStatus {
SUCCESS,
FAILURE,
RUNNING,
QUEUED,
BLOCKED,
}
#[derive(Debug)]
pub struct InterpretError {
msg: String,
}

View File

@@ -0,0 +1,40 @@
#[derive(Debug, new, Clone)]
pub struct InventoryFilter {
target: Vec<Filter>,
}
pub struct InventorySlice;
impl InventoryFilter {
pub fn apply(&self, _inventory: &Inventory) -> InventorySlice {
// TODO apply inventory filter, refactor as a slice
todo!()
}
}
use derive_new::new;
use super::{
filter::Filter,
hardware::{Location, FirewallGroup, HostGroup, SwitchGroup},
};
#[derive(Debug)]
pub struct Inventory {
pub location: Location,
pub host: HostGroup,
pub switch: SwitchGroup,
pub firewall: FirewallGroup,
}
impl Inventory {
#[cfg(test)]
pub fn empty_inventory() -> Self {
Self {
location: Location::test_building(),
host: HostGroup::new(),
switch: SwitchGroup::new(),
firewall: FirewallGroup::new(),
}
}
}

View File

@@ -0,0 +1,38 @@
use derive_new::new;
use log::info;
use super::{interpret::Interpret, inventory::Inventory, score::Score};
#[derive(new)]
pub struct Maestro {
inventory: Inventory,
}
impl Maestro {
pub fn start(&mut self) {
info!("Starting Maestro");
self.load_score();
self.load_inventory();
self.launch_interprets();
}
fn load_score(&mut self) {
todo!()
}
fn load_inventory(&mut self) {
todo!()
}
fn launch_interprets(&mut self) {
todo!()
}
pub fn interpret<S: Score>(&self, score: S) {
info!("Running score {score:?}");
let interpret: S::InterpretType = score.create_interpret();
info!("Launching interpret {interpret:?}");
let result = interpret.execute(&self.inventory);
info!("Got result {result:?}");
}
}

View File

@@ -0,0 +1,9 @@
pub mod data;
pub mod executors;
pub mod filter;
pub mod hardware;
pub mod interpret;
pub mod inventory;
pub mod maestro;
pub mod score;
pub mod topology;

View File

@@ -0,0 +1,7 @@
use super::{interpret::Interpret, inventory::InventorySlice};
pub trait Score: Send + Sync + std::fmt::Debug {
type InterpretType: Interpret + std::fmt::Debug;
fn get_inventory_filter(&self) -> InventorySlice;
fn create_interpret(self) -> Self::InterpretType;
}

View File

@@ -0,0 +1,13 @@
use std::net::IpAddr;
use super::hardware::HostGroup;
pub struct OKDHACluster {
firewall: HostGroup,
control_plane: HostGroup,
workers: HostGroup,
ceph_hosts: HostGroup,
switch: HostGroup,
}
pub struct IpAddress(IpAddr);

View File

@@ -0,0 +1,2 @@
pub mod russh;

View File

@@ -0,0 +1,41 @@
use std::sync::Arc;
use async_trait::async_trait;
use russh::{client, keys::key};
use crate::domain::executors::{ExecutorError, SshClient};
pub struct RusshClient;
#[async_trait]
impl SshClient for RusshClient {
async fn test_connection(&self, _username: String, _password: String) -> Result<(), crate::domain::executors::ExecutorError> {
let config = client::Config::default();
let c = Client{};
let _client = client::connect(Arc::new(config), ("192.168.12.1", 22), c).await?;
Ok(())
}
}
struct Client {}
// More SSH event handlers
// can be defined in this trait
// In this example, we're only using Channel, so these aren't needed.
#[async_trait]
impl client::Handler for Client {
type Error = ExecutorError;
async fn check_server_key(
&mut self,
_server_public_key: &key::PublicKey,
) -> Result<bool, Self::Error> {
Ok(true)
}
}
impl From<russh::Error> for ExecutorError {
fn from(_value: russh::Error) -> Self {
todo!()
}
}

View File

@@ -0,0 +1 @@
pub mod executors;

View File

@@ -0,0 +1,3 @@
pub mod domain;
pub mod infra;
pub mod modules;

View File

@@ -0,0 +1,19 @@
use harmony::{
domain::{
inventory::{Inventory, InventoryFilter},
maestro::Maestro,
},
modules::opnsense_dhcp::OPNSenseDhcpScore,
};
pub fn main() {
env_logger::init();
let maestro = Maestro::new(get_inventory());
let score = OPNSenseDhcpScore::new(InventoryFilter::new(vec![]));
maestro.interpret(score);
}
fn get_inventory() -> Inventory {
todo!()
}

View File

@@ -0,0 +1,16 @@
use rust_ipmi::{IPMIClient, NetFn};
fn main() {
let connection_string = "192.168.11.132:443";
println!("Hello, world! {}", connection_string);
let mut client: IPMIClient = IPMIClient::new(connection_string).expect("Failed to create ipmi client");
client.establish_connection("root", "YOUR_PASSWORD")
.expect("Failed to establish connection with the BMC");
let response = client.send_raw_request(NetFn::App, 0x3b, Some(vec![0x04]));
match response {
Ok(response) => println!("{}", response),
Err(err) => println!("Got error {:?}", err),
}
}

View File

@@ -0,0 +1,20 @@
use libredfish::{Config, Redfish};
use reqwest::blocking::Client;
pub fn main() {
let client = Client::builder().danger_accept_invalid_certs(true).build().expect("Failed to build reqwest client");
let redfish = Redfish::new(
client,
Config {
user: Some(String::from("Administrator")),
endpoint: String::from("10.10.8.104/redfish/v1"),
// password: Some(String::from("YOUR_PASSWORD")),
password: Some(String::from("wrongpass")),
port: None,
},
);
let response = redfish.get_power_status().expect("Failed redfish request");
println!("Got power {:?}", response);
}

View File

@@ -0,0 +1 @@
pub mod opnsense_dhcp;

View File

@@ -0,0 +1,119 @@
use derive_new::new;
use log::info;
use crate::{domain::{
data::{Id, Version}, hardware::NetworkInterface, interpret::{InterpretError, InterpretStatus, Outcome}, inventory::Inventory, topology::IpAddress
}, infra::executors::russh::RusshClient};
use crate::domain::{
interpret::Interpret, interpret::InterpretName, inventory::InventoryFilter,
inventory::InventorySlice, score::Score,
};
use crate::domain::executors::{ExecutorError, ExecutorResult};
#[derive(Debug, new)]
pub struct OPNSenseDhcpScore {
inventory_filter: InventoryFilter,
}
impl Score for OPNSenseDhcpScore {
type InterpretType = OPNSenseDhcpInterpret;
fn get_inventory_filter(&self) -> InventorySlice {
todo!()
}
fn create_interpret(self) -> OPNSenseDhcpInterpret {
OPNSenseDhcpInterpret::new(self)
}
}
/// https://docs.opnsense.org/manual/dhcp.html#advanced-settings
#[derive(Debug)]
pub struct OPNSenseDhcpInterpret {
score: OPNSenseDhcpScore,
version: Version,
id: Id,
name: String,
status: InterpretStatus,
}
impl OPNSenseDhcpInterpret {
pub fn new(score: OPNSenseDhcpScore) -> Self {
let version = Version::from("1.0.0").expect("Version should be valid");
let name = "OPNSenseDhcpScore".to_string();
let id = Id::from_string(format!("{name}_{version}"));
Self {
version,
id,
name,
score,
status: InterpretStatus::QUEUED,
}
}
}
impl Interpret for OPNSenseDhcpInterpret {
fn get_name(&self) -> InterpretName {
InterpretName::OPNSenseDHCP
}
fn get_version(&self) -> crate::domain::data::Version {
self.version.clone()
}
fn get_status(&self) -> InterpretStatus {
self.status.clone()
}
fn get_children(&self) -> Vec<crate::domain::data::Id> {
todo!()
}
fn execute(&self, _inventory: &Inventory) -> Result<Outcome, InterpretError> {
info!("Executing {} on inventory {_inventory:?}", self.get_name());
let _ssh_client = RusshClient{};
// ssh_client.test_connection("username", "password");
todo!()
}
}
pub trait OPNSenseDhcpConfigEditor {
fn add_static_host(
&self,
opnsense_host: IpAddress,
credentials: OPNSenseCredentials,
interface: NetworkInterface,
address: IpAddress,
) -> Result<ExecutorResult, ExecutorError>;
}
pub struct OPNSenseCredentials {
pub user: String,
pub password: String,
}
#[cfg(test)]
mod test {
#[test]
fn opnsense_dns_score_should_do_nothing_on_empty_inventory() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_bootstrap_node() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_control_plane_members() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_workers() {
todo!();
}
}