22import socket
33from dataclasses import dataclass , field
44from enum import Enum , IntEnum
5+ from typing import Any , Dict , Tuple
56
67from pysnmp .carrier .asyncio .dgram import udp
78from pysnmp .entity import config , engine
@@ -116,9 +117,7 @@ def _setup_snmp(self):
116117 def client (cls ) -> str :
117118 return "jumpstarter_driver_snmp.client.SNMPServerClient"
118119
119- def _snmp_set (self , state : PowerState ):
120- result = {"success" : False , "error" : None }
121-
120+ def _create_snmp_callback (self , result : Dict [str , Any ], response_received : asyncio .Event ):
122121 def callback (snmpEngine , sendRequestHandle , errorIndication , errorStatus , errorIndex , varBinds , cbCtx ):
123122 self .logger .debug (f"Callback { errorIndication } { errorStatus } { errorIndex } { varBinds } " )
124123 if errorIndication :
@@ -135,20 +134,35 @@ def callback(snmpEngine, sendRequestHandle, errorIndication, errorStatus, errorI
135134 for oid , val in varBinds :
136135 self .logger .debug (f"{ oid .prettyPrint ()} = { val .prettyPrint ()} " )
137136 self .logger .debug (f"SNMP set result: { result } " )
137+ response_received .set ()
138138
139+ return callback
140+
141+ def _setup_event_loop (self ) -> Tuple [asyncio .AbstractEventLoop , bool ]:
139142 try :
140- self .logger .info (f"Sending power { state .name } command to { self .host } " )
141- created_loop = False
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 ):
151+ snmp_engine .open_dispatcher ()
152+ await response_received .wait ()
153+ snmp_engine .close_dispatcher ()
142154
143- try :
144- asyncio .get_running_loop ()
145- except RuntimeError :
146- loop = asyncio .new_event_loop ()
147- asyncio .set_event_loop (loop )
148- created_loop = True
155+ def _snmp_set (self , state : PowerState ):
156+ result = {"success" : False , "error" : None }
157+ response_received = asyncio .Event ()
158+ loop = None
159+ created_loop = False
149160
161+ try :
162+ self .logger .info (f"Sending power { state .name } command to { self .host } " )
163+ loop , created_loop = self ._setup_event_loop ()
150164 snmp_engine = self ._setup_snmp ()
151-
165+ callback = self . _create_snmp_callback ( result , response_received )
152166 cmdgen .SetCommandGenerator ().send_varbinds (
153167 snmp_engine ,
154168 "my-target" ,
@@ -158,11 +172,15 @@ def callback(snmpEngine, sendRequestHandle, errorIndication, errorStatus, errorI
158172 callback ,
159173 )
160174
161- snmp_engine .open_dispatcher (self .timeout )
162- snmp_engine .close_dispatcher ()
175+ dispatcher_task = loop .create_task (self ._run_snmp_dispatcher (snmp_engine , response_received ))
176+ try :
177+ loop .run_until_complete (asyncio .wait_for (dispatcher_task , self .timeout ))
178+ except asyncio .TimeoutError :
179+ self .logger .warning (f"SNMP operation timed out after { self .timeout } seconds" )
180+ result ["error" ] = "SNMP operation timed out"
163181
164182 if not result ["success" ]:
165- raise SNMPError (result ["error" ])
183+ raise SNMPError (result ["error" ] or "Unknown SNMP error" )
166184
167185 return f"Power { state .name } command sent successfully"
168186
@@ -171,7 +189,7 @@ def callback(snmpEngine, sendRequestHandle, errorIndication, errorStatus, errorI
171189 self .logger .error (error_msg )
172190 raise SNMPError (error_msg ) from e
173191 finally :
174- if created_loop :
192+ if created_loop and loop :
175193 loop .close ()
176194
177195 @export
0 commit comments