32 Commits

Author SHA1 Message Date
a095444222 fixed stuck in loop
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m8s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m26s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m47s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 4s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m16s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-09 11:14:13 +02:00
5e7bc3df54 added debugging
Some checks failed
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 5s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m12s
Rust Cross-Platform Build / Set Tag Name (push) Has been cancelled
Rust Cross-Platform Build / Build and Push Docker Image (push) Has been cancelled
Rust Cross-Platform Build / Create Tag (push) Has been cancelled
Rust Cross-Platform Build / Workflow Summary (push) Has been cancelled
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Has been cancelled
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Has been cancelled
2025-10-09 11:10:53 +02:00
1c7a169956 added container broadcasting
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 5s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m14s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 3m18s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 4m4s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 5s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m19s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-09 10:59:21 +02:00
c7bce926e9 added docker registration dto 2025-10-09 10:39:52 +02:00
711083daa0 fixed server message handle 2025-10-06 13:01:03 +02:00
06cec6ff9f added Option to data structs 2025-10-06 12:43:15 +02:00
a7cae5e93f added docker metrics
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m3s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m56s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m37s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 5s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m12s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-05 22:43:48 +02:00
66428863e6 moved stats into own folder
Some checks failed
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Failing after 1m9s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Has been skipped
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Has been skipped
Rust Cross-Platform Build / Set Tag Name (push) Has been skipped
Rust Cross-Platform Build / Build and Push Docker Image (push) Has been skipped
Rust Cross-Platform Build / Create Tag (push) Has been skipped
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
2025-10-05 15:21:24 +02:00
b35cac0dbe changed Dtos and Docker structs 2025-10-04 22:05:28 +02:00
bb55b46c34 fixed image version detectionr
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m4s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m49s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m35s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 4s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m8s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 1s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-04 20:38:20 +02:00
76d54cb433 fixed image detection in client container
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m8s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 3m0s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m43s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 4s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m8s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 1s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-04 20:25:35 +02:00
4dc8c56a5c moved eprintln to println 2025-10-04 17:11:40 +02:00
c81040b16b moved github to gitea
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 3s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m2s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m45s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m36s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 5s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m2s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-04 13:22:14 +02:00
68c307e258 added debug print
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m6s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m52s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m40s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 5s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m13s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 1s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-04 11:57:06 +02:00
5d00f072d8 fixed syntax
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 5s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m38s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m20s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 2m36s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 4s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m3s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 1s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-04 01:21:03 +02:00
9072e253ec removed else handling from set-tag
Some checks failed
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 6s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m44s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Has been cancelled
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Has been cancelled
2025-10-04 00:56:57 +02:00
063ad113d7 added staging to branches for cicd 2025-10-04 00:55:43 +02:00
097be0f555 added staging to branches for cicd 2025-10-04 00:55:36 +02:00
c7b8e24a54 excluding builds on subbranches
Some checks failed
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m26s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Has been cancelled
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Has been cancelled
2025-10-04 00:50:00 +02:00
fb1d016b36 excluding builds on subbranches
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m36s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m21s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 2m38s
2025-10-04 00:42:03 +02:00
89d58e9696 moved set tag before docker build
Some checks failed
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m2s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m44s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m26s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 3s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 3m15s
Rust Cross-Platform Build / Create Tag (push) Failing after 6s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
2025-10-04 00:31:30 +02:00
45c6e8180c added only tagging on main dev and staging 2025-10-04 00:23:52 +02:00
c395885bbd removed pull request
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 5s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m4s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m49s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m31s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m2s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
2025-10-04 00:01:13 +02:00
75494e2a71 changed api endpoints register and hardware-infor
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 5s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 5s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m16s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m19s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m44s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 3m6s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 4s
2025-10-03 23:53:34 +02:00
8e6e291f6a changed api endpoints register and hardware-infor 2025-10-03 23:53:26 +02:00
bfeb43f38a added docker management
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m2s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m18s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m25s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m0s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-03 20:42:35 +02:00
2f5e2391f7 removed useless if statemnt 2025-10-03 14:30:41 +02:00
820952089a removed useless if statemnt 2025-10-03 14:19:48 +02:00
c745125f20 check if checks are needed or needs is enough
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 5s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m4s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m47s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m30s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m20s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-03 13:47:00 +02:00
758fa7608f added toolcache
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 5s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 4s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m10s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 3m1s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m47s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m8s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 5s
2025-10-03 13:17:48 +02:00
ee6b947f29 update
All checks were successful
Rust Cross-Platform Build / Detect Rust Project (push) Successful in 4s
Rust Cross-Platform Build / Set Tag Name (push) Successful in 5s
Rust Cross-Platform Build / Run Tests (push) Successful in 1m23s
Rust Cross-Platform Build / Build (x86_64-unknown-linux-gnu) (push) Successful in 2m54s
Rust Cross-Platform Build / Build (x86_64-pc-windows-gnu) (push) Successful in 3m35s
Rust Cross-Platform Build / Build and Push Docker Image (push) Successful in 2m4s
Rust Cross-Platform Build / Workflow Summary (push) Successful in 2s
Rust Cross-Platform Build / Create Tag (push) Successful in 4s
2025-10-03 12:22:12 +02:00
18dd1ef528 keine verdreckte doc 2025-10-02 00:38:23 +02:00
12 changed files with 1052 additions and 338 deletions

View File

