module Backup module Utilities class Error < Backup::Error; end UTILITIES_NAMES = %w[ tar cat split sudo chown hostname gzip bzip2 mongo mongodump mysqldump innobackupex pg_dump pg_dumpall redis-cli riak-admin gpg openssl rsync ssh sendmail exim send_nsca zabbix_sender ].freeze # @api private class DSL def initialize(utils) @utilities = utils end # Helper methods to allow users to set the path for all utilities in the # .configure block. # # Utility names with dashes (`redis-cli`) will be set using method calls # with an underscore (`redis_cli`). UTILITIES_NAMES.each do |util_name| define_method util_name.tr("-", "_") do |raw_path| path = File.expand_path(raw_path) unless File.executable?(path) raise Utilities::Error, <<-EOS The path given for '#{util_name}' was not found or not executable. Path was: #{path} EOS end @utilities.utilities[util_name] = path end end # Allow users to set the +tar+ distribution if needed. (:gnu or :bsd) def tar_dist(val) Utilities.tar_dist(val) end end class << self ## # Configure the path to system utilities used by Backup. # # Backup will attempt to locate any required system utilities using a # +which+ command call. If a utility can not be found, or you need to # specify an alternate path for a utility, you may do so in your # +config.rb+ file using this method. # # Backup supports both GNU and BSD utilities. # While Backup uses these utilities in a manner compatible with either # version, the +tar+ utility requires some special handling with respect # to +Archive+s. Backup will attempt to detect if the +tar+ command # found (or set here) is GNU or BSD. If for some reason this fails, # this may be set using the +tar_dist+ command shown below. # # Backup::Utilities.configure do # # General Utilites # tar '/path/to/tar' # tar_dist :gnu # or :bsd # cat '/path/to/cat' # split '/path/to/split' # sudo '/path/to/sudo' # chown '/path/to/chown' # hostname '/path/to/hostname' # # # Compressors # gzip '/path/to/gzip' # bzip2 '/path/to/bzip2' # # # Database Utilities # mongo '/path/to/mongo' # mongodump '/path/to/mongodump' # mysqldump '/path/to/mysqldump' # pg_dump '/path/to/pg_dump' # pg_dumpall '/path/to/pg_dumpall' # redis_cli '/path/to/redis-cli' # riak_admin '/path/to/riak-admin' # # # Encryptors # gpg '/path/to/gpg' # openssl '/path/to/openssl' # # # Syncer and Storage # rsync '/path/to/rsync' # ssh '/path/to/ssh' # # # Notifiers # sendmail '/path/to/sendmail' # exim '/path/to/exim' # send_nsca '/path/to/send_nsca' # zabbix_sender '/path/to/zabbix_sender' # end # # These paths may be set using absolute paths, or relative to the # working directory when Backup is run. def configure(&block) DSL.new(self).instance_eval(&block) end def tar_dist(val) # the acceptance tests need to be able to reset this to nil @gnu_tar = val.nil? ? nil : val == :gnu end def gnu_tar? return @gnu_tar unless @gnu_tar.nil? @gnu_tar = !!run("#{utility(:tar)} --version").match(/GNU/) end def utilities @utilities ||= {} end private ## # Returns the full path to the specified utility. # Raises an error if utility can not be found in the system's $PATH def utility(name) name = name.to_s.strip raise Error, "Utility Name Empty" if name.empty? utilities[name] ||= `which '#{name}' 2>/dev/null`.chomp raise Error, <<-EOS if utilities[name].empty? Could not locate '#{name}'. Make sure the specified utility is installed and available in your system's $PATH, or specify it's location in your 'config.rb' file using Backup::Utilities.configure EOS utilities[name].dup end ## # Returns the name of the command name from the given command line. # This is only used to simplify log messages. def command_name(command) parts = [] command = command.split(" ") command.shift while command[0].to_s.include?("=") parts << command.shift.split("/")[-1] if parts[0] == "sudo" until command.empty? part = command.shift if part.include?("/") parts << part.split("/")[-1] break else parts << part end end end parts.join(" ") end ## # Runs a system command # # All messages generated by the command will be logged. # Messages on STDERR will be logged as warnings. # # If the command fails to execute, or returns a non-zero exit status # an Error will be raised. # # Returns STDOUT def run(command) name = command_name(command) Logger.info "Running system utility '#{name}'..." begin out = "" err = "" ps = Open4.popen4(command) do |_pid, stdin, stdout, stderr| stdin.close out = stdout.read.strip err = stderr.read.strip end rescue Exception => e raise Error.wrap(e, "Failed to execute '#{name}'") end unless ps.success? raise Error, <<-EOS '#{name}' failed with exit status: #{ps.exitstatus} STDOUT Messages: #{out.empty? ? "None" : "\n#{out}"} STDERR Messages: #{err.empty? ? "None" : "\n#{err}"} EOS end unless out.empty? Logger.info(out.lines.map { |line| "#{name}:STDOUT: #{line}" }.join) end unless err.empty? Logger.warn(err.lines.map { |line| "#{name}:STDERR: #{line}" }.join) end out end def reset! utilities.clear @gnu_tar = nil end end # Allows these utility methods to be included in other classes, # while allowing them to be stubbed in spec_helper for all specs. module Helpers [:utility, :command_name, :run].each do |name| define_method name do |arg| Utilities.send(name, arg) end private name end private def gnu_tar? Utilities.gnu_tar? end end end end