conf backup?

This commit is contained in:
lda
2026-04-13 00:44:09 +07:00 Unverified
parent 2b4e5fa814
commit e912e7c7b3
3 changed files with 88 additions and 11 deletions
+43 -2
View File
@@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
use std::path::{Path, PathBuf};
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<()> {
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() {
std::fs::create_dir_all(parent)
.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");
std::fs::write(&tmp, content)
.with_context(|| format!("failed to write temp config {}", tmp.display()))?;
+16 -6
View File
@@ -1,10 +1,15 @@
use anyhow::{Context, Result};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::path::{Path, PathBuf};
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)]
struct EnrollRequest<'a> {
@@ -22,7 +27,7 @@ pub async fn enroll(
server_url: &str,
enroll_token: &str,
config_path: &Path,
) -> Result<AgentConfig> {
) -> Result<EnrollOutcome> {
let server_url = normalize_server_url(server_url);
let endpoint = format!("{server_url}/api/v1/agents/enroll");
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_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");
Ok(config)
Ok(EnrollOutcome {
config,
backup_path,
})
}
pub fn normalize_server_url(server_url: &str) -> String {
@@ -131,13 +139,15 @@ mod tests {
));
let path = dir.join("config.toml");
let config = enroll(&server_url, "enroll-abc", &path)
let outcome = enroll(&server_url, "enroll-abc", &path)
.await
.expect("enroll should succeed");
let config = outcome.config;
assert_eq!(config.agent_id, "agent-123");
assert_eq!(config.agent_token, "token-xyz");
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");
assert_eq!(persisted, config);
+29 -3
View File
@@ -23,15 +23,41 @@ async fn main() -> Result<()> {
}
Command::Enroll(args) => {
::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?;
println!("agent_id={}", config.agent_id);
let outcome =
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_write=updated");
if let Some(backup_path) = &outcome.backup_path {
println!("config_backup={}", backup_path.display());
}
if args.reload_running {
match serve::reload_daemon(&args.pid_file) {
Ok(()) => println!("reload=signaled pid_file={}", args.pid_file.display()),
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 {