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.
- {multissh3-5.39 → multissh3-5.41}/PKG-INFO +1 -1
- {multissh3-5.39 → multissh3-5.41}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-5.39 → multissh3-5.41}/multiSSH3.py +69 -26
- {multissh3-5.39 → multissh3-5.41}/setup.py +2 -1
- {multissh3-5.39 → multissh3-5.41}/LICENSE +0 -0
- {multissh3-5.39 → multissh3-5.41}/README.md +0 -0
- {multissh3-5.39 → multissh3-5.41}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-5.39 → multissh3-5.41}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-5.39 → multissh3-5.41}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-5.39 → multissh3-5.41}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-5.39 → multissh3-5.41}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-5.39 → multissh3-5.41}/setup.cfg +0 -0
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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 =
|
|
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=
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|