#!/usr/bin/python
#
# 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': ['preview'],
                    'supported_by': 'community'}

DOCUMENTATION = '''
---
module: ce_eth_trunk
version_added: "2.4"
short_description: Manages Eth-Trunk interfaces on HUAWEI CloudEngine switches.
description:
    - Manages Eth-Trunk specific configuration parameters on HUAWEI CloudEngine switches.
author: QijunPan (@CloudEngine-Ansible)
notes:
    - C(state=absent) removes the Eth-Trunk config and interface if it
      already exists. If members to be removed are not explicitly
      passed, all existing members (if any), are removed,
      and Eth-Trunk removed.
    - Members must be a list.
options:
    trunk_id:
        description:
            - Eth-Trunk interface number.
              The value is an integer.
              The value range depends on the assign forward eth-trunk mode command.
              When 256 is specified, the value ranges from 0 to 255.
              When 512 is specified, the value ranges from 0 to 511.
              When 1024 is specified, the value ranges from 0 to 1023.
        required: true
    mode:
        description:
            - Specifies the working mode of an Eth-Trunk interface.
        choices: ['manual','lacp-dynamic','lacp-static']
    min_links:
        description:
            - Specifies the minimum number of Eth-Trunk member links in the Up state.
              The value is an integer ranging from 1 to the maximum number of interfaces
              that can be added to a Eth-Trunk interface.
    hash_type:
        description:
            - Hash algorithm used for load balancing among Eth-Trunk member interfaces.
        choices: ['src-dst-ip', 'src-dst-mac', 'enhanced', 'dst-ip', 'dst-mac', 'src-ip', 'src-mac']
    members:
        description:
            - List of interfaces that will be managed in a given Eth-Trunk.
              The interface name must be full name.
    force:
        description:
            - When true it forces Eth-Trunk members to match what is
              declared in the members param. This can be used to remove
              members.
        type: bool
        default: 'no'
    state:
        description:
            - Manage the state of the resource.
        default: present
        choices: ['present','absent']
'''
EXAMPLES = '''
- name: eth_trunk module test
  hosts: cloudengine
  connection: local
  gather_facts: no
  vars:
    cli:
      host: "{{ inventory_hostname }}"
      port: "{{ ansible_ssh_port }}"
      username: "{{ username }}"
      password: "{{ password }}"
      transport: cli

  tasks:
  - name: Ensure Eth-Trunk100 is created, add two members, and set to mode lacp-static
    ce_eth_trunk:
      trunk_id: 100
      members: ['10GE1/0/24','10GE1/0/25']
      mode: 'lacp-static'
      state: present
      provider: '{{ cli }}'
'''

RETURN = '''
proposed:
    description: k/v pairs of parameters passed into module
    returned: always
    type: dict
    sample: {"trunk_id": "100", "members": ['10GE1/0/24','10GE1/0/25'], "mode": "lacp-static"}
existing:
    description: k/v pairs of existing Eth-Trunk
    returned: always
    type: dict
    sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [
            {"memberIfName": "10GE1/0/25", "memberIfState": "Down"}],
            "min_links": "1", "mode": "manual"}
end_state:
    description: k/v pairs of Eth-Trunk info after module execution
    returned: always
    type: dict
    sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [
            {"memberIfName": "10GE1/0/24", "memberIfState": "Down"},
            {"memberIfName": "10GE1/0/25", "memberIfState": "Down"}],
            "min_links": "1", "mode": "lacp-static"}
updates:
    description: command sent to the device
    returned: always
    type: list
    sample: ["interface Eth-Trunk 100",
             "mode lacp-static",
             "interface 10GE1/0/25",
             "eth-trunk 100"]
changed:
    description: check to see if a change was made on the device
    returned: always
    type: boolean
    sample: true
'''

import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec

CE_NC_GET_TRUNK = """
<filter type="subtree">
  <ifmtrunk xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
    <TrunkIfs>
      <TrunkIf>
        <ifName>Eth-Trunk%s</ifName>
        <minUpNum></minUpNum>
        <maxUpNum></maxUpNum>
        <trunkType></trunkType>
        <hashType></hashType>
        <workMode></workMode>
        <upMemberIfNum></upMemberIfNum>
        <memberIfNum></memberIfNum>
        <TrunkMemberIfs>
          <TrunkMemberIf>
            <memberIfName></memberIfName>
            <memberIfState></memberIfState>
          </TrunkMemberIf>
        </TrunkMemberIfs>
      </TrunkIf>
    </TrunkIfs>
  </ifmtrunk>
</filter>
"""

CE_NC_XML_BUILD_TRUNK_CFG = """
<config>
  <ifmtrunk xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
    <TrunkIfs>%s</TrunkIfs>
  </ifmtrunk>
</config>
"""

CE_NC_XML_DELETE_TRUNK = """
<TrunkIf operation="delete">
  <ifName>Eth-Trunk%s</ifName>
</TrunkIf>
"""

CE_NC_XML_CREATE_TRUNK = """
<TrunkIf operation="merge">
  <ifName>Eth-Trunk%s</ifName>
</TrunkIf>
"""

CE_NC_XML_MERGE_MINUPNUM = """
<TrunkIf operation="merge">
  <ifName>Eth-Trunk%s</ifName>
  <minUpNum>%s</minUpNum>
</TrunkIf>
"""

CE_NC_XML_MERGE_HASHTYPE = """
<TrunkIf operation="merge">
  <ifName>Eth-Trunk%s</ifName>
  <hashType>%s</hashType>
</TrunkIf>
"""

CE_NC_XML_MERGE_WORKMODE = """
<TrunkIf operation="merge">
  <ifName>Eth-Trunk%s</ifName>
  <workMode>%s</workMode>
</TrunkIf>
"""

CE_NC_XML_BUILD_MEMBER_CFG = """
<TrunkIf>
  <ifName>Eth-Trunk%s</ifName>
  <TrunkMemberIfs>%s</TrunkMemberIfs>
</TrunkIf>
"""

CE_NC_XML_MERGE_MEMBER = """
<TrunkMemberIf operation="merge">
  <memberIfName>%s</memberIfName>
</TrunkMemberIf>
"""

CE_NC_XML_DELETE_MEMBER = """
<TrunkMemberIf operation="delete">
  <memberIfName>%s</memberIfName>
</TrunkMemberIf>
"""

MODE_XML2CLI = {"Manual": "manual", "Dynamic": "lacp-dynamic", "Static": "lacp-static"}
MODE_CLI2XML = {"manual": "Manual", "lacp-dynamic": "Dynamic", "lacp-static": "Static"}
HASH_XML2CLI = {"IP": "src-dst-ip", "MAC": "src-dst-mac", "Enhanced": "enhanced",
                "Desip": "dst-ip", "Desmac": "dst-mac", "Sourceip": "src-ip", "Sourcemac": "src-mac"}
HASH_CLI2XML = {"src-dst-ip": "IP", "src-dst-mac": "MAC", "enhanced": "Enhanced",
                "dst-ip": "Desip", "dst-mac": "Desmac", "src-ip": "Sourceip", "src-mac": "Sourcemac"}


def get_interface_type(interface):
    """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF..."""

    if interface is None:
        return None

    iftype = None

    if interface.upper().startswith('GE'):
        iftype = 'ge'
    elif interface.upper().startswith('10GE'):
        iftype = '10ge'
    elif interface.upper().startswith('25GE'):
        iftype = '25ge'
    elif interface.upper().startswith('4X10GE'):
        iftype = '4x10ge'
    elif interface.upper().startswith('40GE'):
        iftype = '40ge'
    elif interface.upper().startswith('100GE'):
        iftype = '100ge'
    elif interface.upper().startswith('VLANIF'):
        iftype = 'vlanif'
    elif interface.upper().startswith('LOOPBACK'):
        iftype = 'loopback'
    elif interface.upper().startswith('METH'):
        iftype = 'meth'
    elif interface.upper().startswith('ETH-TRUNK'):
        iftype = 'eth-trunk'
    elif interface.upper().startswith('VBDIF'):
        iftype = 'vbdif'
    elif interface.upper().startswith('NVE'):
        iftype = 'nve'
    elif interface.upper().startswith('TUNNEL'):
        iftype = 'tunnel'
    elif interface.upper().startswith('ETHERNET'):
        iftype = 'ethernet'
    elif interface.upper().startswith('FCOE-PORT'):
        iftype = 'fcoe-port'
    elif interface.upper().startswith('FABRIC-PORT'):
        iftype = 'fabric-port'
    elif interface.upper().startswith('STACK-PORT'):
        iftype = 'stack-port'
    elif interface.upper().startswith('NULL'):
        iftype = 'null'
    else:
        return None

    return iftype.lower()


