multiSSH3 5.4__tar.gz → 5.10__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.4 → multissh3-5.10}/PKG-INFO +1 -1
- {multissh3-5.4 → multissh3-5.10}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-5.4 → multissh3-5.10}/multiSSH3.py +527 -125
- {multissh3-5.4 → multissh3-5.10}/setup.py +1 -1
- {multissh3-5.4 → multissh3-5.10}/LICENSE +0 -0
- {multissh3-5.4 → multissh3-5.10}/README.md +0 -0
- {multissh3-5.4 → multissh3-5.10}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-5.4 → multissh3-5.10}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-5.4 → multissh3-5.10}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-5.4 → multissh3-5.10}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-5.4 → multissh3-5.10}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-5.4 → multissh3-5.10}/setup.cfg +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
2
|
+
__curses_available = False
|
|
3
|
+
try:
|
|
4
|
+
import curses
|
|
5
|
+
__curses_available = True
|
|
6
|
+
except ImportError:
|
|
7
|
+
pass
|
|
3
8
|
import subprocess
|
|
4
9
|
import threading
|
|
5
10
|
import time,os
|
|
@@ -31,7 +36,7 @@ except AttributeError:
|
|
|
31
36
|
# If neither is available, use a dummy decorator
|
|
32
37
|
def cache_decorator(func):
|
|
33
38
|
return func
|
|
34
|
-
version = '5.
|
|
39
|
+
version = '5.10'
|
|
35
40
|
VERSION = version
|
|
36
41
|
|
|
37
42
|
CONFIG_FILE = '/etc/multiSSH3.config.json'
|
|
@@ -39,7 +44,11 @@ CONFIG_FILE = '/etc/multiSSH3.config.json'
|
|
|
39
44
|
import sys
|
|
40
45
|
|
|
41
46
|
def eprint(*args, **kwargs):
|
|
42
|
-
|
|
47
|
+
try:
|
|
48
|
+
print(*args, file=sys.stderr, **kwargs)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"Error: Cannot print to stderr: {e}")
|
|
51
|
+
print(*args, **kwargs)
|
|
43
52
|
|
|
44
53
|
def load_config_file(config_file):
|
|
45
54
|
'''
|
|
@@ -57,7 +66,7 @@ def load_config_file(config_file):
|
|
|
57
66
|
with open(config_file,'r') as f:
|
|
58
67
|
config = json.load(f)
|
|
59
68
|
except:
|
|
60
|
-
eprint(f"Error: Cannot load config file {config_file}")
|
|
69
|
+
eprint(f"Error: Cannot load config file {config_file!r}")
|
|
61
70
|
return {}
|
|
62
71
|
return config
|
|
63
72
|
|
|
@@ -200,7 +209,7 @@ def get_i():
|
|
|
200
209
|
return __host_i_counter
|
|
201
210
|
|
|
202
211
|
class Host:
|
|
203
|
-
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None):
|
|
212
|
+
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,bash=False,i = get_i(),uuid=uuid.uuid4()):
|
|
204
213
|
self.name = name # the name of the host (hostname or IP address)
|
|
205
214
|
self.command = command # the command to run on the host
|
|
206
215
|
self.returncode = None # the return code of the command
|
|
@@ -211,14 +220,15 @@ class Host:
|
|
|
211
220
|
self.lastUpdateTime = time.time() # the last time the output was updated
|
|
212
221
|
self.files = files # the files to be copied to the host
|
|
213
222
|
self.ipmi = ipmi # whether to use ipmi to connect to the host
|
|
223
|
+
self.bash = bash # whether to use bash to run the command
|
|
214
224
|
self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
|
|
215
225
|
self.scp = scp # whether to use scp to copy files to the host
|
|
216
226
|
self.gatherMode = gatherMode # whether the host is in gather mode
|
|
217
227
|
self.extraargs = extraargs # extra arguments to be passed to ssh
|
|
218
228
|
self.resolvedName = None # the resolved IP address of the host
|
|
219
229
|
# also store a globally unique integer i from 0
|
|
220
|
-
self.i =
|
|
221
|
-
self.uuid = uuid
|
|
230
|
+
self.i = i
|
|
231
|
+
self.uuid = uuid
|
|
222
232
|
self.identity_file = identity_file
|
|
223
233
|
|
|
224
234
|
def __iter__(self):
|
|
@@ -265,8 +275,323 @@ def check_path(program_name):
|
|
|
265
275
|
|
|
266
276
|
[check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash','ssh-copy-id']]
|
|
267
277
|
|
|
278
|
+
#%%
|
|
279
|
+
def tokenize_hostname(hostname):
|
|
280
|
+
"""
|
|
281
|
+
Tokenize the hostname into a list of tokens.
|
|
282
|
+
Tokens will be separated by symbols or numbers.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
hostname (str): The hostname to tokenize.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
list: A list of tokens.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> tokenize_hostname('www.example.com')
|
|
292
|
+
('www', '.', 'example', '.', 'com')
|
|
293
|
+
>>> tokenize_hostname('localhost')
|
|
294
|
+
('localhost',)
|
|
295
|
+
>>> tokenize_hostname('Sub-S1')
|
|
296
|
+
('Sub', '-', 'S', '1')
|
|
297
|
+
>>> tokenize_hostname('Sub-S10')
|
|
298
|
+
('Sub', '-', 'S', '10')
|
|
299
|
+
>>> tokenize_hostname('Process-Client10-1')
|
|
300
|
+
('Process', '-', 'Client', '10', '-', '1')
|
|
301
|
+
>>> tokenize_hostname('Process-C5-15')
|
|
302
|
+
('Process', '-', 'C', '5', '-', '15')
|
|
303
|
+
>>> tokenize_hostname('192.168.1.1')
|
|
304
|
+
('192', '.', '168', '.', '1', '.', '1')
|
|
305
|
+
"""
|
|
306
|
+
# Regular expression to match sequences of letters, digits, or symbols
|
|
307
|
+
tokens = re.findall(r'[A-Za-z]+|\d+|[^A-Za-z0-9]', hostname)
|
|
308
|
+
return tuple(tokens)
|
|
309
|
+
|
|
310
|
+
@cache_decorator
|
|
311
|
+
def hashTokens(tokens):
|
|
312
|
+
"""
|
|
313
|
+
Translate a list of tokens in string to a list of integers with positional information.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
tokens (tuple): A tuple of tokens.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
list: A list of integers.
|
|
320
|
+
|
|
321
|
+
Example:
|
|
322
|
+
>>> tuple(hashTokens(('1')))
|
|
323
|
+
(1,)
|
|
324
|
+
>>> tuple(hashTokens(('1', '2')))
|
|
325
|
+
(1, 2)
|
|
326
|
+
>>> tuple(hashTokens(('1', '.', '2')))
|
|
327
|
+
(1, -5047856122680242044, 2)
|
|
328
|
+
>>> tuple(hashTokens(('Process', '-', 'C', '5', '-', '15')))
|
|
329
|
+
(117396829274297939, 7549860403020794775, 8629208860073383633, 5, 7549860403020794775, 15)
|
|
330
|
+
>>> tuple(hashTokens(('192', '.', '168', '.', '1', '.', '1')))
|
|
331
|
+
(192, -5047856122680242044, 168, -5047856122680242044, 1, -5047856122680242044, 1)
|
|
332
|
+
"""
|
|
333
|
+
return tuple(int(token) if token.isdigit() else hash(token) for token in tokens)
|
|
334
|
+
|
|
335
|
+
def findDiffIndex(token1, token2):
|
|
336
|
+
"""
|
|
337
|
+
Find the index of the first difference between two lists of tokens.
|
|
338
|
+
If there is more than one difference, return -1.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
token1 (tuple): A list of tokens.
|
|
342
|
+
token2 (tuple): A list of tokens.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
int: The index of the first difference between the two lists of tokens.
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> findDiffIndex(('1',), ('1',))
|
|
349
|
+
-1
|
|
350
|
+
>>> findDiffIndex(('1','2'), ('1', '1'))
|
|
351
|
+
1
|
|
352
|
+
>>> findDiffIndex(('1','1'), ('1', '1', '1'))
|
|
353
|
+
Traceback (most recent call last):
|
|
354
|
+
...
|
|
355
|
+
ValueError: The two lists must have the same length.
|
|
356
|
+
>>> findDiffIndex(('192', '.', '168', '.', '2', '.', '1'), ('192', '.', '168', '.', '1', '.', '1'))
|
|
357
|
+
4
|
|
358
|
+
>>> findDiffIndex(('192', '.', '168', '.', '2', '.', '1'), ('192', '.', '168', '.', '1', '.', '2'))
|
|
359
|
+
-1
|
|
360
|
+
>>> findDiffIndex(('Process', '-', 'C', '5', '-', '15'), ('Process', '-', 'C', '5', '-', '15'))
|
|
361
|
+
-1
|
|
362
|
+
>>> findDiffIndex(('Process', '-', 'C', '5', '-', '15'), ('Process', '-', 'C', '5', '-', '16'))
|
|
363
|
+
5
|
|
364
|
+
>>> findDiffIndex(tokenize_hostname('nebulahost3'), tokenize_hostname('nebulaleaf3'))
|
|
365
|
+
-1
|
|
366
|
+
>>> findDiffIndex(tokenize_hostname('nebulaleaf3'), tokenize_hostname('nebulaleaf4'))
|
|
367
|
+
1
|
|
368
|
+
"""
|
|
369
|
+
if len(token1) != len(token2):
|
|
370
|
+
raise ValueError('The two lists must have the same length.')
|
|
371
|
+
rtn = -1
|
|
372
|
+
for i, (subToken1, subToken2) in enumerate(zip(token1, token2)):
|
|
373
|
+
if subToken1 != subToken2:
|
|
374
|
+
if rtn == -1 and subToken1.isdigit() and subToken2.isdigit():
|
|
375
|
+
rtn = i
|
|
376
|
+
else:
|
|
377
|
+
return -1
|
|
378
|
+
return rtn
|
|
379
|
+
|
|
380
|
+
def generateSumDic(Hostnames):
|
|
381
|
+
"""
|
|
382
|
+
Generate a dictionary of sums of tokens for a list of hostnames.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
Hostnames (list): A list of hostnames.
|
|
386
|
+
|
|
387
|
+
Example:
|
|
388
|
+
>>> generateSumDic(['localhost'])
|
|
389
|
+
{6564370170492138900: {('localhost',): {}}}
|
|
390
|
+
>>> generateSumDic(['1', '2'])
|
|
391
|
+
{1: {('1',): {}}, 2: {('2',): {}}}
|
|
392
|
+
>>> generateSumDic(['1.1','1.2'])
|
|
393
|
+
{3435203479547611399: {('1', '.', '1'): {}}, 3435203479547611400: {('1', '.', '2'): {}}}
|
|
394
|
+
>>> generateSumDic(['1.2','2.1'])
|
|
395
|
+
{3435203479547611400: {('1', '.', '2'): {}, ('2', '.', '1'): {}}}
|
|
396
|
+
"""
|
|
397
|
+
sumDic = {}
|
|
398
|
+
for hostname in reversed(sorted(Hostnames)):
|
|
399
|
+
tokens = tokenize_hostname(hostname)
|
|
400
|
+
sumHash = sum(hashTokens(tokens))
|
|
401
|
+
sumDic.setdefault(sumHash, {})[tokens] = {}
|
|
402
|
+
return sumDic
|
|
403
|
+
|
|
404
|
+
def filterSumDic(sumDic):
|
|
405
|
+
"""
|
|
406
|
+
Filter the sumDic to do one order of grouping.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
sumDic (dict): A dictionary of sums of tokens.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
dict: A filtered dictionary of sums of tokens.
|
|
413
|
+
|
|
414
|
+
Example:
|
|
415
|
+
>>> filterSumDic(generateSumDic(['server15', 'server16', 'server17']))
|
|
416
|
+
{-6728831096159691241: {('server', '17'): {(1, 0): [15, 17]}}}
|
|
417
|
+
>>> filterSumDic(generateSumDic(['server15', 'server16', 'server17', 'server18']))
|
|
418
|
+
{-6728831096159691240: {('server', '18'): {(1, 0): [15, 18]}}}
|
|
419
|
+
>>> filterSumDic(generateSumDic(['server-1', 'server-2', 'server-3']))
|
|
420
|
+
{1441623239094376437: {('server', '-', '3'): {(2, 0): [1, 3]}}}
|
|
421
|
+
>>> filterSumDic(generateSumDic(['server-1-2', 'server-1-1', 'server-2-1', 'server-2-2']))
|
|
422
|
+
{9612077574348444129: {('server', '-', '1', '-', '2'): {(4, 0): [1, 2]}}, 9612077574348444130: {('server', '-', '2', '-', '2'): {(4, 0): [1, 2]}}}
|
|
423
|
+
>>> filterSumDic(generateSumDic(['server-1-2', 'server-1-1', 'server-2-2']))
|
|
424
|
+
{9612077574348444129: {('server', '-', '1', '-', '2'): {(4, 0): [1, 2]}}, 9612077574348444130: {('server', '-', '2', '-', '2'): {}}}
|
|
425
|
+
>>> filterSumDic(generateSumDic(['test1-a', 'test2-a']))
|
|
426
|
+
{12310874833182455839: {('test', '2', '-', 'a'): {(1, 0): [1, 2]}}}
|
|
427
|
+
>>> filterSumDic(generateSumDic(['sub-s1', 'sub-s2']))
|
|
428
|
+
{15455586825715425366: {('sub', '-', 's', '2'): {(3, 0): [1, 2]}}}
|
|
429
|
+
>>> filterSumDic(generateSumDic(['s9', 's10', 's11']))
|
|
430
|
+
{1169697225593811728: {('s', '11'): {(1, 0): [9, 11]}}}
|
|
431
|
+
>>> filterSumDic(generateSumDic(['s99', 's98', 's100','s101']))
|
|
432
|
+
{1169697225593811818: {('s', '101'): {(1, 0): [98, 101]}}}
|
|
433
|
+
>>> filterSumDic(generateSumDic(['s08', 's09', 's10', 's11']))
|
|
434
|
+
{1169697225593811728: {('s', '11'): {(1, 2): [8, 11]}}}
|
|
435
|
+
>>> filterSumDic(generateSumDic(['s099', 's098', 's100','s101']))
|
|
436
|
+
{1169697225593811818: {('s', '101'): {(1, 3): [98, 101]}}}
|
|
437
|
+
>>> filterSumDic(generateSumDic(['server1', 'server2', 'server3','server04']))
|
|
438
|
+
{-6728831096159691255: {('server', '3'): {(1, 0): [1, 3]}}, -6728831096159691254: {('server', '04'): {}}}
|
|
439
|
+
>>> filterSumDic(generateSumDic(['server9', 'server09', 'server10','server10']))
|
|
440
|
+
{-6728831096159691249: {('server', '09'): {}}, -6728831096159691248: {('server', '10'): {(1, 0): [9, 10]}}}
|
|
441
|
+
>>> filterSumDic(generateSumDic(['server09', 'server9', 'server10']))
|
|
442
|
+
{-6728831096159691249: {('server', '9'): {}}, -6728831096159691248: {('server', '10'): {(1, 2): [9, 10]}}}
|
|
443
|
+
"""
|
|
444
|
+
lastSumHash = None
|
|
445
|
+
newSumDic = {}
|
|
446
|
+
for key, value in sumDic.items():
|
|
447
|
+
newSumDic[key] = value.copy()
|
|
448
|
+
sumDic = newSumDic
|
|
449
|
+
newSumDic = {}
|
|
450
|
+
for sumHash in sorted(sumDic):
|
|
451
|
+
if lastSumHash is None:
|
|
452
|
+
lastSumHash = sumHash
|
|
453
|
+
newSumDic[sumHash] = sumDic[sumHash].copy()
|
|
454
|
+
continue
|
|
455
|
+
if sumHash - lastSumHash == 1:
|
|
456
|
+
# this means the distence between these two group of hostnames is 1, thus we try to group them together
|
|
457
|
+
for hostnameTokens in sumDic[sumHash]:
|
|
458
|
+
added = False
|
|
459
|
+
if lastSumHash in newSumDic and sumDic[lastSumHash]:
|
|
460
|
+
for lastHostnameTokens in sumDic[lastSumHash].copy():
|
|
461
|
+
# if the two hostnames are able to group, we group them together
|
|
462
|
+
# the two hostnames are able to group if:
|
|
463
|
+
# 1. the two hostnames have the same amount of tokens
|
|
464
|
+
# 2. the last hostname is not already been grouped
|
|
465
|
+
# 3. the two hostnames have the same tokens except for one token
|
|
466
|
+
# 4. the two hostnames have the same token groups
|
|
467
|
+
if len(hostnameTokens) == len(lastHostnameTokens) and \
|
|
468
|
+
lastSumHash in newSumDic and lastHostnameTokens in newSumDic[lastSumHash] and \
|
|
469
|
+
(diffIndex:=findDiffIndex(hostnameTokens, lastHostnameTokens)) != -1 and \
|
|
470
|
+
sumDic[sumHash][hostnameTokens] == sumDic[lastSumHash][lastHostnameTokens]:
|
|
471
|
+
# the sumDic[sumHash][hostnameTokens] will ba a dic of 2 element value lists with 2 element key representing:
|
|
472
|
+
# (token position that got grouped, the amount of zero padding (length) ):
|
|
473
|
+
# [ the start int token, the end int token]
|
|
474
|
+
# if we entered here, this means we are able to group the two hostnames together
|
|
475
|
+
|
|
476
|
+
if not diffIndex:
|
|
477
|
+
# should never happen, but just in case, we skip grouping
|
|
478
|
+
continue
|
|
479
|
+
tokenToGroup = hostnameTokens[diffIndex]
|
|
480
|
+
try:
|
|
481
|
+
tokenLength = len(tokenToGroup)
|
|
482
|
+
tokenToGroup = int(tokenToGroup)
|
|
483
|
+
except ValueError:
|
|
484
|
+
# if the token is not an int, we skip grouping
|
|
485
|
+
continue
|
|
486
|
+
# group(09 , 10) -> (x, 2): [9, 10]
|
|
487
|
+
# group(9 , 10) -> (x, 0): [9, 10]
|
|
488
|
+
# group(9 , 010) -> not able to group
|
|
489
|
+
# group(009 , 10) -> not able to group
|
|
490
|
+
# group(08, 09) -> (x, 2): [8, 9]
|
|
491
|
+
# group(08, 9) -> not able to group
|
|
492
|
+
# group(8, 09) -> not able to group
|
|
493
|
+
# group(0099, 0100) -> (x, 4): [99, 100]
|
|
494
|
+
# group(0099, 100) -> not able to groups
|
|
495
|
+
# group(099, 100) -> (x, 3): [99, 100]
|
|
496
|
+
# group(99, 100) -> (x, 0): [99, 100]
|
|
497
|
+
lastTokenToGroup = lastHostnameTokens[diffIndex]
|
|
498
|
+
try:
|
|
499
|
+
minimumTokenLength = 0
|
|
500
|
+
lastTokenLength = len(lastTokenToGroup)
|
|
501
|
+
if lastTokenLength > tokenLength:
|
|
502
|
+
raise ValueError('The last token is longer than the current token.')
|
|
503
|
+
elif lastTokenLength < tokenLength:
|
|
504
|
+
if tokenLength - lastTokenLength != 1:
|
|
505
|
+
raise ValueError('The last token is not one less than the current token.')
|
|
506
|
+
# if the last token is not made out of all 9s, we cannot group
|
|
507
|
+
if any(c != '9' for c in lastTokenToGroup):
|
|
508
|
+
raise ValueError('The last token is not made out of all 9s.')
|
|
509
|
+
elif lastTokenToGroup[0] == '0' and lastTokenLength > 1:
|
|
510
|
+
# we have encoutered a padded last token, will set this as the minimum token length
|
|
511
|
+
minimumTokenLength = lastTokenLength
|
|
512
|
+
lastTokenToGroup = int(lastTokenToGroup)
|
|
513
|
+
except ValueError:
|
|
514
|
+
# if the token is not an int, we skip grouping
|
|
515
|
+
continue
|
|
516
|
+
assert lastTokenToGroup + 1 == tokenToGroup, 'Error! The two tokens are not one apart.'
|
|
517
|
+
# we take the last hostname tokens grouped dic out from the newSumDic
|
|
518
|
+
hostnameGroupDic = newSumDic[lastSumHash][lastHostnameTokens].copy()
|
|
519
|
+
if (diffIndex, minimumTokenLength) in hostnameGroupDic and hostnameGroupDic[(diffIndex, minimumTokenLength)][1] + 1 == tokenToGroup:
|
|
520
|
+
# if the token is already grouped, we just update the end token
|
|
521
|
+
hostnameGroupDic[(diffIndex, minimumTokenLength)][1] = tokenToGroup
|
|
522
|
+
elif (diffIndex, tokenLength) in hostnameGroupDic and hostnameGroupDic[(diffIndex, tokenLength)][1] + 1 == tokenToGroup:
|
|
523
|
+
# alternatively, there is already an exact length padded token grouped
|
|
524
|
+
hostnameGroupDic[(diffIndex, tokenLength)][1] = tokenToGroup
|
|
525
|
+
elif sumDic[lastSumHash][lastHostnameTokens] == newSumDic[lastSumHash][lastHostnameTokens]:
|
|
526
|
+
# only when there are no new groups added to this token group this iter, we can add the new group
|
|
527
|
+
hostnameGroupDic[(diffIndex, minimumTokenLength)] = [lastTokenToGroup, tokenToGroup]
|
|
528
|
+
else:
|
|
529
|
+
# skip grouping if there are new groups added to this token group this iter
|
|
530
|
+
continue
|
|
531
|
+
# move the grouped dic under the new hostname / sum hash
|
|
532
|
+
del newSumDic[lastSumHash][lastHostnameTokens]
|
|
533
|
+
del sumDic[lastSumHash][lastHostnameTokens]
|
|
534
|
+
if not newSumDic[lastSumHash]:
|
|
535
|
+
del newSumDic[lastSumHash]
|
|
536
|
+
newSumDic.setdefault(sumHash, {})[hostnameTokens] = hostnameGroupDic
|
|
537
|
+
# we add the new group to the newSumDic
|
|
538
|
+
added = True
|
|
539
|
+
break
|
|
540
|
+
if not added:
|
|
541
|
+
# if the two hostnames are not able to group, we just add the last group to the newSumDic
|
|
542
|
+
newSumDic.setdefault(sumHash, {})[hostnameTokens] = sumDic[sumHash][hostnameTokens].copy()
|
|
543
|
+
else:
|
|
544
|
+
# this means the distence between these two group of hostnames is not 1, thus we just add the last group to the newSumDic
|
|
545
|
+
newSumDic[sumHash] = sumDic[sumHash].copy()
|
|
546
|
+
lastSumHash = sumHash
|
|
547
|
+
return newSumDic
|
|
548
|
+
|
|
549
|
+
def compact_hostnames(Hostnames):
|
|
550
|
+
"""
|
|
551
|
+
Compact a list of hostnames.
|
|
552
|
+
Compact numeric numbers into ranges.
|
|
268
553
|
|
|
554
|
+
Args:
|
|
555
|
+
Hostnames (list): A list of hostnames.
|
|
269
556
|
|
|
557
|
+
Returns:
|
|
558
|
+
list: A list of comapcted hostname list.
|
|
559
|
+
|
|
560
|
+
Example:
|
|
561
|
+
>>> compact_hostnames(['server15', 'server16', 'server17'])
|
|
562
|
+
['server[15-17]']
|
|
563
|
+
>>> compact_hostnames(['server-1', 'server-2', 'server-3'])
|
|
564
|
+
['server-[1-3]']
|
|
565
|
+
>>> compact_hostnames(['server-1-2', 'server-1-1', 'server-2-1', 'server-2-2'])
|
|
566
|
+
['server-[1-2]-[1-2]']
|
|
567
|
+
>>> compact_hostnames(['server-1-2', 'server-1-1', 'server-2-2'])
|
|
568
|
+
['server-1-[1-2]', 'server-2-2']
|
|
569
|
+
>>> compact_hostnames(['test1-a', 'test2-a'])
|
|
570
|
+
['test[1-2]-a']
|
|
571
|
+
>>> compact_hostnames(['sub-s1', 'sub-s2'])
|
|
572
|
+
['sub-s[1-2]']
|
|
573
|
+
"""
|
|
574
|
+
sumDic = generateSumDic(Hostnames)
|
|
575
|
+
filteredSumDic = filterSumDic(sumDic)
|
|
576
|
+
lastFilteredSumDicLen = len(filteredSumDic) + 1
|
|
577
|
+
while lastFilteredSumDicLen > len(filteredSumDic):
|
|
578
|
+
lastFilteredSumDicLen = len(filteredSumDic)
|
|
579
|
+
filteredSumDic = filterSumDic(filteredSumDic)
|
|
580
|
+
rtnSet = set()
|
|
581
|
+
for sumHash in filteredSumDic:
|
|
582
|
+
for hostnameTokens in filteredSumDic[sumHash]:
|
|
583
|
+
hostnameGroupDic = filteredSumDic[sumHash][hostnameTokens]
|
|
584
|
+
hostnameList = list(hostnameTokens)
|
|
585
|
+
for tokenIndex, tokenLength in hostnameGroupDic:
|
|
586
|
+
startToken, endToken = hostnameGroupDic[(tokenIndex, tokenLength)]
|
|
587
|
+
if tokenLength:
|
|
588
|
+
hostnameList[tokenIndex] = f'[{startToken:0{tokenLength}d}-{endToken:0{tokenLength}d}]'
|
|
589
|
+
else:
|
|
590
|
+
hostnameList[tokenIndex] = f'[{startToken}-{endToken}]'
|
|
591
|
+
rtnSet.add(''.join(hostnameList))
|
|
592
|
+
return rtnSet
|
|
593
|
+
|
|
594
|
+
#%%
|
|
270
595
|
@cache_decorator
|
|
271
596
|
def expandIPv4Address(hosts):
|
|
272
597
|
'''
|
|
@@ -324,6 +649,8 @@ def getIP(hostname,local=False):
|
|
|
324
649
|
str: The IP address of the hostname
|
|
325
650
|
'''
|
|
326
651
|
global _etc_hosts
|
|
652
|
+
if '@' in hostname:
|
|
653
|
+
_, hostname = hostname.rsplit('@',1)
|
|
327
654
|
# First we check if the hostname is an IP address
|
|
328
655
|
try:
|
|
329
656
|
ipaddress.ip_address(hostname)
|
|
@@ -385,7 +712,7 @@ def readEnvFromFile(environemnt_file = ''):
|
|
|
385
712
|
return env
|
|
386
713
|
|
|
387
714
|
@cache_decorator
|
|
388
|
-
def
|
|
715
|
+
def old_expand_hostname(text,validate=True):
|
|
389
716
|
'''
|
|
390
717
|
Expand the hostname range in the text.
|
|
391
718
|
Will search the string for a range ( [] encloused and non enclosed number ranges).
|
|
@@ -464,6 +791,64 @@ def expand_hostname(text,validate=True):
|
|
|
464
791
|
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
465
792
|
return expandedhosts
|
|
466
793
|
|
|
794
|
+
@cache_decorator
|
|
795
|
+
def expand_hostname(text, validate=True):
|
|
796
|
+
'''
|
|
797
|
+
Expand the hostname range in the text.
|
|
798
|
+
Will search the string for a range ( [] enclosed and non-enclosed number ranges).
|
|
799
|
+
Will expand the range, validate them using validate_expand_hostname and return a list of expanded hostnames
|
|
800
|
+
|
|
801
|
+
Args:
|
|
802
|
+
text (str): The text to be expanded
|
|
803
|
+
validate (bool, optional): Whether to validate the hostname. Defaults to True.
|
|
804
|
+
|
|
805
|
+
Returns:
|
|
806
|
+
set: A set of expanded hostnames
|
|
807
|
+
'''
|
|
808
|
+
expandinghosts = [text]
|
|
809
|
+
expandedhosts = set()
|
|
810
|
+
# all valid alphanumeric characters
|
|
811
|
+
alphanumeric = string.digits + string.ascii_letters
|
|
812
|
+
while len(expandinghosts) > 0:
|
|
813
|
+
hostname = expandinghosts.pop()
|
|
814
|
+
match = re.search(r'\[(.*?)]', hostname)
|
|
815
|
+
if not match:
|
|
816
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
817
|
+
continue
|
|
818
|
+
group = match.group(1)
|
|
819
|
+
parts = group.split(',')
|
|
820
|
+
for part in parts:
|
|
821
|
+
part = part.strip()
|
|
822
|
+
if '-' in part:
|
|
823
|
+
try:
|
|
824
|
+
range_start,_, range_end = part.partition('-')
|
|
825
|
+
except ValueError:
|
|
826
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
827
|
+
continue
|
|
828
|
+
range_start = range_start.strip()
|
|
829
|
+
range_end = range_end.strip()
|
|
830
|
+
if range_start.isdigit() and range_end.isdigit():
|
|
831
|
+
padding_length = min(len(range_start), len(range_end))
|
|
832
|
+
format_str = "{:0" + str(padding_length) + "d}"
|
|
833
|
+
for i in range(int(range_start), int(range_end) + 1):
|
|
834
|
+
formatted_i = format_str.format(i)
|
|
835
|
+
expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
|
|
836
|
+
elif all(c in string.hexdigits for c in range_start + range_end):
|
|
837
|
+
for i in range(int(range_start, 16), int(range_end, 16) + 1):
|
|
838
|
+
expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
|
|
839
|
+
else:
|
|
840
|
+
try:
|
|
841
|
+
start_index = alphanumeric.index(range_start)
|
|
842
|
+
end_index = alphanumeric.index(range_end)
|
|
843
|
+
for i in range(start_index, end_index + 1):
|
|
844
|
+
expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
|
|
845
|
+
except ValueError:
|
|
846
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
847
|
+
else:
|
|
848
|
+
expandinghosts.append(hostname.replace(match.group(0), part, 1))
|
|
849
|
+
return expandedhosts
|
|
850
|
+
|
|
851
|
+
|
|
467
852
|
@cache_decorator
|
|
468
853
|
def expand_hostnames(hosts):
|
|
469
854
|
'''
|
|
@@ -529,7 +914,7 @@ def validate_expand_hostname(hostname):
|
|
|
529
914
|
elif getIP(hostname,local=False):
|
|
530
915
|
return [hostname]
|
|
531
916
|
else:
|
|
532
|
-
eprint(f"Error: {hostname} is not a valid hostname or IP address!")
|
|
917
|
+
eprint(f"Error: {hostname!r} is not a valid hostname or IP address!")
|
|
533
918
|
global __mainReturnCode
|
|
534
919
|
__mainReturnCode += 1
|
|
535
920
|
global __failedHosts
|
|
@@ -623,8 +1008,9 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
623
1008
|
if sentInput < len(__keyPressesIn) - 1 :
|
|
624
1009
|
stream.write(''.join(__keyPressesIn[sentInput]).encode())
|
|
625
1010
|
stream.flush()
|
|
626
|
-
|
|
627
|
-
host.
|
|
1011
|
+
line = '> ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵')
|
|
1012
|
+
host.output.append(line)
|
|
1013
|
+
host.stdout.append(line)
|
|
628
1014
|
sentInput += 1
|
|
629
1015
|
host.lastUpdateTime = time.time()
|
|
630
1016
|
else:
|
|
@@ -667,7 +1053,7 @@ def replace_magic_strings(string,keys,value,case_sensitive=False):
|
|
|
667
1053
|
string = re.sub(re.escape(key),value,string,flags=re.IGNORECASE)
|
|
668
1054
|
return string
|
|
669
1055
|
|
|
670
|
-
def
|
|
1056
|
+
def run_command(host, sem, timeout=60,passwds=None):
|
|
671
1057
|
'''
|
|
672
1058
|
Run the command on the host. Will format the commands accordingly. Main execution function.
|
|
673
1059
|
|
|
@@ -706,8 +1092,6 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
706
1092
|
host.command = replace_magic_strings(host.command,['#ID#'],str(id(host)),case_sensitive=False)
|
|
707
1093
|
host.command = replace_magic_strings(host.command,['#I#'],str(host.i),case_sensitive=False)
|
|
708
1094
|
host.command = replace_magic_strings(host.command,['#PASSWD#','#PASSWORD#'],passwds,case_sensitive=False)
|
|
709
|
-
if host.resolvedName:
|
|
710
|
-
host.command = replace_magic_strings(host.command,['#RESOLVEDNAME#','#RESOLVED#'],host.resolvedName,case_sensitive=False)
|
|
711
1095
|
host.command = replace_magic_strings(host.command,['#UUID#'],str(host.uuid),case_sensitive=False)
|
|
712
1096
|
formatedCMD = []
|
|
713
1097
|
if host.extraargs and type(host.extraargs) == str:
|
|
@@ -729,6 +1113,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
729
1113
|
host.resolvedName = host.name
|
|
730
1114
|
else:
|
|
731
1115
|
host.resolvedName = host.name
|
|
1116
|
+
if host.resolvedName:
|
|
1117
|
+
host.command = replace_magic_strings(host.command,['#RESOLVEDNAME#','#RESOLVED#'],host.resolvedName,case_sensitive=False)
|
|
732
1118
|
if host.ipmi:
|
|
733
1119
|
if 'ipmitool' in _binPaths:
|
|
734
1120
|
if host.command.startswith('ipmitool '):
|
|
@@ -737,30 +1123,50 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
737
1123
|
host.command = host.command.replace(_binPaths['ipmitool'],'')
|
|
738
1124
|
if not host.username:
|
|
739
1125
|
host.username = 'admin'
|
|
1126
|
+
if not host.command:
|
|
1127
|
+
host.command = 'power status'
|
|
740
1128
|
if 'bash' in _binPaths:
|
|
741
1129
|
if passwds:
|
|
742
1130
|
formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
|
|
743
1131
|
else:
|
|
744
|
-
|
|
1132
|
+
host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
|
|
1133
|
+
formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P admin {" ".join(extraargs)} {host.command}']
|
|
745
1134
|
else:
|
|
746
1135
|
if passwds:
|
|
747
1136
|
formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P {passwds}'] + extraargs + [host.command]
|
|
748
1137
|
else:
|
|
749
|
-
|
|
1138
|
+
host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
|
|
1139
|
+
formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P admin'] + extraargs + [host.command]
|
|
750
1140
|
elif 'ssh' in _binPaths:
|
|
751
1141
|
host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
|
|
752
1142
|
if __DEBUG_MODE:
|
|
753
1143
|
host.stderr.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
|
|
754
1144
|
host.ipmi = False
|
|
755
1145
|
host.interface_ip_prefix = None
|
|
756
|
-
|
|
757
|
-
|
|
1146
|
+
if not host.command:
|
|
1147
|
+
host.command = 'ipmitool power status'
|
|
1148
|
+
else:
|
|
1149
|
+
host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
|
|
1150
|
+
run_command(host,sem,timeout,passwds)
|
|
758
1151
|
return
|
|
759
1152
|
else:
|
|
760
1153
|
host.output.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
|
|
761
1154
|
host.stderr.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
|
|
762
1155
|
host.returncode = 1
|
|
763
1156
|
return
|
|
1157
|
+
elif host.bash:
|
|
1158
|
+
if 'bash' in _binPaths:
|
|
1159
|
+
host.output.append('Running command in bash mode, ignoring the hosts...')
|
|
1160
|
+
if __DEBUG_MODE:
|
|
1161
|
+
host.stderr.append('Running command in bash mode, ignoring the hosts...')
|
|
1162
|
+
formatedCMD = [_binPaths['bash'],'-c',host.command]
|
|
1163
|
+
else:
|
|
1164
|
+
host.output.append('Bash not found on the local machine! Using ssh localhost instead...')
|
|
1165
|
+
if __DEBUG_MODE:
|
|
1166
|
+
host.stderr.append('Bash not found on the local machine! Using ssh localhost instead...')
|
|
1167
|
+
host.bash = False
|
|
1168
|
+
host.name = 'localhost'
|
|
1169
|
+
run_command(host,sem,timeout,passwds)
|
|
764
1170
|
else:
|
|
765
1171
|
if host.files:
|
|
766
1172
|
if host.scp:
|
|
@@ -916,7 +1322,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
916
1322
|
host.ipmi = False
|
|
917
1323
|
host.interface_ip_prefix = None
|
|
918
1324
|
host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
|
|
919
|
-
|
|
1325
|
+
run_command(host,sem,timeout,passwds)
|
|
920
1326
|
# If transfering files, we will try again using scp if rsync connection is not successful
|
|
921
1327
|
if host.files and not host.scp and not useScp and host.returncode != 0 and host.stderr:
|
|
922
1328
|
host.stderr = []
|
|
@@ -925,11 +1331,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
925
1331
|
if __DEBUG_MODE:
|
|
926
1332
|
host.stderr.append('Rsync connection failed! Trying SCP connection...')
|
|
927
1333
|
host.scp = True
|
|
928
|
-
|
|
1334
|
+
run_command(host,sem,timeout,passwds)
|
|
929
1335
|
|
|
930
1336
|
def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cpu_count()):
|
|
931
1337
|
'''
|
|
932
|
-
Start running the command on the hosts. Wrapper function for
|
|
1338
|
+
Start running the command on the hosts. Wrapper function for run_command
|
|
933
1339
|
|
|
934
1340
|
Args:
|
|
935
1341
|
hosts (list): A list of Host objects
|
|
@@ -943,7 +1349,7 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
|
|
|
943
1349
|
if len(hosts) == 0:
|
|
944
1350
|
return []
|
|
945
1351
|
sem = threading.Semaphore(max_connections) # Limit concurrent SSH sessions
|
|
946
|
-
threads = [threading.Thread(target=
|
|
1352
|
+
threads = [threading.Thread(target=run_command, args=(host, sem,timeout,password), daemon=True) for host in hosts]
|
|
947
1353
|
for thread in threads:
|
|
948
1354
|
thread.start()
|
|
949
1355
|
return threads
|
|
@@ -980,13 +1386,6 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
|
980
1386
|
host.printedLines = 0
|
|
981
1387
|
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
982
1388
|
|
|
983
|
-
# 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...
|
|
984
|
-
# Traceback (most recent call last):
|
|
985
|
-
# File "/usr/local/lib/python3.11/site-packages/multiSSH3.py", line 1030, in generate_display
|
|
986
|
-
# host_window_height = max_y // num_hosts_y
|
|
987
|
-
# ~~~~~~^^~~~~~~~~~~~~
|
|
988
|
-
# ZeroDivisionError: integer division or modulo by zero
|
|
989
|
-
|
|
990
1389
|
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'):
|
|
991
1390
|
try:
|
|
992
1391
|
org_dim = stdscr.getmaxyx()
|
|
@@ -1321,15 +1720,21 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1321
1720
|
if host['stderr']:
|
|
1322
1721
|
if host['stderr'][0].strip().startswith('ssh: connect to host '):
|
|
1323
1722
|
host['stderr'][0] = 'SSH not reachable!'
|
|
1723
|
+
elif host['stderr'][-1].strip().endswith('Connection timed out'):
|
|
1724
|
+
host['stderr'][-1] = 'SSH connection timed out!'
|
|
1725
|
+
elif host['stderr'][-1].strip().endswith('No route to host'):
|
|
1726
|
+
host['stderr'][-1] = 'Cannot find host!'
|
|
1324
1727
|
hostPrintOut += " | stderr: "+'↵ '.join(host['stderr'])
|
|
1325
1728
|
hostPrintOut += f" | return_code: {host['returncode']}"
|
|
1326
|
-
|
|
1327
|
-
outputs[hostPrintOut] = [host['name']]
|
|
1328
|
-
else:
|
|
1329
|
-
outputs[hostPrintOut].append(host['name'])
|
|
1729
|
+
outputs.setdefault(hostPrintOut, set()).add(host['name'])
|
|
1330
1730
|
rtnStr = '*'*80+'\n'
|
|
1331
|
-
for output,
|
|
1332
|
-
|
|
1731
|
+
for output, hostSet in outputs.items():
|
|
1732
|
+
compact_hosts = compact_hostnames(hostSet)
|
|
1733
|
+
if expand_hostnames(frozenset(compact_hosts)) != expand_hostnames(frozenset(hostSet)):
|
|
1734
|
+
eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
|
|
1735
|
+
rtnStr += f"{','.join(hostSet)}{output}\n"
|
|
1736
|
+
else:
|
|
1737
|
+
rtnStr += f"{','.join(compact_hosts)}{output}\n"
|
|
1333
1738
|
rtnStr += '*'*80+'\n'
|
|
1334
1739
|
if __keyPressesIn[-1]:
|
|
1335
1740
|
CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
|
|
@@ -1346,20 +1751,25 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1346
1751
|
if host['stderr']:
|
|
1347
1752
|
if host['stderr'][0].strip().startswith('ssh: connect to host '):
|
|
1348
1753
|
host['stderr'][0] = 'SSH not reachable!'
|
|
1754
|
+
elif host['stderr'][-1].strip().endswith('Connection timed out'):
|
|
1755
|
+
host['stderr'][-1] = 'SSH connection timed out!'
|
|
1756
|
+
elif host['stderr'][-1].strip().endswith('No route to host'):
|
|
1757
|
+
host['stderr'][-1] = 'Cannot find host!'
|
|
1349
1758
|
hostPrintOut += "\n stderr:\n "+'\n '.join(host['stderr'])
|
|
1350
1759
|
hostPrintOut += f"\n return_code: {host['returncode']}"
|
|
1351
|
-
|
|
1352
|
-
outputs[hostPrintOut] = [host['name']]
|
|
1353
|
-
else:
|
|
1354
|
-
outputs[hostPrintOut].append(host['name'])
|
|
1760
|
+
outputs.setdefault(hostPrintOut, set()).add(host['name'])
|
|
1355
1761
|
rtnStr = ''
|
|
1356
|
-
for output,
|
|
1762
|
+
for output, hostSet in outputs.items():
|
|
1763
|
+
compact_hosts = compact_hostnames(hostSet)
|
|
1764
|
+
if expand_hostnames(frozenset(compact_hosts)) != expand_hostnames(frozenset(hostSet)):
|
|
1765
|
+
eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
|
|
1766
|
+
compact_hosts = hostSet
|
|
1357
1767
|
if __global_suppress_printout:
|
|
1358
|
-
rtnStr += f'Abnormal returncode produced by {
|
|
1768
|
+
rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
|
|
1359
1769
|
rtnStr += output+'\n'
|
|
1360
1770
|
else:
|
|
1361
1771
|
rtnStr += '*'*80+'\n'
|
|
1362
|
-
rtnStr += f
|
|
1772
|
+
rtnStr += f'These hosts: {",".join(compact_hosts)} have a response of:\n'
|
|
1363
1773
|
rtnStr += output+'\n'
|
|
1364
1774
|
if not __global_suppress_printout or outputs:
|
|
1365
1775
|
rtnStr += '*'*80+'\n'
|
|
@@ -1377,35 +1787,6 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1377
1787
|
print(rtnStr)
|
|
1378
1788
|
return rtnStr
|
|
1379
1789
|
|
|
1380
|
-
# sshConfigged = False
|
|
1381
|
-
# def verify_ssh_config():
|
|
1382
|
-
# '''
|
|
1383
|
-
# Verify that ~/.ssh/config exists and contains the line "StrictHostKeyChecking no"
|
|
1384
|
-
|
|
1385
|
-
# Args:
|
|
1386
|
-
# None
|
|
1387
|
-
|
|
1388
|
-
# Returns:
|
|
1389
|
-
# None
|
|
1390
|
-
# '''
|
|
1391
|
-
# global sshConfigged
|
|
1392
|
-
# if not sshConfigged:
|
|
1393
|
-
# # first we make sure ~/.ssh/config exists
|
|
1394
|
-
# config = ''
|
|
1395
|
-
# if not os.path.exists(os.path.expanduser('~/.ssh')):
|
|
1396
|
-
# os.makedirs(os.path.expanduser('~/.ssh'))
|
|
1397
|
-
# if os.path.exists(os.path.expanduser('~/.ssh/config')):
|
|
1398
|
-
# with open(os.path.expanduser('~/.ssh/config'),'r') as f:
|
|
1399
|
-
# config = f.read()
|
|
1400
|
-
# if config:
|
|
1401
|
-
# if 'StrictHostKeyChecking no' not in config:
|
|
1402
|
-
# with open(os.path.expanduser('~/.ssh/config'),'a') as f:
|
|
1403
|
-
# f.write('\nHost *\n\tStrictHostKeyChecking no\n')
|
|
1404
|
-
# else:
|
|
1405
|
-
# with open(os.path.expanduser('~/.ssh/config'),'w') as f:
|
|
1406
|
-
# f.write('Host *\n\tStrictHostKeyChecking no\n')
|
|
1407
|
-
# sshConfigged = True
|
|
1408
|
-
|
|
1409
1790
|
def signal_handler(sig, frame):
|
|
1410
1791
|
'''
|
|
1411
1792
|
Handle the Ctrl C signal
|
|
@@ -1432,7 +1813,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
1432
1813
|
global __globalUnavailableHosts
|
|
1433
1814
|
global _no_env
|
|
1434
1815
|
threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
|
|
1435
|
-
if not nowatch and threads and not returnUnfinished and any([thread.is_alive() for thread in threads]) and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
|
|
1816
|
+
if __curses_available and not nowatch and threads and not returnUnfinished and any([thread.is_alive() for thread in threads]) and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
|
|
1436
1817
|
curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
|
|
1437
1818
|
if not returnUnfinished:
|
|
1438
1819
|
# wait until all hosts have a return code
|
|
@@ -1443,7 +1824,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
1443
1824
|
# update the unavailable hosts and global unavailable hosts
|
|
1444
1825
|
if willUpdateUnreachableHosts:
|
|
1445
1826
|
unavailableHosts = set(unavailableHosts)
|
|
1446
|
-
unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or host.stderr[
|
|
1827
|
+
unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or (host.stderr[-1].strip().startswith('Timeout!') and host.returncode == 124))])
|
|
1447
1828
|
# reachable hosts = all hosts - unreachable hosts
|
|
1448
1829
|
reachableHosts = set([host.name for host in hosts]) - unavailableHosts
|
|
1449
1830
|
if __DEBUG_MODE:
|
|
@@ -1480,7 +1861,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
1480
1861
|
os.replace(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'))
|
|
1481
1862
|
|
|
1482
1863
|
except Exception as e:
|
|
1483
|
-
eprint(f'Error writing to temporary file: {e}')
|
|
1864
|
+
eprint(f'Error writing to temporary file: {e!r}')
|
|
1484
1865
|
|
|
1485
1866
|
# print the output, if the output of multiple hosts are the same, we aggragate them
|
|
1486
1867
|
if not called:
|
|
@@ -1517,12 +1898,14 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
1517
1898
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1518
1899
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
|
|
1519
1900
|
file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
1901
|
+
copy_id = False,
|
|
1520
1902
|
shortend = False) -> str:
|
|
1521
1903
|
argsList = []
|
|
1522
1904
|
if oneonone: argsList.append('--oneonone' if not shortend else '-11')
|
|
1523
1905
|
if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
|
|
1524
1906
|
if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
|
|
1525
1907
|
if identity_file and identity_file != DEFAULT_IDENTITY_FILE: argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
|
|
1908
|
+
if copy_id: argsList.append('--copy_id' if not shortend else '-ci')
|
|
1526
1909
|
if nowatch: argsList.append('--nowatch' if not shortend else '-q')
|
|
1527
1910
|
if json: argsList.append('--json' if not shortend else '-j')
|
|
1528
1911
|
if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
|
|
@@ -1548,6 +1931,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1548
1931
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1549
1932
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1550
1933
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
1934
|
+
copy_id = False,
|
|
1551
1935
|
shortend = False):
|
|
1552
1936
|
hosts = hosts if type(hosts) == str else frozenset(hosts)
|
|
1553
1937
|
hostStr = formHostStr(hosts)
|
|
@@ -1557,6 +1941,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1557
1941
|
files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
|
|
1558
1942
|
username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
|
|
1559
1943
|
greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, identity_file = identity_file,
|
|
1944
|
+
copy_id = copy_id,
|
|
1560
1945
|
shortend = shortend)
|
|
1561
1946
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
1562
1947
|
return f'multissh {argsStr} {hostStr} {commandStr}'
|
|
@@ -1567,7 +1952,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1567
1952
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1568
1953
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1569
1954
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1570
|
-
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE
|
|
1955
|
+
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE,
|
|
1956
|
+
copy_id = False):
|
|
1571
1957
|
f'''
|
|
1572
1958
|
Run the command on the hosts, aka multissh. main function
|
|
1573
1959
|
|
|
@@ -1602,6 +1988,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1602
1988
|
error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
|
|
1603
1989
|
quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
|
|
1604
1990
|
identity_file (str, optional): The identity file to use for the ssh connection. Defaults to {DEFAULT_IDENTITY_FILE}.
|
|
1991
|
+
copy_id (bool, optional): Whether to copy the id to the hosts. Defaults to False.
|
|
1605
1992
|
|
|
1606
1993
|
Returns:
|
|
1607
1994
|
list: A list of Host objects
|
|
@@ -1623,17 +2010,20 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1623
2010
|
elif checkTime > 3600:
|
|
1624
2011
|
checkTime = 3600
|
|
1625
2012
|
try:
|
|
2013
|
+
readed = False
|
|
1626
2014
|
if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < checkTime:
|
|
1627
|
-
|
|
1628
|
-
eprint(f"Reading unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
|
|
2015
|
+
|
|
1629
2016
|
with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
|
|
1630
2017
|
for line in f:
|
|
1631
2018
|
line = line.strip()
|
|
1632
2019
|
if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
|
|
1633
2020
|
if int(line.split(',')[1]) > time.time() - checkTime:
|
|
1634
2021
|
__globalUnavailableHosts.add(line.split(',')[0])
|
|
2022
|
+
readed = True
|
|
2023
|
+
if readed and not __global_suppress_printout:
|
|
2024
|
+
eprint(f"Read unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
|
|
1635
2025
|
except Exception as e:
|
|
1636
|
-
eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
|
|
2026
|
+
eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')!r}")
|
|
1637
2027
|
eprint(str(e))
|
|
1638
2028
|
elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
|
|
1639
2029
|
__globalUnavailableHosts.update(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
|
|
@@ -1652,13 +2042,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1652
2042
|
commands = [' '.join(command) if not type(command) == str else command for command in commands]
|
|
1653
2043
|
except:
|
|
1654
2044
|
pass
|
|
1655
|
-
eprint(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.")
|
|
2045
|
+
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.")
|
|
1656
2046
|
#verify_ssh_config()
|
|
1657
2047
|
# load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
|
|
1658
2048
|
if called:
|
|
1659
2049
|
# if called,
|
|
1660
2050
|
# if skipUnreachable is not set, we default to skip unreachable hosts within one command call
|
|
1661
|
-
__global_suppress_printout = True
|
|
1662
2051
|
if skipUnreachable is None:
|
|
1663
2052
|
skipUnreachable = True
|
|
1664
2053
|
if skipUnreachable:
|
|
@@ -1694,10 +2083,34 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1694
2083
|
skipHostStr = ','.join(skipHostStr)
|
|
1695
2084
|
targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
|
|
1696
2085
|
if __DEBUG_MODE:
|
|
1697
|
-
eprint(f"Target hosts: {targetHostsList}")
|
|
2086
|
+
eprint(f"Target hosts: {targetHostsList!r}")
|
|
1698
2087
|
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
|
|
1699
2088
|
if skipHostsList:
|
|
1700
|
-
eprint(f"Skipping hosts: {skipHostsList}")
|
|
2089
|
+
eprint(f"Skipping hosts: {skipHostsList!r}")
|
|
2090
|
+
if copy_id:
|
|
2091
|
+
if 'ssh-copy-id' in _binPaths:
|
|
2092
|
+
# we will copy the id to the hosts
|
|
2093
|
+
hosts = []
|
|
2094
|
+
for host in targetHostsList:
|
|
2095
|
+
if host.strip() in skipHostsList: continue
|
|
2096
|
+
command = f"{_binPaths['ssh-copy-id']} "
|
|
2097
|
+
if identity_file:
|
|
2098
|
+
command = f"{command}-i {identity_file} "
|
|
2099
|
+
if username:
|
|
2100
|
+
command = f"{command} {username}@"
|
|
2101
|
+
command = f"{command}{host}"
|
|
2102
|
+
if password and 'sshpass' in _binPaths:
|
|
2103
|
+
command = f"{_binPaths['sshpass']} -p {password} {command}"
|
|
2104
|
+
hosts.append(Host(host, command,identity_file=identity_file,bash=True))
|
|
2105
|
+
else:
|
|
2106
|
+
eprint(f"> {command}")
|
|
2107
|
+
os.system(command)
|
|
2108
|
+
if hosts:
|
|
2109
|
+
processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
|
|
2110
|
+
else:
|
|
2111
|
+
eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
|
|
2112
|
+
if not commands:
|
|
2113
|
+
sys.exit(0)
|
|
1701
2114
|
if files and not commands:
|
|
1702
2115
|
# if files are specified but not target dir, we default to file sync mode
|
|
1703
2116
|
file_sync = True
|
|
@@ -1714,7 +2127,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1714
2127
|
except:
|
|
1715
2128
|
pathSet.update(glob.glob(file,recursive=True))
|
|
1716
2129
|
if not pathSet:
|
|
1717
|
-
eprint(f'Warning: No source files at {files} are found after resolving globs!')
|
|
2130
|
+
eprint(f'Warning: No source files at {files!r} are found after resolving globs!')
|
|
1718
2131
|
sys.exit(66)
|
|
1719
2132
|
else:
|
|
1720
2133
|
pathSet = set(files)
|
|
@@ -1725,13 +2138,13 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1725
2138
|
else:
|
|
1726
2139
|
files = list(pathSet)
|
|
1727
2140
|
if __DEBUG_MODE:
|
|
1728
|
-
eprint(f"Files: {files}")
|
|
2141
|
+
eprint(f"Files: {files!r}")
|
|
1729
2142
|
if oneonone:
|
|
1730
2143
|
hosts = []
|
|
1731
|
-
if len(commands) != len(targetHostsList) -
|
|
2144
|
+
if len(commands) != len(set(targetHostsList) - set(skipHostsList)):
|
|
1732
2145
|
eprint("Error: the number of commands must be the same as the number of hosts")
|
|
1733
2146
|
eprint(f"Number of commands: {len(commands)}")
|
|
1734
|
-
eprint(f"Number of hosts: {len(targetHostsList - skipHostsList)}")
|
|
2147
|
+
eprint(f"Number of hosts: {len(set(targetHostsList) - set(skipHostsList))}")
|
|
1735
2148
|
sys.exit(255)
|
|
1736
2149
|
if not __global_suppress_printout:
|
|
1737
2150
|
eprint('-'*80)
|
|
@@ -1746,8 +2159,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1746
2159
|
else:
|
|
1747
2160
|
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
|
|
1748
2161
|
if not __global_suppress_printout:
|
|
1749
|
-
eprint(f"Running command: {command} on host: {host}")
|
|
1750
|
-
if not __global_suppress_printout:
|
|
2162
|
+
eprint(f"Running command: {command!r} on host: {host!r}")
|
|
2163
|
+
if not __global_suppress_printout: eprint('-'*80)
|
|
1751
2164
|
if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
|
|
1752
2165
|
return hosts
|
|
1753
2166
|
else:
|
|
@@ -1760,13 +2173,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1760
2173
|
if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
|
|
1761
2174
|
continue
|
|
1762
2175
|
if host.strip() in skipHostsList: continue
|
|
2176
|
+
# TODO: use ip to determine if we skip the host or not, also for unavailable hosts
|
|
1763
2177
|
if file_sync:
|
|
1764
2178
|
eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
|
|
1765
2179
|
return []
|
|
1766
2180
|
elif files:
|
|
1767
2181
|
eprint(f"Error: files need to be specified with at least one path to sync")
|
|
1768
|
-
elif ipmi:
|
|
1769
|
-
eprint(f"Error: ipmi mode is not supported in interactive mode")
|
|
1770
2182
|
else:
|
|
1771
2183
|
hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,identity_file=identity_file))
|
|
1772
2184
|
if not __global_suppress_printout:
|
|
@@ -1920,11 +2332,14 @@ def main():
|
|
|
1920
2332
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
1921
2333
|
parser.add_argument("--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as wells. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
|
|
1922
2334
|
parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
1923
|
-
|
|
2335
|
+
group = parser.add_mutually_exclusive_group()
|
|
2336
|
+
group.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
|
|
2337
|
+
group.add_argument("-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
|
|
2338
|
+
|
|
1924
2339
|
parser.add_argument("-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
|
|
1925
2340
|
parser.add_argument('--store_config_file', action='store_true', help=f'Store / generate the default config file from command line argument and current config at {CONFIG_FILE}')
|
|
1926
2341
|
parser.add_argument('--debug', action='store_true', help='Print debug information')
|
|
1927
|
-
parser.add_argument('
|
|
2342
|
+
parser.add_argument('-ci','--copy_id', action='store_true', help='Copy the ssh id to the hosts')
|
|
1928
2343
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
1929
2344
|
|
|
1930
2345
|
# parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
|
|
@@ -1932,14 +2347,15 @@ def main():
|
|
|
1932
2347
|
#args = parser.parse_args()
|
|
1933
2348
|
|
|
1934
2349
|
# if python version is 3.7 or higher, use parse_intermixed_args
|
|
1935
|
-
|
|
2350
|
+
try:
|
|
1936
2351
|
args = parser.parse_intermixed_args()
|
|
1937
|
-
|
|
2352
|
+
except Exception as e:
|
|
2353
|
+
eprint(f"Error while parsing arguments: {e!r}")
|
|
1938
2354
|
# try to parse the arguments using parse_known_args
|
|
1939
2355
|
args, unknown = parser.parse_known_args()
|
|
1940
2356
|
# if there are unknown arguments, we will try to parse them again using parse_args
|
|
1941
2357
|
if unknown:
|
|
1942
|
-
eprint(f"Warning: Unknown arguments, treating all as commands: {unknown}")
|
|
2358
|
+
eprint(f"Warning: Unknown arguments, treating all as commands: {unknown!r}")
|
|
1943
2359
|
args.commands += unknown
|
|
1944
2360
|
|
|
1945
2361
|
|
|
@@ -1947,22 +2363,22 @@ def main():
|
|
|
1947
2363
|
if args.store_config_file:
|
|
1948
2364
|
try:
|
|
1949
2365
|
if os.path.exists(CONFIG_FILE):
|
|
1950
|
-
eprint(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
|
|
2366
|
+
eprint(f"Warning: {CONFIG_FILE!r} already exists, what to do? (o/b/n)")
|
|
1951
2367
|
eprint(f"o: Overwrite the file")
|
|
1952
|
-
eprint(f"b: Rename the current config file at {CONFIG_FILE}.bak forcefully and write the new config file (default)")
|
|
2368
|
+
eprint(f"b: Rename the current config file at {CONFIG_FILE!r}.bak forcefully and write the new config file (default)")
|
|
1953
2369
|
eprint(f"n: Do nothing")
|
|
1954
2370
|
inStr = input_with_timeout_and_countdown(10)
|
|
1955
2371
|
if (not inStr) or inStr.lower().strip().startswith('b'):
|
|
1956
2372
|
write_default_config(args,CONFIG_FILE,backup = True)
|
|
1957
|
-
eprint(f"Config file written to {CONFIG_FILE}")
|
|
2373
|
+
eprint(f"Config file written to {CONFIG_FILE!r}")
|
|
1958
2374
|
elif inStr.lower().strip().startswith('o'):
|
|
1959
2375
|
write_default_config(args,CONFIG_FILE,backup = False)
|
|
1960
|
-
eprint(f"Config file written to {CONFIG_FILE}")
|
|
2376
|
+
eprint(f"Config file written to {CONFIG_FILE!r}")
|
|
1961
2377
|
else:
|
|
1962
2378
|
write_default_config(args,CONFIG_FILE,backup = True)
|
|
1963
|
-
eprint(f"Config file written to {CONFIG_FILE}")
|
|
2379
|
+
eprint(f"Config file written to {CONFIG_FILE!r}")
|
|
1964
2380
|
except Exception as e:
|
|
1965
|
-
eprint(f"Error while writing config file: {e}")
|
|
2381
|
+
eprint(f"Error while writing config file: {e!r}")
|
|
1966
2382
|
import traceback
|
|
1967
2383
|
eprint(traceback.format_exc())
|
|
1968
2384
|
if not args.commands:
|
|
@@ -1982,9 +2398,9 @@ def main():
|
|
|
1982
2398
|
inStr = input_with_timeout_and_countdown(3)
|
|
1983
2399
|
if (not inStr) or inStr.lower().strip().startswith('1'):
|
|
1984
2400
|
args.commands = [" ".join(args.commands)]
|
|
1985
|
-
eprint(f"\nRunning 1 command: {args.commands[0]} on all hosts")
|
|
2401
|
+
eprint(f"\nRunning 1 command: {args.commands[0]!r} on all hosts")
|
|
1986
2402
|
elif inStr.lower().strip().startswith('m'):
|
|
1987
|
-
eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
|
|
2403
|
+
eprint(f"\nRunning multiple commands: {', '.join(args.commands)!r} on all hosts")
|
|
1988
2404
|
else:
|
|
1989
2405
|
sys.exit(0)
|
|
1990
2406
|
|
|
@@ -1995,26 +2411,7 @@ def main():
|
|
|
1995
2411
|
if os.path.isdir(os.path.expanduser(args.key)):
|
|
1996
2412
|
args.key = find_ssh_key_file(args.key)
|
|
1997
2413
|
elif not os.path.exists(args.key):
|
|
1998
|
-
eprint(f"Warning: Identity file {args.key} not found. Passing to ssh anyway. Proceed with caution.")
|
|
1999
|
-
|
|
2000
|
-
if args.copy_id:
|
|
2001
|
-
if 'ssh-copy-id' in _binPaths:
|
|
2002
|
-
# we will copy the id to the hosts
|
|
2003
|
-
for host in formHostStr(args.hosts).split(','):
|
|
2004
|
-
command = f"{_binPaths['ssh-copy-id']} "
|
|
2005
|
-
if args.key:
|
|
2006
|
-
command = f"{command}-i {args.key} "
|
|
2007
|
-
if args.username:
|
|
2008
|
-
command = f"{command} {args.username}@"
|
|
2009
|
-
command = f"{command}{host}"
|
|
2010
|
-
if args.password and 'sshpass' in _binPaths:
|
|
2011
|
-
command = f"{_binPaths['sshpass']} -p {args.password} {command}"
|
|
2012
|
-
eprint(f"> {command}")
|
|
2013
|
-
os.system(command)
|
|
2014
|
-
else:
|
|
2015
|
-
eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
|
|
2016
|
-
if not args.commands:
|
|
2017
|
-
sys.exit(0)
|
|
2414
|
+
eprint(f"Warning: Identity file {args.key!r} not found. Passing to ssh anyway. Proceed with caution.")
|
|
2018
2415
|
|
|
2019
2416
|
__ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
|
|
2020
2417
|
|
|
@@ -2026,7 +2423,8 @@ def main():
|
|
|
2026
2423
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
2027
2424
|
files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,gather_mode = args.gather_mode,username=args.username,
|
|
2028
2425
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
2029
|
-
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key
|
|
2426
|
+
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key,
|
|
2427
|
+
copy_id=args.copy_id)
|
|
2030
2428
|
eprint('> ' + cmdStr)
|
|
2031
2429
|
if args.error_only:
|
|
2032
2430
|
__global_suppress_printout = True
|
|
@@ -2042,7 +2440,8 @@ def main():
|
|
|
2042
2440
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
2043
2441
|
files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,gather_mode = args.gather_mode,username=args.username,
|
|
2044
2442
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
2045
|
-
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key
|
|
2443
|
+
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key,
|
|
2444
|
+
copy_id=args.copy_id)
|
|
2046
2445
|
#print('*'*80)
|
|
2047
2446
|
|
|
2048
2447
|
if not __global_suppress_printout: eprint('-'*80)
|
|
@@ -2078,3 +2477,6 @@ def main():
|
|
|
2078
2477
|
|
|
2079
2478
|
if __name__ == "__main__":
|
|
2080
2479
|
main()
|
|
2480
|
+
|
|
2481
|
+
# %%
|
|
2482
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|