diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index 4cfa793ab..6eb154ac3 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,548 +1,549 @@ # A module to collect utility functions. require 'English' require 'puppet/external/lock' require 'puppet/error' require 'puppet/util/execution_stub' require 'uri' require 'sync' require 'monitor' require 'tempfile' require 'pathname' module Puppet module Util require 'puppet/util/monkey_patches' require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX @@sync_objects = {}.extend MonitorMixin def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end # Run some code with a specific environment. Resets the environment back to # what it was at the end of the code. def self.withenv(hash) saved = ENV.to_hash hash.each do |name, val| ENV[name.to_s] = val end yield ensure ENV.clear saved.each do |name, val| ENV[name] = val end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| begin dest = File.expand_path(File.join(dir, bin)) rescue ArgumentError => e # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error # was thrown. raise e unless ((dir =~ /~/) && ((ENV['HOME'].nil? || ENV['HOME'] == ""))) # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then # ignore this path element and carry on with our lives. Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.") next end if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. def absolute_path?(path, platform=nil) # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' name = '[^\\\\/]+' regexes = { :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, :posix => %r!^/!, } # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard # library uses that to test what platform it's on. Normally in Puppet we # would use Puppet.features.microsoft_windows?, but this method needs to # be called during the initialization of features so it can't depend on # that. platform ||= File::ALT_SEPARATOR ? :windows : :posix !! (path =~ regexes[platform]) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}" end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_path def safe_posix_fork(stdin=$stdin, stdout=$stdout, stderr=$stderr, &block) child_pid = Kernel.fork do $stdin.reopen(stdin) $stdout.reopen(stdout) $stderr.reopen(stderr) 3.upto(256){|fd| IO::new(fd).close rescue nil} block.call if block end child_pid end module_function :safe_posix_fork # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end newhash end def symbolizehash!(hash) # this is not the most memory-friendly way to accomplish this, but the # code re-use and clarity seems worthwhile. newhash = symbolizehash(hash) hash.clear hash.merge!(newhash) hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark # Because IO#binread is only available in 1.9 def binread(file) File.open(file, 'rb') { |f| f.read } end module_function :binread # utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors # will recognize as links to the line numbers in the trace) def self.pretty_backtrace(backtrace = caller(1)) backtrace.collect do |line| file_path, line_num = line.split(":") file_path = expand_symlinks(File.expand_path(file_path)) file_path + ":" + line_num end .join("\n") end # utility method that takes a path as input, checks each component of the path to see if it is a symlink, and expands # it if it is. returns the expanded path. def self.expand_symlinks(file_path) file_path.split("/").inject do |full_path, next_dir| next_path = full_path + "/" + next_dir if File.symlink?(next_path) then link = File.readlink(next_path) next_path = case link when /^\// then link else File.expand_path(full_path + "/" + link) end end next_path end end # Replace a file, securely. This takes a block, and passes it the file # handle of a file open for writing. Write the replacement content inside # the block and it will safely replace the target file. # # This method will make no changes to the target file until the content is # successfully written and the block returns without raising an error. # # As far as possible the state of the existing file, such as mode, is # preserved. This works hard to avoid loss of any metadata, but will result # in an inode change for the file. # # Arguments: `filename`, `default_mode` # # The filename is the file we are going to replace. # # The default_mode is the mode to use when the target file doesn't already # exist; if the file is present we copy the existing mode/owner/group values # across. def replace_file(file, default_mode, &block) raise Puppet::DevError, "replace_file requires a block" unless block_given? file = Pathname(file) tempfile = Tempfile.new(file.basename.to_s, file.dirname.to_s) file_exists = file.exist? # If the file exists, use its current mode/owner/group. If it doesn't, use # the supplied mode, and default to current user/group. if file_exists if Puppet.features.microsoft_windows? mode = Puppet::Util::Windows::Security.get_mode(file.to_s) uid = Puppet::Util::Windows::Security.get_owner(file.to_s) gid = Puppet::Util::Windows::Security.get_owner(file.to_s) else stat = file.lstat mode = stat.mode uid = stat.uid gid = stat.gid end # We only care about the four lowest-order octets. Higher octets are # filesystem-specific. mode &= 07777 else mode = default_mode uid = Process.euid gid = Process.egid end # Set properties of the temporary file before we write the content, because # Tempfile doesn't promise to be safe from reading by other people, just # that it avoids races around creating the file. if Puppet.features.microsoft_windows? Puppet::Util::Windows::Security.set_mode(mode, tempfile.path) Puppet::Util::Windows::Security.set_owner(uid, tempfile.path) Puppet::Util::Windows::Security.set_group(gid, tempfile.path) else tempfile.chmod(mode) tempfile.chown(uid, gid) end # OK, now allow the caller to write the content of the file. yield tempfile # Now, make sure the data (which includes the mode) is safe on disk. tempfile.flush begin tempfile.fsync rescue NotImplementedError # fsync may not be implemented by Ruby on all platforms, but # there is absolutely no recovery path if we detect that. So, we just # ignore the return code. # # However, don't be fooled: that is accepting that we are running in # an unsafe fashion. If you are porting to a new platform don't stub # that out. end tempfile.close File.rename(tempfile.path, file) # Ideally, we would now fsync the directory as well, but Ruby doesn't # have support for that, and it doesn't matter /that/ much... # Return something true, and possibly useful. file end module_function :replace_file # Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to # exit if the block throws an exception. # # @param [String] message a message to log if the block fails # @param [Integer] code the exit code that the ruby interpreter should return if the block fails # @yield def exit_on_fail(message, code = 1) yield # First, we need to check and see if we are catching a SystemExit error. These will be raised # when we daemonize/fork, and they do not necessarily indicate a failure case. rescue SystemExit => err raise err # Now we need to catch *any* other kind of exception, because we may be calling third-party # code (e.g. webrick), and we have no idea what they might throw. rescue Exception => err Puppet.log_exception(err, "Could not #{message}: #{err}") + Puppet::Util::Log.force_flushqueue() exit(code) end module_function :exit_on_fail ####################################################################################################### # Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution ####################################################################################################### def execpipe(command, failonfail = true, &block) Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe") Puppet::Util::Execution.execpipe(command, failonfail, &block) end module_function :execpipe def execfail(command, exception) Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail") Puppet::Util::Execution.execfail(command, exception) end module_function :execfail def execute(command, arguments = {}) Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute") Puppet::Util::Execution.execute(command, arguments) end module_function :execute end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb index cb664a9d6..e9607cf35 100644 --- a/lib/puppet/util/log.rb +++ b/lib/puppet/util/log.rb @@ -1,276 +1,290 @@ require 'puppet/util/tagging' require 'puppet/util/classgen' # Pass feedback to the user. Log levels are modeled after syslog's, and it is # expected that that will be the most common log destination. Supports # multiple destinations, one of which is a remote server. class Puppet::Util::Log include Puppet::Util extend Puppet::Util::ClassGen include Puppet::Util::Tagging @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @desttypes = {} # Create a new destination type. def self.newdesttype(name, options = {}, &block) dest = genclass( name, :parent => Puppet::Util::Log::Destination, :prefix => "Dest", :block => block, :hash => @desttypes, :attributes => options ) dest.match(dest.name) dest end require 'puppet/util/log/destination' require 'puppet/util/log/destinations' @destinations = {} @queued = [] class << self include Puppet::Util include Puppet::Util::ClassGen attr_reader :desttypes end # Reset log to basics. Basically just flushes and closes files and # undefs other objects. def Log.close(destination) if @destinations.include?(destination) @destinations[destination].flush if @destinations[destination].respond_to?(:flush) @destinations[destination].close if @destinations[destination].respond_to?(:close) @destinations.delete(destination) end end def self.close_all destinations.keys.each { |dest| close(dest) } raise Puppet::DevError.new("Log.close_all failed to close #{@destinations.keys.inspect}") if !@destinations.empty? end # Flush any log destinations that support such operations. def Log.flush @destinations.each { |type, dest| dest.flush if dest.respond_to?(:flush) } end def Log.autoflush=(v) @destinations.each do |type, dest| dest.autoflush = v if dest.respond_to?(:autoflush=) end end # Create a new log message. The primary role of this method is to # avoid creating log messages below the loglevel. def Log.create(hash) raise Puppet::DevError, "Logs require a level" unless hash.include?(:level) raise Puppet::DevError, "Invalid log level #{hash[:level]}" unless @levels.index(hash[:level]) @levels.index(hash[:level]) >= @loglevel ? Puppet::Util::Log.new(hash) : nil end def Log.destinations @destinations end # Yield each valid level in turn def Log.eachlevel @levels.each { |level| yield level } end # Return the current log level. def Log.level @levels[@loglevel] end # Set the current log level. def Log.level=(level) level = level.intern unless level.is_a?(Symbol) raise Puppet::DevError, "Invalid loglevel #{level}" unless @levels.include?(level) @loglevel = @levels.index(level) end def Log.levels @levels.dup end # Create a new log destination. def Log.newdestination(dest) # Each destination can only occur once. if @destinations.find { |name, obj| obj.name == dest } return end name, type = @desttypes.find do |name, klass| klass.match?(dest) end if type.respond_to?(:suitable?) and not type.suitable?(dest) return end raise Puppet::DevError, "Unknown destination type #{dest}" unless type begin if type.instance_method(:initialize).arity == 1 @destinations[dest] = type.new(dest) else @destinations[dest] = type.new end flushqueue @destinations[dest] rescue => detail Puppet.log_exception(detail) # If this was our only destination, then add the console back in. newdestination(:console) if @destinations.empty? and (dest != :console and dest != "console") end end # Route the actual message. FIXME There are lots of things this method # should do, like caching and a bit more. It's worth noting that there's # a potential for a loop here, if the machine somehow gets the destination set as # itself. def Log.newmessage(msg) return if @levels.index(msg.level) < @loglevel queuemessage(msg) if @destinations.length == 0 @destinations.each do |name, dest| threadlock(dest) do dest.handle(msg) end end end def Log.queuemessage(msg) @queued.push(msg) end def Log.flushqueue return unless @destinations.size >= 1 @queued.each do |msg| Log.newmessage(msg) end @queued.clear end + # Flush the logging queue. If there are no destinations available, + # adds in a console logger before flushing the queue. + # This is mainly intended to be used as a last-resort attempt + # to ensure that logging messages are not thrown away before + # the program is about to exit--most likely in a horrific + # error scenario. + # @return nil + def Log.force_flushqueue() + if (@destinations.empty? and !(@queued.empty?)) + newdestination(:console) + end + flushqueue + end + def Log.sendlevel?(level) @levels.index(level) >= @loglevel end # Reopen all of our logs. def Log.reopen Puppet.notice "Reopening log files" types = @destinations.keys @destinations.each { |type, dest| dest.close if dest.respond_to?(:close) } @destinations.clear # We need to make sure we always end up with some kind of destination begin types.each { |type| Log.newdestination(type) } rescue => detail if @destinations.empty? Log.setup_default Puppet.err detail.to_s end end end def self.setup_default Log.newdestination( (Puppet.features.syslog? ? :syslog : (Puppet.features.eventlog? ? :eventlog : Puppet[:puppetdlog]))) end # Is the passed level a valid log level? def self.validlevel?(level) @levels.include?(level) end attr_accessor :time, :remote, :file, :line, :source attr_reader :level, :message def initialize(args) self.level = args[:level] self.message = args[:message] self.source = args[:source] || "Puppet" @time = Time.now if tags = args[:tags] tags.each { |t| self.tag(t) } end [:file, :line].each do |attr| next unless value = args[attr] send(attr.to_s + "=", value) end Log.newmessage(self) end def message=(msg) raise ArgumentError, "Puppet::Util::Log requires a message" unless msg @message = msg.to_s end def level=(level) raise ArgumentError, "Puppet::Util::Log requires a log level" unless level raise ArgumentError, "Puppet::Util::Log requires a symbol or string" unless level.respond_to? "to_sym" @level = level.to_sym raise ArgumentError, "Invalid log level #{@level}" unless self.class.validlevel?(@level) # Tag myself with my log level tag(level) end # If they pass a source in to us, we make sure it is a string, and # we retrieve any tags we can. def source=(source) if source.respond_to?(:source_descriptors) descriptors = source.source_descriptors @source = descriptors[:path] descriptors[:tags].each { |t| tag(t) } [:file, :line].each do |param| next unless descriptors[param] send(param.to_s + "=", descriptors[param]) end else @source = source.to_s end end def to_report "#{time} #{source} (#{level}): #{to_s}" end def to_s message end end # This is for backward compatibility from when we changed the constant to Puppet::Util::Log # because the reports include the constant name. Apparently the alias was created in # March 2007, should could probably be removed soon. Puppet::Log = Puppet::Util::Log