diff --git a/lib/puppet/indirector/catalog/compiler.rb b/lib/puppet/indirector/catalog/compiler.rb index 6f4e2f3e4..592f2a616 100644 --- a/lib/puppet/indirector/catalog/compiler.rb +++ b/lib/puppet/indirector/catalog/compiler.rb @@ -1,182 +1,180 @@ require 'puppet/node' require 'puppet/resource/catalog' require 'puppet/indirector/code' require 'puppet/util/profiler' require 'yaml' class Puppet::Resource::Catalog::Compiler < Puppet::Indirector::Code desc "Compiles catalogs on demand using Puppet's compiler." include Puppet::Util attr_accessor :code def extract_facts_from_request(request) return unless text_facts = request.options[:facts] unless format = request.options[:facts_format] raise ArgumentError, "Facts but no fact format provided for #{request.key}" end Puppet::Util::Profiler.profile("Found facts", [:compiler, :find_facts]) do # 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 # We unescape here because the corresponding code in Puppet::Configurer::FactHandler escapes facts = Puppet::Node::Facts.convert_from(format, CGI.unescape(text_facts)) end unless facts.name == request.key raise Puppet::Error, "Catalog for #{request.key.inspect} was requested with fact definition for the wrong node (#{facts.name.inspect})." end - facts.add_timestamp - options = { :environment => request.environment, :transaction_uuid => request.options[:transaction_uuid], } Puppet::Node::Facts.indirection.save(facts, nil, options) end end # Compile a node's catalog. def find(request) extract_facts_from_request(request) node = node_from_request(request) node.trusted_data = Puppet.lookup(:trusted_information) { Puppet::Context::TrustedInformation.local(node) }.to_h 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 Puppet::Util::Profiler.profile("Setup server facts for compiling", [:compiler, :init_server_facts]) do set_server_facts end 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 benchmark(:notice, str) do Puppet::Util::Profiler.profile(str, [:compiler, :compile, node.environment, node.name]) do begin config = Puppet::Parser::Compiler.compile(node) rescue Puppet::Error => detail Puppet.err(detail.to_s) if networked? raise end end end config end # Turn our host name into a node object. def find_node(name, environment, transaction_uuid) Puppet::Util::Profiler.profile("Found node information", [:compiler, :find_node]) do node = nil begin node = Puppet::Node.indirection.find(name, :environment => environment, :transaction_uuid => transaction_uuid) rescue => detail message = "Failed when searching for node #{name}: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end # Add any external data to the node. if node add_node_data(node) end node end 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] if request.remote? raise Puppet::Error, "Invalid option use_node for a remote request" else return node end 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, request.environment, request.options[:transaction_uuid]) 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/spec/unit/indirector/catalog/compiler_spec.rb b/spec/unit/indirector/catalog/compiler_spec.rb index fb68e710a..8ff228e6e 100755 --- a/spec/unit/indirector/catalog/compiler_spec.rb +++ b/spec/unit/indirector/catalog/compiler_spec.rb @@ -1,270 +1,267 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/indirector/catalog/compiler' describe Puppet::Resource::Catalog::Compiler do before do Facter.stubs(:to_hash).returns({}) 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 Puppet[:node_terminus] = :memory Puppet::Node.indirection.save(Puppet::Node.new("node1")) Puppet::Node.indirection.save(Puppet::Node.new("node2")) compiler = Puppet::Resource::Catalog::Compiler.new compiler.stubs(:compile) compiler.find(Puppet::Indirector::Request.new(:catalog, :find, 'node1', nil, :node => 'node1')) compiler.find(Puppet::Indirector::Request.new(:catalog, :find, 'node2', nil, :node => 'node2')) 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 = Puppet::Indirector::Request.new(:catalog, :find, @name, nil, :node => @name) end it "should directly use provided nodes for a local request" do Puppet::Node.indirection.expects(:find).never @compiler.expects(:compile).with(@node) @request.stubs(:options).returns(:use_node => @node) @request.stubs(:remote?).returns(false) @compiler.find(@request) end it "rejects a provided node if the request is remote" do @request.stubs(:options).returns(:use_node => @node) @request.stubs(:remote?).returns(true) expect { @compiler.find(@request) }.to raise_error Puppet::Error, /invalid option use_node/i 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, 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", 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, 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, 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, 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, anything).returns @node @compiler.expects(:extract_facts_from_request).with(@request) Puppet::Parser::Compiler.stubs(:compile) @compiler.find(@request) end it "requires `facts_format` option if facts are passed in" do facts = Puppet::Node::Facts.new("mynode", :afact => "avalue") request = Puppet::Indirector::Request.new(:catalog, :find, "mynode", nil, :facts => facts) expect { @compiler.find(request) }.to raise_error ArgumentError, /no fact format provided for mynode/ end it "rejects facts in the request from a different node" do facts = Puppet::Node::Facts.new("differentnode", :afact => "avalue") request = Puppet::Indirector::Request.new( :catalog, :find, "mynode", nil, :facts => facts, :facts_format => "unused" ) expect { @compiler.find(request) }.to raise_error Puppet::Error, /fact definition for the wrong node/i 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 end describe "when extracting facts from the request" do before do Puppet::Node::Facts.indirection.terminus_class = :memory Facter.stubs(:value).returns "something" @compiler = Puppet::Resource::Catalog::Compiler.new @facts = Puppet::Node::Facts.new('hostname', "fact" => "value", "architecture" => "i386") end def a_request_that_contains(facts) request = Puppet::Indirector::Request.new(:catalog, :find, "hostname", nil) request.options[:facts_format] = "pson" request.options[:facts] = CGI.escape(facts.render(:pson)) request end it "should do nothing if no facts are provided" do request = Puppet::Indirector::Request.new(:catalog, :find, "hostname", nil) request.options[:facts] = nil @compiler.extract_facts_from_request(request).should be_nil end - it "deserializes the facts and timestamps them" do - @facts.timestamp = Time.parse('2010-11-01') + it "should deserialize the facts without changing the timestamp" do + time = Time.now + @facts.timestamp = time request = a_request_that_contains(@facts) - now = Time.parse('2010-11-02') - Time.stubs(:now).returns(now) - facts = @compiler.extract_facts_from_request(request) - - facts.timestamp.should == now + expect(facts.timestamp).to eq(time) end it "should convert the facts into a fact instance and save it" do request = a_request_that_contains(@facts) options = { :environment => request.environment, :transaction_uuid => request.options[:transaction_uuid], } Puppet::Node::Facts.indirection.expects(:save).with(equals(@facts), nil, options) @compiler.extract_facts_from_request(request) end end describe "when finding nodes" do it "should look node information up via the Node class with the provided key" do Facter.stubs(:value).returns("whatever") node = Puppet::Node.new('node') compiler = Puppet::Resource::Catalog::Compiler.new request = Puppet::Indirector::Request.new(:catalog, :find, "me", nil) compiler.stubs(:compile) Puppet::Node.indirection.expects(:find).with("me", anything).returns(node) compiler.find(request) end it "should pass the transaction_uuid to the node indirection" do uuid = '793ff10d-89f8-4527-a645-3302cbc749f3' node = Puppet::Node.new("thing") compiler = Puppet::Resource::Catalog::Compiler.new compiler.stubs(:compile) request = Puppet::Indirector::Request.new(:catalog, :find, "thing", nil, :transaction_uuid => uuid) Puppet::Node.indirection.expects(:find).with( "thing", has_entries(:transaction_uuid => uuid) ).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 @node = Puppet::Node.new("me") @request = Puppet::Indirector::Request.new(:catalog, :find, "me", nil) @compiler.stubs(:compile) Puppet::Node.indirection.stubs(:find).with("me", 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