multiSSH3 5.59__py3-none-any.whl → 5.62__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.
- multiSSH3.py +201 -80
- {multissh3-5.59.dist-info → multissh3-5.62.dist-info}/METADATA +5 -5
- multissh3-5.62.dist-info/RECORD +6 -0
- {multissh3-5.59.dist-info → multissh3-5.62.dist-info}/WHEEL +1 -1
- multissh3-5.59.dist-info/RECORD +0 -6
- {multissh3-5.59.dist-info → multissh3-5.62.dist-info}/entry_points.txt +0 -0
- {multissh3-5.59.dist-info → multissh3-5.62.dist-info}/top_level.txt +0 -0
multiSSH3.py
CHANGED
|
@@ -54,10 +54,10 @@ except AttributeError:
|
|
|
54
54
|
# If neither is available, use a dummy decorator
|
|
55
55
|
def cache_decorator(func):
|
|
56
56
|
return func
|
|
57
|
-
version = '5.
|
|
57
|
+
version = '5.62'
|
|
58
58
|
VERSION = version
|
|
59
59
|
__version__ = version
|
|
60
|
-
COMMIT_DATE = '2025-
|
|
60
|
+
COMMIT_DATE = '2025-04-17'
|
|
61
61
|
|
|
62
62
|
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
63
63
|
'~/multiSSH3.config.json',
|
|
@@ -226,7 +226,7 @@ class Host:
|
|
|
226
226
|
self.stdout = [] # the stdout of the command
|
|
227
227
|
self.stderr = [] # the stderr of the command
|
|
228
228
|
self.printedLines = -1 # the number of lines printed on the screen
|
|
229
|
-
self.lineNumToReprintSet = set() #
|
|
229
|
+
self.lineNumToReprintSet = set() # line numbers to reprint
|
|
230
230
|
self.lastUpdateTime = time.monotonic() # the last time the output was updated
|
|
231
231
|
self.files = files # the files to be copied to the host
|
|
232
232
|
self.ipmi = ipmi # whether to use ipmi to connect to the host
|
|
@@ -291,6 +291,7 @@ DEFAULT_SCP = False
|
|
|
291
291
|
DEFAULT_FILE_SYNC = False
|
|
292
292
|
DEFAULT_TIMEOUT = 50
|
|
293
293
|
DEFAULT_CLI_TIMEOUT = 0
|
|
294
|
+
DEFAULT_UNAVAILABLE_HOST_EXPIRY = 600
|
|
294
295
|
DEFAULT_REPEAT = 1
|
|
295
296
|
DEFAULT_INTERVAL = 0
|
|
296
297
|
DEFAULT_IPMI = False
|
|
@@ -304,6 +305,8 @@ DEFAULT_ERROR_ONLY = False
|
|
|
304
305
|
DEFAULT_NO_OUTPUT = False
|
|
305
306
|
DEFAULT_NO_ENV = False
|
|
306
307
|
DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
|
|
308
|
+
DEFAULT_NO_HISTORY = False
|
|
309
|
+
DEFAULT_HISTORY_FILE = '~/.mssh_history'
|
|
307
310
|
DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
|
|
308
311
|
DEFAULT_JSON_MODE = False
|
|
309
312
|
DEFAULT_PRINT_SUCCESS_HOSTS = False
|
|
@@ -325,12 +328,6 @@ _DEFAULT_RETURN_UNFINISHED = False
|
|
|
325
328
|
_DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
|
|
326
329
|
_DEFAULT_NO_START = False
|
|
327
330
|
_etc_hosts = {}
|
|
328
|
-
_sshpassPath = None
|
|
329
|
-
_sshPath = None
|
|
330
|
-
_scpPath = None
|
|
331
|
-
_ipmitoolPath = None
|
|
332
|
-
_rsyncPath = None
|
|
333
|
-
_shellPath = None
|
|
334
331
|
__ERROR_MESSAGES_TO_IGNORE_REGEX =None
|
|
335
332
|
__DEBUG_MODE = False
|
|
336
333
|
|
|
@@ -1136,20 +1133,37 @@ def __handle_reading_stream(stream,target, host):
|
|
|
1136
1133
|
host.lastUpdateTime = time.monotonic()
|
|
1137
1134
|
current_line = bytearray()
|
|
1138
1135
|
lastLineCommited = True
|
|
1136
|
+
curser_position = 0
|
|
1137
|
+
previousUpdateTime = time.monotonic()
|
|
1139
1138
|
for char in iter(lambda:stream.read(1), b''):
|
|
1140
1139
|
if char == b'\n':
|
|
1141
|
-
|
|
1142
|
-
add_line(current_line,target, host, keepLastLine=False)
|
|
1143
|
-
elif lastLineCommited:
|
|
1144
|
-
add_line(current_line,target, host, keepLastLine=True)
|
|
1140
|
+
add_line(current_line,target, host, keepLastLine=lastLineCommited)
|
|
1145
1141
|
current_line = bytearray()
|
|
1146
1142
|
lastLineCommited = True
|
|
1143
|
+
curser_position = 0
|
|
1144
|
+
previousUpdateTime = time.monotonic()
|
|
1145
|
+
continue
|
|
1147
1146
|
elif char == b'\r':
|
|
1147
|
+
curser_position = 0
|
|
1148
|
+
elif char == b'\x08':
|
|
1149
|
+
# backspace
|
|
1150
|
+
if curser_position > 0:
|
|
1151
|
+
curser_position -= 1
|
|
1152
|
+
else:
|
|
1153
|
+
# over write the character if the curser is not at the end of the line
|
|
1154
|
+
if curser_position < len(current_line):
|
|
1155
|
+
current_line[curser_position] = char[0]
|
|
1156
|
+
elif curser_position == len(current_line):
|
|
1157
|
+
current_line.append(char[0])
|
|
1158
|
+
else:
|
|
1159
|
+
# curser is bigger than the length of the line
|
|
1160
|
+
current_line += b' '*(curser_position - len(current_line)) + char
|
|
1161
|
+
curser_position += 1
|
|
1162
|
+
if time.monotonic() - previousUpdateTime > 0.1:
|
|
1163
|
+
# if the time since the last update is more than 10ms, we update the output
|
|
1148
1164
|
add_line(current_line,target, host, keepLastLine=lastLineCommited)
|
|
1149
|
-
current_line = bytearray()
|
|
1150
1165
|
lastLineCommited = False
|
|
1151
|
-
|
|
1152
|
-
current_line.extend(char)
|
|
1166
|
+
previousUpdateTime = time.monotonic()
|
|
1153
1167
|
if current_line:
|
|
1154
1168
|
add_line(current_line,target, host, keepLastLine=lastLineCommited)
|
|
1155
1169
|
|
|
@@ -2108,7 +2122,9 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
2108
2122
|
if host.lineNumToReprintSet:
|
|
2109
2123
|
# visible range is from host.printedLines - host_window_height + 1 to host.printedLines
|
|
2110
2124
|
visibleLowerBound = host.printedLines - host_window_height + 1
|
|
2111
|
-
|
|
2125
|
+
lineNumToReprintSet = host.lineNumToReprintSet
|
|
2126
|
+
host.lineNumToReprintSet = set()
|
|
2127
|
+
for lineNumToReprint in lineNumToReprintSet:
|
|
2112
2128
|
# if the line is visible, we will reprint it
|
|
2113
2129
|
if visibleLowerBound <= lineNumToReprint <= host.printedLines:
|
|
2114
2130
|
if visibleLowerBound <= 0:
|
|
@@ -2121,7 +2137,6 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
2121
2137
|
# Thus we will not use any presistent color pair for old lines
|
|
2122
2138
|
cpl = host.current_color_pair if lineNumToReprint == host.printedLines else [-1,-1,1]
|
|
2123
2139
|
_curses_add_string_to_window(window=host_window, y=linePos + 1, line=host.output[lineNumToReprint], color_pair_list=cpl,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
|
|
2124
|
-
host.lineNumToReprintSet = set()
|
|
2125
2140
|
host_window.refresh()
|
|
2126
2141
|
new_configured = False
|
|
2127
2142
|
last_refresh_time = time.perf_counter()
|
|
@@ -2206,7 +2221,13 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
2206
2221
|
def generate_output(hosts, usejson = False, greppable = False):
|
|
2207
2222
|
global __keyPressesIn
|
|
2208
2223
|
global __global_suppress_printout
|
|
2209
|
-
|
|
2224
|
+
if __global_suppress_printout:
|
|
2225
|
+
# remove hosts with returncode 0
|
|
2226
|
+
hosts = [dict(host) for host in hosts if host.returncode != 0]
|
|
2227
|
+
if not hosts:
|
|
2228
|
+
return 'Success'
|
|
2229
|
+
else:
|
|
2230
|
+
hosts = [dict(host) for host in hosts]
|
|
2210
2231
|
if usejson:
|
|
2211
2232
|
# [print(dict(host)) for host in hosts]
|
|
2212
2233
|
#print(json.dumps([dict(host) for host in hosts],indent=4))
|
|
@@ -2217,10 +2238,15 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2217
2238
|
rtnList = [['host_name','return_code','output_type','output']]
|
|
2218
2239
|
for host in hosts:
|
|
2219
2240
|
#header = f"{host['name']} | rc: {host['returncode']} | "
|
|
2241
|
+
hostAdded = False
|
|
2220
2242
|
for line in host['stdout']:
|
|
2221
2243
|
rtnList.append([host['name'],f"rc: {host['returncode']}",'stdout',line])
|
|
2244
|
+
hostAdded = True
|
|
2222
2245
|
for line in host['stderr']:
|
|
2223
2246
|
rtnList.append([host['name'],f"rc: {host['returncode']}",'stderr',line])
|
|
2247
|
+
hostAdded = True
|
|
2248
|
+
if not hostAdded:
|
|
2249
|
+
rtnList.append([host['name'],f"rc: {host['returncode']}",'N/A','<EMPTY>'])
|
|
2224
2250
|
rtnList.append(['','','',''])
|
|
2225
2251
|
rtnStr += pretty_format_table(rtnList)
|
|
2226
2252
|
rtnStr += '*'*80+'\n'
|
|
@@ -2231,8 +2257,6 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2231
2257
|
else:
|
|
2232
2258
|
outputs = {}
|
|
2233
2259
|
for host in hosts:
|
|
2234
|
-
if __global_suppress_printout and host['returncode'] == 0:
|
|
2235
|
-
continue
|
|
2236
2260
|
hostPrintOut = f" Command:\n {host['command']}\n"
|
|
2237
2261
|
hostPrintOut += " stdout:\n "+'\n '.join(host['stdout'])
|
|
2238
2262
|
if host['stderr']:
|
|
@@ -2248,11 +2272,11 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2248
2272
|
rtnStr = ''
|
|
2249
2273
|
for output, hostSet in outputs.items():
|
|
2250
2274
|
compact_hosts = sorted(compact_hostnames(hostSet))
|
|
2275
|
+
rtnStr += '*'*80+'\n'
|
|
2251
2276
|
if __global_suppress_printout:
|
|
2252
2277
|
rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
|
|
2253
2278
|
rtnStr += output+'\n'
|
|
2254
2279
|
else:
|
|
2255
|
-
rtnStr += '*'*80+'\n'
|
|
2256
2280
|
rtnStr += f'These hosts: "{",".join(compact_hosts)}" have a response of:\n'
|
|
2257
2281
|
rtnStr += output+'\n'
|
|
2258
2282
|
if not __global_suppress_printout or outputs:
|
|
@@ -2287,12 +2311,15 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
2287
2311
|
return rtnStr
|
|
2288
2312
|
|
|
2289
2313
|
#%% ------------ Run / Process Hosts Block ----------------
|
|
2290
|
-
def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished,
|
|
2314
|
+
def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, no_watch, json, called, greppable,
|
|
2315
|
+
unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN,
|
|
2316
|
+
curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW,
|
|
2317
|
+
unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY):
|
|
2291
2318
|
global __globalUnavailableHosts
|
|
2292
2319
|
global _no_env
|
|
2293
2320
|
sleep_interval = 1.0e-7 # 0.1 microseconds
|
|
2294
2321
|
threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
|
|
2295
|
-
if __curses_available and not
|
|
2322
|
+
if __curses_available and not no_watch and threads and not returnUnfinished and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
|
|
2296
2323
|
total_sleeped = 0
|
|
2297
2324
|
while any([host.returncode is None for host in hosts]):
|
|
2298
2325
|
time.sleep(sleep_interval) # avoid busy-waiting
|
|
@@ -2340,13 +2367,13 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
2340
2367
|
oldDic[line.split(',')[0]] = int(line.split(',')[1])
|
|
2341
2368
|
except:
|
|
2342
2369
|
pass
|
|
2343
|
-
# remove entries that are either available now or older than min(timeout,3600) seconds
|
|
2344
2370
|
for key in list(oldDic.keys()):
|
|
2345
|
-
if key in reachableHosts or time.monotonic() < oldDic[key] or time.monotonic() - oldDic[key] >
|
|
2371
|
+
if key in reachableHosts or time.monotonic() < oldDic[key] or time.monotonic() - oldDic[key] > unavailable_host_expiry:
|
|
2346
2372
|
del oldDic[key]
|
|
2347
2373
|
# add new entries
|
|
2348
2374
|
for host in unavailableHosts:
|
|
2349
|
-
|
|
2375
|
+
if host not in oldDic:
|
|
2376
|
+
oldDic[host] = int(time.monotonic ())
|
|
2350
2377
|
with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
|
|
2351
2378
|
for key, value in oldDic.items():
|
|
2352
2379
|
f.write(f'{key},{value}\n')
|
|
@@ -2385,20 +2412,24 @@ def formHostStr(host) -> str:
|
|
|
2385
2412
|
|
|
2386
2413
|
@cache_decorator
|
|
2387
2414
|
def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
2388
|
-
|
|
2415
|
+
no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
2389
2416
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
|
|
2390
2417
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
2391
2418
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
|
|
2392
2419
|
file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
2393
|
-
copy_id = False,
|
|
2420
|
+
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY, no_history = DEFAULT_NO_HISTORY,
|
|
2421
|
+
history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
|
|
2422
|
+
repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
|
|
2394
2423
|
shortend = False) -> str:
|
|
2395
2424
|
argsList = []
|
|
2396
2425
|
if oneonone: argsList.append('--oneonone' if not shortend else '-11')
|
|
2397
2426
|
if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
|
|
2427
|
+
if repeat and repeat != DEFAULT_REPEAT: argsList.append(f'--repeat={repeat}' if not shortend else f'-r={repeat}')
|
|
2428
|
+
if interval and interval != DEFAULT_INTERVAL: argsList.append(f'--interval={interval}' if not shortend else f'-i={interval}')
|
|
2398
2429
|
if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
|
|
2399
2430
|
if identity_file and identity_file != DEFAULT_IDENTITY_FILE: argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
|
|
2400
2431
|
if copy_id: argsList.append('--copy_id' if not shortend else '-ci')
|
|
2401
|
-
if
|
|
2432
|
+
if no_watch: argsList.append('--no_watch' if not shortend else '-q')
|
|
2402
2433
|
if json: argsList.append('--json' if not shortend else '-j')
|
|
2403
2434
|
if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
|
|
2404
2435
|
if files: argsList.extend([f'--file="{file}"' for file in files] if not shortend else [f'-f="{file}"' for file in files])
|
|
@@ -2409,7 +2440,11 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
2409
2440
|
if username and username != DEFAULT_USERNAME: argsList.append(f'--username="{username}"' if not shortend else f'-u="{username}"')
|
|
2410
2441
|
if extraargs and extraargs != DEFAULT_EXTRA_ARGS: argsList.append(f'--extraargs="{extraargs}"' if not shortend else f'-ea="{extraargs}"')
|
|
2411
2442
|
if skipUnreachable: argsList.append('--skip_unreachable' if not shortend else '-su')
|
|
2443
|
+
if unavailable_host_expiry and unavailable_host_expiry != DEFAULT_UNAVAILABLE_HOST_EXPIRY: argsList.append(f'--unavailable_host_expiry={unavailable_host_expiry}' if not shortend else f'-uhe={unavailable_host_expiry}')
|
|
2412
2444
|
if no_env: argsList.append('--no_env')
|
|
2445
|
+
if env_file and env_file != DEFAULT_ENV_FILE: argsList.append(f'--env_file="{env_file}"' if not shortend else f'-ef="{env_file}"')
|
|
2446
|
+
if no_history: argsList.append('--no_history' if not shortend else '-nh')
|
|
2447
|
+
if history_file and history_file != DEFAULT_HISTORY_FILE: argsList.append(f'--history_file="{history_file}"' if not shortend else f'-hf="{history_file}"')
|
|
2413
2448
|
if greppable: argsList.append('--greppable' if not shortend else '-g')
|
|
2414
2449
|
if error_only: argsList.append('--error_only' if not shortend else '-eo')
|
|
2415
2450
|
if skip_hosts and skip_hosts != DEFAULT_SKIP_HOSTS: argsList.append(f'--skip_hosts="{skip_hosts}"' if not shortend else f'-sh="{skip_hosts}"')
|
|
@@ -2417,13 +2452,15 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
2417
2452
|
return ' '.join(argsList)
|
|
2418
2453
|
|
|
2419
2454
|
def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
2420
|
-
|
|
2455
|
+
no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
2421
2456
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
|
|
2422
2457
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
2423
2458
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
2424
2459
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
2425
2460
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
2426
|
-
copy_id = False,
|
|
2461
|
+
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = DEFAULT_NO_HISTORY,
|
|
2462
|
+
history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
|
|
2463
|
+
repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
|
|
2427
2464
|
shortend = False):
|
|
2428
2465
|
_ = called
|
|
2429
2466
|
_ = returnUnfinished
|
|
@@ -2436,24 +2473,66 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
2436
2473
|
hostStr = formHostStr(hosts)
|
|
2437
2474
|
files = frozenset(files) if files else None
|
|
2438
2475
|
argsStr = __formCommandArgStr(oneonone = oneonone, timeout = timeout,password = password,
|
|
2439
|
-
|
|
2440
|
-
files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,
|
|
2441
|
-
username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,
|
|
2442
|
-
greppable=greppable,skip_hosts = skip_hosts,
|
|
2443
|
-
|
|
2476
|
+
no_watch = no_watch,json = json,max_connections=max_connections,
|
|
2477
|
+
files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,
|
|
2478
|
+
scp=scp,gather_mode = gather_mode,username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,
|
|
2479
|
+
no_env=no_env, greppable=greppable,skip_hosts = skip_hosts,
|
|
2480
|
+
file_sync = file_sync,error_only = error_only, identity_file = identity_file,
|
|
2481
|
+
copy_id = copy_id, unavailable_host_expiry =unavailable_host_expiry,no_history = no_history,
|
|
2482
|
+
history_file = history_file, env_file = env_file,
|
|
2483
|
+
repeat = repeat,interval = interval,
|
|
2444
2484
|
shortend = shortend)
|
|
2445
2485
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
2446
|
-
|
|
2486
|
+
programName = sys.argv[0] if (sys.argv and sys.argv[0]) else 'mssh'
|
|
2487
|
+
return f'{programName} {argsStr} {hostStr} {commandStr}'
|
|
2488
|
+
|
|
2489
|
+
#%% ------------ Record History Block ----------------
|
|
2490
|
+
def record_command_history(kwargs):
|
|
2491
|
+
'''
|
|
2492
|
+
Record the command history to a file
|
|
2493
|
+
|
|
2494
|
+
Args:
|
|
2495
|
+
args (str): The command arguments to record
|
|
2496
|
+
|
|
2497
|
+
Returns:
|
|
2498
|
+
None
|
|
2499
|
+
'''
|
|
2500
|
+
global __global_suppress_printout
|
|
2501
|
+
global __DEBUG_MODE
|
|
2502
|
+
try:
|
|
2503
|
+
history_file = os.path.expanduser(kwargs.get('history_file', DEFAULT_HISTORY_FILE))
|
|
2504
|
+
import inspect
|
|
2505
|
+
sig = inspect.signature(getStrCommand)
|
|
2506
|
+
wanted = {
|
|
2507
|
+
name: kwargs[name]
|
|
2508
|
+
for name in sig.parameters
|
|
2509
|
+
if name in kwargs
|
|
2510
|
+
}
|
|
2511
|
+
strCommand = getStrCommand(**wanted)
|
|
2512
|
+
with open(history_file, 'a') as f:
|
|
2513
|
+
# it follows <timestamp>\t<strCommand>\n
|
|
2514
|
+
f.write(f'{int(time.time())}\t{strCommand}\n')
|
|
2515
|
+
f.flush()
|
|
2516
|
+
os.fsync(f.fileno())
|
|
2517
|
+
if __DEBUG_MODE:
|
|
2518
|
+
eprint(f'Command history recorded to {history_file}')
|
|
2519
|
+
except Exception as e:
|
|
2520
|
+
eprint(f'Error recording command history: {e!r}')
|
|
2521
|
+
if __DEBUG_MODE:
|
|
2522
|
+
import traceback
|
|
2523
|
+
eprint(traceback.format_exc().strip())
|
|
2447
2524
|
|
|
2448
2525
|
#%% ------------ Main Block ----------------
|
|
2449
2526
|
def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
2450
|
-
|
|
2527
|
+
no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
2451
2528
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
|
|
2452
2529
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
2453
2530
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
2454
2531
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
2455
2532
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE,
|
|
2456
|
-
copy_id = False
|
|
2533
|
+
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = True,
|
|
2534
|
+
history_file = DEFAULT_HISTORY_FILE,
|
|
2535
|
+
):
|
|
2457
2536
|
f'''
|
|
2458
2537
|
Run the command on the hosts, aka multissh. main function
|
|
2459
2538
|
|
|
@@ -2463,7 +2542,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2463
2542
|
oneonone (bool, optional): Whether to run the commands one on one. Defaults to {DEFAULT_ONE_ON_ONE}.
|
|
2464
2543
|
timeout (int, optional): The timeout for the command. Defaults to {DEFAULT_TIMEOUT}.
|
|
2465
2544
|
password (str, optional): The password for the hosts. Defaults to {DEFAULT_PASSWORD}.
|
|
2466
|
-
|
|
2545
|
+
no_watch (bool, optional): Whether to print the output. Defaults to {DEFAULT_NO_WATCH}.
|
|
2467
2546
|
json (bool, optional): Whether to print the output in JSON format. Defaults to {DEFAULT_JSON_MODE}.
|
|
2468
2547
|
called (bool, optional): Whether the function is called by another function. Defaults to {_DEFAULT_CALLED}.
|
|
2469
2548
|
max_connections (int, optional): The maximum number of concurrent SSH sessions. Defaults to 4 * os.cpu_count().
|
|
@@ -2489,6 +2568,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2489
2568
|
quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
|
|
2490
2569
|
identity_file (str, optional): The identity file to use for the ssh connection. Defaults to {DEFAULT_IDENTITY_FILE}.
|
|
2491
2570
|
copy_id (bool, optional): Whether to copy the id to the hosts. Defaults to False.
|
|
2571
|
+
unavailable_host_expiry (int, optional): The time in seconds to keep the unavailable hosts in the global unavailable hosts. Defaults to {DEFAULT_UNAVAILABLE_HOST_EXPIRY}.
|
|
2572
|
+
no_history (bool, optional): Whether to not save the history of the command. Defaults to True.
|
|
2573
|
+
history_file (str, optional): The file to save the history of the command. Defaults to {DEFAULT_HISTORY_FILE}.
|
|
2492
2574
|
|
|
2493
2575
|
Returns:
|
|
2494
2576
|
list: A list of Host objects
|
|
@@ -2503,24 +2585,23 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2503
2585
|
global __keyPressesIn
|
|
2504
2586
|
_emo = False
|
|
2505
2587
|
_no_env = no_env
|
|
2588
|
+
if not no_history:
|
|
2589
|
+
_ = history_file
|
|
2590
|
+
record_command_history(locals())
|
|
2591
|
+
if error_only:
|
|
2592
|
+
__global_suppress_printout = True
|
|
2506
2593
|
if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
|
|
2507
|
-
if
|
|
2508
|
-
|
|
2509
|
-
else:
|
|
2510
|
-
checkTime = timeout
|
|
2511
|
-
if checkTime <= 0:
|
|
2512
|
-
checkTime = 60
|
|
2513
|
-
elif checkTime > 3600:
|
|
2514
|
-
checkTime = 3600
|
|
2594
|
+
if unavailable_host_expiry <= 0:
|
|
2595
|
+
unavailable_host_expiry = 10
|
|
2515
2596
|
try:
|
|
2516
2597
|
readed = False
|
|
2517
|
-
if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) <
|
|
2598
|
+
if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < unavailable_host_expiry:
|
|
2518
2599
|
|
|
2519
2600
|
with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
|
|
2520
2601
|
for line in f:
|
|
2521
2602
|
line = line.strip()
|
|
2522
2603
|
if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
|
|
2523
|
-
if int(line.split(',')[1]) < time.monotonic() and int(line.split(',')[1]) +
|
|
2604
|
+
if int(line.split(',')[1]) < time.monotonic() and int(line.split(',')[1]) + unavailable_host_expiry > time.monotonic():
|
|
2524
2605
|
__globalUnavailableHosts.add(line.split(',')[0])
|
|
2525
2606
|
readed = True
|
|
2526
2607
|
if readed and not __global_suppress_printout:
|
|
@@ -2623,7 +2704,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2623
2704
|
eprint(f"> {command}")
|
|
2624
2705
|
os.system(command)
|
|
2625
2706
|
if hosts:
|
|
2626
|
-
processRunOnHosts(timeout, password, max_connections, hosts
|
|
2707
|
+
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2708
|
+
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
2709
|
+
unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
|
|
2710
|
+
curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
|
|
2711
|
+
single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
|
|
2627
2712
|
else:
|
|
2628
2713
|
eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
|
|
2629
2714
|
if not commands:
|
|
@@ -2678,7 +2763,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2678
2763
|
if not __global_suppress_printout:
|
|
2679
2764
|
eprint(f"Running command: {command!r} on host: {host!r}")
|
|
2680
2765
|
if not __global_suppress_printout: eprint('-'*80)
|
|
2681
|
-
if not no_start:
|
|
2766
|
+
if not no_start:
|
|
2767
|
+
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2768
|
+
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
2769
|
+
unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
|
|
2770
|
+
curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
|
|
2771
|
+
single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
|
|
2682
2772
|
return hosts
|
|
2683
2773
|
else:
|
|
2684
2774
|
allHosts = []
|
|
@@ -2704,7 +2794,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2704
2794
|
if no_start:
|
|
2705
2795
|
eprint(f"Warning: no_start is set, the command will not be started. As we are in interactive mode, no action will be done.")
|
|
2706
2796
|
else:
|
|
2707
|
-
processRunOnHosts(timeout, password, max_connections, hosts
|
|
2797
|
+
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2798
|
+
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
2799
|
+
unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
|
|
2800
|
+
curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
|
|
2801
|
+
single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
|
|
2708
2802
|
return hosts
|
|
2709
2803
|
for command in commands:
|
|
2710
2804
|
hosts = []
|
|
@@ -2721,7 +2815,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2721
2815
|
eprint('-'*80)
|
|
2722
2816
|
eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
2723
2817
|
eprint('-'*80)
|
|
2724
|
-
if not no_start:
|
|
2818
|
+
if not no_start:
|
|
2819
|
+
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2820
|
+
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
2821
|
+
unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
|
|
2822
|
+
curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
|
|
2823
|
+
single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
|
|
2725
2824
|
allHosts += hosts
|
|
2726
2825
|
return allHosts
|
|
2727
2826
|
|
|
@@ -2751,12 +2850,13 @@ def generate_default_config(args):
|
|
|
2751
2850
|
'DEFAULT_FILE_SYNC': args.file_sync,
|
|
2752
2851
|
'DEFAULT_TIMEOUT': DEFAULT_TIMEOUT,
|
|
2753
2852
|
'DEFAULT_CLI_TIMEOUT': args.timeout,
|
|
2853
|
+
'DEFAULT_UNAVAILABLE_HOST_EXPIRY': args.unavailable_host_expiry,
|
|
2754
2854
|
'DEFAULT_REPEAT': args.repeat,
|
|
2755
2855
|
'DEFAULT_INTERVAL': args.interval,
|
|
2756
2856
|
'DEFAULT_IPMI': args.ipmi,
|
|
2757
2857
|
'DEFAULT_IPMI_INTERFACE_IP_PREFIX': args.ipmi_interface_ip_prefix,
|
|
2758
2858
|
'DEFAULT_INTERFACE_IP_PREFIX': args.interface_ip_prefix,
|
|
2759
|
-
'DEFAULT_NO_WATCH': args.
|
|
2859
|
+
'DEFAULT_NO_WATCH': args.no_watch,
|
|
2760
2860
|
'DEFAULT_CURSES_MINIMUM_CHAR_LEN': args.window_width,
|
|
2761
2861
|
'DEFAULT_CURSES_MINIMUM_LINE_LEN': args.window_height,
|
|
2762
2862
|
'DEFAULT_SINGLE_WINDOW': args.single_window,
|
|
@@ -2764,6 +2864,8 @@ def generate_default_config(args):
|
|
|
2764
2864
|
'DEFAULT_NO_OUTPUT': args.no_output,
|
|
2765
2865
|
'DEFAULT_NO_ENV': args.no_env,
|
|
2766
2866
|
'DEFAULT_ENV_FILE': args.env_file,
|
|
2867
|
+
'DEFAULT_NO_HISTORY': args.no_history,
|
|
2868
|
+
'DEFAULT_HISTORY_FILE': args.history_file,
|
|
2767
2869
|
'DEFAULT_MAX_CONNECTIONS': args.max_connections if args.max_connections != 4 * os.cpu_count() else None,
|
|
2768
2870
|
'DEFAULT_JSON_MODE': args.json,
|
|
2769
2871
|
'DEFAULT_PRINT_SUCCESS_HOSTS': args.success_hosts,
|
|
@@ -2839,38 +2941,41 @@ def main():
|
|
|
2839
2941
|
parser.add_argument('-ea','--extraargs',type=str,help=f'Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: {DEFAULT_EXTRA_ARGS})',default=DEFAULT_EXTRA_ARGS)
|
|
2840
2942
|
parser.add_argument("-11",'--oneonone', action='store_true', help=f"Run one corresponding command on each host. (default: {DEFAULT_ONE_ON_ONE})", default=DEFAULT_ONE_ON_ONE)
|
|
2841
2943
|
parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
|
|
2842
|
-
parser.add_argument('-fs','--file_sync', action='store_true', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source
|
|
2843
|
-
parser.add_argument('--scp', action='store_true', help=f'Use scp for copying files instead of rsync. Need to use this on windows. (default: {DEFAULT_SCP})', default=DEFAULT_SCP)
|
|
2844
|
-
parser.add_argument('-gm','--gather_mode', action='store_true', help=f'Gather files from the hosts instead of sending files to the hosts. Will send remote files specified in <FILE> to local path specified in <COMMANDS> (default: False)', default=False)
|
|
2944
|
+
parser.add_argument('-s','-fs','--file_sync', action='store_true', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source and source and destination will be the same in this mode. Infer destination from source path. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
|
|
2945
|
+
parser.add_argument('-W','--scp', action='store_true', help=f'Use scp for copying files instead of rsync. Need to use this on windows. (default: {DEFAULT_SCP})', default=DEFAULT_SCP)
|
|
2946
|
+
parser.add_argument('-G','-gm','--gather_mode', action='store_true', help=f'Gather files from the hosts instead of sending files to the hosts. Will send remote files specified in <FILE> to local path specified in <COMMANDS> (default: False)', default=False)
|
|
2845
2947
|
#parser.add_argument("-d",'-c',"--destination", type=str, help="The destination of the files. Same as specify with commands. Added for compatibility. Use #HOST# or #HOSTNAME# to replace the host name in the destination")
|
|
2846
|
-
parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds (default: {DEFAULT_CLI_TIMEOUT}
|
|
2948
|
+
parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds. Set default value via DEFAULT_CLI_TIMEOUT in config file. Use 0 for disabling timeout. (default: {DEFAULT_CLI_TIMEOUT})", default=DEFAULT_CLI_TIMEOUT)
|
|
2949
|
+
parser.add_argument('-T','--use_script_timeout',action='store_true', help=f'Use shortened timeout suitable to use in a script. Set value via DEFAULT_TIMEOUT field in config file. (current: {DEFAULT_TIMEOUT})', default=False)
|
|
2847
2950
|
parser.add_argument("-r","--repeat", type=int, help=f"Repeat the command for a number of times (default: {DEFAULT_REPEAT})", default=DEFAULT_REPEAT)
|
|
2848
2951
|
parser.add_argument("-i","--interval", type=int, help=f"Interval between repeats in seconds (default: {DEFAULT_INTERVAL})", default=DEFAULT_INTERVAL)
|
|
2849
|
-
parser.add_argument("--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
|
|
2952
|
+
parser.add_argument('-M',"--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
|
|
2850
2953
|
parser.add_argument("-mpre","--ipmi_interface_ip_prefix", type=str, help=f"The prefix of the IPMI interfaces (default: {DEFAULT_IPMI_INTERFACE_IP_PREFIX})", default=DEFAULT_IPMI_INTERFACE_IP_PREFIX)
|
|
2851
2954
|
parser.add_argument("-pre","--interface_ip_prefix", type=str, help=f"The prefix of the for the interfaces (default: {DEFAULT_INTERFACE_IP_PREFIX})", default=DEFAULT_INTERFACE_IP_PREFIX)
|
|
2852
|
-
parser.add_argument("-q","-nw","--
|
|
2955
|
+
parser.add_argument('-S',"-q","-nw","--no_watch","--quiet", action='store_true', help=f"Quiet mode, no curses watch, only print the output. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
|
|
2853
2956
|
parser.add_argument("-ww",'--window_width', type=int, help=f"The minimum character length of the curses window. (default: {DEFAULT_CURSES_MINIMUM_CHAR_LEN})", default=DEFAULT_CURSES_MINIMUM_CHAR_LEN)
|
|
2854
2957
|
parser.add_argument("-wh",'--window_height', type=int, help=f"The minimum line height of the curses window. (default: {DEFAULT_CURSES_MINIMUM_LINE_LEN})", default=DEFAULT_CURSES_MINIMUM_LINE_LEN)
|
|
2855
|
-
parser.add_argument('-sw','--single_window', action='store_true', help=f'Use a single window for all hosts. (default: {DEFAULT_SINGLE_WINDOW})', default=DEFAULT_SINGLE_WINDOW)
|
|
2856
|
-
parser.add_argument('-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
|
|
2857
|
-
parser.add_argument("-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
|
|
2858
|
-
parser.add_argument('--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
|
|
2958
|
+
parser.add_argument('-B','-sw','--single_window', action='store_true', help=f'Use a single window for all hosts. (default: {DEFAULT_SINGLE_WINDOW})', default=DEFAULT_SINGLE_WINDOW)
|
|
2959
|
+
parser.add_argument('-R','-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
|
|
2960
|
+
parser.add_argument('-Q',"-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
|
|
2961
|
+
parser.add_argument('-C','--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
|
|
2859
2962
|
parser.add_argument("--env_file", type=str, help=f"The file to load the mssh file based environment variables from. ( Still work with --no_env ) (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
|
|
2860
2963
|
parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
|
|
2861
2964
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
2862
|
-
parser.add_argument("--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as
|
|
2863
|
-
parser.add_argument("-g","--greppable",'--table', action='store_true', help=f"Output in greppable
|
|
2965
|
+
parser.add_argument('-w',"--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as well. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
|
|
2966
|
+
parser.add_argument('-P',"-g","--greppable",'--table', action='store_true', help=f"Output in greppable table. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
2864
2967
|
group = parser.add_mutually_exclusive_group()
|
|
2865
|
-
group.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
|
|
2866
|
-
group.add_argument("-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
|
|
2867
|
-
|
|
2868
|
-
parser.add_argument("-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
|
|
2968
|
+
group.add_argument('-x',"-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
|
|
2969
|
+
group.add_argument('-a',"-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
|
|
2970
|
+
parser.add_argument('-uhe','--unavailable_host_expiry', type=int, help=f"Time in seconds to expire the unavailable hosts (default: {DEFAULT_UNAVAILABLE_HOST_EXPIRY})", default=DEFAULT_UNAVAILABLE_HOST_EXPIRY)
|
|
2971
|
+
parser.add_argument('-X',"-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
|
|
2869
2972
|
parser.add_argument('--generate_config_file', action='store_true', help=f'Store / generate the default config file from command line argument and current config at --config_file / stdout')
|
|
2870
2973
|
parser.add_argument('--config_file', type=str,nargs='?', help=f'Additional config file to use, will pioritize over config chains. When using with store_config_file, will store the resulting config file at this location. Use without a path will use multiSSH3.config.json',const='multiSSH3.config.json',default=None)
|
|
2871
2974
|
parser.add_argument('--store_config_file',type = str,nargs='?',help=f'Store the default config file from command line argument and current config. Same as --store_config_file --config_file=<path>',const='multiSSH3.config.json')
|
|
2872
2975
|
parser.add_argument('--debug', action='store_true', help='Print debug information')
|
|
2873
2976
|
parser.add_argument('-ci','--copy_id', action='store_true', help='Copy the ssh id to the hosts')
|
|
2977
|
+
parser.add_argument('-I','-nh','--no_history', action='store_true', help=f'Do not record the command to history. Default: {DEFAULT_NO_HISTORY}', default=DEFAULT_NO_HISTORY)
|
|
2978
|
+
parser.add_argument('-hf','--history_file', type=str, help=f'The file to store the history. (default: {DEFAULT_HISTORY_FILE})', default=DEFAULT_HISTORY_FILE)
|
|
2874
2979
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
2875
2980
|
|
|
2876
2981
|
# parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
|
|
@@ -2900,9 +3005,10 @@ def main():
|
|
|
2900
3005
|
else:
|
|
2901
3006
|
configFileToWriteTo = args.config_file
|
|
2902
3007
|
write_default_config(args,configFileToWriteTo)
|
|
2903
|
-
if not args.commands
|
|
2904
|
-
|
|
2905
|
-
|
|
3008
|
+
if not args.commands:
|
|
3009
|
+
if configFileToWriteTo:
|
|
3010
|
+
with open(configFileToWriteTo,'r') as f:
|
|
3011
|
+
eprint(f"Config file content: \n{f.read()}")
|
|
2906
3012
|
sys.exit(0)
|
|
2907
3013
|
if args.config_file:
|
|
2908
3014
|
if os.path.exists(args.config_file):
|
|
@@ -2941,14 +3047,27 @@ def main():
|
|
|
2941
3047
|
|
|
2942
3048
|
if args.no_output:
|
|
2943
3049
|
__global_suppress_printout = True
|
|
3050
|
+
|
|
3051
|
+
if args.unavailable_host_expiry <= 0:
|
|
3052
|
+
eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
|
|
3053
|
+
args.unavailable_host_expiry = 10
|
|
3054
|
+
|
|
3055
|
+
if args.use_script_timeout:
|
|
3056
|
+
# set timeout to the default script timeout if timeout is not set
|
|
3057
|
+
if args.timeout == DEFAULT_CLI_TIMEOUT:
|
|
3058
|
+
args.timeout = DEFAULT_TIMEOUT
|
|
2944
3059
|
|
|
2945
3060
|
if not __global_suppress_printout:
|
|
2946
|
-
cmdStr = getStrCommand(args.hosts,args.commands,
|
|
2947
|
-
|
|
3061
|
+
cmdStr = getStrCommand(args.hosts,args.commands,
|
|
3062
|
+
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
3063
|
+
no_watch=args.no_watch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
2948
3064
|
files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,gather_mode = args.gather_mode,username=args.username,
|
|
2949
3065
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
2950
3066
|
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key,
|
|
2951
|
-
copy_id=args.copy_id
|
|
3067
|
+
copy_id=args.copy_id,unavailable_host_expiry=args.unavailable_host_expiry,no_history=args.no_history,
|
|
3068
|
+
history_file = args.history_file,
|
|
3069
|
+
env_file = args.env_file,
|
|
3070
|
+
repeat = args.repeat,interval = args.interval)
|
|
2952
3071
|
eprint('> ' + cmdStr)
|
|
2953
3072
|
if args.error_only:
|
|
2954
3073
|
__global_suppress_printout = True
|
|
@@ -2961,11 +3080,13 @@ def main():
|
|
|
2961
3080
|
if not __global_suppress_printout: eprint(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
|
|
2962
3081
|
hosts = run_command_on_hosts(args.hosts,args.commands,
|
|
2963
3082
|
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
2964
|
-
|
|
3083
|
+
no_watch=args.no_watch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
2965
3084
|
files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,gather_mode = args.gather_mode,username=args.username,
|
|
2966
3085
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
2967
3086
|
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key,
|
|
2968
|
-
copy_id=args.copy_id
|
|
3087
|
+
copy_id=args.copy_id,unavailable_host_expiry=args.unavailable_host_expiry,no_history=args.no_history,
|
|
3088
|
+
history_file = args.history_file,
|
|
3089
|
+
)
|
|
2969
3090
|
#print('*'*80)
|
|
2970
3091
|
|
|
2971
3092
|
#if not __global_suppress_printout: eprint('-'*80)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: multiSSH3
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.62
|
|
4
4
|
Summary: Run commands on multiple hosts via SSH
|
|
5
5
|
Home-page: https://github.com/yufei-pan/multiSSH3
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -108,7 +108,7 @@ An example .ssh/config:
|
|
|
108
108
|
Host *
|
|
109
109
|
StrictHostKeyChecking no
|
|
110
110
|
ControlMaster auto
|
|
111
|
-
ControlPath /
|
|
111
|
+
ControlPath /run/user/%i/ssh_sockets_%C
|
|
112
112
|
ControlPersist 3600
|
|
113
113
|
```
|
|
114
114
|
|
|
@@ -181,7 +181,7 @@ options:
|
|
|
181
181
|
The prefix of the IPMI interfaces (default: )
|
|
182
182
|
-pre INTERFACE_IP_PREFIX, --interface_ip_prefix INTERFACE_IP_PREFIX
|
|
183
183
|
The prefix of the for the interfaces (default: None)
|
|
184
|
-
-q, -nw, --
|
|
184
|
+
-q, -nw, --no_watch, --quiet
|
|
185
185
|
Quiet mode, no curses watch, only print the output. (default: False)
|
|
186
186
|
-ww WINDOW_WIDTH, --window_width WINDOW_WIDTH
|
|
187
187
|
The minimum character length of the curses window. (default: 40)
|
|
@@ -291,7 +291,7 @@ mssh [options] <hosts> <commands>
|
|
|
291
291
|
| `-j` | `--json` | Output results in JSON format. |
|
|
292
292
|
| | `--success_hosts` | Also display hosts where commands succeeded. |
|
|
293
293
|
| `-g` | `--greppable` | Output results in a greppable format. |
|
|
294
|
-
| `-nw` | `--
|
|
294
|
+
| `-nw` | `--no_watch` | Do not use curses mode; use simple output instead. |
|
|
295
295
|
| `-su` | `--skipunreachable` | Skip hosts that are unreachable. |
|
|
296
296
|
| `-sh` | `--skiphosts` | Comma-separated list of hosts to skip. |
|
|
297
297
|
| `-V` | `--version` | Display the script version and exit. |
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
multiSSH3.py,sha256=aIPftXdNHKopUv-DIOPcBxQd9IQhCAg4mNLgxefh3zM,145959
|
|
2
|
+
multissh3-5.62.dist-info/METADATA,sha256=TEWayZf__9rv11wQTJp8kSY7IULgqQHOPf9CJefLooI,18093
|
|
3
|
+
multissh3-5.62.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
4
|
+
multissh3-5.62.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
+
multissh3-5.62.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
+
multissh3-5.62.dist-info/RECORD,,
|
multissh3-5.59.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=j_9ukdE0WHkS8b0Ls94JRdM7cBJZqv0GUQ2mmgRF_dk,139237
|
|
2
|
-
multissh3-5.59.dist-info/METADATA,sha256=6Ckopi4bXvlQQTuvkdoRN_7VtQsKInmOho0WSU6Tr24,18092
|
|
3
|
-
multissh3-5.59.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
4
|
-
multissh3-5.59.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
-
multissh3-5.59.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
-
multissh3-5.59.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|