multiSSH3 5.39__tar.gz → 5.41__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.2
2
2
  Name: multiSSH3
3
- Version: 5.39
3
+ Version: 5.41
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.2
2
2
  Name: multiSSH3
3
- Version: 5.39
3
+ Version: 5.41
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,13 +1,21 @@
1
1
  #!/usr/bin/env python3
2
2
  __curses_available = False
3
+ __resource_lib_available = False
3
4
  try:
4
5
  import curses
5
6
  __curses_available = True
6
7
  except ImportError:
7
8
  pass
9
+ try:
10
+ import resource
11
+ __resource_lib_available = True
12
+ except ImportError:
13
+ pass
14
+
8
15
  import subprocess
9
16
  import threading
10
- import time,os
17
+ import time
18
+ import os
11
19
  import argparse
12
20
  from itertools import product
13
21
  import re
@@ -37,7 +45,7 @@ except AttributeError:
37
45
  # If neither is available, use a dummy decorator
38
46
  def cache_decorator(func):
39
47
  return func
40
- version = '5.39'
48
+ version = '5.41'
41
49
  VERSION = version
42
50
 
43
51
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -352,26 +360,35 @@ if True:
352
360
  __curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
353
361
  __curses_color_table = {}
354
362
  __curses_current_color_index = 10
363
+ __max_connections_nofile_limit_supported = 0
364
+ __thread_start_delay = 0
365
+ if __resource_lib_available:
366
+ # Get the current limits
367
+ _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
368
+ # Set the soft limit to the hard limit
369
+ resource.setrlimit(resource.RLIMIT_NOFILE, (__system_nofile_limit, __system_nofile_limit))
370
+ __max_connections_nofile_limit_supported = int((__system_nofile_limit - 10) / 3)
355
371
 
356
372
  # Mapping of ANSI 4-bit colors to curses colors
357
- ANSI_TO_CURSES_COLOR = {
358
- 30: curses.COLOR_BLACK,
359
- 31: curses.COLOR_RED,
360
- 32: curses.COLOR_GREEN,
361
- 33: curses.COLOR_YELLOW,
362
- 34: curses.COLOR_BLUE,
363
- 35: curses.COLOR_MAGENTA,
364
- 36: curses.COLOR_CYAN,
365
- 37: curses.COLOR_WHITE,
366
- 90: curses.COLOR_BLACK, # Bright Black (usually gray)
367
- 91: curses.COLOR_RED, # Bright Red
368
- 92: curses.COLOR_GREEN, # Bright Green
369
- 93: curses.COLOR_YELLOW, # Bright Yellow
370
- 94: curses.COLOR_BLUE, # Bright Blue
371
- 95: curses.COLOR_MAGENTA, # Bright Magenta
372
- 96: curses.COLOR_CYAN, # Bright Cyan
373
- 97: curses.COLOR_WHITE # Bright White
374
- }
373
+ if __curses_available:
374
+ ANSI_TO_CURSES_COLOR = {
375
+ 30: curses.COLOR_BLACK,
376
+ 31: curses.COLOR_RED,
377
+ 32: curses.COLOR_GREEN,
378
+ 33: curses.COLOR_YELLOW,
379
+ 34: curses.COLOR_BLUE,
380
+ 35: curses.COLOR_MAGENTA,
381
+ 36: curses.COLOR_CYAN,
382
+ 37: curses.COLOR_WHITE,
383
+ 90: curses.COLOR_BLACK, # Bright Black (usually gray)
384
+ 91: curses.COLOR_RED, # Bright Red
385
+ 92: curses.COLOR_GREEN, # Bright Green
386
+ 93: curses.COLOR_YELLOW, # Bright Yellow
387
+ 94: curses.COLOR_BLUE, # Bright Blue
388
+ 95: curses.COLOR_MAGENTA, # Bright Magenta
389
+ 96: curses.COLOR_CYAN, # Bright Cyan
390
+ 97: curses.COLOR_WHITE # Bright White
391
+ }
375
392
  # ------------ Exportable Help Functions ----------------
376
393
  # check if command sshpass is available
377
394
  _binPaths = {}
@@ -1150,7 +1167,7 @@ def __handle_writing_stream(stream,stop_event,host):
1150
1167
  # host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
1151
1168
  return sentInput
1152
1169
 
