diff options
| author | Steve Kemp <steve@steve.org.uk> | 2015-06-03 15:55:28 +0100 | 
|---|---|---|
| committer | Steve Kemp <steve@steve.org.uk> | 2015-06-03 15:55:28 +0100 | 
| commit | a29380762b93737ae6949121010cd9bceb8196b2 (patch) | |
| tree | 4dfe2931401d0cc73c38b9a90377e31210d6706e /bin/byteback-backup | |
| parent | c4da983bd2a1e35450dcb21bdc7110f5fc0d166a (diff) | |
Relocated the binaries to bin/
Diffstat (limited to 'bin/byteback-backup')
| -rwxr-xr-x | bin/byteback-backup | 264 | 
1 files changed, 264 insertions, 0 deletions
| diff --git a/bin/byteback-backup b/bin/byteback-backup new file mode 100755 index 0000000..ac9165a --- /dev/null +++ b/bin/byteback-backup @@ -0,0 +1,264 @@ +#!/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 +# and safe for most types of hosting customer. +# +# See 'man byteback' for more information. + +require 'resolv' + +$LOAD_PATH.unshift('/usr/lib/byteback') + +require 'trollop' +require 'byteback/util' +require 'byteback/log' +include Byteback::Util +include Byteback::Log + +ME = $PROGRAM_NAME.split('/').last + +opts = Trollop.options do +  banner "#{ME}: Back up this system to a byteback-enabled server\n " + +  opt :destination, 'Backup destination (i.e. user@host:/path)', +      type: :string + +  opt :source, 'Source paths', +      type: :strings, +      default: ['/'] + +  opt :exclude, 'Paths to exclude', +      type: :strings, +      short: 'x' + +  opt :verbose, 'Show debugging messages' + +  opt :retry_number, 'Number of retries on error', +      type: :integer, +      default: 3 + +  opt :retry_delay, 'Number of seconds between retries after an error', +      type: :integer, +      default: 300 + +  opt :ssh_key, 'SSH key filename', +      type: :string, +      default: '/etc/byteback/key', +      short: 'k' + +  opt :help, 'Show this message', +      short: 'h' + +  banner "\nAdditional excludes can be specified using /etc/byteback/rsync_filter, which is an rsync filter file.  See the rsync man page for information on how this works.\n" +end + +lock_out_other_processes('byteback-backup') + +@ssh_key = opts[:ssh_key] +@verbose = opts[:verbose] ? '--verbose' : nil +@sources = opts[:source] if opts[:source] +@excludes = opts[:exclude] if opts[:exclude] +@destination = opts[:destination] +@retry_number = opts[:retry_number] +@retry_delay = opts[:retry_delay] + +# Read the default destination +if File.exist?('/etc/byteback/destination') +  @destination = File.read('/etc/byteback/destination').chomp +end + +# Set the default SSH key +if File.exist?('/etc/byteback/key') +  @ssh_key = '/etc/byteback/key' +end + +# +# Check our destination +# +if @destination =~ /^(?:(.+)@)?([^@:]+):(.+)?$/ +  @destination_user, @destination_host, @destination_path = [Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)] +else +  fatal('Destination must be a remote path, e.g. ssh@host.com:/store/backups') +end + +# +# Validate & normalise source directories +# +@sources = ['/'] if @sources.nil? + +fatal('No sources specified') if @sources.empty? + +@sources = @sources.map do |s| +  s = s.gsub(/\/+/, '/') +  fatal("Can't read directory #{s}") unless File.readable?(s) +  s +end + +# Automatically exclude anything mounted on a non-local filesystem, plus +# various cache and temporary directories common on Bytemark & Debian +# systems +# +if @excludes.nil? + +  PROBABLY_LOCAL = %w( +    btrfs +    ext2 +    ext3 +    ext4 +    reiserfs +    xfs +    nilfs +    jfs +    reiser4 +    zfs +    rootfs +  ) + +  COMMON_JUNK = %w( +    /swap.file +    /tmp +    /var/backups/localhost +    /var/cache/apt/archives +    /var/lib/php5 +    /var/tmp +  ) + +  MOUNT_HEADINGS = %w( spec file vfstype mntops freq passno ) +                   .map(&:to_sym) + +  mounts = File.read('/proc/mounts').split("\n").map do |line| +    Hash[MOUNT_HEADINGS.zip(line.split(' '))] +  end + +  @excludes = + +    mounts +    .select { |m| !PROBABLY_LOCAL.include?(m[:vfstype]) } +    .map { |m| m[:file] } + + +    COMMON_JUNK.select { |f| File.exist?(f) } + +end + +@excludes = @excludes.map do |e| +  e.gsub(/\/+/, '/') +end + +fatal('Must suply --destination or put it into /etc/bytebackup/destination') unless @destination + +# +# Test ssh connection is good before we start +# +fatal("Could not read ssh key #{@ssh_key}") unless File.readable?(@ssh_key) + +def ssh(*ssh_args) +  args = ['ssh', +          '-o', 'BatchMode=yes', +          '-o', 'ConnectionAttempts=5', +          '-o', 'ConnectTimeout=30', +          '-o', 'ServerAliveInterval=60', +          '-o', 'TCPKeepAlive=yes', +          '-x', '-a', +          '-i', @ssh_key, +          '-l', @destination_user, +          @destination_host +         ] + +         ssh_args +         .map { |a| a ? a : '' } + +  log_system(*args) +end + +fatal("Could not connect to #{@destination}") unless +  ssh('byteback-receive', '--ping', @verbose) == 0 + +# +# 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. +  # +  # The timeout is set to 12 hours - rsync can spend a long time at the +  # far end checking over its files at the far end without transfer, so we +  # want to wait as long as possible without jeopardising the timing of the +  # next backup run.  Obviously if rsync itself takes nearly 24 hours for a +  # given filesystem, daily backups (with this tool) are out of the question. +  # +  args = %w( rsync --archive --numeric-ids --delete-delay --inplace --relative --timeout 43200 ) + +  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 + +  # +  # Add in the rsync excludes and sources files, if present. +  # +  if File.exist?('/etc/byteback/excludes') +    args += ['--exclude-from', '/etc/byteback/excludes'] +  end + +  # +  # Add in an rsync_filter if required. +  # +  if File.exist?('/etc/byteback/rsync_filter') +    args += ['--filter', 'merge /etc/byteback/rsync_filter'] +  end + +  # +  # To add extra rsync flags, a file can be used.  This can have flags all on one line, or one per line. +  # +  if File.exist?('/etc/byteback/rsync_flags') +    args += File.readlines('/etc/byteback/rsync_flags').map(&:chomp) +  end + +  args += ['--rsync-path', 'rsync --fake-super'] + +  args += sources +  args << @destination + +  log_system(*args) +end + +# +# We treat exit statuses 0 and 24 as success; 0 is "Success"; 24 is "Partial +# transfer due to vanished source files", which we treat as success otherwise +# on some hosts the backup process never finishes. +# +RSYNC_EXIT_STATUSES_TO_ACCEPT   = [0, 24] +RSYNC_EXIT_STATUSES_TO_RETRY_ON = [10, 11, 20, 21, 22, 23, 30] + +# Run the file copy, retrying if necessary +# +loop do +  status = rsync(*@sources) + +  if RSYNC_EXIT_STATUSES_TO_ACCEPT.any? { |s| s === status } +    break +  elsif RSYNC_EXIT_STATUSES_TO_RETRY_ON.any? { |s| s === status } + +    warn "rsync exited with status #{status}" + +    if @retry_number > 0 +      warn "rsync will retry #{@retry_number} more times, sleeping #{@retry_delay}s" +      @retry_number -= 1 +      sleep @retry_delay +      redo +    else +      fatal('Maximum number of rsync retries reached') +    end +  else +    fatal("Fatal rsync error occurred (#{status})") +  end +end + +info('Backup completed, requesting snapshot') + +# Mark the backup as done on the other end +# +fatal('Backup could not be marked complete') unless +  ssh('byteback-receive', '--complete', @verbose) == 0 + +info('Finished') | 
