rebuild docker image

This commit is contained in:
Graham McIntire 2026-01-13 13:43:59 -06:00
parent 953d044c9b
commit 1f9bdd57cf
No known key found for this signature in database
10 changed files with 780 additions and 11 deletions

View file

@ -1,5 +1,5 @@
# GitLab CI/CD Configuration for Towerops Agent
# Builds and publishes Docker image to GitLab Container Registry
# Builds and publishes Docker image to Docker Hub
stages:
- test
@ -7,9 +7,9 @@ stages:
- release
variables:
# GitLab Container Registry
REGISTRY: registry.gitlab.com
IMAGE_NAME: towerops/towerops-agent
# Docker Hub Registry
REGISTRY: docker.io
IMAGE_NAME: gmcintire/towerops-agent
# Docker BuildKit for better caching
DOCKER_BUILDKIT: 1
DOCKER_TLS_CERTDIR: "/certs"
@ -17,6 +17,8 @@ variables:
# Test stage - compile and check code
test:
stage: test
tags:
- vntx
image: rust:1.83-alpine
before_script:
- apk add --no-cache musl-dev
@ -37,11 +39,13 @@ test:
# Build Docker image for branches (test build)
build:test:
stage: build
tags:
- vntx
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
script:
- |
docker build \
@ -59,11 +63,13 @@ build:test:
# Build and push for main branch
build:main:
stage: build
tags:
- vntx
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
script:
- |
docker build \
@ -82,11 +88,13 @@ build:main:
# Release build for tags (versioned releases)
release:
stage: release
tags:
- vntx
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
script:
- |
# Extract version from tag (assumes tags like v0.1.0)
@ -114,11 +122,13 @@ release:
build:multiarch:
stage: build
tags:
- vntx
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
# Enable buildx for multi-arch
- docker buildx create --use --name multiarch-builder
- docker buildx inspect --bootstrap

390
CLAUDE.md Normal file
View file

