diff options
| -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" : "")) + | 
