diff options
Diffstat (limited to 'byteback-snapshot')
-rwxr-xr-x | byteback-snapshot | 175 |
1 files changed, 14 insertions, 161 deletions
diff --git a/byteback-snapshot b/byteback-snapshot index 1960085..37834d4 100755 --- a/byteback-snapshot +++ b/byteback-snapshot @@ -3,158 +3,11 @@ # Program to create a snapshot and/or rotate a directory of backup snapshots # using btrfs subvolume commands. +$LOAD_PATH.unshift("/usr/lib/byteback") 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+zzzz". -# -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.last) - `du -s -b #{snapshot_path(time)}`.to_i - end - - def average_snapshot_size(number=10) - snapshot_times.sort[0..number].inject(0) { |total, time| 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%z")}" - end - - protected - - def system_no_error(*args) - args[-1] += " > /dev/null" unless @verbose - raise RuntimeError.new("Command failed: "+args.join(" ")) unless - system(*args) - end -end +require 'byteback' +include Byteback +include Byteback::Log opts = Trollop::options do @@ -179,9 +32,9 @@ end @do_list = opts[:list] @do_prune = opts[:prune] -error("Must specify snapshot, prune or list") unless @do_snapshot || @do_prune || @do_list +fatal("Must specify snapshot, prune or list") unless @do_snapshot || @do_prune || @do_list -error("--root not readable") unless File.directory?(@root) +fatal("--root not readable") unless File.directory?("#{@root}") @backups = BackupDirectory.new(@root) @@ -197,11 +50,11 @@ end if @do_snapshot last_snapshot_time = @backups.snapshot_times.last - error("Last snapshot was less than six hours ago") unless + fatal("Last snapshot was less than six hours ago") unless !last_snapshot_time || Time.now - @backups.snapshot_times.last >= 6*60*60 # FIXME: make configurable - verbose "Making new snapshot" + info "Making new snapshot" @backups.new_snapshot! end @@ -214,22 +67,22 @@ if @do_list end if @do_prune - verbose "Counting last 10 backups" + info "Counting last 10 backups" target_free_space = 1.5 * @backups.average_snapshot_size(10) - verbose "Want to ensure we have #{target_free_space}" + info "Want to ensure we have #{target_free_space}" if @backups.free >= target_free_space - verbose "(we have #{@backups.free} so no action needed)" + info "(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}" + info "Deleting #{to_delete}" @backups.delete_snapshot!(to_delete) - verbose "Leaves us with #{@backups.free}" + info "Leaves us with #{@backups.free}" end end end -verbose "Finished" +info "Finished" |