@ -0,0 +1,390 @@
# Towerops Agent - Development Notes
This file provides context for Claude Code when working on the Rust agent.
## Project Overview
Lightweight Rust agent for remote SNMP polling. Deployed on customer networks to poll local SNMP devices and report metrics to Towerops API via HTTPS.
## What's Complete ✅
### Architecture & Design
- [x] Complete module structure (12 source files)
- [x] Configuration types matching Phoenix API responses
- [x] Metric types (SensorReading, InterfaceStat) with proper serialization
- [x] Event loop with 5 concurrent tasks (tokio::select!)
- [x] SQLite buffering with 24-hour retention
- [x] Error types and result handling throughout
### Core Functionality
- [x] **API Client** (`api_client.rs`)
- fetch_config() - GET /api/v1/agent/config
- submit_metrics() - POST /api/v1/agent/metrics
- heartbeat() - POST /api/v1/agent/heartbeat
- Uses reqwest with rustls-tls (30s timeout)
- [x] **Storage** (`buffer/storage.rs`)
- store_metric() - Save metrics to SQLite
- get_pending_metrics() - Retrieve unsent metrics
- mark_metrics_sent() - Track submission
- cleanup_old_metrics() - Remove old data
- Last poll time tracking per equipment
- [x] **Scheduler** (`poller/scheduler.rs`)
- Config refresh every 5 minutes
- Metrics flush every 30 seconds
- Heartbeat every 60 seconds
- Cleanup every hour
- Poll check every 5 seconds
- [x] **Executor** (`poller/executor.rs`)
- poll_sensors() - Poll configured sensors
- poll_interfaces() - Poll interface statistics
- Parallel polling with tokio::join!
- Applies sensor divisors
- [x] **Main** (`main.rs`)
- CLI with clap (--api-url, --token, etc.)
- Environment variable support
- Logging with tracing
- Graceful startup/shutdown
### Build & Deployment
- [x] Cargo.toml with optimized release profile
- opt-level = "z"
- lto = true
- codegen-units = 1
- strip = true
- [x] Multi-stage Dockerfile (Alpine, ~10-20 MB)
- [x] docker-compose.example.yml
- [x] README with user documentation
- [x] .gitignore and .dockerignore
### Build Status
```bash
✅ cargo build --release - SUCCESS
⚠️ 5 warnings (unused code - expected)
📦 Target size optimized for minimal footprint
```
## What's Incomplete 🔄
### CRITICAL: SNMP Library Integration
**Location**: `src/snmp/client.rs`
**Problem**: The `snmp` crate v0.2 API is different than initially designed. Current implementation:
- Validates IP addresses
- Returns error for actual SNMP operations
- Compiles successfully but doesn't poll
**Current Code**:
```rust
pub async fn get(...) -> SnmpResult<SnmpValue> {
// Validates IP
// Returns: "SNMP implementation incomplete"
}
pub async fn walk(...) -> SnmpResult<Vec<(String, SnmpValue)>> {
// Validates IP
// Returns: empty vec
}
```
**What's Needed**:
1. Research the correct `snmp` crate 0.2 API:
```bash
cargo doc --open --package snmp
```
2. Implement actual SNMP operations:
- Create session with proper parameters
- Perform GET request for single OID
- Perform GETNEXT/WALK for OID subtrees
- Handle response parsing
- Map errors (timeout, auth failure, etc.)
3. Key questions to answer:
- How to create SyncSession? (signature changed)
- How to send GET request? (method name/signature)
- How to send GETNEXT? (for walk implementation)
- How to parse responses? (varbinds format)
- How to handle errors? (error enum changed)
4. Reference implementation:
- Check `snmp` crate examples directory
- Look at crate tests for usage patterns
- May need to read source code directly
**Alternatives if `snmp` v0.2 doesn't work**:
- `snmp-mp` crate (more actively maintained)
- `snmp-parser` crate (lower level, more control)
- `rasn-snmp` crate (ASN.1 based)
**Estimated Effort**: 4-8 hours
### Testing Gaps
- [ ] Unit tests for SNMP client (depends on SNMP integration)
- [ ] Unit tests for storage (SQLite operations)
- [ ] Unit tests for API client (mock server)
- [ ] Integration test with real SNMP device
## Development Workflow
### Quick Start
1. **Build the agent**:
```bash
cargo build --release
```
2. **Run locally** (needs Phoenix backend running):
```bash
cargo run -- \
--api-url http://localhost:4000 \
--token <get-from-ui> \
--database-path ./test.db
```
3. **Watch logs**:
```bash
RUST_LOG=debug cargo run -- ...
```
### Testing Changes
1. **Check compilation**:
```bash
cargo check
```
2. **Run tests**:
```bash
cargo test
```
3. **Format code**:
```bash
cargo fmt
```
4. **Check for issues**:
```bash
cargo clippy
```
### Docker Testing
1. **Build image**:
```bash
docker build -t towerops-agent:test .
```
2. **Run container**:
```bash
docker run --rm \
-e TOWEROPS_API_URL=http://host.docker.internal:4000 \
-e TOWEROPS_AGENT_TOKEN=<token> \
-e RUST_LOG=info \
-v $(pwd)/data:/data \
towerops-agent:test
```
### CI/CD Pipeline
**Automated builds** via GitLab CI (`.gitlab-ci.yml`):
- Push to branch → test + build with branch tag
- Push to main → test + build + tag as `latest`
- Create tag (e.g., `v0.1.0`) → test + build + release
**Registry**: `registry.gitlab.com/towerops/towerops-agent`
**See**: `DEPLOYMENT.md` for complete CI/CD documentation
## Integration with Phoenix Backend
### API Endpoints (from agent perspective)
**GET /api/v1/agent/config**
- Headers: `Authorization: Bearer <token>`
- Response: Equipment list with sensors and interfaces
- Called every 5 minutes
**POST /api/v1/agent/metrics**
- Headers: `Authorization: Bearer <token>`
- Body: `{"metrics": [...]}`
- Response: `{"status": "accepted", "received": N}`
- Called every 30 seconds with pending metrics
**POST /api/v1/agent/heartbeat**
- Headers: `Authorization: Bearer <token>`
- Body: `{"version": "0.1.0", "hostname": "...", "uptime_seconds": 3600}`
- Response: `{"status": "ok"}`
- Called every 60 seconds
### Getting a Test Token
1. Start Phoenix: `mix phx.server`
2. Navigate to: `http://localhost:4000/orgs/:slug/agents`
3. Click "Create New Agent"
4. Copy the token (shown only once)
5. Use in agent: `--token <copied-token>`
## Architecture Decisions
### Why Tokio?
- Async event loop for efficient I/O
- Multiple concurrent timers (config, metrics, heartbeat)
- Non-blocking SNMP operations via spawn_blocking
### Why SQLite?
- Embedded, no external dependencies
- Persist metrics during API outages
- Small footprint (~100 MB for 24h of metrics)
- No configuration needed
### Why Rust?
- Small binary size (~10-20 MB with Alpine)
- Low memory usage (<256 MB typical)
- Cross-compile to multiple architectures
- Strong type safety for reliability
### Why Simplified SNMP?
- The `snmp` crate v0.2 API required research
- Chose to ship architecture first, SNMP second
- Allows testing of everything except SNMP polling
- Can be completed independently
## Common Issues
### "SNMP implementation incomplete" Error
**Expected**: This is the TODO for SNMP integration. The agent runs but can't poll devices yet.
### "Failed to fetch config" Error
**Check**:
1. Is Phoenix backend running?
2. Is the token valid (not revoked)?
3. Is the API URL correct?
4. Is there network connectivity?
### High Memory Usage
**Check**:
1. Database size: `ls -lh /data/towerops-agent.db`
2. Are metrics being submitted? (check logs)
3. Is cleanup running? (should see log every hour)
### Agent Not Showing as Online
**Check**:
1. Is heartbeat working? (check Phoenix logs)
2. Check `last_seen_at` in database: `SELECT last_seen_at FROM agent_tokens WHERE token_hash = ...`
3. Time sync between agent and server
## File Organization
```
towerops-agent/
├── src/
│ ├── main.rs # Entry point, CLI, initialization
│ ├── config.rs # Types matching API responses
│ ├── api_client.rs # HTTP client for Towerops API
│ ├── metrics/
│ │ └── mod.rs # Metric types (SensorReading, InterfaceStat)
│ ├── snmp/
│ │ ├── mod.rs # Module exports
│ │ ├── client.rs # ⚠️ INCOMPLETE - needs SNMP integration
│ │ └── types.rs # SNMP types and errors
│ ├── buffer/
│ │ ├── mod.rs # Module exports
│ │ └── storage.rs # SQLite buffering
│ └── poller/
│ ├── mod.rs # Module exports
│ ├── executor.rs # Poll execution logic
│ └── scheduler.rs # Main event loop
├── Cargo.toml # Dependencies and build config
├── Dockerfile # Multi-stage build
├── README.md # User documentation
└── CLAUDE.md # This file
```
## Dependencies
**Key Crates**:
- `tokio` - Async runtime with full features
- `reqwest` - HTTP client (rustls-tls, no default features)
- `rusqlite` - SQLite (bundled)
- `serde` + `serde_json` - Serialization
- `snmp` - SNMP operations (v0.2) ⚠️ needs integration
- `tracing` + `tracing-subscriber` - Logging
- `clap` - CLI argument parsing
- `chrono` - Timestamps
- `anyhow` + `thiserror` - Error handling
- `hostname` - Get system hostname
## Next Actions
**Immediate** (to make agent functional):
1. Research `snmp` crate v0.2 API documentation
2. Update `src/snmp/client.rs` with working implementation
3. Test with a real SNMP device or simulator
4. Add unit tests for SNMP operations
**Short-term** (for production readiness):
1. Add more comprehensive unit tests
2. Integration test with mock SNMP device
3. Load test with 100 devices
4. Stability test (7+ days continuous)
**Long-term** (nice to have):
1. SNMPv3 support
2. Agent-side threshold filtering
3. Configurable sampling rates
4. Agent health metrics endpoint
## Resources
- **Main Implementation Doc**: `/Users/graham/dev/towerops/AGENT_IMPLEMENTATION.md`
- **Next Steps Guide**: `/Users/graham/dev/towerops/AGENT_NEXT_STEPS.md`
- **SNMP Crate Docs**: https://docs.rs/snmp/0.2.2/snmp/
- **SNMP Crate Source**: https://github.com/hroi/snmp-rs
## Success Criteria
Agent is production-ready when:
- [x] Compiles successfully
- [x] Docker image builds
- [x] API client works (config, metrics, heartbeat)
- [x] SQLite buffering works
- [x] Event loop runs without panics
- [ ] **SNMP polling works** ← ONLY REMAINING ITEM
- [ ] Metrics appear in Phoenix database
- [ ] Survives 24h API outage
- [ ] Uses <256 MB memory with 50 devices
- [ ] Runs for 7+ days without issues
## Notes for Future Development
### Adding New Metric Types
1. Add variant to `Metric` enum in `src/metrics/mod.rs`
2. Update `metric_type()` and `timestamp()` methods
3. Update Phoenix API to accept new type
4. Add serialization test
### Adding New Configuration Fields
1. Update structs in `src/config.rs`
2. Update Phoenix API `build_equipment_config/1`
3. Consider backwards compatibility
### Debugging SNMP Issues
- Set `RUST_LOG=debug` to see all SNMP operations
- Check IP reachability: `ping <device-ip>`
- Test SNMP manually: `snmpget -v2c -c public <device-ip> <oid>`
- Verify community string is correct
- Check firewall rules (UDP port 161)
---
**Last Updated**: January 9, 2026
**Status**: Architecture complete, SNMP integration pending
**Version**: 0.1.0 (pre-release)

