diff --git a/CHANGES.txt b/CHANGES.txt index 033227a..fdb8cc6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,23 @@ +0.2.0 +----- + +- Major overhaul aimed at adding value variation features to the Simulator + core: + * data files may now hold not only terminal OIDs but also OID subtrees + * pluggable value variation modules interfaces and basic modules added + * write support added through the use of appropriate variation modules + * SQL backend for keeping and modifying SNMP snapshots added in form of + a value variation module + * subprocess execution variation module added what could be used + for external process invocation on SNMP request to Simulator + * SNMP Notification Originator variation module added what could be + used for sending SNMP TRAP/INFORM messages to SNMP entities + on SNMP requests to Simulator +- SNMP snapshots now being called 'data files' rather than 'device files' + which is a legacy term. +- Data files and variation modules are now installed into Python package + directory to ease package distribution across platforms. + 0.1.6 ----- diff --git a/LICENSE.txt b/LICENSE.txt index 862b43d..033d5d7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2010-2012, Ilya Etingof , all rights reserved. +Copyright (c) 2010-2013, Ilya Etingof , all rights reserved. THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY SITUATION ENDANGERING HUMAN LIFE OR PROPERTY. diff --git a/MANIFEST.in b/MANIFEST.in index 78d714b..2dd7794 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include *.txt -recursive-include devices *.snmprec README.txt +recursive-include snmpsim/data *.snmprec *.txt +recursive-include snmpsim/variation *.py diff --git a/README.txt b/README.txt index 3c94e34..d8e7413 100644 --- a/README.txt +++ b/README.txt @@ -30,8 +30,14 @@ It is also possible with the SNMP Simulator software to vary responses based on Manager's transport address, not only SNMPv3 context or SNMPv1/v2c community names. -Current Simulator version supports trivial SNMP SET operations in volatile -mode meaning that all previously SET values are lost upon daemon restart. +Even more powerful is Simulator's ability to gateway SNMP queries to +its extension (also called "variation" modules). Once a variation module +is invoked by Simulator core, it is expected to return a well-formed +variable-binding sequence to be put into Simulator's response message. + +Simulator is shipped with a collecton of factory-build variation +modules. Users of the Simulator software are welcome to develop their +own variation modules if stock ones appears insufficient. The Simulator software is fully free and open source. It's written from ground-up in an easy to learn and high-level scripting language called @@ -46,11 +52,11 @@ Producing SNMP snapshots Primary method of recording an SNMP snapshot is to run snmprec tool against your donor device. This tool will execute a series of SNMP GETNEXT queries for a specified range of OIDs over a chosen SNMP protocol version and store -response data in a text file (AKA device file). +response data in a text file. -Device file format is optimized to be compact, human-readable and +Data file format is optimized to be compact, human-readable and inexpensive to parse. It's also important to store full and exact -response information in a most intact form. Here's an example device +response information in a most intact form. Here's an example data file content: 1.3.6.1.2.1.1.1.0|4|Linux 2.6.25.5-smp SMP Tue Jun 19 14:58:11 CDT 2007 i686 @@ -66,35 +72,22 @@ There is a pipe-separated triplet of OID-tag-value items where: is appended. * Value is either a printable string, a number or a hexifed value. -Valid tag values and their corresponding ASN.1/SNMP types are: - -* Integer32 - 2 -* OCTET STRING - 4 -* NULL - 5 -* OBJECT IDENTIFIER - 6 -* IpAddress - 64 -* Counter32 - 65 -* Gauge32 - 66 -* TimeTicks - 67 -* Opaque - 68 -* Counter64 - 70 - -No other information or comments is allowed in the device file. +No other information or comments is allowed in the data file. Device file recording would look like this: $ snmprec.py -h Usage: snmprec.py [--help] [--debug=] [--quiet] [--version=<1|2c|3>] [--community=] [--v3-user=] [--v3-auth-key=] [--v3-priv-key=] [--v3-auth-proto=] [--v3-priv-proto=<3DES|AES256|DES|AES|AES128|AES192>] [--context=] [--agent-udpv4-endpoint=] [--agent-udpv6-endpoint=<[X:X:..X]:NNNNN>] [--agent-unix-endpoint=] [--start-oid=] [--stop-oid=] [--output-file=] $ -$ snmprec.py --agent-udpv4-endpoint=127.0.0.1 --start-oid=1.3.6.1.2.1.2.1.0 --stop-oid=1.3.6.1.2.1.5 --output-file=devices/linux/1.3.6.1.2.1/127.0.0.1\@public.snmprec +$ snmprec.py --agent-udpv4-endpoint=127.0.0.1 --start-oid=1.3.6.1.2.1.2.1.0 --stop-oid=1.3.6.1.2.1.5 --output-file=snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1\@public.snmprec SNMP version 1 Community name: public OIDs dumped: 304, elapsed: 1.94 sec, rate: 157.00 OIDs/sec $ -$ ls -l devices/linux/1.3.6.1.2.1/127.0.0.1\@public.snmprec --rw-r--r-- 1 ilya users 16252 Oct 26 14:49 devices/linux/1.3.6.1.2.1/127.0.0.1@public.snmprec +$ ls -l snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1\@public.snmprec +-rw-r--r-- 1 ilya users 16252 Oct 26 14:49 snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1@public.snmprec $ -$ head devices/linux/1.3.6.1.2.1/127.0.0.1\@public.snmprec +$ head snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1\@public.snmprec 1.3.6.1.2.1.2.2.1.1.1|2|1 1.3.6.1.2.1.2.2.1.1.2|2|2 1.3.6.1.2.1.2.2.1.2.1|4|lo @@ -106,11 +99,11 @@ $ head devices/linux/1.3.6.1.2.1/127.0.0.1\@public.snmprec 1.3.6.1.2.1.2.2.1.5.1|66|10000000 1.3.6.1.2.1.2.2.1.5.2|66|100000000 -There are no special requirements for device file name and location. Note, -that Simulator treats device file path as an SNMPv1/v2c community string +There are no special requirements for data file name and location. Note, +that Simulator treats data file path as an SNMPv1/v2c community string and its MD5 hash constitutes SNMPv3 context name. -Another way to produce device files is to run the mib2dev.py tool against +Another way to produce data files is to run the mib2dev.py tool against virtually any MIB file. With that method you do not have to have a donor device and the values, that are normally returned by a donor device, will instead be chosen randomly. @@ -179,47 +172,47 @@ One of the useful options are the --string-pool and --integer32-ranges. They let you specify an alternative set of words and integer values ranges to be used in random values generation. -Finally, you could always modify your device files with a text editor. +Finally, you could always modify your data files with a text editor. Simulating SNMP Agents ---------------------- -Your collection of device files should look like this: +Your collection of data files should look like this: -$ find devices +$ find snmpsim/data devices -devices/linux -devices/linux/1.3.6.1.2.1 -devices/linux/1.3.6.1.2.1/127.0.0.1@public.snmprec -devices/linux/1.3.6.1.2.1/127.0.0.1@public.dbm -devices/3com -devices/3com/switch8800 -devices/3com/switch8800/1.3.6.1.4.1 -devices/3com/switch8800/1.3.6.1.4.1/172.17.1.22@public.snmprec -devices/3com/switch8800/1.3.6.1.4.1/172.17.1.22@public.dbm +snmpsim/data/linux +snmpsim/data/linux/1.3.6.1.2.1 +snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1@public.snmprec +snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1@public.dbm +snmpsim/data/3com +snmpsim/data/3com/switch8800 +snmpsim/data/3com/switch8800/1.3.6.1.4.1 +snmpsim/data/3com/switch8800/1.3.6.1.4.1/172.17.1.22@public.snmprec +snmpsim/data/3com/switch8800/1.3.6.1.4.1/172.17.1.22@public.dbm ... -Notice those .dbm files -- they are by-OID indices of device files used +Notice those .dbm files -- they are by-OID indices of data files used for fast lookup. These indices are created and updated automatically by Simulator. Getting help: $ snmpsimd.py -h -Usage: snmpsimd.py [--help] [--debug=] [--device-dir=] [--force-index-rebuild] [--validate-device-data] [--agent-udpv4-endpoint=] [--agent-udpv6-endpoint=<[X:X:..X]:NNNNN>] [--agent-unix-endpoint=] [--v2c-arch] [--v3-only] [--v3-user=] [--v3-auth-key=] [--v3-auth-proto=] [--v3-priv-key=] [--v3-priv-proto=<3DES|AES256|NONE|DES|AES|AES128|AES192>] +Usage: snmpsimd.py [--help] [--version ] [--debug=] [--data-dir=] [--force-index-rebuild] [--validate-data] [--variation-modules-dir=] [--variation-module-options=] [--agent-udpv4-endpoint=] [--agent-udpv6-endpoint=<[X:X:..X]:NNNNN>] [--agent-unix-endpoint=] [--v2c-arch] [--v3-only] [--v3-user=] [--v3-auth-key=] [--v3-auth-proto=] [--v3-priv-key=] [--v3-priv-proto=<3DES|AES256|NONE|DES|AES|AES128|AES192>] Running Simulator: $ snmpsimd.py --agent-udpv4-endpoint=127.0.0.1:1161 --agent-udpv6-endpoint='[::1]:1161' --agent-unix-endpoint=/tmp/snmpsimd.socket -Index ./devices/linux/1.3.6.1.2.1/127.0.0.1@public.dbm out of date -Indexing device file ./devices/linux/1.3.6.1.2.1/127.0.0.1@public.snmprec... +Index ./snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1@public.dbm out of date +Indexing data file ./snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1@public.snmprec... ...303 entries indexed -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -Device file ./devices/linux/1.3.6.1.2.1/127.0.0.1@public.snmprec, dbhash-indexed, closed +Device file ./snmpsim/data/linux/1.3.6.1.2.1/127.0.0.1@public.snmprec, dbhash-indexed, closed SNMPv1/2c community name: @linux/1.3.6.1.2.1/127.0.0.1@public SNMPv3 context name: 6d42b10f70ddb49c6be1d27f5ce2239e -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -Device file ./devices/3com/switch8800/1.3.6.1.4.1/172.17.1.22@public.dump, dbhash-indexed, closed +Device file ./snmpsim/data/3com/switch8800/1.3.6.1.4.1/172.17.1.22@public.dump, dbhash-indexed, closed SNMPv1/2c community name: @3com/switch8800/1.3.6.1.4.1/172.17.1.22@public SNMPv3 context name: 1a80634d11a76ee4e29b46bc8085d871 @@ -284,18 +277,18 @@ devices and their community/context names. Simulator maintains this information within its internal, dedicated SNMP context 'index': $ snmpwalk -On -v2c -c index localhost:1161 .1.3.6 -.1.3.6.1.4.1.20408.999.1.1.1 = STRING: "./devices/linux/1.3.6.1.2.1.1.1/127.0.0.1@public.snmprec" -.1.3.6.1.4.1.20408.999.1.2.1 = STRING: "devices/linux/1.3.6.1.2.1.1.1/127.0.0.1@public" +.1.3.6.1.4.1.20408.999.1.1.1 = STRING: "./snmpsim/data/linux/1.3.6.1.2.1.1.1/127.0.0.1@public.snmprec" +.1.3.6.1.4.1.20408.999.1.2.1 = STRING: "snmpsim/data/linux/1.3.6.1.2.1.1.1/127.0.0.1@public" .1.3.6.1.4.1.20408.999.1.3.1 = STRING: "9535d96c66759362b3521f4e273fc749" or $ snmpwalk -O n -l authPriv -u simulator -A auctoritas -X privatus -n index localhost:1161 .1.3.6 -.1.3.6.1.4.1.20408.999.1.1.1 = STRING: "./devices/linux/1.3.6.1.2.1.1.1/127.0.0.1@public.snmprec" -.1.3.6.1.4.1.20408.999.1.2.1 = STRING: "devices/linux/1.3.6.1.2.1.1.1/127.0.0.1@public" +.1.3.6.1.4.1.20408.999.1.1.1 = STRING: "./snmpsim/data/linux/1.3.6.1.2.1.1.1/127.0.0.1@public.snmprec" +.1.3.6.1.4.1.20408.999.1.2.1 = STRING: "snmpsim/data/linux/1.3.6.1.2.1.1.1/127.0.0.1@public" .1.3.6.1.4.1.20408.999.1.3.1 = STRING: "9535d96c66759362b3521f4e273fc749" -Where first column holds device file path, second - community string, and +Where first column holds data file path, second - community string, and third - SNMPv3 context name. Transport address based simulation @@ -307,7 +300,7 @@ useful to present the same simulated device to different SNMP Managers differently. When running in --v2c-arch mode, Simulator (version 0.1.4 and later) would -attempt to find device file to fullfill a request by probing files by paths +attempt to find data file to fullfill a request by probing files by paths constructed from pieces of request data. This path construction rules are as follows: @@ -339,12 +332,12 @@ For example, to make Simulator reporting from particular file to a Manager at 192.168.1.10 whenever community name "public" is used and queries are sent to Simulator over UDP/IPv4 to 192.168.1.1 interface (which is reported by Simulator under transport ID 1.3.6.1.6.1.1.0), -device file public/1.3.6.1.6.1.1.0/192.168.1.10.snmprec whould be used +data file public/1.3.6.1.6.1.1.0/192.168.1.10.snmprec whould be used for building responses. When Simulator is NOT running in --v2c-arch mode, e.g. SNMPv3 engine is used, similar rules apply to SNMPv3 context name rather than to SNMPv1/2c -community name. In that case device file path construction would work +community name. In that case data file path construction would work like this: / / .snmprec @@ -355,23 +348,23 @@ For example, to make Simulator reporting from particular file to a Manager at 192.168.1.10 whenever context-name is an empty string and queries are sent to Simulator over UDP/IPv4 to 192.168.1.1 interface (which is reported by Simulator under transport ID 1.3.6.1.6.1.1.0), -device file 1.3.6.1.6.1.1.0/192.168.1.10.snmprec whould be used +data file 1.3.6.1.6.1.1.0/192.168.1.10.snmprec whould be used for building responses. -Sharing device files --------------------- +Sharing data files +------------------ -If a symbolic link is used as a device file, it would serve as an +If a symbolic link is used as a data file, it would serve as an alternative CommunityName/ContextName for the Managed Objects collection -read from the snapshot file being pointed to. Shared device files are +read from the snapshot file being pointed to. Shared data files are mentioned explicitly on Simulator startup: $ snmpsimd.py --agent-udpv4-endpoint=127.0.0.1:1161 -Device file ./devices/public/1.3.6.1.6.1.1.0/127.0.0.1.snmprec, dbhash-indexed, closed +Device file ./snmpsim/data/public/1.3.6.1.6.1.1.0/127.0.0.1.snmprec, dbhash-indexed, closed SNMPv1/2c community name: public/1.3.6.1.6.1.1.0/127.0.0.1 SNMPv3 context name: 6d42b10f70ddb49c6be1d27f5ce2239e -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -Shared device file ./devices/public/1.3.6.1.6.1.1.0/127.0.0.1.snmprec, dbhash-indexed, closed +Shared data file ./snmpsim/data/public/1.3.6.1.6.1.1.0/127.0.0.1.snmprec, dbhash-indexed, closed SNMPv1/2c community name: public/1.3.6.1.6.1.1.0/192.168.1.1 SNMPv3 context name: 1a80634d11a76ee4e29b46bc8085d871 @@ -442,6 +435,111 @@ into text files having .sapwalk suffix and let Simulator find and index them. Once completed, Simulator will report access information for them just as it does for its native .snmprec files. +Using variation modules +----------------------- + +Without variation modules, simulated SNMP Agents are always static +in terms of data returned to SNMP Managers. They are also read-only. +By configuring particular OIDs or whole subtrees to be gatewayed to +variation modules allows you to make returned data changing in time. + +Another way of using variation modules is to gather data from some +external source such as an SQL database or executed process or distant +web-service. + +It's also possible to modify simulated values through SNMP SET operation +and store modified values in a database so they will persist over Simulator +restarts. + +Variation modules may be used for triggering events at other systems. For +instance stock "notification" module will send SNMP TRAP/IMFORM messages +to pre-configured SNMP Managers on SNMP SET request arrival to Simulator. + +Finally, variation module API let you develop your own code in Python +to fulfill your special needs and use your variation module with stock +Simulator. + +Here's the current list of variation modules supplied with Simulator: + +* counter - produces a non-decreasing sequence of integers over time +* gauge - produces a random number in specified range +* notification - sends SNMP TRAP/INFORM messages to disitant SNMP entity +* volatilecache - accepts and stores (in memory) SNMP var-binds through SNMP SET +* involatilecache - accepts and stores (in file) SNMP var-binds through + SNMP SET +* sql - reads/writes var-binds from/to a SQL database +* subprocess - executes external process and puts its stdout values into + response + +To make use of a variation module you will have to *edit* existing +or create a new data file adding variation module into the "tag" field. + +Consider .snmprec file format is a sequence of lines in the following +format: + +:: + +whereas TAG field complies to its own format: + +TAGID[:MODULE] + +For example, the following .snmprec file contents will invoke the +"volatilecache" module: + +1.3.6.1.2.1.1.1.0|4:volatilecache|I'm a string, please modify me +1.3.6.1.2.1.1.3.0|2:volatilecache|42 + +and cast its returned values into ASN.1 OCTET STRING and INTEGER respectively. + +Whenever a subtree is gatewayed to a variation module, TAGID part is left out +as there might be no single type for all values within a subtree. Thus the +empty TAGID sub-field serves as an indicator of a subtree: + +For example, the following data file will serve all OIDs under 1.3.6.1.2.1.1 +prefix to the "sql" variation module: + +1.3.6.1.2.1.1|:sql|system + +The value part is passed to variation module as-is. It is typically holds some +module-specific configuration or initialization values. + +For example, the following .snmprec line invokes the "notification" variation +module instructing it to send SNMP INFORM message to SNMP Manager at +127.0.01:162 over SNMPv3 with specific SNMP params: + +1.3.6.1.2.1.1.3.0|67:notification|3,usr-md5-des,md5,authkey1,des,privkey1,udp,127.0.0.1,162,inform,1.3.6.1.6.3.1.1.5.2, + +A small (but growing) collection of variation modules are shipped along with +Simulator and normally installed into the Python site-packages directory. +User can pass Simulator an alternative modules directory through its command +line. + +Simulator will load and bootstrap all variation modules it finds. Some +modules can accept initialization parameters (like database connection +credentials) through Simulator's --variation-module-options command-line +parameter. + +For example, the following Simulator invocation will configure its +"sql" variation module to use sqlite database (sqlite3 Python module) +and /var/tmp/system.db database file: + +$ snmpsimd.py --variation-module-options=sql:sqlite3:/var/tmp/system.db + +In case you are using multiple database connections or database types +all through the sql variation module, you could refer to each module instance +in .snmprec files through a so-called variation module alias. + +The following command-line runs Simulator with two instances of the +"involatilecache" variation module (dbA & dbB) each instance using +distinct database file for storing their persistent values: + +$ snmpsimd.py --variation-module-options=involatilecache=dbA:/var/tmp/fileA.db --variation-module-options=involatilecache=dbB:/var/tmp/fileB.db + +Whenever you consider coding your own variation module, take a look at the +existing ones. The API is very simple - it basically takes three Python +functions (init, process, shutdown) where process() is expected to return +a var-bind pair per each invocation. + Performance improvement ----------------------- @@ -453,7 +551,7 @@ operations. Use the --v2c-arch command line parameter to switch Simulator into SNMPv2c (v1/v2c) operation mode. -When Simulator runs over thousands of device files, startup may take time +When Simulator runs over thousands of data files, startup may take time (tens of seconds). Most of it goes into configuring SNMPv1/v2c credentials into SNMPv3 engine so startup time can be dramatically reduced by either using --v2c-arch mode (as mentioned above) or by turning off SNMPv1/v2c @@ -511,4 +609,4 @@ your device and send me its output file. Make sure that your device does not have any private information. --- -Written by Ilya Etingof , 2010-2012 +Written by Ilya Etingof , 2010-2013 diff --git a/scripts/mib2dev.py b/scripts/mib2dev.py new file mode 100644 index 0000000..1ac2a1d --- /dev/null +++ b/scripts/mib2dev.py @@ -0,0 +1,199 @@ +# +# SNMP Simulator MIB to data file converter +# +# Written by Ilya Etingof , 2011-2013 +# +import getopt +import sys +import random +from pyasn1.type import univ +from pyasn1.error import PyAsn1Error +from pysnmp.smi import builder, view, error +from pysnmp.proto import rfc1902 +from pysnmp import debug + +# Defaults +verboseFlag = True +startOID = stopOID = None +outputFile = sys.stderr +stringPool = 'Portez ce vieux whisky au juge blond qui fume!'.split() +int32Range = (-2147483648, 2147483648) +automaticValues = True +modNames = [] +mibDirs = [] + +helpMessage = 'Usage: %s [--help] [--debug=] [--quiet] [--pysnmp-mib-dir=] [--mib-module=] [--start-oid=] [--stop-oid=] [--manual-values] [--output-file=] [--string-pool=] [--integer32-range=]\r\n' % sys.argv[0] + +try: + opts, params = getopt.getopt(sys.argv[1:], 'h', + ['help', 'debug=', 'quiet', 'pysnmp-mib-dir=', 'mib-module=', 'start-oid=', 'stop-oid=', 'manual-values', 'output-file=', 'string-pool=', 'integer32-range='] + ) +except Exception: + sys.stdout.write('%s\r\n%s\r\n' % (sys.exc_info()[1], helpMessage)) + sys.exit(-1) + +if not opts: + sys.stdout.write(helpMessage) + sys.exit(-1) + +if params: + sys.stdout.write('extra arguments supplied %s\r\n' % params + helpMessage) + sys.exit(-1) + +for opt in opts: + if opt[0] == '-h' or opt[0] == '--help': + outputFile.write(helpMessage) + sys.exit(-1) + if opt[0] == '--debug': + debug.setLogger(debug.Debug(opt[1])) + if opt[0] == '--quiet': + verboseFlag = False + if opt[0] == '--pysnmp-mib-dir': + mibDirs.append(opt[1]) + if opt[0] == '--mib-module': + modNames.append(opt[1]) + if opt[0] == '--start-oid': + startOID = univ.ObjectIdentifier(opt[1]) + if opt[0] == '--stop-oid': + stopOID = univ.ObjectIdentifier(opt[1]) + if opt[0] == '--manual-values': + automaticValues = False + if opt[0] == '--output-file': + outputFile = open(opt[1], 'w') + if opt[0] == '--string-pool': + stringPool = opt[1].split() + if opt[0] == '--integer32-range': + int32Range = [int(x) for x in opt[1].split(',')] + +def getValue(syntax, hint=''): + # Pick a value + if isinstance(syntax, rfc1902.IpAddress): + val = '.'.join([ str(random.randrange(1, 256)) for x in range(4) ]) + elif isinstance(syntax, (rfc1902.Counter32, rfc1902.Gauge32, rfc1902.TimeTicks, rfc1902.Unsigned32)): + val = random.randrange(0, 0xffffffff) + elif isinstance(syntax, rfc1902.Integer32): + val = random.randrange(int32Range[0], int32Range[1]) + elif isinstance(syntax, rfc1902.Counter64): + val = random.randrange(0, 0xffffffffffffffff) + elif isinstance(syntax, univ.OctetString): + val = ' '.join( + [ stringPool[i] for i in range(random.randrange(0, len(stringPool)), random.randrange(0, len(stringPool))) ] + ) + elif isinstance(syntax, univ.ObjectIdentifier): + val = '.'.join(['1','3','6','1','3'] + ['%d' % random.randrange(0, 255) for x in range(random.randrange(0, 10))]) + elif isinstance(syntax, rfc1902.Bits): + val = [random.randrange(0, 256) for x in range(random.randrange(0,9))] + else: + val = '?' + + # Optionally approve chosen value with the user + makeGuess = automaticValues + while 1: + if makeGuess: + try: + return syntax.clone(val) + except PyAsn1Error: + sys.stdout.write( + '*** Inconsistent value: %s\r\n*** See constraints and suggest a better one for:\r\n' % (sys.exc_info()[1],) + ) + sys.stdout.write( + '%s# Value [\'%s\'] ? ' % (hint,(val is None and '' or val),) + ) + sys.stdout.flush() + line = sys.stdin.readline().strip() + if line: + val = line + makeGuess = True + +mibBuilder = builder.MibBuilder() + +mibBuilder.setMibSources( + *mibBuilder.getMibSources() + tuple( + [ builder.ZipMibSource(m).init() for m in mibDirs ] + ) +) + +# Load MIB tree foundation classes +( MibScalar, + MibTable, + MibTableRow, + MibTableColumn ) = mibBuilder.importSymbols( + 'SNMPv2-SMI', + 'MibScalar', + 'MibTable', + 'MibTableRow', + 'MibTableColumn' + ) + +mibView = view.MibViewController(mibBuilder) + +# MIBs walk +for modName in modNames: + oidCount = 0 + if verboseFlag: + sys.stdout.write('# MIB module: %s\r\n' % modName) + mibBuilder.loadModules(modName) + modOID = oid = univ.ObjectIdentifier( + mibView.getFirstNodeName(modName)[0] + ) + while modOID.isPrefixOf(oid): + try: + oid, label, _ = mibView.getNextNodeName(oid) + except error.NoSuchObjectError: + break + + modName, symName, _ = mibView.getNodeLocation(oid) + node, = mibBuilder.importSymbols(modName, symName) + + if isinstance(node, MibTable): + hint = '# Table %s::%s\r\n' % (modName, symName) + continue + elif isinstance(node, MibTableRow): + suffix = () + hint += '# Row %s::%s\r\n' % (modName, symName) + for impliedFlag, idxModName, idxSymName in node.getIndexNames(): + idxNode, = mibBuilder.importSymbols(idxModName, idxSymName) + hint += '# Index %s::%s (type %s)\r\n' % (idxModName, idxSymName, idxNode.syntax.__class__.__name__) + suffix = suffix + node.getAsName( + getValue(idxNode.syntax, verboseFlag and hint or ''), impliedFlag + ) + continue + elif isinstance(node, MibTableColumn): + oid = node.name + val = getValue(node.syntax, verboseFlag and hint + '# Column %s::%s (type %s)\r\n' % (modName, symName, node.syntax.__class__.__name__) or '') + elif isinstance(node, MibScalar): + oid = node.name + suffix = (0,) + val = getValue(node.syntax, verboseFlag and '# Scalar %s::%s (type %s)\r\n' % (modName, symName, node.syntax.__class__.__name__) or '') + else: + continue + + if startOID and oid < startOID: + continue # skip on premature OID + if stopOID and oid > stopOID: + break # stop on out of range condition + + tag = '%s' % sum([ x for x in val.tagSet[0] ]) + + # Encode non-printable content + + if isinstance(val, univ.OctetString): + nval = val.asNumbers() + if nval and nval[-1] == 32 or [ x for x in nval if x < 32 or x > 126 ]: + tag += 'x' + val = ''.join([ '%.2x' % x for x in nval ]) + elif isinstance(val, univ.ObjectIdentifier): + val = val.prettyPrint() + + oidCount += 1 + + outputFile.write( + '%s.%s|%s|%s\r\n' % ('.'.join(['%d' % x for x in oid]), + '.'.join(['%d' % x for x in suffix]), + tag, val) + ) + + if verboseFlag: + sys.stdout.write( + '# End of %s, %s OID(s) dumped\r\n' % (modName, oidCount) + ) diff --git a/scripts/snmprec.py b/scripts/snmprec.py new file mode 100644 index 0000000..e95cfce --- /dev/null +++ b/scripts/snmprec.py @@ -0,0 +1,332 @@ +# +# SNMP Snapshot Data Recorder +# +# Written by Ilya Etingof , 2010-2013 +# + +import getopt +import time +import sys +from pyasn1.type import univ +from pysnmp.proto import rfc1902 +from pysnmp.entity import engine, config +from pysnmp.carrier.asynsock.dgram import udp +try: + from pysnmp.carrier.asynsock.dgram import udp6 +except ImportError: + udp6 = None +try: + from pysnmp.carrier.asynsock.dgram import unix +except ImportError: + unix = None +from pysnmp.entity.rfc3413 import cmdgen +from pysnmp import debug + +# Defaults +quietFlag = False +snmpVersion = 1 +snmpCommunity = 'public' +v3User = None +v3AuthKey = None +v3PrivKey = None +v3AuthProto = 'NONE' +v3PrivProto = 'NONE' +v3Context = '' +agentUDPv4Address = (None, 161) # obsolete +agentUDPv4Endpoint = None +agentUDPv6Endpoint = None +agentUNIXEndpoint = None +startOID = univ.ObjectIdentifier('1.3.6') +stopOID = None +outputFile = sys.stderr + +authProtocols = { + 'MD5': config.usmHMACMD5AuthProtocol, + 'SHA': config.usmHMACSHAAuthProtocol, + 'NONE': config.usmNoAuthProtocol +} + +privProtocols = { + 'DES': config.usmDESPrivProtocol, + '3DES': config.usm3DESEDEPrivProtocol, + 'AES': config.usmAesCfb128Protocol, + 'AES128': config.usmAesCfb128Protocol, + 'AES192': config.usmAesCfb192Protocol, + 'AES256': config.usmAesCfb256Protocol, + 'NONE': config.usmNoPrivProtocol +} + +helpMessage = 'Usage: %s [--help] [--debug=] [--quiet] [--version=<1|2c|3>] [--community=] [--v3-user=] [--v3-auth-key=] [--v3-priv-key=] [--v3-auth-proto=<%s>] [--v3-priv-proto=<%s>] [--context=] [--agent-udpv4-endpoint=] [--agent-udpv6-endpoint=<[X:X:..X]:NNNNN>] [--agent-unix-endpoint=] [--start-oid=] [--stop-oid=] [--output-file=]\r\n' % (sys.argv[0], '|'.join([ x for x in authProtocols if x != 'NONE' ]), '|'.join([ x for x in privProtocols if x != 'NONE' ])) + +try: + opts, params = getopt.getopt(sys.argv[1:], 'h', + ['help', 'debug=', 'quiet', 'v1', 'v2c', 'v3', 'version=', 'community=', 'v3-user=', 'v3-auth-key=', 'v3-priv-key=', 'v3-auth-proto=', 'v3-priv-proto=', 'context=', 'agent-address=', 'agent-port=', 'agent-udpv4-endpoint=', 'agent-udpv6-endpoint=', 'agent-unix-endpoint=', 'start-oid=', 'stop-oid=', 'output-file='] + ) +except Exception: + sys.stdout.write('%s\r\n%s\r\n' % (sys.exc_info()[1], helpMessage)) + sys.exit(-1) + +if params: + sys.stdout.write('extra arguments supplied %s\r\n' % params + helpMessage) + sys.exit(-1) + +for opt in opts: + if opt[0] == '-h' or opt[0] == '--help': + sys.stdout.write(helpMessage) + sys.exit(-1) + elif opt[0] == '--debug': + debug.setLogger(debug.Debug(opt[1])) + elif opt[0] == '--quiet': + quietFlag = True + elif opt[0] == '--v1': + snmpVersion = 0 + elif opt[0] == '--v2c': + snmpVersion = 1 + elif opt[0] == '--v3': + snmpVersion = 3 + elif opt[0] == '--version': + if opt[1] in ('1', 'v1'): + snmpVersion = 0 + elif opt[1] in ('2', '2c', 'v2c'): + snmpVersion = 1 + elif opt[1] in ('3', 'v3'): + snmpVersion = 3 + else: + sys.stdout.write('unknown SNMP version %s\r\n' % opt[1]) + sys.exit(-1) + elif opt[0] == '--community': + snmpCommunity = opt[1] + elif opt[0] == '--v3-user': + v3User = opt[1] + elif opt[0] == '--v3-auth-key': + v3AuthKey = opt[1] + elif opt[0] == '--v3-auth-proto': + v3AuthProto = opt[1].upper() + if v3AuthProto not in authProtocols: + sys.stdout.write('bad v3 auth protocol %s\r\n' % v3AuthProto) + sys.exit(-1) + elif opt[0] == '--v3-priv-key': + v3PrivKey = opt[1] + elif opt[0] == '--v3-priv-proto': + v3PrivProto = opt[1].upper() + if v3PrivProto not in privProtocols: + sys.stdout.write('bad v3 privacy protocol %s\r\n' % v3PrivProto) + sys.exit(-1) + elif opt[0] == '--context': + v3Context = opt[1] + elif opt[0] == '--agent-address': + agentUDPv4Address = (opt[1], agentUDPv4Address[1]) + elif opt[0] == '--agent-port': + agentUDPv4Address = (agentUDPv4Address[0], int(opt[1])) + elif opt[0] == '--agent-udpv4-endpoint': + f = lambda h,p=161: (h, int(p)) + try: + agentUDPv4Endpoint = f(*opt[1].split(':')) + except: + sys.stdout.write('improper IPv4/UDP endpoint %s\r\n' % opt[1]) + sys.exit(-1) + elif opt[0] == '--agent-udpv6-endpoint': + if not udp6: + sys.stdout.write('This system does not support UDP/IP6\r\n') + sys.exit(-1) + if opt[1].find(']:') != -1 and opt[1][0] == '[': + h, p = opt[1].split(']:') + try: + agentUDPv6Endpoint = h[1:], int(p) + except: + sys.stdout.write('improper IPv6/UDP endpoint %s\r\n' % opt[1]) + sys.exit(-1) + elif opt[1][0] == '[' and opt[1][-1] == ']': + agentUDPv6Endpoint = opt[1][1:-1], 161 + else: + agentUDPv6Endpoint = opt[1], 161 + elif opt[0] == '--agent-unix-endpoint': + if not unix: + sys.stdout.write('This system does not support UNIX domain sockets\r\n') + sys.exit(-1) + agentUNIXEndpoint = opt[1] + elif opt[0] == '--start-oid': + startOID = univ.ObjectIdentifier(opt[1]) + elif opt[0] == '--stop-oid': + stopOID = univ.ObjectIdentifier(opt[1]) + elif opt[0] == '--output-file': + outputFile = open(opt[1], 'w') + +# Catch missing params + +if not agentUDPv4Endpoint and not agentUDPv6Endpoint and not agentUNIXEndpoint: + if agentUDPv4Address[0] is None: + sys.stdout.write('ERROR: agent address endpoint not given\r\n%s' % helpMessage) + sys.exit(-1) + else: + agentUDPv4Endpoint = agentUDPv4Address + +if snmpVersion == 3: + if v3User is None: + sys.stdout.write('--v3-user is missing\r\n%s' % helpMessage) + sys.exit(-1) + if v3PrivKey and not v3AuthKey: + sys.stdout.write('--v3-auth-key is missing\r\n%s' % helpMessage) + sys.exit(-1) + if authProtocols[v3AuthProto] == config.usmNoAuthProtocol: + if v3AuthKey is not None: + v3AuthProto = 'MD5' + else: + if v3AuthKey is None: + sys.stdout.write('--v3-auth-key is missing\r\n%s' % helpMessage) + sys.exit(-1) + if privProtocols[v3PrivProto] == config.usmNoPrivProtocol: + if v3PrivKey is not None: + v3PrivProto = 'DES' + else: + if v3PrivKey is None: + sys.stdout.write('--v3-priv-key is missing\r\n%s' % helpMessage) + sys.exit(-1) + +# SNMP configuration + +snmpEngine = engine.SnmpEngine() + +if snmpVersion == 3: + if v3PrivKey is None and v3AuthKey is None: + secLevel = 'noAuthNoPriv' + elif v3PrivKey is None: + secLevel = 'authNoPriv' + else: + secLevel = 'authPriv' + config.addV3User( + snmpEngine, v3User, + authProtocols[v3AuthProto], v3AuthKey, + privProtocols[v3PrivProto], v3PrivKey + ) + if not quietFlag: + sys.stdout.write('SNMP version 3\r\nContext name: %s\r\nUser: %s\r\nSecurity level: %s\r\nAuthentication key/protocol: %s/%s\r\nEncryption (privacy) key/protocol: %s/%s\r\n' % (v3Context == '' and '\'\'' or v3Context, v3User, secLevel, v3AuthKey is None and '' or v3AuthKey, v3AuthProto, v3PrivKey is None and '' or v3PrivKey, v3PrivProto)) +else: + v3User = 'agt' + secLevel = 'noAuthNoPriv' + config.addV1System( + snmpEngine, v3User, snmpCommunity + ) + if not quietFlag: + sys.stdout.write('SNMP version %s\r\nCommunity name: %s\r\n' % (snmpVersion == 0 and '1' or '2c', snmpCommunity)) + +config.addTargetParams(snmpEngine, 'pms', v3User, secLevel, snmpVersion) + +if agentUDPv6Endpoint: + config.addSocketTransport( + snmpEngine, + udp6.domainName, + udp6.Udp6SocketTransport().openClientMode() + ) + config.addTargetAddr( + snmpEngine, 'tgt', udp6.domainName, agentUDPv6Endpoint, 'pms' + ) + if not quietFlag: + sys.stdout.write('Querying UDP/IPv6 agent at [%s]:%s\r\n' % agentUDPv6Endpoint) +elif agentUNIXEndpoint: + config.addSocketTransport( + snmpEngine, + unix.domainName, + unix.UnixSocketTransport().openClientMode() + ) + config.addTargetAddr( + snmpEngine, 'tgt', unix.domainName, agentUNIXEndpoint, 'pms' + ) + if not quietFlag: + sys.stdout.write('Querying UNIX named pipe agent at %s\r\n' % agentUNIXEndpoint) +elif agentUDPv4Endpoint: + config.addSocketTransport( + snmpEngine, + udp.domainName, + udp.UdpSocketTransport().openClientMode() + ) + config.addTargetAddr( + snmpEngine, 'tgt', udp.domainName, agentUDPv4Endpoint, 'pms' + ) + if not quietFlag: + sys.stdout.write('Querying UDP/IPv4 agent at %s:%s\r\n' % agentUDPv4Endpoint) + +# Data file writer + +def dataFileOutput(oid, val): + outputFile.write('%s|%s' % ( + oid.prettyPrint(), sum([ x for x in val.tagSet[0] ]) + ) ) + if val.tagSet in (univ.OctetString.tagSet, + rfc1902.Opaque.tagSet, + rfc1902.IpAddress.tagSet): + nval = val.asNumbers() + if nval and nval[-1] == 32 or [ x for x in nval if x < 32 or x > 126 ]: + outputFile.write('x') + val = ''.join([ '%.2x' % x for x in nval ]) + else: + val = val.prettyPrint() + + outputFile.write('|%s\n' % val) + +# SNMP worker + +def cbFun(sendRequestHandle, errorIndication, errorStatus, errorIndex, + varBindTable, cbCtx): + if errorIndication and errorIndication != 'oidNotIncreasing': + sys.stdout.write('%s\r\n' % errorIndication) + return + # SNMPv1 response may contain noSuchName error *and* SNMPv2c exception, + # so we ignore noSuchName error here + if errorStatus and errorStatus != 2: + sys.stdout.write('%s\r\n' % errorStatus.prettyPrint()) + return + for varBindRow in varBindTable: + for oid, val in varBindRow: + if val is None: + continue + dataFileOutput(oid, val) + cbCtx['count'] = cbCtx['count'] + 1 + if not quietFlag: + sys.stdout.write('OIDs dumped: %s\r' % cbCtx['count']), + sys.stdout.flush() + for oid, val in varBindTable[-1]: + if stopOID and oid >= stopOID: + return # stop on out of range condition + if val is not None: + break + else: + return # stop on end-of-table + return 1 # continue walking + +cmdGen = cmdgen.NextCommandGenerator() + +cbCtx = { + 'count': 0 + } + +cmdGen.sendReq( + snmpEngine, 'tgt', ((startOID, None),), cbFun, cbCtx, contextName=v3Context + ) + +t = time.time() + +# Python 2.4 does not support the "finally" clause + +exc_info = None + +try: + snmpEngine.transportDispatcher.runDispatcher() +except KeyboardInterrupt: + if not quietFlag: + sys.stdout.write('Process terminated\r\n') +except Exception: + exc_info = sys.exc_info() + +snmpEngine.transportDispatcher.closeDispatcher() + +t = time.time() - t + +if not quietFlag: + sys.stdout.write( + 'OIDs dumped: %s, elapsed: %.2f sec, rate: %.2f OIDs/sec\r\n' % \ + (cbCtx['count'], t, t and cbCtx['count']//t or 0) + ) + +if exc_info: + raise exc_info[1] diff --git a/scripts/snmpsimd.py b/scripts/snmpsimd.py new file mode 100644 index 0000000..7cf3cfd --- /dev/null +++ b/scripts/snmpsimd.py @@ -0,0 +1,1168 @@ +# +# SNMP Agent Simulator +# +# Written by Ilya Etingof , 2010-2013 +# +import os +import stat +import sys +import getopt +if sys.version_info[0] < 3 and sys.version_info[1] < 5: + from md5 import md5 +else: + from hashlib import md5 +import time +if sys.version_info[0] < 3: + import anydbm as dbm + from whichdb import whichdb +else: + import dbm + whichdb = dbm.whichdb +import bisect +from pyasn1.type import univ +from pyasn1.codec.ber import encoder, decoder +from pyasn1.compat.octets import octs2str, str2octs, int2oct +from pyasn1.error import PyAsn1Error +from pysnmp.entity import engine, config +from pysnmp.entity.rfc3413 import cmdrsp, context +from pysnmp.carrier.asynsock.dgram import udp +try: + from pysnmp.carrier.asynsock.dgram import udp6 +except ImportError: + udp6 = None +try: + from pysnmp.carrier.asynsock.dgram import unix +except ImportError: + unix = None +from pysnmp.carrier.asynsock.dispatch import AsynsockDispatcher +from pysnmp.smi import exval, indices +from pysnmp.proto import rfc1902, rfc1905, api +from pysnmp import error +from pysnmp import debug +import snmpsim + +# Process command-line options + +# Defaults +forceIndexBuild = False +validateData = False +v2cArch = False +v3Only = False +v3User = 'simulator' +v3AuthKey = 'auctoritas' +v3AuthProto = 'MD5' +v3PrivKey = 'privatus' +v3PrivProto = 'DES' +agentUDPv4Address = ('127.0.0.1', 161) +agentUDPv4Endpoints = [] +agentUDPv6Endpoints = [] +agentUNIXEndpoints = [] +dataDirs = set() +variationModulesDirs = [] +variationModulesOptions = {} +variationModules = {} + +authProtocols = { + 'MD5': config.usmHMACMD5AuthProtocol, + 'SHA': config.usmHMACSHAAuthProtocol, + 'NONE': config.usmNoAuthProtocol +} + +privProtocols = { + 'DES': config.usmDESPrivProtocol, + '3DES': config.usm3DESEDEPrivProtocol, + 'AES': config.usmAesCfb128Protocol, + 'AES128': config.usmAesCfb128Protocol, + 'AES192': config.usmAesCfb192Protocol, + 'AES256': config.usmAesCfb256Protocol, + 'NONE': config.usmNoPrivProtocol +} + +helpMessage = 'Usage: %s [--help] [--version ] [--debug=] [--data-dir=] [--force-index-rebuild] [--validate-data] [--variation-modules-dir=] [--variation-module-options=] [--agent-udpv4-endpoint=] [--agent-udpv6-endpoint=<[X:X:..X]:NNNNN>] [--agent-unix-endpoint=] [--v2c-arch] [--v3-only] [--v3-user=] [--v3-auth-key=] [--v3-auth-proto=<%s>] [--v3-priv-key=] [--v3-priv-proto=<%s>]' % (sys.argv[0], '|'.join(authProtocols), '|'.join(privProtocols)) + +try: + opts, params = getopt.getopt(sys.argv[1:], 'h', + ['help', 'debug=', 'device-dir=', 'data-dir=', 'force-index-rebuild', 'validate-device-data', 'validate-data', 'variation-modules-dir=', 'variation-module-options=', 'agent-address=', 'agent-port=', 'agent-udpv4-endpoint=', 'agent-udpv6-endpoint=', 'agent-unix-endpoint=', 'v2c-arch', 'v3-only', 'v3-user=', 'v3-auth-key=', 'v3-auth-proto=', 'v3-priv-key=', 'v3-priv-proto='] + ) +except Exception: + sys.stdout.write('%s\r\n%s\r\n' % (sys.exc_info()[1], helpMessage)) + sys.exit(-1) + +if params: + sys.stdout.write('extra arguments supplied %s%s\r\n' % (params, helpMessage)) + sys.exit(-1) + +for opt in opts: + if opt[0] == '-h' or opt[0] == '--help' or \ + opt[0] == '-v' or opt[0] == '--version': + sys.stdout.write('SNMP Simulator version %s, written by Ilya Etingof \r\nSoftware documentation and support at http://snmpsim.sf.net\r\n%s\r\n' % (snmpsim.__version__, helpMessage)) + sys.exit(-1) + elif opt[0] == '--debug': + debug.setLogger(debug.Debug(opt[1])) + elif opt[0] in ('--device-dir', '--data-dir'): + dataDirs.add(opt[1]) + elif opt[0] == '--force-index-rebuild': + forceIndexBuild = True + elif opt[0] in ('--validate-device-data', '--validate-data'): + validateData = True + elif opt[0] == '--variation-modules-dir': + variationModulesDirs.append(opt[1]) + elif opt[0] == '--variation-module-options': + args = opt[1].split(':') + modName, args = args[0], args[1:] + if '=' in modName: + modName, alias = modName.split('=', 1) + else: + alias = os.path.splitext(os.path.basename(modName))[0] + if modName not in variationModulesOptions: + variationModulesOptions[modName] = [] + variationModulesOptions[modName].append((alias, args)) + elif opt[0] == '--agent-udpv4-endpoint': + f = lambda h,p=161: (h, int(p)) + try: + agentUDPv4Endpoints.append(f(*opt[1].split(':'))) + except: + sys.stdout.write('improper IPv4/UDP endpoint %s\r\n' % opt[1]) + sys.exit(-1) + elif opt[0] == '--agent-udpv6-endpoint': + if not udp6: + sys.stdout.write('This system does not support UDP/IP6\r\n') + sys.exit(-1) + if opt[1].find(']:') != -1 and opt[1][0] == '[': + h, p = opt[1].split(']:') + try: + h, p = h[1:], int(p) + except: + sys.stdout.write('improper IPv6/UDP endpoint %s\r\n' % opt[1]) + sys.exit(-1) + elif opt[1][0] == '[' and opt[1][-1] == ']': + h, p = opt[1][1:-1], 161 + else: + h, p = opt[1], 161 + agentUDPv6Endpoints.append((h, p)) + elif opt[0] == '--agent-unix-endpoint': + if not unix: + sys.stdout.write('This system does not support UNIX domain sockets\r\n') + sys.exit(-1) + agentUNIXEndpoints.append(opt[1]) + elif opt[0] == '--agent-address': + agentUDPv4Address = (opt[1], agentUDPv4Address[1]) + elif opt[0] == '--agent-port': + agentUDPv4Address = (agentUDPv4Address[0], int(opt[1])) + elif opt[0] == '--v2c-arch': + v2cArch = True + elif opt[0] == '--v3-only': + v3Only = True + elif opt[0] == '--v3-user': + v3User = opt[1] + elif opt[0] == '--v3-auth-key': + v3AuthKey = opt[1] + elif opt[0] == '--v3-auth-proto': + v3AuthProto = opt[1].upper() + if v3AuthProto not in authProtocols: + sys.stdout.write('bad v3 auth protocol %s\r\n' % v3AuthProto) + sys.exit(-1) + elif opt[0] == '--v3-priv-key': + v3PrivKey = opt[1] + elif opt[0] == '--v3-priv-proto': + v3PrivProto = opt[1].upper() + if v3PrivProto not in privProtocols: + sys.stdout.write('bad v3 privacy protocol %s\r\n' % v3PrivProto) + sys.exit(-1) + +if authProtocols[v3AuthProto] == config.usmNoAuthProtocol and \ + privProtocols[v3PrivProto] != config.usmNoPrivProtocol: + sys.stdout.write('privacy impossible without authentication\r\n') + sys.exit(-1) + +if not dataDirs: + dataDirs.add(snmpsim.root + os.path.sep + 'data') + +if not variationModulesDirs: + variationModulesDirs.append(snmpsim.root + os.path.sep + 'variation') + +for variationModulesDir in variationModulesDirs: + for dFile in os.listdir(variationModulesDir): + if dFile[-3:] != '.py': + continue + _toLoad = [] + modName = os.path.splitext(os.path.basename(dFile))[0] + if modName in variationModulesOptions: + while variationModulesOptions[modName]: + alias, args = variationModulesOptions[modName].pop() + _toLoad.append((alias, args)) + del variationModulesOptions[modName] + else: + _toLoad.append((modName, ())) + + mod = variationModulesDir + os.path.sep + dFile + + for alias, args in _toLoad: + if alias in variationModules: + sys.stdout.write('variation module %s already registered\r\n' % alias) + sys.exit(-1) + + ctx = { 'path': mod, + 'alias': alias, + 'args': args } + try: + execfile(mod, ctx) + except Exception: + sys.stdout.write('variation module %s execution failure: %s\r\n' % (opt[1], sys.exc_info()[1])) + sys.exit(-1) + else: + variationModules[alias] = ctx + +if variationModulesOptions: + sys.stdout.write('ERROR: unused options for variation modules: %s\r\n' % ', '.join(variationModulesOptions.keys())) + sys.exit(-1) + +# for backward compatibility +if not agentUDPv4Endpoints and \ + not agentUDPv6Endpoints and \ + not agentUNIXEndpoints: + agentUDPv4Endpoints.append(agentUDPv4Address) + +# Data file entry parsers + +class DumpParser: + ext = os.path.extsep + 'dump' + tagMap = { + '0': rfc1902.Counter32, + '1': rfc1902.Gauge32, + '2': rfc1902.Integer32, + '3': rfc1902.IpAddress, + '4': univ.Null, + '5': univ.ObjectIdentifier, + '6': rfc1902.OctetString, + '7': rfc1902.TimeTicks, + '8': rfc1902.Counter32, # an alias + '9': rfc1902.Counter64, + } + + def __nullFilter(value): + return '' # simply drop whatever value is there when it's a Null + + def __unhexFilter(value): + if value[:5].lower() == 'hex: ': + value = [ int(x, 16) for x in value[5:].split('.') ] + elif value[0] == '"' and value[-1] == '"': + value = value[1:-1] + return value + + filterMap = { + '4': __nullFilter, + '6': __unhexFilter + } + + def parse(self, line): return octs2str(line).split('|', 2) + + def evaluateOid(self, oid): + return univ.ObjectIdentifier(oid) + + def evaluateValue(self, oid, tag, value, **context): + return oid, tag, self.tagMap[tag]( + self.filterMap.get(tag, lambda x: x)(value.strip()) + ) + + def evaluate(self, line, **context): + oid, tag, value = self.parse(line) + oid = self.evaluateOid(oid) + if context.get('oidOnly'): + value = None + else: + try: + oid, value = self.evaluateValue(oid, tag, value, **context) + except Exception: + sys.stdout.write('ERROR: value evaluation for %s = %r failed: %s\r\n' % (oid, value, sys.exc_info()[1])) + value = context['errorStatus'] + return oid, value + +class MvcParser(DumpParser): + ext = os.path.extsep + 'MVC' # just an alias + +class SapParser(DumpParser): + ext = os.path.extsep + 'sapwalk' # just an alias + tagMap = { + 'Counter': rfc1902.Counter32, + 'Gauge': rfc1902.Gauge32, + 'Integer': rfc1902.Integer32, + 'IpAddress': rfc1902.IpAddress, +# '': univ.Null, + 'ObjectID': univ.ObjectIdentifier, + 'OctetString': rfc1902.OctetString, + 'TimeTicks': rfc1902.TimeTicks, + 'Counter64': rfc1902.Counter64 + } + + def __stringFilter(value): + if value[:2] == '0x': + value = [ int(value[x:x+2], 16) for x in range(2, len(value[2:])+2, 2) ] + return value + + filterMap = { + 'OctetString': __stringFilter + } + + def parse(self, line): + return [ x.strip() for x in octs2str(line).split(',', 2) ] + +class WalkParser(DumpParser): + ext = os.path.extsep + 'snmpwalk' # just an alias + # case-insensitive keys as snmpwalk output tend to vary + tagMap = { + 'OID:': rfc1902.ObjectName, + 'INTEGER:': rfc1902.Integer, + 'STRING:': rfc1902.OctetString, + 'BITS:': rfc1902.Bits, + 'HEX-STRING:': rfc1902.OctetString, + 'GAUGE32:': rfc1902.Gauge32, + 'COUNTER32:': rfc1902.Counter32, + 'COUNTER64:': rfc1902.Counter64, + 'IPADDRESS:': rfc1902.IpAddress, + 'OPAQUE:': rfc1902.Opaque, + 'UNSIGNED32:': rfc1902.Unsigned32, # this is not needed + 'TIMETICKS:': rfc1902.TimeTicks # this is made up + } + + # possible DISPLAY-HINTs parsing should occur here + def __stringFilter(value): + if not value: + return value + elif value[0] == value[-1] == '"': + return value[1:-1] + elif value.find(':') > 0: + for x in value.split(':'): + for y in x: + if y not in '0123456789ABCDEFabcdef': + return value + return [ int(x, 16) for x in value.split(':') ] + else: + return value + + def __opaqueFilter(value): + return [int(y, 16) for y in value.split(' ')] + + def __bitsFilter(value): + return ''.join([int2oct(int(y, 16)) for y in value.split(' ')]) + + def __hexStringFilter(value): + return [int(y, 16) for y in value.split(' ')] + + filterMap = { + 'OPAQUE:': __opaqueFilter, + 'STRING:': __stringFilter, + 'BITS:': __bitsFilter, + 'HEX-STRING:': __hexStringFilter + } + + def parse(self, line): + oid, value = octs2str(line).strip().split(' = ', 1) + if oid and oid[0] == '.': + oid = oid[1:] + try: + tag, value = value.split(' ', 1) + except ValueError: + # this is implicit snmpwalk's fuzziness + if value == '""' or value == 'STRING:': + tag = 'STRING:' + value = '' + else: + tag = 'TimeTicks:' + return oid, tag.upper(), value + +class SnmprecParser: + ext = os.path.extsep + 'snmprec' + tagMap = {} + for t in ( rfc1902.Gauge32, + rfc1902.Integer32, + rfc1902.IpAddress, + univ.Null, + univ.ObjectIdentifier, + rfc1902.OctetString, + rfc1902.TimeTicks, + rfc1902.Opaque, + rfc1902.Counter32, + rfc1902.Counter64 ): + tagMap[str(sum([ x for x in t.tagSet[0] ]))] = t + + def parse(self, line): return octs2str(line).strip().split('|', 2) + + def evaluateOid(self, oid): + return univ.ObjectIdentifier(oid) + + def evaluateValue(self, oid, tag, value, **context): + # Interpolation module reference + if ':' in tag: + modName, tag = tag[tag.index(':')+1:], tag[:tag.index(':')] + else: + modName = None + # Unhexify + if tag and tag[-1] == 'x': + tag = tag[:-1] + value = [int(value[x:x+2], 16) for x in range(0, len(value), 2)] + if modName: + if modName in variationModules: + if 'dataValidation' in context: + return oid, univ.Null + else: + oid, value = variationModules[modName]['process'](oid, tag, value, **context) + else: + sys.stdout.write('ERROR: variation module "%s" referenced but not loaded\r\n' % modName) + return context['origOid'], context['errorStatus'] + else: + if 'dataValidation' in context: + return oid, self.tagMap[tag](value) + if not context['nextFlag'] and not context['exactMatch'] or \ + context['writeMode']: + return context['origOid'], context['errorStatus'] + if not hasattr(value, 'tagSet'): + value = self.tagMap[tag](value) + return oid, value + + def evaluate(self, line, **context): + oid, tag, value = self.parse(line) + oid = self.evaluateOid(oid) + if context.get('oidOnly'): + value = None + else: + try: + oid, value = self.evaluateValue(oid, tag, value, tagMap=self.tagMap, **context) + except Exception: + sys.stdout.write('ERROR: value evaluation for %s = %r failed: %s\r\n' % (oid, value, sys.exc_info()[1])) + oid = context['origOid'] + value = context['errorStatus'] + return oid, value + +parserSet = { + DumpParser.ext: DumpParser(), + MvcParser.ext: MvcParser(), + SapParser.ext: SapParser(), + WalkParser.ext: WalkParser(), + SnmprecParser.ext: SnmprecParser() +} + +class AbstractLayout: + layout = '?' + +# Data text file and OID index + +class DataFile(AbstractLayout): + layout = 'text' + openedQueue = [] + maxQueueEntries = 31 # max number of open text and index files + def __init__(self, textFile, textParser): + self.__textFile = textFile + self.__textParser = textParser + try: + self.__dbFile = textFile[:textFile.rindex(os.path.extsep)] + except ValueError: + self.__dbFile = textFile + + self.__dbFile = self.__dbFile + os.path.extsep + 'dbm' + + self.__db = self.__text = None + self.__dbType = '?' + + def indexText(self, forceIndexBuild=False): + textFileStamp = os.stat(self.__textFile)[8] + + # gdbm on OS X seems to voluntarily append .db, trying to catch that + + indexNeeded = forceIndexBuild + + for dbFile in ( + self.__dbFile + os.path.extsep + 'db', + self.__dbFile + ): + if os.path.exists(dbFile): + if textFileStamp < os.stat(dbFile)[8]: + if indexNeeded: + sys.stdout.write('Forced index rebuild %s\r\n' % dbFile) + elif not whichdb(dbFile): + indexNeeded = True + sys.stdout.write('Unsupported index format, rebuilding index %s\r\n' % dbFile) + else: + indexNeeded = True + sys.stdout.write('Index %s out of date\r\n' % dbFile) + break + else: + indexNeeded = True + sys.stdout.write('Index does not exist for %s\r\n' % self.__textFile) + + if indexNeeded: + # these might speed-up indexing + open_flags = 'nfu' + while open_flags: + try: + db = dbm.open(self.__dbFile, open_flags) + except Exception: + open_flags = open_flags[:-1] + if not open_flags: + raise + else: + break + + text = open(self.__textFile, 'rb') + + sys.stdout.write('Indexing data file %s (open flags \"%s\")...' % (self.__textFile, open_flags)) + sys.stdout.flush() + + lineNo = 0 + offset = 0 + prevOffset = -1 + while 1: + line = text.readline() + if not line: + break + + lineNo += 1 + + try: + oid, tag, val = self.__textParser.parse(line) + except Exception: + db.close() + exc = sys.exc_info()[1] + try: + os.remove(self.__dbFile) + except OSError: + pass + raise Exception( + 'Data error at %s:%d: %s' % ( + self.__textFile, lineNo, exc + ) + ) + + if validateData: + try: + self.__textParser.evaluateOid(oid) + except Exception: + db.close() + exc = sys.exc_info()[1] + try: + os.remove(self.__dbFile) + except OSError: + pass + raise Exception( + 'OID error at %s:%d: %s' % ( + self.__textFile, lineNo, exc + ) + ) + try: + self.__textParser.evaluateValue( + oid, tag, val, dataValidation=True + ) + except Exception: + sys.stdout.write( + '\r\n*** Error at line %s, value %r: %s\r\n' % \ + (lineNo, val, sys.exc_info()[1]) + ) + + # for lines serving subtrees, type is empty in tag field + db[oid] = '%d,%d,%d' % (offset, tag[0] == ':', prevOffset) + + if tag[0] == ':': + prevOffset = offset + else: + prevOffset = -1 # not a subtree - no backreference + + offset += len(line) + + text.close() + db.close() + + sys.stdout.write('...%d entries indexed\r\n' % (lineNo - 1,)) + + self.__dbType = whichdb(self.__dbFile) + + return self + + def close(self): + self.__text.close() + self.__db.close() + self.__db = self.__text = None + + def getHandles(self): + if self.__db is None: + if len(DataFile.openedQueue) > self.maxQueueEntries: + DataFile.openedQueue[0].close() + del DataFile.openedQueue[0] + + DataFile.openedQueue.append(self) + + self.__text = open(self.__textFile, 'rb') + + self.__db = dbm.open(self.__dbFile) + + return self.__text, self.__db + + def getTextFile(self): return self.__textFile + + # In-place, by-OID binary search + + def __searchOid(self, oid, eol=str2octs('\n')): + lo = mid = 0; prev_mid = -1; + self.__text.seek(0, 2) + hi = sz = self.__text.tell() + while lo < hi: + mid = (lo+hi)//2 + self.__text.seek(mid) + while mid: + c = self.__text.read(1) + if c == eol: + mid = mid + 1 + break + mid = mid - 1 # pivot stepping back in search for full line + self.__text.seek(mid) + if mid == prev_mid: # loop condition due to stepping back pivot + break + if mid >= sz: + return sz + line = self.__text.readline() + midval, _ = self.__textParser.evaluate(line, oidOnly=True) + if midval < oid: + lo = mid + len(line) + elif midval > oid: + hi = mid + else: + return mid + prev_mid = mid + if lo == mid: + return lo + else: + return hi + + def processVarBinds(self, varBinds, nextFlag=False, writeMode=False): + rspVarBinds = [] + + if nextFlag: + errorStatus = exval.endOfMib + else: + errorStatus = exval.noSuchInstance + + text, db = self.getHandles() + + varsRemaining = len(varBinds) + + for oid, val in varBinds: + textOid = str( + univ.OctetString('.'.join([ '%s' % x for x in oid ])) + ) + + try: + offset, subtreeFlag, prevOffset = db[textOid].split(',') + exactMatch = True + subtreeFlag = int(subtreeFlag) + except KeyError: + offset = self.__searchOid(oid) + subtreeFlag = exactMatch = False + + offset = int(offset) + + text.seek(offset) + + varsRemaining -= 1 + + line = text.readline() # matched line + + while True: + if exactMatch: + if nextFlag and not subtreeFlag: + _nextLine = text.readline() # next line + if _nextLine: + _nextOid, _ = self.__textParser.evaluate(_nextLine, oidOnly=True) + _, subtreeFlag, _ = db[str(_nextOid)].split(',') + subtreeFlag = int(subtreeFlag) + line = _nextLine + else: # search function above always rounds up to the next OID + _oid, _ = self.__textParser.evaluate(line, oidOnly=True) + _, _, _prevOffset = db[str(_oid)].split(',') + _prevOffset = int(_prevOffset) + + if _prevOffset >= 0: # previous line serves a subtree + text.seek(_prevOffset) + _prevLine = text.readline() + _prevOid, _ = self.__textParser.evaluate(_prevLine, oidOnly=True) + if _prevOid.isPrefixOf(oid): + line = _prevLine # use previous line to the matched one + subtreeFlag = True + + if not line: + _oid = oid + _val = errorStatus + break + + try: + _oid, _val = self.__textParser.evaluate(line, writeMode=writeMode, origOid=oid, origValue=val, dataFile=self.getTextFile(), subtreeFlag=subtreeFlag, nextFlag=nextFlag, exactMatch=exactMatch, errorStatus=errorStatus, varsRemaining=varsRemaining) + if _val is exval.endOfMib: + exactMatch = True + subtreeFlag = False + continue + except PyAsn1Error: + _oid = oid + _val = errorStatus + except Exception: + raise Exception( + 'Data error at %s for %s: %s' % (self, textOid, sys.exc_info()[1]) + ) + + break + + rspVarBinds.append((_oid, _val)) + + return rspVarBinds + + def __str__(self): + return 'file %s, %s-indexed, %s' % ( + self.__textFile, self.__dbType, self.__db and 'opened' or 'closed' + ) + +# Collect data files + +def getDataFiles(tgtDir, topLen=None): + if topLen is None: + topLen = len(tgtDir.split(os.path.sep)) + dirContent = [] + for dFile in os.listdir(tgtDir): + fullPath = tgtDir + os.path.sep + dFile + inode = os.lstat(fullPath) + if stat.S_ISLNK(inode.st_mode): + relPath = fullPath.split(os.path.sep)[topLen:] + fullPath = os.readlink(fullPath) + if not os.path.isabs(fullPath): + fullPath = tgtDir + os.path.sep + fullPath + inode = os.stat(fullPath) + else: + relPath = fullPath.split(os.path.sep)[topLen:] + if stat.S_ISDIR(inode.st_mode): + dirContent = dirContent + getDataFiles(fullPath, topLen) + continue + if not stat.S_ISREG(inode.st_mode): + continue + try: + dExt = dFile[dFile.rindex(os.path.extsep):] + except ValueError: + continue + if dExt not in parserSet: + continue + dirContent.append( + (fullPath, parserSet[dExt], os.path.sep.join(relPath)[:-len(dExt)]) + ) + return dirContent + +# Lightweignt MIB instrumentation (API-compatible with pysnmp's) + +class MibInstrumController: + def __init__(self, dataFile): + self.__dataFile = dataFile + + def __str__(self): return str(self.__dataFile) + + def readVars(self, varBinds, acInfo=None): + return self.__dataFile.processVarBinds(varBinds, False) + + def readNextVars(self, varBinds, acInfo=None): + return self.__dataFile.processVarBinds(varBinds, True) + + def writeVars(self, varBinds, acInfo=None): + return self.__dataFile.processVarBinds(varBinds, False, True) + +# Data files index as a MIB instrumentaion at a dedicated SNMP context + +class DataIndexInstrumController: + indexSubOid = (1,) + def __init__(self, baseOid=(1, 3, 6, 1, 4, 1, 20408, 999)): + self.__db = indices.OidOrderedDict() + self.__indexOid = baseOid + self.indexSubOid + self.__idx = 1 + + def readVars(self, varBinds, acInfo=None): + return [ (vb[0], self.__db.get(vb[0], exval.noSuchInstance)) for vb in varBinds ] + + def __getNextVal(self, key, default): + try: + key = self.__db.nextKey(key) + except KeyError: + return key, default + else: + return key, self.__db[key] + + def readNextVars(self, varBinds, acInfo=None): + return [ self.__getNextVal(vb[0], exval.endOfMib) for vb in varBinds ] + + def writeVars(self, varBinds, acInfo=None): + return [ (vb[0], exval.noSuchInstance) for vb in varBinds ] + + def addDataFile(self, *args): + for idx in range(len(args)): + self.__db[ + self.__indexOid + (idx+1, self.__idx) + ] = rfc1902.OctetString(args[idx]) + self.__idx = self.__idx + 1 + +dataIndexInstrumController = DataIndexInstrumController() + +mibInstrumControllerSet = { + DataFile.layout: MibInstrumController +} + +# Suggest variations of context name based on request data +def probeContext(transportDomain, transportAddress, contextName): + candidate = [ + contextName, '.'.join([ str(x) for x in transportDomain ]) + ] + if transportDomain[:len(udp.domainName)] == udp.domainName: + candidate.append(transportAddress[0]) + elif udp6 and transportDomain[:len(udp6.domainName)] == udp6.domainName: + candidate.append( + str(transportAddress[0]).replace(':', '_') + ) + elif unix and transportDomain[:len(unix.domainName)] == unix.domainName: + candidate.append(transportAddress) + + candidate = [ str(x) for x in candidate if x ] + + while candidate: + yield rfc1902.OctetString( + os.path.normpath(os.path.sep.join(candidate)) + ).asOctets() + del candidate[-1] + +if not v2cArch: + def probeHashContext(self, snmpEngine, stateReference, contextName): + transportDomain, transportAddress = snmpEngine.msgAndPduDsp.getTransportInfo(stateReference) + + for probedContextName in probeContext(transportDomain, transportAddress, contextName): + probedContextName = md5(probedContextName).hexdigest() + try: + self.snmpContext.getMibInstrum(probedContextName) + except error.PySnmpError: + pass + else: + return probedContextName + return contextName + + class GetCommandResponder(cmdrsp.GetCommandResponder): + def handleMgmtOperation( + self, snmpEngine, stateReference, contextName, PDU, acInfo + ): + cmdrsp.GetCommandResponder.handleMgmtOperation( + self, snmpEngine, stateReference, + probeHashContext(self, snmpEngine, stateReference, contextName), + PDU, acInfo + ) + + class SetCommandResponder(cmdrsp.SetCommandResponder): + def handleMgmtOperation( + self, snmpEngine, stateReference, contextName, PDU, acInfo + ): + cmdrsp.SetCommandResponder.handleMgmtOperation( + self, snmpEngine, stateReference, + probeHashContext(self, snmpEngine, stateReference, contextName), + PDU, acInfo + ) + + class NextCommandResponder(cmdrsp.NextCommandResponder): + def handleMgmtOperation( + self, snmpEngine, stateReference, contextName, PDU, acInfo + ): + cmdrsp.NextCommandResponder.handleMgmtOperation( + self, snmpEngine, stateReference, + probeHashContext(self, snmpEngine, stateReference, contextName), + PDU, acInfo + ) + + class BulkCommandResponder(cmdrsp.BulkCommandResponder): + def handleMgmtOperation( + self, snmpEngine, stateReference, contextName, PDU, acInfo + ): + cmdrsp.BulkCommandResponder.handleMgmtOperation( + self, snmpEngine, stateReference, + probeHashContext(self, snmpEngine, stateReference, contextName), + PDU, acInfo + ) + +# Basic SNMP engine configuration + +if v2cArch: + contexts = { univ.OctetString('index'): dataIndexInstrumController } +else: + snmpEngine = engine.SnmpEngine() + + config.addContext(snmpEngine, '') + + snmpContext = context.SnmpContext(snmpEngine) + + config.addV3User( + snmpEngine, + v3User, + authProtocols[v3AuthProto], v3AuthKey, + privProtocols[v3PrivProto], v3PrivKey + ) + +if variationModules: + sys.stdout.write('Initializing variation modules:\r\n') + for name, body in variationModules.items(): + sys.stdout.write(' %s... ' % name) + for x in ('init', 'process', 'shutdown'): + if x not in body: + sys.stdout.write('error: missing %s handler!\r\n' % x) + sys.exit(-1) + try: + body['init'](not v2cArch and snmpEngine or None, *body['args']) + except Exception: + sys.stdout.write('FAILED: %s\r\n' % sys.exc_info()[1]) + else: + sys.stdout.write('OK\r\n') + +# Build pysnmp Managed Objects base from data files information + +_mibInstrums = {} + +for dataDir in dataDirs: + sys.stdout.write( + 'Scanning "%s" directory for %s data files\r\n%s\r\n' % (dataDir, ','.join([' *%s' % x.ext for x in parserSet.values()]), '='*66) + ) + for fullPath, textParser, communityName in getDataFiles(dataDir): + if fullPath in _mibInstrums: + mibInstrum = _mibInstrums[fullPath] + sys.stdout.write('Shared data %s\r\n' % (mibInstrum,)) + else: + dataFile = DataFile(fullPath, textParser).indexText(forceIndexBuild) + mibInstrum = mibInstrumControllerSet[dataFile.layout](dataFile) + + _mibInstrums[fullPath] = mibInstrum + sys.stdout.write('Data file %s\r\n' % (mibInstrum,)) + + sys.stdout.write('SNMPv1/2c community name: %s\r\n' % (communityName,)) + + if v2cArch: + contexts[univ.OctetString(communityName)] = mibInstrum + + dataIndexInstrumController.addDataFile( + fullPath, communityName + ) + else: + agentName = contextName = md5(univ.OctetString(communityName).asOctets()).hexdigest() + + if not v3Only: + config.addV1System( + snmpEngine, agentName, communityName, contextName=contextName + ) + + snmpContext.registerContextName(contextName, mibInstrum) + + dataIndexInstrumController.addDataFile( + fullPath, communityName, contextName + ) + + sys.stdout.write('SNMPv3 context name: %s\r\n' % (contextName,)) + + sys.stdout.write('%s\r\n' % ('-+' * 33,)) + +del _mibInstrums + +if v2cArch: + def getBulkHandler(varBinds, nonRepeaters, maxRepetitions, readNextVars): + if nonRepeaters < 0: nonRepeaters = 0 + if maxRepetitions < 0: maxRepetitions = 0 + N = min(nonRepeaters, len(varBinds)) + M = int(maxRepetitions) + R = max(len(varBinds)-N, 0) + if nonRepeaters: + rspVarBinds = readNextVars(varBinds[:int(nonRepeaters)]) + else: + rspVarBinds = [] + if M and R: + for i in range(N, R): + varBind = varBinds[i] + for r in range(1, M): + rspVarBinds.extend(readNextVars((varBind,))) + varBind = rspVarBinds[-1] + + return rspVarBinds + + def commandResponderCbFun(transportDispatcher, transportDomain, + transportAddress, wholeMsg): + while wholeMsg: + msgVer = api.decodeMessageVersion(wholeMsg) + if msgVer in api.protoModules: + pMod = api.protoModules[msgVer] + else: + sys.stdout.write('Unsupported SNMP version %s\r\n' % (msgVer,)) + return + reqMsg, wholeMsg = decoder.decode( + wholeMsg, asn1Spec=pMod.Message(), + ) + + communityName = reqMsg.getComponentByPosition(1) + for communityName in probeContext(transportDomain, transportAddress, communityName): + if communityName in contexts: + break + else: + return wholeMsg + + rspMsg = pMod.apiMessage.getResponse(reqMsg) + rspPDU = pMod.apiMessage.getPDU(rspMsg) + reqPDU = pMod.apiMessage.getPDU(reqMsg) + + if reqPDU.isSameTypeWith(pMod.GetRequestPDU()): + backendFun = contexts[communityName].readVars + elif reqPDU.isSameTypeWith(pMod.SetRequestPDU()): + backendFun = contexts[communityName].writeVars + elif reqPDU.isSameTypeWith(pMod.GetNextRequestPDU()): + backendFun = contexts[communityName].readNextVars + elif hasattr(pMod, 'GetBulkRequestPDU') and \ + reqPDU.isSameTypeWith(pMod.GetBulkRequestPDU()): + if not msgVer: + sys.stdout.write('GETBULK over SNMPv1 from %s:%s\r\n' % ( + transportDomain, transportAddress + )) + return wholeMsg + backendFun = lambda varBinds: getBulkHandler(varBinds, + pMod.apiBulkPDU.getNonRepeaters(reqPDU), + pMod.apiBulkPDU.getMaxRepetitions(reqPDU), + contexts[communityName].readNextVars) + else: + sys.stdout.write('Unsuppored PDU type %s from %s:%s\r\n' % ( + reqPDU.__class__.__name__, transportDomain, + transportAddress + )) + return wholeMsg + + varBinds = backendFun( + pMod.apiPDU.getVarBinds(reqPDU) + ) + + # Poor man's v2c->v1 translation + errorMap = { rfc1902.Counter64.tagSet: 5, + rfc1905.NoSuchObject.tagSet: 2, + rfc1905.NoSuchInstance.tagSet: 2, + rfc1905.EndOfMibView.tagSet: 2 } + + if not msgVer: + for idx in range(len(varBinds)): + oid, val = varBinds[idx] + if val.tagSet in errorMap: + varBinds = pMod.apiPDU.getVarBinds(reqPDU) + pMod.apiPDU.setErrorStatus(rspPDU, errorMap[val.tagSet]) + pMod.apiPDU.setErrorIndex(rspPDU, idx+1) + break + + pMod.apiPDU.setVarBinds(rspPDU, varBinds) + + transportDispatcher.sendMessage( + encoder.encode(rspMsg), transportDomain, transportAddress + ) + + return wholeMsg + + # Configure access to data index + + contexts['index'] = dataIndexInstrumController + + # Configure socket server + + sys.stdout.write('Listening at:\r\n') + + transportDispatcher = AsynsockDispatcher() + for idx in range(len(agentUDPv4Endpoints)): + transportDispatcher.registerTransport( + udp.domainName + (idx,), + udp.UdpTransport().openServerMode(agentUDPv4Endpoints[idx]) + ) + sys.stdout.write(' UDP/IPv4 endpoint %s:%s, transport ID %s\r\n' % (agentUDPv4Endpoints[idx] + ('.'.join([str(x) for x in udp.domainName + (idx,)]),))) + for idx in range(len(agentUDPv6Endpoints)): + transportDispatcher.registerTransport( + udp6.domainName + (idx,), + udp6.Udp6Transport().openServerMode(agentUDPv6Endpoints[idx]) + ) + sys.stdout.write(' UDP/IPv6 endpoint %s:%s, transport ID %s\r\n' % (agentUDPv6Endpoints[idx] + ('.'.join([str(x) for x in udp6.domainName + (idx,)]),))) + for idx in range(len(agentUNIXEndpoints)): + transportDispatcher.registerTransport( + unix.domainName + (idx,), + unix.UnixTransport().openServerMode(agentUNIXEndpoints[idx]) + ) + sys.stdout.write(' UNIX domain endpoint %s, transport ID %s\r\n' % (agentUNIXEndpoints[idx], '.'.join([str(x) for x in unix.domainName + (idx,)]))) + transportDispatcher.registerRecvCbFun(commandResponderCbFun) +else: + sys.stdout.write('SNMPv3 credentials:\r\nUsername: %s\r\n' % v3User) + if authProtocols[v3AuthProto] != config.usmNoAuthProtocol: + sys.stdout.write('Authentication key: %s\r\nAuthentication protocol: %s\r\n' % (v3AuthKey, v3AuthProto)) + if privProtocols[v3PrivProto] != config.usmNoPrivProtocol: + sys.stdout.write('Encryption (privacy) key: %s\r\nEncryption protocol: %s\r\n' % (v3PrivKey, v3PrivProto)) + + # Configure access to data index + + config.addV1System(snmpEngine, 'index', + 'index', contextName='index') + + snmpContext.registerContextName( + 'index', dataIndexInstrumController + ) + + # Configure socket server + + sys.stdout.write('Listening at:\r\n') + + for idx in range(len(agentUDPv4Endpoints)): + config.addSocketTransport( + snmpEngine, + udp.domainName + (idx,), + udp.UdpTransport().openServerMode(agentUDPv4Endpoints[idx]) + ) + sys.stdout.write(' UDP/IPv4 endpoint %s:%s, transport ID %s\r\n' % (agentUDPv4Endpoints[idx] + ('.'.join([str(x) for x in udp.domainName + (idx,)]),))) + for idx in range(len(agentUDPv6Endpoints)): + config.addSocketTransport( + snmpEngine, + udp6.domainName + (idx,), + udp6.Udp6Transport().openServerMode(agentUDPv6Endpoints[idx]) + ) + sys.stdout.write(' UDP/IPv6 endpoint %s:%s, transport ID %s\r\n' % (agentUDPv6Endpoints[idx] + ('.'.join([str(x) for x in udp6.domainName + (idx,)]),))) + for idx in range(len(agentUNIXEndpoints)): + config.addSocketTransport( + snmpEngine, + unix.domainName + (idx,), + unix.UnixTransport().openServerMode(agentUNIXEndpoints[idx]) + ) + sys.stdout.write(' UNIX domain endpoint %s, transport ID %s\r\n' % (agentUNIXEndpoints[idx], '.'.join([str(x) for x in unix.domainName + (idx,)]))) + + # SNMP applications + + GetCommandResponder(snmpEngine, snmpContext) + SetCommandResponder(snmpEngine, snmpContext) + NextCommandResponder(snmpEngine, snmpContext) + BulkCommandResponder(snmpEngine, snmpContext) + + transportDispatcher = snmpEngine.transportDispatcher + +# Run mainloop + +transportDispatcher.jobStarted(1) # server job would never finish + +# Python 2.4 does not support the "finally" clause + +exc_info = None + +try: + transportDispatcher.runDispatcher() +except KeyboardInterrupt: + sys.stdout.write('Process terminated\r\n') +except Exception: + exc_info = sys.exc_info() + +if variationModules: + sys.stdout.write('Shutting down variation modules:\r\n') + for name, body in variationModules.items(): + sys.stdout.write(' %s... ' % name) + try: + body['shutdown'](not v2cArch and snmpEngine or None, *body['args']) + except Exception: + sys.stdout.write('FAILED: %s\r\n' % sys.exc_info()[1]) + else: + sys.stdout.write('OK\r\n') + +transportDispatcher.closeDispatcher() + +if exc_info: + raise exc_info[0], exc_info[1], exc_info[2] diff --git a/setup.py b/setup.py index 2d63ff2..d559cc8 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ to respond like their original counterparts do. """ import sys +import os classifiers = """\ Development Status :: 5 - Production/Stable @@ -54,12 +55,12 @@ def howto_install_setuptools(): try: from setuptools import setup params = { - 'install_requires': [ 'pysnmp>=4.2.2' ], - 'zip_safe': True + 'install_requires': [ 'pysnmp>=4.2.4' ], + 'zip_safe': False } except ImportError: for arg in sys.argv: - if "egg" in arg: + if 'egg' in arg: if sys.version_info[0] > 2: howto_install_distribute() else: @@ -73,21 +74,29 @@ def howto_install_setuptools(): doclines = [ x.strip() for x in __doc__.split('\n') if x ] params.update( { - 'name': "snmpsim", - 'version': "0.1.6", + 'name': 'snmpsim', + 'version': open(os.path.join('snmpsim, '__init__.py')).read().split('\'')[1]+'rc0', 'description': doclines[0], 'long_description': ' '.join(doclines[1:]), 'maintainer': 'Ilya Etingof ', - 'author': "Ilya Etingof", - 'author_email': "ilya@glas.net ", - 'url': "http://sourceforge.net/projects/snmpsim/", + 'author': 'Ilya Etingof', + 'author_email': 'ilya@glas.net', + 'url': 'http://sourceforge.net/projects/snmpsim/', 'platforms': ['any'], 'classifiers': [ x for x in classifiers.split('\n') if x ], - 'scripts': [ 'snmpsimd.py', 'snmprec.py', 'mib2dev.py' ], - 'license': "BSD" - } ) + 'scripts': [ os.path.join('scripts', 'snmpsimd.py'), + os.path.join('scripts', 'snmprec.py'), + os.path.join('scripts', 'mib2dev.py') ], + 'license': 'BSD', + 'packages': [ 'snmpsim' ], + 'package_data': { 'snmpsim': [ os.path.join('data' ,'*.snmprec'), + os.path.join('data', '*', '*.snmprec'), + os.path.join('data', '*', '*', '*.snmprec'), + os.path.join('data', '*', '*', '*', '*.snmprec'), + os.path.join('variation', '*.py') ] } +} ) -if "py2exe" in sys.argv: +if 'py2exe' in sys.argv: import py2exe # fix executables params['console'] = params['scripts'] diff --git a/snmpsim/__init__.py b/snmpsim/__init__.py new file mode 100644 index 0000000..6958e33 --- /dev/null +++ b/snmpsim/__init__.py @@ -0,0 +1,6 @@ +import os + +root = os.path.abspath(os.path.dirname(__file__)) + +# http://www.python.org/dev/peps/pep-0396/ +__version__ = '0.2.0'