diff --git a/lib/ansible/modules/network/cloudengine/ce_switchport.py b/lib/ansible/modules/network/cloudengine/ce_switchport.py new file mode 100644 index 00000000000..d07f3c4a678 --- /dev/null +++ b/lib/ansible/modules/network/cloudengine/ce_switchport.py @@ -0,0 +1,817 @@ +#!/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_switchport +version_added: "2.4" +short_description: Manages Layer 2 switchport interfaces on HUAWEI CloudEngine switches. +description: + - Manages Layer 2 switchport interfaces on HUAWEI CloudEngine switches. +author: QijunPan (@CloudEngine-Ansible) +notes: + - When C(state=absent), VLANs can be added/removed from trunk links and + the existing access VLAN can be 'unconfigured' to just having VLAN 1 + on that interface. + - When working with trunks VLANs the keywords add/remove are always sent + in the C(port trunk allow-pass vlan) command. Use verbose mode to see + commands sent. + - When C(state=unconfigured), the interface will result with having a default + Layer 2 interface, i.e. vlan 1 in access mode. +options: + interface: + description: + - Full name of the interface, i.e. 40GE1/0/22. + required: true + default: null + mode: + description: + - The link type of an interface. + required: false + default: null + choices: ['access','trunk'] + access_vlan: + description: + - If C(mode=access), used as the access VLAN ID, in the range from 1 to 4094. + required: false + default: null + native_vlan: + description: + - If C(mode=trunk), used as the trunk native VLAN ID, in the range from 1 to 4094. + required: false + default: null + trunk_vlans: + description: + - If C(mode=trunk), used as the VLAN range to ADD or REMOVE + from the trunk, such as 2-10 or 2,5,10-15, etc. + aliases: + - trunk_add_vlans + required: false + default: null + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present', 'absent', 'unconfigured'] +''' + +EXAMPLES = ''' +- name: switchport 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 10GE1/0/22 is in its default switchport state + ce_switchport: + interface: 10GE1/0/22 + state: unconfigured + provider: '{{ cli }}' + + - name: Ensure 10GE1/0/22 is configured for access vlan 20 + ce_switchport: + interface: 10GE1/0/22 + mode: access + access_vlan: 20 + provider: '{{ cli }}' + + - name: Ensure 10GE1/0/22 only has vlans 5-10 as trunk vlans + ce_switchport: + interface: 10GE1/0/22 + mode: trunk + native_vlan: 10 + trunk_vlans: 5-10 + provider: '{{ cli }}' + + - name: Ensure 10GE1/0/22 is a trunk port and ensure 2-50 are being tagged (doesn't mean others aren't also being tagged) + ce_switchport: + interface: 10GE1/0/22 + mode: trunk + native_vlan: 10 + trunk_vlans: 2-50 + provider: '{{ cli }}' + + - name: Ensure these VLANs are not being tagged on the trunk + ce_switchport: + interface: 10GE1/0/22 + mode: trunk + trunk_vlans: 51-4000 + state: absent + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"access_vlan": "20", "interface": "10GE1/0/22", "mode": "access"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {"access_vlan": "10", "interface": "10GE1/0/22", + "mode": "access", "switchport": "enable"} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {"access_vlan": "20", "interface": "10GE1/0/22", + "mode": "access", "switchport": "enable"} +updates: + description: command string sent to the device + returned: always + type: list + sample: ["10GE1/0/22", "port default vlan 20"] +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_INTF = """ + + + + + %s + + + + + +""" + +CE_NC_GET_PORT_ATTR = """ + + + + + %s + + + + + + + + + + +""" + +CE_NC_SET_ACCESS_PORT = """ + + + + + %s + + access + %s + + + + + + + +""" + +CE_NC_SET_TRUNK_PORT_MODE = """ + + + + %s + + trunk + + + + +""" + +CE_NC_SET_TRUNK_PORT_PVID = """ + + + + %s + + trunk + %s + + + + + +""" + +CE_NC_SET_TRUNK_PORT_VLANS = """ + + + + %s + + trunk + %s:%s + + + + + +""" + +CE_NC_SET_DEFAULT_PORT = """ + + + + + %s + + access + 1 + + + + + + + +""" + + +SWITCH_PORT_TYPE = ('ge', '10ge', '25ge', + '4x10ge', '40ge', '100ge', 'eth-trunk') + + +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 is_portswitch_enalbed(iftype): + """"[undo] portswitch""" + + return bool(iftype in SWITCH_PORT_TYPE) + + +def vlan_bitmap_undo(bitmap): + """convert vlan bitmap to undo bitmap""" + + vlan_bit = ['F'] * 1024 + + if not bitmap or len(bitmap) == 0: + return ''.join(vlan_bit) + + bit_len = len(bitmap) + for num in range(bit_len): + undo = (~int(bitmap[num], 16)) & 0xF + vlan_bit[num] = hex(undo)[2] + + return ''.join(vlan_bit) + + +def is_vlan_bitmap_empty(bitmap): + """check vlan bitmap empty""" + + if not bitmap or len(bitmap) == 0: + return True + + bit_len = len(bitmap) + for num in range(bit_len): + if bitmap[num] != '0': + return False + + return True + + +class SwitchPort(object): + """ + Manages Layer 2 switchport interfaces. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # interface and vlan info + self.interface = self.module.params['interface'] + self.mode = self.module.params['mode'] + self.state = self.module.params['state'] + self.access_vlan = self.module.params['access_vlan'] + self.native_vlan = self.module.params['native_vlan'] + self.trunk_vlans = self.module.params['trunk_vlans'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + self.intf_info = dict() # interface vlan info + self.intf_type = None # loopback tunnel ... + + def init_module(self): + """ init module """ + + required_if = [('state', 'absent', ['mode']), ('state', 'present', ['mode'])] + self.module = AnsibleModule( + argument_spec=self.spec, required_if=required_if, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_interface_dict(self, ifname): + """ get one interface attributes dict.""" + + intf_info = dict() + conf_str = CE_NC_GET_PORT_ATTR % ifname + rcv_xml = get_nc_config(self.module, conf_str) + if "" in rcv_xml: + return intf_info + + intf = re.findall( + r'.*(.*).*\s*(.*).*', rcv_xml) + if intf: + intf_info = dict(ifName=intf[0][0], + l2Enable=intf[0][1], + linkType="", + pvid="", + trunkVlans="") + if intf_info["l2Enable"] == "enable": + attr = re.findall( + r'.*(.*).*.*\s*(.*)' + r'.*\s*(.*).*', rcv_xml) + if attr: + intf_info["linkType"] = attr[0][0] + intf_info["pvid"] = attr[0][1] + intf_info["trunkVlans"] = attr[0][2] + + return intf_info + + def is_l2switchport(self): + """Check layer2 switch port""" + + return bool(self.intf_info["l2Enable"] == "enable") + + def merge_access_vlan(self, ifname, access_vlan): + """Merge access interface vlan""" + + change = False + conf_str = "" + self.updates_cmd.append("interface %s" % ifname) + if self.state == "present": + if self.intf_info["linkType"] == "access": + if access_vlan and self.intf_info["pvid"] != access_vlan: + self.updates_cmd.append( + "port default vlan %s" % access_vlan) + conf_str = CE_NC_SET_ACCESS_PORT % (ifname, access_vlan) + change = True + else: # not access + self.updates_cmd.append("port link-type access") + if access_vlan: + self.updates_cmd.append( + "port default vlan %s" % access_vlan) + conf_str = CE_NC_SET_ACCESS_PORT % (ifname, access_vlan) + else: + conf_str = CE_NC_SET_ACCESS_PORT % (ifname, "1") + change = True + elif self.state == "absent": + if self.intf_info["linkType"] == "access": + if access_vlan and self.intf_info["pvid"] == access_vlan and access_vlan != "1": + self.updates_cmd.append( + "undo port default vlan %s" % access_vlan) + conf_str = CE_NC_SET_ACCESS_PORT % (ifname, "1") + change = True + else: # not access + self.updates_cmd.append("port link-type access") + conf_str = CE_NC_SET_ACCESS_PORT % (ifname, "1") + change = True + + if not change: + self.updates_cmd.pop() # remove interface + return + + rcv_xml = set_nc_config(self.module, conf_str) + self.check_response(rcv_xml, "MERGE_ACCESS_PORT") + self.changed = True + + def merge_trunk_vlan(self, ifname, native_vlan, trunk_vlans): + """Merge trunk interface vlan""" + + change = False + xmlstr = "" + self.updates_cmd.append("interface %s" % ifname) + if trunk_vlans: + vlan_list = self.vlan_range_to_list(trunk_vlans) + vlan_map = self.vlan_list_to_bitmap(vlan_list) + + if self.state == "present": + if self.intf_info["linkType"] == "trunk": + if native_vlan and self.intf_info["pvid"] != native_vlan: + self.updates_cmd.append( + "port trunk pvid vlan %s" % native_vlan) + xmlstr += CE_NC_SET_TRUNK_PORT_PVID % (ifname, native_vlan) + change = True + if trunk_vlans: + add_vlans = self.vlan_bitmap_add( + self.intf_info["trunkVlans"], vlan_map) + if not is_vlan_bitmap_empty(add_vlans): + self.updates_cmd.append( + "port trunk allow-pass %s" + % trunk_vlans.replace(',', ' ').replace('-', ' to ')) + xmlstr += CE_NC_SET_TRUNK_PORT_VLANS % ( + ifname, add_vlans, add_vlans) + change = True + else: # not trunk + self.updates_cmd.append("port link-type trunk") + change = True + if native_vlan: + self.updates_cmd.append( + "port trunk pvid vlan %s" % native_vlan) + xmlstr += CE_NC_SET_TRUNK_PORT_PVID % (ifname, native_vlan) + if trunk_vlans: + self.updates_cmd.append( + "port trunk allow-pass %s" + % trunk_vlans.replace(',', ' ').replace('-', ' to ')) + xmlstr += CE_NC_SET_TRUNK_PORT_VLANS % ( + ifname, vlan_map, vlan_map) + if not native_vlan and not trunk_vlans: + xmlstr += CE_NC_SET_TRUNK_PORT_MODE % ifname + self.updates_cmd.append( + "undo port trunk allow-pass vlan 1") + elif self.state == "absent": + if self.intf_info["linkType"] == "trunk": + if native_vlan and self.intf_info["pvid"] == native_vlan and native_vlan != '1': + self.updates_cmd.append( + "undo port trunk pvid vlan %s" % native_vlan) + xmlstr += CE_NC_SET_TRUNK_PORT_PVID % (ifname, 1) + change = True + if trunk_vlans: + del_vlans = self.vlan_bitmap_del( + self.intf_info["trunkVlans"], vlan_map) + if not is_vlan_bitmap_empty(del_vlans): + self.updates_cmd.append( + "undo port trunk allow-pass %s" + % trunk_vlans.replace(',', ' ').replace('-', ' to ')) + undo_map = vlan_bitmap_undo(del_vlans) + xmlstr += CE_NC_SET_TRUNK_PORT_VLANS % ( + ifname, undo_map, del_vlans) + change = True + else: # not trunk + self.updates_cmd.append("port link-type trunk") + self.updates_cmd.append("undo port trunk allow-pass vlan 1") + xmlstr += CE_NC_SET_TRUNK_PORT_MODE % ifname + change = True + + if not change: + self.updates_cmd.pop() + return + + conf_str = "" + xmlstr + "" + rcv_xml = set_nc_config(self.module, conf_str) + self.check_response(rcv_xml, "MERGE_TRUNK_PORT") + self.changed = True + + def default_switchport(self, ifname): + """Set interface default or unconfigured""" + + change = False + if self.intf_info["linkType"] != "access": + self.updates_cmd.append("interface %s" % ifname) + self.updates_cmd.append("port link-type access") + self.updates_cmd.append("port default vlan 1") + change = True + else: + if self.intf_info["pvid"] != "1": + self.updates_cmd.append("interface %s" % ifname) + self.updates_cmd.append("port default vlan 1") + change = True + + if not change: + return + + conf_str = CE_NC_SET_DEFAULT_PORT % ifname + rcv_xml = set_nc_config(self.module, conf_str) + self.check_response(rcv_xml, "DEFAULT_INTF_VLAN") + self.changed = True + + def vlan_series(self, vlanid_s): + """ convert vlan range to vlan list """ + + vlan_list = [] + peerlistlen = len(vlanid_s) + if peerlistlen != 2: + self.module.fail_json(msg='Error: Format of vlanid is invalid.') + for num in range(peerlistlen): + if not vlanid_s[num].isdigit(): + self.module.fail_json( + msg='Error: Format of vlanid is invalid.') + if int(vlanid_s[0]) > int(vlanid_s[1]): + self.module.fail_json(msg='Error: Format of vlanid is invalid.') + elif int(vlanid_s[0]) == int(vlanid_s[1]): + vlan_list.append(str(vlanid_s[0])) + return vlan_list + for num in range(int(vlanid_s[0]), int(vlanid_s[1])): + vlan_list.append(str(num)) + vlan_list.append(vlanid_s[1]) + + return vlan_list + + def vlan_region(self, vlanid_list): + """ convert vlan range to vlan list """ + + vlan_list = [] + peerlistlen = len(vlanid_list) + for num in range(peerlistlen): + if vlanid_list[num].isdigit(): + vlan_list.append(vlanid_list[num]) + else: + vlan_s = self.vlan_series(vlanid_list[num].split('-')) + vlan_list.extend(vlan_s) + + return vlan_list + + def vlan_range_to_list(self, vlan_range): + """ convert vlan range to vlan list """ + + vlan_list = self.vlan_region(vlan_range.split(',')) + + return vlan_list + + def vlan_list_to_bitmap(self, vlanlist): + """ convert vlan list to vlan bitmap """ + + vlan_bit = ['0'] * 1024 + bit_int = [0] * 1024 + + vlan_list_len = len(vlanlist) + for num in range(vlan_list_len): + tagged_vlans = int(vlanlist[num]) + if tagged_vlans <= 0 or tagged_vlans > 4094: + self.module.fail_json( + msg='Error: Vlan id is not in the range from 1 to 4094.') + j = tagged_vlans / 4 + bit_int[j] |= 0x8 >> (tagged_vlans % 4) + vlan_bit[j] = hex(bit_int[j])[2] + + vlan_xml = ''.join(vlan_bit) + + return vlan_xml + + def vlan_bitmap_add(self, oldmap, newmap): + """vlan add bitmap""" + + vlan_bit = ['0'] * 1024 + + if len(newmap) != 1024: + self.module.fail_json(msg='Error: New vlan bitmap is invalid.') + + if len(oldmap) != 1024 and len(oldmap) != 0: + self.module.fail_json(msg='Error: old vlan bitmap is invalid.') + + if len(oldmap) == 0: + return newmap + + for num in range(1024): + new_tmp = int(newmap[num], 16) + old_tmp = int(oldmap[num], 16) + add = (~(new_tmp & old_tmp)) & new_tmp + vlan_bit[num] = hex(add)[2] + + vlan_xml = ''.join(vlan_bit) + + return vlan_xml + + def vlan_bitmap_del(self, oldmap, delmap): + """vlan del bitmap""" + + vlan_bit = ['0'] * 1024 + + if not oldmap or len(oldmap) == 0: + return ''.join(vlan_bit) + + if len(oldmap) != 1024 or len(delmap) != 1024: + self.module.fail_json(msg='Error: vlan bitmap is invalid.') + + for num in range(1024): + tmp = int(delmap[num], 16) & int(oldmap[num], 16) + vlan_bit[num] = hex(tmp)[2] + + vlan_xml = ''.join(vlan_bit) + + return vlan_xml + + def check_params(self): + """Check all input params""" + + # interface type check + if self.interface: + self.intf_type = get_interface_type(self.interface) + if not self.intf_type: + self.module.fail_json( + msg='Error: Interface name of %s is error.' % self.interface) + + if not self.intf_type or not is_portswitch_enalbed(self.intf_type): + self.module.fail_json(msg='Error: Interface %s is error.') + + # check access_vlan + if self.access_vlan: + if not self.access_vlan.isdigit(): + self.module.fail_json(msg='Error: Access vlan id is invalid.') + if int(self.access_vlan) <= 0 or int(self.access_vlan) > 4094: + self.module.fail_json( + msg='Error: Access vlan id is not in the range from 1 to 4094.') + + # check native_vlan + if self.native_vlan: + if not self.native_vlan.isdigit(): + self.module.fail_json(msg='Error: Native vlan id is invalid.') + if int(self.native_vlan) <= 0 or int(self.native_vlan) > 4094: + self.module.fail_json( + msg='Error: Native vlan id is not in the range from 1 to 4094.') + + # get interface info + self.intf_info = self.get_interface_dict(self.interface) + if not self.intf_info: + self.module.fail_json(msg='Error: Interface does not exists.') + + if not self.is_l2switchport(): + self.module.fail_json( + msg='Error: Interface is not layer2 swtich port.') + + def get_proposed(self): + """get proposed info""" + + self.proposed['state'] = self.state + self.proposed['interface'] = self.interface + self.proposed['mode'] = self.mode + self.proposed['access_vlan'] = self.access_vlan + self.proposed['native_vlan'] = self.native_vlan + self.proposed['trunk_vlans'] = self.trunk_vlans + + def get_existing(self): + """get existing info""" + + if self.intf_info: + self.existing["interface"] = self.intf_info["ifName"] + self.existing["mode"] = self.intf_info["linkType"] + self.existing["switchport"] = self.intf_info["l2Enable"] + self.existing['access_vlan'] = self.intf_info["pvid"] + self.existing['native_vlan'] = self.intf_info["pvid"] + self.existing['trunk_vlans'] = self.intf_info["trunkVlans"] + + def get_end_state(self): + """get end state info""" + + if self.intf_info: + end_info = self.get_interface_dict(self.interface) + if end_info: + self.end_state["interface"] = end_info["ifName"] + self.end_state["mode"] = end_info["linkType"] + self.end_state["switchport"] = end_info["l2Enable"] + self.end_state['access_vlan'] = end_info["pvid"] + self.end_state['native_vlan'] = end_info["pvid"] + self.end_state['trunk_vlans'] = end_info["trunkVlans"] + + def work(self): + """worker""" + + self.check_params() + if not self.intf_info: + self.module.fail_json(msg='Error: interface does not exists.') + + self.get_existing() + self.get_proposed() + + # present or absent + if self.state == "present" or self.state == "absent": + if self.mode == "access": + self.merge_access_vlan(self.interface, self.access_vlan) + elif self.mode == "trunk": + self.merge_trunk_vlan( + self.interface, self.native_vlan, self.trunk_vlans) + # unconfigured + else: + self.default_switchport(self.interface) + + 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( + interface=dict(required=True, type='str'), + mode=dict(choices=['access', 'trunk'], required=False), + access_vlan=dict(type='str', required=False), + native_vlan=dict(type='str', required=False), + trunk_vlans=dict(type='str', required=False), + state=dict(choices=['absent', 'present', 'unconfigured'], + default='present') + ) + + argument_spec.update(ce_argument_spec) + switchport = SwitchPort(argument_spec) + switchport.work() + + +if __name__ == '__main__': + main()