@@ -3,10 +3,8 @@ name: Rust Cross-Platform Build
on:
workflow_dispatch:
push:
branches: [ "development", "main", "feature/*", "bugfix/*", "enhancement/*" ]
branches: [ "development", "main", "staging" ]
tags: [ "v*.*.*" ]
pull_request:
branches: [ "development", "main" ]
env:
REGISTRY: git.triggermeelmo.com
@@ -76,44 +74,6 @@ jobs:
# working-directory: ${{ needs.detect-project.outputs.project-dir }}
# run: cargo clippy -- -D warnings
set-tag:
name: Set Tag Name
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.set_tag.outputs.tag_name }}
steps:
- uses: actions/checkout@v4
- name: Determine next semantic version tag
id: set_tag
run: |
git fetch --tags
# Find latest tag matching vX.Y.Z
latest_tag=$(git tag --list 'v*.*.*' --sort=-v:refname | head -n 1)
if [[ -z "$latest_tag" ]]; then
major=0
minor=0
patch=0
else
version="${latest_tag#v}"
IFS='.' read -r major minor patch <<< "$version"
fi
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
major=$((major + 1))
minor=0
patch=0
elif [[ "${GITHUB_REF}" == "refs/heads/development" ]]; then
minor=$((minor + 1))
patch=0
else
patch=$((patch + 1))
fi
new_tag="v${major}.${minor}.${patch}"
echo "tag_name=${new_tag}" >> $GITHUB_OUTPUT
# audit:
# name: Security Audit
# needs: [detect-project]
@@ -189,37 +149,65 @@ jobs:
path: |
${{ needs.detect-project.outputs.project-dir }}/target/${{ matrix.target }}/release/${{ needs.detect-project.outputs.project-name }}${{ matrix.os == 'windows' && '.exe' || '' }}
- name: Build documentation
working-directory: ${{ needs.detect-project.outputs.project-dir }}
set-tag:
name: Set Tag Name
needs: [detect-project, build]
#if: ${{ !failure() && !cancelled() && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.set_tag.outputs.tag_name }}
should_tag: ${{ steps.set_tag.outputs.should_tag }}
steps:
- uses: actions/checkout@v4
- name: Determine next semantic version tag
id: set_tag
run: |
cargo doc --no-deps --all-features
echo "Documentation built in: $PWD/target/doc"
ls -la target/doc/
- name: Prepare documentation for deployment
working-directory: ${{ needs.detect-project.outputs.project-dir }}
run: |
# Create a temporary directory for deployment
mkdir -p ../docs-deploy
cp -r target/doc/* ../docs-deploy/
# Create redirect index if needed
echo '<meta http-equiv="refresh" content="0; url=${{ needs.detect-project.outputs.project-name }}/index.html">' > ../docs-deploy/index.html
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ../docs-deploy
publish_branch: gh-pages
force_orphan: true
git fetch --tags
# Find latest tag matching vX.Y.Z
latest_tag=$(git tag --list 'v*.*.*' --sort=-v:refname | head -n 1)
if [[ -z "$latest_tag" ]]; then
major=0
minor=0
patch=0
else
version="${latest_tag#v}"
IFS='.' read -r major minor patch <<< "$version"
fi
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
major=$((major + 1))
minor=0
patch=0
new_tag="v${major}.${minor}.${patch}"
echo "tag_name=${new_tag}" >> $GITHUB_OUTPUT
echo "should_tag=true" >> $GITHUB_OUTPUT
echo "Creating new major version tag: ${new_tag}"
elif [[ "${GITHUB_REF}" == "refs/heads/development" ]]; then
minor=$((minor + 1))
patch=0
new_tag="v${major}.${minor}.${patch}"
echo "tag_name=${new_tag}" >> $GITHUB_OUTPUT
echo "should_tag=true" >> $GITHUB_OUTPUT
echo "Creating new minor version tag: ${new_tag}"
elif [[ "${GITHUB_REF}" == "refs/heads/staging" ]]; then
patch=$((patch + 1))
new_tag="v${major}.${minor}.${patch}"
echo "tag_name=${new_tag}" >> $GITHUB_OUTPUT
echo "should_tag=true" >> $GITHUB_OUTPUT
echo "Creating new patch version tag: ${new_tag}"
fi
docker-build:
name: Build and Push Docker Image
needs: [detect-project, build, set-tag]
if: |
always() &&
if: |
needs.detect-project.result == 'success' &&
needs.build.result == 'success' &&
needs.set-tag.outputs.should_tag == 'true' &&
github.event_name != 'pull_request'
runs-on: ubuntu-latest
environment: production
@@ -257,9 +245,6 @@ jobs:
tag:
name: Create Tag
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
@@ -275,13 +260,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Creating new tag: ${{ needs.set-tag.outputs.tag_name }}"
git tag ${{ needs.set-tag.outputs.tag_name }}
git push origin ${{ needs.set-tag.outputs.tag_name }}
summary:
name: Workflow Summary
needs: [test, audit, build, docker-build]
needs: [test, build, docker-build]
if: always()
runs-on: ubuntu-latest
steps:

View File

@@ -1,5 +1,3 @@
/// # API Module
///
/// This module provides all HTTP communication between WatcherAgent and the backend server.
@@ -14,9 +12,13 @@
/// These functions are called from the main agent loop and background tasks. All network operations are asynchronous and robust to transient failures.
use std::time::Duration;
use crate::hardware::HardwareInfo;
use crate::models::{HeartbeatDto, IdResponse, MetricDto, RegistrationDto, ServerMessage, Acknowledgment};
use crate::docker::container;
use crate::docker::serverclientcomm::handle_server_message;
use crate::hardware::HardwareInfo;
use crate::models::{
Acknowledgment, DockerMetricDto, DockerRegistrationDto, HeartbeatDto, IdResponse, MetricDto,
RegistrationDto, ServerMessage,
};
use anyhow::Result;
use reqwest::{Client, StatusCode};
@@ -39,7 +41,7 @@ use bollard::Docker;
/// Returns an error if unable to register after repeated attempts.
pub async fn register_with_server(
base_url: &str,
) -> Result<(i32, String), Box<dyn Error + Send + Sync>> {
) -> Result<(u16, String), Box<dyn Error + Send + Sync>> {
// First get local IP
let ip = local_ip_address::local_ip()?.to_string();
println!("Local IP address detected: {}", ip);
@@ -57,7 +59,7 @@ pub async fn register_with_server(
// Prepare registration data
let registration = RegistrationDto {
id: server_id,
server_id: server_id,
ip_address: registered_ip.clone(),
cpu_type: hardware.cpu.name.clone().unwrap_or_default(),
cpu_cores: (hardware.cpu.cores).unwrap_or_default(),
@@ -68,7 +70,7 @@ pub async fn register_with_server(
// Try to register (will retry on failure)
loop {
println!("Attempting to register with server...");
let url = format!("{}/monitoring/register-agent-by-id", base_url);
let url = format!("{}/monitoring/hardware-info", base_url);
match client.post(&url).json(&registration).send().await {
Ok(resp) if resp.status().is_success() => {
println!("✅ Successfully registered with server.");
@@ -103,12 +105,12 @@ pub async fn register_with_server(
async fn get_server_id_by_ip(
base_url: &str,
ip: &str,
) -> Result<(i32, String), Box<dyn Error + Send + Sync>> {
) -> Result<(u16, String), Box<dyn Error + Send + Sync>> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
let url = format!("{}/monitoring/server-id-by-ip?ipAddress={}", base_url, ip);
let url = format!("{}/monitoring/register?ipAddress={}", base_url, ip);
loop {
println!("Attempting to fetch server ID for IP {}...", ip);
@@ -151,6 +153,50 @@ async fn get_server_id_by_ip(
}
}
pub async fn broadcast_docker_containers(
base_url: &str,
server_id: u16,
container_dto: &mut DockerRegistrationDto,
) -> Result<(), Box<dyn Error + Send + Sync>> {
// First get local IP
println!("Preparing to broadcast docker containers...");
// Create HTTP client for registration
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
// Prepare registration data
let container_dto = container_dto;
container_dto.server_id = server_id;
// Try to register (will retry on failure)
loop {
println!("Attempting to broadcast containers...");
let url = format!("{}/monitoring/service-discovery", base_url);
match client.post(&url).json(&container_dto).send().await {
Ok(resp) if resp.status().is_success() => {
println!(
"✅ Successfully broadcasted following docker container: {:?}",
container_dto
);
return Ok(());
}
Ok(resp) => {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
println!(
"⚠️ Broadcasting failed ({}): {} (will retry in 10 seconds)",
status, text
);
}
Err(err) => {
println!("⚠️ Broadcasting failed: {} (will retry in 10 seconds)", err);
}
}
sleep(Duration::from_secs(10)).await;
}
}
/// Periodically sends heartbeat signals to the backend server to indicate agent liveness.
///
/// This function runs in a background task and will retry on network errors.
@@ -224,10 +270,13 @@ pub async fn send_metrics(
///
/// # Returns
/// * `Result<(), Box<dyn Error + Send + Sync>>` - Ok if commands are handled successfully.
pub async fn listening_to_server(docker: &Docker, base_url: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
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;
@@ -238,20 +287,36 @@ pub async fn listening_to_server(docker: &Docker, base_url: &str) -> Result<(),
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 {
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 {
if let Err(e) = send_acknowledgment(
&client,
base_url,
&msg.message_id,
status,
&details,
)
.await
{
eprintln!("Failed to send execution acknowledgment: {}", e);
}
}
@@ -289,7 +354,7 @@ pub async fn listening_to_server(docker: &Docker, base_url: &str) -> Result<(),
///
/// # Returns
/// * `Result<(), Box<dyn Error + Send + Sync>>` - Ok if acknowledgment is sent successfully.
async fn send_acknowledgment(
pub async fn send_acknowledgment(
client: &reqwest::Client,
base_url: &str,
message_id: &str,
@@ -297,24 +362,46 @@ async fn send_acknowledgment(
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?;
let response = client.post(&ack_url).json(&acknowledgment).send().await?;
if response.status().is_success() {
println!("Acknowledgment sent successfully for message {}", message_id);
println!(
"Acknowledgment sent successfully for message {}",
message_id
);
} else {
eprintln!("Server returned error for acknowledgment: {}", response.status());
eprintln!(
"Server returned error for acknowledgment: {}",
response.status()
);
}
Ok(())
}
}
pub async fn send_docker_metrics(
base_url: &str,
docker_metrics: &DockerMetricDto,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let client = Client::new();
let url = format!("{}/monitoring/docker-metric", base_url);
println!("Docker Metrics: {:?}", docker_metrics);
match client.post(&url).json(&docker_metrics).send().await {
Ok(res) => println!(
"✅ Sent docker metrics for server {} | Status: {}",
docker_metrics.server_id,
res.status()
),
Err(err) => eprintln!("❌ Failed to send docker metrics: {}", err),
}
Ok(())
}

View File

@@ -1,15 +1,17 @@
//! Docker container utilities for WatcherAgent
//!
//! Provides functions to list and process Docker containers using the Bollard library.
//!
use crate::docker::stats;
use crate::docker::stats::{ContainerCpuInfo, ContainerNetworkInfo};
use crate::models::DockerContainer;
use bollard::query_parameters::{ListContainersOptions};
use bollard::query_parameters::{
CreateImageOptions, ListContainersOptions, RestartContainerOptions,
};
use bollard::Docker;
use futures_util::StreamExt;
use std::error::Error;
/// Returns a list of available Docker containers.
///
@@ -18,54 +20,41 @@ use bollard::Docker;
///
/// # Returns
/// * `Vec<DockerContainer>` - Vector of Docker container info.
pub async fn get_available_container(docker: &Docker) -> Vec<DockerContainer> {
pub async fn get_available_containers(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()
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 short_id: u32 = short_string_id.trim().parse().unwrap();
let name = container.names
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
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,
id: short_id.to_string(),
image: Some(image),
name: Some(name),
})
})
.collect()
@@ -75,10 +64,96 @@ pub async fn get_available_container(docker: &Docker) -> Vec<DockerContainer> {
Vec::new()
}
};
containers_list
}
/// Pulls a new Docker image and restarts the current container.
///
/// # Arguments
/// * `docker` - Reference to a Bollard Docker client.
/// * `image` - The name of the Docker image to pull.
///
/// # Returns
/// * `Result<(), Box<dyn Error + Send + Sync>>` - Ok if updated successfully, error otherwise.
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 options = Some(ListContainersOptions {
all: true,
..Default::default()
});
let container_id = docker
.list_containers(options)
.await?
.into_iter()
.find_map(|c| {
c.image
.as_ref()
.and_then(|img| if img == image { c.id } else { None })
});
let _ = restart_container(docker, &container_id.unwrap()).await;
Ok(())
}
/// Restarts the agent's own Docker container.
///
/// # Arguments
/// * `docker` - Reference to a Bollard Docker client.
///
/// # Returns
/// * `Result<(), Box<dyn Error + Send + Sync>>` - Ok if restarted successfully, error otherwise.
pub async fn restart_container(
docker: &Docker,
container_id: &str,
) -> Result<(), Box<dyn Error + Send + Sync>> {
println!("Restarting container {}", container_id);
if let Err(e) = docker
.restart_container(
&container_id.to_string(),
Some(RestartContainerOptions {
signal: None,
t: Some(0),
}),
)
.await
{
eprintln!("Failed to restart container: {}", e);
}
Ok(())
}
/*
/// Extracts a Docker container ID from a string line.
///
@@ -90,4 +165,50 @@ pub async fn get_available_container(docker: &Docker) -> Vec<DockerContainer> {
pub fn extract_client_container_id(line: &str) -> Option<String> {
// ...existing code...
}
*/
*/
/// Gets network statistics for a specific container
pub async fn get_network_stats(
docker: &Docker,
container_id: &str,
) -> Result<ContainerNetworkInfo, Box<dyn Error + Send + Sync>> {
let (_, net_info, _) = stats::get_single_container_stats(docker, container_id).await?;
if let Some(net_info) = net_info {
Ok(net_info)
} else {
// Return default network info if not found
println!("No network info found for container {}", container_id);
Ok(ContainerNetworkInfo {
container_id: Some(container_id.to_string()),
rx_bytes: None,
tx_bytes: None,
rx_packets: None,
tx_packets: None,
rx_errors: None,
tx_errors: None,
})
}
}
/// Gets CPU statistics for a specific container
pub async fn get_cpu_stats(
docker: &Docker,
container_id: &str,
) -> Result<ContainerCpuInfo, Box<dyn Error + Send + Sync>> {
let (cpu_info, _, _) = stats::get_single_container_stats(docker, container_id).await?;
if let Some(cpu_info) = cpu_info {
Ok(cpu_info)
} else {
// Return default CPU info if not found
println!("No CPU info found for container {}", container_id);
Ok(ContainerCpuInfo {
container_id: Some(container_id.to_string()),
cpu_usage_percent: None,
system_cpu_usage: None,
container_cpu_usage: None,
online_cpus: None,
})
}
}

