wolsocketproxy 0.2.0__tar.gz → 0.3.1__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.2.0/src/wolsocketproxy.egg-info → wolsocketproxy-0.3.1}/PKG-INFO +42 -4
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/README.md +39 -2
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/pyproject.toml +6 -2
- wolsocketproxy-0.3.1/src/wolsocketproxy/__init__.py +75 -0
- wolsocketproxy-0.3.1/src/wolsocketproxy/common.py +1 -0
- wolsocketproxy-0.3.1/src/wolsocketproxy/keepalive.py +152 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy/proxy.py +107 -7
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1/src/wolsocketproxy.egg-info}/PKG-INFO +42 -4
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/SOURCES.txt +2 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/requires.txt +2 -1
- wolsocketproxy-0.2.0/src/wolsocketproxy/__init__.py +0 -44
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/LICENSE +0 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/setup.cfg +0 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy/__main__.py +0 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy/monitor.py +0 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy/utils.py +0 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/dependency_links.txt +0 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/entry_points.txt +0 -0
- {wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/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.3.1
|
|
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
|
|
@@ -17,6 +17,7 @@ Requires-Dist: aiohttp==3.13.5
|
|
|
17
17
|
Requires-Dist: dataclass-wizard==0.39.1
|
|
18
18
|
Requires-Dist: icmplib==3.0.4
|
|
19
19
|
Requires-Dist: redfish==3.3.5
|
|
20
|
+
Requires-Dist: setproctitle==1.3.7
|
|
20
21
|
Requires-Dist: wakeonlan==3.1.0
|
|
21
22
|
Provides-Extra: dev
|
|
22
23
|
Requires-Dist: wolsocketproxy[lint]; extra == "dev"
|
|
@@ -25,7 +26,7 @@ Provides-Extra: lint
|
|
|
25
26
|
Requires-Dist: ty; extra == "lint"
|
|
26
27
|
Requires-Dist: ruff; extra == "lint"
|
|
27
28
|
Provides-Extra: build
|
|
28
|
-
Requires-Dist: build[virtualenv]==1.
|
|
29
|
+
Requires-Dist: build[virtualenv]==1.5.0; extra == "build"
|
|
29
30
|
Dynamic: license-file
|
|
30
31
|
|
|
31
32
|
# wol-socket-proxy
|
|
@@ -96,10 +97,13 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
96
97
|
// "http" - You must specify "online_check_http_url"
|
|
97
98
|
// "ping" - You must specify "ip_address"
|
|
98
99
|
"online_check_http_url": "http://192.168.1.124/health", // The URL used to check if machine is online, optional
|
|
99
|
-
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is
|
|
100
|
+
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
|
|
100
101
|
"online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
|
|
101
102
|
"ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
|
|
102
|
-
"ipmi_max_reset_try_count": 3
|
|
103
|
+
"ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
|
|
104
|
+
"keep_alive_mode": true, // Indicates whether this machine has a keep-alive daemon, optional, default is false
|
|
105
|
+
// If you enable this, wolsocketproxy will send a request when there is any traffic
|
|
106
|
+
"keep_alive_mode_base_url": "http://192.168.1.124:8080" // Keep-alive daemon provided URL, optional
|
|
103
107
|
},
|
|
104
108
|
// ... more machines ...
|
|
105
109
|
},
|
|
@@ -129,3 +133,37 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
129
133
|
]
|
|
130
134
|
}
|
|
131
135
|
```
|
|
136
|
+
|
|
137
|
+
## Additional modes
|
|
138
|
+
|
|
139
|
+
### Keep-alive daemon mode
|
|
140
|
+
|
|
141
|
+
This mode is used for running on target machine with auto-suspend (e.g. running `circadian` or similar), to keep the target
|
|
142
|
+
machine from auto suspending when running tasks.
|
|
143
|
+
|
|
144
|
+
Use the following command on the target machine to enter this mode:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
wolsocketproxy -k
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The config file `wolsocketproxy.conf` should look like the following:
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
```json5
|
|
154
|
+
// Keep-alive daemon mode
|
|
155
|
+
{
|
|
156
|
+
"listen_address": "0.0.0.0", // The address to listen for HTTP server, default is 127.0.0.1
|
|
157
|
+
"listen_port": 18080, // The port to listen for HTTP server, default is 18080
|
|
158
|
+
"watchdog_feed_interval": 60, // Watchdog feed interval, seconds, default is 600
|
|
159
|
+
"keep_alive_method": "special_process", // How to keep target machine alive, default is "special_process"
|
|
160
|
+
// "special_process" - Fork a child process with specified name
|
|
161
|
+
"special_process_name": "wsp-keepalive", // The display name of the forked child process, default is "wsp-keepalive"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
|
|
166
|
+
or the special process will be killed.
|
|
167
|
+
You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
|
|
168
|
+
|
|
169
|
+
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.
|
|
@@ -66,10 +66,13 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
66
66
|
// "http" - You must specify "online_check_http_url"
|
|
67
67
|
// "ping" - You must specify "ip_address"
|
|
68
68
|
"online_check_http_url": "http://192.168.1.124/health", // The URL used to check if machine is online, optional
|
|
69
|
-
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is
|
|
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
|
|
73
76
|
},
|
|
74
77
|
// ... more machines ...
|
|
75
78
|
},
|
|
@@ -99,3 +102,37 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
99
102
|
]
|
|
100
103
|
}
|
|
101
104
|
```
|
|
105
|
+
|
|
106
|
+
## Additional modes
|
|
107
|
+
|
|
108
|
+
### Keep-alive daemon mode
|
|
109
|
+
|
|
110
|
+
This mode is used for running on target machine with auto-suspend (e.g. running `circadian` or similar), to keep the target
|
|
111
|
+
machine from auto suspending when running tasks.
|
|
112
|
+
|
|
113
|
+
Use the following command on the target machine to enter this mode:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
wolsocketproxy -k
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The config file `wolsocketproxy.conf` should look like the following:
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
```json5
|
|
123
|
+
// Keep-alive daemon mode
|
|
124
|
+
{
|
|
125
|
+
"listen_address": "0.0.0.0", // The address to listen for HTTP server, default is 127.0.0.1
|
|
126
|
+
"listen_port": 18080, // The port to listen for HTTP server, default is 18080
|
|
127
|
+
"watchdog_feed_interval": 60, // Watchdog feed interval, seconds, default is 600
|
|
128
|
+
"keep_alive_method": "special_process", // How to keep target machine alive, default is "special_process"
|
|
129
|
+
// "special_process" - Fork a child process with specified name
|
|
130
|
+
"special_process_name": "wsp-keepalive", // The display name of the forked child process, default is "wsp-keepalive"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
|
|
135
|
+
or the special process will be killed.
|
|
136
|
+
You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
|
|
137
|
+
|
|
138
|
+
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.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wolsocketproxy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "A socket proxy with wake-on-lan feature."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Song Fuchang", email = "song.fc@gmail.com"}
|
|
@@ -23,6 +23,7 @@ dependencies = [
|
|
|
23
23
|
"dataclass-wizard==0.39.1",
|
|
24
24
|
"icmplib==3.0.4",
|
|
25
25
|
"redfish==3.3.5",
|
|
26
|
+
"setproctitle==1.3.7",
|
|
26
27
|
"wakeonlan==3.1.0",
|
|
27
28
|
]
|
|
28
29
|
|
|
@@ -36,7 +37,7 @@ lint = [
|
|
|
36
37
|
"ruff",
|
|
37
38
|
]
|
|
38
39
|
build = [
|
|
39
|
-
"build[virtualenv]==1.
|
|
40
|
+
"build[virtualenv]==1.5.0",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.urls]
|
|
@@ -52,6 +53,7 @@ src = ["src"]
|
|
|
52
53
|
target-version = "py312"
|
|
53
54
|
lint.select = ["ALL"]
|
|
54
55
|
lint.ignore = [
|
|
56
|
+
"ASYNC110",
|
|
55
57
|
"COM812",
|
|
56
58
|
"D100",
|
|
57
59
|
"D101",
|
|
@@ -62,6 +64,7 @@ lint.ignore = [
|
|
|
62
64
|
"D203",
|
|
63
65
|
"D211",
|
|
64
66
|
"D213",
|
|
67
|
+
"DTZ005",
|
|
65
68
|
"S101",
|
|
66
69
|
"EM102",
|
|
67
70
|
"FBT001",
|
|
@@ -69,4 +72,5 @@ lint.ignore = [
|
|
|
69
72
|
"ISC001",
|
|
70
73
|
"TRY003",
|
|
71
74
|
"TRY201",
|
|
75
|
+
"PLR0913",
|
|
72
76
|
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from argparse import ArgumentParser
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from sys import stdout
|
|
6
|
+
|
|
7
|
+
import dataclass_wizard
|
|
8
|
+
|
|
9
|
+
from wolsocketproxy.keepalive import KeepAliveConfig, KeepAliveDaemon
|
|
10
|
+
from wolsocketproxy.proxy import Proxy, ProxyConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
18
|
+
handlers=[
|
|
19
|
+
logging.StreamHandler(
|
|
20
|
+
stream=stdout,
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
force=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger()
|
|
27
|
+
|
|
28
|
+
parser = ArgumentParser(prog="wolsocketproxy", description="A socket proxy with wake-on-lan feature.")
|
|
29
|
+
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"-k",
|
|
32
|
+
"--keep-alive",
|
|
33
|
+
dest="keep_alive_mode",
|
|
34
|
+
default=False,
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Start in keep-alive daemon mode",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-c",
|
|
41
|
+
"--config",
|
|
42
|
+
help="The config file to use, default lookup order: /etc/wolsocketproxy.conf, ./wolsocketproxy.conf",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
config_path: Path
|
|
47
|
+
|
|
48
|
+
if "config" in args and args.config is not None:
|
|
49
|
+
config_path = Path(args.config)
|
|
50
|
+
else:
|
|
51
|
+
config_path = Path("/etc/wolsocketproxy.conf")
|
|
52
|
+
|
|
53
|
+
if not config_path.exists():
|
|
54
|
+
config_path = Path("./wolsocketproxy.conf")
|
|
55
|
+
|
|
56
|
+
if not config_path.exists():
|
|
57
|
+
log.error("Config file path %s does not exist!", config_path)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
config_data = json.loads(config_path.read_text())
|
|
61
|
+
|
|
62
|
+
if args.keep_alive_mode:
|
|
63
|
+
config = dataclass_wizard.fromdict(KeepAliveConfig, config_data)
|
|
64
|
+
|
|
65
|
+
log.info("Loaded keep-alive mode config from %s", config_path)
|
|
66
|
+
|
|
67
|
+
keep_alive_daemon = KeepAliveDaemon(config)
|
|
68
|
+
keep_alive_daemon.start()
|
|
69
|
+
else:
|
|
70
|
+
config = dataclass_wizard.fromdict(ProxyConfig, config_data)
|
|
71
|
+
|
|
72
|
+
log.info("Loaded proxy mode config from %s", config_path)
|
|
73
|
+
|
|
74
|
+
proxy = Proxy(config)
|
|
75
|
+
proxy.start()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
URL_WATCHDOG_FEED = "/watchdog/feed"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from threading import Thread
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from aiohttp import web
|
|
12
|
+
from aiohttp.web import Request, Response
|
|
13
|
+
from setproctitle import setproctitle
|
|
14
|
+
|
|
15
|
+
from wolsocketproxy.common import URL_WATCHDOG_FEED
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class KeepAliveConfig:
|
|
20
|
+
listen_address: str = "127.0.0.1"
|
|
21
|
+
listen_port: int = 18080
|
|
22
|
+
|
|
23
|
+
watchdog_feed_interval: int = 600
|
|
24
|
+
|
|
25
|
+
keep_alive_method: Literal["special_process"] = "special_process"
|
|
26
|
+
|
|
27
|
+
special_process_name: str | None = "wsp-keepalive"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class KeepAliveDaemon:
|
|
31
|
+
_log: logging.Logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_config: KeepAliveConfig
|
|
34
|
+
_web_app: web.Application
|
|
35
|
+
|
|
36
|
+
_watchdog_last_feed_time: datetime
|
|
37
|
+
_watchdog_is_hungry: bool = False
|
|
38
|
+
|
|
39
|
+
_special_process_id: int = -1
|
|
40
|
+
|
|
41
|
+
def __init__(self, config: KeepAliveConfig) -> None:
|
|
42
|
+
self._config = config
|
|
43
|
+
|
|
44
|
+
self._web_app = web.Application()
|
|
45
|
+
|
|
46
|
+
self._web_app.add_routes([web.get(URL_WATCHDOG_FEED, self._handle_watchdog_feed)])
|
|
47
|
+
|
|
48
|
+
async def _watchdog_timer(self) -> None:
|
|
49
|
+
while True:
|
|
50
|
+
previous_is_hungry = self._watchdog_is_hungry
|
|
51
|
+
last_feed_interval = datetime.now() - self._watchdog_last_feed_time
|
|
52
|
+
|
|
53
|
+
if last_feed_interval.total_seconds() > self._config.watchdog_feed_interval:
|
|
54
|
+
self._watchdog_is_hungry = True
|
|
55
|
+
else:
|
|
56
|
+
self._watchdog_is_hungry = False
|
|
57
|
+
|
|
58
|
+
if not previous_is_hungry and self._watchdog_is_hungry:
|
|
59
|
+
Thread(target=self._on_watchdog_hungry, daemon=True).start()
|
|
60
|
+
elif previous_is_hungry and not self._watchdog_is_hungry:
|
|
61
|
+
Thread(target=self._on_watchdog_feed, daemon=True).start()
|
|
62
|
+
|
|
63
|
+
await asyncio.sleep(1)
|
|
64
|
+
|
|
65
|
+
def _on_watchdog_hungry(self) -> None:
|
|
66
|
+
self._log.warning("Watchdog is hungry!")
|
|
67
|
+
|
|
68
|
+
if self._special_process_id > 0:
|
|
69
|
+
os.kill(self._special_process_id, signal.SIGKILL)
|
|
70
|
+
os.waitpid(self._special_process_id, 0)
|
|
71
|
+
self._log.warning("Killed special process PID %d", self._special_process_id)
|
|
72
|
+
self._special_process_id = -1
|
|
73
|
+
|
|
74
|
+
def _on_watchdog_feed(self) -> None:
|
|
75
|
+
self._log.info("Watchdog is feed.")
|
|
76
|
+
|
|
77
|
+
if self._config.keep_alive_method == "special_process" and self._special_process_id < 0:
|
|
78
|
+
self._start_special_process()
|
|
79
|
+
|
|
80
|
+
def _special_process(self) -> None:
|
|
81
|
+
assert self._config.special_process_name is not None
|
|
82
|
+
setproctitle(self._config.special_process_name)
|
|
83
|
+
|
|
84
|
+
while True:
|
|
85
|
+
time.sleep(60)
|
|
86
|
+
|
|
87
|
+
if os.getppid() == 1:
|
|
88
|
+
os._exit(0)
|
|
89
|
+
|
|
90
|
+
def _start_special_process(self) -> bool:
|
|
91
|
+
self._special_process_id = os.fork()
|
|
92
|
+
|
|
93
|
+
if self._special_process_id == 0:
|
|
94
|
+
self._special_process()
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
self._log.info(
|
|
98
|
+
"Started special process with name %s, PID %d", self._config.special_process_name, self._special_process_id
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def start(self) -> None:
|
|
104
|
+
self._watchdog_last_feed_time = datetime.now()
|
|
105
|
+
self._watchdog_is_hungry = False
|
|
106
|
+
|
|
107
|
+
if self._config.keep_alive_method == "special_process": # noqa: SIM102
|
|
108
|
+
if self._start_special_process():
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
loop = asyncio.new_event_loop()
|
|
112
|
+
|
|
113
|
+
def _start_loop() -> None:
|
|
114
|
+
asyncio.set_event_loop(loop)
|
|
115
|
+
loop.run_forever()
|
|
116
|
+
|
|
117
|
+
Thread(target=_start_loop, daemon=True).start()
|
|
118
|
+
|
|
119
|
+
watchdog_task = asyncio.run_coroutine_threadsafe(self._watchdog_timer(), loop)
|
|
120
|
+
|
|
121
|
+
self._log.info(
|
|
122
|
+
"Keep-alive daemon started at %s:%d, watchdog feed interval %ds.",
|
|
123
|
+
self._config.listen_address,
|
|
124
|
+
self._config.listen_port,
|
|
125
|
+
self._config.watchdog_feed_interval,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
web.run_app(self._web_app, host=self._config.listen_address, port=self._config.listen_port)
|
|
129
|
+
|
|
130
|
+
self._log.info("Stopping...")
|
|
131
|
+
|
|
132
|
+
watchdog_task.cancel()
|
|
133
|
+
loop.stop()
|
|
134
|
+
|
|
135
|
+
if self._special_process_id > 0:
|
|
136
|
+
os.kill(self._special_process_id, signal.SIGKILL)
|
|
137
|
+
os.waitpid(self._special_process_id, 0)
|
|
138
|
+
|
|
139
|
+
self._log.info("Stopped.")
|
|
140
|
+
|
|
141
|
+
async def _handle_watchdog_feed(self, _: Request) -> Response:
|
|
142
|
+
last_feed = self._watchdog_last_feed_time
|
|
143
|
+
self._watchdog_last_feed_time = datetime.now()
|
|
144
|
+
interval = (self._watchdog_last_feed_time - last_feed).total_seconds()
|
|
145
|
+
|
|
146
|
+
return web.json_response(
|
|
147
|
+
{
|
|
148
|
+
"status": "ok",
|
|
149
|
+
"last_feed": last_feed.strftime("%Y-%m-%d %H:%M:%S"),
|
|
150
|
+
"interval": interval,
|
|
151
|
+
}
|
|
152
|
+
)
|
|
@@ -3,11 +3,14 @@ import contextlib
|
|
|
3
3
|
import logging
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from logging import Logger
|
|
6
|
+
from threading import Thread
|
|
6
7
|
from typing import Any, Literal, override
|
|
7
8
|
|
|
9
|
+
import aiohttp
|
|
8
10
|
import wakeonlan
|
|
9
11
|
from redfish.rest.v1 import HttpClient, redfish_client
|
|
10
12
|
|
|
13
|
+
from wolsocketproxy.common import URL_WATCHDOG_FEED
|
|
11
14
|
from wolsocketproxy.monitor import Monitor, MonitorConfig
|
|
12
15
|
from wolsocketproxy.utils import perform_ipmi_action
|
|
13
16
|
|
|
@@ -29,6 +32,10 @@ class MachineConfig:
|
|
|
29
32
|
online_check_http_expected_code: int = 200
|
|
30
33
|
online_check_timeout: int = 60
|
|
31
34
|
|
|
35
|
+
keep_alive_mode: bool = False
|
|
36
|
+
keep_alive_mode_base_url: str | None = None
|
|
37
|
+
keep_alive_min_interval: int = 1
|
|
38
|
+
|
|
32
39
|
|
|
33
40
|
@dataclass
|
|
34
41
|
class ProxyRoute:
|
|
@@ -56,6 +63,42 @@ class ProxyConfig:
|
|
|
56
63
|
ipmi_configs: list[IPMIConfig] | None = None
|
|
57
64
|
|
|
58
65
|
|
|
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
|
+
|
|
59
102
|
class ProxyUdpProtocol(asyncio.DatagramProtocol):
|
|
60
103
|
_proxy: "Proxy"
|
|
61
104
|
_monitor: Monitor
|
|
@@ -63,16 +106,24 @@ class ProxyUdpProtocol(asyncio.DatagramProtocol):
|
|
|
63
106
|
_target_machine_name: str
|
|
64
107
|
_target_address: str
|
|
65
108
|
_target_port: int
|
|
109
|
+
_target_keep_alive_sender: TargetKeepAliveSender | None = None
|
|
66
110
|
_target_pair: tuple[str, int]
|
|
67
111
|
|
|
68
112
|
def __init__(
|
|
69
|
-
self,
|
|
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,
|
|
70
120
|
) -> None:
|
|
71
121
|
self._proxy = proxy
|
|
72
122
|
self._monitor = monitor
|
|
73
123
|
self._target_machine_name = target_machine_name
|
|
74
124
|
self._target_address = target_address
|
|
75
125
|
self._target_port = target_port
|
|
126
|
+
self._target_keep_alive_sender = target_keep_alive_sender
|
|
76
127
|
self._target_pair = (target_address, target_port)
|
|
77
128
|
|
|
78
129
|
@override
|
|
@@ -92,6 +143,9 @@ class ProxyUdpProtocol(asyncio.DatagramProtocol):
|
|
|
92
143
|
|
|
93
144
|
self._transport.sendto(data, self._target_pair)
|
|
94
145
|
|
|
146
|
+
if self._target_keep_alive_sender is not None:
|
|
147
|
+
self._target_keep_alive_sender.schedule_send()
|
|
148
|
+
|
|
95
149
|
|
|
96
150
|
class Proxy:
|
|
97
151
|
_log: Logger = logging.getLogger()
|
|
@@ -180,9 +234,23 @@ class Proxy:
|
|
|
180
234
|
|
|
181
235
|
def __create_tcp_route(self, route: ProxyRoute) -> None:
|
|
182
236
|
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
|
+
)
|
|
183
246
|
|
|
184
247
|
cr = asyncio.start_server(
|
|
185
|
-
self.__make_tcp_route_handler(
|
|
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
|
+
),
|
|
186
254
|
route.local_address,
|
|
187
255
|
route.local_port,
|
|
188
256
|
)
|
|
@@ -190,16 +258,31 @@ class Proxy:
|
|
|
190
258
|
loop = asyncio.get_event_loop()
|
|
191
259
|
loop.run_until_complete(cr)
|
|
192
260
|
|
|
193
|
-
async def __pipe(
|
|
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:
|
|
194
268
|
try:
|
|
195
269
|
while not reader.at_eof():
|
|
196
270
|
writer.write(await reader.read(2048))
|
|
271
|
+
|
|
272
|
+
if target_keep_alive_sender is not None:
|
|
273
|
+
target_keep_alive_sender.schedule_send()
|
|
197
274
|
except ConnectionResetError:
|
|
198
275
|
self._log.warning("Connection reset by target %s", target_address)
|
|
199
276
|
finally:
|
|
200
277
|
writer.close()
|
|
201
278
|
|
|
202
|
-
def __make_tcp_route_handler(
|
|
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
|
|
203
286
|
async def handler(local_reader: asyncio.StreamReader, local_writer: asyncio.StreamWriter) -> None:
|
|
204
287
|
if not self._monitor.is_available(target_machine_name):
|
|
205
288
|
await self._wake_up_target(target_machine_name)
|
|
@@ -211,8 +294,8 @@ class Proxy:
|
|
|
211
294
|
self._log.error("Unable to open connection to %s:%d", target_address, target_port)
|
|
212
295
|
raise e
|
|
213
296
|
|
|
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)
|
|
216
299
|
await asyncio.gather(send_pipe, recv_pipe)
|
|
217
300
|
|
|
218
301
|
return handler
|
|
@@ -221,10 +304,27 @@ class Proxy:
|
|
|
221
304
|
target_machine_name = route.target_machine_name
|
|
222
305
|
assert target_machine_name is not None
|
|
223
306
|
|
|
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
|
+
|
|
224
317
|
loop = asyncio.get_event_loop()
|
|
225
318
|
|
|
226
319
|
cr = loop.create_datagram_endpoint(
|
|
227
|
-
lambda: ProxyUdpProtocol(
|
|
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
|
+
),
|
|
228
328
|
local_addr=(route.local_address, route.local_port),
|
|
229
329
|
)
|
|
230
330
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wolsocketproxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -17,6 +17,7 @@ Requires-Dist: aiohttp==3.13.5
|
|
|
17
17
|
Requires-Dist: dataclass-wizard==0.39.1
|
|
18
18
|
Requires-Dist: icmplib==3.0.4
|
|
19
19
|
Requires-Dist: redfish==3.3.5
|
|
20
|
+
Requires-Dist: setproctitle==1.3.7
|
|
20
21
|
Requires-Dist: wakeonlan==3.1.0
|
|
21
22
|
Provides-Extra: dev
|
|
22
23
|
Requires-Dist: wolsocketproxy[lint]; extra == "dev"
|
|
@@ -25,7 +26,7 @@ Provides-Extra: lint
|
|
|
25
26
|
Requires-Dist: ty; extra == "lint"
|
|
26
27
|
Requires-Dist: ruff; extra == "lint"
|
|
27
28
|
Provides-Extra: build
|
|
28
|
-
Requires-Dist: build[virtualenv]==1.
|
|
29
|
+
Requires-Dist: build[virtualenv]==1.5.0; extra == "build"
|
|
29
30
|
Dynamic: license-file
|
|
30
31
|
|
|
31
32
|
# wol-socket-proxy
|
|
@@ -96,10 +97,13 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
96
97
|
// "http" - You must specify "online_check_http_url"
|
|
97
98
|
// "ping" - You must specify "ip_address"
|
|
98
99
|
"online_check_http_url": "http://192.168.1.124/health", // The URL used to check if machine is online, optional
|
|
99
|
-
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is
|
|
100
|
+
"online_check_timeout": 300, // Timeout when checking machine is online, seconds, optional, default is 60
|
|
100
101
|
"online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
|
|
101
102
|
"ipmi_force_reset_if_power_up_failed": true, // Use IPMI power reset if this machine is not online after timeout, optional, default is false
|
|
102
|
-
"ipmi_max_reset_try_count": 3
|
|
103
|
+
"ipmi_max_reset_try_count": 3, // Max IPMI power reset retry count before giving-up, optional, default is 3
|
|
104
|
+
"keep_alive_mode": true, // Indicates whether this machine has a keep-alive daemon, optional, default is false
|
|
105
|
+
// If you enable this, wolsocketproxy will send a request when there is any traffic
|
|
106
|
+
"keep_alive_mode_base_url": "http://192.168.1.124:8080" // Keep-alive daemon provided URL, optional
|
|
103
107
|
},
|
|
104
108
|
// ... more machines ...
|
|
105
109
|
},
|
|
@@ -129,3 +133,37 @@ The `wolsocketproxy.conf` has the following structure:
|
|
|
129
133
|
]
|
|
130
134
|
}
|
|
131
135
|
```
|
|
136
|
+
|
|
137
|
+
## Additional modes
|
|
138
|
+
|
|
139
|
+
### Keep-alive daemon mode
|
|
140
|
+
|
|
141
|
+
This mode is used for running on target machine with auto-suspend (e.g. running `circadian` or similar), to keep the target
|
|
142
|
+
machine from auto suspending when running tasks.
|
|
143
|
+
|
|
144
|
+
Use the following command on the target machine to enter this mode:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
wolsocketproxy -k
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The config file `wolsocketproxy.conf` should look like the following:
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
```json5
|
|
154
|
+
// Keep-alive daemon mode
|
|
155
|
+
{
|
|
156
|
+
"listen_address": "0.0.0.0", // The address to listen for HTTP server, default is 127.0.0.1
|
|
157
|
+
"listen_port": 18080, // The port to listen for HTTP server, default is 18080
|
|
158
|
+
"watchdog_feed_interval": 60, // Watchdog feed interval, seconds, default is 600
|
|
159
|
+
"keep_alive_method": "special_process", // How to keep target machine alive, default is "special_process"
|
|
160
|
+
// "special_process" - Fork a child process with specified name
|
|
161
|
+
"special_process_name": "wsp-keepalive", // The display name of the forked child process, default is "wsp-keepalive"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
You need to periodically send an HTTP GET request to `/watchdog/feed` at the `listen_port` in `watchdog_feed_interval` time,
|
|
166
|
+
or the special process will be killed.
|
|
167
|
+
You can combine this mode with `circadian`'s `process_block` config to keep it from auto-suspending.
|
|
168
|
+
|
|
169
|
+
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.
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import logging
|
|
3
|
-
from argparse import ArgumentParser
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import dataclass_wizard
|
|
7
|
-
|
|
8
|
-
from wolsocketproxy.proxy import Proxy, ProxyConfig
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def main() -> None:
|
|
12
|
-
logging.basicConfig(level=logging.INFO)
|
|
13
|
-
log = logging.getLogger()
|
|
14
|
-
|
|
15
|
-
parser = ArgumentParser(prog="wolsocketproxy", description="A socket proxy with wake-on-lan feature.")
|
|
16
|
-
|
|
17
|
-
parser.add_argument(
|
|
18
|
-
"-c",
|
|
19
|
-
"--config",
|
|
20
|
-
help="The config file to use, default lookup order: /etc/wolsocketproxy.conf, ./wolsocketproxy.conf",
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
args = parser.parse_args()
|
|
24
|
-
config_path: Path
|
|
25
|
-
|
|
26
|
-
if "config" in args and args.config is not None:
|
|
27
|
-
config_path = Path(args.config)
|
|
28
|
-
else:
|
|
29
|
-
config_path = Path("/etc/wolsocketproxy.conf")
|
|
30
|
-
|
|
31
|
-
if not config_path.exists():
|
|
32
|
-
config_path = Path("./wolsocketproxy.conf")
|
|
33
|
-
|
|
34
|
-
if not config_path.exists():
|
|
35
|
-
log.error("Config file path %s does not exist!", config_path)
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
config_data = json.loads(config_path.read_text())
|
|
39
|
-
config = dataclass_wizard.fromdict(ProxyConfig, config_data)
|
|
40
|
-
|
|
41
|
-
log.info("Loaded config from %s", config_path)
|
|
42
|
-
|
|
43
|
-
proxy = Proxy(config)
|
|
44
|
-
proxy.start()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wolsocketproxy-0.2.0 → wolsocketproxy-0.3.1}/src/wolsocketproxy.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|