diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/puppet/defaults.rb | 2 | ||||
-rw-r--r-- | lib/puppet/indirector.rb | 109 | ||||
-rw-r--r-- | lib/puppet/indirector/facts/yaml.rb | 41 | ||||
-rw-r--r-- | lib/puppet/indirector/node/external.rb | 51 | ||||
-rw-r--r-- | lib/puppet/indirector/node/ldap.rb | 138 | ||||
-rw-r--r-- | lib/puppet/indirector/node/none.rb | 10 | ||||
-rw-r--r-- | lib/puppet/node.rb | 121 | ||||
-rwxr-xr-x | lib/puppet/node/facts.rb | 36 | ||||
-rwxr-xr-x | lib/puppet/util/instance_loader.rb | 2 |
9 files changed, 482 insertions, 28 deletions
diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 78364e786..4b442d094 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -495,7 +495,7 @@ module Puppet ) self.setdefaults(:facts, - :factstore => ["yaml", + :fact_store => ["yaml", "The backend store to use for client facts."] ) diff --git a/lib/puppet/indirector.rb b/lib/puppet/indirector.rb index 0ba538355..b8690d7d5 100644 --- a/lib/puppet/indirector.rb +++ b/lib/puppet/indirector.rb @@ -1,25 +1,55 @@ # Manage indirections to termini. They are organized in terms of indirections - # - e.g., configuration, node, file, certificate -- and each indirection has one -# or more terminus types defined. The indirection must have its preferred terminus -# configured via a 'default' in the form of '<indirection>_terminus'; e.g., -# 'node_terminus = ldap'. +# or more terminus types defined. The indirection is configured via the +# +indirects+ method, which will be called by the class extending itself +# with this module. module Puppet::Indirector + # LAK:FIXME We need to figure out how to handle documentation for the + # different indirection types. + + # A simple class that can function as the base class for indirected types. + class Terminus + require 'puppet/util/docs' + extend Puppet::Util::Docs + end + + # This handles creating the terminus classes. + require 'puppet/util/classgen' + extend Puppet::Util::ClassGen + # This manages reading in all of our files for us and then retrieving # loaded instances. We still have to define the 'newX' method, but this # does all of the rest -- loading, storing, and retrieving by name. require 'puppet/util/instance_loader' - include Puppet::Util::InstanceLoader + extend Puppet::Util::InstanceLoader + + # Register a given indirection type. The classes including this module + # handle creating terminus instances, but the module itself handles + # loading them and managing the classes. + def self.register_indirection(name) + # Set up autoloading of the appropriate termini. + instance_load name, "puppet/indirector/%s" % name + end # Define a new indirection terminus. This method is used by the individual # termini in their separate files. Again, the autoloader takes care of # actually loading these files. - def register_terminus(name, options = {}, &block) - genclass(name, :hash => instance_hash(indirection.name), :attributes => options, :block => block) + # Note that the termini are being registered on the Indirector module, not + # on the classes including the module. This allows a given indirection to + # be used in multiple classes. + def self.register_terminus(indirection, terminus, options = {}, &block) + genclass(terminus, + :prefix => indirection.to_s.capitalize, + :hash => instance_hash(indirection), + :attributes => options, + :block => block, + :parent => options[:parent] || Terminus + ) end # Retrieve a terminus class by indirection and name. - def terminus(name) - loaded_instance(name) + def self.terminus(indirection, terminus) + loaded_instance(indirection, terminus) end # Declare that the including class indirects its methods to @@ -27,12 +57,27 @@ module Puppet::Indirector # default, not the value -- if it's the value, then it gets # evaluated at parse time, which is before the user has had a chance # to override it. - def indirects(indirection, options) - @indirection = indirection - @indirect_terminus = options[:to] + # Options are: + # +:to+: What parameter to use as the name of the indirection terminus. + def indirects(indirection, options = {}) + if defined?(@indirection) + raise ArgumentError, "Already performing an indirection of %s; cannot redirect %s" % [@indirection[:name], indirection] + end + options[:name] = indirection + @indirection = options + # Validate the parameter. This requires that indirecting + # classes require 'puppet/defaults', because of ordering issues, + # but it makes problems much easier to debug. + if param_name = options[:to] + begin + name = Puppet[param_name] + rescue + raise ArgumentError, "Configuration parameter '%s' for indirection '%s' does not exist'" % [param_name, indirection] + end + end # Set up autoloading of the appropriate termini. - autoload "puppet/indirector/%s" % indirection + Puppet::Indirector.register_indirection indirection end # Define methods for each of the HTTP methods. These just point to the @@ -47,30 +92,46 @@ module Puppet::Indirector # those methods. [:get, :post, :put, :delete].each do |method_name| define_method(method_name) do |*args| - begin - terminus.send(method_name, *args) - rescue NoMethodError - raise ArgumentError, "Indirection category %s does not respond to REST method %s" % [indirection, method_name] - end + redirect(method_name, *args) end end private + # Create a new terminus instance. - def make_terminus(indirection) + def make_terminus(name) # Load our terminus class. - unless klass = self.class.terminus(indirection, indirection.default) - raise ArgumentError, "Could not find terminus %s for indirection %s" % [indirection.default, indirection] + unless klass = Puppet::Indirector.terminus(@indirection[:name], name) + raise ArgumentError, "Could not find terminus %s for indirection %s" % [name, indirection] end return klass.new end + # Redirect a given HTTP method. + def redirect(method_name, *args) + begin + terminus.send(method_name, *args) + rescue NoMethodError + raise ArgumentError, "Indirection category %s does not respond to REST method %s" % [indirection, method_name] + end + end + # Return the singleton terminus for this indirection. - def terminus - unless terminus = @termini[indirection.name] - terminus = @termini[indirection.name] = make_terminus(indirection) + def terminus(name = nil) + @termini ||= {} + # Get the name of the terminus. + unless name + unless param_name = @indirection[:to] + raise ArgumentError, "You must specify an indirection terminus for indirection %s" % @indirection[:name] + end + name = Puppet[param_name] + name = name.intern if name.is_a?(String) + end + + unless @termini[name] + @termini[name] = make_terminus(name) end - terminus + @termini[name] end end diff --git a/lib/puppet/indirector/facts/yaml.rb b/lib/puppet/indirector/facts/yaml.rb new file mode 100644 index 000000000..87860012f --- /dev/null +++ b/lib/puppet/indirector/facts/yaml.rb @@ -0,0 +1,41 @@ +Puppet::Indirector.register_terminus :facts, :yaml do + desc "Store client facts as flat files, serialized using YAML." + + # Get a client's facts. + def get(node) + file = path(node) + + return nil unless FileTest.exists?(file) + + begin + values = YAML::load(File.read(file)) + rescue => detail + Puppet.err "Could not load facts for %s: %s" % [node, detail] + end + + Puppet::Node::Facts.new(node, values) + end + + def initialize + Puppet.config.use(:yamlfacts) + end + + # Store the facts to disk. + def put(facts) + File.open(path(facts.name), "w", 0600) do |f| + begin + f.print YAML::dump(facts.values) + rescue => detail + Puppet.err "Could not write facts for %s: %s" % [facts.name, detail] + end + end + nil + end + + private + + # Return the path to a given node's file. + def path(name) + File.join(Puppet[:yamlfactdir], name + ".yaml") + end +end diff --git a/lib/puppet/indirector/node/external.rb b/lib/puppet/indirector/node/external.rb new file mode 100644 index 000000000..70fc2505a --- /dev/null +++ b/lib/puppet/indirector/node/external.rb @@ -0,0 +1,51 @@ +Puppet::Indirector.register_terminus :node, :external, :fact_merge => true do + desc "Call an external program to get node information." + + include Puppet::Util + # Look for external node definitions. + def get(name) + return nil unless Puppet[:external_nodes] != "none" + + # This is a very cheap way to do this, since it will break on + # commands that have spaces in the arguments. But it's good + # enough for most cases. + external_node_command = Puppet[:external_nodes].split + external_node_command << name + begin + output = Puppet::Util.execute(external_node_command) + rescue Puppet::ExecutionFailure => detail + if $?.exitstatus == 1 + return nil + else + Puppet.err "Could not retrieve external node information for %s: %s" % [name, detail] + end + return nil + end + + if output =~ /\A\s*\Z/ # all whitespace + Puppet.debug "Empty response for %s from external node source" % name + return nil + end + + begin + result = YAML.load(output).inject({}) { |hash, data| hash[symbolize(data[0])] = data[1]; hash } + rescue => detail + raise Puppet::Error, "Could not load external node results for %s: %s" % [name, detail] + end + + node = newnode(name) + set = false + [:parameters, :classes].each do |param| + if value = result[param] + node.send(param.to_s + "=", value) + set = true + end + end + + if set + return node + else + return nil + end + end +end diff --git a/lib/puppet/indirector/node/ldap.rb b/lib/puppet/indirector/node/ldap.rb new file mode 100644 index 000000000..75a912568 --- /dev/null +++ b/lib/puppet/indirector/node/ldap.rb @@ -0,0 +1,138 @@ +Puppet::Indirector.register_terminus :node, :ldap, :fact_merge => true do + desc "Search in LDAP for node configuration information." + + # Look for our node in ldap. + def get(node) + unless ary = ldapsearch(node) + return nil + end + parent, classes, parameters = ary + + while parent + parent, tmpclasses, tmpparams = ldapsearch(parent) + classes += tmpclasses if tmpclasses + tmpparams.each do |param, value| + # Specifically test for whether it's set, so false values are handled + # correctly. + parameters[param] = value unless parameters.include?(param) + end + end + + return newnode(node, :classes => classes, :source => "ldap", :parameters => parameters) + end + + # Find the ldap node, return the class list and parent node specially, + # and everything else in a parameter hash. + def ldapsearch(node) + filter = Puppet[:ldapstring] + classattrs = Puppet[:ldapclassattrs].split("\s*,\s*") + if Puppet[:ldapattrs] == "all" + # A nil value here causes all attributes to be returned. + search_attrs = nil + else + search_attrs = classattrs + Puppet[:ldapattrs].split("\s*,\s*") + end + pattr = nil + if pattr = Puppet[:ldapparentattr] + if pattr == "" + pattr = nil + else + search_attrs << pattr unless search_attrs.nil? + end + end + + if filter =~ /%s/ + filter = filter.gsub(/%s/, node) + end + + parent = nil + classes = [] + parameters = nil + + found = false + count = 0 + + begin + # We're always doing a sub here; oh well. + ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) do |entry| + found = true + if pattr + if values = entry.vals(pattr) + if values.length > 1 + raise Puppet::Error, + "Node %s has more than one parent: %s" % + [node, values.inspect] + end + unless values.empty? + parent = values.shift + end + end + end + + classattrs.each { |attr| + if values = entry.vals(attr) + values.each do |v| classes << v end + end + } + + parameters = entry.to_hash.inject({}) do |hash, ary| + if ary[1].length == 1 + hash[ary[0]] = ary[1].shift + else + hash[ary[0]] = ary[1] + end + hash + end + end + rescue => detail + if count == 0 + # Try reconnecting to ldap + @ldap = nil + retry + else + raise Puppet::Error, "LDAP Search failed: %s" % detail + end + end + + classes.flatten! + + if classes.empty? + classes = nil + end + + if parent or classes or parameters + return parent, classes, parameters + else + return nil + end + end + + private + + # Create an ldap connection. + def ldap + unless defined? @ldap and @ldap + unless Puppet.features.ldap? + raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" + end + begin + if Puppet[:ldapssl] + @ldap = LDAP::SSLConn.new(Puppet[:ldapserver], Puppet[:ldapport]) + elsif Puppet[:ldaptls] + @ldap = LDAP::SSLConn.new( + Puppet[:ldapserver], Puppet[:ldapport], true + ) + else + @ldap = LDAP::Conn.new(Puppet[:ldapserver], Puppet[:ldapport]) + end + @ldap.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) + @ldap.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON) + @ldap.simple_bind(Puppet[:ldapuser], Puppet[:ldappassword]) + rescue => detail + raise Puppet::Error, "Could not connect to LDAP: %s" % detail + end + end + + return @ldap + end +end diff --git a/lib/puppet/indirector/node/none.rb b/lib/puppet/indirector/node/none.rb new file mode 100644 index 000000000..ce188add5 --- /dev/null +++ b/lib/puppet/indirector/node/none.rb @@ -0,0 +1,10 @@ +Puppet::Network::Handler::Node.newnode_source(:none, :fact_merge => true) do + desc "Always return an empty node object. This is the node source you should + use when you don't have some other, functional source you want to use, + as the compiler will not work without this node information." + + # Just return an empty node. + def nodesearch(name) + newnode(name) + end +end diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb index 2d3ac712e..f04de91c5 100644 --- a/lib/puppet/node.rb +++ b/lib/puppet/node.rb @@ -1,10 +1,129 @@ # A simplistic class for managing the node information itself. class Puppet::Node + # Set up indirection, so that nodes can be looked for in + # the node sources. + require 'puppet/indirector' + extend Puppet::Indirector + + # Use the node source as the indirection terminus. + indirects :node, :to => :node_source + + # Retrieve a node from the node source, with some additional munging + # thrown in for kicks. + # LAK:FIXME Crap. This won't work, because we might have two copies of this class, + # one remote and one local, and we won't know which one should do all of the + # extra crap. + def self.get(key) + return nil unless key + if node = cached?(key) + return node + end + facts = node_facts(key) + node = nil + names = node_names(key, facts) + names.each do |name| + name = name.to_s if name.is_a?(Symbol) + if node = nodesearch(name) and @source != "none" + Puppet.info "Found %s in %s" % [name, @source] + break + end + end + + # If they made it this far, we haven't found anything, so look for a + # default node. + unless node or names.include?("default") + if node = nodesearch("default") + Puppet.notice "Using default node for %s" % key + end + end + + if node + node.source = @source + node.names = names + + # Merge the facts into the parameters. + if fact_merge? + node.fact_merge(facts) + end + + cache(node) + + return node + else + return nil + end + end + + private + + # Store the node to make things a bit faster. + def self.cache(node) + @node_cache[node.name] = node + end + + # If the node is cached, return it. + def self.cached?(name) + # Don't use cache when the filetimeout is set to 0 + return false if [0, "0"].include?(Puppet[:filetimeout]) + + if node = @node_cache[name] and Time.now - node.time < Puppet[:filetimeout] + return node + else + return false + end + end + + # Look up the node facts from our fact handler. + def self.node_facts(key) + if facts = Puppet::Node::Facts.get(key) + facts.values + else + {} + end + end + + # Calculate the list of node names we should use for looking + # up our node. + def self.node_names(key, facts = nil) + facts ||= node_facts(key) + names = [] + + if hostname = facts["hostname"] + unless hostname == key + names << hostname + end + else + hostname = key + end + + if fqdn = facts["fqdn"] + hostname = fqdn + names << fqdn + end + + # Make sure both the fqdn and the short name of the + # host can be used in the manifest + if hostname =~ /\./ + names << hostname.sub(/\..+/,'') + elsif domain = facts['domain'] + names << hostname + "." + domain + end + + # Sort the names inversely by name length. + names.sort! { |a,b| b.length <=> a.length } + + # And make sure the key is first, since that's the most + # likely usage. + ([key] + names).uniq + end + + public + attr_accessor :name, :classes, :parameters, :source, :ipaddress, :names attr_reader :time attr_writer :environment - # Do not return environments tha are empty string, and use + # Do not return environments that are the empty string, and use # explicitly set environments, then facts, then a central env # value. def environment diff --git a/lib/puppet/node/facts.rb b/lib/puppet/node/facts.rb new file mode 100755 index 000000000..eddf44def --- /dev/null +++ b/lib/puppet/node/facts.rb @@ -0,0 +1,36 @@ +# Manage a given node's facts. This either accepts facts and stores them, or +# returns facts for a given node. +class Puppet::Node::Facts + # Set up indirection, so that nodes can be looked for in + # the node sources. + require 'puppet/indirector' + extend Puppet::Indirector + + # Use the node source as the indirection terminus. + indirects :facts, :to => :fact_store + + attr_accessor :name, :values + + def initialize(name, values = {}) + @name = name + @values = values + end + + private + + # FIXME These methods are currently unused. + + # Add internal data to the facts for storage. + def add_internal(facts) + facts = facts.dup + facts[:_puppet_timestamp] = Time.now + facts + end + + # Strip out that internal data. + def strip_internal(facts) + facts = facts.dup + facts.find_all { |name, value| name.to_s =~ /^_puppet_/ }.each { |name, value| facts.delete(name) } + facts + end +end diff --git a/lib/puppet/util/instance_loader.rb b/lib/puppet/util/instance_loader.rb index 1a64c9c69..bc0567866 100755 --- a/lib/puppet/util/instance_loader.rb +++ b/lib/puppet/util/instance_loader.rb @@ -70,5 +70,3 @@ module Puppet::Util::InstanceLoader instances[name] end end - -# $Id$ |