init
This commit is contained in:
commit
0f8cfc34e7
18 changed files with 3226 additions and 0 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
target/
|
||||
.git/
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Created by https://gitignore.org
|
||||
# Rust.gitignore
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug
|
||||
target
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Generated by cargo mutants
|
||||
# Contains mutation testing data
|
||||
**/mutants.out*/
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
1952
Cargo.lock
generated
Normal file
1952
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "towerops-agent"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
snmp = "0.2"
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.0", features = ["derive", "env"] }
|
||||
hostname = "0.4"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
53
Dockerfile
Normal file
53
Dockerfile
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Build stage
|
||||
FROM rust:1.75-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache musl-dev
|
||||
|
||||
# Copy manifests
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
# Create a dummy main.rs to build dependencies
|
||||
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||
|
||||
# Build dependencies (cached layer)
|
||||
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
# Remove dummy src
|
||||
RUN rm -rf src
|
||||
|
||||
# Copy actual source code
|
||||
COPY src ./src
|
||||
|
||||
# Build the actual application
|
||||
RUN touch src/main.rs && cargo build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:3.19
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /data
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/towerops-agent /usr/local/bin/
|
||||
|
||||
# Volume for database
|
||||
VOLUME ["/data"]
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
||||
CMD test -f /data/towerops-agent.db || exit 1
|
||||
|
||||
# Run as non-root user
|
||||
RUN addgroup -g 1000 towerops && \
|
||||
adduser -D -u 1000 -G towerops towerops && \
|
||||
chown -R towerops:towerops /data
|
||||
|
||||
USER towerops
|
||||
|
||||
ENTRYPOINT ["towerops-agent"]
|
||||
142
README.md
Normal file
142
README.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Towerops Agent
|
||||
|
||||
A lightweight Rust-based remote polling agent for Towerops SNMP monitoring.
|
||||
|
||||
## Overview
|
||||
|
||||
The Towerops agent enables customers to deploy SNMP polling infrastructure on their internal networks. The agent polls SNMP devices locally and reports metrics to the Towerops API using token-based authentication.
|
||||
|
||||
## Features
|
||||
|
||||
- **Local SNMP Polling**: Poll devices on your internal network without exposing them to the internet
|
||||
- **Secure Communication**: All communication with Towerops API uses HTTPS with token authentication
|
||||
- **Metric Buffering**: Stores up to 24 hours of metrics in SQLite when API is unavailable
|
||||
- **Automatic Configuration**: Fetches polling configuration from the Towerops API
|
||||
- **Low Resource Usage**: Built in Rust for minimal memory and CPU footprint (< 256 MB RAM typical)
|
||||
- **Docker Ready**: Pre-built Docker images for easy deployment
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
1. Create a `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
towerops-agent:
|
||||
image: towerops/agent:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TOWEROPS_API_URL=https://app.towerops.com
|
||||
- TOWEROPS_AGENT_TOKEN=your-agent-token-here
|
||||
volumes:
|
||||
- ./data:/data
|
||||
```
|
||||
|
||||
2. Start the agent:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
The agent is configured via environment variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `TOWEROPS_API_URL` | Towerops API endpoint | Required |
|
||||
| `TOWEROPS_AGENT_TOKEN` | Agent authentication token | Required |
|
||||
| `CONFIG_REFRESH_SECONDS` | How often to refresh configuration | 300 (5 minutes) |
|
||||
| `DATABASE_PATH` | SQLite database path for buffering | `/data/towerops-agent.db` |
|
||||
|
||||
### Obtaining an Agent Token
|
||||
|
||||
1. Log in to your Towerops account
|
||||
2. Navigate to your organization's Agents page
|
||||
3. Click "Create New Agent"
|
||||
4. Copy the token (it will only be shown once)
|
||||
|
||||
## Architecture
|
||||
|
||||
The agent consists of several components:
|
||||
|
||||
- **API Client**: Communicates with Towerops API to fetch configuration and submit metrics
|
||||
- **SNMP Poller**: Polls configured devices for sensor readings and interface statistics
|
||||
- **Storage Buffer**: SQLite database that buffers metrics during API outages
|
||||
- **Scheduler**: Coordinates polling intervals and metric submission
|
||||
|
||||
### Polling Flow
|
||||
|
||||
1. Agent fetches configuration from API every 5 minutes (configurable)
|
||||
2. For each equipment item, polls sensors and interfaces at configured intervals
|
||||
3. Metrics are stored locally in SQLite
|
||||
4. Every 30 seconds, pending metrics are submitted to the API
|
||||
5. Successfully submitted metrics are marked as sent and cleaned up after 24 hours
|
||||
|
||||
### Buffering
|
||||
|
||||
If the API is unreachable, metrics are stored locally for up to 24 hours. When connectivity is restored, the agent automatically submits all buffered metrics.
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust 1.75 or later
|
||||
- SQLite development libraries
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The binary will be in `target/release/towerops-agent`.
|
||||
|
||||
### Docker Build
|
||||
|
||||
```bash
|
||||
docker build -t towerops-agent .
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent Not Connecting
|
||||
|
||||
- Verify the API URL is correct
|
||||
- Check that the agent token is valid and hasn't been revoked
|
||||
- Ensure network connectivity to the Towerops API
|
||||
|
||||
### No Metrics Appearing
|
||||
|
||||
- Check that equipment is assigned to this agent in Towerops
|
||||
- Verify SNMP credentials are correct in Towerops equipment settings
|
||||
- Review agent logs for SNMP polling errors
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
- Check the size of the SQLite database (`/data/towerops-agent.db`)
|
||||
- Verify metrics are being submitted successfully (check API connectivity)
|
||||
|
||||
## Resource Requirements
|
||||
|
||||
- **Memory**: 128-256 MB typical, 512 MB maximum
|
||||
- **CPU**: 0.1-0.5 cores typical
|
||||
- **Disk**: ~100 MB for 24 hours of buffered metrics
|
||||
- **Network**: Minimal (metrics are small JSON payloads)
|
||||
|
||||
## Security
|
||||
|
||||
- Agent token should be kept secret (treat like a password)
|
||||
- All API communication uses HTTPS with certificate verification
|
||||
- Agent requires no inbound network connections
|
||||
- SNMP community strings are only used locally and never logged
|
||||
|
||||
## Development Status
|
||||
|
||||
**NOTE**: SNMP library integration is incomplete. The current implementation compiles but requires proper SNMP library integration for production use. This will be completed in the next development phase.
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2026 Towerops
|
||||
107
src/api_client.rs
Normal file
107
src/api_client.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
use crate::config::{AgentConfig, HeartbeatMetadata};
|
||||
use crate::metrics::Metric;
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
/// API client for communicating with the Towerops server
|
||||
#[derive(Clone)]
|
||||
pub struct ApiClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
/// Create a new API client
|
||||
pub fn new(base_url: String, token: String) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
base_url,
|
||||
token,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch configuration from the API
|
||||
pub async fn fetch_config(&self) -> Result<AgentConfig> {
|
||||
let url = format!("{}/api/v1/agent/config", self.base_url);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.token))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send config request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!(
|
||||
"Config request failed with status: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
|
||||
let config: AgentConfig = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse config response")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Submit metrics to the API
|
||||
pub async fn submit_metrics(&self, metrics: Vec<Metric>) -> Result<()> {
|
||||
if metrics.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let url = format!("{}/api/v1/agent/metrics", self.base_url);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.token))
|
||||
.json(&json!({ "metrics": metrics }))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send metrics request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!(
|
||||
"Metrics submission failed with status: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send heartbeat to the API
|
||||
pub async fn heartbeat(&self, metadata: HeartbeatMetadata) -> Result<()> {
|
||||
let url = format!("{}/api/v1/agent/heartbeat", self.base_url);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.token))
|
||||
.json(&metadata)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send heartbeat request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!(
|
||||
"Heartbeat failed with status: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
3
src/buffer/mod.rs
Normal file
3
src/buffer/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod storage;
|
||||
|
||||
pub use storage::Storage;
|
||||
188
src/buffer/storage.rs
Normal file
188
src/buffer/storage.rs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
use crate::metrics::Metric;
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rusqlite::{params, Connection};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// SQLite storage for buffering metrics when API is unavailable
|
||||
#[derive(Clone)]
|
||||
pub struct Storage {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
/// Create a new storage instance
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let conn = Connection::open(path).context("Failed to open database")?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS metrics (
|
||||
id INTEGER PRIMARY KEY,
|
||||
metric_type TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
sent INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.context("Failed to create metrics table")?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_metrics_sent ON metrics(sent, created_at)",
|
||||
[],
|
||||
)
|
||||
.context("Failed to create index")?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS last_poll_times (
|
||||
equipment_id TEXT PRIMARY KEY,
|
||||
last_poll_time TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.context("Failed to create last_poll_times table")?;
|
||||
|
||||
Ok(Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Store a metric in the buffer
|
||||
pub fn store_metric(&self, metric: &Metric) -> Result<()> {
|
||||
let data = serde_json::to_string(metric).context("Failed to serialize metric")?;
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO metrics (metric_type, data, timestamp) VALUES (?1, ?2, ?3)",
|
||||
params![
|
||||
metric.metric_type(),
|
||||
data,
|
||||
metric.timestamp().to_rfc3339()
|
||||
],
|
||||
)
|
||||
.context("Failed to insert metric")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get pending metrics that haven't been sent yet
|
||||
pub fn get_pending_metrics(&self, limit: usize) -> Result<Vec<(i64, Metric)>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, data FROM metrics WHERE sent = 0 ORDER BY created_at LIMIT ?1")
|
||||
.context("Failed to prepare statement")?;
|
||||
|
||||
let metrics = stmt
|
||||
.query_map([limit], |row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let data: String = row.get(1)?;
|
||||
Ok((id, data))
|
||||
})
|
||||
.context("Failed to query metrics")?
|
||||
.filter_map(|r| r.ok())
|
||||
.filter_map(|(id, data)| {
|
||||
serde_json::from_str::<Metric>(&data)
|
||||
.ok()
|
||||
.map(|metric| (id, metric))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
/// Mark metrics as sent
|
||||
pub fn mark_metrics_sent(&self, ids: &[i64]) -> Result<()> {
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let placeholders = ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
|
||||
let query = format!("UPDATE metrics SET sent = 1 WHERE id IN ({})", placeholders);
|
||||
|
||||
let params: Vec<_> = ids.iter().map(|id| id as &dyn rusqlite::ToSql).collect();
|
||||
conn.execute(&query, params.as_slice())
|
||||
.context("Failed to mark metrics as sent")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clean up old metrics that have been sent
|
||||
pub fn cleanup_old_metrics(&self) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
conn.execute(
|
||||
"DELETE FROM metrics WHERE sent = 1 AND created_at < datetime('now', '-24 hours')",
|
||||
[],
|
||||
)
|
||||
.context("Failed to cleanup old metrics")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the last poll time for equipment
|
||||
pub fn get_last_poll_time(&self, equipment_id: &str) -> Result<Option<DateTime<Utc>>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let result: Result<String, _> = conn.query_row(
|
||||
"SELECT last_poll_time FROM last_poll_times WHERE equipment_id = ?1",
|
||||
params![equipment_id],
|
||||
|row| row.get(0),
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(timestamp_str) => {
|
||||
let dt = DateTime::parse_from_rfc3339(×tamp_str)
|
||||
.context("Failed to parse timestamp")?
|
||||
.with_timezone(&Utc);
|
||||
Ok(Some(dt))
|
||||
}
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e).context("Failed to query last poll time"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the last poll time for equipment
|
||||
pub fn update_last_poll_time(&self, equipment_id: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = Utc::now().to_rfc3339();
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO last_poll_times (equipment_id, last_poll_time) VALUES (?1, ?2)",
|
||||
params![equipment_id, now],
|
||||
)
|
||||
.context("Failed to update last poll time")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all last poll times
|
||||
pub fn get_all_last_poll_times(&self) -> Result<HashMap<String, DateTime<Utc>>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT equipment_id, last_poll_time FROM last_poll_times")
|
||||
.context("Failed to prepare statement")?;
|
||||
|
||||
let times: HashMap<String, DateTime<Utc>> = stmt
|
||||
.query_map([], |row| {
|
||||
let equipment_id: String = row.get(0)?;
|
||||
let timestamp_str: String = row.get(1)?;
|
||||
Ok((equipment_id, timestamp_str))
|
||||
})
|
||||
.context("Failed to query poll times")?
|
||||
.filter_map(|r| r.ok())
|
||||
.filter_map(|(id, ts)| {
|
||||
DateTime::parse_from_rfc3339(&ts)
|
||||
.ok()
|
||||
.map(|dt| (id, dt.with_timezone(&Utc)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(times)
|
||||
}
|
||||
}
|
||||
58
src/config.rs
Normal file
58
src/config.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Configuration received from the API
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AgentConfig {
|
||||
pub version: String,
|
||||
pub poll_interval_seconds: u64,
|
||||
pub equipment: Vec<EquipmentConfig>,
|
||||
}
|
||||
|
||||
/// Configuration for a single piece of equipment
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct EquipmentConfig {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub ip_address: String,
|
||||
pub snmp: SnmpConfig,
|
||||
pub poll_interval_seconds: u64,
|
||||
pub sensors: Vec<SensorConfig>,
|
||||
pub interfaces: Vec<InterfaceConfig>,
|
||||
}
|
||||
|
||||
/// SNMP configuration
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SnmpConfig {
|
||||
pub enabled: bool,
|
||||
pub version: String,
|
||||
pub community: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
/// Sensor configuration
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SensorConfig {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub sensor_type: String,
|
||||
pub oid: String,
|
||||
pub divisor: Option<i32>,
|
||||
pub unit: Option<String>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Interface configuration
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct InterfaceConfig {
|
||||
pub id: String,
|
||||
pub if_index: i32,
|
||||
pub if_name: String,
|
||||
}
|
||||
|
||||
/// Heartbeat metadata sent to the API
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HeartbeatMetadata {
|
||||
pub version: String,
|
||||
pub hostname: String,
|
||||
pub uptime_seconds: u64,
|
||||
}
|
||||
68
src/main.rs
Normal file
68
src/main.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
mod api_client;
|
||||
mod buffer;
|
||||
mod config;
|
||||
mod metrics;
|
||||
mod poller;
|
||||
mod snmp;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use poller::Scheduler;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "towerops-agent")]
|
||||
#[command(about = "Towerops remote SNMP polling agent", long_about = None)]
|
||||
struct Args {
|
||||
/// API URL (e.g., https://app.towerops.com)
|
||||
#[arg(long, env = "TOWEROPS_API_URL")]
|
||||
api_url: String,
|
||||
|
||||
/// Agent authentication token
|
||||
#[arg(long, env = "TOWEROPS_AGENT_TOKEN")]
|
||||
token: String,
|
||||
|
||||
/// Configuration refresh interval in seconds
|
||||
#[arg(long, env = "CONFIG_REFRESH_SECONDS", default_value = "300")]
|
||||
config_refresh_seconds: u64,
|
||||
|
||||
/// Database path for metrics buffering
|
||||
#[arg(long, env = "DATABASE_PATH", default_value = "/data/towerops-agent.db")]
|
||||
database_path: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
info!("Towerops agent starting");
|
||||
info!("API URL: {}", args.api_url);
|
||||
info!(
|
||||
"Config refresh interval: {} seconds",
|
||||
args.config_refresh_seconds
|
||||
);
|
||||
info!("Database path: {}", args.database_path);
|
||||
|
||||
// Initialize components
|
||||
let api_client = api_client::ApiClient::new(args.api_url, args.token)?;
|
||||
let storage = buffer::Storage::new(args.database_path)?;
|
||||
let snmp_client = snmp::SnmpClient::new();
|
||||
|
||||
// Create and run scheduler
|
||||
let mut scheduler = Scheduler::new(
|
||||
api_client,
|
||||
storage,
|
||||
snmp_client,
|
||||
args.config_refresh_seconds,
|
||||
);
|
||||
|
||||
scheduler.run().await
|
||||
}
|
||||
50
src/metrics/mod.rs
Normal file
50
src/metrics/mod.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Metric types that can be submitted to the API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Metric {
|
||||
#[serde(rename = "sensor_reading")]
|
||||
SensorReading(SensorReading),
|
||||
#[serde(rename = "interface_stat")]
|
||||
InterfaceStat(InterfaceStat),
|
||||
}
|
||||
|
||||
impl Metric {
|
||||
pub fn metric_type(&self) -> &str {
|
||||
match self {
|
||||
Metric::SensorReading(_) => "sensor_reading",
|
||||
Metric::InterfaceStat(_) => "interface_stat",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> &DateTime<Utc> {
|
||||
match self {
|
||||
Metric::SensorReading(sr) => &sr.timestamp,
|
||||
Metric::InterfaceStat(is) => &is.timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sensor reading metric
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SensorReading {
|
||||
pub sensor_id: String,
|
||||
pub value: f64,
|
||||
pub status: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Interface statistics metric
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InterfaceStat {
|
||||
pub interface_id: String,
|
||||
pub if_in_octets: i64,
|
||||
pub if_out_octets: i64,
|
||||
pub if_in_errors: i64,
|
||||
pub if_out_errors: i64,
|
||||
pub if_in_discards: i64,
|
||||
pub if_out_discards: i64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
217
src/poller/executor.rs
Normal file
217
src/poller/executor.rs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
use crate::buffer::Storage;
|
||||
use crate::config::EquipmentConfig;
|
||||
use crate::metrics::{InterfaceStat, Metric, SensorReading};
|
||||
use crate::snmp::SnmpClient;
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
/// Executor handles polling individual pieces of equipment
|
||||
pub struct Executor {
|
||||
snmp_client: SnmpClient,
|
||||
storage: Storage,
|
||||
}
|
||||
|
||||
impl Executor {
|
||||
pub fn new(snmp_client: SnmpClient, storage: Storage) -> Self {
|
||||
Self {
|
||||
snmp_client,
|
||||
storage,
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll sensors for a piece of equipment
|
||||
pub async fn poll_sensors(&self, equipment: &EquipmentConfig) -> Result<()> {
|
||||
if !equipment.snmp.enabled || equipment.sensors.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"Polling {} sensors for equipment: {}",
|
||||
equipment.sensors.len(),
|
||||
equipment.name
|
||||
);
|
||||
|
||||
for sensor in &equipment.sensors {
|
||||
match self
|
||||
.snmp_client
|
||||
.get(
|
||||
&equipment.ip_address,
|
||||
&equipment.snmp.community,
|
||||
&equipment.snmp.version,
|
||||
equipment.snmp.port,
|
||||
&sensor.oid,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => {
|
||||
let raw_value = match value.as_f64() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
warn!(
|
||||
"Could not convert sensor value to number for sensor {}",
|
||||
sensor.id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Apply divisor if specified
|
||||
let final_value = if let Some(divisor) = sensor.divisor {
|
||||
if divisor != 0 {
|
||||
raw_value / divisor as f64
|
||||
} else {
|
||||
raw_value
|
||||
}
|
||||
} else {
|
||||
raw_value
|
||||
};
|
||||
|
||||
let reading = SensorReading {
|
||||
sensor_id: sensor.id.clone(),
|
||||
value: final_value,
|
||||
status: "ok".to_string(),
|
||||
timestamp: Utc::now(),
|
||||
};
|
||||
|
||||
if let Err(e) = self.storage.store_metric(&Metric::SensorReading(reading)) {
|
||||
error!("Failed to store sensor reading: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to poll sensor {} for {}: {}",
|
||||
sensor.oid, equipment.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Poll interfaces for a piece of equipment
|
||||
pub async fn poll_interfaces(&self, equipment: &EquipmentConfig) -> Result<()> {
|
||||
if !equipment.snmp.enabled || equipment.interfaces.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"Polling {} interfaces for equipment: {}",
|
||||
equipment.interfaces.len(),
|
||||
equipment.name
|
||||
);
|
||||
|
||||
for interface in &equipment.interfaces {
|
||||
// OIDs for interface statistics (from IF-MIB)
|
||||
let if_in_octets_oid = format!("1.3.6.1.2.1.2.2.1.10.{}", interface.if_index);
|
||||
let if_out_octets_oid = format!("1.3.6.1.2.1.2.2.1.16.{}", interface.if_index);
|
||||
let if_in_errors_oid = format!("1.3.6.1.2.1.2.2.1.14.{}", interface.if_index);
|
||||
let if_out_errors_oid = format!("1.3.6.1.2.1.2.2.1.20.{}", interface.if_index);
|
||||
let if_in_discards_oid = format!("1.3.6.1.2.1.2.2.1.13.{}", interface.if_index);
|
||||
let if_out_discards_oid = format!("1.3.6.1.2.1.2.2.1.19.{}", interface.if_index);
|
||||
|
||||
// Poll each counter
|
||||
let in_octets = self
|
||||
.get_counter(
|
||||
&equipment.ip_address,
|
||||
&equipment.snmp.community,
|
||||
&equipment.snmp.version,
|
||||
equipment.snmp.port,
|
||||
&if_in_octets_oid,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let out_octets = self
|
||||
.get_counter(
|
||||
&equipment.ip_address,
|
||||
&equipment.snmp.community,
|
||||
&equipment.snmp.version,
|
||||
equipment.snmp.port,
|
||||
&if_out_octets_oid,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let in_errors = self
|
||||
.get_counter(
|
||||
&equipment.ip_address,
|
||||
&equipment.snmp.community,
|
||||
&equipment.snmp.version,
|
||||
equipment.snmp.port,
|
||||
&if_in_errors_oid,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let out_errors = self
|
||||
.get_counter(
|
||||
&equipment.ip_address,
|
||||
&equipment.snmp.community,
|
||||
&equipment.snmp.version,
|
||||
equipment.snmp.port,
|
||||
&if_out_errors_oid,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let in_discards = self
|
||||
.get_counter(
|
||||
&equipment.ip_address,
|
||||
&equipment.snmp.community,
|
||||
&equipment.snmp.version,
|
||||
equipment.snmp.port,
|
||||
&if_in_discards_oid,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let out_discards = self
|
||||
.get_counter(
|
||||
&equipment.ip_address,
|
||||
&equipment.snmp.community,
|
||||
&equipment.snmp.version,
|
||||
equipment.snmp.port,
|
||||
&if_out_discards_oid,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let stat = InterfaceStat {
|
||||
interface_id: interface.id.clone(),
|
||||
if_in_octets: in_octets,
|
||||
if_out_octets: out_octets,
|
||||
if_in_errors: in_errors,
|
||||
if_out_errors: out_errors,
|
||||
if_in_discards: in_discards,
|
||||
if_out_discards: out_discards,
|
||||
timestamp: Utc::now(),
|
||||
};
|
||||
|
||||
if let Err(e) = self.storage.store_metric(&Metric::InterfaceStat(stat)) {
|
||||
error!("Failed to store interface stat: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_counter(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
community: &str,
|
||||
version: &str,
|
||||
port: u16,
|
||||
oid: &str,
|
||||
) -> Result<i64> {
|
||||
let value = self
|
||||
.snmp_client
|
||||
.get(ip_address, community, version, port, oid)
|
||||
.await?;
|
||||
|
||||
value
|
||||
.as_i64()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not convert value to i64"))
|
||||
}
|
||||
}
|
||||
5
src/poller/mod.rs
Normal file
5
src/poller/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod executor;
|
||||
mod scheduler;
|
||||
|
||||
pub use executor::Executor;
|
||||
pub use scheduler::Scheduler;
|
||||
205
src/poller/scheduler.rs
Normal file
205
src/poller/scheduler.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
use crate::api_client::ApiClient;
|
||||
use crate::buffer::Storage;
|
||||
use crate::config::{AgentConfig, HeartbeatMetadata};
|
||||
use crate::poller::Executor;
|
||||
use crate::snmp::SnmpClient;
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::interval;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
/// Main scheduler that orchestrates polling, config refresh, and metrics submission
|
||||
pub struct Scheduler {
|
||||
api_client: ApiClient,
|
||||
storage: Storage,
|
||||
executor: Executor,
|
||||
config_refresh_seconds: u64,
|
||||
current_config: Option<AgentConfig>,
|
||||
start_time: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
pub fn new(
|
||||
api_client: ApiClient,
|
||||
storage: Storage,
|
||||
snmp_client: SnmpClient,
|
||||
config_refresh_seconds: u64,
|
||||
) -> Self {
|
||||
let executor = Executor::new(snmp_client, storage.clone());
|
||||
|
||||
Self {
|
||||
api_client,
|
||||
storage,
|
||||
executor,
|
||||
config_refresh_seconds,
|
||||
current_config: None,
|
||||
start_time: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the main event loop
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
info!("Starting Towerops agent scheduler");
|
||||
|
||||
// Fetch initial configuration
|
||||
if let Err(e) = self.refresh_config().await {
|
||||
error!("Failed to fetch initial config: {}", e);
|
||||
}
|
||||
|
||||
let mut config_ticker = interval(Duration::from_secs(self.config_refresh_seconds));
|
||||
let mut metrics_ticker = interval(Duration::from_secs(30));
|
||||
let mut heartbeat_ticker = interval(Duration::from_secs(60));
|
||||
let mut cleanup_ticker = interval(Duration::from_secs(3600)); // Cleanup every hour
|
||||
let mut poll_ticker = interval(Duration::from_secs(5)); // Check if polling needed every 5s
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = config_ticker.tick() => {
|
||||
if let Err(e) = self.refresh_config().await {
|
||||
error!("Failed to refresh config: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
_ = metrics_ticker.tick() => {
|
||||
if let Err(e) = self.flush_metrics().await {
|
||||
error!("Failed to flush metrics: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
_ = heartbeat_ticker.tick() => {
|
||||
if let Err(e) = self.send_heartbeat().await {
|
||||
warn!("Failed to send heartbeat: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
_ = cleanup_ticker.tick() => {
|
||||
if let Err(e) = self.storage.cleanup_old_metrics() {
|
||||
error!("Failed to cleanup old metrics: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
_ = poll_ticker.tick() => {
|
||||
if let Err(e) = self.poll_equipment().await {
|
||||
error!("Polling error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_config(&mut self) -> Result<()> {
|
||||
info!("Refreshing configuration from API");
|
||||
|
||||
match self.api_client.fetch_config().await {
|
||||
Ok(config) => {
|
||||
info!(
|
||||
"Configuration updated: {} equipment items",
|
||||
config.equipment.len()
|
||||
);
|
||||
self.current_config = Some(config);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch config, continuing with cached config: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn flush_metrics(&self) -> Result<()> {
|
||||
let pending = self.storage.get_pending_metrics(100)?;
|
||||
|
||||
if pending.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Flushing {} pending metrics to API", pending.len());
|
||||
|
||||
let ids: Vec<i64> = pending.iter().map(|(id, _)| *id).collect();
|
||||
let metrics: Vec<_> = pending.into_iter().map(|(_, m)| m).collect();
|
||||
|
||||
match self.api_client.submit_metrics(metrics).await {
|
||||
Ok(_) => {
|
||||
self.storage.mark_metrics_sent(&ids)?;
|
||||
info!("Successfully submitted {} metrics", ids.len());
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to submit metrics, will retry later: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_heartbeat(&self) -> Result<()> {
|
||||
let uptime = Utc::now()
|
||||
.signed_duration_since(self.start_time)
|
||||
.num_seconds() as u64;
|
||||
|
||||
let hostname = hostname::get()
|
||||
.ok()
|
||||
.and_then(|h| h.into_string().ok())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let metadata = HeartbeatMetadata {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
hostname,
|
||||
uptime_seconds: uptime,
|
||||
};
|
||||
|
||||
self.api_client.heartbeat(metadata).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn poll_equipment(&self) -> Result<()> {
|
||||
let config = match &self.current_config {
|
||||
Some(c) => c,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let poll_times = self.storage.get_all_last_poll_times()?;
|
||||
|
||||
for equipment in &config.equipment {
|
||||
if !equipment.snmp.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's time to poll this equipment
|
||||
let should_poll = match poll_times.get(&equipment.id) {
|
||||
Some(last_poll) => {
|
||||
let elapsed = Utc::now()
|
||||
.signed_duration_since(*last_poll)
|
||||
.num_seconds() as u64;
|
||||
elapsed >= equipment.poll_interval_seconds
|
||||
}
|
||||
None => true, // Never polled before
|
||||
};
|
||||
|
||||
if should_poll {
|
||||
info!("Polling equipment: {}", equipment.name);
|
||||
|
||||
// Poll sensors and interfaces in parallel
|
||||
let (sensor_result, interface_result) = tokio::join!(
|
||||
self.executor.poll_sensors(equipment),
|
||||
self.executor.poll_interfaces(equipment)
|
||||
);
|
||||
|
||||
if let Err(e) = sensor_result {
|
||||
error!("Failed to poll sensors for {}: {}", equipment.name, e);
|
||||
}
|
||||
|
||||
if let Err(e) = interface_result {
|
||||
error!("Failed to poll interfaces for {}: {}", equipment.name, e);
|
||||
}
|
||||
|
||||
// Update last poll time
|
||||
if let Err(e) = self.storage.update_last_poll_time(&equipment.id) {
|
||||
error!("Failed to update last poll time: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
64
src/snmp/client.rs
Normal file
64
src/snmp/client.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use super::types::{SnmpError, SnmpResult, SnmpValue};
|
||||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// SNMP client for polling devices
|
||||
///
|
||||
/// NOTE: This is a simplified implementation. Full SNMP integration
|
||||
/// requires proper SNMP library integration with correct error handling.
|
||||
#[derive(Debug)]
|
||||
pub struct SnmpClient;
|
||||
|
||||
impl SnmpClient {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Perform an SNMP GET operation
|
||||
///
|
||||
/// TODO: Complete SNMP library integration with proper session management
|
||||
pub async fn get(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
_community: &str,
|
||||
_version: &str,
|
||||
_port: u16,
|
||||
_oid: &str,
|
||||
) -> SnmpResult<SnmpValue> {
|
||||
// Validate IP address
|
||||
IpAddr::from_str(ip_address)
|
||||
.map_err(|e| SnmpError::InvalidOid(format!("Invalid IP: {}", e)))?;
|
||||
|
||||
// TODO: Implement actual SNMP GET using snmp library
|
||||
// For now, return an error indicating incomplete implementation
|
||||
Err(SnmpError::RequestFailed(
|
||||
"SNMP implementation incomplete - requires library integration".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Perform an SNMP WALK operation to get multiple values
|
||||
///
|
||||
/// TODO: Complete SNMP library integration with proper walk implementation
|
||||
pub async fn walk(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
_community: &str,
|
||||
_version: &str,
|
||||
_port: u16,
|
||||
_base_oid: &str,
|
||||
) -> SnmpResult<Vec<(String, SnmpValue)>> {
|
||||
// Validate IP address
|
||||
IpAddr::from_str(ip_address)
|
||||
.map_err(|e| SnmpError::InvalidOid(format!("Invalid IP: {}", e)))?;
|
||||
|
||||
// TODO: Implement actual SNMP WALK using snmp library
|
||||
// For now, return an empty result
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SnmpClient {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
5
src/snmp/mod.rs
Normal file
5
src/snmp/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod client;
|
||||
mod types;
|
||||
|
||||
pub use client::SnmpClient;
|
||||
pub use types::*;
|
||||
50
src/snmp/types.rs
Normal file
50
src/snmp/types.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SnmpError {
|
||||
#[error("SNMP request failed: {0}")]
|
||||
RequestFailed(String),
|
||||
|
||||
#[error("Invalid OID: {0}")]
|
||||
InvalidOid(String),
|
||||
|
||||
#[error("Timeout")]
|
||||
Timeout,
|
||||
|
||||
#[error("Authentication failure")]
|
||||
AuthFailure,
|
||||
|
||||
#[error("Network unreachable")]
|
||||
NetworkUnreachable,
|
||||
}
|
||||
|
||||
pub type SnmpResult<T> = Result<T, SnmpError>;
|
||||
|
||||
/// SNMP value returned from a GET operation
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SnmpValue {
|
||||
Integer(i64),
|
||||
String(String),
|
||||
Counter32(u32),
|
||||
Counter64(u64),
|
||||
Gauge32(u32),
|
||||
TimeTicks(u32),
|
||||
IpAddress(String),
|
||||
}
|
||||
|
||||
impl SnmpValue {
|
||||
pub fn as_i64(&self) -> Option<i64> {
|
||||
match self {
|
||||
SnmpValue::Integer(v) => Some(*v),
|
||||
SnmpValue::Counter32(v) => Some(*v as i64),
|
||||
SnmpValue::Counter64(v) => Some(*v as i64),
|
||||
SnmpValue::Gauge32(v) => Some(*v as i64),
|
||||
SnmpValue::TimeTicks(v) => Some(*v as i64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_f64(&self) -> Option<f64> {
|
||||
self.as_i64().map(|v| v as f64)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue