diff options
-rw-r--r-- | CHANGELOG | 22 | ||||
-rw-r--r-- | lib/puppet/configuration.rb | 23 | ||||
-rw-r--r-- | lib/puppet/parser/interpreter.rb | 257 | ||||
-rwxr-xr-x | test/language/interpreter.rb | 655 |
4 files changed, 516 insertions, 441 deletions
@@ -1,4 +1,24 @@ - Reworked the idatbase schema used to store configurations with the + COMPATIBILITY ALERT: + Significantly reworked external node support, in a way that's NOT + backward-compatible: + + Only ONE node source can be used -- you can use LDAP, code, or + an external node program, but not more than one. + + LDAP node support has two changes: First, the "ldapattrs" attribute is + now used for setting the attributes to retrieve from the server (in + addition to required attriutes), and second, all retrieved attributes + are set as variables in the top scope. This means you can set attributes + on your LDAP nodes and they will automatically appear as variables + in your configurations. + + External node support has been completely rewritten. These programs must + now generate a YAML dump of a hash, with "classes" and "parameters" keys. + The classes should be an array, and the parameters should be a hash. The + external node program has no support for parent nodes -- the script must + handle that on its own. + + Reworked the database schema used to store configurations with the storeconfigs option. Replaced the obsolete RRD ruby library with the maintained diff --git a/lib/puppet/configuration.rb b/lib/puppet/configuration.rb index 02f62bbcb..5782adc82 100644 --- a/lib/puppet/configuration.rb +++ b/lib/puppet/configuration.rb @@ -539,14 +539,14 @@ module Puppet should be case-sensitive. Case insensitivity is handled by downcasing all values before comparison."], :external_nodes => ["none", - "An external command that can produce node information. The - first line of output must be either the parent node or blank, - and if there is a second line of output it should be a list of - whitespace-separated classes to include on that node. This command - makes it straightforward to store your node mapping information - in other data sources like databases. - - For unknown nodes, the commands should exit with an exit code of 1."]) + "An external command that can produce node information. The output + must be a YAML dump of a hash, and that hash must have one or both of + ``classes`` and ``parameters``, where ``classes`` is an array and + ``parameters`` is a hash. For unknown nodes, the commands should + exit with a non-zero exit code. + + This command makes it straightforward to store your node mapping + information in other data sources like databases."]) setdefaults(:ldap, :ldapnodes => [false, @@ -565,9 +565,14 @@ module Puppet "The LDAP port. Only used if ``ldapnodes`` is enabled."], :ldapstring => ["(&(objectclass=puppetClient)(cn=%s))", "The search string used to find an LDAP node."], - :ldapattrs => ["puppetclass", + :ldapclassattrs => ["puppetclass", "The LDAP attributes to use to define Puppet classes. Values should be comma-separated."], + :ldapattrs => ["all", + "The LDAP attributes to include when querying LDAP for nodes. All + returned attributes are set as variables in the top-level scope. + Multiple values should be comma-separated. The value 'all' returns + all attributes."], :ldapparentattr => ["parentnode", "The attribute to use to define the parent node."], :ldapuser => ["", diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index b9391f902..3a719e03c 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -5,10 +5,54 @@ require 'puppet' require 'timeout' require 'puppet/rails' +require 'puppet/util/methodhelper' require 'puppet/parser/parser' require 'puppet/parser/scope' 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) + rescue => detail + 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 @@ -156,7 +200,8 @@ class Puppet::Parser::Interpreter klass.safeevaluate :scope => scope, :nosubscope => true end - # Next evaluate the node + # 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. @@ -258,51 +303,6 @@ class Puppet::Parser::Interpreter return nil end - # Create a new node, just from a list of names, classes, and an optional parent. - def gennode(name, hash, source = nil) - facts = hash[:facts] - classes = hash[:classes] - parent = hash[:parentnode] - arghash = { - :name => name, - :interp => self, - :classname => name - } - - if (classes.is_a?(Array) and classes.empty?) or classes.nil? - arghash[:code] = AST::ASTArray.new(:children => []) - else - classes = [classes] unless classes.is_a?(Array) - - classcode = @parser.ast(AST::ASTArray, :children => classes.collect do |klass| - @parser.ast(AST::FlatString, :value => klass) - end) - - # Now generate a function call. - code = @parser.ast(AST::Function, - :name => "include", - :arguments => classcode, - :ftype => :statement - ) - - arghash[:code] = code - end - - if parent - arghash[:parentclass] = parent - end - - # Create the node - if source - arghash[:file] = source - else - arghash[:file] = nil - end - arghash[:line] = nil - node = @parser.ast(AST::Node, arghash) - return node - end - # create our interpreter def initialize(hash) if @code = hash[:Code] @@ -317,33 +317,15 @@ class Puppet::Parser::Interpreter @usenodes = true end - # By default, we only search for parsed nodes. - @nodesources = [] if Puppet[:ldapnodes] # Nodes in the file override nodes in ldap. - @nodesources << :ldap - end - - if hash[:NodeSources] - unless hash[:NodeSources].is_a?(Array) - hash[:NodeSources] = [hash[:NodeSources]] - end - hash[:NodeSources].each do |src| - if respond_to? "nodesearch_#{src.to_s}" - @nodesources << src.to_s.intern - else - Puppet.warning "Node source '#{src}' not supported" - end - end - end - - unless @nodesources.include?(:code) - @nodesources << :code - end - - unless Puppet[:external_nodes] == "none" - @nodesources << :external + @nodesource = :ldap + elsif Puppet[:external_nodes] != "none" + @nodesource = :external + else + # By default, we only search for parsed nodes. + @nodesource = :code end @setup = false @@ -389,26 +371,31 @@ class Puppet::Parser::Interpreter @definetable = {} end - # Find the ldap node and extra the info, returning just - # the critical data. + # 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, [] + return nil end end filter = Puppet[:ldapstring] - attrs = Puppet[:ldapattrs].split("\s*,\s*") - sattrs = attrs.dup + 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 - sattrs << pattr + search_attrs << pattr unless search_attrs.nil? end end @@ -418,12 +405,14 @@ class Puppet::Parser::Interpreter 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, sattrs) do |entry| + @ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) do |entry| found = true if pattr if values = entry.vals(pattr) @@ -438,11 +427,20 @@ class Puppet::Parser::Interpreter end end - attrs.each { |attr| + 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 @@ -461,7 +459,11 @@ class Puppet::Parser::Interpreter classes = nil end - return parent, classes + if parent or classes or parameters + return parent, classes, parameters + else + return nil + end end # Split an fq name into a namespace and name @@ -590,21 +592,20 @@ class Puppet::Parser::Interpreter # Search for our node in the various locations. def nodesearch(*nodes) nodes = nodes.collect { |n| n.to_s.downcase } - # At this point, stop at the first source that defines - # the node - @nodesources.each do |source| - method = "nodesearch_%s" % source - if self.respond_to? method - # 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) - nsource = obj.file || source - Puppet.info "Found %s in %s" % [node, nsource] - return obj - end + + 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 @@ -640,41 +641,51 @@ class Puppet::Parser::Interpreter return nil end - if output =~ /\A\s+\Z/ # all whitespace + if output =~ /\A\s*\Z/ # all whitespace Puppet.debug "Empty response for %s from external node source" % name return nil end - - lines = output.split("\n") - - args = {} - parent = lines[0].gsub(/\s+/, '') - args[:parentnode] = parent unless parent == "" - - if lines[1] - classes = lines[1].sub(/^\s+/,'').sub(/\s+$/,'').split(/\s+/) - args[:classes] = classes unless classes.empty? + + 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 - - if args.empty? - Puppet.warning "Somehow got a node with no information" - return nil + + 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 gennode(name, args, Puppet[:external_nodes]) + return nil end end # Look for our node in ldap. def nodesearch_ldap(node) - parent, classes = ldapsearch(node) - if parent or classes - args = {} - args[:classes] = classes if classes - args[:parentnode] = parent if parent - return gennode(node, args, "ldap") - else + 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 @@ -687,15 +698,13 @@ class Puppet::Parser::Interpreter # We have to leave this for after initialization because there # seems to be a problem keeping ldap open after a fork. unless @setup - @nodesources.each { |source| - method = "setup_%s" % source.to_s - if respond_to? method - exceptwrap :type => Puppet::Error, - :message => "Could not set up node source %s" % source do - self.send(method) - end + 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() diff --git a/test/language/interpreter.rb b/test/language/interpreter.rb index fd1847df9..cf2cba854 100755 --- a/test/language/interpreter.rb +++ b/test/language/interpreter.rb @@ -16,13 +16,14 @@ require 'puppettest/servertest' require 'puppettest/railstesting' require 'timeout' -class TestInterpreter < Test::Unit::TestCase +class TestInterpreter < PuppetTest::TestCase include PuppetTest include PuppetTest::ServerTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::RailsTesting AST = Puppet::Parser::AST + NodeDef = Puppet::Parser::Interpreter::NodeDef # create a simple manifest that uses nodes to create a file def mknodemanifest(node, file) @@ -76,199 +77,6 @@ class TestInterpreter < Test::Unit::TestCase assert(config != newconfig, "Configs are somehow the same") end - if Puppet.features.rails? - def test_hoststorage - assert_nothing_raised { - Puppet[:storeconfigs] = true - } - - file = tempfile() - File.open(file, "w") { |f| - f.puts "file { \"/etc\": owner => root }" - } - - interp = nil - assert_nothing_raised { - interp = Puppet::Parser::Interpreter.new( - :Manifest => file, - :UseNodes => false, - :ForkSave => false - ) - } - - facts = {} - Facter.each { |fact, val| facts[fact] = val } - - objects = nil - assert_nothing_raised { - objects = interp.run(facts["hostname"], facts) - } - - obj = Puppet::Rails::Host.find_by_name(facts["hostname"]) - assert(obj, "Could not find host object") - end - else - $stderr.puts "No ActiveRecord -- skipping collection tests" - end - - if Facter["domain"].value == "madstop.com" - - # Only test ldap stuff on luke's network, since that's the only place we - # have data for. - if Puppet.features.ldap? - 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(node) - parent = nil - classes = nil - @ldap.search( "ou=hosts, dc=madstop, dc=com", 2, - "(&(objectclass=puppetclient)(cn=%s))" % node - ) do |entry| - parent = entry.vals("parentnode").shift - classes = entry.vals("puppetclass") || [] - end - - return parent, classes - 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 = nil - assert_nothing_raised do - parent, classes = 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 = interp.ldapsearch("culain") - end - - realparent, realclasses = ldaphost("culain") - assert_equal(realparent, parent) - assert_equal(realclasses, classes) - end - - def test_ldapnodes - Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com" - Puppet[:ldapnodes] = true - - ldapconnect() - - interp = mkinterp :NodeSources => [:ldap, :code] - - # culain uses basenode, so create that - basenode = interp.newnode([:basenode])[0] - - # Make sure we get nothing for nonexistent hosts - none = nil - assert_nothing_raised do - none = interp.nodesearch_ldap("nosuchhost") - end - - assert_nil(none, "Got a node for a non-existent host") - - # Make sure we can find 'culain' in ldap - culain = nil - assert_nothing_raised do - culain = interp.nodesearch_ldap("culain") - end - - assert(culain, "Did not find culain in ldap") - - assert_nothing_raised do - assert_equal(basenode.classname.to_s, culain.parentobj.classname.to_s, - "Did not get parent class") - end - end - - if Puppet::Util::SUIDManager.uid == 0 and Facter["hostname"].value == "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 - else - $stderr.puts "Run as root for ldap reconnect tests" - end - end - else - $stderr.puts "Not in madstop.com; skipping ldap tests" - end - - # Test that node info and default node info in different sources isn't - # bad. - def test_multiple_nodesources - - # Create another node source - Puppet::Parser::Interpreter.send(:define_method, :nodesearch_multi) do |*names| - if names[0] == "default" - gennode("default", {:facts => {}}) - else - nil - end - end - - interp = mkinterp :NodeSources => [:multi, :code] - - interp.newnode(["node"]) - - obj = nil - assert_nothing_raised do - obj = interp.nodesearch("node") - end - assert(obj, "Did not find node") - assert_equal("node", obj.classname) - end - # Make sure searchnode behaves as we expect. def test_nodesearch # We use two sources here to catch a weird bug where the default @@ -410,54 +218,6 @@ class TestInterpreter < Test::Unit::TestCase end end - # Make sure we're correctly generating a node definition. - def test_gennode - interp = mkinterp - - interp.newnode "base" - interp.newclass "yaytest" - - # Go through the different iterations: - [ - [nil, "yaytest"], - [nil, ["yaytest"]], - [nil, nil], - [nil, []], - ["base", nil], - ["base", []], - ["base", "yaytest"], - ["base", ["yaytest"]] - ].each do |parent, classes| - node = nil - assert_nothing_raised { - node = interp.gennode("nodeA", :classes => classes, - :parentnode => parent) - } - - assert_instance_of(Puppet::Parser::AST::Node, node) - - assert_equal("nodeA", node.name) - - scope = mkscope :interp => interp - - assert_nothing_raised do - node.evaluate :scope => scope - end - - # If there's a parent, make sure it got evaluated - if parent - assert(scope.classlist.include?("base"), - "Did not evaluate parent node") - end - - # If there are classes make sure they got evaluated - if classes == ["yaytest"] or classes == "yaytest" - assert(scope.classlist.include?("yaytest"), - "Did not evaluate class") - end - end - end - def test_fqfind interp = mkinterp @@ -955,45 +715,6 @@ class TestInterpreter < Test::Unit::TestCase assert(found.include?("/tmp/klass1"), "Did not evaluate klass1") assert(found.include?("/tmp/klass2"), "Did not evaluate klass2") end - - if Puppet.features.rails? - # We need to make sure finished objects are stored in the db. - def test_finish_before_store - railsinit - interp = mkinterp - - node = interp.newnode ["myhost"], :code => AST::ASTArray.new(:children => [ - resourcedef("file", "/tmp/yay", :group => "root"), - defaultobj("file", :owner => "root") - ]) - - interp.newclass "myclass", :code => AST::ASTArray.new(:children => [ - ]) - - interp.newclass "sub", :parent => "myclass", - :code => AST::ASTArray.new(:children => [ - resourceoverride("file", "/tmp/yay", :owner => "root") - ] - ) - - # Now do the rails crap - Puppet[:storeconfigs] = true - - interp.evaluate("myhost", {}) - - # And then retrieve the object from rails - res = Puppet::Rails::Resource.find_by_restype_and_title("file", "/tmp/yay") - - assert(res, "Did not get resource from rails") - - param = res.param_names.find_by_name("owner", :include => :param_values) - - assert(param, "Did not find owner param") - - pvalue = param.param_values.find_by_value("root") - assert_equal("root", pvalue[:value]) - end - end def mk_node_mapper # First, make sure our nodesearch command works as we expect @@ -1002,17 +723,19 @@ class TestInterpreter < Test::Unit::TestCase ruby = %x{which ruby}.chomp File.open(mapper, "w") { |f| f.puts "#!#{ruby} - name = ARGV[0] + require 'yaml' + name = ARGV[0].chomp + result = {} + if name =~ /a/ - puts ARGV[0].gsub('a', 'b') - else - puts '' + result[:parameters] = {'one' => ARGV[0] + '1', 'two' => ARGV[0] + '2'} end + if name =~ /p/ - puts [1,2,3].collect { |n| ARGV[0] + n.to_s }.join(' ') - else - puts '' + result['classes'] = [1,2,3].collect { |n| ARGV[0] + n.to_s } end + + puts YAML.dump(result) " } File.chmod(0755, mapper) @@ -1022,18 +745,10 @@ class TestInterpreter < Test::Unit::TestCase def test_nodesearch_external interp = mkinterp - # Make a fake gennode method - class << interp - def gennode(name, args, source) - args[:name] = name - return args - end - end - mapper = mk_node_mapper # Make sure it gives the right response - assert_equal("bpple\napple1 apple2 apple3\n", - %x{#{mapper} apple}) + 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 { @@ -1043,23 +758,29 @@ class TestInterpreter < Test::Unit::TestCase 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") - assert_equal({:name => "apple", :classes => %w{apple1 apple2 apple3}, :parentnode => "bpple"}, - node) - - assert_nothing_raised { node = interp.nodesearch_external("plum")} # no a's, thus no parent - assert_equal({:name => "plum", :classes => %w{plum1 plum2 plum3}}, - node) + # 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({:name => "guava", :parentnode => "gubvb"}, - node) + 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 + # A wrapper test, to make sure we're correctly calling the external search method. def test_nodesearch_external_functional mapper = mk_node_mapper @@ -1070,7 +791,7 @@ class TestInterpreter < Test::Unit::TestCase assert_nothing_raised do node = interp.nodesearch("apple") end - assert_instance_of(Puppet::Parser::AST::Node, node, "did not create node") + assert_instance_of(NodeDef, node, "did not create node") end def test_check_resource_collections @@ -1084,6 +805,326 @@ class TestInterpreter < Test::Unit::TestCase 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") + end + + 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 +end + +class LdapNodeTest < PuppetTest::TestCase + include PuppetTest + include PuppetTest::ServerTest + include PuppetTest::ParserTesting + include PuppetTest::ResourceTesting + include PuppetTest::RailsTesting + 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 + include PuppetTest::RailsTesting + 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 + +class InterpreterRailsTests < PuppetTest::TestCase + include PuppetTest + include PuppetTest::ServerTest + include PuppetTest::ParserTesting + include PuppetTest::ResourceTesting + include PuppetTest::RailsTesting + AST = Puppet::Parser::AST + NodeDef = Puppet::Parser::Interpreter::NodeDef + confine "No rails support" => Puppet.features.rails? + + # We need to make sure finished objects are stored in the db. + def test_finish_before_store + railsinit + interp = mkinterp + + node = interp.newnode ["myhost"], :code => AST::ASTArray.new(:children => [ + resourcedef("file", "/tmp/yay", :group => "root"), + defaultobj("file", :owner => "root") + ]) + + interp.newclass "myclass", :code => AST::ASTArray.new(:children => [ + ]) + + interp.newclass "sub", :parent => "myclass", + :code => AST::ASTArray.new(:children => [ + resourceoverride("file", "/tmp/yay", :owner => "root") + ] + ) + + # Now do the rails crap + Puppet[:storeconfigs] = true + + interp.evaluate("myhost", {}) + + # And then retrieve the object from rails + res = Puppet::Rails::Resource.find_by_restype_and_title("file", "/tmp/yay") + + assert(res, "Did not get resource from rails") + + param = res.param_names.find_by_name("owner", :include => :param_values) + + assert(param, "Did not find owner param") + + pvalue = param.param_values.find_by_value("root") + assert_equal("root", pvalue[:value]) + end + + def test_hoststorage + assert_nothing_raised { + Puppet[:storeconfigs] = true + } + + file = tempfile() + File.open(file, "w") { |f| + f.puts "file { \"/etc\": owner => root }" + } + + interp = nil + assert_nothing_raised { + interp = Puppet::Parser::Interpreter.new( + :Manifest => file, + :UseNodes => false, + :ForkSave => false + ) + } + + facts = {} + Facter.each { |fact, val| facts[fact] = val } + + objects = nil + assert_nothing_raised { + objects = interp.run(facts["hostname"], facts) + } + + obj = Puppet::Rails::Host.find_by_name(facts["hostname"]) + assert(obj, "Could not find host object") + end end # $Id$ |