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