From 65fbf3bb31d8a48bb8157e251fcde05b266afefa Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 9 Feb 2026 13:49:19 -0600 Subject: [PATCH] use datestamp instead of creating a version --- Cargo.lock | 2 + Cargo.toml | 4 ++ build.rs | 79 +++------------------------------------ src/snmp/client.rs | 12 +++--- src/snmp/device_poller.rs | 21 +++++++++-- src/version.rs | 14 +++++-- src/websocket_client.rs | 2 +- 7 files changed, 47 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d74a33..c90ab82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3359,6 +3359,7 @@ dependencies = [ "anyhow", "async-trait", "cc", + "chrono", "clap", "crossterm 0.28.1", "futures", @@ -3371,6 +3372,7 @@ dependencies = [ "prost-types", "rand 0.8.5", "ratatui", + "regex", "russh", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e5b53cf..adc10cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ rand = "0.8" ratatui = { version = "0.30", optional = true } crossterm = { version = "0.28", optional = true } +[dev-dependencies] +regex = "1" + [features] default = ["tui"] tui = ["ratatui", "crossterm"] @@ -38,6 +41,7 @@ tui = ["ratatui", "crossterm"] [build-dependencies] prost-build = "0.14" cc = "1.0" +chrono = "0.4" [profile.release] opt-level = "z" diff --git a/build.rs b/build.rs index f6dfc46..a059f8a 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,3 @@ -use std::process::Command; - fn main() { // Compile protobuf definitions prost_build::compile_protos(&["proto/agent.proto"], &["proto/"]).unwrap(); @@ -13,80 +11,15 @@ fn main() { // Link against netsnmp library println!("cargo:rustc-link-lib=netsnmp"); - // Inject git-based version if available, otherwise use Cargo.toml version - // This ensures the binary version matches the Docker image tag + // Inject compile timestamp as version + // This allows tracking when a specific agent binary was built let version = get_version(); println!("cargo:rustc-env=BUILD_VERSION={}", version); } fn get_version() -> String { - // First check if BUILD_VERSION is already set (e.g., from Docker build arg) - if let Ok(version) = std::env::var("BUILD_VERSION") { - if !version.is_empty() && version != "0.1.0-unknown" { - return version; - } - } - - // Try git describe - gives us the most descriptive version - // Examples: - // v0.2.0 -> "0.2.0" (exact tag) - // v0.2.0-5-g831588e -> "0.2.0.5.831588e" (5 commits after v0.2.0) - if let Ok(output) = Command::new("git") - .args(["describe", "--tags", "--always", "--dirty=-modified"]) - .output() - { - if output.status.success() { - let desc = String::from_utf8_lossy(&output.stdout).trim().to_string(); - - // Parse git describe output - if let Some(version) = parse_git_describe(&desc) { - return version; - } - } - } - - // Fallback: Try short commit hash only - if let Ok(output) = Command::new("git") - .args(["rev-parse", "--short", "HEAD"]) - .output() - { - if output.status.success() { - let commit = String::from_utf8_lossy(&output.stdout).trim().to_string(); - return format!("{}.{}", env!("CARGO_PKG_VERSION"), commit); - } - } - - // Final fallback to Cargo.toml version - env!("CARGO_PKG_VERSION").to_string() -} - -fn parse_git_describe(desc: &str) -> Option { - // Strip 'v' prefix if present - let desc = desc.strip_prefix('v').unwrap_or(desc); - - // Check for dirty flag - let dirty = desc.ends_with("-modified"); - let desc = desc.strip_suffix("-modified").unwrap_or(desc); - - if let Some((base, rest)) = desc.split_once('-') { - // Format: tag-N-ghash (e.g., "0.2.0-5-g831588e") - let parts: Vec<&str> = rest.split('-').collect(); - if parts.len() == 2 { - let commit_count = parts[0]; - let hash = parts[1].strip_prefix('g').unwrap_or(parts[1]); - let version = format!("{}.{}.{}", base, commit_count, hash); - return Some(if dirty { - format!("{}-modified", version) - } else { - version - }); - } - } - - // No commits after tag, just use the tag - Some(if dirty { - format!("{}-modified", desc) - } else { - desc.to_string() - }) + // Generate RFC 3339 timestamp at compile time + // Format: YYYY-MM-DDTHH:MM:SSZ + let now = chrono::Utc::now(); + now.format("%Y-%m-%dT%H:%M:%SZ").to_string() } diff --git a/src/snmp/client.rs b/src/snmp/client.rs index 0ab9fa5..ed4e4ba 100644 --- a/src/snmp/client.rs +++ b/src/snmp/client.rs @@ -355,9 +355,7 @@ impl SnmpSession { Err(_) => Ok(SnmpValue::OctetString(value_buf[..value_len].to_vec())), } } - ASN_OPAQUE => { - Ok(SnmpValue::OctetString(value_buf[..value_len].to_vec())) - } + ASN_OPAQUE => Ok(SnmpValue::OctetString(value_buf[..value_len].to_vec())), ASN_IPADDRESS => { // IP addresses are 4 bytes - convert to dotted notation if value_len == 4 { @@ -466,7 +464,9 @@ impl SnmpSession { // Try UTF-8 conversion first match String::from_utf8(res.value[..res.value_len].to_vec()) { Ok(s) => SnmpValue::String(s), - Err(_) => SnmpValue::OctetString(res.value[..res.value_len].to_vec()), + Err(_) => { + SnmpValue::OctetString(res.value[..res.value_len].to_vec()) + } } } ASN_OPAQUE => SnmpValue::OctetString(res.value[..res.value_len].to_vec()), @@ -483,7 +483,9 @@ impl SnmpSession { ASN_OBJECT_ID => { match String::from_utf8(res.value[..res.value_len].to_vec()) { Ok(s) => SnmpValue::Oid(s), - Err(_) => SnmpValue::OctetString(res.value[..res.value_len].to_vec()), + Err(_) => { + SnmpValue::OctetString(res.value[..res.value_len].to_vec()) + } } } ASN_INTEGER | ASN_COUNTER | ASN_GAUGE | ASN_TIMETICKS | ASN_UINTEGER => { diff --git a/src/snmp/device_poller.rs b/src/snmp/device_poller.rs index 3d4b1cc..c061384 100644 --- a/src/snmp/device_poller.rs +++ b/src/snmp/device_poller.rs @@ -58,20 +58,29 @@ impl DevicePoller { let config_clone = config.clone(); // Spawn the polling thread with 8MB stack for SNMPv3 crypto operations - tracing::info!("Spawning device poller thread for {} at {}:{}", device_id, config.ip, config.port); + tracing::info!( + "Spawning device poller thread for {} at {}:{}", + device_id, + config.ip, + config.port + ); std::thread::Builder::new() .name(format!("poller-{}", device_id)) .stack_size(8 * 1024 * 1024) // 8MB stack (default is 2MB) .spawn(move || { tracing::info!("Device poller thread starting for {}", device_id_clone); - if let Err(e) = run_poller_thread(device_id_clone.clone(), config_clone, request_rx) { + if let Err(e) = run_poller_thread(device_id_clone.clone(), config_clone, request_rx) + { tracing::error!("Device poller thread failed for {}: {}", device_id_clone, e); } tracing::info!("Device poller thread exited for {}", device_id_clone); }) .expect("Failed to spawn device poller thread"); - tracing::info!("Successfully spawned device poller thread for {}", device_id); + tracing::info!( + "Successfully spawned device poller thread for {}", + device_id + ); Self { device_id, @@ -184,7 +193,11 @@ fn run_poller_thread( SnmpRequest::Get { oid, response_tx } => { tracing::debug!("Poller thread {} executing GET", device_id); let result = perform_get(&runtime, &client, &config, &oid); - tracing::debug!("Poller thread {} GET result: {:?}", device_id, result.is_ok()); + tracing::debug!( + "Poller thread {} GET result: {:?}", + device_id, + result.is_ok() + ); let _ = response_tx.send(result); } SnmpRequest::Walk { diff --git a/src/version.rs b/src/version.rs index 923d04d..7405e1c 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,4 +1,5 @@ -/// Get version at runtime - prefers BUILD_VERSION from build.rs, falls back to Cargo.toml +/// Get compile timestamp at runtime - returns RFC 3339 formatted timestamp from build.rs +/// Format: YYYY-MM-DDTHH:MM:SSZ (e.g., "2025-02-09T15:30:45Z") pub fn current_version() -> &'static str { option_env!("BUILD_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")) } @@ -25,9 +26,14 @@ mod tests { #[test] fn test_current_version_format() { let version = current_version(); - // Version should be in semver-like format (e.g., "0.1.0" or custom BUILD_VERSION) - // At minimum, should have some content - assert!(!version.is_empty()); + // Version should be RFC 3339 timestamp format (YYYY-MM-DDTHH:MM:SSZ) + // Regex: ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$ + let rfc3339_pattern = regex::Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$").unwrap(); + assert!( + rfc3339_pattern.is_match(version), + "Version should be RFC 3339 timestamp, got: {}", + version + ); } #[test] diff --git a/src/websocket_client.rs b/src/websocket_client.rs index 6468ca8..7bb43e9 100644 --- a/src/websocket_client.rs +++ b/src/websocket_client.rs @@ -686,7 +686,7 @@ impl AgentClient { /// Send heartbeat to server. async fn send_heartbeat(&mut self) -> Result<()> { let heartbeat = AgentHeartbeat { - version: env!("CARGO_PKG_VERSION").to_string(), + version: crate::version::current_version().to_string(), hostname: self.cached_hostname.clone(), uptime_seconds: get_uptime_seconds(), ip_address: get_local_ip().unwrap_or_default(),