#![feature(try_blocks)] use std::path::PathBuf; use clap::Parser; use eyre::{Context, Result}; mod cfg; mod zfs; #[derive(Debug, Parser)] struct Args { /// Path to the configuration file #[clap(long, env = "ZSNAP_CONFIG")] config: Option, #[clap(subcommand)] command: Subcommand, } #[derive(Debug, Parser)] enum Subcommand { Run(RunArgs), Snapshot(SnapshotArgs), Prune(PruneArgs), } /// Run the zsnap daemon. /// /// This command will periodically create and prune snapshots according to the configuration file. #[derive(Debug, Parser)] struct RunArgs {} /// Create a snapshot. #[derive(Debug, Parser)] struct SnapshotArgs { /// the tag to snapshot with #[clap(long)] tag: String, /// whether to snapshot recursively #[clap(long)] recursive: bool, /// the datasets to snapshot dataset: Vec, } /// Prune snapshots. #[derive(Debug, Parser)] struct PruneArgs { /// the tag to prune #[clap(long)] tag: String, /// the number of snapshots to keep #[clap(long)] keep: usize, /// whether to prune recursively #[clap(long)] recursive: bool, /// the datasets to prune dataset: Vec, } fn main() -> Result<()> { color_eyre::install()?; tracing_subscriber::fmt::init(); let args = Args::parse(); let config = match args.config { Some(config_path) => cfg::read(&config_path).context("reading config")?, None => Default::default(), }; match args.command { Subcommand::Run(_) => cmd_run(config), Subcommand::Snapshot(args) => cmd_snapshot(args), Subcommand::Prune(args) => cmd_prune(args), } } fn cmd_run(config: cfg::Config) -> Result<()> { for schedule in config.schedules { let datasets = config.datasets.clone(); std::thread::spawn(move || snapshot_mainloop(schedule, datasets)); } loop { std::thread::park(); } } fn cmd_snapshot(args: SnapshotArgs) -> Result<()> { for dataset in args.dataset { zfs::snapshot_create(&args.tag, &dataset, args.recursive)?; } Ok(()) } fn cmd_prune(args: PruneArgs) -> Result<()> { for dataset in args.dataset { zfs::snapshot_prune(&args.tag, &dataset, args.recursive, args.keep)?; } Ok(()) } fn snapshot_mainloop(schedule: cfg::Schedule, datasets: Vec) { for next_trigger in schedule.cron.upcoming(chrono::Utc) { let now = chrono::Utc::now().timestamp(); let next = next_trigger.timestamp(); let sleep = next.saturating_sub(now) as u64; tracing::debug!( tag = schedule.tag, "sleeping for {} seconds until next trigger", sleep ); std::thread::sleep(std::time::Duration::from_secs(sleep)); for dataset in datasets.iter().filter(|d| d.has_tag(&schedule.tag)) { if let Err(err) = zfs::snapshot_create(&schedule.tag, &dataset.name, dataset.recursive) { tracing::error!( tag = schedule.tag, dataset = dataset.name, "failed to create snapshot: {:#}", err ); } if let Err(err) = zfs::snapshot_prune( &schedule.tag, &dataset.name, dataset.recursive, schedule.keep, ) { tracing::error!( tag = schedule.tag, dataset = dataset.name, "failed to prune snapshots: {:#}", err ); } } } }