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_netstream_export
25short_description: Manages netstream export on HUAWEI CloudEngine switches.
26description:
27    - Configure NetStream flow statistics exporting and versions for exported packets on HUAWEI CloudEngine switches.
28author: Zhijin Zhou (@QijunPan)
29notes:
30    - Recommended connection is C(network_cli).
31    - This module also works with C(local) connections for legacy playbooks.
32options:
33    type:
34        description:
35            - Specifies NetStream feature.
36        required: true
37        choices: ['ip', 'vxlan']
38    source_ip:
39        description:
40            - Specifies source address which can be IPv6 or IPv4 of the exported NetStream packet.
41    host_ip:
42        description:
43            - Specifies destination address which can be IPv6 or IPv4 of the exported NetStream packet.
44    host_port:
45        description:
46            - Specifies the destination UDP port number of the exported packets.
47              The value is an integer that ranges from 1 to 65535.
48    host_vpn:
49        description:
50            - Specifies the VPN instance of the exported packets carrying flow statistics.
51              Ensure the VPN instance has been created on the device.
52    version:
53        description:
54            - Sets the version of exported packets.
55        choices: ['5', '9']
56    as_option:
57        description:
58            - Specifies the AS number recorded in the statistics as the original or the peer AS number.
59        choices: ['origin', 'peer']
60    bgp_nexthop:
61        description:
62            - Configures the statistics to carry BGP next hop information. Currently, only V9 supports the exported
63              packets carrying BGP next hop information.
64        choices: ['enable','disable']
65        default: 'disable'
66    state:
67        description:
68            - Manage the state of the resource.
69        choices: ['present','absent']
70        default: present
71'''
72
73EXAMPLES = '''
74- name: Netstream export module test
75  hosts: cloudengine
76  connection: local
77  gather_facts: no
78  vars:
79    cli:
80      host: "{{ inventory_hostname }}"
81      port: "{{ ansible_ssh_port }}"
82      username: "{{ username }}"
83      password: "{{ password }}"
84      transport: cli
85
86  tasks:
87
88  - name: Configures the source address for the exported packets carrying IPv4 flow statistics.
89    community.network.ce_netstream_export:
90      type: ip
91      source_ip: 192.8.2.2
92      provider: "{{ cli }}"
93
94  - name: Configures the source IP address for the exported packets carrying VXLAN flexible flow statistics.
95    community.network.ce_netstream_export:
96      type: vxlan
97      source_ip: 192.8.2.3
98      provider: "{{ cli }}"
99
100  - name: Configures the destination IP address and destination UDP port number for the exported packets carrying IPv4 flow statistics.
101    community.network.ce_netstream_export:
102      type: ip
103      host_ip: 192.8.2.4
104      host_port: 25
105      host_vpn: test
106      provider: "{{ cli }}"
107
108  - name: Configures the destination IP address and destination UDP port number for the exported packets carrying VXLAN flexible flow statistics.
109    community.network.ce_netstream_export:
110      type: vxlan
111      host_ip: 192.8.2.5
112      host_port: 26
113      host_vpn: test
114      provider: "{{ cli }}"
115
116  - name: Configures the version number of the exported packets carrying IPv4 flow statistics.
117    community.network.ce_netstream_export:
118      type: ip
119      version: 9
120      as_option: origin
121      bgp_nexthop: enable
122      provider: "{{ cli }}"
123
124  - name: Configures the version for the exported packets carrying VXLAN flexible flow statistics.
125    community.network.ce_netstream_export:
126      type: vxlan
127      version: 9
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                "as_option": "origin",
138                "bgp_nexthop": "enable",
139                "host_ip": "192.8.5.6",
140                "host_port": "26",
141                "host_vpn": "test",
142                "source_ip": "192.8.2.5",
143                "state": "present",
144                "type": "ip",
145                "version": "9"
146            }
147existing:
148    description: k/v pairs of existing attributes on the device
149    returned: always
150    type: dict
151    sample: {
152                "as_option": null,
153                "bgp_nexthop": "disable",
154                "host_ip": null,
155                "host_port": null,
156                "host_vpn": null,
157                "source_ip": null,
158                "type": "ip",
159                "version": null
160            }
161end_state:
162    description: k/v pairs of end attributes on the device
163    returned: always
164    type: dict
165    sample: {
166                "as_option": "origin",
167                "bgp_nexthop": "enable",
168                "host_ip": "192.8.5.6",
169                "host_port": "26",
170                "host_vpn": "test",
171                "source_ip": "192.8.2.5",
172                "type": "ip",
173                "version": "9"
174            }
175updates:
176    description: command list sent to the device
177    returned: always
178    type: list
179    sample: [
180                "netstream export ip source 192.8.2.5",
181                "netstream export ip host 192.8.5.6 26 vpn-instance test",
182                "netstream export ip version 9 origin-as bgp-nexthop"
183            ]
184changed:
185    description: check to see if a change was made on the device
186    returned: always
187    type: bool
188    sample: true
189'''
190
191import re
192from ansible.module_utils.basic import AnsibleModule
193from ansible_collections.community.network.plugins.module_utils.network.cloudengine.ce import exec_command, load_config
194from ansible_collections.community.network.plugins.module_utils.network.cloudengine.ce import ce_argument_spec
195
196
197def is_ipv4_addr(ip_addr):
198    """check ipaddress validate"""
199
200    rule1 = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
201    rule2 = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])'
202    ipv4_regex = '%s%s%s%s%s%s' % ('^', rule1, rule1, rule1, rule2, '$')
203
204    return bool(re.match(ipv4_regex, ip_addr))
205
206
207def is_config_exist(cmp_cfg, test_cfg):
208    """is configuration exist"""
209
210    test_cfg_tmp = test_cfg + ' *$' + '|' + test_cfg + ' *\n'
211    obj = re.compile(test_cfg_tmp)
212    result = re.findall(obj, cmp_cfg)
213    if not result:
214        return False
215    return True
216
217
218class NetstreamExport(object):
219    """Manage NetStream export"""
220
221    def __init__(self, argument_spec):
222        self.spec = argument_spec
223        self.module = None
224        self.__init_module__()
225
226        # NetStream export configuration parameters
227        self.type = self.module.params['type']
228        self.source_ip = self.module.params['source_ip']
229        self.host_ip = self.module.params['host_ip']
230        self.host_port = self.module.params['host_port']
231        self.host_vpn = self.module.params['host_vpn']
232        self.version = self.module.params['version']
233        self.as_option = self.module.params['as_option']
234        self.bgp_netxhop = self.module.params['bgp_nexthop']
235        self.state = self.module.params['state']
236
237        self.commands = list()
238        self.config = None
239        self.exist_conf = dict()
240
241        # state
242        self.changed = False
243        self.updates_cmd = list()
244        self.results = dict()
245        self.proposed = dict()
246        self.existing = dict()
247        self.end_state = dict()
248
249    def __init_module__(self):
250        """init module"""
251
252        self.module = AnsibleModule(
253            argument_spec=self.spec, supports_check_mode=True)
254
255    def cli_load_config(self, commands):
256        """load config by cli"""
257
258        if not self.module.check_mode:
259            load_config(self.module, commands)
260
261    def get_netstream_config(self):
262        """get current netstream configuration"""
263
264        cmd = "display current-configuration | include ^netstream export"
265        rc, out, err = exec_command(self.module, cmd)
266        if rc != 0:
267            self.module.fail_json(msg=err)
268        config = str(out).strip()
269        return config
270
271    def get_existing(self):
272        """get existing config"""
273
274        self.existing = dict(type=self.type,
275                             source_ip=self.exist_conf['source_ip'],
276                             host_ip=self.exist_conf['host_ip'],
277                             host_port=self.exist_conf['host_port'],
278                             host_vpn=self.exist_conf['host_vpn'],
279                             version=self.exist_conf['version'],
280                             as_option=self.exist_conf['as_option'],
281                             bgp_nexthop=self.exist_conf['bgp_netxhop'])
282
283    def get_proposed(self):
284        """get proposed config"""
285
286        self.proposed = dict(type=self.type,
287                             source_ip=self.source_ip,
288                             host_ip=self.host_ip,
289                             host_port=self.host_port,
290                             host_vpn=self.host_vpn,
291                             version=self.version,
292                             as_option=self.as_option,
293                             bgp_nexthop=self.bgp_netxhop,
294                             state=self.state)
295
296    def get_end_state(self):
297        """get end config"""
298        self.get_config_data()
299        self.end_state = dict(type=self.type,
300                              source_ip=self.exist_conf['source_ip'],
301                              host_ip=self.exist_conf['host_ip'],
302                              host_port=self.exist_conf['host_port'],
303                              host_vpn=self.exist_conf['host_vpn'],
304                              version=self.exist_conf['version'],
305                              as_option=self.exist_conf['as_option'],
306                              bgp_nexthop=self.exist_conf['bgp_netxhop'])
307
308    def show_result(self):
309        """show result"""
310
311        self.results['changed'] = self.changed
312        self.results['proposed'] = self.proposed
313        self.results['existing'] = self.existing
314        self.results['end_state'] = self.end_state
315        if self.changed:
316            self.results['updates'] = self.updates_cmd
317        else:
318            self.results['updates'] = list()
319
320        self.module.exit_json(**self.results)
321
322    def cli_add_command(self, command, undo=False):
323        """add command to self.update_cmd and self.commands"""
324
325        if undo and command.lower() not in ["quit", "return"]:
326            cmd = "undo " + command
327        else:
328            cmd = command
329
330        self.commands.append(cmd)          # set to device
331        if command.lower() not in ["quit", "return"]:
332            if cmd not in self.updates_cmd:
333                self.updates_cmd.append(cmd)   # show updates result
334
335    def config_nets_export_src_addr(self):
336        """Configures the source address for the exported packets"""
337
338        if is_ipv4_addr(self.source_ip):
339            if self.type == 'ip':
340                cmd = "netstream export ip source %s" % self.source_ip
341            else:
342                cmd = "netstream export vxlan inner-ip source %s" % self.source_ip
343        else:
344            if self.type == 'ip':
345                cmd = "netstream export ip source ipv6 %s" % self.source_ip
346            else:
347                cmd = "netstream export vxlan inner-ip source ipv6 %s" % self.source_ip
348
349        if is_config_exist(self.config, cmd):
350            self.exist_conf['source_ip'] = self.source_ip
351            if self.state == 'present':
352                return
353            else:
354                undo = True
355        else:
356            if self.state == 'absent':
357                return
358            else:
359                undo = False
360
361        self.cli_add_command(cmd, undo)
362
363    def config_nets_export_host_addr(self):
364        """Configures the destination IP address and destination UDP port number"""
365
366        if is_ipv4_addr(self.host_ip):
367            if self.type == 'ip':
368                cmd = 'netstream export ip host %s %s' % (self.host_ip, self.host_port)
369            else:
370                cmd = 'netstream export vxlan inner-ip host %s %s' % (self.host_ip, self.host_port)
371        else:
372            if self.type == 'ip':
373                cmd = 'netstream export ip host ipv6 %s %s' % (self.host_ip, self.host_port)
374            else:
375                cmd = 'netstream export vxlan inner-ip host ipv6 %s %s' % (self.host_ip, self.host_port)
376
377        if self.host_vpn:
378            cmd += " vpn-instance %s" % self.host_vpn
379
380        if is_config_exist(self.config, cmd):
381            self.exist_conf['host_ip'] = self.host_ip
382            self.exist_conf['host_port'] = self.host_port
383            if self.host_vpn:
384                self.exist_conf['host_vpn'] = self.host_vpn
385
386            if self.state == 'present':
387                return
388            else:
389                undo = True
390        else:
391            if self.state == 'absent':
392                return
393            else:
394                undo = False
395
396        self.cli_add_command(cmd, undo)
397
398    def config_nets_export_vxlan_ver(self):
399        """Configures the version for the exported packets carrying VXLAN flexible flow statistics"""
400
401        cmd = 'netstream export vxlan inner-ip version 9'
402
403        if is_config_exist(self.config, cmd):
404            self.exist_conf['version'] = self.version
405
406            if self.state == 'present':
407                return
408            else:
409                undo = True
410        else:
411            if self.state == 'absent':
412                return
413            else:
414                undo = False
415
416        self.cli_add_command(cmd, undo)
417
418    def config_nets_export_ip_ver(self):
419        """Configures the version number of the exported packets carrying IPv4 flow statistics"""
420
421        cmd = 'netstream export ip version %s' % self.version
422        if self.version == '5':
423            if self.as_option == 'origin':
424                cmd += ' origin-as'
425            elif self.as_option == 'peer':
426                cmd += ' peer-as'
427        else:
428            if self.as_option == 'origin':
429                cmd += ' origin-as'
430            elif self.as_option == 'peer':
431                cmd += ' peer-as'
432
433            if self.bgp_netxhop == 'enable':
434                cmd += ' bgp-nexthop'
435
436        if cmd == 'netstream export ip version 5':
437            cmd_tmp = "netstream export ip version"
438            if cmd_tmp in self.config:
439                if self.state == 'present':
440                    self.cli_add_command(cmd, False)
441            else:
442                self.exist_conf['version'] = self.version
443            return
444
445        if is_config_exist(self.config, cmd):
446            self.exist_conf['version'] = self.version
447            self.exist_conf['as_option'] = self.as_option
448            self.exist_conf['bgp_netxhop'] = self.bgp_netxhop
449
450            if self.state == 'present':
451                return
452            else:
453                undo = True
454        else:
455            if self.state == 'absent':
456                return
457            else:
458                undo = False
459
460        self.cli_add_command(cmd, undo)
461
462    def config_netstream_export(self):
463        """configure netstream export"""
464
465        if self.commands:
466            self.cli_load_config(self.commands)
467            self.changed = True
468
469    def check_params(self):
470        """Check all input params"""
471
472        if not self.type:
473            self.module.fail_json(msg='Error: The value of type cannot be empty.')
474
475        if self.host_port:
476            if not self.host_port.isdigit():
477                self.module.fail_json(msg='Error: Host port is invalid.')
478            if int(self.host_port) < 1 or int(self.host_port) > 65535:
479                self.module.fail_json(msg='Error: Host port is not in the range from 1 to 65535.')
480
481        if self.host_vpn:
482            if self.host_vpn == '_public_':
483                self.module.fail_json(
484                    msg='Error: The host vpn name _public_ is reserved.')
485            if len(self.host_vpn) < 1 or len(self.host_vpn) > 31:
486                self.module.fail_json(msg='Error: The host vpn name length is not in the range from 1 to 31.')
487
488        if self.type == 'vxlan' and self.version == '5':
489            self.module.fail_json(msg="Error: When type is vxlan, version must be 9.")
490
491        if self.type == 'ip' and self.version == '5' and self.bgp_netxhop == 'enable':
492            self.module.fail_json(msg="Error: When type=ip and version=5, bgp_netxhop is not supported.")
493
494        if (self.host_ip and not self.host_port) or (self.host_port and not self.host_ip):
495            self.module.fail_json(msg="Error: host_ip and host_port must both exist or not exist.")
496
497    def get_config_data(self):
498        """get configuration commands and current configuration"""
499
500        self.exist_conf['type'] = self.type
501        self.exist_conf['source_ip'] = None
502        self.exist_conf['host_ip'] = None
503        self.exist_conf['host_port'] = None
504        self.exist_conf['host_vpn'] = None
505        self.exist_conf['version'] = None
506        self.exist_conf['as_option'] = None
507        self.exist_conf['bgp_netxhop'] = 'disable'
508
509        self.config = self.get_netstream_config()
510
511        if self.type and self.source_ip:
512            self.config_nets_export_src_addr()
513
514        if self.type and self.host_ip and self.host_port:
515            self.config_nets_export_host_addr()
516
517        if self.type == 'vxlan' and self.version == '9':
518            self.config_nets_export_vxlan_ver()
519
520        if self.type == 'ip' and self.version:
521            self.config_nets_export_ip_ver()
522
523    def work(self):
524        """execute task"""
525
526        self.check_params()
527        self.get_proposed()
528        self.get_config_data()
529        self.get_existing()
530
531        self.config_netstream_export()
532
533        self.get_end_state()
534        self.show_result()
535
536
537def main():
538    """main function entry"""
539
540    argument_spec = dict(
541        type=dict(required=True, type='str', choices=['ip', 'vxlan']),
542        source_ip=dict(required=False, type='str'),
543        host_ip=dict(required=False, type='str'),
544        host_port=dict(required=False, type='str'),
545        host_vpn=dict(required=False, type='str'),
546        version=dict(required=False, type='str', choices=['5', '9']),
547        as_option=dict(required=False, type='str', choices=['origin', 'peer']),
548        bgp_nexthop=dict(required=False, type='str', choices=['enable', 'disable'], default='disable'),
549        state=dict(choices=['absent', 'present'], default='present', required=False)
550    )
551    argument_spec.update(ce_argument_spec)
552    netstream_export = NetstreamExport(argument_spec)
553    netstream_export.work()
554
555
556if __name__ == '__main__':
557    main()
558