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