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