diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb index 47dd44a0e..483905cac 100644 --- a/lib/puppet/agent.rb +++ b/lib/puppet/agent.rb @@ -1,107 +1,111 @@ 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 "Run of #{client_class} is administratively disabled; skipping" + 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; 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..238e83264 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) + Puppet::Util::AnonymousFilelock.new(lockfile_path + ".disabled").unlock end # Stop the daemon from making any catalog runs. def disable - lockfile.lock(:anonymous => true) + Puppet::Util::AnonymousFilelock.new(lockfile_path + ".disabled").lock + end + + def disabled? + Puppet::Util::AnonymousFilelock.new(lockfile_path + ".disabled").locked? 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? end end diff --git a/lib/puppet/util/anonymous_filelock.rb b/lib/puppet/util/anonymous_filelock.rb new file mode 100644 index 000000000..ff09c5d12 --- /dev/null +++ b/lib/puppet/util/anonymous_filelock.rb @@ -0,0 +1,36 @@ + +class Puppet::Util::AnonymousFilelock + attr_reader :lockfile + + def initialize(lockfile) + @lockfile = lockfile + end + + def anonymous? + true + end + + def lock(msg = '') + return false if locked? + + File.open(@lockfile, 'w') { |fd| fd.print(msg) } + true + end + + def unlock + if locked? + File.unlink(@lockfile) + true + else + false + end + end + + def locked? + File.exists? @lockfile + end + + def message + return File.read(@lockfile) if locked? + end +end \ No newline at end of file diff --git a/lib/puppet/util/pidlock.rb b/lib/puppet/util/pidlock.rb index fcf0cf296..9ed86352d 100644 --- a/lib/puppet/util/pidlock.rb +++ b/lib/puppet/util/pidlock.rb @@ -1,72 +1,57 @@ require 'fileutils' +require 'puppet/util/anonymous_filelock' -class Puppet::Util::Pidlock - attr_reader :lockfile - - def initialize(lockfile) - @lockfile = lockfile - end +class Puppet::Util::Pidlock < Puppet::Util::AnonymousFilelock def locked? clear_if_stale File.exists? @lockfile end def mine? Process.pid == lock_pid end def anonymous? - return false unless File.exists?(@lockfile) - File.read(@lockfile) == "" + false end - def lock(opts = {}) - opts = {:anonymous => false}.merge(opts) + def lock + return mine? if locked? - if locked? - mine? - else - if opts[:anonymous] - File.open(@lockfile, 'w') { |fd| true } - else - File.open(@lockfile, "w") { |fd| fd.write(Process.pid) } - end - true - end + File.open(@lockfile, "w") { |fd| fd.write(Process.pid) } + true end def unlock(opts = {}) - opts = {:anonymous => false}.merge(opts) - - if mine? or (opts[:anonymous] and anonymous?) + if mine? File.unlink(@lockfile) true else false end end - private def lock_pid if File.exists? @lockfile File.read(@lockfile).to_i else nil end end + private def clear_if_stale return if lock_pid.nil? errors = [Errno::ESRCH] # Process::Error can only happen, and is only defined, on Windows errors << Process::Error if defined? Process::Error begin Process.kill(0, lock_pid) rescue *errors File.unlink(@lockfile) end end end diff --git a/spec/unit/agent/locker_spec.rb b/spec/unit/agent/locker_spec.rb index 341859e3b..9d0b07639 100755 --- a/spec/unit/agent/locker_spec.rb +++ b/spec/unit/agent/locker_spec.rb @@ -1,99 +1,103 @@ #!/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 @locker = LockerTester.new @locker.stubs(:lockfile_path).returns "/my/lock" end it "should use a Pidlock instance as its lockfile" do @locker.lockfile.should be_instance_of(Puppet::Util::Pidlock) end it "should use 'lockfile_path' to determine its lockfile path" do @locker.expects(:lockfile_path).returns "/my/lock" lock = Puppet::Util::Pidlock.new("/my/lock") Puppet::Util::Pidlock.expects(:new).with("/my/lock").returns lock @locker.lockfile end it "should reuse the same lock file each time" do @locker.lockfile.should equal(@locker.lockfile) end it "should use the lock file to anonymously lock the process when disabled" do - @locker.lockfile.expects(:lock).with(:anonymous => true) + lock = stub 'lock' + Puppet::Util::AnonymousFilelock.expects(:new).with("/my/lock.disabled").returns lock + lock.expects(:lock) @locker.disable end it "should use the lock file to anonymously unlock the process when enabled" do - @locker.lockfile.expects(:unlock).with(:anonymous => true) + lock = stub 'lock' + Puppet::Util::AnonymousFilelock.expects(:new).with("/my/lock.disabled").returns lock + lock.expects(:unlock) @locker.enable end it "should have a method that yields when a lock is attained" do @locker.lockfile.expects(:lock).returns true yielded = false @locker.lock do yielded = true end yielded.should be_true end it "should return true when the lock method successfully locked" do @locker.lockfile.expects(:lock).returns true @locker.lock {}.should be_true end it "should return true when the lock method does not receive the lock" do @locker.lockfile.expects(:lock).returns false @locker.lock {}.should be_false end it "should not yield when the lock method does not receive the lock" do @locker.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.lock {} end it "should unlock after yielding upon obtaining a lock" do @locker.lockfile.stubs(:lock).returns true @locker.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) 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.should be_running end end diff --git a/spec/unit/agent_spec.rb b/spec/unit/agent_spec.rb index d955868a0..c40dd7f19 100755 --- a/spec/unit/agent_spec.rb +++ b/spec/unit/agent_spec.rb @@ -1,277 +1,284 @@ #!/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 + @agent.stubs(:disabled?).returns(false) # 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.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 lockfile = mock 'lockfile' @agent.expects(:lockfile).returns lockfile lockfile.expects(:locked?).returns true @agent.should be_running end describe "when being run" do before do @agent.stubs(:running?).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 "should do nothing if disabled" do + @agent.expects(:disabled?).returns(true) + AgentTestClient.expects(:new).never + @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 diff --git a/spec/unit/util/anonymous_filelock_spec.rb b/spec/unit/util/anonymous_filelock_spec.rb new file mode 100644 index 000000000..784ac0fca --- /dev/null +++ b/spec/unit/util/anonymous_filelock_spec.rb @@ -0,0 +1,78 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/anonymous_filelock' + +describe Puppet::Util::AnonymousFilelock do + require 'puppet_spec/files' + include PuppetSpec::Files + + before(:each) do + @lockfile = tmpfile("lock") + @lock = Puppet::Util::AnonymousFilelock.new(@lockfile) + end + + it "should be anonymous" do + @lock.should be_anonymous + end + + describe "#lock" do + it "should return false if already locked" do + @lock.stubs(:locked?).returns(true) + @lock.lock.should be_false + end + + it "should return true if it successfully locked" do + @lock.lock.should be_true + end + + it "should create a lock file" do + @lock.lock + + File.should be_exists(@lockfile) + end + + it "should create a lock file containing a message" do + @lock.lock("message") + + File.read(@lockfile).should == "message" + end + end + + describe "#unlock" do + it "should return true when unlocking" do + @lock.lock + @lock.unlock.should be_true + end + + it "should return false when not locked" do + @lock.unlock.should be_false + end + + it "should clear the lock file" do + File.open(@lockfile, 'w') { |fd| fd.print("locked") } + @lock.unlock + File.should_not be_exists(@lockfile) + end + end + + it "should be locked when locked" do + @lock.lock + @lock.should be_locked + end + + it "should not be locked when not locked" do + @lock.should_not be_locked + end + + it "should not be locked when unlocked" do + @lock.lock + @lock.unlock + @lock.should_not be_locked + end + + it "should return the lock message" do + @lock.lock("lock message") + @lock.message.should == "lock message" + end +end \ No newline at end of file diff --git a/spec/unit/util/pidlock_spec.rb b/spec/unit/util/pidlock_spec.rb new file mode 100644 index 000000000..37f3ca4f4 --- /dev/null +++ b/spec/unit/util/pidlock_spec.rb @@ -0,0 +1,182 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/pidlock' + +describe Puppet::Util::Pidlock do + require 'puppet_spec/files' + include PuppetSpec::Files + + before(:each) do + @lockfile = tmpfile("lock") + @lock = Puppet::Util::Pidlock.new(@lockfile) + end + + it "should not be anonymous" do + @lock.should_not be_anonymous + end + + describe "#lock" do + it "should not be locked at start" do + @lock.should_not be_locked + end + + it "should not be mine at start" do + @lock.should_not be_mine + end + + it "should become locked" do + @lock.lock + @lock.should be_locked + end + + it "should become mine" do + @lock.lock + @lock.should be_mine + end + + it "should be possible to lock multiple times" do + @lock.lock + lambda { @lock.lock }.should_not raise_error + end + + it "should return true when locking" do + @lock.lock.should be_true + end + + it "should return true if locked by me" do + @lock.lock + @lock.lock.should be_true + end + + it "should return false if locked by someone else" do + Process.stubs(:kill) + File.open(@lockfile, "w") { |fd| fd.print('0') } + + @lock.lock.should be_false + end + + it "should create a lock file" do + @lock.lock + File.should be_exists(@lockfile) + end + + it "should create a lock file containing our pid" do + @lock.lock + File.read(@lockfile).to_i.should == Process.pid.to_i + end + end + + describe "#unlock" do + it "should not be locked anymore" do + @lock.lock + @lock.unlock + @lock.should_not be_locked + end + + it "should return false if not locked" do + @lock.unlock.should be_false + end + + it "should return true if properly unlocked" do + @lock.lock + @lock.unlock.should be_true + end + + it "should get rid of the lock file" do + @lock.lock + @lock.unlock + File.should_not be_exists(@lockfile) + end + end + + describe "#locked?" do + it "should return true if locked" do + @lock.lock + @lock.should be_locked + end + end + + describe "with a stale lock" do + before(:each) do + Process.stubs(:kill).with(0, 6789) + Process.stubs(:kill).with(0, 1234).raises(Errno::ESRCH) + Process.stubs(:pid).returns(6789) + File.open(@lockfile, 'w') { |fd| fd.write("1234") } + end + + it "should not be locked" do + @lock.should_not be_locked + end + + describe "#lock" do + it "should clear stale locks" do + @lock.locked? + File.should_not be_exists(@lockfile) + end + + it "should replace with new locks" do + @lock.lock + File.should be_exists(@lockfile) + @lock.lock_pid.should == 6789 + @lock.should be_mine + @lock.should be_locked + end + end + + describe "#unlock" do + it "should not be allowed" do + @lock.unlock.should be_false + end + + it "should not remove the lock file" do + @lock.unlock + File.should be_exists(@lockfile) + end + end + end + + describe "with another process lock" do + before(:each) do + Process.stubs(:kill).with(0, 6789) + Process.stubs(:kill).with(0, 1234) + Process.stubs(:pid).returns(6789) + File.open(@lockfile, 'w') { |fd| fd.write("1234") } + end + + it "should be locked" do + @lock.should be_locked + end + + it "should not be mine" do + @lock.should_not be_mine + end + + describe "#lock" do + it "should not be possible" do + @lock.lock.should be_false + end + + it "should not overwrite the lock" do + @lock.lock + @lock.should_not be_mine + end + end + + describe "#unlock" do + it "should not be possible" do + @lock.unlock.should be_false + end + + it "should not remove the lock file" do + @lock.unlock + File.should be_exists(@lockfile) + end + + it "should still not be our lock" do + @lock.unlock + @lock.should_not be_mine + end + end + end +end \ No newline at end of file diff --git a/test/util/pidlock.rb b/test/util/pidlock.rb deleted file mode 100755 index beaff1089..000000000 --- a/test/util/pidlock.rb +++ /dev/null @@ -1,126 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest') - -require 'puppet/util/pidlock' -require 'fileutils' - -# This is *fucked* *up* -Puppet.debug = false - -class TestPuppetUtilPidlock < Test::Unit::TestCase - include PuppetTest - - def setup - super - @workdir = tstdir - end - - def teardown - super - FileUtils.rm_rf(@workdir) - end - - def test_00_basic_create - l = nil - assert_nothing_raised { l = Puppet::Util::Pidlock.new(@workdir + '/nothingmuch') } - - assert_equal Puppet::Util::Pidlock, l.class - - assert_equal @workdir + '/nothingmuch', l.lockfile - end - - def test_10_uncontended_lock - l = Puppet::Util::Pidlock.new(@workdir + '/test_lock') - - assert !l.locked? - assert !l.mine? - assert l.lock - assert l.locked? - assert l.mine? - assert !l.anonymous? - # It's OK to call lock multiple times - assert l.lock - assert l.unlock - assert !l.locked? - assert !l.mine? - end - - def test_20_someone_elses_lock - childpid = nil - l = Puppet::Util::Pidlock.new(@workdir + '/someone_elses_lock') - - # First, we need a PID that's guaranteed to be (a) used, (b) someone - # else's, and (c) around for the life of this test. - childpid = fork { loop do; sleep 10; end } - - File.open(l.lockfile, 'w') { |fd| fd.write(childpid) } - - assert l.locked? - assert !l.mine? - assert !l.lock - assert l.locked? - assert !l.mine? - assert !l.unlock - assert l.locked? - assert !l.mine? - ensure - Process.kill("KILL", childpid) unless childpid.nil? - end - - def test_30_stale_lock - # This is a bit hard to guarantee, but we need a PID that is definitely - # unused, and will stay so for the the life of this test. Our best - # bet is to create a process, get it's PID, let it die, and *then* - # lock on it. - childpid = fork { exit } - - # Now we can't continue until we're sure that the PID is dead - Process.wait(childpid) - - l = Puppet::Util::Pidlock.new(@workdir + '/stale_lock') - - # locked? should clear the lockfile - File.open(l.lockfile, 'w') { |fd| fd.write(childpid) } - assert File.exists?(l.lockfile) - assert !l.locked? - assert !File.exists?(l.lockfile) - - # lock should replace the lockfile with our own - File.open(l.lockfile, 'w') { |fd| fd.write(childpid) } - assert File.exists?(l.lockfile) - assert l.lock - assert l.locked? - assert l.mine? - - # unlock should fail, and should *not* molest the existing lockfile, - # despite it being stale - File.open(l.lockfile, 'w') { |fd| fd.write(childpid) } - assert File.exists?(l.lockfile) - assert !l.unlock - assert File.exists?(l.lockfile) - end - - def test_40_not_locked_at_all - l = Puppet::Util::Pidlock.new(@workdir + '/not_locked') - - assert !l.locked? - # We can't unlock if we don't hold the lock - assert !l.unlock - end - - def test_50_anonymous_lock - l = Puppet::Util::Pidlock.new(@workdir + '/anonymous_lock') - - assert !l.locked? - assert l.lock(:anonymous => true) - assert l.locked? - assert l.anonymous? - assert !l.mine? - assert "", File.read(l.lockfile) - assert !l.unlock - assert l.locked? - assert l.anonymous? - assert l.unlock(:anonymous => true) - assert !File.exists?(l.lockfile) - end -end -