def mode_xml_to_cli_str(mode):
    """convert mode to cli format string"""

    if not mode:
        return ""

    return MODE_XML2CLI.get(mode)


def hash_type_xml_to_cli_str(hash_type):
    """convert trunk hash type netconf xml to cli format string"""

    if not hash_type:
        return ""

    return HASH_XML2CLI.get(hash_type)


class EthTrunk(object):
    """
    Manages Eth-Trunk interfaces.
    """

    def __init__(self, argument_spec):
        self.spec = argument_spec
        self.module = None
        self.__init_module__()

        # module input info
        self.trunk_id = self.module.params['trunk_id']
        self.mode = self.module.params['mode']
        self.min_links = self.module.params['min_links']
        self.hash_type = self.module.params['hash_type']
        self.members = self.module.params['members']
        self.state = self.module.params['state']
        self.force = self.module.params['force']

        # state
        self.changed = False
        self.updates_cmd = list()
        self.results = dict()
        self.proposed = dict()
        self.existing = dict()
        self.end_state = dict()

        # interface info
        self.trunk_info = dict()

    def __init_module__(self):
        """ init module """

        self.module = AnsibleModule(
            argument_spec=self.spec, supports_check_mode=True)

    def netconf_set_config(self, xml_str, xml_name):
        """ netconf set config """

        recv_xml = set_nc_config(self.module, xml_str)

        if "<ok/>" not in recv_xml:
            self.module.fail_json(msg='Error: %s failed.' % xml_name)

    def get_trunk_dict(self, trunk_id):
        """ get one interface attributes dict."""

        trunk_info = dict()
        conf_str = CE_NC_GET_TRUNK % trunk_id
        recv_xml = get_nc_config(self.module, conf_str)

        if "<data/>" in recv_xml:
            return trunk_info

        # get trunk base info
        base = re.findall(
            r'.*<ifName>(.*)</ifName>.*\s*'
            r'<minUpNum>(.*)</minUpNum>.*\s*'
            r'<maxUpNum>(.*)</maxUpNum>.*\s*'
            r'<trunkType>(.*)</trunkType>.*\s*'
            r'<hashType>(.*)</hashType>.*\s*'
            r'<workMode>(.*)</workMode>.*\s*'
            r'<upMemberIfNum>(.*)</upMemberIfNum>.*\s*'
            r'<memberIfNum>(.*)</memberIfNum>.*', recv_xml)

        if base:
            trunk_info = dict(ifName=base[0][0],
                              trunkId=base[0][0].lower().replace("eth-trunk", "").replace(" ", ""),
                              minUpNum=base[0][1],
                              maxUpNum=base[0][2],
                              trunkType=base[0][3],
                              hashType=base[0][4],
                              workMode=base[0][5],
                              upMemberIfNum=base[0][6],
                              memberIfNum=base[0][7])

        # get trunk member interface info
        member = re.findall(
            r'.*<memberIfName>(.*)</memberIfName>.*\s*'
            r'<memberIfState>(.*)</memberIfState>.*', recv_xml)
        trunk_info["TrunkMemberIfs"] = list()

        for mem in member:
            trunk_info["TrunkMemberIfs"].append(
                dict(memberIfName=mem[0], memberIfState=mem[1]))

        return trunk_info

    def is_member_exist(self, ifname):
        """is trunk member exist"""

        if not self.trunk_info["TrunkMemberIfs"]:
            return False

        for mem in self.trunk_info["TrunkMemberIfs"]:
            if ifname.replace(" ", "").upper() == mem["memberIfName"].replace(" ", "").upper():
                return True

        return False

    def get_mode_xml_str(self):
        """trunk mode netconf xml fromat string"""

        return MODE_CLI2XML.get(self.mode)

    def get_hash_type_xml_str(self):
        """trunk hash type netconf xml format string"""

        return HASH_CLI2XML.get(self.hash_type)

    def create_eth_trunk(self):
        """Create Eth-Trunk interface"""

        xml_str = CE_NC_XML_CREATE_TRUNK % self.trunk_id
        self.updates_cmd.append("interface Eth-Trunk %s" % self.trunk_id)

        if self.hash_type:
            self.updates_cmd.append("load-balance %s" % self.hash_type)
            xml_str += CE_NC_XML_MERGE_HASHTYPE % (self.trunk_id, self.get_hash_type_xml_str())

        if self.mode:
            self.updates_cmd.append("mode %s" % self.mode)
            xml_str += CE_NC_XML_MERGE_WORKMODE % (self.trunk_id, self.get_mode_xml_str())

        if self.min_links:
            self.updates_cmd.append("least active-linknumber %s" % self.min_links)
            xml_str += CE_NC_XML_MERGE_MINUPNUM % (self.trunk_id, self.min_links)

        if self.members:
            mem_xml = ""
            for mem in self.members:
                mem_xml += CE_NC_XML_MERGE_MEMBER % mem.upper()
                self.updates_cmd.append("interface %s" % mem)
                self.updates_cmd.append("eth-trunk %s" % self.trunk_id)
            xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml)
        cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str
        self.netconf_set_config(cfg_xml, "CREATE_TRUNK")
        self.changed = True

    def delete_eth_trunk(self):
        """Delete Eth-Trunk interface and remove all member"""

        if not self.trunk_info:
            return

        xml_str = ""
        mem_str = ""
        if self.trunk_info["TrunkMemberIfs"]:
            for mem in self.trunk_info["TrunkMemberIfs"]:
                mem_str += CE_NC_XML_DELETE_MEMBER % mem["memberIfName"]
                self.updates_cmd.append("interface %s" % mem["memberIfName"])
                self.updates_cmd.append("undo eth-trunk")
            if mem_str:
                xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_str)

        xml_str += CE_NC_XML_DELETE_TRUNK % self.trunk_id
        self.updates_cmd.append("undo interface Eth-Trunk %s" % self.trunk_id)
        cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str
        self.netconf_set_config(cfg_xml, "DELETE_TRUNK")
        self.changed = True

    def remove_member(self):
        """delete trunk member"""

        if not self.members:
            return

        change = False
        mem_xml = ""
        xml_str = ""
        for mem in self.members:
            if self.is_member_exist(mem):
                mem_xml += CE_NC_XML_DELETE_MEMBER % mem.upper()
                self.updates_cmd.append("interface %s" % mem)
                self.updates_cmd.append("undo eth-trunk")
        if mem_xml:
            xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml)
            change = True

        if not change:
            return

        cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str
        self.netconf_set_config(cfg_xml, "REMOVE_TRUNK_MEMBER")
        self.changed = True

    def merge_eth_trunk(self):
        """Create or merge Eth-Trunk"""

        change = False
        xml_str = ""
        self.updates_cmd.append("interface Eth-Trunk %s" % self.trunk_id)
        if self.hash_type and self.get_hash_type_xml_str() != self.trunk_info["hashType"]:
            self.updates_cmd.append("load-balance %s" %
                                    self.hash_type)
            xml_str += CE_NC_XML_MERGE_HASHTYPE % (
                self.trunk_id, self.get_hash_type_xml_str())
            change = True
        if self.min_links and self.min_links != self.trunk_info["minUpNum"]:
            self.updates_cmd.append(
                "least active-linknumber %s" % self.min_links)
            xml_str += CE_NC_XML_MERGE_MINUPNUM % (
                self.trunk_id, self.min_links)
            change = True
        if self.mode and self.get_mode_xml_str() != self.trunk_info["workMode"]:
            self.updates_cmd.append("mode %s" % self.mode)
            xml_str += CE_NC_XML_MERGE_WORKMODE % (
                self.trunk_id, self.get_mode_xml_str())
            change = True

        if not change:
            self.updates_cmd.pop()   # remove 'interface Eth-Trunk' command

        # deal force:
        # When true it forces Eth-Trunk members to match
        # what is declared in the members param.
        if self.force and self.trunk_info["TrunkMemberIfs"]:
            mem_xml = ""
            for mem in self.trunk_info["TrunkMemberIfs"]:
                if not self.members or mem["memberIfName"].replace(" ", "").upper() not in self.members:
                    mem_xml += CE_NC_XML_DELETE_MEMBER % mem["memberIfName"]
                    self.updates_cmd.append("interface %s" % mem["memberIfName"])
                    self.updates_cmd.append("undo eth-trunk")
            if mem_xml:
                xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml)
                change = True

        if self.members:
            mem_xml = ""
            for mem in self.members:
                if not self.is_member_exist(mem):
                    mem_xml += CE_NC_XML_MERGE_MEMBER % mem.upper()
                    self.updates_cmd.append("interface %s" % mem)
                    self.updates_cmd.append("eth-trunk %s" % self.trunk_id)
            if mem_xml:
                xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (
                    self.trunk_id, mem_xml)
                change = True

        if not change:
            return

        cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str
        self.netconf_set_config(cfg_xml, "MERGE_TRUNK")
        self.changed = True

    def check_params(self):
        """Check all input params"""

        # trunk_id check
        if not self.trunk_id.isdigit():
            self.module.fail_json(msg='The parameter of trunk_id is invalid.')

        # min_links check
        if self.min_links and not self.min_links.isdigit():
            self.module.fail_json(msg='The parameter of min_links is invalid.')

        # members check and convert members to upper
        if self.members:
            for mem in self.members:
                if not get_interface_type(mem.replace(" ", "")):
                    self.module.fail_json(
                        msg='The parameter of members is invalid.')

            for mem_id in range(len(self.members)):
                self.members[mem_id] = self.members[mem_id].replace(" ", "").upper()

    def get_proposed(self):
        """get proposed info"""

        self.proposed["trunk_id"] = self.trunk_id
        self.proposed["mode"] = self.mode
        if self.min_links:
            self.proposed["min_links"] = self.min_links
        self.proposed["hash_type"] = self.hash_type
        if self.members:
            self.proposed["members"] = self.members
        self.proposed["state"] = self.state
        self.proposed["force"] = self.force

    def get_existing(self):
        """get existing info"""

        if not self.trunk_info:
            return

        self.existing["trunk_id"] = self.trunk_info["trunkId"]
        self.existing["min_links"] = self.trunk_info["minUpNum"]
        self.existing["hash_type"] = hash_type_xml_to_cli_str(self.trunk_info["hashType"])
        self.existing["mode"] = mode_xml_to_cli_str(self.trunk_info["workMode"])
        self.existing["members_detail"] = self.trunk_info["TrunkMemberIfs"]

    def get_end_state(self):
        """get end state info"""

        trunk_info = self.get_trunk_dict(self.trunk_id)
        if not trunk_info:
            return

        self.end_state["trunk_id"] = trunk_info["trunkId"]
        self.end_state["min_links"] = trunk_info["minUpNum"]
        self.end_state["hash_type"] = hash_type_xml_to_cli_str(trunk_info["hashType"])
        self.end_state["mode"] = mode_xml_to_cli_str(trunk_info["workMode"])
        self.end_state["members_detail"] = trunk_info["TrunkMemberIfs"]

    def work(self):
        """worker"""

        self.check_params()
        self.trunk_info = self.get_trunk_dict(self.trunk_id)
        self.get_existing()
        self.get_proposed()

        # deal present or absent
        if self.state == "present":
            if not self.trunk_info:
                # create
                self.create_eth_trunk()
            else:
                # merge trunk
                self.merge_eth_trunk()
        else:
            if self.trunk_info:
                if not self.members:
                    # remove all members and delete trunk
                    self.delete_eth_trunk()
                else:
                    # remove some trunk members
                    self.remove_member()
            else:
                self.module.fail_json(msg='Error: Eth-Trunk does not exist.')

        self.get_end_state()
        self.results['changed'] = self.changed
        self.results['proposed'] = self.proposed
        self.results['existing'] = self.existing
        self.results['end_state'] = self.end_state
        if self.changed:
            self.results['updates'] = self.updates_cmd
        else:
            self.results['updates'] = list()

        self.module.exit_json(**self.results)


def main():
    """Module main"""

    argument_spec = dict(
        trunk_id=dict(required=True),
        mode=dict(required=False,
                  choices=['manual', 'lacp-dynamic', 'lacp-static'],
                  type='str'),
        min_links=dict(required=False, type='str'),
        hash_type=dict(required=False,
                       choices=['src-dst-ip', 'src-dst-mac', 'enhanced',
                                'dst-ip', 'dst-mac', 'src-ip', 'src-mac'],
                       type='str'),
        members=dict(required=False, default=None, type='list'),
        force=dict(required=False, default=False, type='bool'),
        state=dict(required=False, default='present',
                   choices=['present', 'absent'])
    )

    argument_spec.update(ce_argument_spec)
    module = EthTrunk(argument_spec)
    module.work()


if __name__ == '__main__':
    main()
