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.
- wolsocketproxy-0.2.0/PKG-INFO +131 -0
- wolsocketproxy-0.2.0/README.md +101 -0
- wolsocketproxy-0.2.0/pyproject.toml +72 -0
- wolsocketproxy-0.2.0/setup.cfg +4 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy/monitor.py +129 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy/proxy.py +318 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy/utils.py +21 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy.egg-info/PKG-INFO +131 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy.egg-info/SOURCES.txt +14 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy.egg-info/dependency_links.txt +1 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy.egg-info/entry_points.txt +2 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy.egg-info/requires.txt +16 -0
- wolsocketproxy-0.2.0/src/wolsocketproxy.egg-info/top_level.txt +1 -0
- wolsocketproxy-0.1.0/PKG-INFO +0 -94
- wolsocketproxy-0.1.0/README.md +0 -69
- wolsocketproxy-0.1.0/pyproject.toml +0 -69
- wolsocketproxy-0.1.0/wolsocketproxy/monitor.py +0 -48
- wolsocketproxy-0.1.0/wolsocketproxy/proxy.py +0 -175
- {wolsocketproxy-0.1.0 → wolsocketproxy-0.2.0}/LICENSE +0 -0
- {wolsocketproxy-0.1.0 → wolsocketproxy-0.2.0/src}/wolsocketproxy/__init__.py +0 -0
- {wolsocketproxy-0.1.0 → wolsocketproxy-0.2.0/src}/wolsocketproxy/__main__.py +0 -0
|
@@ -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,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)
|