module Puppet::Util::Execution

This module defines methods for execution of system commands. It is intended for inclusion in classes that needs to execute system commands. @api public

Constants

NoOptionsSpecified

Default empty options for {execute}

Public Class Methods

execpipe(command, failonfail = true) { |pipe| ... } click to toggle source

The command can be a simple string, which is executed as-is, or an Array, which is treated as a set of command arguments to pass through.

In either case, the command is passed directly to the shell, STDOUT and STDERR are connected together, and STDOUT will be streamed to the yielded pipe.

@param command [String, Array<String>] the command to execute as one string,

or as parts in an array. The parts of the array are joined with one
separating space between each entry when converting to the command line
string to execute.

@param failonfail [Boolean] (true) if the execution should fail with

Exception on failure or not.

@yield [pipe] to a block executing a subprocess @yieldparam pipe [IO] the opened pipe @yieldreturn [String] the output to return @raise [Puppet::ExecutionFailure] if the executed child process did not

exit with status == 0 and `failonfail` is `true`.

@return [String] a string with the output from the subprocess executed by

the given block

@see Kernel#open for `mode` values @api public

   # File lib/puppet/util/execution.rb
59 def self.execpipe(command, failonfail = true)
60   # Paste together an array with spaces.  We used to paste directly
61   # together, no spaces, which made for odd invocations; the user had to
62   # include whitespace between arguments.
63   #
64   # Having two spaces is really not a big drama, since this passes to the
65   # shell anyhow, while no spaces makes for a small developer cost every
66   # time this is invoked. --daniel 2012-02-13
67   command_str = command.respond_to?(:join) ? command.join(' ') : command
68 
69   if respond_to? :debug
70     debug "Executing '#{command_str}'"
71   else
72     Puppet.debug { "Executing '#{command_str}'" }
73   end
74 
75   # force the run of the command with
76   # the user/system locale to "C" (via environment variables LANG and LC_*)
77   # it enables to have non localized output for some commands and therefore
78   # a predictable output
79   english_env = ENV.to_hash.merge( {'LANG' => 'C', 'LC_ALL' => 'C'} )
80   output = Puppet::Util.withenv(english_env) do
81     open("| #{command_str} 2>&1") do |pipe|
82       yield pipe
83     end
84   end
85 
86   if failonfail && exitstatus != 0
87     raise Puppet::ExecutionFailure, output.to_s
88   end
89 
90   output
91 end
execute(command, options = NoOptionsSpecified) click to toggle source

Executes the desired command, and return the status and output. def execute(command, options) @param command [Array<String>, String] the command to execute. If it is

an Array the first element should be the executable and the rest of the
elements should be the individual arguments to that executable.

@param options [Hash] a Hash of options @option options [String] :cwd the directory from which to run the command. Raises an error if the directory does not exist.

This option is only available on the agent. It cannot be used on the master, meaning it cannot be used in, for example,
regular functions, hiera backends, or report processors.

@option options [Boolean] :failonfail if this value is set to true, then this method will raise an error if the

command is not executed successfully.

@option options [Integer, String] :uid (nil) the user id of the user that the process should be run as. Will be ignored if the

user id matches the effective user id of the current process.

@option options [Integer, String] :gid (nil) the group id of the group that the process should be run as. Will be ignored if the

group id matches the effective group id of the current process.

@option options [Boolean] :combine sets whether or not to combine stdout/stderr in the output, if false stderr output is discarded @option options [String] :stdinfile (nil) sets a file that can be used for stdin. Passing a string for stdin is not currently

supported.

