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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multiSSH3
3
- Version: 4.89
3
+ Version: 4.92
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
@@ -0,0 +1,7 @@
1
+ multiSSH3.py,sha256=XGiNVv-ZsYMGFVrZ8zpeK4wz170Fi8YRtltcRxrQQCI,88186
2
+ multiSSH3-4.92.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-4.92.dist-info/METADATA,sha256=lesBhivYV4dahRGYUWvo-jbxcOEhEl6VHhSjQ1U-7-M,16043
4
+ multiSSH3-4.92.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
+ multiSSH3-4.92.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-4.92.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-4.92.dist-info/RECORD,,
multiSSH3.py CHANGED
@@ -29,7 +29,7 @@ except AttributeError:
29
29
  # If neither is available, use a dummy decorator
30
30
  def cache_decorator(func):
31
31
  return func
32
- version = '4.89'
32
+ version = '4.92'
33
33
  VERSION = version
34
34
 
35
35
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -173,6 +173,7 @@ class Host:
173
173
  self.stdout = [] # the stdout of the command
174
174
  self.stderr = [] # the stderr of the command
175
175
  self.printedLines = -1 # the number of lines printed on the screen
176
+ self.lastUpdateTime = time.time() # the last time the output was updated
176
177
  self.files = files # the files to be copied to the host
177
178
  self.ipmi = ipmi # whether to use ipmi to connect to the host
178
179
  self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
@@ -541,6 +542,7 @@ def handle_reading_stream(stream,target, host):
541
542
  current_line_str = current_line.decode('utf-8',errors='backslashreplace')
542
543
  target.append(current_line_str)
543
544
  host.output.append(current_line_str)
545
+ host.lastUpdateTime = time.time()
544
546
  current_line = bytearray()
545
547
  lastLineCommited = True
546
548
  for char in iter(lambda:stream.read(1), b''):
@@ -584,6 +586,7 @@ def handle_writing_stream(stream,stop_event,host):
584
586
  host.output.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
585
587
  host.stdout.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
586
588
  sentInput += 1
589
+ host.lastUpdateTime = time.time()
587
590
  else:
588
591
  time.sleep(0.1)
589
592
  if sentInput < len(__keyPressesIn) - 1 :
@@ -750,14 +753,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
750
753
  stdin_thread = threading.Thread(target=handle_writing_stream, args=(proc.stdin,stdin_stop_event, host), daemon=True)
751
754
  stdin_thread.start()
752
755
  # Monitor the subprocess and terminate it after the timeout
753
- start_time = time.time()
754
- outLength = len(host.output)
756
+ host.lastUpdateTime = time.time()
757
+ timeoutLineAppended = False
755
758
  while proc.poll() is None: # while the process is still running
756
- if len(host.output) > outLength:
757
- start_time = time.time()
758
- outLength = len(host.output)
759
759
  if timeout > 0:
760
- if time.time() - start_time > timeout:
760
+ if time.time() - host.lastUpdateTime > timeout:
761
761
  host.stderr.append('Timeout!')
762
762
  host.output.append('Timeout!')
763
763
  proc.send_signal(signal.SIGINT)
@@ -765,15 +765,19 @@ def ssh_command(host, sem, timeout=60,passwds=None):
765
765
 
766
766
  proc.terminate()
767
767
  break
