more work

This commit is contained in:
timeshifter
2026-05-03 14:57:06 +02:00
parent af6423cb21
commit 5f1cbf5f67
9 changed files with 314 additions and 112 deletions
+14
View File
@@ -17,6 +17,20 @@ pub fn zpool_export(pool_name: &str) -> Result<()> {
run_command("zpool", vec!["export", pool_name])
}
pub fn zpool_scrub(pool_name: &str) -> Result<()> {
run_command("zpool", vec!["scrub", pool_name])
}
pub fn zpool_wait_scrub(pool_name: &str) -> Result<()> {
run_command("zpool", vec!["wait", "-t", "scrub", pool_name])
}
pub fn zpool_status(pool_name: &str) -> Result<String> {
let output = get_output("zpool", vec!["status", pool_name])?;
let status = String::from_utf8(output.stdout)?;
Ok(status)
}
#[rustfmt::skip]
pub fn zfs_list_snapshots(dataset_name: &str) -> Result<Vec<String>> {
let output = get_output(
-15
View File
@@ -1,15 +0,0 @@
use std::path::PathBuf;
use anyhow::Result;
pub fn read_var_from_file(filename: &str) -> Result<String> {
let content = std::fs::read_to_string(PathBuf::from(filename))?;
let s = content.trim();
Ok(s.trim().into())
}
pub fn read_vars_from_file(filename: &str) -> Result<Vec<String>> {
let content = std::fs::read_to_string(PathBuf::from(filename))?;
let s = content.trim();
Ok(s.lines().map(|s| s.to_owned()).collect())
}
-1
View File
@@ -1,3 +1,2 @@
pub mod cmd;
pub mod io;
pub mod zfs;
+103 -56
View File
@@ -1,36 +1,65 @@
use std::env;
use std::fs::read_to_string;
use std::io::stdin;
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use zfs_backup::cmd::cryptsetup;
use zfs_backup::cmd::zfs::{zpool_export, zpool_import};
use zfs_backup::io::read_var_from_file;
use zfs_backup::zfs::{Dataset, Pool, Snapshot};
use zfs_backup::cmd::zfs::zpool_import;
use zfs_backup::zfs::{Dataset, Snapshot, ZPool};
fn main() -> Result<()> {
verify_running_as_root()?;
decrypt_backup()?;
import_backup_pool()?;
do_backup()?;
export_backup_pool()?;
encrypt_backup()?;
let config = Config::read("config.toml")?;
let local_pool = ZPool::new(&config.local_pool_name)?; // will error if pool does not exist
let (found_pool_name, crypt_device) = decrypt_backup(&config.backup_pools)?;
let backup_pool = import_backup_pool(&found_pool_name)?;
backup_pool.scrub()?;
let something_was_done = do_backup(
&local_pool,
&backup_pool,
config.snapshot_tag,
config.snapshot_interval,
)?;
if something_was_done {
backup_pool.scrub()?;
}
backup_pool.export()?;
encrypt_backup(crypt_device)?;
println!("all done");
Ok(())
}
/// Export the ZFS pool after the backup so it can be re-encrypted.
///
/// # Errors
///
/// This function will return an error if the name of the backup pool is not provided or the pool
/// cannot be exported.
fn export_backup_pool() -> Result<()> {
let pool_name = &read_var_from_file("backup_pool.txt")?;
zpool_export(pool_name)?;
Ok(())
#[derive(Deserialize)]
struct Config {
snapshot_tag: String,
snapshot_interval: String,
local_pool_name: String,
backup_pools: BackupPools,
}
#[derive(Deserialize)]
struct BackupPools {
backup1: String,
backup2: String,
}
impl Config {
fn read(filename: &str) -> Result<Self> {
let content = read_to_string(filename).context("reading toml file")?;
let parsed: Self = toml::from_str(&content).context("parsing toml")?;
Ok(parsed)
}
}
/// Import the decrypted ZFS pool so that a backup can be sent.
@@ -39,26 +68,31 @@ fn export_backup_pool() -> Result<()> {
///
/// This function will return an error if the name of the backup pool is not provided or the pool
/// cannot be imported.
fn import_backup_pool() -> Result<()> {
let pool_name = &read_var_from_file("backup_pool.txt")?;
fn import_backup_pool(pool_name: &str) -> Result<ZPool> {
zpool_import(pool_name)?;
Ok(())
ZPool::new(pool_name)
}
fn do_backup() -> Result<()> {
let (local, remote) =
load_zfs_pools("local_pool.txt", "backup_pool.txt").context("loading zfs pools")?;
let snapshot_tag = read_var_from_file("snapshot.txt").context("fetching snapshot tag")?;
let todos = Todo::plan(&local, &remote, snapshot_tag).context("planning todos")?;
if ask_user_confirmation(&todos).context("asking user confirmation")? {
/// Returns whether something was done.
fn do_backup(
local_pool: &ZPool,
backup_pool: &ZPool,
snapshot_tag: String,
snapshot_interval: String,
) -> Result<bool> {
let todos = Todo::plan(local_pool, backup_pool, snapshot_tag, snapshot_interval)
.context("planning todos")?;
if !todos.is_empty() && ask_user_confirmation(&todos).context("asking user confirmation")? {
println!("executing:");
execute_todos(local, remote, todos)?;
execute_todos(local_pool, backup_pool, todos)?;
return Ok(true);
}
Ok(())
Ok(false)
}
fn execute_todos(local: Pool, remote: Pool, todos: Vec<Todo>) -> Result<()> {
fn execute_todos(local: &ZPool, remote: &ZPool, todos: Vec<Todo>) -> Result<()> {
for todo in todos {
todo.print();
match todo {
@@ -95,34 +129,42 @@ fn ask_user_confirmation(todos: &Vec<Todo>) -> Result<bool> {
Ok(false)
}
fn load_zfs_pools(local_pool: &str, backup_pool: &str) -> Result<(Pool, Pool)> {
let mut local =
Pool::new(&read_var_from_file(local_pool).context("getting local pool from config")?)
.context("finding local pool")?;
local
.load_datasets_from_file("datasets.txt")
.context("loading datasets to send")?;
fn decrypt_backup(pools: &BackupPools) -> Result<(String, String)> {
let mut pathbuf = PathBuf::new();
let mut found = false;
let mut remote =
Pool::new(&read_var_from_file(backup_pool).context("getting backup pool from config")?)
.context("finding backup pool")?;
remote
.get_datasets_from_system()
.context("loading remote datasets")?;
let mut the_name = String::new();
let mut the_crypt_device = String::new();
Ok((local, remote))
for (device, name) in [&pools.backup1, &pools.backup2]
.iter()
.zip(["backup1", "backup2"])
{
pathbuf.push("/dev/disk/by-uuid/");
pathbuf.push(device);
if pathbuf.exists() {
found = true;
the_name = name.to_string();
the_crypt_device.push_str("crypt-");
the_crypt_device.push_str(name); // content is then e.g. "crypt-backup1"
println!("found backup disk {name}, will decrypt to {the_crypt_device}");
break;
}
}
if found {
cryptsetup::open(&pathbuf, &the_crypt_device)?;
Ok((the_name, the_crypt_device))
} else {
bail!("no backup disk found")
}
}
fn decrypt_backup() -> Result<()> {
// let mut uuid = "/dev/disk/by-uuid/".to_string(); // TODO change back
let mut pathbuf = PathBuf::from("/home/timeshifter/rust/zfs_backup/");
pathbuf.push(&read_var_from_file("backup_uuid.txt")?);
cryptsetup::open(&pathbuf, "crypt-backup")
}
fn encrypt_backup() -> Result<()> {
cryptsetup::close("crypt-backup")
fn encrypt_backup(crypt_device: String) -> Result<()> {
cryptsetup::close(&crypt_device)
}
fn verify_running_as_root() -> Result<()> {
@@ -154,12 +196,17 @@ impl Todo {
}
}
fn plan(local: &Pool, remote: &Pool, snapshot_tag: String) -> Result<Vec<Self>> {
fn plan(
local: &ZPool,
remote: &ZPool,
snapshot_tag: String,
snapshot_interval: String,
) -> Result<Vec<Self>> {
let mut result = vec![];
for local_dataset in &local.datasets {
let last_snapshot_with_tag =
local_dataset.find_last_snapshot_with_tag(&snapshot_tag)?;
local_dataset.find_last_snapshot_with_tag(&snapshot_tag, &snapshot_interval)?;
let remote_dataset = local_dataset.change_pool_name(remote);
if !remote.contains(&remote_dataset) {
+34 -37
View File
@@ -1,22 +1,17 @@
use std::fmt::Write;
use std::{
fmt::Display,
path::{Path, PathBuf},
process::Child,
};
use std::{fmt::Display, path::PathBuf, process::Child};
use anyhow::{Result, anyhow, bail};
use crate::cmd::zfs::*;
use crate::io::read_vars_from_file;
/// A ZFS pool. It holds the name of the pool and the list of datasets belonging to it.
pub struct Pool {
name: String,
pub struct ZPool {
pub name: String,
pub datasets: Vec<Dataset>,
}
impl Pool {
impl ZPool {
pub fn new(arg: &str) -> Result<Self> {
let result = Self {
name: arg.to_string(),
@@ -62,11 +57,6 @@ impl Pool {
Ok(())
}
pub fn load_datasets_from_file(&mut self, filename: &str) -> Result<()> {
self.datasets = Dataset::from_file(&PathBuf::from(filename), &self.name)?;
Ok(())
}
pub fn get_datasets_from_system(&mut self) -> Result<()> {
let datasets = zfs_list_main_datasets(&self.name)?;
@@ -99,12 +89,24 @@ impl Pool {
None
}
}
}
pub fn import(name: &str) -> Result<Pool> {
zpool_import(name)?;
let result = Pool::new(name)?;
Ok(result)
pub fn export(self) -> Result<()> {
todo!()
}
pub fn scrub(&self) -> Result<()> {
zpool_scrub(&self.name)
}
pub fn status(&self) -> Result<String> {
todo!()
}
pub fn import(name: &str) -> Result<ZPool> {
zpool_import(name)?;
let result = ZPool::new(name)?;
Ok(result)
}
}
/// Full path of a ZFS dataset.
@@ -119,27 +121,18 @@ impl Dataset {
Self(path)
}
fn from_file(filename: &Path, pool: &str) -> Result<Vec<Self>> {
let datasets = read_vars_from_file(filename.to_str().unwrap())?;
Ok(datasets
.into_iter()
.map(|ds| {
let mut pb = PathBuf::new();
pb.push(pool);
pb.push(ds);
Self::new(pb)
})
.collect())
}
pub fn find_snapshots_like_tag(&self, snapshot_tag: &str) -> Result<Vec<Snapshot>> {
pub fn find_snapshots_like_tag(
&self,
snapshot_tag: &str,
snapshot_interval: &str,
) -> Result<Vec<Snapshot>> {
let snapshots = zfs_list_snapshots(self.0.to_str().unwrap())?;
let mut result = vec![];
for item in snapshots {
let snapshot_name = item.split('@').next_back().unwrap();
if snapshot_name.contains(snapshot_tag) {
if snapshot_name.contains(snapshot_tag) && snapshot_name.contains(snapshot_interval) {
result.push(Snapshot(snapshot_name.to_string()));
}
}
@@ -147,8 +140,12 @@ impl Dataset {
Ok(result)
}
pub fn find_last_snapshot_with_tag(&self, snapshot_tag: &str) -> Result<Snapshot> {
let mut snapshots = self.find_snapshots_like_tag(snapshot_tag)?;
pub fn find_last_snapshot_with_tag(
&self,
snapshot_tag: &str,
snapshot_interval: &str,
) -> Result<Snapshot> {
let mut snapshots = self.find_snapshots_like_tag(snapshot_tag, snapshot_interval)?;
snapshots.sort_unstable();
let result = snapshots
.last()
@@ -167,7 +164,7 @@ impl Dataset {
Ok(snapshots)
}
pub fn change_pool_name(&self, new_pool: &Pool) -> Self {
pub fn change_pool_name(&self, new_pool: &ZPool) -> Self {
let new_pool = new_pool.name.as_str();
let mut pathbuf = PathBuf::new();
pathbuf.push(new_pool);