diff options
author | Matthew Bloch <matthew@bytemark.co.uk> | 2014-10-31 02:43:35 +0000 |
---|---|---|
committer | Matthew Bloch <matthew@bytemark.co.uk> | 2014-10-31 02:43:35 +0000 |
commit | 1238c74fa01c009d7f76327f3beb30fee4b9f98f (patch) | |
tree | 2e0e0abde1b35f03ef515acc9ebd57f638af1c25 /lib/byteback | |
parent | f23c6c91e2e2a8eb4154ec545199a5ecbe5136a1 (diff) |
Refactored to improve logging and reduce cut & paste code, bumped Debian version number.
Diffstat (limited to 'lib/byteback')
-rwxr-xr-x | lib/byteback/backup_directory.rb | 103 | ||||
-rwxr-xr-x | lib/byteback/disk_free.rb | 41 | ||||
-rwxr-xr-x | lib/byteback/log.rb | 46 | ||||
-rwxr-xr-x | lib/byteback/util.rb | 68 |
4 files changed, 258 insertions, 0 deletions
diff --git a/lib/byteback/backup_directory.rb b/lib/byteback/backup_directory.rb new file mode 100755 index 0000000..f0ceabc --- /dev/null +++ b/lib/byteback/backup_directory.rb @@ -0,0 +1,103 @@ +module Byteback + # 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 +end diff --git a/lib/byteback/disk_free.rb b/lib/byteback/disk_free.rb new file mode 100755 index 0000000..33952a3 --- /dev/null +++ b/lib/byteback/disk_free.rb @@ -0,0 +1,41 @@ + +module Byteback + # 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 +end diff --git a/lib/byteback/log.rb b/lib/byteback/log.rb new file mode 100755 index 0000000..66b86ef --- /dev/null +++ b/lib/byteback/log.rb @@ -0,0 +1,46 @@ +require 'logger' +require 'syslog' + +module Byteback + # Translates Ruby's Logger calls to similar calls to Syslog + # (implemented in Ruby 2.0 as Syslog::Logger) + # + class SyslogProxy + class << self + def debug(*a); Syslog.log(Syslog::LOG_DEBUG, *a); end + def info(*a); Syslog.log(Syslog::LOG_INFO, *a); end + def warn(*a); Syslog.log(Syslog::LOG_WARN, *a); end + def error(*a); Syslog.log(Syslog::LOG_ERR, *a); end + def fatal(*a); Syslog.log(Syslog::LOG_EMERG, *a); end + end + end + + # Log proxy class that we can include in our scripts for some simple + # logging defaults. + # + module Log + @@me = File.expand_path($0).split("/").last + + @@logger = if STDIN.tty? && !ENV['BYTEBACK_TO_SYSLOG'] + logger = Logger.new(STDERR) + logger.level = Logger::DEBUG + logger.formatter = proc { |severity, datetime, progname, msg| + if severity == "FATAL" || severity == "ERROR" + "*** #{msg}\n" + else + "#{msg}\n" + end + } + logger + else + Syslog.open(@@me) + SyslogProxy + end + + def debug(*a); @@logger.__send__(:debug, *a); end + def info(*a); @@logger.__send__(:info, *a); end + def warn(*a); @@logger.__send__(:warn, *a); end + def fatal(*a); @@logger.__send__(:fatal, *a); exit 1; end + def error(*a); @@logger.__send__(:error, *a); end + end +end diff --git a/lib/byteback/util.rb b/lib/byteback/util.rb new file mode 100755 index 0000000..f9f6c62 --- /dev/null +++ b/lib/byteback/util.rb @@ -0,0 +1,68 @@ +require 'tempfile' + +module Byteback + module Util + @@lockfile = "/var/run/byteback/byteback.lock" + + def remove_lockfile! + begin + File.unlink(@@lockfile) + rescue Errno::ENOENT + end + end + + def claim_lockfile! + # Check the lockfile first + if File.directory?(File.dirname(@@lockfile)) + if File.exists? @@lockfile + # check the lockfile is sane + exist_pid = File.read(@@lockfile).to_i + if exist_pid > 1 and exist_pid < (File.read("/proc/sys/kernel/pid_max").to_i) + begin + Process.getpgid(exist_pid) + # if no exception, process is running, abort + fatal("Process is running (#{exist_pid} from #{@@lockfile})") + rescue Errno::ESRCH + # no process running with that pid, pidfile is stale + remove_lockfile! + end + else + # lockfile isn't sane, remove it and continue + remove_lockfile! + end + end + else + Dir.mkdir(File.dirname(@@lockfile)) + # lockfile didn't exist so just carry on + end + + # Own the pidfile ourselves + File.open(@@lockfile, "w") do |lockfile| + lockfile.puts Process::pid + end + end + + def lock_out_other_processes(name) + @@lockfile = "/var/run/byteback/#{name}.lock" + claim_lockfile! + at_exit { remove_lockfile! } + end + + def log_system(*args) + debug("system: " + args.map { |a| / /.match(a) ? "\"#{a}\"" : a }.join(" ")) + rd, wr = IO.pipe + pid = fork + if pid.nil? # child + rd.close + STDOUT.reopen(wr) + STDERR.reopen(wr) + # any cleanup actually necessary here? + exec(*args) + end + wr.close + rd.each_line { |line| debug(line.chomp) } + pid2, status = Process.waitpid2(pid, 0) + status.exitstatus + end + end +end |