33import logging
44from dataclasses import dataclass
55from logging import Logger
6+ from threading import Thread
67from typing import Any , Literal , override
78
9+ import aiohttp
810import wakeonlan
911from redfish .rest .v1 import HttpClient , redfish_client
1012
13+ from wolsocketproxy .common import URL_WATCHDOG_FEED
1114from wolsocketproxy .monitor import Monitor , MonitorConfig
1215from wolsocketproxy .utils import perform_ipmi_action
1316
@@ -29,6 +32,10 @@ class MachineConfig:
2932 online_check_http_expected_code : int = 200
3033 online_check_timeout : int = 60
3134
35+ keep_alive_mode : bool = False
36+ keep_alive_mode_base_url : str | None = None
37+ keep_alive_min_interval : int = 1
38+
3239
3340@dataclass
3441class ProxyRoute :
@@ -56,23 +63,67 @@ class ProxyConfig:
5663 ipmi_configs : list [IPMIConfig ] | None = None
5764
5865
66+ class TargetKeepAliveSender :
67+ _target_url : str
68+ _keep_alive_min_interval : int
69+ _loop : asyncio .AbstractEventLoop
70+ _queue : asyncio .Queue
71+
72+ def __init__ (self , target_base_url : str , keep_alive_min_interval : int ) -> None :
73+ self ._target_url = target_base_url .removesuffix ("/" ) + URL_WATCHDOG_FEED
74+ self ._keep_alive_min_interval = keep_alive_min_interval
75+
76+ self ._loop = asyncio .new_event_loop ()
77+ self ._queue = asyncio .Queue (1 )
78+
79+ def _loop () -> None :
80+ asyncio .set_event_loop (self ._loop )
81+ self ._loop .run_until_complete (self ._send_worker ())
82+
83+ Thread (target = _loop , daemon = True ).start ()
84+
85+ async def _send_worker (self ) -> None :
86+ while True :
87+ await self ._queue .get ()
88+
89+ async with aiohttp .request ("GET" , self ._target_url ) as resp :
90+ await resp .json ()
91+
92+ await asyncio .sleep (self ._keep_alive_min_interval )
93+
94+ def schedule_send (self ) -> None :
95+ def _no_exception_put () -> None :
96+ with contextlib .suppress (asyncio .QueueFull ):
97+ self ._queue .put_nowait (1 )
98+
99+ self ._loop .call_soon_threadsafe (_no_exception_put )
100+
101+
59102class ProxyUdpProtocol (asyncio .DatagramProtocol ):
60103 _proxy : "Proxy"
61104 _monitor : Monitor
62105 _transport : asyncio .transports .DatagramTransport
63106 _target_machine_name : str
64107 _target_address : str
65108 _target_port : int
109+ _target_keep_alive_sender : TargetKeepAliveSender | None = None
66110 _target_pair : tuple [str , int ]
67111
68112 def __init__ (
69- self , proxy : "Proxy" , monitor : Monitor , target_machine_name : str , target_address : str , target_port : int
113+ self ,
114+ proxy : "Proxy" ,
115+ monitor : Monitor ,
116+ target_machine_name : str ,
117+ target_address : str ,
118+ target_port : int ,
119+ target_keep_alive_sender : TargetKeepAliveSender | None = None ,
70120 ) -> None :
71121 self ._proxy = proxy
72122 self ._monitor = monitor
73123 self ._target_machine_name = target_machine_name
74124 self ._target_address = target_address
75125 self ._target_port = target_port
126+ self ._target_keep_alive_sender = target_keep_alive_sender
76127 self ._target_pair = (target_address , target_port )
77128
78129 @override
@@ -92,6 +143,9 @@ async def handle_datagram(self, data: bytes, addr: tuple[str | Any, int]) -> Non
92143
93144 self ._transport .sendto (data , self ._target_pair )
94145
146+ if self ._target_keep_alive_sender is not None :
147+ self ._target_keep_alive_sender .schedule_send ()
148+
95149
96150class Proxy :
97151 _log : Logger = logging .getLogger ()
@@ -180,26 +234,55 @@ def __create_route(self, route: ProxyRoute) -> None:
180234
181235 def __create_tcp_route (self , route : ProxyRoute ) -> None :
182236 assert route .target_machine_name is not None
237+ machine_config = self ._machines [route .target_machine_name ]
238+ target_keep_alive_sender = None
239+
240+ if machine_config .keep_alive_mode :
241+ assert machine_config .keep_alive_mode_base_url is not None
242+
243+ target_keep_alive_sender = TargetKeepAliveSender (
244+ machine_config .keep_alive_mode_base_url , machine_config .keep_alive_min_interval
245+ )
183246
184247 cr = asyncio .start_server (
185- self .__make_tcp_route_handler (route .target_machine_name , route .target_address , route .target_port ),
248+ self .__make_tcp_route_handler (
249+ route .target_machine_name ,
250+ route .target_address ,
251+ route .target_port ,
252+ target_keep_alive_sender ,
253+ ),
186254 route .local_address ,
187255 route .local_port ,
188256 )
189257
190258 loop = asyncio .get_event_loop ()
191259 loop .run_until_complete (cr )
192260
193- async def __pipe (self , target_address : str , reader : asyncio .StreamReader , writer : asyncio .StreamWriter ) -> None :
261+ async def __pipe (
262+ self ,
263+ target_address : str ,
264+ reader : asyncio .StreamReader ,
265+ writer : asyncio .StreamWriter ,
266+ target_keep_alive_sender : TargetKeepAliveSender | None = None ,
267+ ) -> None :
194268 try :
195269 while not reader .at_eof ():
196270 writer .write (await reader .read (2048 ))
271+
272+ if target_keep_alive_sender is not None :
273+ target_keep_alive_sender .schedule_send ()
197274 except ConnectionResetError :
198275 self ._log .warning ("Connection reset by target %s" , target_address )
199276 finally :
200277 writer .close ()
201278
202- def __make_tcp_route_handler (self , target_machine_name : str , target_address : str , target_port : int ) -> Any : # noqa: ANN401
279+ def __make_tcp_route_handler (
280+ self ,
281+ target_machine_name : str ,
282+ target_address : str ,
283+ target_port : int ,
284+ target_keep_alive_sender : TargetKeepAliveSender | None = None ,
285+ ) -> Any : # noqa: ANN401
203286 async def handler (local_reader : asyncio .StreamReader , local_writer : asyncio .StreamWriter ) -> None :
204287 if not self ._monitor .is_available (target_machine_name ):
205288 await self ._wake_up_target (target_machine_name )
@@ -211,8 +294,8 @@ async def handler(local_reader: asyncio.StreamReader, local_writer: asyncio.Stre
211294 self ._log .error ("Unable to open connection to %s:%d" , target_address , target_port )
212295 raise e
213296
214- send_pipe = self .__pipe (target_address , local_reader , target_writer )
215- recv_pipe = self .__pipe (target_address , target_reader , local_writer )
297+ send_pipe = self .__pipe (target_address , local_reader , target_writer , target_keep_alive_sender )
298+ recv_pipe = self .__pipe (target_address , target_reader , local_writer , target_keep_alive_sender )
216299 await asyncio .gather (send_pipe , recv_pipe )
217300
218301 return handler
@@ -221,10 +304,27 @@ def __create_udp_route(self, route: ProxyRoute) -> None:
221304 target_machine_name = route .target_machine_name
222305 assert target_machine_name is not None
223306
307+ machine_config = self ._machines [target_machine_name ]
308+ target_keep_alive_sender = None
309+
310+ if machine_config .keep_alive_mode :
311+ assert machine_config .keep_alive_mode_base_url is not None
312+
313+ target_keep_alive_sender = TargetKeepAliveSender (
314+ machine_config .keep_alive_mode_base_url , machine_config .keep_alive_min_interval
315+ )
316+
224317 loop = asyncio .get_event_loop ()
225318
226319 cr = loop .create_datagram_endpoint (
227- lambda : ProxyUdpProtocol (self , self ._monitor , target_machine_name , route .target_address , route .target_port ),
320+ lambda : ProxyUdpProtocol (
321+ self ,
322+ self ._monitor ,
323+ target_machine_name ,
324+ route .target_address ,
325+ route .target_port ,
326+ target_keep_alive_sender ,
327+ ),
228328 local_addr = (route .local_address , route .local_port ),
229329 )
230330
0 commit comments