feat: Inventory PhysicalHost persistence with sqlx and local sqlite db
Some checks failed
Run Check Script / check (pull_request) Failing after 34s
Some checks failed
Run Check Script / check (pull_request) Failing after 34s
This commit is contained in:
@@ -65,10 +65,12 @@ kube-derive = "1.1.0"
|
||||
bollard.workspace = true
|
||||
tar.workspace = true
|
||||
base64.workspace = true
|
||||
thiserror.workspace = true
|
||||
once_cell = "1.21.3"
|
||||
harmony_inventory_agent = { path = "../harmony_inventory_agent" }
|
||||
harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" }
|
||||
askama = "0.14.0"
|
||||
askama.workspace = true
|
||||
sqlx.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -24,6 +24,14 @@ pub struct Id {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Id {
|
||||
pub fn empty() -> Self {
|
||||
Id {
|
||||
value: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Id {
|
||||
type Err = ();
|
||||
|
||||
@@ -34,6 +42,12 @@ impl FromStr for Id {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Id {
|
||||
fn from(value: String) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Id {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.value)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use derive_new::new;
|
||||
use harmony_types::net::MacAddress;
|
||||
use serde::{Serialize, Serializer, ser::SerializeStruct};
|
||||
use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
|
||||
use serde_value::Value;
|
||||
|
||||
pub type HostGroup = Vec<PhysicalHost>;
|
||||
@@ -11,6 +11,7 @@ pub type FirewallGroup = Vec<PhysicalHost>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PhysicalHost {
|
||||
pub id: Id,
|
||||
pub category: HostCategory,
|
||||
pub network: Vec<NetworkInterface>,
|
||||
pub management: Arc<dyn ManagementInterface>,
|
||||
@@ -23,6 +24,7 @@ pub struct PhysicalHost {
|
||||
impl PhysicalHost {
|
||||
pub fn empty(category: HostCategory) -> Self {
|
||||
Self {
|
||||
id: Id::empty(),
|
||||
category,
|
||||
network: vec![],
|
||||
storage: vec![],
|
||||
@@ -128,6 +130,15 @@ impl Serialize for PhysicalHost {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PhysicalHost {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(new, Serialize)]
|
||||
pub struct ManualManagementInterface;
|
||||
|
||||
@@ -187,6 +198,8 @@ pub struct NetworkInterface {
|
||||
|
||||
#[cfg(test)]
|
||||
use harmony_macros::mac_address;
|
||||
|
||||
use crate::data::Id;
|
||||
#[cfg(test)]
|
||||
impl NetworkInterface {
|
||||
pub fn dummy() -> Self {
|
||||
@@ -304,6 +317,7 @@ mod tests {
|
||||
fn test_serialize_physical_host_with_hp_ilo() {
|
||||
// Create a PhysicalHost with HP iLO management
|
||||
let host = PhysicalHost {
|
||||
id: Id::empty(),
|
||||
category: HostCategory::Server,
|
||||
network: vec![NetworkInterface::dummy()],
|
||||
management: Arc::new(MockHPIlo {
|
||||
@@ -341,6 +355,7 @@ mod tests {
|
||||
fn test_serialize_physical_host_with_dell_idrac() {
|
||||
// Create a PhysicalHost with Dell iDRAC management
|
||||
let host = PhysicalHost {
|
||||
id: Id::empty(),
|
||||
category: HostCategory::Server,
|
||||
network: vec![NetworkInterface::dummy()],
|
||||
management: Arc::new(MockDellIdrac {
|
||||
@@ -375,6 +390,7 @@ mod tests {
|
||||
fn test_different_management_implementations_produce_valid_json() {
|
||||
// Create hosts with different management implementations
|
||||
let host1 = PhysicalHost {
|
||||
id: Id::empty(),
|
||||
category: HostCategory::Server,
|
||||
network: vec![],
|
||||
management: Arc::new(MockHPIlo {
|
||||
@@ -390,6 +406,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let host2 = PhysicalHost {
|
||||
id: Id::empty(),
|
||||
category: HostCategory::Server,
|
||||
network: vec![],
|
||||
management: Arc::new(MockDellIdrac {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
mod repository;
|
||||
pub use repository::*;
|
||||
|
||||
#[derive(Debug, new, Clone)]
|
||||
pub struct InventoryFilter {
|
||||
target: Vec<Filter>,
|
||||
|
||||
25
harmony/src/domain/inventory/repository.rs
Normal file
25
harmony/src/domain/inventory/repository.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::hardware::PhysicalHost;
|
||||
|
||||
/// Errors that can occur within the repository layer.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum RepoError {
|
||||
#[error("Database query failed: {0}")]
|
||||
QueryFailed(String),
|
||||
#[error("Data serialization failed: {0}")]
|
||||
Serialization(String),
|
||||
#[error("Data deserialization failed: {0}")]
|
||||
Deserialization(String),
|
||||
#[error("Could not connect to the database: {0}")]
|
||||
ConnectionFailed(String),
|
||||
}
|
||||
|
||||
// --- Trait and Implementation ---
|
||||
|
||||
/// Defines the contract for inventory persistence.
|
||||
#[async_trait]
|
||||
pub trait InventoryRepository: Send + Sync + 'static {
|
||||
async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError>;
|
||||
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError>;
|
||||
}
|
||||
1
harmony/src/infra/inventory/mod.rs
Normal file
1
harmony/src/infra/inventory/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod sqlite;
|
||||
69
harmony/src/infra/inventory/sqlite.rs
Normal file
69
harmony/src/infra/inventory/sqlite.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::{
|
||||
data::Id,
|
||||
hardware::PhysicalHost,
|
||||
inventory::{InventoryRepository, RepoError},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use log::{info, warn};
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
|
||||
/// A thread-safe, connection-pooled repository using SQLite.
|
||||
#[derive(Debug)]
|
||||
pub struct SqliteInventoryRepository {
|
||||
pool: Pool<Sqlite>,
|
||||
}
|
||||
|
||||
impl SqliteInventoryRepository {
|
||||
pub async fn new(database_url: &str) -> Result<Self, RepoError> {
|
||||
let pool = SqlitePool::connect(database_url)
|
||||
.await
|
||||
.map_err(|e| RepoError::ConnectionFailed(e.to_string()))?;
|
||||
|
||||
todo!("make sure migrations are up to date");
|
||||
info!(
|
||||
"SQLite inventory repository initialized at '{}'",
|
||||
database_url,
|
||||
);
|
||||
Ok(Self { pool })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl InventoryRepository for SqliteInventoryRepository {
|
||||
async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError> {
|
||||
let data = serde_json::to_vec(host).map_err(|e| RepoError::Serialization(e.to_string()))?;
|
||||
|
||||
let id = Id::default().to_string();
|
||||
let host_id = host.id.to_string();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO physical_hosts (id, version_id, data) VALUES (?, ?, ?)",
|
||||
host_id,
|
||||
id,
|
||||
data,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
info!("Saved new inventory version for host '{}'", host.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError> {
|
||||
let row = sqlx::query_as!(
|
||||
DbHost,
|
||||
r#"SELECT id, version_id, data as "data: Json<PhysicalHost>" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1"#,
|
||||
host_id
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
use sqlx::types::Json;
|
||||
struct DbHost {
|
||||
data: Json<PhysicalHost>,
|
||||
id: Id,
|
||||
version_id: Id,
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod executors;
|
||||
pub mod hp_ilo;
|
||||
pub mod intel_amt;
|
||||
pub mod inventory;
|
||||
pub mod opnsense;
|
||||
mod sqlx;
|
||||
|
||||
36
harmony/src/infra/sqlx.rs
Normal file
36
harmony/src/infra/sqlx.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use crate::inventory::RepoError;
|
||||
|
||||
impl From<sqlx::Error> for RepoError {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
match value {
|
||||
sqlx::Error::Configuration(_)
|
||||
| sqlx::Error::Io(_)
|
||||
| sqlx::Error::Tls(_)
|
||||
| sqlx::Error::Protocol(_)
|
||||
| sqlx::Error::PoolTimedOut
|
||||
| sqlx::Error::PoolClosed
|
||||
| sqlx::Error::WorkerCrashed => RepoError::ConnectionFailed(value.to_string()),
|
||||
sqlx::Error::InvalidArgument(_)
|
||||
| sqlx::Error::Database(_)
|
||||
| sqlx::Error::RowNotFound
|
||||
| sqlx::Error::TypeNotFound { .. }
|
||||
| sqlx::Error::ColumnIndexOutOfBounds { .. }
|
||||
| sqlx::Error::ColumnNotFound(_)
|
||||
| sqlx::Error::AnyDriverError(_)
|
||||
| sqlx::Error::Migrate(_)
|
||||
| sqlx::Error::InvalidSavePointStatement
|
||||
| sqlx::Error::BeginFailed => RepoError::QueryFailed(value.to_string()),
|
||||
sqlx::Error::Encode(_) => RepoError::Serialization(value.to_string()),
|
||||
sqlx::Error::Decode(_) | sqlx::Error::ColumnDecode { .. } => {
|
||||
RepoError::Deserialization(value.to_string())
|
||||
}
|
||||
_ => RepoError::QueryFailed(value.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for RepoError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
RepoError::Serialization(value.to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user