#-- # Copyright 2006 Luke Kanies # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # #-- class Facter include Comparable include Enumerable FACTERVERSION = '1.3.7' # = Facter # Functions as a hash of 'facts' you might care about about your # system, such as mac address, IP address, Video card, etc. # returns them dynamically # == Synopsis # # Generally, treat Facter as a hash: # == Example # require 'facter' # puts Facter['operatingsystem'] # @@facts = Hash.new { |hash, key| key = key.to_s.downcase.intern if hash.include?(key) hash[key] else nil end } GREEN = "" RESET = "" @@debug = 0 attr_accessor :name, :searching, :ldapname # module methods # Return the version of the library. def self.version return FACTERVERSION end # Add some debugging def self.debug(string) if string.nil? return end if @@debug != 0 puts GREEN + string + RESET end end # Return a fact object by name. If you use this, you still have to call # 'value' on it to retrieve the actual value. def self.[](name) @@facts[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 self.add(name, &block) fact = nil unless fact = @@facts[name] fact = Facter.new(name) end unless block return fact end fact.add(&block) return fact end class << self include Enumerable # Iterate across all of the facts. def each @@facts.each { |name,fact| if fact.suitable? value = fact.value unless value.nil? yield name.to_s, fact.value end end } end # Allow users to call fact names directly on the Facter class, # either retrieving the value or comparing it to an existing value. def method_missing(name, *args) question = false if name.to_s =~ /\?$/ question = true name = name.to_s.sub(/\?$/,'') end if fact = @@facts[name] if question value = fact.value.downcase args.each do |arg| if arg.to_s.downcase == value return true end end # If we got this far, there was no match. return false else return fact.value end else # Else, fail like a normal missing method. return super end end end # Clear all facts. Mostly used for testing. def self.clear Facter.flush Facter.reset end # Set debugging on or off. def self.debugging(bit) if bit case bit when TrueClass: @@debug = 1 when FalseClass: @@debug = 0 when Fixnum: if bit > 0 @@debug = 1 else @@debug = 0 end when String: if bit.downcase == 'off' @@debug = 0 else @@debug = 1 end else @@debug = 0 end else @@debug = 0 end end # Flush all cached values. def self.flush @@facts.each { |name, fact| fact.flush } end # Return a list of all of the facts. def self.list return @@facts.keys end # Remove them all. def self.reset @@facts.clear end # Return a hash of all of our facts. def self.to_hash(*tags) @@facts.inject({}) do |h, ary| if ary[1].suitable? and (tags.empty? or ary[1].tagged?(*tags)) value = ary[1].value if value # For backwards compatibility, convert the fact name to a string. h[ary[0].to_s] = value end end h end end def self.value(name) if fact = @@facts[name] fact.value else nil end end # Compare one value to another. def <=>(other) return self.value <=> other end # Are we the same? Used for case statements. def ===(value) self.value == value end # Create a new fact, with no resolution mechanisms. def initialize(name) @name = name.to_s.downcase.intern if @@facts.include?(@name) raise ArgumentError, "A fact named %s already exists" % @name else @@facts[@name] = self end @resolves = [] @tags = [] @searching = false @value = nil @ldapname = name 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) unless block_given? raise ArgumentError, "You must pass a block to Fact.add" end resolve = Resolution.new(@name) resolve.fact = self resolve.instance_eval(&block) # skip resolves that will never be suitable for us unless resolve.suitable? return end # insert resolves in order of number of confinements inserted = false @resolves.each_with_index { |r,index| if resolve.length > r.length @resolves.insert(index,resolve) inserted = true break end } unless inserted @resolves.push resolve end end # Return a count of resolution mechanisms available. def count return @resolves.length end # Iterate across all of the fact resolution mechanisms and yield each in # turn. These are inserted in order of most confinements. def each @resolves.each { |r| yield r } end # Flush any cached values. def flush @value = nil @suitable = nil end # Is this fact suitable for finding answers on this host? This is used # to throw away any initially unsuitable mechanisms. def suitable? if @resolves.length == 0 return false end unless defined? @suitable or (defined? @suitable and @suitable.nil?) @suitable = false @resolves.each { |resolve| if resolve.suitable? @suitable = true break end } end return @suitable end # Add one ore more tags def tag(*tags) tags.each do |t| t = t.to_s.downcase.intern @tags << t unless @tags.include?(t) end end # Is our fact tagged with all of the specified tags? def tagged?(*tags) tags.each do |t| unless @tags.include? t.to_s.downcase.intern return false end end return true end def tags @tags.dup 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 foundsuits = false if @resolves.length == 0 Facter.debug "No resolves for %s" % @name return nil end @searching = true @resolves.each { |resolve| #Facter.debug "Searching resolves for %s" % @name if resolve.suitable? @value = resolve.value foundsuits = true end unless @value.nil? or @value == "" break end } @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 # 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. class Resolution attr_accessor :interpreter, :code, :name, :fact def Resolution.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 Resolution.exec(code, interpreter = "/bin/sh") if interpreter == "/bin/sh" binary = code.split(/\s+/).shift if have_which path = nil if binary !~ /^\// path = %x{which #{binary} 2>/dev/null}.chomp if path == "" # we don't have the binary necessary return nil end else path = binary end unless FileTest.exists?(path) # our binary does not exist return nil end end out = nil begin out = %x{#{code}}.chomp rescue => detail $stderr.puts detail return nil end if out == "" return nil else return out end else raise ArgumentError, "non-sh interpreters are not currently supported" end end # Add a new confine to the resolution mechanism. def confine(*args) if args[0].is_a? Hash args[0].each do |fact, values| @confines.push Confine.new(fact,*values) end else fact = args.shift @confines.push Confine.new(fact,*args) 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 # Set the name by which this parameter is known in LDAP. The default # is just the fact name. def setldapname(name) @fact.ldapname = name end # Is this resolution mechanism suitable on the system in question? def suitable? unless defined? @suitable @suitable = true if @confines.length == 0 return true end @confines.each { |confine| unless confine.true? @suitable = false end } end return @suitable end # Set tags on our parent fact. def tag(*values) @fact.tag(*values) end def to_s return self.value() end # How we get a value for our resolution mechanism. def value value = nil if @code.is_a?(Proc) value = @code.call() else unless defined? @interpreter @interpreter = "/bin/sh" end if @code.nil? $stderr.puts "Code for %s is nil" % @name else value = Resolution.exec(@code,@interpreter) end end if value == "" value = nil end return value end end # A restricting tag for fact resolution mechanisms. The tag must be true # for the resolution mechanism to be suitable. class Confine attr_accessor :fact, :op, :value # Add the tag. Requires the fact name, an operator, and the value # we're comparing to. def initialize(fact, *values) 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? fact = nil unless fact = Facter[@fact] Facter.debug "No fact for %s" % @fact return false end value = fact.value if value.nil? return false end retval = @values.find { |v| if value.downcase == v.downcase break true end } if retval retval = true else retval = false end return retval || false end end # Load all of the default facts def self.loadfacts Facter.add(:facterversion) do setcode { FACTERVERSION.to_s } end Facter.add(:rubyversion) do setcode { RUBY_VERSION.to_s } end Facter.add(:puppetversion) do setcode { begin require 'puppet' Puppet::PUPPETVERSION.to_s rescue LoadError nil end } end Facter.add :rubysitedir do setcode do version = RUBY_VERSION.to_s.sub(/\.\d+$/, '') $:.find do |dir| dir =~ /#{File.join("site_ruby", version)}$/ end end end Facter.add(:kernel) do setcode 'uname -s' end Facter.add(:kernelrelease) do setcode 'uname -r' end { "LSBRelease" => "^LSB Version:\t(.*)$", "LSBDistId" => "^Distributor ID:\t(.*)$", "LSBDistRelease" => "^Release:\t(.*)$", "LSBDistDescription" => "^Description:\t(.*)$", "LSBDistCodeName" => "^Codename:\t(.*)$" }.each do |fact, pattern| Facter.add(fact) do setcode do output = Resolution.exec('lsb_release -a 2>/dev/null') if output =~ /#{pattern}/ $1 else nil end end end end Facter.add(:operatingsystem) do # Default to just returning the kernel as the operating system setcode do Facter[:kernel].value end end Facter.add(:operatingsystemrelease) do setcode do Facter[:kernelrelease].value end end Facter.add(:operatingsystem) do confine :kernel => :sunos setcode do "Solaris" end end Facter.add(:operatingsystem) do confine :kernel => :linux setcode do if FileTest.exists?("/etc/debian_version") "Debian" elsif FileTest.exists?("/etc/gentoo-release") "Gentoo" elsif FileTest.exists?("/etc/fedora-release") "Fedora" elsif FileTest.exists?("/etc/redhat-release") txt = File.read("/etc/redhat-release") if txt =~ /centos/i "CentOS" else "RedHat" end elsif FileTest.exists?("/etc/SuSE-release") "SuSE" end end end Facter.add(:hardwaremodel) do setcode 'uname -m' end Facter.add(:architecture) do confine :kernel => :linux setcode do model = Facter.hardwaremodel case model # most linuxen use "x86_64" when 'x86_64': Facter.operatingsystem == "Debian" ? "amd64" : model; when /(i[3456]86|pentium)/: "i386" else model end end end Facter.add(:Cfkey) do setcode do value = nil ["/usr/local/etc/cfkey.pub", "/etc/cfkey.pub", "/var/cfng/keys/localhost.pub", "/var/cfengine/ppkeys/localhost.pub", "/var/lib/cfengine/ppkeys/localhost.pub", "/var/lib/cfengine2/ppkeys/localhost.pub" ].each { |file| if FileTest.file?(file) File.open(file) { |openfile| value = openfile.readlines.reject { |line| line =~ /PUBLIC KEY/ }.collect { |line| line.chomp }.join("") } end if value break end } value end end Facter.add(:domain) do setcode do # First force the hostname to be checked Facter.hostname # Now check to see if it set the domain if defined? $domain and ! $domain.nil? $domain else nil end end end # Look for the DNS domain name command first. Facter.add(:domain) do setcode do domain = Resolution.exec('dnsdomainname') or nil # make sure it's a real domain if domain and domain =~ /.+\..+/ domain else nil end end end Facter.add(:domain) do setcode do domain = Resolution.exec('domainname') or nil # make sure it's a real domain if domain and domain =~ /.+\..+/ domain else nil end end end Facter.add(:domain) do setcode do value = nil if FileTest.exists?("/etc/resolv.conf") File.open("/etc/resolv.conf") { |file| # is the domain set? file.each { |line| if line =~ /domain\s+(\S+)/ value = $1 break end } } ! value and File.open("/etc/resolv.conf") { |file| # is the search path set? file.each { |line| if line =~ /search\s+(\S+)/ value = $1 break end } } value else nil end end end Facter.add(:hostname) do setldapname "cn" setcode do hostname = nil name = Resolution.exec('hostname') or nil if name if name =~ /^([\w-]+)\.(.+)$/ hostname = $1 # the Domain class uses this $domain = $2 else hostname = name end hostname else nil end end end Facter.add(:fqdn) do setcode do host = Facter.value(:hostname) domain = Facter.value(:domain) if host and domain [host, domain].join(".") else nil end end end Facter.add(:ipaddress) do setldapname "iphostnumber" setcode do require 'resolv' begin if hostname = Facter.hostname ip = Resolv.getaddress(hostname) unless ip == "127.0.0.1" ip end else nil end rescue Resolv::ResolvError nil rescue NoMethodError # i think this is a bug in resolv.rb? nil end end end Facter.add(:ipaddress) do setcode do if hostname = Facter.hostname # we need Hostname to exist for this to work host = nil if host = Resolution.exec("host #{hostname}") host = host.chomp.split(/\s/) if defined? list[-1] and list[-1] =~ /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ list[-1] end else nil end else nil end end end ["/etc/ssh","/usr/local/etc/ssh","/etc","/usr/local/etc"].each { |dir| {"SSHDSAKey" => "ssh_host_dsa_key.pub", "SSHRSAKey" => "ssh_host_rsa_key.pub"}.each { |name,file| Facter.add(name) do setcode do value = nil filepath = File.join(dir,file) if FileTest.file?(filepath) begin value = File.open(filepath).read.chomp.split(/\s+/)[1] rescue value = nil end end value end # end of proc end # end of add } # end of hash each } # end of dir each Facter.add(:uniqueid) do setcode 'hostid', '/bin/sh' confine :operatingsystem => %w{Solaris Linux Fedora RedHat CentOS SuSE Debian Gentoo} end Facter.add(:hardwareisa) do setcode 'uname -p', '/bin/sh' confine :operatingsystem => %w{Solaris Linux Fedora RedHat CentOS SuSE Debian Gentoo} end Facter.add(:macaddress) do confine :operatingsystem => %w{Solaris Linux Fedora RedHat CentOS SuSE Debian Gentoo} setcode do ether = [] output = %x{/sbin/ifconfig -a} output.each {|s| ether.push($1) if s =~ /(?:ether|HWaddr) (\w{1,2}:\w{1,2}:\w{1,2}:\w{1,2}:\w{1,2}:\w{1,2})/ } ether[0] end end Facter.add(:macaddress) do confine :kernel => :darwin setcode do ether = nil output = %x{/sbin/ifconfig} output.split(/^\S/).each { |str| if str =~ /10baseT/ # we're wired str =~ /ether (\w\w:\w\w:\w\w:\w\w:\w\w:\w\w)/ ether = $1 end } ether end end Facter.add(:ipaddress) do confine :kernel => :linux setcode do ip = nil output = %x{/sbin/ifconfig} output.split(/^\S/).each { |str| if str =~ /inet addr:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/ tmp = $1 unless tmp =~ /127\./ ip = tmp break end end } ip end end Facter.add(:ipaddress) do confine :kernel => %w{FreeBSD NetBSD OpenBSD solaris} setcode do ip = nil output = %x{/sbin/ifconfig} output.split(/^\S/).each { |str| if str =~ /inet ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/ tmp = $1 unless tmp =~ /127\./ ip = tmp break end end } ip end end Facter.add(:ipaddress) do confine :kernel => %w{darwin} setcode do ip = nil iface = "" output = %x{/usr/sbin/netstat -rn} if output =~ /^default\s*\S*\s*\S*\s*\S*\s*\S*\s*(\S*).*/ iface = $1 else warn "Could not find a default route. Using first non-loopback interface" end if(iface != "") output = %x{/sbin/ifconfig #{iface}} else output = %x{/sbin/ifconfig} end output.split(/^\S/).each { |str| if str =~ /inet ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/ tmp = $1 unless tmp =~ /127\./ ip = tmp break end end } ip end end Facter.add(:hostname) do confine :kernel => :darwin, :kernelrelease => "R7" setcode do %x{/usr/sbin/scutil --get LocalHostName} end end Facter.add(:iphostnumber) do confine :kernel => :darwin, :kernelrelease => "R6" setcode do %x{/usr/sbin/scutil --get LocalHostName} end end Facter.add(:iphostnumber) do confine :kernel => :darwin, :kernelrelease => "R6" setcode do ether = nil output = %x{/sbin/ifconfig} output =~ /HWaddr (\w\w:\w\w:\w\w:\w\w:\w\w:\w\w)/ ether = $1 ether end end Facter.add(:ps) do setcode do 'ps -ef' end end Facter.add(:ps) do confine :operatingsystem => %w{FreeBSD NetBSD OpenBSD Darwin} setcode do 'ps -auxwww' end end Facter.add(:id) do #confine :kernel => %w{Solaris Linux} confine :operatingsystem => %w{Solaris Linux Fedora RedHat CentOS SuSE Debian Gentoo} setcode "whoami" end locals = [] # Now find all our loadable facts factdirs = [] # All the places to check for facts # 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 Facter.loadfacts end # $Id: facter.rb 202 2007-03-21 16:44:44Z luke $