1#!/usr/bin/python
2#
3# (c) 2018 Extreme Networks Inc.
4#
5# This file is part of Ansible
6#
7# Ansible is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Ansible is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
19#
20from __future__ import absolute_import, division, print_function
21__metaclass__ = type
22
23
24ANSIBLE_METADATA = {'metadata_version': '1.1',
25                    'status': ['preview'],
26                    'supported_by': 'community'}
27
28
29DOCUMENTATION = """
30---
31module: nos_facts
32version_added: "2.7"
33author: "Lindsay Hill (@LindsayHill)"
34short_description: Collect facts from devices running Extreme NOS
35description:
36  - Collects a base set of device facts from a remote device that
37    is running NOS. This module prepends all of the
38    base network fact keys with C(ansible_net_<fact>). The facts
39    module will always collect a base set of facts from the device
40    and can enable or disable collection of additional facts.
41notes:
42  - Tested against NOS 7.2.0
43options:
44  gather_subset:
45    description:
46      - When supplied, this argument will restrict the facts collected
47        to a given subset. Possible values for this argument include
48        all, hardware, config, and interfaces. Can specify a list of
49        values to include a larger subset. Values can also be used
50        with an initial C(M(!)) to specify that a specific subset should
51        not be collected.
52    required: false
53    default: '!config'
54"""
55
56EXAMPLES = """
57# Collect all facts from the device
58- nos_facts:
59    gather_subset: all
60
61# Collect only the config and default facts
62- nos_facts:
63    gather_subset:
64      - config
65
66# Do not collect hardware facts
67- nos_facts:
68    gather_subset:
69      - "!hardware"
70"""
71
72RETURN = """
73ansible_net_gather_subset:
74  description: The list of fact subsets collected from the device
75  returned: always
76  type: list
77
78# default
79ansible_net_model:
80  description: The model name returned from the device
81  returned: always
82  type: str
83ansible_net_serialnum:
84  description: The serial number of the remote device
85  returned: always
86  type: str
87ansible_net_version:
88  description: The operating system version running on the remote device
89  returned: always
90  type: str
91ansible_net_hostname:
92  description: The configured hostname of the device
93  returned: always
94  type: str
95
96# hardware
97ansible_net_memfree_mb:
98  description: The available free memory on the remote device in Mb
99  returned: when hardware is configured
100  type: int
101ansible_net_memtotal_mb:
102  description: The total memory on the remote device in Mb
103  returned: when hardware is configured
104  type: int
105
106# config
107ansible_net_config:
108  description: The current active config from the device
109  returned: when config is configured
110  type: str
111
112# interfaces
113ansible_net_all_ipv4_addresses:
114  description: All IPv4 addresses configured on the device
115  returned: when interfaces is configured
116  type: list
117ansible_net_all_ipv6_addresses:
118  description: All Primary IPv6 addresses configured on the device
119  returned: when interfaces is configured
120  type: list
121ansible_net_interfaces:
122  description: A hash of all interfaces running on the system
123  returned: when interfaces is configured
124  type: dict
125ansible_net_neighbors:
126  description: The list of LLDP neighbors from the remote device
127  returned: when interfaces is configured
128  type: dict
129"""
130import re
131
132from ansible.module_utils.network.nos.nos import run_commands
133from ansible.module_utils.basic import AnsibleModule
134from ansible.module_utils.six import iteritems
135
136
137class FactsBase(object):
138
139    COMMANDS = list()
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)
148
149    def run(self, cmd):
150        return run_commands(self.module, cmd)
151
152
153class Default(FactsBase):
154
155    COMMANDS = [
156        'show version',
157        'show inventory chassis',
158        r'show running-config | include host\-name'
159    ]
160
161    def populate(self):
162        super(Default, self).populate()
163        data = self.responses[0]
164        if data:
165            self.facts['version'] = self.parse_version(data)
166
167        data = self.responses[1]
168        if data:
169            self.facts['model'] = self.parse_model(data)
170            self.facts['serialnum'] = self.parse_serialnum(data)
171
172        data = self.responses[2]
173        if data:
174            self.facts['hostname'] = self.parse_hostname(data)
175
176    def parse_version(self, data):
177        match = re.search(r'Network Operating System Version: (\S+)', data)
178        if match:
179            return match.group(1)
180
181    def parse_model(self, data):
182        match = re.search(r'SID:(\S+)', data, re.M)
183        if match:
184            return match.group(1)
185
186    def parse_hostname(self, data):
187        match = re.search(r'switch-attributes host-name (\S+)', data, re.M)
188        if match:
189            return match.group(1)
190
191    def parse_serialnum(self, data):
192        match = re.search(r'SN:(\S+)', data, re.M)
193        if match:
194            return match.group(1)
195
196
197class Hardware(FactsBase):
198
199    COMMANDS = [
200        'show process memory summary'
201    ]
202
203    def populate(self):
204        super(Hardware, self).populate()
205        data = self.responses[0]
206        if data:
207            self.facts['memtotal_mb'] = int(round(int(self.parse_memtotal(data)) / 1024, 0))
208            self.facts['memfree_mb'] = int(round(int(self.parse_memfree(data)) / 1024, 0))
209
210    def parse_memtotal(self, data):
211        match = re.search(r'TotalMemory: (\d+)\s', data, re.M)
212        if match:
213            return match.group(1)
214
215    def parse_memfree(self, data):
216        match = re.search(r'Total Free: (\d+)\s', data, re.M)
217        if match:
218            return match.group(1)
219
220
221class Config(FactsBase):
222
223    COMMANDS = ['show running-config']
224
225    def populate(self):
226        super(Config, self).populate()
227        data = self.responses[0]
228        if data:
229            self.facts['config'] = data
230
231
232class Interfaces(FactsBase):
233
234    COMMANDS = [
235        'show interface',
236        'show ipv6 interface brief',
237        r'show lldp nei detail | inc ^Local\ Interface|^Remote\ Interface|^System\ Name'
238    ]
239
240    def populate(self):
241        super(Interfaces, self).populate()
242
243        self.facts['all_ipv4_addresses'] = list()
244        self.facts['all_ipv6_addresses'] = list()
245
246        data = self.responses[0]
247        if data:
248            interfaces = self.parse_interfaces(data)
249            self.facts['interfaces'] = self.populate_interfaces(interfaces)
250            self.populate_ipv4_interfaces(interfaces)
251
252        data = self.responses[1]
253        if data:
254            self.populate_ipv6_interfaces(data)
255
256        data = self.responses[2]
257        if data:
258            self.facts['neighbors'] = self.parse_neighbors(data)
259        else:
260            self.facts['neighbors'] = dict()
261
262    def populate_interfaces(self, interfaces):
263        facts = dict()
264        for key, value in iteritems(interfaces):
265            intf = dict()
266            intf['description'] = self.parse_description(value)
267            intf['macaddress'] = self.parse_macaddress(value)
268            intf['mtu'] = self.parse_mtu(value)
269            intf['bandwidth'] = self.parse_bandwidth(value)
270            intf['duplex'] = self.parse_duplex(value)
271            intf['lineprotocol'] = self.parse_lineprotocol(value)
272            intf['operstatus'] = self.parse_operstatus(value)
273            intf['type'] = self.parse_type(value)
274
275            facts[key] = intf
276        return facts
277
278    def populate_ipv4_interfaces(self, data):
279        for key, value in data.items():
280            self.facts['interfaces'][key]['ipv4'] = list()
281            primary_address = addresses = []
282            primary_address = re.findall(r'Primary Internet Address is (\S+)', value, re.M)
283            addresses = re.findall(r'Secondary Internet Address is (\S+)', value, re.M)
284            if not primary_address:
285                continue
286            addresses.append(primary_address[0])
287            for address in addresses:
288                addr, subnet = address.split("/")
289                ipv4 = dict(address=addr.strip(), subnet=subnet.strip())
290                self.add_ip_address(addr.strip(), 'ipv4')
291                self.facts['interfaces'][key]['ipv4'].append(ipv4)
292
293    # Only gets primary IPv6 addresses
294    def populate_ipv6_interfaces(self, data):
295        interfaces = re.split('=+', data)[1].strip()
296        matches = re.findall(r'(\S+ \S+) +[\w-]+.+\s+([\w:/]+/\d+)', interfaces, re.M)
297        for match in matches:
298            interface = match[0]
299            self.facts['interfaces'][interface]['ipv6'] = list()
300            address, masklen = match[1].split('/')
301            ipv6 = dict(address=address, masklen=int(masklen))
302            self.add_ip_address(ipv6['address'], 'ipv6')
303            self.facts['interfaces'][interface]['ipv6'].append(ipv6)
304
305    def add_ip_address(self, address, family):
306        if family == 'ipv4':
307            self.facts['all_ipv4_addresses'].append(address)
308        else:
309            self.facts['all_ipv6_addresses'].append(address)
310
311    def parse_neighbors(self, neighbors):
312        facts = dict()
313        lines = neighbors.split('Local Interface: ')
314        if not lines:
315            return facts
316        for line in lines:
317            match = re.search(r'(\w+ \S+)\s+\(Local Int.+?\)[\s\S]+Remote Interface: (\S+.+?) \(Remote Int.+?\)[\s\S]+System Name: (\S+)', line, re.M)
318            if match:
319                intf = match.group(1)
320                if intf not in facts:
321                    facts[intf] = list()
322                fact = dict()
323                fact['host'] = match.group(3)
324                fact['port'] = match.group(2)
325                facts[intf].append(fact)
326        return facts
327
328    def parse_interfaces(self, data):
329        parsed = dict()
330        for interface in data.split('\n\n'):
331            match = re.match(r'^(\S+ \S+)', interface, re.M)
332            if not match:
333                continue
334            else:
335                parsed[match.group(1)] = interface
336        return parsed
337
338    def parse_description(self, data):
339        match = re.search(r'Description: (.+)$', data, re.M)
340        if match:
341            return match.group(1)
342
343    def parse_macaddress(self, data):
344        match = re.search(r'Hardware is Ethernet, address is (\S+)', data)
345        if match:
346            return match.group(1)
347
348    def parse_ipv4(self, data):
349        match = re.search(r'Primary Internet Address is ([^\s,]+)', data)
350        if match:
351            addr, masklen = match.group(1).split('/')
352            return dict(address=addr, masklen=int(masklen))
353
354    def parse_mtu(self, data):
355        match = re.search(r'MTU (\d+) bytes', data)
356        if match:
357            return int(match.group(1))
358
359    def parse_bandwidth(self, data):
360        match = re.search(r'LineSpeed Actual\s+:\s(.+)', data)
361        if match:
362            return match.group(1)
363
364    def parse_duplex(self, data):
365        match = re.search(r'Duplex: (\S+)', data, re.M)
366        if match:
367            return match.group(1)
368
369    def parse_type(self, data):
370        match = re.search(r'Hardware is (.+),', data, re.M)
371        if match:
372            return match.group(1)
373
374    def parse_lineprotocol(self, data):
375        match = re.search(r'line protocol is (\S+)', data, re.M)
376        if match:
377            return match.group(1)
378
379    def parse_operstatus(self, data):
380        match = re.match(r'^(?:.+) is (.+),', data, re.M)
381        if match:
382            return match.group(1)
383
384
385FACT_SUBSETS = dict(
386    default=Default,
387    hardware=Hardware,
388    interfaces=Interfaces,
389    config=Config)
390
391VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
392
393
394def main():
395    """main entry point for module execution
396    """
397    argument_spec = dict(
398        gather_subset=dict(default=["!config"], type='list')
399    )
400
401    module = AnsibleModule(argument_spec=argument_spec,
402                           supports_check_mode=True)
403
404    gather_subset = module.params['gather_subset']
405
406    runable_subsets = set()
407    exclude_subsets = set()
408
409    for subset in gather_subset:
410        if subset == 'all':
411            runable_subsets.update(VALID_SUBSETS)
412            continue
413
414        if subset.startswith('!'):
415            subset = subset[1:]
416            if subset == 'all':
417                exclude_subsets.update(VALID_SUBSETS)
418                continue
419            exclude = True
420        else:
421            exclude = False
422
423        if subset not in VALID_SUBSETS:
424            module.fail_json(msg='Bad subset')
425
426        if exclude:
427            exclude_subsets.add(subset)
428        else:
429            runable_subsets.add(subset)
430
431    if not runable_subsets:
432        runable_subsets.update(VALID_SUBSETS)
433
434    runable_subsets.difference_update(exclude_subsets)
435    runable_subsets.add('default')
436
437    facts = dict()
438    facts['gather_subset'] = list(runable_subsets)
439
440    instances = list()
441    for key in runable_subsets:
442        instances.append(FACT_SUBSETS[key](module))
443
444    for inst in instances:
445        inst.populate()
446        facts.update(inst.facts)
447
448    ansible_facts = dict()
449    for key, value in iteritems(facts):
450        key = 'ansible_net_%s' % key
451        ansible_facts[key] = value
452
453    warnings = list()
454
455    module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
456
457
458if __name__ == '__main__':
459    main()
460