diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb index 550e31b00..b34e9ddd0 100644 --- a/lib/puppet/configurer.rb +++ b/lib/puppet/configurer.rb @@ -1,245 +1,242 @@ # The client for interacting with the puppetmaster config server. require 'sync' require 'timeout' require 'puppet/network/http_pool' require 'puppet/util' +require 'puppet/util/config_timeout' class Puppet::Configurer require 'puppet/configurer/fact_handler' require 'puppet/configurer/plugin_handler' - require 'puppet/util/config_timeout' + extend Puppet::Util::ConfigTimeout include Puppet::Configurer::FactHandler include Puppet::Configurer::PluginHandler - class < detail Puppet.log_exception(detail, "Removing corrupt state file #{Puppet[:statefile]}: #{detail}") begin ::File.unlink(Puppet[:statefile]) retry rescue => detail raise Puppet::Error.new("Cannot remove #{Puppet[:statefile]}: #{detail}") end end # Just so we can specify that we are "the" instance. def initialize Puppet.settings.use(:main, :ssl, :agent) self.class.instance = self @running = false @splayed = false end # Prepare for catalog retrieval. Downloads everything necessary, etc. def prepare(options) dostorage download_plugins unless options[:skip_plugin_download] end # Get the remote catalog, yo. Returns nil if no catalog can be found. def retrieve_catalog(fact_options) fact_options ||= {} # First try it with no cache, then with the cache. unless (Puppet[:use_cached_catalog] and result = retrieve_catalog_from_cache(fact_options)) or result = retrieve_new_catalog(fact_options) if ! Puppet[:usecacheonfailure] Puppet.warning "Not using cache on failed catalog" return nil end result = retrieve_catalog_from_cache(fact_options) end return nil unless result convert_catalog(result, @duration) end # Convert a plain resource catalog into our full host catalog. def convert_catalog(result, duration) catalog = result.to_ral catalog.finalize catalog.retrieval_duration = duration catalog.write_class_file catalog.write_resource_file catalog end def prepare_and_retrieve_catalog(options) prepare(options) if Puppet::Resource::Catalog.indirection.terminus_class == :rest # This is a bit complicated. We need the serialized and escaped facts, # and we need to know which format they're encoded in. Thus, we # get a hash with both of these pieces of information. fact_options = facts_for_uploading end # set report host name now that we have the fact options[:report].host = Puppet[:node_name_value] unless catalog = (options.delete(:catalog) || retrieve_catalog(fact_options)) Puppet.err "Could not retrieve catalog; skipping run" return end catalog end # Retrieve (optionally) and apply a catalog. If a catalog is passed in # the options, then apply that one, otherwise retrieve it. def apply_catalog(catalog, options) report = options[:report] report.configuration_version = catalog.version report.environment = Puppet[:environment] benchmark(:notice, "Finished catalog run") do catalog.apply(options) end report.finalize_report report end # The code that actually runs the catalog. # This just passes any options on to the catalog, # which accepts :tags and :ignoreschedules. def run(options = {}) options[:report] ||= Puppet::Transaction::Report.new("apply") report = options[:report] Puppet::Util::Log.newdestination(report) begin begin unless catalog = prepare_and_retrieve_catalog(options) return nil end # Here we set the local environment based on what we get from the # catalog. Since a change in environment means a change in facts, and # facts may be used to determine which catalog we get, we need to # rerun the process if the environment is changed. tries = 0 while catalog.environment and not catalog.environment.empty? and catalog.environment != Puppet[:environment] if tries > 3 raise Puppet::Error, "Catalog environment didn't stabilize after #{tries} fetches, aborting run" end Puppet.warning "Local environment: \"#{Puppet[:environment]}\" doesn't match server specified environment \"#{catalog.environment}\", restarting agent run with new environment" Puppet[:environment] = catalog.environment return nil unless catalog = prepare_and_retrieve_catalog(options) tries += 1 end execute_prerun_command or return nil apply_catalog(catalog, options) report.exit_status rescue SystemExit,NoMemoryError raise rescue => detail Puppet.log_exception(detail, "Failed to apply catalog: #{detail}") return nil ensure execute_postrun_command or return nil end ensure # Make sure we forget the retained module_directories of any autoload # we might have used. Thread.current[:env_module_directories] = nil end ensure Puppet::Util::Log.close(report) send_report(report) end def send_report(report) puts report.summary if Puppet[:summarize] save_last_run_summary(report) Puppet::Transaction::Report.indirection.save(report) if Puppet[:report] rescue => detail Puppet.log_exception(detail, "Could not send report: #{detail}") end def save_last_run_summary(report) mode = Puppet.settings.setting(:lastrunfile).mode Puppet::Util.replace_file(Puppet[:lastrunfile], mode) do |fh| fh.print YAML.dump(report.raw_summary) end rescue => detail Puppet.log_exception(detail, "Could not save last run local report: #{detail}") end private def execute_from_setting(setting) return true if (command = Puppet[setting]) == "" begin Puppet::Util::Execution.execute([command]) true rescue => detail Puppet.log_exception(detail, "Could not run command from #{setting}: #{detail}") false end end def retrieve_catalog_from_cache(fact_options) result = nil @duration = thinmark do result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], fact_options.merge(:ignore_terminus => true)) end Puppet.notice "Using cached catalog" result rescue => detail Puppet.log_exception(detail, "Could not retrieve catalog from cache: #{detail}") return nil end def retrieve_new_catalog(fact_options) result = nil @duration = thinmark do result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], fact_options.merge(:ignore_cache => true)) end result rescue SystemExit,NoMemoryError raise rescue Exception => detail Puppet.log_exception(detail, "Could not retrieve catalog from remote server: #{detail}") return nil end end diff --git a/lib/puppet/configurer/downloader.rb b/lib/puppet/configurer/downloader.rb index 9a0af4e29..4814ce3b2 100644 --- a/lib/puppet/configurer/downloader.rb +++ b/lib/puppet/configurer/downloader.rb @@ -1,72 +1,70 @@ require 'puppet/configurer' require 'puppet/resource/catalog' +require 'puppet/util/config_timeout' class Puppet::Configurer::Downloader - require 'puppet/util/config_timeout' - class < detail Puppet.log_exception(detail, "Could not retrieve #{name}: #{detail}") end files end def initialize(name, path, source, ignore = nil) @name, @path, @source, @ignore = name, path, source, ignore end def catalog catalog = Puppet::Resource::Catalog.new catalog.host_config = false catalog.add_resource(file) catalog end def file args = default_arguments.merge(:path => path, :source => source) args[:ignore] = ignore.split if ignore Puppet::Type.type(:file).new(args) end private require 'sys/admin' if Puppet.features.microsoft_windows? def default_arguments { :path => path, :recurse => true, :source => source, :tag => name, :purge => true, :force => true, :backup => false, :noop => false }.merge( Puppet.features.microsoft_windows? ? {} : { :owner => Process.uid, :group => Process.gid } ) end end diff --git a/lib/puppet/configurer/fact_handler.rb b/lib/puppet/configurer/fact_handler.rb index 6027732ce..9a319d559 100644 --- a/lib/puppet/configurer/fact_handler.rb +++ b/lib/puppet/configurer/fact_handler.rb @@ -1,43 +1,44 @@ require 'puppet/indirector/facts/facter' +require 'puppet/configurer' 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 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 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 message = "Could not retrieve local facts: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message 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 end diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb index 407d2a06e..99fab9773 100644 --- a/lib/puppet/indirector/facts/facter.rb +++ b/lib/puppet/indirector/facts/facter.rb @@ -1,83 +1,82 @@ require 'puppet/node/facts' require 'puppet/indirector/code' +require 'puppet/util/config_timeout' class Puppet::Node::Facts::Facter < Puppet::Indirector::Code - require 'puppet/util/config_timeout' - class < detail Puppet.warning "Could not load fact file #{fqfile}: #{detail}" end end end 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/lib/puppet/util/config_timeout.rb b/lib/puppet/util/config_timeout.rb index 6e05ea231..5da6507e6 100644 --- a/lib/puppet/util/config_timeout.rb +++ b/lib/puppet/util/config_timeout.rb @@ -1,18 +1,24 @@ module Puppet::Util::ConfigTimeout - def timeout + # NOTE: in the future it might be a good idea to add an explicit "integer" type to + # the settings types, in which case this would no longer be necessary. + + # Get the value of puppet's "configtimeout" setting, as an integer. Raise an + # ArgumentError if the setting does not contain a valid integer value. + # @return Puppet config timeout setting value, as an integer + def timeout_interval 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 end \ No newline at end of file diff --git a/spec/unit/configurer/downloader_spec.rb b/spec/unit/configurer/downloader_spec.rb index 439a25f0a..9dba85b31 100755 --- a/spec/unit/configurer/downloader_spec.rb +++ b/spec/unit/configurer/downloader_spec.rb @@ -1,218 +1,218 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/configurer/downloader' describe Puppet::Configurer::Downloader do require 'puppet_spec/files' include PuppetSpec::Files it "should require a name" do lambda { Puppet::Configurer::Downloader.new }.should raise_error(ArgumentError) end it "should require a path and a source at initialization" do lambda { Puppet::Configurer::Downloader.new("name") }.should raise_error(ArgumentError) end it "should set the name, path and source appropriately" do dler = Puppet::Configurer::Downloader.new("facts", "path", "source") dler.name.should == "facts" dler.path.should == "path" dler.source.should == "source" end it "should be able to provide a timeout value" do - Puppet::Configurer::Downloader.should respond_to(:timeout) + Puppet::Configurer::Downloader.should respond_to(:timeout_interval) end it "should use the configtimeout, converted to an integer, as its timeout" do Puppet.settings.expects(:value).with(:configtimeout).returns "50" - Puppet::Configurer::Downloader.timeout.should == 50 + Puppet::Configurer::Downloader.timeout_interval.should == 50 end describe "when creating the file that does the downloading" do before do @dler = Puppet::Configurer::Downloader.new("foo", "path", "source") end it "should create a file instance with the right path and source" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:path] == "path" and opts[:source] == "source" } @dler.file end it "should tag the file with the downloader name" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:tag] == "foo" } @dler.file end it "should always recurse" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:recurse] == true } @dler.file end it "should always purge" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:purge] == true } @dler.file end it "should never be in noop" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:noop] == false } @dler.file end describe "on POSIX" do before :each do Puppet.features.stubs(:microsoft_windows?).returns false end it "should always set the owner to the current UID" do Process.expects(:uid).returns 51 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == 51 } @dler.file end it "should always set the group to the current GID" do Process.expects(:gid).returns 61 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == 61 } @dler.file end end describe "on Windows", :if => Puppet.features.microsoft_windows? do it "should omit the owner" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == nil } @dler.file end it "should omit the group" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == nil } @dler.file end end it "should always force the download" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:force] == true } @dler.file end it "should never back up when downloading" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:backup] == false } @dler.file end it "should support providing an 'ignore' parameter" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == [".svn"] } @dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn") @dler.file end it "should split the 'ignore' parameter on whitespace" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == %w{.svn CVS} } @dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn CVS") @dler.file end end describe "when creating the catalog to do the downloading" do before do @path = make_absolute("/download/path") @dler = Puppet::Configurer::Downloader.new("foo", @path, make_absolute("source")) end it "should create a catalog and add the file to it" do catalog = @dler.catalog catalog.resources.size.should == 1 catalog.resources.first.class.should == Puppet::Type::File catalog.resources.first.name.should == @path end it "should specify that it is not managing a host catalog" do @dler.catalog.host_config.should == false end end describe "when downloading" do before do @dl_name = tmpfile("downloadpath") source_name = tmpfile("source") File.open(source_name, 'w') {|f| f.write('hola mundo') } @dler = Puppet::Configurer::Downloader.new("foo", @dl_name, source_name) end it "should not skip downloaded resources when filtering on tags" do Puppet[:tags] = 'maytag' @dler.evaluate File.exists?(@dl_name).should be_true end it "should log that it is downloading" do Puppet.expects(:info) Timeout.stubs(:timeout) @dler.evaluate end it "should set a timeout for the download" do - Puppet::Configurer::Downloader.expects(:timeout).returns 50 + Puppet::Configurer::Downloader.expects(:timeout_interval).returns 50 Timeout.expects(:timeout).with(50) @dler.evaluate end it "should apply the catalog within the timeout block" do catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) Timeout.expects(:timeout).yields catalog.expects(:apply) @dler.evaluate end it "should return all changed file paths" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) Timeout.expects(:timeout).yields resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) @dler.evaluate.should == %w{/changed/file} end it "should yield the resources if a block is given" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) Timeout.expects(:timeout).yields resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) yielded = nil @dler.evaluate { |r| yielded = r } yielded.should == resource end it "should catch and log exceptions" do Puppet.expects(:err) Timeout.stubs(:timeout).raises(Puppet::Error, "testing") lambda { @dler.evaluate }.should_not raise_error end end end diff --git a/spec/unit/puppet/provider/README.markdown b/spec/unit/provider/README.markdown similarity index 100% rename from spec/unit/puppet/provider/README.markdown rename to spec/unit/provider/README.markdown diff --git a/spec/unit/puppet/type/README.markdown b/spec/unit/type/README.markdown similarity index 100% rename from spec/unit/puppet/type/README.markdown rename to spec/unit/type/README.markdown diff --git a/spec/unit/util/config_timeout_spec.rb b/spec/unit/util/config_timeout_spec.rb new file mode 100644 index 000000000..bada38c65 --- /dev/null +++ b/spec/unit/util/config_timeout_spec.rb @@ -0,0 +1,57 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/util/config_timeout' + + + +describe Puppet::Util::ConfigTimeout do + # NOTE: in the future it might be a good idea to add an explicit "integer" type to + # the settings types, in which case this would no longer be necessary. + + + class TestConfigTimeout + include Puppet::Util::ConfigTimeout + end + + let :instance do TestConfigTimeout.new end + + + context "when the config setting is a String" do + context "which contains an integer" do + it "should convert the string to an integer" do + Puppet[:configtimeout] = "12" + instance.timeout_interval.should == 12 + end + end + + context "which does not contain an integer do" do + it "should raise an ArgumentError" do + Puppet[:configtimeout] = "foo" + expect { + instance.timeout_interval + }.to raise_error(ArgumentError) + end + end + end + + context "when the config setting is an Integer" do + it "should return the integer" do + Puppet[:configtimeout] = 12 + instance.timeout_interval.should == 12 + end + end + + context "when the config setting is some other type" do + # test a random smattering of types + [Hash.new, Array.new, Object.new].each do |obj| + context "when the config setting is a #{obj.class}" do + it "should raise an ArgumentError" do + Puppet[:configtimeout] = Hash.new + expect { + instance.timeout_interval + }.to raise_error(ArgumentError) + end + end + end + end +end \ No newline at end of file