multiSSH3 5.0__tar.gz → 5.6__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.1
2
2
  Name: multiSSH3
3
- Version: 5.0
3
+ Version: 5.6
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.1
2
2
  Name: multiSSH3
3
- Version: 5.0
3
+ Version: 5.6
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
@@ -31,7 +31,7 @@ except AttributeError:
31
31
  # If neither is available, use a dummy decorator
32
32
  def cache_decorator(func):
33
33
  return func
34
- version = '5.00'
34
+ version = '5.06'
35
35
  VERSION = version
36
36
 
37
37
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -741,12 +741,14 @@ def ssh_command(host, sem, timeout=60,passwds=None):
741
741
  if passwds:
742
742
  formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
743
743
  else:
744
- formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} {" ".join(extraargs)} {host.command}']
744
+ host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
745
+ formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P admin {" ".join(extraargs)} {host.command}']
745
746
  else:
746
747
  if passwds:
747
748
  formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P {passwds}'] + extraargs + [host.command]
748
749
  else:
749
- formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}'] + extraargs + [host.command]
750
+ host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
751
+ formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P admin'] + extraargs + [host.command]
750
752
  elif 'ssh' in _binPaths:
751
753
  host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
752
754
  if __DEBUG_MODE:
@@ -980,7 +982,14 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
980
982
  host.printedLines = 0
981
983
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
982
984
 
983
- def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window=DEFAULT_SINGLE_WINDOW):
985
+ # Error: integer division or modulo by zero, Reloading Configuration: min_char_len=40, min_line_len=1, single_window=False with window size (61, 186) and 1 hosts...
986
+ # Traceback (most recent call last):
987
+ # File "/usr/local/lib/python3.11/site-packages/multiSSH3.py", line 1030, in generate_display
988
+ # host_window_height = max_y // num_hosts_y
989
+ # ~~~~~~^^~~~~~~~~~~~~
990
+ # ZeroDivisionError: integer division or modulo by zero
991
+
992
+ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window=DEFAULT_SINGLE_WINDOW, config_reason= 'New Configuration'):
984
993
  try:
985
994
  org_dim = stdscr.getmaxyx()
986
995
  new_configured = True
@@ -996,9 +1005,9 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
996
1005
  min_line_len_local = max_y-1
997
1006
  # return True if the terminal is too small
998
1007
  if max_x < 2 or max_y < 2:
999
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1008
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small')
1000
1009
  if min_char_len_local < 1 or min_line_len_local < 1:
1001
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1010
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Minimum character or line length too small')
1002
1011
  # We need to figure out how many hosts we can fit in the terminal
1003
1012
  # We will need at least 2 lines per host, one for its name, one for its output
1004
1013
  # Each line will be at least 61 characters long (60 for the output, 1 for the borders)
@@ -1006,14 +1015,14 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1006
1015
  max_num_hosts_y = max_y // (min_line_len_local + 1)
1007
1016
  max_num_hosts = max_num_hosts_x * max_num_hosts_y
1008
1017
  if max_num_hosts < 1:
1009
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1018
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small to display any hosts')
1010
1019
  hosts_to_display , host_stats = get_hosts_to_display(hosts, max_num_hosts)
1011
1020
  if len(hosts_to_display) == 0:
1012
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1021
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'No hosts to display')
1013
1022
  # Now we calculate the actual number of hosts we will display for x and y
1014
1023
  optimal_len_x = max(min_char_len_local, 80)
1015
- num_hosts_x = max(min(max_num_hosts_x, max_x // optimal_len_x),1)
1016
- num_hosts_y = len(hosts_to_display) // num_hosts_x
1024
+ num_hosts_x = min(max(min(max_num_hosts_x, max_x // optimal_len_x),1),len(hosts_to_display))
1025
+ num_hosts_y = len(hosts_to_display) // num_hosts_x + (len(hosts_to_display) % num_hosts_x > 0)
1017
1026
  while num_hosts_y > max_num_hosts_y:
1018
1027
  num_hosts_x += 1
1019
1028
  # round up for num_hosts_y
@@ -1025,12 +1034,12 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1025
1034
  num_hosts_x += 1
1026
1035
  num_hosts_y = len(hosts_to_display) // num_hosts_x + (len(hosts_to_display) % num_hosts_x > 0)
1027
1036
  break
1028
-
1037
+ num_hosts_y = max(num_hosts_y,1)
1029
1038
  # We calculate the size of each window
1030
1039
  host_window_height = max_y // num_hosts_y
1031
1040
  host_window_width = max_x // num_hosts_x
1032
1041
  if host_window_height < 1 or host_window_width < 1:
1033
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1042
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Host window too small')
1034
1043
 
1035
1044
  old_stat = ''
1036
1045
  old_bottom_stat = ''
@@ -1071,24 +1080,24 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1071
1080
  # with open('keylog.txt','a') as f:
1072
1081
  # f.write(str(key)+'\n')
1073
1082
  if key == 410: # 410 is the key code for resize
1074
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1083
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize requested')
1075
1084
  elif key == 95 and not __keyPressesIn[-1]: # 95 is the key code for _
1076
1085
  # if last line is empty, we will reconfigure the wh to be smaller
1077
1086
  if min_line_len != 1:
1078
- return (lineToDisplay,curserPosition , min_char_len , max(min_line_len -1,1), single_window)
1087
+ return (lineToDisplay,curserPosition , min_char_len , max(min_line_len -1,1), single_window, 'Decrease line length')
1079
1088
  elif key == 43 and not __keyPressesIn[-1]: # 43 is the key code for +
1080
1089
  # if last line is empty, we will reconfigure the wh to be larger
1081
- return (lineToDisplay,curserPosition , min_char_len , min_line_len +1, single_window)
1090
+ return (lineToDisplay,curserPosition , min_char_len , min_line_len +1, single_window, 'Increase line length')
1082
1091
  elif key == 123 and not __keyPressesIn[-1]: # 123 is the key code for {
1083
1092
  # if last line is empty, we will reconfigure the ww to be smaller
1084
1093
  if min_char_len != 1:
1085
- return (lineToDisplay,curserPosition , max(min_char_len -1,1), min_line_len, single_window)
1094
+ return (lineToDisplay,curserPosition , max(min_char_len -1,1), min_line_len, single_window, 'Decrease character length')
1086
1095
  elif key == 124 and not __keyPressesIn[-1]: # 124 is the key code for |
1087
1096
  # if last line is empty, we will toggle the single window mode
1088
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, not single_window)
1097
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, not single_window, 'Toggle single window mode')
1089
1098
  elif key == 125 and not __keyPressesIn[-1]: # 125 is the key code for }
1090
1099
  # if last line is empty, we will reconfigure the ww to be larger
1091
- return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window)
1100
+ return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window, 'Increase character length')
1092
1101
  # We handle positional keys
1093
1102
  # if the key is up arrow, we will move the line to display up
1094
1103
  elif key == 259: # 259 is the key code for up arrow
@@ -1147,7 +1156,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1147
1156
  curserPosition += 1
1148
1157
  # reconfigure when the terminal size changes
1149
1158
  if org_dim != stdscr.getmaxyx():
1150
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1159
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
1151
1160
  # We generate the aggregated stats if user did not input anything
1152
1161
  if not __keyPressesIn[lineToDisplay]:
1153
1162
  stats = '┍'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} ww: {min_char_len} wh:{min_line_len} "[:max_x - 2].center(max_x - 2, "━")
@@ -1212,11 +1221,12 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1212
1221
  # print(str(e).strip())
1213
1222
  # print(traceback.format_exc().strip())
1214
1223
  if org_dim != stdscr.getmaxyx():
1215
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1224
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
1216
1225
  new_configured = False
1217
1226
  last_refresh_time = time.perf_counter()
1218
1227
  except Exception as e:
1219
- return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
1228
+ import traceback
1229
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, f'Error: {str(e)}',traceback.format_exc())
1220
1230
  return None
1221
1231
 
1222
1232
  def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
@@ -1255,7 +1265,12 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1255
1265
  curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
1256
1266
  curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
1257
1267
  curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
1258
- params = (-1,0 , min_char_len, min_line_len, single_window)
1268
+
1269
+ # do not generate display if the output window have a size of zero
1270
+ if stdscr.getmaxyx()[0] < 2 or stdscr.getmaxyx()[1] < 2:
1271
+ return
1272
+
1273
+ params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
1259
1274
  while params:
1260
1275
  params = generate_display(stdscr, hosts, *params)
1261
1276
  if not params:
@@ -1265,8 +1280,19 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1265
1280
  break
1266
1281
  # print the current configuration
1267
1282
  stdscr.clear()
1268
- stdscr.addstr(0, 0, f"Loading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]}")
1269
- stdscr.refresh()
1283
+ try:
1284
+ stdscr.addstr(0, 0, f"{params[5]}, Reloading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]} with window size {stdscr.getmaxyx()} and {len(hosts)} hosts...")
1285
+ if len(params) > 6:
1286
+ # traceback is available, print it
1287
+ i = 1
1288
+ for line in params[6].split('\n'):
1289
+ stdscr.addstr(i, 0, line)
1290
+ i += 1
1291
+ stdscr.refresh()
1292
+ except:
1293
+ pass
1294
+ params = params[:5]
1295
+ time.sleep(0.01)
1270
1296
  #time.sleep(0.25)
1271
1297
 
