luxos 0.0.0__py3-none-any.whl
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.
- luxos/__init__.py +0 -0
- luxos/__main__.py +4 -0
- luxos/api.json +215 -0
- luxos/api.py +32 -0
- luxos/asyncops.py +208 -0
- luxos/exceptions.py +24 -0
- luxos/misc.py +31 -0
- luxos/scripts/__init__.py +0 -0
- luxos/scripts/async_luxos.py +54 -0
- luxos/scripts/health_checker.py +862 -0
- luxos/scripts/luxos.py +386 -0
- luxos-0.0.0.0.dist-info/METADATA +100 -0
- luxos-0.0.0.0.dist-info/RECORD +15 -0
- luxos-0.0.0.0.dist-info/WHEEL +4 -0
- luxos-0.0.0.0.dist-info/entry_points.txt +3 -0
luxos/__init__.py
ADDED
|
File without changes
|
luxos/__main__.py
ADDED
luxos/api.json
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
{
|
|
2
|
+
"addgroup": {
|
|
3
|
+
"logon_required": false
|
|
4
|
+
},
|
|
5
|
+
"addpool": {
|
|
6
|
+
"logon_required": false
|
|
7
|
+
},
|
|
8
|
+
"asc": {
|
|
9
|
+
"logon_required": false
|
|
10
|
+
},
|
|
11
|
+
"asccount": {
|
|
12
|
+
"logon_required": false
|
|
13
|
+
},
|
|
14
|
+
"atm": {
|
|
15
|
+
"logon_required": false
|
|
16
|
+
},
|
|
17
|
+
"atmset": {
|
|
18
|
+
"logon_required": true
|
|
19
|
+
},
|
|
20
|
+
"autotunerget": {
|
|
21
|
+
"logon_required": false
|
|
22
|
+
},
|
|
23
|
+
"autotunerset": {
|
|
24
|
+
"logon_required": true
|
|
25
|
+
},
|
|
26
|
+
"check": {
|
|
27
|
+
"logon_required": false
|
|
28
|
+
},
|
|
29
|
+
"coin": {
|
|
30
|
+
"logon_required": false
|
|
31
|
+
},
|
|
32
|
+
"config": {
|
|
33
|
+
"logon_required": false
|
|
34
|
+
},
|
|
35
|
+
"curtail": {
|
|
36
|
+
"logon_required": true
|
|
37
|
+
},
|
|
38
|
+
"devdetails": {
|
|
39
|
+
"logon_required": false
|
|
40
|
+
},
|
|
41
|
+
"devs": {
|
|
42
|
+
"logon_required": false
|
|
43
|
+
},
|
|
44
|
+
"disableboard": {
|
|
45
|
+
"logon_required": true
|
|
46
|
+
},
|
|
47
|
+
"disablepool": {
|
|
48
|
+
"logon_required": false
|
|
49
|
+
},
|
|
50
|
+
"edevs": {
|
|
51
|
+
"logon_required": false
|
|
52
|
+
},
|
|
53
|
+
"enableboard": {
|
|
54
|
+
"logon_required": true
|
|
55
|
+
},
|
|
56
|
+
"enablepool": {
|
|
57
|
+
"logon_required": false
|
|
58
|
+
},
|
|
59
|
+
"estats": {
|
|
60
|
+
"logon_required": false
|
|
61
|
+
},
|
|
62
|
+
"fans": {
|
|
63
|
+
"logon_required": false
|
|
64
|
+
},
|
|
65
|
+
"fanset": {
|
|
66
|
+
"logon_required": true
|
|
67
|
+
},
|
|
68
|
+
"frequencyget": {
|
|
69
|
+
"logon_required": false
|
|
70
|
+
},
|
|
71
|
+
"frequencyset": {
|
|
72
|
+
"logon_required": true
|
|
73
|
+
},
|
|
74
|
+
"frequencystop": {
|
|
75
|
+
"logon_required": true
|
|
76
|
+
},
|
|
77
|
+
"groupquota": {
|
|
78
|
+
"logon_required": false
|
|
79
|
+
},
|
|
80
|
+
"healthchipget": {
|
|
81
|
+
"logon_required": false
|
|
82
|
+
},
|
|
83
|
+
"healthchipset": {
|
|
84
|
+
"logon_required": true
|
|
85
|
+
},
|
|
86
|
+
"healthctrl": {
|
|
87
|
+
"logon_required": false
|
|
88
|
+
},
|
|
89
|
+
"healthctrlset": {
|
|
90
|
+
"logon_required": true
|
|
91
|
+
},
|
|
92
|
+
"immersionswitch": {
|
|
93
|
+
"logon_required": true
|
|
94
|
+
},
|
|
95
|
+
"kill": {
|
|
96
|
+
"logon_required": false
|
|
97
|
+
},
|
|
98
|
+
"lcd": {
|
|
99
|
+
"logon_required": false
|
|
100
|
+
},
|
|
101
|
+
"limits": {
|
|
102
|
+
"logon_required": false
|
|
103
|
+
},
|
|
104
|
+
"logoff": {
|
|
105
|
+
"logon_required": true
|
|
106
|
+
},
|
|
107
|
+
"logon": {
|
|
108
|
+
"logon_required": false
|
|
109
|
+
},
|
|
110
|
+
"logset": {
|
|
111
|
+
"logon_required": false
|
|
112
|
+
},
|
|
113
|
+
"netset": {
|
|
114
|
+
"logon_required": true
|
|
115
|
+
},
|
|
116
|
+
"poolopts": {
|
|
117
|
+
"logon_required": false
|
|
118
|
+
},
|
|
119
|
+
"pooloptsset": {
|
|
120
|
+
"logon_required": false
|
|
121
|
+
},
|
|
122
|
+
"pools": {
|
|
123
|
+
"logon_required": false
|
|
124
|
+
},
|
|
125
|
+
"power": {
|
|
126
|
+
"logon_required": false
|
|
127
|
+
},
|
|
128
|
+
"profileget": {
|
|
129
|
+
"logon_required": false
|
|
130
|
+
},
|
|
131
|
+
"profilenew": {
|
|
132
|
+
"logon_required": true
|
|
133
|
+
},
|
|
134
|
+
"profilerem": {
|
|
135
|
+
"logon_required": true
|
|
136
|
+
},
|
|
137
|
+
"profiles": {
|
|
138
|
+
"logon_required": false
|
|
139
|
+
},
|
|
140
|
+
"profilerestore": {
|
|
141
|
+
"logon_required": true
|
|
142
|
+
},
|
|
143
|
+
"profileset": {
|
|
144
|
+
"logon_required": true
|
|
145
|
+
},
|
|
146
|
+
"reboot": {
|
|
147
|
+
"logon_required": true
|
|
148
|
+
},
|
|
149
|
+
"rebootdevice": {
|
|
150
|
+
"logon_required": true
|
|
151
|
+
},
|
|
152
|
+
"removegroup": {
|
|
153
|
+
"logon_required": false
|
|
154
|
+
},
|
|
155
|
+
"resetminer": {
|
|
156
|
+
"logon_required": true
|
|
157
|
+
},
|
|
158
|
+
"resetconfig": {
|
|
159
|
+
"logon_required": true
|
|
160
|
+
},
|
|
161
|
+
"removepool": {
|
|
162
|
+
"logon_required": false
|
|
163
|
+
},
|
|
164
|
+
"session": {
|
|
165
|
+
"logon_required": false
|
|
166
|
+
},
|
|
167
|
+
"stats": {
|
|
168
|
+
"logon_required": false
|
|
169
|
+
},
|
|
170
|
+
"summary": {
|
|
171
|
+
"logon_required": false
|
|
172
|
+
},
|
|
173
|
+
"switchpool": {
|
|
174
|
+
"logon_required": false
|
|
175
|
+
},
|
|
176
|
+
"tempctrl": {
|
|
177
|
+
"logon_required": false
|
|
178
|
+
},
|
|
179
|
+
"tempctrlset": {
|
|
180
|
+
"logon_required": true
|
|
181
|
+
},
|
|
182
|
+
"temps": {
|
|
183
|
+
"logon_required": false
|
|
184
|
+
},
|
|
185
|
+
"tempsensor": {
|
|
186
|
+
"logon_required": false
|
|
187
|
+
},
|
|
188
|
+
"tempsensorset": {
|
|
189
|
+
"logon_required": true
|
|
190
|
+
},
|
|
191
|
+
"tunerstatus": {
|
|
192
|
+
"logon_required": false
|
|
193
|
+
},
|
|
194
|
+
"tunerswitch": {
|
|
195
|
+
"logon_required": true
|
|
196
|
+
},
|
|
197
|
+
"uninstallluxos": {
|
|
198
|
+
"logon_required": true
|
|
199
|
+
},
|
|
200
|
+
"updaterun": {
|
|
201
|
+
"logon_required": true
|
|
202
|
+
},
|
|
203
|
+
"updateset": {
|
|
204
|
+
"logon_required": true
|
|
205
|
+
},
|
|
206
|
+
"version": {
|
|
207
|
+
"logon_required": false
|
|
208
|
+
},
|
|
209
|
+
"voltageget": {
|
|
210
|
+
"logon_required": false
|
|
211
|
+
},
|
|
212
|
+
"voltageset": {
|
|
213
|
+
"logon_required": true
|
|
214
|
+
}
|
|
215
|
+
}
|
luxos/api.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import json
|
|
5
|
+
import importlib.resources
|
|
6
|
+
|
|
7
|
+
log = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
COMMANDS = json.loads(
|
|
11
|
+
(importlib.resources.files("luxos") / "api.json")
|
|
12
|
+
.read_text()
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def logon_required(cmd: str, commands_list=COMMANDS) -> bool | None:
|
|
17
|
+
# Check if the command requires logon to LuxOS API
|
|
18
|
+
|
|
19
|
+
if cmd not in COMMANDS:
|
|
20
|
+
log.info("%s command is not supported. "
|
|
21
|
+
"Try again with a different command.", cmd)
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
return COMMANDS[cmd]["logon_required"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# TODO timeouts should be float | None
|
|
28
|
+
def execute_command(host: str, port: int, timeout: int, cmd: str, params: list[str], verbose: bool):
|
|
29
|
+
from .scripts import luxos
|
|
30
|
+
return luxos.execute_command(host, port, timeout, cmd, params, verbose)
|
|
31
|
+
|
|
32
|
+
|
luxos/asyncops.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from . import exceptions
|
|
11
|
+
from . import api
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
TIMEOUT = 3.0 # default timeout for operations
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def wrapped(function):
|
|
20
|
+
"""wraps a function acting on a host and re-raise with internal exceptions
|
|
21
|
+
|
|
22
|
+
This re-raise exceptions so they all derive from MinerConnectionError, eg:
|
|
23
|
+
@wrapped
|
|
24
|
+
def somcode(host: str, port: int, ...):
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
await somecode()
|
|
29
|
+
except MinerConnectionError as e: <- this will catch all exceptions!
|
|
30
|
+
e.address
|
|
31
|
+
raise MyNewExecption() from e <- this will re-raise
|
|
32
|
+
"""
|
|
33
|
+
@functools.wraps(function)
|
|
34
|
+
async def _function(host: str, port: int, *args, **kwargs):
|
|
35
|
+
try:
|
|
36
|
+
return await function(host, port, *args, **kwargs)
|
|
37
|
+
except asyncio.TimeoutError as e:
|
|
38
|
+
# we augment underlying TimeOuts
|
|
39
|
+
raise exceptions.MinerCommandTimeoutError(host, port) from e
|
|
40
|
+
except exceptions.MinerConnectionError:
|
|
41
|
+
raise
|
|
42
|
+
except Exception as e:
|
|
43
|
+
# we augment any other exception with (host, port) info
|
|
44
|
+
log.exception("internal error")
|
|
45
|
+
raise exceptions.MinerConnectionError(host, port, "internal error") from e
|
|
46
|
+
return _function
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _roundtrip(
|
|
50
|
+
host: str, port: int, cmd: bytes | str, timeout: float | None
|
|
51
|
+
) -> str:
|
|
52
|
+
"""simple asyncio socket based send/receive function
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
print(await _roundtrip(host, port, version))
|
|
56
|
+
-> (str) "{'STATUS': [{'Code': 22, 'Description'...."
|
|
57
|
+
"""
|
|
58
|
+
reader, writer = await asyncio.wait_for(
|
|
59
|
+
asyncio.open_connection(host, port), timeout
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
writer.write(cmd.encode() if isinstance(cmd, str) else cmd)
|
|
63
|
+
await writer.drain()
|
|
64
|
+
|
|
65
|
+
response = bytearray()
|
|
66
|
+
while True:
|
|
67
|
+
data = await asyncio.wait_for(reader.read(1), timeout=timeout)
|
|
68
|
+
if not data:
|
|
69
|
+
break
|
|
70
|
+
null_index = data.find(b"\x00")
|
|
71
|
+
if null_index >= 0:
|
|
72
|
+
response += data[:null_index]
|
|
73
|
+
break
|
|
74
|
+
response += data
|
|
75
|
+
|
|
76
|
+
return response.decode()
|
|
77
|
+
|
|
78
|
+
# TODO add annotations
|
|
79
|
+
async def roundtrip(
|
|
80
|
+
host: str,
|
|
81
|
+
port: int,
|
|
82
|
+
cmd: bytes | str | dict[str, Any],
|
|
83
|
+
timeout: float | None = None,
|
|
84
|
+
asjson: bool | None = True,
|
|
85
|
+
retry: int = 0,
|
|
86
|
+
retry_delay: float | None = None,
|
|
87
|
+
):
|
|
88
|
+
"""utility wrapper around _roundrip
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
print(await roundtrip(host, port, {"version"}))
|
|
92
|
+
-> (json) {'STATUS': [{'Code': 22, 'Description': 'LUXminer 20 ...
|
|
93
|
+
print(await roundtrip(host, port, "version"))
|
|
94
|
+
-> (str) "{'STATUS': [{'Code': 22, 'Description': 'LUXminer 20 ..
|
|
95
|
+
"""
|
|
96
|
+
timeout = TIMEOUT if timeout is None else timeout
|
|
97
|
+
count = 0
|
|
98
|
+
|
|
99
|
+
if not isinstance(cmd, (bytes, str)):
|
|
100
|
+
cmd = json.dumps(cmd, indent=2, sort_keys=True)
|
|
101
|
+
if asjson is None:
|
|
102
|
+
asjson = True
|
|
103
|
+
|
|
104
|
+
last_exception = None
|
|
105
|
+
while count <= retry:
|
|
106
|
+
try:
|
|
107
|
+
timeout = TIMEOUT if timeout is None else timeout
|
|
108
|
+
res = await _roundtrip(host, port, cmd, timeout)
|
|
109
|
+
if asjson:
|
|
110
|
+
return json.loads(res)
|
|
111
|
+
else:
|
|
112
|
+
return res
|
|
113
|
+
except (Exception, asyncio.TimeoutError) as e:
|
|
114
|
+
last_exception = e
|
|
115
|
+
if retry_delay:
|
|
116
|
+
await asyncio.sleep(retry_delay)
|
|
117
|
+
count += 1
|
|
118
|
+
if last_exception is not None:
|
|
119
|
+
raise last_exception
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate_message(
|
|
123
|
+
host: str,
|
|
124
|
+
port: int,
|
|
125
|
+
res: dict[str, Any],
|
|
126
|
+
extrakey: str | None = None,
|
|
127
|
+
minfields: None | int = None,
|
|
128
|
+
maxfields: None | int = None,
|
|
129
|
+
) -> dict[str, Any]:
|
|
130
|
+
# all miner message comes with a STATUS
|
|
131
|
+
for key in ["STATUS", "id", *([extrakey] if extrakey else [])]:
|
|
132
|
+
if key in res:
|
|
133
|
+
continue
|
|
134
|
+
raise exceptions.MinerCommandMalformedMessageError(host, port, f"missing {key} from logon message", res)
|
|
135
|
+
|
|
136
|
+
if not extrakey or not (minfields or maxfields):
|
|
137
|
+
return res
|
|
138
|
+
|
|
139
|
+
n = len(res[extrakey])
|
|
140
|
+
msg = None
|
|
141
|
+
if minfields and (n < minfields):
|
|
142
|
+
msg = f"found {n} fields for {extrakey} invalid: " f"{n} <= {minfields}"
|
|
143
|
+
elif maxfields and (n > maxfields):
|
|
144
|
+
msg = f"found {n} fields for {extrakey} invalid: " f"{n} >= {maxfields}"
|
|
145
|
+
if msg is None:
|
|
146
|
+
return res[extrakey]
|
|
147
|
+
raise exceptions.MinerCommandMalformedMessageError(host, port, msg, res)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@wrapped
|
|
151
|
+
async def logon(host: str, port: int, timeout: float | None = 3) -> str:
|
|
152
|
+
timeout = TIMEOUT if timeout is None else timeout
|
|
153
|
+
res = await roundtrip(host, port, {"command": "logon"}, timeout)
|
|
154
|
+
|
|
155
|
+
# when we first logon, we'll receive a token (session_id)
|
|
156
|
+
# [STATUS][SessionID]
|
|
157
|
+
# on subsequent logon, we receive a
|
|
158
|
+
# [STATUS][Msg] == "Another session is active" ([STATUS][Code] 402)
|
|
159
|
+
if "SESSION" not in res and res.get("STATUS", [{}])[0].get("Code") == 402:
|
|
160
|
+
raise exceptions.MinerCommandSessionAlreadyActive(host, port, "connection active", res)
|
|
161
|
+
sessions = validate_message(host, port, res, "SESSION", 1, 1)
|
|
162
|
+
|
|
163
|
+
session = sessions[0] # type: ignore
|
|
164
|
+
|
|
165
|
+
if "SessionID" not in session:
|
|
166
|
+
raise exceptions.MinerCommandSessionAlreadyActive(host, port, "no SessionID in data", res)
|
|
167
|
+
return str(session["SessionID"])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@wrapped
|
|
171
|
+
async def logoff(
|
|
172
|
+
host: str, port: int, sid: str, timeout: float | None = 3
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
timeout = TIMEOUT if timeout is None else timeout
|
|
175
|
+
return await roundtrip(host, port, {"command": "logoff", "parameter": sid}, timeout)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@wrapped
|
|
179
|
+
async def execute_command(
|
|
180
|
+
host: str,
|
|
181
|
+
port: int,
|
|
182
|
+
timeout_sec: float | None,
|
|
183
|
+
cmd: str,
|
|
184
|
+
parameters: list[str] | None = None,
|
|
185
|
+
verbose: bool = False,
|
|
186
|
+
asjson: bool | None = True,
|
|
187
|
+
add_address: bool = False
|
|
188
|
+
) -> tuple[tuple[str, int], dict[str, Any]] | dict[str, Any]:
|
|
189
|
+
timeout = TIMEOUT if timeout_sec is None else timeout_sec
|
|
190
|
+
parameters = parameters or []
|
|
191
|
+
|
|
192
|
+
sid = None
|
|
193
|
+
if api.logon_required(cmd):
|
|
194
|
+
sid = await logon(host, port)
|
|
195
|
+
parameters = [sid, *parameters]
|
|
196
|
+
log.info("session id requested & obtained for %s:%i (%s)", host, port, sid)
|
|
197
|
+
else:
|
|
198
|
+
log.debug("no logon required for command %s on %s:%i", cmd, host, port)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
packet = {"command": cmd}
|
|
202
|
+
if parameters:
|
|
203
|
+
packet["parameter"] = ",".join(parameters)
|
|
204
|
+
ret = await roundtrip(host, port, packet, timeout, asjson=asjson)
|
|
205
|
+
return ((host, port), ret) if add_address else ret
|
|
206
|
+
finally:
|
|
207
|
+
if sid:
|
|
208
|
+
await logoff(host, port, sid)
|
luxos/exceptions.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class LuxosBaseException(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MinerConnectionError(LuxosBaseException):
|
|
6
|
+
def __init__(self, host: str, port: int, *args, **kwargs):
|
|
7
|
+
super().__init__(host, port, *args, **kwargs)
|
|
8
|
+
self.address = (host, port)
|
|
9
|
+
|
|
10
|
+
def __str__(self):
|
|
11
|
+
return (f"<{self.address[0]}:{self.address[1]}>: {self.__class__.__name__}, "
|
|
12
|
+
f"{self.args[2] if self.args[2:] else 'unknown reason'}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MinerCommandSessionAlreadyActive(MinerConnectionError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MinerCommandTimeoutError(MinerConnectionError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MinerCommandMalformedMessageError(MinerConnectionError):
|
|
24
|
+
pass
|
luxos/misc.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# catch-all module (find later a better place)
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import sys
|
|
4
|
+
import itertools
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if sys.version_info >= (3, 12):
|
|
8
|
+
batched = itertools.batched
|
|
9
|
+
else:
|
|
10
|
+
def batched(iterable, n):
|
|
11
|
+
if n < 1:
|
|
12
|
+
raise ValueError("n must be at least one")
|
|
13
|
+
it = iter(iterable)
|
|
14
|
+
while batch := tuple(itertools.islice(it, n)):
|
|
15
|
+
yield batch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def indent(txt: str, pre: str = " " * 2) -> str:
|
|
19
|
+
"""simple text indentation"""
|
|
20
|
+
|
|
21
|
+
from textwrap import dedent
|
|
22
|
+
|
|
23
|
+
txt = dedent(txt)
|
|
24
|
+
if txt.endswith("\n"):
|
|
25
|
+
last_eol = "\n"
|
|
26
|
+
txt = txt[:-1]
|
|
27
|
+
else:
|
|
28
|
+
last_eol = ""
|
|
29
|
+
|
|
30
|
+
result = pre + txt.replace("\n", "\n" + pre) + last_eol
|
|
31
|
+
return result if result.strip() else result.strip()
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from .. import misc
|
|
6
|
+
from .. import asyncops
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def run(
|
|
10
|
+
ip_list: list[str],
|
|
11
|
+
port: int,
|
|
12
|
+
cmd: str,
|
|
13
|
+
params: list[str],
|
|
14
|
+
timeout: float,
|
|
15
|
+
delay: float | None,
|
|
16
|
+
details: bool,
|
|
17
|
+
batchsize: int = 0,
|
|
18
|
+
) -> None:
|
|
19
|
+
result = {}
|
|
20
|
+
batchsize = max(batchsize, 2)
|
|
21
|
+
|
|
22
|
+
for grupid, addresses in enumerate(
|
|
23
|
+
misc.batched([(ip, port) for ip in ip_list], n=batchsize)
|
|
24
|
+
):
|
|
25
|
+
tasks = []
|
|
26
|
+
for host, port in addresses:
|
|
27
|
+
tasks.append(
|
|
28
|
+
asyncops.execute_command(
|
|
29
|
+
host, port, timeout, cmd, params, add_address=True
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
result[grupid] = await asyncio.gather(*tasks, return_exceptions=True)
|
|
33
|
+
|
|
34
|
+
# runs only on batchsize, wait delay then proceed onto the next batch
|
|
35
|
+
if delay:
|
|
36
|
+
await asyncio.sleep(delay)
|
|
37
|
+
|
|
38
|
+
alltasks = [task for group in result.values() for task in group]
|
|
39
|
+
successes = [task for task in alltasks if not isinstance(task, Exception)]
|
|
40
|
+
failures = [task for task in alltasks if isinstance(task, Exception)]
|
|
41
|
+
|
|
42
|
+
# print a nice report
|
|
43
|
+
print(f"task executed sucessfully: {len(successes)}")
|
|
44
|
+
if details:
|
|
45
|
+
for (host, port), task in successes: # type: ignore
|
|
46
|
+
print(f" > {host}:{port}")
|
|
47
|
+
print(misc.indent(json.dumps(task, indent=2, sort_keys=True), pre=" | "))
|
|
48
|
+
print(f"task executed failures: {len(failures)}")
|
|
49
|
+
for failure in failures:
|
|
50
|
+
print(f" {failure}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(*args, **kwargs) -> None:
|
|
54
|
+
asyncio.run(run(*args, **kwargs))
|