View File

@@ -1,4 +1,3 @@
/// # Docker Module
///
/// This module provides Docker integration for WatcherAgent, including container enumeration, statistics, and lifecycle management.
@@ -10,71 +9,227 @@
///
pub mod container;
pub mod serverclientcomm;
pub mod stats;
use crate::models::{
DockerCollectMetricDto, DockerContainer, DockerContainerCpuDto, DockerContainerInfo,
DockerContainerNetworkDto, DockerContainerRamDto, DockerMetricDto, DockerRegistrationDto,
};
use bollard::Docker;
use std::error::Error;
use crate::models::DockerContainer;
/// Aggregated Docker statistics for all managed containers.
///
/// # Fields
/// - `number`: Number of running containers (optional)
/// - `net_in_total`: Total network receive rate in **bytes per second (B/s)** (optional)
/// - `net_out_total`: Total network transmit rate in **bytes per second (B/s)** (optional)
/// - `dockers`: List of [`DockerContainer`] statistics (optional)
/// Main Docker manager that holds the Docker client and provides all operations
#[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>>,
pub struct DockerManager {
pub docker: Docker,
}
impl DockerInfo {
/// Collects Docker statistics for all managed containers.
///
/// # Returns
/// * `Result<DockerInfo, Box<dyn Error + Send + Sync>>` - Aggregated Docker statistics or error if collection fails.
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 })
impl Default for DockerManager {
fn default() -> Self {
Self {
docker: Docker::connect_with_local_defaults()
.unwrap_or_else(|e| panic!("Failed to create default Docker connection: {}", e)),
}
}
}
impl DockerManager {
/// Creates a new DockerManager instance
pub fn new() -> Result<Self, Box<dyn Error + Send + Sync>> {
let docker = Docker::connect_with_local_defaults()
.map_err(|e| format!("Failed to connect to Docker: {}", e))?;
Ok(Self { docker })
}
/// Creates a DockerManager instance with optional Docker connection
pub fn new_optional() -> Option<Self> {
Docker::connect_with_local_defaults()
.map(|docker| Self { docker })
.ok()
}
/// Finds the Docker container running the agent by image name
pub async fn get_client_container(
&self,
) -> Result<Option<DockerContainer>, Box<dyn Error + Send + Sync>> {
let containers = container::get_available_containers(&self.docker).await;
let client_image = "watcher-agent";
Ok(containers
.into_iter()
.find(|c| c.clone().image.unwrap().contains(client_image))
.map(|container| DockerContainer {
id: container.id,
image: container.image,
name: container.name,
}))
}
/// Gets the current client version (image name) if running in Docker
pub async fn get_client_version(&self) -> String {
match self.get_client_container().await {
Ok(Some(container)) => container
.image
.clone()
.unwrap()
.split(':')
.next()
.unwrap_or("unknown")
.to_string(),
Ok(None) => {
println!("Warning: No WatcherAgent container found");
"unknown".to_string()
}
Err(e) => {
println!("Warning: Could not get current image version: {}", e);
"unknown".to_string()
}
}
}
/// Checks if Docker is available and the agent is running in a container
pub async fn is_dockerized(&self) -> bool {
self.get_client_container()
.await
.map(|c| c.is_some())
.unwrap_or(false)
}
/// Gets all available containers as DTOs for registration
pub async fn get_containers(
&self,
) -> Result<Vec<DockerContainer>, Box<dyn Error + Send + Sync>> {
let containers = container::get_available_containers(&self.docker).await;
Ok(containers
.into_iter()
.map(|container| DockerContainer {
id: container.id,
image: container.image,
name: container.name,
})
.collect())
}
/// Gets the number of running containers
pub async fn get_container_count(&self) -> Result<usize, Box<dyn Error + Send + Sync>> {
let containers = container::get_available_containers(&self.docker).await;
Ok(containers.len())
}
/// Restarts a specific container by ID
pub async fn restart_container(
&self,
container_id: &str,
) -> Result<(), Box<dyn Error + Send + Sync>> {
container::restart_container(&self.docker, container_id).await
}
/// Collects Docker metrics for all containers
pub async fn collect_metrics(&self) -> Result<DockerMetricDto, Box<dyn Error + Send + Sync>> {
let containers = self.get_containers().await?;
let (cpu_stats, net_stats, mem_stats) = stats::get_container_stats(&self.docker).await?;
let container_infos_total: Vec<_> = containers
.into_iter()
.map(|container| {
let cpu = cpu_stats
.iter()
.find(|c| c.container_id == Some(container.id.clone()))
.cloned();
let network = net_stats
.iter()
.find(|n| n.container_id == Some(container.id.clone()))
.cloned();
let ram = mem_stats
.iter()
.find(|m| m.container_id == Some(container.id.clone()))
.cloned();
DockerContainerInfo {
container: Some(container),
status: None, // Status can be fetched if needed
cpu,
network,
ram,
}
})
.collect();
let container_infos: Vec<DockerCollectMetricDto> = container_infos_total
.into_iter()
.map(|info| DockerCollectMetricDto {
id: Some(info.container.unwrap().id).unwrap_or("".to_string()),
cpu: info
.cpu
.unwrap()
.cpu_usage_percent
.map(|load| DockerContainerCpuDto {
cpu_load: Some(load),
})
.unwrap_or(DockerContainerCpuDto { cpu_load: None }),
ram: info
.ram
.unwrap()
.memory_usage_percent
.map(|load| DockerContainerRamDto {
cpu_load: Some(load),
})
.unwrap_or(DockerContainerRamDto { cpu_load: None }),
network: DockerContainerNetworkDto {
net_in: info
.network
.as_ref()
.unwrap()
.rx_bytes
.map(|bytes| bytes as f64)
.or(Some(0.0)),
net_out: info
.network
.unwrap()
.tx_bytes
.map(|bytes| bytes as f64)
.or(Some(0.0)),
},
})
.collect();
let dto = DockerMetricDto {
server_id: 0,
containers: serde_json::to_string(&container_infos)?,
};
Ok(dto)
}
pub async fn create_registration_dto(
&self,
) -> Result<DockerRegistrationDto, Box<dyn Error + Send + Sync>> {
let containers = self.get_containers().await?;
let dto = DockerRegistrationDto {
server_id: 0,
//container_count,
containers: serde_json::to_string(&containers)?,
};
Ok(dto)
}
}
// Keep these as utility functions if needed, but they should use DockerManager internally
impl DockerContainer {
/*
/// Restarts the specified Docker container by ID.
///
/// # Arguments
/// * `docker` - Reference to a Bollard Docker client
///
/// # Returns
/// * `Result<(), Box<dyn Error + Send + Sync>>` - Ok if restarted successfully, error otherwise.
pub async fn restart_container(docker: &Docker) -> Result<(), Box<dyn Error + Send + Sync>> {
// ...existing code...
}
*/
/// Returns the container ID for a given [`DockerContainer`].
///
/// # Arguments
/// * `container` - Reference to a [`DockerContainer`]
///
/// # Returns
/// * `Result<u32, Box<dyn Error + Send + Sync>>` - Container ID as integer.
pub async fn get_docker_container_id(container: DockerContainer) -> Result<String, Box<dyn Error + Send + Sync>> {
Ok(container.ID)
/// Returns the container ID
pub fn id(&self) -> &str {
&self.id
}
/// Returns the image name for a given [`DockerContainer`].
///
/// # Arguments
/// * `container` - Reference to a [`DockerContainer`]
///
/// # Returns
/// * `Result<String, Box<dyn Error + Send + Sync>>` - Image name as string.
pub async fn get_docker_container_image(container: DockerContainer) -> Result<String, Box<dyn Error + Send + Sync>> {
Ok(container.image)
/// Returns the image name
pub fn image(&self) -> &str {
&self.image.as_deref().unwrap_or("unknown")
}
}
/// Returns the container name
pub fn name(&self) -> &str {
&self.name.as_deref().unwrap_or("unknown")
}
}

View File

@@ -1,15 +1,13 @@
//! Server-client communication utilities for WatcherAgent
//!
//! Handles server commands, Docker image updates, and container management using the Bollard library.
//!
use crate::models::{DockerContainer, ServerMessage};
use crate::docker::container::{get_available_container};
use crate::models::ServerMessage;
use std::error::Error;
use super::container::{restart_container, update_docker_image};
//use bollard::query_parameters::{CreateImageOptions, RestartContainerOptions};
use bollard::Docker;
use bollard::query_parameters::{CreateImageOptions, RestartContainerOptions};
use futures_util::StreamExt;
use std::error::Error;
/// Handles a message from the backend server and dispatches the appropriate action.
///
@@ -19,7 +17,10 @@ use futures_util::StreamExt;
///
/// # Returns
/// * `Result<(), Box<dyn Error + Send + Sync>>` - Ok if handled successfully, error otherwise.
pub async fn handle_server_message(docker: &Docker, msg: ServerMessage) -> Result<(), Box<dyn Error + Send + Sync>> {
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);
@@ -36,10 +37,14 @@ pub async fn handle_server_message(docker: &Docker, msg: ServerMessage) -> Resul
}
}
"restart_container" => {
println!("Received restart container command");
// Call your restart_container function here
restart_container(docker).await?;
Ok(())
if let Some(image_name) = msg.data.get("image").and_then(|v| v.as_str()) {
println!("Received restart command for image: {}", image_name);
// Call your update_docker_image function here
restart_container(docker, image_name).await?;
Ok(())
} else {
Err("Missing image name in update message".into())
}
}
"stop_agent" => {
println!("Received stop agent command");
@@ -52,87 +57,3 @@ pub async fn handle_server_message(docker: &Docker, msg: ServerMessage) -> Resul
}
}
}
/// Pulls a new Docker image and restarts the current container.
///
/// # Arguments
/// * `docker` - Reference to a Bollard Docker client.
/// * `image` - The name of the Docker image to pull.
///
/// # Returns
/// * `Result<(), Box<dyn Error + Send + Sync>>` - Ok if updated successfully, error otherwise.
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(())
}
/// Finds the Docker container running the agent by image name.
///
/// # Arguments
/// * `docker` - Reference to a Bollard Docker client.
///
/// # Returns
/// * `Result<Option<DockerContainer>, Box<dyn Error + Send + Sync>>` - The agent's container info if found.
pub async fn get_client_container(docker: &Docker) -> Result<Option<DockerContainer>, Box<dyn Error + Send + Sync>> {
let containers = get_available_container(docker).await;
let client_image = "watcher-agent";
// Find container with the specific image
if let Some(container) = containers.iter().find(|c| c.image == client_image) {
Ok(Some(container.clone()))
} else {
Ok(None)
}
}
/// Restarts the agent's own Docker container.
///
/// # Arguments
/// * `docker` - Reference to a Bollard Docker client.
///
/// # Returns
/// * `Result<(), Box<dyn Error + Send + Sync>>` - Ok if restarted successfully, error otherwise.
pub async fn restart_container(docker: &Docker) -> Result<(), Box<dyn Error + Send + Sync>> {
if let Ok(Some(container)) = get_client_container(docker).await {
let container_id = container.clone().ID;
println!("Restarting container {}", container_id);
if let Err(e) = docker.restart_container(&container_id.to_string(), Some(RestartContainerOptions { signal: None, t: Some(0) }))
.await
{
eprintln!("Failed to restart container: {}", e);
}
} else {
eprintln!("No container ID found (HOSTNAME not set?)");
}
Ok(())
}

