Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement DESTROY_AFTER optional argument for bin/zfs-auto-snapshot. #44

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ This will handle automatically snapshotting datasets similar to time-sliderd fro

### Usage

/usr/local/bin/zfs-auto-snapshot INTERVAL KEEP
/usr/local/bin/zfs-auto-snapshot INTERVAL KEEP [DESTROY_AFTER]

* INTERVAL - The interval for the snapshot. This is something such as `frequent`, `hourly`, `daily`, `weekly`, `monthly`, etc.
* KEEP - How many to keep for this INTERVAL. Older ones will be destroyed.
* DESTROY_AFTER - Create snapshot[s] with maximum lifetime of DESTROY_AFTER days starting from invocation timestamp.
Snapshot with an expired `zfstools:destroy_after` property will be deleted upon first `zfs-auto-snapshot` invocation with no relation to KEEP argument.
Userful for snapshots that are created upon non-recurring events (e.g. on boot or manually) so they do not stuck on the pool forever.

#### Crontab

Expand All @@ -36,6 +39,7 @@ This will handle automatically snapshotting datasets similar to time-sliderd fro
7 0 * * * root /usr/local/bin/zfs-auto-snapshot daily 7
14 0 * * 7 root /usr/local/bin/zfs-auto-snapshot weekly 4
28 0 1 * * root /usr/local/bin/zfs-auto-snapshot monthly 12
@reboot root /usr/local/bin/zfs-auto-snapshot boot 3 30

#### Dataset setup

Expand Down Expand Up @@ -101,7 +105,9 @@ The `zfs-auto-snapshot` script will automatically flush the tables before saving

### zfs-cleanup-snapshots

Cleans up zero-sized snapshots. This ignores snapshots created by `zfs-auto-snapshot` as it handles zero-sized in its own special way.
Cleans up:
* zero-sized snapshots created not by `zfs-auto-snapshot` as it handles zero-sized in its own special way;
* snapshots with an expired `zfstools:destroy_after` property.

#### Usage

Expand Down
12 changes: 10 additions & 2 deletions bin/zfs-auto-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ end

def usage
puts <<-EOF
Usage: #{$0} [-dknpuv] <INTERVAL> <KEEP>
Usage: #{$0} [-dknpuv] <INTERVAL> <KEEP> [DESTROY_AFTER]
EOF
format = " %-15s %s"
puts format % ["-d", "Show debug output."]
Expand All @@ -58,18 +58,26 @@ Usage: #{$0} [-dknpuv] <INTERVAL> <KEEP>
puts format % ["-v", "Show what is being done."]
puts format % ["INTERVAL", "The interval to snapshot."]
puts format % ["KEEP", "How many snapshots to keep."]
puts format % ["DESTROY_AFTER", "Keep created stapshots no more than DESTROY_AFTER days. Snapshots will be deleted upon expiration by this utility."]
exit
end

usage if ARGV.length < 2

interval=ARGV[0]
keep=ARGV[1].to_i
destroy_after=nil
if ARGV.length == 3
destroy_after = (Time.now.to_i + 24*3600*Integer(ARGV[2])) rescue usage
end

datasets = find_eligible_datasets(interval, pool)

# Generate new snapshots
do_new_snapshots(datasets, interval) if keep > 0
do_new_snapshots(datasets, interval, destroy_after) if keep > 0

# Remove all snapshots with expired destroy_after attribute
cleanup_attr_expired_snapshots(pool)

