diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb index 6c998a4a6..6b254c0cd 100644 --- a/lib/puppet/agent.rb +++ b/lib/puppet/agent.rb @@ -1,107 +1,112 @@ require 'sync' require 'puppet/external/event-loop' require 'puppet/application' # A general class for triggering a run of another # class. class Puppet::Agent require 'puppet/agent/locker' include Puppet::Agent::Locker attr_reader :client_class, :client, :splayed # Just so we can specify that we are "the" instance. def initialize(client_class) @splayed = false @client_class = client_class end def lockfile_path client_class.lockfile_path end def needing_restart? Puppet::Application.restart_requested? end # Perform a run with our client. def run(*args) if running? Puppet.notice "Run of #{client_class} already in progress; skipping" return end + if disabled? + Puppet.notice "Skipping run of #{client_class}; administratively disabled; use 'puppet #{client_class} --enable' to re-enable." + return + end + result = nil block_run = Puppet::Application.controlled_run do splay with_client do |client| begin sync.synchronize { lock { result = client.run(*args) } } rescue SystemExit,NoMemoryError raise rescue Exception => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not run #{client_class}: #{detail}" end end true end Puppet.notice "Shutdown/restart in progress (#{Puppet::Application.run_status.inspect}); skipping run" unless block_run result end def stopping? Puppet::Application.stop_requested? end # Have we splayed already? def splayed? splayed end # Sleep when splay is enabled; else just return. def splay return unless Puppet[:splay] return if splayed? time = rand(Integer(Puppet[:splaylimit]) + 1) Puppet.info "Sleeping for #{time} seconds (splay is enabled)" sleep(time) @splayed = true end # Start listening for events. We're pretty much just listening for # timer events here. def start # Create our timer. Puppet will handle observing it and such. timer = EventLoop::Timer.new(:interval => Puppet[:runinterval], :tolerance => 1, :start? => true) do run end # Run once before we start following the timer timer.sound_alarm end def sync @sync ||= Sync.new end private # Create and yield a client instance, keeping a reference # to it during the yield. def with_client begin @client = client_class.new rescue SystemExit,NoMemoryError raise rescue Exception => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not create instance of #{client_class}: #{detail}" return end yield @client ensure @client = nil end end diff --git a/lib/puppet/agent/locker.rb b/lib/puppet/agent/locker.rb index 98f5b38d9..6d9a00bdc 100644 --- a/lib/puppet/agent/locker.rb +++ b/lib/puppet/agent/locker.rb @@ -1,40 +1,44 @@ require 'puppet/util/pidlock' # Break out the code related to locking the agent. This module is just # included into the agent, but having it here makes it easier to test. module Puppet::Agent::Locker # Let the daemon run again, freely in the filesystem. def enable lockfile.unlock(:anonymous => true) end # Stop the daemon from making any catalog runs. def disable lockfile.lock(:anonymous => true) end # Yield if we get a lock, else do nothing. Return # true/false depending on whether we get the lock. def lock if lockfile.lock begin yield ensure lockfile.unlock end return true else return false end end def lockfile @lockfile ||= Puppet::Util::Pidlock.new(lockfile_path) @lockfile end def running? - lockfile.locked? + lockfile.locked? and !lockfile.anonymous? + end + + def disabled? + lockfile.locked? and lockfile.anonymous? end end diff --git a/spec/unit/agent_backward_compatibility_spec.rb b/spec/unit/agent_backward_compatibility_spec.rb index 1cd587fa1..4f2acfd01 100644 --- a/spec/unit/agent_backward_compatibility_spec.rb +++ b/spec/unit/agent_backward_compatibility_spec.rb @@ -1,154 +1,152 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/agent' ############################################################################ # NOTE # ############################################################################ # # # This entire spec is only here for backwards compatibility from 2.7.12+ # # with 2.7.10 and 2.7.11. The entire file should be able to be removed # # for the 3.x series. # # # # For more info, see the comments on the #handle_2_7_10_disabled_lockfile # # method in pidlock.rb # # # # --cprice 2012-03-01 # ############################################################################ class AgentTestClient def run # no-op end def stop # no-op end end describe Puppet::Agent do include PuppetSpec::Files let(:agent) { Puppet::Agent.new(AgentTestClient) } describe "in order to be backwards-compatibility with versions 2.7.10 and 2.7.11" do describe "when the 2.7.10/2.7.11 'disabled' lockfile exists" do # the "normal" lockfile let(:lockfile_path) { tmpfile("agent_spec_lockfile") } # the 2.7.10/2.7.11 "disabled" lockfile # (can't use PuppetSpec::Files.tmpfile here because we need the ".disabled" file to have *exactly* the same # path/name as the original file, plus the ".disabled" suffix.) let(:disabled_lockfile_path) { lockfile_path + ".disabled" } # some regexes to match log messages let(:warning_regex) { /^Found special lockfile '#{disabled_lockfile_path}'.*renaming/ } - let(:run_in_progress_regex) { /^Run of .* already in progress; skipping/ } + let(:disabled_regex) { /^Skipping run of .*; administratively disabled/ } before(:each) do # create the 2.7.10 "disable" lockfile. FileUtils.touch(disabled_lockfile_path) # stub in our temp lockfile path. AgentTestClient.expects(:lockfile_path).returns lockfile_path end after(:each) do # manually clean up the files that we didn't create via PuppetSpec::Files.tmpfile begin File.unlink(disabled_lockfile_path) rescue Errno::ENOENT # some of the tests expect for the agent code to take care of deleting this file, # so it may (validly) not exist. end end describe "when the 'regular' lockfile also exists" do # the logic here is that if a 'regular' lockfile already exists, then there is some state that the # current version of puppet is responsible for dealing with. All of the tests in this block are # simply here to make sure that our backwards-compatibility hack does *not* interfere with this. # # Even if the ".disabled" lockfile exists--it can be dealt with at another time, when puppet is # in *exactly* the state that we want it to be in (mostly meaning that the 'regular' lockfile # does not exist.) before(:each) do # create the "regular" lockfile FileUtils.touch(lockfile_path) end - it "should be recognized as 'running'" do - agent.should be_running + it "should be recognized as 'disabled'" do + agent.should be_disabled end it "should not try to start a new agent run" do AgentTestClient.expects(:new).never - Puppet.expects(:notice).with { |msg| msg =~ run_in_progress_regex } + Puppet.expects(:notice).with(regexp_matches(disabled_regex)) agent.run end it "should not delete the 2.7.10/2.7.11 lockfile" do agent.run File.exists?(disabled_lockfile_path).should == true end it "should not print the warning message" do - Puppet.expects(:warning).with {|msg| msg =~ warning_regex } .never + Puppet.expects(:warning).with(regexp_matches(warning_regex)).never agent.run end end describe "when the 'regular' lockfile does not exist" do # this block of tests is for actually testing the backwards compatibility hack. This # is where we're in a clean state and we know it's safe(r) to muck with the lockfile # situation. it "should recognize that the agent is disabled" do - # TODO change this to use "be_disabled" when that is re-introduced. - agent.should be_running + agent.should be_disabled end describe "when an agent run is requested" do it "should not try to start a new agent run" do AgentTestClient.expects(:new).never - Puppet.expects(:notice).with { |msg| msg =~ run_in_progress_regex } + Puppet.expects(:notice).with(regexp_matches(disabled_regex)) agent.run end it "should warn, remove the 2.7.10/2.7.11 lockfile, and create the 'normal' lockfile" do - Puppet.expects(:warning).with { |msg| msg =~ warning_regex } + Puppet.expects(:warning).with(regexp_matches(warning_regex)) agent.run File.exists?(disabled_lockfile_path).should == false File.exists?(lockfile_path).should == true end end describe "when running --enable" do it "should recognize that the agent is disabled" do - # TODO change this to use "be_disabled" when that is re-introduced. - agent.should be_running + agent.should be_disabled end it "should warn and clean up the 2.7.10/2.7.11 lockfile" do - Puppet.expects(:warning).with { |msg| msg =~ warning_regex } + Puppet.expects(:warning).with(regexp_matches(warning_regex)) agent.enable File.exists?(disabled_lockfile_path).should == false File.exists?(lockfile_path).should == false end end end end end end diff --git a/spec/unit/agent_spec.rb b/spec/unit/agent_spec.rb index 8d837e1b2..f24630eb9 100755 --- a/spec/unit/agent_spec.rb +++ b/spec/unit/agent_spec.rb @@ -1,287 +1,305 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/agent' class AgentTestClient def run # no-op end def stop # no-op end end def without_warnings flag = $VERBOSE $VERBOSE = nil yield $VERBOSE = flag end describe Puppet::Agent do before do @agent = Puppet::Agent.new(AgentTestClient) # So we don't actually try to hit the filesystem. @agent.stubs(:lock).yields # make Puppet::Application safe for stubbing; restore in an :after block; silence warnings for this. without_warnings { Puppet::Application = Class.new(Puppet::Application) } Puppet::Application.stubs(:clear?).returns(true) Puppet::Application.class_eval do class << self def controlled_run(&block) block.call end end end end after do # restore Puppet::Application from stub-safe subclass, and silence warnings without_warnings { Puppet::Application = Puppet::Application.superclass } end it "should set its client class at initialization" do Puppet::Agent.new("foo").client_class.should == "foo" end it "should include the Locker module" do Puppet::Agent.ancestors.should be_include(Puppet::Agent::Locker) end it "should create an instance of its client class and run it when asked to run" do client = mock 'client' AgentTestClient.expects(:new).returns client client.expects(:run) @agent.stubs(:running?).returns false + @agent.stubs(:disabled?).returns false @agent.run end it "should determine its lock file path by asking the client class" do AgentTestClient.expects(:lockfile_path).returns "/my/lock" @agent.lockfile_path.should == "/my/lock" end - it "should be considered running if the lock file is locked" do + it "should be considered running if the lock file is locked and not anonymous" do lockfile = mock 'lockfile' - @agent.expects(:lockfile).returns lockfile + @agent.expects(:lockfile).returns(lockfile).twice lockfile.expects(:locked?).returns true + lockfile.expects(:anonymous?).returns false @agent.should be_running end + it "should be considered disabled if the lock file is locked and anonymous" do + lockfile = mock 'lockfile' + + @agent.expects(:lockfile).returns(lockfile).at_least_once + lockfile.expects(:locked?).returns(true).at_least_once + lockfile.expects(:anonymous?).returns(true).at_least_once + + @agent.should be_disabled + end + describe "when being run" do before do @agent.stubs(:running?).returns false + @agent.stubs(:disabled?).returns false end it "should splay" do @agent.expects(:splay) - @agent.stubs(:running?).returns false @agent.run end it "should do nothing if already running" do @agent.expects(:running?).returns true AgentTestClient.expects(:new).never @agent.run end it "(#11057) should notify the user about why a run is skipped" do Puppet::Application.stubs(:controlled_run).returns(false) Puppet::Application.stubs(:run_status).returns('MOCK_RUN_STATUS') # This is the actual test that we inform the user why the run is skipped. # We assume this information is contained in # Puppet::Application.run_status Puppet.expects(:notice).with(regexp_matches(/MOCK_RUN_STATUS/)) @agent.run end + it "should display an informative message if the agent is administratively disabled" do + @agent.expects(:disabled?).returns true + Puppet.expects(:notice).with(regexp_matches(/Skipping run of .*; administratively disabled/)) + @agent.run + end + it "should use Puppet::Application.controlled_run to manage process state behavior" do calls = sequence('calls') Puppet::Application.expects(:controlled_run).yields.in_sequence(calls) AgentTestClient.expects(:new).once.in_sequence(calls) @agent.run end it "should not fail if a client class instance cannot be created" do AgentTestClient.expects(:new).raises "eh" Puppet.expects(:err) @agent.run end it "should not fail if there is an exception while running its client" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client client.expects(:run).raises "eh" Puppet.expects(:err) @agent.run end it "should use a mutex to restrict multi-threading" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client mutex = mock 'mutex' @agent.expects(:sync).returns mutex mutex.expects(:synchronize) client.expects(:run).never # if it doesn't run, then we know our yield is what triggers it @agent.run end it "should use a filesystem lock to restrict multiple processes running the agent" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client @agent.expects(:lock) client.expects(:run).never # if it doesn't run, then we know our yield is what triggers it @agent.run end it "should make its client instance available while running" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client client.expects(:run).with { @agent.client.should equal(client); true } @agent.run end it "should run the client instance with any arguments passed to it" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client client.expects(:run).with("testargs") @agent.run("testargs") end end describe "when splaying" do before do Puppet.settings.stubs(:value).with(:splay).returns true Puppet.settings.stubs(:value).with(:splaylimit).returns "10" end it "should do nothing if splay is disabled" do Puppet.settings.expects(:value).returns false @agent.expects(:sleep).never @agent.splay end it "should do nothing if it has already splayed" do @agent.expects(:splayed?).returns true @agent.expects(:sleep).never @agent.splay end it "should log that it is splaying" do @agent.stubs :sleep Puppet.expects :info @agent.splay end it "should sleep for a random portion of the splaylimit plus 1" do Puppet.settings.expects(:value).with(:splaylimit).returns "50" @agent.expects(:rand).with(51).returns 10 @agent.expects(:sleep).with(10) @agent.splay end it "should mark that it has splayed" do @agent.stubs(:sleep) @agent.splay @agent.should be_splayed end end describe "when checking execution state" do describe 'with regular run status' do before :each do Puppet::Application.stubs(:restart_requested?).returns(false) Puppet::Application.stubs(:stop_requested?).returns(false) Puppet::Application.stubs(:interrupted?).returns(false) Puppet::Application.stubs(:clear?).returns(true) end it 'should be false for :stopping?' do @agent.stopping?.should be_false end it 'should be false for :needing_restart?' do @agent.needing_restart?.should be_false end end describe 'with a stop requested' do before :each do Puppet::Application.stubs(:clear?).returns(false) Puppet::Application.stubs(:restart_requested?).returns(false) Puppet::Application.stubs(:stop_requested?).returns(true) Puppet::Application.stubs(:interrupted?).returns(true) end it 'should be true for :stopping?' do @agent.stopping?.should be_true end it 'should be false for :needing_restart?' do @agent.needing_restart?.should be_false end end describe 'with a restart requested' do before :each do Puppet::Application.stubs(:clear?).returns(false) Puppet::Application.stubs(:restart_requested?).returns(true) Puppet::Application.stubs(:stop_requested?).returns(false) Puppet::Application.stubs(:interrupted?).returns(true) end it 'should be false for :stopping?' do @agent.stopping?.should be_false end it 'should be true for :needing_restart?' do @agent.needing_restart?.should be_true end end end describe "when starting" do before do @agent.stubs(:observe_signal) end it "should create a timer with the runinterval, a tolerance of 1, and :start? set to true" do Puppet.settings.expects(:value).with(:runinterval).returns 5 timer = stub 'timer', :sound_alarm => nil EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1).returns timer @agent.stubs(:run) @agent.start end it "should run once immediately" do timer = mock 'timer' EventLoop::Timer.expects(:new).returns timer timer.expects(:sound_alarm) @agent.start end it "should run within the block passed to the timer" do timer = stub 'timer', :sound_alarm => nil EventLoop::Timer.expects(:new).returns(timer).yields @agent.expects(:run) @agent.start end end end