multiSSH3 5.67__tar.gz → 5.69__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.67
3
+ Version: 5.69
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.67
3
+ Version: 5.69
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
@@ -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.67'
57
+ version = '5.69'
58
58
  VERSION = version
59
59
  __version__ = version
60
- COMMIT_DATE = '2025-05-08'
60
+ COMMIT_DATE = '2025-05-09'
61
61
 
62
62
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
63
63
  '~/multiSSH3.config.json',
@@ -349,7 +349,7 @@ __failedHosts = set()
349
349
  __wildCharacters = ['*','?','x']
350
350
  _no_env = DEFAULT_NO_ENV
351
351
  _env_file = DEFAULT_ENV_FILE
352
- __globalUnavailableHosts = set()
352
+ __globalUnavailableHosts = dict()
353
353
  __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
354
354
  __keyPressesIn = [[]]
355
355
  _emo = False
@@ -389,6 +389,7 @@ if __curses_available:
389
389
  #%% ------------ Exportable Help Functions ----------------
390
390
  # check if command sshpass is available
391
391
  _binPaths = {}
392
+ _binCalled = set(['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id'])
392
393
  def check_path(program_name):
393
394
  global __configs_from_file
394
395
  global _binPaths
@@ -403,7 +404,7 @@ def check_path(program_name):
403
404
  return True
404
405
  return False
405
406
 
