<commit_before>require 'colorize'

module Sonic
  class Ssh
    autoload :IdentifierDetector, 'sonic/ssh/identifier_detector'
    autoload :CliOptions, 'sonic/ssh/cli_options'

    include AwsService
    include CliOptions

    def initialize(identifier, options)
      @options = options

      @user, @identifier = extract_user!(identifier) # extracts/strips user from identifier
      # While --user option is supported at the class level, don't expose at the CLI level
      # to encourage users to use user@host notation.
      @user ||= options[:user] || setting.data["user"]

      @service = @identifier # always set service even though it's not always used as the identifier
      @cluster = options[:cluster] || setting.default_cluster(@service)
      @bastion = options[:bastion] || setting.default_bastion(@bastion)
    end

    def run
      ssh = build_ssh_command
      retry_until_success(*ssh) if @options[:retry]
      kernel_exec(*ssh) # must splat the Array here
    end

    def bastion_host
      return @identifier if @options[:noop] # for specs
      @bastion_host ||= build_bastion_host
    end

    def build_bastion_host
      host = @bastion
      host = "#{@user}@#{host}" unless host.include?('@')
      host
    end

    # used by child Classes
    def ssh_host
      return @identifier if @options[:noop] # for specs
      @ssh_host ||= build_ssh_host
    end

    def build_ssh_host
      return @identifier if ENV['TEST']

      detector = Ssh::IdentifierDetector.new(@cluster, @service, @identifier, @options)
      instance_id = detector.detect!
      instance_hostname(instance_id)
    end

    def instance_hostname(ec2_instance_id)
      begin
        resp = ec2.describe_instances(instance_ids: [ec2_instance_id])
      rescue Aws::EC2::Errors::InvalidInstanceIDNotFound => e
        # e.message: The instance ID 'i-027363802c6ff3141' does not exist
        UI.error(e.message)
        exit 1
      end
      instance = resp.reservations[0].instances[0]
      # struct Aws::EC2::Types::Instance
      # http://docs.aws.amazon.com/sdkforruby/api/Aws/EC2/Types/Instance.html
      host = if @bastion
              instance.private_ip_address
            else
              instance.public_ip_address
            end
      "#{@user}@#{host}"
    end

    # Will use Kernel.exec so that the ssh process takes over this ruby process.
    def kernel_exec(*args)
      # append the optional command that can be provided to the ssh command
      full_command = args + @options[:command]
      puts "=> #{full_command.join(' ')}".colorize(:green)
      # https://ruby-doc.org/core-2.3.1/Kernel.html#method-i-exec
      # Using 2nd form
      Kernel.exec(*full_command) unless @options[:noop]
    end

private
    def setting
      @setting ||= Setting.new(@options[:project_root])
    end

    # Returns Array of flags.
    # Example:
    #   ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
    def ssh_options
      host_key_check_options = setting.host_key_check_options
      keys_option + host_key_check_options
    end

    # Will prepend the bastion host if required
    # When bastion set
    #   ssh [options] -At [bastion_host] ssh -At [ssh_host]
    #
    # When bastion not set
    #   ssh [options] -At [ssh_host]
    #
    # Builds up ssh command to be used with Kernel.exec. Will look something like this:
    #   ssh -At ec2-user@172.16.58.3 ssh ec2-user@10.10.110.135
    # It is imporant to use an Array for the command so it gets intrepreted as if you are
    # executing it from the shell directly. For example, globs gets expanded with the
    # Array notation but not the String notation.
    #
    # ssh options:
    # -A = Enables forwarding of the authentication agent connection
    # -t = Force pseudo-terminal allocati
    def build_ssh_command
      ssh = ["ssh"] + ssh_options
      ssh += ["-At", bastion_host, "ssh"] + ssh_options if @bastion
      # ssh_host is internal ip when bastion is set
      # ssh_host is public ip when bastion is not set
      ssh += ["-At", ssh_host]
      ssh
    end

    # Private: Extracts and strips the user from the identifier.
    #
    # identifier  - Can be a variety of things: instance_id, ecs service, ecs task, etc.
    #
    # Examples
    #
    #   extract_user!("i-0f7f833131a51ce35")
    #   # => [nil, "i-0f7f833131a51ce35"]
    #
    #   extract_user!("ec2-user@i-0f7f833131a51ce35")
    #   # => ["ec2-user", "i-0f7f833131a51ce35"]
    #
    # Returns the a tuple cotaining the user and identifier
    def extract_user!(identifier)
      md = identifier.match(/(.*)@(.*)/)
      if md
        [md[1], md[2]]
      else
        [nil, identifier]<commit_msg>use JumpProxy option for bastion<commit_after>require 'colorize'

