diff options
| author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-02-07 23:56:59 +0000 |
|---|---|---|
| committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-02-07 23:56:59 +0000 |
| commit | 6d8068eddd0d29ec53f62557eb53f6ebb8e40591 (patch) | |
| tree | 8c93181b9325fee95d7ecdc6e79341ff6d3604b0 /lib/puppet/util | |
| parent | 162602323406117444ce4375ead91d8f92f2b31a (diff) | |
| download | puppet-6d8068eddd0d29ec53f62557eb53f6ebb8e40591.tar.gz puppet-6d8068eddd0d29ec53f62557eb53f6ebb8e40591.tar.xz puppet-6d8068eddd0d29ec53f62557eb53f6ebb8e40591.zip | |
Moving some of the stand-alone classes into the util/ subdirectory, to clean up the top-level namespace a bit. This is a lot of file modifications, but most of them just change class names and file paths.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@2178 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib/puppet/util')
| -rw-r--r-- | lib/puppet/util/autoload.rb | 107 | ||||
| -rw-r--r-- | lib/puppet/util/config.rb | 959 | ||||
| -rw-r--r-- | lib/puppet/util/feature.rb | 76 | ||||
| -rwxr-xr-x | lib/puppet/util/filetype.rb | 300 | ||||
| -rw-r--r-- | lib/puppet/util/inifile.rb | 209 | ||||
| -rwxr-xr-x | lib/puppet/util/loadedfile.rb | 71 | ||||
| -rw-r--r-- | lib/puppet/util/log.rb | 548 | ||||
| -rw-r--r-- | lib/puppet/util/logging.rb | 6 | ||||
| -rw-r--r-- | lib/puppet/util/metric.rb | 158 | ||||
| -rw-r--r-- | lib/puppet/util/storage.rb | 103 | ||||
| -rw-r--r-- | lib/puppet/util/suidmanager.rb | 86 |
11 files changed, 2620 insertions, 3 deletions
diff --git a/lib/puppet/util/autoload.rb b/lib/puppet/util/autoload.rb new file mode 100644 index 000000000..f171254af --- /dev/null +++ b/lib/puppet/util/autoload.rb @@ -0,0 +1,107 @@ +# Autoload paths, either based on names or all at once. +class Puppet::Util::Autoload + include Puppet::Util + + @autoloaders = {} + + attr_accessor :object, :path, :objwarn, :wrap + + + class << self + attr_reader :autoloaders + private :autoloaders + end + Puppet::Util.classproxy self, :autoloaders, "[]", "[]=", :clear + + attr_reader :loaded + private :loaded + + Puppet::Util.proxy self, :loaded, :clear + + def handle_libdir + dir = Puppet[:libdir] + + $: << dir unless $:.include?(dir) + end + + def initialize(obj, path, options = {}) + @path = path.to_s + @object = obj + + self.class[obj] = self + + options.each do |opt, value| + opt = opt.intern if opt.is_a? String + begin + self.send(opt.to_s + "=", value) + rescue NoMethodError + raise ArgumentError, "%s is not a valid option" % opt + end + end + + unless defined? @wrap + @wrap = true + end + + @loaded = {} + end + + def load(name) + name = symbolize(name) + handle_libdir() + + path = File.join(@path, name.to_s + ".rb") + + begin + Kernel.load path, @wrap + @loaded[name] = true + return true + rescue LoadError => detail + # I have no idea what's going on here, but different versions + # of ruby are raising different errors on missing files. + unless detail.to_s =~ /^no such file/i + warn "Could not autoload %s: %s" % [name, detail] + if Puppet[:trace] + puts detail.backtrace + end + end + return false + end + end + + def loaded?(name) + name = symbolize(name) + @loaded[name] + end + + def loadall + handle_libdir() + # Load every instance of everything we can find. + $:.each do |dir| + fdir = File.join(dir, @path) + if FileTest.exists?(fdir) and FileTest.directory?(fdir) + Dir.glob("#{fdir}/*.rb").each do |file| + # Load here, rather than require, so that facts + # can be reloaded. This has some short-comings, I + # believe, but it works as long as real classes + # aren't used. + name = File.basename(file).sub(".rb", '').intern + next if @loaded.include? name + next if $".include?(File.join(@path, name.to_s + ".rb")) + filepath = File.join(@path, name.to_s + ".rb") + begin + Kernel.require filepath + @loaded[name] = true + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + warn "Could not autoload %s: %s" % [file.inspect, detail] + end + end + end + end + end +end + +# $Id$ diff --git a/lib/puppet/util/config.rb b/lib/puppet/util/config.rb new file mode 100644 index 000000000..097d59b9f --- /dev/null +++ b/lib/puppet/util/config.rb @@ -0,0 +1,959 @@ +require 'puppet' +require 'sync' +require 'puppet/transportable' + +# The class for handling configuration files. +class Puppet::Util::Config + include Enumerable + include Puppet::Util + + @@sync = Sync.new + + attr_reader :file, :timer + + # Retrieve a config value + def [](param) + param = symbolize(param) + + # Yay, recursion. + self.reparse() unless param == :filetimeout + + # Cache the returned values; this method was taking close to + # 10% of the compile time. + unless @returned[param] + if @config.include?(param) + if @config[param] + @returned[param] = @config[param].value + end + else + raise ArgumentError, "Undefined configuration parameter '%s'" % param + end + end + + return @returned[param] + end + + # Set a config value. This doesn't set the defaults, it sets the value itself. + def []=(param, value) + @@sync.synchronize do # yay, thread-safe + param = symbolize(param) + unless @config.include?(param) + raise Puppet::Error, + "Unknown configuration parameter %s" % param.inspect + end + unless @order.include?(param) + @order << param + end + @config[param].value = value + if @returned.include?(param) + @returned.delete(param) + end + end + + return value + end + + # A simplified equality operator. + def ==(other) + self.each { |myname, myobj| + unless other[myname] == myobj.value + return false + end + } + + return true + end + + # Generate the list of valid arguments, in a format that GetoptLong can + # understand, and add them to the passed option list. + def addargs(options) + require 'getoptlong' + + # Hackish, but acceptable. Copy the current ARGV for restarting. + Puppet.args = ARGV.dup + + # Add all of the config parameters as valid options. + self.each { |param, obj| + if self.boolean?(param) + options << ["--#{param}", GetoptLong::NO_ARGUMENT] + options << ["--no-#{param}", GetoptLong::NO_ARGUMENT] + else + options << ["--#{param}", GetoptLong::REQUIRED_ARGUMENT] + end + } + + return options + end + + # Turn the config into a transaction and apply it + def apply + trans = self.to_transportable + begin + comp = trans.to_type + trans = comp.evaluate + trans.evaluate + comp.remove + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + Puppet.err "Could not configure myself: %s" % detail + end + end + + # Is our parameter a boolean parameter? + def boolean?(param) + param = symbolize(param) + if @config.include?(param) and @config[param].kind_of? CBoolean + return true + else + return false + end + end + + # Remove all set values, potentially skipping cli values. + def clear(exceptcli = false) + @config.each { |name, obj| + unless exceptcli and obj.setbycli + obj.clear + end + } + @returned.clear + + # Don't clear the 'used' in this case, since it's a config file reparse, + # and we want to retain this info. + unless exceptcli + @used = [] + end + end + + # This is mostly just used for testing. + def clearused + @returned.clear + @used = [] + end + + def each + @order.each { |name| + if @config.include?(name) + yield name, @config[name] + else + raise Puppet::DevError, "%s is in the order but does not exist" % name + end + } + end + + # Iterate over each section name. + def eachsection + yielded = [] + @order.each { |name| + if @config.include?(name) + section = @config[name].section + unless yielded.include? section + yield section + yielded << section + end + else + raise Puppet::DevError, "%s is in the order but does not exist" % name + end + } + end + + # Return an object by name. + def element(param) + param = symbolize(param) + @config[param] + end + + # Handle a command-line argument. + def handlearg(opt, value = nil) + value = mungearg(value) if value + str = opt.sub(/^--/,'') + bool = true + newstr = str.sub(/^no-/, '') + if newstr != str + str = newstr + bool = false + end + if self.valid?(str) + if self.boolean?(str) + self[str] = bool + else + self[str] = value + end + + # Mark that this was set on the cli, so it's not overridden if the + # config gets reread. + @config[str.intern].setbycli = true + else + raise ArgumentError, "Invalid argument %s" % opt + end + end + + def include?(name) + name = name.intern if name.is_a? String + @config.include?(name) + end + + # Create a new config object + def initialize + @order = [] + @config = {} + + @created = [] + @returned = {} + end + + # Make a directory with the appropriate user, group, and mode + def mkdir(default) + obj = nil + unless obj = @config[default] + raise ArgumentError, "Unknown default %s" % default + end + + unless obj.is_a? CFile + raise ArgumentError, "Default %s is not a file" % default + end + + Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do + mode = obj.mode || 0750 + Dir.mkdir(obj.value, mode) + end + end + + # Convert arguments appropriately. + def mungearg(value) + # Handle different data types correctly + return case value + when /^false$/i: false + when /^true$/i: true + when /^\d+$/i: Integer(value) + else + value.gsub(/^["']|["']$/,'').sub(/\s+$/, '') + end + end + + # Return all of the parameters associated with a given section. + def params(section) + section = section.intern if section.is_a? String + @config.find_all { |name, obj| + obj.section == section + }.collect { |name, obj| + name + } + end + + # Parse a configuration file. + def parse(file) + text = nil + + if file.is_a? Puppet::Util::LoadedFile + @file = file + else + @file = Puppet::Util::LoadedFile.new(file) + end + + # Create a timer so that this. + settimer() + + begin + text = File.read(@file.file) + rescue Errno::ENOENT + raise Puppet::Error, "No such file %s" % file + rescue Errno::EACCES + raise Puppet::Error, "Permission denied to file %s" % file + end + + @values = Hash.new { |names, name| + names[name] = {} + } + + # Get rid of the values set by the file, keeping cli values. + self.clear(true) + + section = "puppet" + metas = %w{owner group mode} + values = Hash.new { |hash, key| hash[key] = {} } + text.split(/\n/).each { |line| + case line + when /^\[(\w+)\]$/: section = $1 # Section names + when /^\s*#/: next # Skip comments + when /^\s*$/: next # Skip blanks + when /^\s*(\w+)\s*=\s*(.+)$/: # settings + var = $1.intern + value = mungearg($2) + + # Mmm, "special" attributes + if metas.include?(var.to_s) + unless values.include?(section) + values[section] = {} + end + values[section][var.to_s] = value + + # Do some annoying skullduggery here. This is so that + # the group can be set in the config file. The problem + # is that we're using the word 'group' twice, which is + # confusing. + if var == :group and section == Puppet.name and @config.include?(:group) + @config[:group].value = value + end + next + end + + # Don't override set parameters, since the file is parsed + # after cli arguments are handled. + unless @config.include?(var) and @config[var].setbycli + Puppet.debug "%s: Setting %s to '%s'" % [section, var, value] + self[var] = value + end + @config[var].section = symbolize(section) + + metas.each { |meta| + if values[section][meta] + if @config[var].respond_to?(meta + "=") + @config[var].send(meta + "=", values[section][meta]) + end + end + } + else + raise Puppet::Error, "Could not match line %s" % line + end + } + end + + # Create a new element. The value is passed in because it's used to determine + # what kind of element we're creating, but the value itself might be either + # a default or a value, so we can't actually assign it. + def newelement(hash) + value = hash[:value] || hash[:default] + klass = nil + if hash[:section] + hash[:section] = symbolize(hash[:section]) + end + case value + when true, false, "true", "false": + klass = CBoolean + when /^\$\w+\//, /^\//: + klass = CFile + when String, Integer, Float: # nothing + klass = CElement + else + raise Puppet::Error, "Invalid value '%s' for %s" % [value.inspect, hash[:name]] + end + hash[:parent] = self + element = klass.new(hash) + + @order << element.name + + return element + end + + # Iterate across all of the objects in a given section. + def persection(section) + section = symbolize(section) + self.each { |name, obj| + if obj.section == section + yield obj + end + } + end + + # Reparse our config file, if necessary. + def reparse + if defined? @file and @file.changed? + Puppet.notice "Reparsing %s" % @file.file + @@sync.synchronize do + parse(@file) + end + reuse() + end + end + + def reuse + return unless defined? @used + @@sync.synchronize do # yay, thread-safe + @used.each do |section| + @used.delete(section) + self.use(section) + end + end + end + + # Get a list of objects per section + def sectionlist + sectionlist = [] + self.each { |name, obj| + section = obj.section || "puppet" + sections[section] ||= [] + unless sectionlist.include?(section) + sectionlist << section + end + sections[section] << obj + } + + return sectionlist, sections + end + + # Convert a single section into transportable objects. + def section_to_transportable(section, done = nil, includefiles = true) + done ||= Hash.new { |hash, key| hash[key] = {} } + objects = [] + persection(section) do |obj| + if @config[:mkusers] and @config[:mkusers].value + [:owner, :group].each do |attr| + type = nil + if attr == :owner + type = :user + else + type = attr + end + # If a user and/or group is set, then make sure we're + # managing that object + if obj.respond_to? attr and name = obj.send(attr) + # Skip root or wheel + next if %w{root wheel}.include?(name.to_s) + + # Skip owners and groups we've already done, but tag + # them with our section if necessary + if done[type].include?(name) + tags = done[type][name].tags + unless tags.include?(section) + done[type][name].tags = tags << section + end + elsif newobj = Puppet::Type.type(type)[name] + unless newobj.property(:ensure) + newobj[:ensure] = "present" + end + newobj.tag(section) + if type == :user + newobj[:comment] ||= "%s user" % name + end + else + newobj = Puppet::TransObject.new(name, type.to_s) + newobj.tags = ["puppet", "configuration", section] + newobj[:ensure] = "present" + if type == :user + newobj[:comment] ||= "%s user" % name + end + # Set the group appropriately for the user + if type == :user + newobj[:gid] = Puppet[:group] + end + done[type][name] = newobj + objects << newobj + end + end + end + end + + if obj.respond_to? :to_transportable + next if obj.value =~ /^\/dev/ + transobjects = obj.to_transportable + transobjects = [transobjects] unless transobjects.is_a? Array + transobjects.each do |trans| + # transportable could return nil + next unless trans + unless done[:file].include? trans.name + @created << trans.name + objects << trans + done[:file][trans.name] = trans + end + end + end + end + + bucket = Puppet::TransBucket.new + bucket.type = section + bucket.push(*objects) + bucket.keyword = "class" + + return bucket + end + + # Set a bunch of defaults in a given section. The sections are actually pretty + # pointless, but they help break things up a bit, anyway. + def setdefaults(section, defs) + section = symbolize(section) + defs.each { |name, hash| + if hash.is_a? Array + tmp = hash + hash = {} + [:default, :desc].zip(tmp).each { |p,v| hash[p] = v } + end + name = symbolize(name) + hash[:name] = name + hash[:section] = section + name = hash[:name] + if @config.include?(name) + raise Puppet::Error, "Parameter %s is already defined" % name + end + @config[name] = newelement(hash) + } + end + + # Create a timer to check whether the file should be reparsed. + def settimer + if Puppet[:filetimeout] > 0 + @timer = Puppet.newtimer( + :interval => Puppet[:filetimeout], + :tolerance => 1, + :start? => true + ) do + self.reparse() + end + end + end + + # Convert our list of objects into a component that can be applied. + def to_component + transport = self.to_transportable + return transport.to_type + end + + # Convert our list of objects into a configuration file. + def to_config + str = %{The configuration file for #{Puppet.name}. Note that this file +is likely to have unused configuration parameters in it; any parameter that's +valid anywhere in Puppet can be in any config file, even if it's not used. + +Every section can specify three special parameters: owner, group, and mode. +These parameters affect the required permissions of any files specified after +their specification. Puppet will sometimes use these parameters to check its +own configured state, so they can be used to make Puppet a bit more self-managing. + +Note also that the section names are entirely for human-level organizational +purposes; they don't provide separate namespaces. All parameters are in a +single namespace. + +Generated on #{Time.now}. + +}.gsub(/^/, "# ") + + eachsection do |section| + str += "[#{section}]\n" + persection(section) do |obj| + str += obj.to_config + "\n" + end + end + + return str + end + + # Convert our configuration into a list of transportable objects. + def to_transportable + done = Hash.new { |hash, key| + hash[key] = {} + } + + topbucket = Puppet::TransBucket.new + if defined? @file.file and @file.file + topbucket.name = @file.file + else + topbucket.name = "configtop" + end + topbucket.type = "puppetconfig" + topbucket.top = true + + # Now iterate over each section + eachsection do |section| + topbucket.push section_to_transportable(section, done) + end + + topbucket + end + + # Convert to a parseable manifest + def to_manifest + transport = self.to_transportable + + manifest = transport.to_manifest + "\n" + eachsection { |section| + manifest += "include #{section}\n" + } + + return manifest + end + + # Create the necessary objects to use a section. This is idempotent; + # you can 'use' a section as many times as you want. + def use(*sections) + @@sync.synchronize do # yay, thread-safe + unless defined? @used + @used = [] + end + + runners = sections.collect { |s| + symbolize(s) + }.find_all { |s| + ! @used.include? s + } + return if runners.empty? + + bucket = Puppet::TransBucket.new + bucket.type = "puppetconfig" + bucket.top = true + + # Create a hash to keep track of what we've done so far. + @done = Hash.new { |hash, key| hash[key] = {} } + runners.each do |section| + bucket.push section_to_transportable(section, @done, false) + end + + objects = bucket.to_type + + objects.finalize + tags = nil + if Puppet[:tags] + tags = Puppet[:tags] + Puppet[:tags] = "" + end + trans = objects.evaluate + trans.ignoretags = true + trans.configurator = true + trans.evaluate + if tags + Puppet[:tags] = tags + end + + # Remove is a recursive process, so it's sufficient to just call + # it on the component. + objects.remove(true) + + objects = nil + + runners.each { |s| @used << s } + end + end + + def valid?(param) + param = symbolize(param) + @config.has_key?(param) + end + + # Open a file with the appropriate user, group, and mode + def write(default, *args) + obj = nil + unless obj = @config[default] + raise ArgumentError, "Unknown default %s" % default + end + + unless obj.is_a? CFile + raise ArgumentError, "Default %s is not a file" % default + end + + chown = nil + if Puppet::Util::SUIDManager.uid == 0 + chown = [obj.owner, obj.group] + else + chown = [nil, nil] + end + Puppet::Util::SUIDManager.asuser(*chown) do + mode = obj.mode || 0640 + + if args.empty? + args << "w" + end + + args << mode + + File.open(obj.value, *args) do |file| + yield file + end + end + end + + # Open a non-default file under a default dir with the appropriate user, + # group, and mode + def writesub(default, file, *args) + obj = nil + unless obj = @config[default] + raise ArgumentError, "Unknown default %s" % default + end + + unless obj.is_a? CFile + raise ArgumentError, "Default %s is not a file" % default + end + + chown = nil + if Puppet::Util::SUIDManager.uid == 0 + chown = [obj.owner, obj.group] + else + chown = [nil, nil] + end + + Puppet::Util::SUIDManager.asuser(*chown) do + mode = obj.mode || 0640 + if args.empty? + args << "w" + end + + args << mode + + # Update the umask to make non-executable files + Puppet::Util.withumask(File.umask ^ 0111) do + File.open(file, *args) do |file| + yield file + end + end + end + end + + # The base element type. + class CElement + attr_accessor :name, :section, :default, :parent, :setbycli + attr_reader :desc + + # Unset any set value. + def clear + @value = nil + end + + # Do variable interpolation on the value. + def convert(value) + return value unless value + return value unless value.is_a? String + if value =~ /\$(\w+)/ + parent = $1 + if pval = @parent[parent] + newval = value.to_s.sub(/\$#{parent.to_s}/, pval.to_s) + #return File.join(newval.split("/")) + return newval + else + raise Puppet::DevError, "Could not find value for %s" % parent + end + else + return value + end + end + + def desc=(value) + @desc = value.gsub(/^\s*/, '') + end + + def hook=(block) + meta_def :handle, &block + end + + # Create the new element. Pretty much just sets the name. + def initialize(args = {}) + if args.include?(:parent) + self.parent = args[:parent] + args.delete(:parent) + end + args.each do |param, value| + method = param.to_s + "=" + unless self.respond_to? method + raise ArgumentError, "%s does not accept %s" % [self.class, param] + end + + self.send(method, value) + end + + unless self.desc + raise ArgumentError, "You must provide a description for the %s config option" % self.name + end + end + + def iscreated + @iscreated = true + end + + def iscreated? + if defined? @iscreated + return @iscreated + else + return false + end + end + + def set? + if defined? @value and ! @value.nil? + return true + else + return false + end + end + + # Convert the object to a config statement. + def to_config + str = @desc.gsub(/^/, "# ") + "\n" + + # Add in a statement about the default. + if defined? @default and @default + str += "# The default value is '%s'.\n" % @default + end + + line = "%s = %s" % [@name, self.value] + + # If the value has not been overridden, then print it out commented + # and unconverted, so it's clear that that's the default and how it + # works. + if defined? @value and ! @value.nil? + line = "%s = %s" % [@name, self.value] + else + line = "# %s = %s" % [@name, @default] + end + + str += line + "\n" + + str.gsub(/^/, " ") + end + + # Retrieves the value, or if it's not set, retrieves the default. + def value + retval = nil + if defined? @value and ! @value.nil? + retval = @value + elsif defined? @default + retval = @default + else + return nil + end + + if retval.is_a? String + return convert(retval) + else + return retval + end + end + + # Set the value. + def value=(value) + if respond_to?(:validate) + validate(value) + end + + if respond_to?(:munge) + @value = munge(value) + else + @value = value + end + + if respond_to?(:handle) + handle(@value) + end + end + end + + # A file. + class CFile < CElement + attr_writer :owner, :group + attr_accessor :mode, :create + + def group + if defined? @group + return convert(@group) + else + return nil + end + end + + def owner + if defined? @owner + return convert(@owner) + else + return nil + end + end + + # Set the type appropriately. Yep, a hack. This supports either naming + # the variable 'dir', or adding a slash at the end. + def munge(value) + if value.to_s =~ /\/$/ + @type = :directory + return value.sub(/\/$/, '') + end + return value + end + + # Return the appropriate type. + def type + value = self.value + if @name.to_s =~ /dir/ + return :directory + elsif value.to_s =~ /\/$/ + return :directory + elsif value.is_a? String + return :file + else + return nil + end + end + + # Convert the object to a TransObject instance. + # FIXME There's no dependency system in place right now; if you use + # a section that requires another section, there's nothing done to + # correct that for you, at the moment. + def to_transportable + type = self.type + return nil unless type + path = self.value.split(File::SEPARATOR) + path.shift # remove the leading nil + + objects = [] + obj = Puppet::TransObject.new(self.value, "file") + + # Only create directories, or files that are specifically marked to + # create. + if type == :directory or self.create + obj[:ensure] = type + end + [:mode].each { |var| + if value = self.send(var) + obj[var] = "%o" % value + end + } + + # Only chown or chgrp when root + if Puppet::Util::SUIDManager.uid == 0 + [:group, :owner].each { |var| + if value = self.send(var) + obj[var] = value + end + } + end + + # And set the loglevel to debug for everything + obj[:loglevel] = "debug" + + # We're not actually modifying any files here, and if we allow a + # filebucket to get used here we get into an infinite recursion + # trying to set the filebucket up. + obj[:backup] = false + + if self.section + obj.tags += ["puppet", "configuration", self.section, self.name] + end + objects << obj + objects + end + + # Make sure any provided variables look up to something. + def validate(value) + return true unless value.is_a? String + value.scan(/\$(\w+)/) { |name| + name = $1 + unless @parent.include?(name) + raise ArgumentError, + "Configuration parameter '%s' is undefined" % + name + end + } + end + end + + # A simple boolean. + class CBoolean < CElement + def munge(value) + case value + when true, "true": return true + when false, "false": return false + else + raise Puppet::Error, "Invalid value '%s' for %s" % + [value.inspect, @name] + end + end + end +end + +# $Id$ diff --git a/lib/puppet/util/feature.rb b/lib/puppet/util/feature.rb new file mode 100644 index 000000000..30c38e286 --- /dev/null +++ b/lib/puppet/util/feature.rb @@ -0,0 +1,76 @@ +# Created by Luke Kanies on 2006-11-07. +# Copyright (c) 2006. All rights reserved. + +class Puppet::Util::Feature + attr_reader :path + + # Create a new feature test. You have to pass the feature name, + # and it must be unique. You can either provide a block that + # will get executed immediately to determine if the feature + # is present, or you can pass an option to determine it. + # Currently, the only supported option is 'libs' (must be + # passed as a symbol), which will make sure that each lib loads + # successfully. + def add(name, options = {}) + method = name.to_s + "?" + if self.class.respond_to?(method) + raise ArgumentError, "Feature %s is already defined" % name + end + + result = true + if block_given? + begin + result = yield + rescue => detail + warn "Failed to load feature test for %s: %s" % [name, detail] + result = false + end + end + + if ary = options[:libs] + ary = [ary] unless ary.is_a?(Array) + + ary.each do |lib| + unless lib.is_a?(String) + raise ArgumentError, "Libraries must be passed as strings not %s" % lib.class + end + + begin + require lib + rescue Exception + Puppet.debug "Failed to load library '%s' for feature '%s'" % [lib, name] + result = false + end + end + end + + meta_def(method) do + result + end + end + + # Create a new feature collection. + def initialize(path) + @path = path + @loader = Puppet::Util::Autoload.new(self, @path) + end + + def load + @loader.loadall + end + + def method_missing(method, *args) + return super unless method.to_s =~ /\?$/ + + feature = method.to_s.sub(/\?$/, '') + @loader.load(feature) + + if respond_to?(method) + return self.send(method) + else + return false + end + end +end + +# $Id$ diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb new file mode 100755 index 000000000..8abe0cc00 --- /dev/null +++ b/lib/puppet/util/filetype.rb @@ -0,0 +1,300 @@ +# Basic classes for reading, writing, and emptying files. Not much +# to see here. +class Puppet::Util::FileType + attr_accessor :loaded, :path, :synced + + class << self + attr_accessor :name + include Puppet::Util::ClassGen + end + + # Create a new filetype. + def self.newfiletype(name, &block) + @filetypes ||= {} + + klass = genclass(name, + :block => block, + :prefix => "FileType", + :hash => @filetypes + ) + + # Rename the read and write methods, so that we're sure they + # maintain the stats. + klass.class_eval do + # Rename the read method + define_method(:real_read, instance_method(:read)) + define_method(:read) do + begin + val = real_read() + @loaded = Time.now + if val + return val.gsub(/# HEADER.*\n/,'') + else + return "" + end + rescue Puppet::Error => detail + raise + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + raise Puppet::Error, "%s could not read %s: %s" % + [self.class, @path, detail] + end + end + + # And then the write method + define_method(:real_write, instance_method(:write)) + define_method(:write) do |text| + begin + val = real_write(text) + @synced = Time.now + return val + rescue Puppet::Error => detail + raise + rescue => detail + if Puppet[:debug] + puts detail.backtrace + end + raise Puppet::Error, "%s could not write %s: %s" % + [self.class, @path, detail] + end + end + end + end + + def self.filetype(type) + @filetypes[type] + end + + def initialize(path) + @path = path + end + + # Operate on plain files. + newfiletype(:flat) do + # Read the file. + def read + if File.exists?(@path) + File.read(@path) + else + return nil + end + end + + # Remove the file. + def remove + if File.exists?(@path) + File.unlink(@path) + end + end + + # Overwrite the file. + def write(text) + File.open(@path, "w") { |f| f.print text; f.flush } + end + end + + # Operate on plain files. + newfiletype(:ram) do + @@tabs = {} + + def self.clear + @@tabs.clear + end + + def initialize(path) + super + @@tabs[@path] ||= "" + end + + # Read the file. + def read + Puppet.info "Reading %s from RAM" % @path + @@tabs[@path] + end + + # Remove the file. + def remove + Puppet.info "Removing %s from RAM" % @path + @@tabs[@path] = "" + end + + # Overwrite the file. + def write(text) + Puppet.info "Writing %s to RAM" % @path + @@tabs[@path] = text + end + end + + # Handle Linux-style cron tabs. + newfiletype(:crontab) do + def initialize(user) + self.path = user + end + + def path=(user) + begin + @uid = Puppet::Util.uid(user) + rescue Puppet::Error => detail + raise Puppet::Error, "Could not retrieve user %s" % user + end + + # XXX We have to have the user name, not the uid, because some + # systems *cough*linux*cough* require it that way + @path = user + end + + # Read a specific @path's cron tab. + def read + %x{#{cmdbase()} -l 2>/dev/null} + end + + # Remove a specific @path's cron tab. + def remove + if Facter.value("operatingsystem") == "FreeBSD" + %x{/bin/echo yes | #{cmdbase()} -r 2>/dev/null} + else + %x{#{cmdbase()} -r 2>/dev/null} + end + end + + # Overwrite a specific @path's cron tab; must be passed the @path name + # and the text with which to create the cron tab. + def write(text) + IO.popen("#{cmdbase()} -", "w") { |p| + p.print text + } + end + + private + + # Only add the -u flag when the @path is different. Fedora apparently + # does not think I should be allowed to set the @path to my own user name + def cmdbase + cmd = nil + if @uid == Puppet::Util::SUIDManager.uid + return "crontab" + else + return "crontab -u #{@path}" + end + end + end + + # SunOS has completely different cron commands; this class implements + # its versions. + newfiletype(:suntab) do + # Read a specific @path's cron tab. + def read + Puppet::Util::SUIDManager.asuser(@path) { + %x{crontab -l 2>/dev/null} + } + end + + # Remove a specific @path's cron tab. + def remove + Puppet::Util::SUIDManager.asuser(@path) { + %x{crontab -r 2>/dev/null} + } + end + + # Overwrite a specific @path's cron tab; must be passed the @path name + # and the text with which to create the cron tab. + def write(text) + Puppet::Util::SUIDManager.asuser(@path) { + IO.popen("crontab", "w") { |p| + p.print text + } + } + end + end + + # Treat netinfo tables as a single file, just for simplicity of certain + # types + newfiletype(:netinfo) do + class << self + attr_accessor :format + end + def read + %x{nidump -r /#{@path} /} + end + + # This really only makes sense for cron tabs. + def remove + %x{nireport / /#{@path} name}.split("\n").each do |name| + newname = name.gsub(/\//, '\/').sub(/\s+$/, '') + output = %x{niutil -destroy / '/#{@path}/#{newname}'} + + unless $? == 0 + raise Puppet::Error, "Could not remove %s from %s" % + [name, @path] + end + end + end + + # Convert our table to an array of hashes. This only works for + # handling one table at a time. + def to_array(text = nil) + unless text + text = read + end + + hash = nil + + # Initialize it with the first record + records = [] + text.split("\n").each do |line| + next if line =~ /^[{}]$/ # Skip the wrapping lines + next if line =~ /"name" = \( "#{@path}" \)/ # Skip the table name + next if line =~ /CHILDREN = \(/ # Skip this header + next if line =~ /^ \)/ # and its closer + + # Now we should have nothing but records, wrapped in braces + + case line + when /^\s+\{/: hash = {} + when /^\s+\}/: records << hash + when /\s+"(\w+)" = \( (.+) \)/ + field = $1 + values = $2 + + # Always use an array + hash[field] = [] + + values.split(/, /).each do |value| + if value =~ /^"(.*)"$/ + hash[field] << $1 + else + raise ArgumentError, "Could not match value %s" % value + end + end + else + raise ArgumentError, "Could not match line %s" % line + end + end + + records + end + + def write(text) + text.gsub!(/^#.*\n/,'') + text.gsub!(/^$/,'') + if text == "" or text == "\n" + self.remove + return + end + unless format = self.class.format + raise Puppe::DevError, "You must define the NetInfo format to inport" + end + IO.popen("niload -d #{format} . 1>/dev/null 2>/dev/null", "w") { |p| + p.print text + } + + unless $? == 0 + raise ArgumentError, "Failed to write %s" % @path + end + end + end +end + +# $Id$ diff --git a/lib/puppet/util/inifile.rb b/lib/puppet/util/inifile.rb new file mode 100644 index 000000000..d050e6dd1 --- /dev/null +++ b/lib/puppet/util/inifile.rb @@ -0,0 +1,209 @@ +# Module Puppet::IniConfig +# A generic way to parse .ini style files and manipulate them in memory +# One 'file' can be made up of several physical files. Changes to sections +# on the file are tracked so that only the physical files in which +# something has changed are written back to disk +# Great care is taken to preserve comments and blank lines from the original +# files +# +# The parsing tries to stay close to python's ConfigParser + +require 'puppet/util/filetype' + +module Puppet::Util::IniConfig + # A section in a .ini file + class Section + attr_reader :name, :file + + def initialize(name, file) + @name = name + @file = file + @dirty = false + @entries = [] + end + + # Has this section been modified since it's been read in + # or written back to disk + def dirty? + @dirty + end + + # Should only be used internally + def mark_clean + @dirty = false + end + + # Add a line of text (e.g., a comment) Such lines + # will be written back out in exactly the same + # place they were read in + def add_line(line) + @entries << line + end + + # Set the entry 'key=value'. If no entry with the + # given key exists, one is appended to teh end of the section + def []=(key, value) + entry = find_entry(key) + @dirty = true + if entry.nil? + @entries << [key, value] + else + entry[1] = value + end + end + + # Return the value associated with KEY. If no such entry + # exists, return nil + def [](key) + entry = find_entry(key) + if entry.nil? + return nil + end + return entry[1] + end + + # Format the section as text in the way it should be + # written to file + def format + text = "[#{name}]\n" + @entries.each do |entry| + if entry.is_a?(Array) + key, value = entry + unless value.nil? + text << "#{key}=#{value}\n" + end + else + text << entry + end + end + return text + end + + private + def find_entry(key) + @entries.each do |entry| + if entry.is_a?(Array) && entry[0] == key + return entry + end + end + return nil + end + + end + + # A logical .ini-file that can be spread across several physical + # files. For each physical file, call #read with the filename + class File + def initialize + @files = {} + end + + # Add the contents of the file with name FILE to the + # already existing sections + def read(file) + text = Puppet::Util::FileType.filetype(:flat).new(file).read + if text.nil? + raise "Could not find #{file}" + end + + section = nil # The name of the current section + optname = nil # The name of the last option in section + line = 0 + @files[file] = [] + text.each_line do |l| + line += 1 + if l.strip.empty? || "#;".include?(l[0,1]) || + (l.split(nil, 2)[0].downcase == "rem" && + l[0,1].downcase == "r") + # Whitespace or comment + if section.nil? + @files[file] << l + else + section.add_line(l) + end + elsif " \t\r\n\f".include?(l[0,1]) && section && optname + # continuation line + section[optname] += "\n" + l.chomp + elsif l =~ /^\[([^\]]+)\]/ + # section heading + section.mark_clean unless section.nil? + section = add_section($1, file) + optname = nil + elsif l =~ /^\s*([^\s=]+)\s*\=(.+)$/ + # We allow space around the keys, but not the values + # For the values, we don't know if space is significant + if section.nil? + raise "#{file}:#{line}:Key/value pair outside of a section for key #{$1}" + else + section[$1] = $2 + optname = $1 + end + else + raise "#{file}:#{line}: Can't parse '#{l.chomp}'" + end + end + section.mark_clean unless section.nil? + end + + # Store all modifications made to sections in this file back + # to the physical files. If no modifications were made to + # a physical file, nothing is written + def store + @files.each do |file, lines| + text = "" + dirty = false + lines.each do |l| + if l.is_a?(Section) + dirty ||= l.dirty? + text << l.format + l.mark_clean + else + text << l + end + end + if dirty + Puppet::Util::FileType.filetype(:flat).new(file).write(text) + end + end + end + + # Execute BLOCK, passing each section in this file + # as an argument + def each_section(&block) + @files.each do |file, list| + list.each do |entry| + if entry.is_a?(Section) + yield(entry) + end + end + end + end + + # Return the Section with the given name or nil + def [](name) + name = name.to_s + each_section do |section| + return section if section.name == name + end + return nil + end + + # Return true if the file contains a section with name NAME + def include?(name) + return ! self[name].nil? + end + + # Add a section to be stored in FILE when store is called + def add_section(name, file) + if include?(name) + raise "A section with name #{name} already exists" + end + result = Section.new(name, file) + @files[file] ||= [] + @files[file] << result + return result + end + end +end + +# $Id$ diff --git a/lib/puppet/util/loadedfile.rb b/lib/puppet/util/loadedfile.rb new file mode 100755 index 000000000..362b5df09 --- /dev/null +++ b/lib/puppet/util/loadedfile.rb @@ -0,0 +1,71 @@ +# A simple class that tells us when a file has changed and thus whether we +# should reload it + +require 'puppet' + +module Puppet + class NoSuchFile < Puppet::Error; end + class Util::LoadedFile + attr_reader :file, :statted + + # Provide a hook for setting the timestamp during testing, so we don't + # have to depend on the granularity of the filesystem. + attr_writer :tstamp + + Puppet.config.setdefaults(:puppet, + :filetimeout => [ 15, + "The minimum time to wait between checking for updates in + configuration files." + ] + ) + + # Determine whether the file has changed and thus whether it should + # be reparsed. + def changed? + tmp = stamp() + + # We use a different internal variable than the stamp method + # because it doesn't keep historical state and we do -- that is, + # we will always be comparing two timestamps, whereas + # stamp() just always wants the latest one. + if tmp == @tstamp + return false + else + @tstamp = tmp + return @tstamp + end + end + + # Create the file. Must be passed the file path. + def initialize(file) + @file = file + unless FileTest.exists?(@file) + raise Puppet::NoSuchFile, + "Can not use a non-existent file for parsing" + end + @statted = 0 + @stamp = nil + @tstamp = stamp() + end + + # Retrieve the filestamp, but only refresh it if we're beyond our + # filetimeout + def stamp + if @stamp.nil? or (Time.now.to_i - @statted >= Puppet[:filetimeout]) + @statted = Time.now.to_i + begin + @stamp = File.stat(@file).ctime + rescue Errno::ENOENT + @stamp = Time.now + end + end + return @stamp + end + + def to_s + @file + end + end +end + +# $Id$ diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb new file mode 100644 index 000000000..dd7544dae --- /dev/null +++ b/lib/puppet/util/log.rb @@ -0,0 +1,548 @@ +require 'syslog' + +# 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 + + @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] + @loglevel = 2 + + @desttypes = {} + + # A type of log destination. + class Destination + class << self + attr_accessor :name + end + + def self.initvars + @matches = [] + end + + # Mark the things we're supposed to match. + def self.match(obj) + @matches ||= [] + @matches << obj + end + + # See whether we match a given thing. + def self.match?(obj) + # Convert single-word strings into symbols like :console and :syslog + if obj.is_a? String and obj =~ /^\w+$/ + obj = obj.downcase.intern + end + + @matches.each do |thing| + # Search for direct matches or class matches + return true if thing === obj or thing == obj.class.to_s + end + return false + end + + def name + if defined? @name + return @name + else + return self.class.name + end + end + + # Set how to handle a message. + def self.sethandler(&block) + define_method(:handle, &block) + end + + # Mark how to initialize our object. + def self.setinit(&block) + define_method(:initialize, &block) + end + end + + # Create a new destination type. + def self.newdesttype(name, options = {}, &block) + dest = genclass(name, :parent => Destination, :prefix => "Dest", + :block => block, + :hash => @desttypes, + :attributes => options + ) + dest.match(dest.name) + + return dest + end + + @destinations = {} + + class << self + include Puppet::Util + include Puppet::Util::ClassGen + end + + # Reset all logs to basics. Basically just closes all files and undefs + # all of the other objects. + def Log.close(dest = nil) + if dest + if @destinations.include?(dest) + if @destinations.respond_to?(:close) + @destinations[dest].close + end + @destinations.delete(dest) + end + else + @destinations.each { |name, dest| + if dest.respond_to?(:flush) + dest.flush + end + if dest.respond_to?(:close) + dest.close + end + } + @destinations = {} + end + end + + # Flush any log destinations that support such operations. + def Log.flush + @destinations.each { |type, dest| + if dest.respond_to?(:flush) + dest.flush + 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) + unless hash.include?(:level) + raise Puppet::DevError, "Logs require a level" + end + unless @levels.index(hash[:level]) + raise Puppet::DevError, "Invalid log level %s" % hash[:level] + end + if @levels.index(hash[:level]) >= @loglevel + return Puppet::Util::Log.new(hash) + else + return nil + end + end + + def Log.destinations + return @destinations.keys + end + + # Yield each valid level in turn + def Log.eachlevel + @levels.each { |level| yield level } + end + + # Return the current log level. + def Log.level + return @levels[@loglevel] + end + + # Set the current log level. + def Log.level=(level) + unless level.is_a?(Symbol) + level = level.intern + end + + unless @levels.include?(level) + raise Puppet::DevError, "Invalid loglevel %s" % level + end + + @loglevel = @levels.index(level) + end + + def Log.levels + @levels.dup + end + + newdesttype :syslog do + def close + Syslog.close + end + + def initialize + if Syslog.opened? + Syslog.close + end + name = Puppet.name + name = "puppet-#{name}" unless name =~ /puppet/ + + options = Syslog::LOG_PID | Syslog::LOG_NDELAY + + # XXX This should really be configurable. + str = Puppet[:syslogfacility] + begin + facility = Syslog.const_get("LOG_#{str.upcase}") + rescue NameError + raise Puppet::Error, "Invalid syslog facility %s" % str + end + + @syslog = Syslog.open(name, options, facility) + end + + def handle(msg) + # XXX Syslog currently has a bug that makes it so you + # cannot log a message with a '%' in it. So, we get rid + # of them. + if msg.source == "Puppet" + @syslog.send(msg.level, msg.to_s.gsub("%", '%%')) + else + @syslog.send(msg.level, "(%s) %s" % + [msg.source.to_s.gsub("%", ""), + msg.to_s.gsub("%", '%%') + ] + ) + end + end + end + + newdesttype :file do + match(/^\//) + + def close + if defined? @file + @file.close + @file = nil + end + end + + def flush + if defined? @file + @file.flush + end + end + + def initialize(path) + @name = path + # first make sure the directory exists + # We can't just use 'Config.use' here, because they've + # specified a "special" destination. + unless FileTest.exist?(File.dirname(path)) + Puppet.recmkdir(File.dirname(path)) + Puppet.info "Creating log directory %s" % File.dirname(path) + end + + # create the log file, if it doesn't already exist + file = File.open(path, File::WRONLY|File::CREAT|File::APPEND) + + @file = file + + @autoflush = Puppet[:autoflush] + end + + def handle(msg) + @file.puts("%s %s (%s): %s" % + [msg.time, msg.source, msg.level, msg.to_s]) + + @file.flush if @autoflush + end + end + + newdesttype :console do + + + PINK = {:console => "[0;31m", :html => "FFA0A0"} + GREEN = {:console => "[0;32m", :html => "00CD00"} + YELLOW = {:console => "[0;33m", :html => "FFFF60"} + SLATE = {:console => "[0;34m", :html => "80A0FF"} + ORANGE = {:console => "[0;35m", :html => "FFA500"} + BLUE = {:console => "[0;36m", :html => "40FFFF"} + RESET = {:console => "[0m", :html => ""} + + @@colormap = { + :debug => SLATE, + :info => GREEN, + :notice => PINK, + :warning => ORANGE, + :err => YELLOW, + :alert => BLUE, + :emerg => RESET, + :crit => RESET + } + + def colorize(level, str) + case Puppet[:color] + when false: str + when true, :ansi, "ansi": console_color(level, str) + when :html, "html": html_color(level, str) + end + end + + def console_color(level, str) + @@colormap[level][:console] + str + RESET[:console] + end + + def html_color(level, str) + %{<span style="color: %s">%s</span>} % [@@colormap[level][:html], str] + end + + def initialize + # Flush output immediately. + $stdout.sync = true + end + + def handle(msg) + if msg.source == "Puppet" + puts colorize(msg.level, "%s: %s" % [msg.level, msg.to_s]) + else + puts colorize(msg.level, "%s: %s: %s" % [msg.level, msg.source, msg.to_s]) + end + end + end + + newdesttype :host do + def initialize(host) + Puppet.info "Treating %s as a hostname" % host + args = {} + if host =~ /:(\d+)/ + args[:Port] = $1 + args[:Server] = host.sub(/:\d+/, '') + else + args[:Server] = host + end + + @name = host + + @driver = Puppet::Client::LogClient.new(args) + end + + def handle(msg) + unless msg.is_a?(String) or msg.remote + unless defined? @hostname + @hostname = Facter["hostname"].value + end + unless defined? @domain + @domain = Facter["domain"].value + if @domain + @hostname += "." + @domain + end + end + if msg.source =~ /^\// + msg.source = @hostname + ":" + msg.source + elsif msg.source == "Puppet" + msg.source = @hostname + " " + msg.source + else + msg.source = @hostname + " " + msg.source + end + begin + #puts "would have sent %s" % msg + #puts "would have sent %s" % + # CGI.escape(YAML.dump(msg)) + begin + tmp = CGI.escape(YAML.dump(msg)) + rescue => detail + puts "Could not dump: %s" % detail.to_s + return + end + # Add the hostname to the source + @driver.addlog(tmp) + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + Puppet.err detail + Puppet::Util::Log.close(self) + end + end + end + end + + # Log to a transaction report. + newdesttype :report do + match "Puppet::Transaction::Report" + + def initialize(report) + @report = report + end + + def handle(msg) + # Only add messages from objects, since anything else is + # probably unrelated to this run. + if msg.objectsource? + @report.newlog(msg) + end + end + end + + # Log to an array, just for testing. + newdesttype :array do + match "Array" + + def initialize(array) + @array = array + end + + def handle(msg) + @array << msg + end + 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 + + unless type + raise Puppet::DevError, "Unknown destination type %s" % dest + end + + begin + if type.instance_method(:initialize).arity == 1 + @destinations[dest] = type.new(dest) + else + @destinations[dest] = type.new() + end + rescue => detail + if Puppet[:debug] + puts detail.backtrace + end + + # If this was our only destination, then add the console back in. + if @destinations.empty? and (dest != :console and dest != "console") + newdestination(:console) + end + end + end + + # Route the actual message. FIXME There are lots of things this method + # should do, like caching, storing messages when there are not yet + # destinations, 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) + if @levels.index(msg.level) < @loglevel + return + end + + @destinations.each do |name, dest| + threadlock(dest) do + dest.handle(msg) + end + end + 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| + if dest.respond_to?(:close) + dest.close + end + } + @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.newdestination(:syslog) + Puppet.err detail.to_s + end + end + end + + # Is the passed level a valid log level? + def self.validlevel?(level) + @levels.include?(level) + end + + attr_accessor :level, :message, :time, :tags, :remote + attr_reader :source + + def initialize(args) + unless args.include?(:level) && args.include?(:message) + raise Puppet::DevError, "Puppet::Util::Log called incorrectly" + end + + if args[:level].class == String + @level = args[:level].intern + elsif args[:level].class == Symbol + @level = args[:level] + else + raise Puppet::DevError, + "Level is not a string or symbol: #{args[:level].class}" + end + + # Just return unless we're actually at a level we should send + #return unless self.class.sendlevel?(@level) + + @message = args[:message].to_s + @time = Time.now + # this should include the host name, and probly lots of other + # stuff, at some point + unless self.class.validlevel?(level) + raise Puppet::DevError, "Invalid message level #{level}" + end + + if args.include?(:tags) + @tags = args[:tags] + end + + if args.include?(:source) + self.source = args[:source] + else + @source = "Puppet" + end + + Log.newmessage(self) + end + + # Was the source of this log an object? + def objectsource? + if defined? @objectsource and @objectsource + @objectsource + else + false + end + 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) + # We can't store the actual source, we just store the path. + # We can't just check for whether it responds to :path, because + # plenty of providers respond to that in their normal function. + if source.is_a?(Puppet::Element) and source.respond_to?(:path) + @objectsource = true + @source = source.path + else + @objectsource = false + @source = source.to_s + end + unless defined? @tags and @tags + if source.respond_to?(:tags) + @tags = source.tags + end + end + end + + def tagged?(tag) + @tags.detect { |t| t.to_s == tag.to_s } + end + + def to_report + "%s %s (%s): %s" % [self.time, self.source, self.level, self.to_s] + end + + def to_s + return @message + end +end + +# $Id$ diff --git a/lib/puppet/util/logging.rb b/lib/puppet/util/logging.rb index 1245e24de..298df93ba 100644 --- a/lib/puppet/util/logging.rb +++ b/lib/puppet/util/logging.rb @@ -1,14 +1,14 @@ # A module to make logging a bit easier. -require 'puppet/log' +require 'puppet/util/log' module Puppet::Util::Logging # Create a method for each log level. - Puppet::Log.eachlevel do |level| + Puppet::Util::Log.eachlevel do |level| define_method(level) do |args| if args.is_a?(Array) args = args.join(" ") end - Puppet::Log.create( + Puppet::Util::Log.create( :level => level, :source => self, :message => args diff --git a/lib/puppet/util/metric.rb b/lib/puppet/util/metric.rb new file mode 100644 index 000000000..4c62df315 --- /dev/null +++ b/lib/puppet/util/metric.rb @@ -0,0 +1,158 @@ +# included so we can test object types +require 'puppet' + +# A class for handling metrics. This is currently ridiculously hackish. +class Puppet::Util::Metric + Puppet.config.setdefaults("metrics", + :rrddir => {:default => "$vardir/rrd", + :owner => "$user", + :group => "$group", + :desc => "The directory where RRD database files are stored. + Directories for each reporting host will be created under + this directory." + }, + :rrdgraph => [false, "Whether RRD information should be graphed."], + :rrdinterval => ["$runinterval", "How often RRD should expect data. + This should match how often the hosts report back to the server."] + ) + + # Load the library as a feature, so we can test its presence. + Puppet.features.add :rrd, :libs => 'RRD' + + attr_accessor :type, :name, :value, :label + + attr_writer :basedir + + def basedir + if defined? @basedir + @basedir + else + Puppet[:rrddir] + end + end + + def create(start = nil) + Puppet.config.use(:metrics) + + start ||= Time.now.to_i - 5 + + path = self.path + args = [ + path, + "--start", start, + "--step", Puppet[:rrdinterval] + ] + + values.each { |value| + # the 7200 is the heartbeat -- this means that any data that isn't + # more frequently than every two hours gets thrown away + args.push "DS:%s:GAUGE:7200:U:U" % [value[0]] + } + args.push "RRA:AVERAGE:0.5:1:300" + + begin + RRD.create(*args) + rescue => detail + raise "Could not create RRD file %s: %s" % [path,detail] + end + end + + def dump + puts RRD.info(self.path) + end + + def graph(range = nil) + unless Puppet.features.rrd? + Puppet.warning "RRD library is missing; cannot graph metrics" + return + end + + unit = 60 * 60 * 24 + colorstack = %w{#ff0000 #00ff00 #0000ff #099000 #000990 #f00990 #0f0f0f} + + {:daily => unit, :weekly => unit * 7, :monthly => unit * 30, :yearly => unit * 365}.each do |name, time| + file = self.path.sub(/\.rrd$/, "-%s.png" % name) + args = [file] + + args.push("--title",self.label) + args.push("--imgformat","PNG") + args.push("--interlace") + i = 0 + defs = [] + lines = [] + #p @values.collect { |s,l| s } + values.zip(colorstack).each { |value,color| + next if value.nil? + # this actually uses the data label + defs.push("DEF:%s=%s:%s:AVERAGE" % [value[0],self.path,value[0]]) + lines.push("LINE2:%s%s:%s" % [value[0],color,value[1]]) + } + args << defs + args << lines + args.flatten! + if range + args.push("--start",range[0],"--end",range[1]) + else + args.push("--start", Time.now.to_i - time, "--end", Time.now.to_i) + end + + begin + RRD.graph(*args) + rescue => detail + Puppet.err "Failed to graph %s: %s" % [self.name,detail] + end + end + end + + def initialize(name,label = nil) + @name = name.to_s + + if label + @label = label + else + @label = name.to_s.capitalize.gsub("_", " ") + end + + @values = [] + end + + def path + return File.join(self.basedir, @name + ".rrd") + end + + def newvalue(name,value,label = nil) + unless label + label = name.to_s.capitalize.gsub("_", " ") + end + @values.push [name,label,value] + end + + def store(time) + unless Puppet.features.rrd? + Puppet.warning "RRD library is missing; cannot store metrics" + return + end + unless FileTest.exists?(self.path) + self.create(time - 5) + end + + # XXX this is not terribly error-resistant + args = [time] + values.each { |value| + args.push value[2] + } + arg = args.join(":") + begin + RRD.update(self.path,arg) + #system("rrdtool updatev %s '%s'" % [self.path, arg]) + rescue => detail + raise Puppet::Error, "Failed to update %s: %s" % [self.name,detail] + end + end + + def values + @values.sort { |a, b| a[1] <=> b[1] } + end +end + +# $Id$ diff --git a/lib/puppet/util/storage.rb b/lib/puppet/util/storage.rb new file mode 100644 index 000000000..d76c67433 --- /dev/null +++ b/lib/puppet/util/storage.rb @@ -0,0 +1,103 @@ +require 'yaml' +require 'sync' + +# a class for storing state +class Puppet::Util::Storage + include Singleton + include Puppet::Util + + def initialize + self.class.load + end + + # Return a hash that will be stored to disk. It's worth noting + # here that we use the object's full path, not just the name/type + # combination. At the least, this is useful for those non-isomorphic + # types like exec, but it also means that if an object changes locations + # in the configuration it will lose its cache. + def self.cache(object) + if object.is_a? Puppet::Type + # We used to store things by path, now we store them by ref. + # In oscar(0.20.0) this changed to using the ref. + if @@state.include?(object.path) + @@state[object.ref] = @@state[object.path] + @@state.delete(object.path) + end + name = object.ref + elsif object.is_a?(Symbol) + name = object + else + raise ArgumentError, "You can only cache information for Types and symbols" + end + + return @@state[name] ||= {} + end + + def self.clear + @@state.clear + Storage.init + end + + def self.init + @@state = {} + @@splitchar = "\t" + end + + self.init + + def self.load + Puppet.config.use(:puppet) + + unless File.exists?(Puppet[:statefile]) + unless defined? @@state and ! @@state.nil? + self.init + end + return + end + Puppet::Util.benchmark(:debug, "Loaded state") do + Puppet::Util.readlock(Puppet[:statefile]) do |file| + begin + @@state = YAML.load(file) + rescue => detail + Puppet.err "Checksumfile %s is corrupt (%s); replacing" % + [Puppet[:statefile], detail] + begin + File.rename(Puppet[:statefile], + Puppet[:statefile] + ".bad") + rescue + raise Puppet::Error, + "Could not rename corrupt %s; remove manually" % + Puppet[:statefile] + end + end + end + end + + unless @@state.is_a?(Hash) + Puppet.err "State got corrupted" + self.init + end + + #Puppet.debug "Loaded state is %s" % @@state.inspect + end + + def self.stateinspect + @@state.inspect + end + + def self.store + Puppet.debug "Storing state" + + unless FileTest.exist?(Puppet[:statefile]) + Puppet.info "Creating state file %s" % Puppet[:statefile] + end + + Puppet::Util.benchmark(:debug, "Stored state") do + Puppet::Util.writelock(Puppet[:statefile], 0660) do |file| + file.print YAML.dump(@@state) + end + end + end +end + +# $Id$ diff --git a/lib/puppet/util/suidmanager.rb b/lib/puppet/util/suidmanager.rb new file mode 100644 index 000000000..7a0c3ae2c --- /dev/null +++ b/lib/puppet/util/suidmanager.rb @@ -0,0 +1,86 @@ +require 'facter' +require 'puppet/util/warnings' + +module Puppet::Util::SUIDManager + include Puppet::Util::Warnings + + platform = Facter["kernel"].value + [:uid=, :gid=, :uid, :gid].each do |method| + define_method(method) do |*args| + # NOTE: 'method' is closed here. + newmethod = method + + if platform == "Darwin" and (method == :uid= or method == :gid=) + Puppet::Util::Warnings.warnonce "Cannot change real UID on Darwin" + newmethod = ("e" + method.to_s).intern + end + + return Process.send(newmethod, *args) + end + module_function method + end + + [:euid=, :euid, :egid=, :egid].each do |method| + define_method(method) do |*args| + Process.send(method, *args) + end + module_function method + end + + def asuser(new_euid=nil, new_egid=nil) + # Unless we're root, don't do a damn thing. + unless Process.uid == 0 + return yield + end + old_egid = old_euid = nil + if new_egid + old_egid = self.egid + self.egid = convert_xid(:gid, new_egid) + end + if new_euid + old_euid = self.euid + self.euid = convert_xid(:uid, new_euid) + end + + return yield + ensure + self.euid = old_euid if old_euid + self.egid = old_egid if old_egid + end + + # Make sure the passed argument is a number. + def convert_xid(type, id) + map = {:gid => :group, :uid => :user} + raise ArgumentError, "Invalid id type %s" % type unless map.include?(type) + ret = Puppet::Util.send(type, id) + if ret == nil + raise Puppet::Error, "Invalid %s: %s" % [map[type], id] + end + return ret + end + + module_function :asuser, :convert_xid + + def run_and_capture(command, new_uid=nil, new_gid=nil) + output = nil + + output = Puppet::Util.execute(command, false, new_uid, new_gid) + + [output, $?.dup] + end + + module_function :run_and_capture + + def system(command, new_uid=nil, new_gid=nil) + status = nil + asuser(new_uid, new_gid) do + Kernel.system(command) + status = $?.dup + end + status + end + + module_function :system +end + +# $Id$ |
