#!/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
result = {}
if name =~ /a/
result[:parameters] = {'one' => ARGV[0] + '1', 'two' => ARGV[0] + '2'}
end
if name =~ /p/
result['classes'] = [1,2,3].collect { |n| ARGV[0] + 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
# 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: interpreter.rb 2742 2007-08-03 23:49:53Z luke $
syntax highlighted by Code2HTML, v. 0.9.1