768
- elif time.time() - start_time > min(10, timeout // 2):
769
- timeoutLine = f'Timeout in [{timeout - int(time.time() - start_time)}] seconds!'
768
+ elif time.time() - host.lastUpdateTime > min(30, timeout // 2):
769
+ timeoutLine = f'Timeout in [{timeout - int(time.time() - host.lastUpdateTime)}] seconds!'
770
770
  if host.output and not host.output[-1].strip().startswith(timeoutLine):
771
771
  # remove last line if it is a countdown
772
- if host.output and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
772
+ if host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
773
773
  host.output.pop()
774
774
  host.printedLines -= 1
775
775
  host.output.append(timeoutLine)
776
- outLength = len(host.output)
776
+ timeoutLineAppended = True
777
+ elif host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
778
+ host.output.pop()
779
+ host.printedLines -= 1
780
+ timeoutLineAppended = False
777
781
  if _emo:
778
782
  host.stderr.append('Ctrl C detected, Emergency Stop!')
779
783
  host.output.append('Ctrl C detected, Emergency Stop!')
@@ -966,7 +970,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
966
970
  bottom_border = None
967
971
  if y + host_window_height < org_dim[0]:
968
972
  bottom_border = curses.newwin(1, max_x, y + host_window_height, 0)
969
- bottom_border.clear()
973
+ #bottom_border.clear()
970
974
  bottom_border.addstr(0, 0, '-' * (max_x - 1))
971
975
  bottom_border.refresh()
972
976
  while host_stats['running'] > 0 or host_stats['waiting'] > 0:
@@ -1059,7 +1063,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
1059
1063
  bottom_stats = '└'+ f"Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']}"[:max_x - 2].center(max_x - 2, "─")
1060
1064
  if bottom_stats != old_bottom_stat:
1061
1065
  old_bottom_stat = bottom_stats
1062
- bottom_border.clear()
1066
+ #bottom_border.clear()
1063
1067
  bottom_border.addstr(0, 0, bottom_stats)
1064
1068
  bottom_border.refresh()
1065
1069
  if stats != old_stat or curserPosition != old_cursor_position:
@@ -1070,7 +1074,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
1070
1074
  curserPositionStats = min(min(curserPosition,len(encodedLine) -1) + stats.find('Send CMD: ')+len('Send CMD: '), max_x -2)
1071
1075
  else:
1072
1076
  curserPositionStats = max_x -2
1073
- stat_window.clear()
1077
+ #stat_window.clear()
1074
1078
  #stat_window.addstr(0, 0, stats)
1075
1079
  # add the line with curser that inverses the color at the curser position
1076
1080
  stat_window.addstr(0, 0, stats[:curserPositionStats], curses.color_pair(1))
@@ -1086,7 +1090,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
1086
1090
  # we will only update the window if there is new output or the window is not fully printed
1087
1091
  if new_configured or host.printedLines < len(host.output):
1088
1092
  try:
1089
- host_window.clear()
1093
+ #host_window.clear()
1090
1094
  # we will try to center the name of the host with ┼ at the beginning and end and ─ in between
1091
1095
  linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
1092
1096
  host_window.addstr(0, 0, linePrintOut)
@@ -1094,12 +1098,12 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
1094
1098
  for i, line in enumerate(host.output[-(host_window_height - 1):]):
1095
1099
  # print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
1096
1100
  # time.sleep(10)
1097
- linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
1101
+ linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip().ljust(host_window_width - 1, ' ')
1098
1102
  host_window.addstr(i + 1, 0, linePrintOut)
1099
1103
  # we draw the rest of the available lines
1100
1104
  for i in range(len(host.output), host_window_height - 1):
1101
1105
  # print(f"Printng a line at {i + 1} with length of {len('│')}")
1102
- host_window.addstr(i + 1, 0, '│')
1106
+ host_window.addstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '))
1103
1107
  host.printedLines = len(host.output)
1104
1108
  host_window.refresh()
1105
1109
  except Exception as e:
@@ -1134,6 +1138,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1134
1138
  # We create all the windows we need
1135
1139
  # We initialize the color pair
1136
1140
  curses.start_color()
1141
+ curses.curs_set(0)
1137
1142
  curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
1138
1143
  curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
1139
1144
  curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
@@ -1216,7 +1221,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1216
1221
  rtnStr = ''
1217
1222
  for output, hosts in outputs.items():
1218
1223
  if __global_suppress_printout:
1219
- rtnStr += f'Error returncode produced by {hosts}:\n'
1224
+ rtnStr += f'Abnormal returncode produced by {hosts}:\n'
1220
1225
  rtnStr += output+'\n'
1221
1226
  else:
1222
1227
  rtnStr += '*'*80+'\n'
@@ -1373,7 +1378,8 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1373
1378
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1374
1379
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1375
1380
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1376
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, shortend = False):
1381
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
1382
+ shortend = False):
1377
1383
  hosts = hosts if type(hosts) == str else frozenset(hosts)
1378
1384
  hostStr = formHostStr(hosts)
1379
1385
  files = frozenset(files) if files else None
@@ -1391,7 +1397,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1391
1397
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1392
1398
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1393
1399
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1394
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY):
1400
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False):
1395
1401
  f'''
1396
1402
  Run the command on the hosts, aka multissh. main function
1397
1403
 
@@ -1423,6 +1429,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1423
1429
  min_line_len (int, optional): The minimum line number for each window of the curses output. Defaults to {DEFAULT_CURSES_MINIMUM_LINE_LEN}.
1424
1430
  single_window (bool, optional): Whether to use a single window for the curses output. Defaults to {DEFAULT_SINGLE_WINDOW}.
1425
1431
  file_sync (bool, optional): Whether to use file sync mode to sync directories. Defaults to {DEFAULT_FILE_SYNC}.
1432
+ error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
1433
+ quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
1426
1434
 
1427
1435
  Returns:
1428
1436
  list: A list of Host objects
@@ -1445,6 +1453,14 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1445
1453
  max_connections = (-max_connections) * os.cpu_count()
1446
1454
  if not commands:
1447
1455
  commands = []
1456
+ else:
1457
+ commands = [commands] if type(commands) == str else commands
1458
+ # reformat commands into a list of strings, join the iterables if they are not strings
1459
+ try:
1460
+ commands = [' '.join(command) if not type(command) == str else command for command in commands]
1461
+ except:
1462
+ pass
1463
+ print(f"Warning: commands should ideally be a list of strings. Now mssh had failed to convert {commands} to a list of strings. Continuing anyway but expect failures.")
1448
1464
  #verify_ssh_config()
1449
1465
  # load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
1450
1466
  if called:
@@ -1464,7 +1480,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1464
1480
  else:
1465
1481
  unavailableHosts = set()
1466
1482
  skipUnreachable = True
1467
-
1483
+ if quiet:
1484
+ __global_suppress_printout = True
1468
1485
  # We create the hosts
1469
1486
  hostStr = formHostStr(hosts)
1470
1487
  skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
@@ -1653,13 +1670,13 @@ def main():
1653
1670
  # We parse the arguments
1654
1671
  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: {CONFIG_FILE}')
1655
1672
  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)
1656
- 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.')
1673
+ 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.')
1657
1674
  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)
1658
1675
  parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
1659
1676
  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)
1660
1677
  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)
1661
1678
  parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
1662
- parser.add_argument('--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)
1679
+ 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)
1663
1680
  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)
1664
1681
  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)
1665
1682
  #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")
@@ -1,7 +0,0 @@
1
- multiSSH3.py,sha256=g_U5Kk6pDABART5Tb4yoViPZ-5fiARpb_8Irvs7f2QA,86990
2
- multiSSH3-4.89.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- multiSSH3-4.89.dist-info/METADATA,sha256=J9K4EyDzcP7Pvj4p779YAkYz7vbDGfQoxz9hmkWRra8,16043
4
- multiSSH3-4.89.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
- multiSSH3-4.89.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
- multiSSH3-4.89.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
- multiSSH3-4.89.dist-info/RECORD,,