wolsocketproxy 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {wolsocketproxy-0.3.0/src/wolsocketproxy.egg-info → wolsocketproxy-0.4.0}/PKG-INFO +27 -2
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/README.md +25 -1
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/pyproject.toml +3 -1
- wolsocketproxy-0.4.0/src/wolsocketproxy/common.py +1 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy/keepalive.py +7 -8
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy/proxy.py +253 -7
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0/src/wolsocketproxy.egg-info}/PKG-INFO +27 -2
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/SOURCES.txt +1 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/requires.txt +1 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/LICENSE +0 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/setup.cfg +0 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy/__init__.py +0 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy/__main__.py +0 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy/monitor.py +0 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy/utils.py +0 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/dependency_links.txt +0 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/entry_points.txt +0 -0
- {wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wolsocketproxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A socket proxy with wake-on-lan feature.
|
|
5
5
|
Author-email: Song Fuchang <song.fc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Requires-Python: >=3.12
|
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: aiohttp==3.13.5
|
|
17
|
+
Requires-Dist: croniter==6.0.0
|
|
17
18
|
Requires-Dist: dataclass-wizard==0.39.1
|
|
18
19
|
Requires-Dist: icmplib==3.0.4
|
|
19
20
|
Requires-Dist: redfish==3.3.5
|
|
@@ -100,7 +101,16 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
100
101
|
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
|
|
101
102
|
"online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
|
|
102
103
|
"ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
|
|
103
|
-
"ipmi_max_reset_try_count": 3
|
|
104
|
+
"ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
|
|
105
|
+
"keep_alive_mode": true, // Indicates whether this machine has a keep-alive daemon, optional, default is false
|
|
106
|
+
// If you enable this, wolsocketproxy will send a request when there is any traffic
|
|
107
|
+
"keep_alive_mode_base_url": "http://192.168.1.124:8080", // Keep-alive daemon provided URL, optional
|
|
108
|
+
"scheduled_power_up_times": [ // Scheduled power-up times, optional
|
|
109
|
+
{
|
|
110
|
+
"cron": "0 7 * * 1-5", // Cron expression for auto power-up
|
|
111
|
+
"keep_alive_time": 7200 // Send keep-alive requests for N seconds after power-up
|
|
112
|
+
}
|
|
113
|
+
]
|
|
104
114
|
},
|
|
105
115
|
// ... more machines ...
|
|
106
116
|
},
|
|
@@ -162,3 +172,18 @@ The config file `wolsocketproxy.conf` should look like the following:
|
|
|
162
172
|
You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
|
|
163
173
|
or the special process will be killed.
|
|
164
174
|
You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
|
|
175
|
+
|
|
176
|
+
If you enable `keep_alive_mode` in proxy mode's config, the proxy will send requests to `/watchdog/feed` of this machine whenever there is any traffic towards this machine through it.
|
|
177
|
+
|
|
178
|
+
### Scheduled power-up
|
|
179
|
+
|
|
180
|
+
You can configure `scheduled_power_up_times` in each machine entry to automatically power up the machine at specified times using
|
|
181
|
+
a cron expression. After the machine comes online, the proxy sends keep-alive requests to the machine's keep-alive daemon
|
|
182
|
+
(for the duration specified by `keep_alive_time`) to prevent it from auto-suspending during the expected usage window.
|
|
183
|
+
|
|
184
|
+
Each entry in `scheduled_power_up_times` contains:
|
|
185
|
+
- `cron`: A standard 5-field cron expression (e.g. `"0 7 * * 1-5"` for weekdays at 7:00)
|
|
186
|
+
- `keep_alive_time`: Duration in seconds to send keep-alive requests after power-up (e.g. `7200` for 2 hours)
|
|
187
|
+
|
|
188
|
+
If multiple entries overlap in time, the keep-alive period is automatically extended to cover the longest window, and no
|
|
189
|
+
duplicate keep-alive requests are sent.
|
|
@@ -69,7 +69,16 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
69
69
|
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
|
|
70
70
|
"online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
|
|
71
71
|
"ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
|
|
72
|
-
"ipmi_max_reset_try_count": 3
|
|
72
|
+
"ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
|
|
73
|
+
"keep_alive_mode": true, // Indicates whether this machine has a keep-alive daemon, optional, default is false
|
|
74
|
+
// If you enable this, wolsocketproxy will send a request when there is any traffic
|
|
75
|
+
"keep_alive_mode_base_url": "http://192.168.1.124:8080", // Keep-alive daemon provided URL, optional
|
|
76
|
+
"scheduled_power_up_times": [ // Scheduled power-up times, optional
|
|
77
|
+
{
|
|
78
|
+
"cron": "0 7 * * 1-5", // Cron expression for auto power-up
|
|
79
|
+
"keep_alive_time": 7200 // Send keep-alive requests for N seconds after power-up
|
|
80
|
+
}
|
|
81
|
+
]
|
|
73
82
|
},
|
|
74
83
|
// ... more machines ...
|
|
75
84
|
},
|
|
@@ -131,3 +140,18 @@ The config file `wolsocketproxy.conf` should look like the following:
|
|
|
131
140
|
You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
|
|
132
141
|
or the special process will be killed.
|
|
133
142
|
You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
|
|
143
|
+
|
|
144
|
+
If you enable `keep_alive_mode` in proxy mode's config, the proxy will send requests to `/watchdog/feed` of this machine whenever there is any traffic towards this machine through it.
|
|
145
|
+
|
|
146
|
+
### Scheduled power-up
|
|
147
|
+
|
|
148
|
+
You can configure `scheduled_power_up_times` in each machine entry to automatically power up the machine at specified times using
|
|
149
|
+
a cron expression. After the machine comes online, the proxy sends keep-alive requests to the machine's keep-alive daemon
|
|
150
|
+
(for the duration specified by `keep_alive_time`) to prevent it from auto-suspending during the expected usage window.
|
|
151
|
+
|
|
152
|
+
Each entry in `scheduled_power_up_times` contains:
|
|
153
|
+
- `cron`: A standard 5-field cron expression (e.g. `"0 7 * * 1-5"` for weekdays at 7:00)
|
|
154
|
+
- `keep_alive_time`: Duration in seconds to send keep-alive requests after power-up (e.g. `7200` for 2 hours)
|
|
155
|
+
|
|
156
|
+
If multiple entries overlap in time, the keep-alive period is automatically extended to cover the longest window, and no
|
|
157
|
+
duplicate keep-alive requests are sent.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wolsocketproxy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "A socket proxy with wake-on-lan feature."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Song Fuchang", email = "song.fc@gmail.com"}
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
requires-python = ">=3.12"
|
|
21
21
|
dependencies = [
|
|
22
22
|
"aiohttp==3.13.5",
|
|
23
|
+
"croniter==6.0.0",
|
|
23
24
|
"dataclass-wizard==0.39.1",
|
|
24
25
|
"icmplib==3.0.4",
|
|
25
26
|
"redfish==3.3.5",
|
|
@@ -72,4 +73,5 @@ lint.ignore = [
|
|
|
72
73
|
"ISC001",
|
|
73
74
|
"TRY003",
|
|
74
75
|
"TRY201",
|
|
76
|
+
"PLR0913",
|
|
75
77
|
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
URL_WATCHDOG_FEED = "/watchdog/feed"
|
|
@@ -12,6 +12,8 @@ from aiohttp import web
|
|
|
12
12
|
from aiohttp.web import Request, Response
|
|
13
13
|
from setproctitle import setproctitle
|
|
14
14
|
|
|
15
|
+
from wolsocketproxy.common import URL_WATCHDOG_FEED
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
@dataclass
|
|
17
19
|
class KeepAliveConfig:
|
|
@@ -41,11 +43,7 @@ class KeepAliveDaemon:
|
|
|
41
43
|
|
|
42
44
|
self._web_app = web.Application()
|
|
43
45
|
|
|
44
|
-
self._web_app.add_routes(
|
|
45
|
-
[
|
|
46
|
-
web.get("/watchdog/feed", self._handle_watchdog_feed)
|
|
47
|
-
]
|
|
48
|
-
)
|
|
46
|
+
self._web_app.add_routes([web.get(URL_WATCHDOG_FEED, self._handle_watchdog_feed)])
|
|
49
47
|
|
|
50
48
|
async def _watchdog_timer(self) -> None:
|
|
51
49
|
while True:
|
|
@@ -97,8 +95,7 @@ class KeepAliveDaemon:
|
|
|
97
95
|
return True
|
|
98
96
|
|
|
99
97
|
self._log.info(
|
|
100
|
-
"Started special process with name %s, PID %d",
|
|
101
|
-
self._config.special_process_name, self._special_process_id
|
|
98
|
+
"Started special process with name %s, PID %d", self._config.special_process_name, self._special_process_id
|
|
102
99
|
)
|
|
103
100
|
|
|
104
101
|
return False
|
|
@@ -123,7 +120,9 @@ class KeepAliveDaemon:
|
|
|
123
120
|
|
|
124
121
|
self._log.info(
|
|
125
122
|
"Keep-alive daemon started at %s:%d, watchdog feed interval %ds.",
|
|
126
|
-
self._config.listen_address,
|
|
123
|
+
self._config.listen_address,
|
|
124
|
+
self._config.listen_port,
|
|
125
|
+
self._config.watchdog_feed_interval,
|
|
127
126
|
)
|
|
128
127
|
|
|
129
128
|
web.run_app(self._web_app, host=self._config.listen_address, port=self._config.listen_port)
|
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextlib
|
|
3
3
|
import logging
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable, Coroutine
|
|
4
6
|
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
5
8
|
from logging import Logger
|
|
9
|
+
from threading import Event, Thread
|
|
6
10
|
from typing import Any, Literal, override
|
|
7
11
|
|
|
12
|
+
import aiohttp
|
|
8
13
|
import wakeonlan
|
|
14
|
+
from croniter import croniter
|
|
9
15
|
from redfish.rest.v1 import HttpClient, redfish_client
|
|
10
16
|
|
|
17
|
+
from wolsocketproxy.common import URL_WATCHDOG_FEED
|
|
11
18
|
from wolsocketproxy.monitor import Monitor, MonitorConfig
|
|
12
19
|
from wolsocketproxy.utils import perform_ipmi_action
|
|
13
20
|
|
|
14
21
|
|
|
22
|
+
@dataclass
|
|
23
|
+
class ScheduledPowerUpTime:
|
|
24
|
+
cron: str
|
|
25
|
+
keep_alive_time: int = 0
|
|
26
|
+
|
|
27
|
+
|
|
15
28
|
@dataclass
|
|
16
29
|
class MachineConfig:
|
|
17
30
|
wake_up_method: Literal["ipmi", "wol"] = "wol"
|
|
@@ -29,6 +42,12 @@ class MachineConfig:
|
|
|
29
42
|
online_check_http_expected_code: int = 200
|
|
30
43
|
online_check_timeout: int = 60
|
|
31
44
|
|
|
45
|
+
keep_alive_mode: bool = False
|
|
46
|
+
keep_alive_mode_base_url: str | None = None
|
|
47
|
+
keep_alive_min_interval: int = 1
|
|
48
|
+
|
|
49
|
+
scheduled_power_up_times: list[ScheduledPowerUpTime] | None = None
|
|
50
|
+
|
|
32
51
|
|
|
33
52
|
@dataclass
|
|
34
53
|
class ProxyRoute:
|
|
@@ -56,6 +75,168 @@ class ProxyConfig:
|
|
|
56
75
|
ipmi_configs: list[IPMIConfig] | None = None
|
|
57
76
|
|
|
58
77
|
|
|
78
|
+
class TargetKeepAliveSender:
|
|
79
|
+
_log: logging.Logger = logging.getLogger(__name__)
|
|
80
|
+
|
|
81
|
+
_target_url: str
|
|
82
|
+
_keep_alive_min_interval: int
|
|
83
|
+
_loop: asyncio.AbstractEventLoop
|
|
84
|
+
_queue: asyncio.Queue
|
|
85
|
+
_stop_event: Event
|
|
86
|
+
|
|
87
|
+
def __init__(self, target_base_url: str, keep_alive_min_interval: int) -> None:
|
|
88
|
+
self._target_url = target_base_url.removesuffix("/") + URL_WATCHDOG_FEED
|
|
89
|
+
self._keep_alive_min_interval = keep_alive_min_interval
|
|
90
|
+
|
|
91
|
+
self._loop = asyncio.new_event_loop()
|
|
92
|
+
self._queue = asyncio.Queue(1)
|
|
93
|
+
self._stop_event = Event()
|
|
94
|
+
|
|
95
|
+
def _run_loop() -> None:
|
|
96
|
+
asyncio.set_event_loop(self._loop)
|
|
97
|
+
self._loop.run_until_complete(self._send_worker())
|
|
98
|
+
|
|
99
|
+
Thread(target=_run_loop, daemon=True).start()
|
|
100
|
+
|
|
101
|
+
async def _send_worker(self) -> None:
|
|
102
|
+
while not self._stop_event.is_set():
|
|
103
|
+
await self._queue.get()
|
|
104
|
+
if self._stop_event.is_set():
|
|
105
|
+
break
|
|
106
|
+
await self._send()
|
|
107
|
+
await asyncio.sleep(self._keep_alive_min_interval)
|
|
108
|
+
|
|
109
|
+
async def _send(self) -> None:
|
|
110
|
+
try:
|
|
111
|
+
async with aiohttp.request(
|
|
112
|
+
"GET",
|
|
113
|
+
self._target_url,
|
|
114
|
+
timeout=aiohttp.ClientTimeout(total=5),
|
|
115
|
+
) as resp:
|
|
116
|
+
await resp.json()
|
|
117
|
+
except (aiohttp.ClientError, aiohttp.ClientResponseError):
|
|
118
|
+
self._log.warning("Failed to send target keep alive request to %s", self._target_url, exc_info=True)
|
|
119
|
+
|
|
120
|
+
def schedule_send(self) -> None:
|
|
121
|
+
def _no_exception_put() -> None:
|
|
122
|
+
with contextlib.suppress(asyncio.QueueFull):
|
|
123
|
+
self._queue.put_nowait(1)
|
|
124
|
+
|
|
125
|
+
self._loop.call_soon_threadsafe(_no_exception_put)
|
|
126
|
+
|
|
127
|
+
def stop(self) -> None:
|
|
128
|
+
self._stop_event.set()
|
|
129
|
+
with contextlib.suppress(asyncio.QueueFull):
|
|
130
|
+
self._loop.call_soon_threadsafe(lambda: self._queue.put_nowait(None))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ScheduledPowerUpManager:
|
|
134
|
+
_log: Logger = logging.getLogger(__name__)
|
|
135
|
+
|
|
136
|
+
_machines: dict[str, MachineConfig]
|
|
137
|
+
_wake_up_callback: Callable[[str], Coroutine[Any, Any, None]]
|
|
138
|
+
_loop: asyncio.AbstractEventLoop
|
|
139
|
+
_keep_alive_end_times: dict[str, float]
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
machines: dict[str, MachineConfig],
|
|
144
|
+
wake_up_callback: Callable[[str], Coroutine[Any, Any, None]],
|
|
145
|
+
) -> None:
|
|
146
|
+
self._machines = machines
|
|
147
|
+
self._wake_up_callback = wake_up_callback
|
|
148
|
+
self._keep_alive_end_times = {}
|
|
149
|
+
|
|
150
|
+
def start(self) -> None:
|
|
151
|
+
self._loop = asyncio.new_event_loop()
|
|
152
|
+
|
|
153
|
+
def _run_loop() -> None:
|
|
154
|
+
asyncio.set_event_loop(self._loop)
|
|
155
|
+
self._loop.run_forever()
|
|
156
|
+
|
|
157
|
+
Thread(target=_run_loop, daemon=True).start()
|
|
158
|
+
|
|
159
|
+
for machine_name, machine in self._machines.items():
|
|
160
|
+
schedules = machine.scheduled_power_up_times
|
|
161
|
+
if not schedules:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
if machine.keep_alive_mode_base_url is None:
|
|
165
|
+
self._log.warning(
|
|
166
|
+
"Machine %s has scheduled_power_up_times but no keep_alive_mode_base_url configured, "
|
|
167
|
+
"keep-alive after power-up will be skipped",
|
|
168
|
+
machine_name,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for schedule in schedules:
|
|
172
|
+
asyncio.run_coroutine_threadsafe(
|
|
173
|
+
self._process_schedule(machine_name, machine, schedule),
|
|
174
|
+
self._loop,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def _process_schedule(
|
|
178
|
+
self,
|
|
179
|
+
machine_name: str,
|
|
180
|
+
machine: MachineConfig,
|
|
181
|
+
schedule: ScheduledPowerUpTime,
|
|
182
|
+
) -> None:
|
|
183
|
+
while True:
|
|
184
|
+
now = datetime.now()
|
|
185
|
+
cron = croniter(schedule.cron, now)
|
|
186
|
+
next_time = cron.get_next(datetime)
|
|
187
|
+
|
|
188
|
+
delay = (next_time - datetime.now()).total_seconds()
|
|
189
|
+
if delay > 0:
|
|
190
|
+
await asyncio.sleep(delay)
|
|
191
|
+
|
|
192
|
+
self._log.info(
|
|
193
|
+
"Scheduled power-up triggered for %s (cron: %s)",
|
|
194
|
+
machine_name,
|
|
195
|
+
schedule.cron,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
await self._wake_up_callback(machine_name)
|
|
200
|
+
except ConnectionAbortedError:
|
|
201
|
+
self._log.error(
|
|
202
|
+
"Scheduled power-up failed for %s, will retry at next schedule time",
|
|
203
|
+
machine_name,
|
|
204
|
+
)
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if schedule.keep_alive_time > 0 and machine.keep_alive_mode_base_url is not None:
|
|
208
|
+
await self._run_keep_alive(machine_name, machine, schedule.keep_alive_time)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def _run_keep_alive(
|
|
212
|
+
self,
|
|
213
|
+
machine_name: str,
|
|
214
|
+
machine: MachineConfig,
|
|
215
|
+
keep_alive_time: int,
|
|
216
|
+
) -> None:
|
|
217
|
+
assert machine.keep_alive_mode_base_url is not None
|
|
218
|
+
new_end = time.monotonic() + keep_alive_time
|
|
219
|
+
current_end = self._keep_alive_end_times.get(machine_name, 0.0)
|
|
220
|
+
|
|
221
|
+
if new_end > current_end:
|
|
222
|
+
self._keep_alive_end_times[machine_name] = new_end
|
|
223
|
+
|
|
224
|
+
if current_end > time.monotonic():
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
sender = TargetKeepAliveSender(
|
|
228
|
+
machine.keep_alive_mode_base_url,
|
|
229
|
+
machine.keep_alive_min_interval,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
while time.monotonic() < self._keep_alive_end_times.get(machine_name, 0.0):
|
|
234
|
+
sender.schedule_send()
|
|
235
|
+
await asyncio.sleep(machine.keep_alive_min_interval)
|
|
236
|
+
finally:
|
|
237
|
+
sender.stop()
|
|
238
|
+
|
|
239
|
+
|
|
59
240
|
class ProxyUdpProtocol(asyncio.DatagramProtocol):
|
|
60
241
|
_proxy: "Proxy"
|
|
61
242
|
_monitor: Monitor
|
|
@@ -63,16 +244,24 @@ class ProxyUdpProtocol(asyncio.DatagramProtocol):
|
|
|
63
244
|
_target_machine_name: str
|
|
64
245
|
_target_address: str
|
|
65
246
|
_target_port: int
|
|
247
|
+
_target_keep_alive_sender: TargetKeepAliveSender | None = None
|
|
66
248
|
_target_pair: tuple[str, int]
|
|
67
249
|
|
|
68
250
|
def __init__(
|
|
69
|
-
self,
|
|
251
|
+
self,
|
|
252
|
+
proxy: "Proxy",
|
|
253
|
+
monitor: Monitor,
|
|
254
|
+
target_machine_name: str,
|
|
255
|
+
target_address: str,
|
|
256
|
+
target_port: int,
|
|
257
|
+
target_keep_alive_sender: TargetKeepAliveSender | None = None,
|
|
70
258
|
) -> None:
|
|
71
259
|
self._proxy = proxy
|
|
72
260
|
self._monitor = monitor
|
|
73
261
|
self._target_machine_name = target_machine_name
|
|
74
262
|
self._target_address = target_address
|
|
75
263
|
self._target_port = target_port
|
|
264
|
+
self._target_keep_alive_sender = target_keep_alive_sender
|
|
76
265
|
self._target_pair = (target_address, target_port)
|
|
77
266
|
|
|
78
267
|
@override
|
|
@@ -92,6 +281,9 @@ class ProxyUdpProtocol(asyncio.DatagramProtocol):
|
|
|
92
281
|
|
|
93
282
|
self._transport.sendto(data, self._target_pair)
|
|
94
283
|
|
|
284
|
+
if self._target_keep_alive_sender is not None:
|
|
285
|
+
self._target_keep_alive_sender.schedule_send()
|
|
286
|
+
|
|
95
287
|
|
|
96
288
|
class Proxy:
|
|
97
289
|
_log: Logger = logging.getLogger()
|
|
@@ -100,6 +292,7 @@ class Proxy:
|
|
|
100
292
|
_machines: dict[str, MachineConfig]
|
|
101
293
|
_routes: list[ProxyRoute]
|
|
102
294
|
_ipmi_configs: dict[str, IPMIConfig]
|
|
295
|
+
_scheduled_power_up_manager: ScheduledPowerUpManager
|
|
103
296
|
|
|
104
297
|
def __init__(self, config: ProxyConfig) -> None:
|
|
105
298
|
self._config = config
|
|
@@ -139,6 +332,11 @@ class Proxy:
|
|
|
139
332
|
}
|
|
140
333
|
)
|
|
141
334
|
|
|
335
|
+
self._scheduled_power_up_manager = ScheduledPowerUpManager(
|
|
336
|
+
machines=self._machines,
|
|
337
|
+
wake_up_callback=self._wake_up_target,
|
|
338
|
+
)
|
|
339
|
+
|
|
142
340
|
def start(self) -> None:
|
|
143
341
|
self._monitor.start()
|
|
144
342
|
|
|
@@ -147,6 +345,8 @@ class Proxy:
|
|
|
147
345
|
|
|
148
346
|
loop = asyncio.get_event_loop()
|
|
149
347
|
|
|
348
|
+
self._scheduled_power_up_manager.start()
|
|
349
|
+
|
|
150
350
|
self._log.info("Proxy server started.")
|
|
151
351
|
|
|
152
352
|
with contextlib.suppress(KeyboardInterrupt):
|
|
@@ -180,9 +380,23 @@ class Proxy:
|
|
|
180
380
|
|
|
181
381
|
def __create_tcp_route(self, route: ProxyRoute) -> None:
|
|
182
382
|
assert route.target_machine_name is not None
|
|
383
|
+
machine_config = self._machines[route.target_machine_name]
|
|
384
|
+
target_keep_alive_sender = None
|
|
385
|
+
|
|
386
|
+
if machine_config.keep_alive_mode:
|
|
387
|
+
assert machine_config.keep_alive_mode_base_url is not None
|
|
388
|
+
|
|
389
|
+
target_keep_alive_sender = TargetKeepAliveSender(
|
|
390
|
+
machine_config.keep_alive_mode_base_url, machine_config.keep_alive_min_interval
|
|
391
|
+
)
|
|
183
392
|
|
|
184
393
|
cr = asyncio.start_server(
|
|
185
|
-
self.__make_tcp_route_handler(
|
|
394
|
+
self.__make_tcp_route_handler(
|
|
395
|
+
route.target_machine_name,
|
|
396
|
+
route.target_address,
|
|
397
|
+
route.target_port,
|
|
398
|
+
target_keep_alive_sender,
|
|
399
|
+
),
|
|
186
400
|
route.local_address,
|
|
187
401
|
route.local_port,
|
|
188
402
|
)
|
|
@@ -190,16 +404,31 @@ class Proxy:
|
|
|
190
404
|
loop = asyncio.get_event_loop()
|
|
191
405
|
loop.run_until_complete(cr)
|
|
192
406
|
|
|
193
|
-
async def __pipe(
|
|
407
|
+
async def __pipe(
|
|
408
|
+
self,
|
|
409
|
+
target_address: str,
|
|
410
|
+
reader: asyncio.StreamReader,
|
|
411
|
+
writer: asyncio.StreamWriter,
|
|
412
|
+
target_keep_alive_sender: TargetKeepAliveSender | None = None,
|
|
413
|
+
) -> None:
|
|
194
414
|
try:
|
|
195
415
|
while not reader.at_eof():
|
|
196
416
|
writer.write(await reader.read(2048))
|
|
417
|
+
|
|
418
|
+
if target_keep_alive_sender is not None:
|
|
419
|
+
target_keep_alive_sender.schedule_send()
|
|
197
420
|
except ConnectionResetError:
|
|
198
421
|
self._log.warning("Connection reset by target %s", target_address)
|
|
199
422
|
finally:
|
|
200
423
|
writer.close()
|
|
201
424
|
|
|
202
|
-
def __make_tcp_route_handler(
|
|
425
|
+
def __make_tcp_route_handler(
|
|
426
|
+
self,
|
|
427
|
+
target_machine_name: str,
|
|
428
|
+
target_address: str,
|
|
429
|
+
target_port: int,
|
|
430
|
+
target_keep_alive_sender: TargetKeepAliveSender | None = None,
|
|
431
|
+
) -> Any: # noqa: ANN401
|
|
203
432
|
async def handler(local_reader: asyncio.StreamReader, local_writer: asyncio.StreamWriter) -> None:
|
|
204
433
|
if not self._monitor.is_available(target_machine_name):
|
|
205
434
|
await self._wake_up_target(target_machine_name)
|
|
@@ -211,8 +440,8 @@ class Proxy:
|
|
|
211
440
|
self._log.error("Unable to open connection to %s:%d", target_address, target_port)
|
|
212
441
|
raise e
|
|
213
442
|
|
|
214
|
-
send_pipe = self.__pipe(target_address, local_reader, target_writer)
|
|
215
|
-
recv_pipe = self.__pipe(target_address, target_reader, local_writer)
|
|
443
|
+
send_pipe = self.__pipe(target_address, local_reader, target_writer, target_keep_alive_sender)
|
|
444
|
+
recv_pipe = self.__pipe(target_address, target_reader, local_writer, target_keep_alive_sender)
|
|
216
445
|
await asyncio.gather(send_pipe, recv_pipe)
|
|
217
446
|
|
|
218
447
|
return handler
|
|
@@ -221,10 +450,27 @@ class Proxy:
|
|
|
221
450
|
target_machine_name = route.target_machine_name
|
|
222
451
|
assert target_machine_name is not None
|
|
223
452
|
|
|
453
|
+
machine_config = self._machines[target_machine_name]
|
|
454
|
+
target_keep_alive_sender = None
|
|
455
|
+
|
|
456
|
+
if machine_config.keep_alive_mode:
|
|
457
|
+
assert machine_config.keep_alive_mode_base_url is not None
|
|
458
|
+
|
|
459
|
+
target_keep_alive_sender = TargetKeepAliveSender(
|
|
460
|
+
machine_config.keep_alive_mode_base_url, machine_config.keep_alive_min_interval
|
|
461
|
+
)
|
|
462
|
+
|
|
224
463
|
loop = asyncio.get_event_loop()
|
|
225
464
|
|
|
226
465
|
cr = loop.create_datagram_endpoint(
|
|
227
|
-
lambda: ProxyUdpProtocol(
|
|
466
|
+
lambda: ProxyUdpProtocol(
|
|
467
|
+
self,
|
|
468
|
+
self._monitor,
|
|
469
|
+
target_machine_name,
|
|
470
|
+
route.target_address,
|
|
471
|
+
route.target_port,
|
|
472
|
+
target_keep_alive_sender,
|
|
473
|
+
),
|
|
228
474
|
local_addr=(route.local_address, route.local_port),
|
|
229
475
|
)
|
|
230
476
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wolsocketproxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A socket proxy with wake-on-lan feature.
|
|
5
5
|
Author-email: Song Fuchang <song.fc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Requires-Python: >=3.12
|
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: aiohttp==3.13.5
|
|
17
|
+
Requires-Dist: croniter==6.0.0
|
|
17
18
|
Requires-Dist: dataclass-wizard==0.39.1
|
|
18
19
|
Requires-Dist: icmplib==3.0.4
|
|
19
20
|
Requires-Dist: redfish==3.3.5
|
|
@@ -100,7 +101,16 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
100
101
|
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
|
|
101
102
|
"online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
|
|
102
103
|
"ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
|
|
103
|
-
"ipmi_max_reset_try_count": 3
|
|
104
|
+
"ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
|
|
105
|
+
"keep_alive_mode": true, // Indicates whether this machine has a keep-alive daemon, optional, default is false
|
|
106
|
+
// If you enable this, wolsocketproxy will send a request when there is any traffic
|
|
107
|
+
"keep_alive_mode_base_url": "http://192.168.1.124:8080", // Keep-alive daemon provided URL, optional
|
|
108
|
+
"scheduled_power_up_times": [ // Scheduled power-up times, optional
|
|
109
|
+
{
|
|
110
|
+
"cron": "0 7 * * 1-5", // Cron expression for auto power-up
|
|
111
|
+
"keep_alive_time": 7200 // Send keep-alive requests for N seconds after power-up
|
|
112
|
+
}
|
|
113
|
+
]
|
|
104
114
|
},
|
|
105
115
|
// ... more machines ...
|
|
106
116
|
},
|
|
@@ -162,3 +172,18 @@ The config file `wolsocketproxy.conf` should look like the following:
|
|
|
162
172
|
You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
|
|
163
173
|
or the special process will be killed.
|
|
164
174
|
You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
|
|
175
|
+
|
|
176
|
+
If you enable `keep_alive_mode` in proxy mode's config, the proxy will send requests to `/watchdog/feed` of this machine whenever there is any traffic towards this machine through it.
|
|
177
|
+
|
|
178
|
+
### Scheduled power-up
|
|
179
|
+
|
|
180
|
+
You can configure `scheduled_power_up_times` in each machine entry to automatically power up the machine at specified times using
|
|
181
|
+
a cron expression. After the machine comes online, the proxy sends keep-alive requests to the machine's keep-alive daemon
|
|
182
|
+
(for the duration specified by `keep_alive_time`) to prevent it from auto-suspending during the expected usage window.
|
|
183
|
+
|
|
184
|
+
Each entry in `scheduled_power_up_times` contains:
|
|
185
|
+
- `cron`: A standard 5-field cron expression (e.g. `"0 7 * * 1-5"` for weekdays at 7:00)
|
|
186
|
+
- `keep_alive_time`: Duration in seconds to send keep-alive requests after power-up (e.g. `7200` for 2 hours)
|
|
187
|
+
|
|
188
|
+
If multiple entries overlap in time, the keep-alive period is automatically extended to cover the longest window, and no
|
|
189
|
+
duplicate keep-alive requests are sent.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wolsocketproxy-0.3.0 → wolsocketproxy-0.4.0}/src/wolsocketproxy.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|