module Sonic
  class Ssh
    autoload :IdentifierDetector, 'sonic/ssh/identifier_detector'
    autoload :CliOptions, 'sonic/ssh/cli_options'

    include AwsService
    include CliOptions

    def initialize(identifier, options)
      @options = options

      @user, @identifier = extract_user!(identifier) # extracts/strips user from identifier
      # While --user option is supported at the class level, don't expose at the CLI level
      # to encourage users to use user@host notation.
      @user ||= options[:user] || setting.data["user"]

      @service = @identifier # always set service even though it's not always used as the identifier
      @cluster = options[:cluster] || setting.default_cluster(@service)
      @bastion = options[:bastion] || setting.default_bastion(@bastion)
    end

    def run
      ssh = build_ssh_command
      retry_until_success(*ssh) if @options[:retry]
      kernel_exec(*ssh) # must splat the Array here
    end

    def bastion_host
      return @identifier if @options[:noop] # for specs
      @bastion_host ||= build_bastion_host
    end

    def build_bastion_host
      host = @bastion
      host = "#{@user}@#{host}" unless host.include?('@')
      host
    end

    # used by child Classes
    def ssh_host
      return @identifier if @options[:noop] # for specs
      @ssh_host ||= build_ssh_host
    end

    def build_ssh_host
      return @identifier if ENV['TEST']

      detector = Ssh::IdentifierDetector.new(@cluster, @service, @identifier, @options)
      instance_id = detector.detect!
      instance_hostname(instance_id)
    end

    def instance_hostname(ec2_instance_id)
      begin
        resp = ec2.describe_instances(instance_ids: [ec2_instance_id])
      rescue Aws::EC2::Errors::InvalidInstanceIDNotFound => e
        # e.message: The instance ID 'i-027363802c6ff3141' does not exist
        UI.error e.message
        exit 1
      rescue Aws::Errors::NoSuchEndpointError, SocketError
        UI.error "It doesnt look like you have an internet connection. Please double check that you have an internet connection."
        exit 1
      end
      instance = resp.reservations[0].instances[0]
      # struct Aws::EC2::Types::Instance
      # http://docs.aws.amazon.com/sdkforruby/api/Aws/EC2/Types/Instance.html
      if @bastion
        instance.private_ip_address
      else
        instance.public_ip_address
      end
    end

    # Will use Kernel.exec so that the ssh process takes over this ruby process.
    def kernel_exec(*args)
      # append the optional command that can be provided to the ssh command
      full_command = args + @options[:command]
      puts "=> #{full_command.join(' ')}".colorize(:green)
      # https://ruby-doc.org/core-2.3.1/Kernel.html#method-i-exec
      # Using 2nd form
      Kernel.exec(*full_command) unless @options[:noop]
    end

private
    def setting
      @setting ||= Setting.new(@options[:project_root])
    end

    # Returns Array of flags.
    # Example:
    #   ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
    def ssh_options
      host_key_check_options = setting.host_key_check_options
      keys_option + host_key_check_options
    end

    # Will prepend the bastion host if required
    # When bastion set
    #   ssh [options] -At [bastion_host] ssh -At [ssh_host]
    #
    # When bastion not set
    #   ssh [options] -At [ssh_host]
    #
    # Builds up ssh command to be used with Kernel.exec. Will look something like this:
    #   ssh -At ec2-user@172.16.58.3 ssh ec2-user@10.10.110.135
    # It is imporant to use an Array for the command so it gets intrepreted as if you are
    # executing it from the shell directly. For example, globs gets expanded with the
    # Array notation but not the String notation.
    #
    # ssh options:
    # -A = Enables forwarding of the authentication agent connection
    # -t = Force pseudo-terminal allocati
    def build_ssh_command
      command = ["ssh", "-t"] + ssh_options
      if @bastion
        # https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Proxies_and_Jump_Hosts
        # -J xxx is -o ProxyJump=xxx
        proxy_jump = ["-J", bastion_host]
        command += proxy_jump
      end
      command += ["-t", "#{@user}@#{ssh_host}"]
    end

    # Private: Extracts and strips the user from the identifier.
    #
    # identifier  - Can be a variety of things: instance_id, ecs service, ecs task, etc.
    #
    # Examples
    #
    #   extract_user!("i-0f7f833131a51ce35")
    #   # => [nil, "i-0f7f833131a51ce35"]
    #
    #   extract_user!("ec2-user@i-0f7f833131a51ce35")
    #   # => ["ec2-user", "i-0f7f833131a51ce35"]
    #
    # Returns the a tuple cotaining the user and identifier
    def extract_user!(identifier)
      md = identifier.match(/(.*)@(.*)/)
      if md
        [md[1], md[2]]
      else
        [nil, identifier]
{'fraction_non_alphanumeric': 0.10216396171452351, 'fraction_numerical': 0.020703287557220142, 'mean_word_length': 2.7361057131752817, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 11, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '606687', 'n_tokens_mistral': 3187, 'n_tokens_neox': 2979, 'n_words': 848}