diff --git a/lib/puppet/parameter/path.rb b/lib/puppet/parameter/path.rb index 44886afd0..26e4933c7 100644 --- a/lib/puppet/parameter/path.rb +++ b/lib/puppet/parameter/path.rb @@ -1,42 +1,38 @@ require 'puppet/parameter' class Puppet::Parameter::Path < Puppet::Parameter def self.accept_arrays(bool = true) @accept_arrays = !!bool end def self.arrays? @accept_arrays end def validate_path(paths) if paths.is_a?(Array) and ! self.class.arrays? then fail "#{name} only accepts a single path, not an array of paths" end # We *always* support Unix path separators, as Win32 does now too. absolute = "[/#{::Regexp.quote(::File::SEPARATOR)}]" win32 = Puppet.features.microsoft_windows? - Array(paths).each do |path| - next if path =~ %r{^#{absolute}} - next if win32 and path =~ %r{^(?:[a-zA-Z]:)?#{absolute}} - fail("#{name} must be a fully qualified path") - end + fail("#{name} must be a fully qualified path") unless Array(paths).all? {|path| absolute_path?(path)} paths end # This will be overridden if someone uses the validate option, which is why # it just delegates to the other, useful, method. def unsafe_validate(paths) validate_path(paths) end # Likewise, this might be overridden, but by default... def unsafe_munge(paths) if paths.is_a?(Array) and ! self.class.arrays? then fail "#{name} only accepts a single path, not an array of paths" end paths end end diff --git a/lib/puppet/provider/exec/posix.rb b/lib/puppet/provider/exec.rb similarity index 58% copy from lib/puppet/provider/exec/posix.rb copy to lib/puppet/provider/exec.rb index 782f1eac6..ab9d3e4ea 100644 --- a/lib/puppet/provider/exec/posix.rb +++ b/lib/puppet/provider/exec.rb @@ -1,115 +1,79 @@ -Puppet::Type.type(:exec).provide :posix do +class Puppet::Provider::Exec < Puppet::Provider include Puppet::Util::Execution - confine :feature => :posix - defaultfor :feature => :posix - - desc <<-EOT - Executes external binaries directly, without passing through a shell or - performing any interpolation. This is a safer and more predictable way - to execute most commands, but prevents the use of globbing and shell - built-ins (including control logic like "for" and "if" statements). - EOT - def run(command, check = false) output = nil status = nil dir = nil checkexe(command) if dir = resource[:cwd] unless File.directory?(dir) if check dir = nil else self.fail "Working directory '#{dir}' does not exist" end end end dir ||= Dir.pwd debug "Executing#{check ? " check": ""} '#{command}'" begin # Do our chdir Dir.chdir(dir) do environment = {} environment[:PATH] = resource[:path].join(":") if resource[:path] if envlist = resource[:environment] envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| if setting =~ /^(\w+)=((.|\n)+)$/ env_name = $1 value = $2 if environment.include?(env_name) || environment.include?(env_name.to_sym) warning "Overriding environment setting '#{env_name}' with '#{value}'" end environment[env_name] = value else warning "Cannot understand environment setting #{setting.inspect}" end end end withenv environment do Timeout::timeout(resource[:timeout]) do output, status = Puppet::Util::SUIDManager. - run_and_capture([command], resource[:user], resource[:group]) + run_and_capture(command, resource[:user], resource[:group]) end # The shell returns 127 if the command is missing. if status.exitstatus == 127 raise ArgumentError, output end end end rescue Errno::ENOENT => detail self.fail detail.to_s end return output, status end - # Verify that we have the executable - def checkexe(command) - exe = extractexe(command) - - if File.expand_path(exe) == exe - if !File.exists?(exe) - raise ArgumentError, "Could not find command '#{exe}'" - elsif !File.file?(exe) - raise ArgumentError, "'#{exe}' is a #{File.ftype(exe)}, not a file" - elsif !File.executable?(exe) - raise ArgumentError, "'#{exe}' is not executable" - end - return - end - - if resource[:path] - withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do - return if which(exe) - end - end - - # 'which' will only return the command if it's executable, so we can't - # distinguish not found from not executable - raise ArgumentError, "Could not find command '#{exe}'" - end - def extractexe(command) # easy case: command was quoted if command =~ /^"([^"]+)"/ $1 else command.split(/ /)[0] end end def validatecmd(command) exe = extractexe(command) # if we're not fully qualified, require a path - self.fail "'#{command}' is not qualified and no path was specified. Please qualify the command or specify a path." if File.expand_path(exe) != exe and resource[:path].nil? + self.fail "'#{command}' is not qualified and no path was specified. Please qualify the command or specify a path." if !absolute_path?(exe) and resource[:path].nil? end end diff --git a/lib/puppet/provider/exec/posix.rb b/lib/puppet/provider/exec/posix.rb index 782f1eac6..12748dd0c 100644 --- a/lib/puppet/provider/exec/posix.rb +++ b/lib/puppet/provider/exec/posix.rb @@ -1,115 +1,39 @@ -Puppet::Type.type(:exec).provide :posix do - include Puppet::Util::Execution +require 'puppet/provider/exec' +Puppet::Type.type(:exec).provide :posix, :parent => Puppet::Provider::Exec do confine :feature => :posix defaultfor :feature => :posix desc <<-EOT Executes external binaries directly, without passing through a shell or performing any interpolation. This is a safer and more predictable way to execute most commands, but prevents the use of globbing and shell built-ins (including control logic like "for" and "if" statements). EOT - def run(command, check = false) - output = nil - status = nil - dir = nil - - checkexe(command) - - if dir = resource[:cwd] - unless File.directory?(dir) - if check - dir = nil - else - self.fail "Working directory '#{dir}' does not exist" - end - end - end - - dir ||= Dir.pwd - - debug "Executing#{check ? " check": ""} '#{command}'" - begin - # Do our chdir - Dir.chdir(dir) do - environment = {} - - environment[:PATH] = resource[:path].join(":") if resource[:path] - - if envlist = resource[:environment] - envlist = [envlist] unless envlist.is_a? Array - envlist.each do |setting| - if setting =~ /^(\w+)=((.|\n)+)$/ - env_name = $1 - value = $2 - if environment.include?(env_name) || environment.include?(env_name.to_sym) - warning "Overriding environment setting '#{env_name}' with '#{value}'" - end - environment[env_name] = value - else - warning "Cannot understand environment setting #{setting.inspect}" - end - end - end - - withenv environment do - Timeout::timeout(resource[:timeout]) do - output, status = Puppet::Util::SUIDManager. - run_and_capture([command], resource[:user], resource[:group]) - end - # The shell returns 127 if the command is missing. - if status.exitstatus == 127 - raise ArgumentError, output - end - end - end - rescue Errno::ENOENT => detail - self.fail detail.to_s - end - - return output, status - end - # Verify that we have the executable def checkexe(command) exe = extractexe(command) if File.expand_path(exe) == exe if !File.exists?(exe) raise ArgumentError, "Could not find command '#{exe}'" elsif !File.file?(exe) raise ArgumentError, "'#{exe}' is a #{File.ftype(exe)}, not a file" elsif !File.executable?(exe) raise ArgumentError, "'#{exe}' is not executable" end return end if resource[:path] withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do return if which(exe) end end # 'which' will only return the command if it's executable, so we can't # distinguish not found from not executable raise ArgumentError, "Could not find command '#{exe}'" end - - def extractexe(command) - # easy case: command was quoted - if command =~ /^"([^"]+)"/ - $1 - else - command.split(/ /)[0] - end - end - - def validatecmd(command) - exe = extractexe(command) - # if we're not fully qualified, require a path - self.fail "'#{command}' is not qualified and no path was specified. Please qualify the command or specify a path." if File.expand_path(exe) != exe and resource[:path].nil? - end end diff --git a/lib/puppet/provider/exec/windows.rb b/lib/puppet/provider/exec/windows.rb new file mode 100644 index 000000000..9ce29f12b --- /dev/null +++ b/lib/puppet/provider/exec/windows.rb @@ -0,0 +1,35 @@ +require 'puppet/provider/exec' + +Puppet::Type.type(:exec).provide :windows, :parent => Puppet::Provider::Exec do + include Puppet::Util::Execution + + confine :feature => :microsoft_windows + defaultfor :feature => :microsoft_windows + + desc "Execute external binaries directly, on Windows systems. +This does not pass through a shell, or perform any interpolation, but +only directly calls the command with the arguments given." + + # Verify that we have the executable + def checkexe(command) + exe = extractexe(command) + + if absolute_path?(exe) + if !File.exists?(exe) + raise ArgumentError, "Could not find command '#{exe}'" + elsif !File.file?(exe) + raise ArgumentError, "'#{exe}' is a #{File.ftype(exe)}, not a file" + end + return + end + + path = resource[:path] || [] + + exts = [".exe", ".ps1", ".bat", ".com", ""] + withenv :PATH => path.join(File::PATH_SEPARATOR) do + return if exts.any? {|ext| which(exe + ext) } + end + + raise ArgumentError, "Could not find command '#{exe}'" + end +end diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index 35e0c96d7..43303de66 100755 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -1,508 +1,511 @@ module Puppet newtype(:exec) do include Puppet::Util::Execution require 'timeout' @doc = "Executes external commands. It is critical that all commands executed using this mechanism can be run multiple times without harm, i.e., they are *idempotent*. One useful way to create idempotent commands is to use the checks like `creates` to avoid running the command unless some condition is met. Note that you can restrict an `exec` to only run when it receives events by using the `refreshonly` parameter; this is a useful way to have your configuration respond to events with arbitrary commands. Note also that if an `exec` receives an event from another resource, it will get executed again (or execute the command specified in `refresh`, if there is one). There is a strong tendency to use `exec` to do whatever work Puppet can't already do; while this is obviously acceptable (and unavoidable) in the short term, it is highly recommended to migrate work from `exec` to native Puppet types as quickly as possible. If you find that you are doing a lot of work with `exec`, please at least notify us at Puppet Labs what you are doing, and hopefully we can work with you to get a native resource type for the work you are doing. **Autorequires:** If Puppet is managing an exec's cwd or the executable file used in an exec's command, the exec resource will autorequire those files. If Puppet is managing the user that an exec should run as, the exec resource will autorequire that user." # Create a new check mechanism. It's basically just a parameter that # provides one extra 'check' method. def self.newcheck(name, options = {}, &block) @checks ||= {} check = newparam(name, options, &block) @checks[name] = check end def self.checks @checks.keys end newproperty(:returns, :array_matching => :all, :event => :executed_command) do |property| include Puppet::Util::Execution munge do |value| value.to_s end def event_name :executed_command end defaultto "0" attr_reader :output desc "The expected return code(s). An error will be returned if the executed command returns something else. Defaults to 0. Can be specified as an array of acceptable return codes or a single value." # Make output a bit prettier def change_to_s(currentvalue, newvalue) "executed successfully" end # First verify that all of our checks pass. def retrieve # We need to return :notrun to trigger evaluation; when that isn't # true, we *LIE* about what happened and return a "success" for the # value, which causes us to be treated as in_sync?, which means we # don't actually execute anything. I think. --daniel 2011-03-10 if @resource.check_all_attributes return :notrun else return self.should end end # Actually execute the command. def sync olddir = nil # We need a dir to change to, even if it's just the cwd dir = self.resource[:cwd] || Dir.pwd event = :executed_command tries = self.resource[:tries] try_sleep = self.resource[:try_sleep] begin tries.times do |try| # Only add debug messages for tries > 1 to reduce log spam. debug("Exec try #{try+1}/#{tries}") if tries > 1 @output, @status = provider.run(self.resource[:command]) break if self.should.include?(@status.exitstatus.to_s) if try_sleep > 0 and tries > 1 debug("Sleeping for #{try_sleep} seconds between tries") sleep try_sleep end end rescue Timeout::Error self.fail "Command exceeded timeout" % value.inspect end if log = @resource[:logoutput] case log when :true log = @resource[:loglevel] when :on_failure unless self.should.include?(@status.exitstatus.to_s) log = @resource[:loglevel] else log = :false end end unless log == :false @output.split(/\n/).each { |line| self.send(log, line) } end end unless self.should.include?(@status.exitstatus.to_s) self.fail("#{self.resource[:command]} returned #{@status.exitstatus} instead of one of [#{self.should.join(",")}]") end event end end newparam(:command) do isnamevar desc "The actual command to execute. Must either be fully qualified or a search path for the command must be provided. If the command succeeds, any output produced will be logged at the instance's normal log level (usually `notice`), but if the command fails (meaning its return code does not match the specified code) then any output is logged at the `err` log level." end newparam(:path) do desc "The search path used for command execution. Commands must be fully qualified if no path is specified. Paths can be specified as an array or as a colon separated list." # Support both arrays and colon-separated fields. def value=(*values) @value = values.flatten.collect { |val| if val =~ /;/ # recognize semi-colon separated paths val.split(";") elsif val =~ /^\w:[^:]*$/ # heuristic to avoid splitting a driveletter away val else val.split(":") end }.flatten end end newparam(:user) do desc "The user to run the command as. Note that if you use this then any error output is not currently captured. This is because of a bug within Ruby. If you are using Puppet to create this user, the exec will automatically require the user, as long as it is specified by name." # Most validation is handled by the SUIDManager class. validate do |user| self.fail "Only root can execute commands as other users" unless Puppet.features.root? + self.fail "Unable to execute commands as other users on Windows" if Puppet.features.microsoft_windows? end end newparam(:group) do desc "The group to run the command as. This seems to work quite haphazardly on different platforms -- it is a platform issue not a Ruby or Puppet one, since the same variety exists when running commnands as different users in the shell." # Validation is handled by the SUIDManager class. end newparam(:cwd, :parent => Puppet::Parameter::Path) do desc "The directory from which to run the command. If this directory does not exist, the command will fail." end newparam(:logoutput) do desc "Whether to log output. Defaults to logging output at the loglevel for the `exec` resource. Use *on_failure* to only log the output when the command reports an error. Values are **true**, *false*, *on_failure*, and any legal log level." newvalues(:true, :false, :on_failure) end newparam(:refresh) do desc "How to refresh this command. By default, the exec is just called again when it receives an event from another resource, but this parameter allows you to define a different command for refreshing." validate do |command| provider.validatecmd(command) end end newparam(:environment) do desc "Any additional environment variables you want to set for a command. Note that if you use this to set PATH, it will override the `path` attribute. Multiple environment variables should be specified as an array." validate do |values| values = [values] unless values.is_a? Array values.each do |value| unless value =~ /\w+=/ raise ArgumentError, "Invalid environment setting '#{value}'" end end end end newparam(:timeout) do desc "The maximum time the command should take. If the command takes longer than the timeout, the command is considered to have failed and will be stopped. Use 0 to disable the timeout. The time is specified in seconds." munge do |value| value = value.shift if value.is_a?(Array) begin value = Float(value) rescue ArgumentError => e raise ArgumentError, "The timeout must be a number." end [value, 0.0].max end defaultto 300 end newparam(:tries) do desc "The number of times execution of the command should be tried. Defaults to '1'. This many attempts will be made to execute the command until an acceptable return code is returned. Note that the timeout paramater applies to each try rather than to the complete set of tries." munge do |value| if value.is_a?(String) unless value =~ /^[\d]+$/ raise ArgumentError, "Tries must be an integer" end value = Integer(value) end raise ArgumentError, "Tries must be an integer >= 1" if value < 1 value end defaultto 1 end newparam(:try_sleep) do desc "The time to sleep in seconds between 'tries'." munge do |value| if value.is_a?(String) unless value =~ /^[-\d.]+$/ raise ArgumentError, "try_sleep must be a number" end value = Float(value) end raise ArgumentError, "try_sleep cannot be a negative number" if value < 0 value end defaultto 0 end newcheck(:refreshonly) do desc "The command should only be run as a refresh mechanism for when a dependent object is changed. It only makes sense to use this option when this command depends on some other object; it is useful for triggering an action: # Pull down the main aliases file file { \"/etc/aliases\": source => \"puppet://server/module/aliases\" } # Rebuild the database, but only when the file changes exec { newaliases: path => [\"/usr/bin\", \"/usr/sbin\"], subscribe => File[\"/etc/aliases\"], refreshonly => true } Note that only `subscribe` and `notify` can trigger actions, not `require`, so it only makes sense to use `refreshonly` with `subscribe` or `notify`." newvalues(:true, :false) # We always fail this test, because we're only supposed to run # on refresh. def check(value) # We have to invert the values. if value == :true false else true end end end newcheck(:creates, :parent => Puppet::Parameter::Path) do desc <<-EOT A file that this command creates. If this parameter is provided, then the command will only be run if the specified file does not exist. exec { "tar -xf /Volumes/nfs02/important.tar": cwd => "/var/tmp", creates => "/var/tmp/myfile", path => ["/usr/bin", "/usr/sbin"] } In this example, if `/var/tmp/myfile` is ever deleted, the exec will bring it back by re-extracting the tarball. EOT accept_arrays # If the file exists, return false (i.e., don't run the command), # else return true def check(value) ! FileTest.exists?(value) end end newcheck(:unless) do desc "If this parameter is set, then this `exec` will run unless the command returns 0. For example: exec { \"/bin/echo root >> /usr/lib/cron/cron.allow\": path => \"/usr/bin:/usr/sbin:/bin\", unless => \"grep root /usr/lib/cron/cron.allow 2>/dev/null\" } This would add `root` to the cron.allow file (on Solaris) unless `grep` determines it's already there. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. " validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |command| provider.validatecmd(command) end end # Return true if the command does not return 0. def check(value) begin output, status = provider.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false end status.exitstatus != 0 end end newcheck(:onlyif) do desc "If this parameter is set, then this `exec` will only run if the command returns 0. For example: exec { \"logrotate\": path => \"/usr/bin:/usr/sbin:/bin\", onlyif => \"test `du /var/log/messages | cut -f1` -gt 100000\" } This would run `logrotate` only if that test returned true. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. Also note that onlyif can take an array as its value, e.g.: onlyif => [\"test -f /tmp/file1\", \"test -f /tmp/file2\"] This will only run the exec if /all/ conditions in the array return true. " validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |command| provider.validatecmd(command) end end # Return true if the command returns 0. def check(value) begin output, status = provider.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false end status.exitstatus == 0 end end # Exec names are not isomorphic with the objects. @isomorphic = false validate do provider.validatecmd(self[:command]) end # FIXME exec should autorequire any exec that 'creates' our cwd autorequire(:file) do reqs = [] # Stick the cwd in there if we have it reqs << self[:cwd] if self[:cwd] - self[:command].scan(/^(#{File::SEPARATOR}\S+)/) { |str| + file_regex = Puppet.features.microsoft_windows? ? %r{^([a-zA-Z]:[\\/]\S+)} : %r{^(/\S+)} + + self[:command].scan(file_regex) { |str| reqs << str } self[:command].scan(/^"([^"]+)"/) { |str| reqs << str } [:onlyif, :unless].each { |param| next unless tmp = self[param] tmp = [tmp] unless tmp.is_a? Array tmp.each do |line| # And search the command line for files, adding any we # find. This will also catch the command itself if it's # fully qualified. It might not be a bad idea to add # unqualified files, but, well, that's a bit more annoying # to do. - reqs += line.scan(%r{(#{File::SEPARATOR}\S+)}) + reqs += line.scan(file_regex) end } # For some reason, the += isn't causing a flattening reqs.flatten! reqs end autorequire(:user) do # Autorequire users if they are specified by name if user = self[:user] and user !~ /^\d+$/ user end end def self.instances [] end # Verify that we pass all of the checks. The argument determines whether # we skip the :refreshonly check, which is necessary because we now check # within refresh def check_all_attributes(refreshing = false) self.class.checks.each { |check| next if refreshing and check == :refreshonly if @parameters.include?(check) val = @parameters[check].value val = [val] unless val.is_a? Array val.each do |value| return false unless @parameters[check].check(value) end end } true end def output if self.property(:returns).nil? return nil else return self.property(:returns).output end end # Run the command, or optionally run a separately-specified command. def refresh if self.check_all_attributes(true) if cmd = self[:refresh] provider.run(cmd) else self.property(:returns).sync end end end end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index ecedb25a6..80ce96339 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,445 +1,451 @@ # A module to collect utility functions. require 'English' require 'puppet/util/monkey_patches' require 'sync' +require 'tempfile' require 'puppet/external/lock' require 'monitor' require 'puppet/util/execution_stub' module Puppet # A command failed to execute. require 'puppet/error' 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 @@sync_objects = {}.extend MonitorMixin def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin - @@sync_objects.synchronize { + @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure - @@sync_objects.synchronize { + @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) 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| args = args.join(" ") if args.is_a?(Array) 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 #{dir}: basedir #{File.join(path)} is a file" 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 raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{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 which(bin) - if bin =~ /^\// + if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| dest=File.join(dir, bin) return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which + # Determine in a platform-specific way whether a path is absolute. This + # defaults to the local platform if none is specified. + def absolute_path?(path, platform=nil) + # Escape once for the string literal, and once for the regex. + slash = '[\\\\/]' + name = '[^\\\\/]+' + regexes = { + :windows => %r!^([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name})!i, + :posix => %r!^/!, + } + platform ||= Puppet.features.microsoft_windows? ? :windows : :posix + + !! (path =~ regexes[platform]) + end + module_function :absolute_path? + # Execute the provided command in a pipe, yielding the pipe object. def execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '#{command}'" else Puppet.debug "Executing '#{command}'" end output = open("| #{command} 2>&1") do |pipe| yield pipe end if failonfail unless $CHILD_STATUS == 0 raise ExecutionFailure, output end end output end def execfail(command, exception) output = execute(command) return output rescue ExecutionFailure raise exception, output end + def execute_posix(command, arguments, stdin, stdout, stderr) + child_pid = Kernel.fork do + command = Array(command) + Process.setsid + begin + $stdin.reopen(stdin) + $stdout.reopen(stdout) + $stderr.reopen(stderr) + + 3.upto(256){|fd| IO::new(fd).close rescue nil} + + Puppet::Util::SUIDManager.change_group(arguments[:gid], true) if arguments[:gid] + Puppet::Util::SUIDManager.change_user(arguments[:uid], true) if arguments[:uid] + + ENV['LANG'] = ENV['LC_ALL'] = ENV['LC_MESSAGES'] = ENV['LANGUAGE'] = 'C' + Kernel.exec(*command) + rescue => detail + puts detail.to_s + exit!(1) + end + end + child_pid + end + module_function :execute_posix + + def execute_windows(command, arguments, stdin, stdout, stderr) + command = command.map do |part| + part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part + end.join(" ") if command.is_a?(Array) + + process_info = Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr} ) + process_info.process_id + end + module_function :execute_windows + # 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 } + command = command.flatten.map(&: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()" + elsif command.is_a?(String) + str = command end if respond_to? :debug debug "Executing '#{str}'" else Puppet.debug "Executing '#{str}'" end - if execution_stub = Puppet::Util::ExecutionStub.current_value - return execution_stub.call(command, arguments) - end + null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' - @@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") - error_file=output_file if arguments[:combine] - end + stdin = File.open(arguments[:stdinfile] || null_file, 'r') + stdout = arguments[:squelch] ? File.open(null_file, 'w') : Tempfile.new('puppet') + stderr = arguments[:combine] ? stdout : File.open(null_file, 'w') - if Puppet.features.posix? - oldverb = $VERBOSE - $VERBOSE = nil - 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} - Puppet::Util::SUIDManager.change_group(arguments[:gid], true) if arguments[:gid] - Puppet::Util::SUIDManager.change_user(arguments[:uid], true) if arguments[:uid] - 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 - end + + exec_args = [command, arguments, stdin, stdout, stderr] + + if execution_stub = Puppet::Util::ExecutionStub.current_value + return execution_stub.call(*exec_args) + elsif Puppet.features.posix? + child_pid = execute_posix(*exec_args) elsif Puppet.features.microsoft_windows? - command = command.collect {|part| '"' + part.gsub(/"/, '\\"') + '"'}.join(" ") if command.is_a?(Array) - Puppet.debug "Creating process '#{command}'" - processinfo = Process.create( :command_line => command ) - child_status = (Process.waitpid2(child_pid)[1]).to_i >> 8 + child_pid = execute_windows(*exec_args) end - # 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 + child_status = Process.waitpid2(child_pid).last - # The 'true' causes the file to get unlinked right away. - output_file.close(true) - end + [stdin, stdout, stderr].each {|io| io.close rescue nil} + + # read output in if required + unless arguments[:squelch] + output = wait_for_output(stdout) + Puppet.warning "Could not get output" unless output end - if arguments[:failonfail] - unless child_status == 0 - raise ExecutionFailure, "Execution of '#{str}' returned #{child_status}: #{output}" - end + if arguments[:failonfail] and child_status != 0 + raise ExecutionFailure, "Execution of '#{str}' returned #{child_status.exitstatus}: #{output}" end output end module_function :execute + def wait_for_output(stdout) + # Make sure the file's actually been written. This is basically a race + # condition, and is probably a horrible way to handle it, but, well, oh + # well. + 2.times do |try| + if File.exists?(stdout.path) + output = stdout.open.read + + stdout.close(true) + + return output + else + time_to_sleep = try / 2.0 + Puppet.warning "Waiting for output; will sleep #{time_to_sleep} seconds" + sleep(time_to_sleep) + end + end + nil + end + module_function :wait_for_output + # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end # Because some modules provide their own version of this method. alias util_execute execute module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %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 hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark def secure_open(file,must_be_w,&block) raise Puppet::DevError,"secure_open only works with mode 'w'" unless must_be_w == 'w' raise Puppet::DevError,"secure_open only requires a block" unless block_given? Puppet.warning "#{file} was a symlink to #{File.readlink(file)}" if File.symlink?(file) if File.exists?(file) or File.symlink?(file) wait = File.symlink?(file) ? 5.0 : 0.1 File.delete(file) sleep wait # give it a chance to reappear, just in case someone is actively trying something. end begin File.open(file,File::CREAT|File::EXCL|File::TRUNC|File::WRONLY,&block) rescue Errno::EEXIST desc = File.symlink?(file) ? "symlink to #{File.readlink(file)}" : File.stat(file).ftype puts "Warning: #{file} was apparently created by another process (as" puts "a #{desc}) as soon as it was deleted by this process. Someone may be trying" puts "to do something objectionable (such as tricking you into overwriting system" puts "files if you are running as root)." raise end end module_function :secure_open 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/spec/integration/util_spec.rb b/spec/integration/util_spec.rb new file mode 100644 index 000000000..a50f78326 --- /dev/null +++ b/spec/integration/util_spec.rb @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +require 'spec_helper' + +describe Puppet::Util do + describe "#execute" do + it "should properly allow stdout and stderr to share a file" do + command = "ruby -e '(1..10).each {|i| (i%2==0) ? $stdout.puts(i) : $stderr.puts(i)}'" + + Puppet::Util.execute(command, :combine => true).split.should =~ [*'1'..'10'] + end + end +end diff --git a/spec/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb index 725bf2af9..86afafbef 100755 --- a/spec/lib/puppet_spec/files.rb +++ b/spec/lib/puppet_spec/files.rb @@ -1,61 +1,61 @@ require 'fileutils' require 'tempfile' require 'pathname' # A support module for testing files. module PuppetSpec::Files # This code exists only to support tests that run as root, pretty much. # Once they have finally been eliminated this can all go... --daniel 2011-04-08 def self.in_tmp(path) tempdir = Dir.tmpdir Pathname.new(path).ascend do |dir| return true if File.identical?(tempdir, dir) end false end def self.cleanup $global_tempfiles ||= [] while path = $global_tempfiles.pop do fail "Not deleting tmpfile #{path} outside regular tmpdir" unless in_tmp(path) begin FileUtils.rm_r path, :secure => true rescue Errno::ENOENT # nothing to do end end end def make_absolute(path) return path unless Puppet.features.microsoft_windows? # REMIND UNC return path if path =~ /^[A-Za-z]:/ pwd = Dir.getwd - return "#{pwd[0,2]}#{path}" if pwd.length > 2 and pwd =~ /^[A-Za-z]:/ + return "#{pwd[0,2]}#{path}" if pwd =~ /^[A-Za-z]:/ return "C:#{path}" end def tmpfile(name) # Generate a temporary file, just for the name... source = Tempfile.new(name) path = source.path source.close! # ...record it for cleanup, $global_tempfiles ||= [] $global_tempfiles << File.expand_path(path) # ...and bam. path end def tmpdir(name) path = tmpfile(name) FileUtils.mkdir_p(path) path end end diff --git a/spec/shared_behaviours/path_parameters.rb b/spec/shared_behaviours/path_parameters.rb index b5a907900..bdcd4cf25 100755 --- a/spec/shared_behaviours/path_parameters.rb +++ b/spec/shared_behaviours/path_parameters.rb @@ -1,185 +1,187 @@ # In order to use this correctly you must define a method to get an instance # of the type being tested, so that this code can remain generic: # # it_should_behave_like "all path parameters", :path do # def instance(path) # Puppet::Type.type(:example).new( # :name => 'foo', :require => 'bar', :path_param => path # ) # end # # That method will be invoked for each test to create the instance that we # subsequently test through the system; you should ensure that the minimum of # possible attributes are set to keep the tests clean. # # You must also pass the symbolic name of the parameter being tested to the # block, and optionally can pass a hash of additional options to the block. # # The known options are: # :array :: boolean, does this support arrays of paths, default true. shared_examples_for "all pathname parameters with arrays" do |win32| path_types = { - "unix absolute" => "/foo/bar", - "unix relative" => "foo/bar", - "win32 absolute" => %q{\foo\bar}, - "win32 relative" => %q{foo\bar}, - "drive absolute" => %q{c:\foo\bar}, - "drive relative" => %q{c:foo\bar} + "unix absolute" => %q{/foo/bar}, + "unix relative" => %q{foo/bar}, + "win32 non-drive absolute" => %q{\foo\bar}, + "win32 non-drive relative" => %q{foo\bar}, + "win32 drive absolute" => %q{c:\foo\bar}, + "win32 drive relative" => %q{c:foo\bar} } describe "when given an array of paths" do (1..path_types.length).each do |n| path_types.keys.combination(n) do |set| data = path_types.collect { |k, v| set.member?(k) ? v : nil } .compact - reject = true - only_absolute = set.find { |k| k =~ /relative/ } .nil? - only_unix = set.reject { |k| k =~ /unix/ } .length == 0 - if only_absolute and (only_unix or win32) then + has_relative = set.find { |k| k =~ /relative/ or k =~ /non-drive/ } + has_windows = set.find { |k| k =~ /win32/ } + has_unix = set.find { |k| k =~ /unix/ } + + if has_relative or (has_windows and !win32) or (has_unix and win32) + reject = true + else reject = false end it "should #{reject ? 'reject' : 'accept'} #{set.join(", ")}" do if reject then expect { instance(data) }. should raise_error Puppet::Error, /fully qualified/ else instance = instance(data) instance[@param].should == data end end it "should #{reject ? 'reject' : 'accept'} #{set.join(", ")} doubled" do if reject then expect { instance(data + data) }. should raise_error Puppet::Error, /fully qualified/ else instance = instance(data + data) instance[@param].should == (data + data) end end end end end end shared_examples_for "all path parameters" do |param, options| # Extract and process options to the block. options ||= {} array = options[:array].nil? ? true : options.delete(:array) if options.keys.length > 0 then fail "unknown options for 'all path parameters': " + options.keys.sort.join(', ') end def instance(path) fail "we didn't implement the 'instance(path)' method in the it_should_behave_like block" end ######################################################################## # The actual testing code... before :all do @param = param end before :each do @file_separator = File::SEPARATOR end after :each do with_verbose_disabled do verbose, $VERBOSE = $VERBOSE, nil File::SEPARATOR = @file_separator $VERBOSE = verbose end end describe "on a Unix-like platform it" do before :each do with_verbose_disabled do File::SEPARATOR = '/' end Puppet.features.stubs(:microsoft_windows?).returns(false) Puppet.features.stubs(:posix?).returns(true) end if array then it_should_behave_like "all pathname parameters with arrays", false end it "should accept a fully qualified path" do path = File.join('', 'foo') instance = instance(path) instance[@param].should == path end it "should give a useful error when the path is not absolute" do path = 'foo' expect { instance(path) }. should raise_error Puppet::Error, /fully qualified/ end { "Unix" => '/', "Win32" => '\\' }.each do |style, slash| %w{q Q a A z Z c C}.sort.each do |drive| it "should reject drive letter '#{drive}' with #{style} path separators" do path = "#{drive}:#{slash}Program Files" expect { instance(path) }. should raise_error Puppet::Error, /fully qualified/ end end end end describe "on a Windows-like platform it" do before :each do with_verbose_disabled do File::SEPARATOR = '\\' end Puppet.features.stubs(:microsoft_windows?).returns(true) Puppet.features.stubs(:posix?).returns(false) end if array then it_should_behave_like "all pathname parameters with arrays", true end - it "should accept a fully qualified path" do - path = File.join('', 'foo') - instance = instance(path) - instance[@param].should == path + it "should reject a fully qualified unix path" do + path = '/foo' + expect { instance(path) }.to raise_error(Puppet::Error, /fully qualified/) end it "should give a useful error when the path is not absolute" do path = 'foo' expect { instance(path) }. should raise_error Puppet::Error, /fully qualified/ end it "also accepts Unix style path separators" do - path = '/Program Files' + path = 'C:/Program Files' instance = instance(path) instance[@param].should == path end { "Unix" => '/', "Win32" => '\\' }.each do |style, slash| %w{q Q a A z Z c C}.sort.each do |drive| it "should accept drive letter '#{drive}' with #{style} path separators " do path = "#{drive}:#{slash}Program Files" instance = instance(path) instance[@param].should == path end end end - { "UNC paths" => %q{\\foo\bar}, - "unparsed local paths" => %q{\\?\c:\foo}, - "unparsed UNC paths" => %q{\\?\foo\bar} + { "UNC paths" => %q{\\\\foo\bar}, + "unparsed local paths" => %q{\\\\?\c:\foo}, + "unparsed UNC paths" => %q{\\\\?\foo\bar} }.each do |name, path| it "should accept #{name} as absolute" do instance = instance(path) instance[@param].should == path end end end end diff --git a/spec/unit/provider/exec/posix_spec.rb b/spec/unit/provider/exec/posix_spec.rb index 883e3c716..78d0783af 100755 --- a/spec/unit/provider/exec/posix_spec.rb +++ b/spec/unit/provider/exec/posix_spec.rb @@ -1,121 +1,116 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:exec).provider(:posix) do include PuppetSpec::Files def make_exe command = tmpfile('my_command') FileUtils.touch(command) File.chmod(0755, command) command end - let(:resource) { Puppet::Type.type(:exec).new(:title => File.expand_path('/foo')) } + let(:resource) { Puppet::Type.type(:exec).new(:title => File.expand_path('/foo'), :provider => :posix) } let(:provider) { described_class.new(resource) } - before :each do - Puppet.features.stubs(:posix?).returns(true) - Puppet.features.stubs(:microsoft_windows?).returns(false) - end - describe "#validatecmd" do it "should fail if no path is specified and the command is not fully qualified" do expect { provider.validatecmd("foo") }.to raise_error( Puppet::Error, "'foo' is not qualified and no path was specified. Please qualify the command or specify a path." ) end it "should pass if a path is given" do provider.resource[:path] = ['/bogus/bin'] provider.validatecmd("../foo") end it "should pass if command is fully qualifed" do provider.resource[:path] = ['/bogus/bin'] provider.validatecmd(File.expand_path("/bin/blah/foo")) end end describe "#run" do describe "when the command is an absolute path" do let(:command) { tmpfile('foo') } it "should fail if the command doesn't exist" do expect { provider.run(command) }.to raise_error(ArgumentError, "Could not find command '#{command}'") end it "should fail if the command isn't a file" do FileUtils.mkdir(command) FileUtils.chmod(0755, command) expect { provider.run(command) }.to raise_error(ArgumentError, "'#{command}' is a directory, not a file") end it "should fail if the command isn't executable" do FileUtils.touch(command) File.stubs(:executable?).with(command).returns(false) expect { provider.run(command) }.to raise_error(ArgumentError, "'#{command}' is not executable") end end describe "when the command is a relative path" do it "should execute the command if it finds it in the path and is executable" do command = make_exe provider.resource[:path] = [File.dirname(command)] filename = File.basename(command) - Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == [filename]) && (arguments.is_a? Hash) } + Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == filename) && (arguments.is_a? Hash) } provider.run(filename) end it "should fail if the command isn't in the path" do resource[:path] = ["/fake/path"] expect { provider.run('foo') }.to raise_error(ArgumentError, "Could not find command 'foo'") end it "should fail if the command is in the path but not executable" do command = tmpfile('foo') FileUtils.touch(command) FileTest.stubs(:executable?).with(command).returns(false) resource[:path] = [File.dirname(command)] filename = File.basename(command) expect { provider.run(filename) }.to raise_error(ArgumentError, "Could not find command '#{filename}'") end end it "should not be able to execute shell builtins" do provider.resource[:path] = ['/bin'] expect { provider.run("cd ..") }.to raise_error(ArgumentError, "Could not find command 'cd'") end it "should execute the command if the command given includes arguments or subcommands" do provider.resource[:path] = ['/bogus/bin'] command = make_exe - Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == ["#{command} bar --sillyarg=true --blah"]) && (arguments.is_a? Hash) } + Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == "#{command} bar --sillyarg=true --blah") && (arguments.is_a? Hash) } provider.run("#{command} bar --sillyarg=true --blah") end it "should fail if quoted command doesn't exist" do provider.resource[:path] = ['/bogus/bin'] command = "#{File.expand_path('/foo')} bar --sillyarg=true --blah" expect { provider.run(%Q["#{command}"]) }.to raise_error(ArgumentError, "Could not find command '#{command}'") end it "should warn if you're overriding something in environment" do provider.resource[:environment] = ['WHATEVER=/something/else', 'WHATEVER=/foo'] command = make_exe - Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline== [command]) && (arguments.is_a? Hash) } + Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == command) && (arguments.is_a? Hash) } provider.run(command) @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: Overriding environment setting 'WHATEVER' with '/foo'"] end end end diff --git a/spec/unit/provider/exec/windows_spec.rb b/spec/unit/provider/exec/windows_spec.rb new file mode 100644 index 000000000..45830e668 --- /dev/null +++ b/spec/unit/provider/exec/windows_spec.rb @@ -0,0 +1,113 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +describe Puppet::Type.type(:exec).provider(:windows) do + include PuppetSpec::Files + + let(:resource) { Puppet::Type.type(:exec).new(:title => 'C:\foo', :provider => :windows) } + let(:provider) { described_class.new(resource) } + + before :each do + Puppet.features.stubs(:microsoft_windows?).returns(true) + Puppet.features.stubs(:posix?).returns(false) + end + + describe "#extractexe" do + describe "when the command has no arguments" do + it "should return the command if it's quoted" do + provider.extractexe('"foo"').should == 'foo' + end + + it "should return the command if it's quoted and contains spaces" do + provider.extractexe('"foo bar"').should == 'foo bar' + end + + it "should return the command if it's not quoted" do + provider.extractexe('foo').should == 'foo' + end + end + + describe "when the command has arguments" do + it "should return the command if it's quoted" do + provider.extractexe('"foo" bar baz').should == 'foo' + end + + it "should return the command if it's quoted and contains spaces" do + provider.extractexe('"foo bar" baz "quux quiz"').should == 'foo bar' + end + + it "should return the command if it's not quoted" do + provider.extractexe('foo bar baz').should == 'foo' + end + end + end + + describe "#checkexe" do + describe "when the command is absolute", :if => Puppet.features.microsoft_windows? do + it "should return if the command exists and is a file" do + command = tmpfile('command') + FileUtils.touch(command) + + provider.checkexe(command).should == nil + end + it "should fail if the command doesn't exist" do + command = tmpfile('command') + + expect { provider.checkexe(command) }.to raise_error(ArgumentError, "Could not find command '#{command}'") + end + it "should fail if the command isn't a file" do + command = tmpfile('command') + FileUtils.mkdir(command) + + expect { provider.checkexe(command) }.to raise_error(ArgumentError, "'#{command}' is a directory, not a file") + end + end + + describe "when the command is relative" do + describe "and a path is specified" do + before :each do + provider.stubs(:which) + end + + it "should search for executables with no extension" do + provider.expects(:which).with('foo').returns('foo') + + provider.checkexe('foo') + end + + %w[bat com ps1 exe].each do |ext| + it "should search for executables with the extension '#{ext}'" do + provider.expects(:which).with("foo.#{ext}").returns("foo.#{ext}") + + provider.checkexe('foo') + end + end + + it "should fail if the command isn't in the path" do + expect { provider.checkexe('foo') }.to raise_error(ArgumentError, "Could not find command 'foo'") + end + end + + it "should fail if no path is specified" do + expect { provider.checkexe('foo') }.to raise_error(ArgumentError, "Could not find command 'foo'") + end + end + end + + describe "#validatecmd" do + it "should fail if the command isn't absolute and there is no path" do + expect { provider.validatecmd('foo') }.to raise_error(Puppet::Error, /'foo' is not qualified and no path was specified/) + end + + it "should not fail if the command is absolute and there is no path" do + provider.validatecmd('C:\foo').should == nil + end + + it "should not fail if the command is not absolute and there is a path" do + resource[:path] = 'C:\path;C:\another_path' + + provider.validatecmd('foo').should == nil + end + end +end diff --git a/spec/unit/provider/service/redhat_spec.rb b/spec/unit/provider/service/redhat_spec.rb index b7f56e089..aabcf8d0f 100755 --- a/spec/unit/provider/service/redhat_spec.rb +++ b/spec/unit/provider/service/redhat_spec.rb @@ -1,121 +1,123 @@ #!/usr/bin/env rspec # # Unit testing for the RedHat service Provider # require 'spec_helper' provider_class = Puppet::Type.type(:service).provider(:redhat) describe provider_class do before :each do + Puppet.features.stubs(:posix?).returns(true) + Puppet.features.stubs(:microsoft_windows?).returns(false) @class = Puppet::Type.type(:service).provider(:redhat) @resource = stub 'resource' @resource.stubs(:[]).returns(nil) @resource.stubs(:[]).with(:name).returns "myservice" @provider = provider_class.new @resource.stubs(:provider).returns @provider @provider.resource = @resource @provider.stubs(:get).with(:hasstatus).returns false FileTest.stubs(:file?).with('/sbin/service').returns true FileTest.stubs(:executable?).with('/sbin/service').returns true end # test self.instances describe "when getting all service instances" do before :each do @services = ['one', 'two', 'three', 'four', 'kudzu', 'functions', 'halt', 'killall', 'single', 'linuxconf'] @not_services = ['functions', 'halt', 'killall', 'single', 'linuxconf'] Dir.stubs(:entries).returns @services FileTest.stubs(:directory?).returns(true) FileTest.stubs(:executable?).returns(true) end it "should return instances for all services" do (@services-@not_services).each do |inst| @class.expects(:new).with{|hash| hash[:name] == inst && hash[:path] == '/etc/init.d'}.returns("#{inst}_instance") end results = (@services-@not_services).collect {|x| "#{x}_instance"} @class.instances.should == results end it "should call service status when initialized from provider" do @resource.stubs(:[]).with(:status).returns nil @provider.stubs(:get).with(:hasstatus).returns true @provider.expects(:execute).with{|command, *args| command == ['/sbin/service', 'myservice', 'status']} @provider.send(:status) end end it "should have an enabled? method" do @provider.should respond_to(:enabled?) end it "should have an enable method" do @provider.should respond_to(:enable) end it "should have a disable method" do @provider.should respond_to(:disable) end [:start, :stop, :status, :restart].each do |method| it "should have a #{method} method" do @provider.should respond_to(method) end describe "when running #{method}" do it "should use any provided explicit command" do @resource.stubs(:[]).with(method).returns "/user/specified/command" @provider.expects(:execute).with { |command, *args| command == ["/user/specified/command"] } @provider.send(method) end it "should execute the service script with #{method} when no explicit command is provided" do @resource.stubs(:[]).with("has#{method}".intern).returns :true @provider.expects(:execute).with { |command, *args| command == ['/sbin/service', 'myservice', method.to_s]} @provider.send(method) end end end describe "when checking status" do describe "when hasstatus is :true" do before :each do @resource.stubs(:[]).with(:hasstatus).returns :true end it "should execute the service script with fail_on_failure false" do @provider.expects(:texecute).with(:status, ['/sbin/service', 'myservice', 'status'], false) @provider.status end it "should consider the process running if the command returns 0" do @provider.expects(:texecute).with(:status, ['/sbin/service', 'myservice', 'status'], false) $CHILD_STATUS.stubs(:exitstatus).returns(0) @provider.status.should == :running end [-10,-1,1,10].each { |ec| it "should consider the process stopped if the command returns something non-0" do @provider.expects(:texecute).with(:status, ['/sbin/service', 'myservice', 'status'], false) $CHILD_STATUS.stubs(:exitstatus).returns(ec) @provider.status.should == :stopped end } end describe "when hasstatus is not :true" do it "should consider the service :running if it has a pid" do @provider.expects(:getpid).returns "1234" @provider.status.should == :running end it "should consider the service :stopped if it doesn't have a pid" do @provider.expects(:getpid).returns nil @provider.status.should == :stopped end end end describe "when restarting and hasrestart is not :true" do it "should stop and restart the process with the server script" do @provider.expects(:texecute).with(:stop, ['/sbin/service', 'myservice', 'stop'], true) @provider.expects(:texecute).with(:start, ['/sbin/service', 'myservice', 'start'], true) @provider.restart end end end diff --git a/spec/unit/provider/service/smf_spec.rb b/spec/unit/provider/service/smf_spec.rb index fd7d50e3a..dc7438cae 100755 --- a/spec/unit/provider/service/smf_spec.rb +++ b/spec/unit/provider/service/smf_spec.rb @@ -1,138 +1,140 @@ #!/usr/bin/env rspec # # Unit testing for the SMF service Provider # # author Dominic Cleal # require 'spec_helper' provider_class = Puppet::Type.type(:service).provider(:smf) describe provider_class do before(:each) do + Puppet.features.stubs(:posix?).returns(true) + Puppet.features.stubs(:microsoft_windows?).returns(false) # Create a mock resource @resource = Puppet::Type.type(:service).new( :name => "/system/myservice", :ensure => :running, :enable => :true) @provider = provider_class.new(@resource) FileTest.stubs(:file?).with('/usr/sbin/svcadm').returns true FileTest.stubs(:executable?).with('/usr/sbin/svcadm').returns true FileTest.stubs(:file?).with('/usr/bin/svcs').returns true FileTest.stubs(:executable?).with('/usr/bin/svcs').returns true end it "should have a restart method" do @provider.should respond_to(:restart) end it "should have a restartcmd method" do @provider.should respond_to(:restartcmd) end it "should have a start method" do @provider.should respond_to(:start) end it "should have a stop method" do @provider.should respond_to(:stop) end it "should have an enabled? method" do @provider.should respond_to(:enabled?) end it "should have an enable method" do @provider.should respond_to(:enable) end it "should have a disable method" do @provider.should respond_to(:disable) end describe "when checking status" do it "should call the external command 'svcs /system/myservice' once" do @provider.expects(:svcs).with('-H', '-o', 'state,nstate', "/system/myservice").returns("online\t-") @provider.status end it "should return stopped if svcs can't find the service" do @provider.stubs(:svcs).raises(Puppet::ExecutionFailure.new("no svc found")) @provider.status.should == :stopped end it "should return running if online in svcs output" do @provider.stubs(:svcs).returns("online\t-") @provider.status.should == :running end it "should return stopped if disabled in svcs output" do @provider.stubs(:svcs).returns("disabled\t-") @provider.status.should == :stopped end it "should return maintenance if in maintenance in svcs output" do @provider.stubs(:svcs).returns("maintenance\t-") @provider.status.should == :maintenance end it "should return target state if transitioning in svcs output" do @provider.stubs(:svcs).returns("online\tdisabled") @provider.status.should == :stopped end it "should throw error if it's a legacy service in svcs output" do @provider.stubs(:svcs).returns("legacy_run\t-") lambda { @provider.status }.should raise_error(Puppet::Error, "Cannot manage legacy services through SMF") end end describe "when starting" do it "should enable the service if it is not enabled" do @provider.expects(:status).returns :stopped @provider.expects(:texecute) @provider.start end it "should always execute external command 'svcadm enable /system/myservice'" do @provider.stubs(:status).returns :running @provider.expects(:texecute).with(:start, ["/usr/sbin/svcadm", :enable, "/system/myservice"], true) @provider.start end it "should execute external command 'svcadm clear /system/myservice' if in maintenance" do @provider.stubs(:status).returns :maintenance @provider.expects(:texecute).with(:start, ["/usr/sbin/svcadm", :clear, "/system/myservice"], true) @provider.start end end describe "when starting a service with a manifest" do before(:each) do @resource = Puppet::Type.type(:service).new(:name => "/system/myservice", :ensure => :running, :enable => :true, :manifest => "/tmp/myservice.xml") @provider = provider_class.new(@resource) $CHILD_STATUS.stubs(:exitstatus).returns(1) end it "should import the manifest if service is missing" do @provider.expects(:svccfg).with(:import, "/tmp/myservice.xml") @provider.expects(:texecute).with(:start, ["/usr/sbin/svcadm", :enable, "/system/myservice"], true) @provider.expects(:svcs).with('-H', '-o', 'state,nstate', "/system/myservice").returns("online\t-") @provider.start end it "should handle failures if importing a manifest" do @provider.expects(:svccfg).raises(Puppet::ExecutionFailure.new("can't svccfg import")) lambda { @provider.start }.should raise_error(Puppet::Error, "Cannot config /system/myservice to enable it: can't svccfg import") end end describe "when stopping" do it "should execute external command 'svcadm disable /system/myservice'" do @provider.expects(:texecute).with(:stop, ["/usr/sbin/svcadm", :disable, "/system/myservice"], true) @provider.stop end end describe "when restarting" do it "should call 'svcadm restart /system/myservice'" do @provider.expects(:texecute).with(:restart, ["/usr/sbin/svcadm", :restart, "/system/myservice"], true) @provider.restart end end end diff --git a/spec/unit/type/exec_spec.rb b/spec/unit/type/exec_spec.rb index 8146dce92..2599a2367 100755 --- a/spec/unit/type/exec_spec.rb +++ b/spec/unit/type/exec_spec.rb @@ -1,667 +1,676 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:exec) do include PuppetSpec::Files def exec_tester(command, exitstatus = 0, rest = {}) - @user_name = 'some_user_name' - @group_name = 'some_group_name' Puppet.features.stubs(:root?).returns(true) output = rest.delete(:output) || '' tries = rest[:tries] || 1 args = { :name => command, :path => @example_path, - :user => @user_name, - :group => @group_name, :logoutput => false, :loglevel => :err, :returns => 0 }.merge(rest) exec = Puppet::Type.type(:exec).new(args) status = stub "process", :exitstatus => exitstatus Puppet::Util::SUIDManager.expects(:run_and_capture).times(tries). - with([command], @user_name, @group_name).returns([output, status]) + with(command, nil, nil).returns([output, status]) return exec end before do @command = make_absolute('/bin/true whatever') @executable = make_absolute('/bin/true') @bogus_cmd = make_absolute('/bogus/cmd') end - describe "when not stubbing the provider", :fails_on_windows => true do + describe "when not stubbing the provider" do before do - File.stubs(:exists?).returns false - File.stubs(:exists?).with(@executable).returns true - File.stubs(:exists?).with('/bin/false').returns true - @example_path = Puppet.features.posix? ? %w{/usr/bin /bin} : [ "C:/Program Files/something/bin", "C:/Ruby/bin" ] - File.stubs(:exists?).with(File.join(@example_path[0],"true")).returns true - File.stubs(:exists?).with(File.join(@example_path[0],"false")).returns true + path = tmpdir('path') + true_cmd = File.join(path, 'true') + false_cmd = File.join(path, 'false') + + FileUtils.touch(true_cmd) + FileUtils.touch(false_cmd) + + File.chmod(0755, true_cmd) + File.chmod(0755, false_cmd) + + @example_path = [path] end it "should return :executed_command as its event" do resource = Puppet::Type.type(:exec).new :command => @command resource.parameter(:returns).event.name.should == :executed_command end describe "when execing" do it "should use the 'run_and_capture' method to exec" do exec_tester("true").refresh.should == :executed_command end it "should report a failure" do proc { exec_tester('false', 1).refresh }. should raise_error(Puppet::Error, /^false returned 1 instead of/) end it "should not report a failure if the exit status is specified in a returns array" do proc { exec_tester("false", 1, :returns => [0, 1]).refresh }.should_not raise_error end it "should report a failure if the exit status is not specified in a returns array" do proc { exec_tester('false', 1, :returns => [0, 100]).refresh }. should raise_error(Puppet::Error, /^false returned 1 instead of/) end it "should log the output on success" do output = "output1\noutput2\n" exec_tester('false', 0, :output => output, :logoutput => true).refresh output.split("\n").each do |line| log = @logs.shift log.level.should == :err log.message.should == line end end it "should log the output on failure" do output = "output1\noutput2\n" proc { exec_tester('false', 1, :output => output, :logoutput => true).refresh }. should raise_error(Puppet::Error) output.split("\n").each do |line| log = @logs.shift log.level.should == :err log.message.should == line end end end describe "when logoutput=>on_failure is set" do it "should log the output on failure" do output = "output1\noutput2\n" proc { exec_tester('false', 1, :output => output, :logoutput => :on_failure).refresh }. should raise_error(Puppet::Error, /^false returned 1 instead of/) output.split("\n").each do |line| log = @logs.shift log.level.should == :err log.message.should == line end end it "should log the output on failure when returns is specified as an array" do output = "output1\noutput2\n" proc { exec_tester('false', 1, :output => output, :returns => [0, 100], :logoutput => :on_failure).refresh }.should raise_error(Puppet::Error, /^false returned 1 instead of/) output.split("\n").each do |line| log = @logs.shift log.level.should == :err log.message.should == line end end it "shouldn't log the output on success" do exec_tester('true', 0, :output => "a\nb\nc\n", :logoutput => :on_failure).refresh @logs.should == [] end end it "shouldn't log the output on success when non-zero exit status is in a returns array" do exec_tester("true", 100, :output => "a\n", :logoutput => :on_failure, :returns => [1, 100]).refresh @logs.should == [] end describe " when multiple tries are set," do it "should repeat the command attempt 'tries' times on failure and produce an error" do tries = 5 resource = exec_tester("false", 1, :tries => tries, :try_sleep => 0) proc { resource.refresh }.should raise_error(Puppet::Error) end end end - it "should be able to autorequire files mentioned in the command", :fails_on_windows => true do + it "should be able to autorequire files mentioned in the command" do foo = make_absolute('/bin/foo') catalog = Puppet::Resource::Catalog.new tmp = Puppet::Type.type(:file).new(:name => foo) catalog.add_resource tmp execer = Puppet::Type.type(:exec).new(:name => foo) catalog.add_resource execer catalog.relationship_graph.dependencies(execer).should == [tmp] end - describe "when handling the path parameter", :fails_on_windows => true do + describe "when handling the path parameter" do expect = %w{one two three four} { "an array" => expect, "a colon separated list" => "one:two:three:four", "a semi-colon separated list" => "one;two;three;four", "both array and colon lists" => ["one", "two:three", "four"], "both array and semi-colon lists" => ["one", "two;three", "four"], "colon and semi-colon lists" => ["one:two", "three;four"] }.each do |test, input| it "should accept #{test}" do type = Puppet::Type.type(:exec).new(:name => @command, :path => input) type[:path].should == expect end end end describe "when setting user", :fails_on_windows => true do it "should fail if we are not root" do Puppet.features.stubs(:root?).returns(false) expect { Puppet::Type.type(:exec).new(:name => @command, :user => 'input') }. should raise_error Puppet::Error, /Parameter user failed/ end ['one', 2, 'root', 4294967295, 4294967296].each do |value| it "should accept '#{value}' as user if we are root" do Puppet.features.stubs(:root?).returns(true) type = Puppet::Type.type(:exec).new(:name => @command, :user => value) type[:user].should == value end end end describe "when setting group" do shared_examples_for "exec[:group]" do ['one', 2, 'wheel', 4294967295, 4294967296].each do |value| it "should accept '#{value}' without error or judgement", :fails_on_windows => true do type = Puppet::Type.type(:exec).new(:name => @command, :group => value) type[:group].should == value end end end describe "when running as root" do before :each do Puppet.features.stubs(:root?).returns(true) end it_behaves_like "exec[:group]" end describe "when not running as root" do before :each do Puppet.features.stubs(:root?).returns(false) end it_behaves_like "exec[:group]" end end describe "when setting cwd" do it_should_behave_like "all path parameters", :cwd, :array => false do def instance(path) - Puppet::Type.type(:exec).new(:name => @executable, :cwd => path) + # Specify shell provider so we don't have to care about command validation + Puppet::Type.type(:exec).new(:name => @executable, :cwd => path, :provider => :shell) end end end shared_examples_for "all exec command parameters" do |param| { "relative" => "example", "absolute" => "/bin/example" }.sort.each do |name, command| describe "if command is #{name}" do before :each do @param = param end def test(command, valid) if @param == :name then instance = Puppet::Type.type(:exec).new() else instance = Puppet::Type.type(:exec).new(:name => @executable) end if valid then instance.provider.expects(:validatecmd).returns(true) else instance.provider.expects(:validatecmd).raises(Puppet::Error, "from a stub") end instance[@param] = command end it "should work if the provider calls the command valid" do expect { test(command, true) }.should_not raise_error end it "should fail if the provider calls the command invalid" do expect { test(command, false) }. should raise_error Puppet::Error, /Parameter #{@param} failed: from a stub/ end end end end shared_examples_for "all exec command parameters that take arrays" do |param| describe "when given an array of inputs" do before :each do @test = Puppet::Type.type(:exec).new(:name => @executable) end it "should accept the array when all commands return valid" do input = %w{one two three} @test.provider.expects(:validatecmd).times(input.length).returns(true) @test[param] = input @test[param].should == input end it "should reject the array when any commands return invalid" do input = %w{one two three} @test.provider.expects(:validatecmd).with(input.first).returns(false) input[1..-1].each do |cmd| @test.provider.expects(:validatecmd).with(cmd).returns(true) end @test[param] = input @test[param].should == input end it "should reject the array when all commands return invalid" do input = %w{one two three} @test.provider.expects(:validatecmd).times(input.length).returns(false) @test[param] = input @test[param].should == input end end end describe "when setting refresh" do it_should_behave_like "all exec command parameters", :refresh end describe "for simple parameters" do before :each do @exec = Puppet::Type.type(:exec).new(:name => @executable) end describe "when setting environment" do { "single values" => "foo=bar", "multiple values" => ["foo=bar", "baz=quux"], }.each do |name, data| it "should accept #{name}" do @exec[:environment] = data @exec[:environment].should == data end end { "single values" => "foo", "only values" => ["foo", "bar"], "any values" => ["foo=bar", "baz"] }.each do |name, data| it "should reject #{name} without assignment" do expect { @exec[:environment] = data }. should raise_error Puppet::Error, /Invalid environment setting/ end end end describe "when setting timeout" do [0, 0.1, 1, 10, 4294967295].each do |valid| it "should accept '#{valid}' as valid" do @exec[:timeout] = valid @exec[:timeout].should == valid end it "should accept '#{valid}' in an array as valid" do @exec[:timeout] = [valid] @exec[:timeout].should == valid end end ['1/2', '', 'foo', '5foo'].each do |invalid| it "should reject '#{invalid}' as invalid" do expect { @exec[:timeout] = invalid }. should raise_error Puppet::Error, /The timeout must be a number/ end it "should reject '#{invalid}' in an array as invalid" do expect { @exec[:timeout] = [invalid] }. should raise_error Puppet::Error, /The timeout must be a number/ end end - # REMIND: the exec provider is not supported on windows yet it "should fail if timeout is exceeded", :fails_on_windows => true do - File.stubs(:exists?).with('/bin/sleep').returns(true) - File.stubs(:exists?).with('sleep').returns(false) + Puppet::Util.stubs(:execute).with do |cmd,args| + sleep 1 + true + end + FileTest.stubs(:file?).returns(false) + FileTest.stubs(:file?).with('/bin/sleep').returns(true) + FileTest.stubs(:executable?).returns(false) + FileTest.stubs(:executable?).with('/bin/sleep').returns(true) sleep_exec = Puppet::Type.type(:exec).new(:name => 'sleep 1', :path => ['/bin'], :timeout => '0.2') + lambda { sleep_exec.refresh }.should raise_error Puppet::Error, "Command exceeded timeout" end it "should convert timeout to a float" do command = make_absolute('/bin/false') resource = Puppet::Type.type(:exec).new :command => command, :timeout => "12" resource[:timeout].should be_a(Float) resource[:timeout].should == 12.0 end it "should munge negative timeouts to 0.0" do command = make_absolute('/bin/false') resource = Puppet::Type.type(:exec).new :command => command, :timeout => "-12.0" resource.parameter(:timeout).value.should be_a(Float) resource.parameter(:timeout).value.should == 0.0 end end describe "when setting tries" do [1, 10, 4294967295].each do |valid| it "should accept '#{valid}' as valid" do @exec[:tries] = valid @exec[:tries].should == valid end if "REVISIT: too much test log spam" == "a good thing" then it "should accept '#{valid}' in an array as valid" do pending "inconsistent, but this is not supporting arrays, unlike timeout" @exec[:tries] = [valid] @exec[:tries].should == valid end end end [-3.5, -1, 0, 0.2, '1/2', '1_000_000', '+12', '', 'foo'].each do |invalid| it "should reject '#{invalid}' as invalid" do expect { @exec[:tries] = invalid }. should raise_error Puppet::Error, /Tries must be an integer/ end if "REVISIT: too much test log spam" == "a good thing" then it "should reject '#{invalid}' in an array as invalid" do pending "inconsistent, but this is not supporting arrays, unlike timeout" expect { @exec[:tries] = [invalid] }. should raise_error Puppet::Error, /Tries must be an integer/ end end end end describe "when setting try_sleep" do [0, 0.2, 1, 10, 4294967295].each do |valid| it "should accept '#{valid}' as valid" do @exec[:try_sleep] = valid @exec[:try_sleep].should == valid end if "REVISIT: too much test log spam" == "a good thing" then it "should accept '#{valid}' in an array as valid" do pending "inconsistent, but this is not supporting arrays, unlike timeout" @exec[:try_sleep] = [valid] @exec[:try_sleep].should == valid end end end { -3.5 => "cannot be a negative number", -1 => "cannot be a negative number", '1/2' => 'must be a number', '1_000_000' => 'must be a number', '+12' => 'must be a number', '' => 'must be a number', 'foo' => 'must be a number', }.each do |invalid, error| it "should reject '#{invalid}' as invalid" do expect { @exec[:try_sleep] = invalid }. should raise_error Puppet::Error, /try_sleep #{error}/ end if "REVISIT: too much test log spam" == "a good thing" then it "should reject '#{invalid}' in an array as invalid" do pending "inconsistent, but this is not supporting arrays, unlike timeout" expect { @exec[:try_sleep] = [invalid] }. should raise_error Puppet::Error, /try_sleep #{error}/ end end end end describe "when setting refreshonly" do [:true, :false].each do |value| it "should accept '#{value}'" do @exec[:refreshonly] = value @exec[:refreshonly].should == value end end [1, 0, "1", "0", "yes", "y", "no", "n"].each do |value| it "should reject '#{value}'" do expect { @exec[:refreshonly] = value }. should raise_error(Puppet::Error, /Invalid value #{value.inspect}\. Valid values are true, false/ ) end end end describe "when setting creates" do it_should_behave_like "all path parameters", :creates, :array => true do def instance(path) - Puppet::Type.type(:exec).new(:name => @executable, :creates => path) + # Specify shell provider so we don't have to care about command validation + Puppet::Type.type(:exec).new(:name => @executable, :creates => path, :provider => :shell) end end end end describe "when setting unless" do it_should_behave_like "all exec command parameters", :unless it_should_behave_like "all exec command parameters that take arrays", :unless end describe "when setting onlyif" do it_should_behave_like "all exec command parameters", :onlyif it_should_behave_like "all exec command parameters that take arrays", :onlyif end describe "#check" do before :each do @test = Puppet::Type.type(:exec).new(:name => @executable) end describe ":refreshonly" do { :true => false, :false => true }.each do |input, result| it "should return '#{result}' when given '#{input}'" do @test[:refreshonly] = input @test.check_all_attributes.should == result end end end describe ":creates" do - before :all do - @exist = "/" - @unexist = "/this/path/should/never/exist" - while FileTest.exist?(@unexist) do @unexist += "/foo" end + before :each do + @exist = tmpfile('exist') + FileUtils.touch(@exist) + @unexist = tmpfile('unexist') end context "with a single item" do it "should run when the item does not exist" do @test[:creates] = @unexist @test.check_all_attributes.should == true end it "should not run when the item exists" do @test[:creates] = @exist @test.check_all_attributes.should == false end end context "with an array with one item" do it "should run when the item does not exist" do @test[:creates] = [@unexist] @test.check_all_attributes.should == true end it "should not run when the item exists" do @test[:creates] = [@exist] @test.check_all_attributes.should == false end end context "with an array with multiple items" do it "should run when all items do not exist" do @test[:creates] = [@unexist] * 3 @test.check_all_attributes.should == true end it "should not run when one item exists" do @test[:creates] = [@unexist, @exist, @unexist] @test.check_all_attributes.should == false end it "should not run when all items exist" do @test[:creates] = [@exist] * 3 end end end { :onlyif => { :pass => false, :fail => true }, :unless => { :pass => true, :fail => false }, }.each do |param, sense| describe ":#{param}" do before :each do @pass = make_absolute("/magic/pass") @fail = make_absolute("/magic/fail") @pass_status = stub('status', :exitstatus => sense[:pass] ? 0 : 1) @fail_status = stub('status', :exitstatus => sense[:fail] ? 0 : 1) @test.provider.stubs(:checkexe).returns(true) [true, false].each do |check| @test.provider.stubs(:run).with(@pass, check). returns(['test output', @pass_status]) @test.provider.stubs(:run).with(@fail, check). returns(['test output', @fail_status]) end end context "with a single item" do it "should run if the command exits non-zero" do @test[param] = @fail @test.check_all_attributes.should == true end it "should not run if the command exits zero" do @test[param] = @pass @test.check_all_attributes.should == false end end context "with an array with a single item" do it "should run if the command exits non-zero" do @test[param] = [@fail] @test.check_all_attributes.should == true end it "should not run if the command exits zero" do @test[param] = [@pass] @test.check_all_attributes.should == false end end context "with an array with multiple items" do it "should run if all the commands exits non-zero" do @test[param] = [@fail] * 3 @test.check_all_attributes.should == true end it "should not run if one command exits zero" do @test[param] = [@pass, @fail, @pass] @test.check_all_attributes.should == false end it "should not run if all command exits zero" do @test[param] = [@pass] * 3 @test.check_all_attributes.should == false end end end end end describe "#retrieve", :fails_on_windows => true do before :each do @exec_resource = Puppet::Type.type(:exec).new(:name => @bogus_cmd) end it "should return :notrun when check_all_attributes returns true" do @exec_resource.stubs(:check_all_attributes).returns true @exec_resource.retrieve[:returns].should == :notrun end it "should return default exit code 0 when check_all_attributes returns false" do @exec_resource.stubs(:check_all_attributes).returns false @exec_resource.retrieve[:returns].should == ['0'] end it "should return the specified exit code when check_all_attributes returns false" do @exec_resource.stubs(:check_all_attributes).returns false @exec_resource[:returns] = 42 @exec_resource.retrieve[:returns].should == ["42"] end end describe "#output" do before :each do @exec_resource = Puppet::Type.type(:exec).new(:name => @bogus_cmd) end it "should return the provider's run output" do provider = stub 'provider' status = stubs "process_status" status.stubs(:exitstatus).returns("0") provider.expects(:run).returns(["silly output", status]) @exec_resource.stubs(:provider).returns(provider) @exec_resource.refresh @exec_resource.output.should == 'silly output' end end describe "#refresh" do before :each do @exec_resource = Puppet::Type.type(:exec).new(:name => @bogus_cmd) end it "should call provider run with the refresh parameter if it is set" do myother_bogus_cmd = make_absolute('/myother/bogus/cmd') provider = stub 'provider' @exec_resource.stubs(:provider).returns(provider) @exec_resource.stubs(:[]).with(:refresh).returns(myother_bogus_cmd) provider.expects(:run).with(myother_bogus_cmd) @exec_resource.refresh end it "should call provider run with the specified command if the refresh parameter is not set" do provider = stub 'provider' status = stubs "process_status" status.stubs(:exitstatus).returns("0") provider.expects(:run).with(@bogus_cmd).returns(["silly output", status]) @exec_resource.stubs(:provider).returns(provider) @exec_resource.refresh end it "should not run the provider if check_all_attributes is false" do @exec_resource.stubs(:check_all_attributes).returns false provider = stub 'provider' provider.expects(:run).never @exec_resource.stubs(:provider).returns(provider) @exec_resource.refresh end end end diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb new file mode 100644 index 000000000..4481f3863 --- /dev/null +++ b/spec/unit/util_spec.rb @@ -0,0 +1,335 @@ +#!/usr/bin/env ruby + +require 'spec_helper' + +describe Puppet::Util do + describe "#absolute_path?" do + it "should default to the platform of the local system" do + Puppet.features.stubs(:posix?).returns(true) + Puppet.features.stubs(:microsoft_windows?).returns(false) + + Puppet::Util.should be_absolute_path('/foo') + Puppet::Util.should_not be_absolute_path('C:/foo') + + Puppet.features.stubs(:posix?).returns(false) + Puppet.features.stubs(:microsoft_windows?).returns(true) + + Puppet::Util.should be_absolute_path('C:/foo') + Puppet::Util.should_not be_absolute_path('/foo') + end + + describe "when using platform :posix" do + %w[/ /foo /foo/../bar //foo //Server/Foo/Bar //?/C:/foo/bar /\Server/Foo].each do |path| + it "should return true for #{path}" do + Puppet::Util.should be_absolute_path(path, :posix) + end + end + + %w[. ./foo \foo C:/foo \\Server\Foo\Bar \\?\C:\foo\bar \/?/foo\bar \/Server/foo].each do |path| + it "should return false for #{path}" do + Puppet::Util.should_not be_absolute_path(path, :posix) + end + end + end + + describe "when using platform :windows" do + %w[C:/foo C:\foo \\\\Server\Foo\Bar \\\\?\C:\foo\bar //Server/Foo/Bar //?/C:/foo/bar /\?\C:/foo\bar \/Server\Foo/Bar].each do |path| + it "should return true for #{path}" do + Puppet::Util.should be_absolute_path(path, :windows) + end + end + + %w[/ . ./foo \foo /foo /foo/../bar //foo C:foo/bar].each do |path| + it "should return false for #{path}" do + Puppet::Util.should_not be_absolute_path(path, :windows) + end + end + end + end + + describe "execution methods" do + let(:pid) { 5501 } + let(:null_file) { Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' } + + describe "#execute_posix" do + before :each do + # Most of the things this method does are bad to do during specs. :/ + Kernel.stubs(:fork).returns(pid).yields + Process.stubs(:setsid) + Kernel.stubs(:exec) + Puppet::Util::SUIDManager.stubs(:change_user) + Puppet::Util::SUIDManager.stubs(:change_group) + + $stdin.stubs(:reopen) + $stdout.stubs(:reopen) + $stderr.stubs(:reopen) + + @stdin = File.open(null_file, 'r') + @stdout = Tempfile.new('stdout') + @stderr = File.open(null_file, 'w') + end + + it "should fork a child process to execute the command" do + Kernel.expects(:fork).returns(pid).yields + Kernel.expects(:exec).with('test command') + + Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) + end + + it "should start a new session group" do + Process.expects(:setsid) + + Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) + end + + it "should close all open file descriptors except stdin/stdout/stderr" do + # This is ugly, but I can't really think of a better way to do it without + # letting it actually close fds, which seems risky + (0..2).each {|n| IO.expects(:new).with(n).never} + (3..256).each {|n| IO.expects(:new).with(n).returns mock('io', :close) } + + Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) + end + + it "should permanently change to the correct user and group if specified" do + Puppet::Util::SUIDManager.expects(:change_group).with(55, true) + Puppet::Util::SUIDManager.expects(:change_user).with(50, true) + + Puppet::Util.execute_posix('test command', {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) + end + + it "should exit failure if there is a problem execing the command" do + Kernel.expects(:exec).with('test command').raises("failed to execute!") + Puppet::Util.stubs(:puts) + Puppet::Util.expects(:exit!).with(1) + + Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) + end + + it "should properly execute commands specified as arrays" do + Kernel.expects(:exec).with('test command', 'with', 'arguments') + + Puppet::Util.execute_posix(['test command', 'with', 'arguments'], {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) + end + + it "should return the pid of the child process" do + Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr).should == pid + end + end + + describe "#execute_windows" do + let(:proc_info_stub) { stub 'processinfo', :process_id => pid } + + before :each do + Process.stubs(:create).returns(proc_info_stub) + Process.stubs(:waitpid2).with(pid).returns([pid, 0]) + + @stdin = File.open(null_file, 'r') + @stdout = Tempfile.new('stdout') + @stderr = File.open(null_file, 'w') + end + + it "should create a new process for the command" do + Process.expects(:create).with( + :command_line => "test command", + :startup_info => {:stdin => @stdin, :stdout => @stdout, :stderr => @stderr} + ).returns(proc_info_stub) + + Puppet::Util.execute_windows('test command', {}, @stdin, @stdout, @stderr) + end + + it "should return the pid of the child process" do + Puppet::Util.execute_windows('test command', {}, @stdin, @stdout, @stderr).should == pid + end + + it "should quote arguments containing spaces if command is specified as an array" do + Process.expects(:create).with do |args| + args[:command_line] == '"test command" with some "arguments \"with spaces"' + end.returns(proc_info_stub) + + Puppet::Util.execute_windows(['test command', 'with', 'some', 'arguments "with spaces'], {}, @stdin, @stdout, @stderr) + end + end + + describe "#execute" do + before :each do + Process.stubs(:waitpid2).with(pid).returns([pid, 0]) + end + + describe "when an execution stub is specified" do + before :each do + Puppet::Util::ExecutionStub.set do |command,args,stdin,stdout,stderr| + "execution stub output" + end + end + + it "should call the block on the stub" do + Puppet::Util.execute("/usr/bin/run_my_execute_stub").should == "execution stub output" + end + + it "should not actually execute anything" do + Puppet::Util.expects(:execute_posix).never + Puppet::Util.expects(:execute_windows).never + + Puppet::Util.execute("/usr/bin/run_my_execute_stub") + end + end + + describe "when setting up input and output files" do + include PuppetSpec::Files + let(:executor) { Puppet.features.microsoft_windows? ? 'execute_windows' : 'execute_posix' } + + before :each do + Puppet::Util.stubs(:wait_for_output) + end + + it "should set stdin to the stdinfile if specified" do + input = tmpfile('stdin') + FileUtils.touch(input) + + Puppet::Util.expects(executor).with do |_,_,stdin,_,_| + stdin.path == input + end.returns(pid) + + Puppet::Util.execute('test command', :stdinfile => input) + end + + it "should set stdin to the null file if not specified" do + Puppet::Util.expects(executor).with do |_,_,stdin,_,_| + stdin.path == null_file + end.returns(pid) + + Puppet::Util.execute('test command') + end + + describe "when squelch is set" do + it "should set stdout and stderr to the null file" do + Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| + stdout.path == null_file and stderr.path == null_file + end.returns(pid) + + Puppet::Util.execute('test command', :squelch => true) + end + end + + describe "when squelch is not set" do + it "should set stdout to a temporary output file" do + outfile = Tempfile.new('stdout') + Tempfile.stubs(:new).returns(outfile) + + Puppet::Util.expects(executor).with do |_,_,_,stdout,_| + stdout.path == outfile.path + end.returns(pid) + + Puppet::Util.execute('test command', :squelch => false) + end + + it "should set stderr to the same file as stdout if combine is true" do + outfile = Tempfile.new('stdout') + Tempfile.stubs(:new).returns(outfile) + + Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| + stdout.path == outfile.path and stderr.path == outfile.path + end.returns(pid) + + Puppet::Util.execute('test command', :squelch => false, :combine => true) + end + + it "should set stderr to the null device if combine is false" do + outfile = Tempfile.new('stdout') + Tempfile.stubs(:new).returns(outfile) + + Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| + stdout.path == outfile.path and stderr.path == null_file + end.returns(pid) + + Puppet::Util.execute('test command', :squelch => false, :combine => false) + end + end + end + end + + describe "after execution" do + let(:executor) { Puppet.features.microsoft_windows? ? 'execute_windows' : 'execute_posix' } + before :each do + Process.stubs(:waitpid2).with(pid).returns([pid, 0]) + + Puppet::Util.stubs(executor).returns(pid) + end + + it "should wait for the child process to exit" do + Puppet::Util.stubs(:wait_for_output) + + Process.expects(:waitpid2).with(pid).returns([pid, 0]) + + Puppet::Util.execute('test command') + end + + it "should close the stdin/stdout/stderr files used by the child" do + stdin = mock 'file', :close + stdout = mock 'file', :close + stderr = mock 'file', :close + + File.expects(:open). + times(3). + returns(stdin). + then.returns(stdout). + then.returns(stderr) + + Puppet::Util.execute('test command', :squelch => true) + end + + it "should read and return the output if squelch is false" do + stdout = Tempfile.new('test') + Tempfile.stubs(:new).returns(stdout) + stdout.write("My expected command output") + + Puppet::Util.execute('test command').should == "My expected command output" + end + + it "should not read the output if squelch is true" do + stdout = Tempfile.new('test') + Tempfile.stubs(:new).returns(stdout) + stdout.write("My expected command output") + + Puppet::Util.execute('test command', :squelch => true).should == nil + end + + it "should delete the file used for output if squelch is false" do + stdout = Tempfile.new('test') + Tempfile.stubs(:new).returns(stdout) + + Puppet::Util.execute('test command') + + # Tempfile#path returns nil if the file has been unlinked + stdout.path.should == nil + end + + it "should raise an error if failonfail is true and the child failed" do + child_status = stub('child_status', :exitstatus => 1) + + Process.expects(:waitpid2).with(pid).returns([pid, child_status]) + + expect { + Puppet::Util.execute('fail command', :failonfail => true) + }.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/) + end + + it "should not raise an error if failonfail is false and the child failed" do + Process.expects(:waitpid2).with(pid).returns([pid, 1]) + + expect { + Puppet::Util.execute('fail command', :failonfail => false) + }.not_to raise_error + end + + it "should not raise an error if failonfail is true and the child succeeded" do + Process.expects(:waitpid2).with(pid).returns([pid, 0]) + + expect { + Puppet::Util.execute('fail command', :failonfail => true) + }.not_to raise_error + end + end + end +end diff --git a/test/util/utiltest.rb b/test/util/utiltest.rb index 1c934d612..9bc243c1c 100755 --- a/test/util/utiltest.rb +++ b/test/util/utiltest.rb @@ -1,236 +1,223 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest') require 'puppettest' require 'mocha' class TestPuppetUtil < Test::Unit::TestCase include PuppetTest 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 #{var} did not take") 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 #{var} did not take") 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 end