WP-30/31/32: backup targets, strategies and execution
SMB (smbclient) and FTP/FTPS (curl with netrc) targets with encrypted credentials and connection test; strategies with cron schedule, retention, optional openssl AES-256 encryption; sources: PVC hostpath tar, pg_dumpall in the Postgres pod, namespace manifests, host directory. Backup job collects, encrypts, uploads, verifies size, records sha256 and applies retention on the target; scheduler starts due strategies. Backups page with target/strategy forms, run now and history. Restore guide in docs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@ -13,6 +13,20 @@ pub struct Output {
|
||||
pub trait CommandRunner: Send + Sync {
|
||||
async fn run(&self, program: &str, args: &[&str]) -> Result<Output, DomainError>;
|
||||
async fn read_file(&self, path: &str) -> Result<Option<String>, DomainError>;
|
||||
/// Run with extra environment variables (used to pass secrets without exposing them in argv).
|
||||
async fn run_env(
|
||||
&self,
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
env: &[(&str, &str)],
|
||||
) -> Result<Output, DomainError>;
|
||||
/// Run a command and write its stdout to `file`; stderr is captured in the result.
|
||||
async fn run_to_file(
|
||||
&self,
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
file: &std::path::Path,
|
||||
) -> Result<Output, DomainError>;
|
||||
/// Run a command and forward each output line (stdout and stderr) to `out`.
|
||||
async fn run_streaming(
|
||||
&self,
|
||||
@ -27,10 +41,24 @@ pub struct SystemCommandRunner;
|
||||
#[async_trait]
|
||||
impl CommandRunner for SystemCommandRunner {
|
||||
async fn run(&self, program: &str, args: &[&str]) -> Result<Output, DomainError> {
|
||||
let out = tokio::process::Command::new(program)
|
||||
.args(args)
|
||||
self.run_env(program, args, &[]).await
|
||||
}
|
||||
|
||||
async fn run_env(
|
||||
&self,
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
env: &[(&str, &str)],
|
||||
) -> Result<Output, DomainError> {
|
||||
let mut cmd = tokio::process::Command::new(program);
|
||||
cmd.args(args)
|
||||
.env("DEBIAN_FRONTEND", "noninteractive")
|
||||
.env("LC_ALL", "C")
|
||||
.stdin(std::process::Stdio::null());
|
||||
for (k, v) in env {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
let out = cmd
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| DomainError::Unavailable(format!("{program}: {e}")))?;
|
||||
@ -41,6 +69,30 @@ impl CommandRunner for SystemCommandRunner {
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_to_file(
|
||||
&self,
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
file: &std::path::Path,
|
||||
) -> Result<Output, DomainError> {
|
||||
let f = std::fs::File::create(file)
|
||||
.map_err(|e| DomainError::Storage(format!("{}: {e}", file.display())))?;
|
||||
let out = tokio::process::Command::new(program)
|
||||
.args(args)
|
||||
.env("LC_ALL", "C")
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::from(f))
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| DomainError::Unavailable(format!("{program}: {e}")))?;
|
||||
Ok(Output {
|
||||
stdout: String::new(),
|
||||
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||
success: out.status.success(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_streaming(
|
||||
&self,
|
||||
program: &str,
|
||||
|
||||
@ -237,6 +237,22 @@ Conf openssl (3.0.16-1~deb12u1 Debian-Security:12/stable-security [amd64])\n";
|
||||
) -> Result<bool, DomainError> {
|
||||
unreachable!()
|
||||
}
|
||||
async fn run_env(
|
||||
&self,
|
||||
p: &str,
|
||||
a: &[&str],
|
||||
_: &[(&str, &str)],
|
||||
) -> Result<super::super::Output, DomainError> {
|
||||
self.run(p, a).await
|
||||
}
|
||||
async fn run_to_file(
|
||||
&self,
|
||||
_: &str,
|
||||
_: &[&str],
|
||||
_: &std::path::Path,
|
||||
) -> Result<super::super::Output, DomainError> {
|
||||
unreachable!()
|
||||
}
|
||||
async fn read_file(&self, path: &str) -> Result<Option<String>, DomainError> {
|
||||
Ok(match path {
|
||||
"/etc/os-release" => Some(
|
||||
|
||||
@ -79,6 +79,22 @@ mod tests {
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
async fn run_env(
|
||||
&self,
|
||||
p: &str,
|
||||
a: &[&str],
|
||||
_: &[(&str, &str)],
|
||||
) -> Result<crate::host::Output, DomainError> {
|
||||
self.run(p, a).await
|
||||
}
|
||||
async fn run_to_file(
|
||||
&self,
|
||||
_: &str,
|
||||
_: &[&str],
|
||||
_: &std::path::Path,
|
||||
) -> Result<crate::host::Output, DomainError> {
|
||||
unreachable!()
|
||||
}
|
||||
async fn read_file(&self, _: &str) -> Result<Option<String>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user