lib/backup/utilities.rb
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