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: edgeos_facts 18version_added: "2.5" 19author: 20 - Nathaniel Case (@Qalthos) 21 - Sam Doran (@samdoran) 22short_description: Collect facts from remote devices running EdgeOS 23description: 24 - Collects a base set of device facts from a remote device that 25 is running EdgeOS. This module prepends all of the 26 base network fact keys with U(ansible_net_<fact>). The facts 27 module will always collect a base set of facts from the device 28 and can enable or disable collection of additional facts. 29notes: 30 - Tested against EdgeOS 1.9.7 31options: 32 gather_subset: 33 description: 34 - When supplied, this argument will restrict the facts collected 35 to a given subset. Possible values for this argument include 36 all, default, config, and neighbors. Can specify a list of 37 values to include a larger subset. Values can also be used 38 with an initial C(M(!)) to specify that a specific subset should 39 not be collected. 40 required: false 41 default: "!config" 42""" 43 44EXAMPLES = """ 45- name: collect all facts from the device 46 edgeos_facts: 47 gather_subset: all 48 49- name: collect only the config and default facts 50 edgeos_facts: 51 gather_subset: config 52 53- name: collect everything exception the config 54 edgeos_facts: 55 gather_subset: "!config" 56""" 57 58RETURN = """ 59ansible_net_config: 60 description: The running-config from the device 61 returned: when config is configured 62 type: str 63ansible_net_commits: 64 description: The set of available configuration revisions 65 returned: when present 66 type: list 67ansible_net_hostname: 68 description: The configured system hostname 69 returned: always 70 type: str 71ansible_net_model: 72 description: The device model string 73 returned: always 74 type: str 75ansible_net_serialnum: 76 description: The serial number of the device 77 returned: always 78 type: str 79ansible_net_version: 80 description: The version of the software running 81 returned: always 82 type: str 83ansible_net_neighbors: 84 description: The set of LLDP neighbors 85 returned: when interface is configured 86 type: list 87ansible_net_gather_subset: 88 description: The list of subsets gathered by the module 89 returned: always 90 type: list 91""" 92 93import re 94 95from ansible.module_utils.basic import AnsibleModule 96from ansible.module_utils.six import iteritems 97from ansible.module_utils.network.edgeos.edgeos import run_commands 98 99 100class FactsBase(object): 101 102 COMMANDS = frozenset() 103 104 def __init__(self, module): 105 self.module = module 106 self.facts = dict() 107 self.responses = None 108 109 def populate(self): 110 self.responses = run_commands(self.module, list(self.COMMANDS)) 111 112 113class Default(FactsBase): 114 115 COMMANDS = [ 116 'show version', 117 'show host name', 118 ] 119 120 def populate(self): 121 super(Default, self).populate() 122 data = self.responses[0] 123 124 self.facts['version'] = self.parse_version(data) 125 self.facts['serialnum'] = self.parse_serialnum(data) 126 self.facts['model'] = self.parse_model(data) 127 128 self.facts['hostname'] = self.responses[1] 129 130 def parse_version(self, data): 131 match = re.search(r'Version:\s*v(\S+)', data) 132 if match: 133 return match.group(1) 134 135 def parse_model(self, data): 136 match = re.search(r'HW model:\s*([A-Za-z0-9- ]+)', data) 137 if match: 138 return match.group(1) 139 140 def parse_serialnum(self, data): 141 match = re.search(r'HW S/N:\s+(\S+)', data) 142 if match: 143 return match.group(1) 144 145 146class Config(FactsBase): 147 148 COMMANDS = [ 149 'show configuration commands', 150 'show system commit', 151 ] 152 153 def populate(self): 154 super(Config, self).populate() 155 156 self.facts['config'] = self.responses 157 158 commits = self.responses[1] 159 entries = list() 160 entry = None 161 162 for line in commits.split('\n'): 163 match = re.match(r'(\d+)\s+(.+)by(.+)via(.+)', line) 164 if match: 165 if entry: 166 entries.append(entry) 167 168 entry = dict(revision=match.group(1), 169 datetime=match.group(2), 170 by=str(match.group(3)).strip(), 171 via=str(match.group(4)).strip(), 172 comment=None) 173 elif entry: 174 entry['comment'] = line.strip() 175 176 self.facts['commits'] = entries 177 178 179class Neighbors(FactsBase): 180 181 COMMANDS = [ 182 'show lldp neighbors', 183 'show lldp neighbors detail', 184 ] 185 186 def populate(self): 187 super(Neighbors, self).populate() 188 189 all_neighbors = self.responses[0] 190 if 'LLDP not configured' not in all_neighbors: 191 neighbors = self.parse( 192 self.responses[1] 193 ) 194 self.facts['neighbors'] = self.parse_neighbors(neighbors) 195 196 def parse(self, data): 197 parsed = list() 198 values = None 199 for line in data.split('\n'): 200 if not line: 201 continue 202 elif line[0] == ' ': 203 values += '\n%s' % line 204 elif line.startswith('Interface'): 205 if values: 206 parsed.append(values) 207 values = line 208 if values: 209 parsed.append(values) 210 return parsed 211 212 def parse_neighbors(self, data): 213 facts = dict() 214 for item in data: 215 interface = self.parse_interface(item) 216 host = self.parse_host(item) 217 port = self.parse_port(item) 218 if interface not in facts: 219 facts[interface] = list() 220 facts[interface].append(dict(host=host, port=port)) 221 return facts 222 223 def parse_interface(self, data): 224 match = re.search(r'^Interface:\s+(\S+),', data) 225 return match.group(1) 226 227 def parse_host(self, data): 228 match = re.search(r'SysName:\s+(.+)$', data, re.M) 229 if match: 230 return match.group(1) 231 232 def parse_port(self, data): 233 match = re.search(r'PortDescr:\s+(.+)$', data, re.M) 234 if match: 235 return match.group(1) 236 237 238FACT_SUBSETS = dict( 239 default=Default, 240 neighbors=Neighbors, 241 config=Config 242) 243 244VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 245 246 247def main(): 248 spec = dict( 249 gather_subset=dict(default=['!config'], type='list') 250 ) 251 252 module = AnsibleModule(argument_spec=spec, 253 supports_check_mode=True) 254 255 warnings = list() 256 257 gather_subset = module.params['gather_subset'] 258 259 runable_subsets = set() 260 exclude_subsets = set() 261 262 for subset in gather_subset: 263 if subset == 'all': 264 runable_subsets.update(VALID_SUBSETS) 265 continue 266 267 if subset.startswith('!'): 268 subset = subset[1:] 269 if subset == 'all': 270 exclude_subsets.update(VALID_SUBSETS) 271 continue 272 exclude = True 273 else: 274 exclude = False 275 276 if subset not in VALID_SUBSETS: 277 module.fail_json(msg='Subset must be one of [%s], got %s' % 278 (', '.join(VALID_SUBSETS), subset)) 279 280 if exclude: 281 exclude_subsets.add(subset) 282 else: 283 runable_subsets.add(subset) 284 285 if not runable_subsets: 286 runable_subsets.update(VALID_SUBSETS) 287 288 runable_subsets.difference_update(exclude_subsets) 289 runable_subsets.add('default') 290 291 facts = dict() 292 facts['gather_subset'] = list(runable_subsets) 293 294 instances = list() 295 for key in runable_subsets: 296 instances.append(FACT_SUBSETS[key](module)) 297 298 for inst in instances: 299 inst.populate() 300 facts.update(inst.facts) 301 302 ansible_facts = dict() 303 for key, value in iteritems(facts): 304 key = 'ansible_net_%s' % key 305 ansible_facts[key] = value 306 307 module.exit_json(ansible_facts=ansible_facts, warnings=warnings) 308 309 310if __name__ == '__main__': 311 main() 312