Skip to content

Commit 08104c6

Browse files
committed
code cleanup
1 parent 2a5686b commit 08104c6

File tree

2 files changed

+329
-25
lines changed

2 files changed

+329
-25
lines changed

src/luxos/__main__.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/luxos/api.py

Lines changed: 329 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from __future__ import annotations
2-
import ipaddress
2+
import os
3+
import csv
34
import json
5+
import time
6+
import socket
47
import logging
8+
import argparse
9+
import ipaddress
10+
import threading
511
from typing import Any
612
import importlib.resources
713

@@ -15,18 +21,135 @@
1521
)
1622

1723

18-
def generate_ip_range(start_ip: str, end_ip: str) -> list[str]:
19-
# Generate a list of IP addresses from the start and end IP
20-
start = ipaddress.IPv4Address(start_ip)
21-
end = ipaddress.IPv4Address(end_ip)
24+
# internal_send_cgminer_command sends a command to the cgminer API server and returns the response.
25+
def internal_send_cgminer_command(host: str, port: int, command: str,
26+
timeout_sec: int, verbose: bool) -> str:
2227

23-
ip_list = []
28+
# Create a socket connection to the server
29+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
30+
try:
31+
# set timeout
32+
sock.settimeout(timeout_sec)
33+
34+
# Connect to the server
35+
sock.connect((host, port))
36+
37+
# Send the command to the server
38+
sock.sendall(command.encode())
39+
40+
# Receive the response from the server
41+
response = bytearray()
42+
while True:
43+
# Read one byte at a time so we can wait for the null terminator.
44+
# this is to avoid waiting for the timeout as we don't know how long
45+
# the response will be and socket.recv() will block until reading
46+
# the specified number of bytes.
47+
try:
48+
data = sock.recv(1)
49+
except socket.timeout:
50+
# Timeout occurred, check if we have any data so far
51+
if len(response) == 0:
52+
raise ValueError("timeout waiting for data")
53+
else:
54+
break
55+
if not data:
56+
break
57+
null_index = data.find(b'\x00')
58+
if null_index >= 0:
59+
response += data[:null_index]
60+
break
61+
response += data
62+
63+
# Parse the response JSON
64+
r = json.loads(response.decode())
65+
log.debug(r)
66+
return r
67+
68+
except socket.error as e:
69+
raise e
70+
71+
72+
# send_cgminer_command sends a command to the cgminer API server and returns the response.
73+
def send_cgminer_command(host: str, port: int, cmd: str, param: str,
74+
timeout: int, verbose: bool) -> str:
75+
req = str(f"{{\"command\": \"{cmd}\", \"parameter\": \"{param}\"}}\n")
76+
log.debug(f"Executing command: {cmd} with params: {param} to host: {host}")
77+
78+
return internal_send_cgminer_command(host, port, req, timeout, verbose)
79+
80+
81+
# send_cgminer_simple_command sends a command with no params to the miner and returns the response.
82+
def send_cgminer_simple_command(host: str, port: int, cmd: str, timeout: int,
83+
verbose: bool) -> str:
84+
req = str(f"{{\"command\": \"{cmd}\"}}\n")
85+
log.debug(f"Executing command: {cmd} to host: {host}")
86+
return internal_send_cgminer_command(host, port, req, timeout, verbose)
87+
88+
89+
# check_res_structure checks that the response has the expected structure.
90+
def check_res_structure(res: str, structure: str, min: int, max: int) -> str:
91+
# Check the structure of the response.
92+
if structure not in res or "STATUS" not in res or "id" not in res:
93+
raise ValueError("error: invalid response structure")
94+
95+
# Should we check min and max?
96+
if min >= 0 and max >= 0:
97+
# Check the number of structure elements.
98+
if not (min <= len(res[structure]) <= max):
99+
raise ValueError(
100+
f"error: unexpected number of {structure} in response; min: {min}, max: {max}, actual: {len(res[structure])}"
101+
)
102+
103+
# Should we check only min?
104+
if min >= 0:
105+
# Check the minimum number of structure elements.
106+
if len(res[structure]) < min:
107+
raise ValueError(
108+
f"error: unexpected number of {structure} in response; min: {min}, max: {max}, actual: {len(res[structure])}"
109+
)
110+
111+
# Should we check only max?
112+
if max >= 0:
113+
# Check the maximum number of structure elements.
114+
if len(res[structure]) < min:
115+
raise ValueError(
116+
f"error: unexpected number of {structure} in response; min: {min}, max: {max}, actual: {len(res[structure])}"
117+
)
118+
119+
return res
120+
121+
122+
# get_str_field tries to get the field as a string and returns it.
123+
def get_str_field(struct: str, name: str) -> str:
124+
try:
125+
s = str(struct[name])
126+
except Exception as e:
127+
raise ValueError(f"error: invalid {name} str field: {e}")
128+
129+
return s
130+
131+
132+
def logon(host: str, port: int, timeout: int, verbose: bool) -> str:
133+
# Send 'logon' command to cgminer and get the response
134+
res = send_cgminer_simple_command(host, port, "logon", timeout, verbose)
135+
136+
# Check if the response has the expected structure
137+
check_res_structure(res, "SESSION", 1, 1)
138+
139+
# Extract the session data from the response
140+
session = res["SESSION"][0]
141+
142+
# Get the 'SessionID' field from the session data
143+
s = get_str_field(session, "SessionID")
144+
145+
# If 'SessionID' is empty, raise an error indicating invalid session id
146+
if s == "":
147+
raise ValueError("error: invalid session id")
148+
149+
# Return the extracted 'SessionID'
150+
return s
24151

