diff options
author | Matthew Bloch <matthew@bytemark.co.uk> | 2014-01-08 14:19:28 +0000 |
---|---|---|
committer | Matthew Bloch <matthew@bytemark.co.uk> | 2014-01-08 14:19:28 +0000 |
commit | 804a312b41fb5ca68da687f406abff506f7fe226 (patch) | |
tree | aeb9acb01610ff217704cd47cdb433273921742b | |
parent | e3c9140a21794d069f2528531bda16cd74ef8bb2 (diff) |
Bug fixes after testing.
-rwxr-xr-x | byteback-backup | 9 | ||||
-rwxr-xr-x | byteback-receive | 4 | ||||
-rwxr-xr-x | byteback-snapshot | 468 |
3 files changed, 243 insertions, 238 deletions
diff --git a/byteback-backup b/byteback-backup index ffc4186..7494ad7 100755 --- a/byteback-backup +++ b/byteback-backup @@ -110,9 +110,11 @@ def ssh(*args) "-o", "BatchMode=yes", "-x", "-a", "-i", @ssh_key, - "-l", @destination_user + "-l", @destination_user, + @destination_host ] + - args + args. + map { |a| a ? a : "" } end error("Could not connect to #{@destination}") unless @@ -134,7 +136,7 @@ def rsync(*sources) "--rsync-path", "rsync --fake-super", "--rsh", - ssh.join(" "), + ssh[0..-2].join(" "), "--delete", "--one-file-system", "--relative" @@ -146,6 +148,7 @@ def rsync(*sources) args << @destination print args.map { |a| / /.match(a) ? "\"#{a}\"" : a }.join(" ")+"\n" if @verbose + system(*args) return $?.exitstatus diff --git a/byteback-receive b/byteback-receive index 1d87f6f..05e6a7c 100755 --- a/byteback-receive +++ b/byteback-receive @@ -3,6 +3,8 @@ # Program to receive backups and run rsync in receive mode. Must check that # user as authorised by SSH is allowed to access particular directory. +#STDERR.print ARGV.inspect + "\n" + require 'trollop' def error(message) @@ -16,7 +18,7 @@ if ENV['SSH_ORIGINAL_COMMAND'] ARGV.concat(ENV['SSH_ORIGINAL_COMMAND'].split(" ")) end -#STDERR.print "after ARGV=#{ARGV.inspect}" +#STDERR.print "after ARGV=#{ARGV.inspect}\n" byteback_host = ENV['BYTEBACK_HOST'] error("BYTEBACK_HOST environment not set") unless byteback_host diff --git a/byteback-snapshot b/byteback-snapshot index 4e1be92..c59aa18 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.snapshot_times.last + error("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" + @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" |