# The scope class, which handles storing and retrieving variables and types and
# such.
require 'puppet/parser/parser'
require 'puppet/parser/templatewrapper'
require 'puppet/transportable'
require 'strscan'
class Puppet::Parser::Scope
require 'puppet/parser/resource'
AST = Puppet::Parser::AST
Puppet::Util.logmethods(self)
include Enumerable
include Puppet::Util::Errors
attr_accessor :parent, :level, :interp, :source, :host
attr_accessor :name, :type, :topscope, :base, :keyword
attr_accessor :top, :translated, :exported, :virtual
# Whether we behave declaratively. Note that it's a class variable,
# so all scopes behave the same.
@@declarative = true
# Retrieve and set the declarative setting.
def self.declarative
return @@declarative
end
def self.declarative=(val)
@@declarative = val
end
# 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
# 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
# Is the value true? This allows us to control the definition of truth
# in one place.
def self.true?(value)
if value == false or value == "" or value == :undef
return false
else
return true
end
end
# Add to our list of namespaces.
def add_namespace(ns)
return false if @namespaces.include?(ns)
if @namespaces == [""]
@namespaces = [ns]
else
@namespaces << ns
end
end
# Is the type a builtin type?
def builtintype?(type)
if typeklass = Puppet::Type.type(type)
return typeklass
else
return false
end
end
# Create a new child scope.
def child=(scope)
@children.push(scope)
# Copy all of the shared tables over to the child.
@@sharedtables.each do |name|
scope.send(name.to_s + "=", self.send(name))
end
end
# Verify that the given object isn't defined elsewhere.
def chkobjectclosure(obj)
if exobj = @definedtable[obj.ref]
typeklass = Puppet::Type.type(obj.type)
if typeklass and ! typeklass.isomorphic?
Puppet.info "Allowing duplicate %s" % type
else
# Either it's a defined type, which are never
# isomorphic, or it's a non-isomorphic type.
msg = "Duplicate definition: %s is already defined" % obj.ref
if exobj.file and exobj.line
msg << " in file %s at line %s" %
[exobj.file, exobj.line]
end
if obj.line or obj.file
msg << "; cannot redefine"
end
raise Puppet::ParseError.new(msg)
end
end
return true
end
# 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.
def class_scope(klass)
scope = if klass.respond_to?(:classname)
@classtable[klass.classname]
else
@classtable[klass]
end
return nil unless scope
if scope.nodescope? and ! klass.is_a?(AST::Node)
raise Puppet::ParseError, "Node %s has already been evaluated; cannot evaluate class with same name" % [klass.classname]
end
scope
end
# Return the list of collections.
def collections
@collecttable
end
def declarative=(val)
self.class.declarative = val
end
def declarative
self.class.declarative
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
# Remove a specific child.
def delete(child)
@children.delete(child)
end
# Remove a resource from the various tables. This is only used when
# a resource maps to a definition and gets evaluated.
def deleteresource(resource)
if @definedtable[resource.ref]
@definedtable.delete(resource.ref)
end
if @children.include?(resource)
@children.delete(resource)
end
end
# Are we the top scope?
def topscope?
@level == 1
end
# 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
# Evaluate a list of classes.
def evalclasses(*classes)
retval = []
classes.each do |klass|
if obj = findclass(klass)
obj.safeevaluate :scope => self
retval << klass
end
end
retval
end
def exported?
self.exported
end
def findclass(name)
@namespaces.each do |namespace|
if r = interp.findclass(namespace, name)
return r
end
end
return nil
end
def finddefine(name)
@namespaces.each do |namespace|
if r = interp.finddefine(namespace, name)
return r
end
end
return nil
end
def findresource(string, name = nil)
if name
string = "%s[%s]" % [string.capitalize, name]
end
@definedtable[string]
end
# Recursively complete the whole tree, in preparation for
# translation or storage.
def finish
self.each do |obj|
obj.finish
end
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
if hash.include?(:namespace)
if n = hash[:namespace]
@namespaces = [n]
end
hash.delete(:namespace)
else
@namespaces = [""]
end
hash.each { |name, val|
method = name.to_s + "="
if self.respond_to? method
self.send(method, val)
else
raise Puppet::DevError, "Invalid scope argument %s" % name
end
}
@tags = []
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
@source = hash[:source] || @parent.source
@topscope = @parent.topscope
#@inside = @parent.inside # Used for definition inheritance
@host = @parent.host
@type ||= @parent.type
end
# Our child scopes and objects
@children = []
# The symbol table for this scope. This is where we store variables.
@symtable = {}
# All of the defaults set for types. It's a hash of hashes,
# with the first key being the type, then the second key being
# the parameter.
@defaultstable = Hash.new { |dhash,type|
dhash[type] = {}
}
unless @interp
raise Puppet::DevError, "Scopes require an interpreter"
end
end
# 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
# 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 = {}
self.class.declarative = declarative
# The table for all defined objects.
@definedtable = {}
# The list of objects that will available for export.
@exportable = {}
# 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
# 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
@namespaces = [""]
# 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 = []
@topscope = self
@type = "puppet"
@name = "top"
end
# Collect all of the defaults set at any higher scopes.
# This is a different type of lookup because it's additive --
# it collects all of the defaults, with defaults in closer scopes
# overriding those in later scopes.
def lookupdefaults(type)
values = {}
# first collect the values from the parents
unless @parent.nil?
@parent.lookupdefaults(type).each { |var,value|
values[var] = value
}
end
# then override them with any current values
# this should probably be done differently
if @defaultstable.include?(type)
@defaultstable[type].each { |var,value|
values[var] = value
}
end
#Puppet.debug "Got defaults for %s: %s" %
# [type,values.inspect]
return values
end
# Look up all of the exported objects of a given type.
def lookupexported(type)
@definedtable.find_all do |name, r|
r.type == type and r.exported?
end
end
def lookupoverrides(obj)
@overridetable[obj.ref]
end
# Look up a defined type.
def lookuptype(name)
finddefine(name) || findclass(name)
end
def lookup_qualified_var(name, usestring)
parts = name.split(/::/)
shortname = parts.pop
klassname = parts.join("::")
klass = findclass(klassname)
unless klass
raise Puppet::ParseError, "Could not find class %s" % klassname
end
unless kscope = class_scope(klass)
raise Puppet::ParseError, "Class %s has not been evaluated so its variables cannot be referenced" % klass.classname
end
return kscope.lookupvar(shortname, usestring)
end
private :lookup_qualified_var
# Look up a variable. The simplest value search we do. Default to returning
# an empty string for missing values, but support returning a constant.
def lookupvar(name, usestring = true)
# If the variable is qualified, then find the specified scope and look the variable up there instead.
if name =~ /::/
return lookup_qualified_var(name, usestring)
end
# We can't use "if @symtable[name]" here because the value might be false
if @symtable.include?(name)
if usestring and @symtable[name] == :undef
return ""
else
return @symtable[name]
end
elsif self.parent
return @parent.lookupvar(name, usestring)
elsif usestring
return ""
else
return :undefined
end
end
def namespaces
@namespaces.dup
end
# Add a collection to the global list.
def newcollection(coll)
@collecttable << coll
end
# Create a new scope.
def newscope(hash = {})
hash[:parent] = self
#debug "Creating new scope, level %s" % [self.level + 1]
return Puppet::Parser::Scope.new(hash)
end
# Is this class for a node? This is used to make sure that
# nodes and classes with the same name conflict (#620), which
# is required because of how often the names are used throughout
# the system, including on the client.
def nodescope?
defined?(@nodescope) and @nodescope
end
# Return the list of remaining overrides.
def overrides
#@overridetable.collect { |name, overs| overs }.flatten
@overridetable.values.flatten
end
def resources
@definedtable.values
end
# Store the fact that we've evaluated a given class. We use a hash
# that gets inherited from the top scope down, rather than a global
# hash. We store the object ID, not class name, so that we
# can support multiple unrelated classes with the same name.
def setclass(obj)
if obj.is_a?(AST::HostClass)
unless obj.classname
raise Puppet::DevError, "Got a %s with no fully qualified name" %
obj.class
end
@classtable[obj.classname] = self
else
raise Puppet::DevError, "Invalid class %s" % obj.inspect
end
if obj.is_a?(AST::Node)
@nodescope = true
end
nil
end
# Set all of our facts in the top-level scope.
def setfacts(facts)
facts.each { |var, value|
self.setvar(var, value)
}
end
# Add a new object to our object table and the global list, and do any necessary
# checks.
def setresource(obj)
self.chkobjectclosure(obj)
@children << obj
# Mark the resource as virtual or exported, as necessary.
if self.exported?
obj.exported = true
elsif self.virtual?
obj.virtual = true
end
# The global table
@definedtable[obj.ref] = obj
return obj
end
# Override a parameter in an existing object. If the object does not yet
# exist, then cache the override in a global table, so it can be flushed
# at the end.
def setoverride(resource)
resource.override = true
if obj = @definedtable[resource.ref]
obj.merge(resource)
else
@overridetable[resource.ref] << resource
end
end
# Set defaults for a type. The typename should already be downcased,
# so that the syntax is isolated. We don't do any kind of type-checking
# here; instead we let the resource do it when the defaults are used.
def setdefaults(type, params)
table = @defaultstable[type]
# if we got a single param, it'll be in its own array
params = [params] unless params.is_a?(Array)
params.each { |param|
#Puppet.debug "Default for %s is %s => %s" %
# [type,ary[0].inspect,ary[1].inspect]
if @@declarative
if table.include?(param.name)
self.fail "Default already defined for %s { %s }" %
[type,param.name]
end
else
if table.include?(param.name)
# we should maybe allow this warning to be turned off...
Puppet.warning "Replacing default for %s { %s }" %
[type,param.name]
end
end
table[param.name] = param
}
end
# Set a variable in the current scope. This will override settings
# in scopes above, but will not allow variables in the current scope
# to be reassigned if we're declarative (which is the default).
def setvar(name,value, file = nil, line = nil)
#Puppet.debug "Setting %s to '%s' at level %s" %
# [name.inspect,value,self.level]
if @symtable.include?(name)
if @@declarative
error = Puppet::ParseError.new("Cannot reassign variable %s" % name)
if file
error.file = file
end
if line
error.line = line
end
raise error
else
Puppet.warning "Reassigning %s to %s" % [name,value]
end
end
@symtable[name] = value
end
# Return an interpolated string.
def strinterp(string, file = nil, line = nil)
# Most strings won't have variables in them.
ss = StringScanner.new(string)
out = ""
while not ss.eos?
if ss.scan(/^\$\{((\w*::)*\w+)\}|^\$((\w*::)*\w+)/)
# If it matches the backslash, then just retun the dollar sign.
if ss.matched == '\\$'
out << '$'
else # look the variable up
out << lookupvar(ss[1] || ss[3]).to_s || ""
end
elsif ss.scan(/^\\(.)/)
# Puppet.debug("Got escape: pos:%d; m:%s" % [ss.pos, ss.matched])
case ss[1]
when 'n'
out << "\n"
when 't'
out << "\t"
when 's'
out << " "
when '\\'
out << '\\'
when '$'
out << '$'
else
str = "Unrecognised escape sequence '#{ss.matched}'"
if file
str += " in file %s" % file
end
if line
str += " at line %s" % line
end
Puppet.warning str
out << ss.matched
end
elsif ss.scan(/^\$/)
out << '$'
elsif ss.scan(/^\\\n/) # an escaped carriage return
next
else
tmp = ss.scan(/[^\\$]+/)
# Puppet.debug("Got other: pos:%d; m:%s" % [ss.pos, tmp])
unless tmp
error = Puppet::ParseError.new("Could not parse string %s" %
string.inspect)
{:file= => file, :line= => line}.each do |m,v|
error.send(m, v) if v
end
raise error
end
out << tmp
end
end
return out
end
# Add a tag to our current list. These tags will be added to all
# of the objects contained in this scope.
def tag(*ary)
ary.each { |tag|
if tag.nil? or tag == ""
puts caller
Puppet.debug "got told to tag with %s" % tag.inspect
next
end
unless tag =~ /^\w[-\w]*$/
fail Puppet::ParseError, "Invalid tag %s" % tag.inspect
end
tag = tag.to_s
unless @tags.include?(tag)
#Puppet.info "Tagging scope %s with %s" % [self.object_id, tag]
@tags << tag
end
}
end
# Return the tags associated with this scope. It's basically
# just our parents' tags, plus our type. We don't cache this value
# because our parent tags might change between calls.
def tags
tmp = [] + @tags
unless ! defined? @type or @type.nil? or @type == ""
tmp << @type.to_s
end
if @parent
#info "Looking for tags in %s" % @parent.type
@parent.tags.each { |tag|
if tag.nil? or tag == ""
Puppet.debug "parent returned tag %s" % tag.inspect
next
end
unless tmp.include?(tag)
tmp << tag
end
}
end
return tmp.sort.uniq
end
# Used mainly for logging
def to_s
if self.name
return "%s[%s]" % [@type, @name]
else
return self.type.to_s
end
end
# Convert all of our objects as necessary.
def translate
ret = @children.collect do |child|
case child
when Puppet::Parser::Resource
child.to_trans
when self.class
child.translate
else
devfail "Got %s for translation" % child.class
end
end.reject { |o| o.nil? }
bucket = Puppet::TransBucket.new ret
case self.type
when "": bucket.type = "main"
when nil: devfail "A Scope with no type"
else
bucket.type = @type
end
if self.name
bucket.name = self.name
end
return bucket
end
# Undefine a variable; only used for testing.
def unsetvar(var)
if @symtable.include?(var)
@symtable.delete(var)
end
end
# 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 ary.empty?
return nil
else
return ary
end
end
def virtual?
self.virtual || self.exported?
end
end
# $Id: scope.rb 2646 2007-07-04 21:06:26Z luke $
syntax highlighted by Code2HTML, v. 0.9.1