commit dfe963e461fa671cbe33a5a891590afb578b5a1e Author: Daniel de Cloet Date: Fri Feb 9 14:16:43 2024 +0100 Import zfs snapshots service as Arch package into git diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e698f51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/*.src.tar.gz +/*.pkg.tar.zst diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..2cda6c3 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,20 @@ +# Maintainer: Daniel +pkgname=zfs-snapshotter +pkgver=0.0.1 +pkgrel=1 +pkgdesc='Systemd snapshotter tool' + +source=("zfs-daily-snapshot@.timer" "zfs-daily-snapshot@.service" "zfs-snapshot.rb") +arch=('x86_64' 'aarch64' 'armv7h') +depends=('systemd' 'zfs' 'ruby') + +sha256sums=('SKIP' + 'SKIP' + 'SKIP') + +package() { + install -Dm755 "${srcdir}/zfs-snapshot.rb" "${pkgdir}/usr/bin/zfs-snapshot.rb" + + install -Dm644 "${srcdir}/zfs-daily-snapshot@.timer" "${pkgdir}/etc/systemd/system/zfs-daily-snapshot@.timer" + install -Dm644 "${srcdir}/zfs-daily-snapshot@.service" "${pkgdir}/etc/systemd/system/zfs-daily-snapshot@.service" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..84dd13f --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# ZFS Snapshotter + +This is my personal opiniated package for automatically creating and cleaning up ZFS snapshots. + +## Usage: +- Clone this repository. +- Build and install: `$ makepkg -Si` +- Enable the systemd timer for your datasets: + - Use `-` instead of `/`, due to systemd encoding: `# systemctl enable --now zfs-daily-snapshot@pool-my-dataset.timer` diff --git a/zfs-daily-snapshot@.service b/zfs-daily-snapshot@.service new file mode 100644 index 0000000..2c7d878 --- /dev/null +++ b/zfs-daily-snapshot@.service @@ -0,0 +1,7 @@ +[Unit] +Description=Create a ZFS snapshot for %I +DefaultDependencies=no + +[Service] +Type=oneshot +ExecStart=/usr/bin/zfs-snapshot.rb "%I" diff --git a/zfs-daily-snapshot@.timer b/zfs-daily-snapshot@.timer new file mode 100644 index 0000000..a3083d6 --- /dev/null +++ b/zfs-daily-snapshot@.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Create a daily ZFS snapshot for %I + +[Timer] +OnCalendar=daily +Persistent=true + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/zfs-snapshot.rb b/zfs-snapshot.rb new file mode 100644 index 0000000..ef8bb70 --- /dev/null +++ b/zfs-snapshot.rb @@ -0,0 +1,76 @@ +#!/usr/bin/env ruby + +require 'date' + +SNAPSHOTS_TO_RETAIN = 7 +SNAPSHOT_FORMAT = "daily-%Y-%m-%d" + +input = ARGV + +def usage + puts "Usage: " + puts " #{$PROGRAM_NAME} " + + exit -1 +end + +def run_command(*cmd) + pipe = IO.popen(cmd) + stdout = pipe.read + puts stdout + pipe.close + + return stdout, $?.success? +end + +class Array + def drop_tail(n) + slice(0...(length - n)) + end +end + +class Dataset + def initialize(name) + _, dataset_exists = run_command("zfs", "list", "-H", name) + raise "No such pool: #{name}" unless dataset_exists + + @name = name + end + + def has_snapshot(name) + _, snapshot_exists = run_command("zfs", "list", "-H", "#{@name}@#{name}") + return snapshot_exists + end + + def create_snapshot_today + current_snapshot_name = Date.today.strftime(SNAPSHOT_FORMAT) + raise "Snapshot already created: #{@name}@#{current_snapshot_name}" if has_snapshot(current_snapshot_name) + + _, made_snapshot = run_command("zfs", "snapshot", "#{@name}@#{current_snapshot_name}") + raise "Could not create new snapshot #{@name}@#{current_snapshot_name}" unless made_snapshot + end + + def get_all_snapshots + all_snapshots, could_list_all_snapshot = run_command("zfs", "list", "-t", "snapshot", "-H", @name) + raise "Could not list snapshots for #{@name}" unless could_list_all_snapshot + + return all_snapshots.lines.map { |line| line.split("\t").first }.map { |snapshot_name| snapshot_name.split("@").last } + end + + def clean_up_old_snapshots + snapshots_to_delete = get_all_snapshots.drop_tail(SNAPSHOTS_TO_RETAIN) + p snapshots_to_delete + + return if snapshots_to_delete.length == 0 + + output, could_delete_old_snapshots = run_command("zfs", "destroy", "#{@name}@#{snapshots_to_delete.first}%#{snapshots_to_delete.last}") + raise "Could not delete old snapshots for #{@name}" unless could_delete_old_snapshots + end +end + +usage unless input.length == 1 +usage if input[0] == "-h" or input[0] == "--help" + +dataset = Dataset.new input[0] +dataset.create_snapshot_today +dataset.clean_up_old_snapshots