diff --git a/lib/puppet/provider/exec/posix.rb b/lib/puppet/provider/exec/posix.rb index 12748dd0c..82d6068ea 100644 --- a/lib/puppet/provider/exec/posix.rb +++ b/lib/puppet/provider/exec/posix.rb @@ -1,39 +1,39 @@ 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 # 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 + Puppet::Util.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 end diff --git a/lib/puppet/provider/exec/windows.rb b/lib/puppet/provider/exec/windows.rb index 76ca1b360..974ac5b94 100644 --- a/lib/puppet/provider/exec/windows.rb +++ b/lib/puppet/provider/exec/windows.rb @@ -1,34 +1,33 @@ require 'puppet/provider/exec' Puppet::Type.type(:exec).provide :windows, :parent => Puppet::Provider::Exec do - include Puppet::Util::Execution confine :operatingsystem => :windows defaultfor :operatingsystem => :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 if resource[:path] - withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do + Puppet::Util.withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do return if which(exe) end end raise ArgumentError, "Could not find command '#{exe}'" end end diff --git a/lib/puppet/provider/package/blastwave.rb b/lib/puppet/provider/package/blastwave.rb index 5ec854208..17a10a02c 100755 --- a/lib/puppet/provider/package/blastwave.rb +++ b/lib/puppet/provider/package/blastwave.rb @@ -1,111 +1,111 @@ # Packaging using Blastwave's pkg-get program. Puppet::Type.type(:package).provide :blastwave, :parent => :sun, :source => :sun do desc "Package management using Blastwave.org's `pkg-get` command on Solaris." pkgget = "pkg-get" pkgget = "/opt/csw/bin/pkg-get" if FileTest.executable?("/opt/csw/bin/pkg-get") confine :operatingsystem => :solaris commands :pkgget => pkgget def pkgget_with_cat(*args) - Puppet::Util::Execution::withenv(:PAGER => "/usr/bin/cat") { pkgget(*args) } + Puppet::Util.withenv(:PAGER => "/usr/bin/cat") { pkgget(*args) } end def self.extended(mod) unless command(:pkgget) != "pkg-get" raise Puppet::Error, "The pkg-get command is missing; blastwave packaging unavailable" end unless FileTest.exists?("/var/pkg-get/admin") Puppet.notice "It is highly recommended you create '/var/pkg-get/admin'." Puppet.notice "See /var/pkg-get/admin-fullauto" end end def self.instances(hash = {}) blastlist(hash).collect do |bhash| bhash.delete(:avail) new(bhash) end end # Turn our blastwave listing into a bunch of hashes. def self.blastlist(hash) command = ["-c"] command << hash[:justme] if hash[:justme] - output = Puppet::Util::Execution::withenv(:PAGER => "/usr/bin/cat") { pkgget command } + output = Puppet::Util.withenv(:PAGER => "/usr/bin/cat") { pkgget command } list = output.split("\n").collect do |line| next if line =~ /^#/ next if line =~ /^WARNING/ next if line =~ /localrev\s+remoterev/ blastsplit(line) end.reject { |h| h.nil? } if hash[:justme] return list[0] else list.reject! { |h| h[:ensure] == :absent } return list end end # Split the different lines into hashes. def self.blastsplit(line) if line =~ /\s*(\S+)\s+((\[Not installed\])|(\S+))\s+(\S+)/ hash = {} hash[:name] = $1 hash[:ensure] = if $2 == "[Not installed]" :absent else $2 end hash[:avail] = $5 hash[:avail] = hash[:ensure] if hash[:avail] == "SAME" # Use the name method, so it works with subclasses. hash[:provider] = self.name return hash else Puppet.warning "Cannot match #{line}" return nil end end def install pkgget_with_cat "-f", :install, @resource[:name] end # Retrieve the version from the current package file. def latest hash = self.class.blastlist(:justme => @resource[:name]) hash[:avail] end def query if hash = self.class.blastlist(:justme => @resource[:name]) hash else {:ensure => :absent} end end # Remove the old package, and install the new one def update pkgget_with_cat "-f", :upgrade, @resource[:name] end def uninstall pkgget_with_cat "-f", :remove, @resource[:name] end end diff --git a/lib/puppet/provider/package/freebsd.rb b/lib/puppet/provider/package/freebsd.rb index eaafdbfc7..5008c9143 100755 --- a/lib/puppet/provider/package/freebsd.rb +++ b/lib/puppet/provider/package/freebsd.rb @@ -1,49 +1,49 @@ Puppet::Type.type(:package).provide :freebsd, :parent => :openbsd do desc "The specific form of package management on FreeBSD. This is an extremely quirky packaging system, in that it freely mixes between ports and packages. Apparently all of the tools are written in Ruby, so there are plans to rewrite this support to directly use those libraries." commands :pkginfo => "/usr/sbin/pkg_info", :pkgadd => "/usr/sbin/pkg_add", :pkgdelete => "/usr/sbin/pkg_delete" confine :operatingsystem => :freebsd def self.listcmd command(:pkginfo) end def install should = @resource.should(:ensure) if @resource[:source] =~ /\/$/ if @resource[:source] =~ /^(ftp|https?):/ - Puppet::Util::Execution::withenv :PACKAGESITE => @resource[:source] do + Puppet::Util.withenv :PACKAGESITE => @resource[:source] do pkgadd "-r", @resource[:name] end else - Puppet::Util::Execution::withenv :PKG_PATH => @resource[:source] do + Puppet::Util.withenv :PKG_PATH => @resource[:source] do pkgadd @resource[:name] end end else Puppet.warning "source is defined but does not have trailing slash, ignoring #{@resource[:source]}" if @resource[:source] pkgadd "-r", @resource[:name] end end def query self.class.instances.each do |provider| if provider.name == @resource.name return provider.properties end end nil end def uninstall pkgdelete "#{@resource[:name]}-#{@resource.should(:ensure)}" end end diff --git a/lib/puppet/provider/package/openbsd.rb b/lib/puppet/provider/package/openbsd.rb index d97d571d8..65b1167e2 100755 --- a/lib/puppet/provider/package/openbsd.rb +++ b/lib/puppet/provider/package/openbsd.rb @@ -1,115 +1,115 @@ require 'puppet/provider/package' # Packaging on OpenBSD. Doesn't work anywhere else that I know of. Puppet::Type.type(:package).provide :openbsd, :parent => Puppet::Provider::Package do desc "OpenBSD's form of `pkg_add` support." commands :pkginfo => "pkg_info", :pkgadd => "pkg_add", :pkgdelete => "pkg_delete" defaultfor :operatingsystem => :openbsd confine :operatingsystem => :openbsd has_feature :versionable def self.instances packages = [] begin execpipe(listcmd) do |process| # our regex for matching pkg_info output regex = /^(.*)-(\d[^-]*)[-]?(\D*)(.*)$/ fields = [:name, :ensure, :flavor ] hash = {} # now turn each returned line into a package object process.each { |line| if match = regex.match(line.split[0]) fields.zip(match.captures) { |field,value| hash[field] = value } yup = nil name = hash[:name] hash[:provider] = self.name packages << new(hash) hash = {} else # Print a warning on lines we can't match, but move # on, since it should be non-fatal warning("Failed to match line #{line}") end } end return packages rescue Puppet::ExecutionFailure return nil end end def self.listcmd [command(:pkginfo), " -a"] end def install should = @resource.should(:ensure) unless @resource[:source] raise Puppet::Error, "You must specify a package source for BSD packages" end if @resource[:source][-1,1] == ::File::PATH_SEPARATOR e_vars = { :PKG_PATH => @resource[:source] } full_name = [ @resource[:name], get_version || @resource[:ensure], @resource[:flavor] ].join('-').chomp('-') else e_vars = {} full_name = @resource[:source] end - Puppet::Util::Execution::withenv(e_vars) { pkgadd full_name } + Puppet::Util.withenv(e_vars) { pkgadd full_name } end def get_version execpipe([command(:pkginfo), " -I ", @resource[:name]]) do |process| # our regex for matching pkg_info output regex = /^(.*)-(\d[^-]*)[-]?(\D*)(.*)$/ fields = [ :name, :version, :flavor ] master_version = 0 process.each do |line| if match = regex.match(line.split[0]) # now we return the first version, unless ensure is latest version = match.captures[1] return version unless @resource[:ensure] == "latest" master_version = version unless master_version > version end end return master_version unless master_version == 0 raise Puppet::Error, "#{version} is not available for this package" end rescue Puppet::ExecutionFailure return nil end def query hash = {} info = pkginfo @resource[:name] # Search for the version info if info =~ /Information for (inst:)?#{@resource[:name]}-(\S+)/ hash[:ensure] = $2 else return nil end hash end def uninstall pkgdelete @resource[:name] end end diff --git a/lib/puppet/provider/package/portage.rb b/lib/puppet/provider/package/portage.rb index 30f0e4a25..652d515fd 100644 --- a/lib/puppet/provider/package/portage.rb +++ b/lib/puppet/provider/package/portage.rb @@ -1,122 +1,122 @@ require 'puppet/provider/package' require 'fileutils' Puppet::Type.type(:package).provide :portage, :parent => Puppet::Provider::Package do desc "Provides packaging support for Gentoo's portage system." has_feature :versionable commands :emerge => "/usr/bin/emerge", :eix => "/usr/bin/eix", :update_eix => "/usr/bin/eix-update" confine :operatingsystem => :gentoo defaultfor :operatingsystem => :gentoo def self.instances result_format = /^(\S+)\s+(\S+)\s+\[(\S+)\]\s+\[(\S+)\]\s+(\S+)\s+(.*)$/ result_fields = [:category, :name, :ensure, :version_available, :vendor, :description] version_format = "{last}{}" search_format = " [] [] \n" begin update_eix if !FileUtils.uptodate?("/var/cache/eix", %w{/usr/bin/eix /usr/portage/metadata/timestamp}) search_output = nil - Puppet::Util::Execution.withenv :LASTVERSION => version_format do + Puppet::Util.withenv :LASTVERSION => version_format do search_output = eix "--nocolor", "--pure-packages", "--stable", "--installed", "--format", search_format end packages = [] search_output.each do |search_result| match = result_format.match(search_result) if match package = {} result_fields.zip(match.captures) do |field, value| package[field] = value unless !value or value.empty? end package[:provider] = :portage packages << new(package) end end return packages rescue Puppet::ExecutionFailure => detail raise Puppet::Error.new(detail) end end def install should = @resource.should(:ensure) name = package_name unless should == :present or should == :latest # We must install a specific version name = "=#{name}-#{should}" end emerge name end # The common package name format. def package_name @resource[:category] ? "#{@resource[:category]}/#{@resource[:name]}" : @resource[:name] end def uninstall emerge "--unmerge", package_name end def update self.install end def query result_format = /^(\S+)\s+(\S+)\s+\[(\S*)\]\s+\[(\S+)\]\s+(\S+)\s+(.*)$/ result_fields = [:category, :name, :ensure, :version_available, :vendor, :description] version_format = "{last}{}" search_format = " [] [] \n" search_field = package_name.count('/') > 0 ? "--category-name" : "--name" search_value = package_name begin update_eix if !FileUtils.uptodate?("/var/cache/eix", %w{/usr/bin/eix /usr/portage/metadata/timestamp}) search_output = nil - Puppet::Util::Execution.withenv :LASTVERSION => version_format do + Puppet::Util.withenv :LASTVERSION => version_format do search_output = eix "--nocolor", "--pure-packages", "--stable", "--format", search_format, "--exact", search_field, search_value end packages = [] search_output.each do |search_result| match = result_format.match(search_result) if match package = {} result_fields.zip(match.captures) do |field, value| package[field] = value unless !value or value.empty? end package[:ensure] = package[:ensure] ? package[:ensure] : :absent packages << package end end case packages.size when 0 not_found_value = "#{@resource[:category] ? @resource[:category] : ""}/#{@resource[:name]}" raise Puppet::Error.new("No package found with the specified name [#{not_found_value}]") when 1 return packages[0] else raise Puppet::Error.new("More than one package with the specified name [#{search_value}], please use the category parameter to disambiguate") end rescue Puppet::ExecutionFailure => detail raise Puppet::Error.new(detail) end end def latest self.query[:version_available] end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index a14a3fafc..e13c0c01d 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,556 +1,397 @@ # A module to collect utility functions. require 'English' require 'puppet/util/monkey_patches' require 'sync' require 'tempfile' require 'puppet/external/lock' +require 'puppet/error' require 'monitor' require 'puppet/util/execution_stub' require 'uri' 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 + + # Run some code with a specific environment. Resets the environment back to + # what it was at the end of the code. + def self.withenv(hash) + saved = ENV.to_hash + hash.each do |name, val| + ENV[name.to_s] = val + end + + yield + ensure + ENV.clear + saved.each do |name, val| + ENV[name] = val + 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 self.synchronize_on(x,type) sync_object,users = 0,1 begin @@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.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 - # 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 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.expand_path(File.join(dir, bin)) if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end 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!^/!, } require 'puppet' platform ||= Puppet.features.microsoft_windows? ? :windows : :posix !! (path =~ regexes[platform]) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}" end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_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 - - command_str = command.respond_to?(:join) ? command.join('') : command - output = open("| #{command_str} 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 - # We can't just call Array(command), and rely on it returning - # things like ['foo'], when passed ['foo'], because - # Array(command) will call command.to_a internally, which when - # given a string can end up doing Very Bad Things(TM), such as - # turning "/tmp/foo;\r\n /bin/echo" into ["/tmp/foo;\r\n", " /bin/echo"] - command = [command].flatten - Process.setsid - begin - $stdin.reopen(stdin) - $stdout.reopen(stdout) - $stderr.reopen(stderr) - - # we are in a forked process, so we currently have access to all of the file descriptors - # from the parent process... which, in this case, is bad because we don't want - # to allow the user's command to have access to them. Therefore, we'll close them off. - # (assumes that there are only 256 file descriptors used) - 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] - - # if the caller has requested that we override locale environment variables, - if (arguments[:override_locale]) then - # loop over them and clear them - Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) } - # set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output - # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in - # a forked process. - ENV['LANG'] = 'C' - ENV['LC_ALL'] = 'C' - end - - # unset all of the user-related environment variables so that different methods of starting puppet - # (automatic start during boot, via 'service', via /etc/init.d, etc.) won't have unexpected side - # effects relating to user / home dir environment vars. - # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in - # a forked process. - Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) } - - arguments[:custom_environment] ||= {} - Puppet::Util::Execution.withenv(arguments[:custom_environment]) do - Kernel.exec(*command) - end - rescue => detail - puts detail.message - puts detail.backtrace if Puppet[:trace] - Puppet.err "Could not execute posix command: #{detail}" - 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) - - arguments[:custom_environment] ||= {} - process_info = - Puppet::Util::Execution.withenv(arguments[:custom_environment]) do - Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr} ) - end - process_info.process_id - end - module_function :execute_windows - - # Execute the desired command, and return the status and output. - # def execute(command, arguments) - # [arguments] a Hash optionally containing any of the following keys: - # :failonfail (default true) -- if this value is set to true, then this method will raise an error if the - # command is not executed successfully. - # :uid (default nil) -- the user id of the user that the process should be run as - # :gid (default nil) -- the group id of the group that the process should be run as - # :combine (default true) -- sets whether or not to combine stdout/stderr in the output - # :stdinfile (default nil) -- sets a file that can be used for stdin. Passing a string for stdin is not currently - # supported. - # :squelch (default false) -- if true, ignore stdout / stderr completely - # :override_locale (default true) -- by default (and if this option is set to true), we will temporarily override - # the user/system locale to "C" (via environment variables LANG and LC_*) while we are executing the command. - # This ensures that the output of the command will be formatted consistently, making it predictable for parsing. - # Passing in a value of false for this option will allow the command to be executed using the user/system locale. - # :custom_environment (default {}) -- a hash of key/value pairs to set as environment variables for the duration - # of the command - def execute(command, arguments = {}) - - # specifying these here rather than in the method signature to allow callers to pass in a partial - # set of overrides without affecting the default values for options that they don't pass in - default_arguments = { - :failonfail => true, - :uid => nil, - :gid => nil, - :combine => true, - :stdinfile => nil, - :squelch => false, - :override_locale => true, - :custom_environment => {}, - } - - arguments = default_arguments.merge(arguments) - - if command.is_a?(Array) - command = command.flatten.map(&:to_s) - str = command.join(" ") - elsif command.is_a?(String) - str = command - end - - if respond_to? :debug - debug "Executing '#{str}'" - else - Puppet.debug "Executing '#{str}'" - end - - null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' - - 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') - - 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) - exit_status = Process.waitpid2(child_pid).last.exitstatus - elsif Puppet.features.microsoft_windows? - child_pid = execute_windows(*exec_args) - exit_status = Process.waitpid2(child_pid).last - # $CHILD_STATUS is not set when calling win32/process Process.create - # and since it's read-only, we can't set it. But we can execute a - # a shell that simply returns the desired exit status, which has the - # desired effect. - %x{#{ENV['COMSPEC']} /c exit #{exit_status}} - 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] and exit_status != 0 - raise ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{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. - # (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry - # about a race condition because all of the places that we call this from are preceded by a call to "waitpid2", - # meaning that the processes responsible for writing the file have completed before we get here.) - 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 + + # Because IO#binread is only available in 1.9 + def binread(file) + File.open(file, 'rb') { |f| f.read } + end + module_function :binread + + + ####################################################################################################### + # Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution + ####################################################################################################### + + + def execpipe(command, failonfail = true, &block) + Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe") + Puppet::Util::Execution.execpipe(command, failonfail, &block) + end + module_function :execpipe + + def execfail(command, exception) + #Puppet::Util::Warnings.warnonce("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail") + Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail") + Puppet::Util::Execution.execfail(command, exception) + end + module_function :execfail + + def execute(command, arguments = {}) + #Puppet::Util::Warnings.warnonce("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute") + Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute") + Puppet::Util::Execution.execute(command, arguments) + end + module_function :execute + + end end + require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/execution.rb b/lib/puppet/util/execution.rb index 69f4f2c15..2833ece25 100644 --- a/lib/puppet/util/execution.rb +++ b/lib/puppet/util/execution.rb @@ -1,20 +1,237 @@ -module Puppet::Util::Execution - module_function +module Puppet - # Run some code with a specific environment. Resets the environment back to - # what it was at the end of the code. - def withenv(hash) - saved = ENV.to_hash - hash.each do |name, val| - ENV[name.to_s] = val + # A command failed to execute. + require 'puppet/error' + class ExecutionFailure < Puppet::Error + end + +module Util::Execution + + # Execute the provided command in a pipe, yielding the pipe object. + def self.execpipe(command, failonfail = true) + if respond_to? :debug + debug "Executing '#{command}'" + else + Puppet.debug "Executing '#{command}'" + end + + command_str = command.respond_to?(:join) ? command.join('') : command + output = open("| #{command_str} 2>&1") do |pipe| + yield pipe end - yield - ensure - ENV.clear - saved.each do |name, val| - ENV[name] = val + if failonfail + unless $CHILD_STATUS == 0 + raise ExecutionFailure, output + end end + + output end -end + def self.execfail(command, exception) + output = execute(command) + return output + rescue ExecutionFailure + raise exception, output + end + + + + # Execute the desired command, and return the status and output. + # def execute(command, arguments) + # [arguments] a Hash optionally containing any of the following keys: + # :failonfail (default true) -- if this value is set to true, then this method will raise an error if the + # command is not executed successfully. + # :uid (default nil) -- the user id of the user that the process should be run as + # :gid (default nil) -- the group id of the group that the process should be run as + # :combine (default true) -- sets whether or not to combine stdout/stderr in the output + # :stdinfile (default nil) -- sets a file that can be used for stdin. Passing a string for stdin is not currently + # supported. + # :squelch (default false) -- if true, ignore stdout / stderr completely + # :override_locale (default true) -- by default (and if this option is set to true), we will temporarily override + # the user/system locale to "C" (via environment variables LANG and LC_*) while we are executing the command. + # This ensures that the output of the command will be formatted consistently, making it predictable for parsing. + # Passing in a value of false for this option will allow the command to be executed using the user/system locale. + # :custom_environment (default {}) -- a hash of key/value pairs to set as environment variables for the duration + # of the command + def self.execute(command, arguments = {}) + + # specifying these here rather than in the method signature to allow callers to pass in a partial + # set of overrides without affecting the default values for options that they don't pass in + default_arguments = { + :failonfail => true, + :uid => nil, + :gid => nil, + :combine => true, + :stdinfile => nil, + :squelch => false, + :override_locale => true, + :custom_environment => {}, + } + + arguments = default_arguments.merge(arguments) + + if command.is_a?(Array) + command = command.flatten.map(&:to_s) + str = command.join(" ") + elsif command.is_a?(String) + str = command + end + + if respond_to? :debug + debug "Executing '#{str}'" + else + Puppet.debug "Executing '#{str}'" + end + + null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' + + 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') + + 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) + exit_status = Process.waitpid2(child_pid).last.exitstatus + elsif Puppet.features.microsoft_windows? + child_pid = execute_windows(*exec_args) + exit_status = Process.waitpid2(child_pid).last + # $CHILD_STATUS is not set when calling win32/process Process.create + # and since it's read-only, we can't set it. But we can execute a + # a shell that simply returns the desired exit status, which has the + # desired effect. + %x{#{ENV['COMSPEC']} /c exit #{exit_status}} + 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] and exit_status != 0 + raise ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output}" + end + + output + end + + + + # Because some modules provide their own version of this method. + class << self + alias util_execute execute + end + + + # this is private method, see call to private_class_method after method definition + def self.execute_posix(command, arguments, stdin, stdout, stderr) + child_pid = Kernel.fork do + # We can't just call Array(command), and rely on it returning + # things like ['foo'], when passed ['foo'], because + # Array(command) will call command.to_a internally, which when + # given a string can end up doing Very Bad Things(TM), such as + # turning "/tmp/foo;\r\n /bin/echo" into ["/tmp/foo;\r\n", " /bin/echo"] + command = [command].flatten + Process.setsid + begin + $stdin.reopen(stdin) + $stdout.reopen(stdout) + $stderr.reopen(stderr) + + # we are in a forked process, so we currently have access to all of the file descriptors + # from the parent process... which, in this case, is bad because we don't want + # to allow the user's command to have access to them. Therefore, we'll close them off. + # (assumes that there are only 256 file descriptors used) + 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] + + # if the caller has requested that we override locale environment variables, + if (arguments[:override_locale]) then + # loop over them and clear them + Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) } + # set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output + # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in + # a forked process. + ENV['LANG'] = 'C' + ENV['LC_ALL'] = 'C' + end + + # unset all of the user-related environment variables so that different methods of starting puppet + # (automatic start during boot, via 'service', via /etc/init.d, etc.) won't have unexpected side + # effects relating to user / home dir environment vars. + # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in + # a forked process. + Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) } + + arguments[:custom_environment] ||= {} + Puppet::Util.withenv(arguments[:custom_environment]) do + Kernel.exec(*command) + end + rescue => detail + puts detail.message + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not execute posix command: #{detail}" + exit!(1) + end + end + child_pid + end + private_class_method :execute_posix + + + # this is private method, see call to private_class_method after method definition + def self.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) + + arguments[:custom_environment] ||= {} + process_info = + Puppet::Util.withenv(arguments[:custom_environment]) do + Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr} ) + end + process_info.process_id + end + private_class_method :execute_windows + + + # this is private method, see call to private_class_method after method definition + def self.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. + # (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry + # about a race condition because all of the places that we call this from are preceded by a call to "waitpid2", + # meaning that the processes responsible for writing the file have completed before we get here.) + 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 + private_class_method :wait_for_output + + + + +end +end diff --git a/spec/integration/defaults_spec.rb b/spec/integration/defaults_spec.rb index 59bf22d8c..f2826f520 100755 --- a/spec/integration/defaults_spec.rb +++ b/spec/integration/defaults_spec.rb @@ -1,316 +1,315 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/defaults' require 'puppet/rails' describe "Puppet defaults" do - include Puppet::Util::Execution after { Puppet.settings.clear } describe "when setting the :factpath" do it "should add the :factpath to Facter's search paths" do Facter.expects(:search).with("/my/fact/path") Puppet.settings[:factpath] = "/my/fact/path" end end describe "when setting the :certname" do it "should fail if the certname is not downcased" do lambda { Puppet.settings[:certname] = "Host.Domain.Com" }.should raise_error(ArgumentError) end end describe "when setting :node_name_value" do it "should default to the value of :certname" do Puppet.settings[:certname] = 'blargle' Puppet.settings[:node_name_value].should == 'blargle' end end describe "when setting the :node_name_fact" do it "should fail when also setting :node_name_value" do lambda do Puppet.settings[:node_name_value] = "some value" Puppet.settings[:node_name_fact] = "some_fact" end.should raise_error("Cannot specify both the node_name_value and node_name_fact settings") end it "should not fail when using the default for :node_name_value" do lambda do Puppet.settings[:node_name_fact] = "some_fact" end.should_not raise_error end end describe "when :certdnsnames is set" do it "should not fail" do expect { Puppet[:certdnsnames] = 'fred:wilma' }.should_not raise_error end it "should warn the value is ignored" do Puppet.expects(:warning).with {|msg| msg =~ /CVE-2011-3872/ } Puppet[:certdnsnames] = 'fred:wilma' end end describe "when configuring the :crl" do it "should warn if :cacrl is set to false" do Puppet.expects(:warning) Puppet.settings[:cacrl] = 'false' end end describe "when setting the :catalog_format" do it "should log a deprecation notice" do Puppet.expects(:warning) Puppet.settings[:catalog_format] = 'marshal' end it "should copy the value to :preferred_serialization_format" do Puppet.settings[:catalog_format] = 'marshal' Puppet.settings[:preferred_serialization_format].should == 'marshal' end end it "should have a clientyamldir setting" do Puppet.settings[:clientyamldir].should_not be_nil end it "should have different values for the yamldir and clientyamldir" do Puppet.settings[:yamldir].should_not == Puppet.settings[:clientyamldir] end it "should have a client_datadir setting" do Puppet.settings[:client_datadir].should_not be_nil end it "should have different values for the server_datadir and client_datadir" do Puppet.settings[:server_datadir].should_not == Puppet.settings[:client_datadir] end # See #1232 it "should not specify a user or group for the clientyamldir" do Puppet.settings.setting(:clientyamldir).owner.should be_nil Puppet.settings.setting(:clientyamldir).group.should be_nil end it "should use the service user and group for the yamldir" do Puppet.settings.stubs(:service_user_available?).returns true Puppet.settings.setting(:yamldir).owner.should == Puppet.settings[:user] Puppet.settings.setting(:yamldir).group.should == Puppet.settings[:group] end # See #1232 it "should not specify a user or group for the rundir" do Puppet.settings.setting(:rundir).owner.should be_nil Puppet.settings.setting(:rundir).group.should be_nil end it "should specify that the host private key should be owned by the service user" do Puppet.settings.stubs(:service_user_available?).returns true Puppet.settings.setting(:hostprivkey).owner.should == Puppet.settings[:user] end it "should specify that the host certificate should be owned by the service user" do Puppet.settings.stubs(:service_user_available?).returns true Puppet.settings.setting(:hostcert).owner.should == Puppet.settings[:user] end it "should use a bind address of ''" do Puppet.settings.clear Puppet.settings[:bindaddress].should == "" end [:modulepath, :factpath].each do |setting| it "should configure '#{setting}' not to be a file setting, so multi-directory settings are acceptable" do Puppet.settings.setting(setting).should be_instance_of(Puppet::Util::Settings::Setting) end end it "should add /usr/sbin and /sbin to the path if they're not there" do - withenv("PATH" => "/usr/bin:/usr/local/bin") do + Puppet::Util.withenv("PATH" => "/usr/bin:/usr/local/bin") do Puppet.settings[:path] = "none" # this causes it to ignore the setting ENV["PATH"].split(File::PATH_SEPARATOR).should be_include("/usr/sbin") ENV["PATH"].split(File::PATH_SEPARATOR).should be_include("/sbin") end end it "should default to pson for the preferred serialization format" do Puppet.settings.value(:preferred_serialization_format).should == "pson" end describe "when enabling storeconfigs" do before do Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:cache_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet.features.stubs(:rails?).returns true end it "should set the Catalog cache class to :store_configs" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:store_configs) Puppet.settings[:storeconfigs] = true end it "should not set the Catalog cache class to :store_configs if asynchronous storeconfigs is enabled" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:store_configs).never Puppet.settings.expects(:value).with(:async_storeconfigs).returns true Puppet.settings[:storeconfigs] = true end it "should set the Facts cache class to :store_configs" do Puppet::Node::Facts.indirection.expects(:cache_class=).with(:store_configs) Puppet.settings[:storeconfigs] = true end it "should set the Node cache class to :store_configs" do Puppet::Node.indirection.expects(:cache_class=).with(:store_configs) Puppet.settings[:storeconfigs] = true end end describe "when enabling asynchronous storeconfigs" do before do Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:cache_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet.features.stubs(:rails?).returns true end it "should set storeconfigs to true" do Puppet.settings[:async_storeconfigs] = true Puppet.settings[:storeconfigs].should be_true end it "should set the Catalog cache class to :queue" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:queue) Puppet.settings[:async_storeconfigs] = true end it "should set the Facts cache class to :store_configs" do Puppet::Node::Facts.indirection.expects(:cache_class=).with(:store_configs) Puppet.settings[:storeconfigs] = true end it "should set the Node cache class to :store_configs" do Puppet::Node.indirection.expects(:cache_class=).with(:store_configs) Puppet.settings[:storeconfigs] = true end end describe "when enabling thin storeconfigs" do before do Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:cache_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet.features.stubs(:rails?).returns true end it "should set storeconfigs to true" do Puppet.settings[:thin_storeconfigs] = true Puppet.settings[:storeconfigs].should be_true end end it "should have a setting for determining the configuration version and should default to an empty string" do Puppet.settings[:config_version].should == "" end describe "when enabling reports" do it "should use the default server value when report server is unspecified" do Puppet.settings[:server] = "server" Puppet.settings[:report_server].should == "server" end it "should use the default masterport value when report port is unspecified" do Puppet.settings[:masterport] = "1234" Puppet.settings[:report_port].should == "1234" end it "should set report_server when reportserver is set" do Puppet.settings[:reportserver] = "reportserver" Puppet.settings[:report_server].should == "reportserver" end it "should use report_port when set" do Puppet.settings[:masterport] = "1234" Puppet.settings[:report_port] = "5678" Puppet.settings[:report_port].should == "5678" end it "should prefer report_server over reportserver" do Puppet.settings[:reportserver] = "reportserver" Puppet.settings[:report_server] = "report_server" Puppet.settings[:report_server].should == "report_server" end end it "should have a :caname setting that defaults to the cert name" do Puppet.settings[:certname] = "foo" Puppet.settings[:ca_name].should == "Puppet CA: foo" end it "should have a 'prerun_command' that defaults to the empty string" do Puppet.settings[:prerun_command].should == "" end it "should have a 'postrun_command' that defaults to the empty string" do Puppet.settings[:postrun_command].should == "" end it "should have a 'certificate_revocation' setting that defaults to true" do Puppet.settings[:certificate_revocation].should be_true end it "should have an http_compression setting that defaults to false" do Puppet.settings[:http_compression].should be_false end describe "reportdir" do subject { Puppet.settings[:reportdir] } it { should == "#{Puppet[:vardir]}/reports" } end describe "reporturl" do subject { Puppet.settings[:reporturl] } it { should == "http://localhost:3000/reports/upload" } end describe "when configuring color" do it "should default to ansi", :unless => Puppet.features.microsoft_windows? do Puppet.settings[:color].should == 'ansi' end it "should default to false", :if => Puppet.features.microsoft_windows? do Puppet.settings[:color].should == 'false' end end describe "daemonize" do it "should default to true", :unless => Puppet.features.microsoft_windows? do Puppet.settings[:daemonize].should == true end describe "on Windows", :if => Puppet.features.microsoft_windows? do it "should default to false" do Puppet.settings[:daemonize].should == false end it "should raise an error if set to true" do lambda { Puppet.settings[:daemonize] = true }.should raise_error(/Cannot daemonize on Windows/) end end end describe "diff" do it "should default to 'diff' on POSIX", :unless => Puppet.features.microsoft_windows? do Puppet.settings[:diff].should == 'diff' end it "should default to '' on Windows", :if => Puppet.features.microsoft_windows? do Puppet.settings[:diff].should == '' end end end diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index d5d3068a1..05115c849 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -1,339 +1,339 @@ #!/usr/bin/env rspec require 'spec_helper' require 'tmpdir' require 'puppet/node/environment' require 'puppet/util/execution' describe Puppet::Node::Environment do let(:env) { Puppet::Node::Environment.new("testing") } include PuppetSpec::Files after do Puppet::Node::Environment.clear end it "should use the filetimeout for the ttl for the modulepath" do Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout]) end it "should use the filetimeout for the ttl for the module list" do Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout]) end it "should use the default environment if no name is provided while initializing an environment" do Puppet.settings.expects(:value).with(:environment).returns("one") Puppet::Node::Environment.new.name.should == :one end it "should treat environment instances as singletons" do Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one")) end it "should treat an environment specified as names or strings as equivalent" do Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one")) end it "should return its name when converted to a string" do Puppet::Node::Environment.new(:one).to_s.should == "one" end it "should just return any provided environment if an environment is provided as the name" do one = Puppet::Node::Environment.new(:one) Puppet::Node::Environment.new(one).should equal(one) end describe "when managing known resource types" do before do @collection = Puppet::Resource::TypeCollection.new(env) env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) Thread.current[:known_resource_types] = nil end it "should create a resource type collection if none exists" do Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection env.known_resource_types.should equal(@collection) end it "should reuse any existing resource type collection" do env.known_resource_types.should equal(env.known_resource_types) end it "should perform the initial import when creating a new collection" do env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) env.known_resource_types end it "should return the same collection even if stale if it's the same thread" do Puppet::Resource::TypeCollection.stubs(:new).returns @collection env.known_resource_types.stubs(:stale?).returns true env.known_resource_types.should equal(@collection) end it "should return the current thread associated collection if there is one" do Thread.current[:known_resource_types] = @collection env.known_resource_types.should equal(@collection) end it "should give to all threads using the same environment the same collection if the collection isn't stale" do original_thread_type_collection = Puppet::Resource::TypeCollection.new(env) Puppet::Resource::TypeCollection.expects(:new).with(env).returns original_thread_type_collection env.known_resource_types.should equal(original_thread_type_collection) original_thread_type_collection.expects(:require_reparse?).returns(false) Puppet::Resource::TypeCollection.stubs(:new).with(env).returns @collection t = Thread.new { env.known_resource_types.should equal(original_thread_type_collection) } t.join end it "should generate a new TypeCollection if the current one requires reparsing" do old_type_collection = env.known_resource_types old_type_collection.stubs(:require_reparse?).returns true Thread.current[:known_resource_types] = nil new_type_collection = env.known_resource_types new_type_collection.should be_a Puppet::Resource::TypeCollection new_type_collection.should_not equal(old_type_collection) end end it "should validate the modulepath directories" do real_file = tmpdir('moduledir') path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR) Puppet[:modulepath] = path env.modulepath.should == [real_file] end it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do - Puppet::Util::Execution.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do + Puppet::Util.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do module_path = %w{/one /two}.join(File::PATH_SEPARATOR) env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two} env.expects(:[]).with(:modulepath).returns module_path env.modulepath.should == %w{/l1 /l2 /one /two} end end describe "when validating modulepath or manifestdir directories" do before :each do @path_one = make_absolute('/one') @path_two = make_absolute('/two') end it "should not return non-directories" do FileTest.expects(:directory?).with(@path_one).returns true FileTest.expects(:directory?).with(@path_two).returns false env.validate_dirs([@path_one, @path_two]).should == [@path_one] end it "should use the current working directory to fully-qualify unqualified paths" do FileTest.stubs(:directory?).returns true two = File.expand_path(File.join(Dir.getwd, "two")) env.validate_dirs([@path_one, 'two']).should == [@path_one, two] end end describe "when modeling a specific environment" do it "should have a method for returning the environment name" do Puppet::Node::Environment.new("testing").name.should == :testing end it "should provide an array-like accessor method for returning any environment-specific setting" do env.should respond_to(:[]) end it "should ask the Puppet settings instance for the setting qualified with the environment name" do Puppet.settings.expects(:value).with("myvar", :testing).returns("myval") env["myvar"].should == "myval" end it "should be able to return an individual module that exists in its module path" do mod = mock 'module' Puppet::Module.expects(:new).with("one", :environment => env).returns mod mod.expects(:exist?).returns true env.module("one").should equal(mod) end it "should return nil if asked for a module that does not exist in its path" do mod = mock 'module' Puppet::Module.expects(:new).with("one", :environment => env).returns mod mod.expects(:exist?).returns false env.module("one").should be_nil end describe ".modules_by_path" do before do dir = tmpdir("deep_path") @first = File.join(dir, "first") @second = File.join(dir, "second") Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" FileUtils.mkdir_p(@first) FileUtils.mkdir_p(@second) end it "should return an empty list if there are no modules" do env.modules_by_path.should == { @first => [], @second => [] } end it "should include modules even if they exist in multiple dirs in the modulepath" do modpath1 = File.join(@first, "foo") FileUtils.mkdir_p(modpath1) modpath2 = File.join(@second, "foo") FileUtils.mkdir_p(modpath2) env.modules_by_path.should == { @first => [Puppet::Module.new('foo', :environment => env, :path => modpath1)], @second => [Puppet::Module.new('foo', :environment => env, :path => modpath2)] } end end describe ".modules" do it "should return an empty list if there are no modules" do env.modulepath = %w{/a /b} Dir.expects(:entries).with("/a").returns [] Dir.expects(:entries).with("/b").returns [] env.modules.should == [] end it "should return a module named for every directory in each module path" do env.modulepath = %w{/a /b} Dir.expects(:entries).with("/a").returns %w{foo bar} Dir.expects(:entries).with("/b").returns %w{bee baz} env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort end it "should remove duplicates" do env.modulepath = %w{/a /b} Dir.expects(:entries).with("/a").returns %w{foo} Dir.expects(:entries).with("/b").returns %w{foo} env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should ignore invalid modules" do env.modulepath = %w{/a} Dir.expects(:entries).with("/a").returns %w{foo bar} Puppet::Module.expects(:new).with { |name, env| name == "foo" }.returns mock("foomod", :name => "foo") Puppet::Module.expects(:new).with { |name, env| name == "bar" }.raises( Puppet::Module::InvalidName, "name is invalid" ) env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should create modules with the correct environment" do env.modulepath = %w{/a} Dir.expects(:entries).with("/a").returns %w{foo} env.modules.each {|mod| mod.environment.should == env } end it "should cache the module list" do env.modulepath = %w{/a} Dir.expects(:entries).once.with("/a").returns %w{foo} env.modules env.modules end end end describe Puppet::Node::Environment::Helper do before do @helper = Object.new @helper.extend(Puppet::Node::Environment::Helper) end it "should be able to set and retrieve the environment as a symbol" do @helper.environment = :foo @helper.environment.name.should == :foo end it "should accept an environment directly" do @helper.environment = Puppet::Node::Environment.new(:foo) @helper.environment.name.should == :foo end it "should accept an environment as a string" do @helper.environment = 'foo' @helper.environment.name.should == :foo end end describe "when performing initial import" do before do @parser = Puppet::Parser::Parser.new("test") Puppet::Parser::Parser.stubs(:new).returns @parser end it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) env.instance_eval { perform_initial_import } end it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do filename = tmpfile('myfile') File.open(filename, 'w'){|f| } Puppet.settings[:manifest] = filename @parser.expects(:file=).with filename @parser.expects(:parse) env.instance_eval { perform_initial_import } end it "should pass the manifest file to the parser even if it does not exist on disk" do filename = tmpfile('myfile') Puppet.settings[:code] = "" Puppet.settings[:manifest] = filename @parser.expects(:file=).with(filename).once @parser.expects(:parse).once env.instance_eval { perform_initial_import } end it "should fail helpfully if there is an error importing" do File.stubs(:exist?).returns true env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) @parser.expects(:file=).once @parser.expects(:parse).raises ArgumentError lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) end it "should not do anything if the ignore_import settings is set" do Puppet.settings[:ignoreimport] = true @parser.expects(:string=).never @parser.expects(:file=).never @parser.expects(:parse).never env.instance_eval { perform_initial_import } end it "should mark the type collection as needing a reparse when there is an error parsing" do @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) env.known_resource_types.require_reparse?.should be_true end end end diff --git a/spec/unit/provider/exec/posix_spec.rb b/spec/unit/provider/exec/posix_spec.rb index ff872976e..b3d8f504d 100755 --- a/spec/unit/provider/exec/posix_spec.rb +++ b/spec/unit/provider/exec/posix_spec.rb @@ -1,204 +1,204 @@ #!/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'), :provider => :posix) } let(:provider) { described_class.new(resource) } 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) } 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) } 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) } provider.run(command) @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: Overriding environment setting 'WHATEVER' with '/foo'"] end describe "posix locale settings", :unless => Puppet.features.microsoft_windows? do # a sentinel value that we can use to emulate what locale environment variables might be set to on an international # system. lang_sentinel_value = "es_ES.UTF-8" # a temporary hash that contains sentinel values for each of the locale environment variables that we override in # "exec" locale_sentinel_env = {} Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value } command = "/bin/echo $%s" it "should not override user's locale during execution" do # we'll do this once without any sentinel values, to give us a little more test coverage orig_env = {} Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| orig_env[var] = ENV[var] if ENV[var] } orig_env.keys.each do |var| output, status = provider.run(command % var) output.strip.should == orig_env[var] end # now, once more... but with our sentinel values - Puppet::Util::Execution.withenv(locale_sentinel_env) do + Puppet::Util.withenv(locale_sentinel_env) do Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| output, status = provider.run(command % var) output.strip.should == locale_sentinel_env[var] end end end it "should respect locale overrides in user's 'environment' configuration" do provider.resource[:environment] = ['LANG=foo', 'LC_ALL=bar'] output, status = provider.run(command % 'LANG') output.strip.should == 'foo' output, status = provider.run(command % 'LC_ALL') output.strip.should == 'bar' end end describe "posix user-related environment vars", :unless => Puppet.features.microsoft_windows? do # a temporary hash that contains sentinel values for each of the user-related environment variables that we # are expected to unset during an "exec" user_sentinel_env = {} Puppet::Util::POSIX::USER_ENV_VARS.each { |var| user_sentinel_env[var] = "Abracadabra" } command = "/bin/echo $%s" it "should unset user-related environment vars during execution" do # first we set up a temporary execution environment with sentinel values for the user-related environment vars # that we care about. - Puppet::Util::Execution.withenv(user_sentinel_env) do + Puppet::Util.withenv(user_sentinel_env) do # with this environment, we loop over the vars in question Puppet::Util::POSIX::USER_ENV_VARS.each do |var| # ensure that our temporary environment is set up as we expect ENV[var].should == user_sentinel_env[var] # run an "exec" via the provider and ensure that it unsets the vars output, status = provider.run(command % var) output.strip.should == "" # ensure that after the exec, our temporary env is still intact ENV[var].should == user_sentinel_env[var] end end end it "should respect overrides to user-related environment vars in caller's 'environment' configuration" do sentinel_value = "Abracadabra" # set the "environment" property of the resource, populating it with a hash containing sentinel values for # each of the user-related posix environment variables provider.resource[:environment] = Puppet::Util::POSIX::USER_ENV_VARS.collect { |var| "#{var}=#{sentinel_value}"} # loop over the posix user-related environment variables Puppet::Util::POSIX::USER_ENV_VARS.each do |var| # run an 'exec' to get the value of each variable output, status = provider.run(command % var) # ensure that it matches our expected sentinel value output.strip.should == sentinel_value end end end end end diff --git a/spec/unit/provider/package/freebsd_spec.rb b/spec/unit/provider/package/freebsd_spec.rb index 9c8038791..e5550f43b 100755 --- a/spec/unit/provider/package/freebsd_spec.rb +++ b/spec/unit/provider/package/freebsd_spec.rb @@ -1,54 +1,54 @@ #!/usr/bin/env rspec require 'spec_helper' provider_class = Puppet::Type.type(:package).provider(:freebsd) describe provider_class do before :each do # Create a mock resource @resource = stub 'resource' # A catch all; no parameters set @resource.stubs(:[]).returns(nil) # But set name and source @resource.stubs(:[]).with(:name).returns "mypackage" @resource.stubs(:[]).with(:ensure).returns :installed @provider = provider_class.new @provider.resource = @resource end it "should have an install method" do @provider = provider_class.new @provider.should respond_to(:install) end describe "when installing" do before :each do @resource.stubs(:should).with(:ensure).returns(:installed) end it "should install a package from a path to a directory" do # For better or worse, trailing '/' is needed. --daniel 2011-01-26 path = '/path/to/directory/' @resource.stubs(:[]).with(:source).returns(path) - Puppet::Util::Execution.expects(:withenv).once.with({:PKG_PATH => path}).yields + Puppet::Util.expects(:withenv).once.with({:PKG_PATH => path}).yields @provider.expects(:pkgadd).once.with("mypackage") expect { @provider.install }.should_not raise_error end %w{http https ftp}.each do |protocol| it "should install a package via #{protocol}" do # For better or worse, trailing '/' is needed. --daniel 2011-01-26 path = "#{protocol}://localhost/" @resource.stubs(:[]).with(:source).returns(path) - Puppet::Util::Execution.expects(:withenv).once.with({:PACKAGESITE => path}).yields + Puppet::Util.expects(:withenv).once.with({:PACKAGESITE => path}).yields @provider.expects(:pkgadd).once.with('-r', "mypackage") expect { @provider.install }.should_not raise_error end end end end diff --git a/spec/unit/util/execution_spec.rb b/spec/unit/util/execution_spec.rb index 5b8b8a527..d2b021836 100755 --- a/spec/unit/util/execution_spec.rb +++ b/spec/unit/util/execution_spec.rb @@ -1,48 +1,461 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Util::Execution do include Puppet::Util::Execution - describe "#withenv" do - before :each do - @original_path = ENV["PATH"] - @new_env = {:PATH => "/some/bogus/path"} + + + # utility method to help deal with some windows vs. unix differences + def process_status(exitstatus) + return exitstatus if Puppet.features.microsoft_windows? + + stub('child_status', :exitstatus => exitstatus) + end + + # utility methods to help us test some private methods without being quite so verbose + def call_exec_posix(command, arguments, stdin, stdout, stderr) + Puppet::Util::Execution.send(:execute_posix, command, arguments, stdin, stdout, stderr) + end + def call_exec_windows(command, arguments, stdin, stdout, stderr) + Puppet::Util::Execution.send(:execute_windows, command, arguments, stdin, stdout, stderr) + end + + + describe "execution methods" do + let(:pid) { 5501 } + let(:null_file) { Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' } + + describe "#execute_posix (stubs)" 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') + + call_exec_posix('test command', {}, @stdin, @stdout, @stderr) + end + + it "should start a new session group" do + Process.expects(:setsid) + + call_exec_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) } + + call_exec_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) + + call_exec_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::Execution.stubs(:puts) + Puppet::Util::Execution.expects(:exit!).with(1) + + call_exec_posix('test command', {}, @stdin, @stdout, @stderr) + end + + it "should properly execute commands specified as arrays" do + Kernel.expects(:exec).with('test command', 'with', 'arguments') + + call_exec_posix(['test command', 'with', 'arguments'], {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) + end + + it "should properly execute string commands with embedded newlines" do + Kernel.expects(:exec).with("/bin/echo 'foo' ; \n /bin/echo 'bar' ;") + + call_exec_posix("/bin/echo 'foo' ; \n /bin/echo 'bar' ;", {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) + end + + it "should return the pid of the child process" do + call_exec_posix('test command', {}, @stdin, @stdout, @stderr).should == pid + end end - it "should change environment variables within the block then reset environment variables to their original values" do - withenv @new_env do - ENV["PATH"].should == "/some/bogus/path" + describe "#execute_windows (stubs)" 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, process_status(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) + + call_exec_windows('test command', {}, @stdin, @stdout, @stderr) + end + + it "should return the pid of the child process" do + call_exec_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) + + call_exec_windows(['test command', 'with', 'some', 'arguments "with spaces'], {}, @stdin, @stdout, @stderr) end - ENV["PATH"].should == @original_path end - it "should reset environment variables to their original values even if the block fails" do - begin - withenv @new_env do - ENV["PATH"].should == "/some/bogus/path" - raise "This is a failure" + describe "#execute (stubs)" do + before :each do + Process.stubs(:waitpid2).with(pid).returns([pid, process_status(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::Execution.execute("/usr/bin/run_my_execute_stub").should == "execution stub output" + end + + it "should not actually execute anything" do + Puppet::Util::Execution.expects(:execute_posix).never + Puppet::Util::Execution.expects(:execute_windows).never + + Puppet::Util::Execution.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::Execution.stubs(:wait_for_output) + end + + it "should set stdin to the stdinfile if specified" do + input = tmpfile('stdin') + FileUtils.touch(input) + + Puppet::Util::Execution.expects(executor).with do |_,_,stdin,_,_| + stdin.path == input + end.returns(pid) + + Puppet::Util::Execution.execute('test command', :stdinfile => input) + end + + it "should set stdin to the null file if not specified" do + Puppet::Util::Execution.expects(executor).with do |_,_,stdin,_,_| + stdin.path == null_file + end.returns(pid) + + Puppet::Util::Execution.execute('test command') + end + + describe "when squelch is set" do + it "should set stdout and stderr to the null file" do + Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr| + stdout.path == null_file and stderr.path == null_file + end.returns(pid) + + Puppet::Util::Execution.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::Execution.expects(executor).with do |_,_,_,stdout,_| + stdout.path == outfile.path + end.returns(pid) + + Puppet::Util::Execution.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::Execution.expects(executor).with do |_,_,_,stdout,stderr| + stdout.path == outfile.path and stderr.path == outfile.path + end.returns(pid) + + Puppet::Util::Execution.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::Execution.expects(executor).with do |_,_,_,stdout,stderr| + stdout.path == outfile.path and stderr.path == null_file + end.returns(pid) + + Puppet::Util::Execution.execute('test command', :squelch => false, :combine => false) + end end - rescue end - ENV["PATH"].should == @original_path end - it "should reset environment variables even when they are set twice" do - # Setting Path & Environment parameters in Exec type can cause weirdness - @new_env["PATH"] = "/someother/bogus/path" - withenv @new_env do - # When assigning duplicate keys, can't guarantee order of evaluation - ENV["PATH"].should =~ /\/some.*\/bogus\/path/ + describe "#execute (posix locale)", :unless => Puppet.features.microsoft_windows? do + # build up a printf-style string that contains a command to get the value of an environment variable + # from the operating system. We can substitute into this with the names of the desired environment variables later. + get_env_var_cmd = 'echo $%s' + + # a sentinel value that we can use to emulate what locale environment variables might be set to on an international + # system. + lang_sentinel_value = "es_ES.UTF-8" + # a temporary hash that contains sentinel values for each of the locale environment variables that we override in + # "execute" + locale_sentinel_env = {} + Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value } + + it "should override the locale environment variables when :override_locale is not set (defaults to true)" do + # temporarily override the locale environment vars with a sentinel value, so that we can confirm that + # execute is actually setting them. + Puppet::Util.withenv(locale_sentinel_env) do + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| + # we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL + expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : "" + Puppet::Util::execute(get_env_var_cmd % var).strip.should == expected_value + end + end + end + + it "should override the LANG environment variable when :override_locale is set to true" do + # temporarily override the locale environment vars with a sentinel value, so that we can confirm that + # execute is actually setting them. + Puppet::Util.withenv(locale_sentinel_env) do + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| + # we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL + expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : "" + Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => true}).strip.should == expected_value + end + end + end + + it "should *not* override the LANG environment variable when :override_locale is set to false" do + # temporarily override the locale environment vars with a sentinel value, so that we can confirm that + # execute is not setting them. + Puppet::Util.withenv(locale_sentinel_env) do + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| + Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => false}).strip.should == lang_sentinel_value + end + end + end + + it "should have restored the LANG and locale environment variables after execution" do + # we'll do this once without any sentinel values, to give us a little more test coverage + orig_env_vals = {} + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| + orig_env_vals[var] = ENV[var] + end + # now we can really execute any command--doesn't matter what it is... + Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true}) + # now we check and make sure the original environment was restored + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| + ENV[var].should == orig_env_vals[var] + end + + # now, once more... but with our sentinel values + Puppet::Util.withenv(locale_sentinel_env) do + # now we can really execute any command--doesn't matter what it is... + Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true}) + # now we check and make sure the original environment was restored + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| + ENV[var].should == locale_sentinel_env[var] + end + end + end - ENV["PATH"].should == @original_path end - it "should remove any new environment variables after the block ends" do - @new_env[:FOO] = "bar" - withenv @new_env do - ENV["FOO"].should == "bar" + describe "#execute (posix user env vars)", :unless => Puppet.features.microsoft_windows? do + # build up a printf-style string that contains a command to get the value of an environment variable + # from the operating system. We can substitute into this with the names of the desired environment variables later. + get_env_var_cmd = 'echo $%s' + + # a sentinel value that we can use to emulate what locale environment variables might be set to on an international + # system. + user_sentinel_value = "Abracadabra" + # a temporary hash that contains sentinel values for each of the locale environment variables that we override in + # "execute" + user_sentinel_env = {} + Puppet::Util::POSIX::USER_ENV_VARS.each { |var| user_sentinel_env[var] = user_sentinel_value } + + it "should unset user-related environment vars during execution" do + # first we set up a temporary execution environment with sentinel values for the user-related environment vars + # that we care about. + Puppet::Util.withenv(user_sentinel_env) do + # with this environment, we loop over the vars in question + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| + # ensure that our temporary environment is set up as we expect + ENV[var].should == user_sentinel_env[var] + + # run an "exec" via the provider and ensure that it unsets the vars + Puppet::Util::execute(get_env_var_cmd % var).strip.should == "" + + # ensure that after the exec, our temporary env is still intact + ENV[var].should == user_sentinel_env[var] + end + + end + end + + it "should have restored the user-related environment variables after execution" do + # we'll do this once without any sentinel values, to give us a little more test coverage + orig_env_vals = {} + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| + orig_env_vals[var] = ENV[var] + end + # now we can really execute any command--doesn't matter what it is... + Puppet::Util::execute(get_env_var_cmd % 'anything') + # now we check and make sure the original environment was restored + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| + ENV[var].should == orig_env_vals[var] + end + + # now, once more... but with our sentinel values + Puppet::Util.withenv(user_sentinel_env) do + # now we can really execute any command--doesn't matter what it is... + Puppet::Util::execute(get_env_var_cmd % 'anything') + # now we check and make sure the original environment was restored + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| + ENV[var].should == user_sentinel_env[var] + end + end + end - ENV["FOO"].should == nil 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, process_status(0)]) + + Puppet::Util::Execution.stubs(executor).returns(pid) + end + + it "should wait for the child process to exit" do + Puppet::Util::Execution.stubs(:wait_for_output) + + Process.expects(:waitpid2).with(pid).returns([pid, process_status(0)]) + + Puppet::Util::Execution.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::Execution.execute('test command', {:squelch => true, :combine => false}) + 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::Execution.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::Execution.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') + path = stdout.path + Tempfile.stubs(:new).returns(stdout) + + Puppet::Util::Execution.execute('test command') + + File.should_not be_exist(path) + end + + it "should raise an error if failonfail is true and the child failed" do + Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) + + expect { + Puppet::Util::Execution.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, process_status(1)]) + + expect { + Puppet::Util::Execution.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, process_status(0)]) + + expect { + Puppet::Util::Execution.execute('fail command', :failonfail => true) + }.not_to raise_error + end + + it "should respect default values for args that aren't overridden if a partial arg list is passed in" do + Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) + expect { + # here we are passing in a non-nil value for "arguments", but we aren't specifying a value for + # :failonfail. We expect it to be set to its normal default value (true). + Puppet::Util::Execution.execute('fail command', { :squelch => true }) + }.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/) + end + + end + + end + + end diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb index ec335a893..3369c49ec 100755 --- a/spec/unit/util_spec.rb +++ b/spec/unit/util_spec.rb @@ -1,693 +1,296 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Util do include PuppetSpec::Files - def process_status(exitstatus) - return exitstatus if Puppet.features.microsoft_windows? - stub('child_status', :exitstatus => exitstatus) + describe "#withenv" do + before :each do + @original_path = ENV["PATH"] + @new_env = {:PATH => "/some/bogus/path"} + end + + it "should change environment variables within the block then reset environment variables to their original values" do + Puppet::Util.withenv @new_env do + ENV["PATH"].should == "/some/bogus/path" + end + ENV["PATH"].should == @original_path + end + + it "should reset environment variables to their original values even if the block fails" do + begin + Puppet::Util.withenv @new_env do + ENV["PATH"].should == "/some/bogus/path" + raise "This is a failure" + end + rescue + end + ENV["PATH"].should == @original_path + end + + it "should reset environment variables even when they are set twice" do + # Setting Path & Environment parameters in Exec type can cause weirdness + @new_env["PATH"] = "/someother/bogus/path" + Puppet::Util.withenv @new_env do + # When assigning duplicate keys, can't guarantee order of evaluation + ENV["PATH"].should =~ /\/some.*\/bogus\/path/ + end + ENV["PATH"].should == @original_path + end + + it "should remove any new environment variables after the block ends" do + @new_env[:FOO] = "bar" + Puppet::Util.withenv @new_env do + ENV["FOO"].should == "bar" + end + ENV["FOO"].should == nil + end + end 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 /foo//bar/baz].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 foo//bar/baz].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 c:/foo//bar//baz].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 foo//bar/baz].each do |path| it "should return false for #{path}" do Puppet::Util.should_not be_absolute_path(path, :windows) end end end end describe "#path_to_uri" do %w[. .. foo foo/bar foo/../bar].each do |path| it "should reject relative path: #{path}" do lambda { Puppet::Util.path_to_uri(path) }.should raise_error(Puppet::Error) end end it "should perform URI escaping" do Puppet::Util.path_to_uri("/foo bar").path.should == "/foo%20bar" end describe "when using platform :posix" do before :each do Puppet.features.stubs(:posix).returns true Puppet.features.stubs(:microsoft_windows?).returns false end %w[/ /foo /foo/../bar].each do |path| it "should convert #{path} to URI" do Puppet::Util.path_to_uri(path).path.should == path end end end describe "when using platform :windows" do before :each do Puppet.features.stubs(:posix).returns false Puppet.features.stubs(:microsoft_windows?).returns true end it "should normalize backslashes" do Puppet::Util.path_to_uri('c:\\foo\\bar\\baz').path.should == '/' + 'c:/foo/bar/baz' end %w[C:/ C:/foo/bar].each do |path| it "should convert #{path} to absolute URI" do Puppet::Util.path_to_uri(path).path.should == '/' + path end end %w[share C$].each do |path| it "should convert UNC #{path} to absolute URI" do uri = Puppet::Util.path_to_uri("\\\\server\\#{path}") uri.host.should == 'server' uri.path.should == '/' + path end end end end describe ".uri_to_path" do require 'uri' it "should strip host component" do Puppet::Util.uri_to_path(URI.parse('http://foo/bar')).should == '/bar' end it "should accept puppet URLs" do Puppet::Util.uri_to_path(URI.parse('puppet:///modules/foo')).should == '/modules/foo' end it "should return unencoded path" do Puppet::Util.uri_to_path(URI.parse('http://foo/bar%20baz')).should == '/bar baz' end it "should be nil-safe" do Puppet::Util.uri_to_path(nil).should be_nil end describe "when using platform :posix",:if => Puppet.features.posix? do it "should accept root" do Puppet::Util.uri_to_path(URI.parse('file:/')).should == '/' end it "should accept single slash" do Puppet::Util.uri_to_path(URI.parse('file:/foo/bar')).should == '/foo/bar' end it "should accept triple slashes" do Puppet::Util.uri_to_path(URI.parse('file:///foo/bar')).should == '/foo/bar' end end describe "when using platform :windows", :if => Puppet.features.microsoft_windows? do it "should accept root" do Puppet::Util.uri_to_path(URI.parse('file:/C:/')).should == 'C:/' end it "should accept single slash" do Puppet::Util.uri_to_path(URI.parse('file:/C:/foo/bar')).should == 'C:/foo/bar' end it "should accept triple slashes" do Puppet::Util.uri_to_path(URI.parse('file:///C:/foo/bar')).should == 'C:/foo/bar' end it "should accept file scheme with double slashes as a UNC path" do Puppet::Util.uri_to_path(URI.parse('file://host/share/file')).should == '//host/share/file' end end end - describe "execution methods" do - let(:pid) { 5501 } - let(:null_file) { Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' } - - describe "#execute_posix (stubs)" 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 properly execute string commands with embedded newlines" do - Kernel.expects(:exec).with("/bin/echo 'foo' ; \n /bin/echo 'bar' ;") - - Puppet::Util.execute_posix("/bin/echo 'foo' ; \n /bin/echo 'bar' ;", {: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 (stubs)" 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, process_status(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 (stubs)" do - before :each do - Process.stubs(:waitpid2).with(pid).returns([pid, process_status(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 "#execute (posix locale)", :unless => Puppet.features.microsoft_windows? do - # build up a printf-style string that contains a command to get the value of an environment variable - # from the operating system. We can substitute into this with the names of the desired environment variables later. - get_env_var_cmd = 'echo $%s' - - # a sentinel value that we can use to emulate what locale environment variables might be set to on an international - # system. - lang_sentinel_value = "es_ES.UTF-8" - # a temporary hash that contains sentinel values for each of the locale environment variables that we override in - # "execute" - locale_sentinel_env = {} - Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value } - - it "should override the locale environment variables when :override_locale is not set (defaults to true)" do - # temporarily override the locale environment vars with a sentinel value, so that we can confirm that - # execute is actually setting them. - Puppet::Util::Execution.withenv(locale_sentinel_env) do - Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| - # we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL - expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : "" - Puppet::Util::execute(get_env_var_cmd % var).strip.should == expected_value - end - end - end - - it "should override the LANG environment variable when :override_locale is set to true" do - # temporarily override the locale environment vars with a sentinel value, so that we can confirm that - # execute is actually setting them. - Puppet::Util::Execution.withenv(locale_sentinel_env) do - Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| - # we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL - expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : "" - Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => true}).strip.should == expected_value - end - end - end - - it "should *not* override the LANG environment variable when :override_locale is set to false" do - # temporarily override the locale environment vars with a sentinel value, so that we can confirm that - # execute is not setting them. - Puppet::Util::Execution.withenv(locale_sentinel_env) do - Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| - Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => false}).strip.should == lang_sentinel_value - end - end - end - - it "should have restored the LANG and locale environment variables after execution" do - # we'll do this once without any sentinel values, to give us a little more test coverage - orig_env_vals = {} - Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| - orig_env_vals[var] = ENV[var] - end - # now we can really execute any command--doesn't matter what it is... - Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true}) - # now we check and make sure the original environment was restored - Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| - ENV[var].should == orig_env_vals[var] - end - - # now, once more... but with our sentinel values - Puppet::Util::Execution.withenv(locale_sentinel_env) do - # now we can really execute any command--doesn't matter what it is... - Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true}) - # now we check and make sure the original environment was restored - Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| - ENV[var].should == locale_sentinel_env[var] - end - end - - end - end - - describe "#execute (posix user env vars)", :unless => Puppet.features.microsoft_windows? do - # build up a printf-style string that contains a command to get the value of an environment variable - # from the operating system. We can substitute into this with the names of the desired environment variables later. - get_env_var_cmd = 'echo $%s' - - # a sentinel value that we can use to emulate what locale environment variables might be set to on an international - # system. - user_sentinel_value = "Abracadabra" - # a temporary hash that contains sentinel values for each of the locale environment variables that we override in - # "execute" - user_sentinel_env = {} - Puppet::Util::POSIX::USER_ENV_VARS.each { |var| user_sentinel_env[var] = user_sentinel_value } - - it "should unset user-related environment vars during execution" do - # first we set up a temporary execution environment with sentinel values for the user-related environment vars - # that we care about. - Puppet::Util::Execution.withenv(user_sentinel_env) do - # with this environment, we loop over the vars in question - Puppet::Util::POSIX::USER_ENV_VARS.each do |var| - # ensure that our temporary environment is set up as we expect - ENV[var].should == user_sentinel_env[var] - - # run an "exec" via the provider and ensure that it unsets the vars - Puppet::Util::execute(get_env_var_cmd % var).strip.should == "" - - # ensure that after the exec, our temporary env is still intact - ENV[var].should == user_sentinel_env[var] - end - - end - end - - it "should have restored the user-related environment variables after execution" do - # we'll do this once without any sentinel values, to give us a little more test coverage - orig_env_vals = {} - Puppet::Util::POSIX::USER_ENV_VARS.each do |var| - orig_env_vals[var] = ENV[var] - end - # now we can really execute any command--doesn't matter what it is... - Puppet::Util::execute(get_env_var_cmd % 'anything') - # now we check and make sure the original environment was restored - Puppet::Util::POSIX::USER_ENV_VARS.each do |var| - ENV[var].should == orig_env_vals[var] - end - - # now, once more... but with our sentinel values - Puppet::Util::Execution.withenv(user_sentinel_env) do - # now we can really execute any command--doesn't matter what it is... - Puppet::Util::execute(get_env_var_cmd % 'anything') - # now we check and make sure the original environment was restored - Puppet::Util::POSIX::USER_ENV_VARS.each do |var| - ENV[var].should == user_sentinel_env[var] - 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, process_status(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, process_status(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, :combine => false}) - 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') - path = stdout.path - Tempfile.stubs(:new).returns(stdout) - - Puppet::Util.execute('test command') - - File.should_not be_exist(path) - end - - it "should raise an error if failonfail is true and the child failed" do - Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) - - 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, process_status(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, process_status(0)]) - - expect { - Puppet::Util.execute('fail command', :failonfail => true) - }.not_to raise_error - end - - it "should respect default values for args that aren't overridden if a partial arg list is passed in" do - Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) - expect { - # here we are passing in a non-nil value for "arguments", but we aren't specifying a value for - # :failonfail. We expect it to be set to its normal default value (true). - Puppet::Util.execute('fail command', { :squelch => true }) - }.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/) - end - - end - - - end - describe "#which" do let(:base) { File.expand_path('/bin') } let(:path) { File.join(base, 'foo') } before :each do FileTest.stubs(:file?).returns false FileTest.stubs(:file?).with(path).returns true FileTest.stubs(:executable?).returns false FileTest.stubs(:executable?).with(path).returns true end it "should accept absolute paths" do Puppet::Util.which(path).should == path end it "should return nil if no executable found" do Puppet::Util.which('doesnotexist').should be_nil end it "should reject directories" do Puppet::Util.which(base).should be_nil end describe "on POSIX systems" do before :each do Puppet.features.stubs(:posix?).returns true Puppet.features.stubs(:microsoft_windows?).returns false end it "should walk the search PATH returning the first executable" do ENV.stubs(:[]).with('PATH').returns(File.expand_path('/bin')) Puppet::Util.which('foo').should == path end end describe "on Windows systems" do let(:path) { File.expand_path(File.join(base, 'foo.CMD')) } before :each do Puppet.features.stubs(:posix?).returns false Puppet.features.stubs(:microsoft_windows?).returns true end describe "when a file extension is specified" do it "should walk each directory in PATH ignoring PATHEXT" do ENV.stubs(:[]).with('PATH').returns(%w[/bar /bin].map{|dir| File.expand_path(dir)}.join(File::PATH_SEPARATOR)) FileTest.expects(:file?).with(File.join(File.expand_path('/bar'), 'foo.CMD')).returns false ENV.expects(:[]).with('PATHEXT').never Puppet::Util.which('foo.CMD').should == path end end describe "when a file extension is not specified" do it "should walk each extension in PATHEXT until an executable is found" do bar = File.expand_path('/bar') ENV.stubs(:[]).with('PATH').returns("#{bar}#{File::PATH_SEPARATOR}#{base}") ENV.stubs(:[]).with('PATHEXT').returns(".EXE#{File::PATH_SEPARATOR}.CMD") exts = sequence('extensions') FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.EXE')).returns false FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.CMD')).returns false FileTest.expects(:file?).in_sequence(exts).with(File.join(base, 'foo.EXE')).returns false FileTest.expects(:file?).in_sequence(exts).with(path).returns true Puppet::Util.which('foo').should == path end it "should walk the default extension path if the environment variable is not defined" do ENV.stubs(:[]).with('PATH').returns(base) ENV.stubs(:[]).with('PATHEXT').returns(nil) exts = sequence('extensions') %w[.COM .EXE .BAT].each do |ext| FileTest.expects(:file?).in_sequence(exts).with(File.join(base, "foo#{ext}")).returns false end FileTest.expects(:file?).in_sequence(exts).with(path).returns true Puppet::Util.which('foo').should == path end it "should fall back if no extension matches" do ENV.stubs(:[]).with('PATH').returns(base) ENV.stubs(:[]).with('PATHEXT').returns(".EXE") FileTest.stubs(:file?).with(File.join(base, 'foo.EXE')).returns false FileTest.stubs(:file?).with(File.join(base, 'foo')).returns true FileTest.stubs(:executable?).with(File.join(base, 'foo')).returns true Puppet::Util.which('foo').should == File.join(base, 'foo') end end end end end diff --git a/test/util/execution.rb b/test/util/execution.rb deleted file mode 100755 index 316231b66..000000000 --- a/test/util/execution.rb +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env ruby - -require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest') - -require 'puppet' -require 'puppettest' - -class TestPuppetUtilExecution < Test::Unit::TestCase - include PuppetTest - - def test_withenv - ENV["testing"] = "yay" - - assert_nothing_raised do - Puppet::Util::Execution.withenv :testing => "foo" do - $ran = ENV["testing"] - end - end - - assert_equal("yay", ENV["testing"]) - assert_equal("foo", $ran) - - ENV["rah"] = "yay" - assert_raise(ArgumentError) do - Puppet::Util::Execution.withenv :testing => "foo" do - raise ArgumentError, "yay" - end - end - - assert_equal("yay", ENV["rah"]) - end -end -