#!/usr/bin/python
# Copyright (c) 2017-present Alibaba Group Holding Limited. He Guimin <heguimin36@163.com.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see http://www.gnu.org/licenses/.

ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['deprecated'],
                    'supported_by': 'community'}

DOCUMENTATION = '''
---
module: alicloud_security_group
version_added: "1.0.9"
short_description: Create, Query or Delete Security Group.
description:
  - Create and Delete Security Group, and it contains security group rules management.
deprecated:
  removed_in: "1.5.0"
  why: Alibaba Cloud module name prefix "ali" will be more concise.
  alternative: Use M(ali_security_group) instead.
options:
  state:
    description:
      - Create, delete a security group
    default: 'present'
    choices: ['present', 'absent']
  group_name:
    description:
      - Name of the security group, which is a string of 2 to 128 Chinese or English characters. It must begin with an
        uppercase/lowercase letter or a Chinese character and can contain numerals, "_", "." or "-".
        It cannot begin with http:// or https://.
    aliases: ['name']
  description:
    description:
      - Description of the security group, which is a string of 2 to 256 characters.
      - It cannot begin with http:// or https://.
  vpc_id:
    description:
      - ID of the VPC to which the security group belongs.
  rules:
    description:
      - List of hash/dictionaries firewall inbound rules to enforce in this group.
    suboptions:
        ip_protocol:
          description:
            - IP protocol
          required: true
          choices: ["tcp", "udp", "icmp", "gre", "all"]
          aliases: ['proto']
        port_range:
          description:
            - The range of port numbers. Tcp and udp's valid range is 1 to 65535, and other protocol's valid value is -1/-1.
          required: true
        source_group_id:
          description:
            - The source security group id.
          aliases: ['group_id']
        source_group_owner_id:
          description:
            - The source security group owner id.
          aliases: ['group_owner_id']
        source_cidr_ip:
          description:
            - The source IP address range
          aliases: ['cidr_ip']
        policy:
          description:
            - Authorization policy
          default: "accept"
          choices: ["accept", "drop"]
        priority:
          description:
            - Authorization policy priority
          default: 1
          choices: ["1~100"]
        nic_type:
          description:
            - Network type
          default: "internet"
          choices: ["internet", "intranet"]
  rules_egress:
    description:
      - List of hash/dictionaries firewall outbound rules to enforce in this group.
        Keys allowed are:ip_protocol, port_range, dest_group_id, dest_group_owner_id, dest_cidr_ip, policy, priority,nic_type.
        And these keys's attribution same as rules keys.
    suboptions:
        ip_protocol:
          description:
            - IP protocol
          required: true
          choices: ["tcp", "udp", "icmp", "gre", "all"]
          aliases: ['proto']
        port_range:
          description:
            - The range of port numbers. Tcp and udp's valid range is 1 to 65535, and other protocol's valid value is "-1/-1".
          required: true
        dest_group_id:
          description:
            - The destination security group id.
          aliases: ['group_id']
        dest_group_owner_id:
          description:
            - The destination security group owner id.
          aliases: ['group_owner_id']
        dest_cidr_ip:
          description:
            - The destination IP address range
          aliases: ['cidr_ip']
        policy:
          description:
            - Authorization policy
          default: "accept"
          choices: ["accept", "drop"]
        priority:
          description:
            - Authorization policy priority
          default: 1
          choices: ["1~100"]
        nic_type:
          description:
            - Network type
          default: "internet"
          choices: ["internet", "intranet"]
  group_id:
    description:
      - Security group ID. It is required when deleting or querying security group or performing rules authorization.
requirements:
    - "python >= 2.6"
    - "footmark >= 1.1.16"
extends_documentation_fragment:
    - alicloud
author:
  - "He Guimin (@xiaozhu36)"
'''

EXAMPLES = '''
#
# Provisioning new Security Group
#

# Basic provisioning example to create security group
- name: create security group
  hosts: localhost
  connection: local
  vars:
    alicloud_access_key: xxxxxxxxxx
    alicloud_secret_key: xxxxxxxxxx
    alicloud_region: cn-shenzhen
  tasks:
    - name: create security grp
      alicloud_security_group:
        alicloud_access_key: '{{ alicloud_access_key }}'
        alicloud_secret_key: '{{ alicloud_secret_key }}'
        alicloud_region: '{{ alicloud_region }}'
        group_name: 'AliyunSG'


# Basic provisioning example authorize security group
- name: authorize security grp
  hosts: localhost
  connection: local
  vars:
    alicloud_access_key: xxxxxxxxxx
    alicloud_secret_key: xxxxxxxxxx
    alicloud_region: cn-shenzhen
  tasks:
    - name: authorize security group
      alicloud_security_group:
        alicloud_access_key: '{{ alicloud_access_key }}'
        alicloud_secret_key: '{{ alicloud_secret_key }}'
        group_id: xxxxxxxxxx
        alicloud_region: '{{ alicloud_region }}'
        rules:
          - ip_protocol: tcp
            port_range: 1/122
            source_cidr_ip: '10.159.6.18/12'
        rules_egress:
          - proto: all
            port_range: -1/-1
            dest_group_id: xxxxxxxxxx
            nic_type: intranet


# Provisioning example create and authorize security group
- name: create and authorize security group
  hosts: localhost
  connection: local
  vars:
    alicloud_access_key: xxxxxxxxxx
    alicloud_secret_key: xxxxxxxxxx
    alicloud_region: cn-shenzhen
  tasks:
    - name: create and authorize security grp
      alicloud_security_group:
        alicloud_access_key: '{{ alicloud_access_key }}'
        alicloud_secret_key: '{{ alicloud_secret_key }}'
        group_name: 'AliyunSG'
        description: 'an example ECS group'
        alicloud_region: '{{ alicloud_region }}'
        rules:
          - ip_protocol: tcp
            port_range: 1/122
            source_cidr_ip: '10.159.6.18/12'
            priority: 10
            policy: drop
            nic_type: intranet
        rules_egress:
          - proto: all
            port_range: -1/-1
            dest_group_id: xxxxxxxxxx
            group_owner_id: xxxxxxxxxx
            priority: 10
            policy: accept
            nic_type: intranet


# Provisioning example to delete security group
- name: delete security grp
  hosts: localhost
  connection: local
  vars:
    alicloud_access_key: xxxxxxxxxx
    alicloud_secret_key: xxxxxxxxxx
    alicloud_region: us-west-1
    group_ids:
     - xxxxxxxxxx
    state: absent
  tasks:
    - name: delete security grp
      alicloud_security_group:
        alicloud_access_key: '{{ alicloud_access_key }}'
        alicloud_secret_key: '{{ alicloud_secret_key }}'
        alicloud_region: '{{ alicloud_region }}'
        group_ids: '{{ group_ids }}'
        state: '{{ state }}'

'''

RETURN = '''
group_id:
    description: Deprecated from version 1.3.1 and replaced by 'id'.
    returned:
    type:
    sample:
id:
    description: ID of the security group.
    returned: when present
    type: string
    sample: "sd-safhi3gsv"
name:
    description: Name of the security group.
    returned: when present
    type: string
    sample: "new-group"
group:
    description: Details about the security group that was created
    returned: when present
    type: dict
    sample: {
        "description": "travis-ansible-instance",
        "id": "sg-2ze1hhyn7tac4p85gh13",
        "name": "travis-ansible-instance",
        "region_id": "cn-beijing",
        "rules": [
            {
                "create_time": "2017-06-19T02:43:29Z",
                "description": "",
                "dest_cidr_ip": "",
                "dest_group_id": "",
                "dest_group_name": "",
                "dest_group_owner_account": "",
                "direction": "ingress",
                "ip_protocol": "TCP",
                "nic_type": "internet",
                "policy": "Accept",
                "port_range": "80/86",
                "priority": 1,
                "source_cidr_ip": "192.168.0.54/32",
                "source_group_id": "",
                "source_group_name": "",
                "source_group_owner_account": ""
            },
            {
                "create_time": "2017-06-19T02:43:30Z",
                "description": "",
                "dest_cidr_ip": "47.89.23.33/32",
                "dest_group_id": "",
                "dest_group_name": "",
                "dest_group_owner_account": "",
                "direction": "egress",
                "ip_protocol": "TCP",
                "nic_type": "internet",
                "policy": "Accept",
                "port_range": "8080/8085",
                "priority": 1,
                "source_cidr_ip": "",
                "source_group_id": "",
                "source_group_name": "",
                "source_group_owner_account": ""
            }
        ],
        "tags": {},
        "vpc_id": ""
    }
vpc_id:
    description: ID of the VPC to which the security group belongs
    returned: when present
    type: string
    sample: "vpc-snif3g3iv"
'''

import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.alicloud_ecs import ecs_argument_spec, ecs_connect


try:
    from footmark.exception import ECSResponseError

    HAS_FOOTMARK = True
except ImportError:
    HAS_FOOTMARK = False


def authorize_security_group(module, ecs, group_id, inbound_rules, outbound_rules):
    """
    authorize security group in ecs
    :param module: Ansible module object
    :param ecs: authenticated ecs connection object
    :param group_id: Security Group Id for authorization
    :param inbound_rules: Inbound rules for authorization
    :param outbound_rules: Outbound rules for authorization
    :param add_to_fail_result: message to add to failed message at the beginning, if two tasks are performed
    :return: Returns changed state, security group id and custom message.
    """

    try:
        changed = False
        inbound_failed_rules, outbound_failed_rules, result = ecs.authorize_security_group(
            group_id, inbound_rules, outbound_rules)

        if 'error' in (''.join(str(result))).lower():
            module.fail_json(changed=changed, msg="Authorizing SecurityGroup is failed, error: %s ; group_id: %s ; "
                                                  "failed inbound rules: %s ; failed outbound rules: %s."
                                                  % (str(result), group_id, inbound_failed_rules, outbound_failed_rules))
        changed = True

    except ECSResponseError as e:
        module.fail_json(msg='Unable to authorize security group, error: {0}'.format(e))

    return changed


def validate_format_sg_rules(module, inbound_rules=None, outbound_rules=None):
    """
    Validate and format security group for inbound and outbound rules
    :param module: Ansible module object
    :param inbound_rules: Inbound rules for authorization to validate and format
    :param outbound_rules: Outbound rules for authorization to validate and format
    :return:
    """
    # aliases for rule
    ip_protocol_aliases = ('ip_protocol', 'proto')
    inbound_cidr_ip_aliases = ('source_cidr_ip', 'cidr_ip')
    outbound_cidr_ip_aliases = ('dest_cidr_ip', 'cidr_ip')
    inbound_group_id_aliases = ('source_group_id', 'group_id')
    outbound_group_id_aliases = ('dest_group_id', 'group_id')
    inbound_group_owner_aliases = ('source_group_owner_id', 'group_owner_id')
    outbound_group_owner_aliases = ('dest_group_owner_id', 'group_owner_id')

    cidr_ip_aliases = {
        "inbound": inbound_cidr_ip_aliases,
        "outbound": outbound_cidr_ip_aliases,
    }

    group_id_aliases = {
        "inbound": inbound_group_id_aliases,
        "outbound": outbound_group_id_aliases,
    }

    group_owner_aliases = {
        "inbound": inbound_group_owner_aliases,
        "outbound": outbound_group_owner_aliases,
    }

    COMMON_VALID_PARAMS = ('proto', 'ip_protocol', 'cidr_ip', 'group_id', 'group_owner_id',
                           'nic_type', 'policy', 'priority', 'port_range')
    INBOUND_VALID_PARAMS = ('source_cidr_ip', 'source_group_id', 'source_group_owner_id')
    OUTBOUND_VALID_PARAMS = ('dest_cidr_ip', 'dest_group_id', 'dest_group_owner_id')

    rule_types = []

    rule_choice = {
        "inbound": inbound_rules,
        "outbound": outbound_rules,
    }
    valid_params = {
        "inbound": INBOUND_VALID_PARAMS,
        "outbound": OUTBOUND_VALID_PARAMS,
    }

    if inbound_rules:
        rule_types.append('inbound')

    if outbound_rules:
        rule_types.append('outbound')

    for rule_type in rule_types:

        rules = rule_choice.get(rule_type)
        total_rules = 0
        if rules:
            total_rules = len(rules)

        if total_rules != 0:

            for rule in rules:

                if not isinstance(rule, dict):
                    module.fail_json(msg='Invalid rule parameter type [%s].' % type(rule))

                for k in rule:
                    if k not in COMMON_VALID_PARAMS and k not in valid_params.get(rule_type):
                        module.fail_json(msg='Invalid rule parameter \'{}\''.format(k))

                ip_protocol = get_alias_value(rule, ip_protocol_aliases)
                if ip_protocol is None:
                    module.fail_json(msg="Ip Protocol required for rule authorization")

                port_range = get_alias_value(rule, ['port_range'])
                if port_range is None:
                    module.fail_json(msg="Port range is required for rule authorization")

                # verifying whether group_id is provided and cidr_ip is not, so nic_type should be set to intranet
                cidr_ip = get_alias_value(rule, cidr_ip_aliases.get(rule_type))
                if cidr_ip is None:
                    if get_alias_value(rule, group_id_aliases.get(rule_type)) is not None:
                        if 'nic_type' in rule:
                            if not rule['nic_type'] == "intranet":
                                module.fail_json(msg="In mutual security group authorization (namely, "
                                                     "GroupId is specified, while CidrIp is not specified), "
                                                     "you must specify the nic_type as intranet")
                        else:
                            module.fail_json(msg="In mutual security group authorization (namely, "
                                                 "GroupId is specified, while CidrIp is not specified), "
                                                 "you must specify the nic_type as intranet")

                # format rules to return for authorization
                formatted_rule = {}

                formatted_rule['ip_protocol'] = ip_protocol
                formatted_rule['port_range'] = port_range

                if cidr_ip:
                    formatted_rule['cidr_ip'] = cidr_ip

                group_id = get_alias_value(rule, group_id_aliases.get(rule_type))
                if group_id:
                    formatted_rule['group_id'] = group_id

                group_owner_id = get_alias_value(rule, group_owner_aliases.get(rule_type))
                if group_owner_id:
                    formatted_rule['group_owner_id'] = group_owner_id

                if 'nic_type' in rule:
                    if rule['nic_type']:
                        formatted_rule['nic_type'] = rule['nic_type']

                if 'policy' in rule:
                    if rule['policy']:
                        formatted_rule['policy'] = rule['policy']

                if 'priority' in rule:
                    if rule['priority']:
                        formatted_rule['priority'] = rule['priority']

                rule.clear()
                rule.update(formatted_rule)


