refactor: remove 4 more dependencies (rand, ureq, tiny_http, anyhow)

Removed dependencies:
- rand: Replaced with SystemTime-based random for ICMP ID/sequence
- ureq: Removed Docker Hub version checking (Watchtower handles updates)
- tiny_http: Replaced with raw TCP listener for /health endpoint
- anyhow: Replaced with std::error::Error trait

Benefits:
- Binary size: 2.3M → 1.5M (800KB reduction, 35% smaller!)
- Dependencies: 19 → 11 direct dependencies (-8 total)
- Tests: 80 → 67 (removed Docker Hub API tests)
- All tests passing

Custom implementations:
- ICMP random: Use nanosecond timestamp for ID/sequence
- Health endpoint: Simple HTTP/1.1 parser on raw TCP
- Error handling: type Result<T> = std::result::Result<T, Box<dyn Error>>

Total progress from initial state:
- Binary: 2.4M → 1.5M (37.5% reduction!)
- Dependencies removed: 8 (thiserror, hostname, chrono, base64, rand, ureq, tiny_http, anyhow)
This commit is contained in:
Graham McIntire 2026-01-19 17:20:49 -06:00
parent d02193921d
commit 55fb691440
No known key found for this signature in database
6 changed files with 86 additions and 448 deletions

126
Cargo.lock generated
View file

