1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# This file is part of Networklore's snmp library for Ansible 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10DOCUMENTATION = r''' 11--- 12module: snmp_facts 13author: 14- Patrick Ogenstad (@ogenstad) 15short_description: Retrieve facts for a device using SNMP 16description: 17 - Retrieve facts for a device using SNMP, the facts will be 18 inserted to the ansible_facts key. 19requirements: 20 - pysnmp 21options: 22 host: 23 description: 24 - Set to target SNMP server (normally C({{ inventory_hostname }})). 25 type: str 26 required: true 27 version: 28 description: 29 - SNMP Version to use, C(v2), C(v2c) or C(v3). 30 type: str 31 required: true 32 choices: [ v2, v2c, v3 ] 33 community: 34 description: 35 - The SNMP community string, required if I(version) is C(v2) or C(v2c). 36 type: str 37 level: 38 description: 39 - Authentication level. 40 - Required if I(version) is C(v3). 41 type: str 42 choices: [ authNoPriv, authPriv ] 43 username: 44 description: 45 - Username for SNMPv3. 46 - Required if I(version) is C(v3). 47 type: str 48 integrity: 49 description: 50 - Hashing algorithm. 51 - Required if I(version) is C(v3). 52 type: str 53 choices: [ md5, sha ] 54 authkey: 55 description: 56 - Authentication key. 57 - Required I(version) is C(v3). 58 type: str 59 privacy: 60 description: 61 - Encryption algorithm. 62 - Required if I(level) is C(authPriv). 63 type: str 64 choices: [ aes, des ] 65 privkey: 66 description: 67 - Encryption key. 68 - Required if I(level) is C(authPriv). 69 type: str 70 timeout: 71 description: 72 - Response timeout in seconds. 73 type: int 74 version_added: 2.3.0 75 retries: 76 description: 77 - Maximum number of request retries, 0 retries means just a single request. 78 type: int 79 version_added: 2.3.0 80''' 81 82EXAMPLES = r''' 83- name: Gather facts with SNMP version 2 84 community.general.snmp_facts: 85 host: '{{ inventory_hostname }}' 86 version: v2c 87 community: public 88 delegate_to: local 89 90- name: Gather facts using SNMP version 3 91 community.general.snmp_facts: 92 host: '{{ inventory_hostname }}' 93 version: v3 94 level: authPriv 95 integrity: sha 96 privacy: aes 97 username: snmp-user 98 authkey: abc12345 99 privkey: def6789 100 delegate_to: localhost 101''' 102 103RETURN = r''' 104ansible_sysdescr: 105 description: A textual description of the entity. 106 returned: success 107 type: str 108 sample: Linux ubuntu-user 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC 2017 x86_64 109ansible_sysobjectid: 110 description: The vendor's authoritative identification of the network management subsystem contained in the entity. 111 returned: success 112 type: str 113 sample: 1.3.6.1.4.1.8072.3.2.10 114ansible_sysuptime: 115 description: The time (in hundredths of a second) since the network management portion of the system was last re-initialized. 116 returned: success 117 type: int 118 sample: 42388 119ansible_syscontact: 120 description: The textual identification of the contact person for this managed node, together with information on how to contact this person. 121 returned: success 122 type: str 123 sample: Me <me@example.org> 124ansible_sysname: 125 description: An administratively-assigned name for this managed node. 126 returned: success 127 type: str 128 sample: ubuntu-user 129ansible_syslocation: 130 description: The physical location of this node (e.g., `telephone closet, 3rd floor'). 131 returned: success 132 type: str 133 sample: Sitting on the Dock of the Bay 134ansible_all_ipv4_addresses: 135 description: List of all IPv4 addresses. 136 returned: success 137 type: list 138 sample: ["127.0.0.1", "172.17.0.1"] 139ansible_interfaces: 140 description: Dictionary of each network interface and its metadata. 141 returned: success 142 type: dict 143 sample: { 144 "1": { 145 "adminstatus": "up", 146 "description": "", 147 "ifindex": "1", 148 "ipv4": [ 149 { 150 "address": "127.0.0.1", 151 "netmask": "255.0.0.0" 152 } 153 ], 154 "mac": "", 155 "mtu": "65536", 156 "name": "lo", 157 "operstatus": "up", 158 "speed": "65536" 159 }, 160 "2": { 161 "adminstatus": "up", 162 "description": "", 163 "ifindex": "2", 164 "ipv4": [ 165 { 166 "address": "192.168.213.128", 167 "netmask": "255.255.255.0" 168 } 169 ], 170 "mac": "000a305a52a1", 171 "mtu": "1500", 172 "name": "Intel Corporation 82545EM Gigabit Ethernet Controller (Copper)", 173 "operstatus": "up", 174 "speed": "1500" 175 } 176 } 177''' 178 179import binascii 180import traceback 181from collections import defaultdict 182 183PYSNMP_IMP_ERR = None 184try: 185 from pysnmp.entity.rfc3413.oneliner import cmdgen 186 from pysnmp.proto.rfc1905 import EndOfMibView 187 HAS_PYSNMP = True 188except Exception: 189 PYSNMP_IMP_ERR = traceback.format_exc() 190 HAS_PYSNMP = False 191 192from ansible.module_utils.basic import AnsibleModule, missing_required_lib 193from ansible.module_utils.common.text.converters import to_text 194 195 196class DefineOid(object): 197 198 def __init__(self, dotprefix=False): 199 if dotprefix: 200 dp = "." 201 else: 202 dp = "" 203 204 # From SNMPv2-MIB 205 self.sysDescr = dp + "1.3.6.1.2.1.1.1.0" 206 self.sysObjectId = dp + "1.3.6.1.2.1.1.2.0" 207 self.sysUpTime = dp + "1.3.6.1.2.1.1.3.0" 208 self.sysContact = dp + "1.3.6.1.2.1.1.4.0" 209 self.sysName = dp + "1.3.6.1.2.1.1.5.0" 210 self.sysLocation = dp + "1.3.6.1.2.1.1.6.0" 211 212 # From IF-MIB 213 self.ifIndex = dp + "1.3.6.1.2.1.2.2.1.1" 214 self.ifDescr = dp + "1.3.6.1.2.1.2.2.1.2" 215 self.ifMtu = dp + "1.3.6.1.2.1.2.2.1.4" 216 self.ifSpeed = dp + "1.3.6.1.2.1.2.2.1.5" 217 self.ifPhysAddress = dp + "1.3.6.1.2.1.2.2.1.6" 218 self.ifAdminStatus = dp + "1.3.6.1.2.1.2.2.1.7" 219 self.ifOperStatus = dp + "1.3.6.1.2.1.2.2.1.8" 220 self.ifAlias = dp + "1.3.6.1.2.1.31.1.1.1.18" 221 222 # From IP-MIB 223 self.ipAdEntAddr = dp + "1.3.6.1.2.1.4.20.1.1" 224 self.ipAdEntIfIndex = dp + "1.3.6.1.2.1.4.20.1.2" 225 self.ipAdEntNetMask = dp + "1.3.6.1.2.1.4.20.1.3" 226 227 228def decode_hex(hexstring): 229 230 if len(hexstring) < 3: 231 return hexstring 232 if hexstring[:2] == "0x": 233 return to_text(binascii.unhexlify(hexstring[2:])) 234 return hexstring 235 236 237def decode_mac(hexstring): 238 239 if len(hexstring) != 14: 240 return hexstring 241 if hexstring[:2] == "0x": 242 return hexstring[2:] 243 return hexstring 244 245 246def lookup_adminstatus(int_adminstatus): 247 adminstatus_options = { 248 1: 'up', 249 2: 'down', 250 3: 'testing' 251 } 252 if int_adminstatus in adminstatus_options: 253 return adminstatus_options[int_adminstatus] 254 return "" 255 256 257def lookup_operstatus(int_operstatus): 258 operstatus_options = { 259 1: 'up', 260 2: 'down', 261 3: 'testing', 262 4: 'unknown', 263 5: 'dormant', 264 6: 'notPresent', 265 7: 'lowerLayerDown' 266 } 267 if int_operstatus in operstatus_options: 268 return operstatus_options[int_operstatus] 269 return "" 270 271 272def main(): 273 module = AnsibleModule( 274 argument_spec=dict( 275 host=dict(type='str', required=True), 276 version=dict(type='str', required=True, choices=['v2', 'v2c', 'v3']), 277 community=dict(type='str'), 278 username=dict(type='str'), 279 level=dict(type='str', choices=['authNoPriv', 'authPriv']), 280 integrity=dict(type='str', choices=['md5', 'sha']), 281 privacy=dict(type='str', choices=['aes', 'des']), 282 authkey=dict(type='str', no_log=True), 283 privkey=dict(type='str', no_log=True), 284 timeout=dict(type='int'), 285 retries=dict(type='int'), 286 ), 287 required_together=( 288 ['username', 'level', 'integrity', 'authkey'], 289 ['privacy', 'privkey'], 290 ), 291 supports_check_mode=True, 292 ) 293 294 m_args = module.params 295 296 if not HAS_PYSNMP: 297 module.fail_json(msg=missing_required_lib('pysnmp'), exception=PYSNMP_IMP_ERR) 298 299 cmdGen = cmdgen.CommandGenerator() 300 transport_opts = dict((k, m_args[k]) for k in ('timeout', 'retries') if m_args[k] is not None) 301 302 # Verify that we receive a community when using snmp v2 303 if m_args['version'] in ("v2", "v2c"): 304 if m_args['community'] is None: 305 module.fail_json(msg='Community not set when using snmp version 2') 306 307 if m_args['version'] == "v3": 308 if m_args['username'] is None: 309 module.fail_json(msg='Username not set when using snmp version 3') 310 311 if m_args['level'] == "authPriv" and m_args['privacy'] is None: 312 module.fail_json(msg='Privacy algorithm not set when using authPriv') 313 314 if m_args['integrity'] == "sha": 315 integrity_proto = cmdgen.usmHMACSHAAuthProtocol 316 elif m_args['integrity'] == "md5": 317 integrity_proto = cmdgen.usmHMACMD5AuthProtocol 318 319 if m_args['privacy'] == "aes": 320 privacy_proto = cmdgen.usmAesCfb128Protocol 321 elif m_args['privacy'] == "des": 322 privacy_proto = cmdgen.usmDESPrivProtocol 323 324 # Use SNMP Version 2 325 if m_args['version'] in ("v2", "v2c"): 326 snmp_auth = cmdgen.CommunityData(m_args['community']) 327 328 # Use SNMP Version 3 with authNoPriv 329 elif m_args['level'] == "authNoPriv": 330 snmp_auth = cmdgen.UsmUserData(m_args['username'], authKey=m_args['authkey'], authProtocol=integrity_proto) 331 332 # Use SNMP Version 3 with authPriv 333 else: 334 snmp_auth = cmdgen.UsmUserData(m_args['username'], authKey=m_args['authkey'], privKey=m_args['privkey'], authProtocol=integrity_proto, 335 privProtocol=privacy_proto) 336 337 # Use p to prefix OIDs with a dot for polling 338 p = DefineOid(dotprefix=True) 339 # Use v without a prefix to use with return values 340 v = DefineOid(dotprefix=False) 341 342 def Tree(): 343 return defaultdict(Tree) 344 345 results = Tree() 346 347 errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd( 348 snmp_auth, 349 cmdgen.UdpTransportTarget((m_args['host'], 161), **transport_opts), 350 cmdgen.MibVariable(p.sysDescr,), 351 cmdgen.MibVariable(p.sysObjectId,), 352 cmdgen.MibVariable(p.sysUpTime,), 353 cmdgen.MibVariable(p.sysContact,), 354 cmdgen.MibVariable(p.sysName,), 355 cmdgen.MibVariable(p.sysLocation,), 356 lookupMib=False 357 ) 358 359 if errorIndication: 360 module.fail_json(msg=str(errorIndication)) 361 362 for oid, val in varBinds: 363 current_oid = oid.prettyPrint() 364 current_val = val.prettyPrint() 365 if current_oid == v.sysDescr: 366 results['ansible_sysdescr'] = decode_hex(current_val) 367 elif current_oid == v.sysObjectId: 368 results['ansible_sysobjectid'] = current_val 369 elif current_oid == v.sysUpTime: 370 results['ansible_sysuptime'] = current_val 371 elif current_oid == v.sysContact: 372 results['ansible_syscontact'] = current_val 373 elif current_oid == v.sysName: 374 results['ansible_sysname'] = current_val 375 elif current_oid == v.sysLocation: 376 results['ansible_syslocation'] = current_val 377 378 errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd( 379 snmp_auth, 380 cmdgen.UdpTransportTarget((m_args['host'], 161), **transport_opts), 381 cmdgen.MibVariable(p.ifIndex,), 382 cmdgen.MibVariable(p.ifDescr,), 383 cmdgen.MibVariable(p.ifMtu,), 384 cmdgen.MibVariable(p.ifSpeed,), 385 cmdgen.MibVariable(p.ifPhysAddress,), 386 cmdgen.MibVariable(p.ifAdminStatus,), 387 cmdgen.MibVariable(p.ifOperStatus,), 388 cmdgen.MibVariable(p.ipAdEntAddr,), 389 cmdgen.MibVariable(p.ipAdEntIfIndex,), 390 cmdgen.MibVariable(p.ipAdEntNetMask,), 391 392 cmdgen.MibVariable(p.ifAlias,), 393 lookupMib=False 394 ) 395 396 if errorIndication: 397 module.fail_json(msg=str(errorIndication)) 398 399 interface_indexes = [] 400 401 all_ipv4_addresses = [] 402 ipv4_networks = Tree() 403 404 for varBinds in varTable: 405 for oid, val in varBinds: 406 if isinstance(val, EndOfMibView): 407 continue 408 current_oid = oid.prettyPrint() 409 current_val = val.prettyPrint() 410 if v.ifIndex in current_oid: 411 ifIndex = int(current_oid.rsplit('.', 1)[-1]) 412 results['ansible_interfaces'][ifIndex]['ifindex'] = current_val 413 interface_indexes.append(ifIndex) 414 if v.ifDescr in current_oid: 415 ifIndex = int(current_oid.rsplit('.', 1)[-1]) 416 results['ansible_interfaces'][ifIndex]['name'] = current_val 417 if v.ifMtu in current_oid: 418 ifIndex = int(current_oid.rsplit('.', 1)[-1]) 419 results['ansible_interfaces'][ifIndex]['mtu'] = current_val 420 if v.ifSpeed in current_oid: 421 ifIndex = int(current_oid.rsplit('.', 1)[-1]) 422 results['ansible_interfaces'][ifIndex]['speed'] = current_val 423 if v.ifPhysAddress in current_oid: 424 ifIndex = int(current_oid.rsplit('.', 1)[-1]) 425 results['ansible_interfaces'][ifIndex]['mac'] = decode_mac(current_val) 426 if v.ifAdminStatus in current_oid: 427 ifIndex = int(current_oid.rsplit('.', 1)[-1]) 428 results['ansible_interfaces'][ifIndex]['adminstatus'] = lookup_adminstatus(int(current_val)) 429 if v.ifOperStatus in current_oid: 430 ifIndex = int(current_oid.rsplit('.', 1)[-1]) 431 results['ansible_interfaces'][ifIndex]['operstatus'] = lookup_operstatus(int(current_val)) 432 if v.ipAdEntAddr in current_oid: 433 curIPList = current_oid.rsplit('.', 4)[-4:] 434 curIP = ".".join(curIPList) 435 ipv4_networks[curIP]['address'] = current_val 436 all_ipv4_addresses.append(current_val) 437 if v.ipAdEntIfIndex in current_oid: 438 curIPList = current_oid.rsplit('.', 4)[-4:] 439 curIP = ".".join(curIPList) 440 ipv4_networks[curIP]['interface'] = current_val 441 if v.ipAdEntNetMask in current_oid: 442 curIPList = current_oid.rsplit('.', 4)[-4:] 443 curIP = ".".join(curIPList) 444 ipv4_networks[curIP]['netmask'] = current_val 445 446 if v.ifAlias in current_oid: 447 ifIndex = int(current_oid.rsplit('.', 1)[-1]) 448 results['ansible_interfaces'][ifIndex]['description'] = current_val 449 450 interface_to_ipv4 = {} 451 for ipv4_network in ipv4_networks: 452 current_interface = ipv4_networks[ipv4_network]['interface'] 453 current_network = { 454 'address': ipv4_networks[ipv4_network]['address'], 455 'netmask': ipv4_networks[ipv4_network]['netmask'] 456 } 457 if current_interface not in interface_to_ipv4: 458 interface_to_ipv4[current_interface] = [] 459 interface_to_ipv4[current_interface].append(current_network) 460 else: 461 interface_to_ipv4[current_interface].append(current_network) 462 463 for interface in interface_to_ipv4: 464 results['ansible_interfaces'][int(interface)]['ipv4'] = interface_to_ipv4[interface] 465 466 results['ansible_all_ipv4_addresses'] = all_ipv4_addresses 467 468 module.exit_json(ansible_facts=results) 469 470 471if __name__ == '__main__': 472 main() 473