#!/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 . # from __future__ import (absolute_import, division, print_function) __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: ce_ip_interface short_description: Manages L3 attributes for IPv4 and IPv6 interfaces on HUAWEI CloudEngine switches. description: - Manages Layer 3 attributes for IPv4 and IPv6 interfaces on HUAWEI CloudEngine switches. author: QijunPan (@QijunPan) notes: - Interface must already be a L3 port when using this module. - Logical interfaces (loopback, vlanif) must be created first. - C(mask) must be inserted in decimal format (i.e. 24) for both IPv6 and IPv4. - A single interface can have multiple IPv6 configured. - This module requires the netconf system service be enabled on the remote device being managed. - Recommended connection is C(netconf). - This module also works with C(local) connections for legacy playbooks. options: interface: description: - Full name of interface, i.e. 40GE1/0/22, vlanif10. required: true addr: description: - IPv4 or IPv6 Address. mask: description: - Subnet mask for IPv4 or IPv6 Address in decimal format. version: description: - IP address version. default: v4 choices: ['v4','v6'] ipv4_type: description: - Specifies an address type. The value is an enumerated type. main, primary IP address. sub, secondary IP address. default: main choices: ['main','sub'] state: description: - Specify desired state of the resource. default: present choices: ['present','absent'] ''' EXAMPLES = ''' - name: ip_interface 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 ipv4 address is configured on 10GE1/0/22 ce_ip_interface: interface: 10GE1/0/22 version: v4 state: present addr: 20.20.20.20 mask: 24 provider: '{{ cli }}' - name: Ensure ipv4 secondary address is configured on 10GE1/0/22 ce_ip_interface: interface: 10GE1/0/22 version: v4 state: present addr: 30.30.30.30 mask: 24 ipv4_type: sub provider: '{{ cli }}' - name: Ensure ipv6 is enabled on 10GE1/0/22 ce_ip_interface: interface: 10GE1/0/22 version: v6 state: present provider: '{{ cli }}' - name: Ensure ipv6 address is configured on 10GE1/0/22 ce_ip_interface: interface: 10GE1/0/22 version: v6 state: present addr: 2001::db8:800:200c:cccb mask: 64 provider: '{{ cli }}' ''' RETURN = ''' proposed: description: k/v pairs of parameters passed into module returned: always type: dict sample: {"addr": "20.20.20.20", "interface": "10GE1/0/22", "mask": "24"} existing: description: k/v pairs of existing IP attributes on the interface returned: always type: dict sample: {"ipv4": [{"ifIpAddr": "11.11.11.11", "subnetMask": "255.255.0.0", "addrType": "main"}], "interface": "10GE1/0/22"} end_state: description: k/v pairs of IP attributes after module execution returned: always type: dict sample: {"ipv4": [{"ifIpAddr": "20.20.20.20", "subnetMask": "255.255.255.0", "addrType": "main"}], "interface": "10GE1/0/22"} updates: description: commands sent to the device returned: always type: list sample: ["interface 10GE1/0/22", "ip address 20.20.20.20 24"] changed: description: check to see if a change was made on the device returned: always type: bool sample: true ''' import re from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec CE_NC_GET_INTF = """ %s """ CE_NC_ADD_IPV4 = """ %s %s %s %s """ CE_NC_MERGE_IPV4 = """ %s %s %s main %s %s main """ CE_NC_DEL_IPV4 = """ %s %s %s %s """ CE_NC_ADD_IPV6 = """ %s %s %s global """ CE_NC_DEL_IPV6 = """ %s %s %s global """ CE_NC_MERGE_IPV6_ENABLE = """ %s %s """ def get_interface_type(interface): """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" if interface is None: return None if interface.upper().startswith('GE'): return 'ge' elif interface.upper().startswith('10GE'): return '10ge' elif interface.upper().startswith('25GE'): return '25ge' elif interface.upper().startswith('4X10GE'): return '4x10ge' elif interface.upper().startswith('40GE'): return '40ge' elif interface.upper().startswith('100GE'): return '100ge' elif interface.upper().startswith('VLANIF'): return 'vlanif' elif interface.upper().startswith('LOOPBACK'): return 'loopback' elif interface.upper().startswith('METH'): return 'meth' elif interface.upper().startswith('ETH-TRUNK'): return 'eth-trunk' elif interface.upper().startswith('VBDIF'): return 'vbdif' elif interface.upper().startswith('NVE'): return 'nve' elif interface.upper().startswith('TUNNEL'): return 'tunnel' elif interface.upper().startswith('ETHERNET'): return 'ethernet' elif interface.upper().startswith('FCOE-PORT'): return 'fcoe-port' elif interface.upper().startswith('FABRIC-PORT'): return 'fabric-port' elif interface.upper().startswith('STACK-PORT'): return 'stack-port' elif interface.upper().startswith('NULL'): return 'null' else: return None def is_valid_v4addr(addr): """check is ipv4 addr is valid""" if not addr: return False if addr.find('.') != -1: addr_list = addr.split('.') if len(addr_list) != 4: return False for each_num in addr_list: if not each_num.isdigit(): return False if int(each_num) > 255: return False return True return False class IpInterface(object): """ Manages L3 attributes for IPv4 and IPv6 interfaces. """ def __init__(self, argument_spec): self.spec = argument_spec self.module = None self.__init_module__() # module input info] self.interface = self.module.params['interface'] self.addr = self.module.params['addr'] self.mask = self.module.params['mask'] self.version = self.module.params['version'] self.ipv4_type = self.module.params['ipv4_type'] self.state = self.module.params['state'] # state self.changed = False self.updates_cmd = list() self.results = dict() self.proposed = dict() self.existing = dict() self.end_state = dict() # interface info self.intf_info = dict() self.intf_type = None def __init_module__(self): """ init module """ required_if = [("version", "v4", ("addr", "mask"))] required_together = [("addr", "mask")] self.module = AnsibleModule( argument_spec=self.spec, required_if=required_if, required_together=required_together, supports_check_mode=True ) def netconf_set_config(self, xml_str, xml_name): """ netconf set config """ rcv_xml = set_nc_config(self.module, xml_str) if "" not in rcv_xml: 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_INTF % ifname rcv_xml = get_nc_config(self.module, conf_str) if "" in rcv_xml: return intf_info # get interface base info intf = re.findall( r'.*(.*).*\s*' r'(.*).*', rcv_xml) if intf: intf_info = dict(ifName=intf[0][0], isL2SwitchPort=intf[0][1]) # get interface ipv4 address info ipv4_info = re.findall( r'.*(.*).*\s*(.*)' r'.*\s*(.*).*', rcv_xml) intf_info["am4CfgAddr"] = list() for info in ipv4_info: intf_info["am4CfgAddr"].append( dict(ifIpAddr=info[0], subnetMask=info[1], addrType=info[2])) # get interface ipv6 address info ipv6_info = re.findall( r'.*.*\s*(.*).*', rcv_xml) if not ipv6_info: self.module.fail_json(msg='Error: Fail to get interface %s IPv6 state.' % self.interface) else: intf_info["enableFlag"] = ipv6_info[0] # get interface ipv6 enable info ipv6_info = re.findall( r'.*(.*).*\s*(.*)' r'.*\s*(.*).*', rcv_xml) intf_info["am6CfgAddr"] = list() for info in ipv6_info: intf_info["am6CfgAddr"].append( dict(ifIp6Addr=info[0], addrPrefixLen=info[1], addrType6=info[2])) return intf_info def convert_len_to_mask(self, masklen): """convert mask length to ip address mask, i.e. 24 to 255.255.255.0""" mask_int = ["0"] * 4 length = int(masklen) if length > 32: self.module.fail_json(msg='Error: IPv4 ipaddress mask length is invalid.') if length < 8: mask_int[0] = str(int((0xFF << (8 - length % 8)) & 0xFF)) if length >= 8: mask_int[0] = '255' mask_int[1] = str(int((0xFF << (16 - (length % 16))) & 0xFF)) if length >= 16: mask_int[1] = '255' mask_int[2] = str(int((0xFF << (24 - (length % 24))) & 0xFF)) if length >= 24: mask_int[2] = '255' mask_int[3] = str(int((0xFF << (32 - (length % 32))) & 0xFF)) if length == 32: mask_int[3] = '255' return '.'.join(mask_int) def is_ipv4_exist(self, addr, maskstr, ipv4_type): """"Check IPv4 address exist""" addrs = self.intf_info["am4CfgAddr"] if not addrs: return False for address in addrs: if address["ifIpAddr"] == addr: return address["subnetMask"] == maskstr and address["addrType"] == ipv4_type return False def get_ipv4_main_addr(self): """get IPv4 main address""" addrs = self.intf_info["am4CfgAddr"] if not addrs: return None for address in addrs: if address["addrType"] == "main": return address return None def is_ipv6_exist(self, addr, masklen): """Check IPv6 address exist""" addrs = self.intf_info["am6CfgAddr"] if not addrs: return False for address in addrs: if address["ifIp6Addr"] == addr.upper(): if address["addrPrefixLen"] == masklen and address["addrType6"] == "global": return True else: self.module.fail_json( msg="Error: Input IPv6 address or mask is invalid.") return False def set_ipv4_addr(self, ifname, addr, mask, ipv4_type): """Set interface IPv4 address""" if not addr or not mask or not type: return maskstr = self.convert_len_to_mask(mask) if self.state == "present": if not self.is_ipv4_exist(addr, maskstr, ipv4_type): # primary IP address if ipv4_type == "main": main_addr = self.get_ipv4_main_addr() if not main_addr: # no ipv4 main address in this interface xml_str = CE_NC_ADD_IPV4 % (ifname, addr, maskstr, ipv4_type) self.netconf_set_config(xml_str, "ADD_IPV4_ADDR") else: # remove old address and set new xml_str = CE_NC_MERGE_IPV4 % (ifname, main_addr["ifIpAddr"], main_addr["subnetMask"], addr, maskstr) self.netconf_set_config(xml_str, "MERGE_IPV4_ADDR") # secondary IP address else: xml_str = CE_NC_ADD_IPV4 % (ifname, addr, maskstr, ipv4_type) self.netconf_set_config(xml_str, "ADD_IPV4_ADDR") self.updates_cmd.append("interface %s" % ifname) if ipv4_type == "main": self.updates_cmd.append("ip address %s %s" % (addr, maskstr)) else: self.updates_cmd.append("ip address %s %s sub" % (addr, maskstr)) self.changed = True else: if self.is_ipv4_exist(addr, maskstr, ipv4_type): xml_str = CE_NC_DEL_IPV4 % (ifname, addr, maskstr, ipv4_type) self.netconf_set_config(xml_str, "DEL_IPV4_ADDR") self.updates_cmd.append("interface %s" % ifname) if ipv4_type == "main": self.updates_cmd.append("undo ip address %s %s" % (addr, maskstr)) else: self.updates_cmd.append("undo ip address %s %s sub" % (addr, maskstr)) self.changed = True def set_ipv6_addr(self, ifname, addr, mask): """Set interface IPv6 address""" if not addr or not mask: return if self.state == "present": self.updates_cmd.append("interface %s" % ifname) if self.intf_info["enableFlag"] == "false": xml_str = CE_NC_MERGE_IPV6_ENABLE % (ifname, "true") self.netconf_set_config(xml_str, "SET_IPV6_ENABLE") self.updates_cmd.append("ipv6 enable") self.changed = True if not self.is_ipv6_exist(addr, mask): xml_str = CE_NC_ADD_IPV6 % (ifname, addr, mask) self.netconf_set_config(xml_str, "ADD_IPV6_ADDR") self.updates_cmd.append("ipv6 address %s %s" % (addr, mask)) self.changed = True if not self.changed: self.updates_cmd.pop() else: if self.is_ipv6_exist(addr, mask): xml_str = CE_NC_DEL_IPV6 % (ifname, addr, mask) self.netconf_set_config(xml_str, "DEL_IPV6_ADDR") self.updates_cmd.append("interface %s" % ifname) self.updates_cmd.append( "undo ipv6 address %s %s" % (addr, mask)) self.changed = True def set_ipv6_enable(self, ifname): """Set interface IPv6 enable""" if self.state == "present": if self.intf_info["enableFlag"] == "false": xml_str = CE_NC_MERGE_IPV6_ENABLE % (ifname, "true") self.netconf_set_config(xml_str, "SET_IPV6_ENABLE") self.updates_cmd.append("interface %s" % ifname) self.updates_cmd.append("ipv6 enable") self.changed = True else: if self.intf_info["enableFlag"] == "true": xml_str = CE_NC_MERGE_IPV6_ENABLE % (ifname, "false") self.netconf_set_config(xml_str, "SET_IPV6_DISABLE") self.updates_cmd.append("interface %s" % ifname) self.updates_cmd.append("undo ipv6 enable") self.changed = True def check_params(self): """Check all input params""" # check interface type 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) # ipv4 addr and mask check if self.version == "v4": if not is_valid_v4addr(self.addr): self.module.fail_json( msg='Error: The %s is not a valid address.' % self.addr) if not self.mask.isdigit(): self.module.fail_json(msg='Error: mask is invalid.') if int(self.mask) > 32 or int(self.mask) < 1: self.module.fail_json( msg='Error: mask must be an integer between 1 and 32.') # ipv6 mask check if self.version == "v6": if self.addr: if not self.mask.isdigit(): self.module.fail_json(msg='Error: mask is invalid.') if int(self.mask) > 128 or int(self.mask) < 1: self.module.fail_json( msg='Error: mask must be an integer between 1 and 128.') # interface and layer3 check self.intf_info = self.get_interface_dict(self.interface) if not self.intf_info: self.module.fail_json(msg='Error: interface %s does not exist.' % self.interface) if self.intf_info["isL2SwitchPort"] == "true": self.module.fail_json(msg='Error: interface %s is layer2.' % self.interface) def get_proposed(self): """get proposed info""" self.proposed["state"] = self.state self.proposed["addr"] = self.addr self.proposed["mask"] = self.mask self.proposed["ipv4_type"] = self.ipv4_type self.proposed["version"] = self.version self.proposed["interface"] = self.interface def get_existing(self): """get existing info""" self.existing["interface"] = self.interface self.existing["ipv4addr"] = self.intf_info["am4CfgAddr"] self.existing["ipv6addr"] = self.intf_info["am6CfgAddr"] self.existing["ipv6enalbe"] = self.intf_info["enableFlag"] def get_end_state(self): """get end state info""" intf_info = self.get_interface_dict(self.interface) self.end_state["interface"] = self.interface self.end_state["ipv4addr"] = intf_info["am4CfgAddr"] self.end_state["ipv6addr"] = intf_info["am6CfgAddr"] self.end_state["ipv6enalbe"] = intf_info["enableFlag"] def work(self): """worker""" self.check_params() self.get_existing() self.get_proposed() # deal present or absent if self.version == "v4": self.set_ipv4_addr(self.interface, self.addr, self.mask, self.ipv4_type) else: if not self.addr and not self.mask: self.set_ipv6_enable(self.interface) else: self.set_ipv6_addr(self.interface, self.addr, self.mask) 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), addr=dict(required=False), version=dict(required=False, choices=['v4', 'v6'], default='v4'), mask=dict(type='str', required=False), ipv4_type=dict(required=False, choices=['main', 'sub'], default='main'), state=dict(required=False, default='present', choices=['present', 'absent']) ) argument_spec.update(ce_argument_spec) module = IpInterface(argument_spec) module.work() if __name__ == '__main__': main()