Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e65770407c | |||
| 9e77d961f5 | |||
| 3933301cff | |||
| 9f87c9294d | |||
| 638d347bb0 | |||
| 8f3dbd0532 | |||
| 99bdf79a63 | |||
| a22d1f4cd2 | |||
| d8f395cb53 | |||
| f69c22039b | |||
| 2bda420728 | |||
| f604e22f6e | |||
| 8d4dc0e853 | |||
| 311bdbc66d | |||
| 0df716b8b0 | |||
| 243c97d71b | |||
| e0ed96309c | |||
| 93747e4a04 | |||
| b93b55d5a8 | |||
| 46209b2965 | |||
| 775d54f07c | |||
| e9524858f5 | |||
| 1a1202abcf | |||
| c526a6e5ca |
@@ -0,0 +1,9 @@
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
linker = "musl-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[env]
|
||||
CC_aarch64_unknown_linux_musl = "aarch64-linux-gnu-gcc"
|
||||
AR_aarch64_unknown_linux_musl = "aarch64-linux-gnu-ar"
|
||||
@@ -10,4 +10,10 @@ server/*.test
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
config.toml
|
||||
# Config locaux (contiennent des IPs/secrets)
|
||||
agent/config.toml
|
||||
server/config.toml
|
||||
|
||||
# Sauf les configs cargo (pas de secrets)
|
||||
!.cargo/config.toml
|
||||
!agent/.cargo/config.toml
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
linker = "musl-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[env]
|
||||
CC_aarch64_unknown_linux_musl = "aarch64-linux-gnu-gcc"
|
||||
AR_aarch64_unknown_linux_musl = "aarch64-linux-gnu-ar"
|
||||
@@ -38,9 +38,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -97,21 +97,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
@@ -142,6 +127,17 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
@@ -247,13 +243,14 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanometrics-agent"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rumqttc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -262,23 +259,6 @@ dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.3"
|
||||
@@ -294,48 +274,11 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
@@ -343,12 +286,6 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -383,6 +320,20 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rumqttc"
|
||||
version = "0.24.0"
|
||||
@@ -393,10 +344,12 @@ dependencies = [
|
||||
"flume",
|
||||
"futures-util",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"rustls-webpki",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -409,7 +362,63 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -418,7 +427,7 @@ version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -429,9 +438,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
@@ -527,7 +536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -539,6 +548,12 @@ dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@@ -571,10 +586,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -609,7 +624,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -624,12 +639,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
name = "tokio-rustls"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -687,10 +703,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
@@ -797,6 +813,15 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -973,6 +998,12 @@ dependencies = [
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nanometrics-agent"
|
||||
version = "0.1.0"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -19,10 +19,11 @@ codegen-units = 1
|
||||
|
||||
[dependencies]
|
||||
sysinfo = { version = "0.30", default-features = false }
|
||||
libc = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
rumqttc = { version = "0.24", default-features = false, features = ["use-native-tls"] }
|
||||
rumqttc = { version = "0.24", default-features = false, features = ["use-rustls"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
use nanometrics_agent::{config, metrics, payload, transport};
|
||||
use sysinfo::{Components, Disks, Networks, System};
|
||||
use sysinfo::{Components, Networks, System};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static RUNNING: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
extern "C" fn handle_signal(_: libc::c_int) {
|
||||
RUNNING.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn get_local_ip() -> String {
|
||||
use std::net::UdpSocket;
|
||||
@@ -34,7 +41,6 @@ fn main() {
|
||||
|
||||
let mut sys = System::new();
|
||||
let mut networks = Networks::new_with_refreshed_list();
|
||||
let mut disks = Disks::new_with_refreshed_list();
|
||||
let mut components = Components::new_with_refreshed_list();
|
||||
|
||||
let udp_sender = if cfg.protocols.udp.enabled {
|
||||
@@ -50,12 +56,17 @@ fn main() {
|
||||
None
|
||||
};
|
||||
|
||||
unsafe {
|
||||
libc::signal(libc::SIGTERM, handle_signal as libc::sighandler_t);
|
||||
libc::signal(libc::SIGINT, handle_signal as libc::sighandler_t);
|
||||
}
|
||||
|
||||
let mut last_slow = Instant::now();
|
||||
let mut last_medium = Instant::now();
|
||||
let mut first_medium = true;
|
||||
let mut first_slow = true;
|
||||
|
||||
loop {
|
||||
while RUNNING.load(Ordering::Relaxed) {
|
||||
let now = Instant::now();
|
||||
|
||||
while let Ok(transport::mqtt::MqttIncoming::ConfigUpdate(data)) = incoming_rx.try_recv() {
|
||||
@@ -69,6 +80,7 @@ fn main() {
|
||||
hostname: hostname.clone(),
|
||||
ip: ip.clone(),
|
||||
status: "online".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -102,9 +114,8 @@ fn main() {
|
||||
}
|
||||
|
||||
if first_slow || now.duration_since(last_slow).as_secs() >= 60 {
|
||||
disks.refresh();
|
||||
if cfg.metrics.disk.udp || cfg.metrics.disk.mqtt {
|
||||
let (used, free, total) = metrics::disk::get(&disks);
|
||||
let (used, free, total) = metrics::disk::get();
|
||||
m.hdd_used = Some(used);
|
||||
m.hdd_free = Some(free);
|
||||
m.hdd_total = Some(total);
|
||||
@@ -135,4 +146,24 @@ fn main() {
|
||||
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
}
|
||||
|
||||
// Déconnexion propre : notifier le serveur avant de quitter
|
||||
let offline = serde_json::to_string(&payload::AgentMetrics {
|
||||
hostname: hostname.clone(),
|
||||
ip: ip.clone(),
|
||||
status: "offline".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
..Default::default()
|
||||
}).unwrap_or_default();
|
||||
|
||||
if let Some(ref udp) = udp_sender {
|
||||
udp.send(&offline);
|
||||
}
|
||||
if let Some(ref client) = mqtt_client {
|
||||
transport::mqtt::publish_status(
|
||||
client, &cfg.protocols.mqtt.topic_base, &hostname, "offline",
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(200)); // laisser le temps au broker de recevoir
|
||||
let _ = client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
use sysinfo::Disks;
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
pub fn get(disks: &Disks) -> (u64, u64, u64) {
|
||||
for disk in disks.list() {
|
||||
let mount = disk.mount_point().to_string_lossy();
|
||||
if mount == "/" {
|
||||
let total = disk.total_space();
|
||||
let free = disk.available_space();
|
||||
let used = total.saturating_sub(free);
|
||||
return (used, free, total);
|
||||
}
|
||||
/// Retourne (used, free, total) en octets pour le système de fichiers racine "/".
|
||||
/// Utilise statvfs() directement pour correspondre exactement aux chiffres de `df` :
|
||||
/// - total = f_blocks × f_frsize
|
||||
/// - used = (f_blocks − f_bfree) × f_frsize (blocs effectivement écrits)
|
||||
/// - free = f_bavail × f_frsize (disponible pour utilisateurs non-root)
|
||||
pub fn get() -> (u64, u64, u64) {
|
||||
let path = b"/\0";
|
||||
let mut stat = MaybeUninit::<libc::statvfs>::uninit();
|
||||
let ret = unsafe { libc::statvfs(path.as_ptr() as *const libc::c_char, stat.as_mut_ptr()) };
|
||||
if ret != 0 {
|
||||
return (0, 0, 0);
|
||||
}
|
||||
if let Some(disk) = disks.list().first() {
|
||||
let total = disk.total_space();
|
||||
let free = disk.available_space();
|
||||
return (total.saturating_sub(free), free, total);
|
||||
}
|
||||
(0, 0, 0)
|
||||
let stat = unsafe { stat.assume_init() };
|
||||
let bsize = stat.f_frsize as u64;
|
||||
let total = stat.f_blocks.saturating_mul(bsize);
|
||||
let used = stat.f_blocks.saturating_sub(stat.f_bfree).saturating_mul(bsize);
|
||||
let free = stat.f_bavail.saturating_mul(bsize);
|
||||
(used, free, total)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use sysinfo::Disks;
|
||||
|
||||
#[test]
|
||||
fn test_disk_coherent() {
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let (used, free, total) = get(&disks);
|
||||
let (used, free, total) = get();
|
||||
eprintln!("résultat statvfs : used={used} free={free} total={total}");
|
||||
if total > 0 {
|
||||
assert!(used + free <= total + 1024, "used + free > total");
|
||||
assert!(used + free <= total + 1024 * 1024, "used + free > total");
|
||||
assert!(total > 0, "total doit être > 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ pub struct AgentMetrics {
|
||||
pub hostname: String,
|
||||
pub ip: String,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub version: String,
|
||||
pub cpu_percent: Option<f32>,
|
||||
pub memory_used: Option<u64>,
|
||||
pub memory_free: Option<u64>,
|
||||
|
||||
@@ -78,3 +78,8 @@ pub fn publish_metrics(client: &Client, topic_base: &str, hostname: &str, json:
|
||||
let topic = format!("{}/{}/metrics", topic_base, hostname);
|
||||
let _ = client.publish(topic, QoS::AtMostOnce, false, json);
|
||||
}
|
||||
|
||||
pub fn publish_status(client: &Client, topic_base: &str, hostname: &str, status: &str) {
|
||||
let topic = format!("{}/{}/status", topic_base, hostname);
|
||||
let _ = client.publish(topic, QoS::AtLeastOnce, true, status);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ fn test_serialize_json_complet() {
|
||||
temperature: None,
|
||||
smart: None,
|
||||
status: "online".to_string(),
|
||||
version: "0.0.0".to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&m).unwrap();
|
||||
assert!(json.contains("\"hostname\":\"srv-01\""));
|
||||
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -115,9 +115,15 @@ body{background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-s
|
||||
.g-bar{flex:1;height:5px;border-radius:3px;background:var(--bg-1);overflow:hidden}
|
||||
.g-fill{height:100%;border-radius:3px;background:var(--ok);transition:width .3s}
|
||||
.g-fill.w{background:var(--warn)}.g-fill.e{background:var(--err)}
|
||||
.g-val{font-family:var(--font-mono);font-size:11px;color:var(--ink-2);width:34px;text-align:right}
|
||||
.g-val{font-family:var(--font-mono);font-size:11px;color:var(--ink-2);
|
||||
min-width:34px;white-space:nowrap;flex-shrink:0;text-align:right}
|
||||
.tile-foot{font-family:var(--font-terminal);font-size:10px;color:var(--ink-4);
|
||||
display:flex;align-items:center;gap:5px;user-select:none}
|
||||
display:flex;align-items:center;justify-content:space-between;user-select:none}
|
||||
.tile-foot-info{display:flex;align-items:center;gap:5px;min-width:0;overflow:hidden}
|
||||
.btn-del-agent{border:none;background:transparent;cursor:pointer;flex-shrink:0;
|
||||
color:var(--ink-5);font-size:11px;padding:2px 4px;border-radius:4px;
|
||||
line-height:1;transition:color .15s,background .15s;user-select:none}
|
||||
.btn-del-agent:hover{color:var(--err);background:color-mix(in srgb,var(--err) 12%,transparent)}
|
||||
|
||||
/* FOOTER */
|
||||
.footer{background:var(--bg-0);border-top:1px solid var(--border-2);height:26px;
|
||||
|
||||
|
After Width: | Height: | Size: 173 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 235 B |
|
After Width: | Height: | Size: 306 B |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 768 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<!-- Fond arrondi -->
|
||||
<rect width="64" height="64" rx="14" fill="#282828"/>
|
||||
<!-- Cercle accent -->
|
||||
<circle cx="32" cy="32" r="22" fill="none" stroke="#fe8019" stroke-width="4"/>
|
||||
<!-- Barre CPU style jauge -->
|
||||
<rect x="18" y="28" width="28" height="4" rx="2" fill="#504945"/>
|
||||
<rect x="18" y="28" width="18" height="4" rx="2" fill="#fe8019"/>
|
||||
<!-- Point LED vert -->
|
||||
<circle cx="43" cy="21" r="4" fill="#b8bb26"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 498 B |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Nanometrics",
|
||||
"short_name": "Nanometrics",
|
||||
"description": "Tableau de bord de surveillance système",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#282828",
|
||||
"theme_color": "#fe8019",
|
||||
"icons": [
|
||||
{ "src": "favicon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "favicon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nanometrics</title>
|
||||
<link rel="icon" type="image/x-icon" href="favicon/favicon.ico">
|
||||
<link rel="icon" type="image/svg+xml" href="favicon/favicon.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="favicon/favicon-180.png">
|
||||
<link rel="manifest" href="favicon/site.webmanifest">
|
||||
<meta name="theme-color" content="#282828">
|
||||
<link rel="stylesheet" href="vendor/fontawesome/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
</head>
|
||||
@@ -66,12 +73,12 @@
|
||||
<img id="pop-icon-img" src="" alt="" style="display:none">
|
||||
<div class="agent-icon-overlay"><i class="fa-solid fa-camera"></i><span>Changer</span></div>
|
||||
</div>
|
||||
<input type="file" id="icon-upload" accept=".svg,.jpg,.jpeg,.png,.webp" style="display:none">
|
||||
<input type="file" id="icon-upload" accept=".jpg,.jpeg,.png,.webp" style="display:none">
|
||||
<div style="flex:1">
|
||||
<div class="pop-host" id="pop-host">—</div>
|
||||
<div class="pop-ip" id="pop-ip">—</div>
|
||||
<div style="font-size:10px;color:var(--ink-4);font-family:var(--font-terminal);margin-top:2px">
|
||||
Cliquer sur l'icône pour personnaliser · SVG JPG PNG WEBP · max 128×128 px
|
||||
<div id="icon-hint" style="font-size:10px;color:var(--ink-4);font-family:var(--font-terminal);margin-top:2px">
|
||||
Cliquer sur l'icône pour personnaliser · JPG PNG WEBP · max 128×128 px
|
||||
</div>
|
||||
</div>
|
||||
<div class="pop-led" id="pop-led"></div>
|
||||
@@ -147,6 +154,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIALOG SUPPRESSION AGENT -->
|
||||
<div class="overlay" id="overlay-del" style="display:none;z-index:400" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div class="popup" style="width:360px;max-width:96vw" onclick="event.stopPropagation()">
|
||||
<div style="padding:20px 20px 0">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:color-mix(in srgb,var(--err) 15%,transparent);display:flex;align-items:center;justify-content:center;color:var(--err);font-size:16px;flex-shrink:0"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:14px">Supprimer l'agent</div>
|
||||
<div style="font-size:12px;color:var(--fg2);margin-top:2px">Cette action est irréversible</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:13px;margin:0 0 8px">Supprimer <strong id="del-agent-name">—</strong> ?<br>
|
||||
<span style="font-size:11px;color:var(--fg2)">Toutes les métriques historiques seront effacées.</span></p>
|
||||
</div>
|
||||
<div style="padding:16px 20px;display:flex;justify-content:flex-end;gap:8px">
|
||||
<button class="btn" onclick="document.getElementById('overlay-del').style.display='none'">Annuler</button>
|
||||
<button class="btn" id="del-agent-confirm"
|
||||
style="background:var(--err);color:#fff;border-color:var(--err)"
|
||||
onclick="Popups.doDeleteAgent()"><i class="fa-solid fa-trash"></i> Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/charts.js"></script>
|
||||
<script src="js/grid.js"></script>
|
||||
|
||||
@@ -27,6 +27,11 @@ const API = (() => {
|
||||
if (!r.ok) throw new Error(`PUT ${path}: ${r.status}`);
|
||||
}
|
||||
|
||||
async function del(path) {
|
||||
const r = await fetch(BASE + path, { method: 'DELETE' });
|
||||
if (!r.ok) throw new Error(`DELETE ${path}: ${r.status}`);
|
||||
}
|
||||
|
||||
async function postForm(path, formData) {
|
||||
const r = await fetch(BASE + path, { method: 'POST', body: formData });
|
||||
if (!r.ok) throw new Error(`POST ${path}: ${r.status}`);
|
||||
@@ -44,6 +49,7 @@ const API = (() => {
|
||||
fd.append('icon', file);
|
||||
return postForm(`/api/agents/${id}/icon`, fd);
|
||||
},
|
||||
deleteAgent: (id) => del(`/api/agents/${id}`),
|
||||
iconUrl: (id) => `/api/agents/${id}/icon`,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -58,7 +58,7 @@ const Grid = (() => {
|
||||
const iconContent = `<img src="${API.iconUrl(id)}" alt=""
|
||||
style="width:100%;height:100%;object-fit:cover;border-radius:7px"
|
||||
onerror="this.style.display='none';this.nextSibling.style.display='flex'">
|
||||
<span style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--accent)">
|
||||
<span style="display:none;align-items:center;justify-content:center;width:100%;height:100%;color:var(--accent)">
|
||||
<i class="fa-solid fa-server"></i></span>`;
|
||||
|
||||
return `<div class="tile ${sc}" id="tile-${id}" onclick="Popups.showDetail('${esc(id)}')">
|
||||
@@ -81,19 +81,25 @@ const Grid = (() => {
|
||||
<div class="g-ico" data-tip="RAM"><i class="fa-solid fa-memory"></i></div>
|
||||
<div class="g-bar"><div class="g-fill ${offline ? '' : gFill(memPct ?? 0)}"
|
||||
style="width:${offline ? 0 : (memPct ?? 0).toFixed(0)}%"></div></div>
|
||||
<span class="g-val">${offline ? '—' : fmtPct(memPct)}</span>
|
||||
<span class="g-val">${offline ? '—' : (metrics?.memory_used && metrics?.memory_total ? fmt(metrics.memory_used) + '/' + fmt(metrics.memory_total) : '—')}</span>
|
||||
</div>
|
||||
<div class="g-row">
|
||||
<div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div>
|
||||
<div class="g-bar"><div class="g-fill ${offline ? '' : (diskPct >= (App.serverConfig?.warn_disk ?? 75) ? 'w' : '')}"
|
||||
style="width:${offline ? 0 : (diskPct ?? 0).toFixed(0)}%"></div></div>
|
||||
<span class="g-val">${offline ? '—' : fmtPct(diskPct)}</span>
|
||||
<span class="g-val">${offline ? '—' : (metrics?.hdd_used && metrics?.hdd_total ? fmt(metrics.hdd_used) + '/' + fmt(metrics.hdd_total) : '—')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile-foot">
|
||||
${offline
|
||||
<span class="tile-foot-info">
|
||||
${offline
|
||||
? '<i class="fa-solid fa-circle-xmark" style="color:var(--err)"></i><span style="color:var(--err)">Hors ligne</span>'
|
||||
: `<i class="fa-solid fa-clock"></i><span>${uptimeStr}</span>`}
|
||||
: `<i class="fa-solid fa-clock"></i><span>${uptimeStr || '—'}</span>`}
|
||||
</span>
|
||||
<button class="btn-del-agent" title="Supprimer cet agent"
|
||||
onclick="event.stopPropagation();Popups.confirmDeleteAgent('${esc(id)}','${esc(agent.hostname)}')">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -101,6 +107,12 @@ const Grid = (() => {
|
||||
function update(agentId, metrics) {
|
||||
const entry = _agents.get(agentId);
|
||||
if (!entry) return;
|
||||
// Conserver les valeurs lentes (disque, smart) quand le paquet ne les contient pas
|
||||
if (entry.metrics) {
|
||||
for (const k of Object.keys(entry.metrics)) {
|
||||
if (metrics[k] == null && entry.metrics[k] != null) metrics[k] = entry.metrics[k];
|
||||
}
|
||||
}
|
||||
entry.metrics = metrics;
|
||||
const el = document.getElementById('tile-' + agentId);
|
||||
if (el) {
|
||||
@@ -112,7 +124,7 @@ const Grid = (() => {
|
||||
function refresh(agents) {
|
||||
agents.forEach(a => {
|
||||
if (!_agents.has(a.id)) {
|
||||
_agents.set(a.id, { agent: a, metrics: null });
|
||||
_agents.set(a.id, { agent: a, metrics: a.last_metrics || null });
|
||||
} else {
|
||||
_agents.get(a.id).agent = a;
|
||||
}
|
||||
@@ -139,6 +151,13 @@ const Grid = (() => {
|
||||
document.getElementById('stat-err').textContent = err;
|
||||
}
|
||||
|
||||
function removeAgent(id) {
|
||||
_agents.delete(id);
|
||||
const el = document.getElementById('tile-' + id);
|
||||
if (el) el.remove();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
function getAgent(id) { return _agents.get(id); }
|
||||
|
||||
function updateStatus(agentId, status) {
|
||||
@@ -150,5 +169,5 @@ const Grid = (() => {
|
||||
updateStats();
|
||||
}
|
||||
|
||||
return { refresh, update, updateStatus, getAgent, fmt, fmtPct };
|
||||
return { refresh, update, updateStatus, removeAgent, getAgent, fmt, fmtPct };
|
||||
})();
|
||||
|
||||
@@ -30,8 +30,27 @@ const Popups = (() => {
|
||||
document.getElementById('icon-upload').onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
await API.uploadIcon(agentId, file);
|
||||
img.src = API.iconUrl(agentId) + '?t=' + Date.now();
|
||||
const hint = document.getElementById('icon-hint');
|
||||
try {
|
||||
await API.uploadIcon(agentId, file);
|
||||
const ts = '?t=' + Date.now();
|
||||
img.src = API.iconUrl(agentId) + ts;
|
||||
img.style.display = 'block';
|
||||
document.getElementById('pop-icon-fa').style.display = 'none';
|
||||
const tileImg = document.querySelector(`#tile-${CSS.escape(agentId)} .t-icon img`);
|
||||
if (tileImg) tileImg.src = API.iconUrl(agentId) + ts;
|
||||
} catch (err) {
|
||||
if (hint) {
|
||||
hint.style.color = 'var(--err)';
|
||||
hint.textContent = 'Erreur : ' + (err.message || 'téléversement échoué');
|
||||
setTimeout(() => {
|
||||
hint.style.color = '';
|
||||
hint.textContent = 'Cliquer sur l\'icône pour personnaliser · JPG PNG WEBP · max 128×128 px';
|
||||
}, 4000);
|
||||
}
|
||||
} finally {
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// Uptime
|
||||
@@ -119,6 +138,9 @@ const Popups = (() => {
|
||||
<div class="meta-grid">
|
||||
<div class="meta"><div class="meta-lbl">HOSTNAME</div><div class="meta-val">${esc(agent.hostname)}</div></div>
|
||||
<div class="meta"><div class="meta-lbl">ADRESSE IP</div><div class="meta-val">${esc(agent.ip) || '—'}</div></div>
|
||||
<div class="meta"><div class="meta-lbl">VERSION AGENT</div><div class="meta-val" style="display:flex;align-items:center;gap:6px">
|
||||
${agent.version ? `<span style="font-family:var(--font-mono);background:var(--bg-1);border:1px solid var(--border-2);border-radius:5px;padding:1px 7px;font-size:11px;color:var(--accent)">v${esc(agent.version)}</span>` : '<span style="color:var(--ink-4)">—</span>'}
|
||||
</div></div>
|
||||
<div class="meta"><div class="meta-lbl">PROTOCOLES ACTIFS</div><div style="display:flex;gap:5px;margin-top:4px">${protos || '—'}</div></div>
|
||||
<div class="meta"><div class="meta-lbl">DERNIER CONTACT</div><div class="meta-val">${new Date(agent.last_seen * 1000).toLocaleTimeString('fr-FR')}</div></div>
|
||||
</div>
|
||||
@@ -305,6 +327,21 @@ const Popups = (() => {
|
||||
${[[15,'15 min'],[30,'30 min'],[60,'1 heure'],[360,'6 heures']].map(([v,l]) =>
|
||||
`<option value="${v}" ${(cfg.chart_duration_min??30)==v?'selected':''}>${l}</option>`).join('')}
|
||||
</select></div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<div class="scfg-sec-title">INSTALLATION AGENT</div>
|
||||
<div class="scfg-row" style="flex-direction:column;align-items:stretch;gap:6px">
|
||||
<label style="font-size:0.85em;color:var(--fg2)">Commande curl — copiez et lancez en root sur la machine cible</label>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<input type="text" id="s-install-cmd" readonly
|
||||
style="flex:1;font-family:var(--font-mono);font-size:0.78em;padding:6px 8px;
|
||||
background:var(--bg2);border:1px solid var(--border);border-radius:6px;
|
||||
color:var(--fg);cursor:text;min-width:0"
|
||||
value="SERVER_IP=${window.location.hostname} curl -fsSL https://git.maison43gil.com/gilles/nano_metrics/raw/branch/main/deploy/install.sh | sudo bash">
|
||||
<button class="btn" style="padding:5px 10px;font-size:0.8em;white-space:nowrap"
|
||||
onclick="navigator.clipboard.writeText(document.getElementById('s-install-cmd').value).then(()=>{this.textContent='✓ Copié';setTimeout(()=>this.textContent='Copier',1500)})">Copier</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.getElementById('overlay-srvcfg').style.display = 'flex';
|
||||
}
|
||||
@@ -391,10 +428,32 @@ const Popups = (() => {
|
||||
document.getElementById('overlay-smart').style.display = 'flex';
|
||||
}
|
||||
|
||||
// ══ SUPPRESSION AGENT ══
|
||||
let _delAgentId = null;
|
||||
|
||||
function confirmDeleteAgent(id, hostname) {
|
||||
_delAgentId = id;
|
||||
document.getElementById('del-agent-name').textContent = hostname || id;
|
||||
document.getElementById('overlay-del').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function doDeleteAgent() {
|
||||
if (!_delAgentId) return;
|
||||
const id = _delAgentId;
|
||||
document.getElementById('overlay-del').style.display = 'none';
|
||||
_delAgentId = null;
|
||||
try {
|
||||
await API.deleteAgent(id);
|
||||
Grid.removeAgent(id);
|
||||
} catch (e) {
|
||||
alert('Erreur lors de la suppression : ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showDetail, hideDetail,
|
||||
showAgentCfg, sendAgentConfig, toggleCbox,
|
||||
showSrvCfg, hideSrvCfg, saveSrvCfg,
|
||||
showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent,
|
||||
showSmart,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -82,22 +82,23 @@ ok "Binaire téléchargé ($(du -sh "$TMP_BIN" | cut -f1))"
|
||||
echo ""
|
||||
echo "--- Configuration du serveur ---"
|
||||
|
||||
if [ -z "${SERVER_IP:-}" ]; then
|
||||
read -rp "Adresse IP du serveur Nanometrics : " SERVER_IP
|
||||
fi
|
||||
if [ -z "${SERVER_IP:-}" ]; then
|
||||
err "SERVER_IP est requis."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVER_IP="${SERVER_IP:-10.0.0.50}"
|
||||
SERVER_PORT="${SERVER_PORT:-9999}"
|
||||
MQTT_HOST="${MQTT_HOST:-10.0.0.3}"
|
||||
MQTT_ENABLED="${MQTT_ENABLED:-false}"
|
||||
|
||||
ok "Serveur : $SERVER_IP:$SERVER_PORT"
|
||||
ok "Serveur : $SERVER_IP:$SERVER_PORT | MQTT broker : $MQTT_HOST"
|
||||
|
||||
# ── 5. Installer le binaire ────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[1/5] Installation du binaire dans /usr/local/bin/"
|
||||
|
||||
# Arrêter le service si en cours (le binaire ne peut pas être écrasé à chaud)
|
||||
if systemctl is-active --quiet nanometrics-agent 2>/dev/null; then
|
||||
warn "Service en cours — arrêt temporaire..."
|
||||
systemctl stop nanometrics-agent
|
||||
fi
|
||||
|
||||
cp "$TMP_BIN" "$INSTALL_BIN"
|
||||
chmod 755 "$INSTALL_BIN"
|
||||
ok "Binaire installé"
|
||||
@@ -111,10 +112,33 @@ ok "Répertoire créé"
|
||||
# ── 7. Écrire config.toml ─────────────────────────────────────────────────────
|
||||
echo "[3/5] Écriture de $CONFIG_FILE"
|
||||
|
||||
# Ne pas écraser une config existante (upgrade)
|
||||
OVERWRITE_CONFIG="${OVERWRITE_CONFIG:-}"
|
||||
WRITE_CONFIG=true
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
warn "config.toml déjà présent — conservé tel quel"
|
||||
else
|
||||
if [ "${OVERWRITE_CONFIG}" = "true" ]; then
|
||||
warn "OVERWRITE_CONFIG=true — config.toml sera écrasé"
|
||||
WRITE_CONFIG=true
|
||||
elif [ -t 0 ]; then
|
||||
# Mode interactif (bash local, pas curl | bash)
|
||||
echo ""
|
||||
warn "Un config.toml existe déjà :"
|
||||
echo " $CONFIG_FILE"
|
||||
printf " Écraser la configuration existante ? [o/N] : "
|
||||
read -r _ANS
|
||||
if [[ "$_ANS" =~ ^[Oo]$ ]]; then
|
||||
WRITE_CONFIG=true
|
||||
else
|
||||
ok "config.toml conservé"
|
||||
WRITE_CONFIG=false
|
||||
fi
|
||||
else
|
||||
warn "config.toml déjà présent — conservé (relancez avec OVERWRITE_CONFIG=true pour écraser)"
|
||||
WRITE_CONFIG=false
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$WRITE_CONFIG" = "true" ]; then
|
||||
cat > "$CONFIG_FILE" << TOML
|
||||
[server]
|
||||
ip = "$SERVER_IP"
|
||||
@@ -125,7 +149,7 @@ enabled = true
|
||||
|
||||
[protocols.mqtt]
|
||||
enabled = $MQTT_ENABLED
|
||||
host = "10.0.0.3"
|
||||
host = "$MQTT_HOST"
|
||||
port = 1883
|
||||
topic_base = "nanometrics/agents"
|
||||
auto_discovery = true
|
||||
@@ -160,10 +184,14 @@ mqtt = false
|
||||
udp = true
|
||||
mqtt = false
|
||||
TOML
|
||||
chmod 640 "$CONFIG_FILE"
|
||||
chmod 644 "$CONFIG_FILE"
|
||||
ok "config.toml créé"
|
||||
fi
|
||||
|
||||
# S'assurer que le fichier est toujours lisible (cas d'un config existant en 640)
|
||||
chmod 644 "$CONFIG_FILE" 2>/dev/null || true
|
||||
chmod 755 "$CONFIG_DIR"
|
||||
|
||||
# ── 8. Installer le fichier service ──────────────────────────────────────────
|
||||
echo "[4/5] Installation du service systemd"
|
||||
curl -fsSL -o "$SERVICE_FILE" "$SERVICE_URL"
|
||||
|
||||
@@ -11,7 +11,7 @@ RestartSec=5
|
||||
|
||||
DynamicUser=yes
|
||||
ConfigurationDirectory=nanometrics
|
||||
ConfigurationDirectoryMode=0750
|
||||
ConfigurationDirectoryMode=0755
|
||||
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
|
||||
@@ -25,7 +25,9 @@ DESCRIPTION="${2:-Release $TAG}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
CARGO_TOML="$ROOT/agent/Cargo.toml"
|
||||
|
||||
# ── 1. Compiler pour toutes les cibles supportées ──────────────────────────
|
||||
mkdir -p "$ROOT/dist"
|
||||
|
||||
# ── 1. Compiler l'agent pour toutes les cibles supportées ────────────────
|
||||
echo "=== Compilation de l'agent ==="
|
||||
TARGETS=("x86_64-unknown-linux-musl" "aarch64-unknown-linux-musl")
|
||||
LABELS=("linux-amd64" "linux-arm64")
|
||||
@@ -40,7 +42,9 @@ for i in "${!TARGETS[@]}"; do
|
||||
# Installer la cible si absente
|
||||
rustup target add "$TARGET" 2>/dev/null || true
|
||||
|
||||
# Compiler
|
||||
# Compiler (CC requis par la crate ring pour les cibles musl cross)
|
||||
CC_aarch64_unknown_linux_musl=aarch64-linux-gnu-gcc \
|
||||
AR_aarch64_unknown_linux_musl=aarch64-linux-gnu-ar \
|
||||
cargo build --release \
|
||||
--manifest-path "$CARGO_TOML" \
|
||||
--target "$TARGET" \
|
||||
|
||||
@@ -5,10 +5,9 @@ RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o nanometrics-server .
|
||||
|
||||
FROM alpine:3.19
|
||||
RUN apk add --no-cache ca-certificates
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/nanometrics-server .
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /app/nanometrics-server /nanometrics-server
|
||||
VOLUME /data
|
||||
EXPOSE 8080 9999/udp
|
||||
CMD ["./nanometrics-server"]
|
||||
CMD ["/nanometrics-server"]
|
||||
|
||||
@@ -20,7 +20,7 @@ const schema = `
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id TEXT PRIMARY KEY, hostname TEXT NOT NULL,
|
||||
ip TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'offline',
|
||||
last_seen INTEGER NOT NULL DEFAULT 0
|
||||
last_seen INTEGER NOT NULL DEFAULT 0, version TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS metrics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL, ts INTEGER NOT NULL,
|
||||
@@ -57,8 +57,12 @@ func Open(path string) (*DB, error) {
|
||||
}
|
||||
|
||||
func (d *DB) migrate() error {
|
||||
_, err := d.conn.Exec(schema)
|
||||
return err
|
||||
if _, err := d.conn.Exec(schema); err != nil {
|
||||
return err
|
||||
}
|
||||
// Migrations additives pour les colonnes ajoutées après la création initiale
|
||||
_, _ = d.conn.Exec(`ALTER TABLE agents ADD COLUMN version TEXT NOT NULL DEFAULT ''`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() { _ = d.conn.Close() }
|
||||
@@ -66,11 +70,12 @@ func (d *DB) Close() { _ = d.conn.Close() }
|
||||
func (d *DB) UpsertAgent(m *models.AgentMetrics) error {
|
||||
ts := time.Now().Unix()
|
||||
_, err := d.conn.Exec(`
|
||||
INSERT INTO agents (id, hostname, ip, status, last_seen)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO agents (id, hostname, ip, status, last_seen, version)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
ip=excluded.ip, status=excluded.status, last_seen=excluded.last_seen`,
|
||||
m.Hostname, m.Hostname, m.IP, m.Status, ts)
|
||||
ip=excluded.ip, status=excluded.status, last_seen=excluded.last_seen,
|
||||
version=CASE WHEN excluded.version != '' THEN excluded.version ELSE version END`,
|
||||
m.Hostname, m.Hostname, m.IP, m.Status, ts, m.Version)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -104,7 +109,7 @@ func (d *DB) InsertMetrics(m *models.AgentMetrics) error {
|
||||
}
|
||||
|
||||
func (d *DB) GetAgents() ([]models.Agent, error) {
|
||||
rows, err := d.conn.Query(`SELECT id, hostname, ip, status, last_seen FROM agents`)
|
||||
rows, err := d.conn.Query(`SELECT id, hostname, ip, status, last_seen, version FROM agents`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,7 +117,7 @@ func (d *DB) GetAgents() ([]models.Agent, error) {
|
||||
var agents []models.Agent
|
||||
for rows.Next() {
|
||||
var a models.Agent
|
||||
if err := rows.Scan(&a.ID, &a.Hostname, &a.IP, &a.Status, &a.LastSeen); err != nil {
|
||||
if err := rows.Scan(&a.ID, &a.Hostname, &a.IP, &a.Status, &a.LastSeen, &a.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agents = append(agents, a)
|
||||
@@ -123,6 +128,72 @@ func (d *DB) GetAgents() ([]models.Agent, error) {
|
||||
return agents, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetLastMetrics(agentID string) (*models.AgentMetrics, error) {
|
||||
var cpu, temperature *float64
|
||||
var memUsed, memFree, memTotal, hddUsed, hddFree, hddTotal *int64
|
||||
var uptime, netRX, netTX *int64
|
||||
var smartPassed, smartTemp, smartRealloc, smartHours, smartWear *int64
|
||||
|
||||
// Chaque sous-requête prend la dernière valeur NON NULL de sa colonne.
|
||||
// Nécessaire car les paquets rapides (2s) ne contiennent pas les métriques
|
||||
// lentes (disque, smart) qui sont envoyées toutes les 60s.
|
||||
err := d.conn.QueryRow(`
|
||||
SELECT
|
||||
(SELECT cpu_percent FROM metrics WHERE agent_id=? AND cpu_percent IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT memory_used FROM metrics WHERE agent_id=? AND memory_used IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT memory_free FROM metrics WHERE agent_id=? AND memory_free IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT memory_total FROM metrics WHERE agent_id=? AND memory_total IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT hdd_used FROM metrics WHERE agent_id=? AND hdd_used IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT hdd_free FROM metrics WHERE agent_id=? AND hdd_free IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT hdd_total FROM metrics WHERE agent_id=? AND hdd_total IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT uptime FROM metrics WHERE agent_id=? AND uptime IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT network_rx FROM metrics WHERE agent_id=? AND network_rx IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT network_tx FROM metrics WHERE agent_id=? AND network_tx IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT temperature FROM metrics WHERE agent_id=? AND temperature IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_passed FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_temp FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_realloc FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_hours FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
|
||||
(SELECT smart_wear FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1)`,
|
||||
agentID, agentID, agentID, agentID,
|
||||
agentID, agentID, agentID,
|
||||
agentID, agentID, agentID, agentID,
|
||||
agentID, agentID, agentID, agentID, agentID).
|
||||
Scan(&cpu, &memUsed, &memFree, &memTotal,
|
||||
&hddUsed, &hddFree, &hddTotal,
|
||||
&uptime, &netRX, &netTX, &temperature,
|
||||
&smartPassed, &smartTemp, &smartRealloc, &smartHours, &smartWear)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := &models.AgentMetrics{
|
||||
CPUPercent: cpu,
|
||||
MemoryUsed: memUsed,
|
||||
MemoryFree: memFree,
|
||||
MemoryTotal: memTotal,
|
||||
HDDUsed: hddUsed,
|
||||
HDDFree: hddFree,
|
||||
HDDTotal: hddTotal,
|
||||
Uptime: uptime,
|
||||
NetworkRX: netRX,
|
||||
NetworkTX: netTX,
|
||||
Temperature: temperature,
|
||||
}
|
||||
if smartPassed != nil {
|
||||
m.Smart = &models.SmartMetrics{
|
||||
Passed: *smartPassed == 1,
|
||||
Temperature: smartTemp,
|
||||
ReallocatedSectors: smartRealloc,
|
||||
PowerOnHours: smartHours,
|
||||
WearLevel: smartWear,
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetMetricsHistory(agentID string, from, to int64) ([]map[string]interface{}, error) {
|
||||
rows, err := d.conn.Query(`
|
||||
SELECT ts, cpu_percent, memory_used, memory_total, hdd_used, hdd_total
|
||||
@@ -238,6 +309,20 @@ func (d *DB) MarkOffline(timeoutSec int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) DeleteAgent(agentID string) error {
|
||||
for _, q := range []string{
|
||||
`DELETE FROM metrics WHERE agent_id = ?`,
|
||||
`DELETE FROM agent_configs WHERE agent_id = ?`,
|
||||
`DELETE FROM agent_icons WHERE agent_id = ?`,
|
||||
`DELETE FROM agents WHERE id = ?`,
|
||||
} {
|
||||
if _, err := d.conn.Exec(q, agentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkOfflineAndGetIDs marque les agents inactifs et retourne leurs IDs.
|
||||
func (d *DB) MarkOfflineAndGetIDs(timeoutSec int64) ([]string, error) {
|
||||
cutoff := time.Now().Unix() - timeoutSec
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
server:
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
pull: false
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
UDP_ADDR: "0.0.0.0:9999"
|
||||
@@ -16,6 +17,7 @@ services:
|
||||
|
||||
dashboard:
|
||||
image: nginx:alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/user/nanometrics/server/db"
|
||||
)
|
||||
@@ -14,7 +15,26 @@ func AgentsHandler(database *db.DB) http.HandlerFunc {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
for i := range agents {
|
||||
agents[i].LastMetrics, _ = database.GetLastMetrics(agents[i].ID)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(agents)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteAgentHandler(database *db.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||
if len(parts) < 3 {
|
||||
http.Error(w, "invalid path", 400)
|
||||
return
|
||||
}
|
||||
agentID := parts[2]
|
||||
if err := database.DeleteAgent(agentID); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func AgentConfigHandler(database *db.DB, pushConfig func(agentID string, cfg *mo
|
||||
return
|
||||
}
|
||||
if cfg == nil {
|
||||
cfg = &models.AgentConfig{}
|
||||
cfg = models.DefaultAgentConfig()
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(cfg)
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/user/nanometrics/server/db"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
const maxIconSize = 128
|
||||
|
||||
@@ -110,6 +110,8 @@ func main() {
|
||||
handlers.IconUploadHandler(database)(w, r)
|
||||
case endsWith(r.URL.Path, "/icon") && r.Method == http.MethodGet:
|
||||
handlers.IconGetHandler(database)(w, r)
|
||||
case r.Method == http.MethodDelete:
|
||||
handlers.DeleteAgentHandler(database)(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ type AgentMetrics struct {
|
||||
Hostname string `json:"hostname"`
|
||||
IP string `json:"ip"`
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
CPUPercent *float64 `json:"cpu_percent"`
|
||||
MemoryUsed *int64 `json:"memory_used"`
|
||||
MemoryFree *int64 `json:"memory_free"`
|
||||
@@ -32,6 +33,7 @@ type Agent struct {
|
||||
IP string `json:"ip"`
|
||||
Status string `json:"status"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
Version string `json:"version,omitempty"`
|
||||
LastMetrics *AgentMetrics `json:"last_metrics,omitempty"`
|
||||
}
|
||||
|
||||
@@ -88,6 +90,20 @@ type ServerConfig struct {
|
||||
PopupDetailH int `json:"popup_detail_h"`
|
||||
}
|
||||
|
||||
func DefaultAgentConfig() *AgentConfig {
|
||||
on := MetricProto{UDP: true, MQTT: false}
|
||||
return &AgentConfig{
|
||||
Protocols: ProtocolsConfig{UDP: UDPConfig{Enabled: true}},
|
||||
Metrics: MetricsConfig{
|
||||
CPU: on,
|
||||
Memory: on,
|
||||
Disk: on,
|
||||
Smart: on,
|
||||
Uptime: on,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultServerConfig() ServerConfig {
|
||||
return ServerConfig{
|
||||
TileMinWidth: 220, FontSize: 13,
|
||||
|
||||
@@ -2,6 +2,7 @@ server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
client_max_body_size 10m;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:8080;
|
||||
|
||||