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
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
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
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
Private Class Methods
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
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
# File lib/puppet/util/execution.rb 93 def self.exitstatus 94 $CHILD_STATUS.exitstatus 95 end
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