25-
while start <= end:
26-
ip_list.append(str(start))
27-
start += 1
28152

29-
return ip_list
30153

31154

32155
def logon_required(
@@ -44,12 +167,206 @@ def logon_required(
44167
break
45168

46169
if user_cmd is None:
47-
logging.info(
170+
log.info(
48171
f"{cmd} command is not supported. Try again with a different command."
49172
)
50173
return None
51174
return commands_list[cmd]['logon_required']
52175

53176

177+
def add_session_id_parameter(session_id, parameters):
178+
# Add the session id to the parameters
179+
return [session_id, *parameters]
180+
181+
182+
def parameters_to_string(parameters):
183+
# Convert the parameters to a string that LuxOS API accepts
184+
return ",".join(parameters)
185+
186+
187+
def generate_ip_range(start_ip: str, end_ip: str) -> list[str]:
188+
# Generate a list of IP addresses from the start and end IP
189+
start = ipaddress.IPv4Address(start_ip)
190+
end = ipaddress.IPv4Address(end_ip)
191+
192+
ip_list = []
193+
194+
while start <= end:
195+
ip_list.append(str(start))
196+
start += 1
197+
198+
return ip_list
199+
200+
201+
def load_ip_list_from_csv(csv_file: str) -> list[str]:
202+
# Check if file exists
203+
if not os.path.exists(csv_file):
204+
logging.info(f"Error: {csv_file} file not found.")
205+
exit(1)
206+
207+
# Load the IP addresses from the CSV file
208+
ip_list = []
209+
with open(csv_file, 'r') as f:
210+
reader = csv.reader(f)
211+
for i, row in enumerate(reader):
212+
if i == 0 and row and row[0] == "hostname":
213+
continue
214+
if row: # Ignore empty rows
215+
ip_list.extend(row)
216+
return ip_list
217+
218+
219+
def execute_command(host: str, port: int, timeout_sec: int, cmd: str,
220+
parameters: list, verbose: bool):
221+
222+
# Check if logon is required for the command
223+
logon_req = logon_required(cmd)
224+
225+
try:
226+
if logon_req:
227+
# Get a SessionID
228+
sid = logon(host, port, timeout_sec, verbose)
229+
# Add the SessionID to the parameters list at the left.
230+
parameters = add_session_id_parameter(sid, parameters)
231+
232+
if verbose:
233+
logging.info(
234+
f'Command requires a SessionID, logging in for host: {host}'
235+
)
236+
logging.info(f'SessionID obtained for {host}: {sid}')
237+
238+
elif not logon_required and verbose:
239+
logging.info(f"Logon not required for executing {cmd}")
240+
241+
# convert the params to a string that LuxOS API accepts
242+
param_string = parameters_to_string(parameters)
243+
244+
if verbose:
245+
logging.info(f"{cmd} on {host} with parameters: {param_string}")
246+
247+
# Execute the API command
248+
res = send_cgminer_command(host, port, cmd, param_string, timeout_sec,
249+
verbose)
250+
251+
if verbose:
252+
logging.info(res)
253+
254+
# Log off to terminate the session
255+
if logon_req:
256+
send_cgminer_command(host, port, "logoff", sid, timeout_sec,
257+
verbose)
258+
259+
return res
260+
261+
except Exception as e:
262+
logging.info(f"Error executing {cmd} on {host}: {e}")
263+
264+
54265
def main():
55-
print("CALLED!")
266+
# define arguments
267+
parser = argparse.ArgumentParser(description="LuxOS CLI Tool")
268+
parser.add_argument('--range_start', required=False, help="IP start range")
269+
parser.add_argument('--range_end', required=False, help="IP end range")
270+
parser.add_argument('--ipfile',
271+
required=False,
272+
default='ips.csv',
273+
help="File name to store IP addresses")
274+
parser.add_argument('--cmd',
275+
required=True,
276+
help="Command to execute on LuxOS API")
277+
parser.add_argument('--params',
278+
required=False,
279+
default=[],
280+
nargs='+',
281+
help="Parameters for LuxOS API")
282+
parser.add_argument(
283+
'--max_threads',
284+
required=False,
285+
default=10,
286+
type=int,
287+
help="Maximum number of threads to use. Default is 10.")
288+
parser.add_argument('--timeout',
289+
required=False,
290+
default=3,
291+
type=int,
292+
help="Timeout for network scan in seconds")
293+
parser.add_argument('--port',
294+
required=False,
295+
default=4028,
296+
type=int,
297+
help="Port for LuxOS API")
298+
parser.add_argument('--verbose',
299+
required=False,
300+
default=False,
301+
type=bool,
302+
help="Verbose output")
303+
parser.add_argument('--batch_delay',
304+
required=False,
305+
default=0,
306+
type=int,
307+
help="Delay between batches in seconds")
308+
309+
# parse arguments
310+
args = parser.parse_args()
311+
312+
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
313+
format="%(asctime)s [%(levelname)s] %(message)s",
314+
handlers=[logging.StreamHandler(), logging.FileHandler("LuxOS-CLI.log")], )
315+
316+
# set timeout to milliseconds
317+
timeout_sec = args.timeout
318+
319+
# check if IP address range or CSV with list of IP is provided
320+
if args.range_start and args.range_end:
321+
ip_list = generate_ip_range(args.range_start, args.range_end)
322+
elif args.ipfile:
323+
ip_list = load_ip_list_from_csv(args.ipfile)
324+
else:
325+
parser.error("No IP address or IP list found.")
326+
327+
# Set max threads to use, minimum of max threads and number of IP addresses
328+
max_threads = min(args.max_threads, len(ip_list))
329+
330+
# Create a list of threads
331+
threads = []
332+
333+
# Set start time
334+
start_time = time.time()
335+
336+
# Iterate over the IP addresses
337+
for ip in ip_list:
338+
# create new thread for each IP address
339+
thread = threading.Thread(target=execute_command,
340+
args=(ip, args.port, timeout_sec, args.cmd,
341+
args.params, args.verbose))
342+
343+
# start the thread
344+
threads.append(thread)
345+
thread.start()
346+
347+
# Limit the number of concurrent threads
348+
if len(threads) >= max_threads:
349+
# Wait for the threads to finish
350+
for thread in threads:
351+
thread.join()
352+
353+
# Introduce the batch delay if specified
354+
if args.batch_delay > 0:
355+
print(f"Waiting {args.batch_delay} seconds")
356+
time.sleep(args.batch_delay)
357+
358+
# Clear the thread list for the next batch
359+
threads = []
360+
361+
# Wait for the remaining threads to finish
362+
for thread in threads:
363+
thread.join()
364+
365+
# Execution completed
366+
end_time = time.time()
367+
execution_time = end_time - start_time
368+
log.info(f"Execution completed in {execution_time:.2f} seconds.")
369+
370+
371+
if __name__ == "__main__":
372+
main()

0 commit comments

Comments
 (0)