summaryrefslogtreecommitdiffstats
path: root/lib/facter/util
diff options
context:
space:
mode:
authorLuke Kanies <luke@madstop.com>2008-05-15 14:30:29 -0500
committerLuke Kanies <luke@madstop.com>2008-05-15 14:30:29 -0500
commit1ba2bed578debd251d2b9514039082eaa3f136df (patch)
tree907098a846dd7215d59ccfec2d470318bb619fa8 /lib/facter/util
parentbe0a8031fbf8e4a2d608ab600c37c1b01dec16a1 (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.rb84
-rw-r--r--lib/facter/util/confine.rb41
-rw-r--r--lib/facter/util/fact.rb102
-rw-r--r--lib/facter/util/loader.rb127
-rw-r--r--lib/facter/util/resolution.rb107
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