wolsocketproxy 0.1.0__tar.gz → 0.2.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.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: wolsocketproxy
3
+ Version: 0.2.0
4
+ Summary: A socket proxy with wake-on-lan feature.
5
+ Author-email: Song Fuchang <song.fc@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: source, https://github.com/notsyncing/wolsocketproxy
8
+ Project-URL: issues, https://github.com/notsyncing/wol-socket-proxy/issues
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Topic :: System :: Networking
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: aiohttp==3.13.5
17
+ Requires-Dist: dataclass-wizard==0.39.1
18
+ Requires-Dist: icmplib==3.0.4
19
+ Requires-Dist: redfish==3.3.5
20
+ Requires-Dist: wakeonlan==3.1.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: wolsocketproxy[lint]; extra == "dev"
23
+ Requires-Dist: wolsocketproxy[build]; extra == "dev"
24
+ Provides-Extra: lint
25
+ Requires-Dist: ty; extra == "lint"
26
+ Requires-Dist: ruff; extra == "lint"
27
+ Provides-Extra: build
28
+ Requires-Dist: build[virtualenv]==1.4.2; extra == "build"
29
+ Dynamic: license-file
30
+
31
+ # wol-socket-proxy
32
+
33
+ A socket proxy with wake-on-lan and IPMI power control feature.
34
+
35
+ It can forward TCP(bidirectional) and UDP(send-only) traffic from local machine to remote machine, and:
36
+
37
+ - send a magic packet to wake it (wake-on-lan)
38
+ - invoke IPMI power on to wake it
39
+
40
+ before forwarding traffic if the remote machine does not respond to ICMP ping or some HTTP URL.
41
+
42
+ ## Requirements
43
+
44
+ Python 3.12+
45
+
46
+ The remote machine must accept and respond to ICMP ping requests if you use `ping` method.
47
+
48
+ ## Usage
49
+
50
+ You can install it from PyPI or use a prebuilt docker image.
51
+
52
+ ### Install from PyPI
53
+
54
+ Simply install it:
55
+
56
+ ```
57
+ pip install wolsocketproxy
58
+ ```
59
+
60
+ Then create a config file:
61
+
62
+ ```
63
+ cp wolsocketproxy.conf.example /etc/wolsocketproxy.conf
64
+ ```
65
+
66
+ Modify the config file as your requirements.
67
+
68
+ Finally, run it:
69
+
70
+ ```
71
+ wolsocketproxy
72
+ ```
73
+
74
+ ### Use prebuilt docker image
75
+
76
+ See `docker/docker-compose.yml` for more details.
77
+
78
+ ## Config
79
+
80
+ The `wolsocketproxy.conf` has the following structure:
81
+
82
+ ```json5
83
+ {
84
+ // NOTE: Comments are not allowed in actual config file
85
+
86
+ // Define machines
87
+ "machines": {
88
+ "some-server": {
89
+ "ip_address": "192.168.1.124", // the IP address of this machine, optional
90
+ "mac_address": "11:22:33:44:55:66", // The MAC address of this machine, optional
91
+ "wake_up_method": "ipmi", // How to wake up this machine
92
+ // "ipmi" - You must specify "ipmi_config_name"
93
+ // "wol" - You must specify "mac_address"
94
+ "ipmi_config_name": "some-server-ipmi", // IPMI config of this machine, must match what defined in "ipmi_configs"
95
+ "online_check_method": "http", // How to check this machine is online
96
+ // "http" - You must specify "online_check_http_url"
97
+ // "ping" - You must specify "ip_address"
98
+ "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 300
100
+ "online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
101
+ "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 // Max IPMI power reset retry count before giving-up, optional, default is 3
103
+ },
104
+ // ... more machines ...
105
+ },
106
+
107
+ // Define forwarding routes
108
+ "routes": [
109
+ {
110
+ "local_address": "0.0.0.0", // The local address to listen
111
+ "local_port": 12345, // The local port to listen
112
+ "target_machine_name": "some-server", // The target machine name, must match what defined in "machines"
113
+ "target_address": "192.168.1.124", // The target address forwarding to
114
+ "target_port": 12345, // The target port forwarding to
115
+ "protocol": "tcp" // The protocol to use, can choose "tcp" or "udp"
116
+ },
117
+ // ... more routes ...
118
+ ],
119
+
120
+ // Define IPMI configs, optional
121
+ "ipmi_configs": [
122
+ {
123
+ "name": "some-server-ipmi", // IPMI config name
124
+ "redfish_url": "https://192.168.1.123/", // IPMI Redfish API base URL (should not contain /redfish/v1)
125
+ "username": "admin", // IPMI Redfish API login username
126
+ "password": "123456" // IPMI Redfish API login password
127
+ },
128
+ // ... more IPMI configs ...
129
+ ]
130
+ }
131
+ ```
@@ -0,0 +1,101 @@
1
+ # wol-socket-proxy
2
+
3
+ A socket proxy with wake-on-lan and IPMI power control feature.
4
+
5
+ It can forward TCP(bidirectional) and UDP(send-only) traffic from local machine to remote machine, and:
6
+
7
+ - send a magic packet to wake it (wake-on-lan)
8
+ - invoke IPMI power on to wake it
9
+
10
+ before forwarding traffic if the remote machine does not respond to ICMP ping or some HTTP URL.
11
+
12
+ ## Requirements
13
+
14
+ Python 3.12+
15
+
16
+ The remote machine must accept and respond to ICMP ping requests if you use `ping` method.
17
+
18
+ ## Usage
19
+
20
+ You can install it from PyPI or use a prebuilt docker image.
21
+
22
+ ### Install from PyPI
23
+
24
+ Simply install it:
25
+
26
+ ```
27
+ pip install wolsocketproxy
28
+ ```
29
+
30
+ Then create a config file:
31
+
32
+ ```
33
+ cp wolsocketproxy.conf.example /etc/wolsocketproxy.conf
34
+ ```
35
+
36
+ Modify the config file as your requirements.
37
+
38
+ Finally, run it:
39
+
40
+ ```
41
+ wolsocketproxy
42
+ ```
43
+
44
+ ### Use prebuilt docker image
45
+
46
+ See `docker/docker-compose.yml` for more details.
47
+
48
+ ## Config
49
+
50
+ The `wolsocketproxy.conf` has the following structure:
51
+
52
+ ```json5
53
+ {
54
+ // NOTE: Comments are not allowed in actual config file
55
+
56
+ // Define machines
57
+ "machines": {
58
+ "some-server": {
59
+ "ip_address": "192.168.1.124", // the IP address of this machine, optional
60
+ "mac_address": "11:22:33:44:55:66", // The MAC address of this machine, optional
61
+ "wake_up_method": "ipmi", // How to wake up this machine
62
+ // "ipmi" - You must specify "ipmi_config_name"
63
+ // "wol" - You must specify "mac_address"
64
+ "ipmi_config_name": "some-server-ipmi", // IPMI config of this machine, must match what defined in "ipmi_configs"
65
+ "online_check_method": "http", // How to check this machine is online
66
+ // "http" - You must specify "online_check_http_url"
67
+ // "ping" - You must specify "ip_address"
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 300
70
+ "online_check_http_expected_code": 200, // Expected HTTP status code when using "http" method, optional, default is 200
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 // Max IPMI power reset retry count before giving-up, optional, default is 3
73
+ },
74
+ // ... more machines ...
75
+ },
76
+
77
+ // Define forwarding routes
78
+ "routes": [
79
+ {
80
+ "local_address": "0.0.0.0", // The local address to listen
81
+ "local_port": 12345, // The local port to listen
82
+ "target_machine_name": "some-server", // The target machine name, must match what defined in "machines"
83
+ "target_address": "192.168.1.124", // The target address forwarding to
84
+ "target_port": 12345, // The target port forwarding to
85
+ "protocol": "tcp" // The protocol to use, can choose "tcp" or "udp"
86
+ },
87
+ // ... more routes ...
88
+ ],
89
+
90
+ // Define IPMI configs, optional
91
+ "ipmi_configs": [
92
+ {
93
+ "name": "some-server-ipmi", // IPMI config name
94
+ "redfish_url": "https://192.168.1.123/", // IPMI Redfish API base URL (should not contain /redfish/v1)
95
+ "username": "admin", // IPMI Redfish API login username
96
+ "password": "123456" // IPMI Redfish API login password
97
+ },
98
+ // ... more IPMI configs ...
99
+ ]
100
+ }
101
+ ```
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wolsocketproxy"
7
+ version = "0.2.0"
8
+ description = "A socket proxy with wake-on-lan feature."
9
+ authors = [
10
+ {name = "Song Fuchang", email = "song.fc@gmail.com"}
11
+ ]
12
+ license = "Apache-2.0"
13
+ readme = "README.md"
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: System Administrators",
18
+ "Topic :: System :: Networking"
19
+ ]
20
+ requires-python = ">=3.12"
21
+ dependencies = [
22
+ "aiohttp==3.13.5",
23
+ "dataclass-wizard==0.39.1",
24
+ "icmplib==3.0.4",
25
+ "redfish==3.3.5",
26
+ "wakeonlan==3.1.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "wolsocketproxy[lint]",
32
+ "wolsocketproxy[build]",
33
+ ]
34
+ lint = [
35
+ "ty",
36
+ "ruff",
37
+ ]
38
+ build = [
39
+ "build[virtualenv]==1.4.2",
40
+ ]
41
+
42
+ [project.urls]
43
+ source = "https://github.com/notsyncing/wolsocketproxy"
44
+ issues = "https://github.com/notsyncing/wol-socket-proxy/issues"
45
+
46
+ [project.scripts]
47
+ wolsocketproxy = "wolsocketproxy:main"
48
+
49
+ [tool.ruff]
50
+ line-length = 120
51
+ src = ["src"]
52
+ target-version = "py312"
53
+ lint.select = ["ALL"]
54
+ lint.ignore = [
55
+ "COM812",
56
+ "D100",
57
+ "D101",
58
+ "D102",
59
+ "D103",
60
+ "D104",
61
+ "D107",
62
+ "D203",
63
+ "D211",
64
+ "D213",
65
+ "S101",
66
+ "EM102",
67
+ "FBT001",
68
+ "FBT003",
69
+ "ISC001",
70
+ "TRY003",
71
+ "TRY201",
72
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,129 @@
1
+ import asyncio
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from logging import Logger
5
+ from threading import Thread
6
+ from typing import Literal
7
+
8
+ from aiohttp import ClientConnectionError, ClientSession, ClientTimeout
9
+ from icmplib.multiping import async_multiping
10
+
11
+
12
+ @dataclass
13
+ class MonitorConfig:
14
+ online_check_method: Literal["ping", "http"] = "ping"
15
+ online_check_ip_address: str | None = None
16
+ online_check_http_url: str | None = None
17
+ online_check_http_expected_code: int = 200
18
+ online_check_timeout: int = 60
19
+
20
+
21
+ class Monitor:
22
+ _log: Logger = logging.getLogger()
23
+ _monitor_configs: dict[str, MonitorConfig]
24
+ _machine_states: dict[str, bool]
25
+ _stop: bool = False
26
+
27
+ def __init__(self, watching_machines: dict[str, MonitorConfig]) -> None:
28
+ self._monitor_configs = {}
29
+ self._machine_states = {}
30
+
31
+ for name, config in watching_machines.items():
32
+ self._monitor_configs[name] = config
33
+ self._machine_states[name] = False
34
+
35
+ def start(self) -> None:
36
+ def _start() -> None:
37
+ asyncio.run(self.__check_machine_states())
38
+
39
+ t = Thread(target=_start, daemon=True)
40
+ t.start()
41
+
42
+ def stop(self) -> None:
43
+ self._stop = True
44
+
45
+ async def __check_http_urls(self, configs: dict[str, MonitorConfig]) -> dict[str, bool]:
46
+ if len(configs) <= 0:
47
+ return {}
48
+
49
+ async def fetch(session: ClientSession, url: str, timeout: ClientTimeout) -> int: # noqa: ASYNC109
50
+ try:
51
+ async with session.get(url, timeout=timeout) as resp:
52
+ return resp.status
53
+ except ClientConnectionError:
54
+ return -1
55
+
56
+ results = {}
57
+ result_names = []
58
+ result_futures = []
59
+
60
+ async with ClientSession(timeout=ClientTimeout(total=30)) as session:
61
+ for name, conf in configs.items():
62
+ assert conf.online_check_method == "http"
63
+ assert conf.online_check_http_url is not None
64
+
65
+ resp_future = fetch(
66
+ session, conf.online_check_http_url, timeout=ClientTimeout(total=conf.online_check_timeout)
67
+ )
68
+
69
+ result_names.append(name)
70
+ result_futures.append(resp_future)
71
+
72
+ http_results = await asyncio.gather(*result_futures)
73
+
74
+ for i, http_result in enumerate(http_results):
75
+ results[result_names[i]] = http_result == conf.online_check_http_expected_code
76
+
77
+ return results
78
+
79
+ async def __check_machine_states(self) -> None:
80
+ self._log.info("Monitor started.")
81
+
82
+ while not self._stop:
83
+ ping_ip_list = [
84
+ conf.online_check_ip_address
85
+ for conf in self._monitor_configs.values()
86
+ if conf.online_check_method == "ping"
87
+ ]
88
+
89
+ if len(ping_ip_list) > 0:
90
+ ping_results_future = async_multiping(ping_ip_list, count=3, timeout=2, privileged=False)
91
+ else:
92
+ ping_results_future = asyncio.Future()
93
+ ping_results_future.set_result([])
94
+
95
+ http_results_future = self.__check_http_urls(
96
+ configs={
97
+ name: conf for name, conf in self._monitor_configs.items() if conf.online_check_method == "http"
98
+ }
99
+ )
100
+
101
+ ping_results, http_results = await asyncio.gather(ping_results_future, http_results_future)
102
+
103
+ for result in ping_results:
104
+ original_state = self._machine_states[result.address]
105
+ self._machine_states[result.address] = result.is_alive
106
+
107
+ if original_state != result.is_alive:
108
+ if original_state is False:
109
+ self._log.info("Target %s now online by ping.", result.address)
110
+ else:
111
+ self._log.info("Target %s now offline by ping.", result.address)
112
+
113
+ for ip, result in http_results.items():
114
+ original_state = self._machine_states[ip]
115
+ self._machine_states[ip] = result
116
+
117
+ if original_state != result:
118
+ if original_state is False:
119
+ self._log.info("Target %s now online by http.", ip)
120
+ else:
121
+ self._log.info("Target %s now offline by http.", ip)
122
+
123
+ await asyncio.sleep(1)
124
+
125
+ def report_availablity(self, machine_name: str, available: bool) -> None:
126
+ self._machine_states[machine_name] = available
127
+
128
+ def is_available(self, machine_name: str) -> bool:
129
+ return self._machine_states.get(machine_name, False)