diff options
author | Patrick J Cherry <patrick@bytemark.co.uk> | 2014-05-09 11:53:50 +0100 |
---|---|---|
committer | Patrick J Cherry <patrick@bytemark.co.uk> | 2014-05-09 11:53:50 +0100 |
commit | eac2dadf65c9e94363647e2f42634d7ac956a5a9 (patch) | |
tree | d398adcf660a8990661dcb84969f3e9d06d57420 /byteback-backup | |
parent | 804a312b41fb5ca68da687f406abff506f7fe226 (diff) |
byteback-backup: Adjusted not to use trollop. Also sources and excludes can be specified in /etc/byteback/sources and excludes.
Diffstat (limited to 'byteback-backup')
-rwxr-xr-x | byteback-backup | 286 |
1 files changed, 160 insertions, 126 deletions
diff --git a/byteback-backup b/byteback-backup index 7494ad7..22fe0fb 100755 --- a/byteback-backup +++ b/byteback-backup @@ -1,182 +1,216 @@ #!/usr/bin/ruby # # Back up this system to a byteback-enabled server (just some command line -# tools and SSH setup). We aim to make sure this backups are easy, complete +# tools and SSH setup). We aim to make sure this backups are easy, complete # and safe for most types of hosting customer. # # See 'man byteback' for more information. -require 'trollop' +require 'getoptlong' require 'resolv' -@sources = ["/"] -@exclude = ["/swap.file", "/var/backups/localhost"] def error(message) - STDERR.print "*** #{message}\n" - exit 1 + STDERR.print "*** #{message}\n" + exit 1 end def verbose(message) - print "#{message}\n" + print "#{message}\n" end -opts = Trollop::options do - - opt :destination, "Backup destination (i.e. user@host:/path)", - :type => :string - - opt :source, "Source paths", - :type => :strings +def help + puts <<EOF +#{$0}: Back up this system to a byteback-enabled server + +Options: + --destination, -d <s>: Backup destination (i.e. user@host:/path) + --source, -s <s>: Source paths (defaults: / and /boot) + --exclude, -x <s>: Exclude paths (defaults: /swap.file, /var/backups/localhost, /var/cache) + --verbose, -v: Show rsync command and progress + --retry-number, -r <n>: Number of retries on error (default: 3) + --retry-delay, -e <n>: Wait number of seconds between retries (default: 1800) + --ssh-key, -k <s>: SSH key for connection (default: /etc/byteback/key) + --help, -h: Show this message +EOF + exit 0 +end - opt :verbose, "Show rsync command and progress" - opt :retry_number, "Number of retries on error", - :type => :integer, - :default => 3 +opts = GetoptLong.new( + [ '--help', '-h', GetoptLong::NO_ARGUMENT ], + [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], + [ '--source', '-s', GetoptLong::REQUIRED_ARGUMENT ], + [ '--destination', '-d', GetoptLong::REQUIRED_ARGUMENT ], + [ '--retry-number', '-r', GetoptLong::REQUIRED_ARGUMENT ], + [ '--retry-delay', '-e', GetoptLong::REQUIRED_ARGUMENT ], + [ '--ssh-key' ,'-k', GetoptLong::REQUIRED_ARGUMENT ] +) + +@ssh_key = nil +@destination = nil +@retry_number = 3 +@retry_delay = 1800 +@sources = nil +@excludes = nil + +# Read the default destination +if File.exists?("/etc/byteback/destination") + @destination = File.read("/etc/byteback/destination").chomp +end - opt :retry_delay, "Wait number of seconds between retries", - :type => :integer, - :default => 1800 +# Set the default SSH key +if File.exists?("/etc/byteback/key") + @ssh_key = "/etc/byteback/key" +end - opt :ssh_key, "SSH key for connection", - :type => :string, - :default => "/etc/byteback/key" +# Read in the default sources +if File.exists?("/etc/byteback/sources") + @sources = File.readlines("/etc/byteback/sources").map{|m| m.chomp} end -@ssh_key = opts[:ssh_key] -@verbose = opts[:verbose] ? "--verbose" : nil -@sources = opts[:source] if opts[:source] -@destination = opts[:destination] -@retry_number = opts[:retry_number] -@retry_delay = opts[:retry_delay] +# Read in the default excludes +if File.exists?("/etc/byteback/excludes") + @excludes = File.readlines("/etc/byteback/excludes").map{|m| m.chomp} +end -if !@destination && File.exists?("/etc/byteback/destination") - @destination = File.read("/etc/byteback/destination").chomp +begin + opts.each do |opt,arg| + case opt + when '--help' + help = true + when '--verbose' + $VERBOSE = true + when "--source" + @sources ||= [] + @sources << arg + when "--exclude" + @excludes ||= [] + @excludes << arg + when "--destination" + @destination = arg + when "--retry-number" + @retry_number = arg.to_i + when "--retry-delay" + @retry_delay = arg.to_i + when "--ssh-key" + @ssh_key = arg + end + end +rescue => err + # any errors, show the help + warn err.to_s + help = true end -error("Must suply --destination or put it into /etc/bytebackup/destination") unless @destination -_dummy, @destination_user, @destination_host, colon, @destination_path = - /^(.*)?(?:@)([^:]+)(:)(.*)?$/.match(@destination).to_a -error("Must be a remote path") unless colon +# +# Check our destination +# +if @destination =~ /^(?:(.+)@)?([^@:]+):(.+)?$/ + @destination_user, @destination_host, @destination_path = [$1, $2, $3] +else + error("Destination must be a remote path, e.g. ssh@host.com:/store/backups") +end +# # Validate & normalise source directories # +@sources = ["/"] if @sources.nil? + error("No sources specified") if @sources.empty? + @sources = @sources.map do |s| - s = s.gsub(/\/+/,"/") - error("Can't read directory #{s}") unless File.readable?(s) - s + s = s.gsub(/\/+/,"/") + error("Can't read directory #{s}") unless File.readable?(s) + s end -# Guess destination for backup # -if !@destination - guesses = [] - hostname = `hostname -f` - Resolv::DNS.open do |dns| - suffix = hostname.split(".")[2..-1].join(".") - ["byteback." + suffix].each do |name| - [Resolv::DNS::Resource::IN::AAAA, - Resolv::DNS::Resource::IN::A].each do |record_type| - next if !guesses.empty? # only care about first result - guesses += dns.getresources(name, record_type) - end - end - end - - if guesses.empty? - error "Couldn't guess at backup host, please specify --destination" - end - - # ick, do I really have to do this to get a string represnetion of - # the IP address? - # - guess = guesses.first.inspect - match = / (.*)>$/.match(guess)[1] - error "Result #{guesses} is not an IP" if !match - @destination = "byteback@#{match[1]}:#{HOSTNAME}/current/" - - verbose "Guessed destination=#{@destination} from #{guess}" +# Validate and normalise excludes +# +if @excludes.nil? + @excludes = ["/swap.file", "/var/backups/localhost"] + @excludes << "/var/cache/apt/archives" if File.directory?("/var/cache/apt/archives") +end + +@excludes = @excludes.map do |e| + e.gsub(/\/+/,"/") end +error("Must suply --destination or put it into /etc/bytebackup/destination") unless @destination + +# # Test ssh connection is good before we start # error("Could not read ssh key #{@ssh_key}") unless File.readable?(@ssh_key) -def ssh(*args) - ["ssh", - "-o", "BatchMode=yes", - "-x", "-a", - "-i", @ssh_key, - "-l", @destination_user, - @destination_host - ] + - args. - map { |a| a ? a : "" } +def ssh(*ssh_args) + args = ["ssh", + "-o", "BatchMode=yes", + "-x", "-a", + "-i", @ssh_key, + "-l", @destination_user, + @destination_host + ] + + ssh_args. + map { |a| a ? a : "" } + + print args.map { |a| / /.match(a) ? "\"#{a}\"" : a }.join(" ")+"\n" if $VERBOSE + + system(*args) end -error("Could not connect to #{@destination}") unless - system(*ssh("byteback-receive", "--ping", @verbose)) +error("Could not connect to #{@destination}") unless + ssh("byteback-receive", "--ping", ($VERBOSE ? "--verbose" : "" )) +# # Call rsync to copy certain sources, returns exit status (see man rsync) # def rsync(*sources) - # Default options include --inplace because we only care about consistency - # at the end of the job, and rsync will do more work for big files without - # it. - # - args = [ - "rsync", - "--archive", - "--numeric-ids", - "--delete", - "--inplace", - "--rsync-path", - "rsync --fake-super", - "--rsh", - ssh[0..-2].join(" "), - "--delete", - "--one-file-system", - "--relative" - ] - - args << "--verbose" if @verbose - args += @exclude.map { |x| ["--exclude", x] }.flatten - args += sources - args << @destination - - print args.map { |a| / /.match(a) ? "\"#{a}\"" : a }.join(" ")+"\n" if @verbose - - system(*args) - - return $?.exitstatus + # Default options include --inplace because we only care about consistency + # at the end of the job, and rsync will do more work for big files without + # it. + # + args = %w(rsync --archive --numeric-ids --delete --inplace --delete --one-file-system --relative) + args += [ "--rsync-path", "rsync --fake-super"] + args += [ "--rsh", "ssh -o BatchMode=yes -x -a -i #{@ssh_key} -l #{@destination_user}"] + args << "--verbose" if $VERBOSE + args += @excludes.map { |x| ["--exclude", x] }.flatten + args += sources + args << @destination + + print args.map { |a| / /.match(a) ? "\"#{a}\"" : a }.join(" ")+"\n" if $VERBOSE + + system(*args) + + return $?.exitstatus end RSYNC_EXIT_STATUSES_TO_RETRY_ON = [10,11,20,21,22,23,24,30] # Run the file copy, retrying if necessary -# +# loop do - status = rsync(*@sources) - - if status === 0 - break - elsif RSYNC_EXIT_STATUSES_TO_RETRY_ON.include?(status) - if @retry_number > 0 - @retry_number -= 1 - sleep @retry_delay - redo - else - error("Maximum number of rsync retries reached") - end - else - error("Fatal rsync error occurred (#{status})") - end + status = rsync(*@sources) + + if status === 0 + break + elsif RSYNC_EXIT_STATUSES_TO_RETRY_ON.include?(status) + if @retry_number > 0 + @retry_number -= 1 + sleep @retry_delay + redo + else + error("Maximum number of rsync retries reached") + end + else + error("Fatal rsync error occurred (#{status})") + end end # Mark the backup as done on the other end # -error("Backup could not be marked complete") unless - system(*ssh("sudo", "byteback-snapshot", "--snapshot", @verbose)) +error("Backup could not be marked complete") unless + ssh("sudo", "byteback-snapshot", "--snapshot", ($VERBOSE ? "--verbose" : "")) + |