1#!/usr/bin/python
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17#
18
19ANSIBLE_METADATA = {'metadata_version': '1.1',
20                    'status': ['preview'],
21                    'supported_by': 'community'}
22
23DOCUMENTATION = """
24---
25module: ce_bfd_session
26version_added: "2.4"
27short_description: Manages BFD session configuration on HUAWEI CloudEngine devices.
28description:
29    - Manages BFD session configuration, creates a BFD session or deletes a specified BFD session
30      on HUAWEI CloudEngine devices.
31author: QijunPan (@QijunPan)
32notes:
33  - This module requires the netconf system service be enabled on the remote device being managed.
34  - Recommended connection is C(netconf).
35  - This module also works with C(local) connections for legacy playbooks.
36options:
37    session_name:
38        description:
39            - Specifies the name of a BFD session.
40              The value is a string of 1 to 15 case-sensitive characters without spaces.
41        required: true
42    create_type:
43        description:
44            - BFD session creation mode, the currently created BFD session
45              only supports static or static auto-negotiation mode.
46        choices: ['static', 'auto']
47        default: static
48    addr_type:
49        description:
50            - Specifies the peer IP address type.
51        choices: ['ipv4']
52    out_if_name:
53        description:
54            - Specifies the type and number of the interface bound to the BFD session.
55    dest_addr:
56        description:
57            - Specifies the peer IP address bound to the BFD session.
58    src_addr:
59        description:
60            - Indicates the source IP address carried in BFD packets.
61    local_discr:
62        version_added: 2.9
63        description:
64            - The BFD session local identifier does not need to be configured when the mode is auto.
65    remote_discr:
66        version_added: 2.9
67        description:
68            - The BFD session remote identifier does not need to be configured when the mode is auto.
69    vrf_name:
70        description:
71            - Specifies the name of a Virtual Private Network (VPN) instance that is bound to a BFD session.
72              The value is a string of 1 to 31 case-sensitive characters, spaces not supported.
73              When double quotation marks are used around the string, spaces are allowed in the string.
74              The value _public_ is reserved and cannot be used as the VPN instance name.
75    use_default_ip:
76        description:
77            - Indicates the default multicast IP address that is bound to a BFD session.
78              By default, BFD uses the multicast IP address 224.0.0.184.
79              You can set the multicast IP address by running the default-ip-address command.
80              The value is a bool type.
81        type: bool
82        default: 'no'
83    state:
84        description:
85            - Determines whether the config should be present or not on the device.
86        default: present
87        choices: ['present', 'absent']
88"""
89
90EXAMPLES = '''
91- name: bfd session module test
92  hosts: cloudengine
93  connection: local
94  gather_facts: no
95  vars:
96    cli:
97      host: "{{ inventory_hostname }}"
98      port: "{{ ansible_ssh_port }}"
99      username: "{{ username }}"
100      password: "{{ password }}"
101      transport: cli
102
103  tasks:
104  - name: Configuring Single-hop BFD for Detecting Faults on a Layer 2 Link
105    ce_bfd_session:
106      session_name: bfd_l2link
107      use_default_ip: true
108      out_if_name: 10GE1/0/1
109      local_discr: 163
110      remote_discr: 163
111      provider: '{{ cli }}'
112
113  - name: Configuring Single-Hop BFD on a VLANIF Interface
114    ce_bfd_session:
115      session_name: bfd_vlanif
116      dest_addr: 10.1.1.6
117      out_if_name: Vlanif100
118      local_discr: 163
119      remote_discr: 163
120      provider: '{{ cli }}'
121
122  - name: Configuring Multi-Hop BFD
123    ce_bfd_session:
124      session_name: bfd_multi_hop
125      dest_addr: 10.1.1.1
126      local_discr: 163
127      remote_discr: 163
128      provider: '{{ cli }}'
129'''
130
131RETURN = '''
132proposed:
133    description: k/v pairs of parameters passed into module
134    returned: always
135    type: dict
136    sample: {
137        "addr_type": null,
138        "create_type": null,
139        "dest_addr": null,
140        "out_if_name": "10GE1/0/1",
141        "session_name": "bfd_l2link",
142        "src_addr": null,
143        "state": "present",
144        "use_default_ip": true,
145        "vrf_name": null
146    }
147existing:
148    description: k/v pairs of existing configuration
149    returned: always
150    type: dict
151    sample: {
152        "session": {}
153    }
154end_state:
155    description: k/v pairs of configuration after module execution
156    returned: always
157    type: dict
158    sample: {
159        "session": {
160            "addrType": "IPV4",
161            "createType": "SESS_STATIC",
162            "destAddr": null,
163            "outIfName": "10GE1/0/1",
164            "sessName": "bfd_l2link",
165            "srcAddr": null,
166            "useDefaultIp": "true",
167            "vrfName": null
168        }
169    }
170updates:
171    description: commands sent to the device
172    returned: always
173    type: list
174    sample: [
175        "bfd bfd_l2link bind peer-ip default-ip interface 10ge1/0/1"
176    ]
177changed:
178    description: check to see if a change was made on the device
179    returned: always
180    type: bool
181    sample: true
182'''
183
184import sys
185import socket
186from xml.etree import ElementTree
187from ansible.module_utils.basic import AnsibleModule
188from ansible.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr
189
190
191CE_NC_GET_BFD = """
192    <filter type="subtree">
193      <bfd xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
194        <bfdSchGlobal>
195          <bfdEnable></bfdEnable>
196          <defaultIp></defaultIp>
197        </bfdSchGlobal>
198        <bfdCfgSessions>
199          <bfdCfgSession>
200            <sessName>%s</sessName>
201            <createType></createType>
202            <addrType></addrType>
203            <outIfName></outIfName>
204            <destAddr></destAddr>
205            <localDiscr></localDiscr>
206            <remoteDiscr></remoteDiscr>
207            <srcAddr></srcAddr>
208            <vrfName></vrfName>
209            <useDefaultIp></useDefaultIp>
210          </bfdCfgSession>
211        </bfdCfgSessions>
212      </bfd>
213    </filter>
214"""
215
216
217def is_valid_ip_vpn(vpname):
218    """check ip vpn"""
219
220    if not vpname:
221        return False
222
223    if vpname == "_public_":
224        return False
225
226    if len(vpname) < 1 or len(vpname) > 31:
227        return False
228
229    return True
230
231
232def check_default_ip(ipaddr):
233    """check the default multicast IP address"""
234
235    # The value ranges from 224.0.0.107 to 224.0.0.250
236    if not check_ip_addr(ipaddr):
237        return False
238
239    if ipaddr.count(".") != 3:
240        return False
241
242    ips = ipaddr.split(".")
243    if ips[0] != "224" or ips[1] != "0" or ips[2] != "0":
244        return False
245
246    if not ips[3].isdigit() or int(ips[3]) < 107 or int(ips[3]) > 250:
247        return False
248
249    return True
250
251
252def get_interface_type(interface):
253    """get the type of interface, such as 10GE, ETH-TRUNK, VLANIF..."""
254
255    if interface is None:
256        return None
257
258    if interface.upper().startswith('GE'):
259        iftype = 'ge'
260    elif interface.upper().startswith('10GE'):
261        iftype = '10ge'
262    elif interface.upper().startswith('25GE'):
263        iftype = '25ge'
264    elif interface.upper().startswith('4X10GE'):
265        iftype = '4x10ge'
266    elif interface.upper().startswith('40GE'):
267        iftype = '40ge'
268    elif interface.upper().startswith('100GE'):
269        iftype = '100ge'
270    elif interface.upper().startswith('VLANIF'):
271        iftype = 'vlanif'
272    elif interface.upper().startswith('LOOPBACK'):
273        iftype = 'loopback'
274    elif interface.upper().startswith('METH'):
275        iftype = 'meth'
276    elif interface.upper().startswith('ETH-TRUNK'):
277        iftype = 'eth-trunk'
278    elif interface.upper().startswith('VBDIF'):
279        iftype = 'vbdif'
280    elif interface.upper().startswith('NVE'):
281        iftype = 'nve'
282    elif interface.upper().startswith('TUNNEL'):
283        iftype = 'tunnel'
284    elif interface.upper().startswith('ETHERNET'):
285        iftype = 'ethernet'
286    elif interface.upper().startswith('FCOE-PORT'):
287        iftype = 'fcoe-port'
288    elif interface.upper().startswith('FABRIC-PORT'):
289        iftype = 'fabric-port'
290    elif interface.upper().startswith('STACK-PORT'):
291        iftype = 'stack-port'
292    elif interface.upper().startswith('NULL'):
293        iftype = 'null'
294    else:
295        return None
296
297    return iftype.lower()
298
299
300class BfdSession(object):
301    """Manages BFD Session"""
302
303    def __init__(self, argument_spec):
304        self.spec = argument_spec
305        self.module = None
306        self.__init_module__()
307
308        # module input info
309        self.session_name = self.module.params['session_name']
310        self.create_type = self.module.params['create_type']
311        self.addr_type = self.module.params['addr_type']
312        self.out_if_name = self.module.params['out_if_name']
313        self.dest_addr = self.module.params['dest_addr']
314        self.src_addr = self.module.params['src_addr']
315        self.vrf_name = self.module.params['vrf_name']
316        self.use_default_ip = self.module.params['use_default_ip']
317        self.state = self.module.params['state']
318        self.local_discr = self.module.params['local_discr']
319        self.remote_discr = self.module.params['remote_discr']
320        # host info
321        self.host = self.module.params['host']
322        self.username = self.module.params['username']
323        self.port = self.module.params['port']
324
325        # state
326        self.changed = False
327        self.bfd_dict = dict()
328        self.updates_cmd = list()
329        self.commands = list()
330        self.results = dict()
331        self.proposed = dict()
332        self.existing = dict()
333        self.end_state = dict()
334
335    def __init_module__(self):
336        """init module"""
337
338        mutually_exclusive = [('use_default_ip', 'dest_addr')]
339        self.module = AnsibleModule(argument_spec=self.spec,
340                                    mutually_exclusive=mutually_exclusive,
341                                    supports_check_mode=True)
342
343    def get_bfd_dict(self):
344        """bfd config dict"""
345
346        bfd_dict = dict()
347        bfd_dict["global"] = dict()
348        bfd_dict["session"] = dict()
349        conf_str = CE_NC_GET_BFD % self.session_name
350
351        xml_str = get_nc_config(self.module, conf_str)
352        if "<data/>" in xml_str:
353            return bfd_dict
354
355        xml_str = xml_str.replace('\r', '').replace('\n', '').\
356            replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\
357            replace('xmlns="http://www.huawei.com/netconf/vrp"', "")
358        root = ElementTree.fromstring(xml_str)
359
360        # get bfd global info
361        glb = root.find("bfd/bfdSchGlobal")
362        if glb:
363            for attr in glb:
364                bfd_dict["global"][attr.tag] = attr.text
365
366        # get bfd session info
367        sess = root.find("bfd/bfdCfgSessions/bfdCfgSession")
368        if sess:
369            for attr in sess:
370                bfd_dict["session"][attr.tag] = attr.text
371
372        return bfd_dict
373
374    def is_session_match(self):
375        """is bfd session match"""
376
377        if not self.bfd_dict["session"] or not self.session_name:
378            return False
379
380        session = self.bfd_dict["session"]
381        if self.session_name != session.get("sessName", ""):
382            return False
383
384        if self.create_type and self.create_type.upper() not in session.get("createType", "").upper():
385            return False
386
387        if self.addr_type and self.addr_type != session.get("addrType").lower():
388            return False
389
390        if self.dest_addr and self.dest_addr != session.get("destAddr"):
391            return False
392
393        if self.src_addr and self.src_addr != session.get("srcAddr"):
394            return False
395
396        if self.out_if_name:
397            if not session.get("outIfName"):
398                return False
399            if self.out_if_name.replace(" ", "").lower() != session.get("outIfName").replace(" ", "").lower():
400                return False
401
402        if self.vrf_name and self.vrf_name != session.get("vrfName"):
403            return False
404
405        if str(self.use_default_ip).lower() != session.get("useDefaultIp"):
406            return False
407
408        if self.create_type == "static" and self.state == "present":
409            if str(self.local_discr).lower() != session.get("localDiscr", ""):
410                return False
411            if str(self.remote_discr).lower() != session.get("remoteDiscr", ""):
412                return False
413
414        return True
415
416    def config_session(self):
417        """configures bfd session"""
418
419        xml_str = ""
420        cmd_list = list()
421        discr = list()
422
423        if not self.session_name:
424            return xml_str
425
426        if self.bfd_dict["global"].get("bfdEnable", "false") != "true":
427            self.module.fail_json(msg="Error: Please enable BFD globally first.")
428
429        xml_str = "<sessName>%s</sessName>" % self.session_name
430        cmd_session = "bfd %s" % self.session_name
431
432        if self.state == "present":
433            if not self.bfd_dict["session"]:
434                # Parameter check
435                if not self.dest_addr and not self.use_default_ip:
436                    self.module.fail_json(
437                        msg="Error: dest_addr or use_default_ip must be set when bfd session is creating.")
438
439                # Creates a BFD session
440                if self.create_type == "auto":
441                    xml_str += "<createType>SESS_%s</createType>" % self.create_type.upper()
442                else:
443                    xml_str += "<createType>SESS_STATIC</createType>"
444                xml_str += "<linkType>IP</linkType>"
445                cmd_session += " bind"
446                if self.addr_type:
447                    xml_str += "<addrType>%s</addrType>" % self.addr_type.upper()
448                else:
449                    xml_str += "<addrType>IPV4</addrType>"
450                if self.dest_addr:
451                    xml_str += "<destAddr>%s</destAddr>" % self.dest_addr
452                    cmd_session += " peer-%s %s" % ("ipv6" if self.addr_type == "ipv6" else "ip", self.dest_addr)
453                if self.use_default_ip:
454                    xml_str += "<useDefaultIp>%s</useDefaultIp>" % str(self.use_default_ip).lower()
455                    cmd_session += " peer-ip default-ip"
456                if self.vrf_name:
457                    xml_str += "<vrfName>%s</vrfName>" % self.vrf_name
458                    cmd_session += " vpn-instance %s" % self.vrf_name
459                if self.out_if_name:
460                    xml_str += "<outIfName>%s</outIfName>" % self.out_if_name
461                    cmd_session += " interface %s" % self.out_if_name.lower()
462                if self.src_addr:
463                    xml_str += "<srcAddr>%s</srcAddr>" % self.src_addr
464                    cmd_session += " source-%s %s" % ("ipv6" if self.addr_type == "ipv6" else "ip", self.src_addr)
465
466                if self.create_type == "auto":
467                    cmd_session += " auto"
468                else:
469                    xml_str += "<localDiscr>%s</localDiscr>" % self.local_discr
470                    discr.append("discriminator local %s" % self.local_discr)
471                    xml_str += "<remoteDiscr>%s</remoteDiscr>" % self.remote_discr
472                    discr.append("discriminator remote %s" % self.remote_discr)
473
474            elif not self.is_session_match():
475                # Bfd session is not match
476                self.module.fail_json(msg="Error: The specified BFD configuration view has been created.")
477            else:
478                pass
479        else:   # absent
480            if not self.bfd_dict["session"]:
481                self.module.fail_json(msg="Error: BFD session is not exist.")
482            if not self.is_session_match():
483                self.module.fail_json(msg="Error: BFD session parameter is invalid.")
484
485        if self.state == "present":
486            if xml_str.endswith("</sessName>"):
487                # no config update
488                return ""
489            else:
490                cmd_list.insert(0, cmd_session)
491                cmd_list.extend(discr)
492                self.updates_cmd.extend(cmd_list)
493                return '<bfdCfgSessions><bfdCfgSession operation="merge">' + xml_str\
494                       + '</bfdCfgSession></bfdCfgSessions>'
495        else:   # absent
496            cmd_list.append("undo " + cmd_session)
497            self.updates_cmd.extend(cmd_list)
498            return '<bfdCfgSessions><bfdCfgSession operation="delete">' + xml_str\
499                   + '</bfdCfgSession></bfdCfgSessions>'
500
501    def netconf_load_config(self, xml_str):
502        """load bfd config by netconf"""
503
504        if not xml_str:
505            return
506
507        xml_cfg = """
508            <config>
509            <bfd xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
510            %s
511            </bfd>
512            </config>""" % xml_str
513        set_nc_config(self.module, xml_cfg)
514        self.changed = True
515
516    def check_params(self):
517        """Check all input params"""
518
519        # check session_name
520        if not self.session_name:
521            self.module.fail_json(msg="Error: Missing required arguments: session_name.")
522
523        if self.session_name:
524            if len(self.session_name) < 1 or len(self.session_name) > 15:
525                self.module.fail_json(msg="Error: Session name is invalid.")
526
527        # check local_discr
528        # check remote_discr
529
530        if self.local_discr:
531            if self.local_discr < 1 or self.local_discr > 16384:
532                self.module.fail_json(msg="Error: Session local_discr is not ranges from 1 to 16384.")
533        if self.remote_discr:
534            if self.remote_discr < 1 or self.remote_discr > 4294967295:
535                self.module.fail_json(msg="Error: Session remote_discr is not ranges from 1 to 4294967295.")
536
537        if self.state == "present" and self.create_type == "static":
538            if not self.local_discr:
539                self.module.fail_json(msg="Error: Missing required arguments: local_discr.")
540            if not self.remote_discr:
541                self.module.fail_json(msg="Error: Missing required arguments: remote_discr.")
542
543        # check out_if_name
544        if self.out_if_name:
545            if not get_interface_type(self.out_if_name):
546                self.module.fail_json(msg="Error: Session out_if_name is invalid.")
547
548        # check dest_addr
549        if self.dest_addr:
550            if not check_ip_addr(self.dest_addr):
551                self.module.fail_json(msg="Error: Session dest_addr is invalid.")
552
553        # check src_addr
554        if self.src_addr:
555            if not check_ip_addr(self.src_addr):
556                self.module.fail_json(msg="Error: Session src_addr is invalid.")
557
558        # check vrf_name
559        if self.vrf_name:
560            if not is_valid_ip_vpn(self.vrf_name):
561                self.module.fail_json(msg="Error: Session vrf_name is invalid.")
562            if not self.dest_addr:
563                self.module.fail_json(msg="Error: vrf_name and dest_addr must set at the same time.")
564
565        # check use_default_ip
566        if self.use_default_ip and not self.out_if_name:
567            self.module.fail_json(msg="Error: use_default_ip and out_if_name must set at the same time.")
568
569    def get_proposed(self):
570        """get proposed info"""
571
572        # base config
573        self.proposed["session_name"] = self.session_name
574        self.proposed["create_type"] = self.create_type
575        self.proposed["addr_type"] = self.addr_type
576        self.proposed["out_if_name"] = self.out_if_name
577        self.proposed["dest_addr"] = self.dest_addr
578        self.proposed["src_addr"] = self.src_addr
579        self.proposed["vrf_name"] = self.vrf_name
580        self.proposed["use_default_ip"] = self.use_default_ip
581        self.proposed["state"] = self.state
582        self.proposed["local_discr"] = self.local_discr
583        self.proposed["remote_discr"] = self.remote_discr
584
585    def get_existing(self):
586        """get existing info"""
587
588        if not self.bfd_dict:
589            return
590
591        self.existing["session"] = self.bfd_dict.get("session")
592
593    def get_end_state(self):
594        """get end state info"""
595
596        bfd_dict = self.get_bfd_dict()
597        if not bfd_dict:
598            return
599
600        self.end_state["session"] = bfd_dict.get("session")
601        if self.end_state == self.existing:
602            self.changed = False
603
604    def work(self):
605        """worker"""
606
607        self.check_params()
608        self.bfd_dict = self.get_bfd_dict()
609        self.get_existing()
610        self.get_proposed()
611
612        # deal present or absent
613        xml_str = ''
614        if self.session_name:
615            xml_str += self.config_session()
616
617        # update to device
618        if xml_str:
619            self.netconf_load_config(xml_str)
620            self.changed = True
621
622        self.get_end_state()
623        self.results['changed'] = self.changed
624        self.results['proposed'] = self.proposed
625        self.results['existing'] = self.existing
626        self.results['end_state'] = self.end_state
627        if self.changed:
628            self.results['updates'] = self.updates_cmd
629        else:
630            self.results['updates'] = list()
631
632        self.module.exit_json(**self.results)
633
634
635def main():
636    """Module main"""
637
638    argument_spec = dict(
639        session_name=dict(required=True, type='str'),
640        create_type=dict(required=False, default='static', type='str', choices=['static', 'auto']),
641        addr_type=dict(required=False, type='str', choices=['ipv4']),
642        out_if_name=dict(required=False, type='str'),
643        dest_addr=dict(required=False, type='str'),
644        src_addr=dict(required=False, type='str'),
645        vrf_name=dict(required=False, type='str'),
646        use_default_ip=dict(required=False, type='bool', default=False),
647        state=dict(required=False, default='present', choices=['present', 'absent']),
648        local_discr=dict(required=False, type='int'),
649        remote_discr=dict(required=False, type='int')
650    )
651
652    argument_spec.update(ce_argument_spec)
653    module = BfdSession(argument_spec)
654    module.work()
655
656
657if __name__ == '__main__':
658    main()
659