1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# (C) 2017 Red Hat Inc.
5# Copyright (C) 2017 Lenovo.
6#
7# GNU General Public License v3.0+
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
15#
16# Module to Collect facts from Lenovo Switches running Lenovo ENOS commands
17# Lenovo Networking
18#
19from __future__ import absolute_import, division, print_function
20__metaclass__ = type
21
22
23ANSIBLE_METADATA = {'metadata_version': '1.1',
24                    'status': ['preview'],
25                    'supported_by': 'community'}
26
27DOCUMENTATION = '''
28---
29module: enos_facts
30version_added: "2.5"
31author: "Anil Kumar Muraleedharan (@amuraleedhar)"
32short_description: Collect facts from remote devices running Lenovo ENOS
33description:
34  - Collects a base set of device facts from a remote Lenovo device
35    running on ENOS.  This module prepends all of the
36    base network fact keys with C(ansible_net_<fact>).  The facts
37    module will always collect a base set of facts from the device
38    and can enable or disable collection of additional facts.
39extends_documentation_fragment: enos
40notes:
41  - Tested against ENOS 8.4.1
42options:
43  gather_subset:
44    description:
45      - When supplied, this argument will restrict the facts collected
46        to a given subset.  Possible values for this argument include
47        all, hardware, config, and interfaces.  Can specify a list of
48        values to include a larger subset.  Values can also be used
49        with an initial C(M(!)) to specify that a specific subset should
50        not be collected.
51    required: false
52    default: '!config'
53'''
54EXAMPLES = '''
55Tasks: The following are examples of using the module enos_facts.
56---
57- name: Test Enos Facts
58  enos_facts:
59    provider={{ cli }}
60
61  vars:
62    cli:
63      host: "{{ inventory_hostname }}"
64      port: 22
65      username: admin
66      password: admin
67      transport: cli
68      timeout: 30
69      authorize: True
70      auth_pass:
71
72---
73# Collect all facts from the device
74- enos_facts:
75    gather_subset: all
76    provider: "{{ cli }}"
77
78# Collect only the config and default facts
79- enos_facts:
80    gather_subset:
81      - config
82    provider: "{{ cli }}"
83
84# Do not collect hardware facts
85- enos_facts:
86    gather_subset:
87      - "!hardware"
88    provider: "{{ cli }}"
89
90'''
91RETURN = '''
92  ansible_net_gather_subset:
93    description: The list of fact subsets collected from the device
94    returned: always
95    type: list
96# default
97  ansible_net_model:
98    description: The model name returned from the Lenovo ENOS device
99    returned: always
100    type: str
101  ansible_net_serialnum:
102    description: The serial number of the Lenovo ENOS device
103    returned: always
104    type: str
105  ansible_net_version:
106    description: The ENOS operating system version running on the remote device
107    returned: always
108    type: str
109  ansible_net_hostname:
110    description: The configured hostname of the device
111    returned: always
112    type: str
113  ansible_net_image:
114    description: Indicates the active image for the device
115    returned: always
116    type: str
117# hardware
118  ansible_net_memfree_mb:
119    description: The available free memory on the remote device in MB
120    returned: when hardware is configured
121    type: int
122# config
123  ansible_net_config:
124    description: The current active config from the device
125    returned: when config is configured
126    type: str
127# interfaces
128  ansible_net_all_ipv4_addresses:
129    description: All IPv4 addresses configured on the device
130    returned: when interfaces is configured
131    type: list
132  ansible_net_all_ipv6_addresses:
133    description: All IPv6 addresses configured on the device
134    returned: when interfaces is configured
135    type: list
136  ansible_net_interfaces:
137    description: A hash of all interfaces running on the system.
138      This gives information on description, mac address, mtu, speed,
139      duplex and operstatus
140    returned: when interfaces is configured
141    type: dict
142  ansible_net_neighbors:
143    description: The list of LLDP neighbors from the remote device
144    returned: when interfaces is configured
145    type: dict
146'''
147
148import re
149
150from ansible.module_utils.network.enos.enos import run_commands, enos_argument_spec, check_args
151from ansible.module_utils._text import to_text
152from ansible.module_utils.basic import AnsibleModule
153from ansible.module_utils.six import iteritems
154from ansible.module_utils.six.moves import zip
155
156
157class FactsBase(object):
158
159    COMMANDS = list()
160
161    def __init__(self, module):
162        self.module = module
163        self.facts = dict()
164        self.responses = None
165        self.PERSISTENT_COMMAND_TIMEOUT = 60
166
167    def populate(self):
168        self.responses = run_commands(self.module, self.COMMANDS,
169                                      check_rc=False)
170
171    def run(self, cmd):
172        return run_commands(self.module, cmd, check_rc=False)
173
174
175class Default(FactsBase):
176
177    COMMANDS = ['show version', 'show run']
178
179    def populate(self):
180        super(Default, self).populate()
181        data = self.responses[0]
182        data_run = self.responses[1]
183        if data:
184            self.facts['version'] = self.parse_version(data)
185            self.facts['serialnum'] = self.parse_serialnum(data)
186            self.facts['model'] = self.parse_model(data)
187            self.facts['image'] = self.parse_image(data)
188        if data_run:
189            self.facts['hostname'] = self.parse_hostname(data_run)
190
191    def parse_version(self, data):
192        match = re.search(r'^Software Version (.*?) ', data, re.M | re.I)
193        if match:
194            return match.group(1)
195
196    def parse_hostname(self, data_run):
197        for line in data_run.split('\n'):
198            line = line.strip()
199            match = re.match(r'hostname (.*?)', line, re.M | re.I)
200            if match:
201                hosts = line.split()
202                hostname = hosts[1].strip('\"')
203                return hostname
204        return "NA"
205
206    def parse_model(self, data):
207        match = re.search(r'^Lenovo RackSwitch (\S+)', data, re.M | re.I)
208        if match:
209            return match.group(1)
210
211    def parse_image(self, data):
212        match = re.search(r'(.*) image1(.*)', data, re.M | re.I)
213        if match:
214            return "Image1"
215        else:
216            return "Image2"
217
218    def parse_serialnum(self, data):
219        match = re.search(r'^Switch Serial No:  (\S+)', data, re.M | re.I)
220        if match:
221            return match.group(1)
222
223
224class Hardware(FactsBase):
225
226    COMMANDS = [
227        'show system memory'
228    ]
229
230    def populate(self):
231        super(Hardware, self).populate()
232        data = self.run(['show system memory'])
233        data = to_text(data, errors='surrogate_or_strict').strip()
234        data = data.replace(r"\n", "\n")
235        if data:
236            self.facts['memtotal_mb'] = self.parse_memtotal(data)
237            self.facts['memfree_mb'] = self.parse_memfree(data)
238
239    def parse_memtotal(self, data):
240        match = re.search(r'^MemTotal:\s*(.*) kB', data, re.M | re.I)
241        if match:
242            return int(match.group(1)) / 1024
243
244    def parse_memfree(self, data):
245        match = re.search(r'^MemFree:\s*(.*) kB', data, re.M | re.I)
246        if match:
247            return int(match.group(1)) / 1024
248
249
250class Config(FactsBase):
251
252    COMMANDS = ['show running-config']
253
254    def populate(self):
255        super(Config, self).populate()
256        data = self.responses[0]
257        if data:
258            self.facts['config'] = data
259
260
261class Interfaces(FactsBase):
262
263    COMMANDS = ['show interface status']
264
265    def populate(self):
266        super(Interfaces, self).populate()
267
268        self.facts['all_ipv4_addresses'] = list()
269        self.facts['all_ipv6_addresses'] = list()
270
271        data1 = self.run(['show interface status'])
272        data1 = to_text(data1, errors='surrogate_or_strict').strip()
273        data1 = data1.replace(r"\n", "\n")
274        data2 = self.run(['show lldp port'])
275        data2 = to_text(data2, errors='surrogate_or_strict').strip()
276        data2 = data2.replace(r"\n", "\n")
277        lines1 = None
278        lines2 = None
279        if data1:
280            lines1 = self.parse_interfaces(data1)
281        if data2:
282            lines2 = self.parse_interfaces(data2)
283        if lines1 is not None and lines2 is not None:
284            self.facts['interfaces'] = self.populate_interfaces(lines1, lines2)
285        data3 = self.run(['show lldp remote-device port'])
286        data3 = to_text(data3, errors='surrogate_or_strict').strip()
287        data3 = data3.replace(r"\n", "\n")
288
289        lines3 = None
290        if data3:
291            lines3 = self.parse_neighbors(data3)
292        if lines3 is not None:
293            self.facts['neighbors'] = self.populate_neighbors(lines3)
294
295        data4 = self.run(['show interface ip'])
296        data4 = data4[0].split('\n')
297        lines4 = None
298        if data4:
299            lines4 = self.parse_ipaddresses(data4)
300            ipv4_interfaces = self.set_ipv4_interfaces(lines4)
301            self.facts['all_ipv4_addresses'] = ipv4_interfaces
302            ipv6_interfaces = self.set_ipv6_interfaces(lines4)
303            self.facts['all_ipv6_addresses'] = ipv6_interfaces
304
305    def parse_ipaddresses(self, data4):
306        parsed = list()
307        for line in data4:
308            if len(line) == 0:
309                continue
310            else:
311                line = line.strip()
312                if len(line) == 0:
313                    continue
314                match = re.search(r'IP4', line, re.M | re.I)
315                if match:
316                    key = match.group()
317                    parsed.append(line)
318                match = re.search(r'IP6', line, re.M | re.I)
319                if match:
320                    key = match.group()
321                    parsed.append(line)
322        return parsed
323
324    def set_ipv4_interfaces(self, line4):
325        ipv4_addresses = list()
326        for line in line4:
327            ipv4Split = line.split()
328            if ipv4Split[1] == "IP4":
329                ipv4_addresses.append(ipv4Split[2])
330        return ipv4_addresses
331
332    def set_ipv6_interfaces(self, line4):
333        ipv6_addresses = list()
334        for line in line4:
335            ipv6Split = line.split()
336            if ipv6Split[1] == "IP6":
337                ipv6_addresses.append(ipv6Split[2])
338        return ipv6_addresses
339
340    def populate_neighbors(self, lines3):
341        neighbors = dict()
342        for line in lines3:
343            neighborSplit = line.split("|")
344            innerData = dict()
345            innerData['Remote Chassis ID'] = neighborSplit[2].strip()
346            innerData['Remote Port'] = neighborSplit[3].strip()
347            sysName = neighborSplit[4].strip()
348            if sysName is not None:
349                innerData['Remote System Name'] = neighborSplit[4].strip()
350            else:
351                innerData['Remote System Name'] = "NA"
352            neighbors[neighborSplit[0].strip()] = innerData
353        return neighbors
354
355    def populate_interfaces(self, lines1, lines2):
356        interfaces = dict()
357        for line1, line2 in zip(lines1, lines2):
358            line = line1 + "  " + line2
359            intfSplit = line.split()
360            innerData = dict()
361            innerData['description'] = intfSplit[6].strip()
362            innerData['macaddress'] = intfSplit[8].strip()
363            innerData['mtu'] = intfSplit[9].strip()
364            innerData['speed'] = intfSplit[1].strip()
365            innerData['duplex'] = intfSplit[2].strip()
366            innerData['operstatus'] = intfSplit[5].strip()
367            if("up" not in intfSplit[5].strip()) and ("down" not in intfSplit[5].strip()):
368                innerData['description'] = intfSplit[7].strip()
369                innerData['macaddress'] = intfSplit[9].strip()
370                innerData['mtu'] = intfSplit[10].strip()
371                innerData['operstatus'] = intfSplit[6].strip()
372            interfaces[intfSplit[0].strip()] = innerData
373        return interfaces
374
375    def parse_neighbors(self, neighbors):
376        parsed = list()
377        for line in neighbors.split('\n'):
378            if len(line) == 0:
379                continue
380            else:
381                line = line.strip()
382                match = re.match(r'^([0-9]+)', line)
383                if match:
384                    key = match.group(1)
385                    parsed.append(line)
386                match = re.match(r'^(INT+)', line)
387                if match:
388                    key = match.group(1)
389                    parsed.append(line)
390                match = re.match(r'^(EXT+)', line)
391                if match:
392                    key = match.group(1)
393                    parsed.append(line)
394                match = re.match(r'^(MGT+)', line)
395                if match:
396                    key = match.group(1)
397                    parsed.append(line)
398        return parsed
399
400    def parse_interfaces(self, data):
401        parsed = list()
402        for line in data.split('\n'):
403            if len(line) == 0:
404                continue
405            else:
406                line = line.strip()
407                match = re.match(r'^([0-9]+)', line)
408                if match:
409                    key = match.group(1)
410                    parsed.append(line)
411                match = re.match(r'^(INT+)', line)
412                if match:
413                    key = match.group(1)
414                    parsed.append(line)
415                match = re.match(r'^(EXT+)', line)
416                if match:
417                    key = match.group(1)
418                    parsed.append(line)
419                match = re.match(r'^(MGT+)', line)
420                if match:
421                    key = match.group(1)
422                    parsed.append(line)
423        return parsed
424
425
426FACT_SUBSETS = dict(
427    default=Default,
428    hardware=Hardware,
429    interfaces=Interfaces,
430    config=Config,
431)
432
433VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
434
435PERSISTENT_COMMAND_TIMEOUT = 60
436
437
438def main():
439    """main entry point for module execution
440    """
441    argument_spec = dict(
442        gather_subset=dict(default=['!config'], type='list')
443    )
444
445    argument_spec.update(enos_argument_spec)
446
447    module = AnsibleModule(argument_spec=argument_spec,
448                           supports_check_mode=True)
449
450    gather_subset = module.params['gather_subset']
451
452    runable_subsets = set()
453    exclude_subsets = set()
454
455    for subset in gather_subset:
456        if subset == 'all':
457            runable_subsets.update(VALID_SUBSETS)
458            continue
459
460        if subset.startswith('!'):
461            subset = subset[1:]
462            if subset == 'all':
463                exclude_subsets.update(VALID_SUBSETS)
464                continue
465            exclude = True
466        else:
467            exclude = False
468
469        if subset not in VALID_SUBSETS:
470            module.fail_json(msg='Bad subset')
471
472        if exclude:
473            exclude_subsets.add(subset)
474        else:
475            runable_subsets.add(subset)
476
477    if not runable_subsets:
478        runable_subsets.update(VALID_SUBSETS)
479
480    runable_subsets.difference_update(exclude_subsets)
481    runable_subsets.add('default')
482
483    facts = dict()
484    facts['gather_subset'] = list(runable_subsets)
485
486    instances = list()
487    for key in runable_subsets:
488        instances.append(FACT_SUBSETS[key](module))
489
490    for inst in instances:
491        inst.populate()
492        facts.update(inst.facts)
493
494    ansible_facts = dict()
495    for key, value in iteritems(facts):
496        key = 'ansible_net_%s' % key
497        ansible_facts[key] = value
498
499    warnings = list()
500    check_args(module, warnings)
501
502    module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
503
504
505if __name__ == '__main__':
506    main()
507