1
1
from __future__ import annotations
2
- import ipaddress
2
+ import os
3
+ import csv
3
4
import json
5
+ import time
6
+ import socket
4
7
import logging
8
+ import argparse
9
+ import ipaddress
10
+ import threading
5
11
from typing import Any
6
12
import importlib .resources
7
13
15
21
)
16
22
17
23
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 :
22
27
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
24
151
25
- while start <= end :
26
- ip_list .append (str (start ))
27
- start += 1
28
152
29
- return ip_list
30
153
31
154
32
155
def logon_required (
@@ -44,12 +167,206 @@ def logon_required(
44
167
break
45
168
46
169
if user_cmd is None :
47
- logging .info (
170
+ log .info (
48
171
f"{ cmd } command is not supported. Try again with a different command."
49
172
)
50
173
return None
51
174
return commands_list [cmd ]['logon_required' ]
52
175
53
176
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
+
54
265
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