Add automatic self-update capability to agent
The agent now checks for updates every hour and automatically updates itself when a new version is available on Docker Hub. Features: - Periodic update checks (hourly via scheduler) - Automatic Docker image pull when update available - Graceful exit and restart with new version - Non-blocking, runs in background task - Requires Docker socket mount for self-update Changes: - Add UpdateInfo struct and get_update_info() function - Add perform_self_update() to pull new image and restart - Add update check ticker to scheduler (hourly) - Include docker-cli in Dockerfile runtime stage - Update docker-compose.example.yml with socket mount - Update README and CLAUDE.md with auto-update docs The agent will log update status: - "Already running latest version" - no action - "Performing self-update: X -> Y" - pulling new image - "Exiting to allow restart with new version" - restarting Requires: - Docker socket mounted: /var/run/docker.sock:/var/run/docker.sock - restart: unless-stopped in docker-compose (to restart after exit)
This commit is contained in:
parent
3622cbf0ee
commit
824f4388eb
6 changed files with 117 additions and 14 deletions
|
|
@ -37,6 +37,7 @@ Lightweight Rust agent for remote SNMP polling. Deployed on customer networks to
|
||||||
- Heartbeat every 60 seconds
|
- Heartbeat every 60 seconds
|
||||||
- Cleanup every hour
|
- Cleanup every hour
|
||||||
- Poll check every 5 seconds
|
- Poll check every 5 seconds
|
||||||
|
- Update check and auto-update every hour
|
||||||
|
|
||||||
- [x] **Executor** (`poller/executor.rs`)
|
- [x] **Executor** (`poller/executor.rs`)
|
||||||
- poll_sensors() - Poll configured sensors
|
- poll_sensors() - Poll configured sensors
|
||||||
|
|
@ -51,11 +52,14 @@ Lightweight Rust agent for remote SNMP polling. Deployed on customer networks to
|
||||||
- Graceful startup/shutdown
|
- Graceful startup/shutdown
|
||||||
- Docker image version checking on startup
|
- Docker image version checking on startup
|
||||||
|
|
||||||
- [x] **Version Checking** (`version.rs`)
|
- [x] **Version Checking & Auto-Update** (`version.rs`)
|
||||||
- Checks Docker Hub for newer image versions on startup
|
- Checks Docker Hub for newer image versions on startup
|
||||||
|
- Performs periodic checks every hour (configurable in scheduler)
|
||||||
|
- Automatically pulls new image and restarts when update available
|
||||||
- Compares current version with latest available
|
- Compares current version with latest available
|
||||||
- Logs warnings if updates are available
|
- Logs warnings if updates are available
|
||||||
- Non-blocking, fails gracefully if Docker Hub unavailable
|
- Non-blocking, fails gracefully if Docker Hub unavailable
|
||||||
|
- Requires Docker socket mount (`/var/run/docker.sock`) for self-update
|
||||||
|
|
||||||
### Protocol Buffers Integration
|
### Protocol Buffers Integration
|
||||||
- [x] **Protobuf Definitions** (`proto/agent.proto`)
|
- [x] **Protobuf Definitions** (`proto/agent.proto`)
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ RUN RUST_TARGET=$(cat /tmp/rust-target) && \
|
||||||
FROM alpine:3.19
|
FROM alpine:3.19
|
||||||
|
|
||||||
# Install runtime dependencies
|
# Install runtime dependencies
|
||||||
RUN apk add --no-cache ca-certificates su-exec
|
# docker-cli is needed for self-update functionality
|
||||||
|
RUN apk add --no-cache ca-certificates su-exec docker-cli
|
||||||
|
|
||||||
# Create data directory
|
# Create data directory
|
||||||
RUN mkdir -p /data
|
RUN mkdir -p /data
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ The Towerops agent enables customers to deploy SNMP polling infrastructure on th
|
||||||
- **Secure Communication**: All communication with Towerops API uses HTTPS with token authentication
|
- **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
|
- **Metric Buffering**: Stores up to 24 hours of metrics in SQLite when API is unavailable
|
||||||
- **Automatic Configuration**: Fetches polling configuration from the Towerops API
|
- **Automatic Configuration**: Fetches polling configuration from the Towerops API
|
||||||
|
- **Automatic Updates**: Checks for new versions hourly and self-updates when available (requires Docker socket access)
|
||||||
- **Low Resource Usage**: Built in Rust for minimal memory and CPU footprint (< 256 MB RAM typical)
|
- **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
|
- **Docker Ready**: Pre-built Docker images for easy deployment
|
||||||
|
|
||||||
|
|
@ -40,6 +41,8 @@ services:
|
||||||
- TOWEROPS_AGENT_TOKEN=your-agent-token-here
|
- TOWEROPS_AGENT_TOKEN=your-agent-token-here
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
# Required for automatic self-updates
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Start the agent:
|
2. Start the agent:
|
||||||
|
|
@ -48,6 +51,8 @@ services:
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note**: The Docker socket mount (`/var/run/docker.sock`) is required for automatic updates. The agent checks for new versions hourly and will automatically pull and restart with the latest version when available.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
The agent is configured via environment variables:
|
The agent is configured via environment variables:
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ services:
|
||||||
# Persistent storage for metrics buffering
|
# Persistent storage for metrics buffering
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
|
||||||
|
# Docker socket (required for automatic self-updates)
|
||||||
|
# The agent will check for updates hourly and automatically update itself
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
# Optional: Resource limits
|
# Optional: Resource limits
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ impl Scheduler {
|
||||||
let mut heartbeat_ticker = interval(Duration::from_secs(60));
|
let mut heartbeat_ticker = interval(Duration::from_secs(60));
|
||||||
let mut cleanup_ticker = interval(Duration::from_secs(3600)); // Cleanup every hour
|
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
|
let mut poll_ticker = interval(Duration::from_secs(5)); // Check if polling needed every 5s
|
||||||
|
let mut update_ticker = interval(Duration::from_secs(3600)); // Check for updates every hour
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|
@ -133,6 +134,10 @@ impl Scheduler {
|
||||||
error!("Polling error: {}", e);
|
error!("Polling error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = update_ticker.tick() => {
|
||||||
|
self.check_and_update().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,4 +255,27 @@ impl Scheduler {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_and_update(&self) {
|
||||||
|
info!("Checking for agent updates");
|
||||||
|
|
||||||
|
// Run version check in blocking thread to avoid blocking event loop
|
||||||
|
let result = tokio::task::spawn_blocking(crate::version::perform_self_update).await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(Ok(true)) => {
|
||||||
|
info!("Update initiated, container will restart with new version");
|
||||||
|
// perform_self_update calls std::process::exit(0), so we won't reach here
|
||||||
|
}
|
||||||
|
Ok(Ok(false)) => {
|
||||||
|
info!("Already running latest version");
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
warn!("Failed to perform self-update: {}", e);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Update check task failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
|
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
|
||||||
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
@ -16,32 +17,92 @@ struct DockerHubTag {
|
||||||
last_updated: String,
|
last_updated: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct UpdateInfo {
|
||||||
|
pub current_version: String,
|
||||||
|
pub latest_version: Option<String>,
|
||||||
|
pub update_available: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a newer version of the Docker image is available
|
/// Check if a newer version of the Docker image is available
|
||||||
pub fn check_for_updates() {
|
pub fn check_for_updates() {
|
||||||
info!("Current version: {}", CURRENT_VERSION);
|
info!("Current version: {}", CURRENT_VERSION);
|
||||||
info!("Checking for newer Docker image versions...");
|
info!("Checking for newer Docker image versions...");
|
||||||
|
|
||||||
match check_docker_hub() {
|
match get_update_info() {
|
||||||
Ok(Some(latest_version)) => {
|
Ok(info) => {
|
||||||
if latest_version != CURRENT_VERSION {
|
if info.update_available {
|
||||||
warn!(
|
if let Some(ref latest) = info.latest_version {
|
||||||
"⚠️ Newer version available: {} (current: {})",
|
warn!(
|
||||||
latest_version, CURRENT_VERSION
|
"⚠️ Newer version available: {} (current: {})",
|
||||||
);
|
latest, info.current_version
|
||||||
warn!(" Update with: docker pull {}:latest", DOCKER_IMAGE);
|
);
|
||||||
|
warn!(" Update with: docker pull {}:latest", DOCKER_IMAGE);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
info!("✓ Running latest version ({})", CURRENT_VERSION);
|
info!("✓ Running latest version ({})", info.current_version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
|
||||||
info!("Unable to determine latest version from Docker Hub");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to check for updates: {}", e);
|
warn!("Failed to check for updates: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get update information without logging
|
||||||
|
pub fn get_update_info() -> Result<UpdateInfo, String> {
|
||||||
|
let latest_version = check_docker_hub().map_err(|e| e.to_string())?;
|
||||||
|
let update_available = if let Some(ref latest) = latest_version {
|
||||||
|
latest != CURRENT_VERSION
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(UpdateInfo {
|
||||||
|
current_version: CURRENT_VERSION.to_string(),
|
||||||
|
latest_version,
|
||||||
|
update_available,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform self-update by pulling new image and exiting
|
||||||
|
/// Returns Ok(true) if update was initiated, Ok(false) if already up-to-date
|
||||||
|
pub fn perform_self_update() -> Result<bool, String> {
|
||||||
|
let info = get_update_info()?;
|
||||||
|
|
||||||
|
if !info.update_available {
|
||||||
|
info!("Already running latest version, no update needed");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref latest) = info.latest_version {
|
||||||
|
warn!(
|
||||||
|
"Performing self-update: {} -> {}",
|
||||||
|
info.current_version, latest
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pull the new image
|
||||||
|
info!("Pulling new Docker image: {}:latest", DOCKER_IMAGE);
|
||||||
|
let output = Command::new("docker")
|
||||||
|
.args(["pull", &format!("{}:latest", DOCKER_IMAGE)])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to execute docker command: {}", e))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(format!("Failed to pull image: {}", stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Successfully pulled new image");
|
||||||
|
info!("Exiting to allow restart with new version...");
|
||||||
|
|
||||||
|
// Exit with success code - orchestrator (docker-compose/k8s) will restart with new image
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
fn check_docker_hub() -> Result<Option<String>, Box<dyn std::error::Error>> {
|
fn check_docker_hub() -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://hub.docker.com/v2/repositories/{}/tags?page_size=10",
|
"https://hub.docker.com/v2/repositories/{}/tags?page_size=10",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue