multiSSH3 5.86__py3-none-any.whl → 5.91__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 +650 -140
- {multissh3-5.86.dist-info → multissh3-5.91.dist-info}/METADATA +1 -1
- multissh3-5.91.dist-info/RECORD +6 -0
- multissh3-5.86.dist-info/RECORD +0 -6
- {multissh3-5.86.dist-info → multissh3-5.91.dist-info}/WHEEL +0 -0
- {multissh3-5.86.dist-info → multissh3-5.91.dist-info}/entry_points.txt +0 -0
- {multissh3-5.86.dist-info → multissh3-5.91.dist-info}/top_level.txt +0 -0
multiSSH3.py
CHANGED
|
@@ -6,6 +6,32 @@
|
|
|
6
6
|
# "ipaddress",
|
|
7
7
|
# ]
|
|
8
8
|
# ///
|
|
9
|
+
import argparse
|
|
10
|
+
import functools
|
|
11
|
+
import getpass
|
|
12
|
+
import glob
|
|
13
|
+
import io
|
|
14
|
+
import ipaddress
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
import queue
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import signal
|
|
22
|
+
import socket
|
|
23
|
+
import string
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import tempfile
|
|
27
|
+
import textwrap
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
import typing
|
|
31
|
+
import uuid
|
|
32
|
+
from collections import Counter, deque
|
|
33
|
+
from itertools import count, product
|
|
34
|
+
|
|
9
35
|
__curses_available = False
|
|
10
36
|
__resource_lib_available = False
|
|
11
37
|
try:
|
|
@@ -20,30 +46,7 @@ try:
|
|
|
20
46
|
except ImportError:
|
|
21
47
|
pass
|
|
22
48
|
|
|
23
|
-
|
|
24
|
-
import threading
|
|
25
|
-
import time
|
|
26
|
-
import os
|
|
27
|
-
import argparse
|
|
28
|
-
from itertools import product
|
|
29
|
-
import re
|
|
30
|
-
import string
|
|
31
|
-
import ipaddress
|
|
32
|
-
import sys
|
|
33
|
-
import json
|
|
34
|
-
import socket
|
|
35
|
-
import io
|
|
36
|
-
import signal
|
|
37
|
-
import functools
|
|
38
|
-
import glob
|
|
39
|
-
import shutil
|
|
40
|
-
import getpass
|
|
41
|
-
import uuid
|
|
42
|
-
import tempfile
|
|
43
|
-
import math
|
|
44
|
-
from itertools import count
|
|
45
|
-
import queue
|
|
46
|
-
import typing
|
|
49
|
+
|
|
47
50
|
try:
|
|
48
51
|
# Check if functiools.cache is available
|
|
49
52
|
# cache_decorator = functools.cache
|
|
@@ -76,15 +79,15 @@ try:
|
|
|
76
79
|
wrapper.cache_clear = cached_func.cache_clear
|
|
77
80
|
return wrapper
|
|
78
81
|
return decorating_function(user_function)
|
|
79
|
-
except :
|
|
82
|
+
except Exception:
|
|
80
83
|
# If lrucache is not available, use a dummy decorator
|
|
81
84
|
print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
|
|
82
85
|
def cache_decorator(func):
|
|
83
86
|
return func
|
|
84
|
-
version = '5.
|
|
87
|
+
version = '5.91'
|
|
85
88
|
VERSION = version
|
|
86
89
|
__version__ = version
|
|
87
|
-
COMMIT_DATE = '2025-10-
|
|
90
|
+
COMMIT_DATE = '2025-10-17'
|
|
88
91
|
|
|
89
92
|
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
90
93
|
'~/multiSSH3.config.json',
|
|
@@ -93,16 +96,22 @@ CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
|
93
96
|
'/etc/multiSSH3.d/multiSSH3.config.json',
|
|
94
97
|
'/etc/multiSSH3.config.json'] # The first one has the highest priority
|
|
95
98
|
|
|
99
|
+
ERRORS = []
|
|
96
100
|
|
|
97
101
|
# TODO: Add terminal TUI
|
|
98
102
|
|
|
99
103
|
#%% ------------ Pre Helper Functions ----------------
|
|
100
104
|
def eprint(*args, **kwargs):
|
|
105
|
+
global ERRORS
|
|
101
106
|
try:
|
|
102
|
-
|
|
107
|
+
if 'file' in kwargs:
|
|
108
|
+
print(*args, **kwargs)
|
|
109
|
+
else:
|
|
110
|
+
print(*args, file=sys.stderr, **kwargs)
|
|
103
111
|
except Exception as e:
|
|
104
112
|
print(f"Error: Cannot print to stderr: {e}")
|
|
105
113
|
print(*args, **kwargs)
|
|
114
|
+
ERRORS.append(' '.join(map(str,args)))
|
|
106
115
|
|
|
107
116
|
def _exit_with_code(code, message=None):
|
|
108
117
|
'''
|
|
@@ -247,7 +256,7 @@ def getIP(hostname: str,local=False):
|
|
|
247
256
|
# Then we check the DNS
|
|
248
257
|
try:
|
|
249
258
|
return socket.gethostbyname(hostname)
|
|
250
|
-
except:
|
|
259
|
+
except Exception:
|
|
251
260
|
return None
|
|
252
261
|
|
|
253
262
|
|
|
@@ -320,8 +329,8 @@ def load_config_file(config_file):
|
|
|
320
329
|
try:
|
|
321
330
|
with open(config_file,'r') as f:
|
|
322
331
|
config = json.load(f)
|
|
323
|
-
except:
|
|
324
|
-
eprint(f"Error: Cannot load config file {config_file!r}")
|
|
332
|
+
except Exception as e:
|
|
333
|
+
eprint(f"Error: Cannot load config file {config_file!r}: {e}")
|
|
325
334
|
return {}
|
|
326
335
|
return config
|
|
327
336
|
|
|
@@ -346,6 +355,8 @@ DEFAULT_INTERVAL = 0
|
|
|
346
355
|
DEFAULT_IPMI = False
|
|
347
356
|
DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
|
|
348
357
|
DEFAULT_INTERFACE_IP_PREFIX = None
|
|
358
|
+
DEFAULT_IPMI_USERNAME = 'ADMIN'
|
|
359
|
+
DEFAULT_IPMI_PASSWORD = ''
|
|
349
360
|
DEFAULT_NO_WATCH = False
|
|
350
361
|
DEFAULT_CURSES_MINIMUM_CHAR_LEN = 40
|
|
351
362
|
DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
|
|
@@ -364,7 +375,9 @@ DEFAULT_GREPPABLE_MODE = False
|
|
|
364
375
|
DEFAULT_SKIP_UNREACHABLE = True
|
|
365
376
|
DEFAULT_SKIP_HOSTS = ''
|
|
366
377
|
DEFAULT_ENCODING = 'utf-8'
|
|
378
|
+
DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.75
|
|
367
379
|
SSH_STRICT_HOST_KEY_CHECKING = False
|
|
380
|
+
FORCE_TRUECOLOR = False
|
|
368
381
|
ERROR_MESSAGES_TO_IGNORE = [
|
|
369
382
|
'Pseudo-terminal will not be allocated because stdin is not a terminal',
|
|
370
383
|
'Connection to .* closed',
|
|
@@ -492,7 +505,7 @@ def readEnvFromFile(environemnt_file = ''):
|
|
|
492
505
|
try:
|
|
493
506
|
if env:
|
|
494
507
|
return env
|
|
495
|
-
except:
|
|
508
|
+
except Exception:
|
|
496
509
|
env = {}
|
|
497
510
|
global _env_file
|
|
498
511
|
if environemnt_file:
|
|
@@ -633,10 +646,283 @@ def format_commands(commands):
|
|
|
633
646
|
# reformat commands into a list of strings, join the iterables if they are not strings
|
|
634
647
|
try:
|
|
635
648
|
commands = [' '.join(command) if not isinstance(command,str) else command for command in commands]
|
|
636
|
-
except:
|
|
637
|
-
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.")
|
|
649
|
+
except Exception as e:
|
|
650
|
+
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. Error: {e}")
|
|
638
651
|
return commands
|
|
639
652
|
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class OrderedMultiSet(deque):
|
|
656
|
+
"""
|
|
657
|
+
A deque extension with O(1) average lookup time.
|
|
658
|
+
Maintains all deque functionality while tracking item counts.
|
|
659
|
+
"""
|
|
660
|
+
def __init__(self, iterable=None, maxlen=None):
|
|
661
|
+
"""Initialize with optional iterable and maxlen."""
|
|
662
|
+
super().__init__(maxlen=maxlen)
|
|
663
|
+
self._counter = Counter()
|
|
664
|
+
if iterable is not None:
|
|
665
|
+
self.extend(iterable)
|
|
666
|
+
def __decrease_count(self, item):
|
|
667
|
+
"""Decrease count of item in counter."""
|
|
668
|
+
self._counter[item] -= 1
|
|
669
|
+
if self._counter[item] == 0:
|
|
670
|
+
del self._counter[item]
|
|
671
|
+
return self._counter.get(item, 0)
|
|
672
|
+
def append(self, item,left=False):
|
|
673
|
+
"""Add item to the right end. O(1)."""
|
|
674
|
+
removed = None
|
|
675
|
+
if self.maxlen is not None and len(self) == self.maxlen:
|
|
676
|
+
removed = self[-1] if left else self[0] # Item that will be removed
|
|
677
|
+
self.__decrease_count(removed)
|
|
678
|
+
super().appendleft(item) if left else super().append(item)
|
|
679
|
+
self._counter[item] += 1
|
|
680
|
+
return removed
|
|
681
|
+
def appendleft(self, item):
|
|
682
|
+
"""Add item to the left end. O(1)."""
|
|
683
|
+
return self.append(item,left=True)
|
|
684
|
+
def pop(self,left=False):
|
|
685
|
+
"""Remove and return item from right end. O(1)."""
|
|
686
|
+
if not self:
|
|
687
|
+
return None
|
|
688
|
+
item = super().popleft() if left else super().pop()
|
|
689
|
+
self.__decrease_count(item)
|
|
690
|
+
return item
|
|
691
|
+
def popleft(self):
|
|
692
|
+
"""Remove and return item from left end. O(1)."""
|
|
693
|
+
return self.pop(left=True)
|
|
694
|
+
def remove(self, value):
|
|
695
|
+
"""Remove first occurrence of value. O(n)."""
|
|
696
|
+
if value not in self._counter:
|
|
697
|
+
return None
|
|
698
|
+
super().remove(value)
|
|
699
|
+
self.__decrease_count(value)
|
|
700
|
+
def clear(self):
|
|
701
|
+
"""Remove all items. O(1)."""
|
|
702
|
+
super().clear()
|
|
703
|
+
self._counter.clear()
|
|
704
|
+
def extend(self, iterable):
|
|
705
|
+
"""Extend deque by appending elements from iterable. O(k)."""
|
|
706
|
+
for item in iterable:
|
|
707
|
+
self.append(item)
|
|
708
|
+
def extendleft(self, iterable):
|
|
709
|
+
"""Extend left side by appending elements from iterable. O(k)."""
|
|
710
|
+
for item in iterable:
|
|
711
|
+
self.appendleft(item)
|
|
712
|
+
def rotate(self, n=1):
|
|
713
|
+
"""Rotate deque n steps to the right. O(k) where k = min(n, len)."""
|
|
714
|
+
if not self:
|
|
715
|
+
return
|
|
716
|
+
super().rotate(n)
|
|
717
|
+
def __contains__(self, item):
|
|
718
|
+
"""Check if item exists in deque. O(1) average."""
|
|
719
|
+
return item in self._counter
|
|
720
|
+
def count(self, item):
|
|
721
|
+
"""Return number of occurrences of item. O(1)."""
|
|
722
|
+
return self._counter[item]
|
|
723
|
+
def __setitem__(self, index, value):
|
|
724
|
+
"""Set item at index. O(1) for access, O(1) for counter update."""
|
|
725
|
+
old_value = self[index]
|
|
726
|
+
super().__setitem__(index, value)
|
|
727
|
+
self.__decrease_count(old_value)
|
|
728
|
+
self._counter[value] += 1
|
|
729
|
+
return old_value
|
|
730
|
+
def __delitem__(self, index):
|
|
731
|
+
"""Delete item at index. O(n) for deletion, O(1) for counter update."""
|
|
732
|
+
value = self[index]
|
|
733
|
+
super().__delitem__(index)
|
|
734
|
+
self.__decrease_count(value)
|
|
735
|
+
return value
|
|
736
|
+
def insert(self, index, value):
|
|
737
|
+
"""Insert value at index. O(n) for insertion, O(1) for counter update."""
|
|
738
|
+
super().insert(index, value)
|
|
739
|
+
self._counter[value] += 1
|
|
740
|
+
def reverse(self):
|
|
741
|
+
"""Reverse deque in place. O(n)."""
|
|
742
|
+
super().reverse()
|
|
743
|
+
def copy(self):
|
|
744
|
+
"""Create a shallow copy. O(n)."""
|
|
745
|
+
new_deque = OrderedMultiSet(maxlen=self.maxlen)
|
|
746
|
+
new_deque.extend(self)
|
|
747
|
+
return new_deque
|
|
748
|
+
def __copy__(self):
|
|
749
|
+
"""Support for copy.copy()."""
|
|
750
|
+
return self.copy()
|
|
751
|
+
def __repr__(self):
|
|
752
|
+
"""String representation."""
|
|
753
|
+
if self.maxlen is not None:
|
|
754
|
+
return f"OrderedMultiSet({list(self)}, maxlen={self.maxlen})"
|
|
755
|
+
return f"OrderedMultiSet({list(self)})"
|
|
756
|
+
def put(self, item,left=False):
|
|
757
|
+
"""Alias for append - add to right end (FIFO put)."""
|
|
758
|
+
return self.append(item,left=left)
|
|
759
|
+
def get(self,left=True):
|
|
760
|
+
"""Alias for popleft - remove from left end (FIFO get)."""
|
|
761
|
+
return self.pop(left=left)
|
|
762
|
+
def peek(self):
|
|
763
|
+
"""Return leftmost item without removing it."""
|
|
764
|
+
if not self:
|
|
765
|
+
return None
|
|
766
|
+
return self[0]
|
|
767
|
+
def peek_right(self):
|
|
768
|
+
"""Return rightmost item without removing it."""
|
|
769
|
+
if not self:
|
|
770
|
+
return None
|
|
771
|
+
return self[-1]
|
|
772
|
+
|
|
773
|
+
def get_terminal_size():
|
|
774
|
+
'''
|
|
775
|
+
Get the terminal size
|
|
776
|
+
|
|
777
|
+
@params:
|
|
778
|
+
None
|
|
779
|
+
|
|
780
|
+
@returns:
|
|
781
|
+
(int,int): the number of columns and rows of the terminal
|
|
782
|
+
'''
|
|
783
|
+
try:
|
|
784
|
+
import os
|
|
785
|
+
_tsize = os.get_terminal_size()
|
|
786
|
+
except Exception:
|
|
787
|
+
try:
|
|
788
|
+
import fcntl
|
|
789
|
+
import struct
|
|
790
|
+
import termios
|
|
791
|
+
packed = fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
|
|
792
|
+
_tsize = struct.unpack('HHHH', packed)[:2]
|
|
793
|
+
except Exception:
|
|
794
|
+
import shutil
|
|
795
|
+
_tsize = shutil.get_terminal_size(fallback=(120, 30))
|
|
796
|
+
return _tsize
|
|
797
|
+
|
|
798
|
+
@cache_decorator
|
|
799
|
+
def get_terminal_color_capability():
|
|
800
|
+
global FORCE_TRUECOLOR
|
|
801
|
+
if not sys.stdout.isatty():
|
|
802
|
+
return 'None'
|
|
803
|
+
term = os.environ.get("TERM", "")
|
|
804
|
+
if term == "dumb":
|
|
805
|
+
return 'None'
|
|
806
|
+
elif term == "linux":
|
|
807
|
+
return '8'
|
|
808
|
+
elif FORCE_TRUECOLOR:
|
|
809
|
+
return '24bit'
|
|
810
|
+
colorterm = os.environ.get("COLORTERM", "")
|
|
811
|
+
if colorterm in ("truecolor", "24bit", "24-bit"):
|
|
812
|
+
return '24bit'
|
|
813
|
+
if term in ("xterm-truecolor", "xterm-24bit", "xterm-kitty", "alacritty", "wezterm", "foot", "terminology"):
|
|
814
|
+
return '24bit'
|
|
815
|
+
elif "256" in term:
|
|
816
|
+
return '256'
|
|
817
|
+
try:
|
|
818
|
+
curses.setupterm()
|
|
819
|
+
colors = curses.tigetnum("colors")
|
|
820
|
+
# tigetnum returns -1 if the capability isn’t defined
|
|
821
|
+
if colors >= 16777216:
|
|
822
|
+
return '24bit'
|
|
823
|
+
elif colors >= 256:
|
|
824
|
+
return '256'
|
|
825
|
+
elif colors >= 16:
|
|
826
|
+
return '16'
|
|
827
|
+
elif colors > 0:
|
|
828
|
+
return '8'
|
|
829
|
+
else:
|
|
830
|
+
return 'None'
|
|
831
|
+
except Exception:
|
|
832
|
+
return 'None'
|
|
833
|
+
|
|
834
|
+
@cache_decorator
|
|
835
|
+
def get_xterm256_palette():
|
|
836
|
+
palette = []
|
|
837
|
+
# 0–15: system colors (we'll just fill with dummy values;
|
|
838
|
+
# you could fill in real RGB if you need to)
|
|
839
|
+
system_colors = [
|
|
840
|
+
(0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0),
|
|
841
|
+
(0, 0, 128), (128, 0, 128), (0, 128, 128), (192, 192, 192),
|
|
842
|
+
(128, 128, 128), (255, 0, 0), (0, 255, 0), (255, 255, 0),
|
|
843
|
+
(0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255),
|
|
844
|
+
]
|
|
845
|
+
palette.extend(system_colors)
|
|
846
|
+
# 16–231: 6x6x6 color cube
|
|
847
|
+
levels = [0, 95, 135, 175, 215, 255]
|
|
848
|
+
for r in levels:
|
|
849
|
+
for g in levels:
|
|
850
|
+
for b in levels:
|
|
851
|
+
palette.append((r, g, b))
|
|
852
|
+
# 232–255: grayscale ramp, 24 steps from 8 to 238
|
|
853
|
+
for i in range(24):
|
|
854
|
+
level = 8 + i * 10
|
|
855
|
+
palette.append((level, level, level))
|
|
856
|
+
return palette
|
|
857
|
+
|
|
858
|
+
@cache_decorator
|
|
859
|
+
def rgb_to_xterm_index(r, g, b):
|
|
860
|
+
"""
|
|
861
|
+
Map 24-bit RGB to nearest xterm-256 color index.
|
|
862
|
+
r, g, b should be in 0-255.
|
|
863
|
+
Returns an int in 0-255.
|
|
864
|
+
"""
|
|
865
|
+
best_index = 0
|
|
866
|
+
best_dist = float('inf')
|
|
867
|
+
for i, (pr, pg, pb) in enumerate(get_xterm256_palette()):
|
|
868
|
+
dr = pr - r
|
|
869
|
+
dg = pg - g
|
|
870
|
+
db = pb - b
|
|
871
|
+
dist = dr*dr + dg*dg + db*db
|
|
872
|
+
if dist < best_dist:
|
|
873
|
+
best_dist = dist
|
|
874
|
+
best_index = i
|
|
875
|
+
return best_index
|
|
876
|
+
|
|
877
|
+
@cache_decorator
|
|
878
|
+
def hashable_to_color(n, brightness_threshold=500):
|
|
879
|
+
hash_value = hash(str(n))
|
|
880
|
+
r = (hash_value >> 16) & 0xFF
|
|
881
|
+
g = (hash_value >> 8) & 0xFF
|
|
882
|
+
b = hash_value & 0xFF
|
|
883
|
+
if (r + g + b) < brightness_threshold:
|
|
884
|
+
return hashable_to_color(hash_value, brightness_threshold)
|
|
885
|
+
return (r, g, b)
|
|
886
|
+
|
|
887
|
+
__previous_ansi_color_index = -1
|
|
888
|
+
@cache_decorator
|
|
889
|
+
def string_to_unique_ansi_color(string):
|
|
890
|
+
'''
|
|
891
|
+
Convert a string to a unique ANSI color code
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
string (str): The string to convert
|
|
895
|
+
|
|
896
|
+
Returns:
|
|
897
|
+
int: The ANSI color code
|
|
898
|
+
'''
|
|
899
|
+
global __previous_ansi_color_index
|
|
900
|
+
# Use a hash function to generate a consistent integer from the string
|
|
901
|
+
color_capability = get_terminal_color_capability()
|
|
902
|
+
index = None
|
|
903
|
+
if color_capability == 'None':
|
|
904
|
+
return ''
|
|
905
|
+
elif color_capability == '16':
|
|
906
|
+
# Map to one of the 14 colors (31-37, 90-96), avoiding black and white
|
|
907
|
+
index = (hash(string) % 14) + 31
|
|
908
|
+
if index > 37:
|
|
909
|
+
index += 52 # Bright colors (90-97)
|
|
910
|
+
elif color_capability == '8':
|
|
911
|
+
index = (hash(string) % 6) + 31
|
|
912
|
+
r,g,b = hashable_to_color(string)
|
|
913
|
+
if color_capability == '256':
|
|
914
|
+
index = rgb_to_xterm_index(r,g,b)
|
|
915
|
+
if index:
|
|
916
|
+
if index == __previous_ansi_color_index:
|
|
917
|
+
return string_to_unique_ansi_color(hash(string))
|
|
918
|
+
__previous_ansi_color_index = index
|
|
919
|
+
if color_capability == '256':
|
|
920
|
+
return f'\033[38;5;{index}m'
|
|
921
|
+
else:
|
|
922
|
+
return f'\033[{index}m'
|
|
923
|
+
else:
|
|
924
|
+
return f'\033[38;2;{r};{g};{b}m'
|
|
925
|
+
|
|
640
926
|
#%% ------------ Compacting Hostnames ----------------
|
|
641
927
|
def __tokenize_hostname(hostname):
|
|
642
928
|
"""
|
|
@@ -1323,6 +1609,11 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
|
|
|
1323
1609
|
global __ipmiiInterfaceIPPrefix
|
|
1324
1610
|
global _binPaths
|
|
1325
1611
|
global __DEBUG_MODE
|
|
1612
|
+
global DEFAULT_IPMI_USERNAME
|
|
1613
|
+
global DEFAULT_IPMI_PASSWORD
|
|
1614
|
+
global DEFAULT_USERNAME
|
|
1615
|
+
global DEFAULT_PASSWORD
|
|
1616
|
+
global SSH_STRICT_HOST_KEY_CHECKING
|
|
1326
1617
|
if retry_limit < 0:
|
|
1327
1618
|
host.output.append('Error: Retry limit reached!')
|
|
1328
1619
|
host.stderr.append('Error: Retry limit reached!')
|
|
@@ -1366,7 +1657,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
|
|
|
1366
1657
|
host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
|
|
1367
1658
|
host.resolvedName = host.username + '@' if host.username else ''
|
|
1368
1659
|
host.resolvedName += host.address
|
|
1369
|
-
except:
|
|
1660
|
+
except Exception:
|
|
1370
1661
|
host.resolvedName = host.name
|
|
1371
1662
|
else:
|
|
1372
1663
|
host.resolvedName = host.name
|
|
@@ -1378,22 +1669,27 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
|
|
|
1378
1669
|
host.command = host.command.replace('ipmitool ','')
|
|
1379
1670
|
elif host.command.startswith(_binPaths['ipmitool']):
|
|
1380
1671
|
host.command = host.command.replace(_binPaths['ipmitool'],'')
|
|
1381
|
-
if not host.username:
|
|
1382
|
-
|
|
1672
|
+
if not host.username or host.username == DEFAULT_USERNAME:
|
|
1673
|
+
if DEFAULT_IPMI_USERNAME:
|
|
1674
|
+
host.username = DEFAULT_IPMI_USERNAME
|
|
1675
|
+
elif DEFAULT_USERNAME:
|
|
1676
|
+
host.username = DEFAULT_USERNAME
|
|
1677
|
+
else:
|
|
1678
|
+
host.username = 'ADMIN'
|
|
1679
|
+
if not passwds or passwds == DEFAULT_PASSWORD:
|
|
1680
|
+
if DEFAULT_IPMI_PASSWORD:
|
|
1681
|
+
passwds = DEFAULT_IPMI_PASSWORD
|
|
1682
|
+
elif DEFAULT_PASSWORD:
|
|
1683
|
+
passwds = DEFAULT_PASSWORD
|
|
1684
|
+
else:
|
|
1685
|
+
host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
|
|
1686
|
+
passwds = 'admin'
|
|
1383
1687
|
if not host.command:
|
|
1384
1688
|
host.command = 'power status'
|
|
1385
1689
|
if 'sh' in _binPaths:
|
|
1386
|
-
|
|
1387
|
-
formatedCMD = [_binPaths['sh'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
|
|
1388
|
-
else:
|
|
1389
|
-
host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
|
|
1390
|
-
formatedCMD = [_binPaths['sh'],'-c',f'ipmitool -H {host.address} -U {host.username} -P admin {" ".join(extraargs)} {host.command}']
|
|
1690
|
+
formatedCMD = [_binPaths['sh'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
|
|
1391
1691
|
else:
|
|
1392
|
-
|
|
1393
|
-
formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P {passwds}'] + extraargs + [host.command]
|
|
1394
|
-
else:
|
|
1395
|
-
host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
|
|
1396
|
-
formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P admin'] + extraargs + [host.command]
|
|
1692
|
+
formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P {passwds}'] + extraargs + [host.command]
|
|
1397
1693
|
elif 'ssh' in _binPaths:
|
|
1398
1694
|
host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
|
|
1399
1695
|
if __DEBUG_MODE:
|
|
@@ -1544,7 +1840,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
|
|
|
1544
1840
|
stderr_thread.join(timeout=1)
|
|
1545
1841
|
stdin_thread.join(timeout=1)
|
|
1546
1842
|
# here we handle the rest of the stdout after the subprocess returns
|
|
1547
|
-
host.output.append(
|
|
1843
|
+
host.output.append('Pipe Closed. Trying to read the rest of the stdout...')
|
|
1548
1844
|
if not _emo:
|
|
1549
1845
|
stdout = None
|
|
1550
1846
|
stderr = None
|
|
@@ -2262,7 +2558,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
2262
2558
|
# if the line is visible, we will reprint it
|
|
2263
2559
|
if visibleLowerBound <= lineNumToReprint <= len(host.output):
|
|
2264
2560
|
_curses_add_string_to_window(window=host_window, y=lineNumToReprint + 1, line=host.output[lineNumToReprint], color_pair_list=host.current_color_pair,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color,fill_char='')
|
|
2265
|
-
except Exception
|
|
2561
|
+
except Exception:
|
|
2266
2562
|
# import traceback
|
|
2267
2563
|
# print(str(e).strip())
|
|
2268
2564
|
# print(traceback.format_exc().strip())
|
|
@@ -2330,7 +2626,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
2330
2626
|
# print if can change color
|
|
2331
2627
|
_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)
|
|
2332
2628
|
stdscr.refresh()
|
|
2333
|
-
except:
|
|
2629
|
+
except Exception:
|
|
2334
2630
|
pass
|
|
2335
2631
|
params = (-1,0 , min_char_len, min_line_len, single_window,False,'new config')
|
|
2336
2632
|
while params:
|
|
@@ -2351,13 +2647,177 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
2351
2647
|
stdscr.addstr(i, 0, line)
|
|
2352
2648
|
i += 1
|
|
2353
2649
|
stdscr.refresh()
|
|
2354
|
-
except:
|
|
2650
|
+
except Exception:
|
|
2355
2651
|
pass
|
|
2356
2652
|
params = params[:6] + ('new config',)
|
|
2357
2653
|
time.sleep(0.01)
|
|
2358
2654
|
#time.sleep(0.25)
|
|
2359
2655
|
|
|
2360
2656
|
#%% ------------ Generate Output Block ----------------
|
|
2657
|
+
def can_merge(line_bag1, line_bag2, threshold):
|
|
2658
|
+
bag1_iter = iter(line_bag1)
|
|
2659
|
+
found = False
|
|
2660
|
+
for _ in range(max(int(len(line_bag1) * (1-threshold)),1)):
|
|
2661
|
+
try:
|
|
2662
|
+
item = next(bag1_iter)
|
|
2663
|
+
except StopIteration:
|
|
2664
|
+
break
|
|
2665
|
+
if item in line_bag2:
|
|
2666
|
+
found = True
|
|
2667
|
+
break
|
|
2668
|
+
if not found:
|
|
2669
|
+
return False
|
|
2670
|
+
return len(line_bag1.symmetric_difference(line_bag2)) < max((len(line_bag1) + len(line_bag2)) * (1 - threshold),1)
|
|
2671
|
+
|
|
2672
|
+
def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_threshold,line_length):
|
|
2673
|
+
indexes = {hostname: 0 for hostname in merging_hostnames}
|
|
2674
|
+
working_indexes = indexes.copy()
|
|
2675
|
+
previousBuddies = set()
|
|
2676
|
+
hostnameWrapper = textwrap.TextWrapper(width=line_length - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent='├─ ', subsequent_indent='│- ')
|
|
2677
|
+
hostnameWrapper.wordsep_simple_re = re.compile(r'([,]+)')
|
|
2678
|
+
while indexes:
|
|
2679
|
+
futures = {}
|
|
2680
|
+
defer = False
|
|
2681
|
+
sorted_working_indexes = sorted(working_indexes.items(), key=lambda x: x[1])
|
|
2682
|
+
golden_hostname, golden_index = sorted_working_indexes[0]
|
|
2683
|
+
buddy = {golden_hostname}
|
|
2684
|
+
lineToAdd = outputs_by_hostname[golden_hostname][golden_index]
|
|
2685
|
+
for hostname, index in sorted_working_indexes[1:]:
|
|
2686
|
+
if lineToAdd == outputs_by_hostname[hostname][index]:
|
|
2687
|
+
buddy.add(hostname)
|
|
2688
|
+
else:
|
|
2689
|
+
if hostname not in futures:
|
|
2690
|
+
diff_display_item_count = max(int(len(outputs_by_hostname[hostname]) * (1 - diff_display_threshold)),1)
|
|
2691
|
+
tracking_index = min(index + diff_display_item_count,len(outputs_by_hostname[hostname]))
|
|
2692
|
+
futures[hostname] = (OrderedMultiSet(outputs_by_hostname[hostname][index:tracking_index],maxlen=diff_display_item_count),tracking_index)
|
|
2693
|
+
if lineToAdd in futures[hostname]:
|
|
2694
|
+
for hn in buddy:
|
|
2695
|
+
del working_indexes[hn]
|
|
2696
|
+
defer = True
|
|
2697
|
+
break
|
|
2698
|
+
if not defer:
|
|
2699
|
+
if buddy != previousBuddies:
|
|
2700
|
+
hostnameStr = ','.join(compact_hostnames(buddy))
|
|
2701
|
+
hostnameLines = hostnameWrapper.wrap(hostnameStr)
|
|
2702
|
+
hostnameLines = [line.ljust(line_length - 1) + '│' for line in hostnameLines]
|
|
2703
|
+
color = string_to_unique_ansi_color(hostnameStr) if len(buddy) < len(merging_hostnames) else ''
|
|
2704
|
+
hostnameLines[0] = f"\033[0m{color}{hostnameLines[0]}"
|
|
2705
|
+
output.extend(hostnameLines)
|
|
2706
|
+
previousBuddies = buddy
|
|
2707
|
+
output.append(lineToAdd.ljust(line_length - 1) + '│')
|
|
2708
|
+
for hostname in buddy:
|
|
2709
|
+
indexes[hostname] += 1
|
|
2710
|
+
if indexes[hostname] >= len(outputs_by_hostname[hostname]):
|
|
2711
|
+
indexes.pop(hostname, None)
|
|
2712
|
+
futures.pop(hostname, None)
|
|
2713
|
+
continue
|
|
2714
|
+
#advance futures
|
|
2715
|
+
if hostname in futures:
|
|
2716
|
+
tracking_multiset, tracking_index = futures[hostname]
|
|
2717
|
+
tracking_index += 1
|
|
2718
|
+
if tracking_index < len(outputs_by_hostname[hostname]):
|
|
2719
|
+
line = outputs_by_hostname[hostname][tracking_index]
|
|
2720
|
+
tracking_multiset.append(line)
|
|
2721
|
+
else:
|
|
2722
|
+
tracking_multiset.pop_left()
|
|
2723
|
+
futures[hostname] = (tracking_multiset, tracking_index)
|
|
2724
|
+
working_indexes = indexes.copy()
|
|
2725
|
+
|
|
2726
|
+
def mergeOutputs(outputs_by_hostname, merge_groups, remaining_hostnames, diff_display_threshold, line_length):
|
|
2727
|
+
output = []
|
|
2728
|
+
output.append(('┌'+'─'*(line_length-2) + '┐'))
|
|
2729
|
+
for merging_hostnames in merge_groups:
|
|
2730
|
+
mergeOutput(merging_hostnames, outputs_by_hostname, output, diff_display_threshold,line_length)
|
|
2731
|
+
output.append('\033[0m├'+'─'*(line_length-2) + '┤')
|
|
2732
|
+
for hostname in remaining_hostnames:
|
|
2733
|
+
hostnameLines = textwrap.wrap(hostname, width=line_length-1, tabsize=4, replace_whitespace=False, drop_whitespace=False,
|
|
2734
|
+
initial_indent='├─ ', subsequent_indent='│- ')
|
|
2735
|
+
output.extend(line.ljust(line_length - 1) + '│' for line in hostnameLines)
|
|
2736
|
+
output.extend(line.ljust(line_length - 1) + '│' for line in outputs_by_hostname[hostname])
|
|
2737
|
+
output.append('\033[0m├'+'─'*(line_length-2) + '┤')
|
|
2738
|
+
if output:
|
|
2739
|
+
output.pop()
|
|
2740
|
+
# if output and output[0] and output[0].startswith('├'):
|
|
2741
|
+
# output[0] = '┌' + output[0][1:]
|
|
2742
|
+
return output
|
|
2743
|
+
|
|
2744
|
+
def get_host_raw_output(hosts, terminal_width):
|
|
2745
|
+
outputs_by_hostname = {}
|
|
2746
|
+
line_bag_by_hostname = {}
|
|
2747
|
+
hostnames_by_line_bag_len = {}
|
|
2748
|
+
text_wrapper = textwrap.TextWrapper(width=terminal_width - 2, tabsize=4, replace_whitespace=False, drop_whitespace=False,
|
|
2749
|
+
initial_indent='│ ', subsequent_indent='│-')
|
|
2750
|
+
max_length = 20
|
|
2751
|
+
for host in hosts:
|
|
2752
|
+
hostPrintOut = ["│█ EXECUTED COMMAND:"]
|
|
2753
|
+
for line in host['command'].splitlines():
|
|
2754
|
+
hostPrintOut.extend(text_wrapper.wrap(line))
|
|
2755
|
+
lineBag = {(0,host['command'])}
|
|
2756
|
+
prevLine = host['command']
|
|
2757
|
+
if host['stdout']:
|
|
2758
|
+
hostPrintOut.append('│▓ STDOUT:')
|
|
2759
|
+
for line in host['stdout']:
|
|
2760
|
+
hostPrintOut.extend(text_wrapper.wrap(line))
|
|
2761
|
+
lineBag.add((prevLine,1))
|
|
2762
|
+
lineBag.add((1,host['stdout'][0]))
|
|
2763
|
+
if len(host['stdout']) > 1:
|
|
2764
|
+
lineBag.update(zip(host['stdout'], host['stdout'][1:]))
|
|
2765
|
+
lineBag.update(host['stdout'])
|
|
2766
|
+
prevLine = host['stdout'][-1]
|
|
2767
|
+
if host['stderr']:
|
|
2768
|
+
if host['stderr'][0].strip().startswith('ssh: connect to host ') and host['stderr'][0].strip().endswith('Connection refused'):
|
|
2769
|
+
host['stderr'][0] = 'SSH not reachable!'
|
|
2770
|
+
elif host['stderr'][-1].strip().endswith('Connection timed out'):
|
|
2771
|
+
host['stderr'][-1] = 'SSH connection timed out!'
|
|
2772
|
+
elif host['stderr'][-1].strip().endswith('No route to host'):
|
|
2773
|
+
host['stderr'][-1] = 'Cannot find host!'
|
|
2774
|
+
if host['stderr']:
|
|
2775
|
+
hostPrintOut.append('│▒ STDERR:')
|
|
2776
|
+
for line in host['stderr']:
|
|
2777
|
+
hostPrintOut.extend(text_wrapper.wrap(line))
|
|
2778
|
+
lineBag.add((prevLine,2))
|
|
2779
|
+
lineBag.add((2,host['stderr'][0]))
|
|
2780
|
+
lineBag.update(host['stderr'])
|
|
2781
|
+
if len(host['stderr']) > 1:
|
|
2782
|
+
lineBag.update(zip(host['stderr'], host['stderr'][1:]))
|
|
2783
|
+
prevLine = host['stderr'][-1]
|
|
2784
|
+
hostPrintOut.append(f"│░ RETURN CODE: {host['returncode']}")
|
|
2785
|
+
lineBag.add((prevLine,f"{host['returncode']}"))
|
|
2786
|
+
max_length = max(max_length, max(map(len, hostPrintOut)))
|
|
2787
|
+
outputs_by_hostname[host['name']] = hostPrintOut
|
|
2788
|
+
line_bag_by_hostname[host['name']] = lineBag
|
|
2789
|
+
hostnames_by_line_bag_len.setdefault(len(lineBag), set()).add(host['name'])
|
|
2790
|
+
return outputs_by_hostname, line_bag_by_hostname, hostnames_by_line_bag_len, sorted(hostnames_by_line_bag_len), min(max_length+2,terminal_width)
|
|
2791
|
+
|
|
2792
|
+
def form_merge_groups(hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_len_keys, line_bag_by_hostname, diff_display_threshold):
|
|
2793
|
+
merge_groups = []
|
|
2794
|
+
for line_bag_len in hostnames_by_line_bag_len.copy():
|
|
2795
|
+
for this_hostname in hostnames_by_line_bag_len.get(line_bag_len, set()).copy():
|
|
2796
|
+
if this_hostname not in hostnames_by_line_bag_len.get(line_bag_len, set()):
|
|
2797
|
+
continue
|
|
2798
|
+
this_line_bag = line_bag_by_hostname[this_hostname]
|
|
2799
|
+
target_threshold = line_bag_len * (2 - diff_display_threshold)
|
|
2800
|
+
merge_group = []
|
|
2801
|
+
for other_line_bag_len in sorted_hostnames_by_line_bag_len_keys:
|
|
2802
|
+
if other_line_bag_len > target_threshold:
|
|
2803
|
+
break
|
|
2804
|
+
if other_line_bag_len < line_bag_len:
|
|
2805
|
+
continue
|
|
2806
|
+
for other_hostname in hostnames_by_line_bag_len.get(other_line_bag_len, set()).copy():
|
|
2807
|
+
if this_hostname == other_hostname:
|
|
2808
|
+
continue
|
|
2809
|
+
if can_merge(this_line_bag, line_bag_by_hostname[other_hostname], diff_display_threshold):
|
|
2810
|
+
merge_group.append(other_hostname)
|
|
2811
|
+
hostnames_by_line_bag_len.get(line_bag_len, set()).discard(this_hostname)
|
|
2812
|
+
hostnames_by_line_bag_len[other_line_bag_len].remove(other_hostname)
|
|
2813
|
+
if not hostnames_by_line_bag_len[other_line_bag_len]:
|
|
2814
|
+
del hostnames_by_line_bag_len[other_line_bag_len]
|
|
2815
|
+
del line_bag_by_hostname[other_hostname]
|
|
2816
|
+
if merge_group:
|
|
2817
|
+
merge_group.append(this_hostname)
|
|
2818
|
+
merge_groups.append(merge_group)
|
|
2819
|
+
return merge_groups
|
|
2820
|
+
|
|
2361
2821
|
def generate_output(hosts, usejson = False, greppable = False,quiet = False,encoding = _encoding,keyPressesIn = [[]]):
|
|
2362
2822
|
if quiet:
|
|
2363
2823
|
# remove hosts with returncode 0
|
|
@@ -2396,40 +2856,34 @@ def generate_output(hosts, usejson = False, greppable = False,quiet = False,enco
|
|
|
2396
2856
|
rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
|
|
2397
2857
|
#rtnStr += '\n'
|
|
2398
2858
|
else:
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
for output, hostSet in outputs.items():
|
|
2415
|
-
compact_hosts = compact_hostnames(hostSet)
|
|
2416
|
-
rtnStr += '*'*80+'\n'
|
|
2417
|
-
if quiet:
|
|
2418
|
-
rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
|
|
2419
|
-
rtnStr += output+'\n'
|
|
2420
|
-
else:
|
|
2421
|
-
rtnStr += f'These hosts: "{",".join(compact_hosts)}" have a response of:\n'
|
|
2422
|
-
rtnStr += output+'\n'
|
|
2423
|
-
if not quiet or outputs:
|
|
2424
|
-
rtnStr += '*'*80+'\n'
|
|
2859
|
+
try:
|
|
2860
|
+
diff_display_threshold = float(DEFAULT_DIFF_DISPLAY_THRESHOLD)
|
|
2861
|
+
if diff_display_threshold < 0 or diff_display_threshold > 1:
|
|
2862
|
+
raise ValueError
|
|
2863
|
+
except Exception:
|
|
2864
|
+
eprint("Warning: diff_display_threshold should be a float between 0 and 1. Setting to default value of 0.9")
|
|
2865
|
+
diff_display_threshold = 0.9
|
|
2866
|
+
terminal_length = get_terminal_size()[0]
|
|
2867
|
+
outputs_by_hostname, line_bag_by_hostname, hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_len_keys, line_length = get_host_raw_output(hosts,terminal_length)
|
|
2868
|
+
merge_groups = form_merge_groups(hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_len_keys, line_bag_by_hostname, diff_display_threshold)
|
|
2869
|
+
# get the remaining hostnames in the hostnames_by_line_bag_len
|
|
2870
|
+
remaining_hostnames = set()
|
|
2871
|
+
for hostnames in hostnames_by_line_bag_len.values():
|
|
2872
|
+
remaining_hostnames.update(hostnames)
|
|
2873
|
+
outputs = mergeOutputs(outputs_by_hostname, merge_groups,remaining_hostnames, diff_display_threshold,line_length)
|
|
2425
2874
|
if keyPressesIn[-1]:
|
|
2426
2875
|
CMDsOut = [''.join(cmd).encode(encoding=encoding,errors='backslashreplace').decode(encoding=encoding,errors='backslashreplace').replace('\\n', '↵') for cmd in keyPressesIn if cmd]
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2876
|
+
outputs.append("├─ User Inputs:".ljust(line_length -1,'─')+'┤')
|
|
2877
|
+
cmdOut = []
|
|
2878
|
+
for line in CMDsOut:
|
|
2879
|
+
cmdOut.extend(textwrap.wrap(line, width=line_length-1, tabsize=4, replace_whitespace=False, drop_whitespace=False,
|
|
2880
|
+
initial_indent='│ ', subsequent_indent='│-'))
|
|
2881
|
+
outputs.extend(cmd.ljust(line_length -1)+'│' for cmd in cmdOut)
|
|
2430
2882
|
keyPressesIn[-1].clear()
|
|
2431
2883
|
if quiet and not outputs:
|
|
2432
|
-
rtnStr
|
|
2884
|
+
rtnStr = 'Success'
|
|
2885
|
+
else:
|
|
2886
|
+
rtnStr = '\n'.join(outputs + [('\033[0m└'+'─'*(line_length-2)+'┘')])
|
|
2433
2887
|
return rtnStr
|
|
2434
2888
|
|
|
2435
2889
|
def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
@@ -2487,8 +2941,8 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
2487
2941
|
availableHosts = set()
|
|
2488
2942
|
for host in hosts:
|
|
2489
2943
|
if host.stderr and ('No route to host' in host.stderr[0].strip() or 'Connection timed out' in host.stderr[0].strip() or (host.stderr[-1].strip().startswith('Timeout!') and host.returncode == 124)):
|
|
2490
|
-
unavailableHosts[host.name] = int(time.monotonic())
|
|
2491
|
-
__globalUnavailableHosts[host.name] = int(time.monotonic())
|
|
2944
|
+
unavailableHosts[host.name] = int(time.monotonic() + unavailable_host_expiry)
|
|
2945
|
+
__globalUnavailableHosts[host.name] = int(time.monotonic() + unavailable_host_expiry)
|
|
2492
2946
|
else:
|
|
2493
2947
|
availableHosts.add(host.name)
|
|
2494
2948
|
if host.name in unavailableHosts:
|
|
@@ -2513,7 +2967,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
2513
2967
|
expireTime = int(line.split(',')[1])
|
|
2514
2968
|
if expireTime < time.monotonic() and hostname not in availableHosts:
|
|
2515
2969
|
oldDic[hostname] = expireTime
|
|
2516
|
-
except:
|
|
2970
|
+
except Exception:
|
|
2517
2971
|
pass
|
|
2518
2972
|
# add new entries
|
|
2519
2973
|
oldDic.update(unavailableHosts)
|
|
@@ -2567,33 +3021,60 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
2567
3021
|
repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
|
|
2568
3022
|
shortend = False) -> str:
|
|
2569
3023
|
argsList = []
|
|
2570
|
-
if oneonone:
|
|
2571
|
-
|
|
2572
|
-
if
|
|
2573
|
-
|
|
2574
|
-
if
|
|
2575
|
-
|
|
2576
|
-
if
|
|
2577
|
-
|
|
2578
|
-
if
|
|
2579
|
-
|
|
2580
|
-
if
|
|
2581
|
-
|
|
2582
|
-
if
|
|
2583
|
-
|
|
2584
|
-
if
|
|
2585
|
-
|
|
2586
|
-
if
|
|
2587
|
-
|
|
2588
|
-
if
|
|
2589
|
-
|
|
2590
|
-
if
|
|
2591
|
-
|
|
2592
|
-
if
|
|
2593
|
-
|
|
2594
|
-
if
|
|
2595
|
-
|
|
2596
|
-
if
|
|
3024
|
+
if oneonone:
|
|
3025
|
+
argsList.append('--oneonone' if not shortend else '-11')
|
|
3026
|
+
if timeout and timeout != DEFAULT_TIMEOUT:
|
|
3027
|
+
argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
|
|
3028
|
+
if repeat and repeat != DEFAULT_REPEAT:
|
|
3029
|
+
argsList.append(f'--repeat={repeat}' if not shortend else f'-r={repeat}')
|
|
3030
|
+
if interval and interval != DEFAULT_INTERVAL:
|
|
3031
|
+
argsList.append(f'--interval={interval}' if not shortend else f'-i={interval}')
|
|
3032
|
+
if password and password != DEFAULT_PASSWORD:
|
|
3033
|
+
argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
|
|
3034
|
+
if identity_file and identity_file != DEFAULT_IDENTITY_FILE:
|
|
3035
|
+
argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
|
|
3036
|
+
if copy_id:
|
|
3037
|
+
argsList.append('--copy_id' if not shortend else '-ci')
|
|
3038
|
+
if no_watch:
|
|
3039
|
+
argsList.append('--no_watch' if not shortend else '-q')
|
|
3040
|
+
if json:
|
|
3041
|
+
argsList.append('--json' if not shortend else '-j')
|
|
3042
|
+
if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS:
|
|
3043
|
+
argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
|
|
3044
|
+
if files:
|
|
3045
|
+
argsList.extend([f'--file="{file}"' for file in files] if not shortend else [f'-f="{file}"' for file in files])
|
|
3046
|
+
if ipmi:
|
|
3047
|
+
argsList.append('--ipmi')
|
|
3048
|
+
if interface_ip_prefix and interface_ip_prefix != DEFAULT_INTERFACE_IP_PREFIX:
|
|
3049
|
+
argsList.append(f'--interface_ip_prefix="{interface_ip_prefix}"' if not shortend else f'-pre="{interface_ip_prefix}"')
|
|
3050
|
+
if scp:
|
|
3051
|
+
argsList.append('--scp')
|
|
3052
|
+
if gather_mode:
|
|
3053
|
+
argsList.append('--gather_mode' if not shortend else '-gm')
|
|
3054
|
+
if username and username != DEFAULT_USERNAME:
|
|
3055
|
+
argsList.append(f'--username="{username}"' if not shortend else f'-u="{username}"')
|
|
3056
|
+
if extraargs and extraargs != DEFAULT_EXTRA_ARGS:
|
|
3057
|
+
argsList.append(f'--extraargs="{extraargs}"' if not shortend else f'-ea="{extraargs}"')
|
|
3058
|
+
if skipUnreachable:
|
|
3059
|
+
argsList.append('--skip_unreachable' if not shortend else '-su')
|
|
3060
|
+
if unavailable_host_expiry and unavailable_host_expiry != DEFAULT_UNAVAILABLE_HOST_EXPIRY:
|
|
3061
|
+
argsList.append(f'--unavailable_host_expiry={unavailable_host_expiry}' if not shortend else f'-uhe={unavailable_host_expiry}')
|
|
3062
|
+
if no_env:
|
|
3063
|
+
argsList.append('--no_env')
|
|
3064
|
+
if env_file and env_file != DEFAULT_ENV_FILE:
|
|
3065
|
+
argsList.append(f'--env_file="{env_file}"' if not shortend else f'-ef="{env_file}"')
|
|
3066
|
+
if no_history:
|
|
3067
|
+
argsList.append('--no_history' if not shortend else '-nh')
|
|
3068
|
+
if history_file and history_file != DEFAULT_HISTORY_FILE:
|
|
3069
|
+
argsList.append(f'--history_file="{history_file}"' if not shortend else f'-hf="{history_file}"')
|
|
3070
|
+
if greppable:
|
|
3071
|
+
argsList.append('--greppable' if not shortend else '-g')
|
|
3072
|
+
if error_only:
|
|
3073
|
+
argsList.append('--error_only' if not shortend else '-eo')
|
|
3074
|
+
if skip_hosts and skip_hosts != DEFAULT_SKIP_HOSTS:
|
|
3075
|
+
argsList.append(f'--skip_hosts="{skip_hosts}"' if not shortend else f'-sh="{skip_hosts}"')
|
|
3076
|
+
if file_sync:
|
|
3077
|
+
argsList.append('--file_sync' if not shortend else '-fs')
|
|
2597
3078
|
return ' '.join(argsList)
|
|
2598
3079
|
|
|
2599
3080
|
def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
@@ -2753,7 +3234,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2753
3234
|
if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
|
|
2754
3235
|
hostname = line.split(',')[0]
|
|
2755
3236
|
expireTime = int(line.split(',')[1])
|
|
2756
|
-
if expireTime
|
|
3237
|
+
if expireTime > time.monotonic():
|
|
2757
3238
|
__globalUnavailableHosts[hostname] = expireTime
|
|
2758
3239
|
readed = True
|
|
2759
3240
|
if readed and not __global_suppress_printout:
|
|
@@ -2762,7 +3243,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2762
3243
|
eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')!r}")
|
|
2763
3244
|
eprint(str(e))
|
|
2764
3245
|
elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
|
|
2765
|
-
__globalUnavailableHosts.update({host: int(time.monotonic()) for host in readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(',') if host})
|
|
3246
|
+
__globalUnavailableHosts.update({host: int(time.monotonic()+ unavailable_host_expiry) for host in readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(',') if host})
|
|
2766
3247
|
if not max_connections:
|
|
2767
3248
|
max_connections = 4 * os.cpu_count()
|
|
2768
3249
|
elif max_connections == 0:
|
|
@@ -2834,7 +3315,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2834
3315
|
# we will copy the id to the hosts
|
|
2835
3316
|
hosts = []
|
|
2836
3317
|
for host in targetHostDic:
|
|
2837
|
-
if host in skipHostSet or targetHostDic[host] in skipHostSet:
|
|
3318
|
+
if host in skipHostSet or targetHostDic[host] in skipHostSet:
|
|
3319
|
+
continue
|
|
2838
3320
|
command = f"{_binPaths['ssh-copy-id']} "
|
|
2839
3321
|
if identity_file:
|
|
2840
3322
|
command = f"{command}-i {identity_file} "
|
|
@@ -2870,7 +3352,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2870
3352
|
for file in files:
|
|
2871
3353
|
try:
|
|
2872
3354
|
pathSet.update(glob.glob(file,include_hidden=True,recursive=True))
|
|
2873
|
-
except:
|
|
3355
|
+
except Exception:
|
|
2874
3356
|
pathSet.update(glob.glob(file,recursive=True))
|
|
2875
3357
|
if not pathSet:
|
|
2876
3358
|
_exit_with_code(66, f'No source files at {files!r} are found after resolving globs!')
|
|
@@ -2895,17 +3377,19 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2895
3377
|
eprint('-'*80)
|
|
2896
3378
|
eprint("Running in one on one mode")
|
|
2897
3379
|
for host, command in zip(targetHostDic, commands):
|
|
2898
|
-
if not ipmi and skipUnreachable and host in unavailableHosts:
|
|
3380
|
+
if not ipmi and skipUnreachable and host in unavailableHosts and unavailableHosts[host] > time.monotonic():
|
|
2899
3381
|
eprint(f"Skipping unavailable host: {host}")
|
|
2900
3382
|
continue
|
|
2901
|
-
if host in skipHostSet or targetHostDic[host] in skipHostSet:
|
|
3383
|
+
if host in skipHostSet or targetHostDic[host] in skipHostSet:
|
|
3384
|
+
continue
|
|
2902
3385
|
if file_sync:
|
|
2903
3386
|
hosts.append(Host(host, os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip = targetHostDic[host]))
|
|
2904
3387
|
else:
|
|
2905
3388
|
hosts.append(Host(host, command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip=targetHostDic[host]))
|
|
2906
3389
|
if not __global_suppress_printout:
|
|
2907
3390
|
eprint(f"Running command: {command!r} on host: {host!r}")
|
|
2908
|
-
if not __global_suppress_printout:
|
|
3391
|
+
if not __global_suppress_printout:
|
|
3392
|
+
eprint('-'*80)
|
|
2909
3393
|
if not no_start:
|
|
2910
3394
|
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2911
3395
|
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
@@ -2919,15 +3403,17 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2919
3403
|
# run in interactive mode ssh mode
|
|
2920
3404
|
hosts = []
|
|
2921
3405
|
for host in targetHostDic:
|
|
2922
|
-
if not ipmi and skipUnreachable and host in unavailableHosts:
|
|
2923
|
-
if not __global_suppress_printout:
|
|
3406
|
+
if not ipmi and skipUnreachable and host in unavailableHosts and unavailableHosts[host] > time.monotonic():
|
|
3407
|
+
if not __global_suppress_printout:
|
|
3408
|
+
print(f"Skipping unavailable host: {host}")
|
|
3409
|
+
continue
|
|
3410
|
+
if host in skipHostSet or targetHostDic[host] in skipHostSet:
|
|
2924
3411
|
continue
|
|
2925
|
-
if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
|
|
2926
3412
|
if file_sync:
|
|
2927
|
-
eprint(
|
|
3413
|
+
eprint("Error: file sync mode need to be specified with at least one path to sync.")
|
|
2928
3414
|
return []
|
|
2929
3415
|
elif files:
|
|
2930
|
-
eprint(
|
|
3416
|
+
eprint("Error: files need to be specified with at least one path to sync")
|
|
2931
3417
|
else:
|
|
2932
3418
|
hosts.append(Host(host, '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,identity_file=identity_file,ip=targetHostDic[host]))
|
|
2933
3419
|
if not __global_suppress_printout:
|
|
@@ -2935,7 +3421,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2935
3421
|
eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
2936
3422
|
eprint('-'*80)
|
|
2937
3423
|
if no_start:
|
|
2938
|
-
eprint(
|
|
3424
|
+
eprint("Warning: no_start is set, the command will not be started. As we are in interactive mode, no action will be done.")
|
|
2939
3425
|
else:
|
|
2940
3426
|
processRunOnHosts(timeout=timeout, password=password, max_connections=max_connections, hosts=hosts,
|
|
2941
3427
|
returnUnfinished=returnUnfinished, no_watch=no_watch, json=json, called=called, greppable=greppable,
|
|
@@ -2946,10 +3432,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2946
3432
|
for command in commands:
|
|
2947
3433
|
hosts = []
|
|
2948
3434
|
for host in targetHostDic:
|
|
2949
|
-
if not ipmi and skipUnreachable and host in unavailableHosts:
|
|
2950
|
-
if not __global_suppress_printout:
|
|
3435
|
+
if not ipmi and skipUnreachable and host in unavailableHosts and unavailableHosts[host] > time.monotonic():
|
|
3436
|
+
if not __global_suppress_printout:
|
|
3437
|
+
print(f"Skipping unavailable host: {host}")
|
|
3438
|
+
continue
|
|
3439
|
+
if host in skipHostSet or targetHostDic[host] in skipHostSet:
|
|
2951
3440
|
continue
|
|
2952
|
-
if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
|
|
2953
3441
|
if file_sync:
|
|
2954
3442
|
hosts.append(Host(host, os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip=targetHostDic[host]))
|
|
2955
3443
|
else:
|
|
@@ -2999,6 +3487,8 @@ def generate_default_config(args):
|
|
|
2999
3487
|
'DEFAULT_IPMI': args.ipmi,
|
|
3000
3488
|
'DEFAULT_IPMI_INTERFACE_IP_PREFIX': args.ipmi_interface_ip_prefix,
|
|
3001
3489
|
'DEFAULT_INTERFACE_IP_PREFIX': args.interface_ip_prefix,
|
|
3490
|
+
'DEFAULT_IPMI_USERNAME': args.ipmi_username,
|
|
3491
|
+
'DEFAULT_IPMI_PASSWORD': args.ipmi_password,
|
|
3002
3492
|
'DEFAULT_NO_WATCH': args.no_watch,
|
|
3003
3493
|
'DEFAULT_CURSES_MINIMUM_CHAR_LEN': args.window_width,
|
|
3004
3494
|
'DEFAULT_CURSES_MINIMUM_LINE_LEN': args.window_height,
|
|
@@ -3017,8 +3507,10 @@ def generate_default_config(args):
|
|
|
3017
3507
|
'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
|
|
3018
3508
|
'DEFAULT_SKIP_HOSTS': args.skip_hosts,
|
|
3019
3509
|
'DEFAULT_ENCODING': args.encoding,
|
|
3510
|
+
'DEFAULT_DIFF_DISPLAY_THRESHOLD': args.diff_display_threshold,
|
|
3020
3511
|
'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
|
|
3021
3512
|
'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
|
|
3513
|
+
'FORCE_TRUECOLOR': args.force_truecolor,
|
|
3022
3514
|
}
|
|
3023
3515
|
|
|
3024
3516
|
def write_default_config(args,CONFIG_FILE = None):
|
|
@@ -3031,9 +3523,9 @@ def write_default_config(args,CONFIG_FILE = None):
|
|
|
3031
3523
|
backup = True
|
|
3032
3524
|
if os.path.exists(CONFIG_FILE):
|
|
3033
3525
|
eprint(f"Warning: {CONFIG_FILE!r} already exists, what to do? (o/b/n)")
|
|
3034
|
-
eprint(
|
|
3526
|
+
eprint("o: Overwrite the file")
|
|
3035
3527
|
eprint(f"b: Rename the current config file at {CONFIG_FILE!r}.bak forcefully and write the new config file (default)")
|
|
3036
|
-
eprint(
|
|
3528
|
+
eprint("n: Do nothing")
|
|
3037
3529
|
inStr = input_with_timeout_and_countdown(10)
|
|
3038
3530
|
if (not inStr) or inStr.lower().strip().startswith('b'):
|
|
3039
3531
|
backup = True
|
|
@@ -3056,14 +3548,14 @@ def write_default_config(args,CONFIG_FILE = None):
|
|
|
3056
3548
|
eprint(f"Config file written to {CONFIG_FILE!r}")
|
|
3057
3549
|
except Exception as e:
|
|
3058
3550
|
eprint(f"Error: Unable to write to the config file: {e!r}")
|
|
3059
|
-
eprint(
|
|
3551
|
+
eprint('Printing the config file to stdout:')
|
|
3060
3552
|
print(json.dumps(__configs_from_file, indent=4))
|
|
3061
3553
|
|
|
3062
3554
|
#%% ------------ Argument Processing -----------------
|
|
3063
3555
|
def get_parser():
|
|
3064
3556
|
global _binPaths
|
|
3065
3557
|
parser = argparse.ArgumentParser(description=f'Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file chain: {CONFIG_FILE_CHAIN!r}',
|
|
3066
|
-
epilog=f'Found bins: {list(_binPaths.values())}\n Missing bins: {_binCalled - set(_binPaths.keys())}')
|
|
3558
|
+
epilog=f'Found bins: {list(_binPaths.values())}\n Missing bins: {_binCalled - set(_binPaths.keys())}\n Terminal color capability: {get_terminal_color_capability()}',)
|
|
3067
3559
|
parser.add_argument('hosts', metavar='hosts', type=str, nargs='?', help=f'Hosts to run the command on, use "," to seperate hosts. (default: {DEFAULT_HOSTS})',default=DEFAULT_HOSTS)
|
|
3068
3560
|
parser.add_argument('commands', metavar='commands', type=str, nargs='*',default=None,help='the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host name.')
|
|
3069
3561
|
parser.add_argument('-u','--username', type=str,help=f'The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified. (default: {DEFAULT_USERNAME})',default=DEFAULT_USERNAME)
|
|
@@ -3075,7 +3567,7 @@ def get_parser():
|
|
|
3075
3567
|
parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
|
|
3076
3568
|
parser.add_argument('-s','-fs','--file_sync', action='store_true', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source and source and destination will be the same in this mode. Infer destination from source path. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
|
|
3077
3569
|
parser.add_argument('-W','--scp', action='store_true', help=f'Use scp for copying files instead of rsync. Need to use this on windows. (default: {DEFAULT_SCP})', default=DEFAULT_SCP)
|
|
3078
|
-
parser.add_argument('-G','-gm','--gather_mode', action='store_true', help=
|
|
3570
|
+
parser.add_argument('-G','-gm','--gather_mode', action='store_true', help='Gather files from the hosts instead of sending files to the hosts. Will send remote files specified in <FILE> to local path specified in <COMMANDS> (default: False)', default=False)
|
|
3079
3571
|
#parser.add_argument("-d",'-c',"--destination", type=str, help="The destination of the files. Same as specify with commands. Added for compatibility. Use #HOST# or #HOSTNAME# to replace the host name in the destination")
|
|
3080
3572
|
parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds. Set default value via DEFAULT_CLI_TIMEOUT in config file. Use 0 for disabling timeout. (default: {DEFAULT_CLI_TIMEOUT})", default=DEFAULT_CLI_TIMEOUT)
|
|
3081
3573
|
parser.add_argument('-T','--use_script_timeout',action='store_true', help=f'Use shortened timeout suitable to use in a script. Set value via DEFAULT_TIMEOUT field in config file. (current: {DEFAULT_TIMEOUT})', default=False)
|
|
@@ -3084,6 +3576,8 @@ def get_parser():
|
|
|
3084
3576
|
parser.add_argument('-M',"--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
|
|
3085
3577
|
parser.add_argument("-mpre","--ipmi_interface_ip_prefix", type=str, help=f"The prefix of the IPMI interfaces (default: {DEFAULT_IPMI_INTERFACE_IP_PREFIX})", default=DEFAULT_IPMI_INTERFACE_IP_PREFIX)
|
|
3086
3578
|
parser.add_argument("-pre","--interface_ip_prefix", type=str, help=f"The prefix of the for the interfaces (default: {DEFAULT_INTERFACE_IP_PREFIX})", default=DEFAULT_INTERFACE_IP_PREFIX)
|
|
3579
|
+
parser.add_argument('-iu','--ipmi_username', type=str,help=f'The username to use to connect to the hosts via ipmi. (default: {DEFAULT_IPMI_USERNAME})',default=DEFAULT_IPMI_USERNAME)
|
|
3580
|
+
parser.add_argument('-ip','--ipmi_password', type=str,help=f'The password to use to connect to the hosts via ipmi. (default: {DEFAULT_IPMI_PASSWORD})',default=DEFAULT_IPMI_PASSWORD)
|
|
3087
3581
|
parser.add_argument('-S',"-q","-nw","--no_watch","--quiet", action='store_true', help=f"Quiet mode, no curses watch, only print the output. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
|
|
3088
3582
|
parser.add_argument("-ww",'--window_width', type=int, help=f"The minimum character length of the curses window. (default: {DEFAULT_CURSES_MINIMUM_CHAR_LEN})", default=DEFAULT_CURSES_MINIMUM_CHAR_LEN)
|
|
3089
3583
|
parser.add_argument("-wh",'--window_height', type=int, help=f"The minimum line height of the curses window. (default: {DEFAULT_CURSES_MINIMUM_LINE_LEN})", default=DEFAULT_CURSES_MINIMUM_LINE_LEN)
|
|
@@ -3093,7 +3587,7 @@ def get_parser():
|
|
|
3093
3587
|
parser.add_argument('-Z','-rz','--return_zero', action='store_true', help=f"Return 0 even if there are errors. (default: {DEFAULT_RETURN_ZERO})", default=DEFAULT_RETURN_ZERO)
|
|
3094
3588
|
parser.add_argument('-C','--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
|
|
3095
3589
|
parser.add_argument("--env_file", type=str, help=f"The file to load the mssh file based environment variables from. ( Still work with --no_env ) (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
|
|
3096
|
-
parser.add_argument("-m","--max_connections", type=int, help=
|
|
3590
|
+
parser.add_argument("-m","--max_connections", type=int, help="Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
|
|
3097
3591
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
3098
3592
|
parser.add_argument('-w',"--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as well. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
|
|
3099
3593
|
parser.add_argument('-P',"-g","--greppable",'--table', action='store_true', help=f"Output in greppable table. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
@@ -3102,19 +3596,23 @@ def get_parser():
|
|
|
3102
3596
|
su_group.add_argument('-a',"-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)
|
|
3103
3597
|
parser.add_argument('-uhe','--unavailable_host_expiry', type=int, help=f"Time in seconds to expire the unavailable hosts (default: {DEFAULT_UNAVAILABLE_HOST_EXPIRY})", default=DEFAULT_UNAVAILABLE_HOST_EXPIRY)
|
|
3104
3598
|
parser.add_argument('-X',"-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)
|
|
3105
|
-
parser.add_argument('--generate_config_file', action='store_true', help=
|
|
3106
|
-
parser.add_argument('--config_file', type=str,nargs='?', help=
|
|
3107
|
-
parser.add_argument('--store_config_file',type = str,nargs='?',help=
|
|
3599
|
+
parser.add_argument('--generate_config_file', action='store_true', help='Store / generate the default config file from command line argument and current config at --config_file / stdout')
|
|
3600
|
+
parser.add_argument('--config_file', type=str,nargs='?', help='Additional config file to use, will pioritize over config chains. When using with store_config_file, will store the resulting config file at this location. Use without a path will use multiSSH3.config.json',const='multiSSH3.config.json',default=None)
|
|
3601
|
+
parser.add_argument('--store_config_file',type = str,nargs='?',help='Store the default config file from command line argument and current config. Same as --store_config_file --config_file=<path>',const='multiSSH3.config.json')
|
|
3108
3602
|
parser.add_argument('--debug', action='store_true', help='Print debug information')
|
|
3109
3603
|
parser.add_argument('-ci','--copy_id', action='store_true', help='Copy the ssh id to the hosts')
|
|
3110
3604
|
parser.add_argument('-I','-nh','--no_history', action='store_true', help=f'Do not record the command to history. Default: {DEFAULT_NO_HISTORY}', default=DEFAULT_NO_HISTORY)
|
|
3111
3605
|
parser.add_argument('-hf','--history_file', type=str, help=f'The file to store the history. (default: {DEFAULT_HISTORY_FILE})', default=DEFAULT_HISTORY_FILE)
|
|
3112
3606
|
parser.add_argument('--script', action='store_true', help='Run the command in script mode, short for -SCRIPT or --no_watch --skip_unreachable --no_env --no_history --greppable --error_only')
|
|
3113
3607
|
parser.add_argument('-e','--encoding', type=str, help=f'The encoding to use for the output. (default: {DEFAULT_ENCODING})', default=DEFAULT_ENCODING)
|
|
3608
|
+
parser.add_argument('-ddt','--diff_display_threshold', type=float, help=f'The threshold of lines to display the diff when files differ. Set to 0 to always display the diff. (default: {DEFAULT_DIFF_DISPLAY_THRESHOLD})', default=DEFAULT_DIFF_DISPLAY_THRESHOLD)
|
|
3609
|
+
parser.add_argument('--force_truecolor', action='store_true', help=f'Force truecolor output even when not in a truecolor terminal. (default: {FORCE_TRUECOLOR})', default=FORCE_TRUECOLOR)
|
|
3114
3610
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
3115
3611
|
return parser
|
|
3116
3612
|
|
|
3117
3613
|
def process_args(args = None):
|
|
3614
|
+
global DEFAULT_IPMI_USERNAME
|
|
3615
|
+
global DEFAULT_IPMI_PASSWORD
|
|
3118
3616
|
parser = get_parser()
|
|
3119
3617
|
# We handle the signal
|
|
3120
3618
|
signal.signal(signal.SIGINT, signal_handler)
|
|
@@ -3174,10 +3672,10 @@ def process_config_file(args):
|
|
|
3174
3672
|
|
|
3175
3673
|
def process_commands(args):
|
|
3176
3674
|
if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
|
|
3177
|
-
eprint(
|
|
3675
|
+
eprint("Multiple one word command detected, what to do? (1/m/n)")
|
|
3178
3676
|
eprint(f"1: Run 1 command [{' '.join(args.commands)}] on all hosts ( default )")
|
|
3179
3677
|
eprint(f"m: Run multiple commands [{', '.join(args.commands)}] on all hosts")
|
|
3180
|
-
eprint(
|
|
3678
|
+
eprint("n: Exit")
|
|
3181
3679
|
inStr = input_with_timeout_and_countdown(3)
|
|
3182
3680
|
if (not inStr) or inStr.lower().strip().startswith('1'):
|
|
3183
3681
|
args.commands = [" ".join(args.commands)]
|
|
@@ -3208,6 +3706,10 @@ def set_global_with_args(args):
|
|
|
3208
3706
|
global __configs_from_file
|
|
3209
3707
|
global _encoding
|
|
3210
3708
|
global __returnZero
|
|
3709
|
+
global DEFAULT_IPMI_USERNAME
|
|
3710
|
+
global DEFAULT_IPMI_PASSWORD
|
|
3711
|
+
global DEFAULT_DIFF_DISPLAY_THRESHOLD
|
|
3712
|
+
global FORCE_TRUECOLOR
|
|
3211
3713
|
_emo = False
|
|
3212
3714
|
__ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
|
|
3213
3715
|
_env_file = args.env_file
|
|
@@ -3215,6 +3717,12 @@ def set_global_with_args(args):
|
|
|
3215
3717
|
_encoding = args.encoding
|
|
3216
3718
|
if args.return_zero:
|
|
3217
3719
|
__returnZero = True
|
|
3720
|
+
if args.ipmi_username:
|
|
3721
|
+
DEFAULT_IPMI_USERNAME = args.ipmi_username
|
|
3722
|
+
if args.ipmi_password:
|
|
3723
|
+
DEFAULT_IPMI_PASSWORD = args.ipmi_password
|
|
3724
|
+
DEFAULT_DIFF_DISPLAY_THRESHOLD = args.diff_display_threshold
|
|
3725
|
+
FORCE_TRUECOLOR = args.force_truecolor
|
|
3218
3726
|
|
|
3219
3727
|
#%% ------------ Wrapper Block ----------------
|
|
3220
3728
|
def main():
|
|
@@ -3254,7 +3762,8 @@ def main():
|
|
|
3254
3762
|
eprint(f"Sleeping for {args.interval} seconds")
|
|
3255
3763
|
time.sleep(args.interval)
|
|
3256
3764
|
|
|
3257
|
-
if not __global_suppress_printout:
|
|
3765
|
+
if not __global_suppress_printout:
|
|
3766
|
+
eprint(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
|
|
3258
3767
|
hosts = run_command_on_hosts(args.hosts,args.commands,
|
|
3259
3768
|
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
3260
3769
|
no_watch=args.no_watch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
@@ -3280,7 +3789,8 @@ def main():
|
|
|
3280
3789
|
eprint(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
|
|
3281
3790
|
eprint(f'failed_hosts: {",".join(compact_hostnames(__failedHosts))}')
|
|
3282
3791
|
else:
|
|
3283
|
-
if not __global_suppress_printout:
|
|
3792
|
+
if not __global_suppress_printout:
|
|
3793
|
+
eprint('Complete. All hosts returned 0.')
|
|
3284
3794
|
|
|
3285
3795
|
if args.success_hosts and not __global_suppress_printout:
|
|
3286
3796
|
eprint(f'succeeded_hosts: {",".join(compact_hostnames(succeededHosts))}')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
multiSSH3.py,sha256=9hTPnoRn6yswbQ8QaQIvkgIOwlVohz1o8jFlq_-S5D0,172386
|
|
2
|
+
multissh3-5.91.dist-info/METADATA,sha256=kyyw0bHMd-ifFluN0LAlKqhY6udom5D3Q2_AgAuSqqU,18093
|
|
3
|
+
multissh3-5.91.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
+
multissh3-5.91.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
+
multissh3-5.91.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
+
multissh3-5.91.dist-info/RECORD,,
|
multissh3-5.86.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=Mbr7AsHObwHIkMCuS9bsfmfMFWNQ4NngBlM-oVCXAlg,154547
|
|
2
|
-
multissh3-5.86.dist-info/METADATA,sha256=r4ndEU9KGZ5RMn-AtlxzUazkrnbLbT7LFkQWgF9RwqY,18093
|
|
3
|
-
multissh3-5.86.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
-
multissh3-5.86.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
5
|
-
multissh3-5.86.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
6
|
-
multissh3-5.86.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|