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