@option options [Boolean] :squelch (false) if true, ignore stdout / stderr completely. @option options [Boolean] :override_locale (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.

@option options [Hash<{String => String}>] :custom_environment ({}) a hash of key/value pairs to set as environment variables for the duration

of the command.

@return [Puppet::Util::Execution::ProcessOutput] output as specified by options @raise [Puppet::ExecutionFailure] if the executed chiled process did not exit with status == 0 and `failonfail` is

`true`.

@note Unfortunately, the default behavior for failonfail and combine (since

0.22.4 and 0.24.7, respectively) depend on whether options are specified
or not. If specified, then failonfail and combine default to false (even
when the options specified are neither failonfail nor combine). If no
options are specified, then failonfail and combine default to true.

@comment See commits efe9a833c and d32d7f30 @api public

    # File lib/puppet/util/execution.rb
137 def self.execute(command, options = NoOptionsSpecified)
138   # specifying these here rather than in the method signature to allow callers to pass in a partial
139   # set of overrides without affecting the default values for options that they don't pass in
140   default_options = {
141       :failonfail => NoOptionsSpecified.equal?(options),
142       :uid => nil,
143       :gid => nil,
144       :combine => NoOptionsSpecified.equal?(options),
145       :stdinfile => nil,
146       :squelch => false,
147       :override_locale => true,
148       :custom_environment => {},
149       :sensitive => false,
150       :suppress_window => false,
151   }
152 
153   options = default_options.merge(options)
154 
155   if command.is_a?(Array)
156     command = command.flatten.map(&:to_s)
157     command_str = command.join(" ")
158   elsif command.is_a?(String)
159     command_str = command
160   end
161 
162   # do this after processing 'command' array or string
163   command_str = '[redacted]' if options[:sensitive]
164 
165   user_log_s = String.new
166   if options[:uid]
167     user_log_s << " uid=#{options[:uid]}"
168   end
169   if options[:gid]
170     user_log_s << " gid=#{options[:gid]}"
171   end
172   if user_log_s != ''
173     user_log_s.prepend(' with')
174   end
175 
176   if respond_to? :debug
177     debug "Executing#{user_log_s}: '#{command_str}'"
178   else
179     Puppet.debug { "Executing#{user_log_s}: '#{command_str}'" }
180   end
181 
182   null_file = Puppet::Util::Platform.windows? ? 'NUL' : '/dev/null'
183 
184   cwd = options[:cwd]
185   if cwd && ! Puppet::FileSystem.directory?(cwd)
186     raise ArgumentError, _("Working directory %{cwd} does not exist!") % { cwd: cwd }
187   end
188 
189   begin
190     stdin = Puppet::FileSystem.open(options[:stdinfile] || null_file, nil, 'r')
191     # On Windows, continue to use the file-based approach to avoid breaking people's existing
192     # manifests. If they use a script that doesn't background cleanly, such as
193     # `start /b ping 127.0.0.1`, we couldn't handle it with pipes as there's no non-blocking
194     # read available.
195     if options[:squelch]
196       stdout = Puppet::FileSystem.open(null_file, nil, 'w')
197     elsif Puppet.features.posix?
198       reader, stdout = IO.pipe
199     else
200       stdout = Puppet::FileSystem::Uniquefile.new('puppet')
201     end
202     stderr = options[:combine] ? stdout : Puppet::FileSystem.open(null_file, nil, 'w')
203 
204     exec_args = [command, options, stdin, stdout, stderr]
205     output = String.new
206 
207     # We close stdin/stdout/stderr immediately after fork/exec as they're no longer needed by
208     # this process. In most cases they could be closed later, but when `stdout` is the "writer"
209     # pipe we must close it or we'll never reach eof on the `reader` pipe.
210     execution_stub = Puppet::Util::ExecutionStub.current_value
211     if execution_stub
212       child_pid = execution_stub.call(*exec_args)
213       [stdin, stdout, stderr].each {|io| io.close rescue nil}
214       return child_pid
215     elsif Puppet.features.posix?
216       child_pid = nil
217       begin
218         child_pid = execute_posix(*exec_args)
219         [stdin, stdout, stderr].each {|io| io.close rescue nil}
220         if options[:squelch]
221           exit_status = Process.waitpid2(child_pid).last.exitstatus
222         else
223           # Use non-blocking read to check for data. After each attempt,
224           # check whether the child is done. This is done in case the child
225           # forks and inherits stdout, as happens in `foo &`.
226           
227           until results = Process.waitpid2(child_pid, Process::WNOHANG) #rubocop:disable Lint/AssignmentInCondition
228 
229             # If not done, wait for data to read with a timeout
230             # This timeout is selected to keep activity low while waiting on
231             # a long process, while not waiting too long for the pathological
232             # case where stdout is never closed.
233             ready = IO.select([reader], [], [], 0.1)
234             begin
235               output << reader.read_nonblock(4096) if ready
236             rescue Errno::EAGAIN
237             rescue EOFError
238             end
239           end
240 
241           # Read any remaining data. Allow for but don't expect EOF.
242           begin
243             loop do
244               output << reader.read_nonblock(4096)
245             end
246           rescue Errno::EAGAIN
247           rescue EOFError
248           end
249 
250           # Force to external encoding to preserve prior behavior when reading a file.
251           # Wait until after reading all data so we don't encounter corruption when
252           # reading part of a multi-byte unicode character if default_external is UTF-8.
253           output.force_encoding(Encoding.default_external)
254           exit_status = results.last.exitstatus
255         end
256         child_pid = nil
257       rescue Timeout::Error => e
258         # NOTE: For Ruby 2.1+, an explicit Timeout::Error class has to be
259         # passed to Timeout.timeout in order for there to be something for
260         # this block to rescue.
261         unless child_pid.nil?
262           Process.kill(:TERM, child_pid)
263           # Spawn a thread to reap the process if it dies.
264           Thread.new { Process.waitpid(child_pid) }
265         end
266 
267         raise e
268       end
269     elsif Puppet::Util::Platform.windows?
270       process_info = execute_windows(*exec_args)
271       begin
272         [stdin, stderr].each {|io| io.close rescue nil}
273         exit_status = Puppet::Util::Windows::Process.wait_process(process_info.process_handle)
274 
275         # read output in if required
276         unless options[:squelch]
277           output = wait_for_output(stdout)
278           Puppet.warning _("Could not get output") unless output
279         end
280       ensure
281         FFI::WIN32.CloseHandle(process_info.process_handle)
282         FFI::WIN32.CloseHandle(process_info.thread_handle)
283       end
284     end
285 
286     if options[:failonfail] and exit_status != 0
287       raise Puppet::ExecutionFailure, _("Execution of '%{str}' returned %{exit_status}: %{output}") % { str: command_str, exit_status: exit_status, output: output.strip }
288     end
289   ensure
290     # Make sure all handles are closed in case an exception was thrown attempting to execute.
291     [stdin, stdout, stderr].each {|io| io.close rescue nil}
292     if !options[:squelch]
293       # if we opened a pipe, we need to clean it up.
294       reader.close if reader
295       stdout.close! if Puppet::Util::Platform.windows?
296     end
297   end
298 
299   Puppet::Util::Execution::ProcessOutput.new(output || '', exit_status)
300 end
Also aliased as: util_execute
ruby_path() click to toggle source

Returns the path to the ruby executable (available via Config object, even if it's not in the PATH… so this is slightly safer than just using Puppet::Util.which) @return [String] the path to the Ruby executable @api private

    # File lib/puppet/util/execution.rb
307 def self.ruby_path()
308   File.join(RbConfig::CONFIG['bindir'],
309             RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']).
310     sub(/.*\s.*/m, '"\&"')
311 end
util_execute(command, options = NoOptionsSpecified)
Alias for: execute

Private Class Methods

execute_posix(command, options, stdin, stdout, stderr) click to toggle source

This is private method. @comment see call to private_class_method after method definition @api private

    # File lib/puppet/util/execution.rb
323 def self.execute_posix(command, options, stdin, stdout, stderr)
324   child_pid = Puppet::Util.safe_posix_fork(stdin, stdout, stderr) do
325     # We can't just call Array(command), and rely on it returning
326     # things like ['foo'], when passed ['foo'], because
327     # Array(command) will call command.to_a internally, which when
328     # given a string can end up doing Very Bad Things(TM), such as
329     # turning "/tmp/foo;\r\n /bin/echo" into ["/tmp/foo;\r\n", " /bin/echo"]
330     command = [command].flatten
331     Process.setsid
332     begin
333       # We need to chdir to our cwd before changing privileges as there's a
334       # chance that the user may not have permissions to access the cwd, which
335       # would cause execute_posix to fail.
336       cwd = options[:cwd]
337       Dir.chdir(cwd) if cwd
338 
339       Puppet::Util::SUIDManager.change_privileges(options[:uid], options[:gid], true)
340 
341       # if the caller has requested that we override locale environment variables,
342       if (options[:override_locale]) then
343         # loop over them and clear them
344         Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) }
345         # set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output
346         # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in
347         # a forked process.
348         ENV['LANG'] = 'C'
349         ENV['LC_ALL'] = 'C'
350       end
351 
352       # unset all of the user-related environment variables so that different methods of starting puppet
353       # (automatic start during boot, via 'service', via /etc/init.d, etc.) won't have unexpected side
354       # effects relating to user / home dir environment vars.
355       # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in
356       # a forked process.
357       Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) }
358 
359       options[:custom_environment] ||= {}
360       Puppet::Util.withenv(options[:custom_environment]) do
361         Kernel.exec(*command)
362       end
363     rescue => detail
364       Puppet.log_exception(detail, _("Could not execute posix command: %{detail}") % { detail: detail })
365       exit!(1)
366     end
367   end
368   child_pid
369 end
execute_windows(command, options, stdin, stdout, stderr) click to toggle source

