diff --git a/lib/puppet/network/handler/configuration.rb b/lib/puppet/network/handler/configuration.rb index 05c86f22e..2c72d3d2b 100644 --- a/lib/puppet/network/handler/configuration.rb +++ b/lib/puppet/network/handler/configuration.rb @@ -1,218 +1,220 @@ 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) # If we want to use the cert name as our key if Puppet[:node_name] == 'cert' and client key = client end # 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) configuration = compile(node) return translate(configuration) 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) if client and node = node_handler.details(client) update_node_check(node) return interpreter.configuration_version(node) else # Just return something that will always result in a recompile, because # this is local. return (Time.now + 1000).to_i end 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. str = "Compiled configuration for %s" % node.name if node.environment str += " in environment %s" % node.environment end 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 + unless local? + Puppet.err detail.to_s + end 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 elsif options[:Manifest] args[:Manifest] = options[: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).create 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 diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 93a4bc170..a4ea26572 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,98 +1,105 @@ require 'puppet' require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' require 'puppet/parser/compile' require 'puppet/parser/scope' # The interpreter is a very simple entry-point class that # manages the existence of the parser (e.g., replacing it # when files are reparsed). You can feed it a node and # get the node's configuration back. class Puppet::Parser::Interpreter include Puppet::Util attr_accessor :usenodes attr_accessor :code, :file include Puppet::Util::Errors # Determine the configuration version for a given node's environment. def configuration_version(node) parser(node.environment).version end # evaluate our whole tree def compile(node) return Puppet::Parser::Compile.new(node, parser(node.environment), :ast_nodes => usenodes?).compile end # create our interpreter def initialize(options = {}) if @code = options[:Code] elsif @file = options[:Manifest] end if options.include?(:UseNodes) @usenodes = options[:UseNodes] else @usenodes = true end # The class won't always be defined during testing. if Puppet[:storeconfigs] if Puppet.features.rails? Puppet::Rails.init else raise Puppet::Error, "Rails is missing; cannot store configurations" end end @parsers = {} end # Should we parse ast nodes? def usenodes? defined?(@usenodes) and @usenodes end private # Create a new parser object and pre-parse the configuration. def create_parser(environment) begin parser = Puppet::Parser::Parser.new(:environment => environment) if self.code parser.string = self.code elsif self.file parser.file = self.file else file = Puppet.config.value(:manifest, environment) parser.file = file end parser.parse return parser rescue => detail if Puppet[:trace] puts detail.backtrace end - Puppet.err "Could not parse for environment %s: %s" % [environment, detail] - return nil + msg = "Could not parse" + if environment and environment != "" + msg += " for environment %s" % environment + end + msg += ": %s" % detail + raise Puppet::Error, detail end end # Return the parser for a specific environment. def parser(environment) if ! @parsers[environment] or @parsers[environment].reparse? - if tmp = create_parser(environment) + # This will throw an exception if it does not succeed. We only + # want to get rid of the old parser if we successfully create a new + # one. + begin + tmp = create_parser(environment) @parsers[environment].clear if @parsers[environment] @parsers[environment] = tmp - end - unless @parsers[environment] - raise Puppet::Error, "Could not parse any configurations" + rescue + # Nothing, yo. end end @parsers[environment] end end diff --git a/spec/unit/node/configuration.rb b/spec/unit/node/configuration.rb index 90bc56460..4429fe3a3 100755 --- a/spec/unit/node/configuration.rb +++ b/spec/unit/node/configuration.rb @@ -1,125 +1,135 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Node::Configuration, " when compiling" do it "should accept tags" do config = Puppet::Node::Configuration.new("mynode") config.tag("one") config.tags.should == %w{one} end it "should accept multiple tags at once" do config = Puppet::Node::Configuration.new("mynode") config.tag("one", "two") config.tags.should == %w{one two} end it "should convert all tags to strings" do config = Puppet::Node::Configuration.new("mynode") config.tag("one", :two) config.tags.should == %w{one two} end it "should tag with both the qualified name and the split name" do config = Puppet::Node::Configuration.new("mynode") config.tag("one::two") config.tags.include?("one").should be_true config.tags.include?("one::two").should be_true end it "should accept classes" do config = Puppet::Node::Configuration.new("mynode") config.add_class("one") config.classes.should == %w{one} config.add_class("two", "three") config.classes.should == %w{one two three} end it "should tag itself with passed class names" do config = Puppet::Node::Configuration.new("mynode") config.add_class("one") config.tags.should == %w{one} end end describe Puppet::Node::Configuration, " when extracting" do it "should return extraction result as the method result" do config = Puppet::Node::Configuration.new("mynode") config.expects(:extraction_format).returns(:whatever) config.expects(:extract_to_whatever).returns(:result) config.extract.should == :result end end describe Puppet::Node::Configuration, " when extracting transobjects" do + def mkscope + @parser = Puppet::Parser::Parser.new :Code => "" + @node = Puppet::Node.new("mynode") + @compile = Puppet::Parser::Compile.new(@node, @parser) + + # XXX This is ridiculous. + @compile.send(:evaluate_main) + @scope = @compile.topscope + end + def mkresource(type, name) Puppet::Parser::Resource.new(:type => type, :title => name, :source => @source, :scope => @scope) end # This isn't really a spec-style test, but I don't know how better to do it. it "should transform the resource graph into a tree of TransBuckets and TransObjects" do config = Puppet::Node::Configuration.new("mynode") - @scope = mock 'scope' + @scope = mkscope @source = mock 'source' defined = mkresource("class", :main) builtin = mkresource("file", "/yay") config.add_edge!(defined, builtin) bucket = [] bucket.expects(:classes=).with(config.classes) defined.stubs(:builtin?).returns(false) defined.expects(:to_transbucket).returns(bucket) builtin.expects(:to_transobject).returns(:builtin) config.extract_to_transportable.should == [:builtin] end # Now try it with a more complicated graph -- a three tier graph, each tier it "should transform arbitrarily deep graphs into isomorphic trees" do config = Puppet::Node::Configuration.new("mynode") - @scope = mock 'scope' + @scope = mkscope @scope.stubs(:tags).returns([]) @source = mock 'source' # Create our scopes. top = mkresource "class", :main topbucket = [] topbucket.expects(:classes=).with([]) top.expects(:to_trans).returns(topbucket) topres = mkresource "file", "/top" topres.expects(:to_trans).returns(:topres) config.add_edge! top, topres middle = mkresource "class", "middle" middle.expects(:to_trans).returns([]) config.add_edge! top, middle midres = mkresource "file", "/mid" midres.expects(:to_trans).returns(:midres) config.add_edge! middle, midres bottom = mkresource "class", "bottom" bottom.expects(:to_trans).returns([]) config.add_edge! middle, bottom botres = mkresource "file", "/bot" botres.expects(:to_trans).returns(:botres) config.add_edge! bottom, botres toparray = config.extract_to_transportable # This is annoying; it should look like: # [[[:botres], :midres], :topres] # but we can't guarantee sort order. toparray.include?(:topres).should be_true midarray = toparray.find { |t| t.is_a?(Array) } midarray.include?(:midres).should be_true botarray = midarray.find { |t| t.is_a?(Array) } botarray.include?(:botres).should be_true end end diff --git a/spec/unit/parser/interpreter.rb b/spec/unit/parser/interpreter.rb index ebb7d4cbf..c0f9d54b3 100755 --- a/spec/unit/parser/interpreter.rb +++ b/spec/unit/parser/interpreter.rb @@ -1,192 +1,194 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser::Interpreter, " when initializing" do it "should default to neither code nor file" do interp = Puppet::Parser::Interpreter.new interp.code.should be_nil interp.file.should be_nil end it "should set the code to parse" do interp = Puppet::Parser::Interpreter.new :Code => :code interp.code.should equal(:code) end it "should set the file to parse" do interp = Puppet::Parser::Interpreter.new :Manifest => :file interp.file.should equal(:file) end it "should set code and ignore manifest when both are present" do interp = Puppet::Parser::Interpreter.new :Code => :code, :Manifest => :file interp.code.should equal(:code) interp.file.should be_nil end it "should default to usenodes" do interp = Puppet::Parser::Interpreter.new interp.usenodes?.should be_true end it "should allow disabling of usenodes" do interp = Puppet::Parser::Interpreter.new :UseNodes => false interp.usenodes?.should be_false end end describe Puppet::Parser::Interpreter, " when creating parser instances" do before do @interp = Puppet::Parser::Interpreter.new @parser = mock('parser') end it "should create a parser with code passed in at initialization time" do @interp.code = :some_code @parser.expects(:string=).with(:some_code) @parser.expects(:parse) Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) @interp.send(:create_parser, :myenv).object_id.should equal(@parser.object_id) end it "should create a parser with a file passed in at initialization time" do @interp.file = :a_file @parser.expects(:file=).with(:a_file) @parser.expects(:parse) Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) @interp.send(:create_parser, :myenv).should equal(@parser) end it "should create a parser with the main manifest when passed neither code nor file" do @parser.expects(:parse) @parser.expects(:file=).with(Puppet[:manifest]) Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) @interp.send(:create_parser, :myenv).should equal(@parser) end it "should return nothing when new parsers fail" do Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).raises(ArgumentError) - @interp.send(:create_parser, :myenv).should be_nil + proc { @interp.send(:create_parser, :myenv) }.should raise_error(Puppet::Error) end it "should create parsers with environment-appropriate manifests" do # Set our per-environment values. We can't just stub :value, because # it's called by too much of the rest of the code. text = "[env1]\nmanifest = /t/env1.pp\n[env2]\nmanifest = /t/env2.pp" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/whatever") Puppet.config.stubs(:read_file).with(file).returns(text) Puppet.config.parse(file) parser1 = mock 'parser1' Puppet::Parser::Parser.expects(:new).with(:environment => :env1).returns(parser1) parser1.expects(:file=).with("/t/env1.pp") + parser1.expects(:parse) @interp.send(:create_parser, :env1) parser2 = mock 'parser2' Puppet::Parser::Parser.expects(:new).with(:environment => :env2).returns(parser2) parser2.expects(:file=).with("/t/env2.pp") + parser2.expects(:parse) @interp.send(:create_parser, :env2) end end describe Puppet::Parser::Interpreter, " when managing parser instances" do before do @interp = Puppet::Parser::Interpreter.new @parser = mock('parser') end it "it should an exception when nothing is there and nil is returned" do @interp.expects(:create_parser).with(:myenv).returns(nil) - lambda { @interp.send(:parser, :myenv) }.should raise_error(Puppet::Error) + @interp.send(:parser, :myenv).should be_nil end it "should create and return a new parser and use the same parser when the parser does not need reparsing" do @interp.expects(:create_parser).with(:myenv).returns(@parser) @interp.send(:parser, :myenv).should equal(@parser) @parser.expects(:reparse?).returns(false) @interp.send(:parser, :myenv).should equal(@parser) end it "should create a new parser when reparse is true" do oldparser = mock('oldparser') newparser = mock('newparser') oldparser.expects(:reparse?).returns(true) oldparser.expects(:clear) @interp.expects(:create_parser).with(:myenv).returns(oldparser) @interp.send(:parser, :myenv).should equal(oldparser) @interp.expects(:create_parser).with(:myenv).returns(newparser) @interp.send(:parser, :myenv).should equal(newparser) end it "should keep the old parser if create_parser doesn't return anything." do # Get the first parser in the hash. @interp.expects(:create_parser).with(:myenv).returns(@parser) @interp.send(:parser, :myenv).should equal(@parser) # Have it indicate something has changed @parser.expects(:reparse?).returns(true) # But fail to create a new parser @interp.expects(:create_parser).with(:myenv).returns(nil) # And make sure we still get the old valid parser @interp.send(:parser, :myenv).should equal(@parser) end it "should use different parsers for different environments" do # get one for the first env @interp.expects(:create_parser).with(:first_env).returns(@parser) @interp.send(:parser, :first_env).should equal(@parser) other_parser = mock('otherparser') @interp.expects(:create_parser).with(:second_env).returns(other_parser) @interp.send(:parser, :second_env).should equal(other_parser) end end describe Puppet::Parser::Interpreter, " when compiling configurations" do before do @interp = Puppet::Parser::Interpreter.new end it "should create a configuration with the node, parser, and whether to use ast nodes" do node = mock('node') node.expects(:environment).returns(:myenv) compile = mock 'compile' compile.expects(:compile).returns(:config) parser = mock 'parser' @interp.expects(:parser).with(:myenv).returns(parser) @interp.expects(:usenodes?).returns(true) Puppet::Parser::Compile.expects(:new).with(node, parser, :ast_nodes => true).returns(compile) @interp.compile(node) # Now try it when usenodes is true @interp = Puppet::Parser::Interpreter.new :UseNodes => false node.expects(:environment).returns(:myenv) compile.expects(:compile).returns(:config) @interp.expects(:parser).with(:myenv).returns(parser) @interp.expects(:usenodes?).returns(false) Puppet::Parser::Compile.expects(:new).with(node, parser, :ast_nodes => false).returns(compile) @interp.compile(node).should equal(:config) end end describe Puppet::Parser::Interpreter, " when returning configuration version" do before do @interp = Puppet::Parser::Interpreter.new end it "should ask the appropriate parser for the configuration version" do node = mock 'node' node.expects(:environment).returns(:myenv) parser = mock 'parser' parser.expects(:version).returns(:myvers) @interp.expects(:parser).with(:myenv).returns(parser) @interp.configuration_version(node).should equal(:myvers) end end