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