multiSSH3 5.60__py3-none-any.whl → 5.63__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.

Potentially problematic release.


This version of multiSSH3 might be problematic. Click here for more details.

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.60'
57
+ version = '5.63'
58
58
  VERSION = version
59
59
  __version__ = version
60
- COMMIT_DATE = '2025-03-27'
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
- hosts = [dict(host) for host in hosts]
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, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
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 nowatch and threads and not returnUnfinished and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
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] > min(timeout,3600):
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
- oldDic[host] = int(time.monotonic ())
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
- nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
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 nowatch: argsList.append('--nowatch' if not shortend else '-q')
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
- nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
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
- nowatch = nowatch,json = json,max_connections=max_connections,
2458
- files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
2459
- username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
2460
- greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, identity_file = identity_file,
2461
- copy_id = copy_id,
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
- return f'multissh {argsStr} {hostStr} {commandStr}'
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
- nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
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
- nowatch (bool, optional): Whether to print the output. Defaults to {DEFAULT_NO_WATCH}.
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 timeout <= 0:
2526
- checkTime = DEFAULT_TIMEOUT
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')) < checkTime:
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]) + checkTime > time.monotonic():
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, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
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: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
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, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
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: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
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.nowatch,
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 as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
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} (disabled))", 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","--nowatch","--quiet", action='store_true', help=f"Quiet mode, no curses watch, only print the output. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
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 wells. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
2881
- parser.add_argument("-g","--greppable",'--table', action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
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 and configFileToWriteTo:
2922
- with open(configFileToWriteTo,'r') as f:
2923
- eprint(f"Config file content: \n{f.read()}")
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,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
2965
- nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
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
- nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.60
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, --nowatch, --quiet
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` | `--nowatch` | Do not use curses mode; use simple output instead. |
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=VroQaLssLEEanOe2tUMi7necPNQFDPoGsBNeyoNxMYg,146375
2
+ multissh3-5.63.dist-info/METADATA,sha256=_DHi1kk_S1t2Oa1CEdPDlCIqUsbtfTnUjJVgTftJMxk,18093
3
+ multissh3-5.63.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
4
+ multissh3-5.63.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.63.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.63.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=8GeHlj1I5JK8tTPkmG0HhJd9yRvWWWdXfqSnWDtc9bY,139908
2
- multissh3-5.60.dist-info/METADATA,sha256=fFk9RztULUYPZ2vjrEtjMIU0se1yPp_YhGifryFNZSQ,18091
3
- multissh3-5.60.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
4
- multissh3-5.60.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.60.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.60.dist-info/RECORD,,