diff --git a/lib/puppet/indirector/catalog/compiler.rb b/lib/puppet/indirector/catalog/compiler.rb index 352c9a312..09afedf9b 100644 --- a/lib/puppet/indirector/catalog/compiler.rb +++ b/lib/puppet/indirector/catalog/compiler.rb @@ -1,154 +1,154 @@ require 'puppet/node' require 'puppet/resource/catalog' require 'puppet/indirector/code' require 'yaml' class Puppet::Resource::Catalog::Compiler < Puppet::Indirector::Code desc "Puppet's catalog compilation interface, and its back-end is Puppet's compiler" include Puppet::Util attr_accessor :code def extract_facts_from_request(request) return unless text_facts = request.options[:facts] raise ArgumentError, "Facts but no fact format provided for #{request.name}" unless format = request.options[:facts_format] # If the facts were encoded as yaml, then the param reconstitution system # in Network::HTTP::Handler will automagically deserialize the value. if text_facts.is_a?(Puppet::Node::Facts) facts = text_facts else facts = Puppet::Node::Facts.convert_from(format, text_facts) end facts.add_timestamp Puppet::Node::Facts.indirection.save(facts) end # Compile a node's catalog. def find(request) extract_facts_from_request(request) node = node_from_request(request) if catalog = compile(node) return catalog else # This shouldn't actually happen; we should either return # a config or raise an exception. return nil end end # filter-out a catalog to remove exported resources def filter(catalog) return catalog.filter { |r| r.virtual? } if catalog.respond_to?(:filter) catalog end def initialize set_server_facts end # Is our compiler part of a network, or are we just local? def networked? Puppet.run_mode.master? 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.merge(@server_facts) end # Compile the actual catalog. def compile(node) str = "Compiled catalog for #{node.name}" str += " in environment #{node.environment}" if node.environment config = nil loglevel = networked? ? :notice : :none benchmark(loglevel, str) do begin config = Puppet::Parser::Compiler.compile(node) rescue Puppet::Error => detail Puppet.err(detail.to_s) if networked? raise end end config end # Turn our host name into a node object. - def find_node(name) + def find_node(name, *args) begin - return nil unless node = Puppet::Node.indirection.find(name) + return nil unless node = Puppet::Node.indirection.find(name, *args) rescue => detail message = "Failed when searching for node #{name}: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message end # Add any external data to the node. add_node_data(node) node end # Extract the node from the request, or use the request # to find the node. def node_from_request(request) if node = request.options[:use_node] return node end # We rely on our authorization system to determine whether the connected # node is allowed to compile the catalog's node referenced by key. # By default the REST authorization system makes sure only the connected node # can compile his catalog. # This allows for instance monitoring systems or puppet-load to check several # node's catalog with only one certificate and a modification to auth.conf # If no key is provided we can only compile the currently connected node. name = request.key || request.node - if node = find_node(name) + if node = find_node(name, :environment => request.environment) return node end raise ArgumentError, "Could not find node '#{name}'; cannot compile" 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 #{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 end diff --git a/lib/puppet/indirector/node/exec.rb b/lib/puppet/indirector/node/exec.rb index 6e065c6f3..ceee3fedd 100644 --- a/lib/puppet/indirector/node/exec.rb +++ b/lib/puppet/indirector/node/exec.rb @@ -1,48 +1,52 @@ require 'puppet/node' require 'puppet/indirector/exec' class Puppet::Node::Exec < Puppet::Indirector::Exec desc "Call an external program to get node information. See the [External Nodes](http://docs.puppetlabs.com/guides/external_nodes.html) page for more information." include Puppet::Util def command command = Puppet[:external_nodes] raise ArgumentError, "You must set the 'external_nodes' parameter to use the external node terminus" unless command != "none" command.split end # Look for external node definitions. def find(request) output = super or return nil # Translate the output to ruby. result = translate(request.key, output) + # Set the requested environment if it wasn't overridden + # If we don't do this it gets set to the local default + result[:environment] ||= request.environment.name + create_node(request.key, result) end private # Turn our outputted objects into a Puppet::Node instance. def create_node(name, result) node = Puppet::Node.new(name) set = false [:parameters, :classes, :environment].each do |param| if value = result[param] node.send(param.to_s + "=", value) set = true end end node.fact_merge node end # Translate the yaml string into Ruby objects. def translate(name, output) 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 #{name}: #{detail}" end end diff --git a/spec/integration/node_spec.rb b/spec/integration/node_spec.rb index 7efe524c4..4376f774f 100755 --- a/spec/integration/node_spec.rb +++ b/spec/integration/node_spec.rb @@ -1,92 +1,79 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/node' describe Puppet::Node do describe "when delegating indirection calls" do before do Puppet::Node.indirection.reset_terminus_class Puppet::Node.indirection.cache_class = nil @name = "me" @node = Puppet::Node.new(@name) end - it "should be able to use the exec terminus" do - Puppet::Node.indirection.stubs(:terminus_class).returns :exec - - # Load now so we can stub - terminus = Puppet::Node.indirection.terminus(:exec) - - terminus.expects(:query).with(@name).returns "myresults" - terminus.expects(:translate).with(@name, "myresults").returns "translated_results" - terminus.expects(:create_node).with(@name, "translated_results").returns @node - - Puppet::Node.indirection.find(@name).should equal(@node) - end - it "should be able to use the yaml terminus" do Puppet::Node.indirection.stubs(:terminus_class).returns :yaml # Load now, before we stub the exists? method. terminus = Puppet::Node.indirection.terminus(:yaml) terminus.expects(:path).with(@name).returns "/my/yaml/file" FileTest.expects(:exist?).with("/my/yaml/file").returns false Puppet::Node.indirection.find(@name).should be_nil end it "should have an ldap terminus" do Puppet::Node.indirection.terminus(:ldap).should_not be_nil end it "should be able to use the plain terminus" do Puppet::Node.indirection.stubs(:terminus_class).returns :plain # Load now, before we stub the exists? method. Puppet::Node.indirection.terminus(:plain) Puppet::Node.expects(:new).with(@name).returns @node Puppet::Node.indirection.find(@name).should equal(@node) end describe "and using the memory terminus" do before do @name = "me" @old_terminus = Puppet::Node.indirection.terminus_class @terminus = Puppet::Node.indirection.terminus(:memory) Puppet::Node.indirection.stubs(:terminus).returns @terminus @node = Puppet::Node.new(@name) end it "should find no nodes by default" do Puppet::Node.indirection.find(@name).should be_nil end it "should be able to find nodes that were previously saved" do Puppet::Node.indirection.save(@node) Puppet::Node.indirection.find(@name).should equal(@node) end it "should replace existing saved nodes when a new node with the same name is saved" do Puppet::Node.indirection.save(@node) two = Puppet::Node.new(@name) Puppet::Node.indirection.save(two) Puppet::Node.indirection.find(@name).should equal(two) end it "should be able to remove previously saved nodes" do Puppet::Node.indirection.save(@node) Puppet::Node.indirection.destroy(@node.name) Puppet::Node.indirection.find(@name).should be_nil end it "should fail when asked to destroy a node that does not exist" do proc { Puppet::Node.indirection.destroy(@node) }.should raise_error(ArgumentError) end end end end diff --git a/spec/unit/indirector/catalog/compiler_spec.rb b/spec/unit/indirector/catalog/compiler_spec.rb index 3c559e3bb..d0535cd82 100755 --- a/spec/unit/indirector/catalog/compiler_spec.rb +++ b/spec/unit/indirector/catalog/compiler_spec.rb @@ -1,261 +1,261 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/catalog/compiler' require 'puppet/rails' describe Puppet::Resource::Catalog::Compiler do before do require 'puppet/rails' Puppet::Rails.stubs(:init) Facter.stubs(:to_hash).returns({}) Facter.stubs(:value).returns(Facter::Util::Fact.new("something")) end describe "when initializing" do before do Puppet.expects(:version).returns(1) Facter.expects(:value).with('fqdn').returns("my.server.com") Facter.expects(:value).with('ipaddress').returns("my.ip.address") end it "should gather data about itself" do Puppet::Resource::Catalog::Compiler.new end it "should cache the server metadata and reuse it" do compiler = Puppet::Resource::Catalog::Compiler.new node1 = stub 'node1', :merge => nil node2 = stub 'node2', :merge => nil compiler.stubs(:compile) - Puppet::Node.indirection.stubs(:find).with('node1').returns(node1) - Puppet::Node.indirection.stubs(:find).with('node2').returns(node2) + Puppet::Node.indirection.stubs(:find).with('node1', anything).returns(node1) + Puppet::Node.indirection.stubs(:find).with('node2', anything).returns(node2) - compiler.find(stub('request', :key => 'node1', :node => 'node1', :options => {})) - compiler.find(stub('node2request', :key => 'node2', :node => 'node2', :options => {})) + compiler.find(Puppet::Indirector::Request.new(:catalog, :find, 'node1', :node => 'node1')) + compiler.find(Puppet::Indirector::Request.new(:catalog, :find, 'node2', :node => 'node2')) end it "should provide a method for determining if the catalog is networked" do compiler = Puppet::Resource::Catalog::Compiler.new compiler.should respond_to(:networked?) end end describe "when finding catalogs" do before do Facter.stubs(:value).returns("whatever") @compiler = Puppet::Resource::Catalog::Compiler.new @name = "me" @node = Puppet::Node.new @name @node.stubs(:merge) Puppet::Node.indirection.stubs(:find).returns @node - @request = stub 'request', :key => @name, :node => @name, :options => {} + @request = Puppet::Indirector::Request.new(:catalog, :find, @name, :node => @name) end it "should directly use provided nodes" do Puppet::Node.indirection.expects(:find).never @compiler.expects(:compile).with(@node) @request.stubs(:options).returns(:use_node => @node) @compiler.find(@request) end it "should use the authenticated node name if no request key is provided" do @request.stubs(:key).returns(nil) - Puppet::Node.indirection.expects(:find).with(@name).returns(@node) + Puppet::Node.indirection.expects(:find).with(@name, anything).returns(@node) @compiler.expects(:compile).with(@node) @compiler.find(@request) end it "should use the provided node name by default" do @request.expects(:key).returns "my_node" - Puppet::Node.indirection.expects(:find).with("my_node").returns @node + Puppet::Node.indirection.expects(:find).with("my_node", anything).returns @node @compiler.expects(:compile).with(@node) @compiler.find(@request) end it "should fail if no node is passed and none can be found" do - Puppet::Node.indirection.stubs(:find).with(@name).returns(nil) + Puppet::Node.indirection.stubs(:find).with(@name, anything).returns(nil) proc { @compiler.find(@request) }.should raise_error(ArgumentError) end it "should fail intelligently when searching for a node raises an exception" do - Puppet::Node.indirection.stubs(:find).with(@name).raises "eh" + Puppet::Node.indirection.stubs(:find).with(@name, anything).raises "eh" proc { @compiler.find(@request) }.should raise_error(Puppet::Error) end it "should pass the found node to the compiler for compiling" do - Puppet::Node.indirection.expects(:find).with(@name).returns(@node) + Puppet::Node.indirection.expects(:find).with(@name, anything).returns(@node) config = mock 'config' Puppet::Parser::Compiler.expects(:compile).with(@node) @compiler.find(@request) end it "should extract and save any facts from the request" do - Puppet::Node.indirection.expects(:find).with(@name).returns @node + Puppet::Node.indirection.expects(:find).with(@name, anything).returns @node @compiler.expects(:extract_facts_from_request).with(@request) Puppet::Parser::Compiler.stubs(:compile) @compiler.find(@request) end it "should return the results of compiling as the catalog" do Puppet::Node.indirection.stubs(:find).returns(@node) config = mock 'config' result = mock 'result' Puppet::Parser::Compiler.expects(:compile).returns result @compiler.find(@request).should equal(result) end it "should benchmark the compile process" do Puppet::Node.indirection.stubs(:find).returns(@node) @compiler.stubs(:networked?).returns(true) @compiler.expects(:benchmark).with do |level, message| level == :notice and message =~ /^Compiled catalog/ end Puppet::Parser::Compiler.stubs(:compile) @compiler.find(@request) end it "should log the benchmark result" do Puppet::Node.indirection.stubs(:find).returns(@node) @compiler.stubs(:networked?).returns(true) Puppet::Parser::Compiler.stubs(:compile) Puppet.expects(:notice).with { |msg| msg =~ /Compiled catalog/ } @compiler.find(@request) end end describe "when extracting facts from the request" do before do Facter.stubs(:value).returns "something" @compiler = Puppet::Resource::Catalog::Compiler.new @request = stub 'request', :options => {} @facts = Puppet::Node::Facts.new('hostname', "fact" => "value", "architecture" => "i386") Puppet::Node::Facts.indirection.stubs(:save).returns(nil) end it "should do nothing if no facts are provided" do Puppet::Node::Facts.indirection.expects(:convert_from).never @request.options[:facts] = nil @compiler.extract_facts_from_request(@request) end it "should use the Facts class to deserialize the provided facts and update the timestamp" do @request.options[:facts_format] = "foo" @request.options[:facts] = "bar" Puppet::Node::Facts.expects(:convert_from).returns @facts @facts.timestamp = Time.parse('2010-11-01') @now = Time.parse('2010-11-02') Time.expects(:now).returns(@now) @compiler.extract_facts_from_request(@request) @facts.timestamp.should == @now end it "should use the provided fact format" do @request.options[:facts_format] = "foo" @request.options[:facts] = "bar" Puppet::Node::Facts.expects(:convert_from).with { |format, text| format == "foo" }.returns @facts @compiler.extract_facts_from_request(@request) end it "should convert the facts into a fact instance and save it" do @request.options[:facts_format] = "foo" @request.options[:facts] = "bar" Puppet::Node::Facts.expects(:convert_from).returns @facts Puppet::Node::Facts.indirection.expects(:save).with(@facts) @compiler.extract_facts_from_request(@request) end end describe "when finding nodes" do before do Facter.stubs(:value).returns("whatever") @compiler = Puppet::Resource::Catalog::Compiler.new @name = "me" @node = mock 'node' - @request = stub 'request', :key => @name, :options => {} + @request = Puppet::Indirector::Request.new(:catalog, :find, @name) @compiler.stubs(:compile) end it "should look node information up via the Node class with the provided key" do @node.stubs :merge - Puppet::Node.indirection.expects(:find).with(@name).returns(@node) + Puppet::Node.indirection.expects(:find).with(@name, anything).returns(@node) @compiler.find(@request) end end describe "after finding nodes" do before do Puppet.expects(:version).returns(1) Facter.expects(:value).with('fqdn').returns("my.server.com") Facter.expects(:value).with('ipaddress').returns("my.ip.address") @compiler = Puppet::Resource::Catalog::Compiler.new @name = "me" @node = mock 'node' - @request = stub 'request', :key => @name, :options => {} + @request = Puppet::Indirector::Request.new(:catalog, :find, @name) @compiler.stubs(:compile) - Puppet::Node.indirection.stubs(:find).with(@name).returns(@node) + Puppet::Node.indirection.stubs(:find).with(@name, anything).returns(@node) end it "should add the server's Puppet version to the node's parameters as 'serverversion'" do @node.expects(:merge).with { |args| args["serverversion"] == "1" } @compiler.find(@request) end it "should add the server's fqdn to the node's parameters as 'servername'" do @node.expects(:merge).with { |args| args["servername"] == "my.server.com" } @compiler.find(@request) end it "should add the server's IP address to the node's parameters as 'serverip'" do @node.expects(:merge).with { |args| args["serverip"] == "my.ip.address" } @compiler.find(@request) end end describe "when filtering resources" do before :each do Facter.stubs(:value) @compiler = Puppet::Resource::Catalog::Compiler.new @catalog = stub_everything 'catalog' @catalog.stubs(:respond_to?).with(:filter).returns(true) end it "should delegate to the catalog instance filtering" do @catalog.expects(:filter) @compiler.filter(@catalog) end it "should filter out virtual resources" do resource = mock 'resource', :virtual? => true @catalog.stubs(:filter).yields(resource) @compiler.filter(@catalog) end it "should return the same catalog if it doesn't support filtering" do @catalog.stubs(:respond_to?).with(:filter).returns(false) @compiler.filter(@catalog).should == @catalog end it "should return the filtered catalog" do catalog = stub 'filtered catalog' @catalog.stubs(:filter).returns(catalog) @compiler.filter(@catalog).should == catalog end end end diff --git a/spec/unit/indirector/node/exec_spec.rb b/spec/unit/indirector/node/exec_spec.rb index 79f325129..ccd58bee1 100755 --- a/spec/unit/indirector/node/exec_spec.rb +++ b/spec/unit/indirector/node/exec_spec.rb @@ -1,69 +1,76 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/node/exec' +require 'puppet/indirector/request' describe Puppet::Node::Exec do before do @indirection = mock 'indirection' - Puppet.settings.stubs(:value).with(:external_nodes).returns(File.expand_path("/echo")) + Puppet.settings[:external_nodes] = File.expand_path("/echo") @searcher = Puppet::Node::Exec.new end describe "when constructing the command to run" do it "should use the external_node script as the command" do - Puppet.expects(:[]).with(:external_nodes).returns("/bin/echo") + Puppet[:external_nodes] = "/bin/echo" @searcher.command.should == %w{/bin/echo} end it "should throw an exception if no external node command is set" do - Puppet.expects(:[]).with(:external_nodes).returns("none") + Puppet[:external_nodes] = "none" proc { @searcher.find(stub('request', :key => "foo")) }.should raise_error(ArgumentError) end end describe "when handling the results of the command" do before do @name = "yay" @node = Puppet::Node.new(@name) @node.stubs(:fact_merge) Puppet::Node.expects(:new).with(@name).returns(@node) @result = {} # Use a local variable so the reference is usable in the execute definition. result = @result @searcher.meta_def(:execute) do |command, arguments| return YAML.dump(result) end - @request = stub 'request', :key => @name + @request = Puppet::Indirector::Request.new(:node, :find, @name) end it "should translate the YAML into a Node instance" do # Use an empty hash @searcher.find(@request).should equal(@node) end it "should set the resulting parameters as the node parameters" do @result[:parameters] = {"a" => "b", "c" => "d"} @searcher.find(@request) @node.parameters.should == {"a" => "b", "c" => "d"} end it "should set the resulting classes as the node classes" do @result[:classes] = %w{one two} @searcher.find(@request) @node.classes.should == [ 'one', 'two' ] end it "should merge the node's facts with its parameters" do @node.expects(:fact_merge) @searcher.find(@request) end it "should set the node's environment if one is provided" do @result[:environment] = "yay" @searcher.find(@request) @node.environment.to_s.should == 'yay' end + + it "should set the node's environment based on the request if not otherwise provided" do + @request.environment = "boo" + @searcher.find(@request) + @node.environment.to_s.should == 'boo' + end end end