diff --git a/lib/ansible/modules/network/cloudengine/ce_eth_trunk.py b/lib/ansible/modules/network/cloudengine/ce_eth_trunk.py
new file mode 100644
index 0000000000..38c8f153f7
--- /dev/null
+++ b/lib/ansible/modules/network/cloudengine/ce_eth_trunk.py
@@ -0,0 +1,680 @@
+#!/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 .
+#
+
+ANSIBLE_METADATA = {'status': ['preview'],
+ 'supported_by': 'community',
+ 'metadata_version': '1.0'}
+
+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.
+ required: false
+ default: null
+ 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.
+ required: false
+ default: null
+ hash_type:
+ description:
+ - Hash algorithm used for load balancing among Eth-Trunk member interfaces.
+ required: false
+ default: null
+ 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.
+ required: false
+ default: null
+ 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.
+ required: false
+ default: false
+ state:
+ description:
+ - Manage the state of the resource.
+ required: false
+ 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.ce import get_nc_config, set_nc_config, ce_argument_spec
+
+CE_NC_GET_TRUNK = """
+
+
+
+
+ Eth-Trunk%s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+CE_NC_XML_BUILD_TRUNK_CFG = """
+
+
+ %s
+
+
+"""
+
+CE_NC_XML_DELETE_TRUNK = """
+
+ Eth-Trunk%s
+
+"""
+
+CE_NC_XML_CREATE_TRUNK = """
+
+ Eth-Trunk%s
+
+"""
+
+CE_NC_XML_MERGE_MINUPNUM = """
+
+ Eth-Trunk%s
+ %s
+
+"""
+
+CE_NC_XML_MERGE_HASHTYPE = """
+
+ Eth-Trunk%s
+ %s
+
+"""
+
+CE_NC_XML_MERGE_WORKMODE = """
+
+ Eth-Trunk%s
+ %s
+
+"""
+
+CE_NC_XML_BUILD_MEMBER_CFG = """
+
+ Eth-Trunk%s
+ %s
+
+"""
+
+CE_NC_XML_MERGE_MEMBER = """
+
+ %s
+
+"""
+
+CE_NC_XML_DELETE_MEMBER = """
+
+ %s
+
+"""
+
+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 "" 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 "" in recv_xml:
+ return trunk_info
+
+ # get trunk base info
+ base = re.findall(
+ r'.*(.*).*\s*'
+ r'(.*).*\s*'
+ r'(.*).*\s*'
+ r'(.*).*\s*'
+ r'(.*).*\s*'
+ r'(.*).*\s*'
+ r'(.*).*\s*'
+ r'(.*).*', 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'.*(.*).*\s*'
+ r'(.*).*', 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()