diff --git a/Rakefile b/Rakefile index d560b85b9..6b0f6a827 100644 --- a/Rakefile +++ b/Rakefile @@ -1,141 +1,139 @@ # Rakefile for Puppet -*- ruby -*- $: << File.expand_path(File.join(File.dirname(__FILE__), 'lib')) begin require 'rake/reductive' rescue LoadError $stderr.puts "You must have the Reductive build library in your RUBYLIB." exit(14) end TESTHOSTS = %w{rh3a fedora1 centos1 freebsd1 culain} project = Rake::RedLabProject.new("puppet") do |p| p.summary = "System Automation and Configuration Management Software" p.description = "Puppet is a declarative language for expressing system configuration, a client and server for distributing it, and a library for realizing the configuration." p.filelist = [ 'install.rb', '[A-Z]*', 'lib/puppet.rb', 'lib/puppet/**/*.rb', 'test/**/*.rb', 'bin/**/*', 'ext/**/*', 'examples/**/*', 'conf/**/*' ] p.filelist.exclude("bin/pi") p.add_dependency('facter', '1.1.0') #p.epmhosts = %w{culain} #p.sunpkghost = "sol10b" #p.rpmhost = "fedora1" end if project.has?(:gem) # Make our gem task. This actually just fills out the spec. project.mkgemtask do |task| task.require_path = 'lib' # Use these for libraries. task.bindir = "bin" # Use these for applications. task.executables = ["puppet", "puppetd", "puppetmasterd", "puppetdoc", "puppetca", "puppetrun", "ralsh"] task.default_executable = "puppet" task.autorequire = 'puppet' #### Documentation and testing. task.has_rdoc = true #s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a task.rdoc_options << '--title' << 'Puppet - Configuration Management' << '--main' << 'README' << '--line-numbers' task.test_file = "test/Rakefile" end end if project.has?(:epm) project.mkepmtask do |task| task.bins = FileList.new("bin/puppet", "bin/puppetca") task.sbins = FileList.new("bin/puppetmasterd", "bin/puppetd") task.rubylibs = FileList.new('lib/**/*') end end rule(/_is_runnable$/) do |t| available = false executable = t.name.sub(/_is_runnable$/, '') ENV['PATH'].split(':').each do |elem| available = true if File.executable? File.join(elem, executable) end unless available puts "You do not have #{executable} available in your path" exit 1 end end file "debian" => :bzr_is_runnable do system("bzr get http://www.hezmatt.org/~mpalmer/bzr/puppet.debian.svn debian") || exit(1) end task :check_build_deps => 'dpkg-checkbuilddeps_is_runnable' do system("dpkg-checkbuilddeps") || exit(1) end task :debian_packages => [ "debian", :check_build_deps, :fakeroot_is_runnable ] do system("fakeroot debian/rules clean") || exit(1) system("fakeroot debian/rules binary") || exit(1) end def dailyfile(package) "#{downdir}/#{package}/#{package}-daily-#{stamp}.tgz" end def daily(package) edir = "/tmp/daily-export" Dir.mkdir edir Dir.chdir(edir) do sh %{svn export http://reductivelabs.com/svn/#{package}/trunk #{package} >/dev/null} sh %{tar cf - #{package} | gzip -c > #{dailyfile(package)}} end FileUtils.rm_rf(edir) end def downdir ENV['DOWNLOAD_DIR'] || "/opt/rl/docroots/reductivelabs.com/htdocs/downloads" end def stamp [Time.now.year, Time.now.month, Time.now.day].collect { |i| i.to_s}.join end pdaily = dailyfile("puppet") fdaily = dailyfile("facter") file pdaily do daily("puppet") end file fdaily do daily("facter") end task :daily => [pdaily, fdaily] task :dailyclean do Dir.glob("#{downdir}/*/*daily*.tgz").each do |file| puts "Removing %s" % file File.unlink(file) end end - -# $Id$ diff --git a/lib/puppet/network/handler/configuration.rb b/lib/puppet/network/handler/configuration.rb index 7e91d74d6..a1b22207e 100644 --- a/lib/puppet/network/handler/configuration.rb +++ b/lib/puppet/network/handler/configuration.rb @@ -1,209 +1,213 @@ require 'openssl' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/sslcertificates' require 'xmlrpc/server' require 'yaml' class Puppet::Network::Handler class Configuration < Handler desc "Puppet's configuration compilation interface. Passed a node name or other key, retrieves information about the node (using the ``node_source``) and returns a compiled configuration." include Puppet::Util attr_accessor :local @interface = XMLRPC::Service::Interface.new("configuration") { |iface| iface.add_method("string configuration(string)") iface.add_method("string version()") } # Compile a node's configuration. def configuration(key, client = nil, clientip = nil) # Note that this is reasonable, because either their node source should actually # know about the node, or they should be using the ``none`` node source, which # will always return data. unless node = node_handler.details(key) raise Puppet::Error, "Could not find node '%s'" % key end # Add any external data to the node. add_node_data(node) return translate(compile(node)) end def initialize(options = {}) if options[:Local] @local = options[:Local] else @local = false end # Just store the options, rather than creating the interpreter # immediately. Mostly, this is so we can create the interpreter # on-demand, which is easier for testing. @options = options set_server_facts end # Are we running locally, or are our clients networked? def local? self.local end # Return the configuration version. def version(client = nil, clientip = nil) - v = interpreter.parsedate - # If we can find the node, then store the fact that the node - # has checked in. - if client and node = node_handler.details(client) - update_node_check(node) + if client + if node = node_handler.details(client) + update_node_check(node) + return interpreter.configuration_version(node) + else + raise Puppet::Error, "Could not find node '%s'" % client + end + else + # Just return something that will always result in a recompile, because + # this is local. + return 0 end - - return v end private # Add any extra data necessary to the node. def add_node_data(node) # Merge in our server-side facts, so they can be used during compilation. node.fact_merge(@server_facts) # Add any specified classes to the node's class list. if classes = @options[:Classes] classes.each do |klass| node.classes << klass end end end # Compile the actual configuration. def compile(node) # Pick the benchmark level. if local? level = :none else level = :notice end # Ask the interpreter to compile the configuration. config = nil benchmark(level, "Compiled configuration for %s" % node.name) do begin config = interpreter.compile(node) rescue Puppet::Error => detail if Puppet[:trace] puts detail.backtrace end Puppet.err detail raise XMLRPC::FaultException.new( 1, detail.to_s ) end end return config end # Create our interpreter object. def create_interpreter(options) args = {} # Allow specification of a code snippet or of a file if code = options[:Code] args[:Code] = code else args[:Manifest] = options[:Manifest] || Puppet[:manifest] end args[:Local] = local? if options.include?(:UseNodes) args[:UseNodes] = options[:UseNodes] elsif @local args[:UseNodes] = false end # This is only used by the cfengine module, or if --loadclasses was # specified in +puppet+. if options.include?(:Classes) args[:Classes] = options[:Classes] end return Puppet::Parser::Interpreter.new(args) end # Create/return our interpreter. def interpreter unless defined?(@interpreter) and @interpreter @interpreter = create_interpreter(@options) end @interpreter end # Create a node handler instance for looking up our nodes. def node_handler unless defined?(@node_handler) @node_handler = Puppet::Network::Handler.handler(:node).new end @node_handler end # Initialize our server fact hash; we add these to each client, and they # won't change while we're running, so it's safe to cache the values. def set_server_facts @server_facts = {} # Add our server version to the fact list @server_facts["serverversion"] = Puppet.version.to_s # And then add the server name and IP {"servername" => "fqdn", "serverip" => "ipaddress" }.each do |var, fact| if value = Facter.value(fact) @server_facts[var] = value else Puppet.warning "Could not retrieve fact %s" % fact end end if @server_facts["servername"].nil? host = Facter.value(:hostname) if domain = Facter.value(:domain) @server_facts["servername"] = [host, domain].join(".") else @server_facts["servername"] = host end end end # Translate our configuration appropriately for sending back to a client. def translate(config) if local? config else CGI.escape(config.to_yaml(:UseBlock => true)) end end # Mark that the node has checked in. FIXME this needs to be moved into # the Node class, or somewhere that's got abstract backends. def update_node_check(node) if Puppet.features.rails? and Puppet[:storeconfigs] Puppet::Rails.connect host = Puppet::Rails::Host.find_or_create_by_name(node.name) host.last_freshcheck = Time.now host.save end end end end # $Id$ diff --git a/lib/puppet/network/handler/master.rb b/lib/puppet/network/handler/master.rb index acc6c4cda..0cab94f69 100644 --- a/lib/puppet/network/handler/master.rb +++ b/lib/puppet/network/handler/master.rb @@ -1,144 +1,144 @@ require 'openssl' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/sslcertificates' require 'xmlrpc/server' require 'yaml' class Puppet::Network::Handler class MasterError < Puppet::Error; end class Master < Handler desc "Puppet's configuration interface. Used for all interactions related to generating client configurations." include Puppet::Util attr_accessor :ast attr_reader :ca @interface = XMLRPC::Service::Interface.new("puppetmaster") { |iface| iface.add_method("string getconfig(string)") iface.add_method("int freshness()") } # Tell a client whether there's a fresh config for it def freshness(client = nil, clientip = nil) config_handler.version(client, clientip) end def initialize(hash = {}) args = {} # Allow specification of a code snippet or of a file if code = hash[:Code] args[:Code] = code - else - args[:Manifest] = hash[:Manifest] || Puppet[:manifest] + elsif man = hash[:Manifest] + args[:Manifest] = man end if hash[:Local] @local = hash[:Local] else @local = false end args[:Local] = local? if hash.include?(:CA) and hash[:CA] @ca = Puppet::SSLCertificates::CA.new() else @ca = nil end Puppet.debug("Creating interpreter") if hash.include?(:UseNodes) args[:UseNodes] = hash[:UseNodes] elsif @local args[:UseNodes] = false end # This is only used by the cfengine module, or if --loadclasses was # specified in +puppet+. if hash.include?(:Classes) args[:Classes] = hash[:Classes] end @config_handler = Puppet::Network::Handler.handler(:configuration).new(args) end # Call our various handlers; this handler is getting deprecated. def getconfig(facts, format = "marshal", client = nil, clientip = nil) facts = decode_facts(facts) client, clientip = clientname(client, clientip, facts) # Pass the facts to the fact handler fact_handler.set(client, facts) # And get the configuration from the config handler return config_handler.configuration(client) end def local=(val) @local = val config_handler.local = val fact_handler.local = val end private # Manipulate the client name as appropriate. def clientname(name, ip, facts) # Always use the hostname from Facter. client = facts["hostname"] clientip = facts["ipaddress"] if Puppet[:node_name] == 'cert' if name client = name end if ip clientip = ip end end return client, clientip end def config_handler unless defined? @config_handler @config_handler = Puppet::Network::Handler.handler(:config).new :local => local? end @config_handler end # def decode_facts(facts) if @local # we don't need to do anything, since we should already # have raw objects Puppet.debug "Our client is local" else Puppet.debug "Our client is remote" begin facts = YAML.load(CGI.unescape(facts)) rescue => detail raise XMLRPC::FaultException.new( 1, "Could not rebuild facts" ) end end return facts end def fact_handler unless defined? @fact_handler @fact_handler = Puppet::Network::Handler.handler(:facts).new :local => local? end @fact_handler end end end # $Id$ diff --git a/lib/puppet/parser/configuration.rb b/lib/puppet/parser/configuration.rb index 44fb8c476..148f4dcd1 100644 --- a/lib/puppet/parser/configuration.rb +++ b/lib/puppet/parser/configuration.rb @@ -1,555 +1,548 @@ # 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/node' 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 include Puppet::Util include Puppet::Util::Errors attr_reader :topscope, :parser, :node, :facts, :collections 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) if existing = @class_scopes[name] if existing.nodescope? or scope.nodescope? raise Puppet::ParseError, "Cannot have classes, nodes, or definitions with the same name" else raise Puppet::DevError, "Somehow evaluated the same class twice" end end @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_node() evaluate_classes() evaluate_generators() fail_on_unevaluated() finish() if Puppet[:storeconfigs] store() end return extract() end # FIXME There are no tests for this. def delete_collection(coll) @collections.delete(coll) if @collections.include?(coll) end # FIXME There are no tests for this. def delete_resource(resource) @resource_table.delete(resource.ref) if @resource_table.include?(resource.ref) @resource_graph.remove_vertex!(resource) if @resource_graph.vertex?(resource) 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(classes = nil) classes ||= node.classes found = [] 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) found << name else Puppet.info "Could not find class %s for %s" % [name, node.name] tag(name) end end found end # 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 @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, options = {}) parent ||= @topscope options[:configuration] = self options[:parser] ||= self.parser scope = Puppet::Parser::Scope.new(options) @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 = @scope_graph.adjacent(scope, :direction => :in) and ary.length > 0 ary[0] else nil end end # Return any overrides for the given resource. def resource_overrides(resource) @resource_overrides[resource.ref] end # Return a list of all resources. def resources @resource_table.values end # 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 # 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.to_s.downcase] 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 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. if @resource_graph.vertex?(scope) @resource_graph.adjacent(scope, :direction => :out).each do |vertex| # Some resources don't get translated, e.g., virtual resources. if obj = vertex.to_trans bucket.push obj end end end end # Retrive the bucket for the top-level scope and set the appropriate metadata. result = buckets[topscope] - case topscope.type - when "": result.type = "main" - when nil: devfail "A Scope with no type" - else - result.type = topscope.type - end - if topscope.name - result.name = topscope.name - end + + result.copy_type_and_name(topscope) unless classlist.empty? result.classes = classlist end # Clear the cache to encourage the GC 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", :parser => self.parser) # 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 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(@node, @resource_table.values) end # Do the actual storage. def store_to_active_record(node, resources) begin # We store all of the objects, even the collectable ones benchmark(:info, "Stored configuration for #{node.name}") do Puppet::Rails::Host.transaction do Puppet::Rails::Host.store(node, resources) end end rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not store configs: %s" % detail.to_s end end # 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 # 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 fa90838f0..0398115de 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,145 +1,95 @@ require 'puppet' require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' require 'puppet/parser/configuration' require 'puppet/parser/scope' # 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 include Puppet::Util attr_accessor :usenodes - attr_reader :parser + attr_accessor :code, :file include Puppet::Util::Errors + # Determine the configuration version for a given node's environment. + def configuration_version(node) + parser(node.environment).version + end + + # evaluate our whole tree + def compile(node) + return Puppet::Parser::Configuration.new(node, parser(node.environment), :ast_nodes => usenodes?).compile + 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" + def initialize(options = {}) + if @code = options[:Code] + elsif @file = options[:Manifest] end - if hash.include?(:UseNodes) - @usenodes = hash[:UseNodes] + if options.include?(:UseNodes) + @usenodes = options[:UseNodes] else @usenodes = true end - # By default, we only search for parsed nodes. - @nodesource = :code - - @setup = false - - @local = hash[:Local] || false - # 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 + @parsers = {} end - def parsedate - parsefiles() - @parsedate - end - - # evaluate our whole tree - def compile(node) - parsefiles() - - return Puppet::Parser::Configuration.new(node, @parser, :ast_nodes => @usenodes).compile + # Should we parse ast nodes? + def usenodes? + defined?(@usenodes) and @usenodes 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. + # Create a new parser object and pre-parse the configuration. + def create_parser(environment) 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 + parser = Puppet::Parser::Parser.new(environment) + if self.code + parser.code = self.code + elsif self.file + parser.file = self.file end - - # Mark when we parsed, so we can check freshness - @parsedate = Time.now.to_i + parser.parse + return parser rescue => detail if Puppet[:trace] puts detail.backtrace end - Puppet.err "Could not parse; using old configuration: %s" % detail + Puppet.err "Could not parse for environment %s: %s" % [environment, detail] + return nil end end -end -# $Id$ + # Return the parser for a specific environment. + def parser(environment) + if ! @parsers[environment] or @parsers[environment].reparse? + if tmp = create_parser(environment) + @parsers[environment].clear if @parsers[environment] + @parsers[environment] = tmp + end + unless @parsers[environment] + raise Puppet::Error, "Could not parse any configurations" + end + end + @parsers[environment] + end +end diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 967508e56..dfc91ba12 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,460 +1,458 @@ # 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_reader :file, :version 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) + def initialize(environment) + @environment = environment 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 + @version = Time.now.to_i 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 # Add a new file to be checked when we're checking to see if we should be - # reparsed. + # reparsed. This is basically only used by the TemplateWrapper to let the + # parser know about templates that should be parsed. def watch_file(*files) files.each do |file| unless file.is_a? Puppet::Util::LoadedFile file = Puppet::Util::LoadedFile.new(file) end @files << file end end end - -# $Id$ diff --git a/lib/puppet/transportable.rb b/lib/puppet/transportable.rb index aa7eb92f7..acd69fb0c 100644 --- a/lib/puppet/transportable.rb +++ b/lib/puppet/transportable.rb @@ -1,293 +1,306 @@ require 'puppet' require 'yaml' module Puppet # The transportable objects themselves. Basically just a hash with some # metadata and a few extra methods. I used to have the object actually # be a subclass of Hash, but I could never correctly dump them using # YAML. class TransObject include Enumerable attr_accessor :type, :name, :file, :line, :collectable, :collected attr_writer :tags %w{has_key? include? length delete empty? << [] []=}.each { |method| define_method(method) do |*args| @params.send(method, *args) end } def each @params.each { |p,v| yield p, v } end def initialize(name,type) @type = type @name = name @collectable = false @params = {} @tags = [] end def longname return [@type,@name].join('--') end def tags return @tags end def to_hash @params.dup end def to_s return "%s(%s) => %s" % [@type,@name,super] end def to_manifest "#{self.type.to_s} { \'#{self.name}\':\n%s\n}" % @params.collect { |p, v| if v.is_a? Array " #{p} => [\'#{v.join("','")}\']" else " #{p} => \'#{v}\'" end }.join(",\n") end def to_yaml_properties instance_variables end def to_type(parent = nil) retobj = nil if typeklass = Puppet::Type.type(self.type) # FIXME This should really be done differently, but... if retobj = typeklass[self.name] self.each do |param, val| retobj[param] = val end else unless retobj = typeklass.create(self) return nil end end else raise Puppet::Error.new("Could not find object type %s" % self.type) end if parent parent.push retobj end return retobj end end # Just a linear container for objects. Behaves mostly like an array, except # that YAML will correctly dump them even with their instance variables. class TransBucket include Enumerable attr_accessor :name, :type, :file, :line, :classes, :keyword, :top %w{delete shift include? length empty? << []}.each { |method| define_method(method) do |*args| #Puppet.warning "Calling %s with %s" % [method, args.inspect] @children.send(method, *args) #Puppet.warning @params.inspect end } + # Copy a scope's type and name. + def copy_type_and_name(scope) + case scope.type + when "": self.type = "main" + when nil: devfail "A Scope with no type" + else + self.type = scope.type + end + if scope.name + self.name = scope.name + end + end + # Remove all collectable objects from our tree, since the client # should not see them. def collectstrip! @children.dup.each do |child| if child.is_a? self.class child.collectstrip! else if child.collectable and ! child.collected @children.delete(child) end end end end # Recursively yield everything. def delve(&block) @children.each do |obj| block.call(obj) if obj.is_a? self.class obj.delve(&block) else obj end end end def each @children.each { |c| yield c } end # Turn our heirarchy into a flat list def flatten @children.collect do |obj| if obj.is_a? Puppet::TransBucket obj.flatten else obj end end.flatten end def initialize(children = []) @children = children end def push(*args) args.each { |arg| case arg when Puppet::TransBucket, Puppet::TransObject # nada else raise Puppet::DevError, "TransBuckets cannot handle objects of type %s" % arg.class end } @children += args end # Convert to a parseable manifest def to_manifest unless self.top unless defined? @keyword and @keyword raise Puppet::DevError, "No keyword; cannot convert to manifest" end end str = nil if self.top str = "%s" else str = "#{@keyword} #{@type} {\n%s\n}" end str % @children.collect { |child| child.to_manifest }.collect { |str| if self.top str else str.gsub(/^/, " ") # indent everything once end }.join("\n\n") # and throw in a blank line end def to_yaml_properties instance_variables end def to_type(parent = nil) # this container will contain the equivalent of all objects at # this level #container = Puppet::Component.new(:name => @name, :type => @type) #unless defined? @name # raise Puppet::DevError, "TransBuckets must have names" #end unless defined? @type Puppet.debug "TransBucket '%s' has no type" % @name end usetrans = true if usetrans tmpname = nil # Nodes have the same name and type if self.name tmpname = "%s[%s]" % [@type, self.name] else tmpname = @type end trans = TransObject.new(tmpname, :component) if defined? @parameters @parameters.each { |param,value| Puppet.debug "Defining %s on %s of type %s" % [param,@name,@type] trans[param] = value } else #Puppet.debug "%s[%s] has no parameters" % [@type, @name] end container = Puppet::Type::Component.create(trans) else hash = { :name => self.name, :type => @type } if defined? @parameters @parameters.each { |param,value| Puppet.debug "Defining %s on %s of type %s" % [param,@name,@type] hash[param] = value } else #Puppet.debug "%s[%s] has no parameters" % [@type, @name] end #if parent # hash[:parent] = parent #end container = Puppet::Type::Component.create(hash) end #Puppet.info container.inspect if parent parent.push container end # unless we successfully created the container, return an error unless container Puppet.warning "Got no container back" return nil end self.each { |child| # the fact that we descend here means that we are # always going to execute depth-first # which is _probably_ a good thing, but one never knows... unless child.is_a?(Puppet::TransBucket) or child.is_a?(Puppet::TransObject) raise Puppet::DevError, "TransBucket#to_type cannot handle objects of type %s" % child.class end # Now just call to_type on them with the container as a parent begin child.to_type(container) rescue => detail if Puppet[:trace] and ! detail.is_a?(Puppet::Error) puts detail.backtrace end Puppet.err detail.to_s end } # at this point, no objects at are level are still Transportable # objects return container end def param(param,value) unless defined? @parameters @parameters = {} end @parameters[param] = value end end #------------------------------------------------------------ end # $Id$ diff --git a/spec/spec.opts b/spec/spec.opts new file mode 100644 index 000000000..2cac5f260 --- /dev/null +++ b/spec/spec.opts @@ -0,0 +1,5 @@ +--colour +--format +s +--loadby +mtime \ No newline at end of file diff --git a/spec/unit/parser/interpreter.rb b/spec/unit/parser/interpreter.rb index 0e32b8c5b..7328e2651 100755 --- a/spec/unit/parser/interpreter.rb +++ b/spec/unit/parser/interpreter.rb @@ -1,94 +1,170 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' +describe Puppet::Parser::Interpreter, " when initializing" do + it "should default to neither code nor file" do + interp = Puppet::Parser::Interpreter.new + interp.code.should be_nil + interp.file.should be_nil + end + + it "should set the code to parse" do + interp = Puppet::Parser::Interpreter.new :Code => :code + interp.code.should equal(:code) + end + + it "should set the file to parse" do + interp = Puppet::Parser::Interpreter.new :Manifest => :file + interp.file.should equal(:file) + end + + it "should set code and ignore manifest when both are present" do + interp = Puppet::Parser::Interpreter.new :Code => :code, :Manifest => :file + interp.code.should equal(:code) + interp.file.should be_nil + end + + it "should default to usenodes" do + interp = Puppet::Parser::Interpreter.new + interp.usenodes?.should be_true + end + + it "should allow disabling of usenodes" do + interp = Puppet::Parser::Interpreter.new :UseNodes => false + interp.usenodes?.should be_false + end +end + describe Puppet::Parser::Interpreter, " when creating parser instances" do before do @interp = Puppet::Parser::Interpreter.new @parser = mock('parser') end it "should create a parser with code passed in at initialization time" do @interp.code = :some_code @parser.expects(:code=).with(:some_code) @parser.expects(:parse) Puppet::Parser::Parser.expects(:new).with(:environment).returns(@parser) @interp.send(:create_parser, :environment).object_id.should equal(@parser.object_id) end it "should create a parser with a file passed in at initialization time" do @interp.file = :a_file @parser.expects(:file=).with(:a_file) @parser.expects(:parse) Puppet::Parser::Parser.expects(:new).with(:environment).returns(@parser) @interp.send(:create_parser, :environment).should equal(@parser) end it "should create a parser when passed neither code nor file" do @parser.expects(:parse) Puppet::Parser::Parser.expects(:new).with(:environment).returns(@parser) @interp.send(:create_parser, :environment).should equal(@parser) end it "should return nothing when new parsers fail" do Puppet::Parser::Parser.expects(:new).with(:environment).raises(ArgumentError) @interp.send(:create_parser, :environment).should be_nil end end describe Puppet::Parser::Interpreter, " when managing parser instances" do before do @interp = Puppet::Parser::Interpreter.new @parser = mock('parser') end it "it should an exception when nothing is there and nil is returned" do @interp.expects(:create_parser).with(:environment).returns(nil) lambda { @interp.send(:parser, :environment) }.should raise_error(Puppet::Error) end it "should create and return a new parser and use the same parser when the parser does not need reparsing" do @interp.expects(:create_parser).with(:environment).returns(@parser) @interp.send(:parser, :environment).should equal(@parser) @parser.expects(:reparse?).returns(false) @interp.send(:parser, :environment).should equal(@parser) end it "should create a new parser when reparse is true" do oldparser = mock('oldparser') newparser = mock('newparser') oldparser.expects(:reparse?).returns(true) oldparser.expects(:clear) @interp.expects(:create_parser).with(:environment).returns(oldparser) @interp.send(:parser, :environment).should equal(oldparser) @interp.expects(:create_parser).with(:environment).returns(newparser) @interp.send(:parser, :environment).should equal(newparser) end it "should keep the old parser if create_parser doesn't return anything." do # Get the first parser in the hash. @interp.expects(:create_parser).with(:environment).returns(@parser) @interp.send(:parser, :environment).should equal(@parser) # Have it indicate something has changed @parser.expects(:reparse?).returns(true) # But fail to create a new parser @interp.expects(:create_parser).with(:environment).returns(nil) # And make sure we still get the old valid parser @interp.send(:parser, :environment).should equal(@parser) end it "should use different parsers for different environments" do # get one for the first env @interp.expects(:create_parser).with(:first_env).returns(@parser) @interp.send(:parser, :first_env).should equal(@parser) other_parser = mock('otherparser') @interp.expects(:create_parser).with(:second_env).returns(other_parser) @interp.send(:parser, :second_env).should equal(other_parser) end end + +describe Puppet::Parser::Interpreter, " when compiling configurations" do + before do + @interp = Puppet::Parser::Interpreter.new + end + + it "should create a configuration with the node, parser, and whether to use ast nodes" do + node = mock('node') + node.expects(:environment).returns(:myenv) + compile = mock 'compile' + compile.expects(:compile).returns(:config) + parser = mock 'parser' + @interp.expects(:parser).with(:myenv).returns(parser) + @interp.expects(:usenodes?).returns(true) + Puppet::Parser::Configuration.expects(:new).with(node, parser, :ast_nodes => true).returns(compile) + @interp.compile(node) + + # Now try it when usenodes is true + @interp = Puppet::Parser::Interpreter.new :UseNodes => false + node.expects(:environment).returns(:myenv) + compile.expects(:compile).returns(:config) + @interp.expects(:parser).with(:myenv).returns(parser) + @interp.expects(:usenodes?).returns(false) + Puppet::Parser::Configuration.expects(:new).with(node, parser, :ast_nodes => false).returns(compile) + @interp.compile(node).should equal(:config) + end +end + +describe Puppet::Parser::Interpreter, " when returning configuration version" do + before do + @interp = Puppet::Parser::Interpreter.new + end + + it "should ask the appropriate parser for the configuration version" do + node = mock 'node' + node.expects(:environment).returns(:myenv) + parser = mock 'parser' + parser.expects(:version).returns(:myvers) + @interp.expects(:parser).with(:myenv).returns(parser) + @interp.configuration_version(node).should equal(:myvers) + end +end diff --git a/test/language/configuration.rb b/test/language/configuration.rb index a17b5a7ae..409d4ca1d 100755 --- a/test/language/configuration.rb +++ b/test/language/configuration.rb @@ -1,745 +1,755 @@ #!/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 = Puppet::Node def mknode(name = "foo") @node = SimpleNode.new(name) end def mkparser # This should mock an interpreter @parser = mock 'parser' end def mkconfig(options = {}) if node = options[:node] options.delete(:node) else node = mknode end @config = Config.new(node, mkparser, options) end def test_initialize config = nil assert_nothing_raised("Could not init config with all required options") do config = Config.new("foo", "parser") end assert_equal("foo", config.node, "Did not set node correctly") assert_equal("parser", config.parser, "Did not set parser correctly") # 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(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 = 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("@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(config.topscope) 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 options another = nil assert_nothing_raised("Could not create subscope") do another = config.newscope(subscope, :name => "testing") end assert_equal("testing", another.name, "did not set scope option correctly") 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_node, :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} result = nil assert_nothing_raised("could not call evaluate_classes") do result = config.send(:evaluate_classes) end assert_equal(%w{one three}, result, "Did not return the list of evaluated classes") 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) + node = mock 'node' + resource_table = mock 'resources' + resource_table.expects(:values).returns(:resources) + config.instance_variable_set("@node", node) + config.instance_variable_set("@resource_table", resource_table) + config.expects(:store_to_active_record).with(node, :resources) + config.send(:store) end def test_store_to_active_record config = mkconfig - args = {:name => "yay"} + node = mock 'node' + node.expects(:name).returns("myname") Puppet::Rails::Host.stubs(:transaction).yields - Puppet::Rails::Host.expects(:store).with(args) - config.send(:store_to_active_record, args) + Puppet::Rails::Host.expects(:store).with(node, :resources) + config.send(:store_to_active_record, node, :resources) 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) } + bucket = [] scope = mock("scope") - scope.expects(:to_trans).returns([]) + bucket.expects(:copy_type_and_name).with(scope) + scope.expects(:to_trans).returns(bucket) 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")) + topbucket = fakebucket.new "top" + topbucket.expects(:copy_type_and_name).with(top) + top.stubs(:copy_type_and_name) + top.expects(:to_trans).returns(topbucket) # 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 # #620 - Nodes and classes should conflict, else classes don't get evaluated def test_nodes_and_classes_name_conflict # Test node then class config = mkconfig node = stub :nodescope? => true klass = stub :nodescope? => false config.class_set("one", node) assert_raise(Puppet::ParseError, "Did not fail when replacing node with class") do config.class_set("one", klass) end # and class then node config = mkconfig node = stub :nodescope? => true klass = stub :nodescope? => false config.class_set("two", klass) assert_raise(Puppet::ParseError, "Did not fail when replacing node with class") do config.class_set("two", node) end end end diff --git a/test/language/functions.rb b/test/language/functions.rb index 42d8d7585..9314df179 100755 --- a/test/language/functions.rb +++ b/test/language/functions.rb @@ -1,533 +1,536 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/parser/parser' require 'puppet/network/client' require 'puppettest' require 'puppettest/resourcetesting' class TestLangFunctions < Test::Unit::TestCase include PuppetTest::ParserTesting include PuppetTest::ResourceTesting def test_functions assert_raise(Puppet::ParseError) do Puppet::Parser::AST::Function.new( :name => "fakefunction", :arguments => AST::ASTArray.new( :children => [nameobj("avalue")] ) ) end assert_nothing_raised do Puppet::Parser::Functions.newfunction(:fakefunction, :type => :rvalue) do |input| return "output %s" % input[0] end end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "fakefunction", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [nameobj("avalue")] ) ) end scope = mkscope val = nil assert_nothing_raised do val = func.evaluate(:scope => scope) end assert_equal("output avalue", val) end def test_taggedfunction scope = mkscope tag = "yayness" scope.tag(tag) {"yayness" => true, "booness" => false}.each do |tag, retval| func = taggedobj(tag, :rvalue) val = nil assert_nothing_raised do val = func.evaluate(:scope => scope) end assert_equal(retval, val, "'tagged' returned %s for %s" % [val, tag]) end end def test_failfunction func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "fail", :ftype => :statement, :arguments => AST::ASTArray.new( :children => [stringobj("this is a failure"), stringobj("and another")] ) ) end scope = mkscope val = nil assert_raise(Puppet::ParseError) do val = func.evaluate(:scope => scope) end end def test_multipletemplates Dir.mkdir(Puppet[:templatedir]) onep = File.join(Puppet[:templatedir], "one") twop = File.join(Puppet[:templatedir], "two") File.open(onep, "w") do |f| f.puts "template <%= one %>" end File.open(twop, "w") do |f| f.puts "template <%= two %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj("one"), stringobj("two")] ) ) end ast = varobj("output", func) scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("one", "One") assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("two", "Two") assert_nothing_raised do ast.evaluate(:scope => scope) end assert_equal("template One\ntemplate Two\n", scope.lookupvar("output"), "Templates were not handled correctly") end # Now make sure we can fully qualify files, and specify just one def test_singletemplates template = tempfile() File.open(template, "w") do |f| f.puts "template <%= yayness %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("yayness", "this is yayness") assert_nothing_raised do ast.evaluate(:scope => scope) end assert_equal("template this is yayness\n", scope.lookupvar("output"), "Templates were not handled correctly") end def test_tempatefunction_cannot_see_scopes template = tempfile() File.open(template, "w") do |f| f.puts "<%= lookupvar('myvar') %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) scope = mkscope scope.setvar("myvar", "this is yayness") assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end end def test_template_reparses template = tempfile() File.open(template, "w") do |f| f.puts "original text" end manifest = tempfile() file = tempfile() File.open(manifest, "w") do |f| f.puts %{file { "#{file}": content => template("#{template}") }} end interp = Puppet::Parser::Interpreter.new( :Manifest => manifest, :UseNodes => false ) node = mknode + node.stubs(:environment).returns("yay") - parsedate = interp.parsedate() + Puppet[:environment] = "yay" + + version = interp.configuration_version(node) objects = nil assert_nothing_raised { objects = interp.compile(node) } fileobj = objects[0] assert_equal("original text\n", fileobj["content"], "Template did not work") Puppet[:filetimeout] = -5 # Have to sleep because one second is the fs's time granularity. sleep(1) # Now modify the template File.open(template, "w") do |f| f.puts "new text" end assert_nothing_raised { objects = interp.compile(node) } - newdate = interp.parsedate() + newversion = interp.configuration_version(node) - assert(parsedate != newdate, "Parse date did not change") + assert(version != newversion, "Parse date did not change") end def test_template_defined_vars template = tempfile() File.open(template, "w") do |f| f.puts "template <%= yayness %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) { "" => "", false => "false", }.each do |string, value| scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("yayness", string) assert_equal(string, scope.lookupvar("yayness", false)) assert_nothing_raised("An empty string was not a valid variable value") do ast.evaluate(:scope => scope) end assert_equal("template #{value}\n", scope.lookupvar("output"), "%s did not get evaluated correctly" % string.inspect) end end def test_autoloading_functions assert_equal(false, Puppet::Parser::Functions.function(:autofunc), "Got told autofunc already exists") dir = tempfile() $: << dir newpath = File.join(dir, "puppet", "parser", "functions") FileUtils.mkdir_p(newpath) File.open(File.join(newpath, "autofunc.rb"), "w") { |f| f.puts %{ Puppet::Parser::Functions.newfunction(:autofunc, :type => :rvalue) do |vals| Puppet.wanring vals.inspect end } } obj = nil assert_nothing_raised { obj = Puppet::Parser::Functions.function(:autofunc) } assert(obj, "Did not autoload function") assert(Puppet::Parser::Scope.method_defined?(:function_autofunc), "Did not set function correctly") end def test_realize scope = mkscope parser = scope.configuration.parser # Make a definition parser.newdefine("mytype") [%w{file /tmp/virtual}, %w{mytype yay}].each do |type, title| # Make a virtual resource virtual = mkresource(:type => type, :title => title, :virtual => true, :params => {}) scope.setresource virtual ref = Puppet::Parser::Resource::Reference.new( :type => type, :title => title, :scope => scope ) # Now call the realize function assert_nothing_raised do scope.function_realize(ref) end # Make sure it created a collection assert_equal(1, scope.configuration.collections.length, "Did not set collection") assert_nothing_raised do scope.configuration.collections.each do |coll| coll.evaluate end end scope.configuration.collections.clear # Now make sure the virtual resource is no longer virtual assert(! virtual.virtual?, "Did not make virtual resource real") end # Make sure we puke on any resource that doesn't exist none = Puppet::Parser::Resource::Reference.new( :type => "file", :title => "/tmp/nosuchfile", :scope => scope ) # The function works assert_nothing_raised do scope.function_realize(none.to_s) end # Make sure it created a collection assert_equal(1, scope.configuration.collections.length, "Did not set collection") # And the collection has our resource in it assert_equal([none.to_s], scope.configuration.collections[0].resources, "Did not set resources in collection") end def test_defined scope = mkscope parser = scope.configuration.parser parser.newclass("yayness") parser.newdefine("rahness") assert_nothing_raised do assert(scope.function_defined("yayness"), "yayness class was not considered defined") assert(scope.function_defined("rahness"), "rahness definition was not considered defined") assert(scope.function_defined("service"), "service type was not considered defined") assert(! scope.function_defined("fakness"), "fakeness was considered defined") end # Now make sure any match in a list will work assert(scope.function_defined(["booness", "yayness", "fakeness"]), "A single answer was not sufficient to return true") # and make sure multiple falses are still false assert(! scope.function_defined(%w{no otherno stillno}), "Multiple falses were somehow true") # Now make sure we can test resources scope.setresource mkresource(:type => "file", :title => "/tmp/rahness", :scope => scope, :source => scope.source, :params => {:owner => "root"}) yep = Puppet::Parser::Resource::Reference.new(:type => "file", :title => "/tmp/rahness") nope = Puppet::Parser::Resource::Reference.new(:type => "file", :title => "/tmp/fooness") assert(scope.function_defined([yep]), "valid resource was not considered defined") assert(! scope.function_defined([nope]), "invalid resource was considered defined") end def test_search parser = mkparser scope = mkscope(:parser => parser) fun = parser.newdefine("yay::ness") foo = parser.newdefine("foo::bar") search = Puppet::Parser::Functions.function(:search) assert_nothing_raised do scope.function_search(["foo", "yay"]) end ffun = ffoo = nil assert_nothing_raised("Search path change did not work") do ffun = scope.finddefine("ness") ffoo = scope.finddefine('bar') end assert(ffun, "Could not find definition in 'fun' namespace") assert(ffoo, "Could not find definition in 'foo' namespace") end def test_include scope = mkscope parser = scope.configuration.parser assert_raise(Puppet::ParseError, "did not throw error on missing class") do scope.function_include("nosuchclass") end parser.newclass("myclass") assert_nothing_raised do scope.function_include "myclass" end assert(scope.configuration.classlist.include?("myclass"), "class was not evaluated") # Now try multiple classes at once classes = %w{one two three}.each { |c| parser.newclass(c) } assert_nothing_raised do scope.function_include classes end classes.each do |c| assert(scope.configuration.classlist.include?(c), "class %s was not evaluated" % c) end # Now try a scoped class parser.newclass("os::redhat") assert_nothing_raised("Could not include qualified class name") do scope.function_include("os::redhat") end end def test_file parser = mkparser scope = mkscope(:parser => parser) file1 = tempfile file2 = tempfile file3 = tempfile File.open(file2, "w") { |f| f.puts "yaytest" } val = nil assert_nothing_raised("Failed to call file with one arg") do val = scope.function_file([file2]) end assert_equal("yaytest\n", val, "file() failed") assert_nothing_raised("Failed to call file with two args") do val = scope.function_file([file1, file2]) end assert_equal("yaytest\n", val, "file() failed") assert_raise(Puppet::ParseError, "did not fail when files are missing") do val = scope.function_file([file1, file3]) end end def test_generate command = tempfile sh = %x{which sh} File.open(command, "w") do |f| f.puts %{#!#{sh} if [ -n "$1" ]; then echo "yay-$1" else echo yay fi } end File.chmod(0755, command) assert_equal("yay\n", %x{#{command}}, "command did not work") assert_equal("yay-foo\n", %x{#{command} foo}, "command did not work") scope = mkscope parser = scope.configuration.parser val = nil assert_nothing_raised("Could not call generator with no args") do val = scope.function_generate([command]) end assert_equal("yay\n", val, "generator returned wrong results") assert_nothing_raised("Could not call generator with args") do val = scope.function_generate([command, "foo"]) end assert_equal("yay-foo\n", val, "generator returned wrong results") assert_raise(Puppet::ParseError, "Did not fail with an unqualified path") do val = scope.function_generate([File.basename(command), "foo"]) end assert_raise(Puppet::ParseError, "Did not fail when command failed") do val = scope.function_generate([%x{which touch}.chomp, "/this/dir/does/not/exist"]) end fake = File.join(File.dirname(command), "..") dir = File.dirname(command) dirname = File.basename(dir) bad = File.join(dir, "..", dirname, File.basename(command)) assert_raise(Puppet::ParseError, "Did not fail when command failed") do val = scope.function_generate([bad]) end end end # $Id$ diff --git a/test/language/interpreter.rb b/test/language/interpreter.rb deleted file mode 100755 index 1adcb7bde..000000000 --- a/test/language/interpreter.rb +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env ruby - -$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ - -require 'facter' - -require 'puppet' -require 'puppet/parser/interpreter' -require 'puppet/parser/parser' -require 'puppet/network/client' -require 'puppettest' -require 'puppettest/resourcetesting' -require 'puppettest/parsertesting' -require 'puppettest/servertest' -require 'timeout' - -class TestInterpreter < PuppetTest::TestCase - include PuppetTest - include PuppetTest::ServerTest - include PuppetTest::ParserTesting - include PuppetTest::ResourceTesting - AST = Puppet::Parser::AST - - # create a simple manifest that uses nodes to create a file - def mknodemanifest(node, file) - createdfile = tempfile() - - File.open(file, "w") { |f| - f.puts "node %s { file { \"%s\": ensure => file, mode => 755 } }\n" % - [node, createdfile] - } - - return [file, createdfile] - end - - def test_reloadfiles - node = mknode(Facter["hostname"].value) - - file = tempfile() - - # Create a first version - createdfile = mknodemanifest(node.name, file) - - interp = nil - assert_nothing_raised { - interp = Puppet::Parser::Interpreter.new(:Manifest => file) - } - - config = nil - assert_nothing_raised { - config = interp.compile(node) - } - Puppet[:filetimeout] = -5 - - # Now create a new file - createdfile = mknodemanifest(node.name, file) - - newconfig = nil - assert_nothing_raised { - newconfig = interp.compile(node) - } - - assert(config != newconfig, "Configs are somehow the same") - end - - def test_parsedate - Puppet[:filetimeout] = 0 - main = tempfile() - sub = tempfile() - mainfile = tempfile() - subfile = tempfile() - count = 0 - updatemain = proc do - count += 1 - File.open(main, "w") { |f| - f.puts "import '#{sub}' - file { \"#{mainfile}\": content => #{count} } - " - } - end - updatesub = proc do - count += 1 - File.open(sub, "w") { |f| - f.puts "file { \"#{subfile}\": content => #{count} } - " - } - end - - updatemain.call - updatesub.call - - interp = Puppet::Parser::Interpreter.new( - :Manifest => main, - :Local => true - ) - - date = interp.parsedate - - # Now update the site file and make sure we catch it - sleep 1 - updatemain.call - newdate = interp.parsedate - assert(date != newdate, "Parsedate was not updated") - date = newdate - - # And then the subfile - sleep 1 - updatesub.call - newdate = interp.parsedate - assert(date != newdate, "Parsedate was not updated") - end - - # Make sure our whole chain works. - def test_compile - interp = mkinterp - interp.expects(:parsefiles) - parser = interp.instance_variable_get("@parser") - - node = mock('node') - config = mock('config') - config.expects(:compile).returns(:config) - Puppet::Parser::Configuration.expects(:new).with(node, parser, :ast_nodes => interp.usenodes).returns(config) - assert_equal(:config, interp.compile(node), "Did not return the results of config.compile") - end - - # Make sure that reparsing is atomic -- failures don't cause a broken state, and we aren't subject - # to race conditions if someone contacts us while we're reparsing. - def test_atomic_reparsing - Puppet[:filetimeout] = -10 - file = tempfile - File.open(file, "w") { |f| f.puts %{file { '/tmp': ensure => directory }} } - interp = mkinterp :Manifest => file, :UseNodes => false - - assert_nothing_raised("Could not compile the first time") do - interp.compile(mknode("yay")) - end - - oldparser = interp.send(:instance_variable_get, "@parser") - - # Now add a syntax failure - File.open(file, "w") { |f| f.puts %{file { /tmp: ensure => directory }} } - assert_nothing_raised("Could not compile the first time") do - interp.compile(mknode("yay")) - end - - # And make sure the old parser is still there - newparser = interp.send(:instance_variable_get, "@parser") - assert_equal(oldparser.object_id, newparser.object_id, "Failed parser still replaced existing parser") - end -end - -# $Id$ diff --git a/test/lib/puppettest/parsertesting.rb b/test/lib/puppettest/parsertesting.rb index 3e2930728..326c25756 100644 --- a/test/lib/puppettest/parsertesting.rb +++ b/test/lib/puppettest/parsertesting.rb @@ -1,407 +1,408 @@ require 'puppettest' require 'puppet/rails' module PuppetTest::ParserTesting include PuppetTest AST = Puppet::Parser::AST Config = Puppet::Parser::Configuration # A fake class that we can use for testing evaluation. class FakeAST attr_writer :evaluate def evaluated? defined? @evaluated and @evaluated end def evaluate(*args) @evaluated = true return @evaluate end def initialize(val = nil) if val @evaluate = val end end def reset @evaluated = nil end def safeevaluate(*args) evaluate() end end def astarray(*args) AST::ASTArray.new( :children => args ) end def mkconfig(parser = nil) require 'puppet/network/handler/node' parser ||= mkparser node = mknode return Config.new(node, parser) end def mknode(name = nil) + require 'puppet/node' name ||= "nodename" Puppet::Network::Handler.handler(:node) - Puppet::Network::Handler::Node::SimpleNode.new(name) + Puppet::Node.new(name) end def mkinterp(args = {}) args[:Code] ||= "" unless args.include?(:Manifest) args[:Local] ||= true Puppet::Parser::Interpreter.new(args) end def mkparser Puppet::Parser::Parser.new() end def mkscope(hash = {}) hash[:parser] ||= mkparser config ||= mkconfig(hash[:parser]) config.topscope.source = (hash[:parser].findclass("", "") || hash[:parser].newclass("")) unless config.topscope.source raise "Could not find source for scope" end config.topscope end def classobj(name, hash = {}) hash[:file] ||= __FILE__ hash[:line] ||= __LINE__ hash[:type] ||= name AST::HostClass.new(hash) end def tagobj(*names) args = {} newnames = names.collect do |name| if name.is_a? AST name else nameobj(name) end end args[:type] = astarray(*newnames) assert_nothing_raised("Could not create tag %s" % names.inspect) { return AST::Tag.new(args) } end def resourcedef(type, title, params) unless title.is_a?(AST) title = stringobj(title) end assert_nothing_raised("Could not create %s %s" % [type, title]) { return AST::ResourceDef.new( :file => __FILE__, :line => __LINE__, :title => title, :type => type, :params => resourceinst(params) ) } end def virt_resourcedef(*args) res = resourcedef(*args) res.virtual = true res end def resourceoverride(type, title, params) assert_nothing_raised("Could not create %s %s" % [type, name]) { return AST::ResourceOverride.new( :file => __FILE__, :line => __LINE__, :object => resourceref(type, title), :type => type, :params => resourceinst(params) ) } end def resourceref(type, title) assert_nothing_raised("Could not create %s %s" % [type, title]) { return AST::ResourceRef.new( :file => __FILE__, :line => __LINE__, :type => type, :title => stringobj(title) ) } end def fileobj(path, hash = {"owner" => "root"}) assert_nothing_raised("Could not create file %s" % path) { return resourcedef("file", path, hash) } end def nameobj(name) assert_nothing_raised("Could not create name %s" % name) { return AST::Name.new( :file => tempfile(), :line => rand(100), :value => name ) } end def typeobj(name) assert_nothing_raised("Could not create type %s" % name) { return AST::Type.new( :file => tempfile(), :line => rand(100), :value => name ) } end def nodedef(name) assert_nothing_raised("Could not create node %s" % name) { return AST::NodeDef.new( :file => tempfile(), :line => rand(100), :names => nameobj(name), :code => AST::ASTArray.new( :children => [ varobj("%svar" % name, "%svalue" % name), fileobj("/%s" % name) ] ) ) } end def resourceinst(hash) assert_nothing_raised("Could not create resource instance") { params = hash.collect { |param, value| resourceparam(param, value) } return AST::ResourceInst.new( :file => tempfile(), :line => rand(100), :children => params ) } end def resourceparam(param, value) # Allow them to pass non-strings in if value.is_a?(String) value = stringobj(value) end assert_nothing_raised("Could not create param %s" % param) { return AST::ResourceParam.new( :file => tempfile(), :line => rand(100), :param => param, :value => value ) } end def stringobj(value) AST::String.new( :file => tempfile(), :line => rand(100), :value => value ) end def varobj(name, value) unless value.is_a? AST value = stringobj(value) end assert_nothing_raised("Could not create %s code" % name) { return AST::VarDef.new( :file => tempfile(), :line => rand(100), :name => nameobj(name), :value => value ) } end def varref(name) assert_nothing_raised("Could not create %s variable" % name) { return AST::Variable.new( :file => __FILE__, :line => __LINE__, :value => name ) } end def argobj(name, value) assert_nothing_raised("Could not create %s compargument" % name) { return AST::CompArgument.new( :children => [nameobj(name), stringobj(value)] ) } end def defaultobj(type, params) pary = [] params.each { |p,v| pary << AST::ResourceParam.new( :file => __FILE__, :line => __LINE__, :param => p, :value => stringobj(v) ) } past = AST::ASTArray.new( :file => __FILE__, :line => __LINE__, :children => pary ) assert_nothing_raised("Could not create defaults for %s" % type) { return AST::ResourceDefaults.new( :file => __FILE__, :line => __LINE__, :type => type, :params => past ) } end def taggedobj(name, ftype = :statement) functionobj("tagged", name, ftype) end def functionobj(function, name, ftype = :statement) func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => function, :ftype => ftype, :arguments => AST::ASTArray.new( :children => [nameobj(name)] ) ) end return func end # This assumes no nodes def assert_creates(manifest, *files) interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => manifest, :UseNodes => false ) } config = nil assert_nothing_raised { config = interp.compile(mknode) } comp = nil assert_nothing_raised { comp = config.to_type } assert_apply(comp) files.each do |file| assert(FileTest.exists?(file), "Did not create %s" % file) end end def mk_transobject(file = "/etc/passwd") obj = nil assert_nothing_raised { obj = Puppet::TransObject.new("file", file) obj["owner"] = "root" obj["mode"] = "644" } return obj end def mk_transbucket(*resources) bucket = nil assert_nothing_raised { bucket = Puppet::TransBucket.new bucket.name = "yayname" bucket.type = "yaytype" } resources.each { |o| bucket << o } return bucket end # Make a tree of resources, yielding if desired def mk_transtree(depth = 4, width = 2) top = nil assert_nothing_raised { top = Puppet::TransBucket.new top.name = "top" top.type = "bucket" } bucket = top file = tempfile() depth.times do |i| resources = [] width.times do |j| path = tempfile + i.to_s obj = Puppet::TransObject.new("file", path) obj["owner"] = "root" obj["mode"] = "644" # Yield, if they want if block_given? yield(obj, i, j) end resources << obj end newbucket = mk_transbucket(*resources) bucket.push newbucket bucket = newbucket end return top end # Take a list of AST resources, evaluate them, and return the results def assert_evaluate(children) top = nil assert_nothing_raised("Could not create top object") { top = AST::ASTArray.new( :children => children ) } trans = nil scope = nil assert_nothing_raised { scope = Puppet::Parser::Scope.new() trans = scope.evaluate(:ast => top) } return trans end end # $Id$