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"
begin
output = Puppet::Util.execute([Puppet[:external_nodes], name])
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: interpreter.rb 2742 2007-08-03 23:49:53Z luke $
syntax highlighted by Code2HTML, v. 0.9.1