diff options
| author | Luke Kanies <luke@madstop.com> | 2007-08-22 18:03:55 -0500 |
|---|---|---|
| committer | Luke Kanies <luke@madstop.com> | 2007-08-22 18:03:55 -0500 |
| commit | 0682d7e473cfd8f2fe6bee9eae0868b846fd0d50 (patch) | |
| tree | fb66cc4c81e95ee42905410310095b9a8ae23ecc /lib/puppet | |
| parent | ec50484518425ec8ac36f89b087beb27d5a3d2c8 (diff) | |
| parent | 8b3361afae35cfb65754d7bd9aff5b820ed714f0 (diff) | |
| download | puppet-0682d7e473cfd8f2fe6bee9eae0868b846fd0d50.tar.gz puppet-0682d7e473cfd8f2fe6bee9eae0868b846fd0d50.tar.xz puppet-0682d7e473cfd8f2fe6bee9eae0868b846fd0d50.zip | |
Merge branch 'multi_env'
Diffstat (limited to 'lib/puppet')
28 files changed, 1636 insertions, 1306 deletions
diff --git a/lib/puppet/configuration.rb b/lib/puppet/configuration.rb index 65e0d9fa8..a8f6e0c35 100644 --- a/lib/puppet/configuration.rb +++ b/lib/puppet/configuration.rb @@ -122,7 +122,11 @@ module Puppet "The configuration file that defines the rights to the different namespaces and methods. This can be used as a coarse-grained authorization system for both ``puppetd`` and ``puppetmasterd``." - ] + ], + :environment => ["", "The environment Puppet is running in. For clients (e.g., ``puppetd``) this + determines the environment itself, which is used to find modules and much more. For + servers (i.e., ``puppetmasterd``) this provides the default environment for nodes we + know nothing about."] ) hostname = Facter["hostname"].value @@ -544,7 +548,11 @@ module Puppet setdefaults(:parser, :typecheck => [true, "Whether to validate types during parsing."], - :paramcheck => [true, "Whether to validate parameters during parsing."] + :paramcheck => [true, "Whether to validate parameters during parsing."], + :node_source => ["none", "Where to look for node configuration information. + The default node source, ``none``, just returns a node with its facts + filled in, which is required for normal functionality. + See the `NodeSourceReference`:trac: for more information."] ) setdefaults(:main, diff --git a/lib/puppet/dsl.rb b/lib/puppet/dsl.rb index 9c652f082..bd0fcbf96 100644 --- a/lib/puppet/dsl.rb +++ b/lib/puppet/dsl.rb @@ -113,10 +113,6 @@ module Puppet @aspects = {} - # For now, just do some hackery so resources work - @@interp = Puppet::Parser::Interpreter.new :Code => "" - @@scope = Puppet::Parser::Scope.new(:interp => @@interp) - @@objects = Hash.new do |hash, key| hash[key] = {} end @@ -237,7 +233,7 @@ module Puppet end unless obj = @@objects[type][name] obj = Resource.new :title => name, :type => type.name, - :source => source, :scope => @@scope + :source => source, :scope => scope @@objects[type][name] = obj @resources << obj @@ -250,12 +246,26 @@ module Puppet :source => source ) - obj.set(param) + obj.send(:set_parameter, param) end obj end + def scope + unless defined?(@scope) + @interp = Puppet::Parser::Interpreter.new :Code => "" + # Load the class, so the node object class is available. + require 'puppet/network/handler/node' + @node = Puppet::Node.new(Facter.value(:hostname)) + @node.parameters = Facter.to_hash + @interp = Puppet::Parser::Interpreter.new :Code => "" + @config = Puppet::Parser::Configuration.new(@node, @interp.parser) + @scope = @config.topscope + end + @scope + end + def type self.name end diff --git a/lib/puppet/network/handler.rb b/lib/puppet/network/handler.rb index 33343e4fe..c2fbfcba5 100644 --- a/lib/puppet/network/handler.rb +++ b/lib/puppet/network/handler.rb @@ -10,7 +10,7 @@ module Puppet::Network # This is so that the handlers can subclass just 'Handler', rather # then having to specify the full class path. Handler = self - attr_accessor :server + attr_accessor :server, :local extend Puppet::Util::SubclassLoader extend Puppet::Util @@ -44,6 +44,10 @@ module Puppet::Network # Create an empty init method with the same signature. def initialize(hash = {}) end + + def local? + self.local + end end end diff --git a/lib/puppet/network/handler/configuration.rb b/lib/puppet/network/handler/configuration.rb new file mode 100644 index 000000000..7e91d74d6 --- /dev/null +++ b/lib/puppet/network/handler/configuration.rb @@ -0,0 +1,209 @@ +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) + 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 e889c1ba8..acc6c4cda 100644 --- a/lib/puppet/network/handler/master.rb +++ b/lib/puppet/network/handler/master.rb @@ -13,7 +13,7 @@ class Puppet::Network::Handler include Puppet::Util - attr_accessor :ast, :local + attr_accessor :ast attr_reader :ca @interface = XMLRPC::Service::Interface.new("puppetmaster") { |iface| @@ -21,66 +21,9 @@ class Puppet::Network::Handler iface.add_method("int freshness()") } - # FIXME At some point, this should be autodocumenting. - def addfacts(facts) - # Add our server version to the fact list - facts["serverversion"] = Puppet.version.to_s - - # And then add the server name and IP - {"servername" => "fqdn", - "serverip" => "ipaddress" - }.each do |var, fact| - if obj = Facter[fact] - facts[var] = obj.value - else - Puppet.warning "Could not retrieve fact %s" % fact - end - end - - if facts["servername"].nil? - host = Facter.value(:hostname) - if domain = Facter.value(:domain) - facts["servername"] = [host, domain].join(".") - else - facts["servername"] = host - end - end - end - - # 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 - # Tell a client whether there's a fresh config for it def freshness(client = nil, clientip = nil) - if Puppet.features.rails? and Puppet[:storeconfigs] - Puppet::Rails.connect - - host = Puppet::Rails::Host.find_or_create_by_name(client) - host.last_freshcheck = Time.now - if clientip and (! host.ip or host.ip == "" or host.ip == "NULL") - host.ip = clientip - end - host.save - end - if defined? @interpreter - return @interpreter.parsedate - else - return 0 - end + config_handler.version(client, clientip) end def initialize(hash = {}) @@ -99,7 +42,7 @@ class Puppet::Network::Handler @local = false end - args[:Local] = @local + args[:Local] = local? if hash.include?(:CA) and hash[:CA] @ca = Puppet::SSLCertificates::CA.new() @@ -121,104 +64,79 @@ class Puppet::Network::Handler args[:Classes] = hash[:Classes] end - @interpreter = Puppet::Parser::Interpreter.new(args) + @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) - 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" + facts = decode_facts(facts) + client, clientip = clientname(client, clientip, facts) - # XXX this should definitely be done in the protocol, somehow - case format - when "marshal": - Puppet.warning "You should upgrade your client. 'Marshal' will not be supported much longer." - begin - facts = Marshal::load(CGI.unescape(facts)) - rescue => detail - raise XMLRPC::FaultException.new( - 1, "Could not rebuild facts" - ) - end - when "yaml": - begin - facts = YAML.load(CGI.unescape(facts)) - rescue => detail - raise XMLRPC::FaultException.new( - 1, "Could not rebuild facts" - ) - end - else - raise XMLRPC::FaultException.new( - 1, "Unavailable config format %s" % format - ) - end - end + # Pass the facts to the fact handler + fact_handler.set(client, facts) - client, clientip = clientname(client, clientip, facts) + # And get the configuration from the config handler + return config_handler.configuration(client) + end - # Add any server-side facts to our server. - addfacts(facts) + def local=(val) + @local = val + config_handler.local = val + fact_handler.local = val + end - retobjects = nil + private - # This is hackish, but there's no "silence" option for benchmarks - # right now - if @local - #begin - retobjects = @interpreter.run(client, facts) - #rescue Puppet::Error => detail - # Puppet.err detail - # raise XMLRPC::FaultException.new( - # 1, detail.to_s - # ) - #rescue => detail - # Puppet.err detail.to_s - # return "" - #end - else - benchmark(:notice, "Compiled configuration for %s" % client) do - begin - retobjects = @interpreter.run(client, facts) - rescue Puppet::Error => detail - Puppet.err detail - raise XMLRPC::FaultException.new( - 1, detail.to_s - ) - rescue => detail - Puppet.err detail.to_s - return "" - end + # 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 - return retobjects + # we don't need to do anything, since we should already + # have raw objects + Puppet.debug "Our client is local" else - str = nil - case format - when "marshal": - str = Marshal::dump(retobjects) - when "yaml": - str = retobjects.to_yaml(:UseBlock => true) - else + Puppet.debug "Our client is remote" + + begin + facts = YAML.load(CGI.unescape(facts)) + rescue => detail raise XMLRPC::FaultException.new( - 1, "Unavailable config format %s" % format + 1, "Could not rebuild facts" ) end - return CGI.escape(str) end + + return facts end - def local? - if defined? @local and @local - return true - else - return false + def fact_handler + unless defined? @fact_handler + @fact_handler = Puppet::Network::Handler.handler(:facts).new :local => local? end + @fact_handler end end end diff --git a/lib/puppet/network/handler/node.rb b/lib/puppet/network/handler/node.rb new file mode 100644 index 000000000..2c4d3e1b5 --- /dev/null +++ b/lib/puppet/network/handler/node.rb @@ -0,0 +1,227 @@ +# Created by Luke A. Kanies on 2007-08-13. +# Copyright (c) 2007. All rights reserved. + +require 'puppet/util' +require 'puppet/node' +require 'puppet/util/classgen' +require 'puppet/util/instance_loader' + +# Look up a node, along with all the details about it. +class Puppet::Network::Handler::Node < Puppet::Network::Handler + desc "Retrieve information about nodes." + + # Add a new node source. + def self.newnode_source(name, options = {}, &block) + name = symbolize(name) + + fact_merge = options[:fact_merge] + mod = genmodule(name, :extend => SourceBase, :hash => instance_hash(:node_source), :block => block) + mod.send(:define_method, :fact_merge?) do + fact_merge + end + mod + end + + # Collect the docs for all of our node sources. + def self.node_source_docs + docs = "" + + # Use this method so they all get loaded + instance_loader(:node_source).loadall + loaded_instances(:node_source).sort { |a,b| a.to_s <=> b.to_s }.each do |name| + mod = self.node_source(name) + docs += "%s\n%s\n" % [name, "-" * name.to_s.length] + + docs += Puppet::Util::Docs.scrub(mod.doc) + "\n\n" + end + + docs + end + + # List each of the node sources. + def self.node_sources + instance_loader(:node_source).loadall + loaded_instances(:node_source) + end + + # Remove a defined node source; basically only used for testing. + def self.rm_node_source(name) + rmclass(name, :hash => instance_hash(:node_source)) + end + + extend Puppet::Util::ClassGen + extend Puppet::Util::InstanceLoader + + # A simple base module we can use for modifying how our node sources work. + module SourceBase + include Puppet::Util::Docs + end + + @interface = XMLRPC::Service::Interface.new("nodes") { |iface| + iface.add_method("string details(key)") + iface.add_method("string parameters(key)") + iface.add_method("string environment(key)") + iface.add_method("string classes(key)") + } + + # Set up autoloading and retrieving of reports. + autoload :node_source, 'puppet/node_source' + + attr_reader :source + + # Return a given node's classes. + def classes(key) + if node = details(key) + node.classes + else + nil + end + end + + # Return an entire node configuration. This uses the 'nodesearch' method + # defined in the node_source to look for the node. + def details(key, client = nil, clientip = nil) + facts = node_facts(key) + node = nil + names = node_names(key, facts) + names.each do |name| + name = name.to_s if name.is_a?(Symbol) + if node = nodesearch(name) + Puppet.info "Found %s in %s" % [name, @source] + break + end + end + + # If they made it this far, we haven't found anything, so look for a + # default node. + unless node or names.include?("default") + if node = nodesearch("default") + Puppet.notice "Using default node for %s" % key + end + end + + if node + node.source = @source + node.names = names + + # Merge the facts into the parameters. + if fact_merge? + node.fact_merge(facts) + end + return node + else + return nil + end + end + + # Return a given node's environment. + def environment(key, client = nil, clientip = nil) + if node = details(key) + node.environment + else + nil + end + end + + # Create our node lookup tool. + def initialize(hash = {}) + @source = hash[:Source] || Puppet[:node_source] + + unless mod = self.class.node_source(@source) + raise ArgumentError, "Unknown node source '%s'" % @source + end + + extend(mod) + + super + + # We cache node info for speed + @node_cache = {} + end + + # Try to retrieve a given node's parameters. + def parameters(key, client = nil, clientip = nil) + if node = details(key) + node.parameters + else + nil + end + end + + private + + # Store the node to make things a bit faster. + def cache(node) + @node_cache[node.name] = node + end + + # If the node is cached, return it. + def cached?(name) + # Don't use cache when the filetimeout is set to 0 + return false if [0, "0"].include?(Puppet[:filetimeout]) + + if node = @node_cache[name] and Time.now - node.time < Puppet[:filetimeout] + return node + else + return false + end + end + + # Create/cache a fact handler. + def fact_handler + unless defined?(@fact_handler) + @fact_handler = Puppet::Network::Handler.handler(:facts).new + end + @fact_handler + end + + # Short-hand for creating a new node, so the node sources don't need to + # specify the constant. + def newnode(options) + Puppet::Node.new(options) + end + + # Look up the node facts from our fact handler. + def node_facts(key) + if facts = fact_handler.get(key) + facts + else + {} + end + end + + # Calculate the list of node names we should use for looking + # up our node. + def node_names(key, facts = nil) + facts ||= node_facts(key) + names = [] + + if hostname = facts["hostname"] + unless hostname == key + names << hostname + end + else + hostname = key + end + + if fqdn = facts["fqdn"] + hostname = fqdn + names << fqdn + end + + # Make sure both the fqdn and the short name of the + # host can be used in the manifest + if hostname =~ /\./ + names << hostname.sub(/\..+/,'') + elsif domain = facts['domain'] + names << hostname + "." + domain + end + + # Sort the names inversely by name length. + names.sort! { |a,b| b.length <=> a.length } + + # And make sure the key is first, since that's the most + # likely usage. + ([key] + names).uniq + end +end diff --git a/lib/puppet/network/handler/resource.rb b/lib/puppet/network/handler/resource.rb index ca492bd81..ac29dce53 100755 --- a/lib/puppet/network/handler/resource.rb +++ b/lib/puppet/network/handler/resource.rb @@ -26,7 +26,7 @@ class Puppet::Network::Handler # Apply a TransBucket as a transaction. def apply(bucket, format = "yaml", client = nil, clientip = nil) - unless @local + unless local? begin case format when "yaml": @@ -43,7 +43,7 @@ class Puppet::Network::Handler # Create a client, but specify the remote machine as the server # because the class requires it, even though it's unused - client = Puppet::Network::Client.client(:Master).new(:Server => client||"localhost") + client = Puppet::Network::Client.client(:Master).new(:Master => client||"localhost") # Set the objects client.objects = component diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb new file mode 100644 index 000000000..3bbbe5979 --- /dev/null +++ b/lib/puppet/node.rb @@ -0,0 +1,40 @@ +# A simplistic class for managing the node information itself. +class Puppet::Node + attr_accessor :name, :classes, :parameters, :environment, :source, :ipaddress, :names + attr_reader :time + + def initialize(name, options = {}) + @name = name + + # Provide a default value. + @names = [name] + + if classes = options[:classes] + if classes.is_a?(String) + @classes = [classes] + else + @classes = classes + end + else + @classes = [] + end + + @parameters = options[:parameters] || {} + + unless @environment = options[:environment] + if env = Puppet[:environment] and env != "" + @environment = env + end + end + + @time = Time.now + end + + # Merge the node facts with parameters from the node source. + # This is only called if the node source has 'fact_merge' set to true. + def fact_merge(facts) + facts.each do |name, value| + @parameters[name] = value unless @parameters.include?(name) + end + end +end diff --git a/lib/puppet/node_source/external.rb b/lib/puppet/node_source/external.rb new file mode 100644 index 000000000..54111d924 --- /dev/null +++ b/lib/puppet/node_source/external.rb @@ -0,0 +1,51 @@ +Puppet::Network::Handler::Node.newnode_source(:external, :fact_merge => true) do + desc "Call an external program to get node information." + + include Puppet::Util + # Look for external node definitions. + def nodesearch(name) + return nil unless Puppet[:external_nodes] != "none" + + # This is a very cheap way to do this, since it will break on + # commands that have spaces in the arguments. But it's good + # enough for most cases. + external_node_command = Puppet[:external_nodes].split + external_node_command << name + begin + output = Puppet::Util.execute(external_node_command) + rescue Puppet::ExecutionFailure => detail + if $?.exitstatus == 1 + return nil + else + Puppet.err "Could not retrieve external node information for %s: %s" % [name, detail] + end + return nil + end + + if output =~ /\A\s*\Z/ # all whitespace + Puppet.debug "Empty response for %s from external node source" % name + return nil + end + + begin + result = YAML.load(output).inject({}) { |hash, data| hash[symbolize(data[0])] = data[1]; hash } + rescue => detail + raise Puppet::Error, "Could not load external node results for %s: %s" % [name, detail] + end + + node = newnode(name) + set = false + [:parameters, :classes].each do |param| + if value = result[param] + node.send(param.to_s + "=", value) + set = true + end + end + + if set + return node + else + return nil + end + end +end diff --git a/lib/puppet/node_source/ldap.rb b/lib/puppet/node_source/ldap.rb new file mode 100644 index 000000000..7b60a3c62 --- /dev/null +++ b/lib/puppet/node_source/ldap.rb @@ -0,0 +1,138 @@ +Puppet::Network::Handler::Node.newnode_source(:ldap, :fact_merge => true) do + desc "Search in LDAP for node configuration information." + + # Find the ldap node, return the class list and parent node specially, + # and everything else in a parameter hash. + def ldapsearch(node) + filter = Puppet[:ldapstring] + classattrs = Puppet[:ldapclassattrs].split("\s*,\s*") + if Puppet[:ldapattrs] == "all" + # A nil value here causes all attributes to be returned. + search_attrs = nil + else + search_attrs = classattrs + Puppet[:ldapattrs].split("\s*,\s*") + end + pattr = nil + if pattr = Puppet[:ldapparentattr] + if pattr == "" + pattr = nil + else + search_attrs << pattr unless search_attrs.nil? + end + end + + if filter =~ /%s/ + filter = filter.gsub(/%s/, node) + end + + parent = nil + classes = [] + parameters = nil + + found = false + count = 0 + + begin + # We're always doing a sub here; oh well. + ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) do |entry| + found = true + if pattr + if values = entry.vals(pattr) + if values.length > 1 + raise Puppet::Error, + "Node %s has more than one parent: %s" % + [node, values.inspect] + end + unless values.empty? + parent = values.shift + end + end + end + + classattrs.each { |attr| + if values = entry.vals(attr) + values.each do |v| classes << v end + end + } + + parameters = entry.to_hash.inject({}) do |hash, ary| + if ary[1].length == 1 + hash[ary[0]] = ary[1].shift + else + hash[ary[0]] = ary[1] + end + hash + end + end + rescue => detail + if count == 0 + # Try reconnecting to ldap + @ldap = nil + retry + else + raise Puppet::Error, "LDAP Search failed: %s" % detail + end + end + + classes.flatten! + + if classes.empty? + classes = nil + end + + if parent or classes or parameters + return parent, classes, parameters + else + return nil + end + end + + # Look for our node in ldap. + def nodesearch(node) + unless ary = ldapsearch(node) + return nil + end + parent, classes, parameters = ary + + while parent + parent, tmpclasses, tmpparams = ldapsearch(parent) + classes += tmpclasses if tmpclasses + tmpparams.each do |param, value| + # Specifically test for whether it's set, so false values are handled + # correctly. + parameters[param] = value unless parameters.include?(param) + end + end + + return newnode(node, :classes => classes, :source => "ldap", :parameters => parameters) + end + + private + + # Create an ldap connection. + def ldap + unless defined? @ldap and @ldap + unless Puppet.features.ldap? + raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" + end + begin + if Puppet[:ldapssl] + @ldap = LDAP::SSLConn.new(Puppet[:ldapserver], Puppet[:ldapport]) + elsif Puppet[:ldaptls] + @ldap = LDAP::SSLConn.new( + Puppet[:ldapserver], Puppet[:ldapport], true + ) + else + @ldap = LDAP::Conn.new(Puppet[:ldapserver], Puppet[:ldapport]) + end + @ldap.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) + @ldap.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON) + @ldap.simple_bind(Puppet[:ldapuser], Puppet[:ldappassword]) + rescue => detail + raise Puppet::Error, "Could not connect to LDAP: %s" % detail + end + end + + return @ldap + end +end diff --git a/lib/puppet/node_source/none.rb b/lib/puppet/node_source/none.rb new file mode 100644 index 000000000..ce188add5 --- /dev/null +++ b/lib/puppet/node_source/none.rb @@ -0,0 +1,10 @@ +Puppet::Network::Handler::Node.newnode_source(:none, :fact_merge => true) do + desc "Always return an empty node object. This is the node source you should + use when you don't have some other, functional source you want to use, + as the compiler will not work without this node information." + + # Just return an empty node. + def nodesearch(name) + newnode(name) + end +end diff --git a/lib/puppet/parser/ast/astarray.rb b/lib/puppet/parser/ast/astarray.rb index a0bd5bf89..c0212f919 100644 --- a/lib/puppet/parser/ast/astarray.rb +++ b/lib/puppet/parser/ast/astarray.rb @@ -21,45 +21,38 @@ class Puppet::Parser::AST # We basically always operate declaratively, and when we # do we need to evaluate the settor-like statements first. This # is basically variable and type-default declarations. - if scope.declarative? - # This is such a stupid hack. I've no real idea how to make a - # "real" declarative language, so I hack it so it looks like - # one, yay. - settors = [] - others = [] + # This is such a stupid hack. I've no real idea how to make a + # "real" declarative language, so I hack it so it looks like + # one, yay. + settors = [] + others = [] - # Make a new array, so we don't have to deal with the details of - # flattening and such - items = [] - - # First clean out any AST::ASTArrays - @children.each { |child| - if child.instance_of?(AST::ASTArray) - child.each do |ac| - if ac.class.settor? - settors << ac - else - others << ac - end - end - else - if child.class.settor? - settors << child + # Make a new array, so we don't have to deal with the details of + # flattening and such + items = [] + + # First clean out any AST::ASTArrays + @children.each { |child| + if child.instance_of?(AST::ASTArray) + child.each do |ac| + if ac.class.settor? + settors << ac else - others << child + others << ac end end - } - rets = [settors, others].flatten.collect { |child| - child.safeevaluate(:scope => scope) - } - return rets.reject { |o| o.nil? } - else - # If we're not declarative, just do everything in order. - return @children.collect { |item| - item.safeevaluate(:scope => scope) - }.reject { |o| o.nil? } - end + else + if child.class.settor? + settors << child + else + others << child + end + end + } + rets = [settors, others].flatten.collect { |child| + child.safeevaluate(:scope => scope) + } + return rets.reject { |o| o.nil? } end def push(*ary) diff --git a/lib/puppet/parser/ast/collection.rb b/lib/puppet/parser/ast/collection.rb index c817b5c5e..7daf031cf 100644 --- a/lib/puppet/parser/ast/collection.rb +++ b/lib/puppet/parser/ast/collection.rb @@ -20,7 +20,7 @@ class Collection < AST::Branch newcoll = Puppet::Parser::Collector.new(scope, @type, str, code, self.form) - scope.newcollection(newcoll) + scope.configuration.add_collection(newcoll) newcoll end diff --git a/lib/puppet/parser/ast/component.rb b/lib/puppet/parser/ast/component.rb index 65f310212..17cfa9d61 100644 --- a/lib/puppet/parser/ast/component.rb +++ b/lib/puppet/parser/ast/component.rb @@ -27,7 +27,7 @@ class Puppet::Parser::AST false end - def evaluate(hash) + def evaluate_resource(hash) origscope = hash[:scope] title = hash[:title] args = symbolize_options(hash[:arguments] || {}) diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb index 642645824..9b60c692f 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -24,8 +24,10 @@ class Puppet::Parser::AST scope = hash[:scope] args = hash[:arguments] - # Verify that we haven't already been evaluated - if scope.class_scope(self) + # Verify that we haven't already been evaluated, and if we have been evaluated, + # make sure that we match the class. + if existing_scope = scope.class_scope(self) + #if existing_scope.source.object_id == self.object_id Puppet.debug "%s class already evaluated" % @type return nil end diff --git a/lib/puppet/parser/collector.rb b/lib/puppet/parser/collector.rb index 6c49c6d57..0846c40ab 100644 --- a/lib/puppet/parser/collector.rb +++ b/lib/puppet/parser/collector.rb @@ -81,7 +81,7 @@ class Puppet::Parser::Collector # If there are no more resources to find, delete this from the list # of collections. if @resources.empty? - @scope.collections.delete(self) + @scope.configuration.delete_collection(self) end return result @@ -94,7 +94,7 @@ class Puppet::Parser::Collector else method = :virtual? end - scope.resources.find_all do |resource| + scope.configuration.resources.find_all do |resource| resource.type == @type and resource.send(method) and match?(resource) end end @@ -117,13 +117,6 @@ class Puppet::Parser::Collector return objects end end - -# if objects and ! objects.empty? -# objects.each { |r| r.virtual = false } -# return objects -# else -# return false -# end end def initialize(scope, type, equery, vquery, form) diff --git a/lib/puppet/parser/configuration.rb b/lib/puppet/parser/configuration.rb new file mode 100644 index 000000000..44fb8c476 --- /dev/null +++ b/lib/puppet/parser/configuration.rb @@ -0,0 +1,555 @@ +# 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 + + 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/functions.rb b/lib/puppet/parser/functions.rb index 946501154..ad58c7040 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -109,7 +109,8 @@ module Functions # Include the specified classes newfunction(:include, :doc => "Evaluate one or more classes.") do |vals| - klasses = evalclasses(*vals) + vals = [vals] unless vals.is_a?(Array) + klasses = configuration.evaluate_classes(vals) missing = vals.find_all do |klass| ! klasses.include?(klass) @@ -144,7 +145,7 @@ module Functions tells you whether the current container is tagged with the specified tags. The tags are ANDed, so that all of the specified tags must be included for the function to return true.") do |vals| - classlist = self.classlist + classlist = configuration.classlist retval = true vals.each do |val| @@ -234,7 +235,7 @@ module Functions vals = [vals] unless vals.is_a?(Array) coll.resources = vals - newcollection(coll) + configuration.add_collection(coll) end newfunction(:search, :doc => "Add another namespace for this class to search. diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 3ba9c0c7a..e37ef5efe 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -3,274 +3,21 @@ require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' +require 'puppet/parser/configuration' require 'puppet/parser/scope' -# The interpreter's job is to convert from a parsed file to the configuration -# for a given client. It really doesn't do any work on its own, it just collects -# and calls out to other objects. +# The interpreter is a very simple entry-point class that +# manages the existence of the parser (e.g., replacing it +# when files are reparsed). You can feed it a node and +# get the node's configuration back. class Puppet::Parser::Interpreter - class NodeDef - include Puppet::Util::MethodHelper - attr_accessor :name, :classes, :parameters, :source - - def evaluate(options) - begin - parameters.each do |param, value| - # Don't try to override facts with these parameters - options[:scope].setvar(param, value) unless options[:scope].lookupvar(param, false) != :undefined - end - - # Also, set the 'nodename', since it might not be obvious how the node was looked up - options[:scope].setvar("nodename", @name) unless options[:scope].lookupvar(@nodename, false) != :undefined - rescue => detail - raise Puppet::ParseError, "Could not set parameters for %s: %s" % [name, detail] - end - - # Then evaluate the classes. - begin - options[:scope].function_include(classes.find_all { |c| options[:scope].findclass(c) }) - rescue => detail - puts detail.backtrace - raise Puppet::ParseError, "Could not evaluate classes for %s: %s" % [name, detail] - end - end - - def initialize(args) - set_options(args) - - raise Puppet::DevError, "NodeDefs require names" unless self.name - - if self.classes.is_a?(String) - @classes = [@classes] - else - @classes ||= [] - end - @parameters ||= {} - end - - def safeevaluate(args) - evaluate(args) - end - end - include Puppet::Util attr_accessor :usenodes - - class << self - attr_writer :ldap - end - - # just shorten the constant path a bit, using what amounts to an alias - AST = Puppet::Parser::AST + attr_reader :parser include Puppet::Util::Errors - # Create an ldap connection. This is a class method so others can call - # it and use the same variables and such. - def self.ldap - unless defined? @ldap and @ldap - if Puppet[:ldapssl] - @ldap = LDAP::SSLConn.new(Puppet[:ldapserver], Puppet[:ldapport]) - elsif Puppet[:ldaptls] - @ldap = LDAP::SSLConn.new( - Puppet[:ldapserver], Puppet[:ldapport], true - ) - else - @ldap = LDAP::Conn.new(Puppet[:ldapserver], Puppet[:ldapport]) - end - @ldap.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) - @ldap.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON) - @ldap.simple_bind(Puppet[:ldapuser], Puppet[:ldappassword]) - end - - return @ldap - end - - # Make sure we don't have any remaining collections that specifically - # look for resources, because we want to consider those to be - # parse errors. - def check_resource_collections(scope) - remaining = [] - scope.collections.each do |coll| - if r = coll.resources - if r.is_a?(Array) - remaining += r - else - remaining << r - end - end - end - unless remaining.empty? - raise Puppet::ParseError, "Failed to find virtual resources %s" % - remaining.join(', ') - end - end - - # Iteratively evaluate all of the objects. This finds all of the objects - # that represent definitions and evaluates the definitions appropriately. - # It also adds defaults and overrides as appropriate. - def evaliterate(scope) - count = 0 - loop do - count += 1 - done = true - # First perform collections, so we can collect defined types. - if coll = scope.collections and ! coll.empty? - exceptwrap do - coll.each do |c| - # Only keep the loop going if we actually successfully - # collected something. - if o = c.evaluate - done = false - end - end - end - end - - # Then evaluate any defined types. - if ary = scope.unevaluated - ary.each do |resource| - resource.evaluate - end - # If we evaluated, then loop through again. - done = false - end - break if done - - if count > 1000 - raise Puppet::ParseError, "Got 1000 class levels, which is unsupported" - end - end - end - - # Evaluate a specific node. - def evalnode(client, scope, facts) - return unless self.usenodes - - unless client - raise Puppet::Error, - "Cannot evaluate nodes with a nil client" - end - names = [client] - - # Make sure both the fqdn and the short name of the - # host can be used in the manifest - if client =~ /\./ - names << client.sub(/\..+/,'') - else - names << "#{client}.#{facts['domain']}" - end - - if names.empty? - raise Puppet::Error, - "Cannot evaluate nodes with a nil client" - end - - # Look up our node object. - if nodeclass = nodesearch(*names) - nodeclass.safeevaluate :scope => scope - else - raise Puppet::Error, "Could not find %s with names %s" % - [client, names.join(", ")] - end - end - - # Evaluate all of the code we can find that's related to our client. - def evaluate(client, facts) - - scope = Puppet::Parser::Scope.new(:interp => self) # no parent scope - scope.name = "top" - scope.type = "main" - - scope.host = client || facts["hostname"] || Facter.value(:hostname) - - classes = @classes.dup - - # Okay, first things first. Set our facts. - scope.setfacts(facts) - - # Everyone will always evaluate the top-level class, if there is one. - if klass = findclass("", "") - # Set the source, so objects can tell where they were defined. - scope.source = klass - klass.safeevaluate :scope => scope, :nosubscope => true - end - - # Next evaluate the node. We pass the facts so they can be used - # when building the list of names for which to search. - evalnode(client, scope, facts) - - # If we were passed any classes, evaluate those. - if classes - classes.each do |klass| - if klassobj = findclass("", klass) - klassobj.safeevaluate :scope => scope - end - end - end - - # That was the first pass evaluation. Now iteratively evaluate - # until we've gotten rid of all of everything or thrown an error. - evaliterate(scope) - - # Now make sure we fail if there's anything left to do - failonleftovers(scope) - - # Now finish everything. This recursively calls finish on the - # contained scopes and resources. - scope.finish - - # Store everything. We need to do this before translation, because - # it operates on resources, not transobjects. - if Puppet[:storeconfigs] - args = { - :resources => scope.resources, - :name => scope.host, - :facts => facts - } - unless scope.classlist.empty? - args[:classes] = scope.classlist - end - - storeconfigs(args) - end - - # Now, finally, convert our scope tree + resources into a tree of - # buckets and objects. - objects = scope.translate - - # Add the class list - unless scope.classlist.empty? - objects.classes = scope.classlist - end - - return objects - end - - # Fail if there any overrides left to perform. - def failonleftovers(scope) - overrides = scope.overrides - unless overrides.empty? - fail Puppet::ParseError, - "Could not find object(s) %s" % overrides.collect { |o| - o.ref - }.join(", ") - end - - # Now check that there aren't any extra resource collections. - check_resource_collections(scope) - end - - # Create proxy methods, so the scopes can call the interpreter, since - # they don't have access to the parser. - def findclass(namespace, name) - @parser.findclass(namespace, name) - end - def finddefine(namespace, name) - @parser.finddefine(namespace, name) - end - # create our interpreter def initialize(hash) if @code = hash[:Code] @@ -285,33 +32,13 @@ class Puppet::Parser::Interpreter @usenodes = true end - - if Puppet[:ldapnodes] - # Nodes in the file override nodes in ldap. - @nodesource = :ldap - elsif Puppet[:external_nodes] != "none" - @nodesource = :external - else - # By default, we only search for parsed nodes. - @nodesource = :code - end + # By default, we only search for parsed nodes. + @nodesource = :code @setup = false - # Set it to either the value or nil. This is currently only used - # by the cfengine module. - @classes = hash[:Classes] || [] - @local = hash[:Local] || false - if hash.include?(:ForkSave) - @forksave = hash[:ForkSave] - else - # This is just too dangerous right now. Sorry, it's going - # to have to be slow. - @forksave = false - end - # The class won't always be defined during testing. if Puppet[:storeconfigs] if Puppet.features.rails? @@ -327,270 +54,16 @@ class Puppet::Parser::Interpreter parsefiles end - # Find the ldap node, return the class list and parent node specially, - # and everything else in a parameter hash. - def ldapsearch(node) - unless defined? @ldap and @ldap - setup_ldap() - unless @ldap - Puppet.info "Skipping ldap source; no ldap connection" - return nil - end - end - - filter = Puppet[:ldapstring] - classattrs = Puppet[:ldapclassattrs].split("\s*,\s*") - if Puppet[:ldapattrs] == "all" - # A nil value here causes all attributes to be returned. - search_attrs = nil - else - search_attrs = classattrs + Puppet[:ldapattrs].split("\s*,\s*") - end - pattr = nil - if pattr = Puppet[:ldapparentattr] - if pattr == "" - pattr = nil - else - search_attrs << pattr unless search_attrs.nil? - end - end - - if filter =~ /%s/ - filter = filter.gsub(/%s/, node) - end - - parent = nil - classes = [] - parameters = nil - - found = false - count = 0 - - begin - # We're always doing a sub here; oh well. - @ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) do |entry| - found = true - if pattr - if values = entry.vals(pattr) - if values.length > 1 - raise Puppet::Error, - "Node %s has more than one parent: %s" % - [node, values.inspect] - end - unless values.empty? - parent = values.shift - end - end - end - - classattrs.each { |attr| - if values = entry.vals(attr) - values.each do |v| classes << v end - end - } - - parameters = entry.to_hash.inject({}) do |hash, ary| - if ary[1].length == 1 - hash[ary[0]] = ary[1].shift - else - hash[ary[0]] = ary[1] - end - hash - end - end - rescue => detail - if count == 0 - # Try reconnecting to ldap - @ldap = nil - setup_ldap() - retry - else - raise Puppet::Error, "LDAP Search failed: %s" % detail - end - end - - classes.flatten! - - if classes.empty? - classes = nil - end - - if parent or classes or parameters - return parent, classes, parameters - else - return nil - end - end - - # Pass these methods through to the parser. - [:newclass, :newdefine, :newnode].each do |name| - define_method(name) do |*args| - @parser.send(name, *args) - end - end - - # Add a new file to be checked when we're checking to see if we should be - # reparsed. - def newfile(*files) - files.each do |file| - unless file.is_a? Puppet::Util::LoadedFile - file = Puppet::Util::LoadedFile.new(file) - end - @files << file - end - end - - # Search for our node in the various locations. - def nodesearch(*nodes) - nodes = nodes.collect { |n| n.to_s.downcase } - - method = "nodesearch_%s" % @nodesource - # Do an inverse sort on the length, so the longest match always - # wins - nodes.sort { |a,b| b.length <=> a.length }.each do |node| - node = node.to_s if node.is_a?(Symbol) - if obj = self.send(method, node) - if obj.is_a?(AST::Node) - nsource = obj.file - else - nsource = obj.source - end - Puppet.info "Found %s in %s" % [node, nsource] - return obj - end - end - - # If they made it this far, we haven't found anything, so look for a - # default node. - unless nodes.include?("default") - if defobj = self.nodesearch("default") - Puppet.notice "Using default node for %s" % [nodes[0]] - return defobj - end - end - - return nil - end - - # See if our node was defined in the code. - def nodesearch_code(name) - @parser.nodes[name] - end - - # Look for external node definitions. - def nodesearch_external(name) - return nil unless Puppet[:external_nodes] != "none" - - # This is a very cheap way to do this, since it will break on - # commands that have spaces in the arguments. But it's good - # enough for most cases. - external_node_command = Puppet[:external_nodes].split - external_node_command << name - begin - output = Puppet::Util.execute(external_node_command) - rescue Puppet::ExecutionFailure => detail - if $?.exitstatus == 1 - return nil - else - Puppet.err "Could not retrieve external node information for %s: %s" % [name, detail] - end - return nil - end - - if output =~ /\A\s*\Z/ # all whitespace - Puppet.debug "Empty response for %s from external node source" % name - return nil - end - - begin - result = YAML.load(output).inject({}) { |hash, data| hash[symbolize(data[0])] = data[1]; hash } - rescue => detail - raise Puppet::Error, "Could not load external node results for %s: %s" % [name, detail] - end - - node_args = {:source => "external node source", :name => name} - set = false - [:parameters, :classes].each do |param| - if value = result[param] - node_args[param] = value - set = true - end - end - - if set - return NodeDef.new(node_args) - else - return nil - end - end - - # Look for our node in ldap. - def nodesearch_ldap(node) - unless ary = ldapsearch(node) - return nil - end - parent, classes, parameters = ary - - while parent - parent, tmpclasses, tmpparams = ldapsearch(parent) - classes += tmpclasses if tmpclasses - tmpparams.each do |param, value| - # Specifically test for whether it's set, so false values are handled - # correctly. - parameters[param] = value unless parameters.include?(param) - end - end - - return NodeDef.new(:name => node, :classes => classes, :source => "ldap", :parameters => parameters) - end - def parsedate parsefiles() @parsedate end # evaluate our whole tree - def run(client, facts) - # We have to leave this for after initialization because there - # seems to be a problem keeping ldap open after a fork. - unless @setup - method = "setup_%s" % @nodesource.to_s - if respond_to? method - exceptwrap :type => Puppet::Error, - :message => "Could not set up node source %s" % @nodesource do - self.send(method) - end - end - end + def compile(node) parsefiles() - # Evaluate all of the appropriate code. - objects = evaluate(client, facts) - - # And return it all. - return objects - end - - # Connect to the LDAP Server - def setup_ldap - self.class.ldap = nil - unless Puppet.features.ldap? - Puppet.notice( - "Could not set up LDAP Connection: Missing ruby/ldap libraries" - ) - @ldap = nil - return - end - - begin - @ldap = self.class.ldap() - rescue => detail - raise Puppet::Error, "Could not connect to LDAP: %s" % detail - end - end - - def scope - return @scope + return Puppet::Parser::Configuration.new(node, @parser).compile end private @@ -667,47 +140,6 @@ class Puppet::Parser::Interpreter Puppet.err "Could not parse; using old configuration: %s" % detail end end - - # Store the configs into the database. - def storeconfigs(hash) - unless Puppet.features.rails? - raise Puppet::Error, - "storeconfigs is enabled but rails is unavailable" - end - - unless ActiveRecord::Base.connected? - Puppet::Rails.connect - end - - # Fork the storage, since we don't need the client waiting - # on that. How do I avoid this duplication? - if @forksave - fork { - # We store all of the objects, even the collectable ones - benchmark(:info, "Stored configuration for #{hash[:name]}") do - # Try to batch things a bit, by putting them into - # a transaction - Puppet::Rails::Host.transaction do - Puppet::Rails::Host.store(hash) - end - end - } - else - begin - # We store all of the objects, even the collectable ones - benchmark(:info, "Stored configuration for #{hash[:name]}") do - Puppet::Rails::Host.transaction do - Puppet::Rails::Host.store(hash) - end - end - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - Puppet.err "Could not store configs: %s" % detail.to_s - end - end - end end # $Id$ diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 728f75a69..967508e56 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,3 +1,5 @@ +# 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' @@ -442,6 +444,17 @@ class Puppet::Parser::Parser 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. + 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/parser/resource.rb b/lib/puppet/parser/resource.rb index 18ec15ac0..f946c1328 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -10,9 +10,9 @@ class Puppet::Parser::Resource include Puppet::Util::Logging attr_accessor :source, :line, :file, :scope, :rails_id - attr_accessor :virtual, :override, :params, :translated + attr_accessor :virtual, :override, :translated - attr_reader :exported + attr_reader :exported, :evaluated, :params attr_writer :tags @@ -24,7 +24,7 @@ class Puppet::Parser::Resource end # Set up some boolean test methods - [:exported, :translated, :override].each do |method| + [:exported, :translated, :override, :virtual, :evaluated].each do |method| newmeth = (method.to_s + "?").intern define_method(newmeth) do self.send(method) @@ -43,44 +43,6 @@ class Puppet::Parser::Resource end end - # Add default values from our definition. - def adddefaults - defaults = scope.lookupdefaults(self.type) - - defaults.each do |name, param| - unless @params.include?(param.name) - self.debug "Adding default for %s" % param.name - - @params[param.name] = param - end - end - end - - # Add any metaparams defined in our scope. This actually adds any metaparams - # from any parent scope, and there's currently no way to turn that off. - def addmetaparams - Puppet::Type.eachmetaparam do |name| - next if self[name] - if val = scope.lookupvar(name.to_s, false) - unless val == :undefined - set Param.new(:name => name, :value => val, - :source => scope.source) - end - end - end - end - - # Add any overrides for this object. - def addoverrides - overrides = scope.lookupoverrides(self) - - overrides.each do |over| - self.merge(over) - end - - overrides.clear - end - def builtin=(bool) @ref.builtin = bool end @@ -89,12 +51,11 @@ class Puppet::Parser::Resource def evaluate if klass = @ref.definedtype finish() - scope.deleteresource(self) - return klass.evaluate(:scope => scope, + scope.configuration.delete_resource(self) + return klass.evaluate_resource(:scope => scope, :type => self.type, :title => self.title, :arguments => self.to_hash, - :scope => self.scope, :virtual => self.virtual, :exported => self.exported ) @@ -107,6 +68,8 @@ class Puppet::Parser::Resource @evaluated = true end + # Mark this resource as both exported and virtual, + # or remove the exported mark. def exported=(value) if value @virtual = true @@ -116,63 +79,73 @@ class Puppet::Parser::Resource end end - def evaluated? - if defined? @evaluated and @evaluated - true - else - false - end - end - # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish - addoverrides() - adddefaults() - addmetaparams() + add_overrides() + add_defaults() + add_metaparams() + validate() end def initialize(options) - options = symbolize_options(options) - - # Collect the options necessary to make the reference. - refopts = [:type, :title].inject({}) do |hash, param| - hash[param] = options[param] - options.delete(param) - hash + # Set all of the options we can. + options.each do |option, value| + if respond_to?(option.to_s + "=") + send(option.to_s + "=", value) + options.delete(option) + end end - @params = {} - tmpparams = nil - if tmpparams = options[:params] - options.delete(:params) + [:scope, :source].each do |attribute| + unless self.send(attribute) + raise ArgumentError, "Resources require a %s" % attribute + end end - # Now set the rest of the options. - set_options(options) + options = symbolize_options(options) - @ref = Reference.new(refopts) + # Set up our reference. + if type = options[:type] and title = options[:title] + options.delete(:type) + options.delete(:title) + else + raise ArgumentError, "Resources require a type and title" + end - requiredopts(:scope, :source) + @ref = Reference.new(:type => type, :title => title, :scope => self.scope) - @ref.scope = self.scope + @params = {} - if tmpparams - tmpparams.each do |param| - # We use the method here, because it does type-checking. - set(param) + # Define all of the parameters + if params = options[:params] + options.delete(:params) + params.each do |param| + set_parameter(param) end end + + # Throw an exception if we've got any arguments left to set. + unless options.empty? + raise ArgumentError, "Resources do not accept %s" % options.keys.collect { |k| k.to_s }.join(", ") + end end - # Merge an override resource in. + # Merge an override resource in. This will throw exceptions if + # any overrides aren't allowed. def merge(resource) + # Test the resource scope, to make sure the resource is even allowed + # to override. + unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source) + raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file) + end # Some of these might fail, but they'll fail in the way we want. resource.params.each do |name, param| - set(param) + override_parameter(param) end end + # Modify this resource in the Rails database. Poor design, yo. def modify_rails(db_resource) args = rails_args args.each do |param, value| @@ -223,23 +196,6 @@ class Puppet::Parser::Resource @@paramcheck end - # Verify that all passed parameters are valid. This throws an error if - # there's a problem, so we don't have to worry about the return value. - def paramcheck(param) - param = param.to_s - # Now make sure it's a valid argument to our class. These checks - # are organized in order of commonhood -- most types, it's a valid - # argument and paramcheck is enabled. - if @ref.typeclass.validattr?(param) - true - elsif %w{name title}.include?(param) # always allow these - true - elsif paramcheck? - self.fail Puppet::ParseError, "Invalid parameter '%s' for type '%s'" % - [param.inspect, @ref.type] - end - end - # A temporary occasion, until I get paths in the scopes figured out. def path to_s @@ -250,59 +206,6 @@ class Puppet::Parser::Resource @ref.to_s end - # You have to pass a Resource::Param to this. - def set(param, value = nil, source = nil) - if value and source - param = Puppet::Parser::Resource::Param.new( - :name => param, :value => value, :source => source - ) - elsif ! param.is_a?(Puppet::Parser::Resource::Param) - raise ArgumentError, "Must pass a parameter or all necessary values" - end - # Because definitions are now parse-time, I can paramcheck immediately. - paramcheck(param.name) - - if current = @params[param.name] - # This is where we'd ignore any equivalent values if we wanted to, - # but that would introduce a lot of really bad ordering issues. - if param.source.child_of?(current.source) - if param.add - # Merge with previous value. - param.value = [ current.value, param.value ].flatten - end - - # Replace it, keeping all of its info. - @params[param.name] = param - else - if Puppet[:trace] - puts caller - end - msg = "Parameter '%s' is already set on %s" % - [param.name, self.to_s] - if current.source.to_s != "" - msg += " by %s" % current.source - end - if current.file or current.line - fields = [] - fields << current.file if current.file - fields << current.line.to_s if current.line - msg += " at %s" % fields.join(":") - end - msg += "; cannot redefine" - error = Puppet::ParseError.new(msg) - error.file = param.file if param.file - error.line = param.line if param.line - raise error - end - else - if self.source == param.source or param.source.child_of?(self.source) - @params[param.name] = param - else - fail Puppet::ParseError, "Only subclasses can set parameters" - end - end - end - def tags unless defined? @tags @tags = scope.tags @@ -384,12 +287,102 @@ class Puppet::Parser::Resource return obj end - - def virtual? - self.virtual - end private + + # Add default values from our definition. + def add_defaults + scope.lookupdefaults(self.type).each do |name, param| + unless @params.include?(name) + self.debug "Adding default for %s" % name + + @params[name] = param + end + end + end + + # Add any metaparams defined in our scope. This actually adds any metaparams + # from any parent scope, and there's currently no way to turn that off. + def add_metaparams + Puppet::Type.eachmetaparam do |name| + # Skip metaparams that we already have defined. + next if @params[name] + if val = scope.lookupvar(name.to_s, false) + unless val == :undefined + set_parameter(name, val) + end + end + end + end + + # Add any overrides for this object. + def add_overrides + if overrides = scope.configuration.resource_overrides(self) + overrides.each do |over| + self.merge(over) + end + + # Remove the overrides, so that the configuration knows there + # are none left. + overrides.clear + end + end + + # Accept a parameter from an override. + def override_parameter(param) + # This can happen if the override is defining a new parameter, rather + # than replacing an existing one. + unless current = @params[param.name] + @params[param.name] = param + return + end + + # The parameter is already set. See if they're allowed to override it. + if param.source.child_of?(current.source) + if param.add + # Merge with previous value. + param.value = [ current.value, param.value ].flatten + end + + # Replace it, keeping all of its info. + @params[param.name] = param + else + if Puppet[:trace] + puts caller + end + msg = "Parameter '%s' is already set on %s" % + [param.name, self.to_s] + if current.source.to_s != "" + msg += " by %s" % current.source + end + if current.file or current.line + fields = [] + fields << current.file if current.file + fields << current.line.to_s if current.line + msg += " at %s" % fields.join(":") + end + msg += "; cannot redefine" + raise Puppet::ParseError.new(msg, param.line, param.file) + end + end + + # Verify that all passed parameters are valid. This throws an error if + # there's a problem, so we don't have to worry about the return value. + def paramcheck(param) + param = param.to_s + # Now make sure it's a valid argument to our class. These checks + # are organized in order of commonhood -- most types, it's a valid + # argument and paramcheck is enabled. + if @ref.typeclass.validattr?(param) + true + elsif %w{name title}.include?(param) # always allow these + true + elsif paramcheck? + self.fail Puppet::ParseError, "Invalid parameter '%s' for type '%s'" % + [param, @ref.type] + end + end + def rails_args return [:type, :title, :line, :exported].inject({}) do |hash, param| # 'type' isn't a valid column name, so we have to use another name. @@ -400,6 +393,26 @@ class Puppet::Parser::Resource hash end end -end -# $Id$ + # Define a parameter in our resource. + def set_parameter(param, value = nil) + if value + param = Puppet::Parser::Resource::Param.new( + :name => param, :value => value, :source => self.source + ) + elsif ! param.is_a?(Puppet::Parser::Resource::Param) + raise ArgumentError, "Must pass a parameter or all necessary values" + end + + # And store it in our parameter hash. + @params[param.name] = param + end + + # Make sure the resource's parameters are all valid for the type. + def validate + @params.each do |name, param| + # Make sure it's a valid parameter. + paramcheck(name) + end + end +end diff --git a/lib/puppet/parser/resource/reference.rb b/lib/puppet/parser/resource/reference.rb index 19d179660..b19dd2258 100644 --- a/lib/puppet/parser/resource/reference.rb +++ b/lib/puppet/parser/resource/reference.rb @@ -67,5 +67,3 @@ class Puppet::Parser::Resource::Reference @typeclass end end - -# $Id$ diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 6feeefc46..9e6739c53 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -15,36 +15,18 @@ class Puppet::Parser::Scope 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 + attr_accessor :parent, :level, :parser, :source + attr_accessor :name, :type, :base, :keyword + attr_accessor :top, :translated, :exported, :virtual, :configuration - # 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 + # Proxy accessors + def host + @configuration.node.name 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 + def interpreter + @configuration.interpreter 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) @@ -74,96 +56,9 @@ class Puppet::Parser::Scope 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. + # Retrieve a given class scope from the configuration. 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 + configuration.class_scope(klass) end # Are we the top scope? @@ -171,40 +66,13 @@ class Puppet::Parser::Scope @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) + if r = parser.findclass(namespace, name) return r end end @@ -213,7 +81,7 @@ class Puppet::Parser::Scope def finddefine(name) @namespaces.each do |namespace| - if r = interp.finddefine(namespace, name) + if r = parser.finddefine(namespace, name) return r end end @@ -221,28 +89,11 @@ class Puppet::Parser::Scope end def findresource(string, name = nil) - if name - string = "%s[%s]" % [string.capitalize, name] - end - - @definedtable[string] - end - - # Recursively complete the whole tree, in preparation for - # translation or storage. - def finish - self.each do |obj| - obj.finish - end + configuration.findresource(string, name) end - # Initialize our new scope. Defaults to having no parent and to - # being declarative. + # Initialize our new scope. Defaults to having no parent. def initialize(hash = {}) - @parent = nil - @type = nil - @name = nil - @finished = false if hash.include?(:namespace) if n = hash[:namespace] @namespaces = [n] @@ -262,94 +113,15 @@ class Puppet::Parser::Scope @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| + @defaults = 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. @@ -360,16 +132,16 @@ class Puppet::Parser::Scope values = {} # first collect the values from the parents - unless @parent.nil? - @parent.lookupdefaults(type).each { |var,value| + 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| + if @defaults.include?(type) + @defaults[type].each { |var,value| values[var] = value } end @@ -379,17 +151,6 @@ class Puppet::Parser::Scope 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) @@ -426,7 +187,7 @@ class Puppet::Parser::Scope return @symtable[name] end elsif self.parent - return @parent.lookupvar(name, usestring) + return parent.lookupvar(name, usestring) elsif usestring return "" else @@ -438,16 +199,9 @@ class Puppet::Parser::Scope @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) + # Create a new scope and set these options. + def newscope(options = {}) + configuration.newscope(self, options) end # Is this class for a node? This is used to make sure that @@ -458,10 +212,23 @@ class Puppet::Parser::Scope defined?(@nodescope) and @nodescope end - # Return the list of remaining overrides. - def overrides - #@overridetable.collect { |name, overs| overs }.flatten - @overridetable.values.flatten + # We probably shouldn't cache this value... But it's a lot faster + # than doing lots of queries. + def parent + unless defined?(@parent) + @parent = configuration.parent(self) + end + @parent + end + + # Return the list of scopes up to the top scope, ordered with our own first. + # This is used for looking up variables and defaults. + def scope_path + if parent + [self, parent.scope_path].flatten.compact + else + [self] + end end def resources @@ -472,66 +239,49 @@ class Puppet::Parser::Scope # 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 + def setclass(klass) + if klass.is_a?(AST::HostClass) + unless name = klass.classname raise Puppet::DevError, "Got a %s with no fully qualified name" % - obj.class + klass.class end - @classtable[obj.classname] = self + @configuration.class_set(name, self) else - raise Puppet::DevError, "Invalid class %s" % obj.inspect + raise Puppet::DevError, "Invalid class %s" % klass.inspect end - if obj.is_a?(AST::Node) + if klass.is_a?(AST::Node) @nodescope = true end nil end - # Set all of our facts in the top-level scope. - def setfacts(facts) - facts.each { |var, value| - self.setvar(var, value) - } - end - # Add a new object to our object table and the global list, and do any necessary # checks. - def setresource(obj) - self.chkobjectclosure(obj) - - @children << obj + def setresource(resource) + @configuration.store_resource(self, resource) # Mark the resource as virtual or exported, as necessary. if self.exported? - obj.exported = true + resource.exported = true elsif self.virtual? - obj.virtual = true + resource.virtual = true end - # The global table - @definedtable[obj.ref] = obj - - return obj + return resource end # Override a parameter in an existing object. If the object does not yet # exist, then cache the override in a global table, so it can be flushed # at the end. def setoverride(resource) - resource.override = true - if obj = @definedtable[resource.ref] - obj.merge(resource) - else - @overridetable[resource.ref] << resource - end + @configuration.store_override(resource) 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] + table = @defaults[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) @@ -539,17 +289,8 @@ class Puppet::Parser::Scope 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 + if table.include?(param.name) + raise Puppet::ParseError.new("Default already defined for %s { %s }; cannot redefine" % [type, param.name], param.line, param.file) end table[param.name] = param } @@ -557,23 +298,19 @@ class Puppet::Parser::Scope # 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). + # to be reassigned. 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] + error = Puppet::ParseError.new("Cannot reassign variable %s" % name) + if file + error.file = file + end + if line + error.line = line end + raise error end @symtable[name] = value end @@ -665,9 +402,9 @@ class Puppet::Parser::Scope 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 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 @@ -689,19 +426,9 @@ class Puppet::Parser::Scope 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 + # Convert our resource to a TransBucket. + def to_trans + bucket = Puppet::TransBucket.new([]) case self.type when "": bucket.type = "main" @@ -722,19 +449,6 @@ class Puppet::Parser::Scope 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 diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb index 3b8cc3a3a..9c3c6c0d0 100644 --- a/lib/puppet/parser/templatewrapper.rb +++ b/lib/puppet/parser/templatewrapper.rb @@ -15,8 +15,8 @@ class Puppet::Parser::TemplateWrapper end # We'll only ever not have an interpreter in testing, but, eh. - if @scope.interp - @scope.interp.newfile(@file) + if @scope.parser + @scope.parser.watch_file(@file) end end diff --git a/lib/puppet/rails/host.rb b/lib/puppet/rails/host.rb index ca1e10c93..b7ca4c2e4 100644 --- a/lib/puppet/rails/host.rb +++ b/lib/puppet/rails/host.rb @@ -28,7 +28,7 @@ class Puppet::Rails::Host < ActiveRecord::Base end # Store our host in the database. - def self.store(hash) + def self.store(node, resources) unless name = hash[:name] raise ArgumentError, "You must specify the hostname for storage" end @@ -40,23 +40,19 @@ class Puppet::Rails::Host < ActiveRecord::Base #unless host = find_by_name(name) seconds = Benchmark.realtime { unless host = find_by_name(name) - host = new(:name => name) + host = new(:name => node.name) end } Puppet.notice("Searched for host in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) - if ip = hash[:facts]["ipaddress"] + if ip = node.parameters["ipaddress"] host.ip = ip end # Store the facts into the database. - host.setfacts(hash[:facts]) - - unless hash[:resources] - raise ArgumentError, "You must pass resources" - end + host.setfacts node.parameters seconds = Benchmark.realtime { - host.setresources(hash[:resources]) + host.setresources(resources) } Puppet.notice("Handled resources in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) diff --git a/lib/puppet/rails/resource.rb b/lib/puppet/rails/resource.rb index 19aeb9205..785c63419 100644 --- a/lib/puppet/rails/resource.rb +++ b/lib/puppet/rails/resource.rb @@ -104,16 +104,16 @@ class Puppet::Rails::Resource < ActiveRecord::Base end hash[:scope] = scope hash[:source] = scope.source - obj = Puppet::Parser::Resource.new(hash) - + hash[:params] = [] names = [] self.param_names.each do |pname| # We can get the same name multiple times because of how the # db layout works. next if names.include?(pname.name) names << pname.name - obj.set(pname.to_resourceparam(self, scope.source)) + hash[:params] << pname.to_resourceparam(self, scope.source) end + obj = Puppet::Parser::Resource.new(hash) # Store the ID, so we can check if we're re-collecting the same resource. obj.rails_id = self.id @@ -121,5 +121,3 @@ class Puppet::Rails::Resource < ActiveRecord::Base return obj end end - -# $Id$ diff --git a/lib/puppet/reference/node_sources.rb b/lib/puppet/reference/node_sources.rb new file mode 100644 index 000000000..33a4c539c --- /dev/null +++ b/lib/puppet/reference/node_sources.rb @@ -0,0 +1,7 @@ +noderef = Puppet::Util::Reference.newreference :node_source, :doc => "Sources of node configuration information" do + Puppet::Network::Handler.node.sourcedocs +end + +nodref.header = " +Nodes can be searched for in different locations. This document describes those different locations. +" diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index d1d14977c..5a10f5344 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -201,7 +201,7 @@ module Util raise Puppet::DevError, "Failed to provide level to :benchmark" end - unless object.respond_to? level + unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to %s" % level end |
