summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeffrey J McCune <jeff.mccune@northstarlabs.net>2007-08-15 15:52:27 -0400
committerJeffrey J McCune <jeff.mccune@northstarlabs.net>2007-08-15 15:52:27 -0400
commit7d09d4624dd1d4d69cf5acd355762503e9cb448c (patch)
tree1a306f28c07992195c56fe581ea5494d6d6c8971
parentfb4ab97580f40d664e76aa7107e58d16097570b8 (diff)
parentaabad8e1e262fb2f63fa4eef0f0e6fc00cc4b01f (diff)
downloadpuppet-7d09d4624dd1d4d69cf5acd355762503e9cb448c.tar.gz
puppet-7d09d4624dd1d4d69cf5acd355762503e9cb448c.tar.xz
puppet-7d09d4624dd1d4d69cf5acd355762503e9cb448c.zip
Merge commit 'aabad8e'
-rw-r--r--lib/puppet/configuration.rb12
-rw-r--r--lib/puppet/network/handler/configuration.rb226
-rw-r--r--lib/puppet/network/handler/node.rb240
-rw-r--r--lib/puppet/node_source/external.rb51
-rw-r--r--lib/puppet/node_source/ldap.rb118
-rw-r--r--lib/puppet/node_source/none.rb10
-rw-r--r--lib/puppet/parser/ast/hostclass.rb7
-rw-r--r--lib/puppet/parser/configuration.rb133
-rw-r--r--lib/puppet/parser/interpreter.rb163
-rw-r--r--lib/puppet/parser/scope.rb222
-rw-r--r--lib/puppet/reference/node_sources.rb7
-rwxr-xr-xtest/language/configuration.rb131
-rwxr-xr-xtest/language/interpreter.rb279
-rwxr-xr-xtest/language/node.rb126
-rwxr-xr-xtest/network/handler/node.rb677
15 files changed, 1648 insertions, 754 deletions
diff --git a/lib/puppet/configuration.rb b/lib/puppet/configuration.rb
index 65e0d9fa8..a8f6e0c35 100644
--- a/lib/puppet/configuration.rb
+++ b/lib/puppet/configuration.rb
@@ -122,7 +122,11 @@ module Puppet
"The configuration file that defines the rights to the different
namespaces and methods. This can be used as a coarse-grained
authorization system for both ``puppetd`` and ``puppetmasterd``."
- ]
+ ],
+ :environment => ["", "The environment Puppet is running in. For clients (e.g., ``puppetd``) this
+ determines the environment itself, which is used to find modules and much more. For
+ servers (i.e., ``puppetmasterd``) this provides the default environment for nodes we
+ know nothing about."]
)
hostname = Facter["hostname"].value
@@ -544,7 +548,11 @@ module Puppet
setdefaults(:parser,
:typecheck => [true, "Whether to validate types during parsing."],
- :paramcheck => [true, "Whether to validate parameters during parsing."]
+ :paramcheck => [true, "Whether to validate parameters during parsing."],
+ :node_source => ["none", "Where to look for node configuration information.
+ The default node source, ``none``, just returns a node with its facts
+ filled in, which is required for normal functionality.
+ See the `NodeSourceReference`:trac: for more information."]
)
setdefaults(:main,
diff --git a/lib/puppet/network/handler/configuration.rb b/lib/puppet/network/handler/configuration.rb
new file mode 100644
index 000000000..7f81879ba
--- /dev/null
+++ b/lib/puppet/network/handler/configuration.rb
@@ -0,0 +1,226 @@
+require 'openssl'
+require 'puppet'
+require 'puppet/parser/interpreter'
+require 'puppet/sslcertificates'
+require 'xmlrpc/server'
+require 'yaml'
+
+class Puppet::Network::Handler
+ class Configuration < Handler
+ desc "Puppet's configuration compilation interface. Passed a node name
+ or other key, retrieves information about the node and returns a
+ compiled configuration."
+
+ include Puppet::Util
+
+ attr_accessor :ast, :local
+ attr_reader :ca
+
+ @interface = XMLRPC::Service::Interface.new("configuration") { |iface|
+ iface.add_method("string configuration(string)")
+ iface.add_method("string version()")
+ }
+
+ # FIXME At some point, this should be autodocumenting.
+ def addfacts(facts)
+ # Add our server version to the fact list
+ facts["serverversion"] = Puppet.version.to_s
+
+ # And then add the server name and IP
+ {"servername" => "fqdn",
+ "serverip" => "ipaddress"
+ }.each do |var, fact|
+ if obj = Facter[fact]
+ facts[var] = obj.value
+ else
+ Puppet.warning "Could not retrieve fact %s" % fact
+ end
+ end
+
+ if facts["servername"].nil?
+ host = Facter.value(:hostname)
+ if domain = Facter.value(:domain)
+ facts["servername"] = [host, domain].join(".")
+ else
+ facts["servername"] = host
+ end
+ end
+ end
+
+ # Manipulate the client name as appropriate.
+ def clientname(name, ip, facts)
+ # Always use the hostname from Facter.
+ client = facts["hostname"]
+ clientip = facts["ipaddress"]
+ if Puppet[:node_name] == 'cert'
+ if name
+ client = name
+ end
+ if ip
+ clientip = ip
+ end
+ end
+
+ return client, clientip
+ end
+
+ # Tell a client whether there's a fresh config for it
+ def freshness(client = nil, clientip = nil)
+ if Puppet.features.rails? and Puppet[:storeconfigs]
+ Puppet::Rails.connect
+
+ host = Puppet::Rails::Host.find_or_create_by_name(client)
+ host.last_freshcheck = Time.now
+ if clientip and (! host.ip or host.ip == "" or host.ip == "NULL")
+ host.ip = clientip
+ end
+ host.save
+ end
+ if defined? @interpreter
+ return @interpreter.parsedate
+ else
+ return 0
+ end
+ end
+
+ def initialize(hash = {})
+ args = {}
+
+ # Allow specification of a code snippet or of a file
+ if code = hash[:Code]
+ args[:Code] = code
+ else
+ args[:Manifest] = hash[:Manifest] || Puppet[:manifest]
+ end
+
+ if hash[:Local]
+ @local = hash[:Local]
+ else
+ @local = false
+ end
+
+ args[:Local] = @local
+
+ if hash.include?(:CA) and hash[:CA]
+ @ca = Puppet::SSLCertificates::CA.new()
+ else
+ @ca = nil
+ end
+
+ Puppet.debug("Creating interpreter")
+
+ if hash.include?(:UseNodes)
+ args[:UseNodes] = hash[:UseNodes]
+ elsif @local
+ args[:UseNodes] = false
+ end
+
+ # This is only used by the cfengine module, or if --loadclasses was
+ # specified in +puppet+.
+ if hash.include?(:Classes)
+ args[:Classes] = hash[:Classes]
+ end
+
+ @interpreter = Puppet::Parser::Interpreter.new(args)
+ end
+
+ def getconfig(facts, format = "marshal", client = nil, clientip = nil)
+ if @local
+ # we don't need to do anything, since we should already
+ # have raw objects
+ Puppet.debug "Our client is local"
+ else
+ Puppet.debug "Our client is remote"
+
+ # XXX this should definitely be done in the protocol, somehow
+ case format
+ when "marshal":
+ Puppet.warning "You should upgrade your client. 'Marshal' will not be supported much longer."
+ begin
+ facts = Marshal::load(CGI.unescape(facts))
+ rescue => detail
+ raise XMLRPC::FaultException.new(
+ 1, "Could not rebuild facts"
+ )
+ end
+ when "yaml":
+ begin
+ facts = YAML.load(CGI.unescape(facts))
+ rescue => detail
+ raise XMLRPC::FaultException.new(
+ 1, "Could not rebuild facts"
+ )
+ end
+ else
+ raise XMLRPC::FaultException.new(
+ 1, "Unavailable config format %s" % format
+ )
+ end
+ end
+
+ client, clientip = clientname(client, clientip, facts)
+
+ # Add any server-side facts to our server.
+ addfacts(facts)
+
+ retobjects = nil
+
+ # This is hackish, but there's no "silence" option for benchmarks
+ # right now
+ if @local
+ #begin
+ retobjects = @interpreter.run(client, facts)
+ #rescue Puppet::Error => detail
+ # Puppet.err detail
+ # raise XMLRPC::FaultException.new(
+ # 1, detail.to_s
+ # )
+ #rescue => detail
+ # Puppet.err detail.to_s
+ # return ""
+ #end
+ else
+ benchmark(:notice, "Compiled configuration for %s" % client) do
+ begin
+ retobjects = @interpreter.run(client, facts)
+ rescue Puppet::Error => detail
+ Puppet.err detail
+ raise XMLRPC::FaultException.new(
+ 1, detail.to_s
+ )
+ rescue => detail
+ Puppet.err detail.to_s
+ return ""
+ end
+ end
+ end
+
+ if @local
+ return retobjects
+ else
+ str = nil
+ case format
+ when "marshal":
+ str = Marshal::dump(retobjects)
+ when "yaml":
+ str = retobjects.to_yaml(:UseBlock => true)
+ else
+ raise XMLRPC::FaultException.new(
+ 1, "Unavailable config format %s" % format
+ )
+ end
+ return CGI.escape(str)
+ end
+ end
+
+ def local?
+ if defined? @local and @local
+ return true
+ else
+ return false
+ end
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/handler/node.rb b/lib/puppet/network/handler/node.rb
new file mode 100644
index 000000000..898db7c22
--- /dev/null
+++ b/lib/puppet/network/handler/node.rb
@@ -0,0 +1,240 @@
+# Created by Luke A. Kanies on 2007-08-13.
+# Copyright (c) 2007. All rights reserved.
+
+require 'puppet/util'
+require 'puppet/util/classgen'
+require 'puppet/util/instance_loader'
+
+# Look up a node, along with all the details about it.
+class Puppet::Network::Handler::Node < Puppet::Network::Handler
+ # A simplistic class for managing the node information itself.
+ class SimpleNode
+ attr_accessor :name, :classes, :parameters, :environment, :source, :ipaddress
+
+ def initialize(name, options = {})
+ @name = name
+
+ if classes = options[:classes]
+ if classes.is_a?(String)
+ @classes = [classes]
+ else
+ @classes = classes
+ end
+ else
+ @classes = []
+ end
+
+ @parameters = options[:parameters] || {}
+
+ unless @environment = options[:environment]
+ if env = Puppet[:environment] and env != ""
+ @environment = env
+ end
+ end
+ end
+
+ # Merge the node facts with parameters from the node source.
+ # This is only called if the node source has 'fact_merge' set to true.
+ def fact_merge(facts)
+ facts.each do |name, value|
+ @parameters[name] = value unless @parameters.include?(name)
+ end
+ end
+ end
+
+ desc "Retrieve information about nodes."
+
+ extend Puppet::Util::ClassGen
+ extend Puppet::Util::InstanceLoader
+
+ # A simple base module we can use for modifying how our node sources work.
+ module SourceBase
+ include Puppet::Util::Docs
+ end
+
+ @interface = XMLRPC::Service::Interface.new("nodes") { |iface|
+ iface.add_method("string details(key)")
+ iface.add_method("string parameters(key)")
+ iface.add_method("string environment(key)")
+ iface.add_method("string classes(key)")
+ }
+
+ # Set up autoloading and retrieving of reports.
+ autoload :node_source, 'puppet/node_source'
+
+ attr_reader :source
+
+ # Add a new node source.
+ def self.newnode_source(name, options = {}, &block)
+ name = symbolize(name)
+
+ fact_merge = options[:fact_merge]
+ mod = genmodule(name, :extend => SourceBase, :hash => instance_hash(:node_source), :block => block)
+ mod.send(:define_method, :fact_merge?) do
+ fact_merge
+ end
+ mod
+ end
+
+ # Collect the docs for all of our node sources.
+ def self.node_source_docs
+ docs = ""
+
+ # Use this method so they all get loaded
+ instance_loader(:node_source).loadall
+ loaded_instances(:node_source).sort { |a,b| a.to_s <=> b.to_s }.each do |name|
+ mod = self.node_source(name)
+ docs += "%s\n%s\n" % [name, "-" * name.to_s.length]
+
+ docs += Puppet::Util::Docs.scrub(mod.doc) + "\n\n"
+ end
+
+ docs
+ end
+
+ # List each of the node sources.
+ def self.node_sources
+ instance_loader(:node_source).loadall
+ loaded_instances(:node_source)
+ end
+
+ # Remove a defined node source; basically only used for testing.
+ def self.rm_node_source(name)
+ rmclass(name, :hash => instance_hash(:node_source))
+ end
+
+ # Return a given node's classes.
+ def classes(key)
+ if node = details(key)
+ node.classes
+ else
+ nil
+ end
+ end
+
+ # Return an entire node configuration. This uses the 'nodesearch' method
+ # defined in the node_source to look for the node.
+ def details(key, client = nil, clientip = nil)
+ 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)
+ 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
+
+ # Merge the facts into the parameters.
+ if fact_merge?
+ node.fact_merge(facts)
+ end
+ return node
+ else
+ return nil
+ end
+ end
+
+ # Return a given node's environment.
+ def environment(key, client = nil, clientip = nil)
+ if node = details(key)
+ node.environment
+ else
+ nil
+ end
+ end
+
+ # Create our node lookup tool.
+ def initialize(hash = {})
+ @source = hash[:Source] || Puppet[:node_source]
+
+ unless mod = self.class.node_source(@source)
+ raise ArgumentError, "Unknown node source '%s'" % @source
+ end
+
+ extend(mod)
+
+ super
+ end
+
+ # Try to retrieve a given node's parameters.
+ def parameters(key, client = nil, clientip = nil)
+ if node = details(key)
+ node.parameters
+ else
+ nil
+ end
+ end
+
+ private
+
+ # Create/cache a fact handler.
+ def fact_handler
+ unless defined?(@fact_handler)
+ @fact_handler = Puppet::Network::Handler.handler(:facts).new
+ end
+ @fact_handler
+ end
+
+ # Short-hand for creating a new node, so the node sources don't need to
+ # specify the constant.
+ def newnode(options)
+ SimpleNode.new(options)
+ end
+
+ # Look up the node facts from our fact handler.
+ def node_facts(key)
+ if facts = fact_handler.get(key)
+ facts
+ else
+ {}
+ end
+ end
+
+ # Calculate the list of node names we should use for looking
+ # up our node.
+ def 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
+end
diff --git a/lib/puppet/node_source/external.rb b/lib/puppet/node_source/external.rb
new file mode 100644
index 000000000..54111d924
--- /dev/null
+++ b/lib/puppet/node_source/external.rb
@@ -0,0 +1,51 @@
+Puppet::Network::Handler::Node.newnode_source(:external, :fact_merge => true) do
+ desc "Call an external program to get node information."
+
+ include Puppet::Util
+ # Look for external node definitions.
+ def nodesearch(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/node_source/ldap.rb b/lib/puppet/node_source/ldap.rb
new file mode 100644
index 000000000..9332fcb40
--- /dev/null
+++ b/lib/puppet/node_source/ldap.rb
@@ -0,0 +1,118 @@
+Puppet::Network::Handler::Node.newnode_source(:ldap, :fact_merge => true) do
+ desc "Search in LDAP for node configuration information."
+
+ # Find the ldap node, return the class list and parent node specially,
+ # and everything else in a parameter hash.
+ def ldapsearch(node)
+ unless defined? @ldap and @ldap
+ setup_ldap()
+ unless @ldap
+ Puppet.info "Skipping ldap source; no ldap connection"
+ return nil
+ end
+ end
+
+ 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
+ setup_ldap()
+ 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
+
+ # Look for our node in ldap.
+ def nodesearch(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
+end
diff --git a/lib/puppet/node_source/none.rb b/lib/puppet/node_source/none.rb
new file mode 100644
index 000000000..ce188add5
--- /dev/null
+++ b/lib/puppet/node_source/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/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb
index 642645824..d1ce370da 100644
--- a/lib/puppet/parser/ast/hostclass.rb
+++ b/lib/puppet/parser/ast/hostclass.rb
@@ -24,8 +24,11 @@ class Puppet::Parser::AST
scope = hash[:scope]
args = hash[:arguments]
- # Verify that we haven't already been evaluated
- if scope.class_scope(self)
+ # Verify that we haven't already been evaluated, and if we have been evaluated,
+ # make sure that we match the class.
+ if existing_scope = scope.class_scope(self)
+ raise "Fix this portion of the code -- check that the scopes match classes"
+ #if existing_scope.source.object_id == self.object_id
Puppet.debug "%s class already evaluated" % @type
return nil
end
diff --git a/lib/puppet/parser/configuration.rb b/lib/puppet/parser/configuration.rb
new file mode 100644
index 000000000..c7979e51f
--- /dev/null
+++ b/lib/puppet/parser/configuration.rb
@@ -0,0 +1,133 @@
+# Created by Luke A. Kanies on 2007-08-13.
+# Copyright (c) 2007. All rights reserved.
+
+require 'puppet/external/gratr/digraph'
+require 'puppet/external/gratr/import'
+require 'puppet/external/gratr/dot'
+
+# Maintain a graph of scopes, along with a bunch of data
+# about the individual configuration we're compiling.
+class Puppet::Parser::Configuration
+ attr_reader :topscope, :interpreter, :host, :facts
+
+ # Add a collection to the global list.
+ def add_collection(coll)
+ @collections << coll
+ end
+
+ # Store the fact that we've evaluated a class, and store a reference to
+ # the scope in which it was evaluated, so that we can look it up later.
+ def class_set(name, scope)
+ @class_scopes[name] = scope
+ end
+
+ # Return the scope associated with a class. This is just here so
+ # that subclasses can set their parent scopes to be the scope of
+ # their parent class, and it's also used when looking up qualified
+ # variables.
+ def class_scope(klass)
+ # They might pass in either the class or class name
+ if klass.respond_to?(:classname)
+ @class_scopes[klass.classname]
+ else
+ @class_scopes[klass]
+ end
+ end
+
+ # Return a list of all of the defined classes.
+ def classlist
+ return @class_scopes.keys.reject { |k| k == "" }
+ end
+
+ # Should the scopes behave declaratively?
+ def declarative?
+ true
+ end
+
+ # Set up our configuration. We require an interpreter
+ # and a host name, and we normally are passed facts, too.
+ def initialize(options)
+ @interpreter = options[:interpreter] or
+ raise ArgumentError, "You must pass an interpreter to the configuration"
+ @facts = options[:facts] || {}
+ @host = options[:host] or
+ raise ArgumentError, "You must pass a host name to the configuration"
+
+ # Call the setup methods from the base class.
+ super()
+
+ initvars()
+ end
+
+ # Create a new scope, with either a specified parent scope or
+ # using the top scope. Adds an edge between the scope and
+ # its parent to the graph.
+ def newscope(parent = nil)
+ parent ||= @topscope
+ scope = Puppet::Parser::Scope.new(:configuration => self)
+ @graph.add_edge!(parent, scope)
+ scope
+ end
+
+ # Find the parent of a given scope. Assumes scopes only ever have
+ # one in edge, which will always be true.
+ def parent(scope)
+ if ary = @graph.adjacent(scope, :direction => :in) and ary.length > 0
+ ary[0]
+ else
+ nil
+ end
+ end
+
+ # Return an array of all of the unevaluated objects
+ def unevaluated
+ ary = @definedtable.find_all do |name, object|
+ ! object.builtin? and ! object.evaluated?
+ end.collect { |name, object| object }
+
+ if ary.empty?
+ return nil
+ else
+ return ary
+ end
+ end
+
+ private
+
+ # Set up all of our internal variables.
+ def initvars
+ # The table for storing class singletons. This will only actually
+ # be used by top scopes and node scopes.
+ @class_scopes = {}
+
+ # The table for all defined resources.
+ @resource_table = {}
+
+ # The list of objects that will available for export.
+ @exported_resources = {}
+
+ # The list of overrides. This is used to cache overrides on objects
+ # that don't exist yet. We store an array of each override.
+ @resource_overrides = Hash.new do |overs, ref|
+ overs[ref] = []
+ end
+
+ # The list of collections that have been created. This is a global list,
+ # but they each refer back to the scope that created them.
+ @collections = []
+
+ # Create our initial scope, our scope graph, and add the initial scope to the graph.
+ @topscope = Puppet::Parser::Scope.new(:configuration => self, :type => "main", :name => "top")
+ @graph = GRATR::Digraph.new
+ @graph.add_vertex!(@topscope)
+ end
+
+ # Return the list of remaining overrides.
+ def overrides
+ @resource_overrides.values.flatten
+ end
+
+ def resources
+ @resourcetable
+ end
+end
diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb
index 3ba9c0c7a..18bf31087 100644
--- a/lib/puppet/parser/interpreter.rb
+++ b/lib/puppet/parser/interpreter.rb
@@ -178,7 +178,6 @@ class Puppet::Parser::Interpreter
# Evaluate all of the code we can find that's related to our client.
def evaluate(client, facts)
-
scope = Puppet::Parser::Scope.new(:interp => self) # no parent scope
scope.name = "top"
scope.type = "main"
@@ -327,101 +326,6 @@ class Puppet::Parser::Interpreter
parsefiles
end
- # Find the ldap node, return the class list and parent node specially,
- # and everything else in a parameter hash.
- def ldapsearch(node)
- unless defined? @ldap and @ldap
- setup_ldap()
- unless @ldap
- Puppet.info "Skipping ldap source; no ldap connection"
- return nil
- end
- end
-
- 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
- setup_ldap()
- 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
-
# Pass these methods through to the parser.
[:newclass, :newdefine, :newnode].each do |name|
define_method(name) do |*args|
@@ -476,73 +380,6 @@ class Puppet::Parser::Interpreter
def nodesearch_code(name)
@parser.nodes[name]
end
-
- # Look for external node definitions.
- def nodesearch_external(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_args = {:source => "external node source", :name => name}
- set = false
- [:parameters, :classes].each do |param|
- if value = result[param]
- node_args[param] = value
- set = true
- end
- end
-
- if set
- return NodeDef.new(node_args)
- else
- return nil
- end
- end
-
- # Look for our node in ldap.
- def nodesearch_ldap(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 NodeDef.new(:name => node, :classes => classes, :source => "ldap", :parameters => parameters)
- end
def parsedate
parsefiles()
diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb
index 6feeefc46..1fb4f6906 100644
--- a/lib/puppet/parser/scope.rb
+++ b/lib/puppet/parser/scope.rb
@@ -15,36 +15,18 @@ class Puppet::Parser::Scope
include Enumerable
include Puppet::Util::Errors
- attr_accessor :parent, :level, :interp, :source, :host
- attr_accessor :name, :type, :topscope, :base, :keyword
- attr_accessor :top, :translated, :exported, :virtual
+ attr_accessor :parent, :level, :interp, :source
+ attr_accessor :name, :type, :base, :keyword
+ attr_accessor :top, :translated, :exported, :virtual, :configuration
- # Whether we behave declaratively. Note that it's a class variable,
- # so all scopes behave the same.
- @@declarative = true
-
- # Retrieve and set the declarative setting.
- def self.declarative
- return @@declarative
- end
-
- def self.declarative=(val)
- @@declarative = val
+ # Proxy accessors
+ def host
+ @configuration.host
end
-
- # This handles the shared tables that all scopes have. They're effectively
- # global tables, except that they're only global for a single scope tree,
- # which is why I can't use class variables for them.
- def self.sharedtable(*names)
- attr_accessor(*names)
- @@sharedtables ||= []
- @@sharedtables += names
+ def interpreter
+ @configuration.interpreter
end
- # This is probably not all that good of an idea, but...
- # This way a parent can share its tables with all of its children.
- sharedtable :classtable, :definedtable, :exportable, :overridetable, :collecttable
-
# Is the value true? This allows us to control the definition of truth
# in one place.
def self.true?(value)
@@ -111,44 +93,6 @@ class Puppet::Parser::Scope
return true
end
- # Return the scope associated with a class. This is just here so
- # that subclasses can set their parent scopes to be the scope of
- # their parent class.
- def class_scope(klass)
- scope = if klass.respond_to?(:classname)
- @classtable[klass.classname]
- else
- @classtable[klass]
- end
-
- return nil unless scope
-
- if scope.nodescope? and ! klass.is_a?(AST::Node)
- raise Puppet::ParseError, "Node %s has already been evaluated; cannot evaluate class with same name" % [klass.classname]
- end
-
- scope
- end
-
- # Return the list of collections.
- def collections
- @collecttable
- end
-
- def declarative=(val)
- self.class.declarative = val
- end
-
- def declarative
- self.class.declarative
- end
-
- # Test whether a given scope is declarative. Even though it's
- # a global value, the calling objects don't need to know that.
- def declarative?
- @@declarative
- end
-
# Remove a specific child.
def delete(child)
@children.delete(child)
@@ -171,14 +115,6 @@ class Puppet::Parser::Scope
@level == 1
end
- # Return a list of all of the defined classes.
- def classlist
- unless defined? @classtable
- raise Puppet::DevError, "Scope did not receive class table"
- end
- return @classtable.keys.reject { |k| k == "" }
- end
-
# Yield each child scope in turn
def each
@children.each { |child|
@@ -239,9 +175,6 @@ class Puppet::Parser::Scope
# Initialize our new scope. Defaults to having no parent and to
# being declarative.
def initialize(hash = {})
- @parent = nil
- @type = nil
- @name = nil
@finished = false
if hash.include?(:namespace)
if n = hash[:namespace]
@@ -262,26 +195,6 @@ class Puppet::Parser::Scope
@tags = []
- if @parent.nil?
- unless hash.include?(:declarative)
- hash[:declarative] = true
- end
- self.istop(hash[:declarative])
- @inside = nil
- else
- # This is here, rather than in newchild(), so that all
- # of the later variable initialization works.
- @parent.child = self
-
- @level = @parent.level + 1
- @interp = @parent.interp
- @source = hash[:source] || @parent.source
- @topscope = @parent.topscope
- #@inside = @parent.inside # Used for definition inheritance
- @host = @parent.host
- @type ||= @parent.type
- end
-
# Our child scopes and objects
@children = []
@@ -294,62 +207,6 @@ class Puppet::Parser::Scope
@defaultstable = Hash.new { |dhash,type|
dhash[type] = {}
}
-
- unless @interp
- raise Puppet::DevError, "Scopes require an interpreter"
- end
- end
-
- # Associate the object directly with the scope, so that contained objects
- # can look up what container they're running within.
- def inside(arg = nil)
- return @inside unless arg
-
- old = @inside
- @inside = arg
- yield
- ensure
- #Puppet.warning "exiting %s" % @inside.name
- @inside = old
- end
-
- # Mark that we're the top scope, and set some hard-coded info.
- def istop(declarative = true)
- # the level is mostly used for debugging
- @level = 1
-
- # The table for storing class singletons. This will only actually
- # be used by top scopes and node scopes.
- @classtable = {}
-
- self.class.declarative = declarative
-
- # The table for all defined objects.
- @definedtable = {}
-
- # The list of objects that will available for export.
- @exportable = {}
-
- # The list of overrides. This is used to cache overrides on objects
- # that don't exist yet. We store an array of each override.
- @overridetable = Hash.new do |overs, ref|
- overs[ref] = []
- end
-
- # Eventually, if we support sites, this will allow definitions
- # of nodes with the same name in different sites. For now
- # the top-level scope is always the only site scope.
- @sitescope = true
-
- @namespaces = [""]
-
- # The list of collections that have been created. This is a global list,
- # but they each refer back to the scope that created them.
- @collecttable = []
-
- @topscope = self
- @type = "puppet"
- @name = "top"
end
# Collect all of the defaults set at any higher scopes.
@@ -360,8 +217,8 @@ class Puppet::Parser::Scope
values = {}
# first collect the values from the parents
- unless @parent.nil?
- @parent.lookupdefaults(type).each { |var,value|
+ unless parent.nil?
+ parent.lookupdefaults(type).each { |var,value|
values[var] = value
}
end
@@ -426,7 +283,7 @@ class Puppet::Parser::Scope
return @symtable[name]
end
elsif self.parent
- return @parent.lookupvar(name, usestring)
+ return parent.lookupvar(name, usestring)
elsif usestring
return ""
else
@@ -438,11 +295,6 @@ class Puppet::Parser::Scope
@namespaces.dup
end
- # Add a collection to the global list.
- def newcollection(coll)
- @collecttable << coll
- end
-
# Create a new scope.
def newscope(hash = {})
hash[:parent] = self
@@ -464,6 +316,25 @@ class Puppet::Parser::Scope
@overridetable.values.flatten
end
+ # We probably shouldn't cache this value... But it's a lot faster
+ # than doing lots of queries.
+ def parent
+ unless defined?(@parent)
+ @parent = configuration.parent(self)
+ end
+ @parent
+ end
+
+ # Return the list of scopes up to the top scope, ordered with our own first.
+ # This is used for looking up variables and defaults.
+ def scope_path
+ if parent
+ [self, parent.scope_path].flatten.compact
+ else
+ [self]
+ end
+ end
+
def resources
@definedtable.values
end
@@ -472,17 +343,17 @@ class Puppet::Parser::Scope
# that gets inherited from the top scope down, rather than a global
# hash. We store the object ID, not class name, so that we
# can support multiple unrelated classes with the same name.
- def setclass(obj)
- if obj.is_a?(AST::HostClass)
- unless obj.classname
+ def setclass(klass)
+ if klass.is_a?(AST::HostClass)
+ unless klass.classname
raise Puppet::DevError, "Got a %s with no fully qualified name" %
- obj.class
+ klass.class
end
- @classtable[obj.classname] = self
+ @configuration.class_set(klass.classname, self)
else
- raise Puppet::DevError, "Invalid class %s" % obj.inspect
+ raise Puppet::DevError, "Invalid class %s" % klass.inspect
end
- if obj.is_a?(AST::Node)
+ if klass.is_a?(AST::Node)
@nodescope = true
end
nil
@@ -665,9 +536,9 @@ class Puppet::Parser::Scope
unless ! defined? @type or @type.nil? or @type == ""
tmp << @type.to_s
end
- if @parent
- #info "Looking for tags in %s" % @parent.type
- @parent.tags.each { |tag|
+ if parent
+ #info "Looking for tags in %s" % parent.type
+ parent.tags.each { |tag|
if tag.nil? or tag == ""
Puppet.debug "parent returned tag %s" % tag.inspect
next
@@ -722,19 +593,6 @@ class Puppet::Parser::Scope
end
end
- # Return an array of all of the unevaluated objects
- def unevaluated
- ary = @definedtable.find_all do |name, object|
- ! object.builtin? and ! object.evaluated?
- end.collect { |name, object| object }
-
- if ary.empty?
- return nil
- else
- return ary
- end
- end
-
def virtual?
self.virtual || self.exported?
end
diff --git a/lib/puppet/reference/node_sources.rb b/lib/puppet/reference/node_sources.rb
new file mode 100644
index 000000000..33a4c539c
--- /dev/null
+++ b/lib/puppet/reference/node_sources.rb
@@ -0,0 +1,7 @@
+noderef = Puppet::Util::Reference.newreference :node_source, :doc => "Sources of node configuration information" do
+ Puppet::Network::Handler.node.sourcedocs
+end
+
+nodref.header = "
+Nodes can be searched for in different locations. This document describes those different locations.
+"
diff --git a/test/language/configuration.rb b/test/language/configuration.rb
new file mode 100755
index 000000000..4cbba8063
--- /dev/null
+++ b/test/language/configuration.rb
@@ -0,0 +1,131 @@
+#!/usr/bin/env ruby
+
+$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/
+
+require 'mocha'
+require 'puppettest'
+require 'puppettest/parsertesting'
+require 'puppet/parser/configuration'
+
+# Test our configuration object.
+class TestConfiguration < Test::Unit::TestCase
+ include PuppetTest
+ include PuppetTest::ParserTesting
+
+ Config = Puppet::Parser::Configuration
+ Scope = Puppet::Parser::Scope
+
+ def mkconfig
+ Config.new(:host => "foo", :interpreter => "interp")
+ end
+
+ def test_initialize
+ # Make sure we get an error if we don't send an interpreter
+ assert_raise(ArgumentError, "Did not fail when missing host") do
+ Config.new(:interpreter => "yay" )
+ end
+ assert_raise(ArgumentError, "Did not fail when missing interp") do
+ Config.new(:host => "foo")
+ end
+
+ # Now check the defaults
+ config = nil
+ assert_nothing_raised("Could not init config with all required options") do
+ config = Config.new(:host => "foo", :interpreter => "interp")
+ end
+
+ assert_equal("foo", config.host, "Did not set host correctly")
+ assert_equal("interp", config.interpreter, "Did not set interpreter correctly")
+ assert_equal({}, config.facts, "Did not set default facts")
+
+ # Now make a new one with facts, to make sure the facts get set appropriately
+ assert_nothing_raised("Could not init config with all required options") do
+ config = Config.new(:host => "foo", :interpreter => "interp", :facts => {"a" => "b"})
+ end
+ assert_equal({"a" => "b"}, config.facts, "Did not set facts")
+ end
+
+ def test_initvars
+ config = mkconfig
+ [:class_scopes, :resource_table, :exported_resources, :resource_overrides].each do |table|
+ assert_instance_of(Hash, config.send(:instance_variable_get, "@#{table}"), "Did not set %s table correctly" % table)
+ end
+ end
+
+ # Make sure we store and can retrieve references to classes and their scopes.
+ def test_class_set_and_class_scope
+ klass = Object.new
+ klass.expects(:classname).returns("myname")
+
+ config = mkconfig
+
+ assert_nothing_raised("Could not set class") do
+ config.class_set "myname", "myscope"
+ end
+ # First try to retrieve it by name.
+ assert_equal("myscope", config.class_scope("myname"), "Could not retrieve class scope by name")
+
+ # Then by object
+ assert_equal("myscope", config.class_scope(klass), "Could not retrieve class scope by object")
+ end
+
+ def test_classlist
+ config = mkconfig
+
+ config.class_set "", "empty"
+ config.class_set "one", "yep"
+ config.class_set "two", "nope"
+
+ # Make sure our class list is correct
+ assert_equal(%w{one two}.sort, config.classlist.sort, "Did not get correct class list")
+ end
+
+ # Make sure collections get added to our internal array
+ def test_add_collection
+ config = mkconfig
+ assert_nothing_raised("Could not add collection") do
+ config.add_collection "nope"
+ end
+ assert_equal(%w{nope}, config.instance_variable_get("@collections"), "Did not add collection")
+ end
+
+ # Make sure we create a graph of scopes.
+ def test_newscope
+ config = mkconfig
+ graph = config.instance_variable_get("@graph")
+ assert_instance_of(Scope, config.topscope, "Did not create top scope")
+ assert_instance_of(GRATR::Digraph, graph, "Did not create graph")
+
+ assert(graph.vertex?(config.topscope), "The top scope is not a vertex in the graph")
+
+ # Now that we've got the top scope, create a new, subscope
+ subscope = nil
+ assert_nothing_raised("Could not create subscope") do
+ subscope = config.newscope
+ end
+ assert_instance_of(Scope, subscope, "Did not create subscope")
+ assert(graph.edge?(config.topscope, subscope), "An edge between top scope and subscope was not added")
+
+ # Make sure a scope can find its parent.
+ assert(config.parent(subscope), "Could not look up parent scope on configuration")
+ assert_equal(config.topscope.object_id, config.parent(subscope).object_id, "Did not get correct parent scope from configuration")
+ assert_equal(config.topscope.object_id, subscope.parent.object_id, "Scope did not correctly retrieve its parent scope")
+
+ # Now create another, this time specifying the parent scope
+ another = nil
+ assert_nothing_raised("Could not create subscope") do
+ another = config.newscope(subscope)
+ end
+ assert_instance_of(Scope, another, "Did not create second subscope")
+ assert(graph.edge?(subscope, another), "An edge between parent scope and second subscope was not added")
+
+ # Make sure it can find its parent.
+ assert(config.parent(another), "Could not look up parent scope of second subscope on configuration")
+ assert_equal(subscope.object_id, config.parent(another).object_id, "Did not get correct parent scope of second subscope from configuration")
+ assert_equal(subscope.object_id, another.parent.object_id, "Second subscope did not correctly retrieve its parent scope")
+
+ # And make sure both scopes show up in the right order in the search path
+ assert_equal([another.object_id, subscope.object_id, config.topscope.object_id], another.scope_path.collect { |p| p.object_id },
+ "Did not get correct scope path")
+ end
+end
diff --git a/test/language/interpreter.rb b/test/language/interpreter.rb
index 75800cc41..070e2e77e 100755
--- a/test/language/interpreter.rb
+++ b/test/language/interpreter.rb
@@ -431,96 +431,6 @@ class TestInterpreter < PuppetTest::TestCase
assert(found.include?("/tmp/klass1"), "Did not evaluate klass1")
assert(found.include?("/tmp/klass2"), "Did not evaluate klass2")
end
-
- def mk_node_mapper
- # First, make sure our nodesearch command works as we expect
- # Make a nodemapper
- mapper = tempfile()
- ruby = %x{which ruby}.chomp
- File.open(mapper, "w") { |f|
- f.puts "#!#{ruby}
- require 'yaml'
- name = ARGV.last.chomp
- result = {}
-
- if name =~ /a/
- result[:parameters] = {'one' => ARGV.last + '1', 'two' => ARGV.last + '2'}
- end
-
- if name =~ /p/
- result['classes'] = [1,2,3].collect { |n| ARGV.last + n.to_s }
- end
-
- puts YAML.dump(result)
- "
- }
- File.chmod(0755, mapper)
- mapper
- end
-
- def test_nodesearch_external
- interp = mkinterp
-
- mapper = mk_node_mapper
- # Make sure it gives the right response
- assert_equal({'classes' => %w{apple1 apple2 apple3}, :parameters => {"one" => "apple1", "two" => "apple2"}},
- YAML.load(%x{#{mapper} apple}))
-
- # First make sure we get nil back by default
- assert_nothing_raised {
- assert_nil(interp.nodesearch_external("apple"),
- "Interp#nodesearch_external defaulted to a non-nil response")
- }
- assert_nothing_raised { Puppet[:external_nodes] = mapper }
-
- node = nil
- # Both 'a' and 'p', so we get classes and parameters
- assert_nothing_raised { node = interp.nodesearch_external("apple") }
- assert_equal("apple", node.name, "node name was not set correctly for apple")
- assert_equal(%w{apple1 apple2 apple3}, node.classes, "node classes were not set correctly for apple")
- assert_equal( {"one" => "apple1", "two" => "apple2"}, node.parameters, "node parameters were not set correctly for apple")
-
- # A 'p' but no 'a', so we only get classes
- assert_nothing_raised { node = interp.nodesearch_external("plum") }
- assert_equal("plum", node.name, "node name was not set correctly for plum")
- assert_equal(%w{plum1 plum2 plum3}, node.classes, "node classes were not set correctly for plum")
- assert_equal({}, node.parameters, "node parameters were not set correctly for plum")
-
- # An 'a' but no 'p', so we only get parameters.
- assert_nothing_raised { node = interp.nodesearch_external("guava")} # no p's, thus no classes
- assert_equal("guava", node.name, "node name was not set correctly for guava")
- assert_equal([], node.classes, "node classes were not set correctly for guava")
- assert_equal({"one" => "guava1", "two" => "guava2"}, node.parameters, "node parameters were not set correctly for guava")
-
- assert_nothing_raised { node = interp.nodesearch_external("honeydew")} # neither, thus nil
- assert_nil(node)
- end
-
- # Make sure a nodesearch with arguments works
- def test_nodesearch_external_arguments
- mapper = mk_node_mapper
- Puppet[:external_nodes] = "#{mapper} -s something -p somethingelse"
- interp = mkinterp
- node = nil
- assert_nothing_raised do
- node = interp.nodesearch("apple")
- end
- assert_instance_of(NodeDef, node, "did not create node")
- end
-
- # A wrapper test, to make sure we're correctly calling the external search method.
- def test_nodesearch_external_functional
- mapper = mk_node_mapper
-
- Puppet[:external_nodes] = mapper
- interp = mkinterp
-
- node = nil
- assert_nothing_raised do
- node = interp.nodesearch("apple")
- end
- assert_instance_of(NodeDef, node, "did not create node")
- end
def test_check_resource_collections
interp = mkinterp
@@ -598,64 +508,6 @@ class TestInterpreter < PuppetTest::TestCase
assert(scope.classlist.include?("base"), "NodeDef did not evaluate class")
end
- # This can stay in the main test suite because it doesn't actually use ldapsearch,
- # it just overrides the method so it behaves as though it were hitting ldap.
- def test_ldapnodes
- interp = mkinterp
-
- nodetable = {}
-
- # Override the ldapsearch definition, so we don't have to actually set it up.
- interp.meta_def(:ldapsearch) do |name|
- nodetable[name]
- end
-
- # Make sure we get nothing for nonexistent hosts
- node = nil
- assert_nothing_raised do
- node = interp.nodesearch_ldap("nosuchhost")
- end
-
- assert_nil(node, "Got a node for a non-existent host")
-
- # Now add a base node with some classes and parameters
- nodetable["base"] = [nil, %w{one two}, {"base" => "true"}]
-
- assert_nothing_raised do
- node = interp.nodesearch_ldap("base")
- end
-
- assert_instance_of(NodeDef, node, "Did not get node from ldap nodesearch")
- assert_equal("base", node.name, "node name was not set")
-
- assert_equal(%w{one two}, node.classes, "node classes were not set")
- assert_equal({"base" => "true"}, node.parameters, "node parameters were not set")
-
- # Now use a different with this as the base
- nodetable["middle"] = ["base", %w{three}, {"center" => "boo"}]
- assert_nothing_raised do
- node = interp.nodesearch_ldap("middle")
- end
-
- assert_instance_of(NodeDef, node, "Did not get node from ldap nodesearch")
- assert_equal("middle", node.name, "node name was not set")
-
- assert_equal(%w{one two three}.sort, node.classes.sort, "node classes were not set correctly with a parent node")
- assert_equal({"base" => "true", "center" => "boo"}, node.parameters, "node parameters were not set correctly with a parent node")
-
- # And one further, to make sure we fully recurse
- nodetable["top"] = ["middle", %w{four five}, {"master" => "far"}]
- assert_nothing_raised do
- node = interp.nodesearch_ldap("top")
- end
-
- assert_instance_of(NodeDef, node, "Did not get node from ldap nodesearch")
- assert_equal("top", node.name, "node name was not set")
-
- assert_equal(%w{one two three four five}.sort, node.classes.sort, "node classes were not set correctly with the top node")
- assert_equal({"base" => "true", "center" => "boo", "master" => "far"}, node.parameters, "node parameters were not set correctly with the top node")
- end
-
# Make sure that reparsing is atomic -- failures don't cause a broken state, and we aren't subject
# to race conditions if someone contacts us while we're reparsing.
def test_atomic_reparsing
@@ -682,135 +534,4 @@ class TestInterpreter < PuppetTest::TestCase
end
end
-class LdapNodeTest < PuppetTest::TestCase
- include PuppetTest
- include PuppetTest::ServerTest
- include PuppetTest::ParserTesting
- include PuppetTest::ResourceTesting
- AST = Puppet::Parser::AST
- NodeDef = Puppet::Parser::Interpreter::NodeDef
- confine "LDAP is not available" => Puppet.features.ldap?
- confine "No LDAP test data for networks other than Luke's" => Facter.value(:domain) == "madstop.com"
- def ldapconnect
-
- @ldap = LDAP::Conn.new("ldap", 389)
- @ldap.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 )
- @ldap.simple_bind("", "")
-
- return @ldap
- end
-
- def ldaphost(name)
- node = NodeDef.new(:name => name)
- parent = nil
- found = false
- @ldap.search( "ou=hosts, dc=madstop, dc=com", 2,
- "(&(objectclass=puppetclient)(cn=%s))" % name
- ) do |entry|
- node.classes = entry.vals("puppetclass") || []
- node.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
- parent = node.parameters["parentnode"]
- found = true
- end
- raise "Could not find node %s" % name unless found
-
- return node, parent
- end
-
- def test_ldapsearch
- Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com"
- Puppet[:ldapnodes] = true
-
- ldapconnect()
-
- interp = mkinterp :NodeSources => [:ldap, :code]
-
- # Make sure we get nil and nil back when we search for something missing
- parent, classes, parameters = nil
- assert_nothing_raised do
- parent, classes, parameters = interp.ldapsearch("nosuchhost")
- end
-
- assert_nil(parent, "Got a parent for a non-existent host")
- assert_nil(classes, "Got classes for a non-existent host")
-
- # Make sure we can find 'culain' in ldap
- assert_nothing_raised do
- parent, classes, parameters = interp.ldapsearch("culain")
- end
-
- node, realparent = ldaphost("culain")
- assert_equal(realparent, parent, "did not get correct parent node from ldap")
- assert_equal(node.classes, classes, "did not get correct ldap classes from ldap")
- assert_equal(node.parameters, parameters, "did not get correct ldap parameters from ldap")
-
- # Now compare when we specify the attributes to get.
- Puppet[:ldapattrs] = "cn"
- assert_nothing_raised do
- parent, classes, parameters = interp.ldapsearch("culain")
- end
- assert_equal(realparent, parent, "did not get correct parent node from ldap")
- assert_equal(node.classes, classes, "did not get correct ldap classes from ldap")
-
- list = %w{cn puppetclass parentnode dn}
- should = node.parameters.inject({}) { |h, a| h[a[0]] = a[1] if list.include?(a[0]); h }
- assert_equal(should, parameters, "did not get correct ldap parameters from ldap")
- end
-end
-
-class LdapReconnectTests < PuppetTest::TestCase
- include PuppetTest
- include PuppetTest::ServerTest
- include PuppetTest::ParserTesting
- include PuppetTest::ResourceTesting
- AST = Puppet::Parser::AST
- NodeDef = Puppet::Parser::Interpreter::NodeDef
- confine "Not running on culain as root" => (Puppet::Util::SUIDManager.uid == 0 and Facter.value("hostname") == "culain")
-
- def test_ldapreconnect
- Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com"
- Puppet[:ldapnodes] = true
-
- interp = nil
- assert_nothing_raised {
- interp = Puppet::Parser::Interpreter.new(
- :Manifest => mktestmanifest()
- )
- }
- hostname = "culain.madstop.com"
-
- # look for our host
- assert_nothing_raised {
- parent, classes = interp.nodesearch_ldap(hostname)
- }
-
- # Now restart ldap
- system("/etc/init.d/slapd restart 2>/dev/null >/dev/null")
- sleep(1)
-
- # and look again
- assert_nothing_raised {
- parent, classes = interp.nodesearch_ldap(hostname)
- }
-
- # Now stop ldap
- system("/etc/init.d/slapd stop 2>/dev/null >/dev/null")
- cleanup do
- system("/etc/init.d/slapd start 2>/dev/null >/dev/null")
- end
-
- # And make sure we actually fail here
- assert_raise(Puppet::Error) {
- parent, classes = interp.nodesearch_ldap(hostname)
- }
- end
-end
-
# $Id$
diff --git a/test/language/node.rb b/test/language/node.rb
deleted file mode 100755
index 61983df92..000000000
--- a/test/language/node.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/usr/bin/env ruby
-
-$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/
-
-require 'puppet'
-require 'puppet/parser/parser'
-require 'puppettest'
-
-class TestParser < Test::Unit::TestCase
- include PuppetTest::ParserTesting
-
- def setup
- super
- Puppet[:parseonly] = true
- end
-
- def test_simple_hostname
- check_parseable "host1"
- check_parseable "'host2'"
- check_parseable "\"host3\""
- check_parseable [ "'host1'", "host2" ]
- check_parseable [ "'host1'", "'host2'" ]
- check_parseable [ "'host1'", "\"host2\"" ]
- check_parseable [ "\"host1\"", "host2" ]
- check_parseable [ "\"host1\"", "'host2'" ]
- check_parseable [ "\"host1\"", "\"host2\"" ]
- end
-
- def test_qualified_hostname
- check_parseable "'host.example.com'"
- check_parseable "\"host.example.com\""
- check_parseable [ "'host.example.com'", "host1" ]
- check_parseable [ "\"host.example.com\"", "host1" ]
- check_parseable "'host-1.37examples.example.com'"
- check_parseable "\"host-1.37examples.example.com\""
- check_parseable "'svn.23.nu'"
- check_parseable "\"svn.23.nu\""
- check_parseable "'HOST'"
- check_parseable "\"HOST\""
- end
-
- def test_inherits_from_default
- check_parseable(["default", "host1"], "node default {}\nnode host1 inherits default {}")
- end
-
- def test_reject_hostname
- check_nonparseable "host.example.com"
- check_nonparseable "host@example.com"
- check_nonparseable "'$foo.example.com'"
- check_nonparseable "\"$foo.example.com\""
- check_nonparseable "'host1 host2'"
- check_nonparseable "\"host1 host2\""
- check_nonparseable "HOST"
- end
-
- AST = Puppet::Parser::AST
-
- def check_parseable(hostnames, code = nil)
- unless hostnames.is_a?(Array)
- hostnames = [ hostnames ]
- end
- interp = nil
- code ||= "node #{hostnames.join(", ")} { }"
- parser = mkparser
- parser.string = code
- # Strip quotes
- hostnames.map! { |s| s.sub(/^['"](.*)['"]$/, "\\1") }
-
- # parse
- assert_nothing_raised("Could not parse '%s'" % code) {
- parser.parse
- }
-
- # Now make sure we can look up each of the names
- hostnames.each do |name|
- assert(parser.findnode(name),
- "Could not find node %s" % name.inspect)
- end
- end
-
- def check_nonparseable(hostname)
- interp = nil
- parser = mkparser
- parser.string = "node #{hostname} { }"
- assert_raise(Puppet::DevError, Puppet::ParseError, "#{hostname} passed") {
- parser.parse
- }
- end
-
- # Make sure we can find default nodes if there's no other entry
- def test_default_node
- Puppet[:parseonly] = false
-
- fileA = tempfile()
- fileB = tempfile()
- code = %{
-node mynode {
- file { "#{fileA}": ensure => file }
-}
-
-node default {
- file { "#{fileB}": ensure => file }
-}
-}
- interp = nil
- assert_nothing_raised {
- interp = mkinterp :Code => code
- }
-
- # First make sure it parses
- assert_nothing_raised {
- interp.send(:parsefiles)
- }
-
- # Make sure we find our normal node
- assert(interp.nodesearch("mynode"),
- "Did not find normal node")
-
- # Now look for the default node
- default = interp.nodesearch("someother")
- assert(default,
- "Did not find default node")
-
- assert_equal("default", default.classname)
- end
-end
diff --git a/test/network/handler/node.rb b/test/network/handler/node.rb
new file mode 100755
index 000000000..9fad3f765
--- /dev/null
+++ b/test/network/handler/node.rb
@@ -0,0 +1,677 @@
+#!/usr/bin/env ruby
+
+$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/
+
+require 'mocha'
+require 'puppettest'
+require 'puppettest/resourcetesting'
+require 'puppettest/parsertesting'
+require 'puppettest/servertest'
+require 'puppet/network/handler/node'
+
+module NodeTesting
+ include PuppetTest
+ Node = Puppet::Network::Handler::Node
+ SimpleNode = Puppet::Network::Handler::Node::SimpleNode
+
+ def mk_node_mapper
+ # First, make sure our nodesearch command works as we expect
+ # Make a nodemapper
+ mapper = tempfile()
+ ruby = %x{which ruby}.chomp
+ File.open(mapper, "w") { |f|
+ f.puts "#!#{ruby}
+ require 'yaml'
+ name = ARGV.last.chomp
+ result = {}
+
+ if name =~ /a/
+ result[:parameters] = {'one' => ARGV.last + '1', 'two' => ARGV.last + '2'}
+ end
+
+ if name =~ /p/
+ result['classes'] = [1,2,3].collect { |n| ARGV.last + n.to_s }
+ end
+
+ puts YAML.dump(result)
+ "
+ }
+ File.chmod(0755, mapper)
+ mapper
+ end
+
+ def mk_searcher(name)
+ searcher = Object.new
+ searcher.extend(Node.node_source(name))
+ searcher.meta_def(:newnode) do |name, *args|
+ SimpleNode.new(name, *args)
+ end
+ searcher
+ end
+
+ def mk_node_source
+ @node_info = {}
+ @node_source = Node.newnode_source(:testing, :fact_merge => true) do
+ def nodesearch(key)
+ if info = @node_info[key]
+ SimpleNode.new(info)
+ else
+ nil
+ end
+ end
+ end
+ Puppet[:node_source] = "testing"
+
+ cleanup { Node.rm_node_source(:testing) }
+ end
+end
+
+class TestNodeInterface < Test::Unit::TestCase
+ include NodeTesting
+
+ def setup
+ super
+ mk_node_source
+ end
+
+ # Make sure that the handler includes the appropriate
+ # node source.
+ def test_initialize
+ # First try it when passing in the node source
+ handler = nil
+ assert_nothing_raised("Could not specify a node source") do
+ handler = Node.new(:Source => :testing)
+ end
+ assert(handler.metaclass.included_modules.include?(@node_source), "Handler did not include node source")
+
+ # Now use the Puppet[:node_source]
+ Puppet[:node_source] = "testing"
+ assert_nothing_raised("Could not specify a node source") do
+ handler = Node.new()
+ end
+ assert(handler.metaclass.included_modules.include?(@node_source), "Handler did not include node source")
+
+ # And make sure we throw an exception when an invalid node source is used
+ assert_raise(ArgumentError, "Accepted an invalid node source") do
+ handler = Node.new(:Source => "invalid")
+ end
+ end
+
+ # Make sure we can find and we cache a fact handler.
+ def test_fact_handler
+ handler = Node.new
+ fhandler = nil
+ assert_nothing_raised("Could not retrieve the fact handler") do
+ fhandler = handler.send(:fact_handler)
+ end
+ assert_instance_of(Puppet::Network::Handler::Facts, fhandler, "Did not get a fact handler back")
+
+ # Now call it again, making sure we're caching the value.
+ fhandler2 = nil
+ assert_nothing_raised("Could not retrieve the fact handler") do
+ fhandler2 = handler.send(:fact_handler)
+ end
+ assert_instance_of(Puppet::Network::Handler::Facts, fhandler2, "Did not get a fact handler on the second run")
+ assert_equal(fhandler.object_id, fhandler2.object_id, "Did not cache fact handler")
+ end
+
+ # Make sure we can get node facts from the fact handler.
+ def test_node_facts
+ # Check the case where we find the node.
+ handler = Node.new
+ fhandler = handler.send(:fact_handler)
+ fhandler.expects(:get).with("present").returns("a" => "b")
+
+ result = nil
+ assert_nothing_raised("Could not get facts from fact handler") do
+ result = handler.send(:node_facts, "present")
+ end
+ assert_equal({"a" => "b"}, result, "Did not get correct facts back")
+
+ # Now try the case where the fact handler knows nothing about our host
+ fhandler.expects(:get).with('missing').returns(nil)
+ result = nil
+ assert_nothing_raised("Could not get facts from fact handler when host is missing") do
+ result = handler.send(:node_facts, "missing")
+ end
+ assert_equal({}, result, "Did not get empty hash when no facts are known")
+ end
+
+ # Test our simple shorthand
+ def test_newnode
+ SimpleNode.expects(:new).with("stuff")
+ handler = Node.new
+ handler.send(:newnode, "stuff")
+ end
+
+ # Make sure we can build up the correct node names to search for
+ def test_node_names
+ handler = Node.new
+
+ # Verify that the handler asks for the facts if we don't pass them in
+ handler.expects(:node_facts).with("testing").returns({})
+ handler.send(:node_names, "testing")
+
+ handler = Node.new
+ # Test it first with no parameters
+ assert_equal(%w{testing}, handler.send(:node_names, "testing"), "Node names did not default to an array including just the node name")
+
+ # Now test it with a fully qualified name
+ assert_equal(%w{testing.domain.com testing}, handler.send(:node_names, "testing.domain.com"),
+ "Fully qualified names did not get turned into multiple names, longest first")
+
+ # And try it with a short name + domain fact
+ assert_equal(%w{testing host.domain.com host}, handler.send(:node_names, "testing", "domain" => "domain.com", "hostname" => "host"),
+ "The domain fact was not used to build up an fqdn")
+
+ # And with an fqdn
+ assert_equal(%w{testing host.domain.com host}, handler.send(:node_names, "testing", "fqdn" => "host.domain.com"),
+ "The fqdn was not used")
+
+ # And make sure the fqdn beats the domain
+ assert_equal(%w{testing host.other.com host}, handler.send(:node_names, "testing", "domain" => "domain.com", "fqdn" => "host.other.com"),
+ "The domain was used in preference to the fqdn")
+ end
+
+ # Make sure we can retrieve a whole node by name.
+ def test_details_when_we_find_nodes
+ handler = Node.new
+
+ # Make sure we get the facts first
+ handler.expects(:node_facts).with("host").returns(:facts)
+
+ # Find the node names
+ handler.expects(:node_names).with("host", :facts).returns(%w{a b c})
+
+ # Iterate across them
+ handler.expects(:nodesearch).with("a").returns(nil)
+ handler.expects(:nodesearch).with("b").returns(nil)
+
+ # Create an example node to return
+ node = SimpleNode.new("host")
+
+ # Make sure its source is set
+ node.expects(:source=).with(handler.source)
+
+ # And make sure we actually get it back
+ handler.expects(:nodesearch).with("c").returns(node)
+
+ handler.expects(:fact_merge?).returns(true)
+
+ # Make sure we merge the facts with the node's parameters.
+ node.expects(:fact_merge).with(:facts)
+
+ # Now call the method
+ result = nil
+ assert_nothing_raised("could not call 'details'") do
+ result = handler.details("host")
+ end
+ assert_equal(node, result, "Did not get correct node back")
+ end
+
+ # But make sure we pass through to creating default nodes when appropriate.
+ def test_details_using_default_node
+ handler = Node.new
+
+ # Make sure we get the facts first
+ handler.expects(:node_facts).with("host").returns(:facts)
+
+ # Find the node names
+ handler.expects(:node_names).with("host", :facts).returns([])
+
+ # Create an example node to return
+ node = SimpleNode.new("host")
+
+ # Make sure its source is set
+ node.expects(:source=).with(handler.source)
+
+ # And make sure we actually get it back
+ handler.expects(:nodesearch).with("default").returns(node)
+
+ # This time, have it return false
+ handler.expects(:fact_merge?).returns(false)
+
+ # And because fact_merge was false, we don't merge them.
+ node.expects(:fact_merge).never
+
+ # Now call the method
+ result = nil
+ assert_nothing_raised("could not call 'details'") do
+ result = handler.details("host")
+ end
+ assert_equal(node, result, "Did not get correct node back")
+ end
+
+ # Make sure our handler behaves rationally when it comes to getting environment data.
+ def test_environment
+ # What happens when we can't find the node
+ handler = Node.new
+ handler.expects(:details).with("fake").returns(nil)
+
+ result = nil
+ assert_nothing_raised("Could not call 'Node.environment'") do
+ result = handler.environment("fake")
+ end
+ assert_nil(result, "Got an environment for a node we could not find")
+
+ # Now for nodes we can find
+ handler = Node.new
+ node = SimpleNode.new("fake")
+ handler.expects(:details).with("fake").returns(node)
+ node.expects(:environment).returns("dev")
+
+ result = nil
+ assert_nothing_raised("Could not call 'Node.environment'") do
+ result = handler.environment("fake")
+ end
+ assert_equal("dev", result, "Did not get environment back")
+ end
+
+ # Make sure our handler behaves rationally when it comes to getting parameter data.
+ def test_parameters
+ # What happens when we can't find the node
+ handler = Node.new
+ handler.expects(:details).with("fake").returns(nil)
+
+ result = nil
+ assert_nothing_raised("Could not call 'Node.parameters'") do
+ result = handler.parameters("fake")
+ end
+ assert_nil(result, "Got parameters for a node we could not find")
+
+ # Now for nodes we can find
+ handler = Node.new
+ node = SimpleNode.new("fake")
+ handler.expects(:details).with("fake").returns(node)
+ node.expects(:parameters).returns({"a" => "b"})
+
+ result = nil
+ assert_nothing_raised("Could not call 'Node.parameters'") do
+ result = handler.parameters("fake")
+ end
+ assert_equal({"a" => "b"}, result, "Did not get parameters back")
+ end
+
+ def test_classes
+ # What happens when we can't find the node
+ handler = Node.new
+ handler.expects(:details).with("fake").returns(nil)
+
+ result = nil
+ assert_nothing_raised("Could not call 'Node.classes'") do
+ result = handler.classes("fake")
+ end
+ assert_nil(result, "Got classes for a node we could not find")
+
+ # Now for nodes we can find
+ handler = Node.new
+ node = SimpleNode.new("fake")
+ handler.expects(:details).with("fake").returns(node)
+ node.expects(:classes).returns(%w{yay foo})
+
+ result = nil
+ assert_nothing_raised("Could not call 'Node.classes'") do
+ result = handler.classes("fake")
+ end
+ assert_equal(%w{yay foo}, result, "Did not get classes back")
+ end
+end
+
+class TestSimpleNode < Test::Unit::TestCase
+ include NodeTesting
+
+ # Make sure we get all the defaults correctly.
+ def test_simplenode_initialize
+ node = nil
+ assert_nothing_raised("could not create a node without classes or parameters") do
+ node = SimpleNode.new("testing")
+ end
+ assert_equal("testing", node.name, "Did not set name correctly")
+ assert_equal({}, node.parameters, "Node parameters did not default correctly")
+ assert_equal([], node.classes, "Node classes did not default correctly")
+
+ # Now test it with values for both
+ params = {"a" => "b"}
+ classes = %w{one two}
+ assert_nothing_raised("could not create a node with classes and parameters") do
+ node = SimpleNode.new("testing", :parameters => params, :classes => classes)
+ end
+ assert_equal("testing", node.name, "Did not set name correctly")
+ assert_equal(params, node.parameters, "Node parameters did not get set correctly")
+ assert_equal(classes, node.classes, "Node classes did not get set correctly")
+
+ # And make sure a single class gets turned into an array
+ assert_nothing_raised("could not create a node with a class as a string") do
+ node = SimpleNode.new("testing", :classes => "test")
+ end
+ assert_equal(%w{test}, node.classes, "A node class string was not converted to an array")
+
+ # Make sure we get environments
+ assert_nothing_raised("could not create a node with an environment") do
+ node = SimpleNode.new("testing", :environment => "test")
+ end
+ assert_equal("test", node.environment, "Environment was not set")
+
+ # Now make sure we get the default env
+ Puppet[:environment] = "prod"
+ assert_nothing_raised("could not create a node with no environment") do
+ node = SimpleNode.new("testing")
+ end
+ assert_equal("prod", node.environment, "Did not get default environment")
+
+ # But that it stays nil if there's no default env set
+ Puppet[:environment] = ""
+ assert_nothing_raised("could not create a node with no environment and no default env") do
+ node = SimpleNode.new("testing")
+ end
+ assert_nil(node.environment, "Got a default env when none was set")
+
+ end
+
+ # Verify that the node source wins over facter.
+ def test_fact_merge
+ node = SimpleNode.new("yay", :parameters => {"a" => "one", "b" => "two"})
+
+ assert_nothing_raised("Could not merge parameters") do
+ node.fact_merge("b" => "three", "c" => "yay")
+ end
+ params = node.parameters
+ assert_equal("one", params["a"], "Lost nodesource parameters in parameter merge")
+ assert_equal("two", params["b"], "Overrode nodesource parameters in parameter merge")
+ assert_equal("yay", params["c"], "Did not get facts in parameter merge")
+ end
+end
+
+# Test our configuration object.
+class TestNodeSources < Test::Unit::TestCase
+ include NodeTesting
+
+ def test_node_sources
+ mod = nil
+ assert_nothing_raised("Could not add new search type") do
+ mod = Node.newnode_source(:testing) do
+ def nodesearch(name)
+ end
+ end
+ end
+ assert_equal(mod, Node.node_source(:testing), "Did not get node_source back")
+
+ cleanup do
+ Node.rm_node_source(:testing)
+ assert(! Node.const_defined?("Testing"), "Did not remove constant")
+ end
+ end
+
+ def test_external_node_source
+ source = Node.node_source(:external)
+ assert(source, "Could not find external node source")
+ mapper = mk_node_mapper
+ searcher = mk_searcher(:external)
+ assert(searcher.fact_merge?, "External node source does not merge facts")
+
+ # Make sure it gives the right response
+ assert_equal({'classes' => %w{apple1 apple2 apple3}, :parameters => {"one" => "apple1", "two" => "apple2"}},
+ YAML.load(%x{#{mapper} apple}))
+
+ # First make sure we get nil back by default
+ assert_nothing_raised {
+ assert_nil(searcher.nodesearch("apple"),
+ "Interp#nodesearch_external defaulted to a non-nil response")
+ }
+ assert_nothing_raised { Puppet[:external_nodes] = mapper }
+
+ node = nil
+ # Both 'a' and 'p', so we get classes and parameters
+ assert_nothing_raised { node = searcher.nodesearch("apple") }
+ assert_equal("apple", node.name, "node name was not set correctly for apple")
+ assert_equal(%w{apple1 apple2 apple3}, node.classes, "node classes were not set correctly for apple")
+ assert_equal( {"one" => "apple1", "two" => "apple2"}, node.parameters, "node parameters were not set correctly for apple")
+
+ # A 'p' but no 'a', so we only get classes
+ assert_nothing_raised { node = searcher.nodesearch("plum") }
+ assert_equal("plum", node.name, "node name was not set correctly for plum")
+ assert_equal(%w{plum1 plum2 plum3}, node.classes, "node classes were not set correctly for plum")
+ assert_equal({}, node.parameters, "node parameters were not set correctly for plum")
+
+ # An 'a' but no 'p', so we only get parameters.
+ assert_nothing_raised { node = searcher.nodesearch("guava")} # no p's, thus no classes
+ assert_equal("guava", node.name, "node name was not set correctly for guava")
+ assert_equal([], node.classes, "node classes were not set correctly for guava")
+ assert_equal({"one" => "guava1", "two" => "guava2"}, node.parameters, "node parameters were not set correctly for guava")
+
+ assert_nothing_raised { node = searcher.nodesearch("honeydew")} # neither, thus nil
+ assert_nil(node)
+ end
+
+ # Make sure a nodesearch with arguments works
+ def test_nodesearch_external_arguments
+ mapper = mk_node_mapper
+ Puppet[:external_nodes] = "#{mapper} -s something -p somethingelse"
+ searcher = mk_searcher(:external)
+ node = nil
+ assert_nothing_raised do
+ node = searcher.nodesearch("apple")
+ end
+ assert_instance_of(SimpleNode, node, "did not create node")
+ end
+
+ # A wrapper test, to make sure we're correctly calling the external search method.
+ def test_nodesearch_external_functional
+ mapper = mk_node_mapper
+ searcher = mk_searcher(:external)
+
+ Puppet[:external_nodes] = mapper
+
+ node = nil
+ assert_nothing_raised do
+ node = searcher.nodesearch("apple")
+ end
+ assert_instance_of(SimpleNode, node, "did not create node")
+ end
+
+ # This can stay in the main test suite because it doesn't actually use ldapsearch,
+ # it just overrides the method so it behaves as though it were hitting ldap.
+ def test_ldap_nodesearch
+ source = Node.node_source(:ldap)
+ assert(source, "Could not find ldap node source")
+ searcher = mk_searcher(:ldap)
+ assert(searcher.fact_merge?, "LDAP node source does not merge facts")
+
+ nodetable = {}
+
+ # Override the ldapsearch definition, so we don't have to actually set it up.
+ searcher.meta_def(:ldapsearch) do |name|
+ nodetable[name]
+ end
+
+ # Make sure we get nothing for nonexistent hosts
+ node = nil
+ assert_nothing_raised do
+ node = searcher.nodesearch("nosuchhost")
+ end
+
+ assert_nil(node, "Got a node for a non-existent host")
+
+ # Now add a base node with some classes and parameters
+ nodetable["base"] = [nil, %w{one two}, {"base" => "true"}]
+
+ assert_nothing_raised do
+ node = searcher.nodesearch("base")
+ end
+
+ assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch")
+ assert_equal("base", node.name, "node name was not set")
+
+ assert_equal(%w{one two}, node.classes, "node classes were not set")
+ assert_equal({"base" => "true"}, node.parameters, "node parameters were not set")
+
+ # Now use a different with this as the base
+ nodetable["middle"] = ["base", %w{three}, {"center" => "boo"}]
+ assert_nothing_raised do
+ node = searcher.nodesearch("middle")
+ end
+
+ assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch")
+ assert_equal("middle", node.name, "node name was not set")
+
+ assert_equal(%w{one two three}.sort, node.classes.sort, "node classes were not set correctly with a parent node")
+ assert_equal({"base" => "true", "center" => "boo"}, node.parameters, "node parameters were not set correctly with a parent node")
+
+ # And one further, to make sure we fully recurse
+ nodetable["top"] = ["middle", %w{four five}, {"master" => "far"}]
+ assert_nothing_raised do
+ node = searcher.nodesearch("top")
+ end
+
+ assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch")
+ assert_equal("top", node.name, "node name was not set")
+
+ assert_equal(%w{one two three four five}.sort, node.classes.sort, "node classes were not set correctly with the top node")
+ assert_equal({"base" => "true", "center" => "boo", "master" => "far"}, node.parameters, "node parameters were not set correctly with the top node")
+ end
+
+ # Make sure we always get a node back from the 'none' nodesource.
+ def test_nodesource_none
+ source = Node.node_source(:none)
+ assert(source, "Could not find 'none' node source")
+ searcher = mk_searcher(:none)
+ assert(searcher.fact_merge?, "'none' node source does not merge facts")
+
+ # Run a couple of node names through it
+ node = nil
+ %w{192.168.0.1 0:0:0:3:a:f host host.domain.com}.each do |name|
+ assert_nothing_raised("Could not create an empty node with name '%s'" % name) do
+ node = searcher.nodesearch(name)
+ end
+ assert_instance_of(SimpleNode, node, "Did not get a simple node back for %s" % name)
+ assert_equal(name, node.name, "Name was not set correctly")
+ end
+ end
+end
+
+class LdapNodeTest < PuppetTest::TestCase
+ include NodeTesting
+ include PuppetTest::ServerTest
+ include PuppetTest::ParserTesting
+ include PuppetTest::ResourceTesting
+ AST = Puppet::Parser::AST
+ confine "LDAP is not available" => Puppet.features.ldap?
+ confine "No LDAP test data for networks other than Luke's" => Facter.value(:domain) == "madstop.com"
+
+ def ldapconnect
+
+ @ldap = LDAP::Conn.new("ldap", 389)
+ @ldap.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 )
+ @ldap.simple_bind("", "")
+
+ return @ldap
+ end
+
+ def ldaphost(name)
+ node = NodeDef.new(:name => name)
+ parent = nil
+ found = false
+ @ldap.search( "ou=hosts, dc=madstop, dc=com", 2,
+ "(&(objectclass=puppetclient)(cn=%s))" % name
+ ) do |entry|
+ node.classes = entry.vals("puppetclass") || []
+ node.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
+ parent = node.parameters["parentnode"]
+ found = true
+ end
+ raise "Could not find node %s" % name unless found
+
+ return node, parent
+ end
+
+ def test_ldapsearch
+ Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com"
+ Puppet[:ldapnodes] = true
+
+ searcher = Object.new
+ searcher.extend(Node.node_source(:ldap))
+
+ ldapconnect()
+
+ # Make sure we get nil and nil back when we search for something missing
+ parent, classes, parameters = nil
+ assert_nothing_raised do
+ parent, classes, parameters = searcher.ldapsearch("nosuchhost")
+ end
+
+ assert_nil(parent, "Got a parent for a non-existent host")
+ assert_nil(classes, "Got classes for a non-existent host")
+
+ # Make sure we can find 'culain' in ldap
+ assert_nothing_raised do
+ parent, classes, parameters = searcher.ldapsearch("culain")
+ end
+
+ node, realparent = ldaphost("culain")
+ assert_equal(realparent, parent, "did not get correct parent node from ldap")
+ assert_equal(node.classes, classes, "did not get correct ldap classes from ldap")
+ assert_equal(node.parameters, parameters, "did not get correct ldap parameters from ldap")
+
+ # Now compare when we specify the attributes to get.
+ Puppet[:ldapattrs] = "cn"
+ assert_nothing_raised do
+ parent, classes, parameters = searcher.ldapsearch("culain")
+ end
+ assert_equal(realparent, parent, "did not get correct parent node from ldap")
+ assert_equal(node.classes, classes, "did not get correct ldap classes from ldap")
+
+ list = %w{cn puppetclass parentnode dn}
+ should = node.parameters.inject({}) { |h, a| h[a[0]] = a[1] if list.include?(a[0]); h }
+ assert_equal(should, parameters, "did not get correct ldap parameters from ldap")
+ end
+end
+
+class LdapReconnectTests < PuppetTest::TestCase
+ include NodeTesting
+ include PuppetTest::ServerTest
+ include PuppetTest::ParserTesting
+ include PuppetTest::ResourceTesting
+ AST = Puppet::Parser::AST
+ confine "Not running on culain as root" => (Puppet::Util::SUIDManager.uid == 0 and Facter.value("hostname") == "culain")
+
+ def test_ldapreconnect
+ Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com"
+ Puppet[:ldapnodes] = true
+
+ searcher = Object.new
+ searcher.extend(Node.node_source(:ldap))
+ hostname = "culain.madstop.com"
+
+ # look for our host
+ assert_nothing_raised {
+ parent, classes = searcher.nodesearch(hostname)
+ }
+
+ # Now restart ldap
+ system("/etc/init.d/slapd restart 2>/dev/null >/dev/null")
+ sleep(1)
+
+ # and look again
+ assert_nothing_raised {
+ parent, classes = searcher.nodesearch(hostname)
+ }
+
+ # Now stop ldap
+ system("/etc/init.d/slapd stop 2>/dev/null >/dev/null")
+ cleanup do
+ system("/etc/init.d/slapd start 2>/dev/null >/dev/null")
+ end
+
+ # And make sure we actually fail here
+ assert_raise(Puppet::Error) {
+ parent, classes = searcher.nodesearch(hostname)
+ }
+ end
+end