diff --git a/lib/puppet/configurer/fact_handler.rb b/lib/puppet/configurer/fact_handler.rb index 803495773..7d18aa9f2 100644 --- a/lib/puppet/configurer/fact_handler.rb +++ b/lib/puppet/configurer/fact_handler.rb @@ -1,77 +1,56 @@ require 'puppet/indirector/facts/facter' require 'puppet/configurer/downloader' # Break out the code related to facts. This module is # just included into the agent, but having it here makes it # easier to test. module Puppet::Configurer::FactHandler def download_fact_plugins? Puppet[:factsync] end def find_facts # This works because puppet agent configures Facts to use 'facter' for # finding facts and the 'rest' terminus for caching them. Thus, we'll # compile them and then "cache" them on the server. begin - reload_facter facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value]) unless Puppet[:node_name_fact].empty? Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]] facts.name = Puppet[:node_name_value] end facts rescue SystemExit,NoMemoryError raise rescue Exception => detail puts detail.backtrace if Puppet[:trace] raise Puppet::Error, "Could not retrieve local facts: #{detail}" end end def facts_for_uploading facts = find_facts #format = facts.class.default_format if facts.support_format?(:b64_zlib_yaml) format = :b64_zlib_yaml else format = :yaml end text = facts.render(format) {:facts_format => format, :facts => CGI.escape(text)} end # Retrieve facts from the central server. def download_fact_plugins return unless download_fact_plugins? # Deprecated prior to 0.25, as of 5/19/2008 Puppet.warning "Fact syncing is deprecated as of 0.25 -- use 'pluginsync' instead" Puppet::Configurer::Downloader.new("fact", Puppet[:factdest], Puppet[:factsource], Puppet[:factsignore]).evaluate end - - # Clear out all of the loaded facts and reload them from disk. - # NOTE: This is clumsy and shouldn't be required for later (1.5.x) versions - # of Facter. - def reload_facter - Facter.clear - - # Reload everything. - if Facter.respond_to? :loadfacts - Facter.loadfacts - elsif Facter.respond_to? :load - Facter.load - else - Puppet.warning "You should upgrade your version of Facter to at least 1.3.8" - end - - # This loads all existing facts and any new ones. We have to remove and - # reload because there's no way to unload specific facts. - Puppet::Node::Facts::Facter.load_fact_plugins - end end diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb index 0f38ba393..e41ef02e7 100644 --- a/lib/puppet/indirector/facts/facter.rb +++ b/lib/puppet/indirector/facts/facter.rb @@ -1,83 +1,96 @@ require 'puppet/node/facts' require 'puppet/indirector/code' class Puppet::Node::Facts::Facter < Puppet::Indirector::Code desc "Retrieve facts from Facter. This provides a somewhat abstract interface between Puppet and Facter. It's only `somewhat` abstract because it always returns the local host's facts, regardless of what you attempt to find." + # Clear out all of the loaded facts. Reload facter but not puppet facts. + # NOTE: This is clumsy and shouldn't be required for later (1.5.x) versions + # of Facter. + def self.reload_facter + Facter.clear + + # Reload everything. + if Facter.respond_to? :loadfacts + Facter.loadfacts + elsif Facter.respond_to? :load + Facter.load + else + Puppet.warning "You should upgrade your version of Facter to at least 1.3.8" + end + end + def self.load_fact_plugins # Add any per-module fact directories to the factpath module_fact_dirs = Puppet[:modulepath].split(File::PATH_SEPARATOR).collect do |d| ["lib", "plugins"].map do |subdirectory| Dir.glob("#{d}/*/#{subdirectory}/facter") end end.flatten dirs = module_fact_dirs + Puppet[:factpath].split(File::PATH_SEPARATOR) x = dirs.uniq.each do |dir| load_facts_in_dir(dir) end end def self.load_facts_in_dir(dir) return unless FileTest.directory?(dir) Dir.chdir(dir) do Dir.glob("*.rb").each do |file| fqfile = ::File.join(dir, file) begin - Puppet.info "Loading facts in #{::File.basename(file.sub(".rb",''))}" + Puppet.info "Loading facts in #{fqfile}" Timeout::timeout(self.timeout) do load file end rescue SystemExit,NoMemoryError raise rescue Exception => detail Puppet.warning "Could not load fact file #{fqfile}: #{detail}" end end end end def self.timeout timeout = Puppet[:configtimeout] case timeout when String if timeout =~ /^\d+$/ timeout = Integer(timeout) else raise ArgumentError, "Configuration timeout must be an integer" end when Integer # nothing else raise ArgumentError, "Configuration timeout must be an integer" end timeout end - def initialize(*args) - super - self.class.load_fact_plugins - end - def destroy(facts) raise Puppet::DevError, "You cannot destroy facts in the code store; it is only used for getting facts from Facter" end # Look a host's facts up in Facter. def find(request) + self.class.reload_facter + self.class.load_fact_plugins result = Puppet::Node::Facts.new(request.key, Facter.to_hash) result.add_local_facts result.stringify result.downcase_if_necessary result end def save(facts) raise Puppet::DevError, "You cannot save facts to the code store; it is only used for getting facts from Facter" end end diff --git a/spec/unit/configurer/fact_handler_spec.rb b/spec/unit/configurer/fact_handler_spec.rb index 4a3fe8b3b..41b4862ca 100755 --- a/spec/unit/configurer/fact_handler_spec.rb +++ b/spec/unit/configurer/fact_handler_spec.rb @@ -1,171 +1,147 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/configurer' require 'puppet/configurer/fact_handler' class FactHandlerTester include Puppet::Configurer::FactHandler end describe Puppet::Configurer::FactHandler do before do @facthandler = FactHandlerTester.new end it "should download fact plugins when :factsync is true" do Puppet.settings.expects(:value).with(:factsync).returns true @facthandler.should be_download_fact_plugins end it "should not download fact plugins when :factsync is false" do Puppet.settings.expects(:value).with(:factsync).returns false @facthandler.should_not be_download_fact_plugins end it "should not download fact plugins when downloading is disabled" do Puppet::Configurer::Downloader.expects(:new).never @facthandler.expects(:download_fact_plugins?).returns false @facthandler.download_fact_plugins end it "should use an Agent Downloader, with the name, source, destination, and ignore set correctly, to download fact plugins when downloading is enabled" do downloader = mock 'downloader' Puppet.settings.expects(:value).with(:factsource).returns "fsource" Puppet.settings.expects(:value).with(:factdest).returns "fdest" Puppet.settings.expects(:value).with(:factsignore).returns "fignore" Puppet::Configurer::Downloader.expects(:new).with("fact", "fdest", "fsource", "fignore").returns downloader downloader.expects(:evaluate) @facthandler.expects(:download_fact_plugins?).returns true @facthandler.download_fact_plugins end describe "when finding facts" do before :each do @facthandler.stubs(:reload_facter) Puppet::Node::Facts.indirection.terminus_class = :memory end it "should use the node name value to retrieve the facts" do foo_facts = Puppet::Node::Facts.new('foo') bar_facts = Puppet::Node::Facts.new('bar') Puppet::Node::Facts.indirection.save(foo_facts) Puppet::Node::Facts.indirection.save(bar_facts) Puppet[:certname] = 'foo' Puppet[:node_name_value] = 'bar' @facthandler.find_facts.should == bar_facts end it "should set the facts name based on the node_name_fact" do facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name') Puppet::Node::Facts.indirection.save(facts) Puppet[:node_name_fact] = 'my_name_fact' @facthandler.find_facts.name.should == 'other_node_name' end it "should set the node_name_value based on the node_name_fact" do facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name') Puppet::Node::Facts.indirection.save(facts) Puppet[:node_name_fact] = 'my_name_fact' @facthandler.find_facts Puppet[:node_name_value].should == 'other_node_name' end - it "should reload Facter before finding facts" do - @facthandler.expects(:reload_facter) - - @facthandler.find_facts - end - it "should fail if finding facts fails" do Puppet[:trace] = false Puppet[:certname] = "myhost" Puppet::Node::Facts.indirection.expects(:find).raises RuntimeError lambda { @facthandler.find_facts }.should raise_error(Puppet::Error) end end + it "should only load fact plugins once" do + Puppet::Node::Facts.indirection.expects(:find).once + @facthandler.find_facts + end + it "should warn about factsync deprecation when factsync is enabled" do Puppet::Configurer::Downloader.stubs(:new).returns mock("downloader", :evaluate => nil) @facthandler.expects(:download_fact_plugins?).returns true Puppet.expects(:warning) @facthandler.download_fact_plugins end # I couldn't get marshal to work for this, only yaml, so we hard-code yaml. it "should serialize and CGI escape the fact values for uploading" do facts = stub 'facts' facts.expects(:support_format?).with(:b64_zlib_yaml).returns true facts.expects(:render).returns "my text" text = CGI.escape("my text") @facthandler.expects(:find_facts).returns facts @facthandler.facts_for_uploading.should == {:facts_format => :b64_zlib_yaml, :facts => text} end it "should properly accept facts containing a '+'" do facts = stub 'facts' facts.expects(:support_format?).with(:b64_zlib_yaml).returns true facts.expects(:render).returns "my+text" text = "my%2Btext" @facthandler.expects(:find_facts).returns facts @facthandler.facts_for_uploading.should == {:facts_format => :b64_zlib_yaml, :facts => text} end it "use compressed yaml as the serialization if zlib is supported" do facts = stub 'facts' facts.expects(:support_format?).with(:b64_zlib_yaml).returns true facts.expects(:render).with(:b64_zlib_yaml).returns "my text" text = CGI.escape("my text") @facthandler.expects(:find_facts).returns facts @facthandler.facts_for_uploading end it "should use yaml as the serialization if zlib is not supported" do facts = stub 'facts' facts.expects(:support_format?).with(:b64_zlib_yaml).returns false facts.expects(:render).with(:yaml).returns "my text" text = CGI.escape("my text") @facthandler.expects(:find_facts).returns facts @facthandler.facts_for_uploading end - - describe "when reloading Facter" do - before do - Facter.stubs(:clear) - Facter.stubs(:load) - Facter.stubs(:loadfacts) - end - - it "should clear Facter" do - Facter.expects(:clear) - @facthandler.reload_facter - end - - it "should load all Facter facts" do - Facter.expects(:loadfacts) - @facthandler.reload_facter - end - - it "should use the Facter terminus load all Puppet Fact plugins" do - Puppet::Node::Facts::Facter.expects(:load_fact_plugins) - @facthandler.reload_facter - end - end end diff --git a/spec/unit/indirector/facts/facter_spec.rb b/spec/unit/indirector/facts/facter_spec.rb index 3b1574e52..0cb8037aa 100755 --- a/spec/unit/indirector/facts/facter_spec.rb +++ b/spec/unit/indirector/facts/facter_spec.rb @@ -1,139 +1,161 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/facts/facter' describe Puppet::Node::Facts::Facter do it "should be a subclass of the Code terminus" do Puppet::Node::Facts::Facter.superclass.should equal(Puppet::Indirector::Code) end it "should have documentation" do Puppet::Node::Facts::Facter.doc.should_not be_nil end it "should be registered with the configuration store indirection" do indirection = Puppet::Indirector::Indirection.instance(:facts) Puppet::Node::Facts::Facter.indirection.should equal(indirection) end it "should have its name set to :facter" do Puppet::Node::Facts::Facter.name.should == :facter end - it "should load facts on initialization" do - Puppet::Node::Facts::Facter.expects(:load_fact_plugins) - Puppet::Node::Facts::Facter.new + describe "when reloading Facter" do + before do + @facter_class = Puppet::Node::Facts::Facter + Facter.stubs(:clear) + Facter.stubs(:load) + Facter.stubs(:loadfacts) + end + + it "should clear Facter" do + Facter.expects(:clear) + @facter_class.reload_facter + end + + it "should load all Facter facts" do + Facter.expects(:loadfacts) + @facter_class.reload_facter + end end end describe Puppet::Node::Facts::Facter do before :each do + Puppet::Node::Facts::Facter.stubs(:reload_facter) @facter = Puppet::Node::Facts::Facter.new Facter.stubs(:to_hash).returns({}) @name = "me" @request = stub 'request', :key => @name end describe Puppet::Node::Facts::Facter, " when finding facts" do + it "should reset and load facts" do + clear = sequence 'clear' + Puppet::Node::Facts::Facter.expects(:reload_facter).in_sequence(clear) + Puppet::Node::Facts::Facter.expects(:load_fact_plugins).in_sequence(clear) + @facter.find(@request) + end + it "should return a Facts instance" do @facter.find(@request).should be_instance_of(Puppet::Node::Facts) end it "should return a Facts instance with the provided key as the name" do @facter.find(@request).name.should == @name end it "should return the Facter facts as the values in the Facts instance" do Facter.expects(:to_hash).returns("one" => "two") facts = @facter.find(@request) facts.values["one"].should == "two" end it "should add local facts" do facts = Puppet::Node::Facts.new("foo") Puppet::Node::Facts.expects(:new).returns facts facts.expects(:add_local_facts) @facter.find(@request) end it "should convert all facts into strings" do facts = Puppet::Node::Facts.new("foo") Puppet::Node::Facts.expects(:new).returns facts facts.expects(:stringify) @facter.find(@request) end it "should call the downcase hook" do facts = Puppet::Node::Facts.new("foo") Puppet::Node::Facts.expects(:new).returns facts facts.expects(:downcase_if_necessary) @facter.find(@request) end end describe Puppet::Node::Facts::Facter, " when saving facts" do it "should fail" do proc { @facter.save(@facts) }.should raise_error(Puppet::DevError) end end describe Puppet::Node::Facts::Facter, " when destroying facts" do it "should fail" do proc { @facter.destroy(@facts) }.should raise_error(Puppet::DevError) end end it "should skip files when asked to load a directory" do FileTest.expects(:directory?).with("myfile").returns false Puppet::Node::Facts::Facter.load_facts_in_dir("myfile") end it "should load each ruby file when asked to load a directory" do FileTest.expects(:directory?).with("mydir").returns true Dir.expects(:chdir).with("mydir").yields Dir.expects(:glob).with("*.rb").returns %w{a.rb b.rb} Puppet::Node::Facts::Facter.expects(:load).with("a.rb") Puppet::Node::Facts::Facter.expects(:load).with("b.rb") Puppet::Node::Facts::Facter.load_facts_in_dir("mydir") end describe Puppet::Node::Facts::Facter, "when loading fact plugins from disk" do it "should load each directory in the Fact path" do Puppet.settings.stubs(:value).returns "foo" Puppet.settings.expects(:value).with(:factpath).returns("one#{File::PATH_SEPARATOR}two") Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("one") Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("two") Puppet::Node::Facts::Facter.load_fact_plugins end it "should load all facts from the modules" do Puppet.settings.stubs(:value).returns "foo" Puppet::Node::Facts::Facter.stubs(:load_facts_in_dir) Puppet.settings.expects(:value).with(:modulepath).returns("one#{File::PATH_SEPARATOR}two") Dir.stubs(:glob).returns [] Dir.expects(:glob).with("one/*/lib/facter").returns %w{oneA oneB} Dir.expects(:glob).with("two/*/lib/facter").returns %w{twoA twoB} Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("oneA") Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("oneB") Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("twoA") Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("twoB") Puppet::Node::Facts::Facter.load_fact_plugins end end end