diff --git a/Cargo.lock b/Cargo.lock index df18e97..1cb8811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,13 +4,162 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "zfs_backup" version = "0.1.0" dependencies = [ "anyhow", + "serde", + "toml", ] diff --git a/Cargo.toml b/Cargo.toml index 29a89d6..aed3913 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,5 @@ edition = "2024" [dependencies] anyhow = "1.0.98" +serde = { version = "1.0", features = ["derive"] } +toml = "0.9.2" diff --git a/backup_pool.txt b/backup_pool.txt deleted file mode 100644 index ec76ec2..0000000 --- a/backup_pool.txt +++ /dev/null @@ -1 +0,0 @@ -backup diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..505baa1 --- /dev/null +++ b/config.toml @@ -0,0 +1,10 @@ +snapshot_tag = "znap" +snapshot_interval = "monthly" + +local_pool_name = "tank" + +[backup_pools] +backup1 = 'a235c116-764f-4082-97fc-47ca31e11e53' +backup2 = '200ced55-894c-4f70-b089-b140e60193fc' + + diff --git a/src/cmd/zfs.rs b/src/cmd/zfs.rs index f9de591..67d9823 100644 --- a/src/cmd/zfs.rs +++ b/src/cmd/zfs.rs @@ -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 { + 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> { let output = get_output( diff --git a/src/io.rs b/src/io.rs deleted file mode 100644 index 6fc2fc2..0000000 --- a/src/io.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; - -pub fn read_var_from_file(filename: &str) -> Result { - 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> { - let content = std::fs::read_to_string(PathBuf::from(filename))?; - let s = content.trim(); - Ok(s.lines().map(|s| s.to_owned()).collect()) -} diff --git a/src/lib.rs b/src/lib.rs index 84c1de3..74cc132 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,2 @@ pub mod cmd; -pub mod io; pub mod zfs; diff --git a/src/main.rs b/src/main.rs index b4a51ef..98f4cdb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { + 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_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 { + 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) -> Result<()> { +fn execute_todos(local: &ZPool, remote: &ZPool, todos: Vec) -> Result<()> { for todo in todos { todo.print(); match todo { @@ -95,34 +129,42 @@ fn ask_user_confirmation(todos: &Vec) -> Result { 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> { + fn plan( + local: &ZPool, + remote: &ZPool, + snapshot_tag: String, + snapshot_interval: String, + ) -> Result> { 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) { diff --git a/src/zfs.rs b/src/zfs.rs index 797fdff..e3c53e1 100644 --- a/src/zfs.rs +++ b/src/zfs.rs @@ -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, } -impl Pool { +impl ZPool { pub fn new(arg: &str) -> Result { 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 { - 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 { + todo!() + } + + pub fn import(name: &str) -> Result { + 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> { - 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> { + pub fn find_snapshots_like_tag( + &self, + snapshot_tag: &str, + snapshot_interval: &str, + ) -> Result> { 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 { - 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 { + 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);