This is private method. @comment see call to private_class_method after method definition @api private

    # File lib/puppet/util/execution.rb
377 def self.execute_windows(command, options, stdin, stdout, stderr)
378   command = command.map do |part|
379     part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part
380   end.join(" ") if command.is_a?(Array)
381 
382   options[:custom_environment] ||= {}
383   Puppet::Util.withenv(options[:custom_environment]) do
384     Puppet::Util::Windows::Process.execute(command, options, stdin, stdout, stderr)
385   end
386 end
exitstatus() click to toggle source
   # File lib/puppet/util/execution.rb
93 def self.exitstatus
94   $CHILD_STATUS.exitstatus
95 end
wait_for_output(stdout) click to toggle source

This is private method. @comment see call to private_class_method after method definition @api private

    # File lib/puppet/util/execution.rb
394 def self.wait_for_output(stdout)
395   # Make sure the file's actually been written.  This is basically a race
396   # condition, and is probably a horrible way to handle it, but, well, oh
397   # well.
398   # (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry
399   #  about a race condition because all of the places that we call this from are preceded by a call to "waitpid2",
400   #  meaning that the processes responsible for writing the file have completed before we get here.)
401   2.times do |try|
402     if Puppet::FileSystem.exist?(stdout.path)
403       stdout.open
404       begin
405         return stdout.read
406       ensure
407         stdout.close
408         stdout.unlink
409       end
410     else
411       time_to_sleep = try / 2.0
412       Puppet.warning _("Waiting for output; will sleep %{time_to_sleep} seconds") % { time_to_sleep: time_to_sleep }
413       sleep(time_to_sleep)
414     end
415   end
416   nil
417 end