1272
1298
 
@@ -1418,7 +1444,10 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1418
1444
  thread.join(timeout=3)
1419
1445
  # update the unavailable hosts and global unavailable hosts
1420
1446
  if willUpdateUnreachableHosts:
1447
+ unavailableHosts = set(unavailableHosts)
1421
1448
  unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or host.stderr[0].strip().startswith('Timeout!'))])
1449
+ # reachable hosts = all hosts - unreachable hosts
1450
+ reachableHosts = set([host.name for host in hosts]) - unavailableHosts
1422
1451
  if __DEBUG_MODE:
1423
1452
  print(f'Unreachable hosts: {unavailableHosts}')
1424
1453
  __globalUnavailableHosts.update(unavailableHosts)
@@ -1427,18 +1456,31 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1427
1456
  # create a temporary file to store the unavailable hosts
1428
1457
  try:
1429
1458
  # check for the old content, only update if the new content is different
1430
- if not os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')):
1431
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'w') as f:
1432
- f.write(','.join(unavailableHosts))
1459
+ if not os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
1460
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
1461
+ f.write(f',{int(time.time())}\n'.join(unavailableHosts) + f',{int(time.time())}\n')
1433
1462
  else:
1463
+ oldDic = {}
1434
1464
  try:
1435
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'r') as f:
1436
- oldSet = set(f.read().strip().split(','))
1465
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
1466
+ for line in f:
1467
+ line = line.strip()
1468
+ if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
1469
+ oldDic[line.split(',')[0]] = int(line.split(',')[1])
1437
1470
  except:
1438
- oldSet = None
1439
- if not oldSet or set(oldSet) != unavailableHosts:
1440
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'w') as f:
1441
- f.write(','.join(unavailableHosts))
1471
+ pass
1472
+ # remove entries that are either available now or older than min(timeout,3600) seconds
1473
+ for key in list(oldDic.keys()):
1474
+ if key in reachableHosts or time.time() - oldDic[key] > min(timeout,3600):
1475
+ del oldDic[key]
1476
+ # add new entries
1477
+ for host in unavailableHosts:
1478
+ oldDic[host] = int(time.time())
1479
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),'w') as f:
1480
+ for key, value in oldDic.items():
1481
+ f.write(f'{key},{value}\n')
1482
+ os.replace(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'))
1483
+
1442
1484
  except Exception as e:
1443
1485
  eprint(f'Error writing to temporary file: {e}')
1444
1486
 
@@ -1573,23 +1615,27 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1573
1615
  global __DEBUG_MODE
1574
1616
  _emo = False
1575
1617
  _no_env = no_env
1576
- if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')):
1618
+ if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
1577
1619
  if timeout <= 0:
1578
1620
  checkTime = DEFAULT_TIMEOUT
1579
1621
  else:
1580
1622
  checkTime = timeout
1581
1623
  if checkTime <= 0:
1582
1624
  checkTime = 60
1625
+ elif checkTime > 3600:
1626
+ checkTime = 3600
1583
1627
  try:
1584
- if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')) < checkTime:
1628
+ if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < checkTime:
1585
1629
  if not __global_suppress_printout:
1586
- eprint(f"Reading unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')}")
1587
- with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'r') as f:
1588
- __globalUnavailableHosts.update(f.read().strip().split(','))
1589
- if __DEBUG_MODE:
1590
- eprint(f"Unavailable hosts: {__globalUnavailableHosts}")
1630
+ eprint(f"Reading unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
1631
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
1632
+ for line in f:
1633
+ line = line.strip()
1634
+ if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
1635
+ if int(line.split(',')[1]) > time.time() - checkTime:
1636
+ __globalUnavailableHosts.add(line.split(',')[0])
1591
1637
  except Exception as e:
1592
- eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')}")
1638
+ eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
1593
1639
  eprint(str(e))
1594
1640
  elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
1595
1641
  __globalUnavailableHosts.update(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
@@ -1888,9 +1934,9 @@ def main():
1888
1934
  #args = parser.parse_args()
1889
1935
 
1890
1936
  # if python version is 3.7 or higher, use parse_intermixed_args
1891
- if sys.version_info >= (3,7):
1937
+ try:
1892
1938
  args = parser.parse_intermixed_args()
1893
- else:
1939
+ except:
1894
1940
  # try to parse the arguments using parse_known_args
1895
1941
  args, unknown = parser.parse_known_args()
1896
1942
  # if there are unknown arguments, we will try to parse them again using parse_args
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name='multiSSH3',
5
- version='5.00',
5
+ version='5.06',
6
6
  description='Run commands on multiple hosts via SSH',
7
7
  long_description=open('README.md').read(),
8
8
  long_description_content_type='text/markdown',
File without changes
File without changes
File without changes