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