summaryrefslogtreecommitdiffstats
path: root/lib/puppet/parser/compile.rb
diff options
context:
space:
mode:
authorLuke Kanies <luke@madstop.com>2007-08-25 15:54:06 -0500
committerLuke Kanies <luke@madstop.com>2007-08-25 15:54:06 -0500
commit37f0eed9657810c1b9e1d6383e6d6ebb027aff2f (patch)
tree4a99038864044be35e38cd52bba495883cc36344 /lib/puppet/parser/compile.rb
parentdeb0107ed945c25776d10f9e9b9d721b69ec3478 (diff)
downloadpuppet-37f0eed9657810c1b9e1d6383e6d6ebb027aff2f.tar.gz
puppet-37f0eed9657810c1b9e1d6383e6d6ebb027aff2f.tar.xz
puppet-37f0eed9657810c1b9e1d6383e6d6ebb027aff2f.zip
Renaming the "configuration" object to "compile", because it is only a transitional object and I want the real "configuration" object to be the thing that I pass from the server to the client; it will be a subclass of GRATR::Digraph.
Diffstat (limited to 'lib/puppet/parser/compile.rb')
-rw-r--r--lib/puppet/parser/compile.rb558
1 files changed, 558 insertions, 0 deletions
diff --git a/lib/puppet/parser/compile.rb b/lib/puppet/parser/compile.rb
new file mode 100644
index 000000000..6cfe66fce
--- /dev/null
+++ b/lib/puppet/parser/compile.rb
@@ -0,0 +1,558 @@
+# 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::Compile
+ 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
+
+ # Return the node's environment.
+ def environment
+ unless defined? @environment
+ if node.environment and node.environment != ""
+ @environment = node.environment
+ end
+ end
+ @environment
+ 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, "Compile 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[:compile] = 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
+ # compile 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]
+
+ 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(:compile => 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