require 'rspec' require 'rspec/core/rake_task' require 'net/ssh' require 'specinfra' module NoradSpecRunner # Class to run Rspec tests over SSH class RemoteTask < Task SSH_TIMEOUT = 10 attr_reader :host, :username, :sshkey, :ssh_port, :obj, :platform, :results_file def initialize(encoded_key, options) @host = options.fetch(:host) @sshkey = options.fetch(:sshkey) # Decode the key and store File.open(@sshkey, "w") do |f| f.write Base64.decode64(encoded_key) end @ssh_port = options.fetch(:port, nil) @username = options.fetch(:username, nil) tests = options.fetch(:tests) sub_tests = options.fetch(:sub_tests) detect_os = options.fetch(:detect_os, false) @obj = RSpec::Core::RakeTask.new(@host) do |_| true end @results_file = options.fetch(:results_file) @obj.pattern = detect_os ? autodetect_test_pattern(tests) : tests @obj.rspec_opts = "-e #{sub_tests} --format json -o #{@results_file}" end def run pstderr = STDERR.dup ftmp = Tempfile.open('eout') FileUtils.touch results_file return if platform.nil? && unable_to_ssh? ENV['AUDIT_HOST'] = host ENV['AUDIT_USERNAME'] = username ENV['AUDIT_SSHKEY'] = sshkey ENV['AUDIT_SSH_PORT'] = ssh_port # Capture STDERR for SSH related errors STDERR.reopen(ftmp) obj.run_task(true) rescue SystemExit => e ftmp.rewind err = ftmp.read ftmp.close p err # We land here even on successful run (SystemExit exception), only report error if stderr is not empty if err =~ /Please set sudo password to Specinfra.configuration.sudo_password|set :request_pty, true/ write_error_to_results_file "SSH user #{username} requires SUDO permission with NOPASSWD: option set." elsif not err.empty? write_error_to_results_file err end rescue Exception => e # Unknown exception! write_error_to_results_file e.message ensure STDERR.reopen pstderr end private def write_error_to_results_file(error) File.open(results_file, "w") do |f| f.write "!! NORAD SPECS SSH ERROR !!\nError: #{error}\n" end end def start_ssh_session Net::SSH.start(host, username, port: ssh_port, timeout: SSH_TIMEOUT, auth_methods: ['publickey'], keys: [sshkey] ) end def autodetect_test_pattern(tests) if target_is_ios_device?(start_ssh_session) || target_is_linux?(start_ssh_session) x = "#{tests}/#{platform}_spec.rb" puts "HEEEY HEEEY HEEY WE will try to run #{x}" x else puts "COULD NOT DETECT OS!" "" # we need to do something to signify OS could not be detected end end def target_is_ios_device?(ssh_session) out = ssh_session.exec!('show version') if out =~ /Cisco ((?:\w+\s?){1,6}) Software.+Version ([[:alnum:][:punct:]]+)/ @platform = $1.gsub(/\s/, '_').downcase end !@platform.nil? end def target_is_linux?(ssh_session) spec_infra_backend = Specinfra::Backend::Ssh.new spec_infra_backend.set_config(:ssh, ssh_session) spec_infra_backend.set_config(:ssh_options, { user: username }) Specinfra::Helper::DetectOs.subclasses.each do |c| detector = c.new(spec_infra_backend) res = detector.detect if res res[:arch] ||= spec_infra_backend.run_command('uname -m').stdout.strip @platform = res[:family] # we can optionally add release here if we want break end end !@platform.nil? end end # Check if given host/ip is reachable and we can ssh as root # If not, then create empty log file for that host and return false. def unable_to_ssh? session = start_ssh_session session.exec('ls') session.close() false rescue Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout, Net::SSH::Timeout, Net::SSH::Exception, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e p e write_error_to_results_file e.message true end end