module OpenShift::Runtime::Utils

Public Class Methods

oo_spawn(command, options = {}) click to toggle source

::oo_spawn(command, [, options]) -> [stdout, stderr, exit status]

spawn executes specified command and return its stdout, stderr and exit status. Or, raise exceptions if certain conditions are not met.

command: command line string which is passed to the standard shell

options: hash

:env: hash
  name => val : set the environment variable
  name => nil : unset the environment variable
:unsetenv_others => true   : clear environment variables except specified by :env
:chdir => path             : set current directory when running command
:expected_exitstatus       : An Integer value for the expected return code of command
                           : If not set spawn() returns exitstatus from command otherwise
                           : raise an error if exitstatus is not expected_exitstatus
:timeout                   : Maximum number of seconds to wait for command to finish. default: 3600
:uid                       : spawn command as given user in a SELinux context using runuser/runcon,
                           : stdin for the command is /dev/ull
:out                       : If specified, STDOUT from the child process will be redirected to the
                             provided +IO+ object.
:err                       : If specified, STDERR from the child process will be redirected to the
                             provided +IO+ object.
:quiet                     : If specified, the output from the command will not be logged

NOTE: If the out or err options are specified, the corresponding return value from oo_spawn will be the incoming/provided IO objects instead of the buffered String output. It's the responsibility of the caller to correctly handle the resulting data type.

# File lib/openshift-origin-node/utils/shell_exec.rb, line 83
def self.oo_spawn(command, options = {})

  options[:env]         ||= (options[:env] || {})
  options[:timeout]     ||= 3600
  options[:buffer_size] ||= 32768

  opts                   = {}
  opts[:unsetenv_others] = (options[:unsetenv_others] || false)
  opts[:close_others]    = true
  opts[:in]              = (options[:in] || '/dev/null')
  opts[:chdir]           = options[:chdir] if options[:chdir]

  IO.pipe do |read_stderr, write_stderr|
    IO.pipe do |read_stdout, write_stdout|
      opts[:out] = write_stdout
      opts[:err] = write_stderr

      if options[:uid]
        # lazy init otherwise we end up with a cyclic require...
        require 'openshift-origin-node/utils/selinux'

        current_context  = SELinux.getcon
        target_context   = SELinux.context_from_defaults(SELinux.get_mcs_label(options[:uid]))

        # Only switch contexts if necessary
        if (current_context != target_context) || (Process.uid != options[:uid])
          target_name = Etc.getpwuid(options[:uid]).name
          exec        = %Q{exec /usr/bin/runcon '#{target_context}' /bin/sh -c \\"#{command}\\"}
          command     = %Q{/sbin/runuser -s /bin/sh #{target_name} -c "#{exec}"}
        end
      end

      NodeLogger.logger.trace { "oo_spawn running #{command}: #{opts}" }
      pid = Kernel.spawn(options[:env], command, opts)

      unless pid
        raise ::OpenShift::Runtime::Utils::ShellExecutionException.new(
                  "Kernel.spawn failed for command '#{command}'")
      end

      begin
        write_stdout.close
        write_stderr.close

        out, err, status = read_results(pid, read_stdout, read_stderr, options)
        NodeLogger.logger.debug { "Shell command '#{command}' ran. rc=#{status.exitstatus} out=#{options[:quiet] ? "[SILENCED]" : Runtime::Utils.sanitize_credentials(out)}" }

        if (!options[:expected_exitstatus].nil?) && (status.exitstatus != options[:expected_exitstatus])
          raise ::OpenShift::Runtime::Utils::ShellExecutionException.new(
                    "Shell command '#{command}' returned an error. rc=#{status.exitstatus}",
                    status.exitstatus, out, err)
        end

        return [out, err, status.exitstatus]
      rescue TimeoutExceeded => e
        kill_process_tree(pid)
        raise ::OpenShift::Runtime::Utils::ShellExecutionException.new(
                  "Shell command '#{command}' exceeded timeout of #{e.seconds}", -1, out, err)
      end
    end
  end
end
sanitize_argument(arg) click to toggle source
# File lib/openshift-origin-node/utils/sanitize.rb, line 14
def self.sanitize_argument(arg)
  arg.to_s.gsub(/'/, '')
end
sanitize_credentials(arg) click to toggle source
# File lib/openshift-origin-node/utils/sanitize.rb, line 4
def self.sanitize_credentials(arg)
  begin
    arg.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
      .gsub(/(passwo?r?d\s*[:=]+\s*)\S+/, '\1[HIDDEN]')
      .gsub(/(usern?a?m?e?\s*[:=]+\s*)\S+/,'\1[HIDDEN]')
  rescue
    # If we were unable to encode the string, then just return it verbatim
    arg
  end
end
sanitize_url_argument(url) click to toggle source
# File lib/openshift-origin-node/utils/sanitize.rb, line 17
def self.sanitize_url_argument(url)
  url.to_s.gsub(/'/, '%27')
end

Private Class Methods

kill_process_tree(pid) click to toggle source

::kill_process_tree 2199 -> fixnum

Given a pid find it and KILL it and all it's children

# File lib/openshift-origin-node/utils/shell_exec.rb, line 200
def self.kill_process_tree(pid)
  ps_results = %xps -e -opid,ppid --no-headers`.split("\n")

  ps_tree = Hash.new { |h, k| h[k] = [k] }
  ps_results.each { |pair|
    p, pp = pair.split(' ')
    ps_tree[pp.to_i] << p.to_i
  }
  Process.kill("KILL", *(ps_tree[pid].flatten))
  Process.detach(pid)
end
read_results(pid, stdout, stderr, options) click to toggle source

::read_results(stdout pipe, stderr pipe, options) -> [*standard out, *standard error]

read stdout and stderr from spawned command until timeout

options: hash

:timeout     => seconds to wait for command to finish. Default: 3600
:buffer_size => how many bytes to read from pipe per iteration. Default: 32768
# File lib/openshift-origin-node/utils/shell_exec.rb, line 154
def self.read_results(pid, stdout, stderr, options)
  # TODO: Are these variables thread safe...?
  out     = (options[:out] || '')
  err     = (options[:err] || '')
  status  = nil
  readers = [stdout, stderr]

  begin
    Timeout::timeout(options[:timeout]) do
      while readers.any?
        ready = IO.select(readers, nil, nil, 10)

        if ready.nil?
          # If there is no IO to process check if child has exited...
          _, status = Process.wait2(pid, Process::WNOHANG)
        else
          # Otherwise, process us some IO...
          ready[0].each do |fd|
            buffer = (fd == stdout) ? out : err
            begin
              partial = fd.readpartial(options[:buffer_size])
              buffer << partial

              NodeLogger.logger.trace { "oo_spawn buffer(#{fd.fileno}/#{fd.pid}) #{Runtime::Utils.sanitize_credentials(partial)}" }
            rescue Errno::EAGAIN, Errno::EINTR
            rescue EOFError
              readers.delete(fd)
              fd.close
            end
          end
        end
      end

      _, status = Process.wait2 pid
      [out, err, status]
    end
  rescue Timeout::Error
    raise TimeoutExceeded, options[:timeout]
  rescue Errno::ECHILD
    return [out, err, status]
  end
end