219
Cargo.lock generated
View file

@ -73,6 +73,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "base64"
version = "0.22.1"
@ -85,6 +91,12 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.52"
@ -158,6 +170,12 @@ dependencies = [
"syn",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "0.1.4"
@ -181,6 +199,22 @@ dependencies = [
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@ -193,12 +227,24 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@ -208,6 +254,18 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@ -217,13 +275,19 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown",
"hashbrown 0.14.5",
]
[[package]]
@ -345,12 +409,31 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.17"
@ -398,6 +481,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
version = "0.8.1"
@ -416,6 +505,12 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "multimap"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]]
name = "once_cell"
version = "1.21.3"
@ -434,6 +529,16 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@ -470,6 +575,16 @@ dependencies = [
"zerovec",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.105"
@ -479,6 +594,58 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
dependencies = [
"heck",
"itertools",
"log",
"multimap",
"once_cell",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "prost-types"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
dependencies = [
"prost",
]
[[package]]
name = "quote"
version = "1.0.43"
@ -488,6 +655,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.12.2"
@ -531,6 +704,19 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rustix"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.228"
@ -626,6 +812,19 @@ dependencies = [
"syn",
]
[[package]]
name = "tempfile"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@ -685,6 +884,9 @@ dependencies = [
"env_logger",
"hostname",
"log",
"prost",
"prost-build",
"prost-types",
"rusqlite",
"serde",
"serde_json",
@ -750,6 +952,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
@ -765,6 +976,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
version = "0.6.2"

View file

@ -15,6 +15,11 @@ env_logger = "0.11"
thiserror = "1.0"
clap = { version = "4.0", features = ["derive", "env"] }
hostname = "0.4"
prost = "0.13"
prost-types = "0.13"
[build-dependencies]
prost-build = "0.13"
[profile.release]
opt-level = "z"

3
build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
prost_build::compile_protos(&["proto/agent.proto"], &["proto/"]).unwrap();
}

83
proto/agent.proto Normal file
View file

@ -0,0 +1,83 @@
syntax = "proto3";
package towerops.agent;
// Configuration received from the API
message AgentConfig {
string version = 1;
uint32 poll_interval_seconds = 2;
repeated Equipment equipment = 3;
}
message Equipment {
string id = 1;
string name = 2;
string ip_address = 3;
SnmpConfig snmp = 4;
uint32 poll_interval_seconds = 5;
repeated Sensor sensors = 6;
repeated Interface interfaces = 7;
}
message SnmpConfig {
bool enabled = 1;
string version = 2;
string community = 3;
uint32 port = 4;
}
message Sensor {
string id = 1;
string type = 2;
string oid = 3;
double divisor = 4;
string unit = 5;
map<string, string> metadata = 6;
}
message Interface {
string id = 1;
uint32 if_index = 2;
string if_name = 3;
}
// Metrics submitted to the API
message MetricBatch {
repeated Metric metrics = 1;
}
message Metric {
oneof metric_type {
SensorReading sensor_reading = 1;
InterfaceStat interface_stat = 2;
}
}
message SensorReading {
string sensor_id = 1;
double value = 2;
string status = 3;
int64 timestamp = 4; // Unix timestamp in seconds
}
message InterfaceStat {
string interface_id = 1;
int64 if_in_octets = 2;
int64 if_out_octets = 3;
int64 if_in_errors = 4;
int64 if_out_errors = 5;
int64 if_in_discards = 6;
int64 if_out_discards = 7;
int64 timestamp = 8; // Unix timestamp in seconds
}
// Heartbeat metadata
message HeartbeatMetadata {
string version = 1;
string hostname = 2;
uint64 uptime_seconds = 3;
}
message HeartbeatResponse {
string status = 1;
}

View file

@ -1,5 +1,7 @@
use crate::config::{AgentConfig, HeartbeatMetadata};
use crate::metrics::Metric;
use crate::proto::agent;
use prost::Message;
use serde_json::json;
use std::time::Duration;
use thiserror::Error;
@ -63,7 +65,7 @@ impl ApiClient {
Ok(config)
}
/// Submit metrics to the API
/// Submit metrics to the API using Protocol Buffers
pub async fn submit_metrics(&self, metrics: Vec<Metric>) -> Result<()> {
if metrics.is_empty() {
return Ok(());
@ -73,10 +75,28 @@ impl ApiClient {
let token = self.token.clone();
tokio::task::spawn_blocking(move || {
// Convert metrics to protobuf
let proto_metrics: Vec<agent::Metric> = metrics
.into_iter()
.map(|m| convert_metric_to_proto(&m))
.collect();
let batch = agent::MetricBatch {
metrics: proto_metrics,
};
// Encode to bytes
let mut buf = Vec::new();
batch
.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(json!({ "metrics": metrics }))
.send_bytes(&buf)
.map_err(|e| ApiError::RequestFailed(e.to_string()))?;
let status = response.status();
@ -117,3 +137,35 @@ impl ApiClient {
Ok(())
}
}
/// Convert internal Metric type to protobuf Metric
fn convert_metric_to_proto(metric: &Metric) -> agent::Metric {
use crate::metrics::Metric as M;
match metric {
M::SensorReading(sr) => agent::Metric {
metric_type: Some(agent::metric::MetricType::SensorReading(
agent::SensorReading {
sensor_id: sr.sensor_id.clone(),
value: sr.value,
status: sr.status.clone(),
timestamp: sr.timestamp.to_unix_timestamp(),
},
)),
},
M::InterfaceStat(is) => agent::Metric {
metric_type: Some(agent::metric::MetricType::InterfaceStat(
agent::InterfaceStat {
interface_id: is.interface_id.clone(),
if_in_octets: is.if_in_octets,
if_out_octets: is.if_out_octets,
if_in_errors: is.if_in_errors,
if_out_errors: is.if_out_errors,
if_in_discards: is.if_in_discards,
if_out_discards: is.if_out_discards,
timestamp: is.timestamp.to_unix_timestamp(),
},
)),
},
}
}

View file

@ -3,6 +3,7 @@ mod buffer;
mod config;
mod metrics;
mod poller;
mod proto;
mod snmp;
use clap::Parser;

View file

@ -21,6 +21,10 @@ impl Timestamp {
Self::now().secs - self.secs
}
pub fn to_unix_timestamp(&self) -> i64 {
self.secs
}
pub fn to_rfc3339(self) -> String {
// Convert Unix timestamp to RFC3339 format
// This is a simplified implementation that produces UTC timestamps

4
src/proto.rs Normal file
View file

@ -0,0 +1,4 @@
// Generated protobuf code
pub mod agent {
include!(concat!(env!("OUT_DIR"), "/towerops.agent.rs"));
}