Private
Public Access
1
0

feat: populate os_family, os_name, arch, agent_version from health poller and enrollment
Some checks failed
CI Pipeline / Rust Format Check (push) Failing after 2s
CI Pipeline / Clippy Lints (push) Failing after 1s
CI Pipeline / Rust Unit Tests (push) Failing after 2s
CI Pipeline / Security Audit (push) Failing after 2s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 3s
CI Pipeline / Build .deb & Release (push) Has been skipped

- health_poller: persist agent_version from HealthData.version
- health_poller: call /system/info to update os_family, os_name, arch
- enrollment: set os_family and arch from os_details during approval
- enrollment: build os_name from os+os_version when name field absent
- COALESCE in UPDATE preserves existing values when new data unavailable
- version bump 0.1.7 -> 0.1.8
This commit is contained in:
2026-05-21 00:09:57 +00:00
parent f70c5e53f9
commit 6c72dc3ac6
57 changed files with 119 additions and 42 deletions

0
crates/pm-worker/src/agent_loader.rs Normal file → Executable file
View File

0
crates/pm-worker/src/audit_verifier.rs Normal file → Executable file
View File

0
crates/pm-worker/src/email.rs Normal file → Executable file
View File

0
crates/pm-worker/src/health_check_poller.rs Normal file → Executable file
View File

View File

@ -2,7 +2,8 @@
//!
//! Polls every host via the agent `/health` endpoint on each tick of
//! `health_poll_interval_secs`, with bounded concurrency controlled by a
//! [`tokio::sync::Semaphore`].
//! [`tokio::sync::Semaphore`]. Also calls `/system/info` to refresh
//! `os_family`, `os_name`, `arch`, and `agent_version` in the hosts table.
use std::sync::Arc;
@ -114,6 +115,9 @@ pub async fn run_health_poller(pool: PgPool, config: Arc<AppConfig>) {
}
/// Poll a single host, persist the result, and return the determined status.
///
/// Also updates `agent_version` from the health response and
/// `os_family`/`os_name`/`arch` from the `/system/info` endpoint when available.
async fn poll_host_health(
pool: PgPool,
host: HostRow,
@ -121,8 +125,8 @@ async fn poll_host_health(
client_key: &[u8],
ca_cert: &[u8],
) -> HostHealthStatus {
// Determine status and optional health payload.
let (status, payload) = match AgentClient::new(
// Determine status, payload, agent version, and optional system info.
let (status, payload, agent_version, sys_info) = match AgentClient::new(
&host.ip_address,
host.agent_port as u16,
client_cert,
@ -138,34 +142,60 @@ async fn poll_host_health(
(
HostHealthStatus::Unreachable,
serde_json::Value::Object(Default::default()),
None,
None,
)
},
Ok(client) => match client.health().await {
Ok(data) => {
let payload = serde_json::to_value(&data).unwrap_or_default();
(HostHealthStatus::Healthy, payload)
},
Err(AgentClientError::Timeout) => {
tracing::warn!(host_id = %host.id, "Health poller: agent timed out");
(
HostHealthStatus::Unreachable,
serde_json::Value::Object(Default::default()),
)
},
Err(AgentClientError::Connect(_)) => {
tracing::warn!(host_id = %host.id, "Health poller: agent connection refused");
(
HostHealthStatus::Unreachable,
serde_json::Value::Object(Default::default()),
)
},
Err(e) => {
tracing::warn!(host_id = %host.id, error = %e, "Health poller: agent error");
(
HostHealthStatus::Degraded,
serde_json::Value::Object(Default::default()),
)
},
Ok(client) => {
let (status, payload, version) = match client.health().await {
Ok(data) => {
let payload = serde_json::to_value(&data).unwrap_or_default();
(HostHealthStatus::Healthy, payload, Some(data.version))
},
Err(AgentClientError::Timeout) => {
tracing::warn!(host_id = %host.id, "Health poller: agent timed out");
(
HostHealthStatus::Unreachable,
serde_json::Value::Object(Default::default()),
None,
)
},
Err(AgentClientError::Connect(_)) => {
tracing::warn!(host_id = %host.id, "Health poller: agent connection refused");
(
HostHealthStatus::Unreachable,
serde_json::Value::Object(Default::default()),
None,
)
},
Err(e) => {
tracing::warn!(host_id = %host.id, error = %e, "Health poller: agent error");
(
HostHealthStatus::Degraded,
serde_json::Value::Object(Default::default()),
None,
)
},
};
// Try to fetch system info for OS/arch details (best-effort).
let sys_info = if status != HostHealthStatus::Unreachable {
match client.system_info().await {
Ok(info) => Some(info),
Err(e) => {
tracing::debug!(
host_id = %host.id,
error = %e,
"Health poller: failed to get system info (non-fatal)"
);
None
},
}
} else {
None
};
(status, payload, version, sys_info)
},
};
@ -185,16 +215,30 @@ async fn poll_host_health(
tracing::error!(host_id = %host.id, error = %e, "Health poller: failed to insert health data");
}
// Update hosts table.
// Build OS name from system info components (e.g. "Ubuntu 24.04").
let os_name_from_sysinfo = sys_info
.as_ref()
.map(|i| format!("{} {}", i.os, i.os_version));
// Update hosts table with health status, agent version, and OS details.
// COALESCE preserves existing values when new data is unavailable.
if let Err(e) = sqlx::query(
r#"
UPDATE hosts
SET health_status = $2, last_health_at = NOW()
SET health_status = $2, last_health_at = NOW(),
agent_version = COALESCE($3, agent_version),
os_family = COALESCE($4, os_family),
os_name = COALESCE($5, os_name),
arch = COALESCE($6, arch)
WHERE id = $1
"#,
)
.bind(host.id)
.bind(&status)
.bind(&agent_version)
.bind(sys_info.as_ref().map(|i| i.os.as_str()))
.bind(os_name_from_sysinfo)
.bind(sys_info.as_ref().map(|i| i.architecture.as_str()))
.execute(&pool)
.await
{

0
crates/pm-worker/src/job_executor.rs Normal file → Executable file
View File

0
crates/pm-worker/src/main.rs Normal file → Executable file
View File

0
crates/pm-worker/src/maintenance_scheduler.rs Normal file → Executable file
View File

0
crates/pm-worker/src/patch_poller.rs Normal file → Executable file
View File

0
crates/pm-worker/src/refresh_listener.rs Normal file → Executable file
View File

0
crates/pm-worker/src/ws_relay.rs Normal file → Executable file
View File