diff options
| author | Luke Kanies <luke@madstop.com> | 2008-05-15 14:30:29 -0500 |
|---|---|---|
| committer | Luke Kanies <luke@madstop.com> | 2008-05-15 14:30:29 -0500 |
| commit | 1ba2bed578debd251d2b9514039082eaa3f136df (patch) | |
| tree | 907098a846dd7215d59ccfec2d470318bb619fa8 /lib/facter/util | |
| parent | be0a8031fbf8e4a2d608ab600c37c1b01dec16a1 (diff) | |
Moving all of the support classes to util/.
This makes it easier for our loader to distinguish between
code that Facter uses and new facts.
Diffstat (limited to 'lib/facter/util')
| -rw-r--r-- | lib/facter/util/collection.rb | 84 | ||||
| -rw-r--r-- | lib/facter/util/confine.rb | 41 | ||||
| -rw-r--r-- | lib/facter/util/fact.rb | 102 | ||||
| -rw-r--r-- | lib/facter/util/loader.rb | 127 | ||||
| -rw-r--r-- | lib/facter/util/resolution.rb | 107 |
5 files changed, 461 insertions, 0 deletions
diff --git a/lib/facter/util/collection.rb b/lib/facter/util/collection.rb new file mode 100644 index 0000000..f76b578 --- /dev/null +++ b/lib/facter/util/collection.rb @@ -0,0 +1,84 @@ +require 'facter' +require 'facter/util/fact' + +# Manage which facts exist and how we access them. Largely just a wrapper +# around a hash of facts. +class Facter::Util::Collection + # Return a fact object by name. If you use this, you still have to call + # 'value' on it to retrieve the actual value. + def [](name) + value(name) + end + + # Add a resolution mechanism for a named fact. This does not distinguish + # between adding a new fact and adding a new way to resolve a fact. + def add(name, options = {}, &block) + name = canonize(name) + + unless fact = @facts[name] + fact = Facter::Util::Fact.new(name, options) + @facts[name] = fact + end + + fact.add(&block) if block + + return fact + end + + include Enumerable + + # Iterate across all of the facts. + def each + @facts.each do |name, fact| + value = fact.value + unless value.nil? + yield name.to_s, value + end + end + end + + # Return a fact by name. + def fact(name) + @facts[canonize(name)] + end + + # Flush all cached values. + def flush + @facts.each { |name, fact| fact.flush } + end + + def initialize + @facts = Hash.new + end + + # Return a list of all of the facts. + def list + return @facts.keys + end + + # Return a hash of all of our facts. + def to_hash + @facts.inject({}) do |h, ary| + value = ary[1].value + if ! value.nil? + # For backwards compatibility, convert the fact name to a string. + h[ary[0].to_s] = value + end + h + end + end + + def value(name) + if fact = @facts[canonize(name)] + fact.value + end + end + + private + + # Provide a consistent means of getting the exact same fact name + # every time. + def canonize(name) + name.to_s.downcase.to_sym + end +end diff --git a/lib/facter/util/confine.rb b/lib/facter/util/confine.rb new file mode 100644 index 0000000..a430bbe --- /dev/null +++ b/lib/facter/util/confine.rb @@ -0,0 +1,41 @@ +# A restricting tag for fact resolution mechanisms. The tag must be true +# for the resolution mechanism to be suitable. +class Facter::Util::Confine + attr_accessor :fact, :values + + # Add the restriction. Requires the fact name, an operator, and the value + # we're comparing to. + def initialize(fact, *values) + raise ArgumentError, "The fact name must be provided" unless fact + raise ArgumentError, "One or more values must be provided" if values.empty? + fact = fact.to_s if fact.is_a? Symbol + @fact = fact + @values = values.collect do |value| + if value.is_a? String + value + else + value.to_s + end + end + end + + def to_s + return "'%s' '%s'" % [@fact, @values.join(",")] + end + + # Evaluate the fact, returning true or false. + def true? + unless fact = Facter[@fact] + Facter.debug "No fact for %s" % @fact + return false + end + value = fact.value + + return false if value.nil? + + @values.each { |v| + return true if value.downcase == v.downcase + } + return false + end +end diff --git a/lib/facter/util/fact.rb b/lib/facter/util/fact.rb new file mode 100644 index 0000000..c73dfbf --- /dev/null +++ b/lib/facter/util/fact.rb @@ -0,0 +1,102 @@ +require 'facter' +require 'facter/util/resolution' + +class Facter::Util::Fact + attr_accessor :name, :searching, :ldapname + + # Create a new fact, with no resolution mechanisms. + def initialize(name, options = {}) + @name = name.to_s.downcase.intern + + # LAK:NOTE: This is slow for many options, but generally we won't have any and at + # worst we'll have one. If we add more, this should be made more efficient. + options.each do |name, value| + case name + when :ldapname: self.ldapname = value + else + raise ArgumentError, "Invalid fact option '%s'" % name + end + end + + @ldapname ||= @name.to_s + + @resolves = [] + @searching = false + + @value = nil + end + + # Add a new resolution mechanism. This requires a block, which will then + # be evaluated in the context of the new mechanism. + def add(&block) + raise ArgumentError, "You must pass a block to Fact<instance>.add" unless block_given? + + resolve = Facter::Util::Resolution.new(@name) + + resolve.instance_eval(&block) + + @resolves << resolve + + # Immediately sort the resolutions, so that we always have + # a sorted list for looking up values. + # We always want to look them up in the order of number of + # confines, so the most restricted resolution always wins. + @resolves.sort! { |a, b| b.length <=> a.length } + + return resolve + end + + # Flush any cached values. + def flush + @value = nil + @suitable = nil + end + + # Return the value for a given fact. Searches through all of the mechanisms + # and returns either the first value or nil. + def value + unless @value + # make sure we don't get stuck in recursive dependency loops + if @searching + Facter.debug "Caught recursion on %s" % @name + + # return a cached value if we've got it + if @value + return @value + else + return nil + end + end + @value = nil + + if @resolves.length == 0 + Facter.debug "No resolves for %s" % @name + return nil + end + + @searching = true + foundsuits = false + @value = @resolves.inject(nil) { |result, resolve| + next unless resolve.suitable? + foundsuits = true + + tmp = resolve.value + + break tmp unless tmp.nil? or tmp == "" + } + @searching = false + + unless foundsuits + Facter.debug "Found no suitable resolves of %s for %s" % [@resolves.length, @name] + end + end + + if @value.nil? + # nothing + Facter.debug("value for %s is still nil" % @name) + return nil + else + return @value + end + end +end diff --git a/lib/facter/util/loader.rb b/lib/facter/util/loader.rb new file mode 100644 index 0000000..7031172 --- /dev/null +++ b/lib/facter/util/loader.rb @@ -0,0 +1,127 @@ +require 'facter' + +# Load facts on demand. +class Facter::Util::Loader + # Load all resolutions for a single fact. + def load(fact) + # Now load from the search path + shortname = fact.to_s.downcase + load_env(shortname) + + filename = shortname + ".rb" + search_path.each do |dir| + # Load individual files + file = File.join(dir, filename) + + # We have to specify Kernel.load, because we have a load method. + Kernel.load(file) if FileTest.exist?(file) + + # And load any directories matching the name + factdir = File.join(dir, shortname) + load_dir(factdir) if FileTest.directory?(factdir) + end + end + + # Load all facts from all directories. + def load_all + load_env + + search_path.each do |dir| + Dir.entries(dir).each do |file| + path = File.join(dir, file) + if File.directory?(path) + load_dir(path) + elsif file =~ /\.rb$/ + Kernel.load(File.join(dir, file)) + end + end + end + end + + # The list of directories we're going to search through for facts. + def search_path + result = [] + result += $LOAD_PATH.collect { |d| File.join(d, "facter") } + if ENV.include?("FACTERLIB") + result += ENV["FACTERLIB"].split(":") + end + + if defined?(Puppet) + result << Puppet.settings.value(:factdest) + result << File.join(Puppet.settings.value(:libdir), "facter") + end + result + end + + def old_stuff + # See if we can find any other facts in the regular Ruby lib + # paths + $:.each do |dir| + fdir = File.join(dir, "facter") + if FileTest.exists?(fdir) and FileTest.directory?(fdir) + factdirs.push(fdir) + end + end + # Also check anything in 'FACTERLIB' + if ENV['FACTERLIB'] + ENV['FACTERLIB'].split(":").each do |fdir| + factdirs.push(fdir) + end + end + factdirs.each do |fdir| + Dir.glob("#{fdir}/*.rb").each do |file| + # Load here, rather than require, because otherwise + # the facts won't get reloaded if someone calls + # "loadfacts". Really only important in testing, but, + # well, it's important in testing. + begin + load file + rescue => detail + warn "Could not load %s: %s" % + [file, detail] + end + end + end + + + # Now try to get facts from the environment + ENV.each do |name, value| + if name =~ /^facter_?(\w+)$/i + Facter.add($1) do + setcode { value } + end + end + end + end + + private + + def load_dir(dir) + return if dir =~ /\/util$/ + Dir.entries(dir).find_all { |f| f =~ /\.rb$/ }.each do |file| + Kernel.load(File.join(dir, file)) + end + end + + # Load facts from the environment. If no name is provided, + # all will be loaded. + def load_env(fact = nil) + # Load from the environment, if possible + ENV.each do |name, value| + # Skip anything that doesn't match our regex. + next unless name =~ /^facter_?(\w+)$/i + env_name = $1 + + # If a fact name was specified, skip anything that doesn't + # match it. + next if fact and env_name != fact + + Facter.add($1) do + setcode { value } + end + + # Short-cut, if we are only looking for one value. + break if fact + end + end +end diff --git a/lib/facter/util/resolution.rb b/lib/facter/util/resolution.rb new file mode 100644 index 0000000..b6aae77 --- /dev/null +++ b/lib/facter/util/resolution.rb @@ -0,0 +1,107 @@ +# An actual fact resolution mechanism. These are largely just chunks of +# code, with optional confinements restricting the mechanisms to only working on +# specific systems. Note that the confinements are always ANDed, so any +# confinements specified must all be true for the resolution to be +# suitable. +require 'facter/util/confine' + +class Facter::Util::Resolution + attr_accessor :interpreter, :code, :name + + def self.have_which + if @have_which.nil? + %x{which which 2>/dev/null} + @have_which = ($? == 0) + end + @have_which + end + + # Execute a chunk of code. + def self.exec(code, interpreter = "/bin/sh") + raise ArgumentError, "non-sh interpreters are not currently supported" unless interpreter == "/bin/sh" + binary = code.split(/\s+/).shift + + if have_which + path = nil + if binary !~ /^\// + path = %x{which #{binary} 2>/dev/null}.chomp + # we don't have the binary necessary + return nil if path == "" + else + path = binary + end + + return nil unless FileTest.exists?(path) + end + + out = nil + begin + out = %x{#{code}}.chomp + rescue => detail + $stderr.puts detail + return nil + end + if out == "" + return nil + else + return out + end + end + + # Add a new confine to the resolution mechanism. + def confine(confines) + confines.each do |fact, values| + @confines.push Facter::Util::Confine.new(fact, *values) + end + end + + # Create a new resolution mechanism. + def initialize(name) + @name = name + @confines = [] + @value = nil + end + + # Return the number of confines. + def length + @confines.length + end + + # Set our code for returning a value. + def setcode(string = nil, interp = nil, &block) + if string + @code = string + @interpreter = interp || "/bin/sh" + else + unless block_given? + raise ArgumentError, "You must pass either code or a block" + end + @code = block + end + end + + # Is this resolution mechanism suitable on the system in question? + def suitable? + unless defined? @suitable + @suitable = ! @confines.detect { |confine| ! confine.true? } + end + + return @suitable + end + + def to_s + return self.value() + end + + # How we get a value for our resolution mechanism. + def value + if @code.is_a?(Proc) + result = @code.call() + else + result = Facter::Util::Resolution.exec(@code,@interpreter) + end + + return nil if result == "" + return result + end +end |
