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: slxos_facts 32version_added: "2.6" 33author: "Lindsay Hill (@LindsayHill)" 34short_description: Collect facts from devices running Extreme SLX-OS 35description: 36 - Collects a base set of device facts from a remote device that 37 is running SLX-OS. 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 SLX-OS 17s.1.02 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- slxos_facts: 59 gather_subset: all 60 61# Collect only the config and default facts 62- slxos_facts: 63 gather_subset: 64 - config 65 66# Do not collect hardware facts 67- slxos_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.slxos.slxos 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'SLX-OS 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'Total\s*Memory: (\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 260 def populate_interfaces(self, interfaces): 261 facts = dict() 262 for key, value in iteritems(interfaces): 263 intf = dict() 264 intf['description'] = self.parse_description(value) 265 intf['macaddress'] = self.parse_macaddress(value) 266 intf['mtu'] = self.parse_mtu(value) 267 intf['bandwidth'] = self.parse_bandwidth(value) 268 intf['duplex'] = self.parse_duplex(value) 269 intf['lineprotocol'] = self.parse_lineprotocol(value) 270 intf['operstatus'] = self.parse_operstatus(value) 271 intf['type'] = self.parse_type(value) 272 273 facts[key] = intf 274 return facts 275 276 def populate_ipv4_interfaces(self, data): 277 for key, value in data.items(): 278 self.facts['interfaces'][key]['ipv4'] = list() 279 primary_address = addresses = [] 280 primary_address = re.findall(r'Primary Internet Address is (\S+)', value, re.M) 281 addresses = re.findall(r'Secondary Internet Address is (\S+)', value, re.M) 282 if not primary_address: 283 continue 284 addresses.append(primary_address[0]) 285 for address in addresses: 286 addr, subnet = address.split("/") 287 ipv4 = dict(address=addr.strip(), subnet=subnet.strip()) 288 self.add_ip_address(addr.strip(), 'ipv4') 289 self.facts['interfaces'][key]['ipv4'].append(ipv4) 290 291 # Only gets primary IPv6 addresses 292 def populate_ipv6_interfaces(self, data): 293 interfaces = re.split('=+', data)[1].strip() 294 matches = re.findall(r'(\S+ \S+) +[\w-]+.+\s+([\w:/]+/\d+)', interfaces, re.M) 295 for match in matches: 296 interface = match[0] 297 self.facts['interfaces'][interface]['ipv6'] = list() 298 address, masklen = match[1].split('/') 299 ipv6 = dict(address=address, masklen=int(masklen)) 300 self.add_ip_address(ipv6['address'], 'ipv6') 301 self.facts['interfaces'][interface]['ipv6'].append(ipv6) 302 303 def add_ip_address(self, address, family): 304 if family == 'ipv4': 305 self.facts['all_ipv4_addresses'].append(address) 306 else: 307 self.facts['all_ipv6_addresses'].append(address) 308 309 def parse_neighbors(self, neighbors): 310 facts = dict() 311 lines = neighbors.split('Local Interface: ') 312 if not lines: 313 return facts 314 for line in lines: 315 match = re.search(r'(\w+ \S+)\s+\(Local Int.+?\)[\s\S]+Remote Interface: (\S+.+?) \(Remote Int.+?\)[\s\S]+System Name: (\S+)', line, re.M) 316 if match: 317 intf = match.group(1) 318 if intf not in facts: 319 facts[intf] = list() 320 fact = dict() 321 fact['host'] = match.group(3) 322 fact['port'] = match.group(2) 323 facts[intf].append(fact) 324 return facts 325 326 def parse_interfaces(self, data): 327 parsed = dict() 328 for interface in data.split('\n\n'): 329 match = re.match(r'^(\S+ \S+)', interface, re.M) 330 if not match: 331 continue 332 else: 333 parsed[match.group(1)] = interface 334 return parsed 335 336 def parse_description(self, data): 337 match = re.search(r'Description: (.+)$', data, re.M) 338 if match: 339 return match.group(1) 340 341 def parse_macaddress(self, data): 342 match = re.search(r'Hardware is Ethernet, address is (\S+)', data) 343 if match: 344 return match.group(1) 345 346 def parse_ipv4(self, data): 347 match = re.search(r'Primary Internet Address is ([^\s,]+)', data) 348 if match: 349 addr, masklen = match.group(1).split('/') 350 return dict(address=addr, masklen=int(masklen)) 351 352 def parse_mtu(self, data): 353 match = re.search(r'MTU (\d+) bytes', data) 354 if match: 355 return int(match.group(1)) 356 357 def parse_bandwidth(self, data): 358 match = re.search(r'LineSpeed Actual\s+:\s(.+)', data) 359 if match: 360 return match.group(1) 361 362 def parse_duplex(self, data): 363 match = re.search(r'Duplex: (\S+)', data, re.M) 364 if match: 365 return match.group(1) 366 367 def parse_type(self, data): 368 match = re.search(r'Hardware is (.+),', data, re.M) 369 if match: 370 return match.group(1) 371 372 def parse_lineprotocol(self, data): 373 match = re.search(r'line protocol is (\S+)', data, re.M) 374 if match: 375 return match.group(1) 376 377 def parse_operstatus(self, data): 378 match = re.match(r'^(?:.+) is (.+),', data, re.M) 379 if match: 380 return match.group(1) 381 382 383FACT_SUBSETS = dict( 384 default=Default, 385 hardware=Hardware, 386 interfaces=Interfaces, 387 config=Config) 388 389VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 390 391 392def main(): 393 """main entry point for module execution 394 """ 395 argument_spec = dict( 396 gather_subset=dict(default=["!config"], type='list') 397 ) 398 399 module = AnsibleModule(argument_spec=argument_spec, 400 supports_check_mode=True) 401 402 gather_subset = module.params['gather_subset'] 403 404 runable_subsets = set() 405 exclude_subsets = set() 406 407 for subset in gather_subset: 408 if subset == 'all': 409 runable_subsets.update(VALID_SUBSETS) 410 continue 411 412 if subset.startswith('!'): 413 subset = subset[1:] 414 if subset == 'all': 415 exclude_subsets.update(VALID_SUBSETS) 416 continue 417 exclude = True 418 else: 419 exclude = False 420 421 if subset not in VALID_SUBSETS: 422 module.fail_json(msg='Bad subset') 423 424 if exclude: 425 exclude_subsets.add(subset) 426 else: 427 runable_subsets.add(subset) 428 429 if not runable_subsets: 430 runable_subsets.update(VALID_SUBSETS) 431 432 runable_subsets.difference_update(exclude_subsets) 433 runable_subsets.add('default') 434 435 facts = dict() 436 facts['gather_subset'] = list(runable_subsets) 437 438 instances = list() 439 for key in runable_subsets: 440 instances.append(FACT_SUBSETS[key](module)) 441 442 for inst in instances: 443 inst.populate() 444 facts.update(inst.facts) 445 446 ansible_facts = dict() 447 for key, value in iteritems(facts): 448 key = 'ansible_net_%s' % key 449 ansible_facts[key] = value 450 451 warnings = list() 452 453 module.exit_json(ansible_facts=ansible_facts, warnings=warnings) 454 455 456if __name__ == '__main__': 457 main() 458