conf backup?
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub const DEFAULT_CONFIG_PATH: &str = "/etc/wakey-agent/config.toml";
|
pub const DEFAULT_CONFIG_PATH: &str = "/etc/wakey-agent/config.toml";
|
||||||
|
|
||||||
@@ -44,12 +44,53 @@ pub fn load_config(path: &Path) -> Result<AgentConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_config(path: &Path, config: &AgentConfig) -> Result<()> {
|
pub fn save_config(path: &Path, config: &AgentConfig) -> Result<()> {
|
||||||
|
let content = toml::to_string_pretty(config).context("failed to serialize agent config")?;
|
||||||
|
write_config_atomically(path, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config_with_backup(path: &Path, config: &AgentConfig) -> Result<Option<PathBuf>> {
|
||||||
|
let backup = if path.exists() {
|
||||||
|
Some(snapshot_existing_config(path)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
save_config(path, config)?;
|
||||||
|
Ok(backup)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_backup(path: &Path, backup_path: &Path) -> Result<()> {
|
||||||
|
let content = std::fs::read_to_string(backup_path)
|
||||||
|
.with_context(|| format!("failed to read config backup {}", backup_path.display()))?;
|
||||||
|
write_config_atomically(path, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_existing_config(path: &Path) -> Result<PathBuf> {
|
||||||
|
let ts = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("config.toml");
|
||||||
|
let backup_name = format!("{file_name}.bak.{ts}");
|
||||||
|
let backup_path = path.with_file_name(backup_name);
|
||||||
|
std::fs::copy(path, &backup_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to create config backup {} from {}",
|
||||||
|
backup_path.display(),
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(backup_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_config_atomically(path: &Path, content: &str) -> Result<()> {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent)
|
||||||
.with_context(|| format!("failed to create config dir {}", parent.display()))?;
|
.with_context(|| format!("failed to create config dir {}", parent.display()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = toml::to_string_pretty(config).context("failed to serialize agent config")?;
|
|
||||||
let tmp = path.with_extension("toml.tmp");
|
let tmp = path.with_extension("toml.tmp");
|
||||||
std::fs::write(&tmp, content)
|
std::fs::write(&tmp, content)
|
||||||
.with_context(|| format!("failed to write temp config {}", tmp.display()))?;
|
.with_context(|| format!("failed to write temp config {}", tmp.display()))?;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::config::{AgentConfig, save_config};
|
use crate::config::{AgentConfig, save_config_with_backup};
|
||||||
|
|
||||||
|
pub struct EnrollOutcome {
|
||||||
|
pub config: AgentConfig,
|
||||||
|
pub backup_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct EnrollRequest<'a> {
|
struct EnrollRequest<'a> {
|
||||||
@@ -22,7 +27,7 @@ pub async fn enroll(
|
|||||||
server_url: &str,
|
server_url: &str,
|
||||||
enroll_token: &str,
|
enroll_token: &str,
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
) -> Result<AgentConfig> {
|
) -> Result<EnrollOutcome> {
|
||||||
let server_url = normalize_server_url(server_url);
|
let server_url = normalize_server_url(server_url);
|
||||||
let endpoint = format!("{server_url}/api/v1/agents/enroll");
|
let endpoint = format!("{server_url}/api/v1/agents/enroll");
|
||||||
info!(endpoint = %endpoint, config_path = %config_path.display(), "starting agent enrollment");
|
info!(endpoint = %endpoint, config_path = %config_path.display(), "starting agent enrollment");
|
||||||
@@ -56,9 +61,12 @@ pub async fn enroll(
|
|||||||
reconnect_base_ms: 1_000,
|
reconnect_base_ms: 1_000,
|
||||||
reconnect_max_ms: 30_000,
|
reconnect_max_ms: 30_000,
|
||||||
};
|
};
|
||||||
save_config(config_path, &config)?;
|
let backup_path = save_config_with_backup(config_path, &config)?;
|
||||||
info!(agent_id = %config.agent_id, config_path = %config_path.display(), "agent enrollment succeeded and config was written");
|
info!(agent_id = %config.agent_id, config_path = %config_path.display(), "agent enrollment succeeded and config was written");
|
||||||
Ok(config)
|
Ok(EnrollOutcome {
|
||||||
|
config,
|
||||||
|
backup_path,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normalize_server_url(server_url: &str) -> String {
|
pub fn normalize_server_url(server_url: &str) -> String {
|
||||||
@@ -131,13 +139,15 @@ mod tests {
|
|||||||
));
|
));
|
||||||
let path = dir.join("config.toml");
|
let path = dir.join("config.toml");
|
||||||
|
|
||||||
let config = enroll(&server_url, "enroll-abc", &path)
|
let outcome = enroll(&server_url, "enroll-abc", &path)
|
||||||
.await
|
.await
|
||||||
.expect("enroll should succeed");
|
.expect("enroll should succeed");
|
||||||
|
let config = outcome.config;
|
||||||
|
|
||||||
assert_eq!(config.agent_id, "agent-123");
|
assert_eq!(config.agent_id, "agent-123");
|
||||||
assert_eq!(config.agent_token, "token-xyz");
|
assert_eq!(config.agent_token, "token-xyz");
|
||||||
assert_eq!(config.server_url, "https://control.example.com");
|
assert_eq!(config.server_url, "https://control.example.com");
|
||||||
|
assert!(outcome.backup_path.is_none());
|
||||||
|
|
||||||
let persisted = crate::config::load_config(&path).expect("load persisted config");
|
let persisted = crate::config::load_config(&path).expect("load persisted config");
|
||||||
assert_eq!(persisted, config);
|
assert_eq!(persisted, config);
|
||||||
|
|||||||
+29
-3
@@ -23,15 +23,41 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Enroll(args) => {
|
Command::Enroll(args) => {
|
||||||
::tracing::info!(server_url = %args.server_url, config = %args.config.display(), "wakey-agent command: enroll");
|
::tracing::info!(server_url = %args.server_url, config = %args.config.display(), "wakey-agent command: enroll");
|
||||||
let config = enroll::enroll(&args.server_url, &args.enroll_token, &args.config).await?;
|
let outcome =
|
||||||
println!("agent_id={}", config.agent_id);
|
enroll::enroll(&args.server_url, &args.enroll_token, &args.config).await?;
|
||||||
|
println!("agent_id={}", outcome.config.agent_id);
|
||||||
println!("config={}", args.config.display());
|
println!("config={}", args.config.display());
|
||||||
println!("config_write=updated");
|
println!("config_write=updated");
|
||||||
|
if let Some(backup_path) = &outcome.backup_path {
|
||||||
|
println!("config_backup={}", backup_path.display());
|
||||||
|
}
|
||||||
if args.reload_running {
|
if args.reload_running {
|
||||||
match serve::reload_daemon(&args.pid_file) {
|
match serve::reload_daemon(&args.pid_file) {
|
||||||
Ok(()) => println!("reload=signaled pid_file={}", args.pid_file.display()),
|
Ok(()) => println!("reload=signaled pid_file={}", args.pid_file.display()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
::tracing::warn!(error = %err, pid_file = %args.pid_file.display(), "enroll completed but daemon reload failed")
|
::tracing::warn!(error = %err, pid_file = %args.pid_file.display(), "enroll completed but daemon reload failed");
|
||||||
|
if let Some(backup_path) = &outcome.backup_path {
|
||||||
|
match config::restore_backup(&args.config, backup_path) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("rollback=restored backup={}", backup_path.display());
|
||||||
|
anyhow::bail!(
|
||||||
|
"reload failed after enroll; restored previous config from {}",
|
||||||
|
backup_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(restore_err) => {
|
||||||
|
anyhow::bail!(
|
||||||
|
"reload failed after enroll and rollback failed: reload_error={}, rollback_error={}",
|
||||||
|
err,
|
||||||
|
restore_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
anyhow::bail!(
|
||||||
|
"reload failed after enroll; no previous config backup was available"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user