├── tests ├── __init__.py ├── conftest.py ├── test_fast.py ├── test_docs.py ├── test_policer.py └── test_ci.py ├── docs ├── LICENSE.md ├── CHANGELOG.md ├── dev │ ├── CONTRIBUTING.md │ ├── CODE_OF_CONDUCT.md │ ├── common.md │ ├── index.md │ ├── environment.md │ ├── codequality.md │ ├── codebase.md │ ├── standards.md │ ├── testing.md │ └── types.md ├── man │ ├── index.md │ └── gufo-snmp.md ├── assets │ ├── logo.png │ ├── horizon.svg │ └── hero.css ├── benchmarks │ ├── v2c │ │ ├── getbulk.png │ │ ├── getnext.png │ │ ├── getbulk_p.png │ │ ├── getnext_p.png │ │ ├── SUMMARY.md │ │ ├── getbulk.md │ │ ├── getnext.md │ │ ├── index.md │ │ ├── getbulk_p.md │ │ ├── getnext_p.md │ │ ├── test_v2c_p4_getbulk.txt │ │ ├── test_v2c_p4_getnext.txt │ │ ├── test_v2c_getbulk.txt │ │ └── test_v2c_getnext.txt │ ├── v3 │ │ ├── getbulk.png │ │ ├── getbulk_p.png │ │ ├── getnext.png │ │ ├── getnext_p.png │ │ ├── SUMMARY.md │ │ ├── getbulk.md │ │ ├── getnext.md │ │ ├── getbulk_p.md │ │ ├── getnext_p.md │ │ ├── index.md │ │ ├── test_v3_getbulk.txt │ │ ├── test_v3_getnext.txt │ │ ├── test_v3_p4_getbulk.txt │ │ └── test_v3_p4_getnext.txt │ ├── SUMMARY.md │ ├── feedback.md │ ├── preparing.md │ ├── conclusions.txt │ ├── conclusions.md │ └── index.md ├── examples │ ├── index.md │ ├── sync │ │ ├── index.md │ │ ├── get.md │ │ ├── debugging.md │ │ └── getnext.md │ └── async │ │ ├── index.md │ │ └── get.md ├── installation.md ├── faq.md ├── gen_doc_stubs.py └── overrides │ └── index.html ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature-request.yml │ └── bug-report.yml └── workflows │ ├── security.yml │ └── build-docs.yml ├── MANIFEST.in ├── .snyk ├── codecov.yml ├── src ├── gufo │ └── snmp │ │ ├── py.typed │ │ ├── async_client │ │ └── __init__.py │ │ ├── sync_client │ │ ├── __init__.py │ │ ├── getnext.py │ │ └── getbulk.py │ │ ├── version.py │ │ ├── typing.py │ │ ├── __init__.py │ │ └── protocol.py ├── buf │ ├── mod.rs │ └── pool.rs ├── socket │ └── mod.rs ├── snmp │ ├── msg │ │ ├── mod.rs │ │ └── v3 │ │ │ ├── mod.rs │ │ │ ├── data.rs │ │ │ └── scoped.rs │ ├── report.rs │ ├── mod.rs │ ├── op │ │ ├── refresh.rs │ │ ├── mod.rs │ │ ├── getiter.rs │ │ ├── getmany.rs │ │ ├── get.rs │ │ ├── getnext.rs │ │ └── getbulk.rs │ ├── getresponse.rs │ └── get.rs ├── ber │ ├── sequence.rs │ ├── option.rs │ ├── opaque.rs │ ├── octetstring.rs │ ├── objectdescriptor.rs │ ├── timeticks.rs │ ├── gauge32.rs │ ├── counter32.rs │ ├── counter64.rs │ ├── uinteger32.rs │ ├── bool.rs │ ├── null.rs │ └── ipaddress.rs ├── privacy │ ├── nopriv.rs │ └── mod.rs ├── auth │ └── noauth.rs ├── util.rs ├── lib.rs └── reqid.rs ├── .gitignore ├── benchmarks ├── README.md ├── conftest.py ├── test_v2c_getnext.py ├── test_v2c_getbulk.py └── test_v3_getnext.py ├── examples ├── sync │ ├── get.py │ ├── debugging.py │ ├── getbulk.py │ ├── getnext.py │ ├── engine-id-discovery.py │ ├── fetch.py │ ├── ratelimit.py │ ├── getmany.py │ └── get-v3.py └── async │ ├── get.py │ ├── debugging.py │ ├── engine-id-discovery.py │ ├── getbulk.py │ ├── getnext.py │ ├── fetch.py │ ├── ratelimit.py │ ├── getmany.py │ └── get-v3.py ├── tools ├── build │ ├── build-sdist.sh │ ├── get-rustup-bin.sh │ ├── build-pgo.sh │ ├── setup-rust.sh │ ├── setup-snmpd.sh │ └── build-linux.sh ├── docs │ ├── run-benchmarks.sh │ ├── update-benchmarks.sh │ └── entrypoint.sh └── dev │ └── fmt-iai.py ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── SECURITY.md ├── Cargo.toml ├── CONTRIBUTING.md ├── benches ├── iai_encode.rs ├── iai_buf.rs ├── cri_encode.rs └── iai_auth.rs ├── LICENSE.md └── .devcontainer └── devcontainer.json /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dvolodin7 -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/dev/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ../../CONTRIBUTING.md -------------------------------------------------------------------------------- /docs/dev/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ../../CODE_OF_CONDUCT.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [gufolabs] 2 | buy_me_a_coffee: dvolodin 3 | -------------------------------------------------------------------------------- /docs/man/index.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP Man Pages 2 | 3 | * [gufo-snmp](gufo-snmp.md) -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/benchmarks/v2c/getbulk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/benchmarks/v2c/getbulk.png -------------------------------------------------------------------------------- /docs/benchmarks/v2c/getnext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/benchmarks/v2c/getnext.png -------------------------------------------------------------------------------- /docs/benchmarks/v3/getbulk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/benchmarks/v3/getbulk.png -------------------------------------------------------------------------------- /docs/benchmarks/v3/getbulk_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/benchmarks/v3/getbulk_p.png -------------------------------------------------------------------------------- /docs/benchmarks/v3/getnext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/benchmarks/v3/getnext.png -------------------------------------------------------------------------------- /docs/benchmarks/v3/getnext_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/benchmarks/v3/getnext_p.png -------------------------------------------------------------------------------- /docs/benchmarks/v2c/getbulk_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/benchmarks/v2c/getbulk_p.png -------------------------------------------------------------------------------- /docs/benchmarks/v2c/getnext_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gufolabs/gufo_snmp/HEAD/docs/benchmarks/v2c/getnext_p.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Cargo.toml 2 | include src/*.rs 3 | include benches/*.rs 4 | prune __pycache__ 5 | global-exclude *.py[cod] -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk policy file 2 | exclude: 3 | global: 4 | - Dockerfile 5 | - examples/** 6 | - tests/** 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - benchmarks/ 3 | - tests/ 4 | - "**/*.pyi" 5 | - "coverage.xml" 6 | - "src/gufo/snmp/snmpd.py" 7 | -------------------------------------------------------------------------------- /docs/benchmarks/v2c/SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [Overview](index.md) 2 | * [GETNEXT](getnext.md) 3 | * [GETBULK](getbulk.md) 4 | * [Parallel GETNEXT](getnext_p.md) 5 | * [Parallel GETBULK](getbulk_p.md) 6 | -------------------------------------------------------------------------------- /docs/benchmarks/v3/SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [Overview](index.md) 2 | * [GETNEXT](getnext.md) 3 | * [GETBULK](getbulk.md) 4 | * [Parallel GETNEXT](getnext_p.md) 5 | * [Parallel GETBULK](getbulk_p.md) 6 | -------------------------------------------------------------------------------- /docs/benchmarks/SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [Overview](index.md) 2 | * [Preparing](preparing.md) 3 | * [SNMP v2c](v2c/) 4 | * [SNMP v3](v3/) 5 | * [Conclusions](conclusions.md) 6 | * [Feedback](feedback.md) 7 | -------------------------------------------------------------------------------- /src/gufo/snmp/py.typed: -------------------------------------------------------------------------------- 1 | # PEP-561 Support File. 2 | # "Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing". -------------------------------------------------------------------------------- /docs/benchmarks/feedback.md: -------------------------------------------------------------------------------- 1 | ## Feedback 2 | 3 | If you have any ideas, comment, or thoughts on benchmark suite, 4 | feel free to [discuss it on GitHub][discussion]. 5 | 6 | [discussion]: https://github.com/gufolabs/gufo_snmp/discussions/31 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | .* 3 | !.gitignore 4 | !.gitkeep 5 | !.devcontainer/ 6 | !.github/ 7 | !.snyk 8 | *.pyc 9 | *.pyo 10 | *.so 11 | /dist/ 12 | /target/ 13 | /build/ 14 | /wheelhouse/ 15 | *.egg-info/ 16 | *.dist-info/ 17 | __pycache__ 18 | /Cargo.lock -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP Bencmarks Suite 2 | 3 | This directory contains Gufo SNMP benchmark suite 4 | which evaluates performance agains most commonly 5 | used Python SNMP Clients. 6 | 7 | See [bencmark section][benchmarks] for latest 8 | actual report. 9 | 10 | [benchmarks]: https://docs.gufolabs.com/gufo_snmp/benchmarks/ -------------------------------------------------------------------------------- /examples/sync/get.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from gufo.snmp.sync_client import SnmpSession 4 | 5 | 6 | def main(addr: str, community: str, oid: str) -> None: 7 | with SnmpSession(addr=addr, community=community) as session: 8 | r = session.get(oid) 9 | print(r) 10 | 11 | 12 | main(sys.argv[1], sys.argv[2], sys.argv[3]) 13 | -------------------------------------------------------------------------------- /examples/sync/debugging.py: -------------------------------------------------------------------------------- 1 | from gufo.snmp.snmpd import Snmpd 2 | from gufo.snmp.sync_client import SnmpSession 3 | 4 | 5 | def main() -> None: 6 | with Snmpd(), SnmpSession(addr="127.0.0.1", port=10161) as session: 7 | for oid, value in session.getnext("1.3.6.1.2.1.1"): 8 | print(f"{oid}: {value}") 9 | 10 | 11 | main() 12 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP: Examples 2 | 3 | This part of the documentation contains a detailed 4 | explanation of the samples from the 5 | [examples/][examples] folder. 6 | 7 | * [sync](sync/index.md): Synchronous mode. 8 | * [async](async/index.md): Asynchronous mode. 9 | 10 | [examples]: https://github.com/gufolabs/gufo_snmp/tree/master/examples -------------------------------------------------------------------------------- /examples/sync/getbulk.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from gufo.snmp.sync_client import SnmpSession 4 | 5 | 6 | def main(addr: str, community: str, oid: str) -> None: 7 | with SnmpSession(addr=addr, community=community) as session: 8 | for k, v in session.getbulk(oid): 9 | print(f"{k}: {v}") 10 | 11 | 12 | main(sys.argv[1], sys.argv[2], sys.argv[3]) 13 | -------------------------------------------------------------------------------- /examples/sync/getnext.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from gufo.snmp.sync_client import SnmpSession 4 | 5 | 6 | def main(addr: str, community: str, oid: str) -> None: 7 | with SnmpSession(addr=addr, community=community) as session: 8 | for k, v in session.getnext(oid): 9 | print(f"{k}: {v}") 10 | 11 | 12 | main(sys.argv[1], sys.argv[2], sys.argv[3]) 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/async/get.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from gufo.snmp import SnmpSession 5 | 6 | 7 | async def main(addr: str, community: str, oid: str) -> None: 8 | async with SnmpSession(addr=addr, community=community) as session: 9 | r = await session.get(oid) 10 | print(r) 11 | 12 | 13 | asyncio.run(main(sys.argv[1], sys.argv[2], sys.argv[3])) 14 | -------------------------------------------------------------------------------- /tools/build/build-sdist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ------------------------------------------------------------------------ 3 | # Gufo Labs: Build source distribution 4 | # ------------------------------------------------------------------------ 5 | # Copyright (C) 2022, Gufo Labs 6 | # ------------------------------------------------------------------------ 7 | 8 | python3 -m build --sdist 9 | -------------------------------------------------------------------------------- /examples/async/debugging.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from gufo.snmp import SnmpSession 4 | from gufo.snmp.snmpd import Snmpd 5 | 6 | 7 | async def main() -> None: 8 | async with Snmpd(), SnmpSession(addr="127.0.0.1", port=10161) as session: 9 | async for oid, value in session.getnext("1.3.6.1.2.1.1"): 10 | print(f"{oid}: {value}") 11 | 12 | 13 | asyncio.run(main()) 14 | -------------------------------------------------------------------------------- /examples/sync/engine-id-discovery.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from gufo.snmp import User 4 | from gufo.snmp.sync_client import SnmpSession 5 | 6 | 7 | def main(addr: str, user_name: str) -> None: 8 | with SnmpSession(addr=addr, user=User(user_name)) as session: 9 | engine_id = session.get_engine_id() 10 | print(engine_id.hex()) 11 | 12 | 13 | main(sys.argv[1], sys.argv[2]) 14 | -------------------------------------------------------------------------------- /examples/async/engine-id-discovery.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from gufo.snmp import SnmpSession, User 5 | 6 | 7 | async def main(addr: str, user_name: str) -> None: 8 | async with SnmpSession(addr=addr, user=User(user_name)) as session: 9 | engine_id = session.get_engine_id() 10 | print(engine_id.hex()) 11 | 12 | 13 | asyncio.run(main(sys.argv[1], sys.argv[2])) 14 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: Gufo SNMP 3 | message: >- 4 | If you use this software as part of a publication and wish to cite 5 | it, please use the metadata from this file. 6 | type: software 7 | authors: 8 | - name: Gufo Labs 9 | website: https://gufolabs.com/ 10 | - name: Project's Contributors 11 | website: https://github.com/gufolabs/gufo_snmp 12 | license: BSD-3-Clause 13 | -------------------------------------------------------------------------------- /examples/sync/fetch.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from gufo.snmp.sync_client import SnmpSession 4 | 5 | 6 | def main(addr: str, community: str, oid: str) -> None: 7 | with SnmpSession( 8 | addr=addr, community=community, allow_bulk=True 9 | ) as session: 10 | for k, v in session.fetch(oid): 11 | print(f"{k}: {v}") 12 | 13 | 14 | main(sys.argv[1], sys.argv[2], sys.argv[3]) 15 | -------------------------------------------------------------------------------- /examples/async/getbulk.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from gufo.snmp import SnmpSession 5 | 6 | 7 | async def main(addr: str, community: str, oid: str) -> None: 8 | async with SnmpSession(addr=addr, community=community) as session: 9 | async for k, v in session.getbulk(oid): 10 | print(f"{k}: {v}") 11 | 12 | 13 | asyncio.run(main(sys.argv[1], sys.argv[2], sys.argv[3])) 14 | -------------------------------------------------------------------------------- /examples/async/getnext.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from gufo.snmp import SnmpSession 5 | 6 | 7 | async def main(addr: str, community: str, oid: str) -> None: 8 | async with SnmpSession(addr=addr, community=community) as session: 9 | async for k, v in session.getnext(oid): 10 | print(f"{k}: {v}") 11 | 12 | 13 | asyncio.run(main(sys.argv[1], sys.argv[2], sys.argv[3])) 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Check instructions for submitting your idea on the mailing list first. 3 | title: "ENH: " 4 | labels: [enhancement] 5 | 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: "Proposed new feature or change:" 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /docs/dev/common.md: -------------------------------------------------------------------------------- 1 | # Developer's Common Tasks 2 | 3 | ## Bump Version 4 | 5 | * [ ] Change `__version__` in `src/gufo/snmp/__init__.py` 6 | * [ ] Change `[package]/version` in `Cargo.toml` 7 | * [ ] Add section in `CHANGELOG.md` 8 | 9 | ## Bump Rust Version 10 | 11 | * [ ] Change `RUST_VERSION` in `tools/build/setup-rust.sh` 12 | 13 | ## Bump pyo3 version 14 | 15 | * [ ] Change `[dependencies]/pyo3` in `Cargo.toml` -------------------------------------------------------------------------------- /examples/sync/ratelimit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from gufo.snmp.sync_client import SnmpSession 4 | 5 | 6 | def main(addr: str, community: str, oid: str) -> None: 7 | with SnmpSession( 8 | addr=addr, community=community, allow_bulk=True, limit_rps=10 9 | ) as session: 10 | for k, v in session.fetch(oid): 11 | print(f"{k}: {v}") 12 | 13 | 14 | main(sys.argv[1], sys.argv[2], sys.argv[3]) 15 | -------------------------------------------------------------------------------- /docs/benchmarks/preparing.md: -------------------------------------------------------------------------------- 1 | Install local Net-SNMPd: 2 | 3 | ``` 4 | ./tools/build/setup-snmpd.sh 5 | ``` 6 | 7 | Install system packages: 8 | 9 | === "RHEL/CentOS" 10 | 11 | ``` 12 | sudo yum install net-snmp-devel 13 | ``` 14 | 15 | === "Debian/Ubuntu" 16 | 17 | ``` 18 | sudo apt-get install libsnmp-dev 19 | ``` 20 | 21 | Install dependencies: 22 | ``` 23 | pip install -e gufo-snmp[test,bench] 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/sync/getmany.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import List 3 | 4 | from gufo.snmp.sync_client import SnmpSession 5 | 6 | 7 | def main(addr: str, community: str, oids: List[str]) -> None: 8 | with SnmpSession(addr=addr, community=community) as session: 9 | r = session.get_many(oids) 10 | for k, v in r.items(): 11 | print(f"{k}: {v}") 12 | 13 | 14 | main(sys.argv[1], sys.argv[2], list(sys.argv[3:])) 15 | -------------------------------------------------------------------------------- /examples/async/fetch.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from gufo.snmp import SnmpSession 5 | 6 | 7 | async def main(addr: str, community: str, oid: str) -> None: 8 | async with SnmpSession( 9 | addr=addr, community=community, allow_bulk=True 10 | ) as session: 11 | async for k, v in session.fetch(oid): 12 | print(f"{k}: {v}") 13 | 14 | 15 | asyncio.run(main(sys.argv[1], sys.argv[2], sys.argv[3])) 16 | -------------------------------------------------------------------------------- /examples/async/ratelimit.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from gufo.snmp import SnmpSession 5 | 6 | 7 | async def main(addr: str, community: str, oid: str) -> None: 8 | async with SnmpSession( 9 | addr=addr, community=community, allow_bulk=True, limit_rps=10 10 | ) as session: 11 | async for k, v in session.fetch(oid): 12 | print(f"{k}: {v}") 13 | 14 | 15 | asyncio.run(main(sys.argv[1], sys.argv[2], sys.argv[3])) 16 | -------------------------------------------------------------------------------- /src/buf/mod.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Buffer implementation 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | pub mod buffer; 9 | pub mod pool; 10 | 11 | pub use buffer::Buffer; 12 | pub use pool::get_buffer_pool; 13 | -------------------------------------------------------------------------------- /src/gufo/snmp/async_client/__init__.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: Async SnmpSession 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-24, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | """Async SnmpSession.""" 8 | 9 | from .client import SnmpSession 10 | 11 | __all__ = ["SnmpSession"] 12 | -------------------------------------------------------------------------------- /examples/async/getmany.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from typing import List 4 | 5 | from gufo.snmp import SnmpSession 6 | 7 | 8 | async def main(addr: str, community: str, oids: List[str]) -> None: 9 | async with SnmpSession(addr=addr, community=community) as session: 10 | r = await session.get_many(oids) 11 | for k, v in r.items(): 12 | print(f"{k}: {v}") 13 | 14 | 15 | asyncio.run(main(sys.argv[1], sys.argv[2], list(sys.argv[3:]))) 16 | -------------------------------------------------------------------------------- /src/gufo/snmp/sync_client/__init__.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: Sync SnmpSession 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-24, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | """Sync SnmpSession.""" 8 | 9 | # Gufo SNMP modules 10 | from .client import SnmpSession 11 | 12 | __all__ = ["SnmpSession"] 13 | -------------------------------------------------------------------------------- /docs/benchmarks/conclusions.txt: -------------------------------------------------------------------------------- 1 | | Test | Sync (ms) | Async (ms) | Async
overhead | 2 | | --- | ---: | ---: | ---: | 3 | | SNMPv2c GETNEXT | 215.17 | 273.66 | 27.18% | 4 | | SNMPv2c GETBULK | 46.62 | 52.56 | 12.75% | 5 | | SNMPv2c GETNEXT (x4) | 357.34 | 487.52 | 36.43% | 6 | | SNMPv2c GETBULK (x4) | 155.51 | 185.20 | 19.09% | 7 | | SNMPv3 GETNEXT | 242.40 | 289.18 | 19.30% | 8 | | SNMPv3 GETBULK | 49.45 | 55.80 | 12.84% | 9 | | SNMPv3 GETNEXT (x4) | 409.16 | 582.81 | 42.44% | 10 | | SNMPv3 GETBULK (x4) | 12.77 | 20.42 | 59.89% | 11 | -------------------------------------------------------------------------------- /src/socket/mod.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Socket classes 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | mod snmpsocket; 9 | mod v1; 10 | mod v2c; 11 | mod v3; 12 | pub use v1::SnmpV1ClientSocket; 13 | pub use v2c::SnmpV2cClientSocket; 14 | pub use v3::SnmpV3ClientSocket; 15 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Check vulnerabilities 2 | on: 3 | workflow_dispatch: # allows running manually 4 | permissions: 5 | contents: read 6 | pull-requests: write 7 | jobs: 8 | safety: 9 | name: Run Safety CLI checks 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Run Safety CLI to check for vulnerabilities 16 | uses: pyupio/safety-action@v1 17 | with: 18 | api-key: ${{ secrets.SAFETY_API_KEY }} 19 | -------------------------------------------------------------------------------- /src/snmp/msg/mod.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP Messages 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | mod v1; 9 | mod v2c; 10 | pub mod v3; 11 | pub use super::pdu::SnmpPdu; 12 | pub use v1::SnmpV1Message; 13 | pub use v2c::SnmpV2cMessage; 14 | pub use v3::SnmpV3Message; 15 | -------------------------------------------------------------------------------- /src/snmp/msg/v3/mod.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP v3 Message 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | mod data; 9 | mod msg; 10 | mod scoped; 11 | mod usm; 12 | pub use data::MsgData; 13 | pub use msg::SnmpV3Message; 14 | pub use scoped::ScopedPdu; 15 | pub use usm::UsmParameters; 16 | -------------------------------------------------------------------------------- /tools/build/get-rustup-bin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # --------------------------------------------------------------------- 3 | # Gufo HTTP: Print components bin directory 4 | # --------------------------------------------------------------------- 5 | 6 | set +e 7 | 8 | RUSTUP=`which rustup` 9 | RUSTUP_HOME=`$RUSTUP show home` 10 | DEFAULT_HOST=`$RUSTUP show | grep "Default host" | sed 's/Default host: //'` 11 | TOOLCHAIN=`$RUSTUP show active-toolchain | cut -d" " -f1` 12 | 13 | BIN="${RUSTUP_HOME}/toolchains/${TOOLCHAIN}/lib/rustlib/${DEFAULT_HOST}/bin" 14 | echo $BIN -------------------------------------------------------------------------------- /tools/docs/run-benchmarks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # --------------------------------------------------------------------- 3 | # Gufo SNMP: Run benchmarks in docker 4 | # --------------------------------------------------------------------- 5 | # Copyright (C) 2024-25, Gufo Labs 6 | # See LICENSE.md for details 7 | # --------------------------------------------------------------------- 8 | 9 | docker run --rm\ 10 | -w /workspaces/gufo_snmp\ 11 | -v $PWD:/workspaces/gufo_snmp\ 12 | python:3.14-slim-trixie\ 13 | /workspaces/gufo_snmp/tools/docs/entrypoint.sh 14 | -------------------------------------------------------------------------------- /docs/dev/index.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP: Developer's Guide 2 | 3 | This section is intended for Gufo SNMP developers and for entities, 4 | including both individuals and companies, interested in contributing to the project. 5 | 6 | - [Developer's Environment](environment.md) 7 | - [Building and Testing](testing.md) 8 | - [Common Tasks](common.md) 9 | - [Code Quality](codequality.md) 10 | - [Code Base](codebase.md) 11 | - [Contributing Guide](CONTRIBUTING.md) 12 | - [Code of Conduct](CODE_OF_CONDUCT.md) 13 | - [Supported Standards](standards.md) 14 | - [Supported BER Types](types.md) 15 | -------------------------------------------------------------------------------- /src/gufo/snmp/version.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: SnmpVersion definitionn 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | """SnmpVersion definition.""" 9 | 10 | # Python modules 11 | import enum 12 | 13 | 14 | class SnmpVersion(enum.IntEnum): 15 | """SNMP protocol version.""" 16 | 17 | v1 = 0 18 | v2c = 1 19 | v3 = 3 20 | -------------------------------------------------------------------------------- /src/gufo/snmp/typing.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: Types definitions 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | """Types definition. 9 | 10 | Attributes: 11 | ValueType: Return type for SNMP query operations. 12 | """ 13 | 14 | # Python modules 15 | from typing import Union 16 | 17 | ValueType = Union[None, str, bytes, int, float] 18 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | # Installation 6 | 7 | Install with the pip 8 | 9 | ``` 10 | $ pip install gufo_snmp 11 | ``` 12 | 13 | ## Checking the Installation 14 | 15 | To check the installation just import the module 16 | 17 | ```python 18 | from gufo.snmp import __version__ 19 | ``` 20 | 21 | ## Upgrading 22 | 23 | To upgrade existing Gufo SNMP installation use pip 24 | 25 | ``` 26 | $ pip install --upgrade gufo_snmp 27 | ``` 28 | 29 | ## Uninstalling 30 | 31 | To uninstall Gufo SNMP use pip 32 | 33 | ``` 34 | $ pip uninstall gufo_snmp 35 | ``` 36 | -------------------------------------------------------------------------------- /src/snmp/report.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Report PDU Parser 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use crate::error::SnmpError; 9 | 10 | pub struct SnmpReport<'a>(pub &'a [u8]); 11 | 12 | impl<'a> TryFrom<&'a [u8]> for SnmpReport<'a> { 13 | type Error = SnmpError; 14 | 15 | fn try_from(value: &'a [u8]) -> Result { 16 | Ok(SnmpReport(value)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/examples/sync/index.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP Examples: Sync Mode. 2 | 3 | This part of the documentation contains a detailed 4 | explanation of the samples from the [examples/sync/][examples] folder. 5 | 6 | * [get.py](get.md): Single Get Request 7 | * [getmany.py](getmany.md): Multi Items Get Request 8 | * [getnext.py](getnext.md): GetNext Request 9 | * [getbulk.py](getbulk.md): GetBulk Request 10 | * [fetch.py](fetch.md): Fetch 11 | * [get-v3.py](get-v3.md): SNMPv3 Get Request. 12 | * [engine-id-discovery.py](engine-id-discovery.md): SNMPv3 Engine ID Discovery 13 | * [debugging.py](debugging.md): Debugging 14 | 15 | [examples]: https://github.com/gufolabs/gufo_snmp/tree/master/examples/sync -------------------------------------------------------------------------------- /docs/examples/async/index.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP Examples: Async Mode 2 | 3 | This part of the documentation contains a detailed 4 | explanation of the samples from the [examples/async/][examples] folder. 5 | 6 | * [get.py](get.md): Single Get Request 7 | * [getmany.py](getmany.md): Multi Items Get Request 8 | * [getnext.py](getnext.md): GetNext Request 9 | * [getbulk.py](getbulk.md): GetBulk Request 10 | * [fetch.py](fetch.md): Fetch 11 | * [get-v3.py](get-v3.md): SNMPv3 Get Request. 12 | * [engine-id-discovery.py](engine-id-discovery.md): SNMPv3 Engine ID Discovery 13 | * [debugging.py](debugging.md): Debugging 14 | 15 | [examples]: https://github.com/gufolabs/gufo_snmp/tree/master/examples/async -------------------------------------------------------------------------------- /docs/dev/environment.md: -------------------------------------------------------------------------------- 1 | # Developer's Environment 2 | 3 | To participate in development you need to prepare the developer's 4 | environment first. Depending on the preferable tools, your mileage may vary. 5 | 6 | ## Visual Studio Code Dev Container 7 | 8 | The easiest way to start the development is to use [Visual Studio Code][VSCode] 9 | with [Remote Containers][Remote Containers] plugin. Just click on the green sign 10 | in the lower-left corner and select the "Reopen in Container" menu item. You'll get 11 | all the required formatting and linting settings out of the box. 12 | 13 | [VSCode]: https://code.visualstudio.com/ 14 | [Remote Containers]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Gufo Stack Code of Conduct 2 | 3 | Gufo Stack are the tools built by people for people. We aim to create a respectful, collaborative, 4 | and constructive community where everyone can contribute to building better software. 5 | 6 | ## The Rule 7 | 8 | > *“And as you wish that others would do to you, do so to them.”* 9 | > 10 | > -- Luke 6:31 11 | 12 | ## The Note 13 | 14 | This principle is universal. Similar wisdom can be found in many traditions: 15 | 16 | - Confucius: *“Do not impose on others what you do not wish for yourself.”* (Analects 15:24) 17 | - Prophet Muhammad (peace be upon him): *“None of you [truly] believes until he loves for his brother what he loves for himself.”* (Hadith, Sahih Muslim 45:71) 18 | 19 | That’s all. -------------------------------------------------------------------------------- /docs/benchmarks/v2c/getbulk.md: -------------------------------------------------------------------------------- 1 | Perform SNMP v2c GETBULK requests to iterate through whole MIB. This test evaluates: 2 | 3 | * The efficiency of the network stack. 4 | * The efficiency of BER encoder and decoder. 5 | * The efficiency of the BER-to-Python data types mapping. 6 | 7 | Look at the [source code][source] for details. 8 | 9 | !!! notes 10 | 11 | * easysnmp doesn't supports async mode 12 | 13 | Run tests: 14 | 15 | ``` 16 | pytest benchmarks/test_v2c_getbulk.py 17 | ``` 18 | 19 | **Results (lower is better)** 20 | 21 | ``` 22 | --8<-- "docs/benchmarks/v2c/test_v2c_getbulk.txt" 23 | ``` 24 | 25 | ![Median chart](getbulk.png) 26 | *Lower is better* 27 | 28 | [source]: https://github.com/gufolabs/gufo_snmp/blob/master/benchmarks/test_v2c_getbulk.py -------------------------------------------------------------------------------- /docs/benchmarks/v2c/getnext.md: -------------------------------------------------------------------------------- 1 | Perform SNMP v2c GETNEXT requests to iterate through whole MIB. This test evaluates: 2 | 3 | * The efficiency of the network stack. 4 | * The efficiency of BER encoder and decoder. 5 | * The efficiency of the BER-to-Python data types mapping. 6 | 7 | Look at the [source code][source] for details. 8 | 9 | !!! notes 10 | 11 | * easysnmp doesn't supports async mode 12 | 13 | Run tests: 14 | 15 | ``` 16 | pytest benchmarks/test_v2c_getnext.py 17 | ``` 18 | 19 | **Results (lower is better)** 20 | 21 | ``` 22 | --8<-- "docs/benchmarks/v2c/test_v2c_getnext.txt" 23 | ``` 24 | 25 | ![Median chart](getnext.png) 26 | *Lower is better* 27 | 28 | [source]: https://github.com/gufolabs/gufo_snmp/blob/master/benchmarks/test_v2c_getnext.py -------------------------------------------------------------------------------- /docs/benchmarks/v2c/index.md: -------------------------------------------------------------------------------- 1 | **SNMP v2c** uses plaintext, non-encrypted BER-encoded messages with simple, community-based authorization. 2 | 3 | This benchmark suite evaluates the following aspects of SNMP v2c operations: 4 | 5 | - [**GETNEXT**](getnext.md) — Sequential iteration over the entire MIB using 6 | the **GETNEXT** operation. 7 | - [**GETBULK**](getbulk.md) — Sequential iteration over the entire MIB 8 | using the **GETBULK** operation. 9 | - [**GETNEXT (Parallel)**](getnext_p.md) — Four parallel sessions performing 10 | sequential iteration over the entire MIB using the **GETNEXT** operation. 11 | - [**GETBULK (Parallel)**](getbulk_p.md) — Four parallel sessions performing 12 | sequential iteration over the entire MIB using the **GETBULK** operation. 13 | -------------------------------------------------------------------------------- /docs/assets/horizon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/benchmarks/v3/getbulk.md: -------------------------------------------------------------------------------- 1 | Perform SNMP v3 GETBULK requests to iterate through whole MIB. 2 | Use SHA-1 hasing and AES-128 encryption. This test evaluates: 3 | 4 | * The efficiency of the network stack. 5 | * The efficiency of BER encoder and decoder. 6 | * The efficiency of the BER-to-Python data types mapping. 7 | * The efficiency of the crypto stack. 8 | 9 | Look at the [source code][source] for details. 10 | 11 | !!! notes 12 | 13 | * easysnmp doesn't supports async mode 14 | 15 | Run tests: 16 | 17 | ``` 18 | pytest benchmarks/test_v3_getbulk.py 19 | ``` 20 | 21 | **Results (lower is better)** 22 | 23 | ``` 24 | --8<-- "docs/benchmarks/v3/test_v3_getbulk.txt" 25 | ``` 26 | 27 | ![Median chart](getbulk.png) 28 | *Lower is better* 29 | 30 | [source]: https://github.com/gufolabs/gufo_snmp/blob/master/benchmarks/test_v3_getbulk.py -------------------------------------------------------------------------------- /docs/benchmarks/v3/getnext.md: -------------------------------------------------------------------------------- 1 | Perform SNMP v3 GETNEXT requests to iterate through whole MIB. 2 | Use SHA-1 hasing and AES-128 encryption. This test evaluates: 3 | 4 | * The efficiency of the network stack. 5 | * The efficiency of BER encoder and decoder. 6 | * The efficiency of the BER-to-Python data types mapping. 7 | * The efficiency of the crypto stack. 8 | 9 | Look at the [source code][source] for details. 10 | 11 | !!! notes 12 | 13 | * easysnmp doesn't supports async mode 14 | 15 | Run tests: 16 | 17 | ``` 18 | pytest benchmarks/test_v3_getnext.py 19 | ``` 20 | 21 | **Results (lower is better)** 22 | 23 | ``` 24 | --8<-- "docs/benchmarks/v3/.txt"test_v3_getnext 25 | ``` 26 | 27 | ![Median chart](getnext.png) 28 | *Lower is better* 29 | 30 | [source]: https://github.com/gufolabs/gufo_snmp/blob/master/benchmarks/test_v3_getnext.py -------------------------------------------------------------------------------- /docs/dev/codequality.md: -------------------------------------------------------------------------------- 1 | # Code Quality Guide 2 | 3 | We share the common code quality standards between all Gufo Labs projects. 4 | 5 | ## Python Code Formatting 6 | 7 | All Python code must be formatting using [Ruff][Ruff] code formatter 8 | with settings defined in the project's `pyproject.toml` file. 9 | 10 | ## Python Code Linting 11 | 12 | All Python code must pass [ruff][ruff] tests with the project's settings. 13 | 14 | ## Python Code Static Checks 15 | 16 | All python code must pass [Mypy][Mypy] type checks in the `strict` mode. 17 | 18 | ## Test Suite Coverage 19 | 20 | The test suite must provide 100% code coverage whenever possible. 21 | 22 | ## Documentation Standards 23 | 24 | * Documentation must be clean and mean. 25 | 26 | [Ruff]: https://github.com/charliermarsh/ruff 27 | [Mypy]: https://mypy.readthedocs.io/en/stable/ 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.14-slim-trixie AS dev 2 | COPY . /workspaces/gufo_snmp 3 | WORKDIR /workspaces/gufo_snmp 4 | ENV \ 5 | PATH=/usr/local/cargo/bin:$PATH\ 6 | RUSTUP_HOME=/usr/local/rustup\ 7 | CARGO_HOME=/usr/local/cargo 8 | RUN \ 9 | set -x \ 10 | && apt-get clean \ 11 | && apt-get update \ 12 | && apt-get -y dist-upgrade \ 13 | && apt-get -y autoremove\ 14 | && apt-get install -y --no-install-recommends\ 15 | git\ 16 | ca-certificates\ 17 | gcc\ 18 | libc6-dev\ 19 | curl\ 20 | snmpd\ 21 | && ./tools/build/setup-rust.sh \ 22 | && rustup component add\ 23 | rust-analysis\ 24 | rust-src \ 25 | rust-analyzer\ 26 | clippy\ 27 | rustfmt\ 28 | && pip install --upgrade pip\ 29 | && pip install --upgrade build\ 30 | && pip install -e .[build,docs,ipython,lint,test] 31 | -------------------------------------------------------------------------------- /src/ber/sequence.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: BER SEQUENCE Class 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_SEQUENCE, Tag}; 9 | use crate::error::SnmpResult; 10 | 11 | pub struct SnmpSequence<'a>(pub(crate) &'a [u8]); 12 | 13 | impl<'a> BerDecoder<'a> for SnmpSequence<'a> { 14 | const ALLOW_PRIMITIVE: bool = false; 15 | const ALLOW_CONSTRUCTED: bool = true; 16 | const TAG: Tag = TAG_SEQUENCE; 17 | 18 | // Implement X.690 pp 8.9: Encoding of a sequence value 19 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 20 | Ok(SnmpSequence(&i[..h.length])) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/snmp/mod.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP module definition 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use crate::ber::Tag; 9 | 10 | const SNMP_V1: u8 = 0; 11 | const SNMP_V2C: u8 = 1; 12 | const SNMP_V3: u8 = 3; 13 | 14 | const PDU_GET_REQUEST: Tag = 0; 15 | const PDU_GETNEXT_REQUEST: Tag = 1; 16 | const PDU_GET_RESPONSE: Tag = 2; 17 | // const PDU_SET_REQUEST: Tag = 3; 18 | // const PDU_TRAP: Tag = 4; 19 | const PDU_GET_BULK_REQUEST: Tag = 5; 20 | const PDU_REPORT: Tag = 8; 21 | 22 | pub mod get; 23 | pub mod getbulk; 24 | pub mod getresponse; 25 | pub mod msg; 26 | pub mod op; 27 | pub mod pdu; 28 | pub mod report; 29 | pub mod value; 30 | -------------------------------------------------------------------------------- /docs/benchmarks/v3/getbulk_p.md: -------------------------------------------------------------------------------- 1 | Perform SNMP v3 GETBULK requests to iterate through whole MIB with concurrency of 4 2 | maintaining single client session per thread/coroutine. Use SHA-1 hasing and AES-128 encryption. 3 | This test evaluates: 4 | 5 | * The efficiency of the network stack. 6 | * The efficiency of BER encoder and decoder. 7 | * The efficiency of the BER-to-Python data types mapping. 8 | * Granularity of the internal locks. 9 | * Ability to release GIL when runnning native code. 10 | 11 | Look at the [source code][source] for details. 12 | 13 | !!! notes 14 | 15 | * easysnmp doesn't supports async mode 16 | 17 | Run tests: 18 | 19 | ``` 20 | pytest benchmarks/test_v3_p4_getbulk.py 21 | ``` 22 | 23 | **Results (lower is better)** 24 | 25 | ``` 26 | --8<-- "docs/benchmarks/v3/test_v3_p4_getbulk.txt" 27 | ``` 28 | 29 | ![Median chart](getbulk_p.png) 30 | *Lower is better* 31 | 32 | [source]: https://github.com/gufolabs/gufo_snmp/blob/master/benchmarks/test_v3_p4_getbulk.py -------------------------------------------------------------------------------- /docs/benchmarks/v3/getnext_p.md: -------------------------------------------------------------------------------- 1 | Perform SNMP v2c GETNEXT requests to iterate through whole MIB with concurrency of 4 2 | maintaining single client session per thread/coroutine. Use SHA-1 hasing and AES-128 encryption. 3 | This test evaluates: 4 | 5 | * The efficiency of the network stack. 6 | * The efficiency of BER encoder and decoder. 7 | * The efficiency of the BER-to-Python data types mapping. 8 | * Granularity of the internal locks. 9 | * Ability to release GIL when runnning native code. 10 | 11 | Look at the [source code][source] for details. 12 | 13 | !!! notes 14 | 15 | * easysnmp doesn't supports async mode 16 | 17 | Run tests: 18 | 19 | ``` 20 | pytest benchmarks/test_v3_p4_getnext.py 21 | ``` 22 | 23 | **Results (lower is better)** 24 | 25 | ``` 26 | --8<-- "docs/benchmarks/v3/test_v3_p4_getnext.txt" 27 | ``` 28 | 29 | ![Median chart](getnext_p.png) 30 | *Lower is better* 31 | 32 | [source]: https://github.com/gufolabs/gufo_snmp/blob/master/benchmarks/test_v3_p4_getnext.py -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | # FAQ 6 | 7 | What is "Gufo"? 8 | 9 | *Gufo* means *the Owl* in Italian. 10 | 11 | Why the owls? 12 | 13 | We love owls and the viable parts of our technologies 14 | were proven at the project, named "the Owl". 15 | 16 | What is "Gufo Labs"? 17 | 18 | [Gufo Labs](https://gufolabs.com/) is the Milan-based company specialized on 19 | network and IT consulting, and on software research. 20 | 21 | What is "Gufo Stack"? 22 | 23 | We've extracted core components behind the [NOC](https://getnoc.com/) 24 | and released them as independent packages, available under the terms 25 | of the 3-clause BSD license. Our software shares common code quality standards 26 | and is battle-proven under the high load. We hope our key components will help 27 | the engineers and the developers to build reliable networks and robust network 28 | management software. 29 | See [more for details](https://gufolabs.com/products/gufo-stack/). 30 | -------------------------------------------------------------------------------- /docs/benchmarks/v2c/getbulk_p.md: -------------------------------------------------------------------------------- 1 | Perform SNMP v2c GETBULK requests to iterate through whole MIB with concurrency of 4 2 | maintaining single client session per thread/coroutine. 3 | 4 | * The efficiency of the network stack. 5 | * The efficiency of BER encoder and decoder. 6 | * The efficiency of the BER-to-Python data types mapping. 7 | * Granularity of the internal locks. 8 | * Ability to release GIL when runnning native code. 9 | 10 | Look at the [source code][source] for details. 11 | 12 | !!! notes 13 | 14 | * easysnmp doesn't supports async mode. 15 | * easysnmp causes almost constant SEGFAULTs, so we were forced to turn off appropriate test. 16 | 17 | Run tests: 18 | 19 | ``` 20 | pytest benchmarks/test_v2c_p4_getnext.py 21 | ``` 22 | 23 | **Results (lower is better)** 24 | 25 | ``` 26 | --8<-- "docs/benchmarks/v2c/test_v2c_p4_getbulk.txt" 27 | ``` 28 | 29 | ![Median chart](getbulk_p.png) 30 | *Lower is better* 31 | 32 | [source]: https://github.com/gufolabs/gufo_snmp/blob/master/benchmarks/test_v2c_p4_getbulk.py -------------------------------------------------------------------------------- /docs/benchmarks/v2c/getnext_p.md: -------------------------------------------------------------------------------- 1 | Perform SNMP v2c GETNEXT requests to iterate through whole MIB with concurrency of 4 2 | maintaining single client session per thread/coroutine. 3 | 4 | * The efficiency of the network stack. 5 | * The efficiency of BER encoder and decoder. 6 | * The efficiency of the BER-to-Python data types mapping. 7 | * Granularity of the internal locks. 8 | * Ability to release GIL when runnning native code. 9 | 10 | Look at the [source code][source] for details. 11 | 12 | !!! notes 13 | 14 | * easysnmp doesn't supports async mode. 15 | * easysnmp causes almost constant SEGFAULTs, so we were forced to turn off appropriate test. 16 | 17 | Run tests: 18 | 19 | ``` 20 | pytest benchmarks/test_v2c_p4_getnext.py 21 | ``` 22 | 23 | **Results (lower is better)** 24 | 25 | ``` 26 | --8<-- "docs/benchmarks/v2c/test_v2c_p4_getnext.txt" 27 | ``` 28 | 29 | ![Median chart](getnext_p.png) 30 | *Lower is better* 31 | 32 | [source]: https://github.com/gufolabs/gufo_snmp/blob/master/benchmarks/test_v2c_p4_getnext.py -------------------------------------------------------------------------------- /tools/build/build-pgo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ------------------------------------------------------------------------ 3 | # Build version which collects PGO data 4 | # ------------------------------------------------------------------------ 5 | # Copyright (C) 2022-25, Gufo Labs 6 | # ------------------------------------------------------------------------ 7 | 8 | set -e 9 | 10 | PGO_DATA_DIR=$1 11 | if [ "$PGO_DATA_DIR" = "" ]; then 12 | echo "PGO data dir must be set" 13 | exit 1 14 | fi 15 | 16 | # Collect PGO 17 | echo "Building profiling version" 18 | rm -rf target/ 19 | RUSTFLAGS="-Cprofile-generate=$PGO_DATA_DIR -Cllvm-args=-pgo-warn-missing-function" python3 -m pip install --editable . 20 | echo "Collecting PGO data" 21 | PYTHONPATH=src/:$PYTHONPATH python3 ./tools/build/pgo-runner.py 22 | rm -f src/gufo/snmp/*.so 23 | echo "Merging profdata" 24 | $(./tools/build/get-rustup-bin.sh)/llvm-profdata merge -o $PGO_DATA_DIR/merged.profdata $PGO_DATA_DIR 25 | echo "PGO profile is written into $PGO_DATA_DIR/merged.profdata" 26 | -------------------------------------------------------------------------------- /examples/sync/get-v3.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from gufo.snmp import Aes128Key, DesKey, Md5Key, Sha1Key, User 4 | from gufo.snmp.sync_client import SnmpSession 5 | 6 | AUTH_ALG = { 7 | "md5": Md5Key, 8 | "sha": Sha1Key, 9 | } 10 | 11 | PRIV_ALG = { 12 | "des": DesKey, 13 | "aes128": Aes128Key, 14 | } 15 | 16 | 17 | def get_user() -> User: 18 | name = sys.argv[2] 19 | if len(sys.argv) > 4: 20 | auth_alg, key = sys.argv[4].split(":", 1) 21 | auth_key = AUTH_ALG[auth_alg](key.encode()) 22 | else: 23 | auth_key = None 24 | if len(sys.argv) > 5: 25 | priv_alg, key = sys.argv[5].split(":", 1) 26 | priv_key = PRIV_ALG[priv_alg](key.encode()) 27 | else: 28 | priv_key = None 29 | return User(name, auth_key=auth_key, priv_key=priv_key) 30 | 31 | 32 | def main(addr: str, user: User, oid: str) -> None: 33 | with SnmpSession(addr=addr, user=user) as session: 34 | r = session.get(oid) 35 | print(r) 36 | 37 | 38 | main(sys.argv[1], get_user(), sys.argv[3]) 39 | -------------------------------------------------------------------------------- /docs/benchmarks/v3/index.md: -------------------------------------------------------------------------------- 1 | **SNMP v3** introduces the following operation modes: 2 | 3 | - **Plaintext** — Matches SNMP v2c but introduces a User Security Model (USM). 4 | - **Integrity Protection** — Protects messages from tampering using a hash-based signature. 5 | - **Privacy Protection** — Encrypts messages to ensure confidentiality. 6 | 7 | This benchmark suite focuses on the efficiency of cryptographic operations using 8 | AES-128 and SHA-1 modes, and evaluates the following aspects of SNMP v3 operations: 9 | 10 | - [**GETNEXT**](getnext.md) — Sequential iteration over the entire MIB using 11 | the **GETNEXT** operation. 12 | - [**GETBULK**](getbulk.md) — Sequential iteration over the entire MIB 13 | using the **GETBULK** operation. 14 | - [**GETNEXT (Parallel)**](getnext_p.md) — Four parallel sessions performing 15 | sequential iteration over the entire MIB using the **GETNEXT** operation. 16 | - [**GETBULK (Parallel)**](getbulk_p.md) — Four parallel sessions performing 17 | sequential iteration over the entire MIB using the **GETBULK** operation. 18 | -------------------------------------------------------------------------------- /examples/async/get-v3.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from gufo.snmp import Aes128Key, DesKey, Md5Key, Sha1Key, SnmpSession, User 5 | 6 | AUTH_ALG = { 7 | "md5": Md5Key, 8 | "sha": Sha1Key, 9 | } 10 | 11 | PRIV_ALG = { 12 | "des": DesKey, 13 | "aes128": Aes128Key, 14 | } 15 | 16 | 17 | def get_user() -> User: 18 | name = sys.argv[2] 19 | if len(sys.argv) > 4: 20 | auth_alg, key = sys.argv[4].split(":", 1) 21 | auth_key = AUTH_ALG[auth_alg](key.encode()) 22 | else: 23 | auth_key = None 24 | if len(sys.argv) > 5: 25 | priv_alg, key = sys.argv[5].split(":", 1) 26 | priv_key = PRIV_ALG[priv_alg](key.encode()) 27 | else: 28 | priv_key = None 29 | return User(name, auth_key=auth_key, priv_key=priv_key) 30 | 31 | 32 | async def main(addr: str, user: User, oid: str) -> None: 33 | async with SnmpSession(addr=addr, user=user) as session: 34 | r = await session.get(oid) 35 | print(r) 36 | 37 | 38 | asyncio.run(main(sys.argv[1], get_user(), sys.argv[3])) 39 | -------------------------------------------------------------------------------- /src/snmp/op/refresh.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Get+Report operation 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{GetIter, PyOp}; 9 | use crate::snmp::get::SnmpGet; 10 | use crate::snmp::msg::SnmpPdu; 11 | use pyo3::{prelude::*, types::PyNone}; 12 | 13 | pub struct OpRefresh; 14 | 15 | impl<'a> PyOp<'a, ()> for OpRefresh { 16 | // Obj is str 17 | fn from_python(_obj: (), request_id: i64) -> PyResult> { 18 | Ok(SnmpPdu::GetRequest(SnmpGet { 19 | request_id, 20 | vars: vec![], 21 | })) 22 | } 23 | fn to_python<'py>( 24 | _pdu: &SnmpPdu, 25 | _iter: Option<&mut GetIter>, 26 | py: Python<'py>, 27 | ) -> PyResult> { 28 | Ok(PyNone::get(py).as_any().to_owned()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/snmp/op/mod.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Python interface for requests and reply 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | pub mod get; 9 | pub mod getbulk; 10 | pub mod getiter; 11 | pub mod getmany; 12 | pub mod getnext; 13 | pub mod refresh; 14 | 15 | use super::msg::SnmpPdu; 16 | pub use get::OpGet; 17 | pub use getbulk::OpGetBulk; 18 | pub use getiter::GetIter; 19 | pub use getmany::OpGetMany; 20 | pub use getnext::OpGetNext; 21 | use pyo3::prelude::*; 22 | pub use refresh::OpRefresh; 23 | 24 | pub trait PyOp<'a, T> 25 | where 26 | T: 'a, 27 | { 28 | fn from_python(obj: T, request_id: i64) -> PyResult>; 29 | fn to_python<'py>( 30 | pdu: &SnmpPdu, 31 | iter: Option<&mut GetIter>, 32 | py: Python<'py>, 33 | ) -> PyResult>; 34 | } 35 | -------------------------------------------------------------------------------- /tools/docs/update-benchmarks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # --------------------------------------------------------------------- 3 | # Gufo SNMP: Run benchmarks suit and update docs results 4 | # --------------------------------------------------------------------- 5 | # Copyright (C) 2024-25, Gufo Labs 6 | # See LICENSE.md for details 7 | # --------------------------------------------------------------------- 8 | 9 | set -e 10 | 11 | PYTEST=pytest 12 | PYTHON=python3 13 | 14 | # Collecting PGO 15 | PGO_DATA_DIR=`mktemp -d` 16 | ./tools/build/build-pgo.sh $PGO_DATA_DIR 17 | # Build 18 | python -m pip install --editable . 19 | # Clearing PGO 20 | rm -rf $PGO_DATA_DIR 21 | 22 | # Run benchmarks 23 | for f in benchmarks/test_*.py; do 24 | # Extract parts from filename 25 | base=$(basename "$f" .py) # e.g., test_v2c_getbulk 26 | proto=$(echo "$base" | cut -d_ -f2) # v2c or v3 27 | outfile="docs/benchmarks/$proto/${base}.txt" 28 | echo "Running $f..." 29 | $PYTEST "$f" > "$outfile" 30 | done 31 | 32 | # Update charts 33 | $PYTHON tools/docs/update-bench-charts.py 34 | -------------------------------------------------------------------------------- /docs/gen_doc_stubs.py: -------------------------------------------------------------------------------- 1 | # Generate reference pages 2 | from pathlib import Path 3 | 4 | import mkdocs_gen_files 5 | 6 | nav = mkdocs_gen_files.Nav() 7 | 8 | for path in sorted(Path("src").rglob("*.py")): 9 | module_path = path.relative_to("src").with_suffix("") 10 | doc_path = path.relative_to("src").with_suffix(".md") 11 | full_doc_path = Path("reference", doc_path) 12 | parts = tuple(module_path.parts) 13 | if parts[-1] == "__init__": 14 | parts = parts[:-1] 15 | doc_path = doc_path.with_name("index.md") 16 | full_doc_path = full_doc_path.with_name("index.md") 17 | elif parts[-1] == "__main__": 18 | continue 19 | nav[("gufo.snmp",) + parts[2:]] = str(doc_path) 20 | with mkdocs_gen_files.open(full_doc_path, "w") as fd: 21 | identifier = ".".join(parts) 22 | print(f"# {identifier}\n\n::: {identifier}", file=fd) 23 | mkdocs_gen_files.set_edit_path(full_doc_path, path) 24 | 25 | with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: 26 | nav_file.writelines(nav.build_literate_nav()) 27 | -------------------------------------------------------------------------------- /tools/docs/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # --------------------------------------------------------------------- 3 | # Gufo SNMP: Docker entrypoint for benchmarks 4 | # --------------------------------------------------------------------- 5 | # Copyright (C) 2024-25, Gufo Labs 6 | # See LICENSE.md for details 7 | # --------------------------------------------------------------------- 8 | 9 | set -e 10 | 11 | export PATH=/usr/local/cargo/bin:$PATH 12 | export RUSTUP_HOME=/usr/local/rustup 13 | export CARGO_HOME=/usr/local/cargo 14 | export PYTHONPATH=src 15 | 16 | echo "Installing system dependencies..." 17 | apt-get clean 18 | apt-get update 19 | apt-get install -y --no-install-recommends\ 20 | ca-certificates\ 21 | curl\ 22 | gcc\ 23 | libc6-dev\ 24 | snmpd\ 25 | libsnmp-dev 26 | echo "Updating pip..." 27 | pip install --upgrade pip 28 | echo "Installing requirements..." 29 | pip install -e .[test,bench] 30 | echo "Setting up rust..." 31 | ./tools/build/setup-rust.sh 32 | rustup component add llvm-tools-preview 33 | echo "Benchmarking" 34 | ./tools/docs/update-benchmarks.sh 35 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Docs 2 | env: 3 | PIP_CACHE_DIR: .pip 4 | PYTHONPATH: src 5 | on: 6 | push: 7 | paths: 8 | - ".github/workflows/build-docs.yml" 9 | - "pyproject.toml" 10 | - "docs/**" 11 | - "examples/**" 12 | - "**.md" 13 | - "**.py" 14 | branches: 15 | - master 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | jobs: 20 | build-push: 21 | name: "Build & Push Docs" 22 | runs-on: ubuntu-24.04 23 | steps: 24 | # Checkout source code 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | # Cache dependencies 29 | - name: Cache Dependencies 30 | uses: actions/cache@v4 31 | with: 32 | path: ./.pip 33 | key: ${{ runner.os }}-docs-${{ hashFiles('pyproject.toml') }} 34 | 35 | # Install dependencies 36 | - name: Install Dependencies 37 | run: | 38 | pip install -IU -e .[docs] 39 | 40 | # Build documentationn 41 | - name: Build & Deploy Docs 42 | run: | 43 | mkdocs gh-deploy --force 44 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities. Which versions 6 | are eligible for receiving such patches depending on the CVSS v3.0 Rating: 7 | 8 | | CVSS v3.0 | Supported Versions | 9 | | --------- | --------------------------------------- | 10 | | 9.0-10.0 | Releases within the previous six months | 11 | | 4.0-8.9 | Most recent release | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Please report (suspected) security vulnerabilities via 16 | [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). 17 | From the repository's main page: 18 | 19 | * Select "Security". 20 | * In the "Reporting" section select "Advisories". 21 | * Press the "New draft security advisory" button. 22 | * Complete the security report. 23 | 24 | You will receive a response from us within 48 hours. 25 | Once the issue is confirmed, we will release a patch as soon 26 | as possible depending on complexity but historically within a few days. 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2024" 3 | name = "gufo_snmp" 4 | version = "0.10.0" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] # Comment for bench 8 | # crate-type = ["cdylib", "rlib"] # Uncomment for bench 9 | name = "gufo_snmp" 10 | 11 | [profile.release] 12 | lto = "fat" # Full link-time optimization 13 | strip = "debuginfo" 14 | 15 | [dependencies] 16 | aes = "0.8" 17 | cbc = "0.1" 18 | cfb-mode = "0.8" 19 | cipher = "0.4" 20 | des = "0.8" 21 | digest = "0.10" 22 | enum_dispatch = "0.3" 23 | md-5 = "0.10" 24 | nom = "8.0" 25 | pyo3 = {version = "0.26", features = ["extension-module"]} 26 | rand = "0.9" 27 | sha1 = "0.10" 28 | socket2 = {version = "0.6", features = ["all"]} 29 | 30 | [dev-dependencies] 31 | criterion = "0.4" 32 | iai = "0.1" 33 | test-case = "3" 34 | 35 | # [[bench]] 36 | # harness = false 37 | # name = "cri_decode" 38 | 39 | # [[bench]] 40 | # harness = false 41 | # name = "cri_encode" 42 | 43 | [[bench]] 44 | harness = false 45 | name = "iai_decode" 46 | 47 | [[bench]] 48 | harness = false 49 | name = "iai_encode" 50 | 51 | [[bench]] 52 | harness = false 53 | name = "iai_buf" 54 | 55 | [[bench]] 56 | harness = false 57 | name = "iai_auth" 58 | -------------------------------------------------------------------------------- /docs/overrides/index.html: -------------------------------------------------------------------------------- 1 | {% extends "main.html" %} 2 | {% block styles %} 3 | {{ super() }} 4 | 5 | {% endblock %} 6 | {% block hero %} 7 |
8 |
9 |
10 |
11 | Hero Image 12 |
13 |
14 |

{{ page.meta.hero.title }}

15 |

{{ page.meta.hero.subtitle }}

16 | 18 | {{ page.meta.hero.install_button }} 19 | 20 | {{ 21 | page.meta.hero.source_button 22 | }} 23 | 24 |
25 |
26 |
27 |
28 | {{ super() }} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /src/privacy/nopriv.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: No privacy implementation 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::SnmpPriv; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use crate::snmp::msg::v3::{ScopedPdu, UsmParameters}; 11 | 12 | #[derive(Default)] 13 | pub struct NoPriv; 14 | 15 | impl SnmpPriv for NoPriv { 16 | fn as_localized(&mut self, _key: &[u8]) -> SnmpResult<()> { 17 | Ok(()) 18 | } 19 | fn has_priv(&self) -> bool { 20 | false 21 | } 22 | fn encrypt<'a>( 23 | &'a mut self, 24 | _pdu: &ScopedPdu, 25 | _boots: u32, 26 | _time: u32, 27 | ) -> SnmpResult<(&'a [u8], &'a [u8])> { 28 | Err(SnmpError::NotImplemented) 29 | } 30 | fn decrypt<'a: 'c, 'b, 'c>( 31 | &'a mut self, 32 | _data: &'b [u8], 33 | _usm: &'b UsmParameters<'b>, 34 | ) -> SnmpResult> { 35 | Err(SnmpError::NotImplemented) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Types of contributions 2 | 3 | You can contribute to the Gufo Labs projects in several way. This [repo][Repo] is a place 4 | to discuss and collaborate on [GitHub][GitHub]! Our team is maintaining this repo 5 | to preserve our bandwidth, off topic conversations will be closed. 6 | 7 | ### Discussions 8 | Discussions are where we have conversations. 9 | 10 | If you'd like help troubleshooting a PR you're working on, have a great new idea, or want to share something amazing you've learned in our docs, join us in [discussions][Discussions]. 11 | 12 | ### Issues 13 | Issues are used to track tasks that contributors can help with. 14 | 15 | If you've found bug, or something in the content of the documentation that should be updated, 16 | search open issues to see if someone else has reported the same thing. If it's something new, open an issue. We'll use the issue to have a conversation about the problem you want to fix. 17 | 18 | ### Pull requests 19 | A [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) is a way to suggest changes in our repository. 20 | 21 | [Repo]: https://github.com/gufolabs/gufo_snmp 22 | [Discussions]: https://github.com/gufolabs/gufo_snmp/discussions/ 23 | [GitHub]: https://github.com/ -------------------------------------------------------------------------------- /tools/build/setup-rust.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ------------------------------------------------------------------------ 3 | # Gufo Labs: Install and setup rust 4 | # ------------------------------------------------------------------------ 5 | # Copyright (C) 2023-25, Gufo Labs 6 | # ------------------------------------------------------------------------ 7 | 8 | set -x 9 | set -e 10 | 11 | RUST_VERSION=${RUST_VERSION:-1.90.0} 12 | 13 | # @todo: Allow override 14 | export RUSTUP_HOME=${RUSTUP_HOME:-/usr/local/rustup} 15 | export CARGO_HOME=${CARGO_HOME:-/usr/local/cargo} 16 | export PATH=${CARGO_HOME}/bin:${PATH} 17 | 18 | echo "Install Rust ${RUST_ARCH}" 19 | echo "PATH = ${PATH}" 20 | echo "RUSTUP_HOME = ${RUSTUP_HOME}" 21 | echo "CARGO_HOME = ${CARGO_HOME}" 22 | 23 | # Install rust 24 | # rustup-init tries to check /proc/self/exe 25 | # which is not accessible during Docker build 26 | # on aarch64, so we will patch it 27 | curl -s --tlsv1.3 https://sh.rustup.rs \ 28 | | sed 's#/proc/self/exe#/bin/sh#g' \ 29 | | sh -s -- \ 30 | -y --no-modify-path --profile minimal \ 31 | --default-toolchain ${RUST_VERSION} 32 | # Check 33 | cargo --version 34 | rustc --version 35 | # Install components 36 | rustup component add clippy 37 | rustup component add rustfmt 38 | -------------------------------------------------------------------------------- /src/auth/noauth.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP v3 No Auth 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::SnmpAuth; 9 | use crate::error::SnmpResult; 10 | 11 | #[derive(Default)] 12 | pub struct NoAuth; 13 | 14 | const PLACEHOLDER: [u8; 0] = []; 15 | 16 | impl SnmpAuth for NoAuth { 17 | fn as_localized(&mut self, _key: &[u8]) {} 18 | fn as_master(&mut self, _key: &[u8], _locality: &[u8]) {} 19 | fn as_password(&mut self, _password: &[u8], _locality: &[u8]) {} 20 | fn get_key_size(&self) -> usize { 21 | 0 22 | } 23 | fn get_key(&self) -> &[u8] { 24 | &PLACEHOLDER 25 | } 26 | fn has_auth(&self) -> bool { 27 | false 28 | } 29 | fn placeholder(&self) -> &'static [u8] { 30 | &PLACEHOLDER 31 | } 32 | fn localize(&self, _key: &[u8], _locality: &[u8], _out: &mut [u8]) {} 33 | fn password_to_master(&self, _password: &[u8], _out: &mut [u8]) {} 34 | fn sign(&self, _data: &mut [u8], _offset: usize) -> SnmpResult<()> { 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: snmpd fixture 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-24, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Python modules 9 | import logging 10 | from typing import Iterator 11 | 12 | # Third-party modules 13 | import pytest 14 | 15 | # Gufo SNMP modules 16 | from gufo.snmp.snmpd import Snmpd 17 | 18 | from .util import ( 19 | SNMP_COMMUNITY, 20 | SNMP_CONTACT, 21 | SNMP_LOCATION, 22 | SNMP_USERS, 23 | SNMPD_ADDRESS, 24 | SNMPD_PATH, 25 | SNMPD_PORT, 26 | ) 27 | 28 | 29 | @pytest.fixture(scope="session") 30 | def snmpd() -> Iterator[Snmpd]: 31 | logger = logging.getLogger("gufo.snmp.snmpd") 32 | logger.setLevel(logging.DEBUG) 33 | with Snmpd( 34 | path=SNMPD_PATH, 35 | address=SNMPD_ADDRESS, 36 | port=SNMPD_PORT, 37 | community=SNMP_COMMUNITY, 38 | location=SNMP_LOCATION, 39 | contact=SNMP_CONTACT, 40 | users=SNMP_USERS, 41 | # Uncomment for debugging 42 | # verbose=True, 43 | # log_packets=True, 44 | ) as snmpd: 45 | yield snmpd 46 | -------------------------------------------------------------------------------- /benches/iai_encode.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Benchmarks for encode functions (Iai) 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use gufo_snmp::{ 9 | ber::{BerEncoder, SnmpOid}, 10 | buf::Buffer, 11 | snmp::get::SnmpGet, 12 | snmp::msg::SnmpV2cMessage, 13 | snmp::pdu::SnmpPdu, 14 | }; 15 | use iai::black_box; 16 | 17 | pub fn encode_get() { 18 | let community = [0x70u8, 0x75, 0x62, 0x6c, 0x69, 0x63]; 19 | let msg = SnmpV2cMessage { 20 | community: &community, 21 | pdu: SnmpPdu::GetRequest(SnmpGet { 22 | request_id: 0x63ccac7d, 23 | vars: vec![ 24 | SnmpOid::from(vec![1, 3, 6, 1, 2, 1, 1, 3]), 25 | SnmpOid::from(vec![1, 3, 6, 1, 2, 1, 1, 2]), 26 | SnmpOid::from(vec![1, 3, 6, 1, 2, 1, 1, 6]), 27 | SnmpOid::from(vec![1, 3, 6, 1, 2, 1, 1, 4]), 28 | ], 29 | }), 30 | }; 31 | let mut buf = Buffer::default(); 32 | let _ = msg.push_ber(black_box(&mut buf)); 33 | } 34 | 35 | iai::main!(encode_get); 36 | -------------------------------------------------------------------------------- /benches/iai_buf.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Benchmarks for Buf functions (Iai) 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use gufo_snmp::buf::Buffer; 9 | use iai::black_box; 10 | 11 | fn buf_default() { 12 | black_box(Buffer::default()); 13 | } 14 | 15 | fn buf_push_u8() { 16 | let mut b = Buffer::default(); 17 | let _ = b.push_u8(black_box(10)); 18 | } 19 | 20 | fn buf_push() { 21 | let mut b = Buffer::default(); 22 | let chunk = [1u8, 2, 3]; 23 | let _ = b.push(&chunk); 24 | } 25 | 26 | fn buf_push_tag_len_short() { 27 | let mut b = Buffer::default(); 28 | let _ = b.push_tag_len(4, 1); 29 | } 30 | 31 | fn buf_push_tag_len_long1() { 32 | let mut b = Buffer::default(); 33 | let _ = b.push_tag_len(4, 128); 34 | } 35 | fn buf_push_tag_len_long2() { 36 | let mut b = Buffer::default(); 37 | let _ = b.push_tag_len(4, 256); 38 | } 39 | 40 | iai::main!( 41 | buf_default, 42 | buf_push_u8, 43 | buf_push, 44 | buf_push_tag_len_short, 45 | buf_push_tag_len_long1, 46 | buf_push_tag_len_long2 47 | ); 48 | -------------------------------------------------------------------------------- /docs/benchmarks/conclusions.md: -------------------------------------------------------------------------------- 1 | Here is the summary table for Gufo SNMP bencmarks. 2 | 3 | --8<-- "docs/benchmarks/conclusions.txt" 4 | 5 | **Conclusions:** 6 | 7 | * **Gufo SNMP is the clear winner** in terms of performance. 8 | * **Async mode** adds significant overhead to each I/O operation (~30%). 9 | This is especially noticeable in **GETNEXT** mode. We have to address this issue in future releases. 10 | * **GETBULK** consistently outperforms **GETNEXT**. As expected, it delivers better performance and should be preferred whenever supported by the equipment. 11 | * **The encryption overhead of SNMPv3** (AES128 + SHA1) is minimal, with little impact on overall performance. 12 | * **Gufo SNMP demonstrates good scalability:** running four parallel tasks takes only about 1.5× the time of a single task, indicating efficient performance even beyond Python’s GIL limitations. 13 | * **BER parsing** is a complex algorithmic operation, so native CPU implementations provide significant performance gains. 14 | * **Purpose-tailored BER parsers** that map directly to Python types offer substantial advantages over generic SNMP implementations. 15 | * **Complex abstractions** are slow. A lean and efficient API is key to high performance. 16 | * **Wrappers over C-libraries** may demonstrate an unexpected behaviour in multi-threaded applications. -------------------------------------------------------------------------------- /src/gufo/snmp/__init__.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: Python SNMP Library 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | """Gufo SNMP: The accelerated Python SNMP client library. 9 | 10 | Attributes: 11 | __version__: Current version 12 | """ 13 | 14 | # Gufo Labs modules 15 | from ._fast import ( 16 | NoSuchInstance, 17 | SnmpAuthError, 18 | SnmpDecodeError, 19 | SnmpEncodeError, 20 | SnmpError, 21 | ) 22 | from .async_client import SnmpSession 23 | from .typing import ValueType 24 | from .user import ( 25 | Aes128Key, 26 | BaseAuthKey, 27 | BasePrivKey, 28 | DesKey, 29 | Md5Key, 30 | Sha1Key, 31 | User, 32 | ) 33 | from .version import SnmpVersion 34 | 35 | __version__: str = "0.10.0" 36 | __all__ = [ 37 | "Aes128Key", 38 | "BaseAuthKey", 39 | "BasePrivKey", 40 | "DesKey", 41 | "Md5Key", 42 | "NoSuchInstance", 43 | "Sha1Key", 44 | "SnmpAuthError", 45 | "SnmpDecodeError", 46 | "SnmpEncodeError", 47 | "SnmpError", 48 | "SnmpSession", 49 | "SnmpVersion", 50 | "User", 51 | "ValueType", 52 | "__version__", 53 | ] 54 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Utilities 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use crate::auth::{AuthKey, SnmpAuth}; 9 | use pyo3::exceptions::PyValueError; 10 | use pyo3::prelude::*; 11 | use pyo3::types::PyBytes; 12 | 13 | // Convert key to master key 14 | #[pyfunction] 15 | pub fn get_master_key(py: Python, alg: u8, passwd: &[u8]) -> PyResult> { 16 | let auth = AuthKey::new(alg)?; 17 | let mut out = vec![0u8; auth.get_key_size()]; 18 | auth.password_to_master(passwd, &mut out); 19 | Ok(PyBytes::new(py, &out).into()) 20 | } 21 | 22 | // Convert master key to localized key 23 | #[pyfunction] 24 | pub fn get_localized_key( 25 | py: Python, 26 | alg: u8, 27 | master_key: &[u8], 28 | engine_id: &[u8], 29 | ) -> PyResult> { 30 | let auth = AuthKey::new(alg)?; 31 | let ks = auth.get_key_size(); 32 | if master_key.len() != ks { 33 | return Err(PyValueError::new_err("invalid key size")); 34 | } 35 | let mut out = vec![0u8; auth.get_key_size()]; 36 | auth.localize(master_key, engine_id, &mut out); 37 | Ok(PyBytes::new(py, &out).into()) 38 | } 39 | -------------------------------------------------------------------------------- /tools/build/setup-snmpd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ------------------------------------------------------------------------ 3 | # Gufo Labs: Install snmpd 4 | # ------------------------------------------------------------------------ 5 | # Copyright (C) 2023, Gufo Labs 6 | # ------------------------------------------------------------------------ 7 | 8 | set -e 9 | OSNAME=$(uname -s) 10 | OS="unknown" 11 | 12 | if [ -f /etc/redhat-release ]; then 13 | OS="rhel" 14 | elif [ -f /etc/debian_version ]; then 15 | OS="debian" 16 | elif [ -f /etc/alpine-release ]; then 17 | OS="alpine" 18 | elif [ "$OSNAME" == "Darwin" ]; then 19 | OS="darwin" 20 | else 21 | echo "Cannot detect OS" 22 | exit 1 23 | fi 24 | 25 | if [ $(id -u) -eq 0 ]; then 26 | SUDO="" 27 | else 28 | SUDO="sudo" 29 | fi 30 | 31 | echo "Installing snmpd for $OS" 32 | case $OS in 33 | rhel) 34 | $SUDO yum install -y net-snmp 35 | # Test 36 | /usr/sbin/snmpd --version 37 | ;; 38 | debian) 39 | $SUDO apt-get update 40 | $SUDO apt-get install -y --no-install-recommends snmpd 41 | # Test 42 | /usr/sbin/snmpd --version 43 | ;; 44 | alpine) 45 | $SUDO apk add net-snmp 46 | ;; 47 | darwin) 48 | brew install net-snmp 49 | ;; 50 | *) 51 | echo "Unsupported OS: $OS" 52 | exit 1 53 | ;; 54 | esac -------------------------------------------------------------------------------- /docs/dev/codebase.md: -------------------------------------------------------------------------------- 1 | # Project's Code Base 2 | 3 | The code base of the project has following structure: 4 | 5 | * `.devcontainer/` - Developer's container configuration for 6 | [VSCode Remote Containers][Remote Containers]. Just reopen 7 | project in remote container to get ready-to-development 8 | environment. 9 | * `.github/` - GitHub settings 10 | 11 | * `workflows/` - [GitHub Actions Workflows][GitHub Workflows] settings. 12 | Used to run tests and build the documentation. 13 | 14 | * `docs/` - [Mkdocs][Mkdocs] documentation. 15 | * `examples/` - Project's examples. 16 | * `src/` - Project's source code. 17 | * `tests/` - Project's [Pytest][Pytest] test suite. 18 | * `.gitignore` - [Gitignore][Gitignore] file. 19 | * `Dockerfile` - [Dockerfile][Dockerfile] for development container. 20 | * `mkdocs.yml` - [Mkdocs][Mkdocs] configuration file. 21 | * `pyproject.toml` - [pyproject.toml][Pyproject] file for python tools configuration. 22 | 23 | [Remote Containers]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers 24 | [GitHub Workflows]: https://docs.github.com/en/actions/using-workflows 25 | [Mkdocs]: https://www.mkdocs.org 26 | [Pytest]: https://docs.pytest.org/ 27 | [Dockerfile]: https://docs.docker.com/engine/reference/builder/ 28 | [Gitignore]: https://git-scm.com/docs/gitignore 29 | [Pyproject]: https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/ 30 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Module definition 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use pyo3::prelude::*; 9 | pub mod auth; 10 | pub mod ber; 11 | pub mod buf; 12 | pub mod error; 13 | mod privacy; 14 | pub mod reqid; 15 | pub mod snmp; 16 | mod socket; 17 | mod util; 18 | 19 | /// Module index 20 | #[pymodule] 21 | #[pyo3(name = "_fast")] 22 | fn gufo_snmp(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 23 | m.add("SnmpError", py.get_type::())?; 24 | m.add("SnmpEncodeError", py.get_type::())?; 25 | m.add("SnmpDecodeError", py.get_type::())?; 26 | m.add("SnmpAuthError", py.get_type::())?; 27 | m.add("NoSuchInstance", py.get_type::())?; 28 | m.add_class::()?; 29 | m.add_class::()?; 30 | m.add_class::()?; 31 | m.add_class::()?; 32 | m.add_function(wrap_pyfunction!(util::get_master_key, m)?)?; 33 | m.add_function(wrap_pyfunction!(util::get_localized_key, m)?)?; 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /src/reqid.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Id Generator 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use rand::Rng; 9 | 10 | const MAX_REQUEST_ID: i64 = 0x7fffffff; 11 | 12 | #[derive(Default)] 13 | pub struct RequestId(i64); 14 | 15 | impl RequestId { 16 | /// Get next value 17 | pub fn get_next(&mut self) -> i64 { 18 | let mut rng = rand::rng(); 19 | let x: i64 = rng.random(); 20 | self.0 = x & MAX_REQUEST_ID; 21 | self.0 22 | } 23 | /// Check values for match 24 | pub fn check(&self, v: i64) -> bool { 25 | self.0 == v 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | 33 | #[test] 34 | fn test_default() { 35 | let r = RequestId::default(); 36 | assert!(r.check(0)) 37 | } 38 | 39 | #[test] 40 | fn test_check() { 41 | let mut r = RequestId::default(); 42 | let v1 = r.get_next(); 43 | assert!(r.check(v1)) 44 | } 45 | 46 | #[test] 47 | fn test_seq() { 48 | let mut r = RequestId::default(); 49 | let v1 = r.get_next(); 50 | let v2 = r.get_next(); 51 | assert!(v1 != v2) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /benches/cri_encode.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Benchmarks for encode functions (Criterion) 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use criterion::{Criterion, criterion_group, criterion_main}; 9 | use gufo_snmp::{ 10 | ber::{BerEncoder, SnmpOid}, 11 | buf::Buffer, 12 | snmp::get::SnmpGet, 13 | snmp::msg::SnmpV2cMessage, 14 | snmp::pdu::SnmpPdu, 15 | }; 16 | 17 | pub fn bench_get(c: &mut Criterion) { 18 | let community = [0x70u8, 0x75, 0x62, 0x6c, 0x69, 0x63]; 19 | let msg = SnmpV2cMessage { 20 | community: &community, 21 | pdu: SnmpPdu::GetRequest(SnmpGet { 22 | request_id: 0x63ccac7d, 23 | vars: vec![ 24 | SnmpOid::try_from("1.3.6.1.2.1.1.3"), 25 | SnmpOid::try_from("1.3.6.1.2.1.1.2"), 26 | SnmpOid::try_from("1.3.6.1.2.1.1.6"), 27 | SnmpOid::try_from("1.3.6.1.2.1.1.4"), 28 | ], 29 | }), 30 | }; 31 | let mut buf = Buffer::default(); 32 | 33 | c.bench_function("encode GET", |b| { 34 | b.iter(|| { 35 | buf.reset(); 36 | let _ = msg.push_ber(&mut buf); 37 | }) 38 | }); 39 | } 40 | 41 | criterion_group!(benches, bench_get); 42 | criterion_main!(benches); 43 | -------------------------------------------------------------------------------- /src/snmp/msg/v3/data.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: MsgData 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::scoped::ScopedPdu; 9 | use crate::ber::{BerDecoder, BerEncoder, SnmpOctetString, TAG_OCTET_STRING}; 10 | use crate::buf::Buffer; 11 | use crate::error::{SnmpError, SnmpResult}; 12 | 13 | pub enum MsgData<'a> { 14 | Plaintext(ScopedPdu<'a>), 15 | Encrypted(&'a [u8]), 16 | } 17 | 18 | impl<'a> TryFrom<&'a [u8]> for MsgData<'a> { 19 | type Error = SnmpError; 20 | 21 | fn try_from(i: &'a [u8]) -> SnmpResult> { 22 | if i.is_empty() { 23 | return Err(SnmpError::Incomplete); 24 | } 25 | Ok(if i[0] == TAG_OCTET_STRING { 26 | // Encryped 27 | let (_, os) = SnmpOctetString::from_ber(i)?; 28 | MsgData::Encrypted(os.0) 29 | } else { 30 | // Plaintext 31 | MsgData::Plaintext(ScopedPdu::try_from(i)?) 32 | }) 33 | } 34 | } 35 | 36 | impl BerEncoder for MsgData<'_> { 37 | fn push_ber(&self, buf: &mut Buffer) -> SnmpResult<()> { 38 | match self { 39 | MsgData::Plaintext(x) => x.push_ber(buf), 40 | MsgData::Encrypted(x) => buf.push_tagged(TAG_OCTET_STRING, x), 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /benchmarks/conftest.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: Benchmarks configuration 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Python modules 9 | import random 10 | from typing import Iterator 11 | 12 | # Third-party modules 13 | import pytest 14 | 15 | # Gufo SNMP modules 16 | from gufo.snmp.snmpd import Snmpd 17 | from gufo.snmp.user import Aes128Key, KeyType, Sha1Key, User 18 | 19 | SNMPD_ADDRESS = "127.0.0.1" 20 | SNMPD_PORT = random.randint(52000, 53999) 21 | SNMPD_PATH = "/usr/sbin/snmpd" 22 | SNMP_COMMUNITY = "public" 23 | SNMP_LOCATION = "Gufo SNMP Test" 24 | SNMP_CONTACT = "test " 25 | SNMP_USERS = [ 26 | User( 27 | name="user22", 28 | auth_key=Sha1Key(b"user22key", key_type=KeyType.Master), 29 | priv_key=Aes128Key(b"USER22KEY", key_type=KeyType.Master), 30 | ), 31 | ] 32 | 33 | 34 | @pytest.fixture(scope="session") 35 | def snmpd() -> Iterator[Snmpd]: 36 | with Snmpd( 37 | path=SNMPD_PATH, 38 | address=SNMPD_ADDRESS, 39 | port=SNMPD_PORT, 40 | community=SNMP_COMMUNITY, 41 | location=SNMP_LOCATION, 42 | contact=SNMP_CONTACT, 43 | users=SNMP_USERS, 44 | # Uncomment for debugging 45 | # verbose=True, 46 | # log_packets=True, 47 | ) as snmpd: 48 | yield snmpd 49 | -------------------------------------------------------------------------------- /src/ber/option.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: BER Option Class 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerClass, BerDecoder, BerHeader, TAG_SEQUENCE, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use nom::{Err, IResult}; 11 | 12 | pub struct SnmpOption<'a> { 13 | pub tag: Tag, 14 | pub value: &'a [u8], 15 | } 16 | 17 | impl<'a> BerDecoder<'a> for SnmpOption<'a> { 18 | const ALLOW_PRIMITIVE: bool = false; 19 | const ALLOW_CONSTRUCTED: bool = true; 20 | const TAG: Tag = TAG_SEQUENCE; 21 | 22 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 23 | Ok(SnmpOption { 24 | tag: h.tag, 25 | value: &i[..h.length], 26 | }) 27 | } 28 | 29 | fn from_ber(i: &'a [u8]) -> IResult<&'a [u8], Self, SnmpError> { 30 | if i.len() < 3 { 31 | return Err(Err::Failure(SnmpError::Incomplete)); 32 | } 33 | let (tail, hdr) = BerHeader::from_ber(i)?; 34 | if !hdr.constructed || (hdr.class != BerClass::Context && hdr.class != BerClass::Universal) 35 | { 36 | return Err(Err::Failure(SnmpError::UnexpectedTag)); 37 | } 38 | // 39 | Ok(( 40 | &tail[hdr.length..], 41 | Self::decode(tail, &hdr).map_err(Err::Failure)?, 42 | )) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | Copyright © 2023-2025, Gufo Labs. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, 12 | this list of conditions and the following disclaimer. 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 3. Neither the name of Gufo Labs nor the names of its contributors may be used 17 | to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/ber/opaque.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP Application Class Opaque 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_APP_OPAQUE, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python, types::PyBytes}; 11 | 12 | pub struct SnmpOpaque<'a>(pub(crate) &'a [u8]); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpOpaque<'a> { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_APP_OPAQUE; 18 | 19 | // Implement X.690 pp 8.7: Encoding of an Opaque value 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | Ok(SnmpOpaque(&i[..h.length])) 22 | } 23 | } 24 | 25 | impl<'py> IntoPyObject<'py> for &SnmpOpaque<'_> { 26 | type Target = PyAny; 27 | type Output = Bound<'py, Self::Target>; 28 | type Error = SnmpError; 29 | 30 | fn into_pyobject(self, py: Python<'py>) -> Result { 31 | Ok(PyBytes::new(py, self.0).into_any()) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | 39 | #[test] 40 | fn test_parse_ber() -> SnmpResult<()> { 41 | let data = [0x44, 5, 0, 1, 2, 3, 4]; 42 | let (tail, s) = SnmpOpaque::from_ber(&data)?; 43 | assert_eq!(tail.len(), 0); 44 | assert_eq!(s.0, &data[2..]); 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ber/octetstring.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: OCTET STRING type 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_OCTET_STRING, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python, types::PyBytes}; 11 | 12 | pub struct SnmpOctetString<'a>(pub(crate) &'a [u8]); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpOctetString<'a> { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_OCTET_STRING; 18 | 19 | // Implement X.690 pp 8.7: Encoding of an octetstring value 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | Ok(SnmpOctetString(&i[..h.length])) 22 | } 23 | } 24 | 25 | impl<'a, 'py> IntoPyObject<'py> for &'a SnmpOctetString<'a> { 26 | type Target = PyAny; 27 | type Output = Bound<'py, Self::Target>; 28 | type Error = SnmpError; 29 | 30 | fn into_pyobject(self, py: Python<'py>) -> Result { 31 | Ok(PyBytes::new(py, self.0).into_any()) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | 39 | #[test] 40 | fn test_parse_ber() -> SnmpResult<()> { 41 | let data = [4u8, 5, 0, 1, 2, 3, 4]; 42 | let (tail, s) = SnmpOctetString::from_ber(&data)?; 43 | assert_eq!(tail.len(), 0); 44 | assert_eq!(s.0, &data[2..]); 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/assets/hero.css: -------------------------------------------------------------------------------- 1 | .hero-container { 2 | padding: 2rem 0; 3 | background: url(horizon.svg), 4 | linear-gradient(180deg, 5 | var(--md-primary-fg-color), 6 | var(--md-accent-fg-color) 99%, 7 | var(--md-default-bg-color) 0); 8 | background-size: contain; 9 | background-repeat: no-repeat; 10 | background-position: bottom; 11 | } 12 | 13 | .mdx-hero { 14 | color: var(--md-primary-bg-color); 15 | } 16 | 17 | .mdx-hero .md-button { 18 | color: var(--md-primary-bg-color); 19 | } 20 | 21 | .mdx-hero .md-button--primary { 22 | color: #000000; 23 | background-color: var(--md-primary-bg-color); 24 | border-color: var(--md-primary-bg-color); 25 | } 26 | 27 | .hero-image { 28 | order: 1; 29 | } 30 | 31 | .hero-image img { 32 | height: auto; 33 | max-width: 100%; 34 | animation: Sway 10s ease-in-out infinite; 35 | filter: invert(85%); 36 | } 37 | 38 | .hero-content { 39 | margin-top: 5rem; 40 | padding-bottom: 20vh; 41 | text-align: left; 42 | } 43 | 44 | .hero-content h1 { 45 | font-weight: 700; 46 | margin-bottom: 1rem; 47 | color: var(--md-primary-bg-color); 48 | } 49 | 50 | .md-content__inner h1 { 51 | display: none; 52 | } 53 | 54 | @keyframes Sway { 55 | 56 | 0%, 57 | 100% { 58 | transform: translateY(0); 59 | } 60 | 61 | 50% { 62 | transform: translateY(20px); 63 | } 64 | } 65 | 66 | @media screen and (min-width: 960px) { 67 | .mdx-hero { 68 | display: flex; 69 | align-items: stretch; 70 | } 71 | 72 | .hero-image { 73 | width: 50%; 74 | } 75 | 76 | .hero-content { 77 | width: 50%; 78 | } 79 | } -------------------------------------------------------------------------------- /tests/test_fast.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo Labs: Test _fast module 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2022-2023, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Third-party modules 9 | import pytest 10 | 11 | # Gufo Labs modules 12 | from gufo.snmp._fast import SnmpV1ClientSocket, SnmpV2cClientSocket 13 | 14 | 15 | def test_v1_invalid_address() -> None: 16 | with pytest.raises(OSError, match="invalid address"): 17 | SnmpV1ClientSocket("127.0.0.500:161", "public", 0, 0, 0, 0) 18 | 19 | 20 | def test_v2c_invalid_address() -> None: 21 | with pytest.raises(OSError, match="invalid address"): 22 | SnmpV2cClientSocket("127.0.0.500:161", "public", 0, 0, 0, 0) 23 | 24 | 25 | def test_v1_invalid_port() -> None: 26 | with pytest.raises(OSError, match="invalid address"): 27 | SnmpV1ClientSocket("127.0.0.1:100000", "public", 0, 0, 0, 0) 28 | 29 | 30 | def test_v2c_invalid_port() -> None: 31 | with pytest.raises(OSError, match="invalid address"): 32 | SnmpV2cClientSocket("127.0.0.1:100000", "public", 0, 0, 0, 0) 33 | 34 | 35 | def test_v1_set_tos() -> None: 36 | SnmpV1ClientSocket("127.0.0.1:161", "public", 10, 0, 0, 0) 37 | 38 | 39 | def test_v2c_set_tos() -> None: 40 | SnmpV2cClientSocket("127.0.0.1:161", "public", 10, 0, 0, 0) 41 | 42 | 43 | def test_v1_get_fd() -> None: 44 | SnmpV1ClientSocket("127.0.0.1:161", "public", 0, 0, 0, 0).get_fd() 45 | 46 | 47 | def test_v2c_get_fd() -> None: 48 | SnmpV2cClientSocket("127.0.0.1:161", "public", 0, 0, 0, 0).get_fd() 49 | -------------------------------------------------------------------------------- /src/ber/objectdescriptor.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: OBJECT DESCRIPTOR type 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_OBJECT_DESCRIPTOR, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python, types::PyBytes}; 11 | 12 | pub struct SnmpObjectDescriptor<'a>(pub(crate) &'a [u8]); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpObjectDescriptor<'a> { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = true; 17 | const TAG: Tag = TAG_OBJECT_DESCRIPTOR; 18 | 19 | // Implement X.690 pp 8.7: Encoding of an octetstring value 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | Ok(SnmpObjectDescriptor(&i[..h.length])) 22 | } 23 | } 24 | 25 | impl<'a, 'py> IntoPyObject<'py> for &'a SnmpObjectDescriptor<'a> { 26 | type Target = PyAny; 27 | type Output = Bound<'py, Self::Target>; 28 | type Error = SnmpError; 29 | 30 | fn into_pyobject(self, py: Python<'py>) -> Result { 31 | Ok(PyBytes::new(py, self.0).into_any()) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | 39 | #[test] 40 | fn test_parse_ber() -> SnmpResult<()> { 41 | let data = [7u8, 5, 0, 1, 2, 3, 4]; 42 | let (tail, s) = SnmpObjectDescriptor::from_ber(&data)?; 43 | assert_eq!(tail.len(), 0); 44 | assert_eq!(s.0, &data[2..]); 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/buf/pool.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Buffer pool implementation 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::buffer::Buffer; 9 | use std::sync::{Arc, Mutex, OnceLock}; 10 | 11 | pub struct BufferPool { 12 | pool: Arc>>, 13 | } 14 | 15 | impl Default for BufferPool { 16 | fn default() -> Self { 17 | BufferPool { 18 | pool: Arc::new(Mutex::new(Vec::default())), 19 | } 20 | } 21 | } 22 | 23 | impl BufferPool { 24 | pub fn acquire(&self) -> BufferHandle { 25 | let mut pool = self.pool.lock().unwrap(); 26 | BufferHandle { 27 | pool: Arc::clone(&self.pool), 28 | buf: Some(pool.pop().unwrap_or_default()), 29 | } 30 | } 31 | } 32 | 33 | pub struct BufferHandle { 34 | pool: Arc>>, 35 | buf: Option, // to use with take 36 | } 37 | 38 | impl AsMut for BufferHandle { 39 | fn as_mut(&mut self) -> &mut Buffer { 40 | self.buf.as_mut().unwrap() 41 | } 42 | } 43 | 44 | impl Drop for BufferHandle { 45 | fn drop(&mut self) { 46 | if let Some(mut buf) = self.buf.take() { 47 | buf.reset(); 48 | let mut pool = self.pool.lock().unwrap(); 49 | pool.push(buf); 50 | } 51 | } 52 | } 53 | 54 | pub static BUFFER_POOL: OnceLock = OnceLock::new(); 55 | 56 | pub fn get_buffer_pool() -> &'static BufferPool { 57 | BUFFER_POOL.get_or_init(BufferPool::default) 58 | } 59 | -------------------------------------------------------------------------------- /src/snmp/op/getiter.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Iterator wrapper 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use crate::ber::{SnmpOid, objectid::OidStorage}; 9 | use pyo3::{exceptions::PyValueError, prelude::*}; 10 | 11 | #[pyclass] 12 | pub struct GetIter { 13 | start_oid: Vec, 14 | next_oid: Vec, 15 | max_repetitions: i64, 16 | } 17 | 18 | #[pymethods] 19 | impl GetIter { 20 | /// Python constructor 21 | #[new] 22 | #[pyo3(signature = (oid, max_repetitions = None))] 23 | fn new(oid: &str, max_repetitions: Option) -> PyResult { 24 | let b_oid = SnmpOid::try_from(oid).map_err(|_| PyValueError::new_err("invalid oid"))?; 25 | Ok(GetIter { 26 | start_oid: (&b_oid).into(), 27 | next_oid: (&b_oid).into(), 28 | max_repetitions: max_repetitions.unwrap_or_default(), 29 | }) 30 | } 31 | } 32 | 33 | impl GetIter { 34 | pub fn get_next_oid<'a>(&self) -> SnmpOid<'a> { 35 | self.next_oid.as_owned() 36 | } 37 | // Save oid for next request. 38 | // Return true if next request may be send or return false otherwise 39 | pub fn set_next_oid(&mut self, oid: &SnmpOid) -> bool { 40 | if self.start_oid.as_borrowed().starts_with(oid) { 41 | self.next_oid.store(oid); 42 | true 43 | } else { 44 | false 45 | } 46 | } 47 | pub fn get_max_repetitions(&self) -> i64 { 48 | self.max_repetitions 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/gufo/snmp/sync_client/getnext.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: GetNextIter 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-24, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | """GetNextIter iterator.""" 9 | 10 | # Python modules 11 | from typing import Optional, Tuple 12 | 13 | # Gufo Labs Modules 14 | from .._fast import GetIter as _Iter 15 | from ..policer import BasePolicer 16 | from ..protocol import SnmpClientSocketProtocol 17 | from ..typing import ValueType 18 | 19 | 20 | class GetNextIter(object): 21 | """Wrap the series of the GetNext requests. 22 | 23 | Args: 24 | sock: Requsting SnmpClientSocket instance. 25 | oid: Base oid. 26 | policer: Optional BasePolicer instance to limit 27 | outgoing requests. 28 | """ 29 | 30 | def __init__( 31 | self: "GetNextIter", 32 | sock: SnmpClientSocketProtocol, 33 | oid: str, 34 | policer: Optional[BasePolicer] = None, 35 | ) -> None: 36 | self._sock = sock 37 | self._ctx = _Iter(oid) 38 | self._policer = policer 39 | 40 | def __iter__(self: "GetNextIter") -> "GetNextIter": 41 | """Return iterator.""" 42 | return self 43 | 44 | def __next__(self: "GetNextIter") -> Tuple[str, ValueType]: 45 | """Get next value.""" 46 | if self._policer: 47 | self._policer.wait_sync() 48 | try: 49 | return self._sock.get_next(self._ctx) 50 | except StopAsyncIteration as e: 51 | raise StopIteration from e 52 | except BlockingIOError as e: 53 | raise TimeoutError from e 54 | -------------------------------------------------------------------------------- /src/ber/timeticks.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP Application Class TimeTicks 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_APP_TIMETICKS, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python}; 11 | 12 | pub struct SnmpTimeTicks(pub(crate) u32); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpTimeTicks { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_APP_TIMETICKS; 18 | 19 | // Implement RFC 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | let v = i 22 | .iter() 23 | .take(h.length) 24 | .map(|x| *x as u32) 25 | .reduce(|acc, x| (acc << 8) | x) 26 | .unwrap_or(0); 27 | Ok(SnmpTimeTicks(v)) 28 | } 29 | } 30 | 31 | impl<'py> IntoPyObject<'py> for &SnmpTimeTicks { 32 | type Target = PyAny; 33 | type Output = Bound<'py, Self::Target>; 34 | type Error = SnmpError; 35 | 36 | fn into_pyobject(self, py: Python<'py>) -> Result { 37 | Ok(self.0.into_pyobject(py)?.into_any()) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | use test_case::test_case; 45 | 46 | #[test_case(vec![0x43, 0x4, 0, 0x89, 0x92, 0xDB], 0x008992DB; "1")] 47 | #[test_case(vec![67, 4, 1, 53, 16, 171], 0x013510AB; "2")] 48 | fn test_parse(data: Vec, expected: u32) -> SnmpResult<()> { 49 | let (tail, tt) = SnmpTimeTicks::from_ber(&data)?; 50 | assert_eq!(tail.len(), 0); 51 | assert_eq!(tt.0, expected); 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tools/build/build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ------------------------------------------------------------------------ 3 | # Build binary wheels for Linux 4 | # ------------------------------------------------------------------------ 5 | # Copyright (C) 2022-25, Gufo Labs 6 | # ------------------------------------------------------------------------ 7 | 8 | set -e 9 | 10 | # Define all valid builds 11 | VALID_IMAGES=" 12 | manylinux2014_x86_64 13 | manylinux_2_28_x86_64 14 | manylinux_2_28_aarch64 15 | musllinux_1_2_x86_64 16 | musllinux_1_2_aarch64 17 | " 18 | 19 | # Check if a given combination is valid 20 | is_valid_image() { 21 | echo "$VALID_IMAGES" | grep -qE "^$1$" 22 | } 23 | 24 | # Build for platform 25 | build() { 26 | local image="$1" 27 | 28 | case "$image" in 29 | *_x86_64) 30 | local image_arch="linux/amd64" 31 | local rust_arch="x86_64-unknown-linux-gnu" 32 | ;; 33 | *_aarch64) 34 | local image_arch="linux/aarch64" 35 | local rust_arch="aarch64-unknown-linux-gnu" 36 | ;; 37 | *) return 1 ;; # Unknown platform 38 | esac 39 | docker run --rm\ 40 | -e RUST_ARCH=${rust_arch}\ 41 | -v $PWD:/workdir\ 42 | -w /workdir\ 43 | --user 0\ 44 | --platform ${image_arch}\ 45 | quay.io/pypa/${image}:latest\ 46 | ./tools/build/build-many.sh 3.9 3.10 3.11 3.12 3.13 3.14 47 | } 48 | 49 | if [ "$#" -eq 0 ]; then 50 | # No arguments: Run all builds 51 | echo "$VALID_IMAGES" | while read -r image; do 52 | build "$image" 53 | done 54 | elif [ "$#" -eq 1 ]; then 55 | # Two arguments: Check validity and run if valid 56 | if is_valid_image "$1"; then 57 | build "$1" 58 | else 59 | echo "Error: Invalid image '$1'" 60 | exit 1 61 | fi 62 | else 63 | echo "Usage: $0 [image]" 64 | echo "Where [image] is one of: $VALID_IMAGES" 65 | exit 1 66 | fi 67 | -------------------------------------------------------------------------------- /src/snmp/msg/v3/scoped.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Scoped PDU 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use crate::ber::{BerDecoder, BerEncoder, SnmpOctetString, SnmpSequence, TAG_OCTET_STRING}; 9 | use crate::buf::Buffer; 10 | use crate::error::{SnmpError, SnmpResult}; 11 | use crate::snmp::pdu::SnmpPdu; 12 | 13 | pub struct ScopedPdu<'a> { 14 | pub engine_id: &'a [u8], 15 | pub pdu: SnmpPdu<'a>, 16 | } 17 | 18 | const EMPTY_BER: [u8; 2] = [TAG_OCTET_STRING, 0]; 19 | 20 | impl<'a> TryFrom<&'a [u8]> for ScopedPdu<'a> { 21 | type Error = SnmpError; 22 | 23 | fn try_from(i: &'a [u8]) -> SnmpResult> { 24 | let (_, envelope) = SnmpSequence::from_ber(i)?; 25 | // Context engine id 26 | let (tail, engine_id) = SnmpOctetString::from_ber(envelope.0)?; 27 | // Context engine name 28 | let (tail, _ctx_engine_name) = SnmpOctetString::from_ber(tail)?; 29 | // Decode PDU and return 30 | Ok(ScopedPdu { 31 | engine_id: engine_id.0, 32 | pdu: SnmpPdu::try_from(tail)?, 33 | }) 34 | } 35 | } 36 | 37 | impl BerEncoder for ScopedPdu<'_> { 38 | fn push_ber(&self, buf: &mut Buffer) -> SnmpResult<()> { 39 | let rest = buf.len(); 40 | // Push PDU 41 | self.pdu.push_ber(buf)?; 42 | // Push context engine name 43 | buf.push(&EMPTY_BER)?; 44 | // Push context engine id 45 | if self.engine_id.is_empty() { 46 | buf.push(&EMPTY_BER)?; 47 | } else { 48 | buf.push_tagged(TAG_OCTET_STRING, self.engine_id)?; 49 | } 50 | // Push option header 51 | buf.push_tag_len(0x30, buf.len() - rest)?; 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ber/gauge32.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP Application Class Gauge32 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_APP_GAUGE32, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python}; 11 | 12 | pub struct SnmpGauge32(pub(crate) u32); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpGauge32 { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_APP_GAUGE32; 18 | 19 | // Implement RFC 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | let v = i 22 | .iter() 23 | .take(h.length) 24 | .map(|x| *x as u32) 25 | .reduce(|acc, x| (acc << 8) | x) 26 | .unwrap_or(0); 27 | Ok(SnmpGauge32(v)) 28 | } 29 | } 30 | 31 | impl<'py> IntoPyObject<'py> for &SnmpGauge32 { 32 | type Target = PyAny; 33 | type Output = Bound<'py, Self::Target>; 34 | type Error = SnmpError; 35 | 36 | fn into_pyobject(self, py: Python<'py>) -> Result { 37 | Ok(self.0.into_pyobject(py)?.into_any()) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_parse1() -> SnmpResult<()> { 47 | let data = [0x42, 0x4, 0, 0x89, 0x92, 0xDB]; 48 | let (tail, tt) = SnmpGauge32::from_ber(&data)?; 49 | assert_eq!(tail.len(), 0); 50 | assert_eq!(tt.0, 0x008992DB); 51 | Ok(()) 52 | } 53 | #[test] 54 | fn test_parse2() -> SnmpResult<()> { 55 | let data = [0x42, 4, 1, 53, 16, 171]; 56 | let (tail, tt) = SnmpGauge32::from_ber(&data)?; 57 | assert_eq!(tail.len(), 0); 58 | assert_eq!(tt.0, 0x013510AB); 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ber/counter32.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP Application Class Counter32 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_APP_COUNTER32, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python}; 11 | 12 | pub struct SnmpCounter32(pub(crate) u32); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpCounter32 { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_APP_COUNTER32; 18 | 19 | // Implement RFC 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | let v = i 22 | .iter() 23 | .take(h.length) 24 | .map(|x| *x as u32) 25 | .reduce(|acc, x| (acc << 8) | x) 26 | .unwrap_or(0); 27 | Ok(SnmpCounter32(v)) 28 | } 29 | } 30 | 31 | impl<'py> IntoPyObject<'py> for &SnmpCounter32 { 32 | type Target = PyAny; 33 | type Output = Bound<'py, Self::Target>; 34 | type Error = SnmpError; 35 | 36 | fn into_pyobject(self, py: Python<'py>) -> Result { 37 | Ok(self.0.into_pyobject(py)?.into_any()) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_parse1() -> SnmpResult<()> { 47 | let data = [0x41, 0x4, 0, 0x89, 0x92, 0xDB]; 48 | let (tail, tt) = SnmpCounter32::from_ber(&data)?; 49 | assert_eq!(tail.len(), 0); 50 | assert_eq!(tt.0, 0x008992DB); 51 | Ok(()) 52 | } 53 | #[test] 54 | fn test_parse2() -> SnmpResult<()> { 55 | let data = [0x41, 4, 1, 53, 16, 171]; 56 | let (tail, tt) = SnmpCounter32::from_ber(&data)?; 57 | assert_eq!(tail.len(), 0); 58 | assert_eq!(tt.0, 0x013510AB); 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ber/counter64.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP Application Class Counter64 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_APP_COUNTER64, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python}; 11 | 12 | pub struct SnmpCounter64(pub(crate) u64); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpCounter64 { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_APP_COUNTER64; 18 | 19 | // Implement RFC 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | let v = i 22 | .iter() 23 | .take(h.length) 24 | .map(|x| *x as u64) 25 | .reduce(|acc, x| (acc << 8) | x) 26 | .unwrap_or(0); 27 | Ok(SnmpCounter64(v)) 28 | } 29 | } 30 | 31 | impl<'py> IntoPyObject<'py> for &SnmpCounter64 { 32 | type Target = PyAny; 33 | type Output = Bound<'py, Self::Target>; 34 | type Error = SnmpError; 35 | 36 | fn into_pyobject(self, py: Python<'py>) -> Result { 37 | Ok(self.0.into_pyobject(py)?.into_any()) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_parse1() -> SnmpResult<()> { 47 | let data = [0x46, 0x4, 0, 0x89, 0x92, 0xDB]; 48 | let (tail, tt) = SnmpCounter64::from_ber(&data)?; 49 | assert_eq!(tail.len(), 0); 50 | assert_eq!(tt.0, 0x008992DB); 51 | Ok(()) 52 | } 53 | #[test] 54 | fn test_parse2() -> SnmpResult<()> { 55 | let data = [0x46, 4, 1, 53, 16, 171]; 56 | let (tail, tt) = SnmpCounter64::from_ber(&data)?; 57 | assert_eq!(tail.len(), 0); 58 | assert_eq!(tt.0, 0x013510AB); 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ber/uinteger32.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP Application Class UInteger32 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_APP_UINTEGER32, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python}; 11 | 12 | pub struct SnmpUInteger32(pub(crate) u32); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpUInteger32 { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_APP_UINTEGER32; 18 | 19 | // Implement RFC 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | let v = i 22 | .iter() 23 | .take(h.length) 24 | .map(|x| *x as u32) 25 | .reduce(|acc, x| (acc << 8) | x) 26 | .unwrap_or(0); 27 | Ok(SnmpUInteger32(v)) 28 | } 29 | } 30 | 31 | impl<'py> IntoPyObject<'py> for &SnmpUInteger32 { 32 | type Target = PyAny; 33 | type Output = Bound<'py, Self::Target>; 34 | type Error = SnmpError; 35 | 36 | fn into_pyobject(self, py: Python<'py>) -> Result { 37 | Ok(self.0.into_pyobject(py)?.into_any()) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_parse1() -> SnmpResult<()> { 47 | let data = [0x47, 0x4, 0, 0x89, 0x92, 0xDB]; 48 | let (tail, tt) = SnmpUInteger32::from_ber(&data)?; 49 | assert_eq!(tail.len(), 0); 50 | assert_eq!(tt.0, 0x008992DB); 51 | Ok(()) 52 | } 53 | #[test] 54 | fn test_parse2() -> SnmpResult<()> { 55 | let data = [0x47, 4, 1, 53, 16, 171]; 56 | let (tail, tt) = SnmpUInteger32::from_ber(&data)?; 57 | assert_eq!(tail.len(), 0); 58 | assert_eq!(tt.0, 0x013510AB); 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug. For security vulnerabilities see Report a security vulnerability in the templates. 3 | title: "BUG: " 4 | labels: [bug] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: > 10 | Thank you for taking the time to file a bug report. Before creating a new 11 | issue, please make sure to take a few minutes to check the issue tracker 12 | for existing issues about the bug. 13 | 14 | - type: textarea 15 | attributes: 16 | label: "Describe the issue:" 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | attributes: 22 | label: "Reproduce the code example:" 23 | description: > 24 | A short code example that reproduces the problem/missing feature. It 25 | should be self-contained, i.e., can be copy-pasted into the Python 26 | interpreter or run as-is via `python myproblem.py`. 27 | placeholder: | 28 | from gufo.snmp import SnmpSession 29 | << your code here >> 30 | render: python 31 | validations: 32 | required: false 33 | 34 | - type: textarea 35 | attributes: 36 | label: "Error message:" 37 | description: > 38 | Please include full error message, if any. 39 | placeholder: | 40 | << Full traceback starting from `Traceback: ...` >> 41 | render: shell 42 | 43 | - type: textarea 44 | attributes: 45 | label: "Python version information" 46 | description: Output from `import sys; print(sys.version)` 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | attributes: 52 | label: "Gufo SNMP version information" 53 | description: Output from `import gufo.snmp;print(gufo.snmp.__version__)` 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | attributes: 59 | label: "Operation system version" 60 | description: Attach your operation system and version 61 | validations: 62 | required: true 63 | -------------------------------------------------------------------------------- /benches/iai_auth.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Benchmarks for Buf functions (Iai) 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use gufo_snmp::auth::{Md5AuthKey, Sha1AuthKey, SnmpAuth}; 9 | use iai::black_box; 10 | 11 | fn md5_default() { 12 | Md5AuthKey::default(); 13 | } 14 | 15 | const PASSWORD: &[u8; 10] = b"maplesyrup"; 16 | const ENGINE_ID: [u8; 12] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]; 17 | const MD5_MASTER_KEY: [u8; 16] = [ 18 | 0x9f, 0xaf, 0x32, 0x83, 0x88, 0x4e, 0x92, 0x83, 0x4e, 0xbc, 0x98, 0x47, 0xd8, 0xed, 0xd9, 0x63, 19 | ]; 20 | const SHA1_MASTER_KEY: [u8; 20] = [ 21 | 0x9f, 0xb5, 0xcc, 0x03, 0x81, 0x49, 0x7b, 0x37, 0x93, 0x52, 0x89, 0x39, 0xff, 0x78, 0x8d, 0x5d, 22 | 0x79, 0x14, 0x52, 0x11, 23 | ]; 24 | 25 | fn md5_password_to_master() { 26 | let key = Md5AuthKey::default(); 27 | let mut out = [0u8; 16]; 28 | key.password_to_master(black_box(PASSWORD), black_box(&mut out)); 29 | } 30 | 31 | fn md5_localize() { 32 | let auth = Md5AuthKey::default(); 33 | let mut out = [0u8; 16]; 34 | auth.localize( 35 | black_box(&MD5_MASTER_KEY), 36 | black_box(&ENGINE_ID), 37 | black_box(&mut out), 38 | ); 39 | } 40 | 41 | fn sha1_default() { 42 | Sha1AuthKey::default(); 43 | } 44 | 45 | fn sha1_password_to_master() { 46 | let key = Sha1AuthKey::default(); 47 | let mut out = [0u8; 20]; 48 | key.password_to_master(black_box(PASSWORD), black_box(&mut out)); 49 | } 50 | 51 | fn sha1_localize() { 52 | let auth = Sha1AuthKey::default(); 53 | let mut out = [0u8; 20]; 54 | auth.localize( 55 | black_box(&SHA1_MASTER_KEY), 56 | black_box(&ENGINE_ID), 57 | black_box(&mut out), 58 | ); 59 | } 60 | 61 | iai::main!( 62 | md5_default, 63 | md5_password_to_master, 64 | md5_localize, 65 | sha1_default, 66 | sha1_password_to_master, 67 | sha1_localize, 68 | ); 69 | -------------------------------------------------------------------------------- /src/ber/bool.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: BER BOOLEAN class 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_BOOL, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python, types::PyBool}; 11 | 12 | pub struct SnmpBool(bool); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpBool { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_BOOL; 18 | 19 | // Implement X.690 pp 8.2: Encoding of a boolean value 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | if h.length != 1 { 22 | return Err(SnmpError::InvalidData); 23 | } 24 | Ok(SnmpBool(i[0] != 0)) 25 | } 26 | } 27 | 28 | impl From for bool { 29 | fn from(value: SnmpBool) -> Self { 30 | value.0 31 | } 32 | } 33 | 34 | impl<'py> IntoPyObject<'py> for &SnmpBool { 35 | type Target = PyAny; 36 | type Output = Bound<'py, Self::Target>; 37 | type Error = SnmpError; 38 | 39 | fn into_pyobject(self, py: Python<'py>) -> Result { 40 | Ok(PyBool::new(py, self.0).to_owned().into_any()) 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | 48 | #[test] 49 | fn test_long() { 50 | let data = [1u8, 2, 0, 0]; 51 | let r = SnmpBool::from_ber(&data); 52 | assert!(r.is_err()); 53 | } 54 | 55 | #[test] 56 | fn test_parse_ber() -> SnmpResult<()> { 57 | let data = [[1u8, 1, 0], [1, 1, 1], [1, 1, 255]]; 58 | let expected = [false, true, true]; 59 | for i in 0..data.len() { 60 | let (tail, v) = SnmpBool::from_ber(&data[i])?; 61 | assert_eq!(bool::from(v), expected[i]); 62 | assert_eq!(tail.len(), 0); 63 | } 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ber/null.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: BER NULL Class 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerEncoder, BerHeader, TAG_NULL, Tag}; 9 | use crate::buf::Buffer; 10 | use crate::error::{SnmpError, SnmpResult}; 11 | 12 | pub struct SnmpNull; 13 | 14 | impl<'a> BerDecoder<'a> for SnmpNull { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_NULL; 18 | 19 | // Implement X.690 pp 8.8: Encoding of a null value 20 | fn decode(_: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | if h.length != 0 { 22 | return Err(SnmpError::InvalidTagFormat); 23 | } 24 | Ok(SnmpNull) 25 | } 26 | } 27 | 28 | const NULL_BER: [u8; 2] = [5u8, 0]; 29 | 30 | impl BerEncoder for SnmpNull { 31 | fn push_ber(&self, buf: &mut Buffer) -> SnmpResult<()> { 32 | buf.push(&NULL_BER)?; 33 | Ok(()) 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | 41 | #[test] 42 | fn test_parse() -> SnmpResult<()> { 43 | let data = [5u8, 0]; 44 | let (tail, _) = SnmpNull::from_ber(&data)?; 45 | assert_eq!(tail.len(), 0); 46 | Ok(()) 47 | } 48 | #[test] 49 | fn test_invalid_length() { 50 | let data = [5u8, 1, 0]; 51 | let r = SnmpNull::from_ber(&data); 52 | assert!(r.is_err()); 53 | } 54 | #[test] 55 | fn test_encode() -> SnmpResult<()> { 56 | let mut b = Buffer::default(); 57 | SnmpNull {}.push_ber(&mut b)?; 58 | let expected = [5u8, 0]; 59 | assert_eq!(b.data(), &expected); 60 | Ok(()) 61 | } 62 | #[test] 63 | fn test_encode_decode() -> SnmpResult<()> { 64 | let mut b = Buffer::default(); 65 | SnmpNull {}.push_ber(&mut b)?; 66 | SnmpNull::from_ber(b.data())?; 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/test_docs.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------- 2 | # Gufo Traceroute: docs tests 3 | # ---------------------------------------------------------------------- 4 | # Copyright (C) 2022-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # ---------------------------------------------------------------------- 7 | 8 | # Python modules 9 | import os 10 | import re 11 | from functools import lru_cache 12 | from typing import List, Set 13 | 14 | # Third-party modules 15 | import pytest 16 | 17 | rx_link = re.compile(r"\[([^\]]*)\]\[([^\]]+)\]", re.MULTILINE) 18 | rx_link_def = re.compile(r"^\[([^\]]+)\]:", re.MULTILINE) 19 | rx_footnote = re.compile(r"[^\]]\[(\^\d+)\][^\[]", re.MULTILINE) 20 | rx_datatracker_ietf = re.compile( 21 | r"https://datatracker\.ietf\.org/doc/[^/]+/(rfc\d+)", re.MULTILINE 22 | ) 23 | 24 | 25 | @lru_cache(maxsize=1) 26 | def get_docs() -> List[str]: 27 | doc_files: List[str] = [] 28 | for root, _, files in os.walk("docs"): 29 | for f in files: 30 | if f.endswith(".md") and not f.startswith("."): 31 | doc_files.append(os.path.join(root, f)) 32 | return doc_files 33 | 34 | 35 | def get_file(path: str) -> str: 36 | with open(path) as f: 37 | return f.read() 38 | 39 | 40 | @pytest.mark.parametrize("doc", get_docs()) 41 | def test_links(doc: str) -> None: 42 | data = get_file(doc) 43 | links: Set[str] = set() 44 | defs: Set[str] = set() 45 | for match in rx_link.finditer(data): 46 | links.add(match.group(2)) 47 | for match in rx_footnote.finditer(data): 48 | links.add(match.group(1)) 49 | for match in rx_link_def.finditer(data): 50 | d = match.group(1) 51 | assert d not in defs, f"Link already defined: {d}" 52 | assert d in links, f"Unused link definition: {d}" 53 | 54 | 55 | @pytest.mark.parametrize("doc", get_docs()) 56 | def test_rfc_links(doc: str) -> None: 57 | data = get_file(doc) 58 | match = rx_datatracker_ietf.search(data) 59 | assert not match, ( 60 | f"{match.group(0)} link used. Must be https://www.rfc-editor.org/rfc/{match.group(1)}.html" 61 | ) 62 | -------------------------------------------------------------------------------- /src/gufo/snmp/protocol.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: Socket protocol definition 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Python modules 9 | from typing import Dict, List, Protocol, Tuple, Union 10 | 11 | # Gufo Labs modules 12 | from ._fast import GetIter 13 | from .typing import ValueType 14 | 15 | 16 | class SnmpClientSocketProtocol(Protocol): 17 | def get_fd(self: "SnmpClientSocketProtocol") -> int: ... 18 | 19 | # .get() 20 | def get(self: "SnmpClientSocketProtocol", oid: str) -> ValueType: ... 21 | 22 | def send_get(self: "SnmpClientSocketProtocol", oid: str) -> None: ... 23 | 24 | def recv_get(self: "SnmpClientSocketProtocol") -> ValueType: ... 25 | 26 | # .get_many() 27 | def get_many( 28 | self: "SnmpClientSocketProtocol", oids: List[str] 29 | ) -> Dict[str, ValueType]: ... 30 | 31 | def send_get_many( 32 | self: "SnmpClientSocketProtocol", oids: List[str] 33 | ) -> None: ... 34 | 35 | def recv_get_many( 36 | self: "SnmpClientSocketProtocol", 37 | ) -> Dict[str, ValueType]: ... 38 | 39 | # .get_next 40 | def get_next( 41 | self: "SnmpClientSocketProtocol", iter_getnext: GetIter 42 | ) -> Tuple[str, ValueType]: ... 43 | 44 | def send_get_next( 45 | self: "SnmpClientSocketProtocol", iter_getnext: GetIter 46 | ) -> None: ... 47 | 48 | def recv_get_next( 49 | self: "SnmpClientSocketProtocol", iter_getnext: GetIter 50 | ) -> Tuple[str, ValueType]: ... 51 | 52 | # .get_bulk 53 | def get_bulk( 54 | self: "SnmpClientSocketProtocol", iter_getbulk: GetIter 55 | ) -> List[Union[Tuple[str, ValueType], None]]: ... 56 | 57 | def send_get_bulk( 58 | self: "SnmpClientSocketProtocol", iter_getbulk: GetIter 59 | ) -> None: ... 60 | 61 | def recv_get_bulk( 62 | self: "SnmpClientSocketProtocol", iter_getnext: GetIter 63 | ) -> List[Union[Tuple[str, ValueType], None]]: ... 64 | -------------------------------------------------------------------------------- /src/privacy/mod.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP v3 privacy primitives 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-24, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | mod aes128; 9 | mod des; 10 | mod nopriv; 11 | use crate::error::{SnmpError, SnmpResult}; 12 | use crate::snmp::msg::v3::{ScopedPdu, UsmParameters}; 13 | use aes128::Aes128Key; 14 | use des::DesKey; 15 | use enum_dispatch::enum_dispatch; 16 | use nopriv::NoPriv; 17 | 18 | #[enum_dispatch(SnmpPriv)] 19 | pub enum PrivKey { 20 | NoPriv(NoPriv), 21 | Des(DesKey), 22 | Aes128(Aes128Key), 23 | } 24 | 25 | #[enum_dispatch] 26 | pub trait SnmpPriv { 27 | // Localized key 28 | fn as_localized(&mut self, key: &[u8]) -> SnmpResult<()>; 29 | // 30 | fn has_priv(&self) -> bool; 31 | // Encrypt data. 32 | // Returns (encrypted data, priv parameters) 33 | fn encrypt<'a>( 34 | &'a mut self, 35 | pdu: &ScopedPdu, 36 | boots: u32, 37 | time: u32, 38 | ) -> SnmpResult<(&'a [u8], &'a [u8])>; 39 | // Decrypt data 40 | fn decrypt<'a: 'c, 'b, 'c>( 41 | &'a mut self, 42 | data: &'b [u8], 43 | usm: &'b UsmParameters<'b>, 44 | ) -> SnmpResult>; 45 | } 46 | 47 | #[inline] 48 | fn get_padded_len(buf_len: usize, block_size: usize) -> usize { 49 | let rem = buf_len % block_size; 50 | if rem == 0 { 51 | buf_len 52 | } else { 53 | buf_len - rem + block_size 54 | } 55 | } 56 | 57 | const NO_PRIV: u8 = 0; 58 | const DES: u8 = 1; 59 | const AES128: u8 = 2; 60 | // - - X X X X X X 61 | const KT_ALG_MASK: u8 = 0x3f; 62 | 63 | impl PrivKey { 64 | pub fn new(code: u8) -> SnmpResult { 65 | Ok(match code & KT_ALG_MASK { 66 | NO_PRIV => PrivKey::NoPriv(NoPriv), 67 | DES => PrivKey::Des(DesKey::default()), 68 | AES128 => PrivKey::Aes128(Aes128Key::default()), 69 | _ => return Err(SnmpError::InvalidVersion(code)), 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docs/benchmarks/v2c/test_v2c_p4_getbulk.txt: -------------------------------------------------------------------------------- 1 | ============================= test session starts ============================== 2 | platform linux -- Python 3.13.2, pytest-8.3.3, pluggy-1.5.0 3 | benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=50 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 4 | rootdir: /workspaces/gufo_snmp 5 | configfile: pyproject.toml 6 | plugins: benchmark-5.1.0 7 | collected 3 items 8 | 9 | benchmarks/test_v2c_p4_getbulk.py ... [100%] 10 | 11 | 12 | ----------------------------------------------------------------------------------------- benchmark: 3 tests ---------------------------------------------------------------------------------------- 13 | Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 14 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 15 | test_gufo_snmp_sync 150.5068 (1.0) 161.4152 (1.0) 155.5240 (1.0) 1.7776 (1.0) 155.5074 (1.0) 1.8687 (1.0) 10;2 6.4299 (1.0) 50 1 16 | test_gufo_snmp_async 171.8042 (1.14) 193.7454 (1.20) 184.5445 (1.19) 3.9756 (2.24) 185.2006 (1.19) 3.8316 (2.05) 12;4 5.4187 (0.84) 50 1 17 | test_pysnmp_async 2,203.8696 (14.64) 2,304.7855 (14.28) 2,242.9824 (14.42) 22.8359 (12.85) 2,237.4564 (14.39) 31.9016 (17.07) 12;0 0.4458 (0.07) 50 1 18 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 19 | 20 | Legend: 21 | Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. 22 | OPS: Operations Per Second, computed as 1 / Mean 23 | ======================== 3 passed in 135.41s (0:02:15) ========================= 24 | -------------------------------------------------------------------------------- /docs/benchmarks/v2c/test_v2c_p4_getnext.txt: -------------------------------------------------------------------------------- 1 | ============================= test session starts ============================== 2 | platform linux -- Python 3.13.2, pytest-8.3.3, pluggy-1.5.0 3 | benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=50 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 4 | rootdir: /workspaces/gufo_snmp 5 | configfile: pyproject.toml 6 | plugins: benchmark-5.1.0 7 | collected 3 items 8 | 9 | benchmarks/test_v2c_p4_getnext.py ... [100%] 10 | 11 | 12 | ----------------------------------------------------------------------------------------- benchmark: 3 tests ---------------------------------------------------------------------------------------- 13 | Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 14 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 15 | test_gufo_snmp_sync 345.9976 (1.0) 369.1155 (1.0) 357.0097 (1.0) 4.3024 (1.0) 357.3423 (1.0) 5.1624 (1.0) 16;2 2.8010 (1.0) 50 1 16 | test_gufo_snmp_async 413.7641 (1.20) 519.0843 (1.41) 480.6624 (1.35) 21.4717 (4.99) 487.5226 (1.36) 8.5229 (1.65) 8;9 2.0805 (0.74) 50 1 17 | test_pysnmp_async 5,549.9992 (16.04) 5,821.7834 (15.77) 5,670.1119 (15.88) 50.9505 (11.84) 5,668.7624 (15.86) 54.0314 (10.47) 12;3 0.1764 (0.06) 50 1 18 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 19 | 20 | Legend: 21 | Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. 22 | OPS: Operations Per Second, computed as 1 / Mean 23 | ======================== 3 passed in 339.49s (0:05:39) ========================= 24 | -------------------------------------------------------------------------------- /src/snmp/op/getmany.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: GetMany operation 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{GetIter, PyOp}; 9 | use crate::ber::SnmpOid; 10 | use crate::error::SnmpError; 11 | use crate::snmp::{get::SnmpGet, msg::SnmpPdu, value::SnmpValue}; 12 | use pyo3::{exceptions::PyRuntimeError, prelude::*, pybacked::PyBackedStr, types::PyDict}; 13 | 14 | pub struct OpGetMany; 15 | 16 | impl<'a> PyOp<'a, Vec> for OpGetMany { 17 | // obj is list[str] 18 | fn from_python(obj: Vec, request_id: i64) -> PyResult> { 19 | Ok(SnmpPdu::GetRequest(SnmpGet { 20 | request_id, 21 | vars: obj 22 | .into_iter() 23 | .map(|x| SnmpOid::try_from(x.as_ref())) 24 | .collect::, SnmpError>>()?, 25 | })) 26 | } 27 | fn to_python<'py>( 28 | pdu: &SnmpPdu, 29 | _iter: Option<&mut GetIter>, 30 | py: Python<'py>, 31 | ) -> PyResult> { 32 | match pdu { 33 | SnmpPdu::GetResponse(resp) => { 34 | // Build resulting dict 35 | let dict = PyDict::new(py); 36 | for var in resp.vars.iter() { 37 | match &var.value { 38 | SnmpValue::Null 39 | | SnmpValue::NoSuchObject 40 | | SnmpValue::NoSuchInstance 41 | | SnmpValue::EndOfMibView => continue, 42 | _ => dict 43 | .set_item(&var.oid, &var.value) 44 | .map_err(|e| PyRuntimeError::new_err(e.to_string()))?, 45 | } 46 | } 47 | Ok(dict.as_any().to_owned()) 48 | } 49 | SnmpPdu::Report(_) => Err(SnmpError::AuthenticationFailed.into()), 50 | _ => Err(SnmpError::InvalidPdu.into()), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/dev/standards.md: -------------------------------------------------------------------------------- 1 | # Supported Standards 2 | 3 | Gufo SNMP implements and is guided by the following standards: 4 | 5 | ## IETF RFC 6 | 7 | * [RFC-1157][RFC-1157]: A Simple Network Management Protocol (SNMP). 8 | * [RFC-1441][RFC-1441]: Introduction to version 2 of the Internet-standard Network Management Framework. 9 | * [RFC-1442][RFC-1442]: Structure of Management Information for version 2 of the Simple Network Management Protocol (SNMPv2). 10 | * [RFC-1905][RFC-1905]: Protocol Operations for Version 2 of the Simple Network Management Protocol (SNMPv2) 11 | * [RFC-2578][RFC-2578]: Structure of Management Information Version 2 (SMIv2). 12 | * [RFC-3411][RFC-3411]: An Architecture for Describing Simple Network Management Protocol (SNMP) Management Frameworks. 13 | * [RFC-3412][RFC-3412]: Message Processing and Dispatching for the Simple Network Management Protocol (SNMP) 14 | * [RFC-3414][RFC-3414]: User-based Security Model (USM) for version 3 of the Simple Network Management Protocol (SNMPv3) 15 | * [RFC-3826][RFC-3826]: The Advanced Encryption Standard (AES) Cipher Algorithm in the SNMP User-based Security Model 16 | 17 | ## ITU-T 18 | 19 | * [X-690][X-690]: Information technology – ASN.1 encoding rules: Specification of Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and Distinguished Encoding Rules (DER). 20 | 21 | ## :simple-python: Python PEP 22 | 23 | * [PEP8][PEP8]: Style Guide for Python Code. 24 | * [PEP484][PEP484]: Type Hints 25 | * [PEP561][PEP561]: Distributing and Packaging Type Information. 26 | 27 | [RFC-1157]: https://www.rfc-editor.org/rfc/rfc1157.html 28 | [RFC-1441]: https://www.rfc-editor.org/rfc/rfc1441.html 29 | [RFC-1442]: https://www.rfc-editor.org/rfc/rfc1442.html 30 | [RFC-1905]: https://www.rfc-editor.org/rfc/rfc1905.html 31 | [RFC-2578]: https://www.rfc-editor.org/rfc/rfc2578.html 32 | [RFC-3411]: https://www.rfc-editor.org/rfc/rfc3411.html 33 | [RFC-3412]: https://www.rfc-editor.org/rfc/rfc3412.html 34 | [RFC-3414]: https://www.rfc-editor.org/rfc/rfc3414.html 35 | [RFC-3826]: https://www.rfc-editor.org/rfc/rfc3826.html 36 | [PEP8]: https://peps.python.org/pep-0008/ 37 | [PEP484]: https://peps.python.org/pep-0484/ 38 | [PEP561]: https://peps.python.org/pep-0561/ 39 | [X-690]: https://www.itu.int/rec/T-REC-X.690 40 | -------------------------------------------------------------------------------- /src/ber/ipaddress.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: SNMP Application Class IpAddress 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{BerDecoder, BerHeader, TAG_APP_IPADDRESS, Tag}; 9 | use crate::error::{SnmpError, SnmpResult}; 10 | use pyo3::{Bound, IntoPyObject, PyAny, Python, types::PyString}; 11 | 12 | pub struct SnmpIpAddress(u8, u8, u8, u8); 13 | 14 | impl<'a> BerDecoder<'a> for SnmpIpAddress { 15 | const ALLOW_PRIMITIVE: bool = true; 16 | const ALLOW_CONSTRUCTED: bool = false; 17 | const TAG: Tag = TAG_APP_IPADDRESS; 18 | 19 | // Implement RFC 20 | fn decode(i: &'a [u8], h: &BerHeader) -> SnmpResult { 21 | if h.length != 4 { 22 | return Err(SnmpError::InvalidTagFormat); 23 | } 24 | Ok(SnmpIpAddress(i[0], i[1], i[2], i[3])) 25 | } 26 | } 27 | 28 | impl From<&SnmpIpAddress> for String { 29 | fn from(value: &SnmpIpAddress) -> Self { 30 | format!("{}.{}.{}.{}", value.0, value.1, value.2, value.3) 31 | } 32 | } 33 | 34 | impl<'py> IntoPyObject<'py> for &SnmpIpAddress { 35 | type Target = PyAny; 36 | type Output = Bound<'py, Self::Target>; 37 | type Error = SnmpError; 38 | 39 | fn into_pyobject(self, py: Python<'py>) -> Result { 40 | let s: String = self.into(); 41 | Ok(PyString::new(py, &s).into_any()) 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | 49 | #[test] 50 | fn test_parse() -> SnmpResult<()> { 51 | let data = [0x40, 0x4, 127, 0, 0, 1]; 52 | let (tail, ip) = SnmpIpAddress::from_ber(&data)?; 53 | assert_eq!(tail.len(), 0); 54 | assert_eq!(ip.0, 127); 55 | assert_eq!(ip.1, 0); 56 | assert_eq!(ip.2, 0); 57 | assert_eq!(ip.3, 1); 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn test_into_str() { 63 | let ip = &SnmpIpAddress(127, 0, 0, 1); 64 | let s: String = ip.into(); 65 | assert_eq!(s, "127.0.0.1"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/benchmarks/v2c/test_v2c_getbulk.txt: -------------------------------------------------------------------------------- 1 | ============================= test session starts ============================== 2 | platform linux -- Python 3.13.2, pytest-8.3.3, pluggy-1.5.0 3 | benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=50 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 4 | rootdir: /workspaces/gufo_snmp 5 | configfile: pyproject.toml 6 | plugins: benchmark-5.1.0 7 | collected 4 items 8 | 9 | benchmarks/test_v2c_getbulk.py .... [100%] 10 | 11 | 12 | ------------------------------------------------------------------------------------ benchmark: 4 tests ------------------------------------------------------------------------------------ 13 | Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 14 | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 15 | test_gufo_snmp_sync 37.9863 (1.0) 74.4578 (1.25) 45.3796 (1.0) 5.5611 (2.73) 46.6150 (1.0) 7.1543 (7.34) 12;1 22.0364 (1.0) 50 1 16 | test_gufo_snmp_async 46.0101 (1.21) 59.4995 (1.0) 52.9366 (1.17) 2.0354 (1.0) 52.5597 (1.13) 0.9750 (1.0) 8;8 18.8905 (0.86) 50 1 17 | test_easysnmp_sync 49.8938 (1.31) 73.9654 (1.24) 67.1123 (1.48) 3.2053 (1.57) 66.7074 (1.43) 1.4055 (1.44) 6;9 14.9004 (0.68) 50 1 18 | test_pysnmp_async 573.2397 (15.09) 596.9186 (10.03) 583.6291 (12.86) 5.6439 (2.77) 583.4234 (12.52) 7.8223 (8.02) 17;0 1.7134 (0.08) 50 1 19 | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 20 | 21 | Legend: 22 | Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. 23 | OPS: Operations Per Second, computed as 1 / Mean 24 | ============================== 4 passed in 40.01s ============================== 25 | -------------------------------------------------------------------------------- /src/snmp/op/get.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: Get operation 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{GetIter, PyOp}; 9 | use crate::{ 10 | ber::SnmpOid, 11 | error::SnmpError, 12 | snmp::{get::SnmpGet, msg::SnmpPdu, value::SnmpValue}, 13 | }; 14 | use pyo3::{IntoPyObject, prelude::*, pybacked::PyBackedStr, types::PyNone}; 15 | 16 | pub struct OpGet; 17 | 18 | impl<'a> PyOp<'a, PyBackedStr> for OpGet { 19 | // Obj is str 20 | fn from_python(obj: PyBackedStr, request_id: i64) -> PyResult> { 21 | Ok(SnmpPdu::GetRequest(SnmpGet { 22 | request_id, 23 | vars: vec![SnmpOid::try_from(obj.as_ref())?], 24 | })) 25 | } 26 | fn to_python<'py>( 27 | pdu: &SnmpPdu, 28 | _iter: Option<&mut GetIter>, 29 | py: Python<'py>, 30 | ) -> PyResult> { 31 | match pdu { 32 | SnmpPdu::GetResponse(resp) => { 33 | // Check varbinds size 34 | match resp.vars.len() { 35 | // Empty response, return None 36 | 0 => Ok(PyNone::get(py).as_any().to_owned()), 37 | // Return value 38 | 1 => { 39 | let var = &resp.vars[0]; 40 | let value = &var.value; 41 | match value { 42 | SnmpValue::NoSuchObject 43 | | SnmpValue::NoSuchInstance 44 | | SnmpValue::EndOfMibView => Err(SnmpError::NoSuchInstance.into()), 45 | SnmpValue::Null => Ok(PyNone::get(py).as_any().to_owned()), 46 | _ => Ok(value.into_pyobject(py)?), 47 | } 48 | } 49 | // Multiple response, surely an error 50 | _ => Err(SnmpError::InvalidPdu.into()), 51 | } 52 | } 53 | SnmpPdu::Report(_) => Err(SnmpError::AuthenticationFailed.into()), 54 | _ => Err(SnmpError::InvalidPdu.into()), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tools/dev/fmt-iai.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Python modules 4 | import sys 5 | from dataclasses import dataclass 6 | from typing import Iterable, List, Optional 7 | 8 | 9 | @dataclass 10 | class Bench(object): 11 | name: str 12 | inst: int 13 | l1: int 14 | l2: int 15 | ram: int 16 | cycles: int 17 | 18 | def adjust(self, base: "Bench") -> "Bench": 19 | """Adjust benchmark related to base.""" 20 | return Bench( 21 | name=self.name, 22 | inst=self.inst - base.inst, 23 | l1=self.l1 - base.l1, 24 | l2=self.l2 - base.l2, 25 | ram=self.ram - base.ram, 26 | cycles=self.cycles - base.cycles, 27 | ) 28 | 29 | 30 | def iter_blocks() -> Iterable[List[str]]: 31 | r: List[str] = [] 32 | for line in sys.stdin: 33 | line = line.strip() 34 | if not line: 35 | yield r 36 | r = [] 37 | else: 38 | r.append(line) 39 | if r: 40 | yield r 41 | 42 | 43 | def is_valid(v: List[str]) -> bool: 44 | return len(v) == 6 45 | 46 | 47 | def get_value(v: str) -> int: 48 | x = v.split(":", 1)[1].strip() 49 | if " " in x: 50 | return int(x.split()[0]) 51 | return int(x) 52 | 53 | 54 | def iter_benches() -> Iterable[Bench]: 55 | for block in iter_blocks(): 56 | if not is_valid(block): 57 | continue 58 | yield Bench( 59 | name=block[0], 60 | inst=get_value(block[1]), 61 | l1=get_value(block[2]), 62 | l2=get_value(block[3]), 63 | ram=get_value(block[4]), 64 | cycles=get_value(block[5]), 65 | ) 66 | 67 | 68 | def main() -> None: 69 | benches = list(iter_benches()) 70 | buf_default: Optional[Bench] = None 71 | print( 72 | "| Name | Inst.[^1] | L1 Acc.[^2] | L2 Acc.[^3] | " 73 | "RAM Acc.[^4] | Est. Cycles [^5] |" 74 | ) 75 | print("| --- | --: | --: | --: | --: | --: |") 76 | for b in benches: 77 | if b.name == "buf_default": 78 | buf_default = b 79 | elif b.name.startswith("buf_") and buf_default: 80 | b = b.adjust(buf_default) 81 | print( 82 | f"| {b.name} | {b.inst} | {b.l1} | {b.l2} | {b.ram} | {b.cycles} |" 83 | ) 84 | 85 | 86 | if __name__ == "__main__": 87 | main() 88 | -------------------------------------------------------------------------------- /docs/benchmarks/v2c/test_v2c_getnext.txt: -------------------------------------------------------------------------------- 1 | ============================= test session starts ============================== 2 | platform linux -- Python 3.13.2, pytest-8.3.3, pluggy-1.5.0 3 | benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=50 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 4 | rootdir: /workspaces/gufo_snmp 5 | configfile: pyproject.toml 6 | plugins: benchmark-5.1.0 7 | collected 4 items 8 | 9 | benchmarks/test_v2c_getnext.py .... [100%] 10 | 11 | 12 | ----------------------------------------------------------------------------------------- benchmark: 4 tests ---------------------------------------------------------------------------------------- 13 | Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 14 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 15 | test_gufo_snmp_sync 193.4025 (1.0) 235.6148 (1.0) 214.8392 (1.0) 7.4111 (1.08) 215.1661 (1.0) 7.7423 (1.30) 13;2 4.6546 (1.0) 50 1 16 | test_easysnmp_sync 230.7012 (1.19) 269.1656 (1.14) 256.6668 (1.19) 6.8638 (1.0) 257.1268 (1.20) 8.2020 (1.38) 8;2 3.8961 (0.84) 50 1 17 | test_gufo_snmp_async 236.3053 (1.22) 289.3099 (1.23) 271.9603 (1.27) 9.3496 (1.36) 273.6588 (1.27) 5.9558 (1.0) 10;7 3.6770 (0.79) 50 1 18 | test_pysnmp_async 1,437.8537 (7.43) 1,596.1364 (6.77) 1,487.5666 (6.92) 36.1268 (5.26) 1,479.2526 (6.87) 31.4574 (5.28) 10;6 0.6722 (0.14) 50 1 19 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 20 | 21 | Legend: 22 | Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. 23 | OPS: Operations Per Second, computed as 1 / Mean 24 | ======================== 4 passed in 117.12s (0:01:57) ========================= 25 | -------------------------------------------------------------------------------- /tests/test_policer.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo Labs: Test Gufo SNMP 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Python modules 9 | import asyncio 10 | from time import perf_counter_ns 11 | 12 | # Third-party modules 13 | import pytest 14 | 15 | # Gufo Labs modules 16 | from gufo.snmp.policer import BasePolicer, RPSPolicer 17 | 18 | 19 | def test_base_instance() -> None: 20 | with pytest.raises(TypeError): 21 | BasePolicer() 22 | 23 | 24 | @pytest.mark.parametrize("rps", [0.0, -1.0, 10_000_000_000.0]) 25 | def test_invalid_rps(rps: float) -> None: 26 | with pytest.raises(ValueError): 27 | RPSPolicer(rps) 28 | 29 | 30 | def test_rps_timeout() -> None: 31 | def T(s: int, t: int = 0) -> int: 32 | return t0 + s * step + t * tick 33 | 34 | t0 = 0 # perf_counter_ns() 35 | rps = 10 36 | step = 1_000_000_000 // rps 37 | tick = step // 4 38 | 39 | # ts, prev, timeout 40 | scenario = [ 41 | (T(0), T(0), None), 42 | (T(0, 3), T(1), tick), 43 | (T(1, 1), T(2), 3 * tick), 44 | (T(2), T(3), 4 * tick), 45 | (T(4), T(4), None), 46 | (T(7, 1), T(7), None), 47 | ] 48 | p = RPSPolicer(rps) 49 | for ts, prev, timeout in scenario: 50 | print(f"# ts={ts:,} prev={p._prev}") 51 | r = p.get_timeout(ts) 52 | assert timeout == r 53 | assert p._prev == prev 54 | 55 | 56 | def test_rps_flood_async() -> None: 57 | async def inner() -> None: 58 | p = RPSPolicer(rps) 59 | # Emulate flood of requests 60 | for _ in range(requests): 61 | await p.wait() 62 | 63 | rps = 10 64 | duration = 2 65 | requests = duration * rps + 1 66 | t0 = perf_counter_ns() 67 | asyncio.run(inner()) 68 | delta = perf_counter_ns() - t0 69 | # Check duration 70 | assert delta >= duration * 1_000_000_000 71 | 72 | 73 | def test_rps_flood_sync() -> None: 74 | rps = 10 75 | duration = 2 76 | requests = duration * rps + 1 77 | t0 = perf_counter_ns() 78 | p = RPSPolicer(rps) 79 | # Emulate flood of requests 80 | for _ in range(requests): 81 | p.wait_sync() 82 | delta = perf_counter_ns() - t0 83 | # Check duration 84 | assert delta >= duration * 1_000_000_000 85 | -------------------------------------------------------------------------------- /src/gufo/snmp/sync_client/getbulk.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: GetBulkIter 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | """GetBulkIter iterator.""" 9 | 10 | # Python modules 11 | from typing import List, Optional, Tuple, Union 12 | 13 | # Gufo Labs Modules 14 | from .._fast import GetIter as _Iter 15 | from ..policer import BasePolicer 16 | from ..protocol import SnmpClientSocketProtocol 17 | from ..typing import ValueType 18 | 19 | 20 | class GetBulkIter(object): 21 | """Wrap the series of the GetBulk requests. 22 | 23 | Args: 24 | sock: Parent SnmpClientSocket. 25 | oid: Base oid. 26 | max_repetitions: Max amount of iterms per response. 27 | policer: Optional BasePolicer instance to limit requests. 28 | """ 29 | 30 | def __init__( 31 | self: "GetBulkIter", 32 | sock: SnmpClientSocketProtocol, 33 | oid: str, 34 | max_repetitions: int, 35 | policer: Optional[BasePolicer] = None, 36 | ) -> None: 37 | self._sock = sock 38 | self._ctx = _Iter(oid, max_repetitions) 39 | self._max_repetitions = max_repetitions 40 | self._buffer: List[Union[Tuple[str, ValueType], None]] = [] 41 | self._policer = policer 42 | 43 | def __iter__(self: "GetBulkIter") -> "GetBulkIter": 44 | """Return asynchronous iterator.""" 45 | return self 46 | 47 | def __next__(self: "GetBulkIter") -> Tuple[str, ValueType]: 48 | """Get next value.""" 49 | 50 | def pop_or_stop() -> Tuple[str, ValueType]: 51 | v = self._buffer.pop(0) 52 | if v is None: 53 | raise StopIteration 54 | return v 55 | 56 | # Return item from buffer, if present 57 | if self._buffer: 58 | return pop_or_stop() 59 | # Policer 60 | if self._policer: 61 | self._policer.wait_sync() 62 | try: 63 | self._buffer = self._sock.get_bulk(self._ctx) 64 | except BlockingIOError as e: 65 | raise TimeoutError from e 66 | except StopAsyncIteration as e: 67 | raise StopIteration from e 68 | # End 69 | if not self._buffer: 70 | raise StopIteration # End of view 71 | return pop_or_stop() 72 | -------------------------------------------------------------------------------- /docs/benchmarks/index.md: -------------------------------------------------------------------------------- 1 | # Python SNMP Clients Benchmarks 2 | !!! warning "Disclaimer" 3 | 4 | All following information is provided only for reference. 5 | These tests are performed by [Gufo Labs][Gufo Labs] to estimate the performance 6 | of [Gufo SNMP][Gufo SNMP] against major competitors, so they cannot be considered 7 | independent and unbiased. 8 | 9 | !!! note 10 | 11 | Although performance is an absolute requirement for [Gufo Stack][Gufo Stack], 12 | other factors such as maturity, community, features, examples, and existing code base 13 | should also be considered. 14 | 15 | This benchmark evaluates several Python SNMP client libraries: 16 | 17 | | Library | Version | Description | Stars | Sync
Mode | Async
Mode | SNMPv3 | 18 | | ---------------------- | ------- | --------------------------------- | ------------------------- | ---------------- | ---------------- | ---------------- | 19 | | [Gufo SNMP][Gufo SNMP] | 0.8.0 | An accelerated Python SNMP client | ![Stars][Gufo SNMP Stars] | :material-check: | :material-check: | :material-check: | 20 | | [pysnmp][pysnmp] | 7.1.17 | pure-Python SNMP client | ![Stars][pysnmp Stars] | | | | 21 | | [easysnmp][easysnmp] | 0.2.6 | Net-SNMP Python bindings | ![Stars][easysnmp Stars] | :material-check: | :material-close: | | 22 | 23 | The evaluation covers the following aspects: 24 | 25 | * Performance in synchronous (blocking) mode, if supported. 26 | * Performance in asynchronous (non-blocking) mode, if supported. 27 | * Performance in plain-text SNMP (v2c) and encrypted (SNMP v3) modes. 28 | * Ability to release GIL in multi-threaded applications. 29 | 30 | All benchmarks are performed against a local Net-SNMPd installation 31 | using wrapper, provided by `gufo.snmp.snmpd`. 32 | 33 | The benchmarking environment utilizes an docker container running on 34 | Apple M4 Pro processor. 35 | 36 | ## Benchmark Results 37 | 38 | * [Preparing](preparing.md) 39 | * [SNMP v2c](v2c/index.md) 40 | * [SNMP v3](v3/index.md) 41 | * [Conclustions](conclusions.md) 42 | * [Feedback](feedback.md) 43 | 44 | [Gufo Labs]: https://gufolabs.com/ 45 | [Gufo Stack]: https://docs.gufolabs.com/ 46 | [Gufo SNMP]: https://docs.gufolabs.com/gufo_snmp/ 47 | [easysnmp]: https://easysnmp.readthedocs.io/en/latest/ 48 | [pysnmp]: https://docs.lextudio.com/snmp/ 49 | [Gufo SNMP Stars]: https://img.shields.io/github/stars/gufolabs/gufo_snmp 50 | [pysnmp Stars]: https://img.shields.io/github/stars/lextudio/pysnmp.com 51 | [easysnmp Stars]: https://img.shields.io/github/stars/easysnmp/easysnmp -------------------------------------------------------------------------------- /docs/examples/sync/get.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP Example: Single Item Get Request 2 | 3 | `Get` is one of the basic SNMP operations allowing to query of the agent 4 | for one or more management keys. Let's consider the situation of 5 | getting the single key. 6 | 7 | ``` py title="get.py" linenums="1" 8 | --8<-- "examples/sync/get.py" 9 | ``` 10 | 11 | Let's see the details. 12 | 13 | ``` py title="get.py" linenums="1" hl_lines="1" 14 | --8<-- "examples/sync/get.py" 15 | ``` 16 | Import `sys` module to parse the CLI argument. 17 | 18 | !!! warning 19 | 20 | We use `sys.argv` only for demonstration purposes. Use `argsparse` or alternatives 21 | in real-world applications. 22 | 23 | ``` py title="get.py" linenums="1" hl_lines="3" 24 | --8<-- "examples/sync/get.py" 25 | ``` 26 | 27 | `SnmpSession` object holds all necessary API. We're using a synchronous 28 | version from `gufo.snmp.sync_client`. 29 | 30 | ``` py title="get.py" linenums="1" hl_lines="6" 31 | --8<-- "examples/sync/get.py" 32 | ``` 33 | 34 | We define our main function and expect the following arguments: 35 | 36 | * Address of the agent. 37 | * SNMP community to authorize. 38 | * OID to query. 39 | 40 | ``` py title="get.py" linenums="1" hl_lines="7" 41 | --8<-- "examples/sync/get.py" 42 | ``` 43 | 44 | First, we need to create `SnmpSession` object which wraps the client's session. 45 | The `SnmpSession` may be used as an instance directly or operated as context manager 46 | using the `with` clause. When used as a context manager, 47 | the client automatically closes all connections on the exit of context, 48 | so its lifetime is defined explicitly. 49 | 50 | `SnmpSession` constructor offers lots of configuration variables for fine-tuning. Refer to the 51 | [SnmpSession reference][gufo.snmp.sync_client.SnmpSession] 52 | for further details. In our example, we set the agent's address and SNMP community 53 | to the given values. 54 | 55 | ``` py title="get.py" linenums="1" hl_lines="8" 56 | --8<-- "examples/sync/get.py" 57 | ``` 58 | 59 | We use `SnmpSession.get()` function to query OID. See [SnmpSession.get() reference][gufo.snmp.sync_client.SnmpSession.get] for further details. 60 | 61 | ``` py title="get.py" linenums="1" hl_lines="9" 62 | --8<-- "examples/sync/get.py" 63 | ``` 64 | 65 | It is up to the application how to deal with the result. 66 | In our example we just print it. 67 | 68 | ``` py title="get.py" linenums="1" hl_lines="12" 69 | --8<-- "examples/sync/get.py" 70 | ``` 71 | 72 | Lets run our `main()` function pass first command-line parameters as address, community, and OID. 73 | 74 | ## Running 75 | 76 | Let's check our script. Run example as: 77 | 78 | ``` 79 | $ python3 examples/sync/get.py 127.0.0.1 public 1.3.6.1.2.1.1.6.0 80 | Gufo SNMP Test 81 | ``` -------------------------------------------------------------------------------- /tests/test_ci.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo Labs: CI test 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2022-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Python modules 9 | import inspect 10 | import os 11 | import sys 12 | from dataclasses import dataclass 13 | from typing import Iterable 14 | 15 | # Third-party modules 16 | import pytest 17 | import yaml 18 | 19 | 20 | def _get_root() -> str: 21 | mod_path = inspect.getfile(sys.modules[__name__]) 22 | rel_root = os.path.dirname(mod_path) 23 | return os.path.abspath(os.path.join(rel_root, "..")) 24 | 25 | 26 | VERSIONS = [ 27 | "actions/cache@v4", 28 | "actions/checkout@v4", 29 | "actions/setup-python@v5", 30 | "actions/download-artifact@v4", 31 | "actions/upload-artifact@v4", 32 | "pypa/gh-action-pypi-publish@release/v1", 33 | ] 34 | 35 | 36 | @dataclass 37 | class Action(object): 38 | path: str 39 | job: str 40 | step: str 41 | action: str 42 | version: str 43 | expected: str 44 | 45 | 46 | def action_label(action: Action) -> str: 47 | return f"{action.path}: {action.job}/{action.step}: {action.action}" 48 | 49 | 50 | def _iter_actions() -> Iterable[Action]: 51 | versions = {a.split("@")[0]: a.split("@")[1] for a in VERSIONS} 52 | root = os.path.join(_get_root(), ".github", "workflows") 53 | for fn in os.listdir(root): 54 | if fn.startswith(".") or not fn.endswith(".yml"): 55 | continue 56 | path = os.path.join(root, fn) 57 | with open(path) as f: 58 | data = yaml.safe_load(f) 59 | for job in data["jobs"]: 60 | for step in data["jobs"][job]["steps"]: 61 | if "uses" in step: 62 | uses = step["uses"] 63 | for v, expected in versions.items(): 64 | if uses.startswith(f"{v}@"): 65 | yield Action( 66 | path=fn, 67 | job=job, 68 | step=step["name"], 69 | action=v, 70 | version=uses.split("@")[1], 71 | expected=expected, 72 | ) 73 | break 74 | 75 | 76 | @pytest.mark.parametrize("action", list(_iter_actions()), ids=action_label) 77 | def test_actions(action: Action) -> None: 78 | loc = f"{action.path}: {action.job}/{action.step}" 79 | v_exp = f"{action.action}@{action.expected}" 80 | msg = f"{loc}: {v_exp} required (@{action.version} used)" 81 | assert action.version == action.expected, msg 82 | -------------------------------------------------------------------------------- /benchmarks/test_v2c_getnext.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: Getnext benchmarks 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Python modules 9 | import asyncio 10 | 11 | # Third-party modules 12 | # from pysnmp.hlapi import v3arch 13 | from easysnmp import snmp_walk as e_snmp_walk 14 | 15 | # Gufo SNMP modules 16 | from gufo.snmp.async_client import SnmpSession as AsyncSnmpSession 17 | from gufo.snmp.snmpd import Snmpd 18 | from gufo.snmp.sync_client import SnmpSession as SyncSnmpSession 19 | 20 | BASE_OID = "1.3.6" 21 | SNMP_COMMUNITY = "public" 22 | 23 | 24 | def test_gufo_snmp_sync(snmpd: Snmpd, benchmark) -> None: 25 | @benchmark 26 | def bench(): 27 | with SyncSnmpSession( 28 | addr=snmpd.address, port=snmpd.port, community=SNMP_COMMUNITY 29 | ) as session: 30 | for _k, _v in session.getnext(BASE_OID): 31 | pass 32 | 33 | 34 | def test_gufo_snmp_async(snmpd: Snmpd, benchmark) -> None: 35 | async def inner(): 36 | async with AsyncSnmpSession( 37 | addr=snmpd.address, port=snmpd.port, community=SNMP_COMMUNITY 38 | ) as session: 39 | async for _k, _v in session.getnext(BASE_OID): 40 | pass 41 | 42 | @benchmark 43 | def bench(): 44 | asyncio.run(inner()) 45 | 46 | 47 | def test_easysnmp_sync(snmpd: Snmpd, benchmark) -> None: 48 | @benchmark 49 | def bench(): 50 | for item in e_snmp_walk( 51 | oids=BASE_OID, 52 | community=SNMP_COMMUNITY, 53 | hostname=snmpd.address, 54 | version=2, 55 | remote_port=snmpd.port, 56 | use_numeric=True, 57 | ): 58 | _ = item.oid # Force deserialization 59 | _ = item.value # Force deserialization 60 | 61 | 62 | def test_pysnmp_async(snmpd: Snmpd, benchmark) -> None: 63 | from pysnmp.hlapi.v3arch.asyncio import ( 64 | CommunityData, 65 | ContextData, 66 | ObjectIdentity, 67 | ObjectType, 68 | SnmpEngine, 69 | UdpTransportTarget, 70 | walk_cmd, 71 | ) 72 | 73 | async def inner() -> None: 74 | async for _, _, _, var_binds in walk_cmd( 75 | SnmpEngine(), 76 | CommunityData(SNMP_COMMUNITY), 77 | await UdpTransportTarget.create((snmpd.address, snmpd.port)), 78 | ContextData(), 79 | ObjectType(ObjectIdentity(BASE_OID)), 80 | ): 81 | for i in var_binds: 82 | i.prettyPrint() 83 | 84 | @benchmark 85 | def bench(): 86 | asyncio.run(inner()) 87 | -------------------------------------------------------------------------------- /docs/benchmarks/v3/test_v3_getbulk.txt: -------------------------------------------------------------------------------- 1 | ============================= test session starts ============================== 2 | platform linux -- Python 3.13.2, pytest-8.3.3, pluggy-1.5.0 3 | benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=50 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 4 | rootdir: /workspaces/gufo_snmp 5 | configfile: pyproject.toml 6 | plugins: benchmark-5.1.0 7 | collected 3 items 8 | 9 | benchmarks/test_v3_getbulk.py ... [100%] 10 | 11 | =============================== warnings summary =============================== 12 | benchmarks/test_v3_getbulk.py: 1248 warnings 13 | /usr/local/lib/python3.13/site-packages/pysnmp/smi/mibs/SNMPv2-SMI.py:1259: DeprecationWarning: isFixedLength is deprecated. Please use is_fixed_length instead. 14 | if impliedFlag or obj.isFixedLength(): 15 | 16 | benchmarks/test_v3_getbulk.py: 2808 warnings 17 | /usr/local/lib/python3.13/site-packages/pysnmp/smi/mibs/SNMPv2-SMI.py:1231: DeprecationWarning: isFixedLength is deprecated. Please use is_fixed_length instead. 18 | elif obj.isFixedLength(): 19 | 20 | -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html 21 | 22 | ------------------------------------------------------------------------------------ benchmark: 3 tests ------------------------------------------------------------------------------------ 23 | Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 24 | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 25 | test_gufo_snmp_sync 41.6209 (1.0) 52.5063 (1.0) 48.3213 (1.0) 3.2135 (1.69) 49.4517 (1.0) 3.1033 (2.40) 15;7 20.6948 (1.0) 50 1 26 | test_gufo_snmp_async 50.4980 (1.21) 59.5947 (1.13) 55.8961 (1.16) 1.8984 (1.0) 55.8032 (1.13) 1.2932 (1.0) 14;11 17.8904 (0.86) 50 1 27 | test_pysnmp_async 635.4062 (15.27) 665.9244 (12.68) 647.6706 (13.40) 5.6292 (2.97) 647.6818 (13.10) 6.8157 (5.27) 15;1 1.5440 (0.07) 50 1 28 | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 29 | 30 | Legend: 31 | Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. 32 | OPS: Operations Per Second, computed as 1 / Mean 33 | ====================== 3 passed, 4056 warnings in 40.18s ======================= 34 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // VScode development container settings: 2 | // * For format details, see https://aka.ms/devcontainer.json. 3 | // * For config options, see the README at: 4 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.202.3/containers/docker-existing-dockerfile 5 | // * For configuration guidelines see @todo 6 | { 7 | "name": "Gufo SNMP", 8 | "runArgs": [ 9 | "--init", 10 | "--privileged" 11 | ], 12 | "build": { 13 | // Sets the run context to one level up instead of the .devcontainer folder. 14 | "context": "..", 15 | // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. 16 | "dockerfile": "../Dockerfile", 17 | // Dockerfile target 18 | "target": "dev" 19 | }, 20 | "containerEnv": { 21 | "PYTHONPATH": "src" 22 | }, 23 | "customizations": { 24 | "vscode": { 25 | "settings": { 26 | "python.defaultInterpreterPath": "/usr/local/bin/python", 27 | "python.testing.defaultTestFramework": "pytest", 28 | "python.testing.cwd": "${workspaceFolder}", 29 | "python.testing.pytestEnabled": true, 30 | "python.testing.unittestEnabled": false, 31 | "python.testing.nosetestsEnabled": false, 32 | "python.testing.pytestArgs": [ 33 | "tests" 34 | ], 35 | "python.testing.autoTestDiscoverOnSaveEnabled": true, 36 | "python.analysis.extraPaths": [ 37 | "src" 38 | ], 39 | "[python]": { 40 | "editor.defaultFormatter": "charliermarsh.ruff", 41 | "editor.formatOnSave": true, 42 | "editor.codeActionsOnSave": { 43 | "source.fixAll": true 44 | }, 45 | "ruff.path": "/usr/local/bin/ruff" 46 | }, 47 | "[yaml]": { 48 | "editor.defaultFormatter": "redhat.vscode-yaml", 49 | "editor.formatOnSave": true, 50 | "editor.autoIndent": "advanced", 51 | "editor.tabSize": 2, 52 | "editor.detectIndentation": false 53 | } 54 | }, 55 | // Add the IDs of extensions you want installed when the container is created. 56 | "extensions": [ 57 | "ms-python.python", 58 | "ms-vscode.cpptools", 59 | "yzhang.markdown-all-in-one", 60 | "ms-azuretools.vscode-docker", 61 | "be5invis.toml", 62 | "redhat.vscode-yaml", 63 | "charliermarsh.ruff", 64 | "rust-lang.rust-analyzer" 65 | ] 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /benchmarks/test_v2c_getbulk.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: GETBULK benchmarks 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Python modules 9 | import asyncio 10 | 11 | # Third-party modules 12 | # from pysnmp.hlapi import v3arch 13 | from easysnmp import snmp_bulkwalk as e_snmp_bulkwalk 14 | 15 | # Gufo SNMP modules 16 | from gufo.snmp.async_client import SnmpSession as AsyncSnmpSession 17 | from gufo.snmp.snmpd import Snmpd 18 | from gufo.snmp.sync_client import SnmpSession as SyncSnmpSession 19 | 20 | BASE_OID = "1.3.6" 21 | SNMP_COMMUNITY = "public" 22 | 23 | 24 | def test_gufo_snmp_sync(snmpd: Snmpd, benchmark) -> None: 25 | @benchmark 26 | def bench(): 27 | with SyncSnmpSession( 28 | addr=snmpd.address, port=snmpd.port, community=SNMP_COMMUNITY 29 | ) as session: 30 | for _k, _v in session.getbulk(BASE_OID): 31 | pass 32 | 33 | 34 | def test_gufo_snmp_async(snmpd: Snmpd, benchmark) -> None: 35 | async def inner(): 36 | async with AsyncSnmpSession( 37 | addr=snmpd.address, port=snmpd.port, community=SNMP_COMMUNITY 38 | ) as session: 39 | async for _k, _v in session.getbulk(BASE_OID): 40 | pass 41 | 42 | @benchmark 43 | def bench(): 44 | asyncio.run(inner()) 45 | 46 | 47 | def test_easysnmp_sync(snmpd: Snmpd, benchmark) -> None: 48 | @benchmark 49 | def bench(): 50 | for item in e_snmp_bulkwalk( 51 | oids=BASE_OID, 52 | community=SNMP_COMMUNITY, 53 | hostname=snmpd.address, 54 | version=2, 55 | remote_port=snmpd.port, 56 | use_numeric=True, 57 | ): 58 | _ = item.oid # Force deserialization 59 | _ = item.value # Force deserialization 60 | 61 | 62 | def test_pysnmp_async(snmpd: Snmpd, benchmark) -> None: 63 | from pysnmp.hlapi.v3arch.asyncio import ( 64 | CommunityData, 65 | ContextData, 66 | ObjectIdentity, 67 | ObjectType, 68 | SnmpEngine, 69 | UdpTransportTarget, 70 | bulk_walk_cmd, 71 | ) 72 | 73 | async def inner() -> None: 74 | async for _, _, _, var_binds in bulk_walk_cmd( 75 | SnmpEngine(), 76 | CommunityData(SNMP_COMMUNITY), 77 | await UdpTransportTarget.create((snmpd.address, snmpd.port)), 78 | ContextData(), 79 | 0, 80 | 25, 81 | ObjectType(ObjectIdentity(BASE_OID)), 82 | ): 83 | for i in var_binds: 84 | i.prettyPrint() 85 | 86 | @benchmark 87 | def bench(): 88 | asyncio.run(inner()) 89 | -------------------------------------------------------------------------------- /docs/benchmarks/v3/test_v3_getnext.txt: -------------------------------------------------------------------------------- 1 | ============================= test session starts ============================== 2 | platform linux -- Python 3.13.2, pytest-8.3.3, pluggy-1.5.0 3 | benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=50 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 4 | rootdir: /workspaces/gufo_snmp 5 | configfile: pyproject.toml 6 | plugins: benchmark-5.1.0 7 | collected 3 items 8 | 9 | benchmarks/test_v3_getnext.py ... [100%] 10 | 11 | =============================== warnings summary =============================== 12 | benchmarks/test_v3_getnext.py: 1248 warnings 13 | /usr/local/lib/python3.13/site-packages/pysnmp/smi/mibs/SNMPv2-SMI.py:1259: DeprecationWarning: isFixedLength is deprecated. Please use is_fixed_length instead. 14 | if impliedFlag or obj.isFixedLength(): 15 | 16 | benchmarks/test_v3_getnext.py: 2808 warnings 17 | /usr/local/lib/python3.13/site-packages/pysnmp/smi/mibs/SNMPv2-SMI.py:1231: DeprecationWarning: isFixedLength is deprecated. Please use is_fixed_length instead. 18 | elif obj.isFixedLength(): 19 | 20 | -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html 21 | 22 | ----------------------------------------------------------------------------------------- benchmark: 3 tests ---------------------------------------------------------------------------------------- 23 | Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 24 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 25 | test_gufo_snmp_sync 208.0312 (1.0) 252.8327 (1.0) 239.8860 (1.0) 10.1372 (1.0) 242.3964 (1.0) 8.3412 (1.0) 11;5 4.1686 (1.0) 50 1 26 | test_gufo_snmp_async 249.4254 (1.20) 306.4442 (1.21) 286.6626 (1.19) 13.6240 (1.34) 289.1814 (1.19) 19.1647 (2.30) 13;0 3.4884 (0.84) 50 1 27 | test_pysnmp_async 2,320.6673 (11.16) 2,460.6381 (9.73) 2,353.8917 (9.81) 26.0141 (2.57) 2,350.1734 (9.70) 22.2098 (2.66) 9;2 0.4248 (0.10) 50 1 28 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 29 | 30 | Legend: 31 | Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. 32 | OPS: Operations Per Second, computed as 1 / Mean 33 | ================= 3 passed, 4056 warnings in 150.95s (0:02:30) ================= 34 | -------------------------------------------------------------------------------- /docs/benchmarks/v3/test_v3_p4_getbulk.txt: -------------------------------------------------------------------------------- 1 | ============================= test session starts ============================== 2 | platform linux -- Python 3.13.2, pytest-8.3.3, pluggy-1.5.0 3 | benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=50 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 4 | rootdir: /workspaces/gufo_snmp 5 | configfile: pyproject.toml 6 | plugins: benchmark-5.1.0 7 | collected 3 items 8 | 9 | benchmarks/test_v3_p4_getbulk.py ... [100%] 10 | 11 | =============================== warnings summary =============================== 12 | benchmarks/test_v3_p4_getbulk.py: 4992 warnings 13 | /usr/local/lib/python3.13/site-packages/pysnmp/smi/mibs/SNMPv2-SMI.py:1259: DeprecationWarning: isFixedLength is deprecated. Please use is_fixed_length instead. 14 | if impliedFlag or obj.isFixedLength(): 15 | 16 | benchmarks/test_v3_p4_getbulk.py: 11232 warnings 17 | /usr/local/lib/python3.13/site-packages/pysnmp/smi/mibs/SNMPv2-SMI.py:1231: DeprecationWarning: isFixedLength is deprecated. Please use is_fixed_length instead. 18 | elif obj.isFixedLength(): 19 | 20 | -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html 21 | 22 | ----------------------------------------------------------------------------------------- benchmark: 3 tests ----------------------------------------------------------------------------------------- 23 | Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 24 | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 25 | test_gufo_snmp_sync 12.1797 (1.0) 14.0205 (1.0) 12.8193 (1.0) 0.3110 (1.0) 12.7744 (1.0) 0.3250 (1.0) 10;2 78.0076 (1.0) 50 1 26 | test_gufo_snmp_async 14.9465 (1.23) 23.0973 (1.65) 20.0516 (1.56) 1.4909 (4.79) 20.4244 (1.60) 1.0378 (3.19) 8;7 49.8714 (0.64) 64 1 27 | test_pysnmp_async 2,363.2356 (194.03) 2,457.3349 (175.27) 2,401.9015 (187.37) 21.7204 (69.83) 2,399.1742 (187.81) 27.6077 (84.96) 19;1 0.4163 (0.01) 50 1 28 | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 29 | 30 | Legend: 31 | Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. 32 | OPS: Operations Per Second, computed as 1 / Mean 33 | ================ 3 passed, 16224 warnings in 128.03s (0:02:08) ================= 34 | -------------------------------------------------------------------------------- /docs/benchmarks/v3/test_v3_p4_getnext.txt: -------------------------------------------------------------------------------- 1 | ============================= test session starts ============================== 2 | platform linux -- Python 3.13.2, pytest-8.3.3, pluggy-1.5.0 3 | benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=50 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 4 | rootdir: /workspaces/gufo_snmp 5 | configfile: pyproject.toml 6 | plugins: benchmark-5.1.0 7 | collected 3 items 8 | 9 | benchmarks/test_v3_p4_getnext.py ... [100%] 10 | 11 | =============================== warnings summary =============================== 12 | benchmarks/test_v3_p4_getnext.py: 4992 warnings 13 | /usr/local/lib/python3.13/site-packages/pysnmp/smi/mibs/SNMPv2-SMI.py:1259: DeprecationWarning: isFixedLength is deprecated. Please use is_fixed_length instead. 14 | if impliedFlag or obj.isFixedLength(): 15 | 16 | benchmarks/test_v3_p4_getnext.py: 11232 warnings 17 | /usr/local/lib/python3.13/site-packages/pysnmp/smi/mibs/SNMPv2-SMI.py:1231: DeprecationWarning: isFixedLength is deprecated. Please use is_fixed_length instead. 18 | elif obj.isFixedLength(): 19 | 20 | -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html 21 | 22 | ------------------------------------------------------------------------------------------ benchmark: 3 tests ----------------------------------------------------------------------------------------- 23 | Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations 24 | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 25 | test_gufo_snmp_sync 397.5008 (1.0) 425.5758 (1.0) 409.9564 (1.0) 5.3523 (1.0) 409.1588 (1.0) 6.7810 (1.0) 9;1 2.4393 (1.0) 50 1 26 | test_gufo_snmp_async 518.4271 (1.30) 628.2227 (1.48) 580.2550 (1.42) 19.0326 (3.56) 582.8090 (1.42) 11.6073 (1.71) 8;6 1.7234 (0.71) 50 1 27 | test_pysnmp_async 9,061.7766 (22.80) 9,520.1535 (22.37) 9,221.8081 (22.49) 112.1348 (20.95) 9,180.9126 (22.44) 143.2068 (21.12) 14;1 0.1084 (0.04) 50 1 28 | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 29 | 30 | Legend: 31 | Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. 32 | OPS: Operations Per Second, computed as 1 / Mean 33 | ================ 3 passed, 16224 warnings in 532.31s (0:08:52) ================= 34 | -------------------------------------------------------------------------------- /src/snmp/op/getnext.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: GetNext operation 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{GetIter, PyOp}; 9 | use crate::ber::SnmpOid; 10 | use crate::error::SnmpError; 11 | use crate::snmp::{get::SnmpGet, msg::SnmpPdu, value::SnmpValue}; 12 | use pyo3::{ 13 | exceptions::{PyStopAsyncIteration, PyValueError}, 14 | prelude::*, 15 | types::PyTuple, 16 | }; 17 | 18 | pub struct OpGetNext; 19 | 20 | impl<'a> PyOp<'a, SnmpOid<'a>> for OpGetNext { 21 | // obj is iterable[str] 22 | fn from_python(obj: SnmpOid<'a>, request_id: i64) -> PyResult> { 23 | Ok(SnmpPdu::GetNextRequest(SnmpGet { 24 | request_id, 25 | vars: vec![obj], 26 | })) 27 | } 28 | fn to_python<'py>( 29 | pdu: &SnmpPdu, 30 | iter: Option<&mut GetIter>, 31 | py: Python<'py>, 32 | ) -> PyResult> { 33 | let b_iter = iter.ok_or_else(|| PyValueError::new_err("GetIter expected"))?; 34 | match pdu { 35 | SnmpPdu::GetResponse(resp) => { 36 | // Check varbinds size 37 | match resp.vars.len() { 38 | // Empty response, stop iteration 39 | 0 => Err(PyStopAsyncIteration::new_err("stop")), 40 | // Return value 41 | 1 => { 42 | // Extract iterator 43 | let var = &resp.vars[0]; 44 | // Check if we can continue 45 | if !b_iter.set_next_oid(&var.oid) { 46 | return Err(PyStopAsyncIteration::new_err("stop")); 47 | } 48 | // v1 may return Null at end of mib 49 | match &var.value { 50 | SnmpValue::EndOfMibView | SnmpValue::Null => { 51 | Err(PyStopAsyncIteration::new_err("stop")) 52 | } 53 | value => Ok(PyTuple::new( 54 | py, 55 | vec![var.oid.into_pyobject(py)?, value.into_pyobject(py)?], 56 | )? 57 | .as_any() 58 | .to_owned()), 59 | } 60 | } 61 | // Multiple response, surely an error 62 | _ => Err(SnmpError::InvalidPdu.into()), 63 | } 64 | } 65 | SnmpPdu::Report(_) => Err(SnmpError::AuthenticationFailed.into()), 66 | _ => Err(SnmpError::InvalidPdu.into()), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/snmp/getresponse.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: GETRESPONSE PDU Parser 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::value::SnmpValue; 9 | use crate::ber::{ 10 | BerDecoder, SnmpInt, SnmpOid, SnmpRelativeOid, SnmpSequence, TAG_OBJECT_ID, TAG_RELATIVE_OID, 11 | Tag, 12 | }; 13 | use crate::error::SnmpError; 14 | 15 | #[allow(dead_code)] 16 | pub struct SnmpGetResponse<'a> { 17 | pub(crate) request_id: i64, 18 | pub(crate) error_status: i64, 19 | pub(crate) error_index: i64, 20 | pub(crate) vars: Vec>, 21 | } 22 | 23 | pub struct SnmpVar<'a> { 24 | pub oid: SnmpOid<'a>, 25 | pub value: SnmpValue<'a>, 26 | } 27 | 28 | impl<'a> TryFrom<&'a [u8]> for SnmpGetResponse<'a> { 29 | type Error = SnmpError; 30 | 31 | fn try_from(value: &'a [u8]) -> Result { 32 | // Request id 33 | let (tail, request_id) = SnmpInt::from_ber(value)?; 34 | // error status 35 | let (tail, error_status) = SnmpInt::from_ber(tail)?; 36 | // error index 37 | let (tail, error_index) = SnmpInt::from_ber(tail)?; 38 | // varbinds 39 | let (tail, vb) = SnmpSequence::from_ber(tail)?; 40 | if !tail.is_empty() { 41 | return Err(SnmpError::TrailingData); 42 | } 43 | let mut v_tail = vb.0; 44 | let mut vars: Vec = Vec::new(); 45 | while !v_tail.is_empty() { 46 | // Parse enclosing sequence 47 | let (rest, vs) = SnmpSequence::from_ber(v_tail)?; 48 | // Parse oid. May be either absolute or relative 49 | let (tail, oid) = match vs.0[0] as Tag { 50 | TAG_OBJECT_ID => SnmpOid::from_ber(vs.0)?, 51 | TAG_RELATIVE_OID => { 52 | if vars.is_empty() { 53 | // Relative oid must follow absolute one 54 | return Err(SnmpError::UnexpectedTag); 55 | } 56 | // Parse relative oid 57 | let (t, r_oid) = SnmpRelativeOid::from_ber(vs.0)?; 58 | let oid = r_oid.normalize(&vars[vars.len() - 1].oid); 59 | // Apply relative oid 60 | (t, oid) 61 | } 62 | _ => return Err(SnmpError::UnexpectedTag), 63 | }; 64 | // Parse value 65 | let (_, value) = SnmpValue::from_ber(tail)?; 66 | // Append an item 67 | vars.push(SnmpVar { oid, value }); 68 | // Shift to the next var 69 | v_tail = rest; 70 | } 71 | Ok(SnmpGetResponse { 72 | request_id: request_id.into(), 73 | error_status: error_status.into(), 74 | error_index: error_index.into(), 75 | vars, 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /docs/examples/async/get.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP Example: Single Item Get Request 2 | 3 | `Get` is one of the basic SNMP operations allowing to query of the agent 4 | for one or more management keys. Let's consider the situation of 5 | getting the single key. 6 | 7 | ``` py title="get.py" linenums="1" 8 | --8<-- "examples/async/get.py" 9 | ``` 10 | 11 | Let's see the details. 12 | 13 | ``` py title="get.py" linenums="1" hl_lines="1" 14 | --8<-- "examples/async/get.py" 15 | ``` 16 | *Gufo SNMP* is an async library. In our case 17 | we should run the client from our synchronous script, 18 | so we need to import `asyncio` to use `asyncio.run()`. 19 | 20 | ``` py title="get.py" linenums="1" hl_lines="2" 21 | --8<-- "examples/async/get.py" 22 | ``` 23 | Import `sys` module to parse the CLI argument. 24 | 25 | !!! warning 26 | 27 | We use `sys.argv` only for demonstration purposes. Use `argsparse` or alternatives 28 | in real-world applications. 29 | 30 | ``` py title="get.py" linenums="1" hl_lines="4" 31 | --8<-- "examples/async/get.py" 32 | ``` 33 | 34 | `SnmpSession` object holds all necessary API, so import it from `gufo.snmp`. 35 | 36 | ``` py title="get.py" linenums="1" hl_lines="7" 37 | --8<-- "examples/async/get.py" 38 | ``` 39 | 40 | Asynchronous code must be executed in the asynchronous functions or coroutines. 41 | So we define our function as `async`. We expect the following arguments: 42 | 43 | * Address of the agent. 44 | * SNMP community to authorize. 45 | * OID to query. 46 | 47 | ``` py title="get.py" linenums="1" hl_lines="8" 48 | --8<-- "examples/async/get.py" 49 | ``` 50 | 51 | First, we need to create `SnmpSession` object which wraps the client's session. 52 | The `SnmpSession` may be used as an instance directly or operated as async context manager 53 | with the `async with` clause. When used as a context manager, 54 | the client automatically closes all connections on the exit of context, 55 | so its lifetime is defined explicitly. 56 | 57 | `SnmpSession` constructor offers lots of configuration variables for fine-tuning. Refer to the 58 | [SnmpSession reference][gufo.snmp.async_client.SnmpSession] 59 | for further details. In our example, we set the agent's address and SNMP community 60 | to the given values. 61 | 62 | ``` py title="get.py" linenums="1" hl_lines="9" 63 | --8<-- "examples/async/get.py" 64 | ``` 65 | 66 | We use `SnmpSession.get()` function to query OID. The function is asynchronous and 67 | must be awaited. See [SnmpSession.get() reference][gufo.snmp.async_client.SnmpSession.get] for further details. 68 | 69 | ``` py title="get.py" linenums="1" hl_lines="10" 70 | --8<-- "examples/async/get.py" 71 | ``` 72 | 73 | It is up to the application how to deal with the result. 74 | In our example we just print it. 75 | 76 | ``` py title="get.py" linenums="1" hl_lines="13" 77 | --8<-- "examples/async/get.py" 78 | ``` 79 | 80 | Lets run our asynchronous `main()` function via `asyncio.run` 81 | and pass first command-line parameters as address, community, and OID. 82 | 83 | ## Running 84 | 85 | Let's check our script. Run example as: 86 | 87 | ``` 88 | $ python3 examples/async/get.py 127.0.0.1 public 1.3.6.1.2.1.1.6.0 89 | Gufo SNMP Test 90 | ``` -------------------------------------------------------------------------------- /docs/dev/testing.md: -------------------------------------------------------------------------------- 1 | # Building and Testing 2 | 3 | Before starting building and testing package set up 4 | [Developer's Environment](environment.md) first. 5 | From here and below we consider the shell's current 6 | directory matches the project's root directory. 7 | 8 | ## Building Package 9 | 10 | To test the package build run: 11 | 12 | ``` shell 13 | python -m build --sdist --wheel 14 | ``` 15 | 16 | Compiled packages will be available in the `dist/` directory. 17 | 18 | ## Running tests 19 | 20 | Rebuild rust modules, if changed: 21 | 22 | ``` shell 23 | python -m pip install --editable . 24 | ``` 25 | 26 | To run the test suit: 27 | 28 | ``` shell 29 | pytest -vv 30 | ``` 31 | 32 | ## Running Lints 33 | 34 | All lints are checked as part of GitHub Actions Workflow. You may run lints 35 | manually before committing to the project. 36 | 37 | ### Check Formatting 38 | 39 | [Python Code Formatting](codequality.md#python-code-formatting) is the mandatory 40 | requirement in our [Code Quality](codequality.md) standards. To check code 41 | formatting run: 42 | 43 | ``` shell 44 | ruff format --check examples/ src/ tests/ 45 | ``` 46 | 47 | To fix formatting errors run: 48 | ``` shell 49 | ruff format examples/ src/ tests/ 50 | ``` 51 | 52 | We recommend setting python code formatting on file saving 53 | (Done in [VS Code Dev Container](environment.md#visual-studio-code-dev-container) 54 | out of the box). 55 | 56 | ### Python Code Lints 57 | 58 | [Python Code Linting](codequality.md#python-code-linting) is the mandatory 59 | requirement in our [Code Quality](codequality.md) standards. To check code 60 | for linting errors run: 61 | 62 | ``` shell 63 | ruff src/ tests/ 64 | ``` 65 | 66 | ### Python Code Static Checks 67 | 68 | [Python Code Static Checks](codequality.md#python-code-static-checks) is the mandatory 69 | requirement in our [Code Quality](codequality.md) standards. To check code 70 | for typing errors run: 71 | 72 | ``` shell 73 | mypy src/ 74 | ``` 75 | 76 | ## Python Test Code Coverage Check 77 | 78 | To evaluate code coverage run tests: 79 | 80 | ``` shell 81 | coverage run -m pytest -vv 82 | ``` 83 | 84 | To report the coverage after the test run: 85 | 86 | ``` shell 87 | coverage report 88 | ``` 89 | 90 | To show line-by-line coverage: 91 | 92 | ``` 93 | coverage html 94 | ``` 95 | 96 | Then open `dist/coverage/index.html` file in your browser. 97 | 98 | ## Building Documentation 99 | 100 | To rebuild and check documentation run 101 | 102 | ``` shell 103 | mkdocs serve 104 | ``` 105 | 106 | We recommend using [Grammarly][Grammarly] service to check 107 | documentation for common errors. 108 | 109 | ## Benchmarks 110 | 111 | First, edit `Cargo.toml`, comment line in the section `[lib]`: 112 | 113 | ``` 114 | crate-type = ["cdylib"] # Comment for bench 115 | ``` 116 | 117 | and uncomment 118 | 119 | ``` toml 120 | # crate-type = ["cdylib", "rlib"] # Uncomment for bench 121 | ``` 122 | 123 | Then run bencmarks: 124 | 125 | ``` shell 126 | cargo bench 127 | ``` 128 | 129 | Revert `Cargo.toml` when you completed. 130 | 131 | [Grammarly]: https://grammarly.com/ -------------------------------------------------------------------------------- /docs/examples/sync/debugging.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP Example: Debugging 2 | 3 | In our previous examples we have relied on existing 4 | and running SNMP agent. But Gufo SNMP offers the useful 5 | `Snmpd` wrapper to configure and run the local instance 6 | of the `snmpd` which can be started and terminated 7 | along your application. 8 | 9 | !!! note 10 | This feature in requires an installed Net-SNMP package. 11 | Refer to your operation system's manuals for details. 12 | 13 | ``` py title="debugging.py" linenums="1" 14 | --8<-- "examples/sync/debugging.py" 15 | ``` 16 | 17 | Let's see the details. 18 | 19 | ``` py title="debugging.py" linenums="1" hl_lines="1" 20 | --8<-- "examples/sync/debugging.py" 21 | ``` 22 | 23 | `Snmpd` wrapper should be imported from `gufo.snmp.snmpd` directly. 24 | 25 | ``` py title="debugging.py" linenums="1" hl_lines="2" 26 | --8<-- "examples/sync/debugging.py" 27 | ``` 28 | 29 | `SnmpSession` object holds all necessary API. We're using a synchronous 30 | version from `gufo.snmp.sync_client`. 31 | 32 | 33 | ``` py title="debugging.py" linenums="1" hl_lines="5" 34 | --8<-- "examples/sync/debugging.py" 35 | ``` 36 | 37 | We define our `main` function. Unlike our [get](get.md), [getmany](getmany.md), 38 | and [getnext](getnext.md) examples we do not expect any external arguments. 39 | 40 | ``` py title="debugging.py" linenums="1" hl_lines="6" 41 | --8<-- "examples/sync/debugging.py" 42 | ``` 43 | 44 | We need to create `Snmpd` context to run local snmpd instance. 45 | Then we need to create `SnmpSession` object which wraps the client's session. 46 | We using context managers using `with` clause. Refer to the [get](get.md), [getmany](getmany.md), 47 | and [getnext](getnext.md) examples for additional details. 48 | 49 | Both `Snmpd` and `SnmpSession` are highly configurable, so refer to the 50 | [Snmpd][gufo.snmp.snmpd.Snmpd] and 51 | [SnmpSession][gufo.snmp.sync_client.SnmpSession] 52 | references. 53 | 54 | ``` py title="debugging.py" linenums="1" hl_lines="7" 55 | --8<-- "examples/sync/debugging.py" 56 | ``` 57 | 58 | We use `SnmpSession.getnext()` function to iterate within base OID. The function is an 59 | iterator yielding pairs of `(OID, value)`, so we use `for` construction to iterate over the values. 60 | See [SnmpSession.getnext() reference][gufo.snmp.sync_client.SnmpSession.getnext] 61 | for further details. 62 | 63 | ``` py title="debugging.py" linenums="1" hl_lines="8" 64 | --8<-- "examples/sync/debugging.py" 65 | ``` 66 | 67 | It is up to the application how to deal with the result. 68 | In our example we just print it. 69 | 70 | ``` py title="debugging.py" linenums="1" hl_lines="11" 71 | --8<-- "examples/sync/debugging.py" 72 | ``` 73 | 74 | Lets run our `main()` function. 75 | 76 | ## Running 77 | 78 | Let's check our script. Run example as: 79 | 80 | ``` 81 | $ python3 examples/sync/debugging.py 82 | 1.3.6.1.2.1.1.1.0: b'Linux d280d3a0a307 5.15.49-linuxkit #1 SMP Tue Sep 13 07:51:46 UTC 2022 x86_64' 83 | 1.3.6.1.2.1.1.2.0: 1.3.6.1.4.1.8072.3.2.10 84 | 1.3.6.1.2.1.1.3.0: 36567296 85 | 1.3.6.1.2.1.1.4.0: b'test ' 86 | 1.3.6.1.2.1.1.5.0: b'd280d3a0a307' 87 | 1.3.6.1.2.1.1.6.0: b'Gufo SNMP Test' 88 | 1.3.6.1.2.1.1.7.0: 72 89 | ... 90 | ``` 91 | -------------------------------------------------------------------------------- /src/snmp/op/getbulk.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: GetBulk operation 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023-25, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use super::{GetIter, PyOp}; 9 | use crate::ber::SnmpOid; 10 | use crate::error::SnmpError; 11 | use crate::snmp::getbulk::SnmpGetBulk; 12 | use crate::snmp::msg::SnmpPdu; 13 | use crate::snmp::value::SnmpValue; 14 | use pyo3::{ 15 | exceptions::{PyRuntimeError, PyStopAsyncIteration, PyValueError}, 16 | prelude::*, 17 | types::{PyList, PyTuple}, 18 | }; 19 | 20 | pub struct OpGetBulk; 21 | 22 | impl<'a> PyOp<'a, (SnmpOid<'a>, i64)> for OpGetBulk { 23 | // obj is iterable[str] 24 | fn from_python(obj: (SnmpOid<'a>, i64), request_id: i64) -> PyResult> { 25 | let (oid, max_repetitions) = obj; 26 | Ok(SnmpPdu::GetBulkRequest(SnmpGetBulk { 27 | request_id, 28 | non_repeaters: 0, 29 | max_repetitions, 30 | vars: vec![oid], 31 | })) 32 | } 33 | fn to_python<'py>( 34 | pdu: &SnmpPdu, 35 | iter: Option<&mut GetIter>, 36 | py: Python<'py>, 37 | ) -> PyResult> { 38 | let b_iter = iter.ok_or_else(|| PyValueError::new_err("GetIter expected"))?; 39 | match pdu { 40 | SnmpPdu::GetResponse(resp) => { 41 | // Check varbinds size 42 | if resp.vars.is_empty() { 43 | return Err(PyStopAsyncIteration::new_err("stop")); 44 | } 45 | let list = PyList::empty(py); 46 | for var in resp.vars.iter() { 47 | match &var.value { 48 | SnmpValue::Null 49 | | SnmpValue::NoSuchObject 50 | | SnmpValue::NoSuchInstance 51 | | SnmpValue::EndOfMibView => continue, 52 | _ => { 53 | // Check if we can continue 54 | if !b_iter.set_next_oid(&var.oid) { 55 | let _ = list.append(py.None()); 56 | break; 57 | } 58 | // Append to list 59 | list.append(PyTuple::new( 60 | py, 61 | vec![var.oid.into_pyobject(py)?, var.value.into_pyobject(py)?], 62 | )?) 63 | .map_err(|e| PyRuntimeError::new_err(e.to_string()))? 64 | } 65 | } 66 | } 67 | if list.is_empty() { 68 | return Err(PyStopAsyncIteration::new_err("stop")); 69 | } 70 | Ok(list.as_any().to_owned()) 71 | } 72 | SnmpPdu::Report(_) => Err(SnmpError::AuthenticationFailed.into()), 73 | _ => Err(SnmpError::InvalidPdu.into()), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /docs/man/gufo-snmp.md: -------------------------------------------------------------------------------- 1 | # gufo-snmp - SNMP Client Utility 2 | 3 | `gufo-snmp` is a swiss-army knife for SNMP request which close resembles 4 | Net-SNMP family of CLI tools. 5 | 6 | ## Usage 7 | 8 | ``` 9 | usage: gufo-snmp [-h] [--version {v1,v2c,v3}] [-v1 | -v2c | -v3] [--command {GET,GETNEXT,GETBULK}] [-p PORT] 10 | [-c COMMUNITY] [-u USER] [-a {MD5,SHA}] [-A AUTH_PASS] [-x {DES,AES}] [-X SECURITY_PASS] [-O OFLAGS] 11 | address ... 12 | 13 | SNMP Client 14 | 15 | positional arguments: 16 | address Agent 17 | oids OIDs 18 | 19 | options: 20 | -h, --help show this help message and exit 21 | --version {v1,v2c,v3} 22 | SNMP Protocol version 23 | -v1 SNMP v1 24 | -v2c SNMP v2c 25 | -v3 SNMP v3 26 | --command {GET,GETNEXT,GETBULK} 27 | Command 28 | -p, --port PORT Argent port 29 | -c, --community COMMUNITY 30 | Community (v1/v2c) 31 | -u, --user USER User name (v3) 32 | -a, --auth-protocol {MD5,SHA} 33 | Set authentication protocol (v3) 34 | -A, --auth-pass AUTH_PASS 35 | Set authentication protocol pass-phrase (v3) 36 | -x, --security-protocol {DES,AES} 37 | Set security protocol (v3) 38 | -X, --security-pass SECURITY_PASS 39 | Set security protocol pass-phrase (v3) 40 | -O OFLAGS Output formatting flags (may be repeated or combined) 41 | Supported flags: 42 | a : print all strings in ascii format 43 | x : print all strings in hex format 44 | q : quick print for easier parsing 45 | Q : quick print with equal-signs 46 | T : print human-readable text along with hex strings 47 | v : print values only (not OID = value) 48 | ``` 49 | 50 | ## Output Formats 51 | 52 | ### ASCII (-Oa) 53 | 54 | Print strings as text, replacing non-printable characters with dots. 55 | 56 | *Example output*: 57 | ``` 58 | 1.3.6.1.2.1.1.6.0 = Gufo SNMP Test 59 | ``` 60 | 61 | ### HEX (-Ox) 62 | 63 | Print strings in hexadecimal format. 64 | 65 | *Example output*: 66 | ``` 67 | 1.3.6.1.2.1.1.6.0 = 47 75 66 6F 20 53 4E 4D 50 20 54 65 73 74 68 | ``` 69 | 70 | ### ASCII + HEX (-OT) 71 | 72 | Print string in human-readable ASCII along with hexadecimal format. 73 | 74 | *Example output*: 75 | ``` 76 | 1.3.6.1.2.1.1.6.0 = Gufo SNMP Test 47 75 66 6F 20 53 4E 4D 50 20 54 65 73 74 77 | ``` 78 | 79 | ### Without Separator (-Qq) 80 | 81 | Do not print `=` between oid and value. 82 | 83 | *Example output*: 84 | ``` 85 | 1.3.6.1.2.1.1.6.0 Gufo SNMP Test 86 | ``` 87 | 88 | ### With Separator (-QO) 89 | 90 | Print `=` between oid and value. 91 | 92 | *Example output*: 93 | ``` 94 | 1.3.6.1.2.1.1.6.0 = Gufo SNMP Test 95 | ``` 96 | 97 | ### Value Only (-Ov) 98 | 99 | Do not print oid. 100 | 101 | *Example output*: 102 | ``` 103 | Gufo SNMP Test 104 | ``` 105 | -------------------------------------------------------------------------------- /benchmarks/test_v3_getnext.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Gufo SNMP: SNMP v3 Getnext benchmarks 3 | # --------------------------------------------------------------------- 4 | # Copyright (C) 2023-25, Gufo Labs 5 | # See LICENSE.md for details 6 | # --------------------------------------------------------------------- 7 | 8 | # Python modules 9 | import asyncio 10 | 11 | # Third-party modules 12 | # from pysnmp.hlapi import v3arch 13 | # Gufo SNMP modules 14 | from gufo.snmp import SnmpVersion 15 | from gufo.snmp.async_client import SnmpSession as AsyncSnmpSession 16 | from gufo.snmp.snmpd import Snmpd 17 | from gufo.snmp.sync_client import SnmpSession as SyncSnmpSession 18 | from gufo.snmp.user import Aes128Key, KeyType, Sha1Key, User 19 | 20 | BASE_OID = "1.3.6" 21 | SNMP_USER = User( 22 | name="user22", 23 | auth_key=Sha1Key(b"user22key", key_type=KeyType.Master), 24 | priv_key=Aes128Key(b"USER22KEY", key_type=KeyType.Master), 25 | ) 26 | 27 | 28 | def test_gufo_snmp_sync(snmpd: Snmpd, benchmark) -> None: 29 | @benchmark 30 | def bench(): 31 | with SyncSnmpSession( 32 | addr=snmpd.address, 33 | port=snmpd.port, 34 | version=SnmpVersion.v3, 35 | user=SNMP_USER, 36 | ) as session: 37 | for _k, _v in session.getnext(BASE_OID): 38 | pass 39 | 40 | 41 | def test_gufo_snmp_async(snmpd: Snmpd, benchmark) -> None: 42 | async def inner(): 43 | async with AsyncSnmpSession( 44 | addr=snmpd.address, 45 | port=snmpd.port, 46 | version=SnmpVersion.v3, 47 | user=SNMP_USER, 48 | ) as session: 49 | async for _k, _v in session.getnext(BASE_OID): 50 | pass 51 | 52 | @benchmark 53 | def bench(): 54 | asyncio.run(inner()) 55 | 56 | 57 | def test_pysnmp_async(snmpd: Snmpd, benchmark) -> None: 58 | from pysnmp.hlapi.v3arch.asyncio import ( 59 | USM_AUTH_HMAC96_SHA, 60 | USM_KEY_TYPE_MASTER, 61 | USM_PRIV_CFB128_AES, 62 | ContextData, 63 | ObjectIdentity, 64 | ObjectType, 65 | SnmpEngine, 66 | UdpTransportTarget, 67 | UsmUserData, 68 | walk_cmd, 69 | ) 70 | 71 | user_name = SNMP_USER.name 72 | privacy_key = SNMP_USER.priv_key.key.decode() 73 | auth_key = SNMP_USER.auth_key.key.decode() 74 | 75 | async def inner() -> None: 76 | async for x, y, z, var_binds in walk_cmd( 77 | SnmpEngine(), 78 | UsmUserData( 79 | user_name, 80 | authProtocol=USM_AUTH_HMAC96_SHA, 81 | authKey=auth_key, 82 | authKeyType=USM_KEY_TYPE_MASTER, 83 | privProtocol=USM_PRIV_CFB128_AES, 84 | privKey=privacy_key, 85 | privKeyType=USM_KEY_TYPE_MASTER, 86 | ), 87 | await UdpTransportTarget.create((snmpd.address, snmpd.port)), 88 | ContextData(), 89 | ObjectType(ObjectIdentity(BASE_OID)), 90 | ): 91 | for i in var_binds: 92 | i.prettyPrint() 93 | 94 | @benchmark 95 | def bench(): 96 | asyncio.run(inner()) 97 | -------------------------------------------------------------------------------- /src/snmp/get.rs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------ 2 | // Gufo SNMP: GET PDU Parser 3 | // ------------------------------------------------------------------------ 4 | // Copyright (C) 2023, Gufo Labs 5 | // See LICENSE.md for details 6 | // ------------------------------------------------------------------------ 7 | 8 | use crate::ber::{BerDecoder, BerEncoder, SnmpInt, SnmpNull, SnmpOid, SnmpSequence}; 9 | use crate::buf::Buffer; 10 | use crate::error::{SnmpError, SnmpResult}; 11 | use nom::IResult; 12 | 13 | const DOUBLE_ZEROES: [u8; 6] = [2u8, 1, 0, 2, 1, 0]; 14 | 15 | pub struct SnmpGet<'a> { 16 | pub request_id: i64, 17 | pub vars: Vec>, 18 | } 19 | 20 | impl<'a> TryFrom<&'a [u8]> for SnmpGet<'a> { 21 | type Error = SnmpError; 22 | 23 | fn try_from(value: &'a [u8]) -> Result { 24 | // Request id 25 | let (tail, request_id) = SnmpInt::from_ber(value)?; 26 | // error status, must be 0 27 | let (tail, error_status) = SnmpInt::from_ber(tail)?; 28 | if !error_status.is_zero() { 29 | return Err(SnmpError::InvalidPdu); 30 | } 31 | // error index, must be 0 32 | let (tail, error_index) = SnmpInt::from_ber(tail)?; 33 | if !error_index.is_zero() { 34 | return Err(SnmpError::InvalidPdu); 35 | } 36 | // varbinds 37 | let (tail, vb) = SnmpSequence::from_ber(tail)?; 38 | if !tail.is_empty() { 39 | return Err(SnmpError::TrailingData); 40 | } 41 | let mut v_tail = vb.0; 42 | let mut vars = Vec::::new(); 43 | while !v_tail.is_empty() { 44 | let (rest, oid) = SnmpGet::parse_var(v_tail)?; 45 | vars.push(oid); 46 | v_tail = rest; 47 | } 48 | Ok(SnmpGet { 49 | request_id: request_id.into(), 50 | vars, 51 | }) 52 | } 53 | } 54 | 55 | impl BerEncoder for SnmpGet<'_> { 56 | fn push_ber(&self, buf: &mut Buffer) -> SnmpResult<()> { 57 | // Push all vars in the reversed order 58 | let rest = buf.len(); 59 | let null = SnmpNull {}; 60 | for oid in self.vars.iter().rev() { 61 | let start = buf.len(); 62 | // Trailing null 63 | null.push_ber(buf)?; 64 | // OID 65 | oid.push_ber(buf)?; 66 | // Enclosing sequence 67 | buf.push_tag_len(0x30, buf.len() - start)?; 68 | } 69 | // Enclosing sequence for varbinds 70 | // Spans for the end 71 | buf.push_tag_len(0x30, buf.len() - rest)?; 72 | // Error index + error status, both zeroes 73 | buf.push(&DOUBLE_ZEROES)?; 74 | // Request id 75 | let r_id: SnmpInt = self.request_id.into(); 76 | r_id.push_ber(buf)?; 77 | Ok(()) 78 | } 79 | } 80 | 81 | impl SnmpGet<'_> { 82 | fn parse_var(i: &[u8]) -> IResult<&[u8], SnmpOid<'_>, SnmpError> { 83 | // Parse enclosing sequence 84 | let (rest, vs) = SnmpSequence::from_ber(i)?; 85 | // Parse oid 86 | let (tail, oid) = SnmpOid::from_ber(vs.0)?; 87 | // Parse null 88 | let (_, _) = SnmpNull::from_ber(tail)?; 89 | Ok((rest, oid)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docs/examples/sync/getnext.md: -------------------------------------------------------------------------------- 1 | # Gufo SNMP Example: GetNext Request 2 | 3 | We have mastered the requesting of single or multiple keys 4 | in our [get](get.md) and [getmany](getmany.md) examples. 5 | The SNMP also defines the way of retrieving all keys under 6 | the given OID - namely the GetNext request. 7 | 8 | ``` py title="getnext.py" linenums="1" 9 | --8<-- "examples/sync/getnext.py" 10 | ``` 11 | 12 | Let's see the details. 13 | 14 | ``` py title="getnext.py" linenums="1" hl_lines="1" 15 | --8<-- "examples/sync/getnext.py" 16 | ``` 17 | Import `sys` module to parse the CLI argument. 18 | 19 | !!! warning 20 | 21 | We use `sys.argv` only for demonstration purposes. Use `argsparse` or alternatives 22 | in real-world applications. 23 | 24 | ``` py title="getnext.py" linenums="1" hl_lines="3" 25 | --8<-- "examples/sync/getnext.py" 26 | ``` 27 | 28 | `SnmpSession` object holds all necessary API. We're using a synchronous 29 | version from `gufo.snmp.sync_client`. 30 | 31 | ``` py title="getnext.py" linenums="1" hl_lines="6" 32 | --8<-- "examples/sync/getnext.py" 33 | ``` 34 | 35 | We define our main function and expect the following arguments: 36 | 37 | * Address of the agent. 38 | * SNMP community to authorize. 39 | * Base OID to query. 40 | 41 | ``` py title="getnext.py" linenums="1" hl_lines="7" 42 | --8<-- "examples/sync/getnext.py" 43 | ``` 44 | 45 | First, we need to create `SnmpSession` object which wraps the client's session. 46 | The `SnmpSession` may be used as an instance directly or operated as context manager 47 | using the `with` clause. When used as a context manager, 48 | the client automatically closes all connections on the exit of context, 49 | so its lifetime is defined explicitly. 50 | 51 | `SnmpSession` constructor offers lots of configuration variables for fine-tuning. Refer to the 52 | [SnmpSession reference][gufo.snmp.sync_client.SnmpSession] 53 | for further details. In our example, we set the agent's address and SNMP community 54 | to the given values. 55 | 56 | ``` py title="getnext.py" linenums="1" hl_lines="8" 57 | --8<-- "examples/sync/getnext.py" 58 | ``` 59 | 60 | We use `SnmpSession.getnext()` function to iterate within base OID. The function is an 61 | iterator yielding pairs of `(OID, value)`, so we use `for` construction to iterate over the values. 62 | See [SnmpSession.getnext() reference][gufo.snmp.sync_client.SnmpSession.getnext] 63 | for further details. 64 | 65 | ``` py title="getnext.py" linenums="1" hl_lines="9" 66 | --8<-- "examples/sync/getnext.py" 67 | ``` 68 | 69 | It is up to the application how to deal with the result. 70 | In our example we just print it. 71 | 72 | ``` py title="getnext.py" linenums="1" hl_lines="12" 73 | --8<-- "examples/sync/getnext.py" 74 | ``` 75 | 76 | Lets run our `main()` function pass first command-line parameters as address, community, and oid. 77 | 78 | ## Running 79 | 80 | Let's check our script. Run example as: 81 | 82 | ``` 83 | $ python3 examples/sync/getnext.py 127.0.0.1 public 1.3.6.1.2.1.1 84 | 1.3.6.1.2.1.1.1.0: b'Linux d280d3a0a307 5.15.49-linuxkit #1 SMP Tue Sep 13 07:51:46 UTC 2022 x86_64' 85 | 1.3.6.1.2.1.1.2.0: 1.3.6.1.4.1.8072.3.2.10 86 | 1.3.6.1.2.1.1.3.0: 36567296 87 | 1.3.6.1.2.1.1.4.0: b'test ' 88 | 1.3.6.1.2.1.1.5.0: b'd280d3a0a307' 89 | 1.3.6.1.2.1.1.6.0: b'Gufo SNMP Test' 90 | 1.3.6.1.2.1.1.7.0: 72 91 | ... 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/dev/types.md: -------------------------------------------------------------------------------- 1 | # Supported BER Types 2 | 3 | *Gufo SNMP* implements minimalistic [X.690 BER][BER] encoder/decoder. It focuses only on types 4 | and convenctions really used in SNMP protocol. 5 | 6 | The currently supported types are: 7 | 8 | | Type | Class | P/C[^1] | Tag | Python Type | Reference | 9 | | ----------------- | ----------- | ------: | ---: | -------------------- | ------------------------------ | 10 | | BOOLEAN | Universal | P | 1 | bool | [X.690][X-690] pp 8.1 | 11 | | INTEGER | Universal | P | 2 | int | [X.690][X-690] pp 8.2 | 12 | | BITSTRING | Universal | P/C | 3 | :material-close: | [X.690][X-690] pp 8.6 | 13 | | OCTETSTRING | Universal | P | 4 | bytes | [X.690][X-690] pp 8.7 | 14 | | NULL | Universal | P | 5 | :material-close: | [X.690][X-690] pp 8.8 | 15 | | OBJECT IDENTIFIER | Universal | P | 6 | str | [X.690][X-690] pp 8.19 | 16 | | OBJECT DESCRIPTOR | Universal | P/C | 7 | bytes | | 17 | | EXTERNAL | Universal | P | 8 | :material-close: | [X.690][X-690] pp 8.18 | 18 | | REAL | Universal | P | 9 | float | [X.690][X-690] pp 8.5 | 19 | | ENUMERATED | Universal | P | 10 | :material-close: | | 20 | | RELATIVE OID | Universal | P | 13 | str | [X.690][X-690] pp 8.20 | 21 | | SEQUENCE | Universal | C | 16 | :material-check:[^2] | [X.690][X-690] pp 8.9 | 22 | | IpAddress | Application | P | 0 | str | [RFC-1442][RFC-1442] pp 7.1.5 | 23 | | Counter32 | Application | P | 1 | int | [RFC-1442][RFC-1442] pp 7.1.6 | 24 | | Gauge32 | Application | P | 2 | int | [RFC-1442][RFC-1442] pp 7.1.7 | 25 | | TimeTicks | Application | P | 3 | int | [RFC-1442][RFC-1442] pp 7.1.8 | 26 | | Opaque | Application | P | 4 | bytes | [RFC-1442][RFC-1442] pp 7.1.9 | 27 | | NsapAddress | Application | P | 5 | :material-close: | [RFC-1442][RFC-1442] pp 7.1.10 | 28 | | Counter64 | Application | P | 6 | int | [RFC-1442][RFC-1442] pp 7.1.11 | 29 | | UInteger32 | Application | P | 7 | int | [RFC-1442][RFC-1442] pp 7.1.12 | 30 | | noSuchObject | Context | P | 0 | :material-check:[^3] | [RFC-1905][RFC-1905] pp 3 | 31 | | noSuchInstance | Context | P | 1 | :material-check:[^3] | [RFC-1905][RFC-1905] pp 3 | 32 | | endOfMibView | Context | P | 2 | :material-check:[^2] | [RFC-1905][RFC-1905] pp 3 | 33 | 34 | [^1]: Primitive/Constructed 35 | [^2]: Handled internally, never exposed 36 | [^3]: Handled internally, raises NoSuchInstance or ignored. 37 | [X-690]: https://www.itu.int/rec/T-REC-X.690 38 | [BER]: https://en.wikipedia.org/wiki/X.690#BER_encoding 39 | [RFC-1442]: https://www.rfc-editor.org/rfc/rfc1442.html 40 | [RFC-1905]: https://www.rfc-editor.org/rfc/rfc1905.html --------------------------------------------------------------------------------