View File

@@ -0,0 +1,99 @@
use super::ContainerCpuInfo;
use bollard::query_parameters::{ListContainersOptions, StatsOptions};
use bollard::Docker;
use futures_util::stream::TryStreamExt;
use std::error::Error;
/// Get CPU statistics for all containers
pub async fn get_all_containers_cpu_stats(
docker: &Docker,
) -> Result<Vec<ContainerCpuInfo>, Box<dyn Error + Send + Sync>> {
let containers = docker
.list_containers(Some(ListContainersOptions {
all: true,
..Default::default()
}))
.await?;
let mut cpu_infos = Vec::new();
for container in containers {
let id = container.id.unwrap_or_default();
// Skip if no ID
if id.is_empty() {
continue;
}
if let Some(cpu_info) = get_single_container_cpu_stats(docker, &id).await? {
cpu_infos.push(cpu_info);
}
}
Ok(cpu_infos)
}
/// Get CPU statistics for a specific container
pub async fn get_single_container_cpu_stats(
docker: &Docker,
container_id: &str,
) -> Result<Option<ContainerCpuInfo>, Box<dyn Error + Send + Sync>> {
let mut stats_stream = docker.stats(
container_id,
Some(StatsOptions {
stream: false,
one_shot: true,
}),
);
if let Some(stats) = stats_stream.try_next().await? {
if let (Some(cpu_stats), Some(precpu_stats)) = (&stats.cpu_stats, &stats.precpu_stats) {
if let (Some(cpu_usage), Some(pre_cpu_usage)) =
(&cpu_stats.cpu_usage, &precpu_stats.cpu_usage)
{
let cpu_delta = cpu_usage
.total_usage
.unwrap_or(0)
.saturating_sub(pre_cpu_usage.total_usage.unwrap_or(0));
let system_delta = cpu_stats
.system_cpu_usage
.unwrap_or(0)
.saturating_sub(precpu_stats.system_cpu_usage.unwrap_or(0));
let online_cpus = cpu_stats.online_cpus.unwrap_or(1);
let cpu_percent = if system_delta > 0 && online_cpus > 0 {
(cpu_delta as f64 / system_delta as f64) * online_cpus as f64 * 100.0
} else {
0.0
};
return Ok(Some(ContainerCpuInfo {
container_id: Some(container_id.to_string()),
cpu_usage_percent: Some(cpu_percent),
system_cpu_usage: Some(cpu_stats.system_cpu_usage.unwrap_or(0)),
container_cpu_usage: Some(cpu_usage.total_usage.unwrap_or(0)),
online_cpus: Some(online_cpus),
}));
}
}
}
Ok(None)
}
/// Get average CPU usage across all containers
pub async fn get_average_cpu_usage(docker: &Docker) -> Result<f64, Box<dyn Error + Send + Sync>> {
let cpu_infos = get_all_containers_cpu_stats(docker).await?;
if cpu_infos.is_empty() {
return Ok(0.0);
}
let total_cpu: f64 = cpu_infos
.iter()
.map(|cpu| cpu.cpu_usage_percent.unwrap())
.sum();
Ok(total_cpu / cpu_infos.len() as f64)
}

