1#!/usr/local/bin/python3.8
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
19from __future__ import (absolute_import, division, print_function)
20__metaclass__ = type
21
22DOCUMENTATION = '''
23---
24module: ce_bfd_global
25short_description: Manages BFD global configuration on HUAWEI CloudEngine devices.
26description:
27    - Manages BFD global configuration on HUAWEI CloudEngine devices.
28author: QijunPan (@QijunPan)
29notes:
30  - This module requires the netconf system service be enabled on the remote device being managed.
31  - Recommended connection is C(netconf).
32  - This module also works with C(local) connections for legacy playbooks.
33options:
34    bfd_enable:
35        description:
36            - Enables the global Bidirectional Forwarding Detection (BFD) function.
37        choices: ['enable', 'disable']
38    default_ip:
39        description:
40            - Specifies the default multicast IP address.
41              The value ranges from 224.0.0.107 to 224.0.0.250.
42    tos_exp_dynamic:
43        description:
44            - Indicates the priority of BFD control packets for dynamic BFD sessions.
45              The value is an integer ranging from 0 to 7.
46              The default priority is 7, which is the highest priority of BFD control packets.
47    tos_exp_static:
48        description:
49            - Indicates the priority of BFD control packets for static BFD sessions.
50              The value is an integer ranging from 0 to 7.
51              The default priority is 7, which is the highest priority of BFD control packets.
52    damp_init_wait_time:
53        description:
54            - Specifies an initial flapping suppression time for a BFD session.
55              The value is an integer ranging from 1 to 3600000, in milliseconds.
56              The default value is 2000.
57    damp_max_wait_time:
58        description:
59            - Specifies a maximum flapping suppression time for a BFD session.
60              The value is an integer ranging from 1 to 3600000, in milliseconds.
61              The default value is 15000.
62    damp_second_wait_time:
63        description:
64            - Specifies a secondary flapping suppression time for a BFD session.
65              The value is an integer ranging from 1 to 3600000, in milliseconds.
66              The default value is 5000.
67    delay_up_time:
68        description:
69            - Specifies the delay before a BFD session becomes Up.
70              The value is an integer ranging from 1 to 600, in seconds.
71              The default value is 0, indicating that a BFD session immediately becomes Up.
72    state:
73        description:
74            - Determines whether the config should be present or not on the device.
75        default: present
76        choices: ['present', 'absent']
77'''
78
79EXAMPLES = '''
80- name: Bfd global module test
81  hosts: cloudengine
82  connection: local
83  gather_facts: no
84  vars:
85    cli:
86      host: "{{ inventory_hostname }}"
87      port: "{{ ansible_ssh_port }}"
88      username: "{{ username }}"
89      password: "{{ password }}"
90      transport: cli
91
92  tasks:
93  - name: Enable the global BFD function
94    community.network.ce_bfd_global:
95      bfd_enable: enable
96      provider: '{{ cli }}'
97
98  - name: Set the default multicast IP address to 224.0.0.150
99    community.network.ce_bfd_global:
100      bfd_enable: enable
101      default_ip: 224.0.0.150
102      state: present
103      provider: '{{ cli }}'
104
105  - name: Set the priority of BFD control packets for dynamic and static BFD sessions
106    community.network.ce_bfd_global:
107      bfd_enable: enable
108      tos_exp_dynamic: 5
109      tos_exp_static: 6
110      state: present
111      provider: '{{ cli }}'
112
113  - name: Disable the global BFD function
114    community.network.ce_bfd_global:
115      bfd_enable: disable
116      provider: '{{ cli }}'
117'''
118
119RETURN = '''
120proposed:
121    description: k/v pairs of parameters passed into module
122    returned: verbose mode
123    type: dict
124    sample: {
125        "bfd_enalbe": "enable",
126        "damp_init_wait_time": null,
127        "damp_max_wait_time": null,
128        "damp_second_wait_time": null,
129        "default_ip": null,
130        "delayUpTimer": null,
131        "state": "present",
132        "tos_exp_dynamic": null,
133        "tos_exp_static": null
134    }
135existing:
136    description: k/v pairs of existing configuration
137    returned: verbose mode
138    type: dict
139    sample: {
140        "global": {
141            "bfdEnable": "false",
142            "dampInitWaitTime": "2000",
143            "dampMaxWaitTime": "12000",
144            "dampSecondWaitTime": "5000",
145            "defaultIp": "224.0.0.184",
146            "delayUpTimer": null,
147            "tosExp": "7",
148            "tosExpStatic": "7"
149        }
150    }
151end_state:
152    description: k/v pairs of configuration after module execution
153    returned: verbose mode
154    type: dict
155    sample: {
156        "global": {
157            "bfdEnable": "true",
158            "dampInitWaitTime": "2000",
159            "dampMaxWaitTime": "12000",
160            "dampSecondWaitTime": "5000",
161            "defaultIp": "224.0.0.184",
162            "delayUpTimer": null,
163            "tosExp": "7",
164            "tosExpStatic": "7"
165        }
166    }
167updates:
168    description: commands sent to the device
169    returned: always
170    type: list
171    sample: [ "bfd" ]
172changed:
173    description: check to see if a change was made on the device
174    returned: always
175    type: bool
176    sample: true
177'''
178
179import sys
180import socket
181from xml.etree import ElementTree
182from ansible.module_utils.basic import AnsibleModule
183from ansible_collections.community.network.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr
184
185CE_NC_GET_BFD = """
186    <filter type="subtree">
187      <bfd xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
188      %s
189      </bfd>
190    </filter>
191"""
192
193CE_NC_GET_BFD_GLB = """
194        <bfdSchGlobal>
195          <bfdEnable></bfdEnable>
196          <defaultIp></defaultIp>
197          <tosExp></tosExp>
198          <tosExpStatic></tosExpStatic>
199          <dampInitWaitTime></dampInitWaitTime>
200          <dampMaxWaitTime></dampMaxWaitTime>
201          <dampSecondWaitTime></dampSecondWaitTime>
202          <delayUpTimer></delayUpTimer>
203        </bfdSchGlobal>
204"""
205
206
207def check_default_ip(ipaddr):
208    """check the default multicast IP address"""
209
210    # The value ranges from 224.0.0.107 to 224.0.0.250
211    if not check_ip_addr(ipaddr):
212        return False
213
214    if ipaddr.count(".") != 3:
215        return False
216
217    ips = ipaddr.split(".")
218    if ips[0] != "224" or ips[1] != "0" or ips[2] != "0":
219        return False
220
221    if not ips[3].isdigit() or int(ips[3]) < 107 or int(ips[3]) > 250:
222        return False
223
224    return True
225
226
227class BfdGlobal(object):
228    """Manages BFD Global"""
229
230    def __init__(self, argument_spec):
231        self.spec = argument_spec
232        self.module = None
233        self.__init_module__()
234
235        # module input info
236        self.bfd_enable = self.module.params['bfd_enable']
237        self.default_ip = self.module.params['default_ip']
238        self.tos_exp_dynamic = self.module.params['tos_exp_dynamic']
239        self.tos_exp_static = self.module.params['tos_exp_static']
240        self.damp_init_wait_time = self.module.params['damp_init_wait_time']
241        self.damp_max_wait_time = self.module.params['damp_max_wait_time']
242        self.damp_second_wait_time = self.module.params['damp_second_wait_time']
243        self.delay_up_time = self.module.params['delay_up_time']
244        self.state = self.module.params['state']
245
246        # host info
247        self.host = self.module.params['host']
248        self.username = self.module.params['username']
249        self.port = self.module.params['port']
250
251        # state
252        self.changed = False
253        self.bfd_dict = dict()
254        self.updates_cmd = list()
255        self.commands = list()
256        self.results = dict()
257        self.proposed = dict()
258        self.existing = dict()
259        self.end_state = dict()
260
261    def __init_module__(self):
262        """init module"""
263
264        required_together = [('damp_init_wait_time', 'damp_max_wait_time', 'damp_second_wait_time')]
265        self.module = AnsibleModule(argument_spec=self.spec,
266                                    required_together=required_together,
267                                    supports_check_mode=True)
268
269    def get_bfd_dict(self):
270        """bfd config dict"""
271
272        bfd_dict = dict()
273        bfd_dict["global"] = dict()
274        conf_str = CE_NC_GET_BFD % CE_NC_GET_BFD_GLB
275
276        xml_str = get_nc_config(self.module, conf_str)
277        if "<data/>" in xml_str:
278            return bfd_dict
279
280        xml_str = xml_str.replace('\r', '').replace('\n', '').\
281            replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\
282            replace('xmlns="http://www.huawei.com/netconf/vrp"', "")
283        root = ElementTree.fromstring(xml_str)
284
285        # get bfd global info
286        glb = root.find("bfd/bfdSchGlobal")
287        if glb:
288            for attr in glb:
289                if attr.text is not None:
290                    bfd_dict["global"][attr.tag] = attr.text
291
292        return bfd_dict
293
294    def config_global(self):
295        """configures bfd global params"""
296
297        xml_str = ""
298        damp_chg = False
299
300        # bfd_enable
301        if self.bfd_enable:
302            if bool(self.bfd_dict["global"].get("bfdEnable", "false") == "true") != bool(self.bfd_enable == "enable"):
303                if self.bfd_enable == "enable":
304                    xml_str = "<bfdEnable>true</bfdEnable>"
305                    self.updates_cmd.append("bfd")
306                else:
307                    xml_str = "<bfdEnable>false</bfdEnable>"
308                    self.updates_cmd.append("undo bfd")
309
310        # get bfd end state
311        bfd_state = "disable"
312        if self.bfd_enable:
313            bfd_state = self.bfd_enable
314        elif self.bfd_dict["global"].get("bfdEnable", "false") == "true":
315            bfd_state = "enable"
316
317        # default_ip
318        if self.default_ip:
319            if bfd_state == "enable":
320                if self.state == "present" and self.default_ip != self.bfd_dict["global"].get("defaultIp"):
321                    xml_str += "<defaultIp>%s</defaultIp>" % self.default_ip
322                    if "bfd" not in self.updates_cmd:
323                        self.updates_cmd.append("bfd")
324                    self.updates_cmd.append("default-ip-address %s" % self.default_ip)
325                elif self.state == "absent" and self.default_ip == self.bfd_dict["global"].get("defaultIp"):
326                    xml_str += "<defaultIp/>"
327                    if "bfd" not in self.updates_cmd:
328                        self.updates_cmd.append("bfd")
329                    self.updates_cmd.append("undo default-ip-address")
330
331        # tos_exp_dynamic
332        if self.tos_exp_dynamic is not None:
333            if bfd_state == "enable":
334                if self.state == "present" and self.tos_exp_dynamic != int(self.bfd_dict["global"].get("tosExp", "7")):
335                    xml_str += "<tosExp>%s</tosExp>" % self.tos_exp_dynamic
336                    if "bfd" not in self.updates_cmd:
337                        self.updates_cmd.append("bfd")
338                    self.updates_cmd.append("tos-exp %s dynamic" % self.tos_exp_dynamic)
339                elif self.state == "absent" and self.tos_exp_dynamic == int(self.bfd_dict["global"].get("tosExp", "7")):
340                    xml_str += "<tosExp/>"
341                    if "bfd" not in self.updates_cmd:
342                        self.updates_cmd.append("bfd")
343                    self.updates_cmd.append("undo tos-exp dynamic")
344
345        # tos_exp_static
346        if self.tos_exp_static is not None:
347            if bfd_state == "enable":
348                if self.state == "present" \
349                        and self.tos_exp_static != int(self.bfd_dict["global"].get("tosExpStatic", "7")):
350                    xml_str += "<tosExpStatic>%s</tosExpStatic>" % self.tos_exp_static
351                    if "bfd" not in self.updates_cmd:
352                        self.updates_cmd.append("bfd")
353                    self.updates_cmd.append("tos-exp %s static" % self.tos_exp_static)
354                elif self.state == "absent" \
355                        and self.tos_exp_static == int(self.bfd_dict["global"].get("tosExpStatic", "7")):
356                    xml_str += "<tosExpStatic/>"
357                    if "bfd" not in self.updates_cmd:
358                        self.updates_cmd.append("bfd")
359                    self.updates_cmd.append("undo tos-exp static")
360
361        # delay_up_time
362        if self.delay_up_time is not None:
363            if bfd_state == "enable":
364                delay_time = self.bfd_dict["global"].get("delayUpTimer", "0")
365                if not delay_time or not delay_time.isdigit():
366                    delay_time = "0"
367                if self.state == "present" \
368                        and self.delay_up_time != int(delay_time):
369                    xml_str += "<delayUpTimer>%s</delayUpTimer>" % self.delay_up_time
370                    if "bfd" not in self.updates_cmd:
371                        self.updates_cmd.append("bfd")
372                    self.updates_cmd.append("delay-up %s" % self.delay_up_time)
373                elif self.state == "absent" \
374                        and self.delay_up_time == int(delay_time):
375                    xml_str += "<delayUpTimer/>"
376                    if "bfd" not in self.updates_cmd:
377                        self.updates_cmd.append("bfd")
378                    self.updates_cmd.append("undo delay-up")
379
380        # damp_init_wait_time damp_max_wait_time damp_second_wait_time
381        if self.damp_init_wait_time is not None and self.damp_second_wait_time is not None \
382                and self.damp_second_wait_time is not None:
383            if bfd_state == "enable":
384                if self.state == "present":
385                    if self.damp_max_wait_time != int(self.bfd_dict["global"].get("dampMaxWaitTime", "2000")):
386                        xml_str += "<dampMaxWaitTime>%s</dampMaxWaitTime>" % self.damp_max_wait_time
387                        damp_chg = True
388                    if self.damp_init_wait_time != int(self.bfd_dict["global"].get("dampInitWaitTime", "12000")):
389                        xml_str += "<dampInitWaitTime>%s</dampInitWaitTime>" % self.damp_init_wait_time
390                        damp_chg = True
391                    if self.damp_second_wait_time != int(self.bfd_dict["global"].get("dampSecondWaitTime", "5000")):
392                        xml_str += "<dampSecondWaitTime>%s</dampSecondWaitTime>" % self.damp_second_wait_time
393                        damp_chg = True
394                    if damp_chg:
395                        if "bfd" not in self.updates_cmd:
396                            self.updates_cmd.append("bfd")
397                        self.updates_cmd.append("dampening timer-interval maximum %s initial %s secondary %s" % (
398                            self.damp_max_wait_time, self.damp_init_wait_time, self.damp_second_wait_time))
399                else:
400                    damp_chg = True
401                    if self.damp_max_wait_time != int(self.bfd_dict["global"].get("dampMaxWaitTime", "2000")):
402                        damp_chg = False
403                    if self.damp_init_wait_time != int(self.bfd_dict["global"].get("dampInitWaitTime", "12000")):
404                        damp_chg = False
405                    if self.damp_second_wait_time != int(self.bfd_dict["global"].get("dampSecondWaitTime", "5000")):
406                        damp_chg = False
407
408                    if damp_chg:
409                        xml_str += "<dampMaxWaitTime/><dampInitWaitTime/><dampSecondWaitTime/>"
410                        if "bfd" not in self.updates_cmd:
411                            self.updates_cmd.append("bfd")
412                        self.updates_cmd.append("undo dampening timer-interval maximum %s initial %s secondary %s" % (
413                            self.damp_max_wait_time, self.damp_init_wait_time, self.damp_second_wait_time))
414        if xml_str:
415            return '<bfdSchGlobal operation="merge">' + xml_str + '</bfdSchGlobal>'
416        else:
417            return ""
418
419    def netconf_load_config(self, xml_str):
420        """load bfd config by netconf"""
421
422        if not xml_str:
423            return
424
425        xml_cfg = """
426            <config>
427            <bfd xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
428            %s
429            </bfd>
430            </config>""" % xml_str
431        set_nc_config(self.module, xml_cfg)
432        self.changed = True
433
434    def check_params(self):
435        """Check all input params"""
436
437        # check default_ip
438        if self.default_ip:
439            if not check_default_ip(self.default_ip):
440                self.module.fail_json(msg="Error: Default ip is invalid.")
441
442        # check tos_exp_dynamic
443        if self.tos_exp_dynamic is not None:
444            if self.tos_exp_dynamic < 0 or self.tos_exp_dynamic > 7:
445                self.module.fail_json(msg="Error: Session tos_exp_dynamic is not ranges from 0 to 7.")
446
447        # check tos_exp_static
448        if self.tos_exp_static is not None:
449            if self.tos_exp_static < 0 or self.tos_exp_static > 7:
450                self.module.fail_json(msg="Error: Session tos_exp_static is not ranges from 0 to 7.")
451
452        # check damp_init_wait_time
453        if self.damp_init_wait_time is not None:
454            if self.damp_init_wait_time < 1 or self.damp_init_wait_time > 3600000:
455                self.module.fail_json(msg="Error: Session damp_init_wait_time is not ranges from 1 to 3600000.")
456
457        # check damp_max_wait_time
458        if self.damp_max_wait_time is not None:
459            if self.damp_max_wait_time < 1 or self.damp_max_wait_time > 3600000:
460                self.module.fail_json(msg="Error: Session damp_max_wait_time is not ranges from 1 to 3600000.")
461
462        # check damp_second_wait_time
463        if self.damp_second_wait_time is not None:
464            if self.damp_second_wait_time < 1 or self.damp_second_wait_time > 3600000:
465                self.module.fail_json(msg="Error: Session damp_second_wait_time is not ranges from 1 to 3600000.")
466
467        # check delay_up_time
468        if self.delay_up_time is not None:
469            if self.delay_up_time < 1 or self.delay_up_time > 600:
470                self.module.fail_json(msg="Error: Session delay_up_time is not ranges from 1 to 600.")
471
472    def get_proposed(self):
473        """get proposed info"""
474
475        self.proposed["bfd_enalbe"] = self.bfd_enable
476        self.proposed["default_ip"] = self.default_ip
477        self.proposed["tos_exp_dynamic"] = self.tos_exp_dynamic
478        self.proposed["tos_exp_static"] = self.tos_exp_static
479        self.proposed["damp_init_wait_time"] = self.damp_init_wait_time
480        self.proposed["damp_max_wait_time"] = self.damp_max_wait_time
481        self.proposed["damp_second_wait_time"] = self.damp_second_wait_time
482        self.proposed["delay_up_time"] = self.delay_up_time
483        self.proposed["state"] = self.state
484
485    def get_existing(self):
486        """get existing info"""
487
488        if not self.bfd_dict:
489            return
490
491        self.existing["global"] = self.bfd_dict.get("global")
492
493    def get_end_state(self):
494        """get end state info"""
495
496        bfd_dict = self.get_bfd_dict()
497        if not bfd_dict:
498            return
499
500        self.end_state["global"] = bfd_dict.get("global")
501        if self.existing == self.end_state:
502            self.changed = False
503
504    def work(self):
505        """worker"""
506
507        self.check_params()
508        self.bfd_dict = self.get_bfd_dict()
509        self.get_existing()
510        self.get_proposed()
511
512        # deal present or absent
513        xml_str = self.config_global()
514
515        # update to device
516        if xml_str:
517            self.netconf_load_config(xml_str)
518            self.changed = True
519
520        self.get_end_state()
521        self.results['changed'] = self.changed
522        self.results['proposed'] = self.proposed
523        self.results['existing'] = self.existing
524        self.results['end_state'] = self.end_state
525        if self.changed:
526            self.results['updates'] = self.updates_cmd
527        else:
528            self.results['updates'] = list()
529
530        self.module.exit_json(**self.results)
531
532
533def main():
534    """Module main"""
535
536    argument_spec = dict(
537        bfd_enable=dict(required=False, type='str', choices=['enable', 'disable']),
538        default_ip=dict(required=False, type='str'),
539        tos_exp_dynamic=dict(required=False, type='int'),
540        tos_exp_static=dict(required=False, type='int'),
541        damp_init_wait_time=dict(required=False, type='int'),
542        damp_max_wait_time=dict(required=False, type='int'),
543        damp_second_wait_time=dict(required=False, type='int'),
544        delay_up_time=dict(required=False, type='int'),
545        state=dict(required=False, default='present', choices=['present', 'absent'])
546    )
547
548    argument_spec.update(ce_argument_spec)
549    module = BfdGlobal(argument_spec)
550    module.work()
551
552
553if __name__ == '__main__':
554    main()
555