diff --git a/lib/puppet/node_source/ldap.rb b/lib/puppet/node_source/ldap.rb index 9332fcb40..7b60a3c62 100644 --- a/lib/puppet/node_source/ldap.rb +++ b/lib/puppet/node_source/ldap.rb @@ -1,118 +1,138 @@ Puppet::Network::Handler::Node.newnode_source(:ldap, :fact_merge => true) do desc "Search in LDAP for node configuration information." # Find the ldap node, return the class list and parent node specially, # and everything else in a parameter hash. def ldapsearch(node) - unless defined? @ldap and @ldap - setup_ldap() - unless @ldap - Puppet.info "Skipping ldap source; no ldap connection" - return nil - end - end - filter = Puppet[:ldapstring] classattrs = Puppet[:ldapclassattrs].split("\s*,\s*") if Puppet[:ldapattrs] == "all" # A nil value here causes all attributes to be returned. search_attrs = nil else search_attrs = classattrs + Puppet[:ldapattrs].split("\s*,\s*") end pattr = nil if pattr = Puppet[:ldapparentattr] if pattr == "" pattr = nil else search_attrs << pattr unless search_attrs.nil? end end if filter =~ /%s/ filter = filter.gsub(/%s/, node) end parent = nil classes = [] parameters = nil found = false count = 0 begin # We're always doing a sub here; oh well. - @ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) do |entry| + ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) do |entry| found = true if pattr if values = entry.vals(pattr) if values.length > 1 raise Puppet::Error, "Node %s has more than one parent: %s" % [node, values.inspect] end unless values.empty? parent = values.shift end end end classattrs.each { |attr| if values = entry.vals(attr) values.each do |v| classes << v end end } parameters = entry.to_hash.inject({}) do |hash, ary| if ary[1].length == 1 hash[ary[0]] = ary[1].shift else hash[ary[0]] = ary[1] end hash end end rescue => detail if count == 0 # Try reconnecting to ldap @ldap = nil - setup_ldap() retry else raise Puppet::Error, "LDAP Search failed: %s" % detail end end classes.flatten! if classes.empty? classes = nil end if parent or classes or parameters return parent, classes, parameters else return nil end end # Look for our node in ldap. def nodesearch(node) unless ary = ldapsearch(node) return nil end parent, classes, parameters = ary while parent parent, tmpclasses, tmpparams = ldapsearch(parent) classes += tmpclasses if tmpclasses tmpparams.each do |param, value| # Specifically test for whether it's set, so false values are handled # correctly. parameters[param] = value unless parameters.include?(param) end end return newnode(node, :classes => classes, :source => "ldap", :parameters => parameters) end + + private + + # Create an ldap connection. + def ldap + unless defined? @ldap and @ldap + unless Puppet.features.ldap? + raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" + end + begin + 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]) + rescue => detail + raise Puppet::Error, "Could not connect to LDAP: %s" % detail + end + end + + return @ldap + end end diff --git a/lib/puppet/parser/configuration.rb b/lib/puppet/parser/configuration.rb index c7979e51f..617d7d231 100644 --- a/lib/puppet/parser/configuration.rb +++ b/lib/puppet/parser/configuration.rb @@ -1,133 +1,505 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/external/gratr/digraph' require 'puppet/external/gratr/import' require 'puppet/external/gratr/dot' +require 'puppet/util/errors' + # Maintain a graph of scopes, along with a bunch of data # about the individual configuration we're compiling. class Puppet::Parser::Configuration - attr_reader :topscope, :interpreter, :host, :facts + include Puppet::Util + include Puppet::Util::Errors + attr_reader :topscope, :parser, :node, :facts + attr_accessor :extraction_format + + attr_writer :ast_nodes # Add a collection to the global list. def add_collection(coll) @collections << coll end + # Do we use nodes found in the code, vs. the external node sources? + def ast_nodes? + defined?(@ast_nodes) and @ast_nodes + end + # Store the fact that we've evaluated a class, and store a reference to # the scope in which it was evaluated, so that we can look it up later. def class_set(name, scope) @class_scopes[name] = scope + tag(name) end # Return the scope associated with a class. This is just here so # that subclasses can set their parent scopes to be the scope of # their parent class, and it's also used when looking up qualified # variables. def class_scope(klass) # They might pass in either the class or class name if klass.respond_to?(:classname) @class_scopes[klass.classname] else @class_scopes[klass] end end # Return a list of all of the defined classes. def classlist return @class_scopes.keys.reject { |k| k == "" } end + # Compile our configuration. This mostly revolves around finding and evaluating classes. + # This is the main entry into our configuration. + def compile + # Set the client's parameters into the top scope. + set_node_parameters() + + evaluate_main() + + evaluate_ast_nodes() + + evaluate_classes() + + evaluate_generators() + + fail_on_unevaluated() + + finish() + + return extract() + end + # Should the scopes behave declaratively? def declarative? true end - # Set up our configuration. We require an interpreter - # and a host name, and we normally are passed facts, too. - def initialize(options) - @interpreter = options[:interpreter] or - raise ArgumentError, "You must pass an interpreter to the configuration" - @facts = options[:facts] || {} - @host = options[:host] or - raise ArgumentError, "You must pass a host name to the configuration" + # Make sure we support the requested extraction format. + def extraction_format=(value) + unless respond_to?("extract_to_%s" % value) + raise ArgumentError, "Invalid extraction format %s" % value + end + @extraction_format = value + end + + # Return a resource by either its ref or its type and title. + def findresource(string, name = nil) + if name + string = "%s[%s]" % [string.capitalize, name] + end + + @resource_table[string] + end + + # Set up our configuration. We require a parser + # and a node object; the parser is so we can look up classes + # and AST nodes, and the node has all of the client's info, + # like facts and environment. + def initialize(node, parser, options = {}) + @node = node + @parser = parser + + options.each do |param, value| + begin + send(param.to_s + "=", value) + rescue NoMethodError + raise ArgumentError, "Configuration objects do not accept %s" % param + end + end - # Call the setup methods from the base class. - super() + @extraction_format ||= :transportable initvars() end # Create a new scope, with either a specified parent scope or # using the top scope. Adds an edge between the scope and # its parent to the graph. def newscope(parent = nil) parent ||= @topscope scope = Puppet::Parser::Scope.new(:configuration => self) - @graph.add_edge!(parent, scope) + @scope_graph.add_edge!(parent, scope) scope end # Find the parent of a given scope. Assumes scopes only ever have # one in edge, which will always be true. def parent(scope) - if ary = @graph.adjacent(scope, :direction => :in) and ary.length > 0 + if ary = @scope_graph.adjacent(scope, :direction => :in) and ary.length > 0 ary[0] else nil end end - # Return an array of all of the unevaluated objects - def unevaluated - ary = @definedtable.find_all do |name, object| - ! object.builtin? and ! object.evaluated? - end.collect { |name, object| object } + # Return any overrides for the given resource. + def resource_overrides(resource) + @resource_overrides[resource.ref] + end - if ary.empty? - return nil + # Store a resource override. + def store_override(override) + override.override = true + + # If possible, merge the override in immediately. + if resource = @resource_table[override.ref] + resource.merge(override) else - return ary + # Otherwise, store the override for later; these + # get evaluated in Resource#finish. + @resource_overrides[override.ref] << override end end + # Store a resource in our resource table. + def store_resource(scope, resource) + # This might throw an exception + verify_uniqueness(resource) + + # Store it in the global table. + @resource_table[resource.ref] = resource + + # And in the resource graph. At some point, this might supercede + # the global resource table, but the table is a lot faster + # so it makes sense to maintain for now. + @resource_graph.add_edge!(scope, resource) + end + private + # If ast nodes are enabled, then see if we can find and evaluate one. + def evaluate_ast_node + return unless ast_nodes? + + # Now see if we can find the node. + astnode = nil + #nodes = @parser.nodes + @node.names.each do |name| + break if astnode = @parser.nodes[name] + end + + unless astnode + astnode = @parser.nodes["default"] + end + unless astnode + raise Puppet::ParseError, "Could not find default node or by name with '%s'" % node.names.join(", ") + end + + astnode.safeevaluate :scope => topscope + end + + # Evaluate each class in turn. If there are any classes we can't find, + # just tag the configuration and move on. + def evaluate_classes + node.classes.each do |name| + if klass = @parser.findclass("", name) + # This will result in class_set getting called, which + # will in turn result in tags. Yay. + klass.safeevaluate(:scope => topscope) + else + Puppet.info "Could not find class %s for %s" % [name, node.name] + tag(name) + end + end + end + + # Evaluate our collections and return true if anything returned an object. + # The 'true' is used to continue a loop, so it's important. + def evaluate_collections + return false if @collections.empty? + + found_something = false + exceptwrap do + @collections.each do |collection| + if collection.evaluate + found_something = true + end + end + end + + return found_something + end + + # Make sure all of our resources have been evaluated into native resources. + # We return true if any resources have, so that we know to continue the + # evaluate_generators loop. + def evaluate_definitions + exceptwrap do + if ary = unevaluated_resources + ary.each do |resource| + resource.evaluate + end + # If we evaluated, let the loop know. + return true + else + return false + end + end + end + + # Iterate over collections and resources until we're sure that the whole + # configuration is evaluated. This is necessary because both collections + # and defined resources can generate new resources, which themselves could + # be defined resources. + def evaluate_generators + count = 0 + loop do + done = true + + # Call collections first, then definitions. + done = false if evaluate_collections + done = false if evaluate_definitions + break if done + if count > 1000 + raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host configuration" + end + end + end + + # Find and evaluate our main object, if possible. + def evaluate_main + if klass = @parser.findclass("", "") + # Set the source, so objects can tell where they were defined. + topscope.source = klass + klass.safeevaluate :scope => topscope, :nosubscope => true + end + end + + # Turn our configuration graph into whatever the client is expecting. + def extract + send("extract_to_%s" % extraction_format) + end + + # Create the traditional TransBuckets and TransObjects from our configuration + # graph. This will hopefully be deprecated soon. + def extract_to_transportable + top = nil + current = nil + buckets = {} + + # I'm *sure* there's a simple way to do this using a breadth-first search + # or something, but I couldn't come up with, and this is both fast + # and simple, so I'm not going to worry about it too much. + @scope_graph.vertices.each do |scope| + # For each scope, we need to create a TransBucket, and then + # put all of the scope's resources into that bucket, translating + # each resource into a TransObject. + + # Unless the bucket's already been created, make it now and add + # it to the cache. + unless bucket = buckets[scope] + bucket = buckets[scope] = scope.to_trans + end + + # First add any contained scopes + @scope_graph.adjacent(scope, :direction => :out).each do |vertex| + # If there's not already a bucket, then create and cache it. + unless child_bucket = buckets[vertex] + child_bucket = buckets[vertex] = vertex.to_trans + end + bucket.push child_bucket + end + + # Then add the resources. + @resource_graph.adjacent(scope, :direction => :out).each do |vertex| + bucket.push vertex.to_trans + end + end + + # Clear the cache to encourage the GC + result = buckets[topscope] + buckets.clear + return result + end + + # Make sure the entire configuration is evaluated. + def fail_on_unevaluated + fail_on_unevaluated_overrides + fail_on_unevaluated_resource_collections + end + + # If there are any resource overrides remaining, then we could + # not find the resource they were supposed to override, so we + # want to throw an exception. + def fail_on_unevaluated_overrides + remaining = [] + @resource_overrides.each do |name, overrides| + remaining += overrides + end + + unless remaining.empty? + fail Puppet::ParseError, + "Could not find object(s) %s" % remaining.collect { |o| + o.ref + }.join(", ") + end + 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 fail_on_unevaluated_resource_collections + remaining = [] + @collections.each do |coll| + # We're only interested in the 'resource' collections, + # which result from direct calls of 'realize'. Anything + # else is allowed not to return resources. + # Collect all of them, so we have a useful error. + 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 realize virtual resources %s" % + remaining.join(', ') + end + end + + # Make sure all of our resources and such have done any last work + # necessary. + def finish + @resource_table.each { |name, resource| resource.finish if resource.respond_to?(:finish) } + end + # Set up all of our internal variables. def initvars # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} # The table for all defined resources. @resource_table = {} # The list of objects that will available for export. @exported_resources = {} # The list of overrides. This is used to cache overrides on objects # that don't exist yet. We store an array of each override. @resource_overrides = Hash.new do |overs, ref| overs[ref] = [] end # The list of collections that have been created. This is a global list, # but they each refer back to the scope that created them. @collections = [] + # A list of tags we've generated; most class names. + @tags = [] + # Create our initial scope, our scope graph, and add the initial scope to the graph. @topscope = Puppet::Parser::Scope.new(:configuration => self, :type => "main", :name => "top") - @graph = GRATR::Digraph.new - @graph.add_vertex!(@topscope) + + # For maintaining scope relationships. + @scope_graph = GRATR::Digraph.new + @scope_graph.add_vertex!(@topscope) + + # For maintaining the relationship between scopes and their resources. + @resource_graph = GRATR::Digraph.new + end + + # Set the node's parameters into the top-scope as variables. + def set_node_parameters + node.parameters.each do |param, value| + @topscope.setvar(param, value) + end + end + + # Store the configuration into the database. + def store(options) + unless Puppet.features.rails? + raise Puppet::Error, + "storeconfigs is enabled but rails is unavailable" + end + + unless ActiveRecord::Base.connected? + Puppet::Rails.connect + end + + # We used to have hooks here for forking and saving, but I don't + # think it's worth retaining at this point. + store_to_active_record(options) + end + + # Do the actual storage. + def store_to_active_record(options) + begin + # We store all of the objects, even the collectable ones + benchmark(:info, "Stored configuration for #{options[:name]}") do + Puppet::Rails::Host.transaction do + Puppet::Rails::Host.store(options) + end + end + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + Puppet.err "Could not store configs: %s" % detail.to_s + end end - # Return the list of remaining overrides. - def overrides - @resource_overrides.values.flatten + # Add a tag. + def tag(*names) + names.each do |name| + name = name.to_s + @tags << name unless @tags.include?(name) + end + nil + end + + # Return the list of tags. + def tags + @tags.dup + end + + # Return an array of all of the unevaluated resources. These will be definitions, + # which need to get evaluated into native resources. + def unevaluated_resources + ary = @resource_table.find_all do |name, object| + ! object.builtin? and ! object.evaluated? + end.collect { |name, object| object } + + if ary.empty? + return nil + else + return ary + end end - def resources - @resourcetable + # Verify that the given resource isn't defined elsewhere. + def verify_uniqueness(resource) + # Short-curcuit the common case, + unless existing_resource = @resource_table[resource.ref] + return true + end + + if typeclass = Puppet::Type.type(resource.type) and ! typeclass.isomorphic? + Puppet.info "Allowing duplicate %s" % typeclass.name + return true + end + + # Either it's a defined type, which are never + # isomorphic, or it's a non-isomorphic type, so + # we should throw an exception. + msg = "Duplicate definition: %s is already defined" % resource.ref + + if existing_resource.file and existing_resource.line + msg << " in file %s at line %s" % + [existing_resource.file, existing_resource.line] + end + + if resource.line or resource.file + msg << "; cannot redefine" + end + + raise Puppet::ParseError.new(msg) end end diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 18bf31087..f0b9d0179 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,550 +1,175 @@ 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. +# The interpreter is a very simple entry-point class that +# manages the existence of the parser (e.g., replacing it +# when files are reparsed). You can feed it a node and +# get the node's configuration back. 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) + raise "move findclass() out of the interpreter" @parser.findclass(namespace, name) end + def finddefine(namespace, name) + raise "move finddefine() out of the interpreter" @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 + # By default, we only search for parsed nodes. + @nodesource = :code @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 # Pass these methods through to the parser. [:newclass, :newdefine, :newnode].each do |name| define_method(name) do |*args| + raise("move %s out of the interpreter" % name) @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) + raise "who uses newfile?" 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 - 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 + def compile(node) 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 + return Puppet::Parser::Configuration.new(node).compile end private # Check whether any of our files have changed. def checkfiles if @files.find { |f| f.changed? } @parsedate = Time.now.to_i end end # Parse the files, generating our parse tree. This automatically # reparses only if files are updated, so it's safe to call multiple # times. def parsefiles # First check whether there are updates to any non-puppet files # like templates. If we need to reparse, this will get quashed, # but it needs to be done first in case there's no reparse # but there are other file changes. checkfiles() # Check if the parser should reparse. if @file if defined? @parser if stamp = @parser.reparse? Puppet.notice "Reloading files" else return false end end unless FileTest.exists?(@file) # If we've already parsed, then we're ok. if findclass("", "") return else raise Puppet::Error, "Manifest %s must exist" % @file end end end # Create a new parser, just to keep things fresh. Don't replace our # current parser until we know weverything works. newparser = Puppet::Parser::Parser.new() if @code newparser.string = @code else newparser.file = @file end # Parsing stores all classes and defines and such in their # various tables, so we don't worry about the return. begin if @local newparser.parse else benchmark(:info, "Parsed manifest") do newparser.parse end end # We've gotten this far, so it's ok to swap the parsers. oldparser = @parser @parser = newparser if oldparser oldparser.clear end # Mark when we parsed, so we can check freshness @parsedate = Time.now.to_i rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not parse; using old configuration: %s" % detail end end - - # Store the configs into the database. - def storeconfigs(hash) - unless Puppet.features.rails? - raise Puppet::Error, - "storeconfigs is enabled but rails is unavailable" - end - - unless ActiveRecord::Base.connected? - Puppet::Rails.connect - end - - # Fork the storage, since we don't need the client waiting - # on that. How do I avoid this duplication? - if @forksave - fork { - # We store all of the objects, even the collectable ones - benchmark(:info, "Stored configuration for #{hash[:name]}") do - # Try to batch things a bit, by putting them into - # a transaction - Puppet::Rails::Host.transaction do - Puppet::Rails::Host.store(hash) - end - end - } - else - begin - # We store all of the objects, even the collectable ones - benchmark(:info, "Stored configuration for #{hash[:name]}") do - Puppet::Rails::Host.transaction do - Puppet::Rails::Host.store(hash) - end - end - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - Puppet.err "Could not store configs: %s" % detail.to_s - end - end - end end # $Id$ diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 728f75a69..6d069dc07 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,447 +1,449 @@ +# I pulled this into a separate file, because I got +# tired of rebuilding the parser.rb file all the time. class Puppet::Parser::Parser require 'puppet/parser/functions' ASTSet = Struct.new(:classes, :definitions, :nodes) # Define an accessor method for each table. We hide the existence of # the struct. [:classes, :definitions, :nodes].each do |name| define_method(name) do @astset.send(name) end end AST = Puppet::Parser::AST attr_reader :file, :interp attr_accessor :files # Add context to a message; useful for error messages and such. def addcontext(message, obj = nil) obj ||= @lexer message += " on line %s" % obj.line if file = obj.file message += " in file %s" % file end return message end # Create an AST array out of all of the args def aryfy(*args) if args[0].instance_of?(AST::ASTArray) result = args.shift args.each { |arg| result.push arg } else result = ast AST::ASTArray, :children => args end return result end # Create an AST object, and automatically add the file and line information if # available. def ast(klass, hash = nil) hash ||= {} unless hash.include?(:line) hash[:line] = @lexer.line end unless hash.include?(:file) if file = @lexer.file hash[:file] = file end end return klass.new(hash) end # The fully qualifed name, with the full namespace. def classname(name) [@lexer.namespace, name].join("::").sub(/^::/, '') end def clear initvars end # Raise a Parse error. def error(message) if brace = @lexer.expected message += "; expected '%s'" end except = Puppet::ParseError.new(message) except.line = @lexer.line if @lexer.file except.file = @lexer.file end raise except end def file=(file) unless FileTest.exists?(file) unless file =~ /\.pp$/ file = file + ".pp" end unless FileTest.exists?(file) raise Puppet::Error, "Could not find file %s" % file end end if @files.detect { |f| f.file == file } raise Puppet::AlreadyImportedError.new("Import loop detected") else @files << Puppet::Util::LoadedFile.new(file) @lexer.file = file end end # Find a class definition, relative to the current namespace. def findclass(namespace, name) fqfind namespace, name, classes end # Find a component definition, relative to the current namespace. def finddefine(namespace, name) fqfind namespace, name, definitions end # This is only used when nodes are looking up the code for their # parent nodes. def findnode(name) fqfind "", name, nodes end # The recursive method used to actually look these objects up. def fqfind(namespace, name, table) namespace = namespace.downcase name = name.downcase if name =~ /^::/ or namespace == "" classname = name.sub(/^::/, '') unless table[classname] self.load(classname) end return table[classname] end ary = namespace.split("::") while ary.length > 0 newname = (ary + [name]).join("::").sub(/^::/, '') if obj = table[newname] or (self.load(newname) and obj = table[newname]) return obj end # Delete the second to last object, which reduces our namespace by one. ary.pop end # If we've gotten to this point without finding it, see if the name # exists at the top namespace if obj = table[name] or (self.load(name) and obj = table[name]) return obj end return nil end # Import our files. def import(file) if Puppet[:ignoreimport] return AST::ASTArray.new(:children => []) end # use a path relative to the file doing the importing if @lexer.file dir = @lexer.file.sub(%r{[^/]+$},'').sub(/\/$/, '') else dir = "." end if dir == "" dir = "." end result = ast AST::ASTArray # We can't interpolate at this point since we don't have any # scopes set up. Warn the user if they use a variable reference pat = file if pat.index("$") Puppet.warning( "The import of #{pat} contains a variable reference;" + " variables are not interpolated for imports " + "in file #{@lexer.file} at line #{@lexer.line}" ) end files = Puppet::Module::find_manifests(pat, dir) if files.size == 0 raise Puppet::ImportError.new("No file(s) found for import " + "of '#{pat}'") end files.collect { |file| parser = Puppet::Parser::Parser.new(@astset) parser.files = self.files Puppet.debug("importing '%s'" % file) unless file =~ /^#{File::SEPARATOR}/ file = File.join(dir, file) end begin parser.file = file rescue Puppet::AlreadyImportedError # This file has already been imported to just move on next end # This will normally add code to the 'main' class. parser.parse } end def initialize(astset = nil) initvars() if astset @astset = astset end end # Initialize or reset all of our variables. def initvars @lexer = Puppet::Parser::Lexer.new() @files = [] @loaded = [] # This is where we store our classes and definitions and nodes. # Clear each hash, just to help the GC a bit. if defined?(@astset) [:classes, :definitions, :nodes].each do |name| @astset.send(name).clear end end @astset = ASTSet.new({}, {}, {}) end # Try to load a class, since we could not find it. def load(classname) return false if classname == "" filename = classname.gsub("::", File::SEPARATOR) loaded = false # First try to load the top-level module mod = filename.scan(/^[\w-]+/).shift unless @loaded.include?(mod) @loaded << mod begin import(mod) Puppet.info "Autoloaded module %s" % mod loaded = true rescue Puppet::ImportError => detail # We couldn't load the module end end unless filename == mod and ! @loaded.include?(mod) @loaded << mod # Then the individual file begin import(filename) Puppet.info "Autoloaded file %s from module %s" % [filename, mod] loaded = true rescue Puppet::ImportError => detail # We couldn't load the file end end return loaded end # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end # Create a new class, or merge with an existing class. def newclass(name, options = {}) name = name.downcase if definitions.include?(name) raise Puppet::ParseError, "Cannot redefine class %s as a definition" % name end code = options[:code] parent = options[:parent] # If the class is already defined, then add code to it. if other = @astset.classes[name] # Make sure the parents match if parent and other.parentclass and (parent != other.parentclass) error("Class %s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line]) end # This might be dangerous... if parent and ! other.parentclass other.parentclass = parent end # This might just be an empty, stub class. if code tmp = name if tmp == "" tmp = "main" end Puppet.debug addcontext("Adding code to %s" % tmp) # Else, add our code to it. if other.code and code other.code.children += code.children else other.code ||= code end end else # Define it anew. # Note we're doing something somewhat weird here -- we're setting # the class's namespace to its fully qualified name. This means # anything inside that class starts looking in that namespace first. args = {:namespace => name, :classname => name, :parser => self} args[:code] = code if code args[:parentclass] = parent if parent @astset.classes[name] = ast AST::HostClass, args end return @astset.classes[name] end # Create a new definition. def newdefine(name, options = {}) name = name.downcase if @astset.classes.include?(name) raise Puppet::ParseError, "Cannot redefine class %s as a definition" % name end # Make sure our definition doesn't already exist if other = @astset.definitions[name] error("%s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line]) end ns, whatever = namesplit(name) args = { :namespace => ns, :arguments => options[:arguments], :code => options[:code], :parser => self, :classname => name } [:code, :arguments].each do |param| args[param] = options[param] if options[param] end @astset.definitions[name] = ast AST::Component, args end # Create a new node. Nodes are special, because they're stored in a global # table, not according to namespaces. def newnode(names, options = {}) names = [names] unless names.instance_of?(Array) names.collect do |name| name = name.to_s.downcase if other = @astset.nodes[name] error("Node %s is already defined at %s:%s; cannot redefine" % [other.name, other.file, other.line]) end name = name.to_s if name.is_a?(Symbol) args = { :name => name, :parser => self } if options[:code] args[:code] = options[:code] end if options[:parent] args[:parentclass] = options[:parent] end @astset.nodes[name] = ast(AST::Node, args) @astset.nodes[name].classname = name @astset.nodes[name] end end def on_error(token,value,stack) #on '%s' at '%s' in\n'%s'" % [token,value,stack] #error = "line %s: parse error after '%s'" % # [@lexer.line,@lexer.last] error = "Syntax error at '%s'" % [value] if brace = @lexer.expected error += "; expected '%s'" % brace end except = Puppet::ParseError.new(error) except.line = @lexer.line if @lexer.file except.file = @lexer.file end raise except end # how should I do error handling here? def parse(string = nil) if string self.string = string end begin main = yyparse(@lexer,:scan) rescue Racc::ParseError => except error = Puppet::ParseError.new(except) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error rescue Puppet::ParseError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::Error => except # and this is a framework error except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::DevError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue => except error = Puppet::DevError.new(except.message) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error end if main # Store the results as the top-level class. newclass("", :code => main) end return @astset ensure @lexer.clear end # See if any of the files have changed. def reparse? if file = @files.detect { |file| file.changed? } return file.stamp else return false end end def string=(string) @lexer.string = string end end # $Id$ diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index 18ec15ac0..371f56ec1 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,405 +1,408 @@ # A resource that we're managing. This handles making sure that only subclasses # can set parameters. class Puppet::Parser::Resource require 'puppet/parser/resource/param' require 'puppet/parser/resource/reference' ResParam = Struct.new :name, :value, :source, :line, :file include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging attr_accessor :source, :line, :file, :scope, :rails_id attr_accessor :virtual, :override, :params, :translated attr_reader :exported attr_writer :tags # Proxy a few methods to our @ref object. [:builtin?, :type, :title].each do |method| define_method(method) do @ref.send(method) end end # Set up some boolean test methods [:exported, :translated, :override].each do |method| newmeth = (method.to_s + "?").intern define_method(newmeth) do self.send(method) end end def [](param) param = symbolize(param) if param == :title return self.title end if @params.has_key?(param) @params[param].value else nil end end # Add default values from our definition. def adddefaults defaults = scope.lookupdefaults(self.type) defaults.each do |name, param| unless @params.include?(param.name) self.debug "Adding default for %s" % param.name @params[param.name] = param end end end # Add any metaparams defined in our scope. This actually adds any metaparams # from any parent scope, and there's currently no way to turn that off. def addmetaparams Puppet::Type.eachmetaparam do |name| next if self[name] if val = scope.lookupvar(name.to_s, false) unless val == :undefined set Param.new(:name => name, :value => val, :source => scope.source) end end end end # Add any overrides for this object. def addoverrides - overrides = scope.lookupoverrides(self) + overrides = scope.configuration.resource_overrides(self) + raise "fix this test" overrides.each do |over| self.merge(over) end + # Remove the overrides, so that the configuration knows there + # are none left. overrides.clear end def builtin=(bool) @ref.builtin = bool end # Retrieve the associated definition and evaluate it. def evaluate if klass = @ref.definedtype finish() scope.deleteresource(self) return klass.evaluate(:scope => scope, :type => self.type, :title => self.title, :arguments => self.to_hash, :scope => self.scope, :virtual => self.virtual, :exported => self.exported ) elsif builtin? devfail "Cannot evaluate a builtin type" else self.fail "Cannot find definition %s" % self.type end ensure @evaluated = true end def exported=(value) if value @virtual = true @exported = value else @exported = value end end def evaluated? if defined? @evaluated and @evaluated true else false end end # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish addoverrides() adddefaults() addmetaparams() end def initialize(options) options = symbolize_options(options) # Collect the options necessary to make the reference. refopts = [:type, :title].inject({}) do |hash, param| hash[param] = options[param] options.delete(param) hash end @params = {} tmpparams = nil if tmpparams = options[:params] options.delete(:params) end # Now set the rest of the options. set_options(options) @ref = Reference.new(refopts) requiredopts(:scope, :source) @ref.scope = self.scope if tmpparams tmpparams.each do |param| # We use the method here, because it does type-checking. set(param) end end end # Merge an override resource in. def merge(resource) # Some of these might fail, but they'll fail in the way we want. resource.params.each do |name, param| set(param) end end def modify_rails(db_resource) args = rails_args args.each do |param, value| db_resource[param] = value unless db_resource[param] == value end # Handle file specially if (self.file and (!db_resource.file or db_resource.file != self.file)) db_resource.file = self.file end updated_params = @params.inject({}) do |hash, ary| hash[ary[0].to_s] = ary[1] hash end db_resource.ar_hash_merge(db_resource.get_params_hash(db_resource.param_values), updated_params, :create => Proc.new { |name, parameter| parameter.to_rails(db_resource) }, :delete => Proc.new { |values| values.each { |value| db_resource.param_values.delete(value) } }, :modify => Proc.new { |db, mem| mem.modify_rails_values(db) }) updated_tags = tags.inject({}) { |hash, tag| hash[tag] = tag hash } db_resource.ar_hash_merge(db_resource.get_tag_hash(), updated_tags, :create => Proc.new { |name, tag| db_resource.add_resource_tag(name) }, :delete => Proc.new { |tag| db_resource.resource_tags.delete(tag) }, :modify => Proc.new { |db, mem| # nothing here }) end # This *significantly* reduces the number of calls to Puppet.[]. def paramcheck? unless defined? @@paramcheck @@paramcheck = Puppet[:paramcheck] end @@paramcheck end # Verify that all passed parameters are valid. This throws an error if # there's a problem, so we don't have to worry about the return value. def paramcheck(param) param = param.to_s # Now make sure it's a valid argument to our class. These checks # are organized in order of commonhood -- most types, it's a valid # argument and paramcheck is enabled. if @ref.typeclass.validattr?(param) true elsif %w{name title}.include?(param) # always allow these true elsif paramcheck? self.fail Puppet::ParseError, "Invalid parameter '%s' for type '%s'" % [param.inspect, @ref.type] end end # A temporary occasion, until I get paths in the scopes figured out. def path to_s end # Return the short version of our name. def ref @ref.to_s end # You have to pass a Resource::Param to this. def set(param, value = nil, source = nil) if value and source param = Puppet::Parser::Resource::Param.new( :name => param, :value => value, :source => source ) elsif ! param.is_a?(Puppet::Parser::Resource::Param) raise ArgumentError, "Must pass a parameter or all necessary values" end # Because definitions are now parse-time, I can paramcheck immediately. paramcheck(param.name) if current = @params[param.name] # This is where we'd ignore any equivalent values if we wanted to, # but that would introduce a lot of really bad ordering issues. if param.source.child_of?(current.source) if param.add # Merge with previous value. param.value = [ current.value, param.value ].flatten end # Replace it, keeping all of its info. @params[param.name] = param else if Puppet[:trace] puts caller end msg = "Parameter '%s' is already set on %s" % [param.name, self.to_s] if current.source.to_s != "" msg += " by %s" % current.source end if current.file or current.line fields = [] fields << current.file if current.file fields << current.line.to_s if current.line msg += " at %s" % fields.join(":") end msg += "; cannot redefine" error = Puppet::ParseError.new(msg) error.file = param.file if param.file error.line = param.line if param.line raise error end else if self.source == param.source or param.source.child_of?(self.source) @params[param.name] = param else fail Puppet::ParseError, "Only subclasses can set parameters" end end end def tags unless defined? @tags @tags = scope.tags @tags << self.type end @tags end def to_hash @params.inject({}) do |hash, ary| param = ary[1] # Skip "undef" values. if param.value != :undef hash[param.name] = param.value end hash end end # Turn our parser resource into a Rails resource. def to_rails(host) args = rails_args db_resource = host.resources.build(args) # Handle file specially db_resource.file = self.file @params.each { |name, param| param.to_rails(db_resource) } tags.each { |tag| db_resource.add_resource_tag(tag) } return db_resource end def to_s self.ref end # Translate our object to a transportable object. def to_trans unless builtin? devfail "Tried to translate a non-builtin resource" end return nil if virtual? # Now convert to a transobject obj = Puppet::TransObject.new(@ref.title, @ref.type) to_hash.each do |p, v| if v.is_a?(Reference) v = v.to_ref elsif v.is_a?(Array) v = v.collect { |av| if av.is_a?(Reference) av = av.to_ref end av } end # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. obj[p.to_s] = if v.is_a?(Array) and v.length == 1 v[0] else v end end obj.file = self.file obj.line = self.line obj.tags = self.tags return obj end def virtual? self.virtual end private def rails_args return [:type, :title, :line, :exported].inject({}) do |hash, param| # 'type' isn't a valid column name, so we have to use another name. to = (param == :type) ? :restype : param if value = self.send(param) hash[to] = value end hash end end end # $Id$ diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 1fb4f6906..cb6d98584 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,601 +1,476 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'puppet/parser/parser' require 'puppet/parser/templatewrapper' require 'puppet/transportable' require 'strscan' class Puppet::Parser::Scope require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) include Enumerable include Puppet::Util::Errors attr_accessor :parent, :level, :interp, :source attr_accessor :name, :type, :base, :keyword attr_accessor :top, :translated, :exported, :virtual, :configuration # Proxy accessors def host @configuration.host end def interpreter @configuration.interpreter end # Is the value true? This allows us to control the definition of truth # in one place. def self.true?(value) if value == false or value == "" or value == :undef return false else return true end end # Add to our list of namespaces. def add_namespace(ns) return false if @namespaces.include?(ns) if @namespaces == [""] @namespaces = [ns] else @namespaces << ns end end # Is the type a builtin type? def builtintype?(type) if typeklass = Puppet::Type.type(type) return typeklass else return false end end - # Create a new child scope. - def child=(scope) - @children.push(scope) - - # Copy all of the shared tables over to the child. - @@sharedtables.each do |name| - scope.send(name.to_s + "=", self.send(name)) - end - end - - # Verify that the given object isn't defined elsewhere. - def chkobjectclosure(obj) - if exobj = @definedtable[obj.ref] - typeklass = Puppet::Type.type(obj.type) - if typeklass and ! typeklass.isomorphic? - Puppet.info "Allowing duplicate %s" % type - else - # Either it's a defined type, which are never - # isomorphic, or it's a non-isomorphic type. - msg = "Duplicate definition: %s is already defined" % obj.ref - - if exobj.file and exobj.line - msg << " in file %s at line %s" % - [exobj.file, exobj.line] - end - - if obj.line or obj.file - msg << "; cannot redefine" - end - - raise Puppet::ParseError.new(msg) - end - end - - return true - end - - # Remove a specific child. - def delete(child) - @children.delete(child) - end - - # Remove a resource from the various tables. This is only used when - # a resource maps to a definition and gets evaluated. - def deleteresource(resource) - if @definedtable[resource.ref] - @definedtable.delete(resource.ref) - end - - if @children.include?(resource) - @children.delete(resource) - end - end - # Are we the top scope? def topscope? @level == 1 end - # Yield each child scope in turn - def each - @children.each { |child| - yield child - } - end - - # Evaluate a list of classes. - def evalclasses(*classes) - retval = [] - classes.each do |klass| - if obj = findclass(klass) - obj.safeevaluate :scope => self - retval << klass - end - end - retval - end - def exported? self.exported end def findclass(name) @namespaces.each do |namespace| if r = interp.findclass(namespace, name) return r end end return nil end def finddefine(name) @namespaces.each do |namespace| if r = interp.finddefine(namespace, name) return r end end return nil end def findresource(string, name = nil) - if name - string = "%s[%s]" % [string.capitalize, name] - end - - @definedtable[string] - end - - # Recursively complete the whole tree, in preparation for - # translation or storage. - def finish - self.each do |obj| - obj.finish - end + configuration.findresource(string, name) end # Initialize our new scope. Defaults to having no parent and to # being declarative. def initialize(hash = {}) - @finished = false if hash.include?(:namespace) if n = hash[:namespace] @namespaces = [n] end hash.delete(:namespace) else @namespaces = [""] end hash.each { |name, val| method = name.to_s + "=" if self.respond_to? method self.send(method, val) else raise Puppet::DevError, "Invalid scope argument %s" % name end } @tags = [] - # Our child scopes and objects - @children = [] - # The symbol table for this scope. This is where we store variables. @symtable = {} # All of the defaults set for types. It's a hash of hashes, # with the first key being the type, then the second key being # the parameter. @defaultstable = Hash.new { |dhash,type| dhash[type] = {} } end # Collect all of the defaults set at any higher scopes. # This is a different type of lookup because it's additive -- # it collects all of the defaults, with defaults in closer scopes # overriding those in later scopes. def lookupdefaults(type) values = {} # first collect the values from the parents unless parent.nil? parent.lookupdefaults(type).each { |var,value| values[var] = value } end # then override them with any current values # this should probably be done differently if @defaultstable.include?(type) @defaultstable[type].each { |var,value| values[var] = value } end #Puppet.debug "Got defaults for %s: %s" % # [type,values.inspect] return values end - # Look up all of the exported objects of a given type. - def lookupexported(type) - @definedtable.find_all do |name, r| - r.type == type and r.exported? - end - end - - def lookupoverrides(obj) - @overridetable[obj.ref] - end - # Look up a defined type. def lookuptype(name) finddefine(name) || findclass(name) end def lookup_qualified_var(name, usestring) parts = name.split(/::/) shortname = parts.pop klassname = parts.join("::") klass = findclass(klassname) unless klass raise Puppet::ParseError, "Could not find class %s" % klassname end unless kscope = class_scope(klass) raise Puppet::ParseError, "Class %s has not been evaluated so its variables cannot be referenced" % klass.classname end return kscope.lookupvar(shortname, usestring) end private :lookup_qualified_var # Look up a variable. The simplest value search we do. Default to returning # an empty string for missing values, but support returning a constant. def lookupvar(name, usestring = true) # If the variable is qualified, then find the specified scope and look the variable up there instead. if name =~ /::/ return lookup_qualified_var(name, usestring) end # We can't use "if @symtable[name]" here because the value might be false if @symtable.include?(name) if usestring and @symtable[name] == :undef return "" else return @symtable[name] end elsif self.parent return parent.lookupvar(name, usestring) elsif usestring return "" else return :undefined end end def namespaces @namespaces.dup end # Create a new scope. def newscope(hash = {}) hash[:parent] = self #debug "Creating new scope, level %s" % [self.level + 1] return Puppet::Parser::Scope.new(hash) end # Is this class for a node? This is used to make sure that # nodes and classes with the same name conflict (#620), which # is required because of how often the names are used throughout # the system, including on the client. def nodescope? defined?(@nodescope) and @nodescope end - # Return the list of remaining overrides. - def overrides - #@overridetable.collect { |name, overs| overs }.flatten - @overridetable.values.flatten - end - # We probably shouldn't cache this value... But it's a lot faster # than doing lots of queries. def parent unless defined?(@parent) @parent = configuration.parent(self) end @parent end # Return the list of scopes up to the top scope, ordered with our own first. # This is used for looking up variables and defaults. def scope_path if parent [self, parent.scope_path].flatten.compact else [self] end end def resources @definedtable.values end # Store the fact that we've evaluated a given class. We use a hash # that gets inherited from the top scope down, rather than a global # hash. We store the object ID, not class name, so that we # can support multiple unrelated classes with the same name. def setclass(klass) if klass.is_a?(AST::HostClass) unless klass.classname raise Puppet::DevError, "Got a %s with no fully qualified name" % klass.class end @configuration.class_set(klass.classname, self) else raise Puppet::DevError, "Invalid class %s" % klass.inspect end if klass.is_a?(AST::Node) @nodescope = true end nil end - # Set all of our facts in the top-level scope. - def setfacts(facts) - facts.each { |var, value| - self.setvar(var, value) - } - end - # Add a new object to our object table and the global list, and do any necessary # checks. - def setresource(obj) - self.chkobjectclosure(obj) - - @children << obj + def setresource(resource) + @configuration.store_resource(resource) # Mark the resource as virtual or exported, as necessary. if self.exported? - obj.exported = true + resource.exported = true elsif self.virtual? - obj.virtual = true + resource.virtual = true end + raise "setresource's tests aren't fixed" - # The global table - @definedtable[obj.ref] = obj - - return obj + return resource end # Override a parameter in an existing object. If the object does not yet # exist, then cache the override in a global table, so it can be flushed # at the end. def setoverride(resource) - resource.override = true + @configuration.store_override(resource) + raise "setoverride tests aren't fixed" if obj = @definedtable[resource.ref] obj.merge(resource) else @overridetable[resource.ref] << resource end end # Set defaults for a type. The typename should already be downcased, # so that the syntax is isolated. We don't do any kind of type-checking # here; instead we let the resource do it when the defaults are used. def setdefaults(type, params) table = @defaultstable[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) params.each { |param| #Puppet.debug "Default for %s is %s => %s" % # [type,ary[0].inspect,ary[1].inspect] if @@declarative if table.include?(param.name) self.fail "Default already defined for %s { %s }" % [type,param.name] end else if table.include?(param.name) # we should maybe allow this warning to be turned off... Puppet.warning "Replacing default for %s { %s }" % [type,param.name] end end table[param.name] = param } end # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope # to be reassigned if we're declarative (which is the default). def setvar(name,value, file = nil, line = nil) #Puppet.debug "Setting %s to '%s' at level %s" % # [name.inspect,value,self.level] if @symtable.include?(name) if @@declarative error = Puppet::ParseError.new("Cannot reassign variable %s" % name) if file error.file = file end if line error.line = line end raise error else Puppet.warning "Reassigning %s to %s" % [name,value] end end @symtable[name] = value end # Return an interpolated string. def strinterp(string, file = nil, line = nil) # Most strings won't have variables in them. ss = StringScanner.new(string) out = "" while not ss.eos? if ss.scan(/^\$\{((\w*::)*\w+)\}|^\$((\w*::)*\w+)/) # If it matches the backslash, then just retun the dollar sign. if ss.matched == '\\$' out << '$' else # look the variable up out << lookupvar(ss[1] || ss[3]).to_s || "" end elsif ss.scan(/^\\(.)/) # Puppet.debug("Got escape: pos:%d; m:%s" % [ss.pos, ss.matched]) case ss[1] when 'n' out << "\n" when 't' out << "\t" when 's' out << " " when '\\' out << '\\' when '$' out << '$' else str = "Unrecognised escape sequence '#{ss.matched}'" if file str += " in file %s" % file end if line str += " at line %s" % line end Puppet.warning str out << ss.matched end elsif ss.scan(/^\$/) out << '$' elsif ss.scan(/^\\\n/) # an escaped carriage return next else tmp = ss.scan(/[^\\$]+/) # Puppet.debug("Got other: pos:%d; m:%s" % [ss.pos, tmp]) unless tmp error = Puppet::ParseError.new("Could not parse string %s" % string.inspect) {:file= => file, :line= => line}.each do |m,v| error.send(m, v) if v end raise error end out << tmp end end return out end # Add a tag to our current list. These tags will be added to all # of the objects contained in this scope. def tag(*ary) ary.each { |tag| if tag.nil? or tag == "" puts caller Puppet.debug "got told to tag with %s" % tag.inspect next end unless tag =~ /^\w[-\w]*$/ fail Puppet::ParseError, "Invalid tag %s" % tag.inspect end tag = tag.to_s unless @tags.include?(tag) #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag] @tags << tag end } end # Return the tags associated with this scope. It's basically # just our parents' tags, plus our type. We don't cache this value # because our parent tags might change between calls. def tags tmp = [] + @tags unless ! defined? @type or @type.nil? or @type == "" tmp << @type.to_s end if parent #info "Looking for tags in %s" % parent.type parent.tags.each { |tag| if tag.nil? or tag == "" Puppet.debug "parent returned tag %s" % tag.inspect next end unless tmp.include?(tag) tmp << tag end } end return tmp.sort.uniq end # Used mainly for logging def to_s if self.name return "%s[%s]" % [@type, @name] else return self.type.to_s end end - # Convert all of our objects as necessary. - def translate - ret = @children.collect do |child| - case child - when Puppet::Parser::Resource - child.to_trans - when self.class - child.translate - else - devfail "Got %s for translation" % child.class - end - end.reject { |o| o.nil? } + # Convert our resource to a TransBucket. + def to_trans + raise "Scope#to_trans needs to be tested" bucket = Puppet::TransBucket.new ret case self.type when "": bucket.type = "main" when nil: devfail "A Scope with no type" else bucket.type = @type end if self.name bucket.name = self.name end return bucket end # Undefine a variable; only used for testing. def unsetvar(var) if @symtable.include?(var) @symtable.delete(var) end end def virtual? self.virtual || self.exported? end end # $Id$ diff --git a/test/language/configuration.rb b/test/language/configuration.rb index 4cbba8063..aafdf8356 100755 --- a/test/language/configuration.rb +++ b/test/language/configuration.rb @@ -1,131 +1,716 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppettest' require 'puppettest/parsertesting' require 'puppet/parser/configuration' # Test our configuration object. class TestConfiguration < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting Config = Puppet::Parser::Configuration Scope = Puppet::Parser::Scope + Node = Puppet::Network::Handler.handler(:node) + SimpleNode = Node::SimpleNode - def mkconfig - Config.new(:host => "foo", :interpreter => "interp") + def mknode(name = "foo") + @node = SimpleNode.new(name) end - def test_initialize - # Make sure we get an error if we don't send an interpreter - assert_raise(ArgumentError, "Did not fail when missing host") do - Config.new(:interpreter => "yay" ) - end - assert_raise(ArgumentError, "Did not fail when missing interp") do - Config.new(:host => "foo") - end + def mkparser + # This should mock an interpreter + @parser = mock 'parser' + end + + def mkconfig(options = {}) + @config = Config.new(mknode, mkparser, options) + end - # Now check the defaults + def test_initialize config = nil assert_nothing_raised("Could not init config with all required options") do - config = Config.new(:host => "foo", :interpreter => "interp") + config = Config.new("foo", "parser") end - assert_equal("foo", config.host, "Did not set host correctly") - assert_equal("interp", config.interpreter, "Did not set interpreter correctly") - assert_equal({}, config.facts, "Did not set default facts") + assert_equal("foo", config.node, "Did not set node correctly") + assert_equal("parser", config.parser, "Did not set parser correctly") - # Now make a new one with facts, to make sure the facts get set appropriately - assert_nothing_raised("Could not init config with all required options") do - config = Config.new(:host => "foo", :interpreter => "interp", :facts => {"a" => "b"}) + # We're not testing here whether we call initvars, because it's too difficult to + # mock. + + # Now try it with some options + assert_nothing_raised("Could not init config with extra options") do + config = Config.new("foo", "parser", :ast_nodes => false) end - assert_equal({"a" => "b"}, config.facts, "Did not set facts") + + assert_equal(false, config.ast_nodes?, "Did not set ast_nodes? correctly") end def test_initvars config = mkconfig [:class_scopes, :resource_table, :exported_resources, :resource_overrides].each do |table| assert_instance_of(Hash, config.send(:instance_variable_get, "@#{table}"), "Did not set %s table correctly" % table) end + assert_instance_of(Scope, config.topscope, "Did not create a topscope") + graph = config.instance_variable_get("@scope_graph") + assert_instance_of(GRATR::Digraph, graph, "Did not create scope graph") + assert(graph.vertex?(config.topscope), "Did not add top scope as a vertex in the graph") end # Make sure we store and can retrieve references to classes and their scopes. def test_class_set_and_class_scope - klass = Object.new + klass = mock 'ast_class' klass.expects(:classname).returns("myname") config = mkconfig + config.expects(:tag).with("myname") assert_nothing_raised("Could not set class") do config.class_set "myname", "myscope" end # First try to retrieve it by name. assert_equal("myscope", config.class_scope("myname"), "Could not retrieve class scope by name") # Then by object assert_equal("myscope", config.class_scope(klass), "Could not retrieve class scope by object") end def test_classlist config = mkconfig config.class_set "", "empty" config.class_set "one", "yep" config.class_set "two", "nope" # Make sure our class list is correct assert_equal(%w{one two}.sort, config.classlist.sort, "Did not get correct class list") end # Make sure collections get added to our internal array def test_add_collection config = mkconfig assert_nothing_raised("Could not add collection") do config.add_collection "nope" end assert_equal(%w{nope}, config.instance_variable_get("@collections"), "Did not add collection") end # Make sure we create a graph of scopes. def test_newscope config = mkconfig - graph = config.instance_variable_get("@graph") + graph = config.instance_variable_get("@scope_graph") assert_instance_of(Scope, config.topscope, "Did not create top scope") assert_instance_of(GRATR::Digraph, graph, "Did not create graph") assert(graph.vertex?(config.topscope), "The top scope is not a vertex in the graph") # Now that we've got the top scope, create a new, subscope subscope = nil assert_nothing_raised("Could not create subscope") do subscope = config.newscope end assert_instance_of(Scope, subscope, "Did not create subscope") assert(graph.edge?(config.topscope, subscope), "An edge between top scope and subscope was not added") # Make sure a scope can find its parent. assert(config.parent(subscope), "Could not look up parent scope on configuration") assert_equal(config.topscope.object_id, config.parent(subscope).object_id, "Did not get correct parent scope from configuration") assert_equal(config.topscope.object_id, subscope.parent.object_id, "Scope did not correctly retrieve its parent scope") # Now create another, this time specifying the parent scope another = nil assert_nothing_raised("Could not create subscope") do another = config.newscope(subscope) end assert_instance_of(Scope, another, "Did not create second subscope") assert(graph.edge?(subscope, another), "An edge between parent scope and second subscope was not added") # Make sure it can find its parent. assert(config.parent(another), "Could not look up parent scope of second subscope on configuration") assert_equal(subscope.object_id, config.parent(another).object_id, "Did not get correct parent scope of second subscope from configuration") assert_equal(subscope.object_id, another.parent.object_id, "Second subscope did not correctly retrieve its parent scope") # And make sure both scopes show up in the right order in the search path assert_equal([another.object_id, subscope.object_id, config.topscope.object_id], another.scope_path.collect { |p| p.object_id }, "Did not get correct scope path") end + + # The heart of the action. + def test_compile + config = mkconfig + [:set_node_parameters, :evaluate_main, :evaluate_ast_nodes, :evaluate_classes, :evaluate_generators, :fail_on_unevaluated, :finish].each do |method| + config.expects(method) + end + config.expects(:extract).returns(:config) + assert_equal(:config, config.compile, "Did not return the results of the extraction") + end + + # Test setting the node's parameters into the top scope. + def test_set_node_parameters + config = mkconfig + @node.parameters = {"a" => "b", "c" => "d"} + scope = config.topscope + @node.parameters.each do |param, value| + scope.expects(:setvar).with(param, value) + end + + assert_nothing_raised("Could not call 'set_node_parameters'") do + config.send(:set_node_parameters) + end + end + + # Test that we can evaluate the main class, which is the one named "" in namespace + # "". + def test_evaluate_main + config = mkconfig + main = mock 'main_class' + config.topscope.expects(:source=).with(main) + main.expects(:safeevaluate).with(:scope => config.topscope, :nosubscope => true) + @parser.expects(:findclass).with("", "").returns(main) + + assert_nothing_raised("Could not call evaluate_main") do + config.send(:evaluate_main) + end + end + + # Make sure we either don't look for nodes, or that we find and evaluate the right object. + def test_evaluate_ast_node + # First try it with ast_nodes disabled + config = mkconfig :ast_nodes => false + config.expects(:ast_nodes?).returns(false) + config.parser.expects(:nodes).never + + assert_nothing_raised("Could not call evaluate_ast_node when ast nodes are disabled") do + config.send(:evaluate_ast_node) + end + + # Now try it with them enabled, but no node found. + nodes = mock 'node_hash' + config = mkconfig :ast_nodes => true + config.expects(:ast_nodes?).returns(true) + config.parser.expects(:nodes).returns(nodes).times(4) + + # Set some names for our test + @node.names = %w{a b c} + nodes.expects(:[]).with("a").returns(nil) + nodes.expects(:[]).with("b").returns(nil) + nodes.expects(:[]).with("c").returns(nil) + + # It should check this last, of course. + nodes.expects(:[]).with("default").returns(nil) + + # And make sure the lack of a node throws an exception + assert_raise(Puppet::ParseError, "Did not fail when we couldn't find an ast node") do + config.send(:evaluate_ast_node) + end + + # Finally, make sure it works dandily when we have a node + nodes = mock 'hash' + config = mkconfig :ast_nodes => true + config.expects(:ast_nodes?).returns(true) + config.parser.expects(:nodes).returns(nodes).times(3) + + node = mock 'node' + node.expects(:safeevaluate).with(:scope => config.topscope) + # Set some names for our test + @node.names = %w{a b c} + nodes.expects(:[]).with("a").returns(nil) + nodes.expects(:[]).with("b").returns(nil) + nodes.expects(:[]).with("c").returns(node) + nodes.expects(:[]).with("default").never + + # And make sure the lack of a node throws an exception + assert_nothing_raised("Failed when a node was found") do + config.send(:evaluate_ast_node) + end + + # Lastly, check when we actually find the default. + nodes = mock 'hash' + config = mkconfig :ast_nodes => true + config.expects(:ast_nodes?).returns(true) + config.parser.expects(:nodes).returns(nodes).times(4) + + node = mock 'node' + node.expects(:safeevaluate).with(:scope => config.topscope) + # Set some names for our test + @node.names = %w{a b c} + nodes.expects(:[]).with("a").returns(nil) + nodes.expects(:[]).with("b").returns(nil) + nodes.expects(:[]).with("c").returns(nil) + nodes.expects(:[]).with("default").returns(node) + + # And make sure the lack of a node throws an exception + assert_nothing_raised("Failed when a node was found") do + config.send(:evaluate_ast_node) + end + end + + # Make sure our config object handles tags appropriately. + def test_tags + config = mkconfig + config.send(:tag, "one") + assert_equal(%w{one}, config.send(:tags), "Did not add tag") + + config.send(:tag, "two", "three") + assert_equal(%w{one two three}, config.send(:tags), "Did not add new tags") + + config.send(:tag, "two") + assert_equal(%w{one two three}, config.send(:tags), "Allowed duplicate tag") + end + + def test_evaluate_classes + config = mkconfig + classes = { + "one" => mock('class one'), + "three" => mock('class three') + } + + classes.each do |name, obj| + config.parser.expects(:findclass).with("", name).returns(obj) + obj.expects(:safeevaluate).with(:scope => config.topscope) + end + %w{two four}.each do |name| + config.parser.expects(:findclass).with("", name).returns(nil) + end + + config.expects(:tag).with("two") + config.expects(:tag).with("four") + + @node.classes = %w{one two three four} + assert_nothing_raised("could not call evaluate_classes") do + config.send(:evaluate_classes) + end + end + + def test_evaluate_collections + config = mkconfig + + colls = [] + + # Make sure we return false when there's nothing there. + assert(! config.send(:evaluate_collections), "Returned true when there were no collections") + + # And when the collections fail to evaluate. + colls << mock("coll1-false") + colls << mock("coll2-false") + colls.each { |c| c.expects(:evaluate).returns(false) } + + config.instance_variable_set("@collections", colls) + assert(! config.send(:evaluate_collections), "Returned true when collections both evaluated nothing") + + # Now have one of the colls evaluate + colls.clear + colls << mock("coll1-one-true") + colls << mock("coll2-one-true") + colls[0].expects(:evaluate).returns(true) + colls[1].expects(:evaluate).returns(false) + assert(config.send(:evaluate_collections), "Did not return true when one collection evaluated true") + + # And have them both eval true + colls.clear + colls << mock("coll1-both-true") + colls << mock("coll2-both-true") + colls[0].expects(:evaluate).returns(true) + colls[1].expects(:evaluate).returns(true) + assert(config.send(:evaluate_collections), "Did not return true when both collections evaluated true") + end + + def test_unevaluated_resources + config = mkconfig + resources = {} + config.instance_variable_set("@resource_table", resources) + + # First test it when the table is empty + assert_nil(config.send(:unevaluated_resources), "Somehow found unevaluated resources in an empty table") + + # Then add a builtin resources + resources["one"] = mock("builtin only") + resources["one"].expects(:builtin?).returns(true) + assert_nil(config.send(:unevaluated_resources), "Considered a builtin resource unevaluated") + + # And do both builtin and non-builtin but already evaluated + resources.clear + resources["one"] = mock("builtin (with eval)") + resources["one"].expects(:builtin?).returns(true) + resources["two"] = mock("evaled (with builtin)") + resources["two"].expects(:builtin?).returns(false) + resources["two"].expects(:evaluated?).returns(true) + assert_nil(config.send(:unevaluated_resources), "Considered either a builtin or evaluated resource unevaluated") + + # Now a single unevaluated resource. + resources.clear + resources["one"] = mock("unevaluated") + resources["one"].expects(:builtin?).returns(false) + resources["one"].expects(:evaluated?).returns(false) + assert_equal([resources["one"]], config.send(:unevaluated_resources), "Did not find unevaluated resource") + + # With two uneval'ed resources, and an eval'ed one thrown in + resources.clear + resources["one"] = mock("unevaluated one") + resources["one"].expects(:builtin?).returns(false) + resources["one"].expects(:evaluated?).returns(false) + resources["two"] = mock("unevaluated two") + resources["two"].expects(:builtin?).returns(false) + resources["two"].expects(:evaluated?).returns(false) + resources["three"] = mock("evaluated") + resources["three"].expects(:builtin?).returns(false) + resources["three"].expects(:evaluated?).returns(true) + + result = config.send(:unevaluated_resources) + %w{one two}.each do |name| + assert(result.include?(resources[name]), "Did not find %s in the unevaluated list" % name) + end + end + + def test_evaluate_definitions + # First try the case where there's nothing to return + config = mkconfig + config.expects(:unevaluated_resources).returns(nil) + + assert_nothing_raised("Could not test for unevaluated resources") do + assert(! config.send(:evaluate_definitions), "evaluate_definitions returned true when no resources were evaluated") + end + + # Now try it with resources left to evaluate + resources = [] + res1 = mock("resource1") + res1.expects(:evaluate) + res2 = mock("resource2") + res2.expects(:evaluate) + resources << res1 << res2 + config = mkconfig + config.expects(:unevaluated_resources).returns(resources) + + assert_nothing_raised("Could not test for unevaluated resources") do + assert(config.send(:evaluate_definitions), "evaluate_definitions returned false when resources were evaluated") + end + end + + def test_evaluate_generators + # First try the case where we have nothing to do + config = mkconfig + config.expects(:evaluate_definitions).returns(false) + config.expects(:evaluate_collections).returns(false) + + assert_nothing_raised("Could not call :eval_iterate") do + config.send(:evaluate_generators) + end + + # FIXME I could not get this test to work, but the code is short + # enough that I'm ok with it. + # It's important that collections are evaluated before definitions, + # so make sure that's the case by verifying that collections get tested + # twice but definitions only once. + #config = mkconfig + #config.expects(:evaluate_collections).returns(true).returns(false) + #config.expects(:evaluate_definitions).returns(false) + #config.send(:eval_iterate) + end + + def test_store + config = mkconfig + Puppet.features.expects(:rails?).returns(true) + Puppet::Rails.expects(:connect) + + args = {:name => "yay"} + config.expects(:store_to_active_record).with(args) + config.send(:store, args) + end + + def test_store_to_active_record + config = mkconfig + args = {:name => "yay"} + Puppet::Rails::Host.stubs(:transaction).yields + Puppet::Rails::Host.expects(:store).with(args) + config.send(:store_to_active_record, args) + end + + # Make sure that 'finish' gets called on all of our resources. + def test_finish + config = mkconfig + table = config.instance_variable_get("@resource_table") + + # Add a resource that does respond to :finish + yep = mock("finisher") + yep.expects(:respond_to?).with(:finish).returns(true) + yep.expects(:finish) + table["yep"] = yep + + # And one that does not + dnf = mock("dnf") + dnf.expects(:respond_to?).with(:finish).returns(false) + table["dnf"] = dnf + + config.send(:finish) + end + + def test_extract + config = mkconfig + config.expects(:extraction_format).returns(:whatever) + config.expects(:extract_to_whatever).returns(:result) + assert_equal(:result, config.send(:extract), "Did not return extraction result as the method result") + end + + # We want to make sure that the scope and resource graphs translate correctly + def test_extract_to_transportable_simple + # Start with a really simple graph -- one scope, one resource. + config = mkconfig + resources = config.instance_variable_get("@resource_graph") + scopes = config.instance_variable_get("@scope_graph") + + # Get rid of the topscope + scopes.vertices.each { |v| scopes.remove_vertex!(v) } + + scope = mock("scope") + scope.expects(:to_trans).returns([]) + scopes.add_vertex! scope + + # The topscope is the key to picking out the top of the graph. + config.instance_variable_set("@topscope", scope) + + resource = mock "resource" + resource.expects(:to_trans).returns(:resource) + resources.add_edge! scope, resource + + result = nil + assert_nothing_raised("Could not extract transportable configuration") do + result = config.send :extract_to_transportable + end + assert_equal([:resource], result, "Did not translate simple configuration correctly") + end + + def test_extract_to_transportable_complex + # Now try it with a more complicated graph -- a three tier graph, each tier + # having a scope and a resource. + config = mkconfig + resources = config.instance_variable_get("@resource_graph") + scopes = config.instance_variable_get("@scope_graph") + + # Get rid of the topscope + scopes.vertices.each { |v| scopes.remove_vertex!(v) } + + fakebucket = Class.new(Array) do + attr_accessor :name + def initialize(n) + @name = n + end + end + + # Create our scopes. + top = mock("top") + top.expects(:to_trans).returns(fakebucket.new("top")) + # The topscope is the key to picking out the top of the graph. + config.instance_variable_set("@topscope", top) + middle = mock("middle") + middle.expects(:to_trans).returns(fakebucket.new("middle")) + scopes.add_edge! top, middle + bottom = mock("bottom") + bottom.expects(:to_trans).returns(fakebucket.new("bottom")) + scopes.add_edge! middle, bottom + + topres = mock "topres" + topres.expects(:to_trans).returns(:topres) + resources.add_edge! top, topres + + midres = mock "midres" + midres.expects(:to_trans).returns(:midres) + resources.add_edge! middle, midres + + botres = mock "botres" + botres.expects(:to_trans).returns(:botres) + resources.add_edge! bottom, botres + + result = nil + assert_nothing_raised("Could not extract transportable configuration") do + result = config.send :extract_to_transportable + end + assert_equal([[[:botres], :midres], :topres], result, "Did not translate medium configuration correctly") + end + + def test_verify_uniqueness + config = mkconfig + + resources = config.instance_variable_get("@resource_table") + resource = mock("noconflict") + resource.expects(:ref).returns("File[yay]") + assert_nothing_raised("Raised an exception when there should have been no conflict") do + config.send(:verify_uniqueness, resource) + end + + # Now try the case where our type is isomorphic + resources["thing"] = true + + isoconflict = mock("isoconflict") + isoconflict.expects(:ref).returns("thing") + isoconflict.expects(:type).returns("testtype") + faketype = mock("faketype") + faketype.expects(:isomorphic?).returns(false) + faketype.expects(:name).returns("whatever") + Puppet::Type.expects(:type).with("testtype").returns(faketype) + assert_nothing_raised("Raised an exception when was a conflict in non-isomorphic types") do + config.send(:verify_uniqueness, isoconflict) + end + + # Now test for when we actually have an exception + initial = mock("initial") + resources["thing"] = initial + initial.expects(:file).returns(false) + + conflict = mock("conflict") + conflict.expects(:ref).returns("thing").times(2) + conflict.expects(:type).returns("conflict") + conflict.expects(:file).returns(false) + conflict.expects(:line).returns(false) + + faketype = mock("faketype") + faketype.expects(:isomorphic?).returns(true) + Puppet::Type.expects(:type).with("conflict").returns(faketype) + assert_raise(Puppet::ParseError, "Did not fail when two isomorphic resources conflicted") do + config.send(:verify_uniqueness, conflict) + end + end + + def test_store_resource + # Run once when there's no conflict + config = mkconfig + table = config.instance_variable_get("@resource_table") + resource = mock("resource") + resource.expects(:ref).returns("yay") + config.expects(:verify_uniqueness).with(resource) + scope = mock("scope") + + graph = config.instance_variable_get("@resource_graph") + graph.expects(:add_edge!).with(scope, resource) + + assert_nothing_raised("Could not store resource") do + config.store_resource(scope, resource) + end + assert_equal(resource, table["yay"], "Did not store resource in table") + + # Now for conflicts + config = mkconfig + table = config.instance_variable_get("@resource_table") + resource = mock("resource") + config.expects(:verify_uniqueness).with(resource).raises(ArgumentError) + + assert_raise(ArgumentError, "Did not raise uniqueness exception") do + config.store_resource(scope, resource) + end + assert(table.empty?, "Conflicting resource was stored in table") + end + + def test_fail_on_unevaluated + config = mkconfig + config.expects(:fail_on_unevaluated_overrides) + config.expects(:fail_on_unevaluated_resource_collections) + config.send :fail_on_unevaluated + end + + def test_store_override + # First test the case when the resource is not present. + config = mkconfig + overrides = config.instance_variable_get("@resource_overrides") + override = Object.new + override.expects(:ref).returns(:myref).times(2) + override.expects(:override=).with(true) + + assert_nothing_raised("Could not call store_override") do + config.store_override(override) + end + assert_instance_of(Array, overrides[:myref], "Overrides table is not a hash of arrays") + assert_equal(override, overrides[:myref][0], "Did not store override in appropriately named array") + + # And when the resource already exists. + resource = mock 'resource' + resources = config.instance_variable_get("@resource_table") + resources[:resref] = resource + + override = mock 'override' + resource.expects(:merge).with(override) + override.expects(:override=).with(true) + override.expects(:ref).returns(:resref) + assert_nothing_raised("Could not call store_override when the resource already exists.") do + config.store_override(override) + end + end + + def test_resource_overrides + config = mkconfig + overrides = config.instance_variable_get("@resource_overrides") + overrides[:test] = :yay + resource = mock 'resource' + resource.expects(:ref).returns(:test) + + assert_equal(:yay, config.resource_overrides(resource), "Did not return overrides from table") + end + + def test_fail_on_unevaluated_resource_collections + config = mkconfig + collections = config.instance_variable_get("@collections") + + # Make sure we're fine when the list is empty + assert_nothing_raised("Failed when no collections were present") do + config.send :fail_on_unevaluated_resource_collections + end + + # And that we're fine when we've got collections but with no resources + collections << mock('coll') + collections[0].expects(:resources).returns(nil) + assert_nothing_raised("Failed when no resource collections were present") do + config.send :fail_on_unevaluated_resource_collections + end + + # But that we do fail when we've got resource collections left. + collections.clear + + # return both an array and a string, because that's tested internally + collections << mock('coll returns one') + collections[0].expects(:resources).returns(:something) + + collections << mock('coll returns many') + collections[1].expects(:resources).returns([:one, :two]) + + assert_raise(Puppet::ParseError, "Did not fail on unevaluated resource collections") do + config.send :fail_on_unevaluated_resource_collections + end + end + + def test_fail_on_unevaluated_overrides + config = mkconfig + overrides = config.instance_variable_get("@resource_overrides") + + # Make sure we're fine when the list is empty + assert_nothing_raised("Failed when no collections were present") do + config.send :fail_on_unevaluated_overrides + end + + # But that we fail if there are any overrides left in the table. + overrides[:yay] = [] + overrides[:foo] = [] + overrides[:bar] = [mock("override")] + overrides[:bar][0].expects(:ref).returns("yay") + assert_raise(Puppet::ParseError, "Failed to fail when overrides remain") do + config.send :fail_on_unevaluated_overrides + end + end + + def test_find_resource + config = mkconfig + resources = config.instance_variable_get("@resource_table") + + assert_nothing_raised("Could not call findresource when the resource table was empty") do + assert_nil(config.findresource("yay", "foo"), "Returned a non-existent resource") + assert_nil(config.findresource("yay[foo]"), "Returned a non-existent resource") + end + + resources["Foo[bar]"] = :yay + assert_nothing_raised("Could not call findresource when the resource table was not empty") do + assert_equal(:yay, config.findresource("foo", "bar"), "Returned a non-existent resource") + assert_equal(:yay, config.findresource("Foo[bar]"), "Returned a non-existent resource") + end + end end