././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8538616 snimpy-1.0.0/0000755000076400001440000000000000000000000012226 5ustar00bernatusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/.github/0000755000076400001440000000000000000000000013566 5ustar00bernatusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/.github/workflows/0000755000076400001440000000000000000000000015623 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/.github/workflows/tests.yml0000644000076400001440000000245400000000000017515 0ustar00bernatusersname: Tests on: push: pull_request: schedule: - cron: 30 7 2 * * jobs: tests: name: Run tests runs-on: ubuntu-latest strategy: matrix: python-version: - 3.6 - 3.7 - 3.8 - 3.9 steps: - uses: actions/checkout@v1 with: submodules: true - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install non-Python dependencies run: > sudo apt -qy update; sudo apt -qy install pkg-config libsmi2-dev libsnmp-dev snmp-mibs-downloader ncurses-term - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install tox tox-gh-actions wheel - name: Run tests with tox run: tox - name: Collect coveralls data uses: AndreMiras/coveralls-python-action@develop with: parallel: true coveralls_finish: name: Coveralls.io needs: tests runs-on: ubuntu-latest steps: - name: Send results to coveralls.io uses: AndreMiras/coveralls-python-action@develop with: parallel-finished: true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/.gitignore0000644000076400001440000000026500000000000014221 0ustar00bernatusers*.pyc *~ *.so /version.txt /build /snimpy.egg-info /snimpy/_version.py /*.egg /.eggs/ /dist /MANIFEST /.coverage /htmlcov /.tox /.noseids /docs/_build __pycache__ .index cscope.out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/.gitmodules0000644000076400001440000000015400000000000014403 0ustar00bernatusers[submodule "docs/_themes"] path = docs/_themes url = https://github.com/mitsuhiko/flask-sphinx-themes.git ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/AUTHORS.rst0000644000076400001440000000020400000000000014101 0ustar00bernatusersDevelopment Lead ---------------- * Vincent Bernat Contributors ------------ * Jakub Wroniecki * Julian Taylor ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588509428.0 snimpy-1.0.0/CONTRIBUTING.rst0000644000076400001440000000606100000000000014672 0ustar00bernatusers============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/vincentbernat/snimpy/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ Snimpy could always use more documentation, whether as part of the official Snimpy docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/vincentbernat/snimpy/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------ Ready to contribute? Here's how to set up `snimpy` for local development. 1. Fork the `snimpy` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/snimpy.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: $ mkvirtualenv snimpy $ cd snimpy/ $ python setup.py develop 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: $ flake8 snimpy tests $ python setup.py test $ tox To get flake8 and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 3.4+. Check https://travis-ci.org/vincentbernat/snimpy/pull_requests and make sure that the tests pass for all supported Python versions. Tips ---- To run a subset of tests:: $ python -m nose tests/test_snmp.py ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302538.0 snimpy-1.0.0/HISTORY.rst0000644000076400001440000000701700000000000014126 0ustar00bernatusers.. :changelog: History ------- 1.0.0 (2021-05-29) ++++++++++++++++++ * Drop compatibility with Python 2. 0.8.14 (2020-04-26) +++++++++++++++++++ * Add ``items()`` in addition to ``iteritems()`` This is an iterator on Python 3 and return a list of items in Python 2. 0.8.13 (2018-10-12) +++++++++++++++++++ * Compatibility with Python 3.7. * Fix an issue with implied index when reusing indexes between tables. 0.8.12 (2017-10-02) +++++++++++++++++++ * Support for more recent versions of IPython. * Support for SNMPv3 context name. * Support for notification nodes (MIB only). 0.8.11 (2016-08-13) +++++++++++++++++++ * Fix IPython interactive shell. * Fix IPv6 handling for sessions. * Ability for a session to return None instead of raising an exception. 0.8.10 (2016-02-16) +++++++++++++++++++ * Ability to walk a table (if the first index is accessible). * Ability to do a partial walk (courtesy of Alex Unigovsky). 0.8.8 (2015-11-15) ++++++++++++++++++ * Fix thread-safety problem introduced in 0.8.6. This also undo any improvement advertised in 0.8.6 when using multiple threads. However, performance should be kept when using a single thread. 0.8.7 (2015-11-14) ++++++++++++++++++ * Ability to specify a module name when querying a manager. * Compatibility with PySNMP 4.3 * Array-like interface for OIDs. * Ability to restrict lookups to a specific MIB: m['IF-MIB'].ifDescr. * Fix multithread support with SNMPv3 (with a performance impact). 0.8.6 (2015-06-24) ++++++++++++++++++ * Major speed improvement. * Major memory usage improvement. 0.8.5 (2015-04-04) ++++++++++++++++++ * Ability to set SMI search path (with ``mib.path()``) * Fix documentation build on *Read the Doc*. * Add a loose mode to manager to loosen type coercion. 0.8.4 (2015-02-10) ++++++++++++++++++ * More CFFI workarounds, including cross-compilation support. * Ability to override a node type. * Automatic workaround for "SNMP too big" error message. 0.8.3 (2014-08-18) ++++++++++++++++++ * IPv6 support. 0.8.2 (2014-06-08) ++++++++++++++++++ * Minor bugfixes. 0.8.1 (2013-10-25) ++++++++++++++++++ * Workaround a problem with CFFI extension installation. 0.8.0 (2013-10-19) ++++++++++++++++++++ * Python 3.3 support. Pypy support. * PEP8 compliant. * Sphinx documentation. * Octet strings with a display hint are now treated differently than plain octet strings (unicode). Notably, they can now be set using the displayed format (for example, for MAC addresses). 0.7.0 (2013-09-23) ++++++++++++++++++ * Major rewrite. * SNMP support is now provided through PySNMP_. * MIB parsing is still done with ``libsmi`` but through CFFI instead of a C module. * More unittests. Many bugfixes. .. _PySNMP: http://pysnmp.sourceforge.net/ 0.6.4 (2013-03-21) ++++++++++++++++++ * GETBULK support. * MacAddress SMI type support. 0.6.3 (2012-04-13) ++++++++++++++++++ * Support for IPython 0.12. * Minor bugfixes. 0.6.2 (2012-01-19) ++++++++++++++++++ * Ability to return None instead of getting an exception. 0.6.1 (2012-01-14) ++++++++++++++++++ * Thread safety and efficiency. 0.6 (2012-01-10) ++++++++++++++++++ * SNMPv3 support 0.5.1 (2011-08-07) ++++++++++++++++++ * Compatibility with IPython 0.11. * Custom timeouts and retries. 0.5 (2010-02-03) ++++++++++++++++++ * Check conformity of loaded modules. * Many bugfixes. 0.4 (2009-06-06) ++++++++++++++++++ * Allow to cache requests. 0.3 (2008-11-23) ++++++++++++++++++ * Provide a manual page. * Use a context manager to group SET requests. 0.2.1 (2008-09-28) ++++++++++++++++++ * First release on PyPI. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/MANIFEST.in0000644000076400001440000000066500000000000013773 0ustar00bernatusersrecursive-include examples *.py recursive-include tests *.py SNIMPY-MIB.mib SNIMPY-INVALID-MIB.mib # Documentation include man/snimpy.1 include AUTHORS.rst include CONTRIBUTING.rst include HISTORY.rst include README.rst include version.txt recursive-include docs *.rst *.py recursive-include docs/_themes * recursive-include docs/_static * # Remove CFFI files global-exclude __pycache__/* # Remove git directories global-exclude .git ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588595527.0 snimpy-1.0.0/Makefile0000644000076400001440000000263300000000000013672 0ustar00bernatusers.PHONY: clean-pyc clean-build docs open := $(shell { which xdg-open || which open; } 2>/dev/null) help: @echo "Please use \`make ' where is one of" @echo " clean-build to remove build artifacts" @echo " clean-pyc to remove Python file artifacts" @echo " lint to check style with flake8" @echo " test to run tests quickly with the default Python" @echo " testall to run tests on every Python version with tox" @echo " coverage to check code coverage quickly with the default Python" @echo " docs to generate Sphinx HTML documentation, including API docs" @echo " release to package and upload a release" @echo " sdist to package" clean: clean-build clean-pyc clean-build: rm -fr build/ rm -fr dist/ rm -fr *.egg-info clean-pyc: find . -name '*.pyc' -type f -exec rm -f {} + find . -name '*.pyo' -type f -exec rm -f {} + find . -name '*~' -type f -exec rm -f {} + find . -name '__pycache__' -type d -exec rm -rf {} + lint: flake8 snimpy tests interrogate --fail-under 50 -v snimpy tests test: python -m nose test-all: tox coverage: coverage run --source snimpy setup.py test coverage report -m coverage html $(open) htmlcov/index.html docs: $(MAKE) -C docs clean $(MAKE) -C docs html $(open) docs/_build/html/index.html release: clean python setup.py sdist upload sdist: clean python setup.py sdist ls -l dist ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8538616 snimpy-1.0.0/PKG-INFO0000644000076400001440000001755300000000000013336 0ustar00bernatusersMetadata-Version: 2.1 Name: snimpy Version: 1.0.0 Summary: interactive SNMP tool Home-page: https://github.com/vincentbernat/snimpy Author: Vincent Bernat Author-email: bernat@luffy.cx License: UNKNOWN Description: =============================== snimpy =============================== .. image:: https://badge.fury.io/py/snimpy.png :target: http://badge.fury.io/py/snimpy .. image:: https://github.com/vincentbernat/snimpy/workflows/Tests/badge.svg .. image:: https://coveralls.io/repos/vincentbernat/snimpy/badge.png :target: https://coveralls.io/r/vincentbernat/snimpy --- Interactive SNMP tool. *Snimpy* is a Python-based tool providing a simple interface to build SNMP query. Here is a very simplistic example that allows us to display the routing table of a given host:: load("IP-FORWARD-MIB") m=M("localhost", "public", 2) routes = m.ipCidrRouteNextHop for x in routes: net, netmask, tos, src = x print("%15s/%-15s via %-15s src %-15s" % (net, netmask, routes[x], src)) You can either use *Snimpy* interactively throught its console (derived from Python own console or from IPython_ if available) or write *Snimpy* scripts which are just Python scripts with some global variables available. .. _IPython: http://ipython.org * Free software: ISC license * Documentation: http://snimpy.rtfd.org. *Snimpy* requires libsmi_ to work correctly. See the documentation for more information. .. _libsmi: https://www.ibr.cs.tu-bs.de/projects/libsmi/ Features -------- *Snimpy* is aimed at being the more Pythonic possible. You should forget that you are doing SNMP requests. *Snimpy* will rely on MIB to hide SNMP details. Here are some "features": * MIB parser based on libsmi (through CFFI) * SNMP requests are handled by PySNMP (SNMPv1, SNMPv2 and SNMPv3 support) * scalars are just attributes of your session object * columns are like a Python dictionary and made available as an attribute * getting an attribute is like issuing a GET method * setting an attribute is like issuing a SET method * iterating over a table is like using GETNEXT * when something goes wrong, you get an exception History ------- 1.0.0 (2021-05-29) ++++++++++++++++++ * Drop compatibility with Python 2. 0.8.14 (2020-04-26) +++++++++++++++++++ * Add ``items()`` in addition to ``iteritems()`` This is an iterator on Python 3 and return a list of items in Python 2. 0.8.13 (2018-10-12) +++++++++++++++++++ * Compatibility with Python 3.7. * Fix an issue with implied index when reusing indexes between tables. 0.8.12 (2017-10-02) +++++++++++++++++++ * Support for more recent versions of IPython. * Support for SNMPv3 context name. * Support for notification nodes (MIB only). 0.8.11 (2016-08-13) +++++++++++++++++++ * Fix IPython interactive shell. * Fix IPv6 handling for sessions. * Ability for a session to return None instead of raising an exception. 0.8.10 (2016-02-16) +++++++++++++++++++ * Ability to walk a table (if the first index is accessible). * Ability to do a partial walk (courtesy of Alex Unigovsky). 0.8.8 (2015-11-15) ++++++++++++++++++ * Fix thread-safety problem introduced in 0.8.6. This also undo any improvement advertised in 0.8.6 when using multiple threads. However, performance should be kept when using a single thread. 0.8.7 (2015-11-14) ++++++++++++++++++ * Ability to specify a module name when querying a manager. * Compatibility with PySNMP 4.3 * Array-like interface for OIDs. * Ability to restrict lookups to a specific MIB: m['IF-MIB'].ifDescr. * Fix multithread support with SNMPv3 (with a performance impact). 0.8.6 (2015-06-24) ++++++++++++++++++ * Major speed improvement. * Major memory usage improvement. 0.8.5 (2015-04-04) ++++++++++++++++++ * Ability to set SMI search path (with ``mib.path()``) * Fix documentation build on *Read the Doc*. * Add a loose mode to manager to loosen type coercion. 0.8.4 (2015-02-10) ++++++++++++++++++ * More CFFI workarounds, including cross-compilation support. * Ability to override a node type. * Automatic workaround for "SNMP too big" error message. 0.8.3 (2014-08-18) ++++++++++++++++++ * IPv6 support. 0.8.2 (2014-06-08) ++++++++++++++++++ * Minor bugfixes. 0.8.1 (2013-10-25) ++++++++++++++++++ * Workaround a problem with CFFI extension installation. 0.8.0 (2013-10-19) ++++++++++++++++++++ * Python 3.3 support. Pypy support. * PEP8 compliant. * Sphinx documentation. * Octet strings with a display hint are now treated differently than plain octet strings (unicode). Notably, they can now be set using the displayed format (for example, for MAC addresses). 0.7.0 (2013-09-23) ++++++++++++++++++ * Major rewrite. * SNMP support is now provided through PySNMP_. * MIB parsing is still done with ``libsmi`` but through CFFI instead of a C module. * More unittests. Many bugfixes. .. _PySNMP: http://pysnmp.sourceforge.net/ 0.6.4 (2013-03-21) ++++++++++++++++++ * GETBULK support. * MacAddress SMI type support. 0.6.3 (2012-04-13) ++++++++++++++++++ * Support for IPython 0.12. * Minor bugfixes. 0.6.2 (2012-01-19) ++++++++++++++++++ * Ability to return None instead of getting an exception. 0.6.1 (2012-01-14) ++++++++++++++++++ * Thread safety and efficiency. 0.6 (2012-01-10) ++++++++++++++++++ * SNMPv3 support 0.5.1 (2011-08-07) ++++++++++++++++++ * Compatibility with IPython 0.11. * Custom timeouts and retries. 0.5 (2010-02-03) ++++++++++++++++++ * Check conformity of loaded modules. * Many bugfixes. 0.4 (2009-06-06) ++++++++++++++++++ * Allow to cache requests. 0.3 (2008-11-23) ++++++++++++++++++ * Provide a manual page. * Use a context manager to group SET requests. 0.2.1 (2008-09-28) ++++++++++++++++++ * First release on PyPI. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: ISC License (ISCL) Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 3 Classifier: Topic :: System :: Networking Classifier: Topic :: Utilities Classifier: Topic :: System :: Monitoring Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1604672095.0 snimpy-1.0.0/README.rst0000644000076400001440000000363100000000000013720 0ustar00bernatusers=============================== snimpy =============================== .. image:: https://badge.fury.io/py/snimpy.png :target: http://badge.fury.io/py/snimpy .. image:: https://github.com/vincentbernat/snimpy/workflows/Tests/badge.svg .. image:: https://coveralls.io/repos/vincentbernat/snimpy/badge.png :target: https://coveralls.io/r/vincentbernat/snimpy --- Interactive SNMP tool. *Snimpy* is a Python-based tool providing a simple interface to build SNMP query. Here is a very simplistic example that allows us to display the routing table of a given host:: load("IP-FORWARD-MIB") m=M("localhost", "public", 2) routes = m.ipCidrRouteNextHop for x in routes: net, netmask, tos, src = x print("%15s/%-15s via %-15s src %-15s" % (net, netmask, routes[x], src)) You can either use *Snimpy* interactively throught its console (derived from Python own console or from IPython_ if available) or write *Snimpy* scripts which are just Python scripts with some global variables available. .. _IPython: http://ipython.org * Free software: ISC license * Documentation: http://snimpy.rtfd.org. *Snimpy* requires libsmi_ to work correctly. See the documentation for more information. .. _libsmi: https://www.ibr.cs.tu-bs.de/projects/libsmi/ Features -------- *Snimpy* is aimed at being the more Pythonic possible. You should forget that you are doing SNMP requests. *Snimpy* will rely on MIB to hide SNMP details. Here are some "features": * MIB parser based on libsmi (through CFFI) * SNMP requests are handled by PySNMP (SNMPv1, SNMPv2 and SNMPv3 support) * scalars are just attributes of your session object * columns are like a Python dictionary and made available as an attribute * getting an attribute is like issuing a GET method * setting an attribute is like issuing a SET method * iterating over a table is like using GETNEXT * when something goes wrong, you get an exception ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/docs/0000755000076400001440000000000000000000000013156 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/docs/Makefile0000644000076400001440000001517100000000000014623 0ustar00bernatusers# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/docs/_static/0000755000076400001440000000000000000000000014604 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/docs/_static/snimpy.svg0000644000076400001440000002744300000000000016656 0ustar00bernatusers image/svg+xml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/docs/_themes/0000755000076400001440000000000000000000000014602 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193872.0 snimpy-1.0.0/docs/_themes/.gitignore0000644000076400001440000000002600000000000016570 0ustar00bernatusers*.pyc *.pyo .DS_Store ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193872.0 snimpy-1.0.0/docs/_themes/LICENSE0000644000076400001440000000337500000000000015617 0ustar00bernatusersCopyright (c) 2010 by Armin Ronacher. Some rights reserved. Redistribution and use in source and binary forms of the theme, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. We kindly ask you to only use these themes in an unmodified manner just for Flask and Flask-related products, not for unrelated projects. If you like the visual style and want to use it for your own projects, please consider making some larger changes to the themes (such as changing font faces, sizes, colors or margins). THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193872.0 snimpy-1.0.0/docs/_themes/README0000644000076400001440000000210500000000000015460 0ustar00bernatusersFlask Sphinx Styles =================== This repository contains sphinx styles for Flask and Flask related projects. To use this style in your Sphinx documentation, follow this guide: 1. put this folder as _themes into your docs folder. Alternatively you can also use git submodules to check out the contents there. 2. add this to your conf.py: sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'flask' The following themes exist: - 'flask' - the standard flask documentation theme for large projects - 'flask_small' - small one-page theme. Intended to be used by very small addon libraries for flask. The following options exist for the flask_small theme: [options] index_logo = '' filename of a picture in _static to be used as replacement for the h1 in the index.rst file. index_logo_height = 120px height of the index logo github_fork = '' repository name on github for the "fork me" badge ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/docs/_themes/flask/0000755000076400001440000000000000000000000015702 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193914.0 snimpy-1.0.0/docs/_themes/flask/layout.html0000644000076400001440000000126500000000000020111 0ustar00bernatusers{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} {% if theme_touch_icon %} {% endif %} {% endblock %} {%- block relbar2 %}{% endblock %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {%- block footer %} {% if pagename == 'index' %}
{% endif %} {%- endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193914.0 snimpy-1.0.0/docs/_themes/flask/relations.html0000644000076400001440000000111600000000000020567 0ustar00bernatusers

Related Topics

././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/docs/_themes/flask/static/0000755000076400001440000000000000000000000017171 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193916.0 snimpy-1.0.0/docs/_themes/flask/static/flasky.css_t0000644000076400001440000002154600000000000021527 0ustar00bernatusers/* * flasky.css_t * ~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ {% set page_width = '940px' %} {% set sidebar_width = '220px' %} @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; background-color: white; color: #000; margin: 0; padding: 0; } div.document { width: {{ page_width }}; margin: 30px auto 0 auto; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 {{ sidebar_width }}; } div.sphinxsidebar { width: {{ sidebar_width }}; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 0 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { width: {{ page_width }}; margin: 20px auto 30px auto; font-size: 14px; color: #888; text-align: right; } div.footer a { color: #888; } div.related { display: none; } div.sphinxsidebar a { color: #444; text-decoration: none; border-bottom: 1px dotted #999; } div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } div.sphinxsidebar { font-size: 14px; line-height: 1.5; } div.sphinxsidebarwrapper { padding: 18px 10px; } div.sphinxsidebarwrapper p.logo { padding: 0 0 20px 0; margin: 0; text-align: center; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; color: #444; font-size: 24px; font-weight: normal; margin: 0 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p.logo a, div.sphinxsidebar h3 a, div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } div.sphinxsidebar p { color: #555; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: 'Georgia', serif; font-size: 1em; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; } {% endif %} div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: #ddd; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition tt.xref, div.admonition a tt { border-bottom: 1px solid #fafafa; } dd div.admonition { margin-left: -60px; padding-left: 60px; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight { background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; background: #fdfdfd; font-size: 0.9em; } table.footnote + table.footnote { margin-top: -15px; border-top: none; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td.label { width: 0px; padding: 0.3em 0 0.3em 0.5em; } table.footnote td { padding: 0.3em 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } blockquote { margin: 0 0 0 30px; padding: 0; } ul, ol { margin: 10px 0 10px 30px; padding: 0; } pre { background: #eee; padding: 7px 30px; margin: 15px -30px; line-height: 1.3em; } dl pre, blockquote pre, li pre { margin-left: -60px; padding-left: 60px; } dl dl pre { margin-left: -90px; padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; border-bottom: 1px solid white; } a.reference { text-decoration: none; border-bottom: 1px dotted #004B6B; } a.reference:hover { border-bottom: 1px solid #6D4100; } a.footnote-reference { text-decoration: none; font-size: 0.7em; vertical-align: top; border-bottom: 1px dotted #004B6B; } a.footnote-reference:hover { border-bottom: 1px solid #6D4100; } a:hover tt { background: #EEE; } @media screen and (max-width: 870px) { div.sphinxsidebar { display: none; } div.document { width: 100%; } div.documentwrapper { margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0; } div.bodywrapper { margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; } ul { margin-left: 0; } .document { width: auto; } .footer { width: auto; } .bodywrapper { margin: 0; } .footer { width: auto; } .github { display: none; } } @media screen and (max-width: 875px) { body { margin: 0; padding: 20px 30px; } div.documentwrapper { float: none; background: white; } div.sphinxsidebar { display: block; float: none; width: 102.5%; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; color: white; } div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { color: white; } div.sphinxsidebar a { color: #aaa; } div.sphinxsidebar p.logo { display: none; } div.document { width: 100%; margin: 0; } div.related { display: block; margin: 0; padding: 10px 0 20px 0; } div.related ul, div.related ul li { margin: 0; padding: 0; } div.footer { display: none; } div.bodywrapper { margin: 0; } div.body { min-height: 0; padding: 0; } .rtd_doc_footer { display: none; } .document { width: auto; } .footer { width: auto; } .footer { width: auto; } .github { display: none; } } /* scrollbars */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-button:start:decrement, ::-webkit-scrollbar-button:end:increment { display: block; height: 10px; } ::-webkit-scrollbar-button:vertical:increment { background-color: #fff; } ::-webkit-scrollbar-track-piece { background-color: #eee; -webkit-border-radius: 3px; } ::-webkit-scrollbar-thumb:vertical { height: 50px; background-color: #ccc; -webkit-border-radius: 3px; } ::-webkit-scrollbar-thumb:horizontal { width: 50px; background-color: #ccc; -webkit-border-radius: 3px; } /* misc. */ .revsys-inline { display: none!important; }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193914.0 snimpy-1.0.0/docs/_themes/flask/theme.conf0000644000076400001440000000024400000000000017653 0ustar00bernatusers[theme] inherit = basic stylesheet = flasky.css pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px touch_icon = ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/docs/_themes/flask_small/0000755000076400001440000000000000000000000017072 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193914.0 snimpy-1.0.0/docs/_themes/flask_small/layout.html0000644000076400001440000000125300000000000021276 0ustar00bernatusers{% extends "basic/layout.html" %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {% block footer %} {% if pagename == 'index' %}
{% endif %} {% endblock %} {# do not display relbars #} {% block relbar1 %}{% endblock %} {% block relbar2 %} {% if theme_github_fork %} Fork me on GitHub {% endif %} {% endblock %} {% block sidebar1 %}{% endblock %} {% block sidebar2 %}{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8498616 snimpy-1.0.0/docs/_themes/flask_small/static/0000755000076400001440000000000000000000000020361 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193916.0 snimpy-1.0.0/docs/_themes/flask_small/static/flasky.css_t0000644000076400001440000001100100000000000022700 0ustar00bernatusers/* * flasky.css_t * ~~~~~~~~~~~~ * * Sphinx stylesheet -- flasky theme based on nature theme. * * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; background: white; margin: 0; padding: 0; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 40px auto 0 auto; width: 700px; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { text-align: right; color: #888; padding: 10px; font-size: 14px; width: 650px; margin: 0 auto 40px auto; } div.footer a { color: #888; text-decoration: underline; } div.related { line-height: 32px; color: #888; } div.related ul { padding: 0 0 0 10px; } div.related a { color: #444; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body { padding-bottom: 40px; /* saved for footer */ } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; } {% endif %} div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: white; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight{ background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.85em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td { padding: 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } pre { padding: 0; margin: 15px -30px; padding: 8px; line-height: 1.3em; padding: 7px 30px; background: #eee; border-radius: 2px; -moz-border-radius: 2px; -webkit-border-radius: 2px; } dl pre { margin-left: -60px; padding-left: 60px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; } a:hover tt { background: #EEE; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193914.0 snimpy-1.0.0/docs/_themes/flask_small/theme.conf0000644000076400001440000000027000000000000021042 0ustar00bernatusers[theme] inherit = basic stylesheet = flasky.css nosidebar = true pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px github_fork = '' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1391193872.0 snimpy-1.0.0/docs/_themes/flask_theme_support.py0000644000076400001440000001141300000000000021232 0ustar00bernatusers# flasky extensions. flasky pygments style based on tango style from pygments.style import Style from pygments.token import Keyword, Name, Comment, String, Error, \ Number, Operator, Generic, Whitespace, Punctuation, Other, Literal class FlaskyStyle(Style): background_color = "#f8f8f8" default_style = "" styles = { # No corresponding class for the following: #Text: "", # class: '' Whitespace: "underline #f8f8f8", # class: 'w' Error: "#a40000 border:#ef2929", # class: 'err' Other: "#000000", # class 'x' Comment: "italic #8f5902", # class: 'c' Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k' Keyword.Constant: "bold #004461", # class: 'kc' Keyword.Declaration: "bold #004461", # class: 'kd' Keyword.Namespace: "bold #004461", # class: 'kn' Keyword.Pseudo: "bold #004461", # class: 'kp' Keyword.Reserved: "bold #004461", # class: 'kr' Keyword.Type: "bold #004461", # class: 'kt' Operator: "#582800", # class: 'o' Operator.Word: "bold #004461", # class: 'ow' - like keywords Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. Name: "#000000", # class: 'n' Name.Attribute: "#c4a000", # class: 'na' - to be revised Name.Builtin: "#004461", # class: 'nb' Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Class: "#000000", # class: 'nc' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised Name.Decorator: "#888", # class: 'nd' - to be revised Name.Entity: "#ce5c00", # class: 'ni' Name.Exception: "bold #cc0000", # class: 'ne' Name.Function: "#000000", # class: 'nf' Name.Property: "#000000", # class: 'py' Name.Label: "#f57900", # class: 'nl' Name.Namespace: "#000000", # class: 'nn' - to be revised Name.Other: "#000000", # class: 'nx' Name.Tag: "bold #004461", # class: 'nt' - like a keyword Name.Variable: "#000000", # class: 'nv' - to be revised Name.Variable.Class: "#000000", # class: 'vc' - to be revised Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l' Literal.Date: "#000000", # class: 'ld' String: "#4e9a06", # class: 's' String.Backtick: "#4e9a06", # class: 'sb' String.Char: "#4e9a06", # class: 'sc' String.Doc: "italic #8f5902", # class: 'sd' - like a comment String.Double: "#4e9a06", # class: 's2' String.Escape: "#4e9a06", # class: 'se' String.Heredoc: "#4e9a06", # class: 'sh' String.Interpol: "#4e9a06", # class: 'si' String.Other: "#4e9a06", # class: 'sx' String.Regex: "#4e9a06", # class: 'sr' String.Single: "#4e9a06", # class: 's1' String.Symbol: "#4e9a06", # class: 'ss' Generic: "#000000", # class: 'g' Generic.Deleted: "#a40000", # class: 'gd' Generic.Emph: "italic #000000", # class: 'ge' Generic.Error: "#ef2929", # class: 'gr' Generic.Heading: "bold #000080", # class: 'gh' Generic.Inserted: "#00A000", # class: 'gi' Generic.Output: "#888", # class: 'go' Generic.Prompt: "#745334", # class: 'gp' Generic.Strong: "bold #000000", # class: 'gs' Generic.Subheading: "bold #800080", # class: 'gu' Generic.Traceback: "bold #a40000", # class: 'gt' } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/docs/api.rst0000644000076400001440000000143500000000000014464 0ustar00bernatusers============== API reference ============== While *Snimpy* is targeted at being used interactively or through simple scripts, you can also use it from your Python program. It provides a high-level interface as well as lower-level ones. However, the effort is only put in th :mod:`manager` module and other modules are considered as internal details. :mod:`manager` module ---------------------- .. automodule:: snimpy.manager :members: Manager, load Internal modules ---------------- Those modules shouldn't be used directly. :mod:`mib` module ~~~~~~~~~~~~~~~~~ .. automodule:: snimpy.mib :members: :mod:`snmp` module ~~~~~~~~~~~~~~~~~~ .. automodule:: snimpy.snmp :members: :mod:`basictypes` module ~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: snimpy.basictypes :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/docs/conf.py0000644000076400001440000000237600000000000014465 0ustar00bernatusers#!/usr/bin/env python import sys import os rtd = os.environ.get('READTHEDOCS', None) == 'True' cwd = os.getcwd() project_root = os.path.dirname(cwd) sys.path.insert(0, project_root) # -- Don't try to load CFFI (doesn't work on RTD) ----------------------------- if rtd: from mock import Mock sys.modules['cffi'] = Mock() sys.modules['cffi.verifier'] = Mock() import snimpy # -- General configuration ---------------------------------------------------- extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] templates_path = ['_templates'] source_suffix = '.rst' master_doc = 'index' # General information about the project. project = 'Snimpy' copyright = '2015, Vincent Bernat' version = snimpy.__version__ release = snimpy.__version__ exclude_patterns = ['_build'] pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'flask' html_static_path = ['_static'] html_use_modindex = False html_theme_options = { "index_logo": "snimpy.svg", "index_logo_height": "200px" } htmlhelp_basename = 'snimpydoc' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/docs/contributing.rst0000644000076400001440000000004000000000000016411 0ustar00bernatusers.. include:: ../CONTRIBUTING.rst././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/docs/history.rst0000644000076400001440000000003300000000000015405 0ustar00bernatusers.. include:: ../HISTORY.rst././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/docs/index.rst0000644000076400001440000000451300000000000015022 0ustar00bernatusersSnimpy: interactive SNMP tool ==================================================== *Snimpy* is a Python-based tool providing a simple interface to build SNMP query. Here is a very simplistic example that allows us to display the routing table of a given host:: load("IP-FORWARD-MIB") m=M("localhost", "public", 2) routes = m.ipCidrRouteNextHop for x in routes: net, netmask, tos, src = x print("%15s/%-15s via %-15s src %-15s" % (net, netmask, routes[x], src)) You can either use *Snimpy* interactively throught its console (derived from Python own console or from IPython_ if available) or write *Snimpy* scripts which are just Python scripts with some global variables available. .. _IPython: http://ipython.org Why another tool? ----------------- There are a lot of SNMP tools available but most of them have important drawback when you need to reliably automatize operations. `snmpget`, `snmpset` and `snmpwalk` are difficult to use in scripts. Errors are printed on standard output and there is no easy way to tell if the command was successful or not. Moreover, results can be multiline (a long HexString for example). At least, automatisation is done through the shell and OID or bit manipulation are quite difficult. Net-SNMP provides officiel bindings for Perl and Python. Unfortunately, the integration is quite poor. You don't have an easy way to load and browse MIBs and error handling is inexistant. For example, the Python bindings will return None for a non-existant OID. Having to check for this on each request is quite cumbersome. For Python, there are other bindings. For example, pysnmp_ provides a pure Python implementation. However, MIBs need to be compiled. Moreover, the exposed interface is still low-level. Sending a simple SNMP GET can either take 10 lines or one line wrapped into 10 lines. .. _pysnmp: http://pysnmp.sourceforge.net/ The two main points of *Snimpy* are: * very high-level interface relying on MIBs * raise exceptions when something goes wrong Meantime, another Pythonic SNMP library based on Net-SNMP has been released: `Easy SNMP`_. Its interface is a less Pythonic than *Snimpy* but it doesn't need MIBs to work. .. _Easy SNMP: https://github.com/fgimian/easysnmp Contents --------- .. toctree:: :maxdepth: 1 installation usage api contributing license history ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/docs/installation.rst0000644000076400001440000000202300000000000016406 0ustar00bernatusers============ Installation ============ At the command line:: $ easy_install snimpy Or, if you have virtualenvwrapper installed:: $ mkvirtualenv snimpy $ pip install snimpy *Snimpy* requires libsmi_, a library to access SMI MIB information. You need to install both the library and the development headers. If *Snimpy* complains to not find ``smi.h``, you can help by specifying where this file is located by exporting the appropriate environment variable:: $ export C_INCLUDE_PATH=/opt/local/include On Debian/Ubuntu, you can install libsmi with:: $ sudo apt-get install libffi-dev libsmi2-dev snmp-mibs-downloader On RedHat and similar, you can use:: $ sudo yum install libffi-devel libsmi-devel On OS X, if you are using homebrew_, you can use:: $ brew install libffi $ brew install libsmi .. _libsmi: http://www.ibr.cs.tu-bs.de/projects/libsmi/ .. _homebrew: http://brew.sh On Debian and Ubuntu, *Snimpy* is also available as a package you can install with:: $ sudo apt-get install snimpy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/docs/license.rst0000644000076400001440000000174500000000000015341 0ustar00bernatusers======== License ======== *Snimpy* is licensed under the ISC license. It basically means: do whatever you want with it as long as the copyright sticks around, the conditions are not modified and the disclaimer is present. .. include:: ../AUTHORS.rst ISC License ----------- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1617999279.0 snimpy-1.0.0/docs/usage.rst0000644000076400001440000001523600000000000015023 0ustar00bernatusers======== Usage ======== Invocation ---------- There are three ways to use *Snimpy*: 1. Interactively through a console. 2. As a script interpreter. 3. As a regular Python module. Interactive use +++++++++++++++ *Snimpy* can be invoked with either `snimpy` or `python -m snimpy`. Without any other arhument, the interactive console is spawned. Otherwise, the given script is executed and the remaining arguments are served as arguments for the script. When running interactively, you get a classic Python environment. There are two additional objects available: * The `load()` method that takes a MIB name or a path to a filename. The MIB will be loaded into memory and made available in all SNMP managers:: load("SNMPv2-MIB") load("/usr/share/mibs/ietf/IF-MIB") * The `M` class which is used to instantiate a manager (a SNMP client):: m = M() m = M(host="localhost", community="private", version=2) m = M("localhost", "private", 2) m = M(community="private") m = M(version=3, secname="readonly", authprotocol="MD5", authpassword="authpass", privprotocol="AES", privpassword="privpass") A manager instance contains all the scalars and the columns in MIB loaded with the `load()` method. There is no table, node or other entities. For a scalar, getting and setting a value is a simple as:: print(m.sysDescr) m.sysName = "newhostname" For a column, you get a dictionary-like interface:: for index in m.ifTable: print(repr(m.ifDescr[index])) m.ifAdminStatus[3] = "down" If you care about efficiency, note that the above snippet will walk the table twice: once to retrieve the index to loop over and once to retrieve the values. This could be avoided with:: for index, value in m.ifDescr.iteritems(): print(repr(value)) Furthermore, you can pass partial index values to `iteritems()` to limit walked table rows to a specific subset:: for index, value in m.ipNetToMediaPhysAddress.iteritems(10): print(repr(value)) If you don't need values you can use subscript syntax for this as well:: for index in m.ipNetToMediaPhysAddress[10]: print(repr(index)) Another way to avoid those extra SNMP requests is to enable the caching mechanism which is disabled by default:: import time m = M("localhost", cache=True) print(m.sysUpTime) time.sleep(1) print(m.sysUpTime) time.sleep(1) print(m.sysUpTime) time.sleep(10) print(m.sysUpTime) You can also specify the number of seconds data should be cached:: m = M("localhost", cache=20) Also note that iterating over a table require an accessible index. Old MIB usually have accessible indexes. If this is not the case, you'll have to iterate on a column instead. For example, the first example could be written as:: for index in m.ifDescr: print(repr(m.ifDescr[index])) If you want to group several write into a single request, you can do it with `with` keyword:: with M("localhost", "private") as m: m.sysName = "toto" m.ifAdminStatus[20] = "down" It's also possible to set a custom timeout and a custom value for the number of retries. For example, to wait 2.5 seconds before timeout occurs and retry 10 times, you can use:: m = M("localhost", timeout=2.5, retries=10) *Snimpy* will stop on any error with an exception. This allows you to not check the result at each step. Your script can't go awry. If this behaviour does not suit you, it is possible to suppress exceptions when querying inexistant objects. Instead of an exception, you'll get `None`:: m = M("localhost", none=True) If for some reason, you need to specify the module you want to use to lookup a node, you can do that using the following syntax:: print(m['SNMPv2-MIB'].sysDescr) print(m['IF-MIB'].ifNumber) Script interpreter ++++++++++++++++++ *Snimpy* can be run as a script interpreter. There are two ways to do this. The first one is to invoke *Snimpy* and provide a script name as well as any argument you want to pass to the script:: $ snimpy example-script.py arg1 arg2 $ python -m snimpy example-script.py arg1 arg2 The second one is to use *Snimpy* as a shebang_ interpreter. For example, here is a simple script:: #!/usr/bin/env snimpy load("IF-MIB") m = M("localhost") print(m.ifDescr[0]) The script can be invoked as any shell script. .. _shebang: http://en.wikipedia.org/wiki/Shebang_(Unix) Inside the script, you can use any valid Python code. You also get the `load()` method and the `M` class available, like for the interactive use. Regular Python module +++++++++++++++++++++ *Snimpy* can also be imported as a regular Python module:: from snimpy.manager import Manager as M from snimpy.manager import load load("IF-MIB") m = M("localhost") print(m.ifDescr[0]) About "major SMI errors" ------------------------ If you get an exception like `RAPID-CITY contains major SMI errors (check with smilint -s -l1)`, this means that there are some grave errors in this MIB which may lead to segfaults if the MIB is used as is. Usually, this means that some identifier are unknown. Use `smilint -s -l1 YOUR-MIB` to see what the problem is and try to solve all problems reported by lines beginning by `[1]`. For example:: $ smilint -s -l1 rapid_city.mib rapid_city.mib:30: [1] failed to locate MIB module `IGMP-MIB' rapid_city.mib:32: [1] failed to locate MIB module `DVMRP-MIB' rapid_city.mib:34: [1] failed to locate MIB module `IGMP-MIB' rapid_city.mib:27842: [1] unknown object identifier label `igmpInterfaceIfIndex' rapid_city.mib:27843: [1] unknown object identifier label `igmpInterfaceQuerier' rapid_city.mib:27876: [1] unknown object identifier label `dvmrpInterfaceIfIndex' rapid_city.mib:27877: [1] unknown object identifier label `dvmrpInterfaceOperState' rapid_city.mib:27894: [1] unknown object identifier label `dvmrpNeighborIfIndex' rapid_city.mib:27895: [1] unknown object identifier label `dvmrpNeighborAddress' rapid_city.mib:32858: [1] unknown object identifier label `igmpCacheAddress' rapid_city.mib:32858: [1] unknown object identifier label `igmpCacheIfIndex' To solve the problem here, load `IGMP-MIB` and `DVMRP-MIB` before loading `rapid_city.mib`. `IGMP-MIB` should be pretty easy to find. For `DVMRP-MIB`, try Google. Download it and use `smistrip` to get the MIB. You can check that the problem is solved with this command:: $ smilint -p ../cisco/IGMP-MIB.my -p ./DVMRP-MIB -s -l1 rapid_city.mib You will get a lot of errors in `IGMP-MIB` and `DVMRP-MIB` but no line with `[1]`: everything should be fine. To load `rapid_city.mib`, you need to do this:: load("../cisco/IGMP-MIB.my") load("./DVMRP-MIB") load("rapid_city.mib") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8538616 snimpy-1.0.0/examples/0000755000076400001440000000000000000000000014044 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/add-vlan.py0000644000076400001440000000232200000000000016103 0ustar00bernatusers#!/usr/bin/snimpy """ On Nortel switches, create a new VLAN and tag it on "TagAll" ports. """ import os import sys load("SNMPv2-MIB") load(os.path.expanduser("~/.snmp/mibs/RAPID-CITY-MIB")) load(os.path.expanduser("~/.snmp/mibs/RC-VLAN-MIB")) vlanNumber = int(sys.argv[3]) vlanName = sys.argv[4] s = M(host=sys.argv[1], community=sys.argv[2]) # Create the VLAN if vlanNumber not in s.rcVlanId: print("VLAN %d will be created with name %s on %s" % (vlanNumber, vlanName, sys.argv[1])) with s: s.rcVlanRowStatus[vlanNumber] = "createAndGo" s.rcVlanName[vlanNumber] = vlanName s.rcVlanType[vlanNumber] = "byPort" else: print("VLAN %d already exists on %s" % (vlanNumber, sys.argv[1])) # Just set the name if s.rcVlanName[vlanNumber] != vlanName: s.rcVlanName[vlanNumber] = vlanName # Which ports are tagall ? tagged = [port for port in s.rcVlanPortPerformTagging if s.rcVlanPortPerformTagging[port] ] if len(tagged) != 2 and len(tagged) != 3: print("{} does not have exactly two or three tagged ports ({!r})".format(sys.argv[1], tagged)) sys.exit(1) print("VLAN %d will be tagged on ports %s" % (vlanNumber, tagged)) s.rcVlanStaticMembers[vlanNumber] |= tagged ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/disable-port-ingress-filtering.py0000644000076400001440000000102300000000000022430 0ustar00bernatusers#!/usr/bin/snimpy """Disable port ingress filtering on Nortel switch (also known as filter-unregistered-frames).""" import sys load("SNMPv2-MIB") load("Q-BRIDGE-MIB") s = M(host=sys.argv[1], community=sys.argv[2]) if "Ethernet Routing Switch 55" not in s.sysDescr: print("Not a 5510") sys.exit(1) for id in s.dot1qPortIngressFiltering: if s.dot1qPortIngressFiltering[id]: print("Filtering on port %d of %s is not disabled, disable it." % (id, sys.argv[1])) s.dot1qPortIngressFiltering[id] = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/enable-lldp.py0000644000076400001440000000335100000000000016577 0ustar00bernatusers#!/usr/bin/snimpy """Enable LLDP. Generic procedure but we restrict ourself to Nortel 55x0. """ import sys import os load("SNMPv2-MIB") for l in ["LLDP", "LLDP-EXT-DOT3", "LLDP-EXT-DOT1"]: load(os.path.expanduser("~/.snmp/mibs/%s-MIB" % l)) s = M(host=sys.argv[1], community=sys.argv[2]) try: type = s.sysDescr except snmp.SNMPException: print("Cannot process %s: bad community?" % sys.argv[1]) sys.exit(1) if not type.startswith(("Ethernet Routing Switch 55", "Ethernet Switch 425")): print("Not a 55x0: %s" % type) sys.exit(1) print("Processing %s..." % sys.argv[1]) try: for oid in s.lldpConfigManAddrPortsTxEnable: if oid[0] == "ipV4": s.lldpConfigManAddrPortsTxEnable[oid] = "\xff"*10 except snmp.SNMPNoSuchObject: print("No LLDP for this switch") sys.exit(2) dot3 = True for port in s.lldpPortConfigAdminStatus: s.lldpPortConfigAdminStatus[port] = "txAndRx" s.lldpPortConfigTLVsTxEnable[port] = ["portDesc", "sysName", "sysDesc", "sysCap" ] # Dot3 try: if dot3: s.lldpXdot3PortConfigTLVsTxEnable[port] = ["macPhyConfigStatus", "powerViaMDI", "linkAggregation", "maxFrameSize"] except snmp.SNMPException: print("No Dot3") dot3 = False # Dot1 try: for port,vlan in s.lldpXdot1ConfigVlanNameTxEnable: s.lldpXdot1ConfigVlanNameTxEnable[port, vlan] = True except snmp.SNMPException: print("No Dot1") print("Success!") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/get-serial.py0000644000076400001440000000161500000000000016455 0ustar00bernatusers#!/usr/bin/snimpy """ Get serial number of a given equipment using ENTITY-MIB """ import sys load("ENTITY-MIB") host=sys.argv[1] s = M(host=host, community=sys.argv[2]) # Locate parent of all other elements print("[-] %s: Search for parent element" % host) parent = None for i in s.entPhysicalContainedIn: if s.entPhysicalContainedIn[i] == 0: parent = i break if parent is None: print("[!] %s: Unable to find parent" % host) sys.exit(1) print("[+] {}: {}".format(host, s.entPhysicalDescr[parent])) print("[+] {}: HW {}, FW {}, SW {}".format(host, s.entPhysicalHardwareRev[parent], s.entPhysicalFirmwareRev[parent], s.entPhysicalSoftwareRev[parent])) print("[+] {}: SN {}".format(host, s.entPhysicalSerialNum[parent])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/list-interfaces.py0000644000076400001440000000016200000000000017511 0ustar00bernatusers#!/usr/bin/snimpy load("IF-MIB") m=M() for i in m.ifDescr: print("Interface %3d: %s" % (i, m.ifDescr[i])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/list-routes.py0000644000076400001440000000115400000000000016711 0ustar00bernatusers#!/usr/bin/snimpy from socket import inet_ntoa load("IP-FORWARD-MIB") m=M() print("Using IP-FORWARD-MIB::ipCidrRouteTable...") routes = m.ipCidrRouteNextHop for x in routes: net, netmask, tos, src = x print("{:>15}/{:<15} via {:<15} src {:<15}".format(net, netmask, routes[x], src)) print print("Using IP-FORWARD-MIB::inetCidrRouteTable...") routes = m.inetCidrRouteIfIndex for x in routes: dsttype, dst, prefix, oid, nhtype, nh = x if dsttype != "ipv4" or nhtype != "ipv4": print("Non-IPv4 route") continue print("%15s/%-2d via %-15s" % (inet_ntoa(dst), prefix, inet_ntoa(nh))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/rename-vlan.py0000644000076400001440000000111100000000000016615 0ustar00bernatusers#!/usr/bin/snimpy import os import sys load("SNMPv2-MIB") load(os.path.expanduser("~/.snmp/mibs/RAPID-CITY-MIB")) load(os.path.expanduser("~/.snmp/mibs/RC-VLAN-MIB")) vlanNumber = int(sys.argv[3]) newName = sys.argv[4] s = M(host=sys.argv[1], community=sys.argv[2]) try: cur = s.rcVlanName[vlanNumber] except snmp.SNMPException: print("%s is not a Nortel switch or does not have VLAN %d" % (sys.argv[1], vlanNumber)) sys.exit(1) if cur != newName: s.rcVlanName[vlanNumber] = newName print("Setting VLAN %d of %s as %s: done." % (vlanNumber, sys.argv[1], newName)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/set-syslog-ntp.py0000755000076400001440000000420000000000000017325 0ustar00bernatusers#!/usr/bin/snimpy """ Set NTP or syslog server on various switches Usage: ./set-syslog-ntp.py [syslog|ntp] host community first [...] """ import os import sys load("SNMPv2-MIB") host = sys.argv[2] targets = sys.argv[4:] operation = sys.argv[1] try: s = M(host=host, community=sys.argv[3]) sid = str(s.sysObjectID) except snmp.SNMPException, e: print("%s: %s" % (host, e)) sys.exit(1) if sid.startswith("1.3.6.1.4.1.45.3."): # Nortel print("%s is Nortel 55xx" % host) load(os.path.expanduser("~/.snmp/mibs/SYNOPTICS-ROOT-MIB")) load(os.path.expanduser("~/.snmp/mibs/S5-ROOT-MIB")) if operation == "ntp": load(os.path.expanduser("~/.snmp/mibs/S5-AGENT-MIB")) s.s5AgSntpPrimaryServerAddress = targets[0] if len(targets) > 1: s.s5AgSntpSecondaryServerAddress = targets[1] else: s.s5AgSntpSecondaryServerAddress = "0.0.0.0" s.s5AgSntpState = "unicast" s.s5AgSntpManualSyncRequest = "requestSync" elif operation == "syslog": load(os.path.expanduser("~/.snmp/mibs/BN-LOG-MESSAGE-MIB")) s.bnLogMsgRemoteSyslogAddress = targets[0] s.bnLogMsgRemoteSyslogSaveTargets = "msgTypeInformational" s.bnLogMsgRemoteSyslogEnabled = True elif sid.startswith("1.3.6.1.4.1.1872."): print("%s is Alteon" % host) load(os.path.expanduser("~/.snmp/mibs/ALTEON-ROOT-MIB")) if operation == "ntp": s.agNewCfgNTPServer = targets[0] if len(targets) > 1: s.agNewCfgNTPSecServer = targets[1] else: s.agNewCfgNTPSecServer = "0.0.0.0" s.agNewCfgNTPService = "enabled" elif operation == "syslog": s.agNewCfgSyslogHost = targets[0] s.agNewCfgSyslogFac = "local2" if len(targets) > 1: s.agNewCfgSyslog2Host = targets[1] s.agNewCfgSyslog2Fac = "local2" else: s.agNewCfgSyslog2Host = "0.0.0.0" if s.agApplyPending == "applyNeeded": if s.agApplyConfig == "complete": s.agApplyConfig = "idle" s.agApplyConfig = "apply" else: print("%s is unknown (%s)" % (host, s.sysDescr)) sys.exit(1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/examples/vlan-and-interfaces.py0000644000076400001440000000116600000000000020243 0ustar00bernatusers#!/usr/bin/snimpy """ On Nortel switches, list vlan on all active ports """ import os import sys load("SNMPv2-MIB") load("IF-MIB") load(os.path.expanduser("~/.snmp/mibs/RAPID-CITY-MIB")) load(os.path.expanduser("~/.snmp/mibs/RC-VLAN-MIB")) s = M(host=sys.argv[1], community=sys.argv[2]) vlans = {} for interface in s.ifIndex: if s.ifOperStatus[interface] == "up": vlans[int(interface)] = [] for vlan in s.rcVlanId: for interface in vlans: if s.rcVlanStaticMembers[vlan] & interface: vlans[interface].append("{}({})".format(vlan, s.rcVlanName[vlan])) import pprint pprint.pprint(vlans) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8538616 snimpy-1.0.0/man/0000755000076400001440000000000000000000000013001 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/man/snimpy.10000644000076400001440000000176700000000000014415 0ustar00bernatusers.TH SNIMPY 1 "Oct 4, 2008" .SH NAME snimpy \- interactive SNMP tool with Python .SH SYNOPSIS .B snimpy .RI [ options ] .SH DESCRIPTION This manual page documents briefly the .B snimpy command. .PP \fBsnimpy\fP is a Python-based tool providing a simple interface to build SNMP queries. This interface aims at being the most Pythonic possible: you grab scalars using attributes and columns are like dictionaries. .PP \fBsnimpy\fP can be used either interactively through its console (derived from Python own console or from IPython if available) or by writing \fBsnimpy\fP scripts which are just Python scripts with some global variables available. .SH OPTIONS \fBsnimpy\fP does not take any option. If you launch it without any argument, you will get the interactive console. Otherwise, the first argument is the name of a script to be executed and the remaining arguments are the arguments for this script. .SH SEE ALSO .nf https://github.com/vincentbernat/snimpy .fi .SH AUTHOR Vincent Bernat ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8538616 snimpy-1.0.0/setup.cfg0000644000076400001440000000004600000000000014047 0ustar00bernatusers[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588509428.0 snimpy-1.0.0/setup.py0000644000076400001440000000467300000000000013752 0ustar00bernatusersimport os import sys from setuptools import setup from setuptools.command.test import test import snimpy rtd = os.environ.get('READTHEDOCS', None) == 'True' class SnimpyTestCommand(test): def run_tests(self, *args, **kwds): # Ensure we keep a reference to multiprocessing and pysnmp to # avoid errors at the end of the test import multiprocessing import pysnmp SnimpyTestCommand.multiprocessing = multiprocessing SnimpyTestCommand.pysnmp = pysnmp return test.run_tests(self, *args, **kwds) if __name__ == "__main__": readme = open('README.rst').read() history = open('HISTORY.rst').read().replace('.. :changelog:', '') setup(name="snimpy", classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: ISC License (ISCL)', 'Operating System :: POSIX', 'Programming Language :: Python :: 3', 'Topic :: System :: Networking', 'Topic :: Utilities', 'Topic :: System :: Monitoring' ], url='https://github.com/vincentbernat/snimpy', description=snimpy.__doc__, long_description=readme + '\n\n' + history, long_description_content_type='text/x-rst', author=snimpy.__author__, author_email=snimpy.__email__, packages=["snimpy"], entry_points={ 'console_scripts': [ 'snimpy = snimpy.main:interact', ], }, data_files=[('share/man/man1', ['man/snimpy.1'])], zip_safe=False, cffi_modules=(not rtd and ["snimpy/smi_build.py:ffi"] or []), install_requires=["cffi >= 1.0.0", "pysnmp >= 4", "setuptools"], setup_requires=["cffi >= 1.0.0", "vcversioner"], tests_require=list(filter(None, ["cffi >= 1.0.0", "pysnmp >= 4", "nose", sys.version_info < (3, 6) and "mock < 4" or "mock"])), test_suite="nose.collector", cmdclass={ "test": SnimpyTestCommand }, pbr=False, vcversioner={ 'version_module_paths': ['snimpy/_version.py'], }, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8538616 snimpy-1.0.0/snimpy/0000755000076400001440000000000000000000000013545 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1539288139.0 snimpy-1.0.0/snimpy/__init__.py0000644000076400001440000000030500000000000015654 0ustar00bernatusers"""interactive SNMP tool""" __author__ = 'Vincent Bernat' __email__ = 'bernat@luffy.cx' try: from snimpy._version import __version__ # nopep8 except ImportError: __version__ = '0.0~dev' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1539288139.0 snimpy-1.0.0/snimpy/__main__.py0000644000076400001440000000161100000000000015636 0ustar00bernatusers# # snimpy -- Interactive SNMP tool # # Copyright (C) Vincent Bernat # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # if __name__ == "__main__": # pragma: no cover from snimpy import main main.interact() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302682.0 snimpy-1.0.0/snimpy/_version.py0000644000076400001440000000020400000000000015737 0ustar00bernatusers # This file is automatically generated by setup.py. __version__ = '1.0.0' __sha__ = 'gc72a878a653d' __revision__ = 'gc72a878a653d' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/snimpy/basictypes.py0000644000076400001440000007222300000000000016273 0ustar00bernatusers# # snimpy -- Interactive SNMP tool # # Copyright (C) Vincent Bernat # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # """ This module is aimed at providing Pythonic representation of various SNMP types. Each SMIv2 type is mapped to a corresponding class which tries to mimic a basic type from Python. For example, display strings are like Python string while SMIv2 integers are just like Python integers. This module is some kind of a hack and its use outside of *Snimpy* seems convoluted. """ import struct import re import ipaddress from datetime import timedelta from pysnmp.proto import rfc1902 from snimpy import mib def ordering_with_cmp(cls): ops = {'__lt__': lambda self, other: self.__cmp__(other) < 0, '__gt__': lambda self, other: self.__cmp__(other) > 0, '__le__': lambda self, other: self.__cmp__(other) <= 0, '__ge__': lambda self, other: self.__cmp__(other) >= 0, '__eq__': lambda self, other: self.__cmp__(other) == 0, '__ne__': lambda self, other: self.__cmp__(other) != 0} for opname, opfunc in ops.items(): opfunc.__name__ = opname opfunc.__doc__ = getattr(int, opname).__doc__ setattr(cls, opname, opfunc) return cls class Type: """Base class for all types.""" def __new__(cls, entity, value, raw=True): """Create a new typed value. :param entity: A :class:`mib.Node` instance :param value: The value to set :param raw: Whetever the raw value is provided (as opposed to a user-supplied value). This parameter is important when the provided input is ambiguous, for example when it is an array of bytes. :type raw: bool :return: an instance of the new typed value """ if entity.type != cls: raise ValueError("MIB node is {}. We are {}".format(entity.type, cls)) if cls == OctetString and entity.fmt is not None: # Promotion of OctetString to String if we have unicode stuff if isinstance(value, (String, str)) or not raw: cls = String if not isinstance(value, Type): value = cls._internal(entity, value) else: value = cls._internal(entity, value._value) if issubclass(cls, str): self = str.__new__(cls, value) elif issubclass(cls, bytes): self = bytes.__new__(cls, value) elif issubclass(cls, int): self = int.__new__(cls, value) else: self = object.__new__(cls) self._value = value self.entity = entity if cls == OctetString and entity.fmt is not None: # A display-hint propose to use only ascii and UTF-8 # chars. We promote an OCTET-STRING to a DisplayString if # we have a format. This means we won't be able to access # individual bytes in this format, only the full displayed # version. value = String._internal(entity, self) self = str.__new__(String, value) self._value = value self.entity = entity if isinstance(self, String): # Ensure that strings follow their format, if it is applied. # This is safer and simpler than toOid, as it does not do # additional validation. self._toBytes() return self def __init__(self, *args, **kwargs): # Neutralize __init__ from other inherited classes pass @classmethod def _internal(cls, entity, value): """Get internal value for a given value.""" raise NotImplementedError # pragma: no cover def pack(self): """Prepare the instance to be sent on the wire.""" raise NotImplementedError # pragma: no cover def toOid(self, implied=False): """Convert to an OID. If this function is implemented, then class function :meth:`fromOid` should also be implemented as the "invert" function of this one. This function only works if the entity is used as an index! Otherwise, it should raises NotImplementedError. :return: An OID that can be used as index """ raise NotImplementedError # pragma: no cover @classmethod def fromOid(cls, entity, oid, implied=False): """Create instance from an OID. This is the sister function of :meth:`toOid`. :param oid: The OID to use to create an instance :param entity: The MIB entity we want to instantiate :return: A couple `(l, v)` with `l` the number of suboids needed to create the instance and `v` the instance created from the OID """ raise NotImplementedError # pragma: no cover @classmethod def _fixedLen(cls, entity): """Determine if the given entity is fixed-len This function is a helper that is used for String and Oid. When converting a variable-length type to an OID, we need to prefix it by its len or not depending of what the MIB say. Node that the type can be used in an index with IMPLIED keyword. In that case, even when this function returns False, the OID will not be prefixed by its length. :param entity: entity to check :return: `True` if it is fixed-len, `False` otherwise """ if entity.ranges and not isinstance(entity.ranges, (tuple, list)): # Fixed length return True else: # Variable length return False def __str__(self): return str(self._value) def __repr__(self): return '<{}: {}>'.format(self.__class__.__name__, str(self)) @ordering_with_cmp class IpAddress(Type, ipaddress.IPv4Address): """Class representing an IP address/""" @classmethod def _internal(cls, entity, value): if isinstance(value, (list, tuple)): value = ".".join([str(a) for a in value]) try: value = ipaddress.IPv4Address(value) except ipaddress.AddressValueError: raise ValueError("{!r} is not a valid IP".format(value)) return value def pack(self): return rfc1902.IpAddress(str(self._value)) def toOid(self, implied=False): return tuple(self._value.packed) @classmethod def fromOid(cls, entity, oid, implied=False): if len(oid) < 4: raise ValueError( "{!r} is too short for an IP address".format(oid)) return (4, cls(entity, oid[:4])) def __cmp__(self, other): if not isinstance(other, IpAddress): try: other = IpAddress(self.entity, other) except Exception: raise NotImplementedError # pragma: no cover if self._value == other._value: return 0 if self._value < other._value: return -1 return 1 def __getitem__(self, nb): return self._value.packed[nb] class StringOrOctetString(Type): def toOid(self, implied=False): # To convert properly to OID, we need to know if it is a # fixed-len string, an implied string or a variable-len # string. b = self._toBytes() if implied or self._fixedLen(self.entity): return tuple(b) else: return (len(b),) + tuple(b) def _toBytes(self): raise NotImplementedError @classmethod def fromOid(cls, entity, oid, implied=False): oid = tuple(o & 0xff for o in oid) if implied: # Eat everything return (len(oid), cls(entity, bytes(oid))) if cls._fixedLen(entity): length = entity.ranges if len(oid) < length: raise ValueError( "{} is too short for wanted fixed " "string (need at least {:d})".format(oid, length)) return (length, cls(entity, bytes(oid[:length]))) # This is var-len if not oid: raise ValueError("empty OID while waiting for var-len string") length = oid[0] if len(oid) < length + 1: raise ValueError( "{} is too short for variable-len " "string (need at least {:d})".format(oid, length)) return ( (length + 1, cls(entity, bytes(oid[1:(length + 1)])))) def pack(self): return rfc1902.OctetString(self._toBytes()) class OctetString(StringOrOctetString, bytes): """Class for a generic octet string. This class should be compared to :class:`String` which is used to represent a display string. This class is usually used to store raw bytes, like a bitmask of VLANs. """ @classmethod def _internal(cls, entity, value): # Internally, we are using bytes if isinstance(value, bytes): return value if isinstance(value, str): return value.encode("ascii") return bytes(value) def _toBytes(self): return self._value def __ior__(self, value): nvalue = bytearray(self._value) if not isinstance(value, (tuple, list)): value = [value] for v in value: if not isinstance(v, int): raise NotImplementedError( "on string, bit-operation are limited to integers") if len(nvalue) < (v >> 3) + 1: nvalue.extend([0] * ((v >> 3) + 1 - len(self._value))) nvalue[v >> 3] |= 1 << (7 - v % 8) return self.__class__(self.entity, bytes(nvalue)) def __isub__(self, value): nvalue = bytearray(self._value) if not isinstance(value, (tuple, list)): value = [value] for v in value: if not isinstance(v, int): raise NotImplementedError( "on string, bit-operation are limited to integers") if len(nvalue) < (v >> 3) + 1: continue nvalue[v >> 3] &= ~(1 << (7 - v % 8)) return self.__class__(self.entity, bytes(nvalue)) return self def __and__(self, value): nvalue = bytearray(self._value) if not isinstance(value, (tuple, list)): value = [value] for v in value: if not isinstance(v, int): raise NotImplementedError( "on string, bit-operation are limited to integers") if len(nvalue) < (v >> 3) + 1: return False if not(nvalue[v >> 3] & (1 << (7 - v % 8))): return False return True class String(StringOrOctetString, str): """Class for a display string. Such a string is an unicode string and it is therefore expected that only printable characters are used. This is usually the case if the corresponding MIB node comes with a format string. With such an instance, the user is expected to be able to provide a formatted. For example, a MAC address could be written `00:11:22:33:44:55`. """ @classmethod def _parseOctetFormat(cls, fmt, j): # repeater if fmt[j] == "*": dorepeat = True j += 1 else: dorepeat = False # length length = "" while fmt[j].isdigit(): length += fmt[j] j += 1 length = int(length) # format format = fmt[j] j += 1 # seperator if j < len(fmt) and \ fmt[j] != "*" and not fmt[j].isdigit(): sep = fmt[j] j += 1 else: sep = "" # terminator if j < len(fmt) and \ fmt[j] != "*" and not fmt[j].isdigit(): term = fmt[j] j += 1 else: term = "" return (j, dorepeat, length, format, sep, term) @classmethod def _fromBytes(cls, value, fmt): i = 0 # Position in value j = 0 # Position in fmt result = "" term = None sep = None while i < len(value): if j < len(fmt): j, dorepeat, length, format, sep, term = cls._parseOctetFormat( fmt, j) # building if dorepeat: repeat = value[i] i += 1 else: repeat = 1 for r in range(repeat): bb = value[i:i + length] i += length if format in ['o', 'x', 'd']: if length > 4: raise ValueError( "don't know how to handle integers " "more than 4 bytes long") bb = b"\x00" * (4 - length) + bb number = struct.unpack(b"!l", bb)[0] if format == "o": # In Python2, oct() is 01242, while it is 0o1242 in # Python3 result += "".join(oct(number).partition("o")[0:3:2]) elif format == "x": result += hex(number)[2:] else: # format == "d": result += str(number) elif format == "a": result += bb.decode("ascii") elif format == "t": result += bb.decode("utf-8") else: raise ValueError("{!r} cannot be represented with " "the given display string ({})".format( bb, fmt)) result += sep if sep and term: result = result[:-1] result += term if term or sep: result = result[:-1] return result def _toBytes(self): # We need to reverse what was done by `_fromBytes`. This is # not an exact science. In most case, this is easy because a # separator is used but sometimes, this is not. We do some # black magic that will fail. i = 0 j = 0 fmt = self.entity.fmt bb = b"" while i < len(self._value): if j < len(fmt): parsed = self._parseOctetFormat(fmt, j) j, dorepeat, length, format, sep, term = parsed if format == "o": fmatch = "(?P[0-7]{{1,{0}}})".format( int(length * 2.66667) + 1) elif format == "x": fmatch = "(?P[0-9A-Fa-f]{{1,{0}}})".format(length * 2) elif format == "d": fmatch = "(?P[0-9]{{1,{0}}})".format( int(length * 2.4083) + 1) elif format == "a": fmatch = "(?P(?:.|\n){{1,{0}}})".format(length) elif format == "t": fmatch = "(?P(?:.|\n){{1,{0}}})".format(length) else: raise ValueError("{!r} cannot be parsed due to an " "incorrect format ({})".format( self._value, fmt)) repeats = [] while True: mo = re.match(fmatch, self._value[i:]) if not mo: raise ValueError("{!r} cannot be parsed because it " "does not match format {} at " "index {}".format(self._value, fmt, i)) if format in ["o", "x", "d"]: if format == "o": r = int(mo.group("o"), 8) elif format == "x": r = int(mo.group("x"), 16) else: r = int(mo.group("d")) result = struct.pack(b"!l", r)[-length:] else: result = mo.group(1).encode("utf-8") i += len(mo.group(1)) if dorepeat: repeats.append(result) if i < len(self._value): # Approximate... if sep and self._value[i] == sep: i += 1 elif term and self._value[i] == term: i += 1 break else: break else: break if dorepeat: bb += bytes([len(repeats)]) bb += b"".join(repeats) else: bb += result if i < len(self._value) and (sep and self._value[i] == sep or term and self._value[i] == term): i += 1 return bb @classmethod def _internal(cls, entity, value): # Internally, we use the displayed string. We have a special # case if the value is an OctetString to do the conversion. if isinstance(value, OctetString): return cls._fromBytes(value._value, entity.fmt) if isinstance(value, bytes): return value.decode("utf-8") return str(value) def __str__(self): return self._value class Integer(Type, int): """Class for any integer.""" @classmethod def _internal(cls, entity, value): return int(value) def pack(self): if self._value >= (1 << 64): raise OverflowError("too large to be packed") if self._value >= (1 << 32): return rfc1902.Counter64(self._value) if self._value >= 0: return rfc1902.Integer(self._value) if self._value >= -(1 << 31): return rfc1902.Integer(self._value) raise OverflowError("too small to be packed") def toOid(self, implied=False): return (self._value,) @classmethod def fromOid(cls, entity, oid, implied=False): if len(oid) < 1: raise ValueError("{} is too short for an integer".format(oid)) return (1, cls(entity, oid[0])) def __str__(self): if self.entity.fmt: if self.entity.fmt[0] == "x": return hex(self._value) if self.entity.fmt[0] == "o": return oct(self._value) if self.entity.fmt[0] == "b": if self._value == 0: return "0" if self._value > 0: v = self._value r = "" while v > 0: r = str(v % 2) + r v = v >> 1 return r elif self.entity.fmt[0] == "d" and \ len(self.entity.fmt) > 2 and \ self.entity.fmt[1] == "-": dec = int(self.entity.fmt[2:]) result = str(self._value) if len(result) < dec + 1: result = "0" * (dec + 1 - len(result)) + result return "{}.{}".format(result[:-2], result[-2:]) return str(self._value) class Unsigned32(Integer): """Class to represent an unsigned 32bits integer.""" def pack(self): if self._value >= (1 << 32): raise OverflowError("too large to be packed") if self._value < 0: raise OverflowError("too small to be packed") return rfc1902.Unsigned32(self._value) class Unsigned64(Integer): """Class to represent an unsigned 64bits integer.""" def pack(self): if self._value >= (1 << 64): raise OverflowError("too large to be packed") if self._value < 0: raise OverflowError("too small to be packed") return rfc1902.Counter64(self._value) class Enum(Integer): """Class for an enumeration. An enumaration is an integer but labels are attached to some values for a more user-friendly display.""" @classmethod def _internal(cls, entity, value): if value in entity.enum: return value for (k, v) in entity.enum.items(): if (v == value): return k try: return int(value) except Exception: raise ValueError("{!r} is not a valid " "value for {}".format(value, entity)) def pack(self): return rfc1902.Integer(self._value) @classmethod def fromOid(cls, entity, oid, implied=False): if len(oid) < 1: raise ValueError( "{!r} is too short for an enumeration".format(oid)) return (1, cls(entity, oid[0])) def __eq__(self, other): if not isinstance(other, self.__class__): try: other = self.__class__(self.entity, other) except Exception: raise NotImplementedError # pragma: no cover return self._value == other._value def __ne__(self, other): return not(self.__eq__(other)) def __str__(self): if self._value in self.entity.enum: return ( "{}({:d})".format(self.entity.enum[self._value], self._value) ) else: return str(self._value) @ordering_with_cmp class Oid(Type): """Class to represent and OID.""" @classmethod def _internal(cls, entity, value): if isinstance(value, (list, tuple)): return tuple(int(v) for v in value) elif isinstance(value, str): return tuple(int(i) for i in value.split(".") if i) elif isinstance(value, mib.Node): return tuple(value.oid) else: raise TypeError( "don't know how to convert {!r} to OID".format(value)) def pack(self): return rfc1902.univ.ObjectIdentifier(self._value) def toOid(self, implied=False): if implied or self._fixedLen(self.entity): return self._value else: return tuple([len(self._value)] + list(self._value)) @classmethod def fromOid(cls, entity, oid, implied=False): if cls._fixedLen(entity): # A fixed OID? We don't like this. Provide a real example. raise ValueError( "{!r} seems to be a fixed-len OID index. Odd.".format(entity)) if not implied: # This index is not implied. We need the len if len(oid) < 1: raise ValueError( "{!r} is too short for a not " "implied index".format(entity)) length = oid[0] if len(oid) < length + 1: raise ValueError( "{!r} has an incorrect size " "(needs at least {:d})".format(oid, length)) return (length + 1, cls(entity, oid[1:(length + 1)])) else: # This index is implied. Eat everything return (len(oid), cls(entity, oid)) def __str__(self): return ".".join([str(x) for x in self._value]) def __cmp__(self, other): if not isinstance(other, Oid): other = Oid(self.entity, other) if tuple(self._value) == tuple(other._value): return 0 if self._value > other._value: return 1 return -1 def __getitem__(self, index): return self._value[index] def __contains__(self, item): """Test if item is a sub-oid of this OID""" if not isinstance(item, Oid): item = Oid(self.entity, item) return tuple(item._value[:len(self._value)]) == \ tuple(self._value[:len(self._value)]) class Boolean(Enum): """Class for a boolean.""" @classmethod def _internal(cls, entity, value): if isinstance(value, bool): if value: return Enum._internal(entity, "true") else: return Enum._internal(entity, "false") else: return Enum._internal(entity, value) def __nonzero__(self): if self._value == 1: return True else: return False def __bool__(self): return self.__nonzero__() @ordering_with_cmp class Timeticks(Type): """Class for timeticks.""" @classmethod def _internal(cls, entity, value): if isinstance(value, int): # Value in centiseconds return timedelta(0, value / 100.) elif isinstance(value, timedelta): return value else: raise TypeError( "dunno how to handle {!r} ({})".format(value, type(value))) def __int__(self): return self._value.days * 3600 * 24 * 100 + \ self._value.seconds * 100 + \ self._value.microseconds // 10000 def toOid(self, implied=False): return (int(self),) @classmethod def fromOid(cls, entity, oid, implied=False): if len(oid) < 1: raise ValueError("{!r} is too short for a timetick".format(oid)) return (1, cls(entity, oid[0])) def pack(self): return rfc1902.TimeTicks(int(self)) def __str__(self): return str(self._value) def __cmp__(self, other): if isinstance(other, Timeticks): other = other._value elif isinstance(other, int): other = timedelta(0, other / 100.) elif not isinstance(other, timedelta): raise NotImplementedError( "only compare to int or " "timedelta, not {}".format(type(other))) if self._value == other: return 0 if self._value < other: return -1 return 1 class Bits(Type): """Class for bits.""" @classmethod def _internal(cls, entity, value): bits = set() tryalternate = False if isinstance(value, bytes): for i, x in enumerate(value): if x == 0: continue for j in range(8): if x & (1 << (7 - j)): k = (i * 8) + j if k not in entity.enum: tryalternate = True break bits.add(k) if tryalternate: break if not tryalternate: return bits else: bits = set() elif not isinstance(value, (tuple, list, set, frozenset)): value = {value} for v in value: found = False if v in entity.enum: bits.add(v) found = True else: for (k, t) in entity.enum.items(): if (t == v): bits.add(k) found = True break if not found: raise ValueError("{!r} is not a valid bit value".format(v)) return bits def pack(self): if self._value: string = [0] * ((max(self._value) // 8) + 1) else: string = [] for b in self._value: string[b // 8] |= 1 << (7 - b % 8) return rfc1902.Bits(bytes(string)) def __eq__(self, other): if isinstance(other, str): other = [other] if not isinstance(other, Bits): other = Bits(self.entity, other) return self._value == other._value def __ne__(self, other): return not self.__eq__(other) def __str__(self): result = [] for b in sorted(self._value): result.append("{}({:d})".format(self.entity.enum[b], b)) return ", ".join(result) def __and__(self, other): if isinstance(other, str): other = [other] if not isinstance(other, Bits): other = Bits(self.entity, other) return len(self._value & other._value) > 0 def __ior__(self, other): if isinstance(other, str): other = [other] if not isinstance(other, Bits): other = Bits(self.entity, other) self._value |= other._value return self def __isub__(self, other): if isinstance(other, str): other = [other] if not isinstance(other, Bits): other = Bits(self.entity, other) self._value -= other._value return self def build(mibname, node, value): """Build a new basic type with the given value. :param mibname: The MIB to use to locate the entity. :param node: The node that will be attached to this type. :param value: The initial value to set for the type. :return: A :class:`Type` instance """ m = mib.get(mibname, node) t = m.type(m, value) return t ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/snimpy/config.py0000644000076400001440000000333500000000000015370 0ustar00bernatusers# # snimpy -- Interactive SNMP tool # # Copyright (C) Vincent Bernat # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # import os.path import imp class Conf: prompt = "\033[1m[snimpy]>\033[0m " histfile = "~/.snimpy_history" # Not used with IPython userconf = "~/.snimpy.conf" ipython = True ipythonprofile = None # Set for example to "snimpy" mibs = [] def load(self, userconf=None): if userconf is None: userconf = self.userconf try: conffile = open(os.path.expanduser(userconf)) except OSError: pass else: try: confuser = imp.load_module("confuser", conffile, os.path.expanduser(userconf), ("conf", 'r', imp.PY_SOURCE)) for k in confuser.__dict__: if not k.startswith("__"): setattr(self, k, confuser.__dict__[k]) finally: conffile.close() return self ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/snimpy/main.py0000644000076400001440000001326200000000000015047 0ustar00bernatusers# # snimpy -- Interactive SNMP tool # # Copyright (C) Vincent Bernat # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # # We are using readline module of Python. Depending on the Python # distribution, this module may be linked to GNU Readline which is # GPLv2 licensed. """Provide an interactive shell for snimpy. The main method is C{interact()}. It will either use IPython if available or just plain python Shell otherwise. It will also try to use readline if available. For IPython, there is some configuration stuff to use a special profile. This is the recommended way to use it. It allows a separate history. """ import sys import os import atexit import code from datetime import timedelta import snimpy from snimpy import manager from snimpy.config import Conf def interact(argv=sys.argv): # pragma: no cover conf = Conf().load() banner = "\033[1mSnimpy\033[0m ({}) -- {}.\n".format( snimpy.__version__, snimpy.__doc__) banner += " load -> load an additional MIB\n" banner += " M -> manager object" local = {"conf": conf, "M": manager.Manager, "load": manager.load, "timedelta": timedelta, "snmp": manager.snmp } if len(argv) <= 1: manager.Manager._complete = True for ms in conf.mibs: manager.load(ms) globals().update(local) if len(argv) > 1: argv = argv[1:] exec(compile(open(argv[0]).read(), argv[0], 'exec')) in local return try: try: try: # ipython >= 1.0 from IPython.terminal.embed import \ InteractiveShellEmbed except ImportError: # ipython >= 0.11 from IPython.frontend.terminal.embed import \ InteractiveShellEmbed import IPython if IPython.version_info < (4,): from IPython.config.loader import Config else: from traitlets.config.loader import Config cfg = Config() try: # >= 5 from IPython.terminal.prompts import Prompts, Token class SnimpyPrompt(Prompts): def in_prompt_tokens(self, cli=None): return [ (Token.Prompt, "Snimpy["), (Token.PromptNum, str(self.shell.execution_count)), (Token.Prompt, ']> '), ] def out_prompt_tokens(self): return [ (Token.OutPrompt, "Snimpy["), (Token.OutPromptNum, str(self.shell.execution_count)), (Token.OutPrompt, ']: '), ] except ImportError: SnimpyPrompt = None try: # >= 0.12 cfg.PromptManager.in_template = "Snimpy [\\#]> " cfg.PromptManager.out_template = "Snimpy [\\#]: " except ImportError: # 0.11 cfg.InteractiveShellEmbed.prompt_in1 = "Snimpy [\\#]> " cfg.InteractiveShellEmbed.prompt_out = "Snimpy [\\#]: " if conf.ipythonprofile: cfg.InteractiveShellEmbed.profile = conf.ipythonprofile shell = InteractiveShellEmbed( config=cfg, banner1=banner, user_ns=local) # Not interested by traceback in this module shell.InteractiveTB.tb_offset += 1 if SnimpyPrompt is not None: shell.prompts = SnimpyPrompt(shell) except ImportError: # ipython < 0.11 from IPython.Shell import IPShellEmbed argv = ["-prompt_in1", "Snimpy [\\#]> ", "-prompt_out", "Snimpy [\\#]: "] if conf.ipythonprofile: argv += ["-profile", conf.ipythonprofile] shell = IPShellEmbed(argv=argv, banner=banner, user_ns=local) # Not interested by traceback in this module shell.IP.InteractiveTB.tb_offset += 1 except ImportError: shell = None if shell and conf.ipython: shell() else: try: import rlcompleter import readline except ImportError: readline = None if readline: if conf.histfile: try: readline.read_history_file( os.path.expanduser(conf.histfile)) except OSError: pass atexit.register(lambda: readline.write_history_file( os.path.expanduser(conf.histfile))) readline.set_completer(rlcompleter.Completer(local).complete) readline.parse_and_bind("tab: menu-complete") sys.ps1 = conf.prompt code.interact(banner=banner, local=local) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622297217.0 snimpy-1.0.0/snimpy/manager.py0000644000076400001440000005074600000000000015545 0ustar00bernatusers# # snimpy -- Interactive SNMP tool # # Copyright (C) Vincent Bernat # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # """This module is the high-level interface to *Snimpy*. It exposes :class:`Manager` class to instantiate a new manager (which is an SNMP client). This is the preferred interface for *Snimpy*. Here is a simple example of use of this module:: >>> load("IF-MIB") >>> m = Manager("localhost") >>> m.ifDescr[1] """ import inspect from time import time from collections.abc import MutableMapping, Container, Iterable, Sized from snimpy import snmp, mib, basictypes class DelegatedSession: """General class for SNMP session for delegation""" def __init__(self, session): self._session = session def __getattr__(self, attr): return getattr(self._session, attr) def __setattribute__(self, attr, value): return setattr(self._session, attr, value) class DelayedSetSession(DelegatedSession): """SNMP session that is able to delay SET requests. This is an adapter. The constructor takes the original (not delayed) session. """ def __init__(self, session): DelegatedSession.__init__(self, session) self.setters = [] def set(self, *args): self.setters.extend(args) def commit(self): if self.setters: self._session.set(*self.setters) class NoneSession(DelegatedSession): """SNMP session that will return None on unsucessful GET requests. In a normal session, a GET request returning `No such instance` error will trigger an exception. This session will catch such an error and return None instead. """ def get(self, *args): try: return self._session.get(*args) except (snmp.SNMPNoSuchName, snmp.SNMPNoSuchObject, snmp.SNMPNoSuchInstance): if len(args) > 1: # We can't handle this case yet because we don't know # which value is unavailable. raise return ((args[0], None),) class CachedSession(DelegatedSession): """SNMP session using a cache. This is an adapter. The constructor takes the original session. """ def __init__(self, session, timeout=5): DelegatedSession.__init__(self, session) self.cache = {} # contains (operation, oid) -> [time, result] entries self.timeout = timeout self.count = 0 def getorwalk(self, op, *args): self.count += 1 if (op, args) in self.cache: t, v = self.cache[op, args] if time() - t < self.timeout: return v value = getattr(self._session, op)(*args) self.cache[op, args] = [time(), value] if op == "walkmore": # also cache all the get requests we got for free for oid, get_value in value: self.count += 1 self.cache["get", (oid, )] = [time(), ((oid, get_value), )] self.flush() return value def get(self, *args): return self.getorwalk("get", *args) def walk(self, *args): assert(len(args) == 1) # we should ony walk one oid at a time return self.getorwalk("walkmore", *args) def flush(self): keys = list(self.cache.keys()) for k in keys: if time() - self.cache[k][0] > self.timeout: del self.cache[k] self.count = 0 def MibRestrictedManager(original, mibs): """Copy an existing manager but restrict its view to the given set of MIBs. """ clone = Manager(**original._constructor_args) clone._loaded = mibs return clone class Manager: """SNMP manager. An instance of this class will represent an SNMP manager (client). When a MIB is loaded with :func:`load`, scalars and row names from it will be made available as an instance attribute. For a scalar, reading the corresponding attribute will get its value while setting it will allow to modify it: >>> load("SNMPv2-MIB") >>> m = Manager("localhost", "private") >>> m.sysContact >>> m.sysContact = "Brian Jones" >>> m.sysContact For a row name, the provided interface is like a Python dictionary. Requesting an item using its index will retrieve the value from the agent (the server):: >>> load("IF-MIB") >>> m = Manager("localhost", "private") >>> m.ifDescr[1] >>> m.ifName[1] = "Loopback interface" Also, it is possible to iterate on a row name to get all available values for index:: >>> load("IF-MIB") >>> m = Manager("localhost", "private") >>> for idx in m.ifDescr: ... print(m.ifDescr[idx]) You can get a slice of index values from a table by iterating on a row name subscripted by a partial index:: >>> load("IF-MIB") >>> m = Manager("localhost", "private") >>> for idx in m.ipNetToMediaPhysAddress[1]: ... print(idx) (, ) You can use multivalue indexes in two ways: using Pythonic multi-dimensional dict syntax, or by providing a tuple containing index values:: >>> load("IF-MIB") >>> m = Manager("localhost", "private") >>> m.ipNetToMediaPhysAddress[1]['127.0.0.1'] >>> m.ipNetToMediaPhysAddress[1, '127.0.0.1'] A context manager is also provided. Any modification issued inside the context will be delayed until the end of the context and then grouped into a single SNMP PDU to be executed atomically:: >>> load("IF-MIB") >>> m = Manager("localhost", "private") >>> with m: ... m.ifName[1] = "Loopback interface" ... m.ifName[2] = "First interface" Any error will be turned into an exception:: >>> load("IF-MIB") >>> m = Manager("localhost", "private") >>> m.ifDescr[999] Traceback (most recent call last): ... SNMPNoSuchName: There is no such variable name in this MIB. """ # do we want this object to be populated with all nodes? _complete = False def __init__(self, host="localhost", community="public", version=2, cache=False, none=False, timeout=None, retries=None, loose=False, bulk=40, # SNMPv3 secname=None, authprotocol=None, authpassword=None, privprotocol=None, privpassword=None, contextname=None): """Create a new SNMP manager. Some of the parameters are explained in :meth:`snmp.Session.__init__`. :param host: The hostname or IP address of the agent to connect to. Optionally, the port can be specified separated with a double colon. :type host: str :param community: The community to transmit to the agent for authorization purpose. This parameter is ignored if the specified version is 3. :type community: str :param version: The SNMP version to use to talk with the agent. Possible values are `1`, `2` (community-based) or `3`. :type version: int :param cache: Should caching be enabled? This can be either a boolean or an integer to specify the cache timeout in seconds. If `True`, the default timeout is 5 seconds. :type cache: bool or int :param none: Should `None` be returned when the agent does not know the requested OID? If `True`, `None` will be returned when requesting an inexisting scalar or column. :type none: bool :param timeout: Use the specified value in seconds as timeout. :type timeout: int :param retries: How many times the request should be retried? :type retries: int :param loose: Enable loose typing. When type coercion fails (for example when a MIB declare an element to be an ASCII string while it is not), just return the raw result instead of an exception. This mode should be enabled with caution. Patching the MIB is a better idea. :type loose: bool :param bulk: Max-repetition to use to speed up MIB walking with `GETBULK`. Set to `0` to disable. :type bulk: int """ if host is None: host = Manager._host self._host = host self._session = snmp.Session(host, community, version, secname, authprotocol, authpassword, privprotocol, privpassword, contextname=contextname, bulk=bulk) if timeout is not None: self._session.timeout = int(timeout * 1000000) if retries is not None: self._session.retries = retries if cache: if cache is True: self._session = CachedSession(self._session) else: self._session = CachedSession(self._session, cache) if none: self._session = NoneSession(self._session) self._loose = loose self._loaded = loaded # To be able to clone, we save the arguments provided to the # constructor in a generic way frame = inspect.currentframe() args, _, _, values = inspect.getargvalues(frame) self._constructor_args = {a: values[a] for a in args if a != 'self'} def _locate(self, attribute): for m in self._loaded: try: a = mib.get(m, attribute) return (m, a) except mib.SMIException: pass raise AttributeError("{} not found in any MIBs".format(attribute)) def __getattribute__(self, attribute): if attribute.startswith("_"): return object.__getattribute__(self, attribute) m, a = self._locate(attribute) if isinstance(a, mib.Scalar): oid, result = self._session.get(a.oid + (0,))[0] if result is not None: try: return a.type(a, result) except ValueError: if self._loose: return result raise return None elif isinstance(a, mib.Column): return ProxyColumn(self._session, a, self._loose) elif isinstance(a, mib.Table): return ProxyTable(self._session, a, self._loose) raise NotImplementedError def __setattr__(self, attribute, value): if attribute.startswith("_"): return object.__setattr__(self, attribute, value) m, a = self._locate(attribute) if not isinstance(value, basictypes.Type): value = a.type(a, value, raw=False) if isinstance(a, mib.Scalar): self._session.set(a.oid + (0,), value) return raise AttributeError("{} is not writable".format(attribute)) def __getitem__(self, modulename): modulename = modulename.encode('ascii') for m in loaded: if modulename == m: return MibRestrictedManager(self, [m]) raise KeyError("{} is not a loaded module".format(modulename)) def __repr__(self): return "".format(self._host) def __enter__(self): """In a context, we group all "set" into a single request""" self._osession = self._session self._session = DelayedSetSession(self._session) return self def __exit__(self, type, value, traceback): """When we exit, we should execute all "set" requests""" try: if type is None: self._session.commit() finally: self._session = self._osession del self._osession class Proxy: """A proxy for some base type, notably a column or a table.""" def __repr__(self): return "<{} for {}>".format(self.__class__.__name__, repr(self.proxy)[1:-1]) class ProxyIter(Proxy, Sized, Iterable, Container): """Proxy for an iterable sequence. This a proxy offering the ABC of an iterable sequence (something like a set but without set operations). This will be used by both `ProxyColumn` and `ProxyTable`. """ def _op(self, op, index, *args): if not isinstance(index, tuple): index = (index,) indextype = self.proxy.table.index if len(indextype) != len(index): raise ValueError( "{} column uses the following " "indexes: {!r}".format(self.proxy, indextype)) oidindex = [] for i, ind in enumerate(index): # Cast to the correct type since we need "toOid()" ind = indextype[i].type(indextype[i], ind, raw=False) implied = self.proxy.table.implied and i == len(index)-1 oidindex.extend(ind.toOid(implied)) result = getattr( self.session, op)(self.proxy.oid + tuple(oidindex), *args) if op != "set": oid, result = result[0] if result is not None: try: return self.proxy.type(self.proxy, result) except ValueError: if self._loose: return result raise return None def __contains__(self, object): try: self._op("get", object) except Exception: return False return True def __iter__(self): for k, _ in self.iteritems(): yield k def __len__(self): len(list(self.iteritems())) def items(self, *args, **kwargs): return self.iteritems(*args, **kwargs) def iteritems(self, table_filter=None): count = 0 oid = self.proxy.oid indexes = self.proxy.table.index if table_filter is not None: if len(table_filter) >= len(indexes): raise ValueError("Table filter has too many elements") oid_suffix = [] # Convert filter elements to correct types for i, part in enumerate(table_filter): part = indexes[i].type(indexes[i], part, raw=False) # implied = False: # index never includes last element # (see 'len(table_filter) >= len(indexes)') oid_suffix.extend(part.toOid(implied=False)) oid += tuple(oid_suffix) walk_oid = oid for noid, result in self.session.walk(oid): if noid <= oid: noid = None break oid = noid if not(len(oid) >= len(walk_oid) and oid[:len(walk_oid)] == walk_oid[:len(walk_oid)]): noid = None break # oid should be turned into index index = tuple(oid[len(self.proxy.oid):]) target = [] for i, x in enumerate(indexes): implied = self.proxy.table.implied and i == len(indexes)-1 l, o = x.type.fromOid(x, index, implied) target.append(x.type(x, o)) index = index[l:] count = count + 1 if result is not None: try: result = self.proxy.type(self.proxy, result) except ValueError: if not self._loose: raise if len(target) == 1: # Should work most of the time yield target[0], result else: yield tuple(target), result if count == 0: # We did not find any element. Is it because the column is # empty or because the column does not exist. We do a SNMP # GET to know. If we get a "No such instance" exception, # this means the column is empty. If we get "No such # object", this means the column does not exist. We cannot # make such a distinction with SNMPv1. try: self.session.get(self.proxy.oid) except snmp.SNMPNoSuchInstance: # OK, the set of result is really empty return except snmp.SNMPNoAccess: # Some implementations seem to return NoAccess (PySNMP is one) return except snmp.SNMPNoSuchName: # SNMPv1, we don't know pass except snmp.SNMPNoSuchObject: # The result is empty because the column is unknown raise class ProxyTable(ProxyIter): """Proxy for table access. We just use the first accessible index as a column. However, the mapping operations are not available. """ def __init__(self, session, table, loose): self.proxy = None for column in table.columns: if column.accessible: self.proxy = column break if self.proxy is None: raise NotImplementedError("No accessible column in the table.") self.session = session self._loose = loose class ProxyColumn(ProxyIter, MutableMapping): """Proxy for column access""" def __init__(self, session, column, loose, oid_suffix=()): self.proxy = column self.session = session self._loose = loose self._oid_suffix = oid_suffix def __getitem__(self, index): # If supplied index is partial we return new ProxyColumn # with appended OID suffix idx_len = len(self.proxy.table.index) suffix_len = len(self._oid_suffix) if isinstance(index, tuple): if len(index) + suffix_len < idx_len: return self._partial(index) elif idx_len > suffix_len + 1: return self._partial((index,)) # Otherwise a read op is made if not isinstance(index, tuple): index = (index,) return self._op("get", self._oid_suffix + index) def __setitem__(self, index, value): if not isinstance(value, basictypes.Type): value = self.proxy.type(self.proxy, value, raw=False) if not isinstance(index, tuple): index = (index,) self._op("set", self._oid_suffix + index, value) def __delitem__(self, index): raise NotImplementedError("cannot suppress a column") def __contains__(self, index): if not isinstance(index, tuple): index = (index,) return ProxyIter.__contains__(self, self._oid_suffix + index) def _partial(self, index): """Create new ProxyColumn based on current one, but with appended OID suffix""" new_suffix = self._oid_suffix + index return ProxyColumn(self.session, self.proxy, self._loose, new_suffix) def items(self, *args, **kwargs): return self.iteritems(*args, **kwargs) def iteritems(self, table_filter=None): resulting_filter = self._oid_suffix if table_filter is not None: if not isinstance(table_filter, tuple): table_filter = (table_filter,) resulting_filter += table_filter return ProxyIter.iteritems(self, resulting_filter) loaded = [] def load(mibname): """Load a MIB in memory. :param mibname: MIB name or filename :type mibname: str """ m = mib.load(mibname) if m not in loaded: loaded.append(m) if Manager._complete: for o in mib.getScalars(m) + \ mib.getColumns(m) + \ mib.getTables(m): setattr(Manager, str(o), 1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/snimpy/mib.py0000644000076400001440000005620300000000000014674 0ustar00bernatusers# # snimpy -- Interactive SNMP tool # # Copyright (C) Vincent Bernat # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # """This module is a low-level interface to manipulate and extract information from MIB files. It is a CFFI_ wrapper around libsmi_. You may find convenient to use it in other projects but the wrapper is merely here to serve *Snimpy* use and is therefore incomplete. .. _libsmi: http://www.ibr.cs.tu-bs.de/projects/libsmi/ .. _CFFI: http://cffi.readthedocs.io/ """ try: from snimpy._smi import lib as _smi from snimpy._smi import ffi except ImportError: from snimpy.smi_build import ffi, get_lib _smi = get_lib() class SMIException(Exception): """SMI related exception. Any exception thrown in this module is inherited from this one.""" class Node: """MIB node. An instance of this class represents a MIB node. It can be specialized by other classes, like :class:`Scalar`, :class:`Table`, :class:`Column`, :class:`Node`. """ def __init__(self, node): """Create a new MIB node. :param node: libsmi node supporting this node. """ self.node = node self._override_type = None @property def type(self): """Get the basic type associated with this node. :return: The class from :mod:`basictypes` module which can represent the node. When retrieving a valid value for this node, the returned class can be instanciated to get an appropriate representation. """ from snimpy import basictypes if self._override_type: t = self._override_type else: t = _smi.smiGetNodeType(self.node) if t == ffi.NULL: raise SMIException("unable to retrieve type of node") target = { _smi.SMI_BASETYPE_INTEGER32: basictypes.Integer, _smi.SMI_BASETYPE_INTEGER64: basictypes.Integer, _smi.SMI_BASETYPE_UNSIGNED32: {b"TimeTicks": basictypes.Timeticks, None: basictypes.Unsigned32}, _smi.SMI_BASETYPE_UNSIGNED64: basictypes.Unsigned64, _smi.SMI_BASETYPE_OCTETSTRING: {b"IpAddress": basictypes.IpAddress, None: basictypes.OctetString}, _smi.SMI_BASETYPE_OBJECTIDENTIFIER: basictypes.Oid, _smi.SMI_BASETYPE_ENUM: {b"TruthValue": basictypes.Boolean, None: basictypes.Enum}, _smi.SMI_BASETYPE_BITS: basictypes.Bits }.get(t.basetype, None) if isinstance(target, dict): tt = _smi.smiGetParentType(t) target = target.get((t.name != ffi.NULL and ffi.string(t.name)) or (tt.name != ffi.NULL and ffi.string( tt.name)) or None, target.get(None, None)) if target is None: raise SMIException("unable to retrieve type of node") return target @property def typeName(self): """Retrieves the name of the the node's current declared type (not basic type). :return: A string representing the current declared type, suitable for assignment to type.setter. """ if self._override_type: t = self._override_type else: t = _smi.smiGetNodeType(self.node) # This occurs when the type is "implied". if t.name == ffi.NULL: t = _smi.smiGetParentType(t) if t is None or t == ffi.NULL: raise SMIException("unable to retrieve the declared type " "of the node '{}'".format(self.node.name)) return ffi.string(t.name) @typeName.setter def typeName(self, type_name): """Override the node's type to type_name, found using _getType. The new type must resolve to the same basictype. :param type_name: string name of the type. """ current_override = self._override_type declared_type = _smi.smiGetNodeType(self.node) declared_basetype = self.type new_type = _getType(type_name) if not new_type: raise SMIException("no type named {} in any loaded module".format( type_name)) # Easiest way to find the new basetype is to set the override # and ask. self._override_type = new_type new_basetype = self.type if declared_basetype != new_basetype: self._override_type = current_override raise SMIException("override type {1} not compatible with " "basetype of {0}".format( ffi.string(declared_type.name), ffi.string(new_type.name))) @typeName.deleter def typeName(self): """Clears the type override.""" self._override_type = None @property def fmt(self): """Get node format. The node format is a string to use to display a user-friendly version of the node. This is can be used for both octet strings or integers (to make them appear as decimal numbers). :return: The node format as a string or None if there is no format available. """ if self._override_type: t = self._override_type else: t = _smi.smiGetNodeType(self.node) tt = _smi.smiGetParentType(t) f = (t != ffi.NULL and t.format != ffi.NULL and ffi.string(t.format) or tt != ffi.NULL and tt.format != ffi.NULL and ffi.string(tt.format)) or None if f is None: return None return f.decode("ascii") @property def oid(self): """Get OID for the current node. The OID can then be used to request the node from an SNMP agent. :return: OID as a tuple """ return tuple([self.node.oid[i] for i in range(self.node.oidlen)]) @property def ranges(self): """Get node ranges. An node can be restricted by a set of ranges. For example, an integer can only be provided between two values. For strings, the restriction is on the length of the string. The returned value can be `None` if no restriction on range exists for the current node, a single value if the range is fixed or a list of tuples or fixed values otherwise. :return: The valid range for this node. """ t = _smi.smiGetNodeType(self.node) if t == ffi.NULL: return None ranges = [] range = _smi.smiGetFirstRange(t) while range != ffi.NULL: m1 = self._convert(range.minValue) m2 = self._convert(range.maxValue) if m1 == m2: ranges.append(m1) else: ranges.append((m1, m2)) range = _smi.smiGetNextRange(range) if len(ranges) == 0: return None if len(ranges) == 1: return ranges[0] return ranges @property def enum(self): """Get possible enum values. When the node can only take a discrete number of values, those values are defined in the MIB and can be retrieved through this property. :return: The dictionary of possible values keyed by the integer value. """ t = _smi.smiGetNodeType(self.node) if t == ffi.NULL or t.basetype not in (_smi.SMI_BASETYPE_ENUM, _smi.SMI_BASETYPE_BITS): return None result = {} element = _smi.smiGetFirstNamedNumber(t) while element != ffi.NULL: result[self._convert(element.value)] = ffi.string( element.name).decode("ascii") element = _smi.smiGetNextNamedNumber(element) return result @property def accessible(self): return (self.node.access not in (_smi.SMI_ACCESS_NOT_IMPLEMENTED, _smi.SMI_ACCESS_NOT_ACCESSIBLE)) def __str__(self): return ffi.string(self.node.name).decode("ascii") def __repr__(self): r = _smi.smiRenderNode(self.node, _smi.SMI_RENDER_ALL) if r == ffi.NULL: return "".format( self.__class__.__name__, hex(id(self))) r = ffi.gc(r, _smi.free) module = _smi.smiGetNodeModule(self.node) if module == ffi.NULL: raise SMIException("unable to get module for {}".format( self.node.name)) return "<{} {} from '{}'>".format(self.__class__.__name__, ffi.string(r), ffi.string(module.name)) def _convert(self, value): attr = {_smi.SMI_BASETYPE_INTEGER32: "integer32", _smi.SMI_BASETYPE_UNSIGNED32: "unsigned32", _smi.SMI_BASETYPE_INTEGER64: "integer64", _smi.SMI_BASETYPE_UNSIGNED64: "unsigned64"}.get(value.basetype, None) if attr is None: raise SMIException("unexpected type found in range") return getattr(value.value, attr) class Scalar(Node): """MIB scalar node. This class represents a scalar value in the MIB. A scalar value is a value not contained in a table. """ class Table(Node): """MIB table node. This class represents a table. A table is an ordered collection of objects consisting of zero or more rows. Each object in the table is identified using an index. An index can be a single value or a list of values. """ @property def columns(self): """Get table columns. The columns are the different kind of objects that can be retrieved in a table. :return: list of table columns (:class:`Column` instances) """ child = _smi.smiGetFirstChildNode(self.node) if child == ffi.NULL: return [] if child.nodekind != _smi.SMI_NODEKIND_ROW: raise SMIException("child {} of {} is not a row".format( ffi.string(child.name), ffi.string(self.node.name))) columns = [] child = _smi.smiGetFirstChildNode(child) while child != ffi.NULL: if child.nodekind != _smi.SMI_NODEKIND_COLUMN: raise SMIException("child {} of {} is not a column".format( ffi.string(child.name), ffi.string(self.node.name))) columns.append(Column(child)) child = _smi.smiGetNextChildNode(child) return columns @property def _row(self): """Get the table row. :return: row object (as an opaque object) """ child = _smi.smiGetFirstChildNode(self.node) if child != ffi.NULL and child.indexkind == _smi.SMI_INDEX_AUGMENT: child = _smi.smiGetRelatedNode(child) if child == ffi.NULL: raise SMIException("AUGMENT index for {} but " "unable to retrieve it".format( ffi.string(self.node.name))) if child == ffi.NULL: raise SMIException("{} does not have a row".format( ffi.string(self.node.name))) if child.nodekind != _smi.SMI_NODEKIND_ROW: raise SMIException("child {} of {} is not a row".format( ffi.string(child.name), ffi.string(self.node.name))) if child.indexkind != _smi.SMI_INDEX_INDEX: raise SMIException("child {} of {} has an unhandled " "kind of index".format( ffi.string(child.name), ffi.string(self.node.name))) return child @property def implied(self): """Is the last index implied? An implied index is an index whose size is not fixed but who is not prefixed by its size because this is the last index of a table. :return: `True` if and only if the last index is implied. """ child = self._row if child.implied: return True return False @property def index(self): """Get indexes for a table. The indexes are used to locate a precise row in a table. They are a subset of the table columns. :return: The list of indexes (as :class:`Column` instances) of the table. """ child = self._row lindex = [] element = _smi.smiGetFirstElement(child) while element != ffi.NULL: nelement = _smi.smiGetElementNode(element) if nelement == ffi.NULL: raise SMIException("cannot get index " "associated with {}".format( ffi.string(self.node.name))) if nelement.nodekind != _smi.SMI_NODEKIND_COLUMN: raise SMIException("index {} for {} is " "not a column".format( ffi.string(nelement.name), ffi.string(self.node.name))) lindex.append(Column(nelement)) element = _smi.smiGetNextElement(element) return lindex class Column(Node): """MIB column node. This class represent a column of a table.""" @property def table(self): """Get table associated with this column. :return: The :class:`Table` instance associated to this column. """ parent = _smi.smiGetParentNode(self.node) if parent == ffi.NULL: raise SMIException("unable to get parent of {}".format( ffi.string(self.node.name))) if parent.nodekind != _smi.SMI_NODEKIND_ROW: raise SMIException("parent {} of {} is not a row".format( ffi.string(parent.name), ffi.string(self.node.name))) parent = _smi.smiGetParentNode(parent) if parent == ffi.NULL: raise SMIException("unable to get parent of {}".format( ffi.string(self.node.name))) if parent.nodekind != _smi.SMI_NODEKIND_TABLE: raise SMIException("parent {} of {} is not a table".format( ffi.string(parent.name), ffi.string(self.node.name))) t = Table(parent) return t class Notification(Node): """MIB notification node. This class represent a notification.""" @property def objects(self): """Get objects for a notification. :return: The list of objects (as :class:`Column`, :class:`Node` or :class:`Scalar` instances) of the notification. """ child = self.node lindex = [] element = _smi.smiGetFirstElement(child) while element != ffi.NULL: nelement = _smi.smiGetElementNode(element) if nelement == ffi.NULL: raise SMIException("cannot get object " "associated with {}".format( ffi.string(self.node.name))) if nelement.nodekind == _smi.SMI_NODEKIND_COLUMN: lindex.append(Column(nelement)) elif nelement.nodekind == _smi.SMI_NODEKIND_NODE: lindex.append(Node(nelement)) elif nelement.nodekind == _smi.SMI_NODEKIND_SCALAR: lindex.append(Scalar(nelement)) else: raise SMIException("object {} for {} is " "not a node".format( ffi.string(nelement.name), ffi.string(self.node.name))) element = _smi.smiGetNextElement(element) return lindex _lastError = None @ffi.callback("void(char *, int, int, char *, char*)") def _logError(path, line, severity, msg, tag): global _lastError if path != ffi.NULL and msg != ffi.NULL: _lastError = "{}:{}: {}".format(ffi.string(path), line, ffi.string(msg)) else: _lastError = None def reset(): """Reset libsmi to its initial state.""" _smi.smiExit() if _smi.smiInit(b"snimpy") < 0: raise SMIException("unable to init libsmi") _smi.smiSetErrorLevel(1) _smi.smiSetErrorHandler(_logError) try: _smi.smiSetFlags(_smi.SMI_FLAG_ERRORS | _smi.SMI_FLAG_RECURSIVE) except TypeError: pass # We are being mocked def path(path=None): """Set or get a search path to libsmi. When no path is provided, return the current path, unmodified. Otherwise, set the path to the specified value. :param path: The string to be used to change the search path or `None` """ if path is None: # Get the path path = _smi.smiGetPath() if path == ffi.NULL: raise SMIException("unable to get current libsmi path") path = ffi.gc(path, _smi.free) result = ffi.string(path) return result.decode("utf8") # Set the path if not isinstance(path, bytes): path = path.encode("utf8") if _smi.smiSetPath(path) < 0: raise SMIException("unable to set the path {}".format(path)) def _get_module(name): """Get the SMI module from its name. :param name: The name of the module :return: The SMI module or `None` if not found (not loaded) """ if not isinstance(name, bytes): name = name.encode("ascii") m = _smi.smiGetModule(name) if m == ffi.NULL: return None if m.conformance and m.conformance <= 1: return None return m def _kind2object(kind): return { _smi.SMI_NODEKIND_NODE: Node, _smi.SMI_NODEKIND_SCALAR: Scalar, _smi.SMI_NODEKIND_TABLE: Table, _smi.SMI_NODEKIND_NOTIFICATION: Notification, _smi.SMI_NODEKIND_COLUMN: Column }.get(kind, Node) def get(mib, name): """Get a node by its name. :param mib: The MIB name to query :param name: The object name to get from the MIB :return: the requested MIB node (:class:`Node`) """ if not isinstance(mib, bytes): mib = mib.encode("ascii") module = _get_module(mib) if module is None: raise SMIException("no module named {}".format(mib)) node = _smi.smiGetNode(module, name.encode("ascii")) if node == ffi.NULL: raise SMIException("in {}, no node named {}".format( mib, name)) pnode = _kind2object(node.nodekind) return pnode(node) def getByOid(oid): """Get a node by its OID. :param oid: The OID as a tuple :return: The requested MIB node (:class:`Node`) """ node = _smi.smiGetNodeByOID(len(oid), oid) if node == ffi.NULL: raise SMIException("no node for {}".format( ".".join([str(o) for o in oid]))) pnode = _kind2object(node.nodekind) return pnode(node) def _getType(type_name): """Searches for a smi type through all loaded modules. :param type_name: The name of the type to search for. :return: The requested type (:class:`smi.SmiType`), if found, or None. """ if not isinstance(type_name, bytes): type_name = type_name.encode("ascii") for module in _loadedModules(): new_type = _smi.smiGetType(module, type_name) if new_type != ffi.NULL: return new_type return None def _get_kind(mib, kind): """Get nodes of a given kind from a MIB. :param mib: The MIB name to search objects for :param kind: The SMI kind of object :return: The list of matched MIB nodes for the MIB """ if not isinstance(mib, bytes): mib = mib.encode("ascii") module = _get_module(mib) if module is None: raise SMIException("no module named {}".format(mib)) lnode = [] node = _smi.smiGetFirstNode(module, kind) while node != ffi.NULL: lnode.append(_kind2object(kind)(node)) node = _smi.smiGetNextNode(node, kind) return lnode def getNodes(mib): """Return all nodes from a given MIB. :param mib: The MIB name :return: The list of all MIB nodes for the MIB :rtype: list of :class:`Node` instances """ return _get_kind(mib, _smi.SMI_NODEKIND_NODE) def getScalars(mib): """Return all scalars from a given MIB. :param mib: The MIB name :return: The list of all scalars for the MIB :rtype: list of :class:`Scalar` instances """ return _get_kind(mib, _smi.SMI_NODEKIND_SCALAR) def getTables(mib): """Return all tables from a given MIB. :param mib: The MIB name :return: The list of all tables for the MIB :rtype: list of :class:`Table` instances """ return _get_kind(mib, _smi.SMI_NODEKIND_TABLE) def getColumns(mib): """Return all columns from a givem MIB. :param mib: The MIB name :return: The list of all columns for the MIB :rtype: list of :class:`Column` instances """ return _get_kind(mib, _smi.SMI_NODEKIND_COLUMN) def getNotifications(mib): """Return all notifications from a givem MIB. :param mib: The MIB name :return: The list of all notifications for the MIB :rtype: list of :class:`Notification` instances """ return _get_kind(mib, _smi.SMI_NODEKIND_NOTIFICATION) def load(mib): """Load a MIB into the library. :param mib: The MIB to load, either a filename or a MIB name. :return: The MIB name that has been loaded. :except SMIException: The requested MIB cannot be loaded. """ if not isinstance(mib, bytes): mib = mib.encode("ascii") modulename = _smi.smiLoadModule(mib) if modulename == ffi.NULL: raise SMIException("unable to find {} (check the path)".format(mib)) modulename = ffi.string(modulename) if not _get_module(modulename.decode("ascii")): details = "check with smilint -s -l1" if _lastError is not None: details = "{}: {}".format(_lastError, details) raise SMIException( "{} contains major SMI error ({})".format(mib, details)) return modulename def _loadedModules(): """Generates the list of loaded modules. :yield: The :class:`smi.SmiModule` of all currently loaded modules. """ module = _smi.smiGetFirstModule() while module != ffi.NULL: yield module module = _smi.smiGetNextModule(module) def loadedMibNames(): """Generates the list of loaded MIB names. :yield: The names of all currently loaded MIBs. """ for module in _loadedModules(): yield ffi.string(module.name).decode('utf-8') reset() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1539288139.0 snimpy-1.0.0/snimpy/smi_build.py0000644000076400001440000001213200000000000016065 0ustar00bernatusers# # snimpy -- Interactive SNMP tool # # Copyright (C) 2015 Vincent Bernat # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # """This module just exports libsmi through CFFI_. .. _CFFI: http://cffi.readthedocs.io/ """ from cffi import FFI _CDEF = """ typedef char *SmiIdentifier; typedef unsigned long SmiUnsigned32; typedef long SmiInteger32; typedef unsigned long long SmiUnsigned64; typedef long long SmiInteger64; typedef unsigned int SmiSubid; typedef float SmiFloat32; typedef double SmiFloat64; typedef long double SmiFloat128; typedef enum SmiBasetype { SMI_BASETYPE_INTEGER32, SMI_BASETYPE_OCTETSTRING, SMI_BASETYPE_OBJECTIDENTIFIER, SMI_BASETYPE_UNSIGNED32, SMI_BASETYPE_INTEGER64, SMI_BASETYPE_UNSIGNED64, SMI_BASETYPE_ENUM, SMI_BASETYPE_BITS, ... } SmiBasetype; typedef struct SmiType { SmiIdentifier name; SmiBasetype basetype; char *format; ...; } SmiType; typedef enum SmiIndexkind { SMI_INDEX_INDEX, SMI_INDEX_AUGMENT, ... } SmiIndexkind; typedef enum SmiAccess { SMI_ACCESS_NOT_IMPLEMENTED, SMI_ACCESS_NOT_ACCESSIBLE, SMI_ACCESS_READ_ONLY, SMI_ACCESS_READ_WRITE, ... } SmiAccess; typedef unsigned int SmiNodekind; #define SMI_NODEKIND_NODE ... #define SMI_NODEKIND_SCALAR ... #define SMI_NODEKIND_TABLE ... #define SMI_NODEKIND_ROW ... #define SMI_NODEKIND_COLUMN ... #define SMI_NODEKIND_NOTIFICATION ... typedef struct SmiNode { SmiIdentifier name; unsigned int oidlen; SmiSubid *oid; char *format; SmiIndexkind indexkind; int implied; SmiNodekind nodekind; SmiAccess access; ...; } SmiNode; typedef struct SmiValue { SmiBasetype basetype; union { SmiUnsigned64 unsigned64; SmiInteger64 integer64; SmiUnsigned32 unsigned32; SmiInteger32 integer32; SmiFloat32 float32; SmiFloat64 float64; SmiFloat128 float128; SmiSubid *oid; char *ptr; } value; ...; } SmiValue; typedef struct SmiRange { SmiValue minValue; SmiValue maxValue; } SmiRange; typedef struct SmiModule { SmiIdentifier name; int conformance; ...; } SmiModule; typedef struct SmiElement { ...; } SmiElement; typedef struct SmiNamedNumber { SmiIdentifier name; SmiValue value; } SmiNamedNumber; typedef void (SmiErrorHandler) (char *path, int line, int severity, char *msg, char *tag); int smiInit(const char *); void smiExit(void); void smiSetErrorLevel(int); void smiSetErrorHandler(SmiErrorHandler *); void smiSetFlags(int); int smiSetPath(const char *); char *smiGetPath(void); char *smiLoadModule(const char *); SmiModule *smiGetFirstModule(void); SmiModule *smiGetNextModule(SmiModule *); SmiModule *smiGetModule(const char *); SmiModule *smiGetNodeModule(SmiNode *); SmiType *smiGetNodeType(SmiNode *); SmiType *smiGetParentType(SmiType *); SmiType *smiGetType(SmiModule *, char *); SmiModule *smiGetTypeModule(SmiType *); char *smiRenderNode(SmiNode *, int); SmiElement *smiGetFirstElement(SmiNode *); SmiElement *smiGetNextElement(SmiElement *); SmiNode *smiGetElementNode(SmiElement *); SmiRange *smiGetFirstRange(SmiType *); SmiRange *smiGetNextRange(SmiRange *); SmiNode *smiGetNode(SmiModule *, const char *); SmiNode *smiGetNodeByOID(unsigned int oidlen, SmiSubid *); SmiNode *smiGetFirstNode(SmiModule *, SmiNodekind); SmiNode *smiGetNextNode(SmiNode *, SmiNodekind); SmiNode *smiGetParentNode(SmiNode *); SmiNode *smiGetRelatedNode(SmiNode *); SmiNode *smiGetFirstChildNode(SmiNode *); SmiNode *smiGetNextChildNode(SmiNode *); SmiNamedNumber *smiGetFirstNamedNumber(SmiType *); SmiNamedNumber *smiGetNextNamedNumber(SmiNamedNumber *); void free(void *); #define SMI_FLAG_ERRORS ... #define SMI_FLAG_RECURSIVE ... #define SMI_RENDER_ALL ... """ _SOURCE = """ #include """ ffi = FFI() ffi.cdef(_CDEF) if hasattr(ffi, 'set_source'): ffi.set_source("snimpy._smi", _SOURCE, libraries=["smi"]) def get_lib(): return ffi.verify(_SOURCE, libraries=["smi"]) if __name__ == "__main__": ffi.compile() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/snimpy/snmp.py0000644000076400001440000003760000000000000015102 0ustar00bernatusers# # snimpy -- Interactive SNMP tool # # Copyright (C) Vincent Bernat # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # """ This module is a low-level interface to build SNMP requests, send them and receive answers. It is built on top of pysnmp_ but the exposed interface is far simpler. It is also far less complete and there is an important dependency to the :mod:`basictypes` module for type coercing. .. _pysnmp: http://pysnmp.sourceforge.net/ """ import re import socket import inspect import threading import ipaddress from pysnmp.entity.rfc3413.oneliner import cmdgen from pysnmp.proto import rfc1902, rfc1905 from pysnmp.smi import error class SNMPException(Exception): """SNMP related base exception. All SNMP exceptions are inherited from this one. The inherited exceptions are named after the name of the corresponding SNMP error. """ class SNMPTooBig(SNMPException): pass class SNMPNoSuchName(SNMPException): pass class SNMPBadValue(SNMPException): pass class SNMPReadOnly(SNMPException): pass # Dynamically build remaining (v2) exceptions for name, obj in inspect.getmembers(error): if name.endswith("Error") and \ inspect.isclass(obj) and \ issubclass(obj, error.MibOperationError) and \ obj != error.MibOperationError: name = str("SNMP{}".format(name[:-5])) globals()[name] = type(name, (SNMPException,), {}) del name del obj class Session: """SNMP session. An instance of this object will represent an SNMP session. From such an instance, one can get information from the associated agent.""" _tls = threading.local() def __init__(self, host, community="public", version=2, secname=None, authprotocol=None, authpassword=None, privprotocol=None, privpassword=None, contextname=None, bulk=40, none=False): """Create a new SNMP session. :param host: The hostname or IP address of the agent to connect to. Optionally, the port can be specified separated with a double colon. :type host: str :param community: The community to transmit to the agent for authorization purpose. This parameter is ignored if the specified version is 3. :type community: str :param version: The SNMP version to use to talk with the agent. Possible values are `1`, `2` (community-based) or `3`. :type version: int :param secname: Security name to use for SNMPv3 only. :type secname: str :param authprotocol: Authorization protocol to use for SNMPv3. This can be `None` or either the string `SHA` or `MD5`. :type authprotocol: None or str :param authpassword: Authorization password if authorization protocol is not `None`. :type authpassword: str :param privprotocol: Privacy protocol to use for SNMPv3. This can be `None` or either the string `AES`, `AES128`, `AES192`, `AES256` or `3DES`. :type privprotocol: None or str :param privpassword: Privacy password if privacy protocol is not `None`. :type contextname: str :param contextname: Context name for SNMPv3 messages. :type privpassword: str :param bulk: Max repetition value for `GETBULK` requests. Set to `0` to disable. :type bulk: int :param none: When enabled, will return None for not found values (instead of raising an exception) :type none: bool """ self._host = host self._version = version self._none = none if version == 3: self._cmdgen = cmdgen.CommandGenerator() self._contextname = contextname else: if not hasattr(self._tls, "cmdgen"): self._tls.cmdgen = cmdgen.CommandGenerator() self._cmdgen = self._tls.cmdgen self._contextname = None if version == 1 and none: raise ValueError("None-GET requests not compatible with SNMPv1") # Put authentication stuff in self._auth if version in [1, 2]: self._auth = cmdgen.CommunityData( community[0:30], community, version - 1) elif version == 3: if secname is None: secname = community try: authprotocol = { None: cmdgen.usmNoAuthProtocol, "MD5": cmdgen.usmHMACMD5AuthProtocol, "SHA": cmdgen.usmHMACSHAAuthProtocol, "SHA1": cmdgen.usmHMACSHAAuthProtocol }[authprotocol] except KeyError: raise ValueError("{} is not an acceptable authentication " "protocol".format(authprotocol)) try: privprotocol = { None: cmdgen.usmNoPrivProtocol, "DES": cmdgen.usmDESPrivProtocol, "3DES": cmdgen.usm3DESEDEPrivProtocol, "AES": cmdgen.usmAesCfb128Protocol, "AES128": cmdgen.usmAesCfb128Protocol, "AES192": cmdgen.usmAesCfb192Protocol, "AES256": cmdgen.usmAesCfb256Protocol, }[privprotocol] except KeyError: raise ValueError("{} is not an acceptable privacy " "protocol".format(privprotocol)) self._auth = cmdgen.UsmUserData(secname, authpassword, privpassword, authprotocol, privprotocol) else: raise ValueError("unsupported SNMP version {}".format(version)) # Put transport stuff into self._transport mo = re.match(r'^(?:' r'\[(?P[\d:A-Fa-f]+)\]|' r'(?P[\d\.]+)|' r'(?P.*?))' r'(?::(?P\d+))?$', host) if mo.group("port"): port = int(mo.group("port")) else: port = 161 if mo.group("ipv6"): self._transport = cmdgen.Udp6TransportTarget((mo.group("ipv6"), port)) elif mo.group("ipv4"): self._transport = cmdgen.UdpTransportTarget((mo.group("ipv4"), port)) else: results = socket.getaddrinfo(mo.group("any"), port, 0, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # We should try to connect to each result to determine if # the given family is available. However, we cannot do # that over UDP. Let's implement a safe choice. If we have # an IPv4 address, use that. If not, use IPv6. If we want # to add an option to force IPv6, it is a good place. if [x for x in results if x[0] == socket.AF_INET]: self._transport = cmdgen.UdpTransportTarget((mo.group("any"), port)) else: self._transport = cmdgen.Udp6TransportTarget((mo.group("any"), port)) # Bulk stuff self.bulk = bulk def _check_exception(self, value): """Check if the given ASN1 value is an exception""" if isinstance(value, rfc1905.NoSuchObject): raise SNMPNoSuchObject("No such object was found") # noqa: F821 if isinstance(value, rfc1905.NoSuchInstance): raise SNMPNoSuchInstance("No such instance exists") # noqa: F821 if isinstance(value, rfc1905.EndOfMibView): raise SNMPEndOfMibView("End of MIB was reached") # noqa: F821 def _convert(self, value): """Convert a PySNMP value to some native Python type""" try: # With PySNMP 4.3+, an OID is a ObjectIdentity. We try to # extract it while being compatible with earlier releases. value = value.getOid() except AttributeError: pass convertors = {rfc1902.Integer: int, rfc1902.Integer32: int, rfc1902.OctetString: bytes, rfc1902.IpAddress: ipaddress.IPv4Address, rfc1902.Counter32: int, rfc1902.Counter64: int, rfc1902.Gauge32: int, rfc1902.Unsigned32: int, rfc1902.TimeTicks: int, rfc1902.Bits: str, rfc1902.Opaque: str, rfc1902.univ.ObjectIdentifier: tuple} if self._none: convertors[rfc1905.NoSuchObject] = lambda x: None convertors[rfc1905.NoSuchInstance] = lambda x: None for cl, fn in convertors.items(): if isinstance(value, cl): return fn(value) self._check_exception(value) raise NotImplementedError("unable to convert {}".format(repr(value))) def _op(self, cmd, *oids): """Apply an SNMP operation""" kwargs = {} if self._contextname: kwargs['contextName'] = rfc1902.OctetString(self._contextname) errorIndication, errorStatus, errorIndex, varBinds = cmd( self._auth, self._transport, *oids, **kwargs) if errorIndication: self._check_exception(errorIndication) raise SNMPException(str(errorIndication)) if errorStatus: # We try to find a builtin exception with the same message exc = str(errorStatus.prettyPrint()) exc = re.sub(r'\W+', '', exc) exc = "SNMP{}".format(exc[0].upper() + exc[1:]) if str(exc) in globals(): raise globals()[exc] raise SNMPException(errorStatus.prettyPrint()) if cmd in [self._cmdgen.getCmd, self._cmdgen.setCmd]: results = [(tuple(name), val) for name, val in varBinds] else: results = [(tuple(name), val) for row in varBinds for name, val in row] if len(results) > 0 and isinstance(results[-1][1], rfc1905.EndOfMibView): results = results[:-1] if len(results) == 0: if cmd not in [self._cmdgen.nextCmd, self._cmdgen.bulkCmd]: raise SNMPException("empty answer") return tuple([(oid, self._convert(val)) for oid, val in results]) def get(self, *oids): """Retrieve an OID value using GET. :param oids: a list of OID to retrieve. An OID is a tuple. :return: a list of tuples with the retrieved OID and the raw value. """ return self._op(self._cmdgen.getCmd, *oids) def walkmore(self, *oids): """Retrieve OIDs values using GETBULK or GETNEXT. The method is called "walk" but this is either a GETBULK or a GETNEXT. The later is only used for SNMPv1 or if bulk has been disabled using :meth:`bulk` property. :param oids: a list of OID to retrieve. An OID is a tuple. :return: a list of tuples with the retrieved OID and the raw value. """ if self._version == 1 or not self.bulk: return self._op(self._cmdgen.nextCmd, *oids) args = [0, self.bulk] + list(oids) try: return self._op(self._cmdgen.bulkCmd, *args) except SNMPTooBig: # Let's try to ask for less values. We will never increase # bulk again. We cannot increase it just after the walk # because we may end up requesting everything twice (or # more). nbulk = self.bulk / 2 or False if nbulk != self.bulk: self.bulk = nbulk return self.walk(*oids) raise def walk(self, *oids): """Walk from given OIDs but don't return any "extra" results. Only results in the subtree will be returned. :param oid: OIDs used as a start point :return: a list of tuples with the retrieved OID and the raw value. """ return ((noid, result) for oid in oids for noid, result in self.walkmore(oid) if (len(noid) >= len(oid) and noid[:len(oid)] == oid[:len(oid)])) def set(self, *args): """Set an OID value using SET. This function takes an odd number of arguments. They are working by pair. The first member is an OID and the second one is :class:`basictypes.Type` instace whose `pack()` method will be used to transform into the appropriate form. :return: a list of tuples with the retrieved OID and the raw value. """ if len(args) % 2 != 0: raise ValueError("expect an even number of arguments for SET") varbinds = zip(*[args[0::2], [v.pack() for v in args[1::2]]]) return self._op(self._cmdgen.setCmd, *varbinds) def __repr__(self): return "{}(host={},version={})".format( self.__class__.__name__, self._host, self._version) @property def timeout(self): """Get timeout value for the current session. :return: Timeout value in microseconds. """ return self._transport.timeout * 1000000 @timeout.setter def timeout(self, value): """Set timeout value for the current session. :param value: Timeout value in microseconds. """ value = int(value) if value <= 0: raise ValueError("timeout is a positive integer") self._transport.timeout = value / 1000000. @property def retries(self): """Get number of times a request is retried. :return: Number of retries for each request. """ return self._transport.retries @retries.setter def retries(self, value): """Set number of times a request is retried. :param value: Number of retries for each request. """ value = int(value) if value < 0: raise ValueError("retries is a non-negative integer") self._transport.retries = value @property def bulk(self): """Get bulk settings. :return: `False` if bulk is disabled or a non-negative integer for the number of repetitions. """ return self._bulk @bulk.setter def bulk(self, value): """Set bulk settings. :param value: `False` to disable bulk or a non-negative integer for the number of allowed repetitions. """ if value is False: self._bulk = False return value = int(value) if value <= 0: raise ValueError("{} is not an appropriate value " "for max repeater parameter".format( value)) self._bulk = value ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8538616 snimpy-1.0.0/snimpy.egg-info/0000755000076400001440000000000000000000000015237 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302682.0 snimpy-1.0.0/snimpy.egg-info/PKG-INFO0000644000076400001440000001755300000000000016347 0ustar00bernatusersMetadata-Version: 2.1 Name: snimpy Version: 1.0.0 Summary: interactive SNMP tool Home-page: https://github.com/vincentbernat/snimpy Author: Vincent Bernat Author-email: bernat@luffy.cx License: UNKNOWN Description: =============================== snimpy =============================== .. image:: https://badge.fury.io/py/snimpy.png :target: http://badge.fury.io/py/snimpy .. image:: https://github.com/vincentbernat/snimpy/workflows/Tests/badge.svg .. image:: https://coveralls.io/repos/vincentbernat/snimpy/badge.png :target: https://coveralls.io/r/vincentbernat/snimpy --- Interactive SNMP tool. *Snimpy* is a Python-based tool providing a simple interface to build SNMP query. Here is a very simplistic example that allows us to display the routing table of a given host:: load("IP-FORWARD-MIB") m=M("localhost", "public", 2) routes = m.ipCidrRouteNextHop for x in routes: net, netmask, tos, src = x print("%15s/%-15s via %-15s src %-15s" % (net, netmask, routes[x], src)) You can either use *Snimpy* interactively throught its console (derived from Python own console or from IPython_ if available) or write *Snimpy* scripts which are just Python scripts with some global variables available. .. _IPython: http://ipython.org * Free software: ISC license * Documentation: http://snimpy.rtfd.org. *Snimpy* requires libsmi_ to work correctly. See the documentation for more information. .. _libsmi: https://www.ibr.cs.tu-bs.de/projects/libsmi/ Features -------- *Snimpy* is aimed at being the more Pythonic possible. You should forget that you are doing SNMP requests. *Snimpy* will rely on MIB to hide SNMP details. Here are some "features": * MIB parser based on libsmi (through CFFI) * SNMP requests are handled by PySNMP (SNMPv1, SNMPv2 and SNMPv3 support) * scalars are just attributes of your session object * columns are like a Python dictionary and made available as an attribute * getting an attribute is like issuing a GET method * setting an attribute is like issuing a SET method * iterating over a table is like using GETNEXT * when something goes wrong, you get an exception History ------- 1.0.0 (2021-05-29) ++++++++++++++++++ * Drop compatibility with Python 2. 0.8.14 (2020-04-26) +++++++++++++++++++ * Add ``items()`` in addition to ``iteritems()`` This is an iterator on Python 3 and return a list of items in Python 2. 0.8.13 (2018-10-12) +++++++++++++++++++ * Compatibility with Python 3.7. * Fix an issue with implied index when reusing indexes between tables. 0.8.12 (2017-10-02) +++++++++++++++++++ * Support for more recent versions of IPython. * Support for SNMPv3 context name. * Support for notification nodes (MIB only). 0.8.11 (2016-08-13) +++++++++++++++++++ * Fix IPython interactive shell. * Fix IPv6 handling for sessions. * Ability for a session to return None instead of raising an exception. 0.8.10 (2016-02-16) +++++++++++++++++++ * Ability to walk a table (if the first index is accessible). * Ability to do a partial walk (courtesy of Alex Unigovsky). 0.8.8 (2015-11-15) ++++++++++++++++++ * Fix thread-safety problem introduced in 0.8.6. This also undo any improvement advertised in 0.8.6 when using multiple threads. However, performance should be kept when using a single thread. 0.8.7 (2015-11-14) ++++++++++++++++++ * Ability to specify a module name when querying a manager. * Compatibility with PySNMP 4.3 * Array-like interface for OIDs. * Ability to restrict lookups to a specific MIB: m['IF-MIB'].ifDescr. * Fix multithread support with SNMPv3 (with a performance impact). 0.8.6 (2015-06-24) ++++++++++++++++++ * Major speed improvement. * Major memory usage improvement. 0.8.5 (2015-04-04) ++++++++++++++++++ * Ability to set SMI search path (with ``mib.path()``) * Fix documentation build on *Read the Doc*. * Add a loose mode to manager to loosen type coercion. 0.8.4 (2015-02-10) ++++++++++++++++++ * More CFFI workarounds, including cross-compilation support. * Ability to override a node type. * Automatic workaround for "SNMP too big" error message. 0.8.3 (2014-08-18) ++++++++++++++++++ * IPv6 support. 0.8.2 (2014-06-08) ++++++++++++++++++ * Minor bugfixes. 0.8.1 (2013-10-25) ++++++++++++++++++ * Workaround a problem with CFFI extension installation. 0.8.0 (2013-10-19) ++++++++++++++++++++ * Python 3.3 support. Pypy support. * PEP8 compliant. * Sphinx documentation. * Octet strings with a display hint are now treated differently than plain octet strings (unicode). Notably, they can now be set using the displayed format (for example, for MAC addresses). 0.7.0 (2013-09-23) ++++++++++++++++++ * Major rewrite. * SNMP support is now provided through PySNMP_. * MIB parsing is still done with ``libsmi`` but through CFFI instead of a C module. * More unittests. Many bugfixes. .. _PySNMP: http://pysnmp.sourceforge.net/ 0.6.4 (2013-03-21) ++++++++++++++++++ * GETBULK support. * MacAddress SMI type support. 0.6.3 (2012-04-13) ++++++++++++++++++ * Support for IPython 0.12. * Minor bugfixes. 0.6.2 (2012-01-19) ++++++++++++++++++ * Ability to return None instead of getting an exception. 0.6.1 (2012-01-14) ++++++++++++++++++ * Thread safety and efficiency. 0.6 (2012-01-10) ++++++++++++++++++ * SNMPv3 support 0.5.1 (2011-08-07) ++++++++++++++++++ * Compatibility with IPython 0.11. * Custom timeouts and retries. 0.5 (2010-02-03) ++++++++++++++++++ * Check conformity of loaded modules. * Many bugfixes. 0.4 (2009-06-06) ++++++++++++++++++ * Allow to cache requests. 0.3 (2008-11-23) ++++++++++++++++++ * Provide a manual page. * Use a context manager to group SET requests. 0.2.1 (2008-09-28) ++++++++++++++++++ * First release on PyPI. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: ISC License (ISCL) Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 3 Classifier: Topic :: System :: Networking Classifier: Topic :: Utilities Classifier: Topic :: System :: Monitoring Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302682.0 snimpy-1.0.0/snimpy.egg-info/SOURCES.txt0000644000076400001440000000274700000000000017135 0ustar00bernatusers.gitignore .gitmodules AUTHORS.rst CONTRIBUTING.rst HISTORY.rst MANIFEST.in Makefile README.rst setup.py tox.ini version.txt .github/workflows/tests.yml docs/Makefile docs/api.rst docs/conf.py docs/contributing.rst docs/history.rst docs/index.rst docs/installation.rst docs/license.rst docs/usage.rst docs/_static/snimpy.svg docs/_themes/.gitignore docs/_themes/LICENSE docs/_themes/README docs/_themes/flask_theme_support.py docs/_themes/flask/layout.html docs/_themes/flask/relations.html docs/_themes/flask/theme.conf docs/_themes/flask/static/flasky.css_t docs/_themes/flask_small/layout.html docs/_themes/flask_small/theme.conf docs/_themes/flask_small/static/flasky.css_t examples/add-vlan.py examples/disable-port-ingress-filtering.py examples/enable-lldp.py examples/get-serial.py examples/list-interfaces.py examples/list-routes.py examples/rename-vlan.py examples/set-syslog-ntp.py examples/vlan-and-interfaces.py man/snimpy.1 snimpy/__init__.py snimpy/__main__.py snimpy/_version.py snimpy/basictypes.py snimpy/config.py snimpy/main.py snimpy/manager.py snimpy/mib.py snimpy/smi_build.py snimpy/snmp.py snimpy.egg-info/PKG-INFO snimpy.egg-info/SOURCES.txt snimpy.egg-info/dependency_links.txt snimpy.egg-info/entry_points.txt snimpy.egg-info/not-zip-safe snimpy.egg-info/requires.txt snimpy.egg-info/top_level.txt tests/SNIMPY-INVALID-MIB.mib tests/SNIMPY-MIB.mib tests/agent.py tests/test_basictypes.py tests/test_conf.py tests/test_main.py tests/test_manager.py tests/test_mib.py tests/test_snmp.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302682.0 snimpy-1.0.0/snimpy.egg-info/dependency_links.txt0000644000076400001440000000000100000000000021305 0ustar00bernatusers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302682.0 snimpy-1.0.0/snimpy.egg-info/entry_points.txt0000644000076400001440000000006100000000000020532 0ustar00bernatusers[console_scripts] snimpy = snimpy.main:interact ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1447520437.0 snimpy-1.0.0/snimpy.egg-info/not-zip-safe0000644000076400001440000000000100000000000017465 0ustar00bernatusers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302682.0 snimpy-1.0.0/snimpy.egg-info/requires.txt0000644000076400001440000000004100000000000017632 0ustar00bernatuserscffi>=1.0.0 pysnmp>=4 setuptools ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302682.0 snimpy-1.0.0/snimpy.egg-info/top_level.txt0000644000076400001440000000000700000000000017766 0ustar00bernatuserssnimpy ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1622302682.8538616 snimpy-1.0.0/tests/0000755000076400001440000000000000000000000013370 5ustar00bernatusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1479193129.0 snimpy-1.0.0/tests/SNIMPY-INVALID-MIB.mib0000644000076400001440000000116000000000000016567 0ustar00bernatusersSNIMPY-INVALID-MIB DEFINITIONS ::= BEGIN IMPORTS inexistentNode FROM INEXISTENT-SNIMPY-MIB ; invalidSnimpy MODULE-IDENTITY LAST-UPDATED "200809160000Z" ORGANIZATION "snimpy https://github.com/vincentbernat/snimpy" CONTACT-INFO "Lorem ipsum, etc, etc." DESCRIPTION "This is a test MIB module for snimpy." REVISION "200809160000Z" DESCRIPTION "Last revision" ::= { mib-2 45122 } invalidSnimpyNode OBJECT-TYPE SYNTAX INTEGER MAX-ACCESS read-only STATUS current DESCRIPTION "An integer" ::= { inexistentNode 1 } END ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1513432080.0 snimpy-1.0.0/tests/SNIMPY-MIB.mib0000644000076400001440000003277600000000000015524 0ustar00bernatusersSNIMPY-MIB DEFINITIONS ::= BEGIN IMPORTS MODULE-IDENTITY, OBJECT-TYPE, IpAddress, Integer32, Gauge32, TimeTicks, Counter64, Counter32, mib-2 FROM SNMPv2-SMI DisplayString, TEXTUAL-CONVENTION, PhysAddress, TruthValue FROM SNMPv2-TC InetAddressType, InetAddress, InetAddressIPv4, InetAddressIPv6 FROM INET-ADDRESS-MIB IANAifType FROM IANAifType-MIB; snimpy MODULE-IDENTITY LAST-UPDATED "200809160000Z" ORGANIZATION "snimpy https://github.com/vincentbernat/snimpy" CONTACT-INFO "Lorem ipsum, etc, etc." DESCRIPTION "This is a test MIB module for snimpy." REVISION "200809160000Z" DESCRIPTION "Last revision" ::= { mib-2 45121 } OddInteger ::= TEXTUAL-CONVENTION DISPLAY-HINT "d-2" STATUS current DESCRIPTION "Testing fmt" SYNTAX INTEGER (6..18 | 20..23 | 27 | 28..1336) UnicodeString ::= TEXTUAL-CONVENTION DISPLAY-HINT "255t" STATUS current DESCRIPTION "Testing fmt" SYNTAX OCTET STRING (SIZE(0..255)) snimpyScalars OBJECT IDENTIFIER ::= { snimpy 1 } snimpyTables OBJECT IDENTIFIER ::= { snimpy 2 } snimpyTraps OBJECT IDENTIFIER ::= { snimpy 3 } snimpyIpAddress OBJECT-TYPE SYNTAX IpAddress MAX-ACCESS read-only STATUS current DESCRIPTION "An IP address" ::= { snimpyScalars 1 } snimpyString OBJECT-TYPE SYNTAX DisplayString (SIZE (0..255)) MAX-ACCESS read-only STATUS current DESCRIPTION "An string to display" ::= { snimpyScalars 2 } snimpyInteger OBJECT-TYPE SYNTAX OddInteger MAX-ACCESS read-only STATUS current DESCRIPTION "An integer" ::= { snimpyScalars 3 } snimpyEnum OBJECT-TYPE SYNTAX INTEGER { up(1), down(2), testing(3) } MAX-ACCESS read-only STATUS current DESCRIPTION "An enumeration" ::= { snimpyScalars 4 } snimpyObjectId OBJECT-TYPE SYNTAX OBJECT IDENTIFIER MAX-ACCESS read-only STATUS current DESCRIPTION "An oid" ::= { snimpyScalars 5 } snimpyBoolean OBJECT-TYPE SYNTAX TruthValue MAX-ACCESS read-only STATUS current DESCRIPTION "A boolean" ::= { snimpyScalars 6 } snimpyCounter OBJECT-TYPE SYNTAX Counter32 MAX-ACCESS read-only STATUS current DESCRIPTION "A 32 bits counter" ::= { snimpyScalars 7 } snimpyGauge OBJECT-TYPE SYNTAX Gauge32 MAX-ACCESS read-only STATUS current DESCRIPTION "A 32 bits gauge" ::= { snimpyScalars 8 } snimpyTimeticks OBJECT-TYPE SYNTAX TimeTicks MAX-ACCESS read-only STATUS current DESCRIPTION "A timetick" ::= { snimpyScalars 9 } snimpyCounter64 OBJECT-TYPE SYNTAX Counter64 MAX-ACCESS read-only STATUS current DESCRIPTION "A 64-bit counter" ::= { snimpyScalars 10 } snimpyBits OBJECT-TYPE SYNTAX BITS { first(0), second(1), third(2), last(7), secondByte(8) } MAX-ACCESS read-only STATUS current DESCRIPTION "A bit field" ::= { snimpyScalars 11 } snimpyNotImplemented OBJECT-TYPE SYNTAX DisplayString (SIZE (0..255)) MAX-ACCESS read-only STATUS current DESCRIPTION "An string to display (not implemented)" ::= { snimpyScalars 12 } snimpyOctetString OBJECT-TYPE SYNTAX OCTET STRING MAX-ACCESS read-only STATUS current DESCRIPTION "An string to display" ::= { snimpyScalars 13 } snimpyUnicodeString OBJECT-TYPE SYNTAX UnicodeString MAX-ACCESS read-only STATUS current DESCRIPTION "An unicode string to display" ::= { snimpyScalars 14 } snimpyMacAddress OBJECT-TYPE SYNTAX PhysAddress MAX-ACCESS read-only STATUS current DESCRIPTION "A MAC address" ::= { snimpyScalars 15 } snimpyMacAddressInvalid OBJECT-TYPE SYNTAX DisplayString MAX-ACCESS read-only STATUS current DESCRIPTION "A MAC address with invalid syntax" ::= { snimpyScalars 16 } -- A simple table snimpySimpleTable OBJECT-TYPE SYNTAX SEQUENCE OF SnimpySimpleEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "A table" ::= { snimpyTables 1 } snimpySimpleEntry OBJECT-TYPE SYNTAX SnimpySimpleEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "Entry for our simple table" INDEX { snimpySimpleIndex } ::= { snimpySimpleTable 1 } SnimpySimpleEntry ::= SEQUENCE { snimpySimpleIndex Integer32, snimpySimpleDescr DisplayString, snimpySimpleType IANAifType, snimpySimplePhys PhysAddress } snimpySimpleIndex OBJECT-TYPE SYNTAX Integer32 (1..30) MAX-ACCESS not-accessible STATUS current DESCRIPTION "Index for snimpy simple table" ::= { snimpySimpleEntry 1 } snimpySimpleDescr OBJECT-TYPE SYNTAX DisplayString (SIZE (0..255)) MAX-ACCESS read-only STATUS current DESCRIPTION "Blah blah" ::= { snimpySimpleEntry 2 } snimpySimpleType OBJECT-TYPE SYNTAX IANAifType MAX-ACCESS read-only STATUS current DESCRIPTION "Blah blah" ::= { snimpySimpleEntry 3 } snimpySimplePhys OBJECT-TYPE SYNTAX PhysAddress MAX-ACCESS read-only STATUS current DESCRIPTION "Blah blah" ::= { snimpySimpleEntry 4 } -- A more complex table snimpyComplexTable OBJECT-TYPE SYNTAX SEQUENCE OF SnimpyComplexEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "A more complex table" ::= { snimpyTables 2 } snimpyComplexEntry OBJECT-TYPE SYNTAX SnimpyComplexEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "Entry for our complex table" INDEX { snimpyComplexFirstIP, snimpyComplexSecondIP } ::= { snimpyComplexTable 1 } SnimpyComplexEntry ::= SEQUENCE { snimpyComplexFirstIP IpAddress, snimpyComplexSecondIP IpAddress, snimpyComplexState INTEGER } snimpyComplexFirstIP OBJECT-TYPE SYNTAX IpAddress MAX-ACCESS not-accessible STATUS current DESCRIPTION "First IP address for index" ::= { snimpyComplexEntry 1 } snimpyComplexSecondIP OBJECT-TYPE SYNTAX IpAddress MAX-ACCESS not-accessible STATUS current DESCRIPTION "Second IP address for index" ::= { snimpyComplexEntry 2 } snimpyComplexState OBJECT-TYPE SYNTAX INTEGER { up(1), down(2), testing(3) } MAX-ACCESS read-only STATUS current DESCRIPTION "State for our both IP" ::= { snimpyComplexEntry 3 } -- A table with complex indexes snimpyIndexTable OBJECT-TYPE SYNTAX SEQUENCE OF SnimpyIndexEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "A table with complex indexes" ::= { snimpyTables 3 } snimpyIndexEntry OBJECT-TYPE SYNTAX SnimpyIndexEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "Entry for our indexed table" INDEX { snimpyIndexVarLen, snimpyIndexOidVarLen, snimpyIndexFixedLen, IMPLIED snimpyIndexImplied } ::= { snimpyIndexTable 1 } SnimpyIndexEntry ::= SEQUENCE { snimpyIndexVarLen DisplayString, snimpyIndexIntIndex Integer32, snimpyIndexOidVarLen OBJECT IDENTIFIER, snimpyIndexFixedLen DisplayString, snimpyIndexImplied DisplayString, snimpyIndexInt Integer32 } snimpyIndexVarLen OBJECT-TYPE SYNTAX DisplayString (SIZE (1..10)) MAX-ACCESS read-only STATUS current DESCRIPTION "Variable length index" ::= { snimpyIndexEntry 1 } snimpyIndexIntIndex OBJECT-TYPE SYNTAX Integer32 MAX-ACCESS not-accessible STATUS current DESCRIPTION "Integer index" ::= { snimpyIndexEntry 2 } snimpyIndexOidVarLen OBJECT-TYPE SYNTAX OBJECT IDENTIFIER MAX-ACCESS not-accessible STATUS current DESCRIPTION "OID as index" ::= { snimpyIndexEntry 3 } snimpyIndexFixedLen OBJECT-TYPE SYNTAX DisplayString (SIZE (6)) MAX-ACCESS not-accessible STATUS current DESCRIPTION "Fixed length index" ::= { snimpyIndexEntry 4 } snimpyIndexImplied OBJECT-TYPE SYNTAX DisplayString (SIZE (1..30)) MAX-ACCESS not-accessible STATUS current DESCRIPTION "Variable length index, implied" ::= { snimpyIndexEntry 5 } snimpyIndexInt OBJECT-TYPE SYNTAX Integer32 MAX-ACCESS read-only STATUS current DESCRIPTION "An integer of fixed size" ::= { snimpyIndexEntry 6 } -- A table indexed using InetAddresses snimpyInetAddressTable OBJECT-TYPE SYNTAX SEQUENCE OF SnimpyInetAddressEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "A InetAddress table" ::= { snimpyTables 4 } snimpyInetAddressEntry OBJECT-TYPE SYNTAX SnimpyInetAddressEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "Entry for our complex table" INDEX { snimpyInetAddressType, snimpyInetAddress } ::= { snimpyInetAddressTable 1 } SnimpyInetAddressEntry ::= SEQUENCE { snimpyInetAddressType InetAddressType, snimpyInetAddress InetAddress, snimpyInetAddressState INTEGER } snimpyInetAddressType OBJECT-TYPE SYNTAX InetAddressType MAX-ACCESS not-accessible STATUS current DESCRIPTION "Address type identifier for snimpyInetAddress" ::= { snimpyInetAddressEntry 1 } snimpyInetAddress OBJECT-TYPE SYNTAX InetAddress MAX-ACCESS not-accessible STATUS current DESCRIPTION "Type dependent InetAddress" ::= { snimpyInetAddressEntry 2 } snimpyInetAddressState OBJECT-TYPE SYNTAX INTEGER { up(1), down(2), testing(3) } MAX-ACCESS read-only STATUS current DESCRIPTION "State for the IP" ::= { snimpyInetAddressEntry 3 } -- A table that may contain invalid values snimpyInvalidTable OBJECT-TYPE SYNTAX SEQUENCE OF SnimpyInvalidEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "A table" ::= { snimpyTables 5 } snimpyInvalidEntry OBJECT-TYPE SYNTAX SnimpyInvalidEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "Entry for our invalid table" INDEX { snimpyInvalidIndex } ::= { snimpyInvalidTable 1 } SnimpyInvalidEntry ::= SEQUENCE { snimpyInvalidIndex Integer32, snimpyInvalidDescr DisplayString } snimpyInvalidIndex OBJECT-TYPE SYNTAX Integer32 (1..30) MAX-ACCESS not-accessible STATUS current DESCRIPTION "Index for snimpy invalid table" ::= { snimpyInvalidEntry 1 } snimpyInvalidDescr OBJECT-TYPE SYNTAX DisplayString (SIZE (0..255)) MAX-ACCESS read-only STATUS current DESCRIPTION "Blah blah" ::= { snimpyInvalidEntry 2 } -- A table that may be empty snimpyEmptyTable OBJECT-TYPE SYNTAX SEQUENCE OF SnimpyEmptyEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "A table" ::= { snimpyTables 6 } snimpyEmptyEntry OBJECT-TYPE SYNTAX SnimpyEmptyEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "Entry for our empty table" INDEX { snimpyEmptyIndex } ::= { snimpyEmptyTable 1 } SnimpyEmptyEntry ::= SEQUENCE { snimpyEmptyIndex Integer32, snimpyEmptyDescr DisplayString } snimpyEmptyIndex OBJECT-TYPE SYNTAX Integer32 (1..30) MAX-ACCESS not-accessible STATUS current DESCRIPTION "Index for snimpy empty table" ::= { snimpyEmptyEntry 1 } snimpyEmptyDescr OBJECT-TYPE SYNTAX DisplayString (SIZE (0..255)) MAX-ACCESS read-only STATUS current DESCRIPTION "Blah blah" ::= { snimpyEmptyEntry 2 } -- A table whose index re-uses objects from other tables' indexes snimpyReuseIndexTable OBJECT-TYPE SYNTAX SEQUENCE OF SnimpyReuseIndexEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "A table whose index re-uses objects from other tables' indexes" ::= { snimpyTables 7 } snimpyReuseIndexEntry OBJECT-TYPE SYNTAX SnimpyReuseIndexEntry MAX-ACCESS not-accessible STATUS current DESCRIPTION "Entry for our snimpyReuseIndexTable table note that snimpyIndexImplied is not the last item so it cannot be implied" INDEX { snimpyIndexImplied, snimpySimpleIndex } ::= { snimpyReuseIndexTable 1 } SnimpyReuseIndexEntry ::= SEQUENCE { snimpyReuseIndexValue INTEGER } snimpyReuseIndexValue OBJECT-TYPE SYNTAX INTEGER MAX-ACCESS read-only STATUS current DESCRIPTION "Integer value" ::= { snimpyReuseIndexEntry 1 } -- A notification snimpyNotification NOTIFICATION-TYPE OBJECTS { snimpySimpleIndex, snimpySimpleDescr, snimpySimpleType, snimpySimplePhys, snimpyIpAddress } STATUS current DESCRIPTION "A notification" ::= { snimpyTraps 1 } END ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/tests/agent.py0000644000076400001440000004420100000000000015041 0ustar00bernatusersfrom multiprocessing import Process, Queue import random from pysnmp.entity import engine, config from pysnmp.entity.rfc3413 import cmdrsp, context from pysnmp.carrier.asynsock.dgram import udp, udp6 from pysnmp.proto.api import v2c class TestAgent: next_port = [random.randint(22000, 32000)] """Agent for testing purpose""" def __init__(self, ipv6=False, community='public', authpass='authpass', privpass='privpass', emptyTable=True): q = Queue() self.ipv6 = ipv6 self.emptyTable = emptyTable self.community = community self.authpass = authpass self.privpass = privpass self.next_port[0] += 1 self._process = Process(target=self._setup, args=(q, self.next_port[0])) self._process.start() self.port = q.get() def terminate(self): self._process.terminate() def _setup(self, q, port): """Setup a new agent in a separate process. The port the agent is listening too will be returned using the provided queue. """ snmpEngine = engine.SnmpEngine() if self.ipv6: config.addSocketTransport( snmpEngine, udp6.domainName, udp6.Udp6Transport().openServerMode(('::1', port))) else: config.addSocketTransport( snmpEngine, udp.domainName, udp.UdpTransport().openServerMode(('127.0.0.1', port))) # Community is public and MIB is writable config.addV1System(snmpEngine, 'read-write', self.community) config.addVacmUser(snmpEngine, 1, 'read-write', 'noAuthNoPriv', (1, 3, 6), (1, 3, 6)) config.addVacmUser(snmpEngine, 2, 'read-write', 'noAuthNoPriv', (1, 3, 6), (1, 3, 6)) config.addV3User( snmpEngine, 'read-write', config.usmHMACMD5AuthProtocol, self.authpass, config.usmAesCfb128Protocol, self.privpass) config.addVacmUser(snmpEngine, 3, 'read-write', 'authPriv', (1, 3, 6), (1, 3, 6)) # Build MIB def stringToOid(string): return [ord(x) for x in string] def flatten(*args): result = [] for el in args: if isinstance(el, (list, tuple)): for sub in el: result.append(sub) else: result.append(el) return tuple(result) snmpContext = context.SnmpContext(snmpEngine) mibBuilder = snmpContext.getMibInstrum().getMibBuilder() (MibTable, MibTableRow, MibTableColumn, MibScalar, MibScalarInstance) = mibBuilder.importSymbols( 'SNMPv2-SMI', 'MibTable', 'MibTableRow', 'MibTableColumn', 'MibScalar', 'MibScalarInstance') class RandomMibScalarInstance(MibScalarInstance): previous_value = 0 def getValue(self, name, idx): self.previous_value += random.randint(1, 2000) return self.getSyntax().clone(self.previous_value) mibBuilder.exportSymbols( '__MY_SNMPv2_MIB', # SNMPv2-MIB::sysDescr MibScalar((1, 3, 6, 1, 2, 1, 1, 1), v2c.OctetString()), MibScalarInstance((1, 3, 6, 1, 2, 1, 1, 1), (0,), v2c.OctetString( "Snimpy Test Agent {}".format( self.community))), # SNMPv2-MIB::sysObjectID MibScalar((1, 3, 6, 1, 2, 1, 1, 2), v2c.ObjectIdentifier()), MibScalarInstance((1, 3, 6, 1, 2, 1, 1, 2), (0,), v2c.ObjectIdentifier((1, 3, 6, 1, 4, 1, 9, 1, 1208)))) mibBuilder.exportSymbols( '__MY_IF_MIB', # IF-MIB::ifNumber MibScalar((1, 3, 6, 1, 2, 1, 2, 1), v2c.Integer()), MibScalarInstance((1, 3, 6, 1, 2, 1, 2, 1), (0,), v2c.Integer(3)), # IF-MIB::ifTable MibTable((1, 3, 6, 1, 2, 1, 2, 2)), MibTableRow((1, 3, 6, 1, 2, 1, 2, 2, 1)).setIndexNames( (0, '__MY_IF_MIB', 'ifIndex')), # IF-MIB::ifIndex MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 1), (1,), v2c.Integer(1)), MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 1), (2,), v2c.Integer(2)), MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 1), (3,), v2c.Integer(3)), # IF-MIB::ifDescr MibTableColumn((1, 3, 6, 1, 2, 1, 2, 2, 1, 2), v2c.OctetString()), MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 2), (1,), v2c.OctetString("lo")), MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 2), (2,), v2c.OctetString("eth0")), MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 2), (3,), v2c.OctetString("eth1")), # IF-MIB::ifType MibTableColumn((1, 3, 6, 1, 2, 1, 2, 2, 1, 3), v2c.Integer()), MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 3), (1,), v2c.Integer(24)), MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 3), (2,), v2c.Integer(6)), MibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 3), (3,), v2c.Integer(6)), # IF-MIB::ifInOctets MibTableColumn((1, 3, 6, 1, 2, 1, 2, 2, 1, 10), v2c.Integer()), RandomMibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 10), (1,), v2c.Gauge32()), RandomMibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 10), (2,), v2c.Gauge32()), RandomMibScalarInstance( (1, 3, 6, 1, 2, 1, 2, 2, 1, 10), (3,), v2c.Gauge32()), # IF-MIB::ifRcvAddressTable MibTable((1, 3, 6, 1, 2, 1, 31, 1, 4)), MibTableRow((1, 3, 6, 1, 2, 1, 31, 1, 4, 1)).setIndexNames( (0, '__MY_IF_MIB', 'ifIndex'), (1, '__MY_IF_MIB', 'ifRcvAddressAddress')), # IF-MIB::ifRcvAddressStatus MibTableColumn((1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 2), v2c.Integer()), MibScalarInstance( (1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 2), flatten(2, 6, stringToOid("abcdef")), v2c.Integer(1)), MibScalarInstance( (1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 2), flatten(2, 6, stringToOid("ghijkl")), v2c.Integer(1)), MibScalarInstance( (1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 2), flatten(3, 6, stringToOid("mnopqr")), v2c.Integer(1)), # IF-MIB::ifRcvAddressType MibTableColumn((1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 3), v2c.Integer()), MibScalarInstance( (1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 3), flatten(2, 6, stringToOid("abcdef")), v2c.Integer(1)), MibScalarInstance( (1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 3), flatten(2, 6, stringToOid("ghijkl")), v2c.Integer(1)), MibScalarInstance( (1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 3), flatten(3, 6, stringToOid("mnopqr")), v2c.Integer(1)), # IF-MIB::ifIndex ifIndex=MibTableColumn((1, 3, 6, 1, 2, 1, 2, 2, 1, 1), v2c.Integer()), # IF-MIB::ifRcvAddressAddress ifRcvAddressAddress=MibTableColumn((1, 3, 6, 1, 2, 1, 31, 1, 4, 1, 1), v2c.OctetString())) args = ( '__MY_SNIMPY-MIB', # SNIMPY-MIB::snimpyIpAddress MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 1), v2c.OctetString()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 1), (0,), v2c.OctetString("AAAA")), # SNIMPY-MIB::snimpyString MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 2), v2c.OctetString()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 2), (0,), v2c.OctetString("bye")), # SNIMPY-MIB::snimpyInteger MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 3), v2c.Integer()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 3), (0,), v2c.Integer(19)), # SNIMPY-MIB::snimpyEnum MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 4), v2c.Integer()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 4), (0,), v2c.Integer(2)), # SNIMPY-MIB::snimpyObjectId MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 5), v2c.ObjectIdentifier()).setMaxAccess("readwrite"), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 1, 5), ( 0,), v2c.ObjectIdentifier((1, 3, 6, 4454, 0, 0))), # SNIMPY-MIB::snimpyBoolean MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 6), v2c.Integer()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 6), (0,), v2c.Integer(1)), # SNIMPY-MIB::snimpyCounter MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 7), v2c.Counter32()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 7), (0,), v2c.Counter32(47)), # SNIMPY-MIB::snimpyGauge MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 8), v2c.Gauge32()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 8), (0,), v2c.Gauge32(18)), # SNIMPY-MIB::snimpyTimeticks MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 9), v2c.TimeTicks()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 9), (0,), v2c.TimeTicks(12111100)), # SNIMPY-MIB::snimpyCounter64 MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 10), v2c.Counter64()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 10), (0,), v2c.Counter64(2 ** 48 + 3)), # SNIMPY-MIB::snimpyBits MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 11), v2c.OctetString()).setMaxAccess("readwrite"), MibScalarInstance( (1, 3, 6, 1, 2, 1, 45121, 1, 11), (0,), v2c.OctetString(b"\xa0\x80")), # SNIMPY-MIB::snimpyMacAddress MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 15), v2c.OctetString()).setMaxAccess("readwrite"), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 1, 15), ( 0,), v2c.OctetString(b"\x11\x12\x13\x14\x15\x16")), # SNIMPY-MIB::snimpyMacAddressInvalid MibScalar((1, 3, 6, 1, 2, 1, 45121, 1, 16), v2c.OctetString()).setMaxAccess("readwrite"), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 1, 16), ( 0,), v2c.OctetString(b"\xf1\x12\x13\x14\x15\x16")), # SNIMPY-MIB::snimpyIndexTable MibTable((1, 3, 6, 1, 2, 1, 45121, 2, 3)), MibTableRow( (1, 3, 6, 1, 2, 1, 45121, 2, 3, 1)).setIndexNames( (0, "__MY_SNIMPY-MIB", "snimpyIndexVarLen"), (0, "__MY_SNIMPY-MIB", "snimpyIndexOidVarLen"), (0, "__MY_SNIMPY-MIB", "snimpyIndexFixedLen"), (1, "__MY_SNIMPY-MIB", "snimpyIndexImplied")), # SNIMPY-MIB::snimpyIndexVarLen MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 1), flatten(4, stringToOid('row1'), 3, 1, 2, 3, stringToOid('alpha5'), stringToOid('end of row1')), v2c.OctetString(b"row1")), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 1), flatten(4, stringToOid('row2'), 4, 1, 0, 2, 3, stringToOid('beta32'), stringToOid('end of row2')), v2c.OctetString(b"row2")), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 1), flatten(4, stringToOid('row3'), 4, 120, 1, 2, 3, stringToOid('gamma7'), stringToOid('end of row3')), v2c.OctetString(b"row3")), # SNIMPY-MIB::snimpyIndexInt MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 6), flatten(4, stringToOid('row1'), 3, 1, 2, 3, stringToOid('alpha5'), stringToOid('end of row1')), v2c.Integer(4571)), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 6), flatten(4, stringToOid('row2'), 4, 1, 0, 2, 3, stringToOid('beta32'), stringToOid('end of row2')), v2c.Integer(78741)), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 6), flatten(4, stringToOid('row3'), 4, 120, 1, 2, 3, stringToOid('gamma7'), stringToOid('end of row3')), v2c.Integer(4110)), # SNIMPY-MIB::snimpyReuseIndexTable MibTable((1, 3, 6, 1, 2, 1, 45121, 2, 7)), MibTableRow( (1, 3, 6, 1, 2, 1, 45121, 2, 7, 1)).setIndexNames( (0, "__MY_SNIMPY-MIB", "snimpyIndexImplied"), (0, "__MY_SNIMPY-MIB", "snimpySimpleIndex")), # SNIMPY-MIB::snimpyReuseIndexValue MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 7, 1, 1), flatten(11, stringToOid('end of row1'), 4), v2c.Integer(1785)), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 7, 1, 1), flatten(11, stringToOid('end of row1'), 5), v2c.Integer(2458)), # SNIMPY-MIB::snimpyInvalidTable MibTable((1, 3, 6, 1, 2, 1, 45121, 2, 5)), MibTableRow( (1, 3, 6, 1, 2, 1, 45121, 2, 5, 1)).setIndexNames( (0, "__MY_SNIMPY-MIB", "snimpyInvalidIndex")), # SNIMPY-MIB::snimpyInvalidDescr MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 5, 1, 2), (1,), v2c.OctetString(b"Hello")), MibScalarInstance((1, 3, 6, 1, 2, 1, 45121, 2, 5, 1, 2), (2,), v2c.OctetString(b"\xf1\x12\x13\x14\x15\x16"))) if self.emptyTable: args += ( # SNIMPY-MIB::snimpyEmptyTable MibTable((1, 3, 6, 1, 2, 1, 45121, 2, 6)), MibTableRow( (1, 3, 6, 1, 2, 1, 45121, 2, 6, 1)).setIndexNames( (0, "__MY_SNIMPY-MIB", "snimpyEmptyIndex"))) kwargs = dict( # Indexes snimpyIndexVarLen=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 1), v2c.OctetString( )), snimpyIndexIntIndex=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 2), v2c.Integer( )).setMaxAccess( "noaccess"), snimpyIndexOidVarLen=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 3), v2c.ObjectIdentifier( )).setMaxAccess( "noaccess"), snimpyIndexFixedLen=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 4), v2c.OctetString( ).setFixedLength( 6)).setMaxAccess( "noaccess"), snimpyIndexImplied=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 5), v2c.OctetString( )).setMaxAccess("noaccess"), snimpyIndexInt=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 3, 1, 6), v2c.Integer()).setMaxAccess("readwrite"), snimpyInvalidIndex=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 5, 1, 1), v2c.Integer()).setMaxAccess("noaccess"), snimpyInvalidDescr=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 5, 1, 2), v2c.OctetString()).setMaxAccess("readwrite"), snimpyReuseIndexValue=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 7, 1, 1), v2c.Integer()).setMaxAccess("readwrite") ) if self.emptyTable: kwargs.update(dict( snimpyEmptyIndex=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 6, 1, 1), v2c.Integer()).setMaxAccess("noaccess"), snimpyEmptyDescr=MibTableColumn( (1, 3, 6, 1, 2, 1, 45121, 2, 6, 1, 2), v2c.OctetString()).setMaxAccess("readwrite"))) mibBuilder.exportSymbols(*args, **kwargs) # Start agent cmdrsp.GetCommandResponder(snmpEngine, snmpContext) cmdrsp.SetCommandResponder(snmpEngine, snmpContext) cmdrsp.NextCommandResponder(snmpEngine, snmpContext) cmdrsp.BulkCommandResponder(snmpEngine, snmpContext) q.put(port) snmpEngine.transportDispatcher.jobStarted(1) snmpEngine.transportDispatcher.runDispatcher() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/tests/test_basictypes.py0000644000076400001440000006406000000000000017155 0ustar00bernatusersimport unittest import os import re import socket import mock import ipaddress from datetime import timedelta from snimpy import mib, basictypes from pysnmp.proto import rfc1902 class TestBasicTypes(unittest.TestCase): def setUp(self): mib.load(os.path.join(os.path.dirname(os.path.abspath(__file__)), "SNIMPY-MIB.mib")) def tearDown(self): mib.reset() def testInteger(self): """Test integer basic type""" a = basictypes.build("SNIMPY-MIB", "snimpyInteger", 18) self.assertTrue(isinstance(a, basictypes.Integer)) self.assertEqual(a, 18) self.assertEqual(a + 10, 28) a = basictypes.build("SNIMPY-MIB", "snimpyInteger", 4) self.assertEqual(a, 4) self.assertEqual(a * 4, 16) a = basictypes.build("SNIMPY-MIB", "snimpyInteger", 5) self.assertEqual(a, 5) self.assertTrue(a < 6) # self.assertTrue(a > 4.6) # type coercion does not work self.assertTrue(a > 4) self.assertRaises(TypeError, basictypes.build, ("SNIMPY-MIB", "snimpyInteger", [1, 2, 3])) def testString(self): """Test string basic type""" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"hello") self.assertTrue(isinstance(a, basictypes.String)) self.assertEqual(a, "hello") self.assertEqual(a + " john", "hello john") self.assertEqual(a * 2, "hellohello") a = basictypes.build("SNIMPY-MIB", "snimpyString", b"hello john") self.assertTrue("john" in a) self.assertTrue("steve" not in a) self.assertEqual(a[1], 'e') self.assertEqual(a[1:4], 'ell') self.assertEqual(len(a), 10) def testStringFromBytes(self): """Test string basic type when built from bytes""" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"hello") self.assertTrue(isinstance(a, basictypes.String)) self.assertEqual(a, "hello") self.assertEqual(a + " john", "hello john") self.assertEqual(a * 2, "hellohello") def testStringEncoding(self): """Test we can create an UTF-8 encoded string""" a = basictypes.build("SNIMPY-MIB", "snimpyString", "hello") self.assertEqual(a, "hello") a = basictypes.build( "SNIMPY-MIB", "snimpyUnicodeString", "\U0001F60E Hello") self.assertEqual(a, "\U0001F60E Hello") a = basictypes.build( "SNIMPY-MIB", "snimpyUnicodeString", b'\xf0\x9f\x98\x8e Hello') self.assertEqual(a, "\U0001F60E Hello") self.assertRaises(UnicodeError, basictypes.build, "SNIMPY-MIB", "snimpyString", b'\xf0\x9f\x98\x8e Hello') def testOctetString(self): """Test octet string basic type""" a = basictypes.build("SNIMPY-MIB", "snimpyOctetString", b"hello\x41") self.assertTrue(isinstance(a, basictypes.OctetString)) self.assertEqual(a, b"hello\x41") self.assertEqual(len(a), 6) def testIpAddress(self): """Test IP address basic type""" a = basictypes.build( "SNIMPY-MIB", "snimpyIpAddress", socket.inet_aton("10.0.4.5")) self.assertTrue(isinstance(a, basictypes.IpAddress)) self.assertEqual(a, "10.0.4.5") self.assertEqual(a, [10, 0, 4, 5]) self.assertEqual(a[2], 4) self.assertTrue(a < "10.1.2.4") self.assertTrue(a > "10.0.0.1") a = basictypes.build("SNIMPY-MIB", "snimpyIpAddress", [1, 2, 3, 5]) self.assertEqual(a, "1.2.3.5") a = basictypes.build("SNIMPY-MIB", "snimpyIpAddress", "10.0.4.5") self.assertEqual(a, "10.0.4.5") self.assertEqual(a, [10, 0, 4, 5]) a = basictypes.build("SNIMPY-MIB", "snimpyIpAddress", b"1001") self.assertEqual(a, [49, 48, 48, 49]) a = basictypes.build("SNIMPY-MIB", "snimpyIpAddress", b"0101") self.assertEqual(a, [48, 49, 48, 49]) a = basictypes.build("SNIMPY-MIB", "snimpyIpAddress", ipaddress.IPv4Address("1.2.3.4")) self.assertEqual(a, [1, 2, 3, 4]) @unittest.expectedFailure def testIpAddressXFail(self): """Test incomplete IP addresses.""" # While 100 could be expanded to 0.0.0.100, # ipaddress.IPv4Address does not accept it. It's not our job # to be more Pythonic than Python. a = basictypes.build("SNIMPY-MIB", "snimpyIpAddress", "100") self.assertEqual(a, [0, 0, 0, 100]) def testIncorrectIpAddress(self): """Test inappropriate IP addresses""" self.assertRaises(ValueError, basictypes.build, "SNIMPY-MIB", "snimpyIpAddress", "999.5.6.4") self.assertRaises(ValueError, basictypes.build, "SNIMPY-MIB", "snimpyIpAddress", "AAA") self.assertRaises(ValueError, basictypes.build, "SNIMPY-MIB", "snimpyIpAddress", "AAACC") def testEnum(self): """Test enum basic type""" a = basictypes.build("SNIMPY-MIB", "snimpyEnum", 1) self.assertTrue(isinstance(a, basictypes.Enum)) self.assertEqual(a, 1) self.assertEqual(a, "up") a = basictypes.build("SNIMPY-MIB", "snimpyEnum", "down") self.assertEqual(a, "down") self.assertTrue(a != "up") self.assertEqual(a, 2) self.assertEqual(str(a), "down(2)") self.assertRaises(ValueError, basictypes.build, "SNIMPY-MIB", "snimpyEnum", "unknown") self.assertEqual(str(a), "down(2)") a = basictypes.build("SNIMPY-MIB", "snimpyEnum", 54) self.assertEqual(a, 54) def testOid(self): """Test OID basic type""" a = basictypes.build("SNIMPY-MIB", "snimpyObjectId", mib.get("SNIMPY-MIB", "snimpyInteger")) self.assertTrue(isinstance(a, basictypes.Oid)) self.assertEqual(a, mib.get("SNIMPY-MIB", "snimpyInteger")) self.assertEqual(a, mib.get("SNIMPY-MIB", "snimpyInteger").oid) # Suboid self.assertTrue((list(mib.get("SNIMPY-MIB", "snimpyInteger").oid) + [2, 3]) in a) self.assertTrue((list(mib.get("SNIMPY-MIB", "snimpyInteger").oid)[:-1] + [29, 3]) not in a) # Ability to extract a component self.assertEqual(a[0], 1) self.assertEqual(a[1], 3) self.assertEqual(a[-3], 45121) self.assertEqual(a[-1], 3) self.assertEqual(a[:3], (1, 3, 6)) # Also accepts list a = basictypes.build("SNIMPY-MIB", "snimpyObjectId", (1, 2, 3, 4)) self.assertEqual(a, (1, 2, 3, 4)) self.assertTrue((1, 2, 3, 4, 5) in a) self.assertTrue((3, 4, 5, 6) not in a) def testBoolean(self): """Test boolean basic type""" a = basictypes.build("SNIMPY-MIB", "snimpyBoolean", True) self.assertTrue(isinstance(a, basictypes.Boolean)) self.assertEqual(a, True) self.assertTrue(a) self.assertTrue(not(not(a))) self.assertEqual(not(a), False) a = basictypes.build("SNIMPY-MIB", "snimpyBoolean", "false") self.assertEqual(a, False) b = basictypes.build("SNIMPY-MIB", "snimpyBoolean", True) self.assertEqual(a or b, True) self.assertEqual(a and b, False) def testTimeticks(self): """Test timeticks basic type""" a = basictypes.build("SNIMPY-MIB", "snimpyTimeticks", 676544) self.assertTrue(isinstance(a, basictypes.Timeticks)) # We can compare to int but otherwise, this is a timedelta self.assertEqual(a, 676544) self.assertEqual(str(a), '1:52:45.440000') self.assertEqual(a, timedelta(0, 6765, 440000)) a = basictypes.build("SNIMPY-MIB", "snimpyTimeticks", timedelta(1, 3)) self.assertEqual(str(a), '1 day, 0:00:03') self.assertEqual(a, (3 + 3600 * 24) * 100) self.assertTrue(a != (3 + 3600 * 24) * 100 + 1) self.assertTrue(a < timedelta(1, 4)) self.assertTrue(a > timedelta(1, 1)) self.assertTrue(a > 654) self.assertTrue(a >= 654) self.assertTrue(a < (3 + 3600 * 24) * 100 + 2) self.assertEqual(a, basictypes.build("SNIMPY-MIB", "snimpyTimeticks", timedelta(1, 3))) self.assertTrue(a < basictypes.build("SNIMPY-MIB", "snimpyTimeticks", timedelta(100, 30))) def testBits(self): """Test bit basic type""" a = basictypes.build("SNIMPY-MIB", "snimpyBits", [1, 2]) self.assertTrue(isinstance(a, basictypes.Bits)) self.assertEqual(a, [2, 1]) self.assertEqual(a, (1, 2)) self.assertEqual(a, {1, 2}) self.assertEqual(a, ["second", "third"]) self.assertEqual(a, {"second", "third"}) self.assertEqual(a, ["second", 2]) self.assertTrue(a != ["second"]) self.assertFalse(a == ["second"]) self.assertFalse(a != ["second", 2]) a |= "last" a |= ["last", "second"] self.assertEqual(a, ["second", "last", "third"]) self.assertEqual(str(a), "second(1), third(2), last(7)") a -= 1 a -= 1 self.assertEqual(a, ["last", "third"]) self.assertEqual(a & "last", True) self.assertEqual(a & "second", False) self.assertEqual(a & ["last", 2], True) self.assertEqual(a & {"last", 2}, True) self.assertEqual(a & ["last", 0], True) self.assertEqual(a & ["second", 0], False) a = basictypes.build("SNIMPY-MIB", "snimpyBits", {"first", "second"}) self.assertEqual(a, [0, 1]) a = basictypes.build("SNIMPY-MIB", "snimpyBits", []) self.assertEqual(a, []) self.assertEqual(str(a), "") def testInexistentBits(self): """Check we cannot set inexistent bits""" a = basictypes.build("SNIMPY-MIB", "snimpyBits", [1, 2]) self.assertTrue(a & 1) def nope(a): a |= 3 self.assertRaises(ValueError, nope, a) def testStringAsBits(self): """Test using bit specific operator with string""" a = basictypes.build( "SNIMPY-MIB", "snimpyOctetString", b"\x17\x00\x01") self.assertTrue(isinstance(a, basictypes.OctetString)) b = [7, 6, 5, 3, 23] for i in range(30): if i in b: self.assertTrue(a & i) else: self.assertTrue(not(a & i)) self.assertTrue(a & [5, 7]) self.assertTrue(not(a & [5, 9])) a |= [2, 10] a -= 22 a -= [23, 22] self.assertTrue(a & [2, 10]) self.assertTrue(not(a & 23)) self.assertEqual(a, b"\x37\x20\x00") a |= 31 self.assertEqual(a, b"\x37\x20\x00\x01") def testPacking(self): """Test pack() function""" self.assertEqual(basictypes.build("SNIMPY-MIB", "snimpyString", "Hello world").pack(), rfc1902.OctetString("Hello world")) self.assertEqual(basictypes.build("SNIMPY-MIB", "snimpyInteger", 18).pack(), rfc1902.Integer(18)) self.assertEqual(basictypes.build("SNIMPY-MIB", "snimpyInteger", 1804).pack(), rfc1902.Integer(1804)) self.assertEqual(basictypes.build("SNIMPY-MIB", "snimpyEnum", "testing").pack(), rfc1902.Integer(3)) self.assertEqual(basictypes.build("SNIMPY-MIB", "snimpyIpAddress", "10.11.12.13").pack(), rfc1902.IpAddress("10.11.12.13")) self.assertEqual(basictypes.build("SNIMPY-MIB", "snimpyObjectId", (1, 2, 3, 4)).pack(), rfc1902.univ.ObjectIdentifier((1, 2, 3, 4))) self.assertEqual(basictypes.build("SNIMPY-MIB", "snimpyTimeticks", timedelta(3, 2)).pack(), rfc1902.TimeTicks(3 * 3600 * 24 * 100 + 2 * 100)) self.assertEqual(basictypes.build("SNIMPY-MIB", "snimpyBits", [1, 7]).pack(), rfc1902.Bits(b"\x41")) def testOidConversion(self): """Test conversion to/from OID.""" tt = {("snimpySimpleIndex", 47, False): (47,), ("snimpyComplexFirstIP", "10.14.15.4", False): (10, 14, 15, 4), ("snimpyComplexSecondIP", (14, 15, 16, 17), False): (14, 15, 16, 17), ("snimpyIndexOidVarLen", (47, 48, 49), False): (3, 47, 48, 49), ("snimpyIndexVarLen", "hello1", False): tuple([len("hello1")] + [ord(a) for a in "hello1"]), ("snimpyIndexFixedLen", "hello2", False): tuple(ord(a) for a in "hello2"), ("snimpyIndexImplied", "hello3", True): tuple(ord(a) for a in "hello3"), ("snimpyIndexImplied", "hello3", False): tuple([len("hello3")] + [ord(a) for a in "hello3"]), } for key in tt: t, v, implied = key oid = basictypes.build("SNIMPY-MIB", t, v).toOid(implied) self.assertEqual(oid, tt[key]) # Test double conversion self.assertEqual(mib.get("SNIMPY-MIB", t).type.fromOid( mib.get("SNIMPY-MIB", t), oid, implied), (len(tt[key]), v)) def testTooLargeOid(self): """Handle the special case of octet string as OID with too large octets. See: https://github.com/vincentbernat/snimpy/pull/14 """ self.assertEqual(mib.get("SNIMPY-MIB", "snimpyIndexImplied").type.fromOid( mib.get("SNIMPY-MIB", "snimpyIndexImplied"), (104, 0xff00 | 101, 108, 108, 111), implied=True), (5, basictypes.build("SNIMPY-MIB", "snimpyIndexImplied", "hello"))) def testOidGreedy(self): """Test greediness of fromOid.""" tt = { ("snimpyIndexVarLen", False): ((5, 104, 101, 108, 108, 111, 111, 111, 111), (6, "hello")), ("snimpyIndexFixedLen", False): ((104, 101, 108, 108, 111, 49, 49, 111), (6, "hello1")), ("snimpyIndexImplied", True): ((104, 101, 108, 108, 111, 50), (6, "hello2")), ("snimpyIndexImplied", False): ((6, 104, 101, 108, 108, 111, 50), (7, "hello2")), ("snimpyComplexFirstIP", False): ((15, 15, 16, 100, 23, 74, 87), (4, "15.15.16.100")), ("snimpySimpleIndex", False): ((17, 19, 20), (1, 17)), ("snimpyIndexOidVarLen", False): ((3, 247, 145, 475568, 475, 263), (4, (247, 145, 475568))), } for key in tt: t, implied = key self.assertEqual(mib.get("SNIMPY-MIB", t).type.fromOid( mib.get("SNIMPY-MIB", t), tt[key][0], implied), tt[key][1]) # Test if too short tt = {"snimpyComplexFirstIP": ((17, 19, 20), False), "snimpyIndexFixedLen": ((104, 101, 108), False), "snimpyIndexVarLen": ((6, 102, 103, 104, 105), False), "snimpyIndexOidVarLen": ((3, 247, 145), False), "snimpyIndexImplied": ((3, 102, 103), False), } for t in tt: self.assertRaises(ValueError, mib.get("SNIMPY-MIB", t).type.fromOid, mib.get("SNIMPY-MIB", t), tt[t][0], tt[t][1]) def testDisplay(self): """Test string transformation""" self.assertEqual(str(basictypes.build("SNIMPY-MIB", "snimpyInteger", 18)), "0.18") self.assertEqual(str(basictypes.build("SNIMPY-MIB", "snimpyInteger", 8)), "0.08") self.assertEqual(str(basictypes.build("SNIMPY-MIB", "snimpyInteger", 288)), "2.88") self.assertEqual(str(basictypes.build("SNIMPY-MIB", "snimpyInteger", 28801)), "288.01") self.assertEqual(str(basictypes.build("SNIMPY-MIB", "snimpyString", "test")), "test") self.assertEqual(str(basictypes.build("SNIMPY-MIB", "snimpyOctetString", b"test")), str(b"test")) self.assertEqual(str(basictypes.build("SNIMPY-MIB", "snimpyOctetString", b"tes\x05")), str(b"tes\x05")) def testDisplayFormat(self): """Test display some with some formats""" with mock.patch("snimpy.mib.Node.fmt", new_callable=mock.PropertyMock) as e: e.return_value = "255a" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(str(a), "test") a = basictypes.build("SNIMPY-MIB", "snimpyString", b"") self.assertEqual(str(a), "") e.return_value = "1x:" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(str(a), "74:65:73:74") e.return_value = "2a:" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(str(a), "te:st") e.return_value = "3a:" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(str(a), "tes:t") e.return_value = "4a" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(str(a), "test") e.return_value = "2o+1a" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(str(a), "072145+st") e.return_value = "*2a:+255a" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"\x03testtest...") self.assertEqual(str(a), "te:st:te+st...") e.return_value = "2a1x:" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"aatest") self.assertEqual(str(a), "aa74:65:73:74") e.return_value = "*2a+1a:-*3a?=" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"\x04testtestZ\x02testes\x03testestes") self.assertEqual(str(a), "te+st+te+st+Z-tes?tes=tes?tes?tes") def testInputFormat(self): """Test we can input a string with a given format""" with mock.patch("snimpy.mib.Node.fmt", new_callable=mock.PropertyMock) as e: e.return_value = "255a" a = basictypes.build("SNIMPY-MIB", "snimpyString", "test") self.assertEqual(a.pack(), b"test") a = basictypes.build("SNIMPY-MIB", "snimpyString", b"") self.assertEqual(a.pack(), b"") e.return_value = "1x:" a = basictypes.build("SNIMPY-MIB", "snimpyString", "74:65:73:74") self.assertEqual(a.pack(), b"test") a = basictypes.build("SNIMPY-MIB", "snimpyString", "74:6:73:4") self.assertEqual(a.pack(), b"t\x06s\x04") e.return_value = "2a:" a = basictypes.build("SNIMPY-MIB", "snimpyString", "te:st") self.assertEqual(a.pack(), b"test") e.return_value = "3a:" a = basictypes.build("SNIMPY-MIB", "snimpyString", "tes:t") self.assertEqual(a.pack(), b"test") e.return_value = "4a" a = basictypes.build("SNIMPY-MIB", "snimpyString", "test") self.assertEqual(a.pack(), b"test") e.return_value = "2o+1a" a = basictypes.build("SNIMPY-MIB", "snimpyString", "072145+st") self.assertEqual(a.pack(), b"test") e.return_value = "*2a:+255a" a = basictypes.build( "SNIMPY-MIB", "snimpyString", "te:st:te+st...") self.assertEqual(a.pack(), b"\x03testtest...") e.return_value = "2a1x:" a = basictypes.build("SNIMPY-MIB", "snimpyString", "aa74:65:73:74") self.assertEqual(a.pack(), b"aatest") e.return_value = "*2a+@1a:-*3a?=" a = basictypes.build("SNIMPY-MIB", "snimpyString", "te+st+te+st@Z-tes?tes=tes?tes?tes") self.assertEqual(a.pack(), b"\x04testtestZ\x02testes\x03testestes") e.return_value = "3a" a = basictypes.build("SNIMPY-MIB", "snimpyString", "a\n\r") self.assertEqual(a.pack(), b"a\n\r") def testRepr(self): """Test representation""" self.assertEqual(repr(basictypes.build("SNIMPY-MIB", "snimpyInteger", 18)), "") self.assertEqual(repr(basictypes.build("SNIMPY-MIB", "snimpyObjectId", (1, 3, 6, 1, 4, 1, 45, 3, 52, 1))), "") self.assertEqual(repr(basictypes.build("SNIMPY-MIB", "snimpyIpAddress", "124.24.14.3")), "") self.assertEqual(repr(basictypes.build("SNIMPY-MIB", "snimpyString", "45754dfgf")), "") self.assertEqual(repr(basictypes.build("SNIMPY-MIB", "snimpyEnum", 2)), "") self.assertEqual(repr(basictypes.build("SNIMPY-MIB", "snimpyBoolean", False)), "") self.assertEqual(repr(basictypes.build("SNIMPY-MIB", "snimpyCounter", 4547)), "") self.assertEqual(repr(basictypes.build("SNIMPY-MIB", "snimpyBits", ["first", "second"])), "") def testEqualityWithDisplay(self): """Test we can check for equality with displayed form""" a = basictypes.build("SNIMPY-MIB", "snimpyString", "test") self.assertEqual(a, "test") with mock.patch("snimpy.mib.Node.fmt", new_callable=mock.PropertyMock) as e: e.return_value = "255a" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(a, "test") e.return_value = "1x:" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(a, "74:65:73:74") e.return_value = "2a:" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(a, "te:st") e.return_value = "3a:" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(a, "tes:t") e.return_value = "4a" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(a, "test") e.return_value = "2o+1a" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"test") self.assertEqual(a, "072145+st") self.assertNotEqual(a, "072145+sta") self.assertFalse(a != "072145+st") e.return_value = "*2a:+255a" a = basictypes.build("SNIMPY-MIB", "snimpyString", b"\x03testtest...") self.assertEqual(a, "te:st:te+st...") def testEqualityUnicode(self): """Test that equality works for both unicode and bytes""" a = basictypes.build("SNIMPY-MIB", "snimpyString", "test") self.assertEqual(a, "test") a = basictypes.build("SNIMPY-MIB", "snimpyString", "test") self.assertEqual(a, "test") def testLikeAString(self): """Test String is like str""" a = basictypes.build("SNIMPY-MIB", "snimpyString", "4521dgf") self.assertTrue(a.startswith("4521")) self.assertEqual(a.upper(), "4521DGF") self.assertTrue(re.match("[0-9]+[defg]+", a)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/tests/test_conf.py0000644000076400001440000000232600000000000015731 0ustar00bernatusersimport unittest import os import tempfile from snimpy.config import Conf class TestConf(unittest.TestCase): """Test configuration loading""" def test_default_configuration(self): """Check we can load the default configuration""" conf = Conf() loaded = conf.load() self.assertEqual(conf, loaded) self.assertEqual(conf.mibs, []) self.assertEqual(conf.ipython, True) self.assertEqual(conf.prompt, "\033[1m[snimpy]>\033[0m ") def test_inexistent_configuration(self): conf = Conf().load("dontexist") self.assertEqual(conf.mibs, []) self.assertEqual(conf.ipython, True) def test_loading_custom_configuration(self): conffile = tempfile.NamedTemporaryFile(delete=False) try: conffile.write(b""" mibs = [ "IF-MIB", "LLDP-MIB" ] ipython = False unknown = "hey!" """) conffile.close() conf = Conf().load(conffile.name) self.assertEqual(conf.mibs, ["IF-MIB", "LLDP-MIB"]) self.assertEqual(conf.unknown, "hey!") self.assertEqual(conf.ipython, False) self.assertEqual(conf.ipythonprofile, None) finally: os.unlink(conffile.name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/tests/test_main.py0000644000076400001440000000215400000000000015727 0ustar00bernatusersimport os import tempfile import code # noqa: F401 import mock import platform from snimpy.main import interact from multiprocessing import Process import agent import unittest class TestMain(unittest.TestCase): """Test the main shell""" @classmethod def setUpClass(cls): cls.agent = agent.TestAgent() @classmethod def tearDownClass(cls): cls.agent.terminate() @unittest.skipIf(platform.python_implementation() == "PyPy", "setupterm seems unreliable with Pypy") def test_loadfile(self): script = tempfile.NamedTemporaryFile(delete=False) try: script.write(""" load("IF-MIB") m = M(host="127.0.0.1:{}", community="public", version=2) assert(m.ifDescr[1] == "lo") """.format(self.agent.port).encode("ascii")) script.close() with mock.patch("code.InteractiveInterpreter.write"): p = Process(target=interact, args=((script.name,),)) p.start() p.join() self.assertEqual(p.exitcode, 0) finally: os.unlink(script.name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/tests/test_manager.py0000644000076400001440000004754400000000000016431 0ustar00bernatusersimport os import time from datetime import timedelta from snimpy.manager import load, Manager, snmp import agent import unittest class TestManager(unittest.TestCase): @classmethod def setUpClass(cls): load('IF-MIB') load('SNMPv2-MIB') load(os.path.join(os.path.dirname(os.path.abspath(__file__)), "SNIMPY-MIB.mib")) cls.agent = agent.TestAgent() @classmethod def tearDownClass(cls): cls.agent.terminate() def setUp(self): self.manager = Manager(host="127.0.0.1:{}".format(self.agent.port), community="public", version=2) self.session = self.manager._session class TestManagerGet(TestManager): """Test getting stuff from manager""" def testGetScalar(self): """Retrieve some simple scalar values""" self.assertEqual(self.manager.sysDescr, "Snimpy Test Agent public") self.assertEqual(self.manager.ifNumber, 3) def scalarGetAndCheck(self, name, value): self.assertEqual(getattr(self.manager, name), value) def testScalar_IpAddress(self): """Retrieve IpAdress as a scalar""" self.scalarGetAndCheck("snimpyIpAddress", "65.65.65.65") def testScalar_String(self): """Retrieve a String as a scalar""" self.scalarGetAndCheck("snimpyString", "bye") def testScalar_Integer(self): """Retrieve an Integer as a scalar""" self.scalarGetAndCheck("snimpyInteger", 19) def testScalar_Enum(self): """Retrieve an Enum as a scalar""" self.scalarGetAndCheck("snimpyEnum", "down") def testScalar_ObjectId(self): """Retrieve an ObjectId as a scalar""" self.scalarGetAndCheck("snimpyObjectId", (1, 3, 6, 4454, 0, 0)) def testScalar_Boolean(self): """Retrieve a Boolean as a scalar""" self.scalarGetAndCheck("snimpyBoolean", True) def testScalar_Counter(self): """Retrieve a Counter as a scalar""" self.scalarGetAndCheck("snimpyCounter", 47) self.scalarGetAndCheck("snimpyCounter64", 2 ** 48 + 3) def testScalar_Gauge(self): """Retrieve a Gauge as a scalar""" self.scalarGetAndCheck("snimpyGauge", 18) def testScalar_Timeticks(self): """Retrieve a TimeTicks as a scalar""" self.scalarGetAndCheck( "snimpyTimeticks", timedelta(days=1, hours=9, minutes=38, seconds=31)) def testScalar_Bits(self): """Retrieve Bits as a scalar""" self.scalarGetAndCheck("snimpyBits", ["first", "third", "secondByte"]) def testScalar_MacAddress(self): """Retrieve MacAddress as a scalar""" self.scalarGetAndCheck("snimpyMacAddress", "11:12:13:14:15:16") def testContains_IfDescr(self): """Test proxy column membership checking code""" self.assertEqual(2 in self.manager.ifDescr, True) # FIXME: this currently fails under TestManagerWithNone # self.assertEqual(10 in self.manager.ifDescr, # False) def testWalkIfDescr(self): """Test we can walk IF-MIB::ifDescr and IF-MIB::ifTpe""" results = [(idx, self.manager.ifDescr[idx], self.manager.ifType[idx]) for idx in self.manager.ifIndex] self.assertEqual(results, [(1, "lo", 24), (2, "eth0", 6), (3, "eth1", 6)]) def testWalkIfTable(self): """Test we can walk IF-MIB::ifTable""" results = [(idx, self.manager.ifDescr[idx], self.manager.ifType[idx]) for idx in self.manager.ifTable] self.assertEqual(results, [(1, "lo", 24), (2, "eth0", 6), (3, "eth1", 6)]) def testWalkNotAccessible(self): """Test we can walk a table with the first entry not accessible.""" list(self.manager.ifRcvAddressTable) def testWalkIfDescrWithoutBulk(self): """Walk IF-MIB::ifDescr without GETBULK""" self.session.bulk = False self.testWalkIfDescr() def testWalkIfTableWithoutBulk(self): """Walk IF-MIB::ifTable without GETBULK""" self.session.bulk = False self.testWalkIfTable() def testWalkComplexIndexes(self): """Test if we can walk a table with complex indexes""" results = [(idx, self.manager.snimpyIndexInt[idx]) for idx in self.manager.snimpyIndexInt] self.assertEqual(results, [(("row1", (1, 2, 3), "alpha5", "end of row1"), 4571), (("row2", (1, 0, 2, 3), "beta32", "end of row2"), 78741), (("row3", (120, 1, 2, 3), "gamma7", "end of row3"), 4110)]) def testWalkTableWithComplexIndexes(self): """Test if we can walk a table with complex indexes""" results = [(idx, self.manager.snimpyIndexInt[idx]) for idx in self.manager.snimpyIndexTable] self.assertEqual(results, [(("row1", (1, 2, 3), "alpha5", "end of row1"), 4571), (("row2", (1, 0, 2, 3), "beta32", "end of row2"), 78741), (("row3", (120, 1, 2, 3), "gamma7", "end of row3"), 4110)]) def testWalkReuseIndexes(self): """Test if we can walk a table with re-used indexes""" results = [(idx, self.manager.snimpyReuseIndexValue[idx]) for idx in self.manager.snimpyReuseIndexValue] self.assertEqual(results, [(("end of row1", 4), 1785), (("end of row1", 5), 2458)]) def testWalkTableWithReuseIndexes(self): """Test if we can walk a table with re-used indexes""" results = [(idx, self.manager.snimpyReuseIndexValue[idx]) for idx in self.manager.snimpyReuseIndexTable] self.assertEqual(results, [(("end of row1", 4), 1785), (("end of row1", 5), 2458)]) def testWalkPartialIndexes(self): """Test if we can walk a slice of a table given a partial index""" results = [(idx, self.manager.ifRcvAddressType[idx]) for idx in self.manager.ifRcvAddressStatus[2]] self.assertEqual(results, [((2, "61:62:63:64:65:66"), 1), ((2, "67:68:69:6a:6b:6c"), 1)]) results = [(idx, self.manager.ifRcvAddressType[idx]) for idx in self.manager.ifRcvAddressStatus[(3,)]] self.assertEqual(results, [((3, "6d:6e:6f:70:71:72"), 1)]) results = list(self.manager.ifRcvAddressType.iteritems(3)) self.assertEqual(results, [((3, "6d:6e:6f:70:71:72"), 1)]) results = list(self.manager.ifRcvAddressType.items(3)) self.assertEqual(results, [((3, "6d:6e:6f:70:71:72"), 1)]) def testWalkInvalidPartialIndexes(self): """Try to get a table slice with an incorrect index filter""" self.assertRaises(ValueError, lambda: list( self.manager.ifRcvAddressStatus.iteritems( (3, "6d:6e:6f:70:71:72")))) def testContains_Partial(self): """Test proxy column membership checking code with partial indexes""" self.assertEqual( "61:62:63:64:65:66" in self.manager.ifRcvAddressStatus[2], True) # FIXME: this currently fails under TestManagerWithNone # self.assertEqual( # "6d:6e:6f:70:71:72" in self.manager.ifRcvAddressStatus[2], # False) def testScalar_MultipleSubscripts(self): """Retrieve a scalar value using multiple subscript syntax (attr[x][y])""" self.assertEqual(self.manager.ifRcvAddressType[2]["67:68:69:6a:6b:6c"], 1) def testGetInexistentStuff(self): """Try to access stuff that does not exist on the agent""" self.assertRaises(snmp.SNMPNoSuchObject, getattr, self.manager, "snimpyNotImplemented") self.assertRaises(snmp.SNMPNoSuchObject, self.manager.ifName.__getitem__, 47) self.assertRaises(snmp.SNMPNoSuchInstance, self.manager.ifDescr.__getitem__, 47) def testAccessInexistentStuff(self): """Try to access stuff that don't exist in MIB""" self.assertRaises(AttributeError, getattr, self.manager, "iDoNotExist") def testAccessIncorrectIndex(self): """Try to access with incorrect indexes""" self.assertRaises(ValueError, self.manager.ifDescr.__getitem__, (47, 18)) self.assertRaises(ValueError, self.manager.ifDescr.__getitem__, "nothing") def testAccessEmptyTable(self): """Try to walk an empty table""" results = [(idx,) for idx in self.manager.snimpyEmptyDescr] self.assertEqual(results, []) def testAccessNotExistentTable(self): """Try to walk a non-existent table""" agent2 = agent.TestAgent(emptyTable=False) try: manager = Manager(host="127.0.0.1:{}".format(agent2.port), community="public", version=2) [(idx,) for idx in manager.snimpyEmptyDescr] except snmp.SNMPNoSuchObject: pass # That's OK else: self.assertFalse("should raise SNMPNoSuchObject exception") finally: agent2.terminate() def testGetChangingStuff(self): """Get stuff with varying values""" initial = self.manager.ifInOctets[2] current = self.manager.ifInOctets[2] self.assertGreater(current, initial) class TestManagerRestrictModule(TestManager): """Test when we restrict modules to be used by a manager""" def testGetSpecificModule(self): """Get a scalar from a specific module only""" self.assertEqual(self.manager['IF-MIB'].ifNumber, 3) self.assertEqual(self.manager['SNMPv2-MIB'].sysDescr, "Snimpy Test Agent public") def testGetInexistentModule(self): """Get a scalar from a non loaded module""" self.assertRaises(KeyError, lambda: self.manager['IF-MIB2']) def testGetInexistentScalarFromModule(self): """Get a non-existent scalar from a specific module""" self.assertRaises(AttributeError, lambda: self.manager['IF-MIB'].sysDescr) class TestManagerSet(TestManager): """Test setting stuff from manager""" def testSetScalar(self): """Try to set a simple value""" self.manager.snimpyString = "hello" self.assertEqual(self.manager.snimpyString, "hello") def scalarSetAndCheck(self, name, value): setattr(self.manager, name, value) self.assertEqual(getattr(self.manager, name), value) def testScalar_IpAddress(self): """Retrieve IpAdress as a scalar""" self.scalarSetAndCheck("snimpyIpAddress", "165.255.65.65") def testScalar_String(self): """Retrieve a String as a scalar""" self.scalarSetAndCheck("snimpyString", "awesome !!!") def testScalar_Integer(self): """Retrieve an Integer as a scalar""" self.scalarSetAndCheck("snimpyInteger", 1900) def testScalar_Enum(self): """Retrieve an Enum as a scalar""" self.scalarSetAndCheck("snimpyEnum", "up") def testScalar_ObjectId(self): """Retrieve an ObjectId as a scalar""" self.scalarSetAndCheck("snimpyObjectId", (1, 3, 6, 4454, 19, 47)) def testScalar_Boolean(self): """Retrieve a Boolean as a scalar""" self.scalarSetAndCheck("snimpyBoolean", False) def testScalar_Counter(self): """Retrieve a Counter as a scalar""" self.scalarSetAndCheck("snimpyCounter", 4700) self.scalarSetAndCheck("snimpyCounter64", 2 ** 48 + 3 - 18) def testScalar_Gauge(self): """Retrieve a Gauge as a scalar""" self.scalarSetAndCheck("snimpyGauge", 180014) def testScalar_Timeticks(self): """Retrieve a TimeTicks as a scalar""" self.scalarSetAndCheck( "snimpyTimeticks", timedelta(days=1, hours=17, minutes=38, seconds=31)) def testScalar_Bits(self): """Retrieve Bits as a scalar""" self.scalarSetAndCheck("snimpyBits", ["first", "second", "secondByte"]) def testScalar_MacAddress(self): """Retrieve MAC address as a scala""" self.scalarSetAndCheck("snimpyMacAddress", "a0:b0:c0:d0:e:ff") def testNonScalarSet(self): """Check we can set a non-scalar value""" idx = ("row2", (1, 0, 2, 3), "beta32", "end of row2") self.manager.snimpyIndexInt[idx] = 1041 self.assertEqual(self.manager.snimpyIndexInt[idx], 1041) def testSetWithContext(self): """Set several values atomically (inside a context)""" with self.manager as m: m.snimpyString = "Noooooo!" m.snimpyInteger = 42 self.assertEqual(m.snimpyString, "Noooooo!") self.assertEqual(m.snimpyInteger, 42) def testSetWithContextAndAbort(self): """Check if writing several values atomically can be aborted""" try: with self.manager as m: m.snimpyString = "Abort sir!" m.snimpyInteger = 37 raise RuntimeError("Abort now!") except RuntimeError as e: self.assertEqual(str(e), "Abort now!") self.assertNotEqual(m.snimpyString, "Abort sir!") self.assertNotEqual(m.snimpyInteger, 37) def testSetInexistentStuff(self): """Try to access stuff that does not exist on the agent""" self.assertRaises(snmp.SNMPNotWritable, setattr, self.manager, "snimpyNotImplemented", "Hello") self.assertRaises(snmp.SNMPNotWritable, self.manager.ifName.__setitem__, 47, "Wouh") self.assertRaises(snmp.SNMPNotWritable, self.manager.ifDescr.__setitem__, 47, "Noooo") def testAccessInexistentStuff(self): """Try to access stuff that don't exist in MIB""" self.assertRaises(AttributeError, setattr, self.manager, "iDoNotExist", 47) def testAccessIncorrectIndex(self): """Try to access with incorrect indexes""" self.assertRaises(ValueError, self.manager.ifDescr.__setitem__, (47, 18), "Nooo") self.assertRaises(ValueError, self.manager.ifDescr.__setitem__, "nothing", "Neither") class TestManagerWithNone(TestManagerGet): """Test a manager answering None for inexistent stuff""" def setUp(self): self.manager = Manager(host="127.0.0.1:{}".format(self.agent.port), community="public", version=2, none=True) self.session = self.manager._session._session def testGetInexistentStuff(self): """Try to access stuff that does not exist on the agent""" self.assertEqual(self.manager.snimpyNotImplemented, None) self.assertEqual(self.manager.ifName[47], None) self.assertEqual(self.manager.ifDescr[47], None) class TestCachingManager(TestManagerGet): """Test if caching manager works like regular manager""" def setUp(self): self.manager = Manager(host="127.0.0.1:{}".format(self.agent.port), community="public", version=2, cache=1) self.session = self.manager._session._session def testGetChangingStuff(self): """Get stuff with varying values""" initial = self.manager.ifInOctets[2] current = self.manager.ifInOctets[2] self.assertEqual(current, initial) def testCacheFlush(self): """Test cache timeout is working as expected""" first1 = self.manager.ifInOctets[1] second1 = self.manager.ifInOctets[2] third1 = self.manager.ifInOctets[3] time.sleep(0.5) second2 = self.manager.ifInOctets[2] third2 = self.manager.ifInOctets[3] self.assertEqual(second1, second2) # timeout not reached self.assertEqual(third1, third2) # timeout not reached time.sleep(1) first2 = self.manager.ifInOctets[1] self.assertGreater(first2, first1) # timeout was reached class TestCachingManagerWithModificatons(TestManager): """Test if caching manager works with modifications""" def setUp(self): self.manager = Manager(host="127.0.0.1:{}".format(self.agent.port), community="public", version=2, cache=1) self.session = self.manager._session._session def testCacheScalar(self): """Check that a scalar value is kept in cache""" original = self.manager.snimpyString self.manager.snimpyString = "Nooooo" self.assertEqual(self.manager.snimpyString, original) def testCacheNonScalar(self): """Check we can cache a non-scalar value""" idx = ("row2", (1, 0, 2, 3), "beta32", "end of row2") original = self.manager.snimpyIndexInt[idx] self.manager.snimpyIndexInt[idx] = 1041 self.assertEqual(self.manager.snimpyIndexInt[idx], original) def testCacheExpire(self): """Check the cache can expire""" self.manager.snimpyString = "Yeesss" time.sleep(1) self.assertEqual(self.manager.snimpyString, "Yeesss") class TestManagerInvalidValues(TestManager): """Test when the agent is returning invalid values""" def testInvalidValue(self): """Check if an invalid value raises an exception""" self.assertRaises(ValueError, getattr, self.manager, "snimpyMacAddressInvalid") def testInvalidValueInTable(self): """Check if an invalid value in a table raises an exception""" self.assertRaises(ValueError, self.manager.snimpyInvalidDescr.__getitem__, 2) def testInvalidValueWhileIterating(self): """Check if an invalid value while walking raises an exception""" self.assertRaises(ValueError, list, self.manager.snimpyInvalidDescr.iteritems()) class TestManagerLoose(TestManager): """Test when the agent is returning invalid values with loose mode""" def setUp(self): self.manager = Manager(host="127.0.0.1:{}".format(self.agent.port), community="public", version=2, loose=True) self.session = self.manager._session def testInvalidValue(self): """Check if an invalid value is correctly returned""" self.assertEqual(self.manager.snimpyMacAddressInvalid, b"\xf1\x12\x13\x14\x15\x16") def testInvalidValueInTable(self): """Check if an invalid value in a table is correctly returned""" self.assertEqual(self.manager.snimpyInvalidDescr[1], "Hello") self.assertEqual(self.manager.snimpyInvalidDescr[2], b"\xf1\x12\x13\x14\x15\x16") def testInvalidValueWhileIterating(self): """Check if an invalid value while walking works""" self.assertEqual(list(self.manager.snimpyInvalidDescr.iteritems()), [(1, "Hello"), (2, b"\xf1\x12\x13\x14\x15\x16")]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/tests/test_mib.py0000644000076400001440000004467300000000000015566 0ustar00bernatusersimport unittest import os from snimpy import mib, basictypes class TestMibSnimpy(unittest.TestCase): def setUp(self): mib.load(os.path.join(os.path.dirname(os.path.abspath(__file__)), "SNIMPY-MIB.mib")) self.nodes = ["snimpy", "snimpyScalars", "snimpyTables", "snimpyTraps"] self.nodes.sort() self.tables = ["snimpyComplexTable", "snimpyInetAddressTable", "snimpySimpleTable", "snimpyIndexTable", "snimpyInvalidTable", "snimpyEmptyTable", "snimpyReuseIndexTable"] self.tables.sort() self.columns = ["snimpyComplexFirstIP", "snimpyComplexSecondIP", "snimpySimpleIndex", "snimpyComplexState", "snimpyInetAddressType", "snimpyInetAddress", "snimpyInetAddressState", "snimpySimpleDescr", "snimpySimplePhys", "snimpySimpleType", "snimpyIndexVarLen", "snimpyIndexIntIndex", "snimpyIndexOidVarLen", "snimpyIndexFixedLen", "snimpyIndexImplied", "snimpyIndexInt", "snimpyInvalidIndex", "snimpyInvalidDescr", "snimpyEmptyIndex", "snimpyEmptyDescr", "snimpyReuseIndexValue" ] self.columns.sort() self.scalars = ["snimpyIpAddress", "snimpyString", "snimpyInteger", "snimpyEnum", "snimpyObjectId", "snimpyBoolean", "snimpyCounter", "snimpyGauge", "snimpyTimeticks", "snimpyCounter64", "snimpyBits", "snimpyNotImplemented", "snimpyOctetString", "snimpyUnicodeString", "snimpyMacAddress", "snimpyMacAddressInvalid"] self.scalars.sort() self.notifications = ["snimpyNotification"] self.notifications.sort() self.expected_modules = ["SNMPv2-SMI", "SNMPv2-TC", "SNIMPY-MIB", "INET-ADDRESS-MIB", "IANAifType-MIB"] def tearDown(self): mib.reset() def testGetNodes(self): """Test that we can get all nodes""" nodes = mib.getNodes('SNIMPY-MIB') snodes = sorted([str(a) for a in nodes]) self.assertEqual(self.nodes, snodes) for n in nodes: self.assertTrue(isinstance(n, mib.Node)) def testGetTables(self): """Test that we can get all tables""" tables = mib.getTables('SNIMPY-MIB') stables = sorted([str(a) for a in tables]) self.assertEqual(self.tables, stables) for n in tables: self.assertTrue(isinstance(n, mib.Table)) def testGetColumns(self): """Test that we can get all columns""" columns = mib.getColumns('SNIMPY-MIB') scolumns = sorted([str(a) for a in columns]) self.assertEqual(self.columns, scolumns) for n in columns: self.assertTrue(isinstance(n, mib.Column)) def testGetScalars(self): """Test that we can get all scalars""" scalars = mib.getScalars('SNIMPY-MIB') sscalars = sorted([str(a) for a in scalars]) self.assertEqual(self.scalars, sscalars) for n in scalars: self.assertTrue(isinstance(n, mib.Scalar)) def testGetNotifications(self): """Test that we can get all notifications""" notifications = mib.getNotifications('SNIMPY-MIB') snotifications = sorted([str(a) for a in notifications]) self.assertEqual(self.notifications, snotifications) for n in notifications: self.assertTrue(isinstance(n, mib.Notification)) def testGet(self): """Test that we can get all named attributes""" for i in self.scalars: self.assertEqual(str(mib.get('SNIMPY-MIB', i)), i) self.assertTrue(isinstance(mib.get('SNIMPY-MIB', i), mib.Scalar)) for i in self.tables: self.assertEqual(str(mib.get('SNIMPY-MIB', i)), i) self.assertTrue(isinstance(mib.get('SNIMPY-MIB', i), mib.Table)) for i in self.columns: self.assertEqual(str(mib.get('SNIMPY-MIB', i)), i) self.assertTrue(isinstance(mib.get('SNIMPY-MIB', i), mib.Column)) for i in self.nodes: self.assertEqual(str(mib.get('SNIMPY-MIB', i)), i) self.assertTrue(isinstance(mib.get('SNIMPY-MIB', i), mib.Node)) for i in self.notifications: self.assertEqual(str(mib.get('SNIMPY-MIB', i)), i) self.assertTrue(isinstance(mib.get('SNIMPY-MIB', i), mib.Notification)) def testGetByOid(self): """Test that we can get all named attributes by OID.""" for i in self.scalars: nodebyname = mib.get('SNIMPY-MIB', i) self.assertEqual(str(mib.getByOid(nodebyname.oid)), i) self.assertTrue(isinstance(mib.getByOid(nodebyname.oid), mib.Scalar)) for i in self.tables: nodebyname = mib.get('SNIMPY-MIB', i) self.assertEqual(str(mib.getByOid(nodebyname.oid)), i) self.assertTrue(isinstance(mib.getByOid(nodebyname.oid), mib.Table)) for i in self.columns: nodebyname = mib.get('SNIMPY-MIB', i) self.assertEqual(str(mib.getByOid(nodebyname.oid)), i) self.assertTrue(isinstance(mib.getByOid(nodebyname.oid), mib.Column)) for i in self.nodes: nodebyname = mib.get('SNIMPY-MIB', i) self.assertEqual(str(mib.getByOid(nodebyname.oid)), i) self.assertTrue(isinstance(mib.getByOid(nodebyname.oid), mib.Node)) for i in self.notifications: nodebyname = mib.get('SNIMPY-MIB', i) self.assertEqual(str(mib.getByOid(nodebyname.oid)), i) self.assertTrue(isinstance(mib.getByOid(nodebyname.oid), mib.Notification)) def testGetByOid_UnknownOid(self): """Test that unknown OIDs raise an exception.""" self.assertRaises(mib.SMIException, mib.getByOid, (255,)) def testGetType(self): """Test that _getType properly identifies known and unknown types.""" self.assertEqual(b"PhysAddress", mib.ffi.string(mib._getType("PhysAddress").name)) self.assertEqual(b"InetAddress", mib.ffi.string(mib._getType(b"InetAddress").name)) self.assertEqual(None, mib._getType("SomeUnknownType.kjgf")) self.assertEqual(None, mib._getType("snimpySimpleTable")) def testTableColumnRelation(self): """Test that we can get the column from the table and vice-versa""" for i in self.tables: table = mib.get('SNIMPY-MIB', i) for r in table.columns: self.assertTrue(isinstance(r, mib.Column)) self.assertEqual(str(r.table), str(i)) self.assertTrue(str(r).startswith(str(i).replace("Table", ""))) columns = sorted([str(rr) for rr in self.columns if str(rr).startswith(str(i).replace("Table", ""))]) tcolumns = [str(rr) for rr in table.columns] tcolumns.sort() self.assertEqual(columns, tcolumns) for r in self.columns: column = mib.get('SNIMPY-MIB', r) table = column.table self.assertTrue(isinstance(table, mib.Table)) prefix = str(table).replace("Table", "") self.assertEqual(prefix, str(r)[:len(prefix)]) def testTypes(self): """Test that we get the correct types""" tt = {"snimpyIpAddress": basictypes.IpAddress, "snimpyString": basictypes.OctetString, "snimpyOctetString": basictypes.OctetString, "snimpyUnicodeString": basictypes.OctetString, "snimpyMacAddress": basictypes.OctetString, "snimpyInteger": basictypes.Integer, "snimpyEnum": basictypes.Enum, "snimpyObjectId": basictypes.Oid, "snimpyBoolean": basictypes.Boolean, "snimpyCounter": basictypes.Unsigned32, "snimpyGauge": basictypes.Unsigned32, "snimpyTimeticks": basictypes.Timeticks, "snimpyCounter64": basictypes.Unsigned64, "snimpyBits": basictypes.Bits, "snimpySimpleIndex": basictypes.Integer, "snimpyComplexFirstIP": basictypes.IpAddress, "snimpyComplexSecondIP": basictypes.IpAddress, "snimpyComplexState": basictypes.Enum} for t in tt: self.assertEqual(mib.get('SNIMPY-MIB', t).type, tt[t]) # Also check we get an exception when no type available def call(): mib.get('SNIMPY-MIB', 'snimpySimpleTable').type self.assertRaises(mib.SMIException, call) def testRanges(self): tt = {"snimpyIpAddress": 4, "snimpyString": (0, 255), "snimpyOctetString": None, "snimpyInteger": [(6, 18), (20, 23), (27, 1336)], "snimpyEnum": None, "snimpyObjectId": None, "snimpyBoolean": None, "snimpyCounter": (0, 4294967295), "snimpyGauge": (0, 4294967295), "snimpyTimeticks": (0, 4294967295), "snimpyCounter64": (0, 18446744073709551615), "snimpyBits": None, "snimpySimpleIndex": (1, 30), "snimpyComplexFirstIP": 4, "snimpyComplexSecondIP": 4, "snimpyComplexState": None } for t in tt: self.assertEqual(mib.get('SNIMPY-MIB', t).ranges, tt[t]) def testEnums(self): """Test that we got the enum values correctly""" self.assertEqual(mib.get('SNIMPY-MIB', "snimpyInteger").enum, None) self.assertEqual(mib.get("SNIMPY-MIB", "snimpyEnum").enum, {1: "up", 2: "down", 3: "testing"}) self.assertEqual(mib.get("SNIMPY-MIB", "snimpyBits").enum, {0: "first", 1: "second", 2: "third", 7: "last", 8: "secondByte"}) def testIndexes(self): """Test that we can retrieve correctly the index of tables""" self.assertEqual( [str(i) for i in mib.get("SNIMPY-MIB", "snimpySimpleTable").index], ["snimpySimpleIndex"]) self.assertEqual( [str(i) for i in mib.get("SNIMPY-MIB", "snimpyComplexTable").index], ["snimpyComplexFirstIP", "snimpyComplexSecondIP"]) self.assertEqual( [str(i) for i in mib.get("SNIMPY-MIB", "snimpyInetAddressTable").index], ["snimpyInetAddressType", "snimpyInetAddress"]) def testImplied(self): """Check that we can get implied attribute for a given table""" self.assertEqual( mib.get("SNIMPY-MIB", 'snimpySimpleTable').implied, False) self.assertEqual( mib.get("SNIMPY-MIB", 'snimpyComplexTable').implied, False) self.assertEqual( mib.get("SNIMPY-MIB", 'snimpyIndexTable').implied, True) def testOid(self): """Test that objects are rooted at the correct OID""" oids = {"snimpy": (1, 3, 6, 1, 2, 1, 45121), "snimpyScalars": (1, 3, 6, 1, 2, 1, 45121, 1), "snimpyString": (1, 3, 6, 1, 2, 1, 45121, 1, 2), "snimpyInteger": (1, 3, 6, 1, 2, 1, 45121, 1, 3), "snimpyBits": (1, 3, 6, 1, 2, 1, 45121, 1, 11), "snimpyTables": (1, 3, 6, 1, 2, 1, 45121, 2), "snimpySimpleTable": (1, 3, 6, 1, 2, 1, 45121, 2, 1), "snimpySimplePhys": (1, 3, 6, 1, 2, 1, 45121, 2, 1, 1, 4), "snimpyComplexTable": (1, 3, 6, 1, 2, 1, 45121, 2, 2), "snimpyComplexState": (1, 3, 6, 1, 2, 1, 45121, 2, 2, 1, 3), } for o in oids: self.assertEqual(mib.get('SNIMPY-MIB', o).oid, oids[o]) def testLoadedMibNames(self): """Check that only expected modules were loaded.""" for module in self.expected_modules: self.assertTrue(module in list(mib.loadedMibNames())) def testLoadInexistantModule(self): """Check that we get an exception when loading an inexistant module""" self.assertRaises(mib.SMIException, mib.load, "idontexist.gfdgfdg") def testLoadInvalidModule(self): """Check that an obviously invalid module cannot be loaded""" path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "SNIMPY-INVALID-MIB.mib") self.assertRaises(mib.SMIException, mib.load, path) self.assertRaises(mib.SMIException, mib.getNodes, "SNIMPY-INVALID-MIB") self.assertRaises(mib.SMIException, mib.get, "SNIMPY-INVALID-MIB", "invalidSnimpyNode") def testAccesInexistantModule(self): """Check that we get an exception when querying inexistant module""" self.assertRaises(mib.SMIException, mib.getNodes, "idontexist.kjgf") self.assertRaises(mib.SMIException, mib.getScalars, "idontexist.kjgf") self.assertRaises(mib.SMIException, mib.getTables, "idontexist.kjgf") self.assertRaises(mib.SMIException, mib.getColumns, "idontexist.kjgf") def testFmt(self): """Check that we get FMT from types""" self.assertEqual(mib.get("SNIMPY-MIB", 'snimpySimplePhys').fmt, "1x:") self.assertEqual(mib.get("SNIMPY-MIB", 'snimpyInteger').fmt, "d-2") def testTypeOverrides(self): """Check that we can override a type""" table = mib.get("SNIMPY-MIB", "snimpyInetAddressTable") addrtype_attr = table.index[0] addr_attr = table.index[1] # Try overriding to IPv4 with a byte string name. addrtype = addrtype_attr.type(addrtype_attr, "ipv4") self.assertEqual(addrtype, "ipv4") addr_attr.typeName = b"InetAddressIPv4" ipv4 = "127.0.0.1" ipv4_oid = (4, 127, 0, 0, 1) addr = addr_attr.type(addr_attr, ipv4) self.assertEqual(str(addr), ipv4) self.assertEqual(addr.toOid(), ipv4_oid) addr_len, addr = addr_attr.type.fromOid(addr_attr, ipv4_oid) self.assertEqual(addr_len, ipv4_oid[0] + 1) self.assertEqual(str(addr), ipv4) self.assertEqual(addr.toOid(), ipv4_oid) # Try both IPv6 and non-bytes name. addrtype = addrtype_attr.type(addrtype_attr, "ipv6") self.assertEqual(addrtype, "ipv6") addr_attr.typeName = "InetAddressIPv6" # Snimpy does not use leading zeroes. ipv6 = '102:304:506:708:90a:b0c:d0e:f01' ipv6_oid = (16, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01) addr = addr_attr.type(addr_attr, ipv6) self.assertEqual(str(addr), ipv6) self.assertEqual(addr.toOid(), ipv6_oid) addr_len, addr = addr_attr.type.fromOid(addr_attr, ipv6_oid) self.assertEqual(addr_len, ipv6_oid[0] + 1) self.assertEqual(str(addr), ipv6) self.assertEqual(addr.toOid(), ipv6_oid) # Try a type from a different module (chosen because snmpwalk # prints IPv6 addresses incorrectly). ipv6_1xformat = '1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:1' addr_attr.typeName = "PhysAddress" addr = addr_attr.type(addr_attr, ipv6_1xformat) self.assertEqual(str(addr), ipv6_1xformat) self.assertEqual(addr.toOid(), ipv6_oid) # Try overriding back to the default. del addr_attr.typeName addr_len, addr = addr_attr.type.fromOid(addr_attr, ipv4_oid) self.assertEqual(bytes(addr), b"\x7f\0\0\1") def testTypeOverrides_Errors(self): table = mib.get("SNIMPY-MIB", "snimpyInetAddressTable") attr = table.index[1] # Value with the wrong type. self.assertRaises(AttributeError, setattr, attr, "typeName", None) # Unknown type. self.assertRaises(mib.SMIException, setattr, attr, "typeName", "SomeUnknownType.kjgf") # Incompatible basetype. self.assertRaises(mib.SMIException, setattr, attr, "typeName", "InetAddressType") # Parse error. attr.typeName = "InetAddressIPv4" self.assertRaises(ValueError, attr.type, attr, "01:02:03:04") def testTypeName(self): """Check that we can get the current declared type name""" table = mib.get("SNIMPY-MIB", "snimpyInetAddressTable") attr = table.index[1] self.assertEqual(attr.typeName, b"InetAddress") attr.typeName = b"InetAddressIPv4" self.assertEqual(attr.typeName, b"InetAddressIPv4") attr.typeName = b"InetAddressIPv6" self.assertEqual(attr.typeName, b"InetAddressIPv6") attr = mib.get("SNIMPY-MIB", "snimpySimpleIndex") self.assertEqual(attr.typeName, b"Integer32") class TestSmi(unittest.TestCase): def testGetPath(self): """Test we can get default SMI path""" current = mib.path() self.assertTrue(type(current), str) self.assertNotEqual(mib.path(), "") def testSetPath(self): """Test we can set path to some value""" original = mib.path() current = original + ":/some/other/directory" try: mib.path(current) self.assertEqual(mib.path(), current) finally: mib.path(original) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/tests/test_snmp.py0000644000076400001440000003634100000000000015765 0ustar00bernatusersimport unittest import os import threading import multiprocessing import platform from datetime import timedelta from snimpy import basictypes, snmp, mib import agent class TestSnmpRetriesTimeout(unittest.TestCase): """Live modification of retry and timeout values for a session""" def setUp(self): self.session = snmp.Session(host="localhost", community="public", version=2) def testGetRetries(self): """Get default retries value""" self.assertEqual(self.session.retries, 5) def testGetTimeout(self): """Get default timeout value""" self.assertEqual(self.session.timeout, 1000000) def testSetRetries(self): """Try to set a new retry value""" self.session.retries = 2 self.assertEqual(self.session.retries, 2) self.session.retries = 0 self.assertEqual(self.session.retries, 0) def testSetTimeout(self): """Try to set a new timeout value""" self.session.timeout = 500000 self.assertEqual(self.session.timeout, 500000) def testErrors(self): """Try invalid values for timeout and retries""" self.assertRaises(ValueError, setattr, self.session, "timeout", 0) self.assertRaises(ValueError, setattr, self.session, "timeout", -30) self.assertRaises(ValueError, setattr, self.session, "retries", -5) class TestSnmpSession(unittest.TestCase): """Test for session creation using SNMPv1/v2c/v3""" def testSnmpV1(self): """Check initialization of SNMPv1 session""" snmp.Session(host="localhost", community="public", version=1) def testSnmpV2(self): """Check initialization of SNMPv2 session""" snmp.Session(host="localhost", community="public", version=2) def testSnmpV3(self): """Check initialization of SNMPv3 session""" snmp.Session(host="localhost", version=3, secname="readonly", authprotocol="MD5", authpassword="authpass", privprotocol="AES", privpassword="privpass") def testSnmpV3Protocols(self): """Check accepted auth and privacy protocols""" for auth in ["MD5", "SHA"]: for priv in ["AES", "AES128", "DES"]: snmp.Session(host="localhost", version=3, secname="readonly", authprotocol=auth, authpassword="authpass", privprotocol=priv, privpassword="privpass") self.assertRaises(ValueError, snmp.Session, host="localhost", version=3, secname="readonly", authprotocol="NOEXIST", authpassword="authpass", privprotocol="AES", privpassword="privpass") self.assertRaises(ValueError, snmp.Session, host="localhost", version=3, secname="readonly", authprotocol="MD5", authpassword="authpass", privprotocol="NOEXIST", privpassword="privpass") def testRepresentation(self): """Test session representation""" s = snmp.Session(host="localhost", community="public", version=1) self.assertEqual(repr(s), "Session(host=localhost,version=1)") def testSnmpV3SecLevels(self): """Check accepted security levels""" auth = "MD5" priv = "DES" snmp.Session(host="localhost", version=3, secname="readonly", authprotocol=auth, authpassword="authpass", privprotocol=priv, privpassword="privpass") snmp.Session(host="localhost", version=3, secname="readonly", authprotocol=None, privprotocol=None) snmp.Session(host="localhost", version=3, secname="readonly", authprotocol=auth, authpassword="authpass", privprotocol=None) class TestSnmp1(unittest.TestCase): """ Test communication with an agent with SNMPv1. """ version = 1 @classmethod def addAgent(cls, community, auth, priv): a = agent.TestAgent(community=community, authpass=auth, privpass=priv) cls.agents.append(a) return a @classmethod def setUpClass(cls): mib.load('IF-MIB') mib.load('SNMPv2-MIB') cls.agents = [] cls.agent = cls.addAgent('public', 'public-authpass', 'public-privpass') def setUp(self): params = self.setUpSession(self.agent, 'public') self.session = snmp.Session(**params) def setUpSession(self, agent, password): return dict(host="127.0.0.1:{}".format(agent.port), community=password, version=self.version) @classmethod def tearDownClass(cls): for a in cls.agents: a.terminate() def testGetString(self): """Get a string value""" ooid = mib.get('SNMPv2-MIB', 'sysDescr').oid + (0,) oid, a = self.session.get(ooid)[0] self.assertEqual(oid, ooid) self.assertEqual(a, b"Snimpy Test Agent public") def testGetInteger(self): """Get an integer value""" oid, a = self.session.get(mib.get('IF-MIB', 'ifNumber').oid + (0,))[0] self.assertTrue(a > 1) # At least lo and another interface def testGetEnum(self): """Get an enum value""" oid, a = self.session.get(mib.get('IF-MIB', 'ifType').oid + (1,))[0] self.assertEqual(a, 24) # This is software loopback b = basictypes.build('IF-MIB', 'ifType', a) self.assertEqual(b, "softwareLoopback") def testGetMacAddress(self): """Get a MAC address""" mib.load(os.path.join(os.path.dirname(os.path.abspath(__file__)), "SNIMPY-MIB.mib")) oid, a = self.session.get((1, 3, 6, 1, 2, 1, 45121, 1, 15, 0))[0] self.assertEqual(a, b"\x11\x12\x13\x14\x15\x16") b = basictypes.build('SNIMPY-MIB', 'snimpyMacAddress', a) self.assertEqual(b, "11:12:13:14:15:16") def testGetObjectId(self): """Get ObjectId.""" ooid = mib.get('SNMPv2-MIB', 'sysObjectID').oid + (0,) oid, a = self.session.get(ooid)[0] self.assertEqual(oid, ooid) self.assertEqual(a, (1, 3, 6, 1, 4, 1, 9, 1, 1208)) def testInexistant(self): """Get an inexistant value""" try: self.session.get((1, 2, 3)) self.assertFalse("we should have got an exception") except snmp.SNMPException as ex: self.assertTrue(isinstance(ex, snmp.SNMPNoSuchName) or isinstance(ex, snmp.SNMPNoSuchObject)) def testSetIpAddress(self): """Set IpAddress.""" self.setAndCheck('snimpyIpAddress', '10.14.12.12') def testSetString(self): """Set String.""" self.setAndCheck('snimpyString', 'hello') def testSetInteger(self): """Set Integer.""" self.setAndCheck('snimpyInteger', 1574512) def testSetEnum(self): """Set Enum.""" self.setAndCheck('snimpyEnum', 'testing') def testSetObjectId(self): """Set ObjectId.""" self.setAndCheck('snimpyObjectId', (1, 2, 3, 4, 5, 6)) def testSetCounter(self): """Set Counter.""" self.setAndCheck('snimpyCounter', 545424) def testSetGauge(self): """Set Gauge.""" self.setAndCheck('snimpyGauge', 4857544) def testSetBoolean(self): """Set Boolean.""" self.setAndCheck('snimpyBoolean', True) def testSetTimeticks(self): """Set Timeticks.""" self.setAndCheck('snimpyTimeticks', timedelta(3, 18)) def testSetBits(self): """Set Bits.""" self.setAndCheck('snimpyBits', ["third", "last"]) def testSetMacAddress(self): """Set a MAC address.""" self.setAndCheck('snimpyMacAddress', "a0:b0:c0:d0:e:ff") oid, a = self.session.get((1, 3, 6, 1, 2, 1, 45121, 1, 15, 0))[0] # This is software loopback self.assertEqual(a, b"\xa0\xb0\xc0\xd0\x0e\xff") def setAndCheck(self, oid, value): """Set and check a value""" mib.load(os.path.join(os.path.dirname(os.path.abspath(__file__)), "SNIMPY-MIB.mib")) ooid = mib.get('SNIMPY-MIB', oid).oid + (0,) self.session.set(ooid, basictypes.build('SNIMPY-MIB', oid, value)) self.assertEqual( basictypes.build('SNIMPY-MIB', oid, self.session.get(ooid)[0][1]), basictypes.build('SNIMPY-MIB', oid, value)) def testMultipleGet(self): """Get multiple values at once""" ooid1 = mib.get('SNMPv2-MIB', 'sysDescr').oid + (0,) ooid2 = mib.get('IF-MIB', 'ifNumber').oid + (0,) ooid3 = mib.get('IF-MIB', 'ifType').oid + (1,) (oid1, a1), (oid2, a2), (oid3, a3) = self.session.get( ooid1, ooid2, ooid3) self.assertEqual(oid1, ooid1) self.assertEqual(oid2, ooid2) self.assertEqual(oid3, ooid3) self.assertEqual(a1, b"Snimpy Test Agent public") self.assertTrue(a2 > 1) b = basictypes.build('IF-MIB', 'ifType', a3) self.assertEqual(b, "softwareLoopback") def testBulk(self): """Try to set bulk to different values""" self.session.bulk = 32 self.assertEqual(self.session.bulk, 32) self.assertRaises(ValueError, setattr, self.session, "bulk", 0) self.assertRaises(ValueError, setattr, self.session, "bulk", -10) def testWalk(self): """Check if we can walk""" ooid = mib.get("IF-MIB", "ifDescr").oid results = self.session.walk(ooid) self.assertEqual(tuple(results), ((ooid + (1,), b"lo"), (ooid + (2,), b"eth0"), (ooid + (3,), b"eth1"))) def testSeveralSessions(self): """Test with two sessions""" agent2 = self.addAgent('private', 'private-authpass', 'private-privpass') params = self.setUpSession(agent2, 'private') session2 = snmp.Session(**params) ooid = mib.get('SNMPv2-MIB', 'sysDescr').oid + (0,) oid1, a1 = self.session.get(ooid)[0] oid2, a2 = session2.get(ooid)[0] self.assertEqual(oid1, ooid) self.assertEqual(oid2, ooid) self.assertEqual(a1, b"Snimpy Test Agent public") self.assertEqual(a2, b"Snimpy Test Agent private") @unittest.skipIf(platform.python_implementation() == "PyPy", "unreliable test with Pypy") def testMultipleThreads(self): """Test with multiple sessions in different threads.""" count = 20 agents = [] for i in range(count): agents.append(self.addAgent('community{}'.format(i), 'community{}-authpass'.format(i), 'community{}-privpass'.format(i))) ooid = mib.get('SNMPv2-MIB', 'sysDescr').oid + (0,) threads = [] successes = [] failures = [] lock = multiprocessing.Lock() # Start one thread def run(i): params = self.setUpSession(agents[i], 'community{}'.format(i)) session = snmp.Session(**params) session.timeout = 10 * 1000 * 1000 oid, a = session.get(ooid)[0] exp = ("Snimpy Test Agent community{}".format(i)).encode('ascii') with lock: if oid == ooid and \ a == exp: successes.append("community{}".format(i)) else: failures.append("community{}".format(i)) for i in range(count): threads.append(threading.Thread(target=run, args=(i,))) for i in range(count): threads[i].start() for i in range(count): threads[i].join() self.assertEqual(failures, []) self.assertEqual(sorted(successes), sorted(["community{}".format(i) for i in range(count)])) class TestSnmp2(TestSnmp1): """Test communication with an agent with SNMPv2.""" version = 2 def testInexistantNone(self): """Get an inexistant value but request none""" params = self.setUpSession(self.agent, 'public') params['none'] = True session = snmp.Session(**params) oid, a = session.get((1, 2, 3))[0] self.assertEqual(a, None) def testSetCounter64(self): """Set Counter64.""" self.setAndCheck('snimpyCounter64', 2 ** 47 + 1) def testWalk(self): """Check if we can walk""" ooid = mib.get("IF-MIB", "ifDescr").oid self.session.bulk = 4 results = self.session.walk(ooid) self.assertEqual(tuple(results), ((ooid + (1,), b"lo"), (ooid + (2,), b"eth0"), (ooid + (3,), b"eth1"))) self.session.bulk = 2 results = self.session.walk(ooid) self.assertEqual(tuple(results), ((ooid + (1,), b"lo"), (ooid + (2,), b"eth0"), (ooid + (3,), b"eth1"))) class TestSnmp3(TestSnmp2): """Test communicaton with an agent with SNMPv3.""" version = 3 def setUpSession(self, agent, password): return dict(host="127.0.0.1:{}".format(agent.port), version=3, secname="read-write", authprotocol="MD5", authpassword="{}-authpass".format(password), privprotocol="AES", privpassword="{}-privpass".format(password)) class TestSnmpTransports(unittest.TestCase): """Test communication using IPv6.""" ipv6 = True @classmethod def setUpClass(cls): mib.load('IF-MIB') mib.load('SNMPv2-MIB') def _test(self, ipv6, host): m = agent.TestAgent(ipv6) session = snmp.Session( host="{}:{}".format(host, m.port), community="public", version=2) try: ooid = mib.get('SNMPv2-MIB', 'sysDescr').oid + (0,) oid, a = session.get(ooid)[0] self.assertEqual(a, b"Snimpy Test Agent public") finally: m.terminate() def testIpv4(self): """Test IPv4 transport""" self._test(False, "127.0.0.1") def testIpv4WithDNS(self): """Test IPv4 transport with name resolution""" self._test(False, "localhost") def testIpv6(self): """Test IPv6 transport""" self._test(True, "[::1]") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621075020.0 snimpy-1.0.0/tox.ini0000644000076400001440000000126500000000000013545 0ustar00bernatusers[tox] envlist = py{34,35,36,37,38,39}{,-ipython},lint,doc skip_missing_interpreters = True [gh-actions] python = 3.6: py36 3.7: py37 3.8: py38, lint, doc 3.9: py39 [testenv] deps = coverage ipython: ipython commands = {envpython} {envbindir}/coverage run --source=snimpy setup.py test [testenv:lint] basepython = python3 deps = flake8 twine interrogate whitelist_externals = make commands = make lint python setup.py sdist twine check dist/* [testenv:doc] basepython = python3 changedir = docs deps = mock sphinx sphinx-rtd-theme whitelist_externals = make commands = make html READTHEDOCS=True [coverage:run] relative_files = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622302682.0 snimpy-1.0.0/version.txt0000644000076400001440000000002500000000000014451 0ustar00bernatusers1.0.0-0-gc72a878a653d