rebuild docker image
This commit is contained in:
parent
953d044c9b
commit
1f9bdd57cf
10 changed files with 780 additions and 11 deletions
|
|
@ -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
390
CLAUDE.md
Normal 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
219
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
3
build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
prost_build::compile_protos(&["proto/agent.proto"], &["proto/"]).unwrap();
|
||||
}
|
||||
83
proto/agent.proto
Normal file
83
proto/agent.proto
Normal 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;
|
||||
}
|
||||
|
|
@ -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(),
|
||||
},
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod buffer;
|
|||
mod config;
|
||||
mod metrics;
|
||||
mod poller;
|
||||
mod proto;
|
||||
mod snmp;
|
||||
|
||||
use clap::Parser;
|
||||
|
|
|
|||
|
|
@ -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
4
src/proto.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Generated protobuf code
|
||||
pub mod agent {
|
||||
include!(concat!(env!("OUT_DIR"), "/towerops.agent.rs"));
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue