# # Author:: Adam Jacob () # Copyright:: Copyright 2008-2016, Chef Software 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 "chef/log" require "chef/exceptions" require "tmpdir" require "fcntl" require "etc" class Chef module Mixin #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # NOTE: # The popen4 method upon which all the code here is based has a race # condition where it may fail to read all of the data written to stdout and # stderr after the child process exits. The tests for the code here # occasionally fail because of this race condition, so they have been # tagged "volatile". # # This code is considered deprecated, so it should not need to be modified # frequently, if at all. HOWEVER, if you do modify the code here, you must # explicitly enable volatile tests: # # bundle exec rspec spec/unit/mixin/command_spec.rb -t volatile # # In addition, you should make a note that tests need to be run with # volatile tests enabled on any pull request or bug report you submit with # your patch. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! module Command extend self # NOTE: run_command is deprecated in favor of using Chef::Shellout which now comes from the mixlib-shellout gem. NOTE # if RUBY_PLATFORM =~ /mswin|mingw32|windows/ require "chef/mixin/command/windows" include ::Chef::Mixin::Command::Windows extend ::Chef::Mixin::Command::Windows else require "chef/mixin/command/unix" include ::Chef::Mixin::Command::Unix extend ::Chef::Mixin::Command::Unix end # === Parameters # args: A number of required and optional arguments # command, : A complete command with options to execute or a command and options as an Array # creates: The absolute path to a file that prevents the command from running if it exists # cwd: Working directory to execute command in, defaults to Dir.tmpdir # timeout: How many seconds to wait for the command to execute before timing out # returns: The single exit value command is expected to return, otherwise causes an exception # ignore_failure: Whether to raise an exception on failure, or just return the status # output_on_failure: Return output in raised exception regardless of Log.level # # user: The UID or user name of the user to execute the command as # group: The GID or group name of the group to execute the command as # environment: Pairs of environment variable names and their values to set before execution # # === Returns # Returns the exit status of args[:command] def run_command(args = {}) status, stdout, stderr = run_command_and_return_stdout_stderr(args) status end # works same as above, except that it returns stdout and stderr # requirement => platforms like solaris 9,10 has weird issues where # even in command failure the exit code is zero, so we need to lookup stderr. def run_command_and_return_stdout_stderr(args = {}) command_output = "" args[:ignore_failure] ||= false args[:output_on_failure] ||= false # TODO: This is the wrong place for this responsibility. if args.has_key?(:creates) if File.exists?(args[:creates]) Chef::Log.debug("Skipping #{args[:command]} - creates #{args[:creates]} exists.") return false end end status, stdout, stderr = output_of_command(args[:command], args) command_output << "STDOUT: #{stdout}" command_output << "STDERR: #{stderr}" handle_command_failures(status, command_output, args) [status, stdout, stderr] end def output_of_command(command, args) Chef.deprecated(:run_command, "Chef::Mixin::Command.run_command is deprecated, please use shell_out") Chef::Log.debug("Executing #{command}") stderr_string, stdout_string, status = "", "", nil exec_processing_block = lambda do |pid, stdin, stdout, stderr| stdout_string, stderr_string = stdout.string.chomp, stderr.string.chomp end args[:cwd] ||= Dir.tmpdir unless ::File.directory?(args[:cwd]) raise Chef::Exceptions::Exec, "#{args[:cwd]} does not exist or is not a directory" end Dir.chdir(args[:cwd]) do if args[:timeout] begin Timeout.timeout(args[:timeout]) do status = popen4(command, args, &exec_processing_block) end rescue Timeout::Error => e Chef::Log.error("#{command} exceeded timeout #{args[:timeout]}") raise(e) end else status = popen4(command, args, &exec_processing_block) end Chef::Log.debug("---- Begin output of #{command} ----") Chef::Log.debug("STDOUT: #{stdout_string}") Chef::Log.debug("STDERR: #{stderr_string}") Chef::Log.debug("---- End output of #{command} ----") Chef::Log.debug("Ran #{command} returned #{status.exitstatus}") end [status, stdout_string, stderr_string] end def handle_command_failures(status, command_output, opts = {}) return if opts[:ignore_failure] opts[:returns] ||= 0 return if Array(opts[:returns]).include?(status.exitstatus) # if the log level is not debug, through output of command when we fail output = "" if Chef::Log.level == :debug || opts[:output_on_failure] output << "\n---- Begin output of #{opts[:command]} ----\n" output << command_output.to_s output << "\n---- End output of #{opts[:command]} ----\n" end raise Chef::Exceptions::Exec, "#{opts[:command]} returned #{status.exitstatus}, expected #{opts[:returns]}#{output}" end # Call #run_command but set LC_ALL to the system's current environment so it doesn't get changed to C. # # === Parameters # args: A number of required and optional arguments that will be handed out to #run_command # # === Returns # Returns the result of #run_command def run_command_with_systems_locale(args = {}) args[:environment] ||= {} args[:environment]["LC_ALL"] = ENV["LC_ALL"] run_command args end # def popen4(cmd, args={}, &b) # @@os_handler.popen4(cmd, args, &b) # end # module_function :popen4 # FIXME: yard with @yield def chdir_or_tmpdir(dir) dir ||= Dir.tmpdir unless File.directory?(dir) raise Chef::Exceptions::Exec, "#{dir} does not exist or is not a directory" end Dir.chdir(dir) do yield end end end end end