Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
d7a58e00da | |||
d2efc64487 | |||
1f23c303c1 | |||
1cc85bfa14 | |||
8bac357dc6 | |||
7154c01f7a | |||
813bf4e407 | |||
bf6f89c954 | |||
dbe87fedb6 | |||
67b24b33aa | |||
67ebbdaa19 | |||
6fd275802c | |||
9018adf998 | |||
3124697f10 | |||
30382fedef | |||
8910155524 | |||
7a68df41ac | |||
60ce51cd82 | |||
54fca8b1d3 | |||
aa876d9e5d | |||
88625ff986 | |||
428be53fff | |||
83cb815e76 | |||
755617c86f | |||
314bf8c327 | |||
![]() |
0fb3ccd506 | ||
![]() |
a6ef23fd69 |
40
.github/workflows/build.yml
vendored
40
.github/workflows/build.yml
vendored
@@ -9,11 +9,8 @@ on:
|
||||
branches: [ "development", "main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
REGISTRY: git.triggermeelmo.com
|
||||
IMAGE_NAME: donpat1to/watcher-agent
|
||||
TAG: ${{ github.ref == 'refs/heads/main' && 'latest' || github.ref == 'refs/heads/development' && 'development' || github.ref_type == 'tag' && github.ref_name || 'pr' }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -54,24 +51,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
setup-rust:
|
||||
name: Setup Rust Toolchain
|
||||
needs: detect-project
|
||||
if: ${{ !failure() && !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: x86_64-unknown-linux-gnu, x86_64-pc-windows-gnu
|
||||
components: rustfmt, clippy
|
||||
|
||||
test:
|
||||
name: Run Tests
|
||||
needs: [detect-project, setup-rust]
|
||||
needs: [detect-project]
|
||||
if: ${{ !failure() && !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -100,6 +82,8 @@ jobs:
|
||||
outputs:
|
||||
tag_name: ${{ steps.set_tag.outputs.tag_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Determine next semantic version tag
|
||||
id: set_tag
|
||||
run: |
|
||||
@@ -132,7 +116,7 @@ jobs:
|
||||
|
||||
# audit:
|
||||
# name: Security Audit
|
||||
# needs: [detect-project, setup-rust]
|
||||
# needs: [detect-project]
|
||||
# if: ${{ !failure() && !cancelled() }}
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
@@ -152,7 +136,7 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build (${{ matrix.target }})
|
||||
needs: [detect-project, setup-rust, test, audit]
|
||||
needs: [detect-project, test]
|
||||
if: ${{ !failure() && !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -225,14 +209,14 @@ jobs:
|
||||
path: dist/
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.AUTOMATION_USERNAME }}
|
||||
password: ${{ secrets.AUTOMATION_PASSWORD }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
@@ -248,8 +232,10 @@ jobs:
|
||||
|
||||
tag:
|
||||
name: Create Tag
|
||||
needs: [build, set-tag]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
needs: [docker-build, build, set-tag]
|
||||
if: |
|
||||
github.event_name == 'push' &&
|
||||
needs.docker-build.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@@ -19,9 +19,11 @@ nvml-wrapper = "0.11"
|
||||
nvml-wrapper-sys = "0.9.0"
|
||||
anyhow = "1.0.98"
|
||||
|
||||
# Docker .env loading
|
||||
config = "0.13"
|
||||
dotenvy = "0.15"
|
||||
regex = "1.11.3"
|
||||
|
||||
# Docker API access
|
||||
bollard = "0.19"
|
||||
futures-util = "0.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser", "pdh", "ifmib", "iphlpapi", "winerror" ,"wbemcli", "combaseapi"] }
|
||||
|
@@ -1,12 +1,16 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::hardware::HardwareInfo;
|
||||
use crate::models::{HeartbeatDto, IdResponse, MetricDto, RegistrationDto};
|
||||
use crate::models::{HeartbeatDto, IdResponse, MetricDto, RegistrationDto, ServerMessage, Acknowledgment};
|
||||
use crate::docker::serverclientcomm::handle_server_message;
|
||||
|
||||
use anyhow::Result;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::error::Error;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use bollard::Docker;
|
||||
|
||||
pub async fn register_with_server(
|
||||
base_url: &str,
|
||||
) -> Result<(i32, String), Box<dyn Error + Send + Sync>> {
|
||||
@@ -153,3 +157,85 @@ pub async fn send_metrics(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn listening_to_server(docker: &Docker, base_url: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let url = format!("{}/api/message", base_url);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
loop {
|
||||
// Get message from server
|
||||
let resp = client.get(&url).send().await;
|
||||
|
||||
match resp {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
match response.json::<ServerMessage>().await {
|
||||
Ok(msg) => {
|
||||
// Acknowledge receipt immediately
|
||||
if let Err(e) = send_acknowledgment(&client, base_url, &msg.message_id, "received", "Message received successfully").await {
|
||||
eprintln!("Failed to send receipt acknowledgment: {}", e);
|
||||
}
|
||||
|
||||
// Handle the message
|
||||
let result = handle_server_message(docker, msg.clone()).await;
|
||||
|
||||
// Send execution result acknowledgment
|
||||
let (status, details) = match result {
|
||||
Ok(_) => ("success", "Message executed successfully".to_string()),
|
||||
Err(e) => ("error", format!("Execution failed: {}", e)),
|
||||
};
|
||||
|
||||
if let Err(e) = send_acknowledgment(&client, base_url, &msg.message_id, status, &details).await {
|
||||
eprintln!("Failed to send execution acknowledgment: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to parse message: {}", e);
|
||||
}
|
||||
}
|
||||
} else if response.status() == reqwest::StatusCode::NO_CONTENT {
|
||||
// No new messages, continue polling
|
||||
println!("No new messages from server");
|
||||
} else {
|
||||
eprintln!("Server returned error status: {}", response.status());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to reach server: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Poll every 5 seconds (or use WebSocket for real-time)
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_acknowledgment(
|
||||
client: &reqwest::Client,
|
||||
base_url: &str,
|
||||
message_id: &str,
|
||||
status: &str,
|
||||
details: &str,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let ack_url = format!("{}/api/acknowledge", base_url);
|
||||
|
||||
let acknowledgment = Acknowledgment {
|
||||
message_id: message_id.to_string(),
|
||||
status: status.to_string(),
|
||||
details: details.to_string(),
|
||||
};
|
||||
|
||||
let response = client
|
||||
.post(&ack_url)
|
||||
.json(&acknowledgment)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
println!("Acknowledgment sent successfully for message {}", message_id);
|
||||
} else {
|
||||
eprintln!("Server returned error for acknowledgment: {}", response.status());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
97
WatcherAgent/src/docker/container.rs
Normal file
97
WatcherAgent/src/docker/container.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use crate::models::DockerContainer;
|
||||
|
||||
use bollard::query_parameters::{ListContainersOptions};
|
||||
use bollard::Docker;
|
||||
|
||||
|
||||
|
||||
|
||||
pub async fn get_available_container(docker: &Docker) -> Vec<DockerContainer> {
|
||||
println!("=== DOCKER CONTAINER LIST ===");
|
||||
|
||||
let options = Some(ListContainersOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let containers_list = match docker.list_containers(options).await {
|
||||
Ok(containers) => {
|
||||
println!("Available containers ({}):", containers.len());
|
||||
containers.into_iter()
|
||||
.filter_map(|container| {
|
||||
container.id.as_ref()?; // Skip if no ID
|
||||
|
||||
let id = container.id?;
|
||||
let short_id = if id.len() > 12 { &id[..12] } else { &id };
|
||||
|
||||
let name = container.names
|
||||
.and_then(|names| names.into_iter().next())
|
||||
.map(|name| name.trim_start_matches('/').to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let image = container.image
|
||||
.as_ref()
|
||||
.map(|img| img.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let status = container.status
|
||||
.as_ref()
|
||||
.map(|s| match s.to_lowercase().as_str() {
|
||||
s if s.contains("up") || s.contains("running") => "running".to_string(),
|
||||
s if s.contains("exited") || s.contains("stopped") => "stopped".to_string(),
|
||||
_ => s.to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
println!(" - ID: {}, Image: {:?}, Name: {}", short_id, container.image, name);
|
||||
|
||||
Some(DockerContainer {
|
||||
ID: short_id.to_string(),
|
||||
image,
|
||||
Name: name,
|
||||
Status: status,
|
||||
_net_in: 0.0,
|
||||
_net_out: 0.0,
|
||||
_cpu_load: 0.0,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to list containers: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
containers_list
|
||||
}
|
||||
|
||||
pub fn extract_container_id(line: &str) -> Option<String> {
|
||||
// Split by slashes and take the last part
|
||||
if let Some(last_part) = line.split('/').last() {
|
||||
let last_part = last_part.trim();
|
||||
|
||||
// Remove common suffixes
|
||||
let clean_id = last_part
|
||||
.trim_end_matches(".scope")
|
||||
.trim_start_matches("docker-")
|
||||
.trim_start_matches("crio-")
|
||||
.trim_start_matches("containerd-");
|
||||
|
||||
// Check if it looks like a container ID (hex characters)
|
||||
if clean_id.chars().all(|c| c.is_ascii_hexdigit()) && clean_id.len() >= 12 {
|
||||
return Some(clean_id.to_string());
|
||||
}
|
||||
|
||||
// If it's not pure hex, try to extract hex sequence
|
||||
let hex_part: String = clean_id.chars()
|
||||
.take_while(|c| c.is_ascii_hexdigit())
|
||||
.collect();
|
||||
|
||||
if hex_part.len() >= 12 {
|
||||
return Some(hex_part);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
19
WatcherAgent/src/docker/mod.rs
Normal file
19
WatcherAgent/src/docker/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
pub mod container;
|
||||
pub mod serverclientcomm;
|
||||
|
||||
use std::error::Error;
|
||||
use crate::models::DockerContainer;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DockerInfo {
|
||||
pub number: Option<u16>,
|
||||
pub net_in_total: Option<f64>,
|
||||
pub net_out_total: Option<f64>,
|
||||
pub dockers: Option<Vec<DockerContainer>>,
|
||||
}
|
||||
|
||||
impl DockerInfo {
|
||||
pub async fn collect() -> Result<Self, Box<dyn Error + Send + Sync>> {
|
||||
Ok(Self { number: None, net_in_total: None, net_out_total: None, dockers: None })
|
||||
}
|
||||
}
|
152
WatcherAgent/src/docker/serverclientcomm.rs
Normal file
152
WatcherAgent/src/docker/serverclientcomm.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use crate::models::ServerMessage;
|
||||
use crate::docker::container::{extract_container_id, get_available_container};
|
||||
|
||||
use std::error::Error;
|
||||
use bollard::Docker;
|
||||
use bollard::query_parameters::{CreateImageOptions, RestartContainerOptions, InspectContainerOptions};
|
||||
use futures_util::StreamExt;
|
||||
|
||||
pub async fn handle_server_message(docker: &Docker, msg: ServerMessage) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let msg = msg.clone();
|
||||
println!("Handling server message: {:?}", msg);
|
||||
|
||||
// Handle different message types
|
||||
match msg.message_type.as_str() {
|
||||
"update_image" => {
|
||||
if let Some(image_name) = msg.data.get("image").and_then(|v| v.as_str()) {
|
||||
println!("Received update command for image: {}", image_name);
|
||||
// Call your update_docker_image function here
|
||||
update_docker_image(docker, image_name).await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Missing image name in update message".into())
|
||||
}
|
||||
}
|
||||
"restart_container" => {
|
||||
println!("Received restart container command");
|
||||
// Call your restart_container function here
|
||||
restart_container(docker).await?;
|
||||
Ok(())
|
||||
}
|
||||
"stop_agent" => {
|
||||
println!("Received stop agent command");
|
||||
// Implement graceful shutdown
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown message type: {}", msg.message_type);
|
||||
Err(format!("Unknown message type: {}", msg.message_type).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_docker_image(docker: &Docker, image: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
println!("Updating to {}", image);
|
||||
|
||||
// 1. Pull new image
|
||||
let mut stream = docker.create_image(
|
||||
Some(CreateImageOptions {
|
||||
from_image: Some(image.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Use the stream with proper trait bounds
|
||||
while let Some(result) = StreamExt::next(&mut stream).await {
|
||||
match result {
|
||||
Ok(progress) => {
|
||||
if let Some(status) = progress.status {
|
||||
println!("Pull status: {}", status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error pulling image: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Restart the current container
|
||||
let _ = restart_container(docker).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_current_image(docker: &Docker) -> Result<Option<String>, Box<dyn Error + Send + Sync>> {
|
||||
// First, let's debug the environment
|
||||
get_available_container(docker).await;
|
||||
|
||||
// Get the current container ID from /proc/self/cgroup
|
||||
let container_id = match std::fs::read_to_string("/proc/self/cgroup") {
|
||||
Ok(content) => {
|
||||
let mut found_id = None;
|
||||
println!("Searching cgroup for container ID...");
|
||||
|
||||
for line in content.lines() {
|
||||
println!("Checking line: {}", line);
|
||||
|
||||
// Look for container runtime indicators
|
||||
if line.contains("docker") || line.contains("crio") || line.contains("containerd") {
|
||||
if let Some(id) = extract_container_id(line) {
|
||||
println!("Found potential container ID: {}", id);
|
||||
found_id = Some(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
found_id
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error reading cgroup file: {}", e);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let container_id = match container_id {
|
||||
Some(id) if !id.is_empty() => {
|
||||
println!("Using container ID: '{}'", id);
|
||||
id
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Could not find valid container ID in cgroup");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
// Try to inspect the container
|
||||
println!("Attempting to inspect container with ID: '{}'", container_id);
|
||||
|
||||
match docker.inspect_container(&container_id, None::<InspectContainerOptions>).await {
|
||||
Ok(container_info) => {
|
||||
if let Some(config) = container_info.config {
|
||||
if let Some(image) = config.image {
|
||||
println!("Successfully found image: {}", image);
|
||||
return Ok(Some(image));
|
||||
}
|
||||
}
|
||||
eprintln!("Container inspected but no image found in config");
|
||||
Ok(Some("unknown".to_string()))
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error inspecting container: {}", e);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn restart_container(docker: &Docker) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
if let Ok(container_id) = std::env::var("HOSTNAME") {
|
||||
println!("Restarting container {}", container_id);
|
||||
if let Err(e) = docker.restart_container(&container_id, Some(RestartContainerOptions { signal: None, t: Some(0) }))
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to restart container: {}", e);
|
||||
}
|
||||
} else {
|
||||
eprintln!("No container ID found (HOSTNAME not set?)");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,130 +1,138 @@
|
||||
use std::error::Error;
|
||||
use crate::models::DiskInfoDetailed;
|
||||
|
||||
use std::error::Error;
|
||||
use anyhow::Result;
|
||||
use sysinfo::DiskUsage;
|
||||
use sysinfo::{Component, Components, Disk, Disks, System};
|
||||
use sysinfo::{Component, Components, Disk, Disks};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct DiskInfo {
|
||||
pub total: Option<f64>,
|
||||
pub used: Option<f64>,
|
||||
pub free: Option<f64>,
|
||||
pub total_size: Option<f64>,
|
||||
pub total_used: Option<f64>,
|
||||
pub total_available: Option<f64>,
|
||||
pub total_usage: Option<f64>,
|
||||
pub detailed_info: Vec<DiskInfoDetailed>,
|
||||
}
|
||||
|
||||
pub async fn get_disk_info() -> Result<DiskInfo> {
|
||||
pub async fn get_disk_info() -> Result<DiskInfo, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let _disk_types = [
|
||||
sysinfo::DiskKind::HDD,
|
||||
sysinfo::DiskKind::SSD,
|
||||
sysinfo::DiskKind::Unknown(0),
|
||||
];
|
||||
|
||||
let (_, _, _, _) = get_disk_utitlization().unwrap();
|
||||
|
||||
let mut total = 0;
|
||||
let mut used = 0;
|
||||
|
||||
let mut detailed_info = Vec::new();
|
||||
|
||||
// Collect detailed disk information
|
||||
for disk in disks.list() {
|
||||
if disk.total_space() > 100 * 1024 * 1024 {
|
||||
// > 100MB
|
||||
total += disk.total_space();
|
||||
used += disk.total_space() - disk.available_space();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DiskInfo {
|
||||
total: Some(total as f64),
|
||||
used: Some(used as f64),
|
||||
free: Some((total - used) as f64),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_disk_utitlization() -> Result<(f64, f64, f64, f64), Box<dyn Error>> {
|
||||
let mut sys = System::new();
|
||||
sys.refresh_all();
|
||||
let mut count = 0;
|
||||
|
||||
let mut total_size = 0u64;
|
||||
let mut total_used = 0u64;
|
||||
let mut total_available = 0u64;
|
||||
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
for disk in disks.list() {
|
||||
// Only print disks with known kind
|
||||
if disk.kind() == sysinfo::DiskKind::Unknown(0) {
|
||||
continue;
|
||||
}
|
||||
println!(
|
||||
"Disk_Name: {:?}:\n---- Disk_Kind: {},\n---- Total: {},\n---- Available: {},\n---- Used: {}, \n---- Mount_Point: {:?}",
|
||||
disk.name(),
|
||||
disk.kind(),
|
||||
disk.total_space(),
|
||||
disk.available_space(),
|
||||
disk.total_space() - disk.available_space(),
|
||||
disk.mount_point()
|
||||
);
|
||||
|
||||
let disk_used = disk.total_space() - disk.available_space();
|
||||
detailed_info.push(DiskInfoDetailed {
|
||||
disk_name: disk.name().to_string_lossy().into_owned(),
|
||||
disk_kind: format!("{:?}", disk.kind()),
|
||||
disk_total_space: disk.total_space() as f64,
|
||||
disk_available_space: disk.available_space() as f64,
|
||||
disk_used_space: disk_used as f64,
|
||||
disk_mount_point: disk.mount_point().to_string_lossy().into_owned(),
|
||||
component_disk_label: String::new(),
|
||||
component_disk_temperature: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
// Get component temperatures
|
||||
let components = Components::new_with_refreshed_list();
|
||||
for component in &components {
|
||||
if let Some(temperature) = component.temperature() {
|
||||
println!(
|
||||
"Component_Label: {}, Temperature: {}°C",
|
||||
component.label(),
|
||||
temperature
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Berechnungen
|
||||
let total_size = if count > 0 {
|
||||
total_size as f64 // in Bytes
|
||||
} else {
|
||||
// Fallback: Versuche df unter Linux
|
||||
println!("Fallback: Using 'df' command to get disk info.");
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
if let Ok(output) = Command::new("df")
|
||||
.arg("-B1")
|
||||
.arg("--output=size,used")
|
||||
.output()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines().skip(1) {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() == 2 {
|
||||
if let (Ok(size), Ok(used)) =
|
||||
(parts[0].parse::<u64>(), parts[1].parse::<u64>())
|
||||
{
|
||||
total_size += size;
|
||||
total_used += used;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
// Update detailed info with temperature data if it matches a disk component
|
||||
for disk_info in &mut detailed_info {
|
||||
if component.label().contains(&disk_info.disk_name) {
|
||||
disk_info.component_disk_label = component.label().to_string();
|
||||
disk_info.component_disk_temperature = temperature;
|
||||
}
|
||||
total_size as f64 // in Bytes
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
0.0
|
||||
}
|
||||
|
||||
// Calculate totals (only disks > 100MB)
|
||||
let (total_size, total_used, total_available) = calculate_disk_totals(&disks);
|
||||
|
||||
let (total_size, total_used, total_available, total_usage) = if total_size > 0.0 {
|
||||
(total_size, total_used, total_available, (total_used / total_size) * 100.0)
|
||||
} else {
|
||||
match get_disk_info_fallback() {
|
||||
Ok(fallback_data) => fallback_data,
|
||||
Err(_) => (0.0, 0.0, 0.0, 0.0), // Default values if fallback fails
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DiskInfo {
|
||||
total_size: if total_size > 0.0 { Some(total_size) } else { None },
|
||||
total_used: if total_used > 0.0 { Some(total_used) } else { None },
|
||||
total_available: if total_available > 0.0 { Some(total_available) } else { None },
|
||||
total_usage: if total_usage > 0.0 { Some(total_usage) } else { None },
|
||||
detailed_info,
|
||||
})
|
||||
}
|
||||
|
||||
let usage = if total_size > 0.0 {
|
||||
fn calculate_disk_totals(disks: &Disks) -> (f64, f64, f64) {
|
||||
let mut total_size = 0u64;
|
||||
let mut total_used = 0u64;
|
||||
let mut total_available = 0u64;
|
||||
|
||||
for disk in disks.list() {
|
||||
if disk.total_space() > 100 * 1024 * 1024 { // > 100MB
|
||||
total_size += disk.total_space();
|
||||
total_available += disk.available_space();
|
||||
total_used += disk.total_space() - disk.available_space();
|
||||
}
|
||||
}
|
||||
|
||||
(total_size as f64, total_used as f64, total_available as f64)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_disk_info_fallback() -> Result<(f64, f64, f64, f64), Box<dyn Error + Send + Sync>> {
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("df")
|
||||
.arg("-B1")
|
||||
.arg("--output=size,used,avail")
|
||||
.output()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut total_size = 0u64;
|
||||
let mut total_used = 0u64;
|
||||
let mut total_available = 0u64;
|
||||
let mut count = 0;
|
||||
|
||||
for line in stdout.lines().skip(1) {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 3 {
|
||||
if let (Ok(size), Ok(used), Ok(avail)) = (
|
||||
parts[0].parse::<u64>(),
|
||||
parts[1].parse::<u64>(),
|
||||
parts[2].parse::<u64>(),
|
||||
) {
|
||||
total_size += size;
|
||||
total_used += used;
|
||||
total_available += avail;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let usage = if total_size > 0 {
|
||||
(total_used as f64 / total_size as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
Ok((total_size as f64, total_used as f64, total_available as f64, usage))
|
||||
}
|
||||
|
||||
Ok((
|
||||
total_size,
|
||||
total_used as f64,
|
||||
total_available as f64,
|
||||
usage as f64,
|
||||
)) // Disk-Temp bleibt 0.0 ohne spezielle Hardware
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn get_disk_info_fallback() -> Result<(f64, f64, f64, f64), Box<dyn Error + Send + Sync>> {
|
||||
Ok((0.0, 0.0, 0.0, 0.0))
|
||||
}
|
||||
|
||||
pub fn _get_disk_temp_for_component(component: &Component) -> Option<f64> {
|
||||
|
@@ -5,6 +5,7 @@ pub mod api;
|
||||
pub mod hardware;
|
||||
pub mod metrics;
|
||||
pub mod models;
|
||||
pub mod docker;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
@@ -12,6 +13,7 @@ use std::marker::Send;
|
||||
use std::marker::Sync;
|
||||
use std::result::Result;
|
||||
use tokio::task::JoinHandle;
|
||||
use bollard::Docker;
|
||||
|
||||
async fn flatten<T>(
|
||||
handle: JoinHandle<Result<T, Box<dyn Error + Send + Sync>>>,
|
||||
@@ -25,14 +27,30 @@ async fn flatten<T>(
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
// Initialize Docker client
|
||||
let docker = Docker::connect_with_local_defaults()
|
||||
.map_err(|e| format!("Failed to connect to Docker: {}", e))?;
|
||||
|
||||
// Get current image version
|
||||
let client_version = match docker::serverclientcomm::get_current_image(&docker).await {
|
||||
Ok(Some(version)) => version,
|
||||
Ok(None) => {
|
||||
eprintln!("Warning: No image version found");
|
||||
"unknown".to_string()
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Could not get current image version: {}", e);
|
||||
"unknown".to_string()
|
||||
}
|
||||
};
|
||||
println!("Client Version: {}", client_version);
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
// args[0] is the binary name, args[1] is the first actual argument
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} <server-url>", args[0]);
|
||||
return Err("Missing server URL argument".into());
|
||||
}
|
||||
|
||||
let server_url = &args[1];
|
||||
println!("Server URL: {:?}", server_url);
|
||||
|
||||
@@ -46,6 +64,13 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
};
|
||||
|
||||
// Start background tasks
|
||||
// Start server listening for commands
|
||||
let listening_handle = tokio::spawn({
|
||||
let docker = docker.clone();
|
||||
let server_url = server_url.to_string();
|
||||
async move { api::listening_to_server(&docker, &server_url).await }
|
||||
});
|
||||
|
||||
// Start heartbeat in background
|
||||
let heartbeat_handle = tokio::spawn({
|
||||
let ip = ip.clone();
|
||||
@@ -64,14 +89,16 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
}
|
||||
});
|
||||
|
||||
// Warte auf beide Tasks und prüfe explizit auf Fehler
|
||||
let (heartbeat_handle, metrics_handle) =
|
||||
tokio::try_join!(flatten(heartbeat_handle), flatten(metrics_handle))?;
|
||||
// Wait for all tasks and check for errors
|
||||
let (listening_result, heartbeat_result, metrics_result) = tokio::try_join!(
|
||||
flatten(listening_handle),
|
||||
flatten(heartbeat_handle),
|
||||
flatten(metrics_handle)
|
||||
)?;
|
||||
|
||||
let (heartbeat, metrics) = (heartbeat_handle, metrics_handle);
|
||||
println!(
|
||||
"All tasks completed successfully: {:?}, {:?}.",
|
||||
heartbeat, metrics
|
||||
"All tasks completed: listening={:?}, heartbeat={:?}, metrics={:?}",
|
||||
listening_result, heartbeat_result, metrics_result
|
||||
);
|
||||
|
||||
println!("All tasks completed successfully.");
|
||||
|
@@ -62,8 +62,8 @@ impl Collector {
|
||||
gpu_vram_usage: hardware.gpu.vram_used.unwrap_or_default(),
|
||||
ram_load: hardware.memory.used.unwrap_or_default(),
|
||||
ram_size: hardware.memory.total.unwrap_or_default(),
|
||||
disk_size: hardware.disk.total.unwrap_or_default(),
|
||||
disk_usage: hardware.disk.used.unwrap_or_default(),
|
||||
disk_size: hardware.disk.total_size.unwrap_or_default(),
|
||||
disk_usage: hardware.disk.total_used.unwrap_or_default(),
|
||||
disk_temp: 0.0, // not supported
|
||||
net_rx: hardware.network.rx_rate.unwrap_or_default(),
|
||||
net_tx: hardware.network.tx_rate.unwrap_or_default(),
|
||||
|
@@ -51,6 +51,18 @@ pub struct MetricDto {
|
||||
pub net_tx: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct DiskInfoDetailed {
|
||||
pub disk_name: String,
|
||||
pub disk_kind: String,
|
||||
pub disk_total_space: f64,
|
||||
pub disk_available_space: f64,
|
||||
pub disk_used_space: f64,
|
||||
pub disk_mount_point: String,
|
||||
pub component_disk_label: String,
|
||||
pub component_disk_temperature: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct IdResponse {
|
||||
pub id: i32,
|
||||
@@ -72,3 +84,29 @@ pub struct HardwareDto {
|
||||
pub ram_size: f64,
|
||||
pub ip_address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ServerMessage {
|
||||
// Define your message structure here
|
||||
pub message_type: String,
|
||||
pub data: serde_json::Value,
|
||||
pub message_id: String, // Add an ID for acknowledgment
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct Acknowledgment {
|
||||
pub message_id: String,
|
||||
pub status: String, // "success" or "error"
|
||||
pub details: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct DockerContainer {
|
||||
pub ID: String,
|
||||
pub image: String,
|
||||
pub Name: String,
|
||||
pub Status: String, // "running";"stopped";others
|
||||
pub _net_in: f64,
|
||||
pub _net_out: f64,
|
||||
pub _cpu_load: f64,
|
||||
}
|
23
docker-compose.yaml
Normal file
23
docker-compose.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
watcher-agent:
|
||||
image: git.triggermeelmo.com/donpat1to/watcher-agent:development
|
||||
container_name: watcher-agent
|
||||
restart: always
|
||||
privileged: true # Grants full hardware access (use with caution)
|
||||
env_file: .env
|
||||
pid: "host"
|
||||
volumes:
|
||||
# Mount critical system paths for hardware monitoring
|
||||
- /sys:/sys:ro # CPU/GPU temps, sensors
|
||||
- /proc:/proc # Process/CPU stats
|
||||
- /dev:/dev:ro # Disk/GPU device access
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Docker API access
|
||||
- /:/root:ro # Access to for df-command
|
||||
# Application volumes
|
||||
- ./config:/app/config:ro
|
||||
- ./logs:/app/logs
|
||||
network_mode: host # Uses host network (for correct IP/interface detection)
|
||||
healthcheck:
|
||||
test: ["CMD", "/usr/local/bin/WatcherAgent", "healthcheck"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
Reference in New Issue
Block a user