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_sflow 25short_description: Manages sFlow configuration on HUAWEI CloudEngine switches. 26description: 27 - Configure Sampled Flow (sFlow) to monitor traffic on an interface in real time, 28 detect abnormal traffic, and locate the source of attack traffic, 29 ensuring stable running of the network. 30author: QijunPan (@QijunPan) 31notes: 32 - This module requires the netconf system service be enabled on the remote device being managed. 33 - Recommended connection is C(netconf). 34 - This module also works with C(local) connections for legacy playbooks. 35options: 36 agent_ip: 37 description: 38 - Specifies the IPv4/IPv6 address of an sFlow agent. 39 source_ip: 40 description: 41 - Specifies the source IPv4/IPv6 address of sFlow packets. 42 collector_id: 43 description: 44 - Specifies the ID of an sFlow collector. This ID is used when you specify 45 the collector in subsequent sFlow configuration. 46 choices: ['1', '2'] 47 collector_ip: 48 description: 49 - Specifies the IPv4/IPv6 address of the sFlow collector. 50 collector_ip_vpn: 51 description: 52 - Specifies the name of a VPN instance. 53 The value is a string of 1 to 31 case-sensitive characters, spaces not supported. 54 When double quotation marks are used around the string, spaces are allowed in the string. 55 The value C(_public_) is reserved and cannot be used as the VPN instance name. 56 collector_datagram_size: 57 description: 58 - Specifies the maximum length of sFlow packets sent from an sFlow agent to an sFlow collector. 59 The value is an integer, in bytes. It ranges from 1024 to 8100. The default value is 1400. 60 collector_udp_port: 61 description: 62 - Specifies the UDP destination port number of sFlow packets. 63 The value is an integer that ranges from 1 to 65535. The default value is 6343. 64 collector_meth: 65 description: 66 - Configures the device to send sFlow packets through service interfaces, 67 enhancing the sFlow packet forwarding capability. 68 The enhanced parameter is optional. No matter whether you configure the enhanced mode, 69 the switch determines to send sFlow packets through service cards or management port 70 based on the routing information on the collector. 71 When the value is meth, the device forwards sFlow packets at the control plane. 72 When the value is enhanced, the device forwards sFlow packets at the forwarding plane to 73 enhance the sFlow packet forwarding capacity. 74 choices: ['meth', 'enhanced'] 75 collector_description: 76 description: 77 - Specifies the description of an sFlow collector. 78 The value is a string of 1 to 255 case-sensitive characters without spaces. 79 sflow_interface: 80 description: 81 - Full name of interface for Flow Sampling or Counter. 82 It must be a physical interface, Eth-Trunk, or Layer 2 subinterface. 83 sample_collector: 84 description: 85 - Indicates the ID list of the collector. 86 sample_rate: 87 description: 88 - Specifies the flow sampling rate in the format 1/rate. 89 The value is an integer and ranges from 1 to 4294967295. The default value is 8192. 90 sample_length: 91 description: 92 - Specifies the maximum length of sampled packets. 93 The value is an integer and ranges from 18 to 512, in bytes. The default value is 128. 94 sample_direction: 95 description: 96 - Enables flow sampling in the inbound or outbound direction. 97 choices: ['inbound', 'outbound', 'both'] 98 counter_collector: 99 description: 100 - Indicates the ID list of the counter collector. 101 counter_interval: 102 description: 103 - Indicates the counter sampling interval. 104 The value is an integer that ranges from 10 to 4294967295, in seconds. The default value is 20. 105 export_route: 106 description: 107 - Configures the sFlow packets sent by the switch not to carry routing information. 108 choices: ['enable', 'disable'] 109 state: 110 description: 111 - Determines whether the config should be present or not 112 on the device. 113 default: present 114 choices: ['present', 'absent'] 115''' 116 117EXAMPLES = ''' 118--- 119 120- name: Sflow module test 121 hosts: ce128 122 connection: local 123 gather_facts: no 124 vars: 125 cli: 126 host: "{{ inventory_hostname }}" 127 port: "{{ ansible_ssh_port }}" 128 username: "{{ username }}" 129 password: "{{ password }}" 130 transport: cli 131 132 tasks: 133 - name: Configuring sFlow Agent 134 community.network.ce_sflow: 135 agent_ip: 6.6.6.6 136 provider: '{{ cli }}' 137 138 - name: Configuring sFlow Collector 139 community.network.ce_sflow: 140 collector_id: 1 141 collector_ip: 7.7.7.7 142 collector_ip_vpn: vpn1 143 collector_description: Collector1 144 provider: '{{ cli }}' 145 146 - name: Configure flow sampling. 147 community.network.ce_sflow: 148 sflow_interface: 10GE2/0/2 149 sample_collector: 1 150 sample_direction: inbound 151 provider: '{{ cli }}' 152 153 - name: Configure counter sampling. 154 community.network.ce_sflow: 155 sflow_interface: 10GE2/0/2 156 counter_collector: 1 157 counter_interval: 1000 158 provider: '{{ cli }}' 159''' 160 161RETURN = ''' 162proposed: 163 description: k/v pairs of parameters passed into module 164 returned: verbose mode 165 type: dict 166 sample: {"agent_ip": "6.6.6.6", "state": "present"} 167existing: 168 description: k/v pairs of existing configuration 169 returned: verbose mode 170 type: dict 171 sample: {"agent": {}} 172end_state: 173 description: k/v pairs of configuration after module execution 174 returned: verbose mode 175 type: dict 176 sample: {"agent": {"family": "ipv4", "ipv4Addr": "1.2.3.4", "ipv6Addr": null}} 177updates: 178 description: commands sent to the device 179 returned: always 180 type: list 181 sample: ["sflow agent ip 6.6.6.6"] 182changed: 183 description: check to see if a change was made on the device 184 returned: always 185 type: bool 186 sample: true 187''' 188 189import re 190from xml.etree import ElementTree 191from ansible.module_utils.basic import AnsibleModule 192from ansible_collections.community.network.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr 193 194CE_NC_GET_SFLOW = """ 195<filter type="subtree"> 196<sflow xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0"> 197 <sources> 198 <source> 199 <family></family> 200 <ipv4Addr></ipv4Addr> 201 <ipv6Addr></ipv6Addr> 202 </source> 203 </sources> 204 <agents> 205 <agent> 206 <family></family> 207 <ipv4Addr></ipv4Addr> 208 <ipv6Addr></ipv6Addr> 209 </agent> 210 </agents> 211 <collectors> 212 <collector> 213 <collectorID></collectorID> 214 <family></family> 215 <ipv4Addr></ipv4Addr> 216 <ipv6Addr></ipv6Addr> 217 <vrfName></vrfName> 218 <datagramSize></datagramSize> 219 <port></port> 220 <description></description> 221 <meth></meth> 222 </collector> 223 </collectors> 224 <samplings> 225 <sampling> 226 <ifName>%s</ifName> 227 <collectorID></collectorID> 228 <direction></direction> 229 <length></length> 230 <rate></rate> 231 </sampling> 232 </samplings> 233 <counters> 234 <counter> 235 <ifName>%s</ifName> 236 <collectorID></collectorID> 237 <interval></interval> 238 </counter> 239 </counters> 240 <exports> 241 <export> 242 <ExportRoute></ExportRoute> 243 </export> 244 </exports> 245</sflow> 246</filter> 247""" 248 249 250def is_config_exist(cmp_cfg, test_cfg): 251 """is configuration exist?""" 252 253 if not cmp_cfg or not test_cfg: 254 return False 255 256 return bool(test_cfg in cmp_cfg) 257 258 259def is_valid_ip_vpn(vpname): 260 """check ip vpn""" 261 262 if not vpname: 263 return False 264 265 if vpname == "_public_": 266 return False 267 268 if len(vpname) < 1 or len(vpname) > 31: 269 return False 270 271 return True 272 273 274def get_ip_version(address): 275 """get ip version fast""" 276 277 if not address: 278 return None 279 280 if address.count(':') >= 2 and address.count(":") <= 7: 281 return "ipv6" 282 elif address.count('.') == 3: 283 return "ipv4" 284 else: 285 return None 286 287 288def get_interface_type(interface): 289 """get the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" 290 291 if interface is None: 292 return None 293 294 if interface.upper().startswith('GE'): 295 iftype = 'ge' 296 elif interface.upper().startswith('10GE'): 297 iftype = '10ge' 298 elif interface.upper().startswith('25GE'): 299 iftype = '25ge' 300 elif interface.upper().startswith('4X10GE'): 301 iftype = '4x10ge' 302 elif interface.upper().startswith('40GE'): 303 iftype = '40ge' 304 elif interface.upper().startswith('100GE'): 305 iftype = '100ge' 306 elif interface.upper().startswith('VLANIF'): 307 iftype = 'vlanif' 308 elif interface.upper().startswith('LOOPBACK'): 309 iftype = 'loopback' 310 elif interface.upper().startswith('METH'): 311 iftype = 'meth' 312 elif interface.upper().startswith('ETH-TRUNK'): 313 iftype = 'eth-trunk' 314 elif interface.upper().startswith('VBDIF'): 315 iftype = 'vbdif' 316 elif interface.upper().startswith('NVE'): 317 iftype = 'nve' 318 elif interface.upper().startswith('TUNNEL'): 319 iftype = 'tunnel' 320 elif interface.upper().startswith('ETHERNET'): 321 iftype = 'ethernet' 322 elif interface.upper().startswith('FCOE-PORT'): 323 iftype = 'fcoe-port' 324 elif interface.upper().startswith('FABRIC-PORT'): 325 iftype = 'fabric-port' 326 elif interface.upper().startswith('STACK-PORT'): 327 iftype = 'stack-port' 328 elif interface.upper().startswith('NULL'): 329 iftype = 'null' 330 else: 331 return None 332 333 return iftype.lower() 334 335 336class Sflow(object): 337 """Manages sFlow""" 338 339 def __init__(self, argument_spec): 340 self.spec = argument_spec 341 self.module = None 342 self.__init_module__() 343 344 # module input info 345 self.agent_ip = self.module.params['agent_ip'] 346 self.agent_version = None 347 self.source_ip = self.module.params['source_ip'] 348 self.source_version = None 349 self.export_route = self.module.params['export_route'] 350 self.collector_id = self.module.params['collector_id'] 351 self.collector_ip = self.module.params['collector_ip'] 352 self.collector_version = None 353 self.collector_ip_vpn = self.module.params['collector_ip_vpn'] 354 self.collector_datagram_size = self.module.params['collector_datagram_size'] 355 self.collector_udp_port = self.module.params['collector_udp_port'] 356 self.collector_meth = self.module.params['collector_meth'] 357 self.collector_description = self.module.params['collector_description'] 358 self.sflow_interface = self.module.params['sflow_interface'] 359 self.sample_collector = self.module.params['sample_collector'] or list() 360 self.sample_rate = self.module.params['sample_rate'] 361 self.sample_length = self.module.params['sample_length'] 362 self.sample_direction = self.module.params['sample_direction'] 363 self.counter_collector = self.module.params['counter_collector'] or list() 364 self.counter_interval = self.module.params['counter_interval'] 365 self.state = self.module.params['state'] 366 367 # state 368 self.config = "" # current config 369 self.sflow_dict = dict() 370 self.changed = False 371 self.updates_cmd = list() 372 self.commands = list() 373 self.results = dict() 374 self.proposed = dict() 375 self.existing = dict() 376 self.end_state = dict() 377 378 def __init_module__(self): 379 """init module""" 380 381 required_together = [("collector_id", "collector_ip")] 382 self.module = AnsibleModule( 383 argument_spec=self.spec, required_together=required_together, supports_check_mode=True) 384 385 def check_response(self, con_obj, xml_name): 386 """Check if response message is already succeed""" 387 388 xml_str = con_obj.xml 389 if "<ok/>" not in xml_str: 390 self.module.fail_json(msg='Error: %s failed.' % xml_name) 391 392 def netconf_set_config(self, xml_str, xml_name): 393 """netconf set config""" 394 395 rcv_xml = set_nc_config(self.module, xml_str) 396 if "<ok/>" not in rcv_xml: 397 self.module.fail_json(msg='Error: %s failed.' % xml_name) 398 399 def get_sflow_dict(self): 400 """ sflow config dict""" 401 402 sflow_dict = dict(source=list(), agent=dict(), collector=list(), 403 sampling=dict(), counter=dict(), export=dict()) 404 conf_str = CE_NC_GET_SFLOW % ( 405 self.sflow_interface, self.sflow_interface) 406 407 if not self.collector_meth: 408 conf_str = conf_str.replace("<meth></meth>", "") 409 410 rcv_xml = get_nc_config(self.module, conf_str) 411 412 if "<data/>" in rcv_xml: 413 return sflow_dict 414 415 xml_str = rcv_xml.replace('\r', '').replace('\n', '').\ 416 replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ 417 replace('xmlns="http://www.huawei.com/netconf/vrp"', "") 418 root = ElementTree.fromstring(xml_str) 419 420 # get source info 421 srcs = root.findall("sflow/sources/source") 422 if srcs: 423 for src in srcs: 424 attrs = dict() 425 for attr in src: 426 if attr.tag in ["family", "ipv4Addr", "ipv6Addr"]: 427 attrs[attr.tag] = attr.text 428 sflow_dict["source"].append(attrs) 429 430 # get agent info 431 agent = root.find("sflow/agents/agent") 432 if agent: 433 for attr in agent: 434 if attr.tag in ["family", "ipv4Addr", "ipv6Addr"]: 435 sflow_dict["agent"][attr.tag] = attr.text 436 437 # get collector info 438 collectors = root.findall("sflow/collectors/collector") 439 if collectors: 440 for collector in collectors: 441 attrs = dict() 442 for attr in collector: 443 if attr.tag in ["collectorID", "family", "ipv4Addr", "ipv6Addr", 444 "vrfName", "datagramSize", "port", "description", "meth"]: 445 attrs[attr.tag] = attr.text 446 sflow_dict["collector"].append(attrs) 447 448 # get sampling info 449 sample = root.find("sflow/samplings/sampling") 450 if sample: 451 for attr in sample: 452 if attr.tag in ["ifName", "collectorID", "direction", "length", "rate"]: 453 sflow_dict["sampling"][attr.tag] = attr.text 454 455 # get counter info 456 counter = root.find("sflow/counters/counter") 457 if counter: 458 for attr in counter: 459 if attr.tag in ["ifName", "collectorID", "interval"]: 460 sflow_dict["counter"][attr.tag] = attr.text 461 462 # get export info 463 export = root.find("sflow/exports/export") 464 if export: 465 for attr in export: 466 if attr.tag == "ExportRoute": 467 sflow_dict["export"][attr.tag] = attr.text 468 469 return sflow_dict 470 471 def config_agent(self): 472 """configures sFlow agent""" 473 474 xml_str = '' 475 if not self.agent_ip: 476 return xml_str 477 478 self.agent_version = get_ip_version(self.agent_ip) 479 if not self.agent_version: 480 self.module.fail_json(msg="Error: agent_ip is invalid.") 481 482 if self.state == "present": 483 if self.agent_ip != self.sflow_dict["agent"].get("ipv4Addr") \ 484 and self.agent_ip != self.sflow_dict["agent"].get("ipv6Addr"): 485 xml_str += '<agents><agent operation="merge">' 486 xml_str += '<family>%s</family>' % self.agent_version 487 if self.agent_version == "ipv4": 488 xml_str += '<ipv4Addr>%s</ipv4Addr>' % self.agent_ip 489 self.updates_cmd.append("sflow agent ip %s" % self.agent_ip) 490 else: 491 xml_str += '<ipv6Addr>%s</ipv6Addr>' % self.agent_ip 492 self.updates_cmd.append("sflow agent ipv6 %s" % self.agent_ip) 493 xml_str += '</agent></agents>' 494 495 else: 496 if self.agent_ip == self.sflow_dict["agent"].get("ipv4Addr") \ 497 or self.agent_ip == self.sflow_dict["agent"].get("ipv6Addr"): 498 xml_str += '<agents><agent operation="delete"></agent></agents>' 499 self.updates_cmd.append("undo sflow agent") 500 501 return xml_str 502 503 def config_source(self): 504 """configures the source IP address for sFlow packets""" 505 506 xml_str = '' 507 if not self.source_ip: 508 return xml_str 509 510 self.source_version = get_ip_version(self.source_ip) 511 if not self.source_version: 512 self.module.fail_json(msg="Error: source_ip is invalid.") 513 514 src_dict = dict() 515 for src in self.sflow_dict["source"]: 516 if src.get("family") == self.source_version: 517 src_dict = src 518 break 519 520 if self.state == "present": 521 if self.source_ip != src_dict.get("ipv4Addr") \ 522 and self.source_ip != src_dict.get("ipv6Addr"): 523 xml_str += '<sources><source operation="merge">' 524 xml_str += '<family>%s</family>' % self.source_version 525 if self.source_version == "ipv4": 526 xml_str += '<ipv4Addr>%s</ipv4Addr>' % self.source_ip 527 self.updates_cmd.append("sflow source ip %s" % self.source_ip) 528 else: 529 xml_str += '<ipv6Addr>%s</ipv6Addr>' % self.source_ip 530 self.updates_cmd.append( 531 "sflow source ipv6 %s" % self.source_ip) 532 xml_str += '</source ></sources>' 533 else: 534 if self.source_ip == src_dict.get("ipv4Addr"): 535 xml_str += '<sources><source operation="delete"><family>ipv4</family></source ></sources>' 536 self.updates_cmd.append("undo sflow source ip %s" % self.source_ip) 537 elif self.source_ip == src_dict.get("ipv6Addr"): 538 xml_str += '<sources><source operation="delete"><family>ipv6</family></source ></sources>' 539 self.updates_cmd.append("undo sflow source ipv6 %s" % self.source_ip) 540 541 return xml_str 542 543 def config_collector(self): 544 """creates an sFlow collector and sets or modifies optional parameters for the sFlow collector""" 545 546 xml_str = '' 547 if not self.collector_id: 548 return xml_str 549 550 if self.state == "present" and not self.collector_ip: 551 return xml_str 552 553 if self.collector_ip: 554 self.collector_version = get_ip_version(self.collector_ip) 555 if not self.collector_version: 556 self.module.fail_json(msg="Error: collector_ip is invalid.") 557 558 # get collector dict 559 exist_dict = dict() 560 for collector in self.sflow_dict["collector"]: 561 if collector.get("collectorID") == self.collector_id: 562 exist_dict = collector 563 break 564 565 change = False 566 if self.state == "present": 567 if not exist_dict: 568 change = True 569 elif self.collector_version != exist_dict.get("family"): 570 change = True 571 elif self.collector_version == "ipv4" and self.collector_ip != exist_dict.get("ipv4Addr"): 572 change = True 573 elif self.collector_version == "ipv6" and self.collector_ip != exist_dict.get("ipv6Addr"): 574 change = True 575 elif self.collector_ip_vpn and self.collector_ip_vpn != exist_dict.get("vrfName"): 576 change = True 577 elif not self.collector_ip_vpn and exist_dict.get("vrfName") != "_public_": 578 change = True 579 elif self.collector_udp_port and self.collector_udp_port != exist_dict.get("port"): 580 change = True 581 elif not self.collector_udp_port and exist_dict.get("port") != "6343": 582 change = True 583 elif self.collector_datagram_size and self.collector_datagram_size != exist_dict.get("datagramSize"): 584 change = True 585 elif not self.collector_datagram_size and exist_dict.get("datagramSize") != "1400": 586 change = True 587 elif self.collector_meth and self.collector_meth != exist_dict.get("meth"): 588 change = True 589 elif not self.collector_meth and exist_dict.get("meth") and exist_dict.get("meth") != "meth": 590 change = True 591 elif self.collector_description and self.collector_description != exist_dict.get("description"): 592 change = True 593 elif not self.collector_description and exist_dict.get("description"): 594 change = True 595 else: 596 pass 597 else: # absent 598 # collector not exist 599 if not exist_dict: 600 return xml_str 601 if self.collector_version and self.collector_version != exist_dict.get("family"): 602 return xml_str 603 if self.collector_version == "ipv4" and self.collector_ip != exist_dict.get("ipv4Addr"): 604 return xml_str 605 if self.collector_version == "ipv6" and self.collector_ip != exist_dict.get("ipv6Addr"): 606 return xml_str 607 if self.collector_ip_vpn and self.collector_ip_vpn != exist_dict.get("vrfName"): 608 return xml_str 609 if self.collector_udp_port and self.collector_udp_port != exist_dict.get("port"): 610 return xml_str 611 if self.collector_datagram_size and self.collector_datagram_size != exist_dict.get("datagramSize"): 612 return xml_str 613 if self.collector_meth and self.collector_meth != exist_dict.get("meth"): 614 return xml_str 615 if self.collector_description and self.collector_description != exist_dict.get("description"): 616 return xml_str 617 change = True 618 619 if not change: 620 return xml_str 621 622 # update or delete 623 if self.state == "absent": 624 xml_str += '<collectors><collector operation="delete"><collectorID>%s</collectorID>' % self.collector_id 625 self.updates_cmd.append("undo collector %s" % self.collector_id) 626 else: 627 xml_str += '<collectors><collector operation="merge"><collectorID>%s</collectorID>' % self.collector_id 628 cmd = "sflow collector %s" % self.collector_id 629 xml_str += '<family>%s</family>' % self.collector_version 630 if self.collector_version == "ipv4": 631 cmd += " ip %s" % self.collector_ip 632 xml_str += '<ipv4Addr>%s</ipv4Addr>' % self.collector_ip 633 else: 634 cmd += " ipv6 %s" % self.collector_ip 635 xml_str += '<ipv6Addr>%s</ipv6Addr>' % self.collector_ip 636 if self.collector_ip_vpn: 637 cmd += " vpn-instance %s" % self.collector_ip_vpn 638 xml_str += '<vrfName>%s</vrfName>' % self.collector_ip_vpn 639 if self.collector_datagram_size: 640 cmd += " length %s" % self.collector_datagram_size 641 xml_str += '<datagramSize>%s</datagramSize>' % self.collector_datagram_size 642 if self.collector_udp_port: 643 cmd += " udp-port %s" % self.collector_udp_port 644 xml_str += '<port>%s</port>' % self.collector_udp_port 645 if self.collector_description: 646 cmd += " description %s" % self.collector_description 647 xml_str += '<description>%s</description>' % self.collector_description 648 else: 649 xml_str += '<description></description>' 650 if self.collector_meth: 651 if self.collector_meth == "enhanced": 652 cmd += " enhanced" 653 xml_str += '<meth>%s</meth>' % self.collector_meth 654 self.updates_cmd.append(cmd) 655 656 xml_str += "</collector></collectors>" 657 658 return xml_str 659 660 def config_sampling(self): 661 """configure sflow sampling on an interface""" 662 663 xml_str = '' 664 if not self.sflow_interface: 665 return xml_str 666 667 if not self.sflow_dict["sampling"] and self.state == "absent": 668 return xml_str 669 670 self.updates_cmd.append("interface %s" % self.sflow_interface) 671 if self.state == "present": 672 xml_str += '<samplings><sampling operation="merge"><ifName>%s</ifName>' % self.sflow_interface 673 else: 674 xml_str += '<samplings><sampling operation="delete"><ifName>%s</ifName>' % self.sflow_interface 675 676 # sample_collector 677 if self.sample_collector: 678 if self.sflow_dict["sampling"].get("collectorID") \ 679 and self.sflow_dict["sampling"].get("collectorID") != "invalid": 680 existing = self.sflow_dict["sampling"].get("collectorID").split(',') 681 else: 682 existing = list() 683 684 if self.state == "present": 685 diff = list(set(self.sample_collector) - set(existing)) 686 if diff: 687 self.updates_cmd.append( 688 "sflow sampling collector %s" % ' '.join(diff)) 689 new_set = list(self.sample_collector + existing) 690 xml_str += '<collectorID>%s</collectorID>' % ','.join(list(set(new_set))) 691 else: 692 same = list(set(self.sample_collector) & set(existing)) 693 if same: 694 self.updates_cmd.append( 695 "undo sflow sampling collector %s" % ' '.join(same)) 696 xml_str += '<collectorID>%s</collectorID>' % ','.join(list(set(same))) 697 698 # sample_rate 699 if self.sample_rate: 700 exist = bool(self.sample_rate == self.sflow_dict["sampling"].get("rate")) 701 if self.state == "present" and not exist: 702 self.updates_cmd.append( 703 "sflow sampling rate %s" % self.sample_rate) 704 xml_str += '<rate>%s</rate>' % self.sample_rate 705 elif self.state == "absent" and exist: 706 self.updates_cmd.append( 707 "undo sflow sampling rate %s" % self.sample_rate) 708 xml_str += '<rate>%s</rate>' % self.sample_rate 709 710 # sample_length 711 if self.sample_length: 712 exist = bool(self.sample_length == self.sflow_dict["sampling"].get("length")) 713 if self.state == "present" and not exist: 714 self.updates_cmd.append( 715 "sflow sampling length %s" % self.sample_length) 716 xml_str += '<length>%s</length>' % self.sample_length 717 elif self.state == "absent" and exist: 718 self.updates_cmd.append( 719 "undo sflow sampling length %s" % self.sample_length) 720 xml_str += '<length>%s</length>' % self.sample_length 721 722 # sample_direction 723 if self.sample_direction: 724 direction = list() 725 if self.sample_direction == "both": 726 direction = ["inbound", "outbound"] 727 else: 728 direction.append(self.sample_direction) 729 existing = list() 730 if self.sflow_dict["sampling"].get("direction"): 731 if self.sflow_dict["sampling"].get("direction") == "both": 732 existing = ["inbound", "outbound"] 733 else: 734 existing.append( 735 self.sflow_dict["sampling"].get("direction")) 736 737 if self.state == "present": 738 diff = list(set(direction) - set(existing)) 739 if diff: 740 new_set = list(set(direction + existing)) 741 self.updates_cmd.append( 742 "sflow sampling %s" % ' '.join(diff)) 743 if len(new_set) > 1: 744 new_dir = "both" 745 else: 746 new_dir = new_set[0] 747 xml_str += '<direction>%s</direction>' % new_dir 748 else: 749 same = list(set(existing) & set(direction)) 750 if same: 751 self.updates_cmd.append("undo sflow sampling %s" % ' '.join(same)) 752 if len(same) > 1: 753 del_dir = "both" 754 else: 755 del_dir = same[0] 756 xml_str += '<direction>%s</direction>' % del_dir 757 758 if xml_str.endswith("</ifName>"): 759 self.updates_cmd.pop() 760 return "" 761 762 xml_str += '</sampling></samplings>' 763 764 return xml_str 765 766 def config_counter(self): 767 """configures sflow counter on an interface""" 768 769 xml_str = '' 770 if not self.sflow_interface: 771 return xml_str 772 773 if not self.sflow_dict["counter"] and self.state == "absent": 774 return xml_str 775 776 self.updates_cmd.append("interface %s" % self.sflow_interface) 777 if self.state == "present": 778 xml_str += '<counters><counter operation="merge"><ifName>%s</ifName>' % self.sflow_interface 779 else: 780 xml_str += '<counters><counter operation="delete"><ifName>%s</ifName>' % self.sflow_interface 781 782 # counter_collector 783 if self.counter_collector: 784 if self.sflow_dict["counter"].get("collectorID") \ 785 and self.sflow_dict["counter"].get("collectorID") != "invalid": 786 existing = self.sflow_dict["counter"].get("collectorID").split(',') 787 else: 788 existing = list() 789 790 if self.state == "present": 791 diff = list(set(self.counter_collector) - set(existing)) 792 if diff: 793 self.updates_cmd.append("sflow counter collector %s" % ' '.join(diff)) 794 new_set = list(self.counter_collector + existing) 795 xml_str += '<collectorID>%s</collectorID>' % ','.join(list(set(new_set))) 796 else: 797 same = list(set(self.counter_collector) & set(existing)) 798 if same: 799 self.updates_cmd.append( 800 "undo sflow counter collector %s" % ' '.join(same)) 801 xml_str += '<collectorID>%s</collectorID>' % ','.join(list(set(same))) 802 803 # counter_interval 804 if self.counter_interval: 805 exist = bool(self.counter_interval == self.sflow_dict["counter"].get("interval")) 806 if self.state == "present" and not exist: 807 self.updates_cmd.append( 808 "sflow counter interval %s" % self.counter_interval) 809 xml_str += '<interval>%s</interval>' % self.counter_interval 810 elif self.state == "absent" and exist: 811 self.updates_cmd.append( 812 "undo sflow counter interval %s" % self.counter_interval) 813 xml_str += '<interval>%s</interval>' % self.counter_interval 814 815 if xml_str.endswith("</ifName>"): 816 self.updates_cmd.pop() 817 return "" 818 819 xml_str += '</counter></counters>' 820 821 return xml_str 822 823 def config_export(self): 824 """configure sflow export""" 825 826 xml_str = '' 827 if not self.export_route: 828 return xml_str 829 830 if self.export_route == "enable": 831 if self.sflow_dict["export"] and self.sflow_dict["export"].get("ExportRoute") == "disable": 832 xml_str = '<exports><export operation="delete"><ExportRoute>disable</ExportRoute></export></exports>' 833 self.updates_cmd.append("undo sflow export extended-route-data disable") 834 else: # disable 835 if not self.sflow_dict["export"] or self.sflow_dict["export"].get("ExportRoute") != "disable": 836 xml_str = '<exports><export operation="create"><ExportRoute>disable</ExportRoute></export></exports>' 837 self.updates_cmd.append("sflow export extended-route-data disable") 838 839 return xml_str 840 841 def netconf_load_config(self, xml_str): 842 """load sflow config by netconf""" 843 844 if not xml_str: 845 return 846 847 xml_cfg = """ 848 <config> 849 <sflow xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0"> 850 %s 851 </sflow> 852 </config>""" % xml_str 853 854 self.netconf_set_config(xml_cfg, "SET_SFLOW") 855 self.changed = True 856 857 def check_params(self): 858 """Check all input params""" 859 860 # check agent_ip 861 if self.agent_ip: 862 self.agent_ip = self.agent_ip.upper() 863 if not check_ip_addr(self.agent_ip): 864 self.module.fail_json(msg="Error: agent_ip is invalid.") 865 866 # check source_ip 867 if self.source_ip: 868 self.source_ip = self.source_ip.upper() 869 if not check_ip_addr(self.source_ip): 870 self.module.fail_json(msg="Error: source_ip is invalid.") 871 872 # check collector 873 if self.collector_id: 874 # check collector_ip and collector_ip_vpn 875 if self.collector_ip: 876 self.collector_ip = self.collector_ip.upper() 877 if not check_ip_addr(self.collector_ip): 878 self.module.fail_json( 879 msg="Error: collector_ip is invalid.") 880 if self.collector_ip_vpn and not is_valid_ip_vpn(self.collector_ip_vpn): 881 self.module.fail_json( 882 msg="Error: collector_ip_vpn is invalid.") 883 884 # check collector_datagram_size ranges from 1024 to 8100 885 if self.collector_datagram_size: 886 if not self.collector_datagram_size.isdigit(): 887 self.module.fail_json( 888 msg="Error: collector_datagram_size is not digit.") 889 if int(self.collector_datagram_size) < 1024 or int(self.collector_datagram_size) > 8100: 890 self.module.fail_json( 891 msg="Error: collector_datagram_size is not ranges from 1024 to 8100.") 892 893 # check collector_udp_port ranges from 1 to 65535 894 if self.collector_udp_port: 895 if not self.collector_udp_port.isdigit(): 896 self.module.fail_json( 897 msg="Error: collector_udp_port is not digit.") 898 if int(self.collector_udp_port) < 1 or int(self.collector_udp_port) > 65535: 899 self.module.fail_json( 900 msg="Error: collector_udp_port is not ranges from 1 to 65535.") 901 902 # check collector_description 1 to 255 case-sensitive characters 903 if self.collector_description: 904 if self.collector_description.count(" "): 905 self.module.fail_json( 906 msg="Error: collector_description should without spaces.") 907 if len(self.collector_description) < 1 or len(self.collector_description) > 255: 908 self.module.fail_json( 909 msg="Error: collector_description is not ranges from 1 to 255.") 910 911 # check sflow_interface 912 if self.sflow_interface: 913 intf_type = get_interface_type(self.sflow_interface) 914 if not intf_type: 915 self.module.fail_json(msg="Error: intf_type is invalid.") 916 if intf_type not in ['ge', '10ge', '25ge', '4x10ge', '40ge', '100ge', 'eth-trunk']: 917 self.module.fail_json( 918 msg="Error: interface %s is not support sFlow." % self.sflow_interface) 919 920 # check sample_collector 921 if self.sample_collector: 922 self.sample_collector.sort() 923 if self.sample_collector not in [["1"], ["2"], ["1", "2"]]: 924 self.module.fail_json( 925 msg="Error: sample_collector is invalid.") 926 927 # check sample_rate ranges from 1 to 4294967295 928 if self.sample_rate: 929 if not self.sample_rate.isdigit(): 930 self.module.fail_json( 931 msg="Error: sample_rate is not digit.") 932 if int(self.sample_rate) < 1 or int(self.sample_rate) > 4294967295: 933 self.module.fail_json( 934 msg="Error: sample_rate is not ranges from 1 to 4294967295.") 935 936 # check sample_length ranges from 18 to 512 937 if self.sample_length: 938 if not self.sample_length.isdigit(): 939 self.module.fail_json( 940 msg="Error: sample_rate is not digit.") 941 if int(self.sample_length) < 18 or int(self.sample_length) > 512: 942 self.module.fail_json( 943 msg="Error: sample_length is not ranges from 18 to 512.") 944 945 # check counter_collector 946 if self.counter_collector: 947 self.counter_collector.sort() 948 if self.counter_collector not in [["1"], ["2"], ["1", "2"]]: 949 self.module.fail_json( 950 msg="Error: counter_collector is invalid.") 951 952 # counter_interval ranges from 10 to 4294967295 953 if self.counter_interval: 954 if not self.counter_interval.isdigit(): 955 self.module.fail_json( 956 msg="Error: counter_interval is not digit.") 957 if int(self.counter_interval) < 10 or int(self.counter_interval) > 4294967295: 958 self.module.fail_json( 959 msg="Error: sample_length is not ranges from 10 to 4294967295.") 960 961 def get_proposed(self): 962 """get proposed info""" 963 964 # base config 965 if self.agent_ip: 966 self.proposed["agent_ip"] = self.agent_ip 967 if self.source_ip: 968 self.proposed["source_ip"] = self.source_ip 969 if self.export_route: 970 self.proposed["export_route"] = self.export_route 971 if self.collector_id: 972 self.proposed["collector_id"] = self.collector_id 973 if self.collector_ip: 974 self.proposed["collector_ip"] = self.collector_ip 975 self.proposed["collector_ip_vpn"] = self.collector_ip_vpn 976 if self.collector_datagram_size: 977 self.proposed[ 978 "collector_datagram_size"] = self.collector_datagram_size 979 if self.collector_udp_port: 980 self.proposed["collector_udp_port"] = self.collector_udp_port 981 if self.collector_meth: 982 self.proposed["collector_meth"] = self.collector_meth 983 if self.collector_description: 984 self.proposed[ 985 "collector_description"] = self.collector_description 986 987 # sample and counter config 988 if self.sflow_interface: 989 self.proposed["sflow_interface"] = self.sflow_interface 990 if self.sample_collector: 991 self.proposed["sample_collector"] = self.sample_collector 992 if self.sample_rate: 993 self.proposed["sample_rate"] = self.sample_rate 994 if self.sample_length: 995 self.proposed["sample_length"] = self.sample_length 996 if self.sample_direction: 997 self.proposed["sample_direction"] = self.sample_direction 998 if self.counter_collector: 999 self.proposed["counter_collector"] = self.counter_collector 1000 if self.counter_interval: 1001 self.proposed["counter_interval"] = self.counter_interval 1002 1003 self.proposed["state"] = self.state 1004 1005 def get_existing(self): 1006 """get existing info""" 1007 1008 if not self.sflow_dict: 1009 return 1010 1011 if self.agent_ip: 1012 self.existing["agent"] = self.sflow_dict["agent"] 1013 if self.source_ip: 1014 self.existing["source"] = self.sflow_dict["source"] 1015 if self.collector_id: 1016 self.existing["collector"] = self.sflow_dict["collector"] 1017 if self.export_route: 1018 self.existing["export"] = self.sflow_dict["export"] 1019 1020 if self.sflow_interface: 1021 self.existing["sampling"] = self.sflow_dict["sampling"] 1022 self.existing["counter"] = self.sflow_dict["counter"] 1023 1024 def get_end_state(self): 1025 """get end state info""" 1026 1027 sflow_dict = self.get_sflow_dict() 1028 if not sflow_dict: 1029 return 1030 1031 if self.agent_ip: 1032 self.end_state["agent"] = sflow_dict["agent"] 1033 if self.source_ip: 1034 self.end_state["source"] = sflow_dict["source"] 1035 if self.collector_id: 1036 self.end_state["collector"] = sflow_dict["collector"] 1037 if self.export_route: 1038 self.end_state["export"] = sflow_dict["export"] 1039 1040 if self.sflow_interface: 1041 self.end_state["sampling"] = sflow_dict["sampling"] 1042 self.end_state["counter"] = sflow_dict["counter"] 1043 if self.existing == self.end_state: 1044 self.changed = False 1045 1046 def work(self): 1047 """worker""" 1048 1049 self.check_params() 1050 self.sflow_dict = self.get_sflow_dict() 1051 self.get_existing() 1052 self.get_proposed() 1053 1054 # deal present or absent 1055 xml_str = '' 1056 if self.export_route: 1057 xml_str += self.config_export() 1058 if self.agent_ip: 1059 xml_str += self.config_agent() 1060 if self.source_ip: 1061 xml_str += self.config_source() 1062 1063 if self.state == "present": 1064 if self.collector_id and self.collector_ip: 1065 xml_str += self.config_collector() 1066 if self.sflow_interface: 1067 xml_str += self.config_sampling() 1068 xml_str += self.config_counter() 1069 else: 1070 if self.sflow_interface: 1071 xml_str += self.config_sampling() 1072 xml_str += self.config_counter() 1073 if self.collector_id: 1074 xml_str += self.config_collector() 1075 1076 if xml_str: 1077 self.netconf_load_config(xml_str) 1078 self.changed = True 1079 1080 self.get_end_state() 1081 self.results['changed'] = self.changed 1082 self.results['proposed'] = self.proposed 1083 self.results['existing'] = self.existing 1084 self.results['end_state'] = self.end_state 1085 if self.changed: 1086 self.results['updates'] = self.updates_cmd 1087 else: 1088 self.results['updates'] = list() 1089 1090 self.module.exit_json(**self.results) 1091 1092 1093def main(): 1094 """Module main""" 1095 1096 argument_spec = dict( 1097 agent_ip=dict(required=False, type='str'), 1098 source_ip=dict(required=False, type='str'), 1099 export_route=dict(required=False, type='str', 1100 choices=['enable', 'disable']), 1101 collector_id=dict(required=False, type='str', choices=['1', '2']), 1102 collector_ip=dict(required=False, type='str'), 1103 collector_ip_vpn=dict(required=False, type='str'), 1104 collector_datagram_size=dict(required=False, type='str'), 1105 collector_udp_port=dict(required=False, type='str'), 1106 collector_meth=dict(required=False, type='str', 1107 choices=['meth', 'enhanced']), 1108 collector_description=dict(required=False, type='str'), 1109 sflow_interface=dict(required=False, type='str'), 1110 sample_collector=dict(required=False, type='list'), 1111 sample_rate=dict(required=False, type='str'), 1112 sample_length=dict(required=False, type='str'), 1113 sample_direction=dict(required=False, type='str', 1114 choices=['inbound', 'outbound', 'both']), 1115 counter_collector=dict(required=False, type='list'), 1116 counter_interval=dict(required=False, type='str'), 1117 state=dict(required=False, default='present', 1118 choices=['present', 'absent']) 1119 ) 1120 1121 argument_spec.update(ce_argument_spec) 1122 module = Sflow(argument_spec) 1123 module.work() 1124 1125 1126if __name__ == '__main__': 1127 main() 1128