diff --git a/lib/ansible/modules/network/cloudengine/ce_bfd_session.py b/lib/ansible/modules/network/cloudengine/ce_bfd_session.py new file mode 100644 index 00000000000..ba9279b3a52 --- /dev/null +++ b/lib/ansible/modules/network/cloudengine/ce_bfd_session.py @@ -0,0 +1,641 @@ +#!/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 = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: ce_bfd_session +version_added: "2.4" +short_description: Manages BFD session configuration on HUAWEI CloudEngine devices. +description: + - Manages BFD session configuration, creates a BFD session or deletes a specified BFD session + on HUAWEI CloudEngine devices. +author: QijunPan (@CloudEngine-Ansible) +options: + session_name: + description: + - Specifies the name of a BFD session. + The value is a string of 1 to 15 case-sensitive characters without spaces. + required: true + default: null + create_type: + description: + - BFD session creation mode, the currently created BFD session + only supports static or static auto-negotiation mode. + required: false + default: null + choices: ['static', 'auto'] + addr_type: + description: + - Specifies the peer IP address type. + required: false + default: null + choices: ['ipv4'] + out_if_name: + description: + - Specifies the type and number of the interface bound to the BFD session. + required: false + default: null + dest_addr: + description: + - Specifies the peer IP address bound to the BFD session. + required: false + default: null + src_addr: + description: + - Indicates the source IP address carried in BFD packets. + required: false + default: null + vrf_name: + description: + - Specifies the name of a Virtual Private Network (VPN) instance that is bound to a BFD session. + The value is a string of 1 to 31 case-sensitive characters, spaces not supported. + When double quotation marks are used around the string, spaces are allowed in the string. + The value _public_ is reserved and cannot be used as the VPN instance name. + required: false + default: null + use_default_ip: + description: + - Indicates the default multicast IP address that is bound to a BFD session. + By default, BFD uses the multicast IP address 224.0.0.184. + You can set the multicast IP address by running the default-ip-address command. + The value is a bool type. + required: false + default: false + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = ''' +- name: bfd session 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: Configuring Single-hop BFD for Detecting Faults on a Layer 2 Link + ce_bfd_session: + session_name: bfd_l2link + use_default_ip: true + out_if_name: 10GE1/0/1 + provider: '{{ cli }}' + + - name: Configuring Single-Hop BFD on a VLANIF Interface + ce_bfd_session: + session_name: bfd_vlanif + dest_addr: 10.1.1.6 + out_if_name: Vlanif100 + provider: '{{ cli }}' + + - name: Configuring Multi-Hop BFD + ce_bfd_session: + session_name: bfd_multi_hop + dest_addr: 10.1.1.1 + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "addr_type": null, + "create_type": null, + "dest_addr": null, + "out_if_name": "10GE1/0/1", + "session_name": "bfd_l2link", + "src_addr": null, + "state": "present", + "use_default_ip": true, + "vrf_name": null + } +existing: + description: k/v pairs of existing configuration + returned: always + type: dict + sample: { + "session": {} + } +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: { + "session": { + "addrType": "IPV4", + "createType": "SESS_STATIC", + "destAddr": null, + "outIfName": "10GE1/0/1", + "sessName": "bfd_l2link", + "srcAddr": null, + "useDefaultIp": "true", + "vrfName": null + } + } +updates: + description: commands sent to the device + returned: always + type: list + sample: [ + "bfd bfd_l2link bind peer-ip default-ip interface 10ge1/0/1" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import sys +import socket +from xml.etree import ElementTree +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_BFD = """ + + + %s + + +""" + +CE_NC_GET_BFD_GLB = """ + + + + +""" + +CE_NC_GET_BFD_SESSION = """ + + + %s + + + + + + + + + +""" + + +def is_valid_ip_vpn(vpname): + """check ip vpn""" + + if not vpname: + return False + + if vpname == "_public_": + return False + + if len(vpname) < 1 or len(vpname) > 31: + return False + + return True + + +def check_ip_addr(ipaddr): + """check ip address, Supports IPv4 and IPv6""" + + if not ipaddr or '\x00' in ipaddr: + return False + + try: + res = socket.getaddrinfo(ipaddr, 0, socket.AF_UNSPEC, + socket.SOCK_STREAM, + 0, socket.AI_NUMERICHOST) + return bool(res) + except socket.gaierror: + err = sys.exc_info()[1] + if err.args[0] == socket.EAI_NONAME: + return False + raise + + return True + + +def check_default_ip(ipaddr): + """check the default multicast IP address""" + + # The value ranges from 224.0.0.107 to 224.0.0.250 + if not check_ip_addr(ipaddr): + return False + + if ipaddr.count(".") != 3: + return False + + ips = ipaddr.split(".") + if ips[0] != "224" or ips[1] != "0" or ips[2] != "0": + return False + + if not ips[3].isdigit() or int(ips[3]) < 107 or int(ips[3]) > 250: + return False + + return True + + +def get_interface_type(interface): + """get the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return 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() + + +class BfdSession(object): + """Manages BFD Session""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.session_name = self.module.params['session_name'] + self.create_type = self.module.params['create_type'] + self.addr_type = self.module.params['addr_type'] + self.out_if_name = self.module.params['out_if_name'] + self.dest_addr = self.module.params['dest_addr'] + self.src_addr = self.module.params['src_addr'] + self.vrf_name = self.module.params['vrf_name'] + self.use_default_ip = self.module.params['use_default_ip'] + self.state = self.module.params['state'] + + # 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.bfd_dict = dict() + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + + mutually_exclusive = [('use_default_ip', 'dest_addr')] + self.module = AnsibleModule(argument_spec=self.spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + def get_bfd_dict(self): + """bfd config dict""" + + bfd_dict = dict() + bfd_dict["global"] = dict() + bfd_dict["session"] = dict() + conf_str = CE_NC_GET_BFD % (CE_NC_GET_BFD_GLB + (CE_NC_GET_BFD_SESSION % self.session_name)) + + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return bfd_dict + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # get bfd global info + glb = root.find("data/bfd/bfdSchGlobal") + if glb: + for attr in glb: + bfd_dict["global"][attr.tag] = attr.text + + # get bfd session info + sess = root.find("data/bfd/bfdCfgSessions/bfdCfgSession") + if sess: + for attr in sess: + bfd_dict["session"][attr.tag] = attr.text + + return bfd_dict + + def is_session_match(self): + """is bfd session match""" + + if not self.bfd_dict["session"] or not self.session_name: + return False + + session = self.bfd_dict["session"] + if self.session_name != session.get("sessName", ""): + return False + + if self.create_type and self.create_type.upper() not in session.get("createType", "").upper(): + return False + + if self.addr_type and self.addr_type != session.get("addrType").lower(): + return False + + if self.dest_addr and self.dest_addr != session.get("destAddr"): + return False + + if self.src_addr and self.src_addr != session.get("srcAddr"): + return False + + if self.out_if_name: + if not session.get("outIfName"): + return False + if self.out_if_name.replace(" ", "").lower() != session.get("outIfName").replace(" ", "").lower(): + return False + + if self.vrf_name and self.vrf_name != session.get("vrfName"): + return False + + if str(self.use_default_ip).lower() != session.get("useDefaultIp"): + return False + + return True + + def config_session(self): + """configures bfd session""" + + xml_str = "" + cmd_list = list() + + if not self.session_name: + return xml_str + + if self.bfd_dict["global"].get("bfdEnable", "false") != "true": + self.module.fail_json(msg="Error: Please enable BFD globally first.") + + xml_str = "%s" % self.session_name + cmd_session = "bfd %s" % self.session_name + + if self.state == "present": + if not self.bfd_dict["session"]: + # Parameter check + if not self.dest_addr and not self.use_default_ip: + self.module.fail_json( + msg="Error: dest_addr or use_default_ip must be set when bfd session is creating.") + + # Creates a BFD session + if self.create_type: + xml_str += "SESS_%s" % self.create_type.upper() + else: + xml_str += "SESS_STATIC" + xml_str += "IP" + cmd_session += " bind" + if self.addr_type: + xml_str += "%s" % self.addr_type.upper() + else: + xml_str += "IPV4" + if self.dest_addr: + xml_str += "%s" % self.dest_addr + cmd_session += " peer-%s %s" % ("ipv6" if self.addr_type == "ipv6" else "ip", self.dest_addr) + if self.use_default_ip: + xml_str += "%s" % str(self.use_default_ip).lower() + cmd_session += " peer-ip default-ip" + if self.vrf_name: + xml_str += "%s" % self.vrf_name + cmd_session += " vpn-instance %s" % self.vrf_name + if self.out_if_name: + xml_str += "%s" % self.out_if_name + cmd_session += " interface %s" % self.out_if_name.lower() + if self.src_addr: + xml_str += "%s" % self.src_addr + cmd_session += " source-%s %s" % ("ipv6" if self.addr_type == "ipv6" else "ip", self.src_addr) + if self.create_type == "auto": + cmd_session += " auto" + + elif not self.is_session_match(): + # Bfd session is not match + self.module.fail_json(msg="Error: The specified BFD configuration view has been created.") + else: + pass + else: # absent + if not self.bfd_dict["session"]: + self.module.fail_json(msg="Error: BFD session is not exist.") + if not self.is_session_match(): + self.module.fail_json(msg="Error: BFD session parameter is invalid.") + + if self.state == "present": + if xml_str.endswith(""): + # no config update + return "" + else: + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + return '' + xml_str\ + + '' + else: # absent + cmd_list.append("undo " + cmd_session) + self.updates_cmd.extend(cmd_list) + return '' + xml_str\ + + '' + + def netconf_load_config(self, xml_str): + """load bfd config by netconf""" + + if not xml_str: + return + + xml_cfg = """ + + + %s + + """ % xml_str + set_nc_config(self.module, xml_cfg) + self.changed = True + + def check_params(self): + """Check all input params""" + + # check session_name + if not self.session_name: + self.module.fail_json(msg="Error: Missing required arguments: session_name.") + + if self.session_name: + if len(self.session_name) < 1 or len(self.session_name) > 15: + self.module.fail_json(msg="Error: Session name is invalid.") + + # check out_if_name + if self.out_if_name: + if not get_interface_type(self.out_if_name): + self.module.fail_json(msg="Error: Session out_if_name is invalid.") + + # check dest_addr + if self.dest_addr: + if not check_ip_addr(self.dest_addr): + self.module.fail_json(msg="Error: Session dest_addr is invalid.") + + # check src_addr + if self.src_addr: + if not check_ip_addr(self.src_addr): + self.module.fail_json(msg="Error: Session src_addr is invalid.") + + # check vrf_name + if self.vrf_name: + if not is_valid_ip_vpn(self.vrf_name): + self.module.fail_json(msg="Error: Session vrf_name is invalid.") + if not self.dest_addr: + self.module.fail_json(msg="Error: vrf_name and dest_addr must set at the same time.") + + # check use_default_ip + if self.use_default_ip and not self.out_if_name: + self.module.fail_json(msg="Error: use_default_ip and out_if_name must set at the same time.") + + def get_proposed(self): + """get proposed info""" + + # base config + self.proposed["session_name"] = self.session_name + self.proposed["create_type"] = self.create_type + self.proposed["addr_type"] = self.addr_type + self.proposed["out_if_name"] = self.out_if_name + self.proposed["dest_addr"] = self.dest_addr + self.proposed["src_addr"] = self.src_addr + self.proposed["vrf_name"] = self.vrf_name + self.proposed["use_default_ip"] = self.use_default_ip + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.bfd_dict: + return + + self.existing["session"] = self.bfd_dict.get("session") + + def get_end_state(self): + """get end state info""" + + bfd_dict = self.get_bfd_dict() + if not bfd_dict: + return + + self.end_state["session"] = bfd_dict.get("session") + + def work(self): + """worker""" + + self.check_params() + self.bfd_dict = self.get_bfd_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = '' + if self.session_name: + xml_str += self.config_session() + + # update to device + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + 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( + session_name=dict(required=True, type='str'), + create_type=dict(required=False, type='str', choices=['static', 'auto']), + addr_type=dict(required=False, type='str', choices=['ipv4']), + out_if_name=dict(required=False, type='str'), + dest_addr=dict(required=False, type='str'), + src_addr=dict(required=False, type='str'), + vrf_name=dict(required=False, type='str'), + use_default_ip=dict(required=False, type='bool', default=False), + state=dict(required=False, default='present', choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = BfdSession(argument_spec) + module.work() + + +if __name__ == '__main__': + main()