diff options
Diffstat (limited to 'lib/puppet/parser/scope.rb')
-rw-r--r-- | lib/puppet/parser/scope.rb | 1544 |
1 files changed, 499 insertions, 1045 deletions
diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index a26ec938d..9b59ebd64 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -2,1191 +2,645 @@ # such. require 'puppet/parser/parser' +require 'puppet/parser/templatewrapper' require 'puppet/transportable' -module Puppet::Parser - class Scope - class ScopeObj < Hash - attr_accessor :file, :line, :type, :name - end - - # A simple wrapper for templates, so they don't have full access to - # the scope objects. - class TemplateWrapper - attr_accessor :scope, :file - include Puppet::Util - Puppet::Util.logmethods(self) - - def initialize(scope, file) - @scope = scope - if file =~ /^#{File::SEPARATOR}/ - @file = file - else - @file = File.join(Puppet[:templatedir], file) - end +class Puppet::Parser::Scope + require 'puppet/parser/resource' - unless FileTest.exists?(@file) - raise Puppet::ParseError, - "Could not find template %s" % file - end + AST = Puppet::Parser::AST - # We'll only ever not have an interpreter in testing, but, eh. - if @scope.interp - @scope.interp.newfile(@file) - end - end + # This doesn't actually work right now. + Puppet.config.setdefaults(:puppet, + :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], + :templatedir => ["$vardir/templates", + "Where Puppet looks for template files." + ] + ) - # Ruby treats variables like methods, so we can cheat here and - # trap missing vars like they were missing methods. - def method_missing(name, *args) - # We have to tell lookupvar to return :undefined to us when - # appropriate; otherwise it converts to "". - value = @scope.lookupvar(name.to_s, false) - if value != :undefined - return value - else - # Just throw an error immediately, instead of searching for - # other missingmethod things or whatever. - raise Puppet::ParseError, - "Could not find value for '%s'" % name - end - end - - def result - result = nil - benchmark(:debug, "Interpolated template #{@file}") do - template = ERB.new(File.read(@file), 0, "-") - result = template.result(binding) - end - - result - end - - def to_s - "template[%s]" % @file - end - end + Puppet::Util.logmethods(self) - # This doesn't actually work right now. - Puppet.config.setdefaults(:puppet, - :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], - :templatedir => ["$vardir/templates", - "Where Puppet looks for template files." - ] - ) + include Enumerable + include Puppet::Util::Errors + attr_accessor :parent, :level, :interp, :source, :host + attr_accessor :name, :type, :topscope, :base, :keyword, :namespace + attr_accessor :top, :context, :translated, :exported - Puppet::Util.logmethods(self) + # Whether we behave declaratively. Note that it's a class variable, + # so all scopes behave the same. + @@declarative = true - include Enumerable - attr_accessor :parent, :level, :interp - attr_accessor :name, :type, :topscope, :base, :keyword + # Retrieve and set the declarative setting. + def self.declarative + return @@declarative + end - attr_accessor :top, :context, :translated, :collectable + def self.declarative=(val) + @@declarative = val + end - # This is probably not all that good of an idea, but... - # This way a parent can share its tables with all of its children. - attr_writer :nodetable, :classtable, :definedtable, :exportable + # This handles the shared tables that all scopes have. They're effectively + # global tables, except that they're only global for a single scope tree, + # which is why I can't use class variables for them. + def self.sharedtable(*names) + attr_accessor(*names) + @@sharedtables ||= [] + @@sharedtables += names + end - # Whether we behave declaratively. Note that it's a class variable, - # so all scopes behave the same. - @@declarative = true + # This is probably not all that good of an idea, but... + # This way a parent can share its tables with all of its children. + sharedtable :classtable, :definedtable, :exportable, :overridetable, :collecttable - # Retrieve and set the declarative setting. - def self.declarative - return @@declarative + # Is the value true? This allows us to control the definition of truth + # in one place. + def self.true?(value) + if value == false or value == "" + return false + else + return true end + end - def self.declarative=(val) - @@declarative = val + # Is the type a builtin type? + def builtintype?(type) + if typeklass = Puppet::Type.type(type) + return typeklass + else + return false end + end - # Is the value true? This allows us to control the definition of truth - # in one place. - def self.true?(value) - if value == false or value == "" - return false - else - return true - end + # Create a new child scope. + def child=(scope) + @children.push(scope) + + # Copy all of the shared tables over to the child. + @@sharedtables.each do |name| + scope.send(name.to_s + "=", self.send(name)) end + end - # Add all of the defaults for a given object to that object. - def adddefaults(obj) - defaults = lookupdefaults(obj.type) + # Verify that the given object isn't defined elsewhere. + def chkobjectclosure(obj) + if @definedtable.include?(obj.ref) + typeklass = Puppet::Type.type(obj.type) + if typeklass and ! typeklass.isomorphic? + Puppet.info "Allowing duplicate %s" % type + else + exobj = @definedtable[obj.ref] - defaults.each do |var, value| - unless obj[var] - self.debug "Adding default %s for %s" % - [var, obj.type] + # Either it's a defined type, which are never + # isomorphic, or it's a non-isomorphic type. + msg = "Duplicate definition: %s is already defined" % obj.ref - obj[var] = value + if exobj.file and exobj.line + msg << " in file %s at line %s" % + [exobj.file, exobj.line] end - end - end - # Add a single object's tags to the global list of tags for - # that object. - def addtags(obj) - unless defined? @tagtable - raise Puppet::DevError, "Told to add tags, but no tag table" - end - list = @tagtable[obj.type][obj.name] - - obj.tags.each { |tag| - unless list.include?(tag) - if tag.nil? or tag == "" - Puppet.debug "Got tag %s from %s(%s)" % - [tag.inspect, obj.type, obj.name] - else - list << tag - end + if obj.line or obj.file + msg << "; cannot redefine" end - } - end - # Is the type a builtin type? - def builtintype?(type) - if typeklass = Puppet::Type.type(type) - return typeklass - else - return false + raise Puppet::ParseError.new(msg) end end - # Verify that the given object isn't defined elsewhere. - def chkobjectclosure(hash) - type = hash[:type] - name = hash[:name] - unless name - return true - end - if @definedtable[type].include?(name) - typeklass = Puppet::Type.type(type) - if typeklass and ! typeklass.isomorphic? - Puppet.info "Allowing duplicate %s" % type - else - exobj = @definedtable[type][name] - - # Either it's a defined type, which are never - # isomorphic, or it's a non-isomorphic type. - msg = "Duplicate definition: %s[%s] is already defined" % - [type, name] - - if exobj.file and exobj.line - msg << " in file %s at line %s" % - [exobj.file, exobj.line] - end - - if hash[:line] or hash[:file] - msg << "; cannot redefine" - end - - error = Puppet::ParseError.new(msg) - raise error - end - end - - return true - end - - def declarative=(val) - self.class.declarative = val - end - - def declarative - self.class.declarative - end - - # Log the existing tags. At some point this should be in a better - # place, but eh. - def logtags - @tagtable.sort { |a, b| - a[0] <=> b[0] - }.each { |type, names| - names.sort { |a, b| - a[0] <=> b[0] - }.each { |name, tags| - Puppet.info "%s(%s): '%s'" % [type, name, tags.join("' '")] - } - } - end + return true + end - # Create a new child scope. - def child=(scope) - @children.push(scope) + # Return the list of collections. + def collections + @collecttable + end - if defined? @nodetable - scope.nodetable = @nodetable - else - raise Puppet::DevError, "No nodetable has been defined" - end + def declarative=(val) + self.class.declarative = val + end - if defined? @classtable - scope.classtable = @classtable - else - raise Puppet::DevError, "No classtable has been defined" - end + def declarative + self.class.declarative + end - if defined? @exportable - scope.exportable = @exportable - else - raise Puppet::DevError, "No exportable has been defined" - end + # Test whether a given scope is declarative. Even though it's + # a global value, the calling objects don't need to know that. + def declarative? + @@declarative + end - if defined? @definedtable - scope.definedtable = @definedtable - else - raise Puppet::DevError, "No definedtable has been defined" - end - end + # Remove a specific child. + def delete(child) + @children.delete(child) + end - # Test whether a given scope is declarative. Even though it's - # a global value, the calling objects don't need to know that. - def declarative? - @@declarative + # Remove a resource from the various tables. This is only used when + # a resource maps to a definition and gets evaluated. + def deleteresource(resource) + if @definedtable[resource.ref] + @definedtable.delete(resource.ref) end - # Remove a specific child. - def delete(child) - @children.delete(child) + if @children.include?(resource) + @children.delete(resource) end + end - # Are we the top scope? - def topscope? - @level == 1 - end + # Are we the top scope? + def topscope? + @level == 1 + end - # Return a list of all of the defined classes. - def classlist - unless defined? @classtable - raise Puppet::DevError, "Scope did not receive class table" - end - return @classtable.collect { |id, klass| - # The class table can contain scopes or strings as its values - # so support them accordingly. - if klass.is_a? Scope - klass.type - else - klass - end - } + # Return a list of all of the defined classes. + def classlist + unless defined? @classtable + raise Puppet::DevError, "Scope did not receive class table" end + return @classtable.keys.reject { |k| k == "" } + end - # Yield each child scope in turn - def each - @children.each { |child| - yield child - } - end + # Yield each child scope in turn + def each + @children.each { |child| + yield child + } + end - # Evaluate a list of classes. - def evalclasses(classes) - return unless classes - classes.each do |klass| - if code = lookuptype(klass) - # Just reuse the 'include' function, since that's the equivalent - # of what we're doing here. - function_include(klass) - end + # Evaluate a list of classes. + def evalclasses(*classes) + retval = [] + classes.each do |klass| + if obj = findclass(klass) + obj.safeevaluate :scope => self + retval << klass end end + end - # Evaluate a specific node's code. This method will normally be called - # on the top-level scope, but it actually evaluates the node at the - # appropriate scope. - def evalnode(hash) - objects = hash[:ast] - names = hash[:names] or - raise Puppet::DevError, "Node names must be provided to evalnode" - facts = hash[:facts] - classes = hash[:classes] - parent = hash[:parent] - - # Always add "default" to our name list, so we're always searching - # for a default node. - names << "default" - - scope = code = nil - # Find a node that matches one of our names - names.each { |node| - if nodehash = @nodetable[node] - code = nodehash[:node] - scope = nodehash[:scope] - - if node == "default" - Puppet.info "Using default node" - end - break - end - } - - # And fail if we don't find one. - unless scope and code - raise Puppet::Error, "Could not find configuration for %s" % - names.join(" or ") - end + def exported? + self.exported + end - # We need to do a little skullduggery here. We want a - # temporary scope, because we don't want this scope to - # show up permanently in the scope tree -- otherwise we could - # not evaluate the node multiple times. We could conceivably - # cache the results, but it's not worth it at this stage. + def findclass(name) + interp.findclass(namespace, name) + end - # Note that we evaluate the node code with its containing - # scope, not with the top scope. We also retrieve the created - # scope so that we can get any classes set within it - nodescope = code.safeevaluate(:scope => scope, :facts => facts) + def finddefine(name) + interp.finddefine(namespace, name) + end - scope.evalclasses(classes) + def findresource(string, name = nil) + if name + string = "%s[%s]" % [string, name] end - # The top-level evaluate, used to evaluate a whole AST tree. This is - # a strange method, in that it turns around and calls evaluate() on its - # :ast argument. - def evaluate(hash) - objects = hash[:ast] - facts = hash[:facts] || {} - - @@done = [] - - unless objects - raise Puppet::DevError, "Evaluation requires an AST tree" - end - - # Set all of our facts in the top-level scope. - facts.each { |var, value| - self.setvar(var, value) - } - - # Evaluate all of our configuration. This does not evaluate any - # node definitions. - result = objects.safeevaluate(:scope => self) - - # If they've provided a name or a parent, we assume they're looking - # for nodes. - if hash[:searched] - # Specifying a parent node takes precedence, because it is assumed - # that this node was found in a remote repository like ldap. - gennode(hash) - elsif hash.include? :names # else, look for it in the config - evalnode(hash) - else - # Else we're not using nodes at all, so just evaluate any passed-in - # classes. - classes = hash[:classes] || [] - evalclasses(classes) - - # These classes would be passed in manually, via something like - # a cfengine module - end - - bucket = self.to_trans - - # Add our class list - unless self.classlist.empty? - bucket.classes = self.classlist - end - - # Now clean up after ourselves - [@@done].each do |table| - table.clear - end + @definedtable[string] + end - return bucket + # Recursively complete the whole tree, in preparation for + # translation or storage. + def finish + self.each do |obj| + obj.finish end + end - # Return the hash of objects that we specifically exported. We return - # a hash to make it easy for the caller to deduplicate based on name. - def exported(type) - if @exportable.include?(type) - return @exportable[type].dup + # Initialize our new scope. Defaults to having no parent and to + # being declarative. + def initialize(hash = {}) + @parent = nil + @type = nil + @name = nil + @finished = false + hash.each { |name, val| + method = name.to_s + "=" + if self.respond_to? method + self.send(method, val) else - return {} - end - end - - # Store our object in the central export table. - def exportobject(obj) - if @exportable.include?(obj.type) and - @exportable[obj.type].include?(obj.name) - raise Puppet::ParseError, "Object %s[%s] is already exported" % - [obj.type, obj.name] + raise Puppet::DevError, "Invalid scope argument %s" % name end + } - debug "Exporting %s[%s]" % [obj.type, obj.name] + @tags = [] - @exportable[obj.type][obj.name] = obj + if @parent.nil? + unless hash.include?(:declarative) + hash[:declarative] = true + end + self.istop(hash[:declarative]) + @inside = nil + else + # This is here, rather than in newchild(), so that all + # of the later variable initialization works. + @parent.child = self - return obj + @level = @parent.level + 1 + @interp = @parent.interp + @source = hash[:source] || @parent.source + @topscope = @parent.topscope + @context = @parent.context + @inside = @parent.inside + @host = @parent.host + @type ||= @parent.type end - # Pull in all of the appropriate classes and evaluate them. It'd - # be nice if this didn't know quite so much about how AST::Node - # operated internally. This is used when a list of classes is passed in, - # instead of a node definition, such as from the cfengine module. - def gennode(hash) - names = hash[:names] or - raise Puppet::DevError, "Node names must be provided to gennode" - facts = hash[:facts] - classes = hash[:classes] - parent = hash[:parentnode] - name = names.shift - arghash = { - :type => name, - :code => AST::ASTArray.new(:pin => "[]") - } + # Our child scopes and objects + @children = [] - #Puppet.notice "hash is %s" % - # hash.inspect - #Puppet.notice "Classes are %s, parent is %s" % - # [classes.inspect, parent.inspect] + # The symbol table for this scope + @symtable = Hash.new(nil) - if parent - arghash[:parentclass] = parent - end + # All of the defaults set for types. It's a hash of hashes, + # with the first key being the type, then the second key being + # the parameter. + @defaultstable = Hash.new { |dhash,type| + dhash[type] = Hash.new(nil) + } - # Create the node - node = AST::Node.new(arghash) - node.keyword = "node" + # Map the names to the tables. + @map = { + "variable" => @symtable, + "defaults" => @defaultstable + } - # Now evaluate it, which evaluates the parent and nothing else - # but does return the nodescope. - scope = node.safeevaluate(:scope => self) - - # Finally evaluate our list of classes in this new scope. - scope.evalclasses(classes) + unless @interp + raise Puppet::DevError, "Scopes require an interpreter" end + end - # Initialize our new scope. Defaults to having no parent and to - # being declarative. - def initialize(hash = {}) - @parent = nil - @type = nil - @name = nil - @finished = false - hash.each { |name, val| - method = name.to_s + "=" - if self.respond_to? method - self.send(method, val) - else - raise Puppet::DevError, "Invalid scope argument %s" % name - end - } - - @tags = [] - - if @parent.nil? - unless hash.include?(:declarative) - hash[:declarative] = true - end - self.istop(hash[:declarative]) - @inside = nil - else - # This is here, rather than in newchild(), so that all - # of the later variable initialization works. - @parent.child = self - - @level = @parent.level + 1 - @interp = @parent.interp - @topscope = @parent.topscope - @context = @parent.context - @inside = @parent.inside - end - - # Our child scopes and objects - @children = [] - - # The symbol table for this scope - @symtable = Hash.new(nil) + # Associate the object directly with the scope, so that contained objects + # can look up what container they're running within. + def inside(arg = nil) + return @inside unless arg + + old = @inside + @inside = arg + yield + ensure + #Puppet.warning "exiting %s" % @inside.name + @inside = old + end - # The type table for this scope - @typetable = Hash.new(nil) + # Mark that we're the top scope, and set some hard-coded info. + def istop(declarative = true) + # the level is mostly used for debugging + @level = 1 - # All of the defaults set for types. It's a hash of hashes, - # with the first key being the type, then the second key being - # the parameter. - @defaultstable = Hash.new { |dhash,type| - dhash[type] = Hash.new(nil) - } + # The table for storing class singletons. This will only actually + # be used by top scopes and node scopes. + @classtable = Hash.new(nil) - # The object table is similar, but it is actually a hash of hashes - # where the innermost objects are TransObject instances. - @objectable = Hash.new { |typehash,typekey| - # See #newobject for how to create the actual objects - typehash[typekey] = Hash.new(nil) - } + self.class.declarative = declarative - # This is just for collecting statements locally, so we can - # verify that there is no overlap within this specific scope - @localobjectable = Hash.new { |typehash,typekey| - typehash[typekey] = Hash.new(nil) - } + # The table for all defined objects. + @definedtable = {} - # Map the names to the tables. - @map = { - "variable" => @symtable, - "type" => @typetable, - "node" => @nodetable, - "object" => @objectable, - "defaults" => @defaultstable - } - end + # The list of objects that will available for export. + @exportable = {} - # Associate the object directly with the scope, so that contained objects - # can look up what container they're running within. - def inside(arg = nil) - return @inside unless arg - - old = @inside - @inside = arg - yield - ensure - #Puppet.warning "exiting %s" % @inside.name - @inside = old + # The list of overrides. This is used to cache overrides on objects + # that don't exist yet. We store an array of each override. + @overridetable = Hash.new do |overs, ref| + overs[ref] = [] end - # Mark that we're the top scope, and set some hard-coded info. - def istop(declarative = true) - # the level is mostly used for debugging - @level = 1 - - # The table for storing class singletons. This will only actually - # be used by top scopes and node scopes. - @classtable = Hash.new(nil) + # Eventually, if we support sites, this will allow definitions + # of nodes with the same name in different sites. For now + # the top-level scope is always the only site scope. + @sitescope = true - self.class.declarative = declarative + @namespace = "" - # The table for all defined objects. - @definedtable = Hash.new { |types, type| - types[type] = {} - } - - # A table for storing nodes. - @nodetable = Hash.new(nil) + # The list of collections that have been created. This is a global list, + # but they each refer back to the scope that created them. + @collecttable = [] - # The list of objects that will available for export. - @exportable = Hash.new { |types, type| - types[type] = {} - } + @context = nil + @topscope = self + @type = "puppet" + @name = "top" + end - # Eventually, if we support sites, this will allow definitions - # of nodes with the same name in different sites. For now - # the top-level scope is always the only site scope. - @sitescope = true - - # And create a tag table, so we can collect all of the tags - # associated with any objects created in this scope tree - @tagtable = Hash.new { |types, type| - types[type] = Hash.new { |names, name| - names[name] = [] - } + # Collect all of the defaults set at any higher scopes. + # This is a different type of lookup because it's additive -- + # it collects all of the defaults, with defaults in closer scopes + # overriding those in later scopes. + def lookupdefaults(type) + values = {} + + # first collect the values from the parents + unless @parent.nil? + @parent.lookupdefaults(type).each { |var,value| + values[var] = value } - - @context = nil - @topscope = self - @type = "puppet" - @name = "top" - end - - # Look up a given class. This enables us to make sure classes are - # singletons - def lookupclass(klassid) - unless defined? @classtable - raise Puppet::DevError, "Scope did not receive class table" - end - return @classtable[klassid] end - # Collect all of the defaults set at any higher scopes. - # This is a different type of lookup because it's additive -- - # it collects all of the defaults, with defaults in closer scopes - # overriding those in later scopes. - def lookupdefaults(type) - values = {} - - # first collect the values from the parents - unless @parent.nil? - @parent.lookupdefaults(type).each { |var,value| - values[var] = value - } - end - - # then override them with any current values - # this should probably be done differently - if @defaultstable.include?(type) - @defaultstable[type].each { |var,value| - values[var] = value - } - end - #Puppet.debug "Got defaults for %s: %s" % - # [type,values.inspect] - return values - end - - # Look up all of the exported objects of a given type. Just like - # lookupobject, this only searches up through parent classes, not - # the whole scope tree. - def lookupexported(type) - found = [] - sub = proc { |table| - # We always return nil so that it will search all the way - # up the scope tree. - if table.has_key?(type) - table[type].each do |name, obj| - found << obj - end - nil - else - info table.keys.inspect - nil - end + # then override them with any current values + # this should probably be done differently + if @defaultstable.include?(type) + @defaultstable[type].each { |var,value| + values[var] = value } - - value = lookup("object",sub, false) - - return found end - # Look up a node by name - def lookupnode(name) - #Puppet.debug "Looking up type %s" % name - value = lookup("type",name) - if value == :undefined - return nil - else - #Puppet.debug "Found node %s" % name - return value - end - end + #Puppet.debug "Got defaults for %s: %s" % + # [type,values.inspect] + return values + end - # Look up a defined type. - def lookuptype(name) - #Puppet.debug "Looking up type %s" % name - value = lookup("type",name) - if value == :undefined - return nil - else - #Puppet.debug "Found type %s" % name - return value - end + # Look up all of the exported objects of a given type. Just like + # lookupobject, this only searches up through parent classes, not + # the whole scope tree. + def lookupexported(type) + @definedtable.find_all do |name, r| + r.type == type and r.exported? end + end - # Look up an object by name and type. This should only look up objects - # within a class structure, not within the entire scope structure. - def lookupobject(hash) - type = hash[:type] - name = hash[:name] - #Puppet.debug "Looking up object %s of type %s in level %s" % - # [name, type, @level] - sub = proc { |table| - if table.include?(type) - if table[type].include?(name) - table[type][name] - end - else - nil - end - } - value = lookup("object",sub, true) - if value == :undefined - return nil - else - return value - end - end + def lookupoverrides(obj) + @overridetable[obj.ref] + end - # Look up a variable. The simplest value search we do. Default to returning - # an empty string for missing values, but support returning a constant. - def lookupvar(name, usestring = true) - value = lookup("variable", name) - if value == :undefined - if usestring - return "" - else - return :undefined - end + # Look up a defined type. + def lookuptype(name) + finddefine(name) || findclass(name) + end + + # Look up a variable. The simplest value search we do. Default to returning + # an empty string for missing values, but support returning a constant. + def lookupvar(name, usestring = true) + value = lookup("variable", name) + if value == :undefined + if usestring + return "" else - return value + return :undefined end + else + return value end + end - def newcollection(coll) - @children << coll - end - - # Add a new object to our object table. - def newobject(hash) - if @objectable[hash[:type]].include?(hash[:name]) - raise Puppet::DevError, "Object %s[%s] is already defined" % - [hash[:type], hash[:name]] - end - - self.chkobjectclosure(hash) - - obj = nil - - obj = Puppet::TransObject.new(hash[:name], hash[:type]) - - @children << obj + # Add a collection to the global list. + def newcollection(coll) + @collecttable << coll + end - @objectable[hash[:type]][hash[:name]] = obj + # Create a new scope. + def newscope(hash = {}) + hash[:parent] = self + #debug "Creating new scope, level %s" % [self.level + 1] + return Puppet::Parser::Scope.new(hash) + end - @definedtable[hash[:type]][hash[:name]] = obj + # Return the list of remaining overrides. + def overrides + @overridetable.collect { |name, overs| overs }.flatten + end - return obj - end + def resources + @definedtable.values + end - # Create a new scope. - def newscope(hash = {}) - hash[:parent] = self - #debug "Creating new scope, level %s" % [self.level + 1] - return Puppet::Parser::Scope.new(hash) + def setclass?(obj) + if obj.respond_to?(:fqname) + @classtable.has_key?(obj.fqname) + else + @classtable[obj] end + end - # Retrieve a specific node. This is used in ast.rb to find a - # parent node and in findnode to retrieve and evaluate a node. - def node(name) - @nodetable[name] + # Store the fact that we've evaluated a given class. We use a hash + # that gets inherited from the top scope down, rather than a global + # hash. We store the object ID, not class name, so that we + # can support multiple unrelated classes with the same name. + def setclass(obj) + if obj.is_a?(AST::HostClass) + unless obj.fqname + raise Puppet::DevError, "Got a %s with no fully qualified name" % + obj.class + end + @classtable[obj.fqname] = obj + else + raise Puppet::DevError, "Invalid class %s" % obj.inspect end + end - # Store the fact that we've evaluated a given class. We use a hash - # that gets inherited from the top scope down, rather than a global - # hash. We store the object ID, not class name, so that we - # can support multiple unrelated classes with the same name. - def setclass(id, name) - unless name =~ /^[a-z][\w-]*$/ - raise Puppet::ParseError, "Invalid class name '%s'" % name - end + # Set all of our facts in the top-level scope. + def setfacts(facts) + facts.each { |var, value| + self.setvar(var, value) + } + end - @classtable[id] = name - end + # Add a new object to our object table and the global list, and do any necessary + # checks. + def setresource(obj) + self.chkobjectclosure(obj) - # Store the scope for each class, so that other subclasses can look - # them up. - def setscope(id, scope) - @classtable[id] = scope - end + @children << obj - # Set defaults for a type. The typename should already be downcased, - # so that the syntax is isolated. - def setdefaults(type,params) - table = @defaultstable[type] + # The global table + @definedtable[obj.ref] = obj - # if we got a single param, it'll be in its own array - unless params[0].is_a?(Array) - params = [params] - end + return obj + end - params.each { |ary| - #Puppet.debug "Default for %s is %s => %s" % - # [type,ary[0].inspect,ary[1].inspect] - if @@declarative - if table.include?(ary[0]) - error = Puppet::ParseError.new( - "Default already defined for %s { %s }" % - [type,ary[0]] - ) - raise error - end - else - if table.include?(ary[0]) - # we should maybe allow this warning to be turned off... - Puppet.warning "Replacing default for %s { %s }" % - [type,ary[0]] - end - end - table[ary[0]] = ary[1] - } + # Override a parameter in an existing object. If the object does not yet + # exist, then cache the override in a global table, so it can be flushed + # at the end. + def setoverride(resource) + resource.override = true + if obj = @definedtable[resource.ref] + obj.merge(resource) + else + @overridetable[resource.ref] << resource end + end - # Store a host in the site node table. - def setnode(name,code) - unless defined? @nodetable - raise Puppet::DevError, "No node table defined" - end - if @nodetable.include?(name) - raise Puppet::ParseError, "Host %s is already defined" % name + # Set defaults for a type. The typename should already be downcased, + # so that the syntax is isolated. We don't do any kind of type-checking + # here; instead we let the resource do it when the defaults are used. + def setdefaults(type, params) + table = @defaultstable[type] + + # if we got a single param, it'll be in its own array + params = [params] unless params.is_a?(Array) + + params.each { |param| + #Puppet.debug "Default for %s is %s => %s" % + # [type,ary[0].inspect,ary[1].inspect] + if @@declarative + if table.include?(param.name) + self.fail "Default already defined for %s { %s }" % + [type,param.name] + end else - #Puppet.warning "Setting node %s at level %s" % [name, @level] - - # We have to store both the scope that's setting the node and - # the node itself, so that the node gets evaluated in the correct - # scope. - code.scope = self - @nodetable[name] = { - :scope => self, - :node => code - } + if table.include?(param.name) + # we should maybe allow this warning to be turned off... + Puppet.warning "Replacing default for %s { %s }" % + [type,param.name] + end end - end + table[param.name] = param + } + end - # Define our type. - def settype(name,ltype) - unless name - raise Puppet::DevError, "Got told to set type with a nil type" - end - unless ltype - raise Puppet::DevError, "Got told to set type with a nil object" - end - # Don't let them redefine the class in this scope. - if @typetable.include?(name) - raise Puppet::ParseError, - "%s is already defined" % name - else - ltype.scope = self - @typetable[name] = ltype + # Set a variable in the current scope. This will override settings + # in scopes above, but will not allow variables in the current scope + # to be reassigned if we're declarative (which is the default). + def setvar(name,value) + #Puppet.debug "Setting %s to '%s' at level %s" % + # [name.inspect,value,self.level] + if @@declarative and @symtable.include?(name) + raise Puppet::ParseError, "Cannot reassign variable %s" % name + else + if @symtable.include?(name) + Puppet.warning "Reassigning %s to %s" % [name,value] end + @symtable[name] = value end + end - # This method will fail if the named object is already defined anywhere - # in the scope tree, which is what provides some minimal closure-like - # behaviour. - def setobject(hash) - # FIXME This objectlookup stuff should be looking up using both - # the name and the namevar. - - # First see if we can look the object up using normal scope - # rules, i.e., one of our parent classes has defined the - # object or something - - name = hash[:name] - type = hash[:type] - params = hash[:arguments] - file = hash[:file] - line = hash[:line] - - collectable = hash[:collectable] || self.collectable - - # Verify that we're not overriding any already-set parameters. - if localobj = @localobjectable[type][name] - params.each { |var, value| - if localobj.include?(var) - msg = "Cannot reassign attribute %s on %s[%s]" % - [var, type, name] - - error = Puppet::ParseError.new(msg) - error.line = line - error.file = file - raise error - end - } + # Return an interpolated string. + def strinterp(string) + newstring = string.gsub(/\\\$|\$\{(\w+)\}|\$(\w+)/) do |value| + # If it matches the backslash, then just retun the dollar sign. + if value == '\\$' + '$' + else # look the variable up + var = $1 || $2 + lookupvar($1 || $2) end + end - # First look for it in a parent scope - obj = lookupobject(:name => name, :type => type) - - if obj - unless collectable == obj.collectable - msg = nil - if collectable - msg = "Exported %s[%s] cannot override local objects" - [type, name] - else - msg = "Local %s[%s] cannot override exported objects" - [type, name] - end - - error = Puppet::ParseError.new(msg) - error.line = line - error.file = file - raise error - end - end + return newstring.gsub(/\\t/, "\t").gsub(/\\n/, "\n").gsub(/\\s/, "\s") + end - unless obj and obj != :undefined - unless obj = @objectable[type][name] - obj = self.newobject( - :type => type, - :name => name, - :line => line, - :file => file - ) - - obj.collectable = collectable - - # only set these if we've created the object, - # which is the most common case - # FIXME we eventually need to store the file - # and line with each param, not the object - # itself. - obj.file = file - obj.line = line - end + # Add a tag to our current list. These tags will be added to all + # of the objects contained in this scope. + def tag(*ary) + ary.each { |tag| + unless tag =~ /^\w[-\w]+$/ + fail Puppet::ParseError, "Invalid tag %s" % tag end - - # Now add our parameters. This has the function of overriding - # existing values, which might have been defined in a higher - # scope. - params.each { |var,value| - # Add it to our found object - obj[var] = value - } - - # This is only used for override verification -- the local object - # table does not have transobjects or whatever in it, it just has - # simple hashes. This is necessary because setobject can modify - # our object table or a parent class's object table, and we - # still need to make sure param settings cannot be duplicated - # within our scope. - @localobjectable[type][name] ||= {} - params.each { |var,value| - # And add it to the local table; mmm, hack - @localobjectable[type][name][var] = value - } - - return obj - end - - # Set a variable in the current scope. This will override settings - # in scopes above, but will not allow variables in the current scope - # to be reassigned if we're declarative (which is the default). - def setvar(name,value) - #Puppet.debug "Setting %s to '%s' at level %s" % - # [name.inspect,value,self.level] - if @@declarative and @symtable.include?(name) - raise Puppet::ParseError, "Cannot reassign variable %s" % name - else - if @symtable.include?(name) - Puppet.warning "Reassigning %s to %s" % [name,value] - end - @symtable[name] = value + if tag.nil? or tag == "" + puts caller + Puppet.debug "got told to tag with %s" % tag.inspect + next end - end - - # Return an interpolated string. - def strinterp(string) - newstring = string.gsub(/\\\$|\$\{(\w+)\}|\$(\w+)/) do |value| - # If it matches the backslash, then just retun the dollar sign. - if value == '\\$' - '$' - else # look the variable up - var = $1 || $2 - lookupvar($1 || $2) - end + tag = tag.to_s + unless @tags.include?(tag) + #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag] + @tags << tag end + } + end - return newstring.gsub(/\\t/, "\t").gsub(/\\n/, "\n").gsub(/\\s/, "\s") - end - - # Add a tag to our current list. These tags will be added to all - # of the objects contained in this scope. - def tag(*ary) - ary.each { |tag| + # Return the tags associated with this scope. It's basically + # just our parents' tags, plus our type. We don't cache this value + # because our parent tags might change between calls. + def tags + tmp = [] + @tags + unless ! defined? @type or @type.nil? or @type == "" + tmp << @type.to_s + end + if @parent + #info "Looking for tags in %s" % @parent.type + @parent.tags.each { |tag| if tag.nil? or tag == "" - Puppet.debug "got told to tag with %s" % tag.inspect + Puppet.debug "parent returned tag %s" % tag.inspect next end - unless @tags.include?(tag) - #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag] - @tags << tag.to_s + unless tmp.include?(tag) + tmp << tag end } end + return tmp.sort.uniq + end - # Return the tags associated with this scope. It's basically - # just our parents' tags, plus our type. - def tags - tmp = [] + @tags - unless ! defined? @type or @type.nil? or @type == "" - tmp << @type.to_s - end - if @parent - #info "Looking for tags in %s" % @parent.type - @parent.tags.each { |tag| - if tag.nil? or tag == "" - Puppet.debug "parent returned tag %s" % tag.inspect - next - end - unless tmp.include?(tag) - tmp << tag - end - } - end - return tmp + # Used mainly for logging + def to_s + if self.name + return "%s[%s]" % [@type, @name] + else + return self.type.to_s end + end - # Used mainly for logging - def to_s - if @name - return "%s[%s]" % [@type, @name] + # Convert all of our objects as necessary. + def translate + ret = @children.collect do |child| + case child + when self.class + child.translate + when Puppet::Parser::Resource + child.to_trans else - return @type.to_s + devfail "Got %s for translation" % child.class end + end.reject { |o| o.nil? } + bucket = Puppet::TransBucket.new ret + unless self.type + devfail "A Scope with no type" end + if @type == "" + bucket.type = "main" + else + bucket.type = @type + end + if self.name + bucket.name = self.name + end + return bucket + end - # Convert our scope to a TransBucket. Everything in our @localobjecttable - # gets converted to either an evaluated definition, or a TransObject - def to_trans - results = [] - - # Set this on entry, just in case someone tries to get all weird - @translated = true - - @children.dup.each do |child| - if @@done.include?(child) - raise Puppet::DevError, "Already translated %s" % - child.object_id - else - @@done << child - end - #warning "Working on %s of type %s with id %s" % - # [child.type, child.class, child.object_id] - - # If it's a scope, then it can only be a subclass's scope, so - # convert it to a transbucket and store it in our results list - result = nil - case child - when Scope - result = child.to_trans - when Puppet::TransObject - # These objects can map to defined types or builtin types. - # Builtin types should be passed out as they are, but defined - # types need to be evaluated. We have to wait until this - # point so that subclass overrides can happen. - - # Wait until the last minute to set tags, although this - # probably should not matter - child.tags = self.tags - - # Add any defaults. - self.adddefaults(child) - - # Then make sure this child's tags are stored in the - # central table. This should maybe be in the evaluate - # methods, but, eh. - @topscope.addtags(child) - - # Now that all that is done, check to see what kind of object - # it is. - if objecttype = lookuptype(child.type) - # It's a defined type, so evaluate it. Retain whether - # the object is collectable. If the object is collectable, - # then it will store all of its contents into the - # @exportable table, rather than returning them. - result = objecttype.safeevaluate( - :name => child.name, - :type => child.type, - :arguments => child.to_hash, - :scope => self, - :collectable => child.collectable - ) - else - # If it's collectable, then store it. It will be - # stripped out in the interpreter using the collectstrip - # method. If we don't do this, then these objects - # don't get stored in the DB. - if child.collectable - exportobject(child) - end - result = child - end - # This is pretty hackish, but the collection has to actually - # be performed after all of the classes and definitions are - # evaluated, otherwise we won't catch objects that are exported - # in them. I think this will still be pretty limited in some - # cases, especially those where you are both exporting and - # collecting, but it's the best I can do for now. - when Puppet::Parser::AST::Collection - child.perform(self).each do |obj| - results << obj - end - else - raise Puppet::DevError, - "Puppet::Parse::Scope cannot handle objects of type %s" % - child.class - end - - # Skip nil objects or empty transbuckets - if result - unless result.is_a? Puppet::TransBucket and result.empty? - results << result - end - end - end - - # Get rid of any nil objects. - results.reject! { |child| - child.nil? - } + # Undefine a variable; only used for testing. + def unsetvar(var) + if @symtable.include?(var) + @symtable.delete(var) + end + end - # If we have a name and type, then make a TransBucket, which - # becomes a component. - # Else, just stack all of the objects into the current bucket. - if @type - bucket = Puppet::TransBucket.new + # Return an array of all of the unevaluated objects + def unevaluated + ary = @definedtable.find_all do |name, object| + ! object.builtin? and ! object.evaluated? + end.collect { |name, object| object } - if defined? @name and @name - bucket.name = @name - end - # it'd be nice not to have to do this... - results.each { |result| - #Puppet.warning "Result type is %s" % result.class - bucket.push(result) - } - bucket.type = @type - - if defined? @keyword - bucket.keyword = @keyword - end - #Puppet.debug( - # "TransBucket with name %s and type %s in scope %s" % - # [@name,@type,self.object_id] - #) - - # now find metaparams - @symtable.each { |var,value| - if Puppet::Type.metaparam?(var.intern) - #Puppet.debug("Adding metaparam %s" % var) - bucket.param(var,value) - else - #Puppet.debug("%s is not a metaparam" % var) - end - } - #Puppet.debug "Returning bucket %s from scope %s" % - # [bucket.name,self.object_id] - return bucket - else - Puppet.debug "typeless scope; just returning a list" - return results - end + if ary.empty? + return nil + else + return ary end + end - # Undefine a variable; only used for testing. - def unsetvar(var) - if @symtable.include?(var) - @symtable.delete(var) - end - end + protected - protected - - # This method abstracts recursive searching. It accepts the type - # of search being done and then either a literal key to search for or - # a Proc instance to do the searching. - def lookup(type,sub, usecontext = false) - table = @map[type] - if table.nil? - error = Puppet::ParseError.new( - "Could not retrieve %s table at level %s" % - [type,self.level] - ) - raise error - end + # This method abstracts recursive searching. It accepts the type + # of search being done and then either a literal key to search for or + # a Proc instance to do the searching. + def lookup(type,sub, usecontext = false) + table = @map[type] + if table.nil? + self.fail "Could not retrieve %s table at level %s" % + [type,self.level] + end - if sub.is_a?(Proc) and obj = sub.call(table) - return obj - elsif table.include?(sub) - return table[sub] - elsif ! @parent.nil? - # Context is used for retricting overrides. - if usecontext and self.context != @parent.context - return :undefined - else - return @parent.lookup(type,sub, usecontext) - end - else + if sub.is_a?(Proc) and obj = sub.call(table) + return obj + elsif table.include?(sub) + return table[sub] + elsif ! @parent.nil? + # Context is used for retricting overrides. + if usecontext and self.context != @parent.context return :undefined + else + return @parent.lookup(type,sub, usecontext) end + else + return :undefined end end end |