@ -2,12 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.4"
@ -73,18 +67,6 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "ascii"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.10.0"
@ -128,12 +110,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chunked_transfer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
[[package]]
name = "clap"
version = "4.5.54"
@ -189,15 +165,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
@ -275,16 +242,6 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@ -444,12 +401,6 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "icu_collections"
version = "2.1.1"
@ -613,15 +564,6 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.1.1"
@ -878,22 +820,7 @@ dependencies = [
"log",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"subtle",
"zeroize",
]
[[package]]
name = "rustls"
version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.8",
"rustls-webpki",
"subtle",
"zeroize",
]
@ -918,17 +845,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "serde"
version = "1.0.228"
@ -1100,18 +1016,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tiny_http"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
dependencies = [
"ascii",
"chunked_transfer",
"httpdate",
"log",
]
[[package]]
name = "tinystr"
version = "0.8.2"
@ -1154,7 +1058,7 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [
"rustls 0.22.4",
"rustls",
"rustls-pki-types",
"tokio",
]
@ -1167,7 +1071,7 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
dependencies = [
"futures-util",
"log",
"rustls 0.22.4",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
@ -1179,22 +1083,18 @@ dependencies = [
name = "towerops-agent"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"futures",
"log",
"prost",
"prost-build",
"prost-types",
"rand",
"serde",
"serde_json",
"snmp",
"socket2 0.5.10",
"tiny_http",
"tokio",
"tokio-tungstenite",
"ureq",
]
[[package]]
@ -1210,7 +1110,7 @@ dependencies = [
"httparse",
"log",
"rand",
"rustls 0.22.4",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror",
@ -1236,24 +1136,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64",
"flate2",
"log",
"once_cell",
"rustls 0.23.36",
"rustls-pki-types",
"serde",
"serde_json",
"url",
"webpki-roots 0.26.11",
]
[[package]]
name = "url"
version = "2.5.8"

View file

@ -5,7 +5,6 @@ edition = "2021"
[dependencies]
snmp = "0.2"
ureq = { version = "2.10", features = ["json"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net"] }
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
futures = "0.3"
@ -15,10 +14,7 @@ log = { version = "0.4", features = ["std"] }
clap = { version = "4.0", features = ["derive", "env"] }
prost = "0.13"
prost-types = "0.13"
tiny_http = "0.12"
anyhow = "1.0"
socket2 = "0.5"
rand = "0.8"
[build-dependencies]
prost-build = "0.13"

View file

@ -1,9 +1,11 @@
use anyhow::Result;
use log::info;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Instant;
use tiny_http::{Response, Server};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
@ -12,54 +14,64 @@ pub struct HealthStatus {
pub uptime_seconds: u64,
}
/// Start a simple health check HTTP server
/// Start a simple health check HTTP server using raw TCP
pub async fn start_health_server(port: u16) -> Result<()> {
let start_time = Arc::new(Instant::now());
let addr = format!("0.0.0.0:{}", port);
tokio::task::spawn_blocking(move || {
let addr = format!("0.0.0.0:{}", port);
info!("Starting health endpoint on {}", addr);
info!("Starting health endpoint on {}", addr);
let listener = TcpListener::bind(&addr).await?;
let server = Server::http(&addr)
.map_err(|e| anyhow::anyhow!("Failed to start health server: {}", e))?;
loop {
match listener.accept().await {
Ok((mut socket, _)) => {
let start_time = Arc::clone(&start_time);
for request in server.incoming_requests() {
let path = request.url();
tokio::spawn(async move {
let mut buffer = [0u8; 1024];
if path == "/health" {
let uptime = start_time.elapsed().as_secs();
let status = HealthStatus {
status: "healthy".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
uptime_seconds: uptime,
};
// Read the HTTP request
if let Ok(n) = socket.read(&mut buffer).await {
if n > 0 {
let request = String::from_utf8_lossy(&buffer[..n]);
let json = serde_json::to_string(&status).unwrap_or_else(|_| {
r#"{"status":"error","message":"Failed to serialize health status"}"#
.to_string()
// Check if this is a GET request to /health
if request.starts_with("GET /health") {
let uptime = start_time.elapsed().as_secs();
let status = HealthStatus {
status: "healthy".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
uptime_seconds: uptime,
};
let json = serde_json::to_string(&status).unwrap_or_else(|_| {
r#"{"status":"error","message":"Failed to serialize health status"}"#
.to_string()
});
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
json.len(),
json
);
let _ = socket.write_all(response.as_bytes()).await;
} else {
// 404 for other paths
let response = "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot found";
let _ = socket.write_all(response.as_bytes()).await;
}
}
}
let _ = socket.shutdown().await;
});
let response = Response::from_string(json)
.with_header(
tiny_http::Header::from_bytes(
&b"Content-Type"[..],
&b"application/json"[..],
)
.unwrap(),
)
.with_status_code(200);
let _ = request.respond(response);
} else {
let _ = request.respond(Response::from_string("Not found").with_status_code(404));
}
Err(e) => {
log::warn!("Failed to accept health check connection: {}", e);
}
}
Ok::<(), anyhow::Error>(())
})
.await??;
Ok(())
}
}
#[cfg(test)]

View file

@ -1,9 +1,10 @@
use anyhow::{anyhow, Result};
use std::net::{IpAddr, Ipv4Addr};
use std::time::Duration;
use tokio::net::UdpSocket;
use tokio::time::timeout;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
/// ICMP message types
const ICMP_ECHO: u8 = 8;
const ICMP_ECHOREPLY: u8 = 0;
@ -33,7 +34,7 @@ pub async fn ping(ip: IpAddr, timeout_duration: Duration) -> Result<Duration> {
match ip {
IpAddr::V4(ipv4) => ping_ipv4(ipv4, timeout_duration).await?,
IpAddr::V6(_) => {
return Err(anyhow!("IPv6 ping not yet implemented"));
return Err("IPv6 ping not yet implemented".into());
}
}
@ -41,9 +42,13 @@ pub async fn ping(ip: IpAddr, timeout_duration: Duration) -> Result<Duration> {
}
async fn ping_ipv4(ip: Ipv4Addr, timeout_duration: Duration) -> Result<()> {
// Generate unique identifier and sequence number
let identifier = rand::random::<u16>();
let sequence = rand::random::<u16>();
// Generate unique identifier and sequence number using timestamp
// This provides sufficient randomness for ICMP ping identification
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap();
let identifier = (now.as_nanos() & 0xFFFF) as u16;
let sequence = ((now.as_nanos() >> 16) & 0xFFFF) as u16;
// Build ICMP echo request packet
let packet = build_icmp_echo_request(identifier, sequence);
@ -74,8 +79,10 @@ async fn ping_ipv4(ip: Ipv4Addr, timeout_duration: Duration) -> Result<()> {
let mut buf = [0u8; 1024];
let n = timeout(timeout_duration, socket.recv(&mut buf))
.await
.map_err(|_| anyhow!("Ping timeout"))?
.map_err(|e| anyhow!("Failed to receive ping reply: {}", e))?;
.map_err(|_| -> Box<dyn std::error::Error + Send + Sync> { "Ping timeout".into() })?
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to receive ping reply: {}", e).into()
})?;
// Parse and validate the reply
parse_icmp_reply(&buf[..n], identifier, sequence)?;
@ -163,16 +170,16 @@ fn parse_icmp_reply(packet: &[u8], expected_identifier: u16, expected_sequence:
format!("len={} (too short)", packet.len())
};
Err(anyhow!(
Err(format!(
"Invalid ICMP reply packet (expected seq={}): {}",
expected_sequence,
packet_preview
))
).into())
}
fn try_parse_icmp(packet: &[u8], _expected_identifier: u16, expected_sequence: u16) -> Result<()> {
if packet.len() < 8 {
return Err(anyhow!("Packet too short"));
return Err("Packet too short".into());
}
let icmp_type = packet[0];
@ -186,13 +193,13 @@ fn try_parse_icmp(packet: &[u8], _expected_identifier: u16, expected_sequence: u
if icmp_type == ICMP_ECHOREPLY && icmp_code == 0 && sequence == expected_sequence {
Ok(())
} else {
Err(anyhow!(
Err(format!(
"ICMP packet mismatch (type={}, code={}, id={}, seq={})",
icmp_type,
icmp_code,
identifier,
sequence
))
).into())
}
}

View file

@ -1,133 +1,15 @@
use log::{info, warn};
use serde::Deserialize;
use std::cmp::Ordering;
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
use log::info;
// Get version at runtime - prefers BUILD_VERSION from build.rs, falls back to Cargo.toml
fn current_version() -> &'static str {
option_env!("BUILD_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
}
#[derive(Debug, Deserialize)]
struct DockerHubResponse {
results: Vec<DockerHubTag>,
}
#[derive(Debug, Deserialize)]
struct DockerHubTag {
name: String,
}
#[derive(Debug, PartialEq, Eq)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
impl Version {
fn parse(s: &str) -> Option<Self> {
let s = s.strip_prefix('v').unwrap_or(s);
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return None;
}
Some(Version {
major: parts[0].parse().ok()?,
minor: parts[1].parse().ok()?,
patch: parts[2].parse().ok()?,
})
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
match self.major.cmp(&other.major) {
Ordering::Equal => match self.minor.cmp(&other.minor) {
Ordering::Equal => self.patch.cmp(&other.patch),
other => other,
},
other => other,
}
}
}
/// Startup check - logs current version and checks for updates
/// Startup check - logs current version
pub fn check_for_updates() {
let current_ver = current_version();
info!("Current version: {}", current_ver);
match get_latest_version() {
Ok(latest) => {
let current = Version::parse(current_ver);
let latest_version = Version::parse(&latest);
match (current, latest_version) {
(Some(curr), Some(lat)) => {
if lat > curr {
warn!(
"⚠️ Newer version available: {} (current: {})",
latest, current_ver
);
warn!(" Automatic updates will pull new version every hour");
} else {
info!("✓ Running latest version ({})", current_ver);
}
}
_ => {
info!("Could not compare versions, automatic updates enabled");
}
}
}
Err(e) => {
warn!("Could not check for updates: {}", e);
info!("Automatic updates enabled - will check every hour");
}
}
}
/// Get the latest version from Docker Hub
fn get_latest_version() -> Result<String, Box<dyn std::error::Error>> {
let url = format!(
"https://hub.docker.com/v2/repositories/{}/tags?page_size=100",
DOCKER_IMAGE
);
let response: DockerHubResponse = ureq::get(&url)
.timeout(std::time::Duration::from_secs(10))
.call()?
.into_json()?;
extract_latest_version_from_response(response)
}
/// Extract the latest version from Docker Hub response
fn extract_latest_version_from_response(
response: DockerHubResponse,
) -> Result<String, Box<dyn std::error::Error>> {
// Filter for semver tags and find the latest
let mut versions: Vec<Version> = response
.results
.iter()
.filter_map(|tag| Version::parse(&tag.name))
.collect();
versions.sort();
versions.reverse(); // Highest version first
versions
.first()
.map(|v| format!("{}.{}.{}", v.major, v.minor, v.patch))
.ok_or_else(|| "No valid semver tags found".into())
info!("Watchtower will automatically update to new versions");
}
#[cfg(test)]
@ -142,150 +24,6 @@ mod tests {
// Just verify it's a non-empty string
}
#[test]
fn test_version_parse_valid() {
let v = Version::parse("1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
}
#[test]
fn test_version_parse_with_v_prefix() {
let v = Version::parse("v2.5.8").unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 5);
assert_eq!(v.patch, 8);
}
#[test]
fn test_version_parse_invalid() {
assert!(Version::parse("1.2").is_none());
assert!(Version::parse("1.2.3.4").is_none());
assert!(Version::parse("a.b.c").is_none());
assert!(Version::parse("1.2.x").is_none());
assert!(Version::parse("").is_none());
}
#[test]
fn test_version_comparison_major() {
let v1 = Version::parse("2.0.0").unwrap();
let v2 = Version::parse("1.9.9").unwrap();
assert!(v1 > v2);
assert!(v2 < v1);
}
#[test]
fn test_version_comparison_minor() {
let v1 = Version::parse("1.5.0").unwrap();
let v2 = Version::parse("1.4.9").unwrap();
assert!(v1 > v2);
assert!(v2 < v1);
}
#[test]
fn test_version_comparison_patch() {
let v1 = Version::parse("1.2.4").unwrap();
let v2 = Version::parse("1.2.3").unwrap();
assert!(v1 > v2);
assert!(v2 < v1);
}
#[test]
fn test_version_comparison_equal() {
let v1 = Version::parse("1.2.3").unwrap();
let v2 = Version::parse("1.2.3").unwrap();
assert_eq!(v1, v2);
assert!(!(v1 > v2));
assert!(!(v1 < v2));
}
#[test]
fn test_version_sorting() {
let mut versions = vec![
Version::parse("1.2.3").unwrap(),
Version::parse("2.0.0").unwrap(),
Version::parse("1.5.0").unwrap(),
Version::parse("1.2.10").unwrap(),
];
versions.sort();
assert_eq!(versions[0], Version::parse("1.2.3").unwrap());
assert_eq!(versions[1], Version::parse("1.2.10").unwrap());
assert_eq!(versions[2], Version::parse("1.5.0").unwrap());
assert_eq!(versions[3], Version::parse("2.0.0").unwrap());
}
#[test]
fn test_dockerhub_response_deserialize() {
let json = r#"{"results":[{"name":"v1.0.0"},{"name":"v1.0.1"}]}"#;
let response: DockerHubResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.results.len(), 2);
assert_eq!(response.results[0].name, "v1.0.0");
assert_eq!(response.results[1].name, "v1.0.1");
}
#[test]
fn test_extract_latest_version_from_response() {
let response = DockerHubResponse {
results: vec![
DockerHubTag {
name: "v1.0.0".to_string(),
},
DockerHubTag {
name: "v1.2.0".to_string(),
},
DockerHubTag {
name: "v1.1.5".to_string(),
},
DockerHubTag {
name: "latest".to_string(),
}, // Should be ignored
],
};
let latest = extract_latest_version_from_response(response).unwrap();
assert_eq!(latest, "1.2.0");
}
#[test]
fn test_extract_latest_version_no_valid_tags() {
let response = DockerHubResponse {
results: vec![
DockerHubTag {
name: "latest".to_string(),
},
DockerHubTag {
name: "main".to_string(),
},
],
};
let result = extract_latest_version_from_response(response);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No valid semver"));
}
#[test]
fn test_extract_latest_version_empty() {
let response = DockerHubResponse { results: vec![] };
let result = extract_latest_version_from_response(response);
assert!(result.is_err());
}
#[test]
fn test_version_debug() {
let v = Version {
major: 1,
minor: 2,
patch: 3,
};
let debug_str = format!("{:?}", v);
assert!(debug_str.contains("1"));
assert!(debug_str.contains("2"));
assert!(debug_str.contains("3"));
}
// Note: check_for_updates() and get_latest_version() are tested via integration tests
// as they require network access to Docker Hub
// Note: check_for_updates() just logs the version, no testing needed
}

View file

@ -6,7 +6,6 @@
///
/// Connection URL: {url}/socket/agent/websocket
/// Authentication: Token sent in Phoenix channel join payload
use anyhow::{Context, Result};
use futures::{SinkExt, StreamExt};
use prost::Message;
use std::collections::HashMap;
@ -17,6 +16,8 @@ use tokio_tungstenite::{
connect_async, tungstenite::protocol::Message as WsMessage, MaybeTlsStream, WebSocketStream,
};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
use crate::ping::ping;
use crate::proto::agent::{
AgentHeartbeat, AgentJob, AgentJobList, JobType, MonitoringCheck, QueryType, SnmpResult,
@ -66,7 +67,7 @@ impl AgentClient {
log::error!("WebSocket connection failed: {}", e);
e
})
.context("Failed to connect to WebSocket")?;
.map_err(|e| format!("Failed to connect to WebSocket: {}", e))?;
log::info!("Connected to Towerops server at {}", url);
@ -323,7 +324,9 @@ impl AgentClient {
/// Execute an SNMP job and collect results.
async fn execute_job(job: AgentJob, result_tx: mpsc::UnboundedSender<SnmpResult>) -> Result<()> {
let snmp_device = job.snmp_device.context("Job missing SNMP device info")?;
let snmp_device = job
.snmp_device
.ok_or("Job missing SNMP device info")?;
let mut oid_values: HashMap<String, String> = HashMap::new();
let snmp_client = SnmpClient::new();
@ -468,7 +471,7 @@ fn base64_decode(encoded: &str) -> Result<Vec<u8>> {
} else {
let val = decode_map[byte as usize];
if val == 0xFF {
anyhow::bail!("Invalid base64 character");
return Err("Invalid base64 character".into());
}
buf[i] = val;
}