use datestamp instead of creating a version

This commit is contained in:
Graham McIntire 2026-02-09 13:49:19 -06:00
parent bd08bb3aad
commit 65fbf3bb31
No known key found for this signature in database
7 changed files with 47 additions and 87 deletions

2
Cargo.lock generated
View file

@ -3359,6 +3359,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"cc", "cc",
"chrono",
"clap", "clap",
"crossterm 0.28.1", "crossterm 0.28.1",
"futures", "futures",
@ -3371,6 +3372,7 @@ dependencies = [
"prost-types", "prost-types",
"rand 0.8.5", "rand 0.8.5",
"ratatui", "ratatui",
"regex",
"russh", "russh",
"serde", "serde",
"serde_json", "serde_json",

View file

@ -31,6 +31,9 @@ rand = "0.8"
ratatui = { version = "0.30", optional = true } ratatui = { version = "0.30", optional = true }
crossterm = { version = "0.28", optional = true } crossterm = { version = "0.28", optional = true }
[dev-dependencies]
regex = "1"
[features] [features]
default = ["tui"] default = ["tui"]
tui = ["ratatui", "crossterm"] tui = ["ratatui", "crossterm"]
@ -38,6 +41,7 @@ tui = ["ratatui", "crossterm"]
[build-dependencies] [build-dependencies]
prost-build = "0.14" prost-build = "0.14"
cc = "1.0" cc = "1.0"
chrono = "0.4"
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"

View file

@ -1,5 +1,3 @@
use std::process::Command;
fn main() { fn main() {
// Compile protobuf definitions // Compile protobuf definitions
prost_build::compile_protos(&["proto/agent.proto"], &["proto/"]).unwrap(); prost_build::compile_protos(&["proto/agent.proto"], &["proto/"]).unwrap();
@ -13,80 +11,15 @@ fn main() {
// Link against netsnmp library // Link against netsnmp library
println!("cargo:rustc-link-lib=netsnmp"); println!("cargo:rustc-link-lib=netsnmp");
// Inject git-based version if available, otherwise use Cargo.toml version // Inject compile timestamp as version
// This ensures the binary version matches the Docker image tag // This allows tracking when a specific agent binary was built
let version = get_version(); let version = get_version();
println!("cargo:rustc-env=BUILD_VERSION={}", version); println!("cargo:rustc-env=BUILD_VERSION={}", version);
} }
fn get_version() -> String { fn get_version() -> String {
// First check if BUILD_VERSION is already set (e.g., from Docker build arg) // Generate RFC 3339 timestamp at compile time
if let Ok(version) = std::env::var("BUILD_VERSION") { // Format: YYYY-MM-DDTHH:MM:SSZ
if !version.is_empty() && version != "0.1.0-unknown" { let now = chrono::Utc::now();
return version; now.format("%Y-%m-%dT%H:%M:%SZ").to_string()
}
}
// 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<String> {
// 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()
})
} }

View file

@ -355,9 +355,7 @@ impl SnmpSession {
Err(_) => Ok(SnmpValue::OctetString(value_buf[..value_len].to_vec())), Err(_) => Ok(SnmpValue::OctetString(value_buf[..value_len].to_vec())),
} }
} }
ASN_OPAQUE => { ASN_OPAQUE => Ok(SnmpValue::OctetString(value_buf[..value_len].to_vec())),
Ok(SnmpValue::OctetString(value_buf[..value_len].to_vec()))
}
ASN_IPADDRESS => { ASN_IPADDRESS => {
// IP addresses are 4 bytes - convert to dotted notation // IP addresses are 4 bytes - convert to dotted notation
if value_len == 4 { if value_len == 4 {
@ -466,7 +464,9 @@ impl SnmpSession {
// Try UTF-8 conversion first // Try UTF-8 conversion first
match String::from_utf8(res.value[..res.value_len].to_vec()) { match String::from_utf8(res.value[..res.value_len].to_vec()) {
Ok(s) => SnmpValue::String(s), 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()), ASN_OPAQUE => SnmpValue::OctetString(res.value[..res.value_len].to_vec()),
@ -483,7 +483,9 @@ impl SnmpSession {
ASN_OBJECT_ID => { ASN_OBJECT_ID => {
match String::from_utf8(res.value[..res.value_len].to_vec()) { match String::from_utf8(res.value[..res.value_len].to_vec()) {
Ok(s) => SnmpValue::Oid(s), 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 => { ASN_INTEGER | ASN_COUNTER | ASN_GAUGE | ASN_TIMETICKS | ASN_UINTEGER => {

View file

@ -58,20 +58,29 @@ impl DevicePoller {
let config_clone = config.clone(); let config_clone = config.clone();
// Spawn the polling thread with 8MB stack for SNMPv3 crypto operations // 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() std::thread::Builder::new()
.name(format!("poller-{}", device_id)) .name(format!("poller-{}", device_id))
.stack_size(8 * 1024 * 1024) // 8MB stack (default is 2MB) .stack_size(8 * 1024 * 1024) // 8MB stack (default is 2MB)
.spawn(move || { .spawn(move || {
tracing::info!("Device poller thread starting for {}", device_id_clone); 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::error!("Device poller thread failed for {}: {}", device_id_clone, e);
} }
tracing::info!("Device poller thread exited for {}", device_id_clone); tracing::info!("Device poller thread exited for {}", device_id_clone);
}) })
.expect("Failed to spawn device poller thread"); .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 { Self {
device_id, device_id,
@ -184,7 +193,11 @@ fn run_poller_thread(
SnmpRequest::Get { oid, response_tx } => { SnmpRequest::Get { oid, response_tx } => {
tracing::debug!("Poller thread {} executing GET", device_id); tracing::debug!("Poller thread {} executing GET", device_id);
let result = perform_get(&runtime, &client, &config, &oid); 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); let _ = response_tx.send(result);
} }
SnmpRequest::Walk { SnmpRequest::Walk {

View file

@ -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 { pub fn current_version() -> &'static str {
option_env!("BUILD_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")) option_env!("BUILD_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
} }
@ -25,9 +26,14 @@ mod tests {
#[test] #[test]
fn test_current_version_format() { fn test_current_version_format() {
let version = current_version(); let version = current_version();
// Version should be in semver-like format (e.g., "0.1.0" or custom BUILD_VERSION) // Version should be RFC 3339 timestamp format (YYYY-MM-DDTHH:MM:SSZ)
// At minimum, should have some content // Regex: ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$
assert!(!version.is_empty()); 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] #[test]

View file

@ -686,7 +686,7 @@ impl AgentClient {
/// Send heartbeat to server. /// Send heartbeat to server.
async fn send_heartbeat(&mut self) -> Result<()> { async fn send_heartbeat(&mut self) -> Result<()> {
let heartbeat = AgentHeartbeat { let heartbeat = AgentHeartbeat {
version: env!("CARGO_PKG_VERSION").to_string(), version: crate::version::current_version().to_string(),
hostname: self.cached_hostname.clone(), hostname: self.cached_hostname.clone(),
uptime_seconds: get_uptime_seconds(), uptime_seconds: get_uptime_seconds(),
ip_address: get_local_ip().unwrap_or_default(), ip_address: get_local_ip().unwrap_or_default(),