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",
"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",

View file

@ -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"

View file

@ -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<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()
})
// 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()
}

View file

@ -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 => {

View file

@ -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 {

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 {
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]

View file

@ -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(),