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..0c532144a --- /dev/null +++ b/lib/puppet/network/handler/node.rb @@ -0,0 +1,146 @@ +# 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 + + def initialize(options) + unless @name = options[:name] + raise ArgumentError, "Nodes require names" unless self.name + end + + if classes = options[:classes] + if classes.is_a?(String) + @classes = [classes] + else + @classes = classes + end + else + @classes = [] + end + + @parameters = options[:parameters] || {} + end + end + desc "Retrieve information about nodes." + + extend Puppet::Util::ClassGen + extend Puppet::Util::InstanceLoader + + module SourceBase + include Puppet::Util::Docs + end + + @interface = XMLRPC::Service::Interface.new("nodes") { |iface| + iface.add_method("string node(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' + + # Add a new node source. + def self.newnode_source(name, options = {}, &block) + name = symbolize(name) + + genmodule(name, :extend => SourceBase, :hash => instance_hash(:node_source), :block => block) + 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) + instance_hash(:node_source).delete(name) + end + + # Return a given node's classes. + def classes(key) + raise "look up classes" + end + + # Return a given node's environment. + def environment(key) + raise "look up environment" + if node = node(key) + node.environment + else + nil + end + end + + # Return an entire node configuration. + def node(key) + # Try to find our node... + nodes = nodes.collect { |n| n.to_s.downcase } + + method = "nodesearch_%s" % @nodesource + # Do an inverse sort on the length, so the longest match always + # wins + nodes.sort { |a,b| b.length <=> a.length }.each do |node| + node = node.to_s if node.is_a?(Symbol) + if obj = self.send(method, node) + if obj.is_a?(AST::Node) + nsource = obj.file + else + nsource = obj.source + end + Puppet.info "Found %s in %s" % [node, nsource] + return obj + end + end + + # If they made it this far, we haven't found anything, so look for a + # default node. + unless nodes.include?("default") + if defobj = self.nodesearch("default") + Puppet.notice "Using default node for %s" % [nodes[0]] + return defobj + end + end + + return nil + end + + def parameters(key) + raise "Look up parameters" + end + + private + def node_facts(key) + raise "Look up node facts" + end + + def node_names(key, facts = nil) + facts ||= node_facts(key) + raise "Calculate node names" + end +end diff --git a/lib/puppet/node_source/external.rb b/lib/puppet/node_source/external.rb new file mode 100644 index 000000000..4af68d8ae --- /dev/null +++ b/lib/puppet/node_source/external.rb @@ -0,0 +1,51 @@ +Puppet::Network::Handler::Node.newnode_source(:external) 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 = Puppet::Network::Handler::Node::SimpleNode.new(:name => 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..6825f2b68 --- /dev/null +++ b/lib/puppet/node_source/ldap.rb @@ -0,0 +1,118 @@ +Puppet::Network::Handler::Node.newnode_source(:ldap) 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 Puppet::Network::Handler::Node::SimpleNode.new(:name => node, :classes => classes, :source => "ldap", :parameters => parameters) + 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 @@ -1,78 +1,81 @@ require 'puppet/parser/ast/component' class Puppet::Parser::AST # The code associated with a class. This is different from components # in that each class is a singleton -- only one will exist for a given # node. class HostClass < AST::Component @name = :class # Are we a child of the passed class? Do a recursive search up our # parentage tree to figure it out. def child_of?(klass) return false unless self.parentclass if klass == self.parentobj return true else return self.parentobj.child_of?(klass) end end # Evaluate the code associated with this class. def evaluate(hash) 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 pnames = nil if pklass = self.parentobj pklass.safeevaluate :scope => scope scope = parent_scope(scope, pklass) pnames = scope.namespaces end unless hash[:nosubscope] scope = subscope(scope) end if pnames pnames.each do |ns| scope.add_namespace(ns) end end # Set the class before we do anything else, so that it's set # during the evaluation and can be inspected. scope.setclass(self) # Now evaluate our code, yo. if self.code return self.code.evaluate(:scope => scope) else return nil end end def initialize(hash) @parentclass = nil super end def parent_scope(scope, klass) if s = scope.class_scope(klass) return s else raise Puppet::DevError, "Could not find scope for %s" % klass.fqname end end end end # $Id$ 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 @@ -1,713 +1,550 @@ require 'puppet' require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' require 'puppet/parser/scope' # The interpreter's job is to convert from a parsed file to the configuration # for a given client. It really doesn't do any work on its own, it just collects # and calls out to other objects. class Puppet::Parser::Interpreter class NodeDef include Puppet::Util::MethodHelper attr_accessor :name, :classes, :parameters, :source def evaluate(options) begin parameters.each do |param, value| # Don't try to override facts with these parameters options[:scope].setvar(param, value) unless options[:scope].lookupvar(param, false) != :undefined end # Also, set the 'nodename', since it might not be obvious how the node was looked up options[:scope].setvar("nodename", @name) unless options[:scope].lookupvar(@nodename, false) != :undefined rescue => detail raise Puppet::ParseError, "Could not set parameters for %s: %s" % [name, detail] end # Then evaluate the classes. begin options[:scope].function_include(classes.find_all { |c| options[:scope].findclass(c) }) rescue => detail puts detail.backtrace raise Puppet::ParseError, "Could not evaluate classes for %s: %s" % [name, detail] end end def initialize(args) set_options(args) raise Puppet::DevError, "NodeDefs require names" unless self.name if self.classes.is_a?(String) @classes = [@classes] else @classes ||= [] end @parameters ||= {} end def safeevaluate(args) evaluate(args) end end include Puppet::Util attr_accessor :usenodes class << self attr_writer :ldap end # just shorten the constant path a bit, using what amounts to an alias AST = Puppet::Parser::AST include Puppet::Util::Errors # Create an ldap connection. This is a class method so others can call # it and use the same variables and such. def self.ldap unless defined? @ldap and @ldap 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]) end return @ldap end # Make sure we don't have any remaining collections that specifically # look for resources, because we want to consider those to be # parse errors. def check_resource_collections(scope) remaining = [] scope.collections.each do |coll| if r = coll.resources if r.is_a?(Array) remaining += r else remaining << r end end end unless remaining.empty? raise Puppet::ParseError, "Failed to find virtual resources %s" % remaining.join(', ') end end # Iteratively evaluate all of the objects. This finds all of the objects # that represent definitions and evaluates the definitions appropriately. # It also adds defaults and overrides as appropriate. def evaliterate(scope) count = 0 loop do count += 1 done = true # First perform collections, so we can collect defined types. if coll = scope.collections and ! coll.empty? exceptwrap do coll.each do |c| # Only keep the loop going if we actually successfully # collected something. if o = c.evaluate done = false end end end end # Then evaluate any defined types. if ary = scope.unevaluated ary.each do |resource| resource.evaluate end # If we evaluated, then loop through again. done = false end break if done if count > 1000 raise Puppet::ParseError, "Got 1000 class levels, which is unsupported" end end end # Evaluate a specific node. def evalnode(client, scope, facts) return unless self.usenodes unless client raise Puppet::Error, "Cannot evaluate nodes with a nil client" end names = [client] # Make sure both the fqdn and the short name of the # host can be used in the manifest if client =~ /\./ names << client.sub(/\..+/,'') else names << "#{client}.#{facts['domain']}" end if names.empty? raise Puppet::Error, "Cannot evaluate nodes with a nil client" end # Look up our node object. if nodeclass = nodesearch(*names) nodeclass.safeevaluate :scope => scope else raise Puppet::Error, "Could not find %s with names %s" % [client, names.join(", ")] end end # 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" scope.host = client || facts["hostname"] || Facter.value(:hostname) classes = @classes.dup # Okay, first things first. Set our facts. scope.setfacts(facts) # Everyone will always evaluate the top-level class, if there is one. if klass = findclass("", "") # Set the source, so objects can tell where they were defined. scope.source = klass klass.safeevaluate :scope => scope, :nosubscope => true end # Next evaluate the node. We pass the facts so they can be used # when building the list of names for which to search. evalnode(client, scope, facts) # If we were passed any classes, evaluate those. if classes classes.each do |klass| if klassobj = findclass("", klass) klassobj.safeevaluate :scope => scope end end end # That was the first pass evaluation. Now iteratively evaluate # until we've gotten rid of all of everything or thrown an error. evaliterate(scope) # Now make sure we fail if there's anything left to do failonleftovers(scope) # Now finish everything. This recursively calls finish on the # contained scopes and resources. scope.finish # Store everything. We need to do this before translation, because # it operates on resources, not transobjects. if Puppet[:storeconfigs] args = { :resources => scope.resources, :name => scope.host, :facts => facts } unless scope.classlist.empty? args[:classes] = scope.classlist end storeconfigs(args) end # Now, finally, convert our scope tree + resources into a tree of # buckets and objects. objects = scope.translate # Add the class list unless scope.classlist.empty? objects.classes = scope.classlist end return objects end # Fail if there any overrides left to perform. def failonleftovers(scope) overrides = scope.overrides unless overrides.empty? fail Puppet::ParseError, "Could not find object(s) %s" % overrides.collect { |o| o.ref }.join(", ") end # Now check that there aren't any extra resource collections. check_resource_collections(scope) end # Create proxy methods, so the scopes can call the interpreter, since # they don't have access to the parser. def findclass(namespace, name) @parser.findclass(namespace, name) end def finddefine(namespace, name) @parser.finddefine(namespace, name) end # create our interpreter def initialize(hash) if @code = hash[:Code] @file = nil # to avoid warnings elsif ! @file = hash[:Manifest] devfail "You must provide code or a manifest" end if hash.include?(:UseNodes) @usenodes = hash[:UseNodes] else @usenodes = true end if Puppet[:ldapnodes] # Nodes in the file override nodes in ldap. @nodesource = :ldap elsif Puppet[:external_nodes] != "none" @nodesource = :external else # By default, we only search for parsed nodes. @nodesource = :code end @setup = false # Set it to either the value or nil. This is currently only used # by the cfengine module. @classes = hash[:Classes] || [] @local = hash[:Local] || false if hash.include?(:ForkSave) @forksave = hash[:ForkSave] else # This is just too dangerous right now. Sorry, it's going # to have to be slow. @forksave = false end # The class won't always be defined during testing. if Puppet[:storeconfigs] if Puppet.features.rails? Puppet::Rails.init else raise Puppet::Error, "Rails is missing; cannot store configurations" end end @files = [] # Create our parser object 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| @parser.send(name, *args) end end # Add a new file to be checked when we're checking to see if we should be # reparsed. def newfile(*files) files.each do |file| unless file.is_a? Puppet::Util::LoadedFile file = Puppet::Util::LoadedFile.new(file) end @files << file end end # Search for our node in the various locations. def nodesearch(*nodes) nodes = nodes.collect { |n| n.to_s.downcase } method = "nodesearch_%s" % @nodesource # Do an inverse sort on the length, so the longest match always # wins nodes.sort { |a,b| b.length <=> a.length }.each do |node| node = node.to_s if node.is_a?(Symbol) if obj = self.send(method, node) if obj.is_a?(AST::Node) nsource = obj.file else nsource = obj.source end Puppet.info "Found %s in %s" % [node, nsource] return obj end end # If they made it this far, we haven't found anything, so look for a # default node. unless nodes.include?("default") if defobj = self.nodesearch("default") Puppet.notice "Using default node for %s" % [nodes[0]] return defobj end end return nil end # See if our node was defined in the code. 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() @parsedate end # evaluate our whole tree def run(client, facts) # We have to leave this for after initialization because there # seems to be a problem keeping ldap open after a fork. unless @setup method = "setup_%s" % @nodesource.to_s if respond_to? method exceptwrap :type => Puppet::Error, :message => "Could not set up node source %s" % @nodesource do self.send(method) end end end parsefiles() # Evaluate all of the appropriate code. objects = evaluate(client, facts) # And return it all. return objects end # Connect to the LDAP Server def setup_ldap self.class.ldap = nil unless Puppet.features.ldap? Puppet.notice( "Could not set up LDAP Connection: Missing ruby/ldap libraries" ) @ldap = nil return end begin @ldap = self.class.ldap() rescue => detail raise Puppet::Error, "Could not connect to LDAP: %s" % detail end end def scope return @scope end private # Check whether any of our files have changed. def checkfiles if @files.find { |f| f.changed? } @parsedate = Time.now.to_i end end # Parse the files, generating our parse tree. This automatically # reparses only if files are updated, so it's safe to call multiple # times. def parsefiles # First check whether there are updates to any non-puppet files # like templates. If we need to reparse, this will get quashed, # but it needs to be done first in case there's no reparse # but there are other file changes. checkfiles() # Check if the parser should reparse. if @file if defined? @parser if stamp = @parser.reparse? Puppet.notice "Reloading files" else return false end end unless FileTest.exists?(@file) # If we've already parsed, then we're ok. if findclass("", "") return else raise Puppet::Error, "Manifest %s must exist" % @file end end end # Create a new parser, just to keep things fresh. Don't replace our # current parser until we know weverything works. newparser = Puppet::Parser::Parser.new() if @code newparser.string = @code else newparser.file = @file end # Parsing stores all classes and defines and such in their # various tables, so we don't worry about the return. begin if @local newparser.parse else benchmark(:info, "Parsed manifest") do newparser.parse end end # We've gotten this far, so it's ok to swap the parsers. oldparser = @parser @parser = newparser if oldparser oldparser.clear end # Mark when we parsed, so we can check freshness @parsedate = Time.now.to_i rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not parse; using old configuration: %s" % detail end end # Store the configs into the database. def storeconfigs(hash) unless Puppet.features.rails? raise Puppet::Error, "storeconfigs is enabled but rails is unavailable" end unless ActiveRecord::Base.connected? Puppet::Rails.connect end # Fork the storage, since we don't need the client waiting # on that. How do I avoid this duplication? if @forksave fork { # We store all of the objects, even the collectable ones benchmark(:info, "Stored configuration for #{hash[:name]}") do # Try to batch things a bit, by putting them into # a transaction Puppet::Rails::Host.transaction do Puppet::Rails::Host.store(hash) end end } else begin # We store all of the objects, even the collectable ones benchmark(:info, "Stored configuration for #{hash[:name]}") do Puppet::Rails::Host.transaction do Puppet::Rails::Host.store(hash) end end rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not store configs: %s" % detail.to_s end end end end # $Id$ diff --git a/lib/puppet/parser/node.rb b/lib/puppet/parser/node.rb new file mode 100644 index 000000000..c7979e51f --- /dev/null +++ b/lib/puppet/parser/node.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/scope.rb b/lib/puppet/parser/scope.rb index 6feeefc46..1fb4f6906 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,743 +1,601 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'puppet/parser/parser' require 'puppet/parser/templatewrapper' require 'puppet/transportable' require 'strscan' class Puppet::Parser::Scope require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) 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) if value == false or value == "" or value == :undef return false else return true end end # Add to our list of namespaces. def add_namespace(ns) return false if @namespaces.include?(ns) if @namespaces == [""] @namespaces = [ns] else @namespaces << ns end end # Is the type a builtin type? def builtintype?(type) if typeklass = Puppet::Type.type(type) return typeklass else return false end end # Create a new child scope. def child=(scope) @children.push(scope) # Copy all of the shared tables over to the child. @@sharedtables.each do |name| scope.send(name.to_s + "=", self.send(name)) end end # Verify that the given object isn't defined elsewhere. def chkobjectclosure(obj) if exobj = @definedtable[obj.ref] typeklass = Puppet::Type.type(obj.type) if typeklass and ! typeklass.isomorphic? Puppet.info "Allowing duplicate %s" % type else # Either it's a defined type, which are never # isomorphic, or it's a non-isomorphic type. msg = "Duplicate definition: %s is already defined" % obj.ref if exobj.file and exobj.line msg << " in file %s at line %s" % [exobj.file, exobj.line] end if obj.line or obj.file msg << "; cannot redefine" end raise Puppet::ParseError.new(msg) end end 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) end # Remove a resource from the various tables. This is only used when # a resource maps to a definition and gets evaluated. def deleteresource(resource) if @definedtable[resource.ref] @definedtable.delete(resource.ref) end if @children.include?(resource) @children.delete(resource) end end # Are we the top scope? def topscope? @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| yield child } end # Evaluate a list of classes. def evalclasses(*classes) retval = [] classes.each do |klass| if obj = findclass(klass) obj.safeevaluate :scope => self retval << klass end end retval end def exported? self.exported end def findclass(name) @namespaces.each do |namespace| if r = interp.findclass(namespace, name) return r end end return nil end def finddefine(name) @namespaces.each do |namespace| if r = interp.finddefine(namespace, name) return r end end return nil end def findresource(string, name = nil) if name string = "%s[%s]" % [string.capitalize, name] end @definedtable[string] end # Recursively complete the whole tree, in preparation for # translation or storage. def finish self.each do |obj| obj.finish end end # 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] @namespaces = [n] end hash.delete(:namespace) else @namespaces = [""] end hash.each { |name, val| method = name.to_s + "=" if self.respond_to? method self.send(method, val) else raise Puppet::DevError, "Invalid scope argument %s" % name end } @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 = [] # The symbol table for this scope. This is where we store variables. @symtable = {} # All of the defaults set for types. It's a hash of hashes, # with the first key being the type, then the second key being # the parameter. @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. # This is a different type of lookup because it's additive -- # it collects all of the defaults, with defaults in closer scopes # overriding those in later scopes. def lookupdefaults(type) 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 # then override them with any current values # this should probably be done differently if @defaultstable.include?(type) @defaultstable[type].each { |var,value| values[var] = value } end #Puppet.debug "Got defaults for %s: %s" % # [type,values.inspect] return values end # Look up all of the exported objects of a given type. def lookupexported(type) @definedtable.find_all do |name, r| r.type == type and r.exported? end end def lookupoverrides(obj) @overridetable[obj.ref] end # Look up a defined type. def lookuptype(name) finddefine(name) || findclass(name) end def lookup_qualified_var(name, usestring) parts = name.split(/::/) shortname = parts.pop klassname = parts.join("::") klass = findclass(klassname) unless klass raise Puppet::ParseError, "Could not find class %s" % klassname end unless kscope = class_scope(klass) raise Puppet::ParseError, "Class %s has not been evaluated so its variables cannot be referenced" % klass.classname end return kscope.lookupvar(shortname, usestring) end private :lookup_qualified_var # Look up a variable. The simplest value search we do. Default to returning # an empty string for missing values, but support returning a constant. def lookupvar(name, usestring = true) # If the variable is qualified, then find the specified scope and look the variable up there instead. if name =~ /::/ return lookup_qualified_var(name, usestring) end # We can't use "if @symtable[name]" here because the value might be false if @symtable.include?(name) if usestring and @symtable[name] == :undef return "" else return @symtable[name] end elsif self.parent - return @parent.lookupvar(name, usestring) + return parent.lookupvar(name, usestring) elsif usestring return "" else return :undefined end end def namespaces @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 #debug "Creating new scope, level %s" % [self.level + 1] return Puppet::Parser::Scope.new(hash) end # Is this class for a node? This is used to make sure that # nodes and classes with the same name conflict (#620), which # is required because of how often the names are used throughout # the system, including on the client. def nodescope? defined?(@nodescope) and @nodescope end # Return the list of remaining overrides. def overrides #@overridetable.collect { |name, overs| overs }.flatten @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 # Store the fact that we've evaluated a given class. We use a hash # 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 end # Set all of our facts in the top-level scope. def setfacts(facts) facts.each { |var, value| self.setvar(var, value) } end # Add a new object to our object table and the global list, and do any necessary # checks. def setresource(obj) self.chkobjectclosure(obj) @children << obj # Mark the resource as virtual or exported, as necessary. if self.exported? obj.exported = true elsif self.virtual? obj.virtual = true end # The global table @definedtable[obj.ref] = obj return obj end # Override a parameter in an existing object. If the object does not yet # exist, then cache the override in a global table, so it can be flushed # at the end. def setoverride(resource) resource.override = true if obj = @definedtable[resource.ref] obj.merge(resource) else @overridetable[resource.ref] << resource end end # Set defaults for a type. The typename should already be downcased, # so that the syntax is isolated. We don't do any kind of type-checking # here; instead we let the resource do it when the defaults are used. def setdefaults(type, params) table = @defaultstable[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) params.each { |param| #Puppet.debug "Default for %s is %s => %s" % # [type,ary[0].inspect,ary[1].inspect] if @@declarative if table.include?(param.name) self.fail "Default already defined for %s { %s }" % [type,param.name] end else if table.include?(param.name) # we should maybe allow this warning to be turned off... Puppet.warning "Replacing default for %s { %s }" % [type,param.name] end end table[param.name] = param } end # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope # to be reassigned if we're declarative (which is the default). def setvar(name,value, file = nil, line = nil) #Puppet.debug "Setting %s to '%s' at level %s" % # [name.inspect,value,self.level] if @symtable.include?(name) if @@declarative error = Puppet::ParseError.new("Cannot reassign variable %s" % name) if file error.file = file end if line error.line = line end raise error else Puppet.warning "Reassigning %s to %s" % [name,value] end end @symtable[name] = value end # Return an interpolated string. def strinterp(string, file = nil, line = nil) # Most strings won't have variables in them. ss = StringScanner.new(string) out = "" while not ss.eos? if ss.scan(/^\$\{((\w*::)*\w+)\}|^\$((\w*::)*\w+)/) # If it matches the backslash, then just retun the dollar sign. if ss.matched == '\\$' out << '$' else # look the variable up out << lookupvar(ss[1] || ss[3]).to_s || "" end elsif ss.scan(/^\\(.)/) # Puppet.debug("Got escape: pos:%d; m:%s" % [ss.pos, ss.matched]) case ss[1] when 'n' out << "\n" when 't' out << "\t" when 's' out << " " when '\\' out << '\\' when '$' out << '$' else str = "Unrecognised escape sequence '#{ss.matched}'" if file str += " in file %s" % file end if line str += " at line %s" % line end Puppet.warning str out << ss.matched end elsif ss.scan(/^\$/) out << '$' elsif ss.scan(/^\\\n/) # an escaped carriage return next else tmp = ss.scan(/[^\\$]+/) # Puppet.debug("Got other: pos:%d; m:%s" % [ss.pos, tmp]) unless tmp error = Puppet::ParseError.new("Could not parse string %s" % string.inspect) {:file= => file, :line= => line}.each do |m,v| error.send(m, v) if v end raise error end out << tmp end end return out end # Add a tag to our current list. These tags will be added to all # of the objects contained in this scope. def tag(*ary) ary.each { |tag| if tag.nil? or tag == "" puts caller Puppet.debug "got told to tag with %s" % tag.inspect next end unless tag =~ /^\w[-\w]*$/ fail Puppet::ParseError, "Invalid tag %s" % tag.inspect end tag = tag.to_s unless @tags.include?(tag) #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag] @tags << tag end } end # Return the tags associated with this scope. It's basically # just our parents' tags, plus our type. We don't cache this value # because our parent tags might change between calls. def tags tmp = [] + @tags 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 end unless tmp.include?(tag) tmp << tag end } end return tmp.sort.uniq end # Used mainly for logging def to_s if self.name return "%s[%s]" % [@type, @name] else return self.type.to_s end end # Convert all of our objects as necessary. def translate ret = @children.collect do |child| case child when Puppet::Parser::Resource child.to_trans when self.class child.translate else devfail "Got %s for translation" % child.class end end.reject { |o| o.nil? } bucket = Puppet::TransBucket.new ret case self.type when "": bucket.type = "main" when nil: devfail "A Scope with no type" else bucket.type = @type end if self.name bucket.name = self.name end return bucket end # Undefine a variable; only used for testing. def unsetvar(var) if @symtable.include?(var) @symtable.delete(var) 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 end # $Id$ 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 @@ -1,816 +1,537 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'facter' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/network/client' require 'puppettest' require 'puppettest/resourcetesting' require 'puppettest/parsertesting' require 'puppettest/servertest' require 'timeout' class TestInterpreter < PuppetTest::TestCase include PuppetTest include PuppetTest::ServerTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST NodeDef = Puppet::Parser::Interpreter::NodeDef # create a simple manifest that uses nodes to create a file def mknodemanifest(node, file) createdfile = tempfile() File.open(file, "w") { |f| f.puts "node %s { file { \"%s\": ensure => file, mode => 755 } }\n" % [node, createdfile] } return [file, createdfile] end def test_simple file = tempfile() File.open(file, "w") { |f| f.puts "file { \"/etc\": owner => root }" } assert_nothing_raised { Puppet::Parser::Interpreter.new(:Manifest => file) } end def test_reloadfiles hostname = Facter["hostname"].value file = tempfile() # Create a first version createdfile = mknodemanifest(hostname, file) interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new(:Manifest => file) } config = nil assert_nothing_raised { config = interp.run(hostname, {}) } sleep(1) # Now create a new file createdfile = mknodemanifest(hostname, file) newconfig = nil assert_nothing_raised { newconfig = interp.run(hostname, {}) } assert(config != newconfig, "Configs are somehow the same") end # Make sure searchnode behaves as we expect. def test_nodesearch # We use two sources here to catch a weird bug where the default # node is used if the host isn't in the first source. interp = mkinterp # Make some nodes names = %w{node1 node2 node2.domain.com} interp.newnode names interp.newnode %w{default} nodes = {} # Make sure we can find them all, using the direct method names.each do |name| nodes[name] = interp.nodesearch_code(name) assert(nodes[name], "Could not find %s" % name) nodes[name].file = __FILE__ end # Now let's try it with the nodesearch method names.each do |name| node = interp.nodesearch(name) assert(node, "Could not find #{name} via nodesearch") end # Make sure we find the default node when we search for nonexistent nodes assert_nothing_raised do default = interp.nodesearch("nosuchnode") assert(default, "Did not find default node") assert_equal("default", default.classname) end # Now make sure the longest match always wins node = interp.nodesearch(*%w{node2 node2.domain.com}) assert(node, "Did not find node2") assert_equal("node2.domain.com", node.classname, "Did not get longest match") end def test_parsedate Puppet[:filetimeout] = 0 main = tempfile() sub = tempfile() mainfile = tempfile() subfile = tempfile() count = 0 updatemain = proc do count += 1 File.open(main, "w") { |f| f.puts "import '#{sub}' file { \"#{mainfile}\": content => #{count} } " } end updatesub = proc do count += 1 File.open(sub, "w") { |f| f.puts "file { \"#{subfile}\": content => #{count} } " } end updatemain.call updatesub.call interp = Puppet::Parser::Interpreter.new( :Manifest => main, :Local => true ) date = interp.parsedate # Now update the site file and make sure we catch it sleep 1 updatemain.call newdate = interp.parsedate assert(date != newdate, "Parsedate was not updated") date = newdate # And then the subfile sleep 1 updatesub.call newdate = interp.parsedate assert(date != newdate, "Parsedate was not updated") end # Make sure class, node, and define methods are case-insensitive def test_structure_case_insensitivity interp = mkinterp result = nil assert_nothing_raised do result = interp.newclass "Yayness" end assert_equal(result, interp.findclass("", "yayNess")) assert_nothing_raised do result = interp.newdefine "FunTest" end assert_equal(result, interp.finddefine("", "fUntEst"), "%s was not matched" % "fUntEst") assert_nothing_raised do result = interp.newnode("MyNode").shift end assert_equal(result, interp.nodesearch("mYnOde"), "mYnOde was not matched") assert_nothing_raised do result = interp.newnode("YayTest.Domain.Com").shift end assert_equal(result, interp.nodesearch("yaYtEst.domAin.cOm"), "yaYtEst.domAin.cOm was not matched") end # Make sure our whole chain works. def test_evaluate interp, scope, source = mkclassframing # Create a define that we'll be using interp.newdefine("wrapper", :code => AST::ASTArray.new(:children => [ resourcedef("file", varref("name"), "owner" => "root") ])) # Now create a resource that uses that define define = mkresource(:type => "wrapper", :title => "/tmp/testing", :scope => scope, :source => source, :params => :none) scope.setresource define # And a normal resource scope.setresource mkresource(:type => "file", :title => "/tmp/rahness", :scope => scope, :source => source, :params => {:owner => "root"}) # Now evaluate everything objects = nil interp.usenodes = false assert_nothing_raised do objects = interp.evaluate(nil, {}) end assert_instance_of(Puppet::TransBucket, objects) end # Test evaliterate. It's a very simple method, but it's pretty tough # to test. It iterates over collections and instances of defined types # until there's no more work to do. def test_evaliterate interp, scope, source = mkclassframing # Create a top-level definition that creates a builtin object interp.newdefine("one", :arguments => [%w{owner}], :code => AST::ASTArray.new(:children => [ resourcedef("file", varref("name"), "owner" => varref("owner") ) ]) ) # Create another definition to call that one interp.newdefine("two", :arguments => [%w{owner}], :code => AST::ASTArray.new(:children => [ resourcedef("one", varref("name"), "owner" => varref("owner") ) ]) ) # And then a third interp.newdefine("three", :arguments => [%w{owner}], :code => AST::ASTArray.new(:children => [ resourcedef("two", varref("name"), "owner" => varref("owner") ) ]) ) # And create a definition that creates a virtual resource interp.newdefine("virtualizer", :arguments => [%w{owner}], :code => AST::ASTArray.new(:children => [ virt_resourcedef("one", varref("name"), "owner" => varref("owner") ) ]) ) # Now create an instance of three three = Puppet::Parser::Resource.new( :type => "three", :title => "one", :scope => scope, :source => source, :params => paramify(source, :owner => "root") ) scope.setresource(three) # An instance of the virtualizer virt = Puppet::Parser::Resource.new( :type => "virtualizer", :title => "two", :scope => scope, :source => source, :params => paramify(source, :owner => "root") ) scope.setresource(virt) # And a virtual instance of three virt_three = Puppet::Parser::Resource.new( :type => "three", :title => "three", :scope => scope, :source => source, :params => paramify(source, :owner => "root") ) virt_three.virtual = true scope.setresource(virt_three) # Create a normal, virtual resource plainvirt = Puppet::Parser::Resource.new( :type => "user", :title => "five", :scope => scope, :source => source, :params => paramify(source, :uid => "root") ) plainvirt.virtual = true scope.setresource(plainvirt) # Now create some collections for our virtual resources %w{Three[three] One[two]}.each do |ref| coll = Puppet::Parser::Collector.new(scope, "file", nil, nil, :virtual) coll.resources = [ref] scope.newcollection(coll) end # And create a generic user collector for our plain resource coll = Puppet::Parser::Collector.new(scope, "user", nil, nil, :virtual) scope.newcollection(coll) ret = nil assert_nothing_raised do ret = scope.unevaluated end assert_instance_of(Array, ret) assert_equal(3, ret.length, "did not get the correct number of unevaled resources") # Now translate the whole tree assert_nothing_raised do Timeout::timeout(2) do interp.evaliterate(scope) end end # Now make sure we've got all of our files %w{one two three}.each do |name| file = scope.findresource("File[%s]" % name) assert(file, "Could not find file %s" % name) assert_equal("root", file[:owner]) assert(! file.virtual?, "file %s is still virtual" % name) end # Now make sure we found the user assert(! plainvirt.virtual?, "user was not realized") end # Make sure we fail if there are any leftover overrides to perform. # This would normally mean that someone is trying to override an object # that does not exist. def test_failonleftovers interp, scope, source = mkclassframing # Make sure we don't fail, since there are no overrides assert_nothing_raised do interp.failonleftovers(scope) end # Add an override, and make sure it causes a failure over1 = mkresource :scope => scope, :source => source, :params => {:one => "yay"} scope.setoverride(over1) assert_raise(Puppet::ParseError) do interp.failonleftovers(scope) end # Make a new scope to test leftover collections scope = mkscope :interp => interp interp.meta_def(:check_resource_collections) do raise ArgumentError, "yep" end assert_raise(ArgumentError, "did not call check_resource_colls") do interp.failonleftovers(scope) end end def test_evalnode interp = mkinterp interp.usenodes = false scope = Parser::Scope.new(:interp => interp) facts = Facter.to_hash # First make sure we get no failures when client is nil assert_nothing_raised do interp.evalnode(nil, scope, facts) end # Now define a node interp.newnode "mynode", :code => AST::ASTArray.new(:children => [ resourcedef("file", "/tmp/testing", "owner" => "root") ]) # Eval again, and make sure it does nothing assert_nothing_raised do interp.evalnode("mynode", scope, facts) end assert_nil(scope.findresource("File[/tmp/testing]"), "Eval'ed node with nodes off") # Now enable usenodes and make sure it works. interp.usenodes = true assert_nothing_raised do interp.evalnode("mynode", scope, facts) end file = scope.findresource("File[/tmp/testing]") assert_instance_of(Puppet::Parser::Resource, file, "Could not find file") end # This is mostly used for the cfengine module def test_specificclasses interp = mkinterp :Classes => %w{klass1 klass2}, :UseNodes => false # Make sure it's not a failure to be missing classes, since # we're using the cfengine class list, which is huge. assert_nothing_raised do interp.evaluate(nil, {}) end interp.newclass("klass1", :code => AST::ASTArray.new(:children => [ resourcedef("file", "/tmp/klass1", "owner" => "root") ])) interp.newclass("klass2", :code => AST::ASTArray.new(:children => [ resourcedef("file", "/tmp/klass2", "owner" => "root") ])) ret = nil assert_nothing_raised do ret = interp.evaluate(nil, {}) end found = ret.flatten.collect do |res| res.name end 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 scope = mkscope :interp => interp coll = Puppet::Parser::Collector.new(scope, "file", nil, nil, :virtual) coll.resources = ["File[/tmp/virtual1]", "File[/tmp/virtual2]"] scope.newcollection(coll) assert_raise(Puppet::ParseError, "Did not fail on remaining resource colls") do interp.check_resource_collections(scope) end end def test_nodedef interp = mkinterp interp.newclass("base") interp.newclass("sub", :parent => "base") interp.newclass("other") node = nil assert_nothing_raised("Could not create a node definition") do node = NodeDef.new :name => "yay", :classes => "sub", :parameters => {"one" => "two", "three" => "four"} end scope = mkscope :interp => interp assert_nothing_raised("Could not evaluate the node definition") do node.evaluate(:scope => scope) end assert_equal("two", scope.lookupvar("one"), "NodeDef did not set variable") assert_equal("four", scope.lookupvar("three"), "NodeDef did not set variable") assert(scope.classlist.include?("sub"), "NodeDef did not evaluate class") assert(scope.classlist.include?("base"), "NodeDef did not evaluate base class") # Now try a node def with multiple classes assert_nothing_raised("Could not create a node definition") do node = NodeDef.new :name => "yay", :classes => %w{sub other base}, :parameters => {"one" => "two", "three" => "four"} end scope = mkscope :interp => interp assert_nothing_raised("Could not evaluate the node definition") do node.evaluate(:scope => scope) end assert_equal("two", scope.lookupvar("one"), "NodeDef did not set variable") assert_equal("four", scope.lookupvar("three"), "NodeDef did not set variable") assert(scope.classlist.include?("sub"), "NodeDef did not evaluate class") assert(scope.classlist.include?("other"), "NodeDef did not evaluate other class") # And a node def with no params assert_nothing_raised("Could not create a node definition with no params") do node = NodeDef.new :name => "yay", :classes => %w{sub other base} end scope = mkscope :interp => interp assert_nothing_raised("Could not evaluate the node definition") do node.evaluate(:scope => scope) end assert(scope.classlist.include?("sub"), "NodeDef did not evaluate class") assert(scope.classlist.include?("other"), "NodeDef did not evaluate other class") # Now make sure nodedef doesn't fail when some classes are not defined (#687). assert_nothing_raised("Could not create a node definition with some invalid classes") do node = NodeDef.new :name => "yay", :classes => %w{base unknown} end scope = mkscope :interp => interp assert_nothing_raised("Could not evaluate the node definition with some invalid classes") do node.evaluate(:scope => scope) end 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 Puppet[:filetimeout] = -10 file = tempfile File.open(file, "w") { |f| f.puts %{file { '/tmp': ensure => directory }} } interp = mkinterp :Manifest => file, :UseNodes => false assert_nothing_raised("Could not compile the first time") do interp.run("yay", {}) end oldparser = interp.send(:instance_variable_get, "@parser") # Now add a syntax failure File.open(file, "w") { |f| f.puts %{file { /tmp: ensure => directory }} } assert_nothing_raised("Could not compile the first time") do interp.run("yay", {}) end # And make sure the old parser is still there newparser = interp.send(:instance_variable_get, "@parser") assert_equal(oldparser.object_id, newparser.object_id, "Failed parser still replaced existing parser") 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 index 61983df92..4cbba8063 100755 --- a/test/language/node.rb +++ b/test/language/node.rb @@ -1,126 +1,131 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ -require 'puppet' -require 'puppet/parser/parser' +require 'mocha' require 'puppettest' +require 'puppettest/parsertesting' +require 'puppet/parser/configuration' -class TestParser < Test::Unit::TestCase +# Test our configuration object. +class TestConfiguration < Test::Unit::TestCase + include PuppetTest include PuppetTest::ParserTesting - def setup - super - Puppet[:parseonly] = true + 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_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\"" ] + 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 - 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\"" + # 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_inherits_from_default - check_parseable(["default", "host1"], "node default {}\nnode host1 inherits default {}") + 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 - 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" + # 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 - AST = Puppet::Parser::AST + # 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") - def check_parseable(hostnames, code = nil) - unless hostnames.is_a?(Array) - hostnames = [ hostnames ] + # 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 - 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) + 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 - 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") - 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 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") - # 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) + # 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/network/handler/node.rb b/test/network/handler/node.rb new file mode 100755 index 000000000..c682929d9 --- /dev/null +++ b/test/network/handler/node.rb @@ -0,0 +1,341 @@ +#!/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)) + end +end + +class TestNodeInterface < Test::Unit::TestCase + def setup + super + end + + def teardown + end + + def test_node + raise "Failing, yo" + end + + def test_environment + raise "still failing" + end + + def test_parameters + raise "still failing" + end + + def test_classes + raise "still failing" + 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) + end + end + + def test_external_node_source + mapper = mk_node_mapper + searcher = mk_searcher(:external) + + # 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 + searcher = mk_searcher(:ldap) + + 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 +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