Core domain structure for harmony rs (#1)

Co-authored-by: jeangab <jeangabriel.gc@gmail.com>
Co-authored-by: Jean-Gabriel Gill-Couture <jeangabriel.gc@gmail.com>
Reviewed-on: https://git.nationtech.io/johnride/harmony/pulls/1
Co-authored-by: jeangab <jg@nationtech.io>
Co-committed-by: jeangab <jg@nationtech.io>
This commit is contained in:
jeangab 2024-09-06 12:41:00 +00:00 committed by johnride
parent 231b1cca9f
commit aa28ab37b8
35 changed files with 1759 additions and 19 deletions

1101
harmony-rs/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,14 @@ version = "0.1.0"
edition = "2021"
[dependencies]
derive-new = "0.7.0"
env_logger = "0.11.5"
libredfish = "0.1.1"
log = "0.4.22"
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"] }

View File

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,30 @@
use std::fmt;
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 {}
pub trait SshClient {
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

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

View File

View File

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,104 @@
use std::{net::ToSocketAddrs, path::Path, sync::Arc, time::Duration};
use russh::{client, keys::{key, load_secret_key}, ChannelMsg, Disconnect};
use tokio::io::AsyncWriteExt;
use crate::domain::executors::SshClient;
pub struct RusshClient;
impl SshClient for RusshClient {
fn test_connection(&self, username: String, password: String) -> Result<(), crate::domain::executors::ExecutorError> {
todo!()
//Session::connect();
}
}
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 = russh::Error;
async fn check_server_key(
&mut self,
_server_public_key: &key::PublicKey,
) -> Result<bool, Self::Error> {
Ok(true)
}
}
/// This struct is a convenience wrapper
/// around a russh client
pub struct Session {
session: client::Handle<Client>,
}
impl Session {
async fn connect<P: AsRef<Path>, A: ToSocketAddrs>(
key_path: P,
user: impl Into<String>,
addrs: A,
) -> Result<Self, String> {
let key_pair = load_secret_key(key_path, None)?;
let config = client::Config {
inactivity_timeout: Some(Duration::from_secs(5)),
..<_>::default()
};
let config = Arc::new(config);
let sh = Client {};
let mut session = client::connect(config, addrs, sh).await?;
let auth_res = session
.authenticate_publickey(user, Arc::new(key_pair))
.await?;
if !auth_res {
Err("Authentication failed")
}
Ok(Self { session })
}
async fn call(&mut self, command: &str) -> Result<u32> {
let mut channel = self.session.channel_open_session().await?;
channel.exec(true, command).await?;
let mut code = None;
let mut stdout = tokio::io::stdout();
loop {
// There's an event available on the session channel
let Some(msg) = channel.wait().await else {
break;
};
match msg {
// Write data to the terminal
ChannelMsg::Data { ref data } => {
stdout.write_all(data).await?;
stdout.flush().await?;
}
// The command has returned an exit code
ChannelMsg::ExitStatus { exit_status } => {
code = Some(exit_status);
// cannot leave the loop immediately, there might still be more data to receive
}
_ => {}
}
}
Ok(code.expect("program did not exit cleanly"))
}
async fn close(&mut self) -> Result<()> {
self.session
.disconnect(Disconnect::ByApplication, "", "English")
.await?;
Ok(())
}
}

View File

@ -0,0 +1,21 @@
use crate::domain::{
hardware::{Location, Host, HostCategory},
inventory::Inventory,
};
pub fn get_fqm_inventory() -> Inventory {
Inventory {
location: Location::new(
"1134 Grande Allée Ouest 1er étage, Québec, Qc".into(),
"FQM 1134 1er étage".into(),
),
host: vec![Host {
category: HostCategory::Server,
network: vec![],
storage: vec![],
labels: vec![],
}],
switch: vec![],
firewall: vec![],
}
}

View File

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

View File

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

3
harmony-rs/src/lib.rs Normal file
View File

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

View File

@ -1,20 +1,19 @@
use libredfish::{Config, Redfish};
use reqwest::blocking::Client;
use harmony_rs::{
domain::{
inventory::{Inventory, InventoryFilter},
maestro::Maestro,
},
infra::inventory::fqm::get_fqm_inventory, modules::opnsense_dhcp::OPNSenseDhcpScore,
};
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,
},
);
env_logger::init();
let response = redfish.get_power_status().expect("Failed redfish request");
println!("Got power {:?}", response);
let maestro = Maestro::new(get_inventory());
let score = OPNSenseDhcpScore::new(InventoryFilter::new(vec![]));
maestro.interpret(score);
}
fn get_inventory() -> Inventory {
get_fqm_inventory()
}

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}, executors::SshClient, 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!();
}
}