1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# Copyright (c) 2018 Ansible Project
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'community'}
13
14
15DOCUMENTATION = """
16---
17module: edgeswitch_facts
18version_added: "2.8"
19author: "Frederic Bor (@f-bor)"
20short_description: Collect facts from remote devices running Edgeswitch
21description:
22  - Collects a base set of device facts from a remote device that
23    is running Ubiquiti Edgeswitch.  This module prepends all of the
24    base network fact keys with C(ansible_net_<fact>).  The facts
25    module will always collect a base set of facts from the device
26    and can enable or disable collection of additional facts.
27notes:
28  - Tested against Edgeswitch 1.7.4
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, 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    required: false
39    default: '!config'
40"""
41
42EXAMPLES = """
43# Collect all facts from the device
44- edgeswitch_facts:
45    gather_subset: all
46
47# Collect only the config and default facts
48- edgeswitch_facts:
49    gather_subset:
50      - config
51
52"""
53
54RETURN = """
55ansible_net_gather_subset:
56  description: The list of fact subsets collected from the device
57  returned: always
58  type: list
59
60# default
61ansible_net_model:
62  description: The model name returned from the device
63  returned: always
64  type: str
65ansible_net_serialnum:
66  description: The serial number of the remote device
67  returned: always
68  type: str
69ansible_net_version:
70  description: The operating system version running on the remote device
71  returned: always
72  type: str
73ansible_net_hostname:
74  description: The configured hostname of the device
75  returned: always
76  type: str
77
78# config
79ansible_net_config:
80  description: The current active config from the device
81  returned: when config is configured
82  type: str
83
84# interfaces
85ansible_net_interfaces:
86  description: A hash of all interfaces running on the system
87  returned: when interfaces is configured
88  type: dict
89"""
90import re
91
92from ansible.module_utils.network.edgeswitch.edgeswitch import run_commands
93from ansible.module_utils.basic import AnsibleModule
94from ansible.module_utils.six import iteritems
95
96
97class FactsBase(object):
98
99    COMMANDS = list()
100
101    def __init__(self, module):
102        self.module = module
103        self.facts = dict()
104        self.responses = None
105
106    def populate(self):
107        self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False)
108
109    def run(self, cmd):
110        return run_commands(self.module, commands=cmd, check_rc=False)
111
112
113class Default(FactsBase):
114
115    COMMANDS = ['show version', 'show sysinfo']
116
117    def populate(self):
118        super(Default, self).populate()
119        data = self.responses[0]
120        if data:
121            self.facts['version'] = self.parse_version(data)
122            self.facts['serialnum'] = self.parse_serialnum(data)
123            self.facts['model'] = self.parse_model(data)
124            self.facts['hostname'] = self.parse_hostname(self.responses[1])
125
126    def parse_version(self, data):
127        match = re.search(r'Software Version\.+ (.*)', data)
128        if match:
129            return match.group(1)
130
131    def parse_hostname(self, data):
132        match = re.search(r'System Name\.+ (.*)', data)
133        if match:
134            return match.group(1)
135
136    def parse_model(self, data):
137        match = re.search(r'Machine Model\.+ (.*)', data)
138        if match:
139            return match.group(1)
140
141    def parse_serialnum(self, data):
142        match = re.search(r'Serial Number\.+ (.*)', data)
143        if match:
144            return match.group(1)
145
146
147class Config(FactsBase):
148
149    COMMANDS = ['show running-config']
150
151    def populate(self):
152        super(Config, self).populate()
153        data = self.responses[0]
154        if data:
155            self.facts['config'] = data
156
157
158class Interfaces(FactsBase):
159
160    COMMANDS = [
161        'show interfaces description',
162        'show interfaces status all'
163    ]
164
165    def populate(self):
166        super(Interfaces, self).populate()
167
168        interfaces = {}
169
170        data = self.responses[0]
171        self.parse_interfaces_description(data, interfaces)
172
173        data = self.responses[1]
174        self.parse_interfaces_status(data, interfaces)
175
176        self.facts['interfaces'] = interfaces
177
178    def parse_interfaces_description(self, data, interfaces):
179        for line in data.split('\n'):
180            match = re.match(r'(\d\/\d+)\s+(\w+)\s+(\w+)', line)
181            if match:
182                name = match.group(1)
183                interface = {}
184                interface['operstatus'] = match.group(2)
185                interface['lineprotocol'] = match.group(3)
186                interface['description'] = line[30:]
187                interfaces[name] = interface
188
189    def parse_interfaces_status(self, data, interfaces):
190        for line in data.split('\n'):
191            match = re.match(r'(\d\/\d+)', line)
192            if match:
193                name = match.group(1)
194                interface = interfaces[name]
195                interface['physicalstatus'] = line[61:71].strip()
196                interface['mediatype'] = line[73:91].strip()
197
198
199FACT_SUBSETS = dict(
200    default=Default,
201    config=Config,
202    interfaces=Interfaces,
203)
204
205VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
206
207
208def main():
209    """main entry point for module execution
210    """
211    argument_spec = dict(
212        gather_subset=dict(default=['!config'], type='list')
213    )
214
215    module = AnsibleModule(argument_spec=argument_spec,
216                           supports_check_mode=True)
217
218    gather_subset = module.params['gather_subset']
219
220    runable_subsets = set()
221    exclude_subsets = set()
222
223    for subset in gather_subset:
224        if subset == 'all':
225            runable_subsets.update(VALID_SUBSETS)
226            continue
227
228        if subset.startswith('!'):
229            subset = subset[1:]
230            if subset == 'all':
231                exclude_subsets.update(VALID_SUBSETS)
232                continue
233            exclude = True
234        else:
235            exclude = False
236
237        if subset not in VALID_SUBSETS:
238            module.fail_json(msg='Bad subset')
239
240        if exclude:
241            exclude_subsets.add(subset)
242        else:
243            runable_subsets.add(subset)
244
245    if not runable_subsets:
246        runable_subsets.update(VALID_SUBSETS)
247
248    runable_subsets.difference_update(exclude_subsets)
249    runable_subsets.add('default')
250
251    facts = dict()
252    facts['gather_subset'] = list(runable_subsets)
253
254    instances = list()
255    for key in runable_subsets:
256        instances.append(FACT_SUBSETS[key](module))
257
258    for inst in instances:
259        inst.populate()
260        facts.update(inst.facts)
261
262    ansible_facts = dict()
263    for key, value in iteritems(facts):
264        key = 'ansible_net_%s' % key
265        ansible_facts[key] = value
266
267    module.exit_json(ansible_facts=ansible_facts)
268
269
270if __name__ == '__main__':
271    main()
272