multiSSH3 5.60__tar.gz → 5.63__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of multiSSH3 might be problematic. Click here for more details.
- {multissh3-5.60 → multissh3-5.63}/PKG-INFO +3 -3
- {multissh3-5.60 → multissh3-5.63}/README.md +2 -2
- {multissh3-5.60 → multissh3-5.63}/multiSSH3.egg-info/PKG-INFO +3 -3
- {multissh3-5.60 → multissh3-5.63}/multiSSH3.py +185 -70
- {multissh3-5.60 → multissh3-5.63}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-5.60 → multissh3-5.63}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-5.60 → multissh3-5.63}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-5.60 → multissh3-5.63}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-5.60 → multissh3-5.63}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-5.60 → multissh3-5.63}/setup.cfg +0 -0
- {multissh3-5.60 → multissh3-5.63}/setup.py +0 -0
- {multissh3-5.60 → multissh3-5.63}/test/test.py +0 -0
- {multissh3-5.60 → multissh3-5.63}/test/testCurses.py +0 -0
- {multissh3-5.60 → multissh3-5.63}/test/testCursesOld.py +0 -0
- {multissh3-5.60 → multissh3-5.63}/test/testPerfCompact.py +0 -0
- {multissh3-5.60 → multissh3-5.63}/test/testPerfExpand.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multiSSH3
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.63
|
|
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
|
|
@@ -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. |
|
|
@@ -153,7 +153,7 @@ options:
|
|
|
153
153
|
The prefix of the IPMI interfaces (default: )
|
|
154
154
|
-pre INTERFACE_IP_PREFIX, --interface_ip_prefix INTERFACE_IP_PREFIX
|
|
155
155
|
The prefix of the for the interfaces (default: None)
|
|
156
|
-
-q, -nw, --
|
|
156
|
+
-q, -nw, --no_watch, --quiet
|
|
157
157
|
Quiet mode, no curses watch, only print the output. (default: False)
|
|
158
158
|
-ww WINDOW_WIDTH, --window_width WINDOW_WIDTH
|
|
159
159
|
The minimum character length of the curses window. (default: 40)
|
|
@@ -263,7 +263,7 @@ mssh [options] <hosts> <commands>
|
|
|
263
263
|
| `-j` | `--json` | Output results in JSON format. |
|
|
264
264
|
| | `--success_hosts` | Also display hosts where commands succeeded. |
|
|
265
265
|
| `-g` | `--greppable` | Output results in a greppable format. |
|
|
266
|
-
| `-nw` | `--
|
|
266
|
+
| `-nw` | `--no_watch` | Do not use curses mode; use simple output instead. |
|
|
267
267
|
| `-su` | `--skipunreachable` | Skip hosts that are unreachable. |
|
|
268
268
|
| `-sh` | `--skiphosts` | Comma-separated list of hosts to skip. |
|
|
269
269
|
| `-V` | `--version` | Display the script version and exit. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multiSSH3
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.63
|
|
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
|
|
@@ -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. |
|
|
@@ -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.63'
|
|
58
58
|
VERSION = version
|
|
59
59
|
__version__ = version
|
|
60
|
-
COMMIT_DATE = '2025-
|
|
60
|
+
COMMIT_DATE = '2025-04-21'
|
|
61
61
|
|
|
62
62
|
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
63
63
|
'~/multiSSH3.config.json',
|
|
@@ -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
|
|
|
@@ -2224,7 +2221,16 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
2224
2221
|
def generate_output(hosts, usejson = False, greppable = False):
|
|
2225
2222
|
global __keyPressesIn
|
|
2226
2223
|
global __global_suppress_printout
|
|
2227
|
-
|
|
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
|
+
if usejson:
|
|
2229
|
+
return '{"Success": true}'
|
|
2230
|
+
else:
|
|
2231
|
+
return 'Success'
|
|
2232
|
+
else:
|
|
2233
|
+
hosts = [dict(host) for host in hosts]
|
|
2228
2234
|
if usejson:
|
|
2229
2235
|
# [print(dict(host)) for host in hosts]
|
|
2230
2236
|
#print(json.dumps([dict(host) for host in hosts],indent=4))
|
|
@@ -2235,10 +2241,15 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2235
2241
|
rtnList = [['host_name','return_code','output_type','output']]
|
|
2236
2242
|
for host in hosts:
|
|
2237
2243
|
#header = f"{host['name']} | rc: {host['returncode']} | "
|
|
2244
|
+
hostAdded = False
|
|
2238
2245
|
for line in host['stdout']:
|
|
2239
2246
|
rtnList.append([host['name'],f"rc: {host['returncode']}",'stdout',line])
|
|
2247
|
+
hostAdded = True
|
|
2240
2248
|
for line in host['stderr']:
|
|
2241
2249
|
rtnList.append([host['name'],f"rc: {host['returncode']}",'stderr',line])
|
|
2250
|
+
hostAdded = True
|
|
2251
|
+
if not hostAdded:
|
|
2252
|
+
rtnList.append([host['name'],f"rc: {host['returncode']}",'N/A','<EMPTY>'])
|
|
2242
2253
|
rtnList.append(['','','',''])
|
|
2243
2254
|
rtnStr += pretty_format_table(rtnList)
|
|
2244
2255
|
rtnStr += '*'*80+'\n'
|
|
@@ -2249,8 +2260,6 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2249
2260
|
else:
|
|
2250
2261
|
outputs = {}
|
|
2251
2262
|
for host in hosts:
|
|
2252
|
-
if __global_suppress_printout and host['returncode'] == 0:
|
|
2253
|
-
continue
|
|
2254
2263
|
hostPrintOut = f" Command:\n {host['command']}\n"
|
|
2255
2264
|
hostPrintOut += " stdout:\n "+'\n '.join(host['stdout'])
|
|
2256
2265
|
if host['stderr']:
|
|
@@ -2266,11 +2275,11 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2266
2275
|
rtnStr = ''
|
|
2267
2276
|
for output, hostSet in outputs.items():
|
|
2268
2277
|
compact_hosts = sorted(compact_hostnames(hostSet))
|
|
2278
|
+
rtnStr += '*'*80+'\n'
|
|
2269
2279
|
if __global_suppress_printout:
|
|
2270
2280
|
rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
|
|
2271
2281
|
rtnStr += output+'\n'
|
|
2272
2282
|
else:
|
|
2273
|
-
rtnStr += '*'*80+'\n'
|
|
2274
2283
|
rtnStr += f'These hosts: "{",".join(compact_hosts)}" have a response of:\n'
|
|
2275
2284
|
rtnStr += output+'\n'
|
|
2276
2285
|
if not __global_suppress_printout or outputs:
|
|
@@ -2305,12 +2314,15 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
2305
2314
|
return rtnStr
|
|
2306
2315
|
|
|
2307
2316
|
#%% ------------ Run / Process Hosts Block ----------------
|
|
2308
|
-
def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished,
|
|
2317
|
+
def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, no_watch, json, called, greppable,
|
|
2318
|
+
unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN,
|
|
2319
|
+
curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW,
|
|
2320
|
+
unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY):
|
|
2309
2321
|
global __globalUnavailableHosts
|
|
2310
2322
|
global _no_env
|
|
2311
2323
|
sleep_interval = 1.0e-7 # 0.1 microseconds
|
|
2312
2324
|
threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
|
|
2313
|
-
if __curses_available and not
|
|
2325
|
+
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:
|
|
2314
2326
|
total_sleeped = 0
|
|
2315
2327
|
while any([host.returncode is None for host in hosts]):
|
|
2316
2328
|
time.sleep(sleep_interval) # avoid busy-waiting
|
|
@@ -2358,13 +2370,13 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
2358
2370
|
oldDic[line.split(',')[0]] = int(line.split(',')[1])
|
|
2359
2371
|
except:
|
|
2360
2372
|
pass
|
|
2361
|
-
# remove entries that are either available now or older than min(timeout,3600) seconds
|
|
2362
2373
|
for key in list(oldDic.keys()):
|
|
2363
|
-
if key in reachableHosts or time.monotonic() < oldDic[key] or time.monotonic() - oldDic[key] >
|
|
2374
|
+
if key in reachableHosts or time.monotonic() < oldDic[key] or time.monotonic() - oldDic[key] > unavailable_host_expiry:
|
|
2364
2375
|
del oldDic[key]
|
|
2365
2376
|
# add new entries
|
|
2366
2377
|
for host in unavailableHosts:
|
|
2367
|
-
|
|
2378
|
+
if host not in oldDic:
|
|
2379
|
+
oldDic[host] = int(time.monotonic ())
|
|
2368
2380
|
with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
|
|
2369
2381
|
for key, value in oldDic.items():
|
|
2370
2382
|
f.write(f'{key},{value}\n')
|
|
@@ -2403,20 +2415,24 @@ def formHostStr(host) -> str:
|
|
|
2403
2415
|
|
|
2404
2416
|
@cache_decorator
|
|
2405
2417
|
def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
2406
|
-
|
|
2418
|
+
no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
2407
2419
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
|
|
2408
2420
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
2409
2421
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
|
|
2410
2422
|
file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
2411
|
-
copy_id = False,
|
|
2423
|
+
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY, no_history = DEFAULT_NO_HISTORY,
|
|
2424
|
+
history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
|
|
2425
|
+
repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
|
|
2412
2426
|
shortend = False) -> str:
|
|
2413
2427
|
argsList = []
|
|
2414
2428
|
if oneonone: argsList.append('--oneonone' if not shortend else '-11')
|
|
2415
2429
|
if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
|
|
2430
|
+
if repeat and repeat != DEFAULT_REPEAT: argsList.append(f'--repeat={repeat}' if not shortend else f'-r={repeat}')
|
|
2431
|
+
if interval and interval != DEFAULT_INTERVAL: argsList.append(f'--interval={interval}' if not shortend else f'-i={interval}')
|
|
2416
2432
|
if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
|
|
2417
2433
|
if identity_file and identity_file != DEFAULT_IDENTITY_FILE: argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
|
|
2418
2434
|
if copy_id: argsList.append('--copy_id' if not shortend else '-ci')
|
|
2419
|
-
if
|
|
2435
|
+
if no_watch: argsList.append('--no_watch' if not shortend else '-q')
|
|
2420
2436
|
if json: argsList.append('--json' if not shortend else '-j')
|
|
2421
2437
|
if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
|
|
2422
2438
|
if files: argsList.extend([f'--file="{file}"' for file in files] if not shortend else [f'-f="{file}"' for file in files])
|
|
@@ -2427,7 +2443,11 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
2427
2443
|
if username and username != DEFAULT_USERNAME: argsList.append(f'--username="{username}"' if not shortend else f'-u="{username}"')
|
|
2428
2444
|
if extraargs and extraargs != DEFAULT_EXTRA_ARGS: argsList.append(f'--extraargs="{extraargs}"' if not shortend else f'-ea="{extraargs}"')
|
|
2429
2445
|
if skipUnreachable: argsList.append('--skip_unreachable' if not shortend else '-su')
|
|
2446
|
+
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}')
|
|
2430
2447
|
if no_env: argsList.append('--no_env')
|
|
2448
|
+
if env_file and env_file != DEFAULT_ENV_FILE: argsList.append(f'--env_file="{env_file}"' if not shortend else f'-ef="{env_file}"')
|
|
2449
|
+
if no_history: argsList.append('--no_history' if not shortend else '-nh')
|
|
2450
|
+
if history_file and history_file != DEFAULT_HISTORY_FILE: argsList.append(f'--history_file="{history_file}"' if not shortend else f'-hf="{history_file}"')
|
|
2431
2451
|
if greppable: argsList.append('--greppable' if not shortend else '-g')
|
|
2432
2452
|
if error_only: argsList.append('--error_only' if not shortend else '-eo')
|
|
2433
2453
|
if skip_hosts and skip_hosts != DEFAULT_SKIP_HOSTS: argsList.append(f'--skip_hosts="{skip_hosts}"' if not shortend else f'-sh="{skip_hosts}"')
|
|
@@ -2435,13 +2455,15 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
2435
2455
|
return ' '.join(argsList)
|
|
2436
2456
|
|
|
2437
2457
|
def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
2438
|
-
|
|
2458
|
+
no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
2439
2459
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
|
|
2440
2460
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
2441
2461
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
2442
2462
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
2443
2463
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
2444
|
-
copy_id = False,
|
|
2464
|
+
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = DEFAULT_NO_HISTORY,
|
|
2465
|
+
history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILE,
|
|
2466
|
+
repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
|
|
2445
2467
|
shortend = False):
|
|
2446
2468
|
_ = called
|
|
2447
2469
|
_ = returnUnfinished
|
|
@@ -2454,24 +2476,66 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
2454
2476
|
hostStr = formHostStr(hosts)
|
|
2455
2477
|
files = frozenset(files) if files else None
|
|
2456
2478
|
argsStr = __formCommandArgStr(oneonone = oneonone, timeout = timeout,password = password,
|
|
2457
|
-
|
|
2458
|
-
files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,
|
|
2459
|
-
username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,
|
|
2460
|
-
greppable=greppable,skip_hosts = skip_hosts,
|
|
2461
|
-
|
|
2479
|
+
no_watch = no_watch,json = json,max_connections=max_connections,
|
|
2480
|
+
files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,
|
|
2481
|
+
scp=scp,gather_mode = gather_mode,username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,
|
|
2482
|
+
no_env=no_env, greppable=greppable,skip_hosts = skip_hosts,
|
|
2483
|
+
file_sync = file_sync,error_only = error_only, identity_file = identity_file,
|
|
2484
|
+
copy_id = copy_id, unavailable_host_expiry =unavailable_host_expiry,no_history = no_history,
|
|
2485
|
+
history_file = history_file, env_file = env_file,
|
|
2486
|
+
repeat = repeat,interval = interval,
|
|
2462
2487
|
shortend = shortend)
|
|
2463
2488
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
2464
|
-
|
|
2489
|
+
programName = sys.argv[0] if (sys.argv and sys.argv[0]) else 'mssh'
|
|
2490
|
+
return f'{programName} {argsStr} {hostStr} {commandStr}'
|
|
2491
|
+
|
|
2492
|
+
#%% ------------ Record History Block ----------------
|
|
2493
|
+
def record_command_history(kwargs):
|
|
2494
|
+
'''
|
|
2495
|
+
Record the command history to a file
|
|
2496
|
+
|
|
2497
|
+
Args:
|
|
2498
|
+
args (str): The command arguments to record
|
|
2499
|
+
|
|
2500
|
+
Returns:
|
|
2501
|
+
None
|
|
2502
|
+
'''
|
|
2503
|
+
global __global_suppress_printout
|
|
2504
|
+
global __DEBUG_MODE
|
|
2505
|
+
try:
|
|
2506
|
+
history_file = os.path.expanduser(kwargs.get('history_file', DEFAULT_HISTORY_FILE))
|
|
2507
|
+
import inspect
|
|
2508
|
+
sig = inspect.signature(getStrCommand)
|
|
2509
|
+
wanted = {
|
|
2510
|
+
name: kwargs[name]
|
|
2511
|
+
for name in sig.parameters
|
|
2512
|
+
if name in kwargs
|
|
2513
|
+
}
|
|
2514
|
+
strCommand = getStrCommand(**wanted)
|
|
2515
|
+
with open(history_file, 'a') as f:
|
|
2516
|
+
# it follows <timestamp>\t<strCommand>\n
|
|
2517
|
+
f.write(f'{int(time.time())}\t{strCommand}\n')
|
|
2518
|
+
f.flush()
|
|
2519
|
+
os.fsync(f.fileno())
|
|
2520
|
+
if __DEBUG_MODE:
|
|
2521
|
+
eprint(f'Command history recorded to {history_file}')
|
|
2522
|
+
except Exception as e:
|
|
2523
|
+
eprint(f'Error recording command history: {e!r}')
|
|
2524
|
+
if __DEBUG_MODE:
|
|
2525
|
+
import traceback
|
|
2526
|
+
eprint(traceback.format_exc().strip())
|
|
2465
2527
|
|
|
2466
2528
|
#%% ------------ Main Block ----------------
|
|
2467
2529
|
def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
2468
|
-
|
|
2530
|
+
no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
2469
2531
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
|
|
2470
2532
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
2471
2533
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
2472
2534
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
2473
2535
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE,
|
|
2474
|
-
copy_id = False
|
|
2536
|
+
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = True,
|
|
2537
|
+
history_file = DEFAULT_HISTORY_FILE,
|
|
2538
|
+
):
|
|
2475
2539
|
f'''
|
|
2476
2540
|
Run the command on the hosts, aka multissh. main function
|
|
2477
2541
|
|
|
@@ -2481,7 +2545,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2481
2545
|
oneonone (bool, optional): Whether to run the commands one on one. Defaults to {DEFAULT_ONE_ON_ONE}.
|
|
2482
2546
|
timeout (int, optional): The timeout for the command. Defaults to {DEFAULT_TIMEOUT}.
|
|
2483
2547
|
password (str, optional): The password for the hosts. Defaults to {DEFAULT_PASSWORD}.
|
|
2484
|
-
|
|
2548
|
+
no_watch (bool, optional): Whether to print the output. Defaults to {DEFAULT_NO_WATCH}.
|
|
2485
2549
|
json (bool, optional): Whether to print the output in JSON format. Defaults to {DEFAULT_JSON_MODE}.
|
|
2486
2550
|
called (bool, optional): Whether the function is called by another function. Defaults to {_DEFAULT_CALLED}.
|
|
2487
2551
|
max_connections (int, optional): The maximum number of concurrent SSH sessions. Defaults to 4 * os.cpu_count().
|
|
@@ -2507,6 +2571,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2507
2571
|
quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
|
|
2508
2572
|
identity_file (str, optional): The identity file to use for the ssh connection. Defaults to {DEFAULT_IDENTITY_FILE}.
|
|
2509
2573
|
copy_id (bool, optional): Whether to copy the id to the hosts. Defaults to False.
|
|
2574
|
+
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}.
|
|
2575
|
+
no_history (bool, optional): Whether to not save the history of the command. Defaults to True.
|
|
2576
|
+
history_file (str, optional): The file to save the history of the command. Defaults to {DEFAULT_HISTORY_FILE}.
|
|
2510
2577
|
|
|
2511
2578
|
Returns:
|
|
2512
2579
|
list: A list of Host objects
|
|
@@ -2521,24 +2588,23 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2521
2588
|
global __keyPressesIn
|
|
2522
2589
|
_emo = False
|
|
2523
2590
|
_no_env = no_env
|
|
2591
|
+
if not no_history:
|
|
2592
|
+
_ = history_file
|
|
2593
|
+
record_command_history(locals())
|
|
2594
|
+
if error_only:
|
|
2595
|
+
__global_suppress_printout = True
|
|
2524
2596
|
if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
|
|
2525
|
-
if
|
|
2526
|
-
|
|
2527
|
-
else:
|
|
2528
|
-
checkTime = timeout
|
|
2529
|
-
if checkTime <= 0:
|
|
2530
|
-
checkTime = 60
|
|
2531
|
-
elif checkTime > 3600:
|
|
2532
|
-
checkTime = 3600
|
|
2597
|
+
if unavailable_host_expiry <= 0:
|
|
2598
|
+
unavailable_host_expiry = 10
|
|
2533
2599
|
try:
|
|
2534
2600
|
readed = False
|
|
2535
|
-
if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) <
|
|
2601
|
+
if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < unavailable_host_expiry:
|
|
2536
2602
|
|
|
2537
2603
|
with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
|
|
2538
2604
|
for line in f:
|
|
2539
2605
|
line = line.strip()
|
|
2540
2606
|
if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
|
|
2541
|
-
if int(line.split(',')[1]) < time.monotonic() and int(line.split(',')[1]) +
|
|
2607
|
+
if int(line.split(',')[1]) < time.monotonic() and int(line.split(',')[1]) + unavailable_host_expiry > time.monotonic():
|
|
2542
2608
|
__globalUnavailableHosts.add(line.split(',')[0])
|
|
2543
2609
|
readed = True
|
|
2544
2610
|
if readed and not __global_suppress_printout:
|
|
@@ -2641,7 +2707,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2641
2707
|
eprint(f"> {command}")
|
|
2642
2708
|
os.system(command)
|
|
2643
2709
|
if hosts:
|
|
2644
|
-
processRunOnHosts(timeout, password, max_connections, hosts
|
|
2710
|
+
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2711
|
+
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
2712
|
+
unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
|
|
2713
|
+
curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
|
|
2714
|
+
single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
|
|
2645
2715
|
else:
|
|
2646
2716
|
eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
|
|
2647
2717
|
if not commands:
|
|
@@ -2696,7 +2766,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2696
2766
|
if not __global_suppress_printout:
|
|
2697
2767
|
eprint(f"Running command: {command!r} on host: {host!r}")
|
|
2698
2768
|
if not __global_suppress_printout: eprint('-'*80)
|
|
2699
|
-
if not no_start:
|
|
2769
|
+
if not no_start:
|
|
2770
|
+
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2771
|
+
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
2772
|
+
unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
|
|
2773
|
+
curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
|
|
2774
|
+
single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
|
|
2700
2775
|
return hosts
|
|
2701
2776
|
else:
|
|
2702
2777
|
allHosts = []
|
|
@@ -2722,7 +2797,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2722
2797
|
if no_start:
|
|
2723
2798
|
eprint(f"Warning: no_start is set, the command will not be started. As we are in interactive mode, no action will be done.")
|
|
2724
2799
|
else:
|
|
2725
|
-
processRunOnHosts(timeout, password, max_connections, hosts
|
|
2800
|
+
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2801
|
+
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
2802
|
+
unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
|
|
2803
|
+
curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
|
|
2804
|
+
single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
|
|
2726
2805
|
return hosts
|
|
2727
2806
|
for command in commands:
|
|
2728
2807
|
hosts = []
|
|
@@ -2739,7 +2818,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2739
2818
|
eprint('-'*80)
|
|
2740
2819
|
eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
2741
2820
|
eprint('-'*80)
|
|
2742
|
-
if not no_start:
|
|
2821
|
+
if not no_start:
|
|
2822
|
+
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2823
|
+
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
2824
|
+
unavailableHosts=unavailableHosts,willUpdateUnreachableHosts=willUpdateUnreachableHosts,
|
|
2825
|
+
curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,
|
|
2826
|
+
single_window=single_window,unavailable_host_expiry=unavailable_host_expiry)
|
|
2743
2827
|
allHosts += hosts
|
|
2744
2828
|
return allHosts
|
|
2745
2829
|
|
|
@@ -2769,12 +2853,13 @@ def generate_default_config(args):
|
|
|
2769
2853
|
'DEFAULT_FILE_SYNC': args.file_sync,
|
|
2770
2854
|
'DEFAULT_TIMEOUT': DEFAULT_TIMEOUT,
|
|
2771
2855
|
'DEFAULT_CLI_TIMEOUT': args.timeout,
|
|
2856
|
+
'DEFAULT_UNAVAILABLE_HOST_EXPIRY': args.unavailable_host_expiry,
|
|
2772
2857
|
'DEFAULT_REPEAT': args.repeat,
|
|
2773
2858
|
'DEFAULT_INTERVAL': args.interval,
|
|
2774
2859
|
'DEFAULT_IPMI': args.ipmi,
|
|
2775
2860
|
'DEFAULT_IPMI_INTERFACE_IP_PREFIX': args.ipmi_interface_ip_prefix,
|
|
2776
2861
|
'DEFAULT_INTERFACE_IP_PREFIX': args.interface_ip_prefix,
|
|
2777
|
-
'DEFAULT_NO_WATCH': args.
|
|
2862
|
+
'DEFAULT_NO_WATCH': args.no_watch,
|
|
2778
2863
|
'DEFAULT_CURSES_MINIMUM_CHAR_LEN': args.window_width,
|
|
2779
2864
|
'DEFAULT_CURSES_MINIMUM_LINE_LEN': args.window_height,
|
|
2780
2865
|
'DEFAULT_SINGLE_WINDOW': args.single_window,
|
|
@@ -2782,6 +2867,8 @@ def generate_default_config(args):
|
|
|
2782
2867
|
'DEFAULT_NO_OUTPUT': args.no_output,
|
|
2783
2868
|
'DEFAULT_NO_ENV': args.no_env,
|
|
2784
2869
|
'DEFAULT_ENV_FILE': args.env_file,
|
|
2870
|
+
'DEFAULT_NO_HISTORY': args.no_history,
|
|
2871
|
+
'DEFAULT_HISTORY_FILE': args.history_file,
|
|
2785
2872
|
'DEFAULT_MAX_CONNECTIONS': args.max_connections if args.max_connections != 4 * os.cpu_count() else None,
|
|
2786
2873
|
'DEFAULT_JSON_MODE': args.json,
|
|
2787
2874
|
'DEFAULT_PRINT_SUCCESS_HOSTS': args.success_hosts,
|
|
@@ -2857,38 +2944,42 @@ def main():
|
|
|
2857
2944
|
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)
|
|
2858
2945
|
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)
|
|
2859
2946
|
parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
|
|
2860
|
-
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
|
|
2861
|
-
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)
|
|
2862
|
-
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)
|
|
2947
|
+
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)
|
|
2948
|
+
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)
|
|
2949
|
+
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)
|
|
2863
2950
|
#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")
|
|
2864
|
-
parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds (default: {DEFAULT_CLI_TIMEOUT}
|
|
2951
|
+
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)
|
|
2952
|
+
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)
|
|
2865
2953
|
parser.add_argument("-r","--repeat", type=int, help=f"Repeat the command for a number of times (default: {DEFAULT_REPEAT})", default=DEFAULT_REPEAT)
|
|
2866
2954
|
parser.add_argument("-i","--interval", type=int, help=f"Interval between repeats in seconds (default: {DEFAULT_INTERVAL})", default=DEFAULT_INTERVAL)
|
|
2867
|
-
parser.add_argument("--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
|
|
2955
|
+
parser.add_argument('-M',"--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
|
|
2868
2956
|
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)
|
|
2869
2957
|
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)
|
|
2870
|
-
parser.add_argument("-q","-nw","--
|
|
2958
|
+
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)
|
|
2871
2959
|
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)
|
|
2872
2960
|
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)
|
|
2873
|
-
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)
|
|
2874
|
-
parser.add_argument('-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
|
|
2875
|
-
parser.add_argument("-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
|
|
2876
|
-
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)
|
|
2961
|
+
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)
|
|
2962
|
+
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)
|
|
2963
|
+
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)
|
|
2964
|
+
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)
|
|
2877
2965
|
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)
|
|
2878
2966
|
parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
|
|
2879
2967
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
2880
|
-
parser.add_argument("--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as
|
|
2881
|
-
parser.add_argument("-g","--greppable",'--table', action='store_true', help=f"Output in greppable
|
|
2968
|
+
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)
|
|
2969
|
+
parser.add_argument('-P',"-g","--greppable",'--table', action='store_true', help=f"Output in greppable table. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
2882
2970
|
group = parser.add_mutually_exclusive_group()
|
|
2883
|
-
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)
|
|
2884
|
-
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)
|
|
2885
|
-
|
|
2886
|
-
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)
|
|
2971
|
+
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)
|
|
2972
|
+
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)
|
|
2973
|
+
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)
|
|
2974
|
+
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)
|
|
2887
2975
|
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')
|
|
2888
2976
|
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)
|
|
2889
2977
|
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')
|
|
2890
2978
|
parser.add_argument('--debug', action='store_true', help='Print debug information')
|
|
2891
2979
|
parser.add_argument('-ci','--copy_id', action='store_true', help='Copy the ssh id to the hosts')
|
|
2980
|
+
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)
|
|
2981
|
+
parser.add_argument('-hf','--history_file', type=str, help=f'The file to store the history. (default: {DEFAULT_HISTORY_FILE})', default=DEFAULT_HISTORY_FILE)
|
|
2982
|
+
parser.add_argument('--script', action='store_true', help='Run the command in script mode, short for -SCRIPT or --no_watch --skip_unreachable --no_env --no_history --greppable --error_only')
|
|
2892
2983
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
2893
2984
|
|
|
2894
2985
|
# parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
|
|
@@ -2906,6 +2997,14 @@ def main():
|
|
|
2906
2997
|
if unknown:
|
|
2907
2998
|
eprint(f"Warning: Unknown arguments, treating all as commands: {unknown!r}")
|
|
2908
2999
|
args.commands += unknown
|
|
3000
|
+
|
|
3001
|
+
if args.script:
|
|
3002
|
+
args.no_watch = True
|
|
3003
|
+
args.skip_unreachable = True
|
|
3004
|
+
args.no_env = True
|
|
3005
|
+
args.no_history = True
|
|
3006
|
+
args.greppable = True
|
|
3007
|
+
args.error_only = True
|
|
2909
3008
|
|
|
2910
3009
|
if args.generate_config_file or args.store_config_file:
|
|
2911
3010
|
if args.store_config_file:
|
|
@@ -2918,9 +3017,10 @@ def main():
|
|
|
2918
3017
|
else:
|
|
2919
3018
|
configFileToWriteTo = args.config_file
|
|
2920
3019
|
write_default_config(args,configFileToWriteTo)
|
|
2921
|
-
if not args.commands
|
|
2922
|
-
|
|
2923
|
-
|
|
3020
|
+
if not args.commands:
|
|
3021
|
+
if configFileToWriteTo:
|
|
3022
|
+
with open(configFileToWriteTo,'r') as f:
|
|
3023
|
+
eprint(f"Config file content: \n{f.read()}")
|
|
2924
3024
|
sys.exit(0)
|
|
2925
3025
|
if args.config_file:
|
|
2926
3026
|
if os.path.exists(args.config_file):
|
|
@@ -2959,14 +3059,27 @@ def main():
|
|
|
2959
3059
|
|
|
2960
3060
|
if args.no_output:
|
|
2961
3061
|
__global_suppress_printout = True
|
|
3062
|
+
|
|
3063
|
+
if args.unavailable_host_expiry <= 0:
|
|
3064
|
+
eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
|
|
3065
|
+
args.unavailable_host_expiry = 10
|
|
3066
|
+
|
|
3067
|
+
if args.use_script_timeout:
|
|
3068
|
+
# set timeout to the default script timeout if timeout is not set
|
|
3069
|
+
if args.timeout == DEFAULT_CLI_TIMEOUT:
|
|
3070
|
+
args.timeout = DEFAULT_TIMEOUT
|
|
2962
3071
|
|
|
2963
3072
|
if not __global_suppress_printout:
|
|
2964
|
-
cmdStr = getStrCommand(args.hosts,args.commands,
|
|
2965
|
-
|
|
3073
|
+
cmdStr = getStrCommand(args.hosts,args.commands,
|
|
3074
|
+
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
3075
|
+
no_watch=args.no_watch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
2966
3076
|
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,
|
|
2967
3077
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
2968
3078
|
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,
|
|
2969
|
-
copy_id=args.copy_id
|
|
3079
|
+
copy_id=args.copy_id,unavailable_host_expiry=args.unavailable_host_expiry,no_history=args.no_history,
|
|
3080
|
+
history_file = args.history_file,
|
|
3081
|
+
env_file = args.env_file,
|
|
3082
|
+
repeat = args.repeat,interval = args.interval)
|
|
2970
3083
|
eprint('> ' + cmdStr)
|
|
2971
3084
|
if args.error_only:
|
|
2972
3085
|
__global_suppress_printout = True
|
|
@@ -2979,11 +3092,13 @@ def main():
|
|
|
2979
3092
|
if not __global_suppress_printout: eprint(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
|
|
2980
3093
|
hosts = run_command_on_hosts(args.hosts,args.commands,
|
|
2981
3094
|
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
2982
|
-
|
|
3095
|
+
no_watch=args.no_watch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
2983
3096
|
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,
|
|
2984
3097
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
2985
3098
|
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,
|
|
2986
|
-
copy_id=args.copy_id
|
|
3099
|
+
copy_id=args.copy_id,unavailable_host_expiry=args.unavailable_host_expiry,no_history=args.no_history,
|
|
3100
|
+
history_file = args.history_file,
|
|
3101
|
+
)
|
|
2987
3102
|
#print('*'*80)
|
|
2988
3103
|
|
|
2989
3104
|
#if not __global_suppress_printout: eprint('-'*80)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|