1#!/usr/bin/python
2#
3# (c) 2015 Peter Sprygada, <psprygada@ansible.com>
4# Copyright (c) 2017 Dell Inc.
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'community'}
14
15
16DOCUMENTATION = """
17---
18module: dellos10_facts
19version_added: "2.2"
20author: "Senthil Kumar Ganesan (@skg-net)"
21short_description: Collect facts from remote devices running Dell EMC Networking OS10
22description:
23  - Collects a base set of device facts from a remote device that
24    is running OS10.  This module prepends all of the
25    base network fact keys with C(ansible_net_<fact>).  The facts
26    module will always collect a base set of facts from the device
27    and can enable or disable collection of additional facts.
28extends_documentation_fragment: dellos10
29options:
30  gather_subset:
31    description:
32      - When supplied, this argument will restrict the facts collected
33        to a given subset.  Possible values for this argument include
34        all, hardware, config, and interfaces.  Can specify a list of
35        values to include a larger subset.  Values can also be used
36        with an initial C(M(!)) to specify that a specific subset should
37        not be collected.
38    default: [ '!config' ]
39"""
40
41EXAMPLES = """
42# Collect all facts from the device
43- dellos10_facts:
44    gather_subset: all
45
46# Collect only the config and default facts
47- dellos10_facts:
48    gather_subset:
49      - config
50
51# Do not collect hardware facts
52- dellos10_facts:
53    gather_subset:
54      - "!hardware"
55"""
56
57RETURN = """
58ansible_net_gather_subset:
59  description: The list of fact subsets collected from the device
60  returned: always
61  type: list
62
63# default
64ansible_net_name:
65  description: The name of the OS that is running.
66  returned: Always.
67  type: str
68ansible_net_version:
69  description: The operating system version running on the remote device
70  returned: always
71  type: str
72ansible_net_servicetag:
73  description: The service tag number of the remote device.
74  returned: always
75  type: str
76ansible_net_model:
77  description: The model name returned from the device.
78  returned: always
79  type: str
80ansible_net_hostname:
81  description: The configured hostname of the device
82  returned: always
83  type: str
84
85# hardware
86ansible_net_cpu_arch:
87  description: CPU Architecture of the remote device.
88  returned: when hardware is configured
89  type: str
90ansible_net_memfree_mb:
91  description: The available free memory on the remote device in Mb
92  returned: when hardware is configured
93  type: int
94ansible_net_memtotal_mb:
95  description: The total memory on the remote device in Mb
96  returned: when hardware is configured
97  type: int
98
99# config
100ansible_net_config:
101  description: The current active config from the device
102  returned: when config is configured
103  type: str
104
105# interfaces
106ansible_net_all_ipv4_addresses:
107  description: All IPv4 addresses configured on the device
108  returned: when interfaces is configured
109  type: list
110ansible_net_all_ipv6_addresses:
111  description: All IPv6 addresses configured on the device
112  returned: when interfaces is configured
113  type: list
114ansible_net_interfaces:
115  description: A hash of all interfaces running on the system
116  returned: when interfaces is configured
117  type: dict
118ansible_net_neighbors:
119  description: The list of LLDP neighbors from the remote device
120  returned: when interfaces is configured
121  type: dict
122"""
123
124import re
125
126try:
127    from lxml import etree as ET
128except ImportError:
129    import xml.etree.ElementTree as ET
130
131from ansible.module_utils.network.dellos10.dellos10 import run_commands
132from ansible.module_utils.network.dellos10.dellos10 import dellos10_argument_spec, check_args
133from ansible.module_utils.basic import AnsibleModule
134from ansible.module_utils.six import iteritems
135
136
137class FactsBase(object):
138
139    COMMANDS = []
140
141    def __init__(self, module):
142        self.module = module
143        self.facts = dict()
144        self.responses = None
145
146    def populate(self):
147        self.responses = run_commands(self.module, self.COMMANDS, check_rc=False)
148
149    def run(self, cmd):
150        return run_commands(self.module, cmd, check_rc=False)
151
152
153class Default(FactsBase):
154
155    COMMANDS = [
156        'show version | display-xml',
157        'show system | display-xml',
158    ]
159
160    def populate(self):
161        super(Default, self).populate()
162        data = self.responses[0]
163        xml_data = ET.fromstring(data.encode('utf8'))
164
165        self.facts['name'] = self.parse_name(xml_data)
166        self.facts['version'] = self.parse_version(xml_data)
167        self.facts['model'] = self.parse_model(xml_data)
168        self.facts['hostname'] = self.parse_hostname(xml_data)
169
170        data = self.responses[1]
171        xml_data = ET.fromstring(data.encode('utf8'))
172
173        self.facts['servicetag'] = self.parse_servicetag(xml_data)
174
175    def parse_name(self, data):
176        sw_name = data.find('./data/system-sw-state/sw-version/sw-name')
177        if sw_name is not None:
178            return sw_name.text
179        else:
180            return ""
181
182    def parse_version(self, data):
183        sw_ver = data.find('./data/system-sw-state/sw-version/sw-version')
184        if sw_ver is not None:
185            return sw_ver.text
186        else:
187            return ""
188
189    def parse_hostname(self, data):
190        hostname = data.find('./data/system-state/system-status/hostname')
191        if hostname is not None:
192            return hostname.text
193        else:
194            return ""
195
196    def parse_model(self, data):
197        prod_name = data.find('./data/system-sw-state/sw-version/sw-platform')
198        if prod_name is not None:
199            return prod_name.text
200        else:
201            return ""
202
203    def parse_servicetag(self, data):
204        svc_tag = data.find('./data/system/node/unit/mfg-info/service-tag')
205        if svc_tag is not None:
206            return svc_tag.text
207        else:
208            return ""
209
210
211class Hardware(FactsBase):
212
213    COMMANDS = [
214        'show version | display-xml',
215        'show processes node-id 1 | grep Mem:'
216    ]
217
218    def populate(self):
219
220        super(Hardware, self).populate()
221        data = self.responses[0]
222
223        xml_data = ET.fromstring(data.encode('utf8'))
224
225        self.facts['cpu_arch'] = self.parse_cpu_arch(xml_data)
226
227        data = self.responses[1]
228        match = self.parse_memory(data)
229        if match:
230            self.facts['memtotal_mb'] = int(match[0]) // 1024
231            self.facts['memfree_mb'] = int(match[2]) // 1024
232
233    def parse_cpu_arch(self, data):
234        cpu_arch = data.find('./data/system-sw-state/sw-version/cpu-arch')
235        if cpu_arch is not None:
236            return cpu_arch.text
237        else:
238            return ""
239
240    def parse_memory(self, data):
241        return re.findall(r'(\d+)', data, re.M)
242
243
244class Config(FactsBase):
245
246    COMMANDS = ['show running-config']
247
248    def populate(self):
249        super(Config, self).populate()
250        self.facts['config'] = self.responses[0]
251
252
253class Interfaces(FactsBase):
254
255    COMMANDS = [
256        'show interface | display-xml',
257        'show lldp neighbors | display-xml'
258    ]
259
260    def __init__(self, module):
261        self.intf_facts = dict()
262        self.lldp_facts = dict()
263        super(Interfaces, self).__init__(module)
264
265    def populate(self):
266        super(Interfaces, self).populate()
267        self.facts['all_ipv4_addresses'] = list()
268        self.facts['all_ipv6_addresses'] = list()
269
270        int_show_data = (self.responses[0]).splitlines()
271        pattern = '?xml version'
272        data = ''
273        skip = True
274
275        # The output returns multiple xml trees
276        # parse them before handling.
277        for line in int_show_data:
278            if pattern in line:
279                if skip is False:
280                    xml_data = ET.fromstring(data.encode('utf8'))
281                    self.populate_interfaces(xml_data)
282                    data = ''
283                else:
284                    skip = False
285
286            data += line
287
288        if skip is False:
289            xml_data = ET.fromstring(data.encode('utf8'))
290            self.populate_interfaces(xml_data)
291
292        self.facts['interfaces'] = self.intf_facts
293
294        lldp_data = (self.responses[1]).splitlines()
295        data = ''
296        skip = True
297        # The output returns multiple xml trees
298        # parse them before handling.
299        for line in lldp_data:
300            if pattern in line:
301                if skip is False:
302                    xml_data = ET.fromstring(data.encode('utf8'))
303                    self.populate_neighbors(xml_data)
304                    data = ''
305                else:
306                    skip = False
307
308            data += line
309
310        if skip is False:
311            xml_data = ET.fromstring(data.encode('utf8'))
312            self.populate_neighbors(xml_data)
313
314        self.facts['neighbors'] = self.lldp_facts
315
316    def populate_interfaces(self, interfaces):
317
318        for interface in interfaces.findall('./data/interfaces/interface'):
319            intf = dict()
320            name = self.parse_item(interface, 'name')
321
322            intf['description'] = self.parse_item(interface, 'description')
323            intf['duplex'] = self.parse_item(interface, 'duplex')
324            intf['primary_ipv4'] = self.parse_primary_ipv4(interface)
325            intf['secondary_ipv4'] = self.parse_secondary_ipv4(interface)
326            intf['ipv6'] = self.parse_ipv6_address(interface)
327            intf['mtu'] = self.parse_item(interface, 'mtu')
328            intf['type'] = self.parse_item(interface, 'type')
329
330            self.intf_facts[name] = intf
331
332        for interface in interfaces.findall('./bulk/data/interface'):
333            name = self.parse_item(interface, 'name')
334            try:
335                intf = self.intf_facts[name]
336                intf['bandwidth'] = self.parse_item(interface, 'speed')
337                intf['adminstatus'] = self.parse_item(interface, 'admin-status')
338                intf['operstatus'] = self.parse_item(interface, 'oper-status')
339                intf['macaddress'] = self.parse_item(interface, 'phys-address')
340            except KeyError:
341                # skip the reserved interfaces
342                pass
343
344        for interface in interfaces.findall('./data/ports/ports-state/port'):
345            name = self.parse_item(interface, 'name')
346            # media-type name interface name format phy-eth 1/1/1
347            mediatype = self.parse_item(interface, 'media-type')
348
349            typ, sname = name.split('-eth')
350            name = "ethernet" + sname
351            try:
352                intf = self.intf_facts[name]
353                intf['mediatype'] = mediatype
354            except Exception:
355                # fanout
356                for subport in range(1, 5):
357                    name = "ethernet" + sname + ":" + str(subport)
358                    try:
359                        intf = self.intf_facts[name]
360                        intf['mediatype'] = mediatype
361                    except Exception:
362                        # valid case to handle 2x50G
363                        pass
364
365    def add_ip_address(self, address, family):
366        if family == 'ipv4':
367            self.facts['all_ipv4_addresses'].append(address)
368        else:
369            self.facts['all_ipv6_addresses'].append(address)
370
371    def parse_item(self, interface, item):
372        elem = interface.find(item)
373        if elem is not None:
374            return elem.text
375        else:
376            return ""
377
378    def parse_primary_ipv4(self, interface):
379        ipv4 = interface.find('ipv4')
380        ip_address = ""
381        if ipv4 is not None:
382            prim_ipaddr = ipv4.find('./address/primary-addr')
383            if prim_ipaddr is not None:
384                ip_address = prim_ipaddr.text
385                self.add_ip_address(ip_address, 'ipv4')
386
387        return ip_address
388
389    def parse_secondary_ipv4(self, interface):
390        ipv4 = interface.find('ipv4')
391        ip_address = ""
392        if ipv4 is not None:
393            sec_ipaddr = ipv4.find('./address/secondary-addr')
394            if sec_ipaddr is not None:
395                ip_address = sec_ipaddr.text
396                self.add_ip_address(ip_address, 'ipv4')
397
398        return ip_address
399
400    def parse_ipv6_address(self, interface):
401
402        ip_address = list()
403
404        for addr in interface.findall('./ipv6/ipv6-addresses/address'):
405
406            ipv6_addr = addr.find('./ipv6-address')
407
408            if ipv6_addr is not None:
409                ip_address.append(ipv6_addr.text)
410                self.add_ip_address(ipv6_addr.text, 'ipv6')
411
412        return ip_address
413
414    def populate_neighbors(self, interfaces):
415        for interface in interfaces.findall('./bulk/data/interface'):
416            name = interface.find('name').text
417            rem_sys_name = interface.find('./lldp-rem-neighbor-info/info/rem-system-name')
418            if rem_sys_name is not None:
419                self.lldp_facts[name] = list()
420                fact = dict()
421                fact['host'] = rem_sys_name.text
422                rem_sys_port = interface.find('./lldp-rem-neighbor-info/info/rem-lldp-port-id')
423                fact['port'] = rem_sys_port.text
424                self.lldp_facts[name].append(fact)
425
426
427FACT_SUBSETS = dict(
428    default=Default,
429    hardware=Hardware,
430    interfaces=Interfaces,
431    config=Config,
432)
433
434VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
435
436
437def main():
438    """main entry point for module execution
439    """
440    argument_spec = dict(
441        gather_subset=dict(default=['!config'], type='list')
442    )
443
444    argument_spec.update(dellos10_argument_spec)
445
446    module = AnsibleModule(argument_spec=argument_spec,
447                           supports_check_mode=True)
448
449    gather_subset = module.params['gather_subset']
450
451    runable_subsets = set()
452    exclude_subsets = set()
453
454    for subset in gather_subset:
455        if subset == 'all':
456            runable_subsets.update(VALID_SUBSETS)
457            continue
458
459        if subset.startswith('!'):
460            subset = subset[1:]
461            if subset == 'all':
462                exclude_subsets.update(VALID_SUBSETS)
463                continue
464            exclude = True
465        else:
466            exclude = False
467
468        if subset not in VALID_SUBSETS:
469            module.fail_json(msg='Bad subset')
470
471        if exclude:
472            exclude_subsets.add(subset)
473        else:
474            runable_subsets.add(subset)
475
476    if not runable_subsets:
477        runable_subsets.update(VALID_SUBSETS)
478
479    runable_subsets.difference_update(exclude_subsets)
480    runable_subsets.add('default')
481
482    facts = dict()
483    facts['gather_subset'] = list(runable_subsets)
484
485    instances = list()
486    for key in runable_subsets:
487        instances.append(FACT_SUBSETS[key](module))
488
489    for inst in instances:
490        inst.populate()
491        facts.update(inst.facts)
492
493    ansible_facts = dict()
494    for key, value in iteritems(facts):
495        key = 'ansible_net_%s' % key
496        ansible_facts[key] = value
497
498    warnings = list()
499    check_args(module, warnings)
500
501    module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
502
503
504if __name__ == '__main__':
505    main()
506