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