1153
- def run_command(host, sem, timeout=60,passwds=None):
1170
+ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1154
1171
  '''
1155
1172
  Run the command on the host. Will format the commands accordingly. Main execution function.
1156
1173
 
@@ -1168,6 +1185,11 @@ def run_command(host, sem, timeout=60,passwds=None):
1168
1185
  global __ipmiiInterfaceIPPrefix
1169
1186
  global _binPaths
1170
1187
  global __DEBUG_MODE
1188
+ if retry_limit < 0:
1189
+ host.output.append('Error: Retry limit reached!')
1190
+ host.stderr.append('Error: Retry limit reached!')
1191
+ host.returncode = 1
1192
+ return
1171
1193
  try:
1172
1194
  localExtraArgs = []
1173
1195
 
@@ -1244,7 +1266,7 @@ def run_command(host, sem, timeout=60,passwds=None):
1244
1266
  host.command = 'ipmitool power status'
1245
1267
  else:
1246
1268
  host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
1247
- run_command(host,sem,timeout,passwds)
1269
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1)
1248
1270
  return
1249
1271
  else:
1250
1272
  host.output.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
@@ -1263,7 +1285,7 @@ def run_command(host, sem, timeout=60,passwds=None):
1263
1285
  host.stderr.append('shell not found on the local machine! Using ssh localhost instead...')
1264
1286
  host.shell = False
1265
1287
  host.name = 'localhost'
1266
- run_command(host,sem,timeout,passwds)
1288
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1)
1267
1289
  else:
1268
1290
  if host.files:
1269
1291
  if host.scp:
@@ -1404,6 +1426,16 @@ def run_command(host, sem, timeout=60,passwds=None):
1404
1426
  if host.stderr:
1405
1427
  # filter out the error messages that we want to ignore
1406
1428
  host.stderr = [line for line in host.stderr if not __ERROR_MESSAGES_TO_IGNORE_REGEX.search(line)]
1429
+ # except os error too many open files
1430
+ except OSError as e:
1431
+ if e.errno == 24: # Errno 24 corresponds to "Too many open files"
1432
+ host.output.append("Warning: Too many open files. retrying...")
1433
+ # Handle the error, e.g., clean up, retry logic, or exit
1434
+ time.sleep(0.1)
1435
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1)
1436
+ else:
1437
+ # Re-raise the exception if it's not the specific one
1438
+ raise
1407
1439
  except Exception as e:
1408
1440
  import traceback
1409
1441
  host.stderr.extend(str(e).split('\n'))
@@ -1420,7 +1452,7 @@ def run_command(host, sem, timeout=60,passwds=None):
1420
1452
  host.ipmi = False
1421
1453
  host.interface_ip_prefix = None
1422
1454
  host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
1423
- run_command(host,sem,timeout,passwds)
1455
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1)
1424
1456
  # If transfering files, we will try again using scp if rsync connection is not successful
1425
1457
  if host.files and not host.scp and not useScp and host.returncode != 0 and host.stderr:
1426
1458
  host.stderr = []
@@ -1429,7 +1461,7 @@ def run_command(host, sem, timeout=60,passwds=None):
1429
1461
  if __DEBUG_MODE:
1430
1462
  host.stderr.append('Rsync connection failed! Trying SCP connection...')
1431
1463
  host.scp = True
1432
- run_command(host,sem,timeout,passwds)
1464
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1)
1433
1465
 
1434
1466
  # ------------ Start Threading Block ----------------
1435
1467
  def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cpu_count()):
@@ -1445,12 +1477,14 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
1445
1477
  Returns:
1446
1478
  list: A list of threads that get started
1447
1479
  '''
1480
+ global __thread_start_delay
1448
1481
  if len(hosts) == 0:
1449
1482
  return []
1450
1483
  sem = threading.Semaphore(max_connections) # Limit concurrent SSH sessions
1451
1484
  threads = [threading.Thread(target=run_command, args=(host, sem,timeout,password), daemon=True) for host in hosts]
1452
1485
  for thread in threads:
1453
1486
  thread.start()
1487
+ time.sleep(__thread_start_delay)
1454
1488
  return threads
1455
1489
 
1456
1490
  # ------------ Display Block ----------------
@@ -2405,6 +2439,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2405
2439
  global _no_env
2406
2440
  global _emo
2407
2441
  global __DEBUG_MODE
2442
+ global __thread_start_delay
2443
+ global __max_connections_nofile_limit_supported
2408
2444
  _emo = False
2409
2445
  _no_env = no_env
2410
2446
  if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')):
@@ -2437,9 +2473,16 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2437
2473
  if not max_connections:
2438
2474
  max_connections = 4 * os.cpu_count()
2439
2475
  elif max_connections == 0:
2440
- max_connections = 1048576
2476
+ max_connections = __max_connections_nofile_limit_supported
2441
2477
  elif max_connections < 0:
2442
2478
  max_connections = (-max_connections) * os.cpu_count()
2479
+ if __max_connections_nofile_limit_supported > 0:
2480
+ if max_connections > __max_connections_nofile_limit_supported:
2481
+ eprint(f"Warning: The number of maximum connections {max_connections} is larger than estimated limit {__max_connections_nofile_limit_supported} from ulimit nofile limit {__system_nofile_limit}, setting the maximum connections to {__max_connections_nofile_limit_supported}.")
2482
+ max_connections = __max_connections_nofile_limit_supported
2483
+ if max_connections > __max_connections_nofile_limit_supported * 2:
2484
+ # we need to throttle thread start to avoid hitting the nofile limit
2485
+ __thread_start_delay = 0.001
2443
2486
  if not commands:
2444
2487
  commands = []
2445
2488
  else:
@@ -1,8 +1,9 @@
1
1
  from setuptools import setup
2
+ from multiSSH3 import version
2
3
 
3
4
  setup(
4
5
  name='multiSSH3',
5
- version='5.39',
6
+ version=version,
6
7
  description='Run commands on multiple hosts via SSH',
7
8
  long_description=open('README.md').read(),
8
9
  long_description_content_type='text/markdown',
File without changes
File without changes
File without changes