diff options
author | Matthew Bloch <matthew@bytemark.co.uk> | 2014-01-08 13:19:01 +0000 |
---|---|---|
committer | Matthew Bloch <matthew@bytemark.co.uk> | 2014-01-08 13:19:01 +0000 |
commit | ae4967f4cf354a76af18e8d4af91f73af7d648d2 (patch) | |
tree | 1f92dcb35506f6d793250db76cc7b6cc56ff122d /byteback-snapshot | |
parent | 54d53f2091a90e7ab75033a74f29ab9c2cb029c7 (diff) |
Normalized line endings
Diffstat (limited to 'byteback-snapshot')
-rwxr-xr-x | byteback-snapshot | 468 |
1 files changed, 234 insertions, 234 deletions
diff --git a/byteback-snapshot b/byteback-snapshot index daf368b..4e1be92 100755 --- a/byteback-snapshot +++ b/byteback-snapshot @@ -1,234 +1,234 @@ -#!/usr/bin/ruby -# -# Program to create a snapshot and/or rotate a directory of backup snapshots -# using btrfs subvolume commands. - -require 'trollop' -require 'time' - -def error(message) - STDERR.print "*** #{message}\n" - exit 1 -end - -def verbose(message) - print "#{Time.now}: #{message}\n" if @verbose -end - -# Icky way to find out free disc space on our mount -# -class DiskFree - def initialize(mount) - @mount = mount - end - - def total - all[2] - end - - def used - all[3] - end - - def available - all[4] - end - - def fraction_used - disk_device, disk_fs, disk_total, disk_used, disk_available, *rest = all - disk_used.to_f / disk_available - end - - protected - - def all - disk_device, disk_fs, disk_total, disk_used, disk_available, *rest = - df. - split("\n")[1]. - split(/\s+/). - map { |i| /^[0-9]+$/.match(i) ? i.to_i : i } - end - - def df - `/bin/df -T -P -B1 #{@mount}` - end -end - -# Represent a directory full of backups where "current" is a subvolume -# which is snapshotted to frozen backup directories called e.g. -# "yyyy-mm-ddThh:mm". -# -class BackupDirectory - attr_reader :dir - - def initialize(dir) - @dir = Dir.new(dir) - current - end - - # Return total amount of free space in backup directory (bytes) - # - def free - df = DiskFree.new(@dir.path) - df.total - df.used - end - - # Return an array of Times representing the current list of - # snapshots. - # - def snapshot_times - @dir.entries.map do |entry| - begin - Time.parse(entry) - rescue ArgumentError => error - nil - end - end. - compact. - sort - end - - # What order to remove snapshots in to regain disk space? - # - # Order backups by their closeness to defined backup times, which are - # listed in a set order (i.e. today's backup is more important than yesterday's). - # - BACKUP_IMPORTANCE = [0, 1, 2, 3, 7, 14, 21, 28, 56, 112] - def snapshot_times_by_importance - now = Time.now - snapshot_times_unsorted = snapshot_times - snapshot_times_sorted = [] - while !snapshot_times_unsorted.empty? - BACKUP_IMPORTANCE.each do |days| - target_time = now + (days*86400) - closest = snapshot_times_unsorted.inject(nil) do |best, time| - if best.nil? || (time-target_time).abs < (best-target_time).abs - time - else - best - end - end - break unless closest - snapshot_times_sorted << snapshot_times_unsorted.delete(closest) - end - end - snapshot_times_sorted - end - - # Returns the size of the given snapshot (runs du, may be slow) - # - # Would much prefer to take advantage of this feature: - # http://dustymabe.com/2013/09/22/btrfs-how-big-are-my-snapshots/ - # but it's not currently in Debian/wheezy. - # - def snapshot_size(time=snapshot_times.latest) - `du -s -b #{snapshot_path(time)}`.to_i - end - - def average_snapshot_size(number=10) - snapshot_times.sort[0..number].inject(0) { |time, total| snapshot_size(time) } / number - end - - # Create a new snapshot of 'current' - # - def new_snapshot! - system_no_error("btrfs subvolume snapshot -r #{current.path} #{snapshot_path}") - end - - def delete_snapshot!(time) - system_no_error("btrfs subvolume delete #{snapshot_path(time)}") - end - - def current - Dir.new("#{dir.path}/current") - end - - def snapshot_path(time=Time.now) - "#{dir.path}/#{time.strftime("%Y-%m-%dT%H:%M")}" - end - - protected - - def system_no_error(*args) - raise RuntimeError.new("Command failed: "+args.join(" ")) unless - system(*args) - end -end - -opts = Trollop::options do - - opt :root, "Backups directory (must be a btrfs subvolume)", - :type => :string - - opt :snapshot, "Take a new snapshot" - - opt :prune, "Prune old backups" - :type => :string - - opt :list, "List backups (by 'age' or 'importance')", - :type => :string - - opt :verbose, "Print diagnostics" - -end - -@root = opts[:root] -@verbose = opts[:verbose] -@do_snapshot = opts[:snapshot] -@do_list = opts[:list] -@do_prune = opts[:prune] - -error("Must specify snapshot, prune or list") unless @do_snapshot || @do_prune || @do_list - -error("--root not readable") unless File.directory?(@root) - -@backups = BackupDirectory.new(@root) - -def get_snapshots_by(method) - if method == 'importance' - @backups.snapshot_times_by_importance.reverse # least important first - elsif method == 'age' - @backups.snapshot_times - else - raise ArgumentError.new("Unknown snapshot sort method #{method}") - end -end - -if @do_snapshot - last_snapshot_time = @backups.snapshots.last - error("Last snapshot was less than six hours ago") unless - !last_snapshot_time || - Time.now - @backups.snapshots.last >= 6*60*60 # FIXME: make configurable - - verbose "Making new snapshot" - @backups.new_snapshot! -end - -if @do_list - list = get_snapshots_by(@do_list) - print "Backups in #{@root} by #{@do_list}:\n" - list.each_with_index do |time, index| - print "#{sprintf('% 3d',index)}: #{time}\n" - end -end - -if @do_prune - verbose "Counting last 10 backups" - target_free_space = 1.5 * @backups.average_snapshot_size(10) - verbose "Want to ensure we have #{target_free_space}" - - if @backups.free >= target_free_space - verbose "(we have #{@backups.free} so no action needed)" - else - list = get_snapshots_by(@do_prune) - - while @backups.free < target_free_space && !list.empty? - to_delete = list.pop - verbose "Deleting #{to_delete}" - @backups.delete_snapshot!(to_delete) - verbose "Leaves us with #{@backups.free}" - end - end -end - -verbose "Finished" +#!/usr/bin/ruby
+#
+# Program to create a snapshot and/or rotate a directory of backup snapshots
+# using btrfs subvolume commands.
+
+require 'trollop'
+require 'time'
+
+def error(message)
+ STDERR.print "*** #{message}\n"
+ exit 1
+end
+
+def verbose(message)
+ print "#{Time.now}: #{message}\n" if @verbose
+end
+
+# Icky way to find out free disc space on our mount
+#
+class DiskFree
+ def initialize(mount)
+ @mount = mount
+ end
+
+ def total
+ all[2]
+ end
+
+ def used
+ all[3]
+ end
+
+ def available
+ all[4]
+ end
+
+ def fraction_used
+ disk_device, disk_fs, disk_total, disk_used, disk_available, *rest = all
+ disk_used.to_f / disk_available
+ end
+
+ protected
+
+ def all
+ disk_device, disk_fs, disk_total, disk_used, disk_available, *rest =
+ df.
+ split("\n")[1].
+ split(/\s+/).
+ map { |i| /^[0-9]+$/.match(i) ? i.to_i : i }
+ end
+
+ def df
+ `/bin/df -T -P -B1 #{@mount}`
+ end
+end
+
+# Represent a directory full of backups where "current" is a subvolume
+# which is snapshotted to frozen backup directories called e.g.
+# "yyyy-mm-ddThh:mm".
+#
+class BackupDirectory
+ attr_reader :dir
+
+ def initialize(dir)
+ @dir = Dir.new(dir)
+ current
+ end
+
+ # Return total amount of free space in backup directory (bytes)
+ #
+ def free
+ df = DiskFree.new(@dir.path)
+ df.total - df.used
+ end
+
+ # Return an array of Times representing the current list of
+ # snapshots.
+ #
+ def snapshot_times
+ @dir.entries.map do |entry|
+ begin
+ Time.parse(entry)
+ rescue ArgumentError => error
+ nil
+ end
+ end.
+ compact.
+ sort
+ end
+
+ # What order to remove snapshots in to regain disk space?
+ #
+ # Order backups by their closeness to defined backup times, which are
+ # listed in a set order (i.e. today's backup is more important than yesterday's).
+ #
+ BACKUP_IMPORTANCE = [0, 1, 2, 3, 7, 14, 21, 28, 56, 112]
+ def snapshot_times_by_importance
+ now = Time.now
+ snapshot_times_unsorted = snapshot_times
+ snapshot_times_sorted = []
+ while !snapshot_times_unsorted.empty?
+ BACKUP_IMPORTANCE.each do |days|
+ target_time = now + (days*86400)
+ closest = snapshot_times_unsorted.inject(nil) do |best, time|
+ if best.nil? || (time-target_time).abs < (best-target_time).abs
+ time
+ else
+ best
+ end
+ end
+ break unless closest
+ snapshot_times_sorted << snapshot_times_unsorted.delete(closest)
+ end
+ end
+ snapshot_times_sorted
+ end
+
+ # Returns the size of the given snapshot (runs du, may be slow)
+ #
+ # Would much prefer to take advantage of this feature:
+ # http://dustymabe.com/2013/09/22/btrfs-how-big-are-my-snapshots/
+ # but it's not currently in Debian/wheezy.
+ #
+ def snapshot_size(time=snapshot_times.latest)
+ `du -s -b #{snapshot_path(time)}`.to_i
+ end
+
+ def average_snapshot_size(number=10)
+ snapshot_times.sort[0..number].inject(0) { |time, total| snapshot_size(time) } / number
+ end
+
+ # Create a new snapshot of 'current'
+ #
+ def new_snapshot!
+ system_no_error("btrfs subvolume snapshot -r #{current.path} #{snapshot_path}")
+ end
+
+ def delete_snapshot!(time)
+ system_no_error("btrfs subvolume delete #{snapshot_path(time)}")
+ end
+
+ def current
+ Dir.new("#{dir.path}/current")
+ end
+
+ def snapshot_path(time=Time.now)
+ "#{dir.path}/#{time.strftime("%Y-%m-%dT%H:%M")}"
+ end
+
+ protected
+
+ def system_no_error(*args)
+ raise RuntimeError.new("Command failed: "+args.join(" ")) unless
+ system(*args)
+ end
+end
+
+opts = Trollop::options do
+
+ opt :root, "Backups directory (must be a btrfs subvolume)",
+ :type => :string
+
+ opt :snapshot, "Take a new snapshot"
+
+ opt :prune, "Prune old backups"
+ :type => :string
+
+ opt :list, "List backups (by 'age' or 'importance')",
+ :type => :string
+
+ opt :verbose, "Print diagnostics"
+
+end
+
+@root = opts[:root]
+@verbose = opts[:verbose]
+@do_snapshot = opts[:snapshot]
+@do_list = opts[:list]
+@do_prune = opts[:prune]
+
+error("Must specify snapshot, prune or list") unless @do_snapshot || @do_prune || @do_list
+
+error("--root not readable") unless File.directory?(@root)
+
+@backups = BackupDirectory.new(@root)
+
+def get_snapshots_by(method)
+ if method == 'importance'
+ @backups.snapshot_times_by_importance.reverse # least important first
+ elsif method == 'age'
+ @backups.snapshot_times
+ else
+ raise ArgumentError.new("Unknown snapshot sort method #{method}")
+ end
+end
+
+if @do_snapshot
+ last_snapshot_time = @backups.snapshots.last
+ error("Last snapshot was less than six hours ago") unless
+ !last_snapshot_time ||
+ Time.now - @backups.snapshots.last >= 6*60*60 # FIXME: make configurable
+
+ verbose "Making new snapshot"
+ @backups.new_snapshot!
+end
+
+if @do_list
+ list = get_snapshots_by(@do_list)
+ print "Backups in #{@root} by #{@do_list}:\n"
+ list.each_with_index do |time, index|
+ print "#{sprintf('% 3d',index)}: #{time}\n"
+ end
+end
+
+if @do_prune
+ verbose "Counting last 10 backups"
+ target_free_space = 1.5 * @backups.average_snapshot_size(10)
+ verbose "Want to ensure we have #{target_free_space}"
+
+ if @backups.free >= target_free_space
+ verbose "(we have #{@backups.free} so no action needed)"
+ else
+ list = get_snapshots_by(@do_prune)
+
+ while @backups.free < target_free_space && !list.empty?
+ to_delete = list.pop
+ verbose "Deleting #{to_delete}"
+ @backups.delete_snapshot!(to_delete)
+ verbose "Leaves us with #{@backups.free}"
+ end
+ end
+end
+
+verbose "Finished"
|