Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 11cfe35

Browse files
committed
Simplify SNMP driver
1 parent 640229b commit 11cfe35

3 files changed

Lines changed: 28 additions & 69 deletions

File tree

packages/jumpstarter-driver-snmp/jumpstarter_driver_snmp/client.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

packages/jumpstarter-driver-snmp/jumpstarter_driver_snmp/driver.py

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import asyncio
21
import socket
2+
from collections.abc import AsyncGenerator
33
from dataclasses import dataclass, field
44
from enum import Enum, IntEnum
5-
from typing import Any, Dict, Tuple
5+
from typing import Any, Dict
66

7+
from anyio import Event, fail_after
8+
from jumpstarter_driver_power.driver import PowerInterface, PowerReading
79
from pysnmp.carrier.asyncio.dgram import udp
810
from pysnmp.entity import config, engine
911
from pysnmp.entity.rfc3413 import cmdgen
@@ -36,7 +38,7 @@ class SNMPError(Exception):
3638

3739

3840
@dataclass(kw_only=True)
39-
class SNMPServer(Driver):
41+
class SNMPServer(PowerInterface, Driver):
4042
"""SNMP Power Control Driver"""
4143

4244
host: str = field()
@@ -117,7 +119,7 @@ def _setup_snmp(self):
117119
def client(cls) -> str:
118120
return "jumpstarter_driver_snmp.client.SNMPServerClient"
119121

