multiSSH3 4.89__tar.gz → 4.92__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-4.89 → multissh3-4.92}/PKG-INFO +1 -1
- {multissh3-4.89 → multissh3-4.92}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-4.89 → multissh3-4.92}/multiSSH3.py +40 -23
- {multissh3-4.89 → multissh3-4.92}/setup.py +1 -1
- {multissh3-4.89 → multissh3-4.92}/LICENSE +0 -0
- {multissh3-4.89 → multissh3-4.92}/README.md +0 -0
- {multissh3-4.89 → multissh3-4.92}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-4.89 → multissh3-4.92}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-4.89 → multissh3-4.92}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-4.89 → multissh3-4.92}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-4.89 → multissh3-4.92}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-4.89 → multissh3-4.92}/setup.cfg +0 -0
|
@@ -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.
|
|
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
|
-
|
|
754
|
-
|
|
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() -
|
|
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() -
|
|
769
|
-
timeoutLine = f'Timeout in [{timeout - int(time.time() -
|
|
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
|
-
|
|
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'
|
|
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,
|
|
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='
|
|
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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|