diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 9da1928b3..3ba9c0c7a 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,708 +1,713 @@ 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([Puppet[:external_nodes], name]) + 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/test/language/interpreter.rb b/test/language/interpreter.rb index 6352a147e..75800cc41 100755 --- a/test/language/interpreter.rb +++ b/test/language/interpreter.rb @@ -1,804 +1,816 @@ #!/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[0].chomp + name = ARGV.last.chomp result = {} if name =~ /a/ - result[:parameters] = {'one' => ARGV[0] + '1', 'two' => ARGV[0] + '2'} + result[:parameters] = {'one' => ARGV.last + '1', 'two' => ARGV.last + '2'} end if name =~ /p/ - result['classes'] = [1,2,3].collect { |n| ARGV[0] + n.to_s } + 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$