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