#!/usr/bin/env ruby
# Copyright 2011 Red Hat, Inc.
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

require 'rhc-common'
require 'net/http'
require 'net/https'
require 'rbconfig'
require 'yaml'
require 'tempfile'

if RUBY_VERSION.to_f == 1.9
  require 'rubygems'
  gem     'test-unit'
end

require 'test/unit'
require 'test/unit/ui/console/testrunner'


#
# print help
#
def p_usage
  rhlogin = get_var('default_rhlogin') ? "Default: #{get_var('default_rhlogin')}" : "required"
  puts <<USAGE

Usage: #{$0}
Run a simple check on local configs and credentials to confirm tools are
properly setup.  Often run to troubleshoot connection issues.

  -l|--rhlogin   rhlogin    Red Hat login (RHN or OpenShift login with OpenShift Express access) (#{rhlogin})
  -p|--password  password   Red Hat password (for RHN or OpenShift)
  -d|--debug                Print Debug info
  -h|--help                 Show Usage info
  --config  path            Path of alternate config file
  --timeout #               Timeout, in seconds, for connection

USAGE
exit 255
end

begin
  opts = GetoptLong.new(
    ["--debug", "-d", GetoptLong::NO_ARGUMENT],
    ["--help",  "-h", GetoptLong::NO_ARGUMENT],
    ["--rhlogin",  "-l", GetoptLong::REQUIRED_ARGUMENT],
    ["--password",  "-p", GetoptLong::REQUIRED_ARGUMENT],
    ["--config", GetoptLong::REQUIRED_ARGUMENT],
    ["--timeout", GetoptLong::REQUIRED_ARGUMENT]
  )
  $opt = {}
  opts.each do |o, a|
    $opt[o[2..-1]] = a.to_s
  end
rescue Exception => e
  #puts e.message
  p_usage
end

if $opt["help"]
  p_usage
end


# If provided a config path, check it
check_cpath($opt)

# Need to store the config information so tests can use it
#  since they are technically new objects
$opts_config_path   = @opts_config_path 
$local_config_path  = @local_config_path
$opts_config        = @opts_config
$local_config       = @local_config
$global_config      = @global_config

# Pull in configs from files
$libra_server = get_var('libra_server')
$debug = get_var('debug') == 'false' ? nil : get_var('debug')


if $opt["debug"]
  $debug = true
end
RHC::debug($debug)

RHC::timeout($opt["timeout"] ? $opt["timeout"] : get_var('timeout'))

$opt["rhlogin"] = get_var('default_rhlogin') unless $opt["rhlogin"]
if !RHC::check_rhlogin($opt['rhlogin'])
  p_usage
end
$rhlogin = $opt["rhlogin"]

$password = $opt['password']
if !$password
  $password = RHC::get_password
end

#
# Generic Info
#
$debuginfo = {
  'environment' => {
    'Ruby Version' => RUBY_VERSION,
    "host_alias" => Config::CONFIG['host_alias']
  },
  'options' => {
    "Command Line" => $opt,
    "Opts" => $opts_config,
    "Local" => $local_config,
    "Global" => $global_config
  }
}

# Don't actually log the password, but some information to help with troubleshooting
if $opt['password']
  $debuginfo['options']['Command Line']['password'] = {
    "Length" => $opt['password'].length, 
    "Starts with" => $opt['password'][0..0]
  }
end

#
# Check for proxy environment
#
if ENV['http_proxy']
  if ENV['http_proxy']!~/^(\w+):\/\// then
    ENV['http_proxy']="http://" + ENV['http_proxy']
  end
  proxy_uri=URI.parse(ENV['http_proxy'])
  $http = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
  $debuginfo['environment']['proxy'] = "#{proxy_uri.user}:#{proxy_uri.password}@#{proxy_uri.host}:#{proxy_uri.port}"
else
  $http = Net::HTTP
  $debuginfo['environment']['proxy'] = "none"
end

#####################################
#     Done setting up environment   #
#####################################

module TestBase
  def initialize(*args)
    super
    $connectivity ||= false
    # These need to be loaded for each test to be able to access them
    @opts_config_path   = $opts_config_path 
    @local_config_path  = $local_config_path
    @opts_config        = $opts_config
    @local_config       = $local_config
    @global_config      = $global_config
  end

  def fetch_url_json(uri, data)
    json_data = RHC::generate_json(data)
    url = URI.parse("https://#{$libra_server}#{uri}")
    req = $http::Post.new(url.path)
    req.set_form_data({'json_data' => json_data, 'password' => $password})
    http = $http.new(url.host, url.port)
    http.open_timeout = 10
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE

    response = http.start {|http| http.request(req)}
    return response
  ensure
    $debuginfo['fetches'] ||= []
    body = JSON.parse(response.body)
    body['data'] = body['data'] ? JSON.parse(body['data']) : ''
    $debuginfo['fetches'] << {url.to_s => {
      'body' => body,
      'header' => response.instance_variable_get(:@header)
    }}
  end

  def continue_test
    begin
      yield
    rescue Test::Unit::AssertionFailedError => e
      self.send(:add_failure,e.message,e.backtrace)
    end
  end
end

#####################################################
#                Tests start here                   #
#####################################################
#                                                   #                                                  
#  Note: Tests and testcases are run alphabetically # 
#                                                   #                                                  
#####################################################

def error_for(name,*args)
  if name.kind_of? String
    name = name.downcase.to_sym
  end
  sprintf($messages[name],*args)
end

class Test1_Connectivity < Test::Unit::TestCase
  include TestBase
  def teardown
    # Set global variable in case we cannot connect to the server
    if method_name == 'test_connectivity'
      $connectivity = passed?
    end
  end

  #
  # Checking Connectivity / cart list
  #
  def test_connectivity
    data = {'cart_type' => 'standalone'}

    response = fetch_url_json("/broker/cartlist", data)
    assert_equal 200, response.code.to_i, error_for(:no_connectivity, $libra_server)
  end
end

class Test2_Authentication < Test::Unit::TestCase
  include TestBase
  #
  # Checking Authentication
  #
  def test_authentication
    assert $connectivity, error_for(:cant_connect)

    data = {'rhlogin' => $rhlogin}
    response = fetch_url_json("/broker/userinfo", data)
    resp_json = JSON.parse(response.body)

    case response.code.to_i
    when 404
      assert false, error_for(:_404, $rhlogin)     
    when 401
      assert false, error_for(:_401, $rhlogin)     
    when 200
      assert true
    else
      assert false, error_for(:_other_http, resp_json['result'])
    end

    $user_info = JSON.parse(resp_json['data'].to_s)
  end
end

#
# Checking ssh key
#
class Test3_SSH < Test::Unit::TestCase

  @@local_derived_ssh_pubkey = nil
  @@local_ssh_pubkey = nil

  include TestBase

  def setup
    @libra_kfile = get_kfile(false)
    if @libra_kfile
      @libra_kpfile = get_kpfile(@libra_kfile, false)
    end
  end

  def test_01_ssh_private_key
    check_permissions(@libra_kfile, /[4-7]00/) # Needs to at least be readable by user and nobody else

    # Derive the public key from the private key
    key_dump = `ssh-keygen -f #{@libra_kfile} -y`
    @@local_derived_ssh_pubkey = key_dump.to_s.strip.split(' ')[1]

    assert_not_nil @@local_derived_ssh_pubkey, error_for(:no_derive)
    assert_not_nil $user_info, error_for(:no_account) 

    $remote_ssh_pubkeys = []
    additional_ssh_keys = RHC::get_ssh_keys($libra_server, $rhlogin, $password, $http)
    if additional_ssh_keys && additional_ssh_keys.kind_of?(Hash)
      additional_ssh_keys.each do |name, sshkey|
        $remote_ssh_pubkeys.push(sshkey['key'])
      end
    end

    $remote_ssh_pubkeys.push($user_info["user_info"]["ssh_key"])
    assert $remote_ssh_pubkeys.include?(@@local_derived_ssh_pubkey), error_for(:no_match_pub, @libra_kfile)
  end

  def test_02_ssh_public_key
    check_permissions(@libra_kpfile, /.../) # Any permissions are OK

    # Store the public key
    fp = File.open(@libra_kpfile)
    @@local_ssh_pubkey = fp.gets.split(' ')[1]

    assert_not_nil @@local_ssh_pubkey, error_for(:no_pubkey, 'the remote key')

    # Test public key and remote key
    assert $remote_ssh_pubkeys.include?(@@local_ssh_pubkey), error_for(:no_match_pub, @libra_kfile)
  ensure
    fp.close if fp
  end

  def test_05_ssh_config
    check_permissions('~/.ssh/config',/600/)
  end

  def test_06_ssh_agent
    loaded_keys = `ssh-add -L`.lines.to_a
    # Make sure we can load keys from ssh-agent
    assert !loaded_keys.empty?, error_for(:no_keys_loaded)
    loaded_keys.map!{|key| key.split(' ')[1]}
    
    assert_not_nil @@local_ssh_pubkey, error_for(:no_pubkey, 'the keys in ssh-agent')
    assert loaded_keys.include?(@@local_ssh_pubkey),error_for(:pubkey_not_loaded)
  end

  def test_07_ssh_connect
    assert_not_nil $user_info, error_for(:no_account)
    host_template = "%s@%s-%s.%s"
    $user_info['app_info'].each do |k,v|
      uuid = v['uuid']
      host = sprintf("%s-%s",k,$user_info['user_info']['namespace'])
      domain = $user_info['user_info']['rhc_domain']
      ssh_command = sprintf("ssh %s@%s.%s true &> /dev/null", uuid, host, domain)
      system(ssh_command)
      continue_test{ assert_equal $?, 0, error_for(:cant_ssh, host) }
    end
  end

  private
  def check_permissions(file,permission)
    file = File.expand_path(file)

    assert File.exists?(file), error_for(:file_not_found,file)

    perms = sprintf('%o',File.stat(file).mode)[-3..-1]

    assert_match permission, perms, error_for(:bad_permissions,file,perms,permission.source)
  end
end

############################################################
#     Below this line is the custom Test::Runner code      #
# Modification should not be necessary to add/modify tests #
############################################################

class CheckRunner < Test::Unit::UI::Console::TestRunner
  def initialize(*args)
    super
    @output_level = 1
    puts $messages[:started]
  end

  def add_fault(fault)
    @faults << fault
    print(fault.single_character_display)
    @already_outputted = true
  end

  def print_underlined(string)
    string = "||  #{string}  ||"
    line = "=" * string.length
    puts
    puts line
    puts string
    puts line
  end

  def render_message(message)
    lines = message.to_a
    message = [] 
    @num ||= 0
    message << "#{@num+=1}) #{lines.shift.strip}"
    lines.each do |line|
      # Break if we get the standard assert error information or a blank line
      break if line.match(/^\<.*?> /) or line.match(/^\s*$/)
      message << "\t#{line.strip}"
    end
    message.join("\n")
  end

  def finished(*args)
    super
    $debuginfo['errors'] = @faults

    if @faults.empty?
      print_underlined $messages[:passed]
    else
      print_underlined $messages[:failed]
      @errors = []
      # Need to separate the failures from the errors

      @faults.each do |f|
        case f
        when Test::Unit::Failure
          puts render_message(f.message)
        when Test::Unit::Error
          @errors << f
        end
      end

      # Errors mean something in the test is broken, not just failed
      unless @errors.empty?
        @num = 0
        print_underlined $messages[:error]
        @errors.each do |e| 
          lines = e.long_display.to_a[1..-1]
          puts "#{@num+=1}) #{lines.shift}"
          lines.each{|l| puts "\t#{l}"}
        end 
      end

    end

    if $debug
      Tempfile.open(%w(rhc-chk. .log))do |file|
        ObjectSpace.undefine_finalizer(file) # persist tempfile
        file.write $debuginfo.to_yaml
        puts
        puts "Debugging information dumped to #{file.path}"
      end
    end
  end
end

Test::Unit::AutoRunner::RUNNERS[:console] = proc do |r|
  CheckRunner
end

$messages = YAML.load <<-EOF
---
  :started: Analyzing system
  :passed: Congratulations, your system has passed all tests
  :failed: Your system did not pass all of the tests
  :error: Something went wrong, and not all tests completed
  :no_derive: "We were unable to derive your public SSH key and compare it to the remote\n FIX: ???"
  :no_connectivity: "Cannot connect to server, therefore most tests will not work\n FIX: Ensure that you are able to connect to %s"
  :no_account: You must have an account on the server in order to test SSH key matches
  :cant_connect: You need to be able to connect to the server in order to test authentication
  :no_match_pub: "Local %s does not match remote pub key, SSH auth will not work\n FIX: Perhaps you should regenerate your public key, or run rhc-create-domain with -a to alter the remote key"
  :_404: "The user %s does not have an account on this server\n FIX: Please ensure that you've entered the correct username"
  :no_pubkey: We were unable to read your public SSH key to compare to %s
  :_401: "Invalid user credentials for %s\n FIX: Please ensure you used the correct password"
  :file_not_found: File %s does not exist, cannot check permissions
  :_other_http: "There was an error communicating with the server: %s"
  :bad_permissions: "File %s has incorrect permissions %s\n FIX: Permissions should match %s"
  :no_keys_loaded: Either ssh-agent is not running or you do not have any keys loaded 
  :pubkey_not_loaded: Your public key is not loaded into a running ssh-agent
  :cant_ssh: "Cannot SSH into your app: %s"
  EOF