From f383f55c132893d70ae726421f2feee685e885ce Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 13 Jan 2026 13:50:07 -0600 Subject: [PATCH] Complete protobuf integration for all API endpoints - Update fetch_config to use protobuf instead of JSON - Add conversion functions from protobuf types to internal config types - Update heartbeat to use protobuf encoding - All API endpoints now use Protocol Buffers (config, metrics, heartbeat) - Reduces 7 unused struct warnings to 1 (HeartbeatResponse) --- src/api_client.rs | 124 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 6 deletions(-) diff --git a/src/api_client.rs b/src/api_client.rs index 74e0397..9ed965b 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -1,7 +1,10 @@ -use crate::config::{AgentConfig, HeartbeatMetadata}; +use crate::config::{ + AgentConfig, EquipmentConfig, HeartbeatMetadata, InterfaceConfig, SensorConfig, SnmpConfig, +}; use crate::metrics::Metric; use crate::proto::agent; use prost::Message; +use std::io::Read; use std::time::Duration; use thiserror::Error; @@ -35,7 +38,7 @@ impl ApiClient { Ok(Self { base_url, token }) } - /// Fetch configuration from the API + /// Fetch configuration from the API using Protocol Buffers pub async fn fetch_config(&self) -> Result { let url = format!("{}/api/v1/agent/config", self.base_url); let token = self.token.clone(); @@ -43,6 +46,7 @@ impl ApiClient { let config = tokio::task::spawn_blocking(move || { let response = ureq::get(&url) .set("Authorization", &format!("Bearer {}", token)) + .set("Accept", "application/x-protobuf") .timeout(Duration::from_secs(30)) .call() .map_err(|e| ApiError::RequestFailed(e.to_string()))?; @@ -52,10 +56,20 @@ impl ApiClient { return Err(ApiError::StatusError(status)); } - let config: AgentConfig = response - .into_json() + // Read response bytes + let mut bytes = Vec::new(); + response + .into_reader() + .read_to_end(&mut bytes) .map_err(|e| ApiError::RequestFailed(e.to_string()))?; + // Decode protobuf + let proto_config = agent::AgentConfig::decode(&bytes[..]) + .map_err(|e| ApiError::RequestFailed(format!("Protobuf decode error: {}", e)))?; + + // Convert to internal config type + let config = convert_proto_to_config(proto_config); + Ok(config) }) .await @@ -111,16 +125,31 @@ impl ApiClient { Ok(()) } - /// Send heartbeat to the API + /// Send heartbeat to the API using Protocol Buffers pub async fn heartbeat(&self, metadata: HeartbeatMetadata) -> Result<()> { let url = format!("{}/api/v1/agent/heartbeat", self.base_url); let token = self.token.clone(); tokio::task::spawn_blocking(move || { + // Convert to protobuf + let proto_metadata = agent::HeartbeatMetadata { + version: metadata.version, + hostname: metadata.hostname, + uptime_seconds: metadata.uptime_seconds, + }; + + // Encode to bytes + let mut buf = Vec::new(); + proto_metadata + .encode(&mut buf) + .map_err(|e| ApiError::RequestFailed(format!("Protobuf encoding error: {}", e)))?; + + // Send as protobuf let response = ureq::post(&url) .set("Authorization", &format!("Bearer {}", token)) + .set("Content-Type", "application/x-protobuf") .timeout(Duration::from_secs(30)) - .send_json(&metadata) + .send_bytes(&buf) .map_err(|e| ApiError::RequestFailed(e.to_string()))?; let status = response.status(); @@ -137,6 +166,89 @@ impl ApiClient { } } +/// Convert protobuf AgentConfig to internal AgentConfig +fn convert_proto_to_config(proto: agent::AgentConfig) -> AgentConfig { + AgentConfig { + version: proto.version, + poll_interval_seconds: proto.poll_interval_seconds as u64, + equipment: proto + .equipment + .into_iter() + .map(convert_proto_equipment) + .collect(), + } +} + +/// Convert protobuf Equipment to internal EquipmentConfig +fn convert_proto_equipment(proto: agent::Equipment) -> EquipmentConfig { + EquipmentConfig { + id: proto.id, + name: proto.name, + ip_address: proto.ip_address, + snmp: convert_proto_snmp(proto.snmp.unwrap_or_default()), + poll_interval_seconds: proto.poll_interval_seconds as u64, + sensors: proto + .sensors + .into_iter() + .map(convert_proto_sensor) + .collect(), + interfaces: proto + .interfaces + .into_iter() + .map(convert_proto_interface) + .collect(), + } +} + +/// Convert protobuf SnmpConfig to internal SnmpConfig +fn convert_proto_snmp(proto: agent::SnmpConfig) -> SnmpConfig { + SnmpConfig { + enabled: proto.enabled, + version: proto.version, + community: proto.community, + port: proto.port as u16, + } +} + +/// Convert protobuf Sensor to internal SensorConfig +fn convert_proto_sensor(proto: agent::Sensor) -> SensorConfig { + SensorConfig { + id: proto.id, + sensor_type: proto.r#type, + oid: proto.oid, + divisor: if proto.divisor == 0.0 { + None + } else { + Some(proto.divisor as i32) + }, + unit: if proto.unit.is_empty() { + None + } else { + Some(proto.unit) + }, + metadata: if proto.metadata.is_empty() { + None + } else { + // Convert metadata map to JSON value + let json_map: serde_json::Map = proto + .metadata + .into_iter() + .map(|(k, v)| (k, serde_json::Value::String(v))) + .collect(); + Some(serde_json::Value::Object(json_map)) + }, + } +} + +/// Convert protobuf Interface to internal InterfaceConfig +fn convert_proto_interface(proto: agent::Interface) -> InterfaceConfig { + InterfaceConfig { + id: proto.id, + if_index: proto.if_index as i32, + if_name: proto.if_name, + } +} + /// Convert internal Metric type to protobuf Metric fn convert_metric_to_proto(metric: &Metric) -> agent::Metric { use crate::metrics::Metric as M;