diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb index 9d4944a23..47f4fa638 100644 --- a/lib/puppet/agent.rb +++ b/lib/puppet/agent.rb @@ -1,130 +1,122 @@ require 'sync' require 'puppet/application' # A general class for triggering a run of another # class. class Puppet::Agent require 'puppet/agent/locker' include Puppet::Agent::Locker require 'puppet/agent/disabler' include Puppet::Agent::Disabler attr_reader :client_class, :client, :splayed attr_accessor :should_fork # Just so we can specify that we are "the" instance. def initialize(client_class) @splayed = false @client_class = client_class end - def running_lockfile_path - #client_class.lockfile_path - Puppet[:agent_running_lockfile] - end - def disabled_lockfile_path - Puppet[:agent_disabled_lockfile] - 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 (Reason: '#{disable_message}');\nUse 'puppet agent --enable' to re-enable." return end result = nil block_run = Puppet::Application.controlled_run do splay result = run_in_fork(should_fork) do with_client do |client| begin sync.synchronize { lock { client.run(*args) } } rescue SystemExit,NoMemoryError raise rescue Exception => detail Puppet.log_exception(detail, "Could not run #{client_class}: #{detail}") end 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 def sync @sync ||= Sync.new end def run_in_fork(forking = true) return yield unless forking or Puppet.features.windows? child_pid = Kernel.fork do $0 = "puppet agent: applying configuration" begin exit(yield) rescue SystemExit exit(-1) rescue NoMemoryError exit(-2) end end exit_code = Process.waitpid2(child_pid) case exit_code[1].exitstatus when -1 raise SystemExit when -2 raise NoMemoryError end exit_code[1].exitstatus 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 Puppet.log_exception(detail, "Could not create instance of #{client_class}: #{detail}") return end yield @client ensure @client = nil end end diff --git a/lib/puppet/agent/disabler.rb b/lib/puppet/agent/disabler.rb index 3f40c2eba..8b1de1ed3 100644 --- a/lib/puppet/agent/disabler.rb +++ b/lib/puppet/agent/disabler.rb @@ -1,27 +1,33 @@ require 'puppet/util/anonymous_filelock' +# This module is responsible for encapsulating the logic for +# "disabling" the puppet agent during a run; in other words, +# keeping track of enough state to answer the question +# "has the puppet agent been administratively disabled?" module Puppet::Agent::Disabler # Let the daemon run again, freely in the filesystem. def enable disable_lockfile.unlock end # Stop the daemon from making any catalog runs. def disable(msg='') disable_lockfile.lock(msg) end - def disable_lockfile - @disable_lockfile ||= Puppet::Util::AnonymousFilelock.new(disabled_lockfile_path+".disabled") - - @disable_lockfile - end - def disabled? disable_lockfile.locked? end def disable_message disable_lockfile.message end + + + def disable_lockfile + @disable_lockfile ||= Puppet::Util::AnonymousFilelock.new(Puppet[:agent_disabled_lockfile]) + + @disable_lockfile + end + private :disable_lockfile end diff --git a/lib/puppet/agent/locker.rb b/lib/puppet/agent/locker.rb index dc9a9c449..3f624bcea 100644 --- a/lib/puppet/agent/locker.rb +++ b/lib/puppet/agent/locker.rb @@ -1,28 +1,33 @@ 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. +# This module is responsible for encapsulating the logic for +# "locking" the puppet agent during a run; in other words, +# keeping track of enough state to answer the question +# "is there a puppet agent currently running?" module Puppet::Agent::Locker + # 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 end end + def running? + lockfile.locked? and !lockfile.anonymous? + end + def lockfile - @lockfile ||= Puppet::Util::Pidlock.new(running_lockfile_path) + @lockfile ||= Puppet::Util::Pidlock.new(Puppet[:agent_running_lockfile]) @lockfile end + private :lockfile - def running? - lockfile.locked? and !lockfile.anonymous? - end end diff --git a/spec/unit/agent/disabler_spec.rb b/spec/unit/agent/disabler_spec.rb index 41708aa0e..f6323b993 100644 --- a/spec/unit/agent/disabler_spec.rb +++ b/spec/unit/agent/disabler_spec.rb @@ -1,60 +1,69 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/agent' require 'puppet/agent/locker' class DisablerTester include Puppet::Agent::Disabler end describe Puppet::Agent::Disabler do before do @disabler = DisablerTester.new @disabler.stubs(:disabled_lockfile_path).returns "/my/lock" end + + ## These tests are currently very implementation-specific, and they rely heavily on + ## having access to the "disable_lockfile" method. However, I've made this method private + ## because it really shouldn't be exposed outside of our implementation... therefore + ## these tests have to use a lot of ".send" calls. They should probably be cleaned up + ## but for the moment I wanted to make sure not to lose any of the functionality of + ## the tests. --cprice 2012-04-16 + it "should use an AnonymousFilelock instance as its disable_lockfile" do - @disabler.disable_lockfile.should be_instance_of(Puppet::Util::AnonymousFilelock) + @disabler.send(:disable_lockfile).should be_instance_of(Puppet::Util::AnonymousFilelock) end - it "should use 'lockfile_path' to determine its disable_lockfile path" do - @disabler.expects(:disabled_lockfile_path).returns "/my/lock" - lock = Puppet::Util::AnonymousFilelock.new("/my/lock") + + it "should use puppet's :agent_disabled_lockfile' setting to determine its lockfile path" do + Puppet.expects(:[]).with(:agent_disabled_lockfile).returns("/my/lock.disabled") + lock = Puppet::Util::AnonymousFilelock.new("/my/lock.disabled") Puppet::Util::AnonymousFilelock.expects(:new).with("/my/lock.disabled").returns lock - @disabler.disable_lockfile + @disabler.send(:disable_lockfile) end it "should reuse the same lock file each time" do - @disabler.disable_lockfile.should equal(@disabler.disable_lockfile) + @disabler.send(:disable_lockfile).should equal(@disabler.send(:disable_lockfile)) end it "should lock the anonymous lock when disabled" do - @disabler.disable_lockfile.expects(:lock) + @disabler.send(:disable_lockfile).expects(:lock) @disabler.disable end it "should disable with a message" do - @disabler.disable_lockfile.expects(:lock).with("disabled because") + @disabler.send(:disable_lockfile).expects(:lock).with("disabled because") @disabler.disable("disabled because") end it "should unlock the anonymous lock when enabled" do - @disabler.disable_lockfile.expects(:unlock) + @disabler.send(:disable_lockfile).expects(:unlock) @disabler.enable end it "should check the lock if it is disabled" do - @disabler.disable_lockfile.expects(:locked?) + @disabler.send(:disable_lockfile).expects(:locked?) @disabler.disabled? end it "should report the disable message when disabled" do - @disabler.disable_lockfile.expects(:message).returns("message") + @disabler.send(:disable_lockfile).expects(:message).returns("message") @disabler.disable_message.should == "message" end end diff --git a/spec/unit/agent/locker_spec.rb b/spec/unit/agent/locker_spec.rb index 2556c7e0a..79752720e 100755 --- a/spec/unit/agent/locker_spec.rb +++ b/spec/unit/agent/locker_spec.rb @@ -1,87 +1,94 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/agent' require 'puppet/agent/locker' class LockerTester include Puppet::Agent::Locker end describe Puppet::Agent::Locker do - before do + before do @locker = LockerTester.new @locker.stubs(:running_lockfile_path).returns "/my/lock" end + ## These tests are currently very implementation-specific, and they rely heavily on + ## having access to the lockfile object. However, I've made this method private + ## because it really shouldn't be exposed outside of our implementation... therefore + ## these tests have to use a lot of ".send" calls. They should probably be cleaned up + ## but for the moment I wanted to make sure not to lose any of the functionality of + ## the tests. --cprice 2012-04-16 + it "should use a Pidlock instance as its lockfile" do - @locker.lockfile.should be_instance_of(Puppet::Util::Pidlock) + @locker.send(:lockfile).should be_instance_of(Puppet::Util::Pidlock) end - it "should use 'lockfile_path' to determine its lockfile path" do - @locker.expects(:running_lockfile_path).returns "/my/lock" + it "should use puppet's :agent_running_lockfile' setting to determine its lockfile path" do + Puppet.expects(:[]).with(:agent_running_lockfile).returns("/my/lock") lock = Puppet::Util::Pidlock.new("/my/lock") Puppet::Util::Pidlock.expects(:new).with("/my/lock").returns lock - @locker.lockfile + @locker.send(:lockfile) end it "should reuse the same lock file each time" do - @locker.lockfile.should equal(@locker.lockfile) + @locker.send(:lockfile).should equal(@locker.send(:lockfile)) end it "should have a method that yields when a lock is attained" do - @locker.lockfile.expects(:lock).returns true + @locker.send(:lockfile).expects(:lock).returns true yielded = false @locker.lock do yielded = true end yielded.should be_true end it "should return the block result when the lock method successfully locked" do - @locker.lockfile.expects(:lock).returns true + @locker.send(:lockfile).expects(:lock).returns true @locker.lock { :result }.should == :result end it "should return nil when the lock method does not receive the lock" do - @locker.lockfile.expects(:lock).returns false + @locker.send(:lockfile).expects(:lock).returns false @locker.lock {}.should be_nil end it "should not yield when the lock method does not receive the lock" do - @locker.lockfile.expects(:lock).returns false + @locker.send(:lockfile).expects(:lock).returns false yielded = false @locker.lock { yielded = true } yielded.should be_false end it "should not unlock when a lock was not received" do - @locker.lockfile.expects(:lock).returns false - @locker.lockfile.expects(:unlock).never + @locker.send(:lockfile).expects(:lock).returns false + @locker.send(:lockfile).expects(:unlock).never @locker.lock {} end it "should unlock after yielding upon obtaining a lock" do - @locker.lockfile.stubs(:lock).returns true - @locker.lockfile.expects(:unlock) + @locker.send(:lockfile).stubs(:lock).returns true + @locker.send(:lockfile).expects(:unlock) @locker.lock {} end it "should unlock after yielding upon obtaining a lock, even if the block throws an exception" do - @locker.lockfile.stubs(:lock).returns true - @locker.lockfile.expects(:unlock) + @locker.send(:lockfile).stubs(:lock).returns true + @locker.send(:lockfile).expects(:unlock) lambda { @locker.lock { raise "foo" } }.should raise_error(RuntimeError) end it "should be considered running if the lockfile is locked" do - @locker.lockfile.expects(:locked?).returns true + @locker.send(:lockfile).expects(:locked?).returns true @locker.should be_running end end