Skip to content

Commit 1b52cf9

Browse files
committed
Support power up target through IPMI.
1 parent 08c0c6b commit 1b52cf9

6 files changed

Lines changed: 1003 additions & 62 deletions

File tree

docker/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_NO_DEV=1 UV_PYTHON_DOWNLOADS=0
77
WORKDIR /app
88

99
RUN --mount=type=cache,target=/root/.cache/uv \
10-
--mount=type=bind,source=uv.lock,target=uv.lock \
11-
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
10+
--mount=type=bind,source=uv.lock,target=uv.lock,relabel=private \
11+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml,relabel=private \
1212
uv sync --locked --no-install-project
1313

1414
COPY . /app

pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,26 @@ classifiers = [
1919
]
2020
requires-python = ">=3.12"
2121
dependencies = [
22+
"aiohttp==3.13.5",
2223
"dataclass-wizard==0.39.1",
2324
"icmplib==3.0.4",
2425
"redfish==3.3.5",
2526
"wakeonlan==3.1.0",
2627
]
2728

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+
2842
[project.urls]
2943
source = "https://github.com/notsyncing/wolsocketproxy"
3044
issues = "https://github.com/notsyncing/wol-socket-proxy/issues"
@@ -48,6 +62,7 @@ lint.ignore = [
4862
"D203",
4963
"D211",
5064
"D213",
65+
"S101",
5166
"EM102",
5267
"FBT001",
5368
"FBT003",

src/wolsocketproxy/monitor.py

Lines changed: 108 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,131 @@
1+
import asyncio
12
import logging
2-
import time
3-
from collections.abc import Collection
3+
from dataclasses import dataclass
44
from logging import Logger
55
from threading import Thread
6+
from typing import Literal
67

7-
from icmplib import multiping
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
819

920

1021
class Monitor:
1122
_log: Logger = logging.getLogger()
12-
_ip_state: dict[str, bool]
23+
_monitor_configs: dict[str, MonitorConfig]
24+
_machine_states: dict[str, bool]
25+
_stop: bool = False
1326

14-
def __init__(self, watching_ip_list: Collection[str]) -> None:
15-
self._ip_state = {}
27+
def __init__(self, watching_machines: dict[str, MonitorConfig]) -> None:
28+
self._monitor_configs = {}
29+
self._machine_states = {}
1630

17-
for ip in watching_ip_list:
18-
self._ip_state[ip] = False
31+
for name, config in watching_machines.items():
32+
self._monitor_configs[name] = config
33+
self._machine_states[name] = False
1934

2035
def start(self) -> None:
21-
t = Thread(target=self.__check_ip_state)
22-
t.daemon = True
36+
def _start() -> None:
37+
asyncio.run(self.__check_machine_states())
38+
39+
t = Thread(target=_start, daemon=True)
2340
t.start()
2441

25-
def __check_ip_state(self) -> None:
26-
self._log.info("IP monitor is started.")
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,
67+
conf.online_check_http_url,
68+
timeout=ClientTimeout(total=conf.online_check_timeout)
69+
)
70+
71+
result_names.append(name)
72+
result_futures.append(resp_future)
73+
74+
http_results = await asyncio.gather(*result_futures)
2775

28-
while True:
29-
ip_list = self._ip_state.keys()
30-
results = multiping(ip_list, count=3, timeout=2, privileged=False)
76+
for i, http_result in enumerate(http_results):
77+
results[result_names[i]] = http_result == conf.online_check_http_expected_code
3178

32-
for result in results:
33-
original_state = self._ip_state[result.address]
34-
self._ip_state[result.address] = result.is_alive
79+
return results
80+
81+
async def __check_machine_states(self) -> None:
82+
self._log.info("Monitor started.")
83+
84+
while not self._stop:
85+
ping_ip_list = [
86+
conf.online_check_ip_address
87+
for conf in self._monitor_configs.values()
88+
if conf.online_check_method == "ping"
89+
]
90+
91+
if len(ping_ip_list) > 0:
92+
ping_results_future = async_multiping(ping_ip_list, count=3, timeout=2, privileged=False)
93+
else:
94+
ping_results_future = asyncio.Future()
95+
ping_results_future.set_result([])
96+
97+
http_results_future = self.__check_http_urls(
98+
configs={
99+
name: conf for name, conf in self._monitor_configs.items() if conf.online_check_method == "http"
100+
}
101+
)
102+
103+
ping_results, http_results = await asyncio.gather(ping_results_future, http_results_future)
104+
105+
for result in ping_results:
106+
original_state = self._machine_states[result.address]
107+
self._machine_states[result.address] = result.is_alive
35108

36109
if original_state != result.is_alive:
37110
if original_state is False:
38-
self._log.info("Target %s now online.", result.address)
111+
self._log.info("Target %s now online by ping.", result.address)
112+
else:
113+
self._log.info("Target %s now offline by ping.", result.address)
114+
115+
for ip, result in http_results.items():
116+
original_state = self._machine_states[ip]
117+
self._machine_states[ip] = result
118+
119+
if original_state != result:
120+
if original_state is False:
121+
self._log.info("Target %s now online by http.", ip)
39122
else:
40-
self._log.info("Target %s now offline.", result.address)
123+
self._log.info("Target %s now offline by http.", ip)
41124

42-
time.sleep(1)
125+
await asyncio.sleep(1)
43126

44-
def report_availablity(self, ip: str, available: bool) -> None:
45-
self._ip_state[ip] = available
127+
def report_availablity(self, machine_name: str, available: bool) -> None:
128+
self._machine_states[machine_name] = available
46129

47-
def is_available(self, ip: str) -> bool:
48-
return self._ip_state.get(ip, False)
130+
def is_available(self, machine_name: str) -> bool:
131+
return self._machine_states.get(machine_name, False)

0 commit comments

Comments
 (0)