diff --git a/lib/puppet/agent/disabler.rb b/lib/puppet/agent/disabler.rb index ff8ef4cae..0a31d1b76 100644 --- a/lib/puppet/agent/disabler.rb +++ b/lib/puppet/agent/disabler.rb @@ -1,33 +1,42 @@ require 'puppet/util/json_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) + def disable(msg=nil) + data = {} + if (! msg.nil?) + data["disabled_message"] = msg + end + disable_lockfile.lock(data) end def disabled? disable_lockfile.locked? end def disable_message - disable_lockfile.message + data = disable_lockfile.lock_data + return nil if data.nil? + if data.has_key?("disabled_message") + return data["disabled_message"] + end + nil end def disable_lockfile @disable_lockfile ||= Puppet::Util::JsonFilelock.new(Puppet[:agent_disabled_lockfile]) @disable_lockfile end private :disable_lockfile end diff --git a/lib/puppet/util/json_filelock.rb b/lib/puppet/util/json_filelock.rb index 4e789d96d..920e0238c 100644 --- a/lib/puppet/util/json_filelock.rb +++ b/lib/puppet/util/json_filelock.rb @@ -1,32 +1,32 @@ +require 'puppet/util/lockfile' -class Puppet::Util::JsonFilelock - attr_reader :lockfile - - def initialize(lockfile) - @lockfile = lockfile - end - - def lock(msg = '') +class Puppet::Util::JsonFilelock < Puppet::Util::Lockfile + # Lock the lockfile. You may optionally pass a data object, which will be + # retrievable for the duration of time during which the file is locked. + # + # @param [Hash] lock_data an optional Hash of data to associate with the lock. + # This may be used to store pids, descriptive messages, etc. The + # data may be retrieved at any time while the lock is held by + # calling the #lock_data method. NOTE that the JSON serialization + # does NOT support Symbol objects--if you pass them in, they will be + # serialized as Strings, so you should plan accordingly. + # @return [boolean] true if lock is successfully acquired, false otherwise. + def lock(lock_data = nil) return false if locked? - File.open(@lockfile, 'w') { |fd| fd.print(msg) } - true + super(lock_data.to_pson) end - def unlock - if locked? - File.unlink(@lockfile) - true - else - false - end + # Retrieve the (optional) lock data that was specified at the time the file + # was locked. + # @return [Object] the data object. Remember that the serialization does not + # support Symbol objects, so if your data Object originally contained symbols, + # they will be converted to Strings. + def lock_data + return nil unless file_locked? + file_contents = super + return nil if file_contents.nil? + PSON.parse(file_contents) 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/lockfile.rb b/lib/puppet/util/lockfile.rb new file mode 100644 index 000000000..05362283f --- /dev/null +++ b/lib/puppet/util/lockfile.rb @@ -0,0 +1,55 @@ + +class Puppet::Util::Lockfile + attr_reader :file_path + + def initialize(file_path) + @file_path = file_path + end + + # Lock the lockfile. You may optionally pass a data object, which will be + # retrievable for the duration of time during which the file is locked. + # + # @param [String] lock_data an optional String data object to associate + # with the lock. This may be used to store pids, descriptive messages, + # etc. The data may be retrieved at any time while the lock is held by + # calling the #lock_data method. + + # @return [boolean] true if lock is successfully acquired, false otherwise. + def lock(lock_data = nil) + return false if locked? + + File.open(@file_path, 'w') { |fd| fd.print(lock_data) } + true + end + + def unlock + if locked? + File.unlink(@file_path) + true + else + false + end + end + + def locked? + # delegate logic to a more explicit private method + file_locked? + end + + # Retrieve the (optional) lock data that was specified at the time the file + # was locked. + # @return [String] the data object. + def lock_data + return File.read(@file_path) if file_locked? + end + + # Private, internal utility method for encapsulating the logic about + # whether or not the file is locked. This method can be called + # by other methods in this class without as much risk of accidentally + # being overridden by child classes. + # @return [boolean] true if the file is locked, false if it is not. + def file_locked?() + File.exists? @file_path + end + private :file_locked? +end \ No newline at end of file diff --git a/lib/puppet/util/pidlock.rb b/lib/puppet/util/pidlock.rb index f48353611..33c6bf14f 100644 --- a/lib/puppet/util/pidlock.rb +++ b/lib/puppet/util/pidlock.rb @@ -1,54 +1,53 @@ require 'fileutils' -require 'puppet/util/json_filelock' +require 'puppet/util/lockfile' -class Puppet::Util::Pidlock < Puppet::Util::JsonFilelock +class Puppet::Util::Pidlock + + def initialize(lockfile) + @lockfile = Puppet::Util::Lockfile.new(lockfile) + end def locked? clear_if_stale - File.exists? @lockfile + @lockfile.locked? end def mine? Process.pid == lock_pid end def lock return mine? if locked? - File.open(@lockfile, "w") { |fd| fd.write(Process.pid) } - true + @lockfile.lock(Process.pid) end def unlock() if mine? - File.unlink(@lockfile) - true + return @lockfile.unlock else false end end def lock_pid - if File.exists? @lockfile - File.read(@lockfile).to_i - else - nil - end + @lockfile.lock_data.to_i 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) + @lockfile.unlock end end + private :clear_if_stale end diff --git a/spec/unit/agent/disabler_spec.rb b/spec/unit/agent/disabler_spec.rb index c9b290122..ad5b662c6 100644 --- a/spec/unit/agent/disabler_spec.rb +++ b/spec/unit/agent/disabler_spec.rb @@ -1,69 +1,68 @@ #!/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 JsonFilelock instance as its disable_lockfile" do @disabler.send(:disable_lockfile).should be_instance_of(Puppet::Util::JsonFilelock) end 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::JsonFilelock.new("/my/lock.disabled") Puppet::Util::JsonFilelock.expects(:new).with("/my/lock.disabled").returns lock @disabler.send(:disable_lockfile) end it "should reuse the same lock file each time" do @disabler.send(:disable_lockfile).should equal(@disabler.send(:disable_lockfile)) end it "should lock the file when disabled" do @disabler.send(:disable_lockfile).expects(:lock) @disabler.disable end - it "should disable with a message" do - @disabler.send(:disable_lockfile).expects(:lock).with("disabled because") - - @disabler.disable("disabled because") - end - it "should unlock the file when enabled" do @disabler.send(:disable_lockfile).expects(:unlock) @disabler.enable end it "should check the lock if it is disabled" do @disabler.send(:disable_lockfile).expects(:locked?) @disabler.disabled? end it "should report the disable message when disabled" do - @disabler.send(:disable_lockfile).expects(:message).returns("message") - @disabler.disable_message.should == "message" + lockfile = PuppetSpec::Files.tmpfile("lock") + lock = Puppet::Util::JsonFilelock.new(lockfile) + Puppet.expects(:[]).with(:agent_disabled_lockfile).returns("/my/lock.disabled") + Puppet::Util::JsonFilelock.expects(:new).with("/my/lock.disabled").returns lock + + msg = "I'm busy, go away" + @disabler.disable(msg) + @disabler.disable_message.should == msg end end diff --git a/spec/unit/agent/locker_spec.rb b/spec/unit/agent/locker_spec.rb index 79752720e..678d7fe2a 100755 --- a/spec/unit/agent/locker_spec.rb +++ b/spec/unit/agent/locker_spec.rb @@ -1,94 +1,93 @@ #!/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(: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.send(:lockfile).should be_instance_of(Puppet::Util::Pidlock) end 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.send(:lockfile) end it "should reuse the same lock file each time" do @locker.send(:lockfile).should equal(@locker.send(:lockfile)) end it "should have a method that yields when a lock is attained" do @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.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.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.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.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.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.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.send(:lockfile).expects(:locked?).returns true @locker.should be_running end end diff --git a/spec/unit/util/json_filelock_spec.rb b/spec/unit/util/json_filelock_spec.rb index ce1418641..3a72e4fca 100644 --- a/spec/unit/util/json_filelock_spec.rb +++ b/spec/unit/util/json_filelock_spec.rb @@ -1,74 +1,29 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/util/json_filelock' describe Puppet::Util::JsonFilelock do require 'puppet_spec/files' include PuppetSpec::Files before(:each) do @lockfile = tmpfile("lock") @lock = Puppet::Util::JsonFilelock.new(@lockfile) 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 + it "should create a lock file containing a json hash" do + data = { "foo" => "foofoo", "bar" => "barbar" } + @lock.lock(data) - describe "#unlock" do - it "should return true when unlocking" do - @lock.lock - @lock.unlock.should be_true + PSON.parse(File.read(@lockfile)).should == data 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" + it "should return the lock data" do + data = { "foo" => "foofoo", "bar" => "barbar" } + @lock.lock(data) + @lock.lock_data.should == data end end \ No newline at end of file diff --git a/spec/unit/util/json_filelock_spec.rb b/spec/unit/util/lockfile_spec.rb similarity index 75% copy from spec/unit/util/json_filelock_spec.rb copy to spec/unit/util/lockfile_spec.rb index ce1418641..5c3261fe3 100644 --- a/spec/unit/util/json_filelock_spec.rb +++ b/spec/unit/util/lockfile_spec.rb @@ -1,74 +1,76 @@ #!/usr/bin/env rspec require 'spec_helper' -require 'puppet/util/json_filelock' +require 'puppet/util/lockfile' -describe Puppet::Util::JsonFilelock do +describe Puppet::Util::Lockfile do require 'puppet_spec/files' include PuppetSpec::Files before(:each) do @lockfile = tmpfile("lock") - @lock = Puppet::Util::JsonFilelock.new(@lockfile) + @lock = Puppet::Util::Lockfile.new(@lockfile) 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") + it "should create a lock file containing a string" do + data = "foofoo barbar" + @lock.lock(data) - File.read(@lockfile).should == "message" + File.read(@lockfile).should == data 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" + it "should return the lock data" do + data = "foofoo barbar" + @lock.lock(data) + @lock.lock_data.should == data end end \ No newline at end of file diff --git a/spec/unit/util/pidlock_spec.rb b/spec/unit/util/pidlock_spec.rb index 643fd1f76..20f8649b8 100644 --- a/spec/unit/util/pidlock_spec.rb +++ b/spec/unit/util/pidlock_spec.rb @@ -1,178 +1,179 @@ #!/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 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 + # fake our pid to be 1234 + Process.stubs(:pid).returns(1234) + # lock the file + @lock.lock + # fake our pid to be a different pid, to simulate someone else + # holding the lock + Process.stubs(:pid).returns(6789) + 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 + # fake our pid to be 1234 + Process.stubs(:pid).returns(1234) + # lock the file + @lock.lock + # fake our pid to be a different pid, to simulate someone else + # holding the lock + Process.stubs(:pid).returns(6789) + 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