module EasyIO
Public Instance Methods
_full_error_message(error_message, error_options, command)
click to toggle source
# File lib/easy_io/run.rb, line 219 def _full_error_message(error_message, error_options, command) command_message = error_options['show_command_on_error'] && command ? "\nCommand causing exception: " + command + "\n" : '' "Exception: #{error_message}\n#{error_options['info_on_exception']}#{'=' * 120}\n#{command_message}" end
_parse_for_errors(message, error_messages, error_options, command)
click to toggle source
# File lib/easy_io/run.rb, line 209 def _parse_for_errors(message, error_messages, error_options, command) errors_found = error_options['regex_error_filters'].any? { |regex_filter| message =~ regex_filter } _process_error_message(message, error_messages, error_options, command) if errors_found end
_process_error_message(error_message, error_messages, error_options, command)
click to toggle source
# File lib/easy_io/run.rb, line 214 def _process_error_message(error_message, error_messages, error_options, command) raise _full_error_message(error_message, error_options, command) if error_options['raise_on_first_error'] error_messages.push(error_message) # if we're not raising right away, add to the list of errors end
add_as_winrm_trusted_host(remote_host)
click to toggle source
# File lib/easy_io/run.rb, line 204 def add_as_winrm_trusted_host(remote_host) trusted_hosts = EasyIO.powershell_out('(Get-Item WSMan:\localhost\Client\TrustedHosts).value', return_all_stdout: true) EasyIO.powershell_out("Set-Item WSMan:\\localhost\\Client\\TrustedHosts -Value 'trusted_hosts, #{remote_host}' -Force") unless trusted_hosts.include?(remote_host) end
config()
click to toggle source
# File lib/easy_io/config.rb, line 4 def config @config ||= EasyJSON.config(defaults: defaults) end
defaults()
click to toggle source
# File lib/easy_io/config.rb, line 8 def defaults { 'paths' => { 'cache' => Dir.tmpdir, }, } end
execute_out(command, pid_logfile: nil, working_folder: Dir.pwd, regex_error_filters: [], info_on_exception: '', exception_exceptions: [], powershell: false, show_command_on_error: false, raise_on_first_error: true, return_all_stdout: false, output_separator: nil)
click to toggle source
execute a command with real-time output. Any stdout you want returned to the caller must come after the :output_separator which defaults to '#return_data#:'
return_all_stdout: return all output to the caller instead after process completion
# File lib/easy_io/run.rb, line 6 def execute_out(command, pid_logfile: nil, working_folder: Dir.pwd, regex_error_filters: [], info_on_exception: '', exception_exceptions: [], powershell: false, show_command_on_error: false, raise_on_first_error: true, return_all_stdout: false, output_separator: nil) raise "Invalid argument to execute_out! working_folder (#{working_folder}) is not a valid directory!" unless ::File.directory?(working_folder) output_separator ||= '#return_data#:' if return_all_stdout result = '' return_data_flag = true else STDOUT.sync = true result = nil return_data_flag = false end exit_status = nil error_messages = [] info_on_exception = "#{info_on_exception}\n" unless info_on_exception.end_with?("\n") error_options = { 'show_command_on_error' => show_command_on_error, 'info_on_exception' => info_on_exception, 'regex_error_filters' => regex_error_filters, 'raise_on_first_error' => raise_on_first_error } if powershell ps_script_file = "#{EasyIO.config['paths']['cache']}/easy_io/scripts/ps_script-thread_id-#{Thread.current.object_id}.ps1" FileUtils.mkdir_p(::File.dirname(ps_script_file)) unless ::File.directory? ::File.dirname(ps_script_file) ::File.write(ps_script_file, command) end popen_arguments = powershell ? ['powershell.exe', ps_script_file] : [command] Open3.popen3(*popen_arguments, chdir: working_folder) do |_stdin, stdout, stderr, wait_thread| unless pid_logfile.nil? # Log pid in case job or script dies FileUtils.mkdir_p(::File.dirname(pid_logfile)) unless ::File.directory? ::File.dirname(pid_logfile) ::File.write(pid_logfile, wait_thread.pid) end buffers = [stdout, stderr] queued_buffers = IO.select(buffers) || [[]] queued_buffers.first.each do |buffer| case buffer when stdout while (line = buffer.gets) if return_data_flag result += line next end stdout_split = line.split(output_separator) stdout_message = stdout_split.first.strip _parse_for_errors(stdout_message, error_messages, error_options, command) EasyIO.logger.info stdout_message unless stdout_message.empty? if stdout_split.count > 1 return_data_flag = true result = stdout_split.last end end when stderr error_message = '' error_message += line while (line = buffer.gets) next if error_message.empty? if exception_exceptions.any? { |ignore_filter| error_message =~ ignore_filter } EasyIO.logger.info error_message.strip next end _process_error_message(error_message, error_messages, error_options, command) end end exit_status = wait_thread.value end unless error_messages.empty? last_error = _full_error_message(error_messages.pop, error_options, command) error_messages.map! { |error_message| _full_error_message(error_message, error_options, nil) } error_messages.push(last_error) raise error_messages.join("\n") end [result, exit_status] end
levels()
click to toggle source
# File lib/easy_io/logger.rb, line 44 def levels { 'info' => Logger::INFO, 'debug' => Logger::DEBUG, 'warn' => Logger::WARN, 'error' => Logger::ERROR, 'fatal' => Logger::FATAL, 'unknown' => Logger::UNKNOWN, } end
logger()
click to toggle source
# File lib/easy_io/logger.rb, line 40 def logger @logger end
logger=(value)
click to toggle source
For portability, can be overridden with a class that has methods :level, :fatal, :error, :warn, :info, :debug and the others specified below. See ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger.html
For example, when using with Chef, set the logger to Chef::Log
# File lib/easy_io/logger.rb, line 35 def logger=(value) @logger = value @logger.class.class_eval { include LoggerEnhancement } end
notepad_prompt(text_file_path, comment)
click to toggle source
# File lib/easy_io/run.rb, line 232 def notepad_prompt(text_file_path, comment) ::FileUtils.mkdir_p ::File.dirname(text_file_path) unless ::File.directory?(::File.dirname(text_file_path)) ::File.write(text_file_path, "; #{comment}") unless ::File.exist?(text_file_path) EasyIO.logger.info comment.gsub('here', 'in the notepad window') `notepad #{text_file_path}` notepad_content = ::File.read(text_file_path) notepad_content.gsub(/;[^\r\n]*(\r\n|\r|\n)/i, '') # remove comments in text file end
pid_running?(pid)
click to toggle source
# File lib/easy_io/run.rb, line 224 def pid_running?(pid) begin Process.kill(0, pid) # Does not actually kill process, checks if it's running. rescue Errno::ESRCH nil end == 1 end
powershell_out(ps_script, pid_logfile: nil, working_folder: Dir.pwd, regex_error_filters: [], info_on_exception: '', exception_exceptions: [], show_command_on_error: false, return_all_stdout: false, output_separator: nil)
click to toggle source
execute a powershell script with real-time output. Any stdout you want returned to the caller must come after the :output_separator which defaults to '#return_data#:'
return_all_stdout: return all output to the caller instead after process completion
# File lib/easy_io/run.rb, line 76 def powershell_out(ps_script, pid_logfile: nil, working_folder: Dir.pwd, regex_error_filters: [], info_on_exception: '', exception_exceptions: [], show_command_on_error: false, return_all_stdout: false, output_separator: nil) execute_out(ps_script, pid_logfile: pid_logfile, working_folder: working_folder, regex_error_filters: regex_error_filters, info_on_exception: info_on_exception, exception_exceptions: exception_exceptions, powershell: true, show_command_on_error: show_command_on_error, return_all_stdout: return_all_stdout, output_separator: output_separator) end
process_command_output(data, output_stream, output_to_terminal)
click to toggle source
# File lib/easy_io/run.rb, line 161 def process_command_output(data, output_stream, output_to_terminal) output_stream << data if output_stream EasyIO.logger.info data if output_to_terminal end
run_command_on_remote_hosts(remote_hosts, command, credentials, command_message: nil, shell_type: nil, tail_count: nil, set_as_trusted_host: false, transport: :ssh)
click to toggle source
# File lib/easy_io/run.rb, line 166 def run_command_on_remote_hosts(remote_hosts, command, credentials, command_message: nil, shell_type: nil, tail_count: nil, set_as_trusted_host: false, transport: :ssh) tail_count ||= 1 # Return the last (1) line from each remote_host's log to the console shell_type ||= OS.windows? ? :cmd : :bash shell_type = shell_type.to_sym unless shell_type.is_a?(Symbol) supported_shell_types = OS.windows? ? [:cmd, :powershell] : [:bash] raise "Unsupported shell_type for running remote commands: '#{shell_type}'" unless supported_shell_types.include?(shell_type) threads = {} threads_output = {} log_folder = "#{EasyIO.config['paths']['cache']}/easy_io/logs" ::FileUtils.mkdir_p log_folder unless ::File.directory?(log_folder) EasyIO.logger.info "Output logs of processes run on the specified remote hosts will be placed in #{log_folder}..." remote_hosts.each do |remote_host| EasyIO.logger.info "Running `#{command_message || command}` on #{remote_host}..." threads[remote_host] = Thread.new do case shell_type when :powershell threads_output[remote_host] = run_remote_powershell_command(remote_host, command, credentials, set_as_trusted_host: set_as_trusted_host, transport: transport) when :cmd, :bash threads_output[remote_host] = run_remote_command(remote_host, command, credentials) end end end threads.values.each(&:join) # Wait for all commands to complete exceptions = [] threads_output.each do |remote_host, output| ::File.write("#{log_folder}/#{EasyFormat::File.windows_friendly_name(remote_host)}.#{::Time.now.strftime('%Y%m%d_%H%M%S')}.log", "#{output['stdout']}\n#{output['stderr']}") tail_output = output['stdout'].nil? ? '--no standard output--' : output['stdout'].split("\n").last(tail_count).join("\n") EasyIO.logger.info "[#{remote_host}]: #{tail_output}" if output['exception'] exceptions.push "Failed to run command on #{remote_host}: #{output['stderr']}\n#{output['exception'].cause}\n#{output['exception'].message}" next end exceptions.push "The script exited with exit code #{output['exitcode']}.\n\n#{output['stderr']}" unless output['exitcode'] == 0 end raise exceptions.join("\n\n") unless exceptions.empty? end
run_remote_command(remote_host, command, credentials, ssh_key: nil, output_stream: nil, output_file: nil, output_to_terminal: false, show_command_on_error: false)
click to toggle source
# File lib/easy_io/run.rb, line 107 def run_remote_command(remote_host, command, credentials, ssh_key: nil, output_stream: nil, output_file: nil, output_to_terminal: false, show_command_on_error: false) require 'net/ssh' start_params = if credentials['password'].nil? { host_key: 'ssh-rsa', keys: [ssh_key], verify_host_key: false, } else { password: credentials['password'], } end retries ||= 3 return_code = nil keep_stream_open = !output_stream.nil? # Leave the stream open if it was passed in output_stream ||= ::File.open(output_file, ::File::RDWR | ::File::CREAT) unless output_file.nil? stdout = '' Net::SSH.start(remote_host, credentials['user'], **start_params) do |ssh| command_thread = ssh.open_channel do |channel| channel.exec(command) do |ch, success| raise 'could not execute command' unless success ch.on_data { |_c, data| stdout += data; process_command_output(data, output_stream, output_to_terminal) } ch.on_extended_data { |_c, _type, data| stdout += data; process_command_output(data, output_stream, output_to_terminal) } ch.on_request('exit-status') { |_ch, data| return_code = data.read_long } end end command_thread.wait end { 'stdout' => stdout, 'exitcode' => return_code, } rescue Net::SSH::ConnectionTimeout => ex EasyIO.logger.info 'Net::SSH::ConnectionTimeout - retrying...' retry if (retries -= 1) >= 0 { 'exception' => ex, 'stderr' => (show_command_on_error ? "command: #{command}\n\n#{ex.message}" : ex.message) + "\n\n#{ex.backtrace}", 'exitcode' => return_code, } rescue => ex { 'exception' => ex, 'stderr' => (show_command_on_error ? "command: #{command}\n\n#{ex.message}" : ex.message) + "\n\n#{ex.backtrace}", 'exitcode' => return_code, } ensure output_stream.close unless keep_stream_open || !output_stream.respond_to?(:close) end
run_remote_powershell_command(remote_host, command, credentials, set_as_trusted_host: false, transport: :ssh, output_stream: nil, output_file: nil, output_to_terminal: false)
click to toggle source
# File lib/easy_io/run.rb, line 101 def run_remote_powershell_command(remote_host, command, credentials, set_as_trusted_host: false, transport: :ssh, output_stream: nil, output_file: nil, output_to_terminal: false) return run_remote_winrm_command(remote_host, command, credentials, set_as_trusted_host: set_as_trusted_host) if transport == :winrm command = 'powershell.exe ' unless command =~ /powershell/i run_remote_command(remote_host, command, credentials, output_stream: output_stream, output_file: output_file, output_to_terminal: output_to_terminal) end
run_remote_winrm_command(remote_host, command, credentials, set_as_trusted_host: false)
click to toggle source
# File lib/easy_io/run.rb, line 80 def run_remote_winrm_command(remote_host, command, credentials, set_as_trusted_host: false) add_as_winrm_trusted_host(remote_host) if set_as_trusted_host remote_command = <<-EOS $securePassword = ConvertTo-SecureString -AsPlainText '#{credentials['password']}' -Force $credential = New-Object System.Management.Automation.PSCredential -ArgumentList #{credentials['user']}, $securePassword Invoke-Command -ComputerName #{remote_host} -Credential $credential -ScriptBlock { #{command} } EOS output = powershell_out(remote_command, return_all_stdout: true) { 'stdout' => output.first, 'exitcode' => output.last, } rescue => ex { 'exception' => ex, 'stderr' => ex.message, 'exitcode' => 1, } end