diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index a5f3c5b1a..f8a872122 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,475 +1,443 @@ # A module to collect utility functions. require 'sync' require 'puppet/external/lock' module Puppet # A command failed to execute. class ExecutionFailure < Puppet::Error end module Util require 'benchmark' + # These are all for backward compatibility -- these are methods that used + # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX # Create a hash to store the different sync objects. @@syncresources = {} # Return the sync object associated with a given resource. def self.sync(resource) @@syncresources[resource] ||= Sync.new return @@syncresources[resource] end # Change the process to a different user def self.chuser if Facter["operatingsystem"].value == "Darwin" $stderr.puts "Ruby on darwin is broken; puppetmaster will not set its UID to 'puppet' and must run as root" return end if group = Puppet[:group] group = self.gid(group) unless group raise Puppet::Error, "No such group %s" % Puppet[:group] end unless Puppet::Util::SUIDManager.gid == group begin Puppet::Util::SUIDManager.egid = group Puppet::Util::SUIDManager.gid = group rescue => detail Puppet.warning "could not change to group %s: %s" % [group.inspect, detail] $stderr.puts "could not change to group %s" % group.inspect # Don't exit on failed group changes, since it's # not fatal #exit(74) end end end if user = Puppet[:user] user = self.uid(user) unless user raise Puppet::Error, "No such user %s" % Puppet[:user] end unless Puppet::Util::SUIDManager.uid == user begin Puppet::Util::SUIDManager.uid = user Puppet::Util::SUIDManager.euid = user rescue $stderr.puts "could not change to user %s" % user exit(74) end end end end - # Create a shared lock for reading - def self.readlock(file) - self.sync(file).synchronize(Sync::SH) do - File.open(file) { |f| - f.lock_shared { |lf| yield lf } - } - end - end - - # Create an exclusive lock for writing, and do the writing in a - # tmp file. - def self.writelock(file, mode = 0600) - tmpfile = file + ".tmp" - unless FileTest.directory?(File.dirname(tmpfile)) - raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % - [file, File.dirname(file)] - end - self.sync(file).synchronize(Sync::EX) do - File.open(file, "w", mode) do |rf| - rf.lock_exclusive do |lrf| - File.open(tmpfile, "w", mode) do |tf| - yield tf - end - begin - File.rename(tmpfile, file) - rescue => detail - Puppet.err "Could not rename %s to %s: %s" % - [file, tmpfile, detail] - end - end - end - end - end - # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| if args.is_a?(Array) args = args.join(" ") end if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) Dir.mkdir(File.join(path), mode) elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise "Cannot create %s: basedir %s is a file" % [dir, File.join(path)] end } return true end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end unless level raise Puppet::DevError, "Failed to provide level to :benchmark" end unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to %s" % level end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def binary(bin) if bin =~ /^\// if FileTest.file? bin and FileTest.executable? bin return bin else return nil end else x = %x{which #{bin} 2>/dev/null}.chomp if x == "" return nil else return x end end end module_function :binary # Execute the provided command in a pipe, yielding the pipe object. def execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '%s'" % command else Puppet.debug "Executing '%s'" % command end output = open("| #{command} 2>&1") do |pipe| yield pipe end if failonfail unless $? == 0 raise ExecutionFailure, output end end return output end def execfail(command, exception) begin output = execute(command) return output rescue ExecutionFailure raise exception, output end end # Execute the desired command, and return the status and output. # def execute(command, failonfail = true, uid = nil, gid = nil) # :combine sets whether or not to combine stdout/stderr in the output # :stdinfile sets a file that can be used for stdin. Passing a string # for stdin is not currently supported. def execute(command, arguments = {:failonfail => true, :combine => true}) if command.is_a?(Array) command = command.flatten.collect { |i| i.to_s } str = command.join(" ") else # We require an array here so we know where we're incorrectly # using a string instead of an array. Once everything is # switched to an array, we might relax this requirement. raise ArgumentError, "Must pass an array to execute()" end if respond_to? :debug debug "Executing '%s'" % str else Puppet.debug "Executing '%s'" % str end if arguments[:uid] arguments[:uid] = Puppet::Util::SUIDManager.convert_xid(:uid, arguments[:uid]) end if arguments[:gid] arguments[:gid] = Puppet::Util::SUIDManager.convert_xid(:gid, arguments[:gid]) end @@os ||= Facter.value(:operatingsystem) output = nil child_pid, child_status = nil # There are problems with read blocking with badly behaved children # read.partialread doesn't seem to capture either stdout or stderr # We hack around this using a temporary file # The idea here is to avoid IO#read whenever possible. output_file="/dev/null" error_file="/dev/null" if ! arguments[:squelch] require "tempfile" output_file = Tempfile.new("puppet") if arguments[:combine] error_file=output_file end end oldverb = $VERBOSE $VERBOSE = false child_pid = Kernel.fork $VERBOSE = oldverb if child_pid # Parent process executes this child_status = (Process.waitpid2(child_pid)[1]).to_i >> 8 else # Child process executes this Process.setsid begin if arguments[:stdinfile] $stdin.reopen(arguments[:stdinfile]) else $stdin.reopen("/dev/null") end $stdout.reopen(output_file) $stderr.reopen(error_file) 3.upto(256){|fd| IO::new(fd).close rescue nil} if arguments[:gid] Process.egid = arguments[:gid] Process.gid = arguments[:gid] unless @@os == "Darwin" end if arguments[:uid] Process.euid = arguments[:uid] Process.uid = arguments[:uid] unless @@os == "Darwin" end ENV['LANG'] = ENV['LC_ALL'] = ENV['LC_MESSAGES'] = ENV['LANGUAGE'] = 'C' if command.is_a?(Array) Kernel.exec(*command) else Kernel.exec(command) end rescue => detail puts detail.to_s exit!(1) end # begin; rescue end # if child_pid # read output in if required if ! arguments[:squelch] # Make sure the file's actually there. This is # basically a race condition, and is probably a horrible # way to handle it, but, well, oh well. unless FileTest.exists?(output_file.path) Puppet.warning "sleeping" sleep 0.5 unless FileTest.exists?(output_file.path) Puppet.warning "sleeping 2" sleep 1 unless FileTest.exists?(output_file.path) Puppet.warning "Could not get output" output = "" end end end unless output # We have to explicitly open here, so that it reopens # after the child writes. output = output_file.open.read # The 'true' causes the file to get unlinked right away. output_file.close(true) end end if arguments[:failonfail] unless child_status == 0 raise ExecutionFailure, "Execution of '%s' returned %s: %s" % [str, child_status, output] end end return output end module_function :execute # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.sync(resource).synchronize(type) do yield end end # Because some modules provide their own version of this method. alias util_execute execute module_function :benchmark def memory unless defined? @pmap pmap = %x{which pmap 2>/dev/null}.chomp if $? != 0 or pmap =~ /^no/ @pmap = nil else @pmap = pmap end end if @pmap return %x{pmap #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end return hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } return seconds end module_function :memory, :thinmark end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/file_locking.rb b/lib/puppet/util/file_locking.rb new file mode 100644 index 000000000..80a0b2b0c --- /dev/null +++ b/lib/puppet/util/file_locking.rb @@ -0,0 +1,47 @@ +require 'puppet/util' + +module Puppet::Util::FileLocking + module_function + + # Create a shared lock for reading + def readlock(file) + Puppet::Util.sync(file).synchronize(Sync::SH) do + File.open(file) { |f| + f.lock_shared { |lf| yield lf } + } + end + end + + # Create an exclusive lock for writing, and do the writing in a + # tmp file. + def writelock(file, mode = nil) + unless FileTest.directory?(File.dirname(file)) + raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % [file, File.dirname(file)] + end + tmpfile = file + ".tmp" + + unless mode + begin + mode = File.stat(file).mode + rescue + mode = 0600 + end + end + + Puppet::Util.sync(file).synchronize(Sync::EX) do + File.open(file, "w", mode) do |rf| + rf.lock_exclusive do |lrf| + File.open(tmpfile, "w", mode) do |tf| + yield tf + end + begin + File.rename(tmpfile, file) + rescue => detail + File.unlink(tmpfile) if File.exist?(tmpfile) + raise Puppet::Error, "Could not rename %s to %s: %s; file %s was unchanged" % [file, tmpfile, Thread.current.object_id, detail, file] + end + end + end + end + end +end diff --git a/lib/puppet/util/storage.rb b/lib/puppet/util/storage.rb index dc4e9cd71..01c411181 100644 --- a/lib/puppet/util/storage.rb +++ b/lib/puppet/util/storage.rb @@ -1,105 +1,107 @@ require 'yaml' require 'sync' +require 'puppet/util/file_locking' + # a class for storing state class Puppet::Util::Storage include Singleton include Puppet::Util def self.state return @@state end def initialize self.class.load end # Return a hash that will be stored to disk. It's worth noting # here that we use the object's full path, not just the name/type # combination. At the least, this is useful for those non-isomorphic # types like exec, but it also means that if an object changes locations # in the configuration it will lose its cache. def self.cache(object) if object.is_a? Puppet::Type # We used to store things by path, now we store them by ref. # In oscar(0.20.0) this changed to using the ref. if @@state.include?(object.path) @@state[object.ref] = @@state[object.path] @@state.delete(object.path) end name = object.ref elsif object.is_a?(Symbol) name = object else raise ArgumentError, "You can only cache information for Types and symbols" end return @@state[name] ||= {} end def self.clear @@state.clear Storage.init end def self.init @@state = {} @@splitchar = "\t" end self.init def self.load Puppet.settings.use(:main) unless FileTest.directory?(Puppet[:statedir]) unless File.exists?(Puppet[:statefile]) unless defined? @@state and ! @@state.nil? self.init end return end Puppet::Util.benchmark(:debug, "Loaded state") do - Puppet::Util.readlock(Puppet[:statefile]) do |file| + Puppet::Util::FileLocking.readlock(Puppet[:statefile]) do |file| begin @@state = YAML.load(file) rescue => detail Puppet.err "Checksumfile %s is corrupt (%s); replacing" % [Puppet[:statefile], detail] begin File.rename(Puppet[:statefile], Puppet[:statefile] + ".bad") rescue raise Puppet::Error, "Could not rename corrupt %s; remove manually" % Puppet[:statefile] end end end end unless @@state.is_a?(Hash) Puppet.err "State got corrupted" self.init end #Puppet.debug "Loaded state is %s" % @@state.inspect end def self.stateinspect @@state.inspect end def self.store Puppet.debug "Storing state" unless FileTest.exist?(Puppet[:statefile]) Puppet.info "Creating state file %s" % Puppet[:statefile] end Puppet::Util.benchmark(:debug, "Stored state") do - Puppet::Util.writelock(Puppet[:statefile], 0660) do |file| + Puppet::Util::FileLocking.writelock(Puppet[:statefile], 0660) do |file| file.print YAML.dump(@@state) end end end end diff --git a/spec/integration/util/file_locking.rb b/spec/integration/util/file_locking.rb new file mode 100755 index 000000000..171c57a5b --- /dev/null +++ b/spec/integration/util/file_locking.rb @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +require 'puppet/util/file_locking' + +describe Puppet::Util::FileLocking do + it "should be able to keep file corruption from happening when there are multiple writers" do + file = Tempfile.new("puppetspec") + file.close!() + file = file.path + File.open(file, "w") { |f| f.puts "starting" } + + value = {:a => :b} + threads = [] + sync = Sync.new + 9.times { |a| + threads << Thread.new { + 9.times { |b| + sync.synchronize(Sync::SH) { + Puppet::Util::FileLocking.readlock(file) { |f| + f.read + } + } + sleep 0.01 + sync.synchronize(Sync::EX) { + Puppet::Util::FileLocking.writelock(file) { |f| + f.puts "%s %s" % [a, b] + } + } + } + } + } + threads.each { |th| th.join } + end +end diff --git a/spec/unit/util/file_locking.rb b/spec/unit/util/file_locking.rb new file mode 100755 index 000000000..a8b0c1840 --- /dev/null +++ b/spec/unit/util/file_locking.rb @@ -0,0 +1,146 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +require 'puppet/util/file_locking' + +class FileLocker + include Puppet::Util::FileLocking +end + +describe Puppet::Util::FileLocking do + it "should have a module method for getting a read lock on files" do + Puppet::Util::FileLocking.should respond_to(:readlock) + end + + it "should have a module method for getting a write lock on files" do + Puppet::Util::FileLocking.should respond_to(:writelock) + end + + it "should have an instance method for getting a read lock on files" do + FileLocker.new.private_methods.should be_include("readlock") + end + + it "should have an instance method for getting a write lock on files" do + FileLocker.new.private_methods.should be_include("writelock") + end + + describe "when acquiring a read lock" do + it "should use a global shared mutex" do + sync = mock 'sync' + sync.expects(:synchronize).with(Sync::SH) + Puppet::Util.expects(:sync).with("/file").returns sync + + Puppet::Util::FileLocking.readlock '/file' + end + + it "should use a shared lock on the file" do + sync = mock 'sync' + sync.expects(:synchronize).yields + Puppet::Util.expects(:sync).with("/file").returns sync + + fh = mock 'filehandle' + File.expects(:open).with("/file").yields fh + fh.expects(:lock_shared).yields "locked_fh" + + result = nil + Puppet::Util::FileLocking.readlock('/file') { |l| result = l } + result.should == "locked_fh" + end + end + + describe "when acquiring a write lock" do + before do + @sync = mock 'sync' + Puppet::Util.stubs(:sync).returns @sync + @sync.stubs(:synchronize).yields + end + + it "should fail if the parent directory does not exist" do + FileTest.expects(:directory?).with("/my/dir").returns false + + lambda { Puppet::Util::FileLocking.writelock('/my/dir/file') }.should raise_error(Puppet::DevError) + end + + it "should use a global exclusive mutex" do + sync = mock 'sync' + sync.expects(:synchronize).with(Sync::EX) + Puppet::Util.expects(:sync).with("/file").returns sync + + Puppet::Util::FileLocking.writelock '/file' + end + + it "should use any specified mode when opening the file" do + File.expects(:open).with("/file", "w", :mymode) + + Puppet::Util::FileLocking.writelock('/file', :mymode) + end + + it "should use the mode of the existing file if no mode is specified" do + File.expects(:stat).with("/file").returns(mock("stat", :mode => 0755)) + File.expects(:open).with("/file", "w", 0755) + + Puppet::Util::FileLocking.writelock('/file') + end + + it "should use 0600 as the mode if no mode is specified and the file does not exist" do + File.expects(:stat).raises(Errno::ENOENT) + File.expects(:open).with("/file", "w", 0600) + + Puppet::Util::FileLocking.writelock('/file') + end + + it "should create an exclusive file lock" do + fh = mock 'fh' + File.expects(:open).yields fh + fh.expects(:lock_exclusive) + + Puppet::Util::FileLocking.writelock('/file') + end + + it "should write to a temporary file" do + fh = mock 'fh' + File.expects(:open).yields fh + + lfh = mock 'locked_filehandle' + fh.expects(:lock_exclusive).yields lfh + + tf = mock 'tmp_filehandle' + File.expects(:open).with { |path, *args| path == "/file.tmp" }.yields tf + + result = nil + File.stubs(:rename) + Puppet::Util::FileLocking.writelock('/file') { |f| result = f } + result.should equal(tf) + end + + it "should rename the temporary file to the normal file" do + fh = stub 'fh' + fh.stubs(:lock_exclusive).yields fh + File.stubs(:open).yields fh + + File.expects(:rename).with("/file.tmp", "/file") + Puppet::Util::FileLocking.writelock('/file') { |f| } + end + + it "should fail if it cannot rename the file" do + fh = stub 'fh' + fh.stubs(:lock_exclusive).yields fh + File.stubs(:open).yields fh + + File.expects(:rename).with("/file.tmp", "/file").raises(RuntimeError) + lambda { Puppet::Util::FileLocking.writelock('/file') { |f| } }.should raise_error(Puppet::Error) + end + + it "should remove the temporary file if the rename fails" do + fh = stub 'fh' + fh.stubs(:lock_exclusive).yields fh + File.stubs(:open).yields fh + + File.expects(:rename).with("/file.tmp", "/file").raises(RuntimeError) + File.expects(:exist?).with("/file.tmp").returns true + File.expects(:unlink).with("/file.tmp") + lambda { Puppet::Util::FileLocking.writelock('/file') { |f| } }.should raise_error(Puppet::Error) + end + end +end diff --git a/spec/unit/util/storage.rb b/spec/unit/util/storage.rb index eb495bc0b..934df0725 100755 --- a/spec/unit/util/storage.rb +++ b/spec/unit/util/storage.rb @@ -1,248 +1,248 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'yaml' require 'tempfile' require 'puppet/util/storage' describe Puppet::Util::Storage do before(:all) do Puppet[:statedir] = Dir.tmpdir() end before(:each) do Puppet::Util::Storage.clear() end after do Puppet::Type.type(:file).clear end describe "when caching a symbol" do it "should return an empty hash" do Puppet::Util::Storage.cache(:yayness).should == {} Puppet::Util::Storage.cache(:more_yayness).should == {} end it "should add the symbol to its internal state" do Puppet::Util::Storage.cache(:yayness) Puppet::Util::Storage.state().should == {:yayness=>{}} end it "should not clobber existing state when caching additional objects" do Puppet::Util::Storage.cache(:yayness) Puppet::Util::Storage.state().should == {:yayness=>{}} Puppet::Util::Storage.cache(:bubblyness) Puppet::Util::Storage.state().should == {:yayness=>{},:bubblyness=>{}} end end describe "when caching a Puppet::Type" do before(:all) do @file_test = Puppet.type(:file).create(:name => "/yayness", :check => %w{checksum type}) @exec_test = Puppet.type(:exec).create(:name => "/bin/ls /yayness") end it "should return an empty hash" do Puppet::Util::Storage.cache(@file_test).should == {} Puppet::Util::Storage.cache(@exec_test).should == {} end it "should add the resource ref to its internal state" do Puppet::Util::Storage.state().should == {} Puppet::Util::Storage.cache(@file_test) Puppet::Util::Storage.state().should == {"File[/yayness]"=>{}} Puppet::Util::Storage.cache(@exec_test) Puppet::Util::Storage.state().should == {"File[/yayness]"=>{}, "Exec[/bin/ls /yayness]"=>{}} end end describe "when caching invalid objects" do before(:all) do @bogus_objects = [ {}, [], "foo", 42, nil, Tempfile.new('storage_test') ] end it "should raise an ArgumentError" do @bogus_objects.each do |object| proc { Puppet::Util::Storage.cache(object) }.should raise_error() end end it "should not add anything to its internal state" do @bogus_objects.each do |object| begin Puppet::Util::Storage.cache(object) rescue Puppet::Util::Storage.state().should == {} end end end end it "should clear its internal state when clear() is called" do Puppet::Util::Storage.cache(:yayness) Puppet::Util::Storage.state().should == {:yayness=>{}} Puppet::Util::Storage.clear() Puppet::Util::Storage.state().should == {} end describe "when loading from the state file" do before do Puppet.settings.stubs(:use).returns(true) end describe "when the state file/directory does not exist" do before(:each) do transient = Tempfile.new('storage_test') @path = transient.path() transient.close!() end it "should not fail to load()" do FileTest.exists?(@path).should be_false() Puppet[:statedir] = @path proc { Puppet::Util::Storage.load() }.should_not raise_error() Puppet[:statefile] = @path proc { Puppet::Util::Storage.load() }.should_not raise_error() end it "should not lose its internal state when load() is called" do FileTest.exists?(@path).should be_false() Puppet::Util::Storage.cache(:yayness) Puppet::Util::Storage.state().should == {:yayness=>{}} Puppet[:statefile] = @path proc { Puppet::Util::Storage.load() }.should_not raise_error() Puppet::Util::Storage.state().should == {:yayness=>{}} end end describe "when the state file/directory exists" do before(:each) do @state_file = Tempfile.new('storage_test') @saved_statefile = Puppet[:statefile] Puppet[:statefile] = @state_file.path() end it "should overwrite its internal state if load() is called" do # Should the state be overwritten even if Puppet[:statefile] is not valid YAML? Puppet::Util::Storage.cache(:yayness) Puppet::Util::Storage.state().should == {:yayness=>{}} proc { Puppet::Util::Storage.load() }.should_not raise_error() Puppet::Util::Storage.state().should == {} end it "should restore its internal state if the state file contains valid YAML" do test_yaml = {'File["/yayness"]'=>{"name"=>{:a=>:b,:c=>:d}}} YAML.expects(:load).returns(test_yaml) proc { Puppet::Util::Storage.load() }.should_not raise_error() Puppet::Util::Storage.state().should == test_yaml end it "should initialize with a clear internal state if the state file does not contain valid YAML" do @state_file.write(:booness) @state_file.flush() proc { Puppet::Util::Storage.load() }.should_not raise_error() Puppet::Util::Storage.state().should == {} end it "should raise an error if the state file does not contain valid YAML and cannot be renamed" do @state_file.write(:booness) @state_file.flush() YAML.expects(:load).raises(Puppet::Error) File.expects(:rename).raises(SystemCallError) proc { Puppet::Util::Storage.load() }.should raise_error() end it "should attempt to rename the state file if the file is corrupted" do # We fake corruption by causing YAML.load to raise an exception YAML.expects(:load).raises(Puppet::Error) File.expects(:rename).at_least_once proc { Puppet::Util::Storage.load() }.should_not raise_error() end it "should fail gracefully on load() if the state file is not a regular file" do @state_file.close!() Dir.mkdir(Puppet[:statefile]) File.expects(:rename).returns(0) proc { Puppet::Util::Storage.load() }.should_not raise_error() Dir.rmdir(Puppet[:statefile]) end it "should fail gracefully on load() if it cannot get a read lock on the state file" do - Puppet::Util.expects(:readlock).yields(false) + Puppet::Util::FileLocking.expects(:readlock).yields(false) test_yaml = {'File["/yayness"]'=>{"name"=>{:a=>:b,:c=>:d}}} YAML.expects(:load).returns(test_yaml) proc { Puppet::Util::Storage.load() }.should_not raise_error() Puppet::Util::Storage.state().should == test_yaml end after(:each) do @state_file.close!() Puppet[:statefile] = @saved_statefile end end end describe "when storing to the state file" do before(:each) do @state_file = Tempfile.new('storage_test') @saved_statefile = Puppet[:statefile] Puppet[:statefile] = @state_file.path() end it "should create the state file if it does not exist" do @state_file.close!() FileTest.exists?(Puppet[:statefile]).should be_false() Puppet::Util::Storage.cache(:yayness) proc { Puppet::Util::Storage.store() }.should_not raise_error() FileTest.exists?(Puppet[:statefile]).should be_true() end it "should raise an exception if the state file is not a regular file" do @state_file.close!() Dir.mkdir(Puppet[:statefile]) Puppet::Util::Storage.cache(:yayness) proc { Puppet::Util::Storage.store() }.should raise_error() Dir.rmdir(Puppet[:statefile]) end it "should raise an exception if it cannot get a write lock on the state file" do - Puppet::Util.expects(:writelock).yields(false) + Puppet::Util::FileLocking.expects(:writelock).yields(false) Puppet::Util::Storage.cache(:yayness) proc { Puppet::Util::Storage.store() }.should raise_error() end it "should load() the same information that it store()s" do Puppet::Util::Storage.cache(:yayness) Puppet::Util::Storage.state().should == {:yayness=>{}} proc { Puppet::Util::Storage.store() }.should_not raise_error() Puppet::Util::Storage.clear() Puppet::Util::Storage.state().should == {} proc { Puppet::Util::Storage.load() }.should_not raise_error() Puppet::Util::Storage.state().should == {:yayness=>{}} end after(:each) do @state_file.close!() Puppet[:statefile] = @saved_statefile end end end diff --git a/test/util/utiltest.rb b/test/util/utiltest.rb index 35e1446a1..f838b39fc 100755 --- a/test/util/utiltest.rb +++ b/test/util/utiltest.rb @@ -1,287 +1,254 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../lib/puppettest' require 'puppettest' require 'mocha' class TestPuppetUtil < Test::Unit::TestCase include PuppetTest - # we're getting corrupt files, probably because multiple processes - # are reading or writing the file at once - # so we need to test that - def test_multiwrite - file = tempfile() - File.open(file, "w") { |f| f.puts "starting" } - - value = {:a => :b} - threads = [] - sync = Sync.new - 9.times { |a| - threads << Thread.new { - 9.times { |b| - assert_nothing_raised { - sync.synchronize(Sync::SH) { - Puppet::Util.readlock(file) { |f| - f.read - } - } - sleep 0.01 - sync.synchronize(Sync::EX) { - Puppet::Util.writelock(file) { |f| - f.puts "%s %s" % [a, b] - } - } - } - } - } - } - threads.each { |th| th.join } - end - - def test_withumask oldmask = File.umask path = tempfile() # FIXME this fails on FreeBSD with a mode of 01777 Puppet::Util.withumask(000) do Dir.mkdir(path, 0777) end assert(File.stat(path).mode & 007777 == 0777, "File has the incorrect mode") assert_equal(oldmask, File.umask, "Umask was not reset") end def test_benchmark path = tempfile() str = "yayness" File.open(path, "w") do |f| f.print "yayness" end # First test it with the normal args assert_nothing_raised do val = nil result = Puppet::Util.benchmark(:notice, "Read file") do val = File.read(path) end assert_equal(str, val) assert_instance_of(Float, result) end # Now test it with a passed object assert_nothing_raised do val = nil Puppet::Util.benchmark(Puppet, :notice, "Read file") do val = File.read(path) end assert_equal(str, val) end end def test_proxy klass = Class.new do attr_accessor :hash class << self attr_accessor :ohash end end klass.send(:include, Puppet::Util) klass.ohash = {} inst = klass.new inst.hash = {} assert_nothing_raised do Puppet::Util.proxy klass, :hash, "[]", "[]=", :clear, :delete end assert_nothing_raised do Puppet::Util.classproxy klass, :ohash, "[]", "[]=", :clear, :delete end assert_nothing_raised do inst[:yay] = "boo" inst["cool"] = :yayness end [:yay, "cool"].each do |var| assert_equal(inst.hash[var], inst[var], "Var %s did not take" % var) end assert_nothing_raised do klass[:Yay] = "boo" klass["Cool"] = :yayness end [:Yay, "Cool"].each do |var| assert_equal(inst.hash[var], inst[var], "Var %s did not take" % var) end end def test_symbolize ret = nil assert_nothing_raised { ret = Puppet::Util.symbolize("yayness") } assert_equal(:yayness, ret) assert_nothing_raised { ret = Puppet::Util.symbolize(:yayness) } assert_equal(:yayness, ret) assert_nothing_raised { ret = Puppet::Util.symbolize(43) } assert_equal(43, ret) assert_nothing_raised { ret = Puppet::Util.symbolize(nil) } assert_equal(nil, ret) end def test_execute command = tempfile() File.open(command, "w") { |f| f.puts %{#!/bin/sh\n/bin/echo "$1">&1; echo "$2">&2} } File.chmod(0755, command) output = nil assert_nothing_raised do output = Puppet::Util.execute([command, "yaytest", "funtest"]) end assert_equal("yaytest\nfuntest\n", output) # Now try it with a single quote assert_nothing_raised do output = Puppet::Util.execute([command, "yay'test", "funtest"]) end assert_equal("yay'test\nfuntest\n", output) # Now make sure we can squelch output (#565) assert_nothing_raised do output = Puppet::Util.execute([command, "yay'test", "funtest"], :squelch => true) end assert_equal(nil, output) # Now test that we correctly fail if the command returns non-zero assert_raise(Puppet::ExecutionFailure) do out = Puppet::Util.execute(["touch", "/no/such/file/could/exist"]) end # And that we can tell it not to fail assert_nothing_raised() do out = Puppet::Util.execute(["touch", "/no/such/file/could/exist"], :failonfail => false) end if Process.uid == 0 # Make sure we correctly set our uid and gid user = nonrootuser group = nonrootgroup file = tempfile() assert_nothing_raised do Puppet::Util.execute(["touch", file], :uid => user.name, :gid => group.name) end assert(FileTest.exists?(file), "file was not created") assert_equal(user.uid, File.stat(file).uid, "uid was not set correctly") # We can't really check the gid, because it just behaves too # inconsistently everywhere. # assert_equal(group.gid, File.stat(file).gid, # "gid was not set correctly") end # (#565) Test the case of patricide. patricidecommand = tempfile() File.open(patricidecommand, "w") { |f| f.puts %{#!/bin/bash\n/bin/bash -c 'kill -TERM \$PPID' &;\n while [ 1 ]; do echo -n ''; done;\n} } File.chmod(0755, patricidecommand) assert_nothing_raised do output = Puppet::Util.execute([patricidecommand], :squelch => true) end assert_equal(nil, output) # See what happens if we try and read the pipe to the command... assert_raise(Puppet::ExecutionFailure) do output = Puppet::Util.execute([patricidecommand]) end assert_nothing_raised do output = Puppet::Util.execute([patricidecommand], :failonfail => false) end end def test_lang_environ_in_execute orig_lang = ENV["LANG"] orig_lc_all = ENV["LC_ALL"] orig_lc_messages = ENV["LC_MESSAGES"] orig_language = ENV["LANGUAGE"] cleanup do ENV["LANG"] = orig_lang ENV["LC_ALL"] = orig_lc_all ENV["LC_MESSAGES"] = orig_lc_messages ENV["LANGUAGE"] = orig_lc_messages end # Mmm, we love gettext(3) ENV["LANG"] = "en_US" ENV["LC_ALL"] = "en_US" ENV["LC_MESSAGES"] = "en_US" ENV["LANGUAGE"] = "en_US" %w{LANG LC_ALL LC_MESSAGES LANGUAGE}.each do |env| assert_equal('C', Puppet::Util.execute(['ruby', '-e', "print ENV['#{env}']"]), "Environment var #{env} wasn't set to 'C'") assert_equal 'en_US', ENV[env], "Environment var #{env} not set back correctly" end end # Check whether execute() accepts strings in addition to arrays. def test_string_exec cmd = "/bin/echo howdy" output = nil assert_raise(ArgumentError) { output = Puppet::Util.execute(cmd) } #assert_equal("howdy\n", output) #assert_raise(RuntimeError) { # Puppet::Util.execute(cmd, 0, 0) #} end # This is mostly to test #380. def test_get_provider_value group = Puppet::Type.type(:group).create :name => "yayness", :ensure => :present root = Puppet::Type.type(:user).create :name => "root", :ensure => :present val = nil assert_nothing_raised do val = Puppet::Util.get_provider_value(:group, :gid, "yayness") end assert_nil(val, "returned a value on a missing group") # Now make sure we get a value for one we know exists assert_nothing_raised do val = Puppet::Util.get_provider_value(:user, :uid, "root") end assert_equal(0, val, "got invalid uid for root") end end