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