120-
def _create_snmp_callback(self, result: Dict[str, Any], response_received: asyncio.Event):
122+
def _create_snmp_callback(self, result: Dict[str, Any], response_received: Event):
121123
def callback(snmpEngine, sendRequestHandle, errorIndication, errorStatus, errorIndex, varBinds, cbCtx):
122124
self.logger.debug(f"Callback {errorIndication} {errorStatus} {errorIndex} {varBinds}")
123125
if errorIndication:
@@ -138,29 +140,17 @@ def callback(snmpEngine, sendRequestHandle, errorIndication, errorStatus, errorI
138140

139141
return callback
140142

141-
def _setup_event_loop(self) -> Tuple[asyncio.AbstractEventLoop, bool]:
142-
try:
143-
loop = asyncio.get_running_loop()
144-
return loop, False
145-
except RuntimeError:
146-
loop = asyncio.new_event_loop()
147-
asyncio.set_event_loop(loop)
148-
return loop, True
149-
150-
async def _run_snmp_dispatcher(self, snmp_engine: engine.SnmpEngine, response_received: asyncio.Event):
143+
async def _run_snmp_dispatcher(self, snmp_engine: engine.SnmpEngine, response_received: Event):
151144
snmp_engine.open_dispatcher()
152145
await response_received.wait()
153146
snmp_engine.close_dispatcher()
154147

155-
def _snmp_set(self, state: PowerState):
148+
async def _snmp_set(self, state: PowerState):
156149
result = {"success": False, "error": None}
157-
response_received = asyncio.Event()
158-
loop = None
159-
created_loop = False
150+
response_received = Event()
160151

161152
try:
162153
self.logger.info(f"Sending power {state.name} command to {self.host}")
163-
loop, created_loop = self._setup_event_loop()
164154
snmp_engine = self._setup_snmp()
165155
callback = self._create_snmp_callback(result, response_received)
166156
cmdgen.SetCommandGenerator().send_varbinds(
@@ -172,10 +162,10 @@ def _snmp_set(self, state: PowerState):
172162
callback,
173163
)
174164

175-
dispatcher_task = loop.create_task(self._run_snmp_dispatcher(snmp_engine, response_received))
176165
try:
177-
loop.run_until_complete(asyncio.wait_for(dispatcher_task, self.timeout))
178-
except asyncio.TimeoutError:
166+
with fail_after(self.timeout):
167+
await self._run_snmp_dispatcher(snmp_engine, response_received)
168+
except TimeoutError:
179169
self.logger.warning(f"SNMP operation timed out after {self.timeout} seconds")
180170
result["error"] = "SNMP operation timed out"
181171

@@ -188,19 +178,20 @@ def _snmp_set(self, state: PowerState):
188178
error_msg = f"SNMP set failed: {str(e)}"
189179
self.logger.error(error_msg)
190180
raise SNMPError(error_msg) from e
191-
finally:
192-
if created_loop and loop:
193-
loop.close()
194181

195182
@export
196-
def on(self):
183+
async def on(self):
197184
"""Turn power on"""
198-
return self._snmp_set(PowerState.ON)
185+
return await self._snmp_set(PowerState.ON)
199186

200187
@export
201-
def off(self):
188+
async def off(self):
202189
"""Turn power off"""
203-
return self._snmp_set(PowerState.OFF)
190+
return await self._snmp_set(PowerState.OFF)
191+
192+
@export
193+
async def read(self) -> AsyncGenerator[PowerReading, None]:
194+
raise NotImplementedError
204195

205196
def close(self):
206197
"""No cleanup needed since engines are created per operation"""

packages/jumpstarter-driver-snmp/jumpstarter_driver_snmp/driver_test.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ def setup_mock_snmp_engine():
5555
},
5656
],
5757
)
58-
def test_snmp_auth_configurations(auth_config):
58+
@pytest.mark.anyio
59+
async def test_snmp_auth_configurations(auth_config):
5960
"""Test different SNMP authentication configurations"""
6061
with (
6162
patch("pysnmp.entity.config.add_v3_user") as mock_add_user,
@@ -103,15 +104,13 @@ def test_snmp_auth_configurations(auth_config):
103104

104105
@patch("pysnmp.entity.config.add_v3_user")
105106
@patch("pysnmp.entity.engine.SnmpEngine")
106-
def test_power_on_command(mock_engine, mock_add_user):
107+
@pytest.mark.anyio
108+
async def test_power_on_command(mock_engine, mock_add_user):
107109
"""Test power on command execution"""
108110
mock_engine.return_value = setup_mock_snmp_engine()
109111

110112
with (
111113
patch("pysnmp.entity.rfc3413.cmdgen.SetCommandGenerator.send_varbinds") as mock_send,
112-
patch("asyncio.get_running_loop", side_effect=RuntimeError),
113-
patch("asyncio.new_event_loop"),
114-
patch("asyncio.set_event_loop"),
115114
patch("pysnmp.entity.config.add_target_parameters"),
116115
patch("pysnmp.entity.config.add_target_address"),
117116
patch("pysnmp.entity.config.add_transport"),
@@ -124,22 +123,20 @@ def side_effect(*args):
124123

125124
mock_send.side_effect = side_effect
126125

127-
result = server.on()
126+
result = await server.on()
128127
assert "Power ON command sent successfully" in result
129128
mock_send.assert_called_once()
130129

131130

132131
@patch("pysnmp.entity.config.add_v3_user")
133132
@patch("pysnmp.entity.engine.SnmpEngine")
134-
def test_power_off_command(mock_engine, mock_add_user):
133+
@pytest.mark.anyio
134+
async def test_power_off_command(mock_engine, mock_add_user):
135135
"""Test power off command execution"""
136136
mock_engine.return_value = setup_mock_snmp_engine()
137137

138138
with (
139139
patch("pysnmp.entity.rfc3413.cmdgen.SetCommandGenerator.send_varbinds") as mock_send,
140-
patch("asyncio.get_running_loop", side_effect=RuntimeError),
141-
patch("asyncio.new_event_loop"),
142-
patch("asyncio.set_event_loop"),
143140
patch("pysnmp.entity.config.add_target_parameters"),
144141
patch("pysnmp.entity.config.add_target_address"),
145142
patch("pysnmp.entity.config.add_transport"),
@@ -152,6 +149,6 @@ def side_effect(*args):
152149

153150
mock_send.side_effect = side_effect
154151

155-
result = server.off()
152+
result = await server.off()
156153
assert "Power OFF command sent successfully" in result
157154
mock_send.assert_called_once()

0 commit comments

Comments
 (0)