View File

@@ -0,0 +1,90 @@
pub mod cpu;
pub mod network;
pub mod ram;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ContainerCpuInfo {
pub container_id: Option<String>,
pub cpu_usage_percent: Option<f64>,
pub system_cpu_usage: Option<u64>,
pub container_cpu_usage: Option<u64>,
pub online_cpus: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ContainerNetworkInfo {
pub container_id: Option<String>,
pub rx_bytes: Option<u64>,
pub tx_bytes: Option<u64>,
pub rx_packets: Option<u64>,
pub tx_packets: Option<u64>,
pub rx_errors: Option<u64>,
pub tx_errors: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ContainerMemoryInfo {
pub container_id: Option<String>,
pub memory_usage: Option<u64>,
pub memory_limit: Option<u64>,
pub memory_usage_percent: Option<f64>,
}
use bollard::Docker;
use std::error::Error;
/// Get container statistics for all containers using an existing Docker client
pub async fn get_container_stats(
docker: &Docker,
) -> Result<
(
Vec<ContainerCpuInfo>,
Vec<ContainerNetworkInfo>,
Vec<ContainerMemoryInfo>,
),
Box<dyn Error + Send + Sync>,
> {
let cpu_infos = cpu::get_all_containers_cpu_stats(docker).await?;
let net_infos = network::get_all_containers_network_stats(docker).await?;
let mem_infos = ram::get_all_containers_memory_stats(docker).await?;
Ok((cpu_infos, net_infos, mem_infos))
}
/// Get container statistics for a specific container
pub async fn get_single_container_stats(
docker: &Docker,
container_id: &str,
) -> Result<
(
Option<ContainerCpuInfo>,
Option<ContainerNetworkInfo>,
Option<ContainerMemoryInfo>,
),
Box<dyn Error + Send + Sync>,
> {
let cpu_info = cpu::get_single_container_cpu_stats(docker, container_id).await?;
let net_info = network::get_single_container_network_stats(docker, container_id).await?;
let mem_info = ram::get_single_container_memory_stats(docker, container_id).await?;
Ok((cpu_info, net_info, mem_info))
}
/// Get total network statistics across all containers
pub async fn get_total_network_stats(
docker: &Docker,
) -> Result<(u64, u64), Box<dyn Error + Send + Sync>> {
network::get_total_network_stats(docker).await
}
/// Get average CPU usage across all containers
pub async fn get_average_cpu_usage(docker: &Docker) -> Result<f64, Box<dyn Error + Send + Sync>> {
cpu::get_average_cpu_usage(docker).await
}
/// Get total memory usage across all containers
pub async fn get_total_memory_usage(docker: &Docker) -> Result<u64, Box<dyn Error + Send + Sync>> {
ram::get_total_memory_usage(docker).await
}

View File

@@ -0,0 +1,79 @@
use super::ContainerNetworkInfo;
use bollard::query_parameters::{ListContainersOptions, StatsOptions};
use bollard::Docker;
use futures_util::stream::TryStreamExt;
use std::error::Error;
/// Get network statistics for all containers
pub async fn get_all_containers_network_stats(
docker: &Docker,
) -> Result<Vec<ContainerNetworkInfo>, Box<dyn Error + Send + Sync>> {
let containers = docker
.list_containers(Some(ListContainersOptions {
all: true,
..Default::default()
}))
.await?;
let mut net_infos = Vec::new();
for container in containers {
let id = container.id.unwrap_or_default();
// Skip if no ID
if id.is_empty() {
continue;
}
if let Some(net_info) = get_single_container_network_stats(docker, &id).await? {
net_infos.push(net_info);
}
}
Ok(net_infos)
}
/// Get network statistics for a specific container
pub async fn get_single_container_network_stats(
docker: &Docker,
container_id: &str,
) -> Result<Option<ContainerNetworkInfo>, Box<dyn Error + Send + Sync>> {
let mut stats_stream = docker.stats(
container_id,
Some(StatsOptions {
stream: false,
one_shot: true,
}),
);
if let Some(stats) = stats_stream.try_next().await? {
if let Some(networks) = stats.networks {
// Take the first network interface (usually eth0)
if let Some((_name, net)) = networks.into_iter().next() {
return Ok(Some(ContainerNetworkInfo {
container_id: Some(container_id.to_string()),
rx_bytes: net.rx_bytes,
tx_bytes: net.tx_bytes,
rx_packets: net.rx_packets,
tx_packets: net.tx_packets,
rx_errors: net.rx_errors,
tx_errors: net.tx_errors,
}));
}
}
}
Ok(None)
}
/// Get total network statistics across all containers
pub async fn get_total_network_stats(
docker: &Docker,
) -> Result<(u64, u64), Box<dyn Error + Send + Sync>> {
let net_infos = get_all_containers_network_stats(docker).await?;
let total_rx: u64 = net_infos.iter().map(|net| net.rx_bytes.unwrap()).sum();
let total_tx: u64 = net_infos.iter().map(|net| net.tx_bytes.unwrap()).sum();
Ok((total_rx, total_tx))
}

View File

@@ -0,0 +1,77 @@
use super::ContainerMemoryInfo;
use bollard::query_parameters::{ListContainersOptions, StatsOptions};
use bollard::Docker;
use futures_util::stream::TryStreamExt;
use std::error::Error;
/// Get memory statistics for all containers
pub async fn get_all_containers_memory_stats(
docker: &Docker,
) -> Result<Vec<ContainerMemoryInfo>, Box<dyn Error + Send + Sync>> {
let containers = docker
.list_containers(Some(ListContainersOptions {
all: true,
..Default::default()
}))
.await?;
let mut mem_infos = Vec::new();
for container in containers {
let id = container.id.unwrap_or_default();
// Skip if no ID
if id.is_empty() {
continue;
}
if let Some(mem_info) = get_single_container_memory_stats(docker, &id).await? {
mem_infos.push(mem_info);
}
}
Ok(mem_infos)
}
/// Get memory statistics for a specific container
pub async fn get_single_container_memory_stats(
docker: &Docker,
container_id: &str,
) -> Result<Option<ContainerMemoryInfo>, Box<dyn Error + Send + Sync>> {
let mut stats_stream = docker.stats(
container_id,
Some(StatsOptions {
stream: false,
one_shot: true,
}),
);
if let Some(stats) = stats_stream.try_next().await? {
if let Some(memory_stats) = &stats.memory_stats {
let memory_usage = memory_stats.usage.unwrap_or(0);
let memory_limit = memory_stats.limit.unwrap_or(1); // Avoid division by zero
let memory_usage_percent = if memory_limit > 0 {
(memory_usage as f64 / memory_limit as f64) * 100.0
} else {
0.0
};
return Ok(Some(ContainerMemoryInfo {
container_id: Some(container_id.to_string()),
memory_usage: Some(memory_usage),
memory_limit: Some(memory_limit),
memory_usage_percent: Some(memory_usage_percent),
}));
}
}
Ok(None)
}
/// Get total memory usage across all containers
pub async fn get_total_memory_usage(docker: &Docker) -> Result<u64, Box<dyn Error + Send + Sync>> {
let mem_infos = get_all_containers_memory_stats(docker).await?;
let total_memory: u64 = mem_infos.iter().map(|mem| mem.memory_usage.unwrap()).sum();
Ok(total_memory)
}

View File

@@ -1,4 +1,3 @@
/// # WatcherAgent
///
/// **WatcherAgent** is a cross-platform system monitoring agent written in Rust.
@@ -26,18 +25,17 @@
/// ```
///
/// The agent will register itself, start collecting metrics, and listen for remote commands.
pub mod api;
pub mod docker;
pub mod hardware;
pub mod metrics;
pub mod models;
pub mod docker;
use tokio::task::JoinHandle;
use bollard::Docker;
use std::env;
use std::error::Error;
use std::sync::Arc;
use tokio::task::JoinHandle;
/// Awaits a spawned asynchronous task and flattens its nested `Result` type.
///
@@ -82,26 +80,8 @@ async fn flatten<T>(
/// Returns an error if registration or any background task fails, or if required arguments are missing.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
// 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_client_container(&docker).await {
Ok(Some(version)) => version.image,
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);
// Parse command-line arguments
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());
@@ -111,20 +91,52 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
// Registration with backend server
let (server_id, ip) = match api::register_with_server(&server_url).await {
Ok((id, ip)) => (id, ip),
Ok((id, ip)) => {
println!("Registered with server. ID: {}, IP: {}", id, ip);
(id, ip)
}
Err(e) => {
eprintln!("Fehler bei der Registrierung am Server: {e}");
return Err(e);
}
};
// Initialize Docker (optional - agent can run without Docker)
let docker_manager = docker::DockerManager::new_optional();
// Get current image version
let client_version = if let Some(ref docker_manager) = docker_manager {
docker_manager.get_client_version().await
} else {
"unknown".to_string()
};
println!("Client Version: {}", client_version);
// Prepare Docker registration DTO
let container_dto = if let Some(ref docker_manager) = docker_manager {
docker_manager.create_registration_dto().await?
} else {
models::DockerRegistrationDto {
server_id: 0,
//container_count: 0, --- IGNORE ---
containers: "[]".to_string(),
}
};
let _ =
api::broadcast_docker_containers(server_url, server_id, &mut container_dto.clone()).await?;
// 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 server listening for commands (only if Docker is available)
let listening_handle = if let Some(ref docker_manager) = docker_manager {
tokio::spawn({
let docker = docker_manager.docker.clone();
let server_url = server_url.to_string();
async move { api::listening_to_server(&docker, &server_url).await }
})
} else {
println!("Docker not available, skipping server command listener.");
tokio::spawn(async { Ok(()) }) // Dummy task
};
// Start heartbeat in background
let heartbeat_handle = tokio::spawn({
@@ -138,8 +150,9 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let metrics_handle = tokio::spawn({
let ip = ip.clone();
let server_url = server_url.to_string();
let docker_manager = docker_manager.as_ref().cloned().unwrap();
async move {
let mut collector = metrics::Collector::new(server_id, ip);
let mut collector = metrics::Collector::new(server_id, ip, docker_manager);
collector.run(&server_url).await
}
});

View File

@@ -1,5 +1,3 @@
/// # Metrics Module
///
/// This module orchestrates the collection and reporting of hardware and network metrics for WatcherAgent.
@@ -15,10 +13,11 @@ use std::error::Error;
use std::time::Duration;
use crate::api;
use crate::docker::DockerManager;
//use crate::docker::DockerInfo;
use crate::hardware::network::NetworkMonitor;
use crate::hardware::HardwareInfo;
use crate::models::MetricDto;
use crate::models::{DockerMetricDto, MetricDto};
/// Main orchestrator for hardware and network metric collection and reporting.
///
@@ -29,12 +28,12 @@ use crate::models::MetricDto;
/// - `server_id`: Unique server ID assigned by the backend.
/// - `ip_address`: IP address of the agent.
pub struct Collector {
docker_manager: DockerManager,
network_monitor: NetworkMonitor,
server_id: i32,
server_id: u16,
ip_address: String,
}
impl Collector {
/// Creates a new `Collector` instance for metric collection and reporting.
///
@@ -44,8 +43,9 @@ impl Collector {
///
/// # Returns
/// A new `Collector` ready to collect and report metrics.
pub fn new(server_id: i32, ip_address: String) -> Self {
pub fn new(server_id: u16, ip_address: String, docker_manager: DockerManager) -> Self {
Self {
docker_manager,
network_monitor: NetworkMonitor::new(),
server_id,
ip_address,
@@ -75,7 +75,16 @@ impl Collector {
continue;
}
};
let docker_metrics = match self.docker_collect().await {
Ok(metrics) => metrics,
Err(e) => {
eprintln!("Error collecting docker metrics: {}", e);
tokio::time::sleep(Duration::from_secs(10)).await;
continue;
}
};
api::send_metrics(base_url, &metrics).await?;
api::send_docker_metrics(base_url, &docker_metrics).await?;
tokio::time::sleep(Duration::from_secs(20)).await;
}
}
@@ -115,4 +124,14 @@ impl Collector {
net_tx: hardware.network.tx_rate.unwrap_or_default(),
})
}
/// NOTE: This is a compilation-safe stub. Implement the Docker collection using your
/// DockerManager API and container helpers when available.
pub async fn docker_collect(&self) -> Result<DockerMetricDto, Box<dyn Error + Send + Sync>> {
let metrics = self.docker_manager.collect_metrics().await?;
Ok(DockerMetricDto {
server_id: self.server_id,
containers: metrics.containers,
})
}
}

View File

@@ -1,5 +1,3 @@
/// # Models Module
///
/// This module defines all data structures (DTOs) used for communication between WatcherAgent and the backend server, as well as hardware metrics and Docker container info.
@@ -11,6 +9,8 @@
///
/// ## Usage
/// These types are serialized/deserialized for HTTP communication and used throughout the agent for data exchange.
use crate::docker::stats;
use serde::{Deserialize, Serialize};
/// Registration data sent to the backend server.
@@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)]
pub struct RegistrationDto {
#[serde(rename = "id")]
pub id: i32,
pub server_id: u16,
#[serde(rename = "ipAddress")]
pub ip_address: String,
#[serde(rename = "cpuType")]
@@ -59,7 +59,7 @@ pub struct RegistrationDto {
#[derive(Serialize, Debug)]
pub struct MetricDto {
#[serde(rename = "serverId")]
pub server_id: i32,
pub server_id: u16,
#[serde(rename = "ipAddress")]
pub ip_address: String,
#[serde(rename = "cpu_Load")]
@@ -116,7 +116,7 @@ pub struct DiskInfoDetailed {
/// - `ip_address`: IPv4 or IPv6 address (string)
#[derive(Deserialize)]
pub struct IdResponse {
pub id: i32,
pub id: u16,
#[serde(rename = "ipAddress")]
pub ip_address: String,
}
@@ -185,13 +185,81 @@ pub struct Acknowledgment {
/// - `_net_in`: Network receive rate in **bytes per second (B/s)**
/// - `_net_out`: Network transmit rate in **bytes per second (B/s)**
/// - `_cpu_load`: CPU usage as a percentage (**0.0100.0**)
#[derive(Debug, Serialize, Clone)]
pub struct DockerRegistrationDto {
/// Unique server identifier (integer)
pub server_id: u16,
/// Number of currently running containers
// pub container_count: usize, --- IGNORE ---
/// json stringified array of DockerContainer
///
/// ## Json Example
/// json format: [{"id":"234dsf234","image":"nginx:latest","name":"webserver"},...]
///
/// ## Fields
/// id: unique container ID (first 12 hex digits)
/// image: docker image name
/// name: container name
pub containers: String, // Vec<DockerContainer>,
}
#[derive(Debug, Serialize, Clone)]
pub struct DockerMetricDto {
pub server_id: u16,
/// json stringified array of DockerContainer
///
/// ## Json Example
/// json format: [{"id":"234dsf234","status":"running","image":"nginx:latest","name":"webserver","network":{"net_in":1024,"net_out":2048},"cpu":{"cpu_load":12.5},"ram":{"ram_load":10.0}},...]
///
/// ## Fields
/// id: unique container ID (first 12 hex digits)
/// status: "running";"stopped";others
/// image: docker image name
/// name: container name
/// network: network stats
/// cpu: cpu stats
/// ram: ram stats
pub containers: String, // Vec<DockerContainerInfo>,
}
#[derive(Debug, Serialize, Clone)]
pub struct DockerCollectMetricDto {
pub id: String,
pub cpu: DockerContainerCpuDto,
pub ram: DockerContainerRamDto,
pub network: DockerContainerNetworkDto,
}
#[derive(Debug, Serialize, Clone)]
pub struct DockerContainerCpuDto {
pub cpu_load: Option<f64>,
}
#[derive(Debug, Serialize, Clone)]
pub struct DockerContainerRamDto {
pub cpu_load: Option<f64>,
}
#[derive(Debug, Serialize, Clone)]
pub struct DockerContainerNetworkDto {
pub net_in: Option<f64>,
pub net_out: Option<f64>,
}
#[derive(Debug, Serialize, Clone)]
pub struct DockerContainerInfo {
pub container: Option<DockerContainer>,
pub status: Option<String>, // "running";"stopped";others
pub network: Option<stats::ContainerNetworkInfo>,
pub cpu: Option<stats::ContainerCpuInfo>,
pub ram: Option<stats::ContainerMemoryInfo>,
}
#[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,
}
pub id: String,
pub image: Option<String>,
pub name: Option<String>,
}