1#!/usr/bin/python
2#
3# (c) 2015 Peter Sprygada, <psprygada@ansible.com>
4# Copyright (c) 2016 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: dellos9_facts
19version_added: "2.2"
20author: "Dhivya P (@dhivyap)"
21short_description: Collect facts from remote devices running Dell EMC Networking OS9
22description:
23  - Collects a base set of device facts from a remote device that
24    is running OS9.  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: dellos9
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' ]
39notes:
40  - This module requires OS9 version 9.10.0.1P13 or above.
41
42  - This module requires an increase of the SSH connection rate limit.
43    Use the following command I(ip ssh connection-rate-limit 60)
44    to configure the same. This can be also be done with the M(dellos9_config) module.
45"""
46
47EXAMPLES = """
48# Collect all facts from the device
49- dellos9_facts:
50    gather_subset: all
51
52# Collect only the config and default facts
53- dellos9_facts:
54    gather_subset:
55      - config
56
57# Do not collect hardware facts
58- dellos9_facts:
59    gather_subset:
60      - "!hardware"
61"""
62
63RETURN = """
64ansible_net_gather_subset:
65  description: The list of fact subsets collected from the device
66  returned: always
67  type: list
68
69# default
70ansible_net_model:
71  description: The model name returned from the device
72  returned: always
73  type: str
74ansible_net_serialnum:
75  description: The serial number of the remote device
76  returned: always
77  type: str
78ansible_net_version:
79  description: The operating system version running on the remote device
80  returned: always
81  type: str
82ansible_net_hostname:
83  description: The configured hostname of the device
84  returned: always
85  type: str
86ansible_net_image:
87  description: The image file the device is running
88  returned: always
89  type: str
90
91# hardware
92ansible_net_filesystems:
93  description: All file system names available on the device
94  returned: when hardware is configured
95  type: list
96ansible_net_memfree_mb:
97  description: The available free memory on the remote device in Mb
98  returned: when hardware is configured
99  type: int
100ansible_net_memtotal_mb:
101  description: The total memory on the remote device in Mb
102  returned: when hardware is configured
103  type: int
104
105# config
106ansible_net_config:
107  description: The current active config from the device
108  returned: when config is configured
109  type: str
110
111# interfaces
112ansible_net_all_ipv4_addresses:
113  description: All IPv4 addresses configured on the device
114  returned: when interfaces is configured
115  type: list
116ansible_net_all_ipv6_addresses:
117  description: All IPv6 addresses configured on the device
118  returned: when interfaces is configured
119  type: list
120ansible_net_interfaces:
121  description: A hash of all interfaces running on the system
122  returned: when interfaces is configured
123  type: dict
124ansible_net_neighbors:
125  description: The list of LLDP neighbors from the remote device
126  returned: when interfaces is configured
127  type: dict
128"""
129import re
130try:
131    from itertools import izip
132except ImportError:
133    izip = zip
134
135from ansible.module_utils.basic import AnsibleModule
136from ansible.module_utils.network.dellos9.dellos9 import run_commands
137from ansible.module_utils.network.dellos9.dellos9 import dellos9_argument_spec, check_args
138from ansible.module_utils.six import iteritems
139
140
141class FactsBase(object):
142
143    COMMANDS = list()
144
145    def __init__(self, module):
146        self.module = module
147        self.facts = dict()
148        self.responses = None
149
150    def populate(self):
151        self.responses = run_commands(self.module, self.COMMANDS, check_rc=False)
152
153    def run(self, cmd):
154        return run_commands(self.module, cmd, check_rc=False)
155
156
157class Default(FactsBase):
158
159    COMMANDS = [
160        'show version',
161        'show inventory',
162        'show running-config | grep hostname'
163    ]
164
165    def populate(self):
166        super(Default, self).populate()
167        data = self.responses[0]
168        self.facts['version'] = self.parse_version(data)
169        self.facts['model'] = self.parse_model(data)
170        self.facts['image'] = self.parse_image(data)
171
172        data = self.responses[1]
173        self.facts['serialnum'] = self.parse_serialnum(data)
174
175        data = self.responses[2]
176        self.facts['hostname'] = self.parse_hostname(data)
177
178    def parse_version(self, data):
179        match = re.search(r'Software Version:\s*(.+)', data)
180        if match:
181            return match.group(1)
182
183    def parse_hostname(self, data):
184        match = re.search(r'^hostname (.+)', data, re.M)
185        if match:
186            return match.group(1)
187
188    def parse_model(self, data):
189        match = re.search(r'^System Type:\s*(.+)', data, re.M)
190        if match:
191            return match.group(1)
192
193    def parse_image(self, data):
194        match = re.search(r'image file is "(.+)"', data)
195        if match:
196            return match.group(1)
197
198    def parse_serialnum(self, data):
199        for line in data.split('\n'):
200            if line.startswith('*'):
201                match = re.search(
202                    r'\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)', line, re.M)
203                if match:
204                    return match.group(3)
205
206
207class Hardware(FactsBase):
208
209    COMMANDS = [
210        'show file-systems',
211        'show memory | except Processor'
212    ]
213
214    def populate(self):
215        super(Hardware, self).populate()
216        data = self.responses[0]
217        self.facts['filesystems'] = self.parse_filesystems(data)
218
219        data = self.responses[1]
220        match = re.findall(r'\s(\d+)\s', data)
221        if match:
222            self.facts['memtotal_mb'] = int(match[0]) // 1024
223            self.facts['memfree_mb'] = int(match[2]) // 1024
224
225    def parse_filesystems(self, data):
226        return re.findall(r'\s(\S+):$', data, re.M)
227
228
229class Config(FactsBase):
230
231    COMMANDS = ['show running-config']
232
233    def populate(self):
234        super(Config, self).populate()
235        self.facts['config'] = self.responses[0]
236
237
238class Interfaces(FactsBase):
239
240    COMMANDS = [
241        'show interfaces',
242        'show ipv6 interface',
243        'show lldp neighbors detail',
244        'show inventory'
245    ]
246
247    def populate(self):
248        super(Interfaces, self).populate()
249        self.facts['all_ipv4_addresses'] = list()
250        self.facts['all_ipv6_addresses'] = list()
251
252        data = self.responses[0]
253        interfaces = self.parse_interfaces(data)
254
255        for key in list(interfaces.keys()):
256            if "ManagementEthernet" in key:
257                temp_parsed = interfaces[key]
258                del interfaces[key]
259                interfaces.update(self.parse_mgmt_interfaces(temp_parsed))
260
261        for key in list(interfaces.keys()):
262            if "Vlan" in key:
263                temp_parsed = interfaces[key]
264                del interfaces[key]
265                interfaces.update(self.parse_vlan_interfaces(temp_parsed))
266
267        self.facts['interfaces'] = self.populate_interfaces(interfaces)
268
269        data = self.responses[1]
270        if len(data) > 0:
271            data = self.parse_ipv6_interfaces(data)
272            self.populate_ipv6_interfaces(data)
273
274        data = self.responses[3]
275        if 'LLDP' in self.get_protocol_list(data):
276            neighbors = self.responses[2]
277            self.facts['neighbors'] = self.parse_neighbors(neighbors)
278
279    def get_protocol_list(self, data):
280        start = False
281        protocol_list = list()
282        for line in data.split('\n'):
283            match = re.search(r'Software Protocol Configured\s*', line)
284            if match:
285                start = True
286                continue
287            if start:
288                line = line.strip()
289                if line.isalnum():
290                    protocol_list.append(line)
291        return protocol_list
292
293    def populate_interfaces(self, interfaces):
294        facts = dict()
295        for key, value in interfaces.items():
296            intf = dict()
297            intf['description'] = self.parse_description(value)
298            intf['macaddress'] = self.parse_macaddress(value)
299            ipv4 = self.parse_ipv4(value)
300            intf['ipv4'] = self.parse_ipv4(value)
301            if ipv4:
302                self.add_ip_address(ipv4['address'], 'ipv4')
303
304            intf['mtu'] = self.parse_mtu(value)
305            intf['bandwidth'] = self.parse_bandwidth(value)
306            intf['mediatype'] = self.parse_mediatype(value)
307            intf['duplex'] = self.parse_duplex(value)
308            intf['lineprotocol'] = self.parse_lineprotocol(value)
309            intf['operstatus'] = self.parse_operstatus(value)
310            intf['type'] = self.parse_type(value)
311
312            facts[key] = intf
313        return facts
314
315    def populate_ipv6_interfaces(self, data):
316        for key, value in data.items():
317            if key in self.facts['interfaces']:
318                self.facts['interfaces'][key]['ipv6'] = list()
319                addresses = re.findall(r'\s+(.+), subnet', value, re.M)
320                subnets = re.findall(r', subnet is (\S+)', value, re.M)
321                for addr, subnet in izip(addresses, subnets):
322                    ipv6 = dict(address=addr.strip(), subnet=subnet.strip())
323                    self.add_ip_address(addr.strip(), 'ipv6')
324                    self.facts['interfaces'][key]['ipv6'].append(ipv6)
325
326    def add_ip_address(self, address, family):
327        if family == 'ipv4':
328            self.facts['all_ipv4_addresses'].append(address)
329        else:
330            self.facts['all_ipv6_addresses'].append(address)
331
332    def parse_neighbors(self, neighbors):
333        facts = dict()
334
335        for entry in neighbors.split(
336                '========================================================================'):
337            if entry == '':
338                continue
339
340            intf = self.parse_lldp_intf(entry)
341            if intf not in facts:
342                facts[intf] = list()
343            fact = dict()
344            fact['host'] = self.parse_lldp_host(entry)
345            fact['port'] = self.parse_lldp_port(entry)
346            facts[intf].append(fact)
347        return facts
348
349    def parse_interfaces(self, data):
350        parsed = dict()
351        newline_count = 0
352        interface_start = True
353
354        for line in data.split('\n'):
355            if interface_start:
356                newline_count = 0
357            if len(line) == 0:
358                newline_count += 1
359                if newline_count == 2:
360                    interface_start = True
361                continue
362            else:
363                match = re.match(r'^(\S+) (\S+)', line)
364                if match and interface_start:
365                    interface_start = False
366                    key = match.group(0)
367                    parsed[key] = line
368                else:
369                    parsed[key] += '\n%s' % line
370        return parsed
371
372    def parse_mgmt_interfaces(self, data):
373        parsed = dict()
374        interface_start = True
375        for line in data.split('\n'):
376            match = re.match(r'^(\S+) (\S+)', line)
377            if "Time since" in line:
378                interface_start = True
379                parsed[key] += '\n%s' % line
380                continue
381            elif match and interface_start:
382                interface_start = False
383                key = match.group(0)
384                parsed[key] = line
385            else:
386                parsed[key] += '\n%s' % line
387        return parsed
388
389    def parse_vlan_interfaces(self, data):
390        parsed = dict()
391        interface_start = True
392        line_before_end = False
393        for line in data.split('\n'):
394            match = re.match(r'^(\S+) (\S+)', line)
395            match_endline = re.match(r'^\s*\d+ packets, \d+ bytes$', line)
396
397            if "Output Statistics" in line:
398                line_before_end = True
399                parsed[key] += '\n%s' % line
400            elif match_endline and line_before_end:
401                line_before_end = False
402                interface_start = True
403                parsed[key] += '\n%s' % line
404            elif match and interface_start:
405                interface_start = False
406                key = match.group(0)
407                parsed[key] = line
408            else:
409                parsed[key] += '\n%s' % line
410        return parsed
411
412    def parse_ipv6_interfaces(self, data):
413        parsed = dict()
414        for line in data.split('\n'):
415            if len(line) == 0:
416                continue
417            elif line[0] == ' ':
418                parsed[key] += '\n%s' % line
419            else:
420                match = re.match(r'^(\S+) (\S+)', line)
421                if match:
422                    key = match.group(0)
423                    parsed[key] = line
424        return parsed
425
426    def parse_description(self, data):
427        match = re.search(r'Description: (.+)$', data, re.M)
428        if match:
429            return match.group(1)
430
431    def parse_macaddress(self, data):
432        match = re.search(r'address is (\S+)', data)
433        if match:
434            if match.group(1) != "not":
435                return match.group(1)
436
437    def parse_ipv4(self, data):
438        match = re.search(r'Internet address is (\S+)', data)
439        if match:
440            if match.group(1) != "not":
441                addr, masklen = match.group(1).split('/')
442                return dict(address=addr, masklen=int(masklen))
443
444    def parse_mtu(self, data):
445        match = re.search(r'MTU (\d+)', data)
446        if match:
447            return int(match.group(1))
448
449    def parse_bandwidth(self, data):
450        match = re.search(r'LineSpeed (\d+)', data)
451        if match:
452            return int(match.group(1))
453
454    def parse_duplex(self, data):
455        match = re.search(r'(\w+) duplex', data, re.M)
456        if match:
457            return match.group(1)
458
459    def parse_mediatype(self, data):
460        media = re.search(r'(.+) media present, (.+)', data, re.M)
461        if media:
462            match = re.search(r'type is (.+)$', media.group(0), re.M)
463            return match.group(1)
464
465    def parse_type(self, data):
466        match = re.search(r'Hardware is (.+),', data, re.M)
467        if match:
468            return match.group(1)
469
470    def parse_lineprotocol(self, data):
471        match = re.search(r'line protocol is (\w+[ ]?\w*)\(?.*\)?$', data, re.M)
472        if match:
473            return match.group(1)
474
475    def parse_operstatus(self, data):
476        match = re.search(r'^(?:.+) is (.+),', data, re.M)
477        if match:
478            return match.group(1)
479
480    def parse_lldp_intf(self, data):
481        match = re.search(r'^\sLocal Interface (\S+\s\S+)', data, re.M)
482        if match:
483            return match.group(1)
484
485    def parse_lldp_host(self, data):
486        match = re.search(r'Remote System Name: (.+)$', data, re.M)
487        if match:
488            return match.group(1)
489
490    def parse_lldp_port(self, data):
491        match = re.search(r'Remote Port ID: (.+)$', data, re.M)
492        if match:
493            return match.group(1)
494
495
496FACT_SUBSETS = dict(
497    default=Default,
498    hardware=Hardware,
499    interfaces=Interfaces,
500    config=Config,
501)
502
503VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
504
505
506def main():
507    """main entry point for module execution
508    """
509    argument_spec = dict(
510        gather_subset=dict(default=['!config'], type='list')
511    )
512
513    argument_spec.update(dellos9_argument_spec)
514
515    module = AnsibleModule(argument_spec=argument_spec,
516                           supports_check_mode=True)
517
518    gather_subset = module.params['gather_subset']
519
520    runable_subsets = set()
521    exclude_subsets = set()
522
523    for subset in gather_subset:
524        if subset == 'all':
525            runable_subsets.update(VALID_SUBSETS)
526            continue
527
528        if subset.startswith('!'):
529            subset = subset[1:]
530            if subset == 'all':
531                exclude_subsets.update(VALID_SUBSETS)
532                continue
533            exclude = True
534        else:
535            exclude = False
536
537        if subset not in VALID_SUBSETS:
538            module.fail_json(msg='Bad subset')
539
540        if exclude:
541            exclude_subsets.add(subset)
542        else:
543            runable_subsets.add(subset)
544
545    if not runable_subsets:
546        runable_subsets.update(VALID_SUBSETS)
547
548    runable_subsets.difference_update(exclude_subsets)
549    runable_subsets.add('default')
550
551    facts = dict()
552    facts['gather_subset'] = list(runable_subsets)
553
554    instances = list()
555    for key in runable_subsets:
556        instances.append(FACT_SUBSETS[key](module))
557
558    for inst in instances:
559        inst.populate()
560        facts.update(inst.facts)
561
562    ansible_facts = dict()
563    for key, value in iteritems(facts):
564        key = 'ansible_net_%s' % key
565        ansible_facts[key] = value
566
567    warnings = list()
568    check_args(module, warnings)
569
570    module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
571
572
573if __name__ == '__main__':
574    main()
575