1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# (C) 2019 Red Hat Inc.
5# Copyright (C) 2019 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# Module to Collect facts from Lenovo Switches running Lenovo CNOS commands
16# Lenovo Networking
17#
18from __future__ import absolute_import, division, print_function
19__metaclass__ = type
20
21
22ANSIBLE_METADATA = {'metadata_version': '1.1',
23                    'status': ['preview'],
24                    'supported_by': 'community'}
25
26DOCUMENTATION = '''
27---
28module: cnos_facts
29version_added: "2.3"
30author: "Anil Kumar Muraleedharan (@amuraleedhar)"
31short_description: Collect facts from remote devices running Lenovo CNOS
32description:
33  - Collects a base set of device facts from a remote Lenovo device
34    running on CNOS.  This module prepends all of the
35    base network fact keys with C(ansible_net_<fact>).  The facts
36    module will always collect a base set of facts from the device
37    and can enable or disable collection of additional facts.
38notes:
39  - Tested against CNOS 10.8.1
40options:
41  authorize:
42    version_added: "2.6"
43    description:
44      - Instructs the module to enter privileged mode on the remote device
45        before sending any commands.  If not specified, the device will
46        attempt to execute all commands in non-privileged mode. If the value
47        is not specified in the task, the value of environment variable
48        C(ANSIBLE_NET_AUTHORIZE) will be used instead.
49    type: bool
50    default: 'no'
51  auth_pass:
52    version_added: "2.6"
53    description:
54      - Specifies the password to use if required to enter privileged mode
55        on the remote device.  If I(authorize) is false, then this argument
56        does nothing. If the value is not specified in the task, the value of
57        environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead.
58  gather_subset:
59    version_added: "2.6"
60    description:
61      - When supplied, this argument will restrict the facts collected
62        to a given subset.  Possible values for this argument include
63        all, hardware, config, and interfaces.  Can specify a list of
64        values to include a larger subset.  Values can also be used
65        with an initial C(M(!)) to specify that a specific subset should
66        not be collected.
67    required: false
68    default: '!config'
69'''
70EXAMPLES = '''
71Tasks: The following are examples of using the module cnos_facts.
72---
73- name: Test cnos Facts
74  cnos_facts:
75
76---
77# Collect all facts from the device
78- cnos_facts:
79    gather_subset: all
80
81# Collect only the config and default facts
82- cnos_facts:
83    gather_subset:
84      - config
85
86# Do not collect hardware facts
87- cnos_facts:
88    gather_subset:
89      - "!hardware"
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 CNOS device
99    returned: always
100    type: str
101  ansible_net_serialnum:
102    description: The serial number of the Lenovo CNOS device
103    returned: always
104    type: str
105  ansible_net_version:
106    description: The CNOS 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.cnos.cnos import run_commands
151from ansible.module_utils.network.cnos.cnos import check_args
152from ansible.module_utils._text import to_text
153from ansible.module_utils.basic import AnsibleModule
154from ansible.module_utils.six import iteritems
155from ansible.module_utils.six.moves import zip
156
157
158class FactsBase(object):
159
160    COMMANDS = list()
161
162    def __init__(self, module):
163        self.module = module
164        self.facts = dict()
165        self.responses = None
166        self.PERSISTENT_COMMAND_TIMEOUT = 60
167
168    def populate(self):
169        self.responses = run_commands(self.module, self.COMMANDS,
170                                      check_rc=False)
171
172    def run(self, cmd):
173        return run_commands(self.module, cmd, check_rc=False)
174
175
176class Default(FactsBase):
177
178    COMMANDS = ['show sys-info', 'show running-config']
179
180    def populate(self):
181        super(Default, self).populate()
182        data = self.responses[0]
183        data_run = self.responses[1]
184        if data:
185            self.facts['version'] = self.parse_version(data)
186            self.facts['serialnum'] = self.parse_serialnum(data)
187            self.facts['model'] = self.parse_model(data)
188            self.facts['image'] = self.parse_image(data)
189        if data_run:
190            self.facts['hostname'] = self.parse_hostname(data_run)
191
192    def parse_version(self, data):
193        for line in data.split('\n'):
194            line = line.strip()
195            match = re.match(r'System Software Revision (.*?)',
196                             line, re.M | re.I)
197            if match:
198                vers = line.split(':')
199                ver = vers[1].strip()
200                return ver
201        return "NA"
202
203    def parse_hostname(self, data_run):
204        for line in data_run.split('\n'):
205            line = line.strip()
206            match = re.match(r'hostname (.*?)', line, re.M | re.I)
207            if match:
208                hosts = line.split()
209                hostname = hosts[1].strip('\"')
210                return hostname
211        return "NA"
212
213    def parse_model(self, data):
214        for line in data.split('\n'):
215            line = line.strip()
216            match = re.match(r'System Model (.*?)', line, re.M | re.I)
217            if match:
218                mdls = line.split(':')
219                mdl = mdls[1].strip()
220                return mdl
221        return "NA"
222
223    def parse_image(self, data):
224        match = re.search(r'(.*) image(.*)', data, re.M | re.I)
225        if match:
226            return "Image1"
227        else:
228            return "Image2"
229
230    def parse_serialnum(self, data):
231        for line in data.split('\n'):
232            line = line.strip()
233            match = re.match(r'System Serial Number (.*?)', line, re.M | re.I)
234            if match:
235                serNums = line.split(':')
236                ser = serNums[1].strip()
237                return ser
238        return "NA"
239
240
241class Hardware(FactsBase):
242
243    COMMANDS = [
244        'show running-config'
245    ]
246
247    def populate(self):
248        super(Hardware, self).populate()
249        data = self.run(['show process memory'])
250        data = to_text(data, errors='surrogate_or_strict').strip()
251        data = data.replace(r"\n", "\n")
252        if data:
253            for line in data.split('\n'):
254                line = line.strip()
255                match = re.match(r'Mem: (.*?)', line, re.M | re.I)
256                if match:
257                    memline = line.split(':')
258                    mems = memline[1].strip().split()
259                    self.facts['memtotal_mb'] = int(mems[0]) / 1024
260                    self.facts['memused_mb'] = int(mems[1]) / 1024
261                    self.facts['memfree_mb'] = int(mems[2]) / 1024
262                    self.facts['memshared_mb'] = int(mems[3]) / 1024
263                    self.facts['memavailable_mb'] = int(mems[5]) / 1024
264
265    def parse_memtotal(self, data):
266        match = re.search(r'^MemTotal:\s*(.*) kB', data, re.M | re.I)
267        if match:
268            return int(match.group(1)) / 1024
269
270    def parse_memfree(self, data):
271        match = re.search(r'^MemFree:\s*(.*) kB', data, re.M | re.I)
272        if match:
273            return int(match.group(1)) / 1024
274
275
276class Config(FactsBase):
277
278    COMMANDS = ['show running-config']
279
280    def populate(self):
281        super(Config, self).populate()
282        data = self.responses[0]
283        if data:
284            self.facts['config'] = data
285
286
287class Interfaces(FactsBase):
288
289    COMMANDS = ['show interface brief']
290
291    def populate(self):
292        super(Interfaces, self).populate()
293
294        self.facts['all_ipv4_addresses'] = list()
295        self.facts['all_ipv6_addresses'] = list()
296
297        data1 = self.run(['show interface status'])
298        data1 = to_text(data1, errors='surrogate_or_strict').strip()
299        data1 = data1.replace(r"\n", "\n")
300        data2 = self.run(['show interface mac-address'])
301        data2 = to_text(data2, errors='surrogate_or_strict').strip()
302        data2 = data2.replace(r"\n", "\n")
303        lines1 = None
304        lines2 = None
305        if data1:
306            lines1 = self.parse_interfaces(data1)
307        if data2:
308            lines2 = self.parse_interfaces(data2)
309        if lines1 is not None and lines2 is not None:
310            self.facts['interfaces'] = self.populate_interfaces(lines1, lines2)
311        data3 = self.run(['show lldp neighbors'])
312        data3 = to_text(data3, errors='surrogate_or_strict').strip()
313        data3 = data3.replace(r"\n", "\n")
314        if data3:
315            lines3 = self.parse_neighbors(data3)
316        if lines3 is not None:
317            self.facts['neighbors'] = self.populate_neighbors(lines3)
318
319        data4 = self.run(['show ip interface brief vrf all'])
320        data5 = self.run(['show ipv6 interface brief vrf all'])
321        data4 = to_text(data4, errors='surrogate_or_strict').strip()
322        data4 = data4.replace(r"\n", "\n")
323        data5 = to_text(data5, errors='surrogate_or_strict').strip()
324        data5 = data5.replace(r"\n", "\n")
325        lines4 = None
326        lines5 = None
327        if data4:
328            lines4 = self.parse_ipaddresses(data4)
329            ipv4_interfaces = self.set_ip_interfaces(lines4)
330            self.facts['all_ipv4_addresses'] = ipv4_interfaces
331        if data5:
332            lines5 = self.parse_ipaddresses(data5)
333            ipv6_interfaces = self.set_ipv6_interfaces(lines5)
334            self.facts['all_ipv6_addresses'] = ipv6_interfaces
335
336    def parse_ipaddresses(self, data):
337        parsed = list()
338        for line in data.split('\n'):
339            if len(line) == 0:
340                continue
341            else:
342                line = line.strip()
343                match = re.match(r'^(Ethernet+)', line)
344                if match:
345                    key = match.group(1)
346                    parsed.append(line)
347                match = re.match(r'^(po+)', line)
348                if match:
349                    key = match.group(1)
350                    parsed.append(line)
351                match = re.match(r'^(mgmt+)', line)
352                if match:
353                    key = match.group(1)
354                    parsed.append(line)
355                match = re.match(r'^(loopback+)', line)
356                if match:
357                    key = match.group(1)
358                    parsed.append(line)
359        return parsed
360
361    def populate_interfaces(self, lines1, lines2):
362        interfaces = dict()
363        for line1, line2 in zip(lines1, lines2):
364            line = line1 + "  " + line2
365            intfSplit = line.split()
366            innerData = dict()
367            innerData['description'] = intfSplit[1].strip()
368            innerData['macaddress'] = intfSplit[8].strip()
369            innerData['type'] = intfSplit[6].strip()
370            innerData['speed'] = intfSplit[5].strip()
371            innerData['duplex'] = intfSplit[4].strip()
372            innerData['operstatus'] = intfSplit[2].strip()
373            interfaces[intfSplit[0].strip()] = innerData
374        return interfaces
375
376    def parse_interfaces(self, data):
377        parsed = list()
378        for line in data.split('\n'):
379            if len(line) == 0:
380                continue
381            else:
382                line = line.strip()
383                match = re.match(r'^(Ethernet+)', line)
384                if match:
385                    key = match.group(1)
386                    parsed.append(line)
387                match = re.match(r'^(po+)', line)
388                if match:
389                    key = match.group(1)
390                    parsed.append(line)
391                match = re.match(r'^(mgmt+)', line)
392                if match:
393                    key = match.group(1)
394                    parsed.append(line)
395        return parsed
396
397    def set_ip_interfaces(self, line4):
398        ipv4_addresses = list()
399        for line in line4:
400            ipv4Split = line.split()
401            if 'Ethernet' in ipv4Split[0]:
402                ipv4_addresses.append(ipv4Split[1])
403            if 'mgmt' in ipv4Split[0]:
404                ipv4_addresses.append(ipv4Split[1])
405            if 'po' in ipv4Split[0]:
406                ipv4_addresses.append(ipv4Split[1])
407            if 'loopback' in ipv4Split[0]:
408                ipv4_addresses.append(ipv4Split[1])
409        return ipv4_addresses
410
411    def set_ipv6_interfaces(self, line4):
412        ipv6_addresses = list()
413        for line in line4:
414            ipv6Split = line.split()
415            if 'Ethernet' in ipv6Split[0]:
416                ipv6_addresses.append(ipv6Split[1])
417            if 'mgmt' in ipv6Split[0]:
418                ipv6_addresses.append(ipv6Split[1])
419            if 'po' in ipv6Split[0]:
420                ipv6_addresses.append(ipv6Split[1])
421            if 'loopback' in ipv6Split[0]:
422                ipv6_addresses.append(ipv6Split[1])
423        return ipv6_addresses
424
425    def populate_neighbors(self, lines3):
426        neighbors = dict()
427        device_name = ''
428        for line in lines3:
429            neighborSplit = line.split()
430            innerData = dict()
431            count = len(neighborSplit)
432            if count == 5:
433                local_interface = neighborSplit[1].strip()
434                innerData['Device Name'] = neighborSplit[0].strip()
435                innerData['Hold Time'] = neighborSplit[2].strip()
436                innerData['Capability'] = neighborSplit[3].strip()
437                innerData['Remote Port'] = neighborSplit[4].strip()
438                neighbors[local_interface] = innerData
439            elif count == 4:
440                local_interface = neighborSplit[0].strip()
441                innerData['Hold Time'] = neighborSplit[1].strip()
442                innerData['Capability'] = neighborSplit[2].strip()
443                innerData['Remote Port'] = neighborSplit[3].strip()
444                neighbors[local_interface] = innerData
445        return neighbors
446
447    def parse_neighbors(self, neighbors):
448        parsed = list()
449        for line in neighbors.split('\n'):
450            if len(line) == 0:
451                continue
452            else:
453                line = line.strip()
454                if 'Ethernet' in line:
455                    parsed.append(line)
456                if 'mgmt' in line:
457                    parsed.append(line)
458                if 'po' in line:
459                    parsed.append(line)
460                if 'loopback' in line:
461                    parsed.append(line)
462        return parsed
463
464
465FACT_SUBSETS = dict(
466    default=Default,
467    hardware=Hardware,
468    interfaces=Interfaces,
469    config=Config,
470)
471
472VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
473
474PERSISTENT_COMMAND_TIMEOUT = 60
475
476
477def main():
478    """main entry point for module execution
479    """
480    argument_spec = dict(
481        gather_subset=dict(default=['!config'], type='list')
482    )
483
484    module = AnsibleModule(argument_spec=argument_spec,
485                           supports_check_mode=True)
486
487    gather_subset = module.params['gather_subset']
488
489    runable_subsets = set()
490    exclude_subsets = set()
491
492    for subset in gather_subset:
493        if subset == 'all':
494            runable_subsets.update(VALID_SUBSETS)
495            continue
496
497        if subset.startswith('!'):
498            subset = subset[1:]
499            if subset == 'all':
500                exclude_subsets.update(VALID_SUBSETS)
501                continue
502            exclude = True
503        else:
504            exclude = False
505
506        if subset not in VALID_SUBSETS:
507            module.fail_json(msg='Bad subset')
508
509        if exclude:
510            exclude_subsets.add(subset)
511        else:
512            runable_subsets.add(subset)
513
514    if not runable_subsets:
515        runable_subsets.update(VALID_SUBSETS)
516
517    runable_subsets.difference_update(exclude_subsets)
518    runable_subsets.add('default')
519
520    facts = dict()
521    facts['gather_subset'] = list(runable_subsets)
522
523    instances = list()
524    for key in runable_subsets:
525        instances.append(FACT_SUBSETS[key](module))
526
527    for inst in instances:
528        inst.populate()
529        facts.update(inst.facts)
530
531    ansible_facts = dict()
532    for key, value in iteritems(facts):
533        key = 'ansible_net_%s' % key
534        ansible_facts[key] = value
535
536    warnings = list()
537    check_args(module, warnings)
538
539    module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
540
541
542if __name__ == '__main__':
543    main()
544