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 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()))?;
+16 -6
View File
@@ -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
View File
@@ -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 {