summaryrefslogtreecommitdiffstats
path: root/lib/puppet/util
diff options
context:
space:
mode:
authorluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2007-02-07 23:56:59 +0000
committerluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2007-02-07 23:56:59 +0000
commit6d8068eddd0d29ec53f62557eb53f6ebb8e40591 (patch)
tree8c93181b9325fee95d7ecdc6e79341ff6d3604b0 /lib/puppet/util
parent162602323406117444ce4375ead91d8f92f2b31a (diff)
downloadpuppet-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.rb107
-rw-r--r--lib/puppet/util/config.rb959
-rw-r--r--lib/puppet/util/feature.rb76
-rwxr-xr-xlib/puppet/util/filetype.rb300
-rw-r--r--lib/puppet/util/inifile.rb209
-rwxr-xr-xlib/puppet/util/loadedfile.rb71
-rw-r--r--lib/puppet/util/log.rb548
-rw-r--r--lib/puppet/util/logging.rb6
-rw-r--r--lib/puppet/util/metric.rb158
-rw-r--r--lib/puppet/util/storage.rb103
-rw-r--r--lib/puppet/util/suidmanager.rb86
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 => "", :html => "FFA0A0"}
+ GREEN = {:console => "", :html => "00CD00"}
+ YELLOW = {:console => "", :html => "FFFF60"}
+ SLATE = {:console => "", :html => "80A0FF"}
+ ORANGE = {:console => "", :html => "FFA500"}
+ BLUE = {:console => "", :html => "40FFFF"}
+ RESET = {:console => "", :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$