diff options
author | Daniel DeLeo <dan@opscode.com> | 2011-12-01 11:51:25 -0800 |
---|---|---|
committer | Daniel DeLeo <dan@opscode.com> | 2011-12-01 11:51:25 -0800 |
commit | 1f7734e92f30c0b6545bcc8b537e920e3f12b6bc (patch) | |
tree | 0677cc613ef17454cef35c9ce0ad07f50db28603 /lib/mixlib/shellout.rb | |
download | mixlib-shellout-1f7734e92f30c0b6545bcc8b537e920e3f12b6bc.tar.gz |
Initial extraction of ShellOut from Chef
Diffstat (limited to 'lib/mixlib/shellout.rb')
-rw-r--r-- | lib/mixlib/shellout.rb | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/lib/mixlib/shellout.rb b/lib/mixlib/shellout.rb new file mode 100644 index 0000000..88a666d --- /dev/null +++ b/lib/mixlib/shellout.rb @@ -0,0 +1,237 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'etc' +require 'tmpdir' +require 'chef/log' +require 'fcntl' +require 'mixlib/shellout/exceptions' + +module Mixlib + + class ShellOut + READ_WAIT_TIME = 0.01 + READ_SIZE = 4096 + DEFAULT_READ_TIMEOUT = 600 + DEFAULT_ENVIRONMENT = {'LC_ALL' => 'C'} + + if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'mixlib/shellout/windows' + include ShellOut::Windows + else + require 'mixlib/shellout/unix' + include ShellOut::Unix + end + + attr_accessor :user + attr_accessor :group + attr_accessor :cwd + attr_accessor :valid_exit_codes + attr_accessor :live_stream + attr_accessor :command_log_level + attr_accessor :command_log_prepend + + attr_reader :command, :umask, :environment + attr_writer :timeout + attr_reader :execution_time + + attr_reader :stdout, :stderr, :status + + attr_reader :stdin_pipe, :stdout_pipe, :stderr_pipe, :process_status_pipe + + # === Arguments: + # Takes a single command, or a list of command fragments. These are used + # as arguments to Kernel.exec. See the Kernel.exec documentation for more + # explanation of how arguments are evaluated. The last argument can be an + # options Hash. + # === Options: + # If the last argument is a Hash, it is removed from the list of args passed + # to exec and used as an options hash. The following options are available: + # * +user+: the user the commmand should run as. if an integer is given, it is + # used as a uid. A string is treated as a username and resolved to a uid + # with Etc.getpwnam + # * +group+: the group the command should run as. works similarly to +user+ + # * +cwd+: the directory to chdir to before running the command + # * +umask+: a umask to set before running the command. If given as an Integer, + # be sure to use two leading zeros so it's parsed as Octal. A string will + # be treated as an octal integer + # * +returns+: one or more Integer values to use as valid exit codes for the + # subprocess. This only has an effect if you call +error!+ after + # +run_command+. + # * +environment+: a Hash of environment variables to set before the command + # is run. By default, the environment will *always* be set to 'LC_ALL' => 'C' + # to prevent issues with multibyte characters in Ruby 1.8. To avoid this, + # use :environment => nil for *no* extra environment settings, or + # :environment => {'LC_ALL'=>nil, ...} to set other environment settings + # without changing the locale. + # * +timeout+: a Numeric value for the number of seconds to wait on the + # child process before raising an Exception. This is calculated as the + # total amount of time that ShellOut waited on the child process without + # receiving any output (i.e., IO.select returned nil). Default is 60 + # seconds. Note: the stdlib Timeout library is not used. + # === Examples: + # Invoke find(1) to search for .rb files: + # find = Chef::ShellOut.new("find . -name '*.rb'") + # find.run_command + # # If all went well, the results are on +stdout+ + # puts find.stdout + # # find(1) prints diagnostic info to STDERR: + # puts "error messages" + find.stderr + # # Raise an exception if it didn't exit with 0 + # find.error! + # Run a command as the +www+ user with no extra ENV settings from +/tmp+ + # cmd = Chef::ShellOut.new("apachectl", "start", :user => 'www', :env => nil, :cwd => '/tmp') + # cmd.run_command # etc. + def initialize(*command_args) + @stdout, @stderr = '', '' + @live_stream = nil + @command_log_level = :debug + @command_log_prepend = nil + @environment = DEFAULT_ENVIRONMENT + @cwd = nil + @valid_exit_codes = [0] + + if command_args.last.is_a?(Hash) + parse_options(command_args.pop) + end + + @command = command_args.size == 1 ? command_args.first : command_args + end + + def umask=(new_umask) + @umask = (new_umask.respond_to?(:oct) ? new_umask.oct : new_umask.to_i) & 007777 + end + + def uid + return nil unless user + user.kind_of?(Integer) ? user : Etc.getpwnam(user.to_s).uid + end + + def gid + return nil unless group + group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid + end + + def timeout + @timeout || DEFAULT_READ_TIMEOUT + end + + # Creates a String showing the output of the command, including a banner + # showing the exact command executed. Used by +invalid!+ to show command + # results when the command exited with an unexpected status. + def format_for_exception + msg = "" + msg << "---- Begin output of #{command} ----\n" + msg << "STDOUT: #{stdout.strip}\n" + msg << "STDERR: #{stderr.strip}\n" + msg << "---- End output of #{command} ----\n" + msg << "Ran #{command} returned #{status.exitstatus}" if status + msg + end + + def exitstatus + @status && @status.exitstatus + end + + # Run the command, writing the command's standard out and standard error + # to +stdout+ and +stderr+, and saving its exit status object to +status+ + # === Returns + # returns +self+; +stdout+, +stderr+, +status+, and +exitstatus+ will be + # populated with results of the command + # === Raises + # * Errno::EACCES when you are not privileged to execute the command + # * Errno::ENOENT when the command is not available on the system (or not + # in the current $PATH) + # * ::CommandTimeout when the command does not complete + # within +timeout+ seconds (default: 60s) + def run_command + if command_log_prepend + Chef::Log.send(command_log_level, "#{command_log_prepend} sh(#{@command})") + else + Chef::Log.send(command_log_level, "sh(#{@command})") + end + super + end + + # Checks the +exitstatus+ against the set of +valid_exit_codes+. If + # +exitstatus+ is not in the list of +valid_exit_codes+, calls +invalid!+, + # which raises an Exception. + # === Returns + # nil::: always returns nil when it does not raise + # === Raises + # ::ShellCommandFailed::: via +invalid!+ + def error! + unless Array(valid_exit_codes).include?(exitstatus) + invalid!("Expected process to exit with #{valid_exit_codes.inspect}, but received '#{exitstatus}'") + end + end + + # Raises a ::ShellCommandFailed exception, appending the + # command's stdout, stderr, and exitstatus to the exception message. + # === Arguments + # +msg+: A String to use as the basis of the exception message. The + # default explanation is very generic, providing a more informative message + # is highly encouraged. + # === Raises + # ::ShellCommandFailed always + def invalid!(msg=nil) + msg ||= "Command produced unexpected results" + raise ShellCommandFailed, msg + "\n" + format_for_exception + end + + def inspect + "<#{self.class.name}##{object_id}: command: '#@command' process_status: #{@status.inspect} " + + "stdout: '#{stdout.strip}' stderr: '#{stderr.strip}' child_pid: #{@child_pid.inspect} " + + "environment: #{@environment.inspect} timeout: #{timeout} user: #@user group: #@group working_dir: #@cwd >" + end + + private + + def parse_options(opts) + opts.each do |option, setting| + case option.to_s + when 'cwd' + self.cwd = setting + when 'user' + self.user = setting + when 'group' + self.group = setting + when 'umask' + self.umask = setting + when 'timeout' + self.timeout = setting + when 'returns' + self.valid_exit_codes = Array(setting) + when 'live_stream' + self.live_stream = setting + when 'command_log_level' + self.command_log_level = setting + when 'command_log_prepend' + self.command_log_prepend = setting + when 'environment', 'env' + # passing :environment => nil means don't set any new ENV vars + @environment = setting.nil? ? {} : @environment.dup.merge!(setting) + else + raise InvalidCommandOption, "option '#{option.inspect}' is not a valid option for #{self.class.name}" + end + end + end + + + end +end |