
#
# Author:: Seth Chisamore (<schisamo@chef.io>)
# Copyright:: Copyright (c) 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/knife"

class Chef
  class Knife
    module Ec2Base
      # All valid platforms
      VALID_PLATFORMS ||= %w{windows ubuntu debian centos fedora rhel nginx turnkey jumpbox coreos cisco amazon nessus}.freeze

      # @todo Would prefer to do this in a rational way, but can't be done b/c of Mixlib::CLI's design :(
      def self.included(includer)
        includer.class_eval do

          deps do
            require "aws-sdk-ec2"
            require "chef/json_compat"
            require "chef/util/path_helper"
          end

          option :aws_credential_file,
            long: "--aws-credential-file FILE",
            description: "File containing AWS credentials as used by the AWS Command Line Interface."

          option :aws_config_file,
            long: "--aws-config-file FILE",
            description: "File containing AWS configurations as used by the AWS Command Line Interface."

          option :aws_profile,
            long: "--aws-profile PROFILE",
            description: "AWS profile, from AWS credential file and AWS config file, to use",
            default: "default"

          option :aws_access_key_id,
            short: "-A ID",
            long: "--aws-access-key-id KEY",
            description: "Your AWS Access Key ID"

          option :aws_secret_access_key,
            short: "-K SECRET",
            long: "--aws-secret-access-key SECRET",
            description: "Your AWS API Secret Access Key"

          option :aws_session_token,
            long: "--aws-session-token TOKEN",
            description: "Your AWS Session Token, for use with AWS STS Federation or Session Tokens"

          option :region,
            long: "--region REGION",
            description: "Your AWS region"

          option :use_iam_profile,
            long: "--use-iam-profile",
            description: "Use IAM profile assigned to current machine",
            boolean: true,
            default: false
        end
      end

      def connection_string
        conn = {}
        conn[:region] = config[:region] || "us-east-1"
        Chef::Log.debug "Using AWS region #{conn[:region]}"
        conn[:credentials] =
          if config[:use_iam_profile]
            Chef::Log.debug "Using iam profile for authentication as use_iam_profile set"
            Aws::InstanceProfileCredentials.new
          else
            Chef::Log.debug "Setting up AWS connection using aws_access_key_id: #{mask(config[:aws_access_key_id])} aws_secret_access_key: #{mask(config[:aws_secret_access_key])} aws_session_token: #{mask(config[:aws_session_token])}"

            Aws::Credentials.new(config[:aws_access_key_id], config[:aws_secret_access_key], config[:aws_session_token])
          end
        conn
      end

      # @return [Aws::EC2::Client]
      def ec2_connection
        @ec2_connection ||= Aws::EC2::Client.new(connection_string)
      end

      def vpc_mode?
        !!config[:subnet_id]
      end

      def fetch_ami(image_id)
        return nil unless image_id

        ec2_connection.describe_images({
          image_ids: [image_id],
        }).images.first
      end

      def fetch_ec2_instance(instance_id)
        instance = ec2_connection.describe_instances({
          instance_ids: [
            instance_id,
          ],
        }).reservations[0]
        normalize_server_data(server_hashes(instance))
      end

      def fetch_network_interfaces(nic_id)
        ec2_connection.describe_network_interfaces({
          network_interface_ids: [nic_id],
        }).network_interfaces[0]
      end

      def fetch_password_data(server_id)
        ec2_connection.get_password_data({
          instance_id: server_id,
        })
      end

      # @return [String]
      def fetch_region
        ec2_connection.instance_variable_get(:@config).region
      end

      def fetch_subnet(subnet_id)
        ec2_connection.describe_subnets({
          subnet_ids: [subnet_id],
        }).subnets[0]
      end

      # @return [Hash]
      def server_hashes(server_obj)
        server_data = {}
        %w{ebs_optimized image_id instance_id instance_type key_name platform public_dns_name public_ip_address private_dns_name private_ip_address root_device_type}.each do |id|
          server_data[id] = server_obj.instances[0].send(id)
        end
        server_data["availability_zone"] = server_obj.instances[0].placement.availability_zone
        server_data["groups"] = server_obj.groups.map(&:group_name) unless vpc_mode?
        server_data["iam_instance_profile"] = ( server_obj.instances[0].iam_instance_profile.nil? ? nil : server_obj.instances[0].iam_instance_profile.arn[%r{instance-profile/(.*)}] )
        server_data["id"] = server_data["instance_id"]

        tags = server_obj.instances[0].tags.map(&:value)
        server_data["name"] = find_name_tag(server_obj.instances[0].tags)
        server_data["placement_group"] = server_obj.instances[0].placement.group_name
        server_data["security_groups"] = server_obj.instances[0].security_groups.map(&:group_name)
        server_data["security_group_ids"] = server_obj.instances[0].security_groups.map(&:group_id)
        server_data["state"] = server_obj.instances[0].state.name
        server_data["subnet_id"] = server_obj.instances[0].network_interfaces[0].subnet_id if vpc_mode?
        server_data["source_dest_check"] = server_obj.instances[0].network_interfaces[0].source_dest_check if vpc_mode?
        server_data["tags"] = tags
        server_data["tenancy"] = server_obj.instances[0].placement.tenancy
        server_data["volume_id"] = server_obj.instances[0].block_device_mappings[0]&.ebs&.volume_id
        server_data["block_device_mappings"] = server_obj.instances[0].block_device_mappings
        server_data
      end

      # @return [Struct]
      def normalize_server_data(server_hashes)
        require "ostruct" unless defined?(OpenStruct)
        OpenStruct.new(server_hashes)
      end

      def msg_pair(label, value, color = :cyan)
        if value && !value.to_s.empty?
          ui.info("#{ui.color(label, color)}: #{value}")
        end
      end

      def ami
        @ami ||= fetch_ami(config[:image])
      end

      # Name tag value return.
      # @return [String]
      def find_name_tag(tags)
        name_tag = tags.find do |tag|
          key_value = tag.respond_to?(:key) ? tag.key : (tag[:key] || tag["key"])
          key_value == "Name"
        end
        if name_tag
          if name_tag.respond_to?(:value)
            name_tag.value
          else
            name_tag[:value] || name_tag["value"]
          end
        end
      end

      # Platform value return for Windows AMIs; otherwise, it is blank.
      # @return [Boolean]
      def is_image_windows?
        ami && ami.platform == "windows"
      end

      # validate the config options that were passed since some of them cannot be used together
      # also validate the aws configuration file contents if present
      def validate_aws_config!(keys = %i{aws_access_key_id aws_secret_access_key})
        errors = [] # track all errors so we report on all of them

        validate_aws_config_file! if config[:aws_config_file]
        unless config[:use_iam_profile] # skip config file / key validation if we're using iam profile
          # validate the creds file if:
          #   aws keys have not been passed in config / CLI and the default cred file location does exist
          #   OR
          #   the user passed aws_credential_file
          if (config.keys & %i{aws_access_key_id aws_secret_access_key}).empty? && aws_cred_file_location ||
              config[:aws_credential_file]

            unless (config.keys & %i{aws_access_key_id aws_secret_access_key}).empty?
              errors << "Either provide a credentials file or the access key and secret keys but not both."
            end

            validate_aws_credential_file!
          end

          keys.each do |k|
            pretty_key = k.to_s.tr("_", " ").gsub(/\w+/) { |w| (w =~ /(ssh)|(aws)/i) ? w.upcase : w.capitalize }
            if config[k].nil?
              errors << "You did not provide a valid '#{pretty_key}' value."
            end
          end

          if errors.each { |e| ui.error(e) }.any?
            exit 1
          end
        end
      end
    end

    # the path to the aws credentials file.
    # if passed via cli config use that
    # if default location exists on disk fallback to that
    # @return [String, nil] location to aws credentials file or nil if none exists
    def aws_cred_file_location
      @cred_file ||= if !config[:aws_credential_file].nil?
                       config[:aws_credential_file]
                     else
                       Chef::Util::PathHelper.home(".aws", "credentials") if ::File.exist?(Chef::Util::PathHelper.home(".aws", "credentials"))
                     end
    end

    # @return [String]
    def iam_name_from_profile(profile)
      # The IAM profile object only contains the name as part of the arn
      if profile && profile.key?("arn")
        name = profile["arn"].split("/")[-1]
      end
      name || ""
    end

    def ini_parse(file)
      current_section = {}
      map = {}
      file.each_line do |line|
        line = line.split(/^|\s;/).first # remove comments
        section = line.match(/^\s*\[([^\[\]]+)\]\s*$/) unless line.nil?
        if section
          current_section = section[1]
        elsif current_section
          item = line.match(/^\s*(.+?)\s*=\s*(.+?)\s*$/) unless line.nil?
          if item
            map[current_section] ||= {}
            map[current_section][item[1]] = item[2]
          end
        end
      end
      map
    end

    # Get the platform from server name
    # @return [String]
    def find_server_platform(server_name)
      get_platform = VALID_PLATFORMS.select { |name| server_name.downcase.include?(name) }
      get_platform.first
    end

    # Custom Warning
    def custom_warnings!
      unless config[:region]
        ui.warn "No region was specified in knife.rb/config.rb or as an argument. The default region, us-east-1, will be used:"
      end
    end

    private

    # validate the contents of the aws configuration file
    # @return [void]
    def validate_aws_config_file!
      config_file = config[:aws_config_file]
      Chef::Log.debug "Using AWS config file at #{config_file}"

      raise ArgumentError, "The provided --aws_config_file (#{config_file}) cannot be found on disk." unless File.exist?(config_file)

      aws_config = ini_parse(File.read(config_file))
      profile_key = config[:aws_profile]
      profile_key = "profile #{profile_key}" if profile_key != "default"

      unless aws_config.values.empty?
        if aws_config[profile_key]
          config[:region] = aws_config[profile_key]["region"]
        else
          raise ArgumentError, "The provided --aws-profile '#{profile_key}' is invalid."
        end
      end
    end

    # validate the contents of the aws credentials file
    # @return [void]
    def validate_aws_credential_file!
      Chef::Log.debug "Using AWS credential file at #{aws_cred_file_location}"
      raise ArgumentError, "The provided --aws_credential_file (#{aws_cred_file_location}) cannot be found on disk." unless File.exist?(aws_cred_file_location)

      # File format:
      # AWSAccessKeyId=somethingsomethingdarkside
      # AWSSecretKey=somethingsomethingcomplete
      #               OR
      # [default]
      # aws_access_key_id = somethingsomethingdarkside
      # aws_secret_access_key = somethingsomethingdarkside
      aws_creds = ini_parse(File.read(aws_cred_file_location))
      profile = config[:aws_profile]
      Chef::Log.debug "Using AWS profile #{profile}"
      entries = if aws_creds.values.first.key?("AWSAccessKeyId")
                  aws_creds.values.first
                else
                  aws_creds[profile]
                end

      if entries
        config[:aws_access_key_id] = entries["AWSAccessKeyId"] || entries["aws_access_key_id"]
        config[:aws_secret_access_key] = entries["AWSSecretKey"] || entries["aws_secret_access_key"]
        config[:aws_session_token] = entries["AWSSessionToken"] || entries["aws_session_token"]
      else
        raise ArgumentError, "The provided --aws-profile '#{profile}' is invalid. Does the credential file at '#{aws_cred_file_location}' contain this profile?"
      end
    end

    # Mask the given string with char `X`
    # Discard the chars based on from value
    def mask(key, from = 4)
      str = key.dup
      if str && str.length > from
        str[from...str.length] = "X" * (str[from...str.length].length)
      end

      str
    end
  end
end
