#
# Copyright (c) 2010 RightScale 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.
#

module RightAws

  class Ec2

    #-----------------------------------------------------------------
    #      Security groups
    #-----------------------------------------------------------------

    # Retrieve Security Groups information.
    # Options: By default this methods expects security group ids but if you wanna pass their names then :describe_by => :group_name option must be set.
    #
    # Accepts a list of security groups and/or a set of filters as the last parameter.
    #
    # Filters: description, group-name, ip-permission.cidr, ip-permission.from-port, ip-permission.group-name,
    # ip-permission.protocol, ip-permission.to-port, ip-permission.user-id, owner-id
    #
    #  # Amazon cloud:
    #  ec2 = Rightscale::Ec2.new(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
    #  ec2.describe_security_groups #=>
    #    [{:aws_perms=>
    #        [{:protocol=>"-1", :cidr_ips=>"0.0.0.0/0", :direction=>:egress},
    #        {:protocol=>"tcp",
    #          :cidr_ips=>"127.0.0.2/32",
    #          :direction=>:egress,
    #          :from_port=>"1111",
    #          :to_port=>"1111"},
    #        {:protocol=>"tcp",
    #          :cidr_ips=>"127.0.0.1/32",
    #          :direction=>:egress,
    #          :from_port=>"1111",
    #          :to_port=>"1111"}],
    #      :aws_group_name=>"kd-vpc-egress-test-1",
    #      :vpc_id=>"vpc-e16cf988",
    #      :aws_description=>"vpc test",
    #      :aws_owner=>"826693181925",
    #      :group_id=>"sg-b72032db"}]
    #
    #   # Describe by group ids
    #   ec2.describe_security_groups("sg-a0b85dc9", "sg-00b05d39", "sg-a1b86dc8")
    #
    #   # Describe by group names
    #   ec2.describe_security_groups("default", "default1", "kd", :describe_by => :group_name)
    #
    #  # Eucalyptus cloud:
    #  ec2 = Rightscale::Ec2.new(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, :eucalyptus => true)
    #  ec2.describe_security_groups #=>
    #    [{:aws_perms=>
    #       [{:to_port=>"65535",
    #         :group_name=>"default",
    #         :protocol=>"tcp",
    #         :owner=>"048291609141",
    #         :from_port=>"1"},
    #        {:to_port=>"65535",
    #         :group_name=>"default",
    #         :protocol=>"udp",
    #         :owner=>"048291609141",
    #         :from_port=>"1"},
    #        {:to_port=>"-1",
    #         :group_name=>"default",
    #         :protocol=>"icmp",
    #         :owner=>"048291609141",
    #         :from_port=>"-1"},
    #        {:to_port=>"22",
    #         :protocol=>"tcp",
    #         :from_port=>"22",
    #         :cidr_ip=>"0.0.0.0/0"},
    #        {:to_port=>"9997",
    #         :protocol=>"tcp",
    #         :from_port=>"9997",
    #         :cidr_ip=>"0.0.0.0/0"}],
    #      :aws_group_name=>"photo_us",
    #      :aws_description=>"default group",
    #      :aws_owner=>"826693181925"}]
    #
    #  ec2.describe_security_groups(:filters => {'ip-permission.from-port' => '22'})
    #
    # P.S. filters: http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeSecurityGroups.html
    #
    def describe_security_groups(*list_and_options)
      list, options = AwsUtils::split_items_and_params(list_and_options)
      describe_by   = options.delete(:describe_by) == :group_name ? 'GroupName' : 'GroupId'
      describe_resources_with_list_and_options('DescribeSecurityGroups', describe_by, QEc2DescribeSecurityGroupsParser, list_and_options) do |parser|
        result = []
        parser.result.each do |item|
          result_item = { :aws_owner       => item[:owner_id],
                          :aws_group_name  => item[:group_name],
                          :aws_description => item[:group_description] }
          result_item[:group_id] = item[:group_id] unless item[:group_id].right_blank?
          result_item[:vpc_id]   = item[:vpc_id]   unless item[:vpc_id].right_blank?
          aws_perms = []
          item[:ip_permissions].each do |permission|
            result_perm = {}
            result_perm[:from_port] = permission[:from_port]   unless permission[:from_port].right_blank?
            result_perm[:to_port]   = permission[:to_port]     unless permission[:to_port].right_blank?
            result_perm[:protocol]  = permission[:ip_protocol]
            result_perm[:direction] = permission[:direction]
            # IP permissions
            Array(permission[:ip_ranges]).each do |ip_range|
              perm = result_perm.dup
              # Mhhh... For Eucalyptus we somehow get used to use ":cidr_ip" instead of ":cidr_ips"...
              if @params[:eucalyptus] then  perm[:cidr_ip]  = ip_range
              else                          perm[:cidr_ips] = ip_range
              end
              aws_perms << perm
            end
            # Group permissions
            Array(permission[:groups]).each do |group|
              perm = result_perm.dup
              perm[:group_name] = group[:group_name] unless group[:group_name].right_blank?
              perm[:group_id]   = group[:group_id]   unless group[:group_id].right_blank?
              perm[:owner]      = group[:user_id]    unless group[:user_id].right_blank?
              aws_perms << perm
            end
          end
          result_item[:aws_perms] = aws_perms.uniq
          result << result_item
        end
        result
      end
    end

    def describe_security_groups_by_name(*list)
      describe_security_groups(list, :describe_by => :group_name)
    end

    # Create new Security Group. Returns +true+ or an exception.
    # Options: :vpc_id
    #
    #  ec2.create_security_group('default-1',"Default allowing SSH, HTTP, and HTTPS ingress") #=>
    #    { :group_id=>"sg-f0227599", :return=>true }
    #
    #  ec2.create_security_group('default-2',"my VPC group", :vpc_id => 'vpc-e16c0000') #=>
    #    { :group_id=>"sg-76d1c31a", :return=>true }
    #
    def create_security_group(name, description = nil, options = {})
      options = options.dup
      options[:group_name]        = name      
      options[:group_description] = description.right_blank? ? '-' : description # EC2 rejects an empty description...
      link = generate_request("CreateSecurityGroup", map_api_keys_and_values(options, :group_name, :group_description, :vpc_id))
      request_info(link, QEc2CreateSecurityGroupsParser.new(:logger => @logger))
    rescue Exception
      on_exception
    end

    # Remove Security Group. Returns +true+ or an exception.
    # Options: :group_name, :group_id
    #
    #  # Delete security group by group_id:
    #  ec2.delete_security_group('sg-90054ef9') #=> true
    #  ec2.delete_security_group(:group_id => 'sg-90054ef9') #=> true
    #
    #  # Delete security group by name (EC2 only):
    #  ec2.delete_security_group(:group_name => 'my-group']) #=> true
    #
    def delete_security_group(group_id_or_options={})
      options = group_id_or_options.is_a?(Hash) ? group_id_or_options : { :group_id => group_id_or_options } 
      link = generate_request("DeleteSecurityGroup", map_api_keys_and_values(options, :group_name, :group_id))
      request_info(link, RightBoolResponseParser.new(:logger => @logger))
    rescue Exception
      on_exception
    end

    def grant_security_group_ingress(group_id, permissions)
      modify_security_group(:grant, :ingress, group_id, permissions)
    end

    def revoke_security_group_ingress(group_id, permissions)
      modify_security_group(:revoke, :ingress, group_id, permissions)
    end

    def grant_security_group_egress(group_id, permissions)
      modify_security_group(:grant, :egress, group_id, permissions)
    end

    def revoke_security_group_egress(group_id, permissions)
      modify_security_group(:revoke, :egress, group_id, permissions)
    end

    # Modify AWS security group permissions.
    #
    #  Options:
    #    action      - :authorize (or :grant) | :revoke (or :remove)
    #    direction   - :ingress | :egress
    #    group_name  - security group name
    #    permissions - a combination of options below:
    #      # Ports:
    #      :from_port          => from port
    #      :to_port            => to port
    #      :port               => set both :from_port and to_port with the same value
    #      # Protocol
    #      :protocol           => :tcp | :udp | :icmp | -1
    #      # or (ingress)
    #      :groups             => { UserId1 => GroupId1, UserName2 => GroupId2 }
    #      :groups             => [ [ UserId1, GroupId1 ], [ UserName2 => GroupId2 ] ]
    #      # or (egress)
    #      :groups             => [ GroupId1, GroupId2 ]
    #      # CidrIp(s)
    #      :cidr_ip            => '0.0.0.0/0'
    #      :cidr_ips           => ['1.1.1.1/1', '2.2.2.2/2']
    #
    #  # CidrIP based permissions:
    #
    #  ec2.modify_security_group(:authorize, :ingress, 'sg-75d1c319',
    #                            :cidr_ip  =>  "127.0.0.0/31",
    #                            :port     => 811,
    #                            :protocol => 'tcp' ) #=> true
    #
    #  ec2.modify_security_group(:revoke, :ingress, 'sg-75d1c319',
    #                            :cidr_ips =>  ["127.0.0.1/32", "127.0.0.2/32"],
    #                            :port     => 812,
    #                            :protocol => 'tcp' ) #=> true
    #
    #  # Group based permissions:
    #
    #  ec2.modify_security_group(:authorize, :ingress, 'sg-75d1c319',
    #                            :groups   => { "586789340000" => "sg-75d1c300",
    #                                           "635201710000" => "sg-75d1c301" },
    #                            :port     => 801,
    #                            :protocol => 'tcp' ) #=> true
    #
    #  ec2.modify_security_group(:revoke, :ingress, 'sg-75d1c319',
    #                            :groups   => [[ "586789340000", "sg-75d1c300" ],
    #                                          [ "586789340000", "sg-75d1c302" ]],
    #                            :port     => 809,
    #                            :protocol => 'tcp' ) #=> true
    #
    #  # +Permissions+ can be an array of permission hashes:
    #
    #  ec2.modify_security_group(:authorize, :ingress, 'sg-75d1c319',
    #                            [{ :groups   => { "586789340000" => "sg-75d1c300",
    #                                              "635201710000" => "sg-75d1c301" },
    #                                              :port          => 803,
    #                                              :protocol      => 'tcp'},
    #                             { :cidr_ips =>  ["127.0.0.1/32", "127.0.0.2/32"],
    #                               :port     => 812,
    #                               :protocol => 'tcp' }]) #=> true
    #
    def modify_security_group(action, direction, group_id, permissions)
      hash = {}
      raise "Unknown action #{action.inspect}!"       unless [:authorize, :grant, :revoke, :remove].include?(action)
      raise "Unknown direction #{direction.inspect}!" unless [:ingress, :egress].include?(direction)
      # Remote action
      remote_action = case action
                      when :authorize, :grant  then direction == :ingress ? "AuthorizeSecurityGroupIngress" : "AuthorizeSecurityGroupEgress"
                      when :revoke,    :remove then direction == :ingress ? "RevokeSecurityGroupIngress"    : "RevokeSecurityGroupEgress"
                      end
      # Group Name
      hash["GroupId"] = group_id
      # Permissions
      permissions = [permissions] unless permissions.is_a?(Array)
      permissions.each_with_index do |permission, idx|
        pid = idx+1
        # Protocol
        hash["IpPermissions.#{pid}.IpProtocol"] = permission[:protocol]
        # Port
        unless permission[:port].right_blank?
          hash["IpPermissions.#{pid}.FromPort"] = permission[:port]
          hash["IpPermissions.#{pid}.ToPort"]   = permission[:port]
        else
          hash["IpPermissions.#{pid}.FromPort"] = permission[:from_port]
          hash["IpPermissions.#{pid}.ToPort"]   = permission[:to_port]
        end
        # Groups
        case direction
        when :ingress
          #  :groups => {UserId1 => GroupId1, ... UserIdN => GroupIdN}
          #  or (this allows using same UserId multiple times )
          #  :groups => [[UserId1, GroupId1], ... [UserIdN, GroupIdN]]
          #  or even (unset user is == current account user)
          #  :groups => [GroupId1, GroupId2, ... GroupIdN]
          #  :groups => [[UserId1, GroupId1], GroupId2, ... GroupIdN, ... [UserIdM, GroupIdM]]
          #
          index = 1
          unless permission[:group_names].right_blank?
            owner_and_groups = []
            groups_only      = []
            Array(permission[:group_names]).each do |item|
              if item.is_a?(Array) && item.size == 2
                owner_and_groups << item
              else
                groups_only << item
              end
            end
            hash.merge!(amazonize_list( ["IpPermissions.#{pid}.Groups.?.UserId", "IpPermissions.#{pid}.Groups.?.GroupName"], owner_and_groups, :index => index ))
            index += owner_and_groups.size
            groups_only = groups_only.flatten
            hash.merge!(amazonize_list( "IpPermissions.#{pid}.Groups.?.GroupName", groups_only, :index => index ))
            index += groups_only.size
          end
          unless permission[:groups].right_blank?
            owner_and_groups = []
            groups_only      = []
            Array(permission[:groups]).each do |item|
              if item.is_a?(Array) && item.size == 2
                owner_and_groups << item
              else
                groups_only << item
              end
            end
            hash.merge!(amazonize_list( ["IpPermissions.#{pid}.Groups.?.UserId", "IpPermissions.#{pid}.Groups.?.GroupId"], owner_and_groups, :index => index ))
            index += owner_and_groups.size
            groups_only = groups_only.flatten
            hash.merge!(amazonize_list( "IpPermissions.#{pid}.Groups.?.GroupId", groups_only, :index => index ))
          end
        when :egress
          #  :groups => [GroupId1, ... GroupIdN]
          hash.merge!(amazonize_list( "IpPermissions.#{pid}.Groups.?.GroupId", permission[:groups] ))
        end
        # CidrIp(s)
        cidr_ips   = permission[:cidr_ips] unless permission[:cidr_ips].right_blank?
        cidr_ips ||= permission[:cidr_ip]  unless permission[:cidr_ip].right_blank?
        hash.merge!(amazonize_list("IpPermissions.1.IpRanges.?.CidrIp", cidr_ips))
      end
      #
      link = generate_request(remote_action, hash)
      request_info(link, RightBoolResponseParser.new(:logger => @logger))
    rescue Exception
      on_exception
    end

    #-----------------------------------------------------------------
    #   Eucalyptus
    #-----------------------------------------------------------------

    # Edit AWS/Eucaliptus security group permissions.
    #
    #  Options:
    #    action      - :authorize (or :grant) | :revoke (or :remove)
    #    group_name  - security group name
    #    permissions - a combination of options below:
    #      :source_group_owner => UserId
    #      :source_group       => GroupName
    #      :from_port          => from port
    #      :to_port            => to port
    #      :port               => set both :from_port and to_port with the same value
    #      :protocol           => :tcp | :udp | :icmp
    #      :cidr_ip            => '0.0.0.0/0'
    #
    #  ec2.edit_security_group( :grant,
    #                           'kd-sg-test',
    #                           :source_group       => "sketchy",
    #                           :source_group_owner => "600000000006",
    #                           :protocol           => 'tcp',
    #                           :port               => '80',
    #                           :cidr_ip            => '127.0.0.1/32') #=> true
    #
    # P.S. This method is deprecated for AWS and but still good for Eucaliptus clouds.
    # Use +modify_security_group_ingress+ method for AWS clouds.
    #
    def edit_security_group(action, group_name, params)
      hash = {}
      case action
      when :authorize, :grant then action = "AuthorizeSecurityGroupIngress"
      when :revoke, :remove   then action = "RevokeSecurityGroupIngress"
      else raise "Unknown action #{action.inspect}!"
      end
      hash['GroupName']               = group_name
      hash['SourceSecurityGroupName'] = params[:source_group] unless params[:source_group].right_blank?
      hash['IpProtocol']              = params[:protocol]     unless params[:protocol].right_blank?
      unless params[:source_group_owner].right_blank?
        # Do remove dashes only if the source owner is in format of "7011-0219-8268"
        source_group_owner = params[:source_group_owner].to_s
        source_group_owner.gsub!(/-/,'') if source_group_owner[/^\d{4}-\d{4}-\d{4}$/]
        hash['SourceSecurityGroupOwnerId'] = source_group_owner
      end
      unless params[:port].right_blank?
        hash['FromPort'] = params[:port]
        hash['ToPort']   = params[:port]
      end
      hash['FromPort']   = params[:from_port] unless params[:from_port].right_blank?
      hash['ToPort']     = params[:to_port]   unless params[:to_port].right_blank?
      hash['CidrIp']     = params[:cidr_ip]   unless params[:cidr_ip].right_blank?
      #
      link = generate_request(action, hash)
      request_info(link, RightBoolResponseParser.new(:logger => @logger))
    rescue Exception
      on_exception
    end

    # Authorize named ingress for security group. Allows instances that are member of someone
    # else's security group to open connections to instances in my group.
    #
    #  ec2.authorize_security_group_named_ingress('my_awesome_group', '7011-0219-8268', 'their_group_name') #=> true
    #
    def authorize_security_group_named_ingress(name, owner, group)
      edit_security_group( :authorize, name, :source_group_owner => owner, :source_group => group)
    end

    # Revoke named ingress for security group.
    #
    #  ec2.revoke_security_group_named_ingress('my_awesome_group', aws_user_id, 'another_group_name') #=> true
    #
    def revoke_security_group_named_ingress(name, owner, group)
      edit_security_group( :revoke, name, :source_group_owner => owner, :source_group => group)
    end

    # Add permission to a security group. Returns +true+ or an exception. +protocol+ is one of :'tcp'|'udp'|'icmp'.
    #
    #  ec2.authorize_security_group_IP_ingress('my_awesome_group', 80, 82, 'udp', '192.168.1.0/8') #=> true
    #  ec2.authorize_security_group_IP_ingress('my_awesome_group', -1, -1, 'icmp') #=> true
    #
    def authorize_security_group_IP_ingress(name, from_port, to_port, protocol='tcp', cidr_ip='0.0.0.0/0')
      edit_security_group( :authorize, name, :from_port => from_port, :to_port => to_port, :protocol => protocol, :cidr_ip => cidr_ip )
    end

    # Remove permission from a security group. Returns +true+ or an exception. +protocol+ is one of :'tcp'|'udp'|'icmp' ('tcp' is default).
    #
    #  ec2.revoke_security_group_IP_ingress('my_awesome_group', 80, 82, 'udp', '192.168.1.0/8') #=> true
    #
    def revoke_security_group_IP_ingress(name, from_port, to_port, protocol='tcp', cidr_ip='0.0.0.0/0')
      edit_security_group( :revoke, name, :from_port => from_port, :to_port => to_port, :protocol => protocol, :cidr_ip => cidr_ip )
    end

    #-----------------------------------------------------------------
    #      PARSERS: Security Groups
    #-----------------------------------------------------------------

    class QEc2CreateSecurityGroupsParser < RightAWSParser #:nodoc:
      def tagend(name)
        case name
        when 'groupId' then @result[:group_id] = @text
        when 'return'  then @result[:return]   = @text == 'true'
        end
      end
      def reset
        @result = {}
      end
    end

    class QEc2DescribeSecurityGroupsParser < RightAWSParser #:nodoc:
      def tagstart(name, attributes)
        if name == 'item'
          case full_tag_name
          when %r{securityGroupInfo/item$}                  then @item    = { :ip_permissions => [] }
          when %r{ipPermissions/item$}                      then @ip_perm = { :groups => [], :ip_ranges => [], :direction => :ingress }
          when %r{ipPermissionsEgress/item$}                then @ip_perm = { :groups => [], :ip_ranges => [], :direction => :egress  }
          when %r{ipPermissions(Egress)?/item/groups/item$} then @group   = {}
          end
        end
      end
      def tagend(name)
        case name
        when 'ownerId'          then @item[:owner_id]          = @text
        when 'groupDescription' then @item[:group_description] = @text
        when 'vpcId'            then @item[:vpc_id]            = @text
        else
          case full_tag_name
          when %r{securityGroupInfo/item/groupName$}                  then @item[:group_name]      = @text
          when %r{securityGroupInfo/item/groupId$}                    then @item[:group_id]        = @text
          # ipPermission[Egress]
          when %r{ipPermissions(Egress)?/item/ipProtocol$}            then @ip_perm[:ip_protocol]  = @text
          when %r{ipPermissions(Egress)?/item/fromPort$}              then @ip_perm[:from_port]    = @text
          when %r{ipPermissions(Egress)?/item/toPort$}                then @ip_perm[:to_port]      = @text
          when %r{ipPermissions(Egress)?/item/ipRanges/item/cidrIp$}  then @ip_perm[:ip_ranges]   << @text
          # ipPermissions[Egress]/Groups
          when %r{ipPermissions(Egress)?/item/groups/item/groupName$} then @group[:group_name]     = @text
          when %r{ipPermissions(Egress)?/item/groups/item/groupId$}   then @group[:group_id]       = @text
          when %r{ipPermissions(Egress)?/item/groups/item/userId$}    then @group[:user_id]        = @text
          # Sets
          when %r{ipPermissions(Egress)?/item/groups/item$}           then @ip_perm[:groups]      << @group
          when %r{ipPermissions/item$}                                then @item[:ip_permissions] << @ip_perm
          when %r{ipPermissionsEgress/item$}                          then @item[:ip_permissions] << @ip_perm
          when %r{securityGroupInfo/item$}                            then @result                << @item
          end
        end
      end
      def reset
        @result = []
      end
    end
    
  end
end