# Delete expired
cleanup_expired_snapshots(pool, datasets, interval, keep, should_destroy_zero_sized_snapshots)
3 changes: 3 additions & 0 deletions bin/zfs-cleanup-snapshots
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ snapshots = Zfs::Snapshot.list(pool, {'recursive' => true}).select { |snapshot|
datasets = Zfs::Dataset.list(pool)
dataset_snapshots = group_snapshots_into_datasets(snapshots, datasets)
dataset_snapshots = datasets_destroy_zero_sized_snapshots(dataset_snapshots)

# Remove all snapshots with expired destroy_after attribute
cleanup_attr_expired_snapshots(pool)
29 changes: 26 additions & 3 deletions lib/zfstools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def snapshot_format
'%Y-%m-%d-%Hh%M'
end

def destroy_after_property
"zfstools:destroy_after"
end

### Get the name of the snapshot to create
def snapshot_name(interval)
if $use_utc
Expand Down Expand Up @@ -147,13 +151,16 @@ def find_eligible_datasets(interval, pool)
end

### Generate new snapshots
def do_new_snapshots(datasets, interval)
def do_new_snapshots(datasets, interval, destroy_after=nil)
snapshot_name = snapshot_name(interval)
options = {}
options['destroy_after'] = destroy_after if destroy_after

# Snapshot single
Zfs::Snapshot.create_many(snapshot_name, datasets['single'])
Zfs::Snapshot.create_many(snapshot_name, datasets['single'], options)
# Snapshot recursive
Zfs::Snapshot.create_many(snapshot_name, datasets['recursive'], 'recursive'=>true)
options['recursive'] = true
Zfs::Snapshot.create_many(snapshot_name, datasets['recursive'], options)
end

def group_snapshots_into_datasets(snapshots, datasets)
Expand Down Expand Up @@ -226,3 +233,19 @@ def cleanup_expired_snapshots(pool, datasets, interval, keep, should_destroy_zer
end
threads.each { |th| th.join }
end

### Find and destroy snapshots with zfstools:destroy_after attribute expired
### Should be invoked before cleanup_expired_snapshots as we prefer to delete
### snapshots with zfstools:destroy_after expired rather that by count.
def cleanup_attr_expired_snapshots(pool)
current_timestamp = Time.now.to_i;
attr_expired_snapshots = Zfs::Snapshot.list(pool, {'recursive' => true}).select { |snapshot| snapshot.destroy_after?(current_timestamp) }
threads = []
attr_expired_snapshots.each do |snapshot|
threads << Thread.new do
snapshot.destroy
end
threads.last.join unless $use_threads
end
threads.each { |th| th.join }
end
22 changes: 16 additions & 6 deletions lib/zfstools/snapshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ module Zfs
class Snapshot
@@stale_snapshot_size = false
attr_reader :name
def initialize(name, used=nil)
def initialize(name, used=nil, destroy_after=nil)
@name = name
@used = used
@destroy_after = destroy_after
end

def used
Expand All @@ -27,19 +28,25 @@ def is_zero?
used
end

def destroy_after?(timestamp)
return false if @destroy_after.nil? || @destroy_after > timestamp
true
end

### List all snapshots
def self.list(dataset=nil, options={})
snapshots = []
flags=[]
flags << "-d 1" if dataset and !options['recursive']
flags << "-r" if options['recursive']
cmd = "zfs list #{flags.join(" ")} -H -t snapshot -o name,used -S name"
cmd = "zfs list #{flags.join(" ")} -H -t snapshot -o name,used,#{destroy_after_property} -S name"
cmd += " " + dataset.shellescape if dataset
puts cmd if $debug
IO.popen cmd do |io|
io.readlines.each do |line|
snapshot_name,used = line.chomp.split("\t")
snapshots << self.new(snapshot_name, used.to_i)
snapshot_name,used,destroy_after = line.chomp.split("\t")
destroy_after = Integer(destroy_after) rescue nil
snapshots << self.new(snapshot_name, used.to_i, destroy_after)
end
end
snapshots
Expand All @@ -49,6 +56,7 @@ def self.list(dataset=nil, options={})
def self.create(snapshot, options = {})
flags=[]
flags << "-r" if options['recursive']
flags << "-o #{destroy_after_property}=" + options['destroy_after'].to_s if options['destroy_after']
cmd = "zfs snapshot #{flags.join(" ")} "
if snapshot.kind_of?(Array)
cmd += snapshot.shelljoin
Expand Down Expand Up @@ -129,9 +137,11 @@ def self.create_many(snapshot_name, datasets, options={})
threads = []
datasets.each do |dataset|
threads << Thread.new do
self.create("#{dataset.name}@#{snapshot_name}",
self.create("#{dataset.name}@#{snapshot_name}", {
'recursive' => options['recursive'] || false,
'db' => dataset.db)
'db' => dataset.db,
'destroy_after' => options['destroy_after'] || false,
})
end
threads.last.join unless $use_threads
end
Expand Down