406
- [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id']]
407
+ [check_path(program) for program in _binCalled]
407
408
 
408
409
  def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
409
410
  '''
@@ -2234,7 +2235,7 @@ def generate_output(hosts, usejson = False, greppable = False):
2234
2235
  hostPrintOut = f" Command:\n {host['command']}\n"
2235
2236
  hostPrintOut += " stdout:\n "+'\n '.join(host['stdout'])
2236
2237
  if host['stderr']:
2237
- if host['stderr'][0].strip().startswith('ssh: connect to host '):
2238
+ if host['stderr'][0].strip().startswith('ssh: connect to host ') and host['stderr'][0].strip().endswith('Connection refused'):
2238
2239
  host['stderr'][0] = 'SSH not reachable!'
2239
2240
  elif host['stderr'][-1].strip().endswith('Connection timed out'):
2240
2241
  host['stderr'][-1] = 'SSH connection timed out!'
@@ -2286,7 +2287,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
2286
2287
 
2287
2288
  #%% ------------ Run / Process Hosts Block ----------------
2288
2289
  def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, no_watch, json, called, greppable,
2289
- unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN,
2290
+ unavailableHosts:dict,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN,
2290
2291
  curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW,
2291
2292
  unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY):
2292
2293
  global __globalUnavailableHosts
@@ -2316,45 +2317,47 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
2316
2317
  thread.join(timeout=3)
2317
2318
  # update the unavailable hosts and global unavailable hosts
2318
2319
  if willUpdateUnreachableHosts:
2319
- unavailableHosts = set(unavailableHosts)
2320
- unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or (host.stderr[-1].strip().startswith('Timeout!') and host.returncode == 124))])
2321
- # reachable hosts = all hosts - unreachable hosts
2322
- reachableHosts = set([host.name for host in hosts]) - unavailableHosts
2320
+ availableHosts = set()
2321
+ for host in hosts:
2322
+ if host.stderr and ('No route to host' in host.stderr[0].strip() or 'Connection timed out' in host.stderr[0].strip() or (host.stderr[-1].strip().startswith('Timeout!') and host.returncode == 124)):
2323
+ unavailableHosts[host.name] = int(time.monotonic())
2324
+ __globalUnavailableHosts[host.name] = int(time.monotonic())
2325
+ else:
2326
+ availableHosts.add(host.name)
2327
+ if host.name in unavailableHosts:
2328
+ del unavailableHosts[host.name]
2329
+ if host.name in __globalUnavailableHosts:
2330
+ del __globalUnavailableHosts[host.name]
2323
2331
  if __DEBUG_MODE:
2324
2332
  print(f'Unreachable hosts: {unavailableHosts}')
2325
- __globalUnavailableHosts.update(unavailableHosts)
2326
-
2327
- # os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
2328
- # create a temporary file to store the unavailable hosts
2329
2333
  try:
2330
2334
  # check for the old content, only update if the new content is different
2331
- if not os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
2332
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
2333
- f.write(f',{int(time.monotonic())}\n'.join(unavailableHosts) + f',{int(time.monotonic())}\n')
2335
+ if not os.path.exists(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')):
2336
+ with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
2337
+ f.writelines(f'{host},{expTime}' for host,expTime in unavailableHosts.values())
2334
2338
  else:
2335
2339
  oldDic = {}
2336
2340
  try:
2337
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
2341
+ with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
2338
2342
  for line in f:
2339
2343
  line = line.strip()
2340
2344
  if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
2341
- oldDic[line.split(',')[0]] = int(line.split(',')[1])
2345
+ hostname = line.split(',')[0]
2346
+ expireTime = int(line.split(',')[1])
2347
+ if expireTime < time.monotonic() and hostname not in availableHosts:
2348
+ oldDic[hostname] = expireTime
2342
2349
  except:
2343
2350
  pass
2344
- for key in list(oldDic.keys()):
2345
- if key in reachableHosts or time.monotonic() < oldDic[key] or time.monotonic() - oldDic[key] > unavailable_host_expiry:
2346
- del oldDic[key]
2347
2351
  # add new entries
2348
- for host in unavailableHosts:
2349
- if host not in oldDic:
2350
- oldDic[host] = int(time.monotonic ())
2351
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
2352
+ oldDic.update(unavailableHosts)
2353
+ with open(os.path.join(tempfile.gettempdir(),getpass.getuser()+'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
2352
2354
  for key, value in oldDic.items():
2353
2355
  f.write(f'{key},{value}\n')
2354
- os.replace(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'))
2355
-
2356
+ os.replace(os.path.join(tempfile.gettempdir(),getpass.getuser()+'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'))
2356
2357
  except Exception as e:
2357
2358
  eprint(f'Error writing to temporary file: {e!r}')
2359
+ import traceback
2360
+ eprint(traceback.format_exc())
2358
2361
 
2359
2362
  # print the output, if the output of multiple hosts are the same, we aggragate them
2360
2363
  if not called:
@@ -2565,27 +2568,29 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2565
2568
  record_command_history(locals())
2566
2569
  if error_only:
2567
2570
  __global_suppress_printout = True
2568
- if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
2571
+ if os.path.exists(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')):
2569
2572
  if unavailable_host_expiry <= 0:
2570
2573
  unavailable_host_expiry = 10
2571
2574
  try:
2572
2575
  readed = False
2573
- if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < unavailable_host_expiry:
2576
+ if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')) < unavailable_host_expiry:
2574
2577
 
2575
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
2578
+ with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
2576
2579
  for line in f:
2577
2580
  line = line.strip()
2578
2581
  if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
2579
- if int(line.split(',')[1]) < time.monotonic() and int(line.split(',')[1]) + unavailable_host_expiry > time.monotonic():
2580
- __globalUnavailableHosts.add(line.split(',')[0])
2582
+ hostname = line.split(',')[0]
2583
+ expireTime = int(line.split(',')[1])
2584
+ if expireTime < time.monotonic() and expireTime + unavailable_host_expiry > time.monotonic():
2585
+ __globalUnavailableHosts[hostname] = expireTime
2581
2586
  readed = True
2582
2587
  if readed and not __global_suppress_printout:
2583
- eprint(f"Read unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
2588
+ eprint(f"Read unavailable hosts from the file {os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')}")
2584
2589
  except Exception as e:
2585
- eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')!r}")
2590
+ eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')!r}")
2586
2591
  eprint(str(e))
2587
2592
  elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
2588
- __globalUnavailableHosts.update(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
2593
+ __globalUnavailableHosts.update({host: int(time.monotonic()) for host in readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(',') if host})
2589
2594
  if not max_connections:
2590
2595
  max_connections = 4 * os.cpu_count()
2591
2596
  elif max_connections == 0:
@@ -2618,7 +2623,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2618
2623
  if skipUnreachable:
2619
2624
  unavailableHosts = __globalUnavailableHosts
2620
2625
  else:
2621
- unavailableHosts = set()
2626
+ unavailableHosts = dict()
2622
2627
  # set global input to empty
2623
2628
  __keyPressesIn = [[]]
2624
2629
  __global_suppress_printout = True
@@ -2627,7 +2632,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2627
2632
  if skipUnreachable:
2628
2633
  unavailableHosts = __globalUnavailableHosts
2629
2634
  else:
2630
- unavailableHosts = set()
2635
+ unavailableHosts = dict()
2631
2636
  skipUnreachable = True
2632
2637
  if quiet:
2633
2638
  __global_suppress_printout = True
@@ -2906,7 +2911,8 @@ def main():
2906
2911
  # We handle the signal
2907
2912
  signal.signal(signal.SIGINT, signal_handler)
2908
2913
  # We parse the arguments
2909
- parser = argparse.ArgumentParser(description=f'Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file chain: {CONFIG_FILE_CHAIN!r}')
2914
+ parser = argparse.ArgumentParser(description=f'Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file chain: {CONFIG_FILE_CHAIN!r}',
2915
+ epilog=f'Found bins: {list(_binPaths.values())}\n Missing bins: {_binCalled - set(_binPaths.keys())}')
2910
2916
  parser.add_argument('hosts', metavar='hosts', type=str, nargs='?', help=f'Hosts to run the command on, use "," to seperate hosts. (default: {DEFAULT_HOSTS})',default=DEFAULT_HOSTS)
2911
2917
  parser.add_argument('commands', metavar='commands', type=str, nargs='*',default=None,help='the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host name.')
2912
2918
  parser.add_argument('-u','--username', type=str,help=f'The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified. (default: {DEFAULT_USERNAME})',default=DEFAULT_USERNAME)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes