multiSSH3 5.55__py3-none-any.whl → 5.58__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.
- multiSSH3.py +209 -204
- {multissh3-5.55.dist-info → multissh3-5.58.dist-info}/METADATA +1 -1
- multissh3-5.58.dist-info/RECORD +6 -0
- {multissh3-5.55.dist-info → multissh3-5.58.dist-info}/WHEEL +1 -1
- multissh3-5.55.dist-info/RECORD +0 -6
- {multissh3-5.55.dist-info → multissh3-5.58.dist-info}/entry_points.txt +0 -0
- {multissh3-5.55.dist-info → multissh3-5.58.dist-info}/top_level.txt +0 -0
multiSSH3.py
CHANGED
|
@@ -54,10 +54,10 @@ except AttributeError:
|
|
|
54
54
|
# If neither is available, use a dummy decorator
|
|
55
55
|
def cache_decorator(func):
|
|
56
56
|
return func
|
|
57
|
-
version = '5.
|
|
57
|
+
version = '5.58'
|
|
58
58
|
VERSION = version
|
|
59
59
|
__version__ = version
|
|
60
|
-
COMMIT_DATE = '2025-03-
|
|
60
|
+
COMMIT_DATE = '2025-03-06'
|
|
61
61
|
|
|
62
62
|
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
63
63
|
'~/multiSSH3.config.json',
|
|
@@ -69,7 +69,7 @@ CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
|
69
69
|
|
|
70
70
|
# TODO: Add terminal TUI with history support
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
#%% ------------ Pre Helper Functions ----------------
|
|
73
73
|
def eprint(*args, **kwargs):
|
|
74
74
|
try:
|
|
75
75
|
print(*args, file=sys.stderr, **kwargs)
|
|
@@ -216,7 +216,7 @@ def _get_i():
|
|
|
216
216
|
'''
|
|
217
217
|
return next(_i_counter)
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
#%% ------------ Host Object ----------------
|
|
220
220
|
class Host:
|
|
221
221
|
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,shell=False,i = -1,uuid=uuid.uuid4(),ip = None):
|
|
222
222
|
self.name = name # the name of the host (hostname or IP address)
|
|
@@ -255,7 +255,7 @@ identity_file={self.identity_file}, ip={self.ip}, current_color_pair={self.curre
|
|
|
255
255
|
def __str__(self):
|
|
256
256
|
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
#%% ------------ Load Defaults ( Config ) File ----------------
|
|
259
259
|
def load_config_file(config_file):
|
|
260
260
|
'''
|
|
261
261
|
Load the config file to global variables
|
|
@@ -276,103 +276,101 @@ def load_config_file(config_file):
|
|
|
276
276
|
return {}
|
|
277
277
|
return config
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
# Mapping of ANSI 4-bit colors to curses colors
|
|
279
|
+
#%% ------------ Global Variables ----------------
|
|
280
|
+
AUTHOR = 'Yufei Pan'
|
|
281
|
+
AUTHOR_EMAIL = 'pan@zopyr.us'
|
|
282
|
+
DEFAULT_HOSTS = 'all'
|
|
283
|
+
DEFAULT_USERNAME = None
|
|
284
|
+
DEFAULT_PASSWORD = ''
|
|
285
|
+
DEFAULT_IDENTITY_FILE = None
|
|
286
|
+
DEDAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
|
|
287
|
+
DEFAULT_USE_KEY = False
|
|
288
|
+
DEFAULT_EXTRA_ARGS = None
|
|
289
|
+
DEFAULT_ONE_ON_ONE = False
|
|
290
|
+
DEFAULT_SCP = False
|
|
291
|
+
DEFAULT_FILE_SYNC = False
|
|
292
|
+
DEFAULT_TIMEOUT = 50
|
|
293
|
+
DEFAULT_CLI_TIMEOUT = 0
|
|
294
|
+
DEFAULT_REPEAT = 1
|
|
295
|
+
DEFAULT_INTERVAL = 0
|
|
296
|
+
DEFAULT_IPMI = False
|
|
297
|
+
DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
|
|
298
|
+
DEFAULT_INTERFACE_IP_PREFIX = None
|
|
299
|
+
DEFAULT_NO_WATCH = False
|
|
300
|
+
DEFAULT_CURSES_MINIMUM_CHAR_LEN = 40
|
|
301
|
+
DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
|
|
302
|
+
DEFAULT_SINGLE_WINDOW = False
|
|
303
|
+
DEFAULT_ERROR_ONLY = False
|
|
304
|
+
DEFAULT_NO_OUTPUT = False
|
|
305
|
+
DEFAULT_NO_ENV = False
|
|
306
|
+
DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
|
|
307
|
+
DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
|
|
308
|
+
DEFAULT_JSON_MODE = False
|
|
309
|
+
DEFAULT_PRINT_SUCCESS_HOSTS = False
|
|
310
|
+
DEFAULT_GREPPABLE_MODE = False
|
|
311
|
+
DEFAULT_SKIP_UNREACHABLE = True
|
|
312
|
+
DEFAULT_SKIP_HOSTS = ''
|
|
313
|
+
SSH_STRICT_HOST_KEY_CHECKING = False
|
|
314
|
+
ERROR_MESSAGES_TO_IGNORE = [
|
|
315
|
+
'Pseudo-terminal will not be allocated because stdin is not a terminal',
|
|
316
|
+
'Connection to .* closed',
|
|
317
|
+
'Warning: Permanently added',
|
|
318
|
+
'mux_client_request_session',
|
|
319
|
+
'disabling multiplexing',
|
|
320
|
+
'Killed by signal',
|
|
321
|
+
'Connection reset by peer',
|
|
322
|
+
]
|
|
323
|
+
_DEFAULT_CALLED = True
|
|
324
|
+
_DEFAULT_RETURN_UNFINISHED = False
|
|
325
|
+
_DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
|
|
326
|
+
_DEFAULT_NO_START = False
|
|
327
|
+
_etc_hosts = {}
|
|
328
|
+
_sshpassPath = None
|
|
329
|
+
_sshPath = None
|
|
330
|
+
_scpPath = None
|
|
331
|
+
_ipmitoolPath = None
|
|
332
|
+
_rsyncPath = None
|
|
333
|
+
_shellPath = None
|
|
334
|
+
__ERROR_MESSAGES_TO_IGNORE_REGEX =None
|
|
335
|
+
__DEBUG_MODE = False
|
|
336
|
+
|
|
337
|
+
#%% Load Config Based Default Global variables
|
|
338
|
+
__configs_from_file = {}
|
|
339
|
+
for config_file in reversed(CONFIG_FILE_CHAIN.copy()):
|
|
340
|
+
__configs_from_file.update(load_config_file(os.path.expanduser(config_file)))
|
|
341
|
+
globals().update(__configs_from_file)
|
|
342
|
+
# form the regex from the list
|
|
343
|
+
if __ERROR_MESSAGES_TO_IGNORE_REGEX:
|
|
344
|
+
eprint('Using __ERROR_MESSAGES_TO_IGNORE_REGEX, ignoring ERROR_MESSAGES_TO_IGNORE')
|
|
345
|
+
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__ERROR_MESSAGES_TO_IGNORE_REGEX)
|
|
346
|
+
else:
|
|
347
|
+
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
|
|
348
|
+
|
|
349
|
+
#%% Load mssh Functional Global Variables
|
|
350
|
+
__global_suppress_printout = False
|
|
351
|
+
__mainReturnCode = 0
|
|
352
|
+
__failedHosts = set()
|
|
353
|
+
__wildCharacters = ['*','?','x']
|
|
354
|
+
_no_env = DEFAULT_NO_ENV
|
|
355
|
+
_env_file = DEFAULT_ENV_FILE
|
|
356
|
+
__globalUnavailableHosts = set()
|
|
357
|
+
__ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
|
|
358
|
+
__keyPressesIn = [[]]
|
|
359
|
+
_emo = False
|
|
360
|
+
__curses_global_color_pairs = {(-1,-1):1}
|
|
361
|
+
__curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
|
|
362
|
+
__curses_color_table = {}
|
|
363
|
+
__curses_current_color_index = 10
|
|
364
|
+
__max_connections_nofile_limit_supported = 0
|
|
365
|
+
__thread_start_delay = 0
|
|
366
|
+
if __resource_lib_available:
|
|
367
|
+
# Get the current limits
|
|
368
|
+
_, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
369
|
+
# Set the soft limit to the hard limit
|
|
370
|
+
resource.setrlimit(resource.RLIMIT_NOFILE, (__system_nofile_limit, __system_nofile_limit))
|
|
371
|
+
__max_connections_nofile_limit_supported = int((__system_nofile_limit - 10) / 3)
|
|
372
|
+
|
|
373
|
+
#%% Mapping of ANSI 4-bit colors to curses colors
|
|
376
374
|
if __curses_available:
|
|
377
375
|
ANSI_TO_CURSES_COLOR = {
|
|
378
376
|
30: curses.COLOR_BLACK,
|
|
@@ -392,7 +390,7 @@ if __curses_available:
|
|
|
392
390
|
96: curses.COLOR_CYAN, # Bright Cyan
|
|
393
391
|
97: curses.COLOR_WHITE # Bright White
|
|
394
392
|
}
|
|
395
|
-
|
|
393
|
+
#%% ------------ Exportable Help Functions ----------------
|
|
396
394
|
# check if command sshpass is available
|
|
397
395
|
_binPaths = {}
|
|
398
396
|
def check_path(program_name):
|
|
@@ -491,73 +489,74 @@ def replace_magic_strings(string,keys,value,case_sensitive=False):
|
|
|
491
489
|
return string
|
|
492
490
|
|
|
493
491
|
def pretty_format_table(data, delimiter = '\t',header = None):
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
492
|
+
version = 1.11
|
|
493
|
+
_ = version
|
|
494
|
+
if not data:
|
|
495
|
+
return ''
|
|
496
|
+
if isinstance(data, str):
|
|
497
|
+
data = data.strip('\n').split('\n')
|
|
498
|
+
data = [line.split(delimiter) for line in data]
|
|
499
|
+
elif isinstance(data, dict):
|
|
500
|
+
# flatten the 2D dict to a list of lists
|
|
501
|
+
if isinstance(next(iter(data.values())), dict):
|
|
502
|
+
tempData = [['key'] + list(next(iter(data.values())).keys())]
|
|
503
|
+
tempData.extend( [[key] + list(value.values()) for key, value in data.items()])
|
|
504
|
+
data = tempData
|
|
505
|
+
else:
|
|
506
|
+
# it is a dict of lists
|
|
507
|
+
data = [[key] + list(value) for key, value in data.items()]
|
|
508
|
+
elif not isinstance(data, list):
|
|
509
|
+
data = list(data)
|
|
510
|
+
# format the list into 2d list of list of strings
|
|
511
|
+
if isinstance(data[0], dict):
|
|
512
|
+
tempData = [data[0].keys()]
|
|
513
|
+
tempData.extend([list(item.values()) for item in data])
|
|
514
|
+
data = tempData
|
|
515
|
+
data = [[str(item) for item in row] for row in data]
|
|
516
|
+
num_cols = len(data[0])
|
|
517
|
+
col_widths = [0] * num_cols
|
|
518
|
+
# Calculate the maximum width of each column
|
|
519
|
+
for c in range(num_cols):
|
|
520
|
+
#col_widths[c] = max(len(row[c]) for row in data)
|
|
521
|
+
# handle ansii escape sequences
|
|
522
|
+
col_widths[c] = max(len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]','',row[c])) for row in data)
|
|
523
|
+
if header:
|
|
524
|
+
header_widths = [len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', col)) for col in header]
|
|
525
|
+
col_widths = [max(col_widths[i], header_widths[i]) for i in range(num_cols)]
|
|
526
|
+
# Build the row format string
|
|
527
|
+
row_format = ' | '.join('{{:<{}}}'.format(width) for width in col_widths)
|
|
528
|
+
# Print the header
|
|
529
|
+
if not header:
|
|
530
|
+
header = data[0]
|
|
531
|
+
outTable = []
|
|
532
|
+
outTable.append(row_format.format(*header))
|
|
533
|
+
outTable.append('-+-'.join('-' * width for width in col_widths))
|
|
534
|
+
for row in data[1:]:
|
|
535
|
+
# if the row is empty, print an divider
|
|
536
|
+
if not any(row):
|
|
537
|
+
outTable.append('-+-'.join('-' * width for width in col_widths))
|
|
538
|
+
else:
|
|
539
|
+
outTable.append(row_format.format(*row))
|
|
540
|
+
else:
|
|
541
|
+
# pad / truncate header to appropriate length
|
|
542
|
+
if isinstance(header,str):
|
|
543
|
+
header = header.split(delimiter)
|
|
544
|
+
if len(header) < num_cols:
|
|
545
|
+
header += ['']*(num_cols-len(header))
|
|
546
|
+
elif len(header) > num_cols:
|
|
547
|
+
header = header[:num_cols]
|
|
548
|
+
outTable = []
|
|
549
|
+
outTable.append(row_format.format(*header))
|
|
550
|
+
outTable.append('-+-'.join('-' * width for width in col_widths))
|
|
551
|
+
for row in data:
|
|
552
|
+
# if the row is empty, print an divider
|
|
553
|
+
if not any(row):
|
|
554
|
+
outTable.append('-+-'.join('-' * width for width in col_widths))
|
|
555
|
+
else:
|
|
556
|
+
outTable.append(row_format.format(*row))
|
|
557
|
+
return '\n'.join(outTable) + '\n'
|
|
558
|
+
|
|
559
|
+
#%% ------------ Compacting Hostnames ----------------
|
|
561
560
|
def __tokenize_hostname(hostname):
|
|
562
561
|
"""
|
|
563
562
|
Tokenize the hostname into a list of tokens.
|
|
@@ -913,7 +912,7 @@ def compact_hostnames(Hostnames,verify = True):
|
|
|
913
912
|
compact_hosts = hostSet
|
|
914
913
|
return compact_hosts
|
|
915
914
|
|
|
916
|
-
|
|
915
|
+
#%% ------------ Expanding Hostnames ----------------
|
|
917
916
|
@cache_decorator
|
|
918
917
|
def __validate_expand_hostname(hostname):
|
|
919
918
|
'''
|
|
@@ -1078,7 +1077,7 @@ def __expand_hostnames(hosts) -> dict:
|
|
|
1078
1077
|
# seperated by .
|
|
1079
1078
|
# If so, we expand the IP address range
|
|
1080
1079
|
iplist = []
|
|
1081
|
-
if re.match(r'^((((25[0-4]|2[0-4]
|
|
1080
|
+
if re.match(r'^((((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])}|x|\*|\?))?\]))(\.((((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|x|\*|\?)(-((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|x|\*|\?))?)|(\[((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|x|\*|\?)(-((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|x|\*|\?))?\]))){2}(\.(((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4]\d|1\d\d|[1-9]\d|[1-9])}|x|\*|\?))?\]))$', host):
|
|
1082
1081
|
hostSetToAdd = sorted(__expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
|
|
1083
1082
|
iplist = hostSetToAdd
|
|
1084
1083
|
else:
|
|
@@ -1113,7 +1112,7 @@ def expand_hostnames(hosts):
|
|
|
1113
1112
|
return __expand_hostnames(hosts)
|
|
1114
1113
|
|
|
1115
1114
|
|
|
1116
|
-
|
|
1115
|
+
#%% ------------ Run Command Block ----------------
|
|
1117
1116
|
def __handle_reading_stream(stream,target, host):
|
|
1118
1117
|
'''
|
|
1119
1118
|
Read the stream and append the lines to the target list
|
|
@@ -1494,7 +1493,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
|
|
|
1494
1493
|
host.scp = True
|
|
1495
1494
|
run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1)
|
|
1496
1495
|
|
|
1497
|
-
|
|
1496
|
+
#%% ------------ Start Threading Block ----------------
|
|
1498
1497
|
def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cpu_count()):
|
|
1499
1498
|
'''
|
|
1500
1499
|
Start running the command on the hosts. Wrapper function for run_command
|
|
@@ -1518,7 +1517,7 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
|
|
|
1518
1517
|
time.sleep(__thread_start_delay)
|
|
1519
1518
|
return threads
|
|
1520
1519
|
|
|
1521
|
-
|
|
1520
|
+
#%% ------------ Display Block ----------------
|
|
1522
1521
|
def __approximate_color_8bit(color):
|
|
1523
1522
|
"""
|
|
1524
1523
|
Approximate an 8-bit color (0-255) to the nearest curses color.
|
|
@@ -1614,7 +1613,6 @@ def __get_curses_color_pair(fg, bg):
|
|
|
1614
1613
|
if __curses_current_color_pair_index >= curses.COLOR_PAIRS:
|
|
1615
1614
|
print("Warning: Maximum number of color pairs reached, wrapping around.")
|
|
1616
1615
|
__curses_current_color_pair_index = 1
|
|
1617
|
-
# TODO: avoid initializing the same fg and bg color
|
|
1618
1616
|
curses.init_pair(__curses_current_color_pair_index, fg, bg)
|
|
1619
1617
|
__curses_global_color_pairs[(fg, bg)] = __curses_current_color_pair_index
|
|
1620
1618
|
__curses_current_color_pair_index += 1
|
|
@@ -1804,7 +1802,7 @@ def _curses_add_string_to_window(window, line = '', y = 0, x = 0, number_of_char
|
|
|
1804
1802
|
continue
|
|
1805
1803
|
if parse_ansi_colors and segment.startswith("\x1b["):
|
|
1806
1804
|
# Parse ANSI escape sequence
|
|
1807
|
-
|
|
1805
|
+
_ = __parse_ansi_escape_sequence_to_curses_attr(segment,color_pair_list)
|
|
1808
1806
|
else:
|
|
1809
1807
|
# Add text with current color
|
|
1810
1808
|
if charsWritten < numChar and len(segment) > 0:
|
|
@@ -1845,13 +1843,12 @@ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
|
1845
1843
|
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
1846
1844
|
# we will compare the new_hosts_to_display with the old one, if some hosts are not in their original position, we will change its printedLines to 0
|
|
1847
1845
|
for i, host in enumerate(new_hosts_to_display):
|
|
1848
|
-
if host not in hosts_to_display:
|
|
1849
|
-
host.printedLines = 0
|
|
1850
|
-
elif i != hosts_to_display.index(host):
|
|
1846
|
+
if host not in hosts_to_display or i != hosts_to_display.index(host):
|
|
1851
1847
|
host.printedLines = 0
|
|
1852
1848
|
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
1853
1849
|
|
|
1854
|
-
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'):
|
|
1850
|
+
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'):
|
|
1851
|
+
_ = config_reason
|
|
1855
1852
|
try:
|
|
1856
1853
|
box_ansi_color = None
|
|
1857
1854
|
org_dim = stdscr.getmaxyx()
|
|
@@ -2160,23 +2157,23 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
2160
2157
|
# generate some debug information before display initialization
|
|
2161
2158
|
try:
|
|
2162
2159
|
stdscr.clear()
|
|
2163
|
-
_curses_add_string_to_window(window=stdscr, y=0, line='Initializing display...',
|
|
2160
|
+
_curses_add_string_to_window(window=stdscr, y=0, line='Initializing display...', number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2164
2161
|
# print the size
|
|
2165
|
-
_curses_add_string_to_window(window=stdscr, y=1, line=f"Terminal size: {stdscr.getmaxyx()}",
|
|
2162
|
+
_curses_add_string_to_window(window=stdscr, y=1, line=f"Terminal size: {stdscr.getmaxyx()}",number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2166
2163
|
# print the number of hosts
|
|
2167
|
-
_curses_add_string_to_window(window=stdscr, y=2, line=f"Number of hosts: {len(hosts)}",
|
|
2164
|
+
_curses_add_string_to_window(window=stdscr, y=2, line=f"Number of hosts: {len(hosts)}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2168
2165
|
# print the number of threads
|
|
2169
|
-
_curses_add_string_to_window(window=stdscr, y=3, line=f"Number of threads: {len(threads)}",
|
|
2166
|
+
_curses_add_string_to_window(window=stdscr, y=3, line=f"Number of threads: {len(threads)}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2170
2167
|
# print the minimum character length
|
|
2171
|
-
_curses_add_string_to_window(window=stdscr, y=4, line=f"Minimum character length: {min_char_len}",
|
|
2168
|
+
_curses_add_string_to_window(window=stdscr, y=4, line=f"Minimum character length: {min_char_len}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2172
2169
|
# print the minimum line length
|
|
2173
|
-
_curses_add_string_to_window(window=stdscr, y=5, line=f"Minimum line length: {min_line_len}",
|
|
2170
|
+
_curses_add_string_to_window(window=stdscr, y=5, line=f"Minimum line length: {min_line_len}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2174
2171
|
# print the single window mode
|
|
2175
|
-
_curses_add_string_to_window(window=stdscr, y=6, line=f"Single window mode: {single_window}",
|
|
2172
|
+
_curses_add_string_to_window(window=stdscr, y=6, line=f"Single window mode: {single_window}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2176
2173
|
# print COLORS and COLOR_PAIRS count
|
|
2177
|
-
_curses_add_string_to_window(window=stdscr, y=7, line=f"len(COLORS): {curses.COLORS} len(COLOR_PAIRS): {curses.COLOR_PAIRS}",
|
|
2174
|
+
_curses_add_string_to_window(window=stdscr, y=7, line=f"len(COLORS): {curses.COLORS} len(COLOR_PAIRS): {curses.COLOR_PAIRS}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2178
2175
|
# print if can change color
|
|
2179
|
-
_curses_add_string_to_window(window=stdscr, y=8, line=f"Real color capability: {curses.can_change_color()}",
|
|
2176
|
+
_curses_add_string_to_window(window=stdscr, y=8, line=f"Real color capability: {curses.can_change_color()}", number_of_char_to_write=stdscr.getmaxyx()[1] - 1)
|
|
2180
2177
|
stdscr.refresh()
|
|
2181
2178
|
except:
|
|
2182
2179
|
pass
|
|
@@ -2205,7 +2202,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
2205
2202
|
time.sleep(0.01)
|
|
2206
2203
|
#time.sleep(0.25)
|
|
2207
2204
|
|
|
2208
|
-
|
|
2205
|
+
#%% ------------ Generate Output Block ----------------
|
|
2209
2206
|
def generate_output(hosts, usejson = False, greppable = False):
|
|
2210
2207
|
global __keyPressesIn
|
|
2211
2208
|
global __global_suppress_printout
|
|
@@ -2234,8 +2231,7 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2234
2231
|
else:
|
|
2235
2232
|
outputs = {}
|
|
2236
2233
|
for host in hosts:
|
|
2237
|
-
if __global_suppress_printout:
|
|
2238
|
-
if host['returncode'] == 0:
|
|
2234
|
+
if __global_suppress_printout and host['returncode'] == 0:
|
|
2239
2235
|
continue
|
|
2240
2236
|
hostPrintOut = f" Command:\n {host['command']}\n"
|
|
2241
2237
|
hostPrintOut += " stdout:\n "+'\n '.join(host['stdout'])
|
|
@@ -2271,7 +2267,7 @@ def generate_output(hosts, usejson = False, greppable = False):
|
|
|
2271
2267
|
__keyPressesIn = [[]]
|
|
2272
2268
|
if __global_suppress_printout and not outputs:
|
|
2273
2269
|
rtnStr += 'Success'
|
|
2274
|
-
|
|
2270
|
+
return rtnStr
|
|
2275
2271
|
|
|
2276
2272
|
def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
2277
2273
|
'''
|
|
@@ -2290,7 +2286,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
2290
2286
|
print(rtnStr)
|
|
2291
2287
|
return rtnStr
|
|
2292
2288
|
|
|
2293
|
-
|
|
2289
|
+
#%% ------------ Run / Process Hosts Block ----------------
|
|
2294
2290
|
def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
|
|
2295
2291
|
global __globalUnavailableHosts
|
|
2296
2292
|
global _no_env
|
|
@@ -2363,7 +2359,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
2363
2359
|
if not called:
|
|
2364
2360
|
print_output(hosts,json,greppable=greppable)
|
|
2365
2361
|
|
|
2366
|
-
|
|
2362
|
+
#%% ------------ Stringfy Block ----------------
|
|
2367
2363
|
@cache_decorator
|
|
2368
2364
|
def formHostStr(host) -> str:
|
|
2369
2365
|
"""
|
|
@@ -2429,6 +2425,13 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
2429
2425
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
2430
2426
|
copy_id = False,
|
|
2431
2427
|
shortend = False):
|
|
2428
|
+
_ = called
|
|
2429
|
+
_ = returnUnfinished
|
|
2430
|
+
_ = willUpdateUnreachableHosts
|
|
2431
|
+
_ = no_start
|
|
2432
|
+
_ = curses_min_char_len
|
|
2433
|
+
_ = curses_min_line_len
|
|
2434
|
+
_ = single_window
|
|
2432
2435
|
hosts = hosts if isinstance(hosts,str) else frozenset(hosts)
|
|
2433
2436
|
hostStr = formHostStr(hosts)
|
|
2434
2437
|
files = frozenset(files) if files else None
|
|
@@ -2442,7 +2445,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
2442
2445
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
2443
2446
|
return f'multissh {argsStr} {hostStr} {commandStr}'
|
|
2444
2447
|
|
|
2445
|
-
|
|
2448
|
+
#%% ------------ Main Block ----------------
|
|
2446
2449
|
def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
2447
2450
|
nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
2448
2451
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
|
|
@@ -2548,7 +2551,6 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2548
2551
|
try:
|
|
2549
2552
|
commands = [' '.join(command) if not isinstance(command,str) else command for command in commands]
|
|
2550
2553
|
except:
|
|
2551
|
-
pass
|
|
2552
2554
|
eprint(f"Warning: commands should ideally be a list of strings. Now mssh had failed to convert {commands!r} to a list of strings. Continuing anyway but expect failures.")
|
|
2553
2555
|
#verify_ssh_config()
|
|
2554
2556
|
# load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
|
|
@@ -2573,6 +2575,10 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2573
2575
|
if quiet:
|
|
2574
2576
|
__global_suppress_printout = True
|
|
2575
2577
|
# We create the hosts
|
|
2578
|
+
if isinstance(hosts, list):
|
|
2579
|
+
hosts = frozenset(hosts)
|
|
2580
|
+
elif isinstance(hosts, dict):
|
|
2581
|
+
hosts = frozenset(hosts.keys())
|
|
2576
2582
|
hostStr = formHostStr(hosts)
|
|
2577
2583
|
skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
|
|
2578
2584
|
|
|
@@ -2683,7 +2689,6 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2683
2689
|
if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
|
|
2684
2690
|
continue
|
|
2685
2691
|
if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
|
|
2686
|
-
# TODO: use ip to determine if we skip the host or not, also for unavailable hosts
|
|
2687
2692
|
if file_sync:
|
|
2688
2693
|
eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
|
|
2689
2694
|
return []
|
|
@@ -2719,7 +2724,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2719
2724
|
allHosts += hosts
|
|
2720
2725
|
return allHosts
|
|
2721
2726
|
|
|
2722
|
-
|
|
2727
|
+
#%% ------------ Default Config Functions ----------------
|
|
2723
2728
|
def generate_default_config(args):
|
|
2724
2729
|
'''
|
|
2725
2730
|
Get the default config
|
|
@@ -2808,7 +2813,7 @@ def write_default_config(args,CONFIG_FILE = None):
|
|
|
2808
2813
|
eprint(f'Printing the config file to stdout:')
|
|
2809
2814
|
print(json.dumps(__configs_from_file, indent=4))
|
|
2810
2815
|
|
|
2811
|
-
|
|
2816
|
+
#%% ------------ Wrapper Block ----------------
|
|
2812
2817
|
def main():
|
|
2813
2818
|
global _emo
|
|
2814
2819
|
global __global_suppress_printout
|
|
@@ -2874,7 +2879,7 @@ def main():
|
|
|
2874
2879
|
# if python version is 3.7 or higher, use parse_intermixed_args
|
|
2875
2880
|
try:
|
|
2876
2881
|
args = parser.parse_intermixed_args()
|
|
2877
|
-
except Exception
|
|
2882
|
+
except Exception :
|
|
2878
2883
|
#eprint(f"Error while parsing arguments: {e!r}")
|
|
2879
2884
|
# try to parse the arguments using parse_known_args
|
|
2880
2885
|
args, unknown = parser.parse_known_args()
|
|
@@ -2983,8 +2988,8 @@ def main():
|
|
|
2983
2988
|
if args.success_hosts and not __global_suppress_printout:
|
|
2984
2989
|
eprint(f'succeeded_hosts: {",".join(sorted(compact_hostnames(succeededHosts)))}')
|
|
2985
2990
|
|
|
2986
|
-
if threading.active_count() > 1:
|
|
2987
|
-
|
|
2991
|
+
if threading.active_count() > 1 and not __global_suppress_printout:
|
|
2992
|
+
eprint(f'Remaining active thread: {threading.active_count()}')
|
|
2988
2993
|
# os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
2989
2994
|
# os._exit(mainReturnCode)
|
|
2990
2995
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
multiSSH3.py,sha256=CMm3IwWYvN7uuNBXFcYA86AG4wykFuAUtjo5Lct93mw,139201
|
|
2
|
+
multissh3-5.58.dist-info/METADATA,sha256=6dKv2fRT1IYqbt0o636OwXiNr3gijG-p26kTDXvO4tQ,18092
|
|
3
|
+
multissh3-5.58.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
4
|
+
multissh3-5.58.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
+
multissh3-5.58.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
+
multissh3-5.58.dist-info/RECORD,,
|
multissh3-5.55.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=AQCio2dZXYBrwBB1DP4j9E9b1HhqcJYmoLFNGxmsA4U,139465
|
|
2
|
-
multissh3-5.55.dist-info/METADATA,sha256=66dhkZMrRHU4AVTbMT2sucnDbVhqctwWj9sY3DA2VsY,18092
|
|
3
|
-
multissh3-5.55.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
4
|
-
multissh3-5.55.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
-
multissh3-5.55.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
-
multissh3-5.55.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|