firewalla-snmp-proxy 2.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firewalla_snmp_proxy/__init__.py +8 -0
- firewalla_snmp_proxy/__main__.py +6 -0
- firewalla_snmp_proxy/agent.py +184 -0
- firewalla_snmp_proxy/cli.py +780 -0
- firewalla_snmp_proxy/config.py +340 -0
- firewalla_snmp_proxy/counters.py +174 -0
- firewalla_snmp_proxy/mibs/__init__.py +179 -0
- firewalla_snmp_proxy/mibs/bridge.py +73 -0
- firewalla_snmp_proxy/mibs/entity.py +100 -0
- firewalla_snmp_proxy/mibs/ifmib.py +153 -0
- firewalla_snmp_proxy/mibs/lldp.py +105 -0
- firewalla_snmp_proxy/mibs/poe.py +89 -0
- firewalla_snmp_proxy/mibs/sensor.py +49 -0
- firewalla_snmp_proxy/mibs/system.py +58 -0
- firewalla_snmp_proxy/mibs/vendor.py +183 -0
- firewalla_snmp_proxy/model.py +373 -0
- firewalla_snmp_proxy/msp_api.py +272 -0
- firewalla_snmp_proxy/oid.py +93 -0
- firewalla_snmp_proxy/poller.py +284 -0
- firewalla_snmp_proxy/ramp.py +134 -0
- firewalla_snmp_proxy/reachability.py +221 -0
- firewalla_snmp_proxy/snapshot.py +115 -0
- firewalla_snmp_proxy/tree_builder.py +28 -0
- firewalla_snmp_proxy-2.2.1.dist-info/METADATA +775 -0
- firewalla_snmp_proxy-2.2.1.dist-info/RECORD +30 -0
- firewalla_snmp_proxy-2.2.1.dist-info/WHEEL +5 -0
- firewalla_snmp_proxy-2.2.1.dist-info/entry_points.txt +2 -0
- firewalla_snmp_proxy-2.2.1.dist-info/licenses/LICENSE +21 -0
- firewalla_snmp_proxy-2.2.1.dist-info/top_level.txt +1 -0
- mibs/FIREWALLA-SNMP-PROXY-MIB.txt +576 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Firewalla Switch SNMP proxy.
|
|
2
|
+
|
|
3
|
+
Exposes Firewalla Switch (SE / fwsw-*) port data from the Firewalla MSP cloud
|
|
4
|
+
API as a standards-compliant SNMP agent, so any SNMP monitoring system can
|
|
5
|
+
poll a switch that has no SNMP agent of its own.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "2.2.1"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""pysnmp wiring: one SNMP agent per switch.
|
|
2
|
+
|
|
3
|
+
Design notes:
|
|
4
|
+
|
|
5
|
+
* **One UDP port per switch.** Multiplexing several switches onto one port via
|
|
6
|
+
distinct community strings works in some NMSes but breaks any that key a
|
|
7
|
+
device on ``IP:port``. A port each keeps every NMS happy.
|
|
8
|
+
* **All engines share one asyncio loop.** pysnmp registers its transport with
|
|
9
|
+
the running loop, so N agents cost N sockets, not N threads.
|
|
10
|
+
* **The tree is rebuilt only when its shape changes** (port count, SFP presence,
|
|
11
|
+
PoE capability). Values are live via callables, so an ordinary poll needs no
|
|
12
|
+
rebuild and an NMS never sees rows flicker mid-walk.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Optional, Sequence
|
|
19
|
+
|
|
20
|
+
from pysnmp.carrier.asyncio.dgram import udp
|
|
21
|
+
from pysnmp.entity import config, engine
|
|
22
|
+
from pysnmp.entity.rfc3413 import cmdrsp, context
|
|
23
|
+
from pysnmp.proto import rfc1902
|
|
24
|
+
from pysnmp.proto.api import v2c
|
|
25
|
+
from pysnmp.smi.instrum import AbstractMibInstrumController
|
|
26
|
+
|
|
27
|
+
from .mibs import SwitchContext
|
|
28
|
+
from .oid import OidTree
|
|
29
|
+
from .tree_builder import build_tree
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
#: Access-control view root. Note this is ``iso(1)``, not ``1.3.6.1``:
|
|
34
|
+
#: LLDP-MIB lives at ``1.0.8802``, outside the internet subtree. Scoping the
|
|
35
|
+
#: view to 1.3.6.1 -- the obvious choice -- silently hides the entire neighbour
|
|
36
|
+
#: table, and therefore the NMS topology link, with no error anywhere.
|
|
37
|
+
VIEW_ROOT = (1,)
|
|
38
|
+
|
|
39
|
+
#: SNMP security models served: v1 (1) and v2c (2).
|
|
40
|
+
SECURITY_MODELS = (1, 2)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TreeInstrumController(AbstractMibInstrumController):
|
|
44
|
+
"""Serves GET / GETNEXT / GETBULK from an :class:`OidTree`.
|
|
45
|
+
|
|
46
|
+
SET is refused: this is a monitoring proxy and the upstream API's mutating
|
|
47
|
+
endpoints must never be reachable over SNMP.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, tree: OidTree) -> None:
|
|
51
|
+
self.tree = tree
|
|
52
|
+
|
|
53
|
+
def swap_tree(self, tree: OidTree) -> None:
|
|
54
|
+
self.tree = tree
|
|
55
|
+
|
|
56
|
+
# -- GET -------------------------------------------------------------
|
|
57
|
+
def read_variables(self, *varBinds, **context):
|
|
58
|
+
result = []
|
|
59
|
+
for oid, _val in varBinds:
|
|
60
|
+
key = tuple(oid)
|
|
61
|
+
value = self.tree.get(key)
|
|
62
|
+
if value is not None:
|
|
63
|
+
result.append((oid, value))
|
|
64
|
+
continue
|
|
65
|
+
# Distinguish "wrong instance" from "wrong object", as a real agent
|
|
66
|
+
# does. Two ways this OID can still name a real object:
|
|
67
|
+
# - it is an interior node with instances beneath it
|
|
68
|
+
# (e.g. a GET on the ifOperStatus column itself), or
|
|
69
|
+
# - its parent column holds other instances, so the column is
|
|
70
|
+
# real and only this index is missing (e.g. ifOperStatus.99).
|
|
71
|
+
# Anything else genuinely is not implemented here.
|
|
72
|
+
if self.tree.has_descendants(key) or (
|
|
73
|
+
len(key) > 1 and self.tree.has_descendants(key[:-1])
|
|
74
|
+
):
|
|
75
|
+
result.append((oid, v2c.NoSuchInstance("")))
|
|
76
|
+
else:
|
|
77
|
+
result.append((oid, v2c.NoSuchObject("")))
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
# -- GETNEXT / GETBULK ----------------------------------------------
|
|
81
|
+
def read_next_variables(self, *varBinds, **context):
|
|
82
|
+
result = []
|
|
83
|
+
for oid, _val in varBinds:
|
|
84
|
+
nxt = self.tree.get_next(tuple(oid))
|
|
85
|
+
if nxt is None:
|
|
86
|
+
result.append((oid, v2c.EndOfMibView("")))
|
|
87
|
+
else:
|
|
88
|
+
next_oid, value = nxt
|
|
89
|
+
result.append((rfc1902.ObjectName(next_oid), value))
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
# -- SET -------------------------------------------------------------
|
|
93
|
+
def write_variables(self, *varBinds, **context):
|
|
94
|
+
from pysnmp.smi import error
|
|
95
|
+
|
|
96
|
+
raise error.NotWritableError(
|
|
97
|
+
name=varBinds[0][0] if varBinds else None,
|
|
98
|
+
idx=0,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SwitchAgent:
|
|
103
|
+
"""An SNMP agent serving one Firewalla switch on its own UDP port."""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
ctx: SwitchContext,
|
|
108
|
+
listen_host: str,
|
|
109
|
+
listen_port: int,
|
|
110
|
+
community: str = "public",
|
|
111
|
+
) -> None:
|
|
112
|
+
self.ctx = ctx
|
|
113
|
+
self.listen_host = listen_host
|
|
114
|
+
self.listen_port = listen_port
|
|
115
|
+
self.community = community
|
|
116
|
+
self._signature = ctx.port_signature()
|
|
117
|
+
self.tree = build_tree(ctx)
|
|
118
|
+
self._instrum = TreeInstrumController(self.tree)
|
|
119
|
+
self._engine: Optional[engine.SnmpEngine] = None
|
|
120
|
+
|
|
121
|
+
# -- lifecycle -------------------------------------------------------
|
|
122
|
+
def start(self) -> None:
|
|
123
|
+
"""Bind the socket and register command responders.
|
|
124
|
+
|
|
125
|
+
Must be called with an asyncio event loop running, since pysnmp's
|
|
126
|
+
asyncio transport attaches to the current loop.
|
|
127
|
+
"""
|
|
128
|
+
snmp_engine = engine.SnmpEngine()
|
|
129
|
+
transport = udp.UdpTransport().open_server_mode(
|
|
130
|
+
(self.listen_host, self.listen_port)
|
|
131
|
+
)
|
|
132
|
+
config.add_transport(snmp_engine, udp.DOMAIN_NAME, transport)
|
|
133
|
+
|
|
134
|
+
# Read-only v1 + v2c access under one community. writeSubTree is left
|
|
135
|
+
# empty, so SET is rejected by access control before it ever reaches the
|
|
136
|
+
# instrumentation controller.
|
|
137
|
+
config.add_v1_system(snmp_engine, "ro-area", self.community)
|
|
138
|
+
for model in SECURITY_MODELS:
|
|
139
|
+
config.add_vacm_user(
|
|
140
|
+
snmp_engine, model, "ro-area", "noAuthNoPriv", VIEW_ROOT
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
snmp_context = context.SnmpContext(snmp_engine)
|
|
144
|
+
snmp_context.unregister_context_name(v2c.OctetString(""))
|
|
145
|
+
snmp_context.register_context_name(v2c.OctetString(""), self._instrum)
|
|
146
|
+
|
|
147
|
+
cmdrsp.GetCommandResponder(snmp_engine, snmp_context)
|
|
148
|
+
cmdrsp.NextCommandResponder(snmp_engine, snmp_context)
|
|
149
|
+
cmdrsp.BulkCommandResponder(snmp_engine, snmp_context)
|
|
150
|
+
# Registered deliberately even though writes are denied: without a SET
|
|
151
|
+
# responder the agent simply does not reply, and the manager sees a
|
|
152
|
+
# timeout that looks like the agent is down. With it, a SET gets a
|
|
153
|
+
# clean notWritable/noAccess error. VACM (empty writeSubTree) rejects
|
|
154
|
+
# the request before it reaches the instrumentation controller, whose
|
|
155
|
+
# write_variables is a second line of defence.
|
|
156
|
+
cmdrsp.SetCommandResponder(snmp_engine, snmp_context)
|
|
157
|
+
|
|
158
|
+
self._engine = snmp_engine
|
|
159
|
+
log.info(
|
|
160
|
+
"SNMP agent for %s listening on %s:%d (%d objects)",
|
|
161
|
+
self.ctx.switch.name, self.listen_host, self.listen_port, len(self.tree),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def stop(self) -> None:
|
|
165
|
+
if self._engine is not None:
|
|
166
|
+
try:
|
|
167
|
+
self._engine.close_dispatcher()
|
|
168
|
+
except Exception as exc: # pragma: no cover - shutdown best effort
|
|
169
|
+
log.debug("error closing dispatcher: %s", exc)
|
|
170
|
+
self._engine = None
|
|
171
|
+
|
|
172
|
+
# -- refresh ---------------------------------------------------------
|
|
173
|
+
def refresh(self) -> bool:
|
|
174
|
+
"""Rebuild the tree if the port set changed. Returns True if rebuilt."""
|
|
175
|
+
signature = self.ctx.port_signature()
|
|
176
|
+
if signature == self._signature:
|
|
177
|
+
return False
|
|
178
|
+
log.info(
|
|
179
|
+
"port layout changed on %s; rebuilding OID tree", self.ctx.switch.name
|
|
180
|
+
)
|
|
181
|
+
self._signature = signature
|
|
182
|
+
self.tree = build_tree(self.ctx)
|
|
183
|
+
self._instrum.swap_tree(self.tree)
|
|
184
|
+
return True
|