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()