def get_alias_value(dictionary, aliases):
    """
    Get alias or key value from a dictionary
    :param dictionary: a dictionary to check in for keys/aliases
    :param aliases: list of aliases to find in dictionary to retrieve value
    :return: returns value of found alias else None
    """

    if (dictionary and aliases) is not None:
        for alias in aliases:
            if alias in dictionary:
                return dictionary[alias]
        return None
    else:
        return None


def get_group_basic(group):
    """
    Parse security group basic information.
    returns it as a dictionary
    """
    return {'id': group.id, 'name': group.name, 'vpc_id': group.vpc_id}


def get_group_detail(group):
    """
    Parse security group detail information.
    returns it as a dictionary
    """
    return {'id': group.id, 'name': group.name, 'description': group.description, 'region_id': group.region_id,
            'tags': group.tags, 'vpc_id': group.vpc_id, 'rules': group.rules}


def main():
    argument_spec = ecs_argument_spec()
    argument_spec.update(dict(
        state=dict(default='present', type='str', choices=['present', 'absent']),
        group_name=dict(type='str', required=False, aliases=['name']),
        description=dict(type='str', required=False),
        vpc_id=dict(type='str'),
        group_tags=dict(type='list', aliases=['tags']),
        rules=dict(type='list'),
        rules_egress=dict(type='list'),
        group_id=dict(type='str')
    ))

    module = AnsibleModule(argument_spec=argument_spec)

    if HAS_FOOTMARK is False:
        module.fail_json(msg='footmark required for the module alicloud_security_group.')

    ecs = ecs_connect(module)

    state = module.params['state']
    group_name = module.params['group_name']
    description = module.params['description']
    vpc_id = module.params['vpc_id']
    group_id = module.params['group_id']
    group_tags = module.params['group_tags']

    changed = False
    group = None

    try:
        if group_id:
            security_groups = ecs.get_all_security_groups(group_ids=[group_id], vpc_id=vpc_id, name=group_name)
        else:
            security_groups = ecs.get_all_security_groups(vpc_id=vpc_id, name=group_name)
    except ECSResponseError as e:
        module.fail_json(msg='Error in get_all_security_groups: %s' % str(e))

    group_ids = []
    if security_groups and len(security_groups) > 0:
        if group_id or len(security_groups) == 1:
            group = security_groups[0]
        else:
            for cur in security_groups:
                group_ids.append(cur.id)
            module.fail_json(msg="There are several security group in our record based on name {0} or vpc {1}: {2}. "
                                 "Please specified one using 'id' and try again.".format(group_name, vpc_id, group_ids))

    if state == 'absent':
        if not group:
            module.fail_json(changed=changed, msg="Please specify a security group by using 'group_id' or 'group_name' "
                                                  "and 'vpc_id' before deleting a security group.")
        try:
            module.exit_json(changed=group.delete())
        except ECSResponseError as e:
            module.fail_json(msg="Deleting security group {0} is failed. Error: {1}".format(group.id, e))

    if not group:
        try:
            client_token = "Ansible-Alicloud-%s-%s" % (hash(str(module.params)), str(time.time()))
            group = ecs.create_security_group(group_name=group_name, description=description, vpc_id=vpc_id,
                                              group_tags=group_tags, client_token=client_token)
            changed = True

        except ECSResponseError as e:
            module.fail_json(changed=changed, msg='Creating a security group is failed. Error: {0}'.format(e))

    # validating rules if provided
    total_rules_count = 0
    inbound_rules = module.params['rules']
    if inbound_rules:
        total_rules_count = len(inbound_rules)

    outbound_rules = module.params['rules_egress']
    if outbound_rules:
        total_rules_count += len(outbound_rules)

    if total_rules_count > 100:
        module.fail_json(msg='more than 100 rules for authorization are not allowed')

    validate_format_sg_rules(module, inbound_rules, outbound_rules)

    if inbound_rules or outbound_rules:
        changed = authorize_security_group(module, ecs, group_id=group.id, inbound_rules=inbound_rules, outbound_rules=outbound_rules)

    module.exit_json(changed=changed, group_id=group.id, group=get_group_detail(group), vpc_id=group.vpc_id)


if __name__ == '__main__':
    main()
