multiSSH3 4.92__tar.gz → 4.97__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-4.92 → multissh3-4.97}/PKG-INFO +1 -1
- {multissh3-4.92 → multissh3-4.97}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-4.92 → multissh3-4.97}/multiSSH3.py +196 -112
- {multissh3-4.92 → multissh3-4.97}/setup.py +1 -1
- {multissh3-4.92 → multissh3-4.97}/LICENSE +0 -0
- {multissh3-4.92 → multissh3-4.97}/README.md +0 -0
- {multissh3-4.92 → multissh3-4.97}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-4.92 → multissh3-4.97}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-4.92 → multissh3-4.97}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-4.92 → multissh3-4.97}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-4.92 → multissh3-4.97}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-4.92 → multissh3-4.97}/setup.cfg +0 -0
|
@@ -17,6 +17,7 @@ import functools
|
|
|
17
17
|
import glob
|
|
18
18
|
import shutil
|
|
19
19
|
import getpass
|
|
20
|
+
import uuid
|
|
20
21
|
|
|
21
22
|
try:
|
|
22
23
|
# Check if functiools.cache is available
|
|
@@ -29,11 +30,16 @@ except AttributeError:
|
|
|
29
30
|
# If neither is available, use a dummy decorator
|
|
30
31
|
def cache_decorator(func):
|
|
31
32
|
return func
|
|
32
|
-
version = '4.
|
|
33
|
+
version = '4.97'
|
|
33
34
|
VERSION = version
|
|
34
35
|
|
|
35
36
|
CONFIG_FILE = '/etc/multiSSH3.config.json'
|
|
36
37
|
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
def eprint(*args, **kwargs):
|
|
41
|
+
print(*args, file=sys.stderr, **kwargs)
|
|
42
|
+
|
|
37
43
|
def load_config_file(config_file):
|
|
38
44
|
'''
|
|
39
45
|
Load the config file to global variables
|
|
@@ -46,8 +52,12 @@ def load_config_file(config_file):
|
|
|
46
52
|
'''
|
|
47
53
|
if not os.path.exists(config_file):
|
|
48
54
|
return {}
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
try:
|
|
56
|
+
with open(config_file,'r') as f:
|
|
57
|
+
config = json.load(f)
|
|
58
|
+
except:
|
|
59
|
+
eprint(f"Error: Cannot load config file {config_file}")
|
|
60
|
+
return {}
|
|
51
61
|
return config
|
|
52
62
|
|
|
53
63
|
__configs_from_file = load_config_file(CONFIG_FILE)
|
|
@@ -153,7 +163,7 @@ _DEFAULT_NO_START = __configs_from_file.get('_DEFAULT_NO_START', __build_in_defa
|
|
|
153
163
|
# form the regex from the list
|
|
154
164
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = __configs_from_file.get('__ERROR_MESSAGES_TO_IGNORE_REGEX', __build_in_default_config['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
|
|
155
165
|
if __ERROR_MESSAGES_TO_IGNORE_REGEX:
|
|
156
|
-
|
|
166
|
+
eprint('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
|
|
157
167
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__configs_from_file['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
|
|
158
168
|
else:
|
|
159
169
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
|
|
@@ -164,8 +174,25 @@ __global_suppress_printout = True
|
|
|
164
174
|
|
|
165
175
|
__mainReturnCode = 0
|
|
166
176
|
__failedHosts = set()
|
|
177
|
+
__host_i_lock = threading.Lock()
|
|
178
|
+
__host_i_counter = -1
|
|
179
|
+
def get_i():
|
|
180
|
+
'''
|
|
181
|
+
Get the global counter for the host objects
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
int: The global counter for the host objects
|
|
185
|
+
'''
|
|
186
|
+
global __host_i_counter
|
|
187
|
+
global __host_i_lock
|
|
188
|
+
with __host_i_lock:
|
|
189
|
+
__host_i_counter += 1
|
|
190
|
+
return __host_i_counter
|
|
191
|
+
|
|
167
192
|
class Host:
|
|
168
193
|
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False):
|
|
194
|
+
global __host_i_counter
|
|
195
|
+
global __host_i_lock
|
|
169
196
|
self.name = name # the name of the host (hostname or IP address)
|
|
170
197
|
self.command = command # the command to run on the host
|
|
171
198
|
self.returncode = None # the return code of the command
|
|
@@ -181,11 +208,15 @@ class Host:
|
|
|
181
208
|
self.gatherMode = gatherMode # whether the host is in gather mode
|
|
182
209
|
self.extraargs = extraargs # extra arguments to be passed to ssh
|
|
183
210
|
self.resolvedName = None # the resolved IP address of the host
|
|
211
|
+
# also store a globally unique integer i from 0
|
|
212
|
+
self.i = get_i()
|
|
213
|
+
self.uuid = uuid.uuid4()
|
|
214
|
+
|
|
184
215
|
def __iter__(self):
|
|
185
216
|
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
186
217
|
def __repr__(self):
|
|
187
218
|
# return the complete data structure
|
|
188
|
-
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName})"
|
|
219
|
+
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid})"
|
|
189
220
|
def __str__(self):
|
|
190
221
|
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
|
|
191
222
|
|
|
@@ -489,7 +520,7 @@ def validate_expand_hostname(hostname):
|
|
|
489
520
|
elif getIP(hostname,local=False):
|
|
490
521
|
return [hostname]
|
|
491
522
|
else:
|
|
492
|
-
|
|
523
|
+
eprint(f"Error: {hostname} is not a valid hostname or IP address!")
|
|
493
524
|
global __mainReturnCode
|
|
494
525
|
__mainReturnCode += 1
|
|
495
526
|
global __failedHosts
|
|
@@ -509,14 +540,14 @@ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selectio
|
|
|
509
540
|
"""
|
|
510
541
|
import select
|
|
511
542
|
# Print the initial prompt with the countdown
|
|
512
|
-
|
|
543
|
+
eprint(f"{prompt} [{timeout}s]: ", end='', flush=True)
|
|
513
544
|
# Loop until the timeout
|
|
514
545
|
for remaining in range(timeout, 0, -1):
|
|
515
546
|
# If there is an input, return it
|
|
516
547
|
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
|
|
517
548
|
return input().strip()
|
|
518
549
|
# Print the remaining time
|
|
519
|
-
|
|
550
|
+
eprint(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
|
|
520
551
|
# Wait a second
|
|
521
552
|
time.sleep(1)
|
|
522
553
|
# If there is no input, return None
|
|
@@ -590,7 +621,7 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
590
621
|
else:
|
|
591
622
|
time.sleep(0.1)
|
|
592
623
|
if sentInput < len(__keyPressesIn) - 1 :
|
|
593
|
-
|
|
624
|
+
eprint(f"Warning: {len(__keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
|
|
594
625
|
# # send the last line
|
|
595
626
|
# if __keyPressesIn and __keyPressesIn[-1]:
|
|
596
627
|
# stream.write(''.join(__keyPressesIn[-1]).encode())
|
|
@@ -598,6 +629,34 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
598
629
|
# host.output.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
599
630
|
# host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
600
631
|
return sentInput
|
|
632
|
+
|
|
633
|
+
def replace_magic_strings(string,keys,value,case_sensitive=False):
|
|
634
|
+
'''
|
|
635
|
+
Replace the magic strings in the host object
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
string (str): The string to replace the magic strings
|
|
639
|
+
keys (list): Search for keys to replace
|
|
640
|
+
value (str): The value to replace the key
|
|
641
|
+
case_sensitive (bool, optional): Whether to search for the keys in a case sensitive way. Defaults to False.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
str: The string with the magic strings replaced
|
|
645
|
+
'''
|
|
646
|
+
# verify magic strings have # at the beginning and end
|
|
647
|
+
newKeys = []
|
|
648
|
+
for key in keys:
|
|
649
|
+
if key.startswith('#') and key.endswith('#'):
|
|
650
|
+
newKeys.append(key)
|
|
651
|
+
else:
|
|
652
|
+
newKeys.append('#'+key.strip('#')+'#')
|
|
653
|
+
# replace the magic strings
|
|
654
|
+
for key in newKeys:
|
|
655
|
+
if case_sensitive:
|
|
656
|
+
string = string.replace(key,value)
|
|
657
|
+
else:
|
|
658
|
+
string = re.sub(re.escape(key),value,string,flags=re.IGNORECASE)
|
|
659
|
+
return string
|
|
601
660
|
|
|
602
661
|
def ssh_command(host, sem, timeout=60,passwds=None):
|
|
603
662
|
'''
|
|
@@ -626,14 +685,18 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
626
685
|
host.address = host.name
|
|
627
686
|
if '@' in host.name:
|
|
628
687
|
host.username, host.address = host.name.rsplit('@',1)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
688
|
+
host.command = replace_magic_strings(host.command,['#HOST#','#HOSTNAME#'],host.address,case_sensitive=False)
|
|
689
|
+
if host.username:
|
|
690
|
+
host.command = replace_magic_strings(host.command,['#USER#','#USERNAME#'],host.username,case_sensitive=False)
|
|
691
|
+
else:
|
|
692
|
+
current_user = getpass.getuser()
|
|
693
|
+
host.command = replace_magic_strings(host.command,['#USER#','#USERNAME#'],current_user,case_sensitive=False)
|
|
694
|
+
host.command = replace_magic_strings(host.command,['#ID#'],str(id(host)),case_sensitive=False)
|
|
695
|
+
host.command = replace_magic_strings(host.command,['#I#'],str(host.i),case_sensitive=False)
|
|
696
|
+
host.command = replace_magic_strings(host.command,['#PASSWD#','#PASSWORD#'],passwds,case_sensitive=False)
|
|
697
|
+
if host.resolvedName:
|
|
698
|
+
host.command = replace_magic_strings(host.command,['#RESOLVEDNAME#','#RESOLVED#'],host.resolvedName,case_sensitive=False)
|
|
699
|
+
host.command = replace_magic_strings(host.command,['#UUID#'],str(host.uuid),case_sensitive=False)
|
|
637
700
|
formatedCMD = []
|
|
638
701
|
if host.extraargs and type(host.extraargs) == str:
|
|
639
702
|
extraargs = host.extraargs.split()
|
|
@@ -892,7 +955,7 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
|
892
955
|
host.printedLines = 0
|
|
893
956
|
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
894
957
|
|
|
895
|
-
def generate_display(stdscr, hosts,
|
|
958
|
+
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):
|
|
896
959
|
try:
|
|
897
960
|
org_dim = stdscr.getmaxyx()
|
|
898
961
|
new_configured = True
|
|
@@ -906,11 +969,11 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
906
969
|
if single_window:
|
|
907
970
|
min_char_len_local = max_x-1
|
|
908
971
|
min_line_len_local = max_y-1
|
|
909
|
-
#
|
|
972
|
+
# return True if the terminal is too small
|
|
910
973
|
if max_x < 2 or max_y < 2:
|
|
911
|
-
|
|
974
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
912
975
|
if min_char_len_local < 1 or min_line_len_local < 1:
|
|
913
|
-
|
|
976
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
914
977
|
# We need to figure out how many hosts we can fit in the terminal
|
|
915
978
|
# We will need at least 2 lines per host, one for its name, one for its output
|
|
916
979
|
# Each line will be at least 61 characters long (60 for the output, 1 for the borders)
|
|
@@ -918,10 +981,10 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
918
981
|
max_num_hosts_y = max_y // (min_line_len_local + 1)
|
|
919
982
|
max_num_hosts = max_num_hosts_x * max_num_hosts_y
|
|
920
983
|
if max_num_hosts < 1:
|
|
921
|
-
|
|
984
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
922
985
|
hosts_to_display , host_stats = get_hosts_to_display(hosts, max_num_hosts)
|
|
923
986
|
if len(hosts_to_display) == 0:
|
|
924
|
-
|
|
987
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
925
988
|
# Now we calculate the actual number of hosts we will display for x and y
|
|
926
989
|
optimal_len_x = max(min_char_len_local, 80)
|
|
927
990
|
num_hosts_x = max(min(max_num_hosts_x, max_x // optimal_len_x),1)
|
|
@@ -942,7 +1005,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
942
1005
|
host_window_height = max_y // num_hosts_y
|
|
943
1006
|
host_window_width = max_x // num_hosts_x
|
|
944
1007
|
if host_window_height < 1 or host_window_width < 1:
|
|
945
|
-
|
|
1008
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
946
1009
|
|
|
947
1010
|
old_stat = ''
|
|
948
1011
|
old_bottom_stat = ''
|
|
@@ -983,36 +1046,50 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
983
1046
|
# with open('keylog.txt','a') as f:
|
|
984
1047
|
# f.write(str(key)+'\n')
|
|
985
1048
|
if key == 410: # 410 is the key code for resize
|
|
986
|
-
|
|
1049
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1050
|
+
elif key == 95 and not __keyPressesIn[-1]: # 95 is the key code for _
|
|
1051
|
+
# if last line is empty, we will reconfigure the wh to be smaller
|
|
1052
|
+
if min_line_len != 1:
|
|
1053
|
+
return (lineToDisplay,curserPosition , min_char_len , max(min_line_len -1,1), single_window)
|
|
1054
|
+
elif key == 43 and not __keyPressesIn[-1]: # 43 is the key code for +
|
|
1055
|
+
# if last line is empty, we will reconfigure the wh to be larger
|
|
1056
|
+
return (lineToDisplay,curserPosition , min_char_len , min_line_len +1, single_window)
|
|
1057
|
+
elif key == 123 and not __keyPressesIn[-1]: # 123 is the key code for {
|
|
1058
|
+
# if last line is empty, we will reconfigure the ww to be smaller
|
|
1059
|
+
if min_char_len != 1:
|
|
1060
|
+
return (lineToDisplay,curserPosition , max(min_char_len -1,1), min_line_len, single_window)
|
|
1061
|
+
elif key == 124 and not __keyPressesIn[-1]: # 124 is the key code for |
|
|
1062
|
+
# if last line is empty, we will toggle the single window mode
|
|
1063
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, not single_window)
|
|
1064
|
+
elif key == 125 and not __keyPressesIn[-1]: # 125 is the key code for }
|
|
1065
|
+
# if last line is empty, we will reconfigure the ww to be larger
|
|
1066
|
+
return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window)
|
|
987
1067
|
# We handle positional keys
|
|
988
|
-
#
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
elif key == 360: # 360 is the key code for end
|
|
1014
|
-
curserPosition = len(__keyPressesIn[lineToDisplay])
|
|
1015
|
-
# We are left with these are keys that mofidy the current line.
|
|
1068
|
+
# if the key is up arrow, we will move the line to display up
|
|
1069
|
+
elif key == 259: # 259 is the key code for up arrow
|
|
1070
|
+
lineToDisplay = max(lineToDisplay - 1, -len(__keyPressesIn))
|
|
1071
|
+
# if the key is down arrow, we will move the line to display down
|
|
1072
|
+
elif key == 258: # 258 is the key code for down arrow
|
|
1073
|
+
lineToDisplay = min(lineToDisplay + 1, -1)
|
|
1074
|
+
# if the key is left arrow, we will move the cursor left
|
|
1075
|
+
elif key == 260: # 260 is the key code for left arrow
|
|
1076
|
+
curserPosition = min(max(curserPosition - 1, 0), len(__keyPressesIn[lineToDisplay]) -1)
|
|
1077
|
+
# if the key is right arrow, we will move the cursor right
|
|
1078
|
+
elif key == 261: # 261 is the key code for right arrow
|
|
1079
|
+
curserPosition = max(min(curserPosition + 1, len(__keyPressesIn[lineToDisplay])), 0)
|
|
1080
|
+
# if the key is page up, we will move the line to display up by 5 lines
|
|
1081
|
+
elif key == 339: # 339 is the key code for page up
|
|
1082
|
+
lineToDisplay = max(lineToDisplay - 5, -len(__keyPressesIn))
|
|
1083
|
+
# if the key is page down, we will move the line to display down by 5 lines
|
|
1084
|
+
elif key == 338: # 338 is the key code for page down
|
|
1085
|
+
lineToDisplay = min(lineToDisplay + 5, -1)
|
|
1086
|
+
# if the key is home, we will move the cursor to the beginning of the line
|
|
1087
|
+
elif key == 262: # 262 is the key code for home
|
|
1088
|
+
curserPosition = 0
|
|
1089
|
+
# if the key is end, we will move the cursor to the end of the line
|
|
1090
|
+
elif key == 360: # 360 is the key code for end
|
|
1091
|
+
curserPosition = len(__keyPressesIn[lineToDisplay])
|
|
1092
|
+
# We are left with these are keys that mofidy the current line.
|
|
1016
1093
|
else:
|
|
1017
1094
|
# This means the user have done scrolling and is committing to modify the current line.
|
|
1018
1095
|
if lineToDisplay < -1:
|
|
@@ -1044,12 +1121,11 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1044
1121
|
__keyPressesIn[lineToDisplay].insert(curserPosition, chr(key))
|
|
1045
1122
|
curserPosition += 1
|
|
1046
1123
|
# reconfigure when the terminal size changes
|
|
1047
|
-
# raise Exception when max_y or max_x is changed, let parent handle reconfigure
|
|
1048
1124
|
if org_dim != stdscr.getmaxyx():
|
|
1049
|
-
|
|
1125
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1050
1126
|
# We generate the aggregated stats if user did not input anything
|
|
1051
1127
|
if not __keyPressesIn[lineToDisplay]:
|
|
1052
|
-
stats = '┍'+ f"Total: {len(hosts)}
|
|
1128
|
+
stats = '┍'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} ww: {min_char_len} wh:{min_line_len} "[:max_x - 2].center(max_x - 2, "━")
|
|
1053
1129
|
else:
|
|
1054
1130
|
# we use the stat bar to display the key presses
|
|
1055
1131
|
encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
|
|
@@ -1060,7 +1136,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1060
1136
|
# encodedLine = encodedLine[:curserPosition] + ' ' + encodedLine[curserPosition:]
|
|
1061
1137
|
stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
|
|
1062
1138
|
if bottom_border:
|
|
1063
|
-
bottom_stats = '└'+ f"Total: {len(hosts)}
|
|
1139
|
+
bottom_stats = '└'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} "[:max_x - 2].center(max_x - 2, "─")
|
|
1064
1140
|
if bottom_stats != old_bottom_stat:
|
|
1065
1141
|
old_bottom_stat = bottom_stats
|
|
1066
1142
|
#bottom_border.clear()
|
|
@@ -1111,17 +1187,12 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1111
1187
|
# print(str(e).strip())
|
|
1112
1188
|
# print(traceback.format_exc().strip())
|
|
1113
1189
|
if org_dim != stdscr.getmaxyx():
|
|
1114
|
-
|
|
1190
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1115
1191
|
new_configured = False
|
|
1116
1192
|
last_refresh_time = time.perf_counter()
|
|
1117
|
-
|
|
1118
|
-
except ZeroDivisionError:
|
|
1119
|
-
# terminial is too small, we skip the display
|
|
1120
|
-
pass
|
|
1121
1193
|
except Exception as e:
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
generate_display(stdscr, hosts, threads, lineToDisplay, curserPosition, min_char_len, min_line_len, single_window)
|
|
1194
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1195
|
+
return None
|
|
1125
1196
|
|
|
1126
1197
|
def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
|
|
1127
1198
|
'''
|
|
@@ -1159,7 +1230,20 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
1159
1230
|
curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
|
|
1160
1231
|
curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
|
|
1161
1232
|
curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
|
1162
|
-
|
|
1233
|
+
params = (-1,0 , min_char_len, min_line_len, single_window)
|
|
1234
|
+
while params:
|
|
1235
|
+
params = generate_display(stdscr, hosts, *params)
|
|
1236
|
+
if not params:
|
|
1237
|
+
break
|
|
1238
|
+
if not any([host.returncode is None for host in hosts]):
|
|
1239
|
+
# this means no hosts are running
|
|
1240
|
+
break
|
|
1241
|
+
# print the current configuration
|
|
1242
|
+
stdscr.clear()
|
|
1243
|
+
stdscr.addstr(0, 0, f"Loading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]}")
|
|
1244
|
+
stdscr.refresh()
|
|
1245
|
+
#time.sleep(0.25)
|
|
1246
|
+
|
|
1163
1247
|
|
|
1164
1248
|
def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
1165
1249
|
'''
|
|
@@ -1285,10 +1369,10 @@ def signal_handler(sig, frame):
|
|
|
1285
1369
|
'''
|
|
1286
1370
|
global _emo
|
|
1287
1371
|
if not _emo:
|
|
1288
|
-
|
|
1372
|
+
eprint('Ctrl C caught, exiting...')
|
|
1289
1373
|
_emo = True
|
|
1290
1374
|
else:
|
|
1291
|
-
|
|
1375
|
+
eprint('Ctrl C caught again, exiting immediately!')
|
|
1292
1376
|
# wait for 0.1 seconds to allow the threads to exit
|
|
1293
1377
|
time.sleep(0.1)
|
|
1294
1378
|
os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
@@ -1460,7 +1544,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1460
1544
|
commands = [' '.join(command) if not type(command) == str else command for command in commands]
|
|
1461
1545
|
except:
|
|
1462
1546
|
pass
|
|
1463
|
-
|
|
1547
|
+
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.")
|
|
1464
1548
|
#verify_ssh_config()
|
|
1465
1549
|
# load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
|
|
1466
1550
|
if called:
|
|
@@ -1503,7 +1587,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1503
1587
|
targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
|
|
1504
1588
|
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
|
|
1505
1589
|
if skipHostsList:
|
|
1506
|
-
|
|
1590
|
+
eprint(f"Skipping hosts: {skipHostsList}")
|
|
1507
1591
|
if files and not commands:
|
|
1508
1592
|
# if files are specified but not target dir, we default to file sync mode
|
|
1509
1593
|
file_sync = True
|
|
@@ -1520,7 +1604,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1520
1604
|
except:
|
|
1521
1605
|
pathSet.update(glob.glob(file,recursive=True))
|
|
1522
1606
|
if not pathSet:
|
|
1523
|
-
|
|
1607
|
+
eprint(f'Warning: No source files at {files} are found after resolving globs!')
|
|
1524
1608
|
sys.exit(66)
|
|
1525
1609
|
else:
|
|
1526
1610
|
pathSet = set(files)
|
|
@@ -1533,16 +1617,16 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1533
1617
|
if oneonone:
|
|
1534
1618
|
hosts = []
|
|
1535
1619
|
if len(commands) != len(targetHostsList) - len(skipHostsList):
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1620
|
+
eprint("Error: the number of commands must be the same as the number of hosts")
|
|
1621
|
+
eprint(f"Number of commands: {len(commands)}")
|
|
1622
|
+
eprint(f"Number of hosts: {len(targetHostsList - skipHostsList)}")
|
|
1539
1623
|
sys.exit(255)
|
|
1540
1624
|
if not __global_suppress_printout:
|
|
1541
|
-
|
|
1542
|
-
|
|
1625
|
+
eprint('-'*80)
|
|
1626
|
+
eprint("Running in one on one mode")
|
|
1543
1627
|
for host, command in zip(targetHostsList, commands):
|
|
1544
1628
|
if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
|
|
1545
|
-
|
|
1629
|
+
eprint(f"Skipping unavailable host: {host}")
|
|
1546
1630
|
continue
|
|
1547
1631
|
if host.strip() in skipHostsList: continue
|
|
1548
1632
|
if file_sync:
|
|
@@ -1550,7 +1634,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1550
1634
|
else:
|
|
1551
1635
|
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1552
1636
|
if not __global_suppress_printout:
|
|
1553
|
-
|
|
1637
|
+
eprint(f"Running command: {command} on host: {host}")
|
|
1554
1638
|
if not __global_suppress_printout: print('-'*80)
|
|
1555
1639
|
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)
|
|
1556
1640
|
return hosts
|
|
@@ -1565,20 +1649,20 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1565
1649
|
continue
|
|
1566
1650
|
if host.strip() in skipHostsList: continue
|
|
1567
1651
|
if file_sync:
|
|
1568
|
-
|
|
1652
|
+
eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
|
|
1569
1653
|
return []
|
|
1570
1654
|
elif files:
|
|
1571
|
-
|
|
1655
|
+
eprint(f"Error: files need to be specified with at least one path to sync")
|
|
1572
1656
|
elif ipmi:
|
|
1573
|
-
|
|
1657
|
+
eprint(f"Error: ipmi mode is not supported in interactive mode")
|
|
1574
1658
|
else:
|
|
1575
1659
|
hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
|
|
1576
1660
|
if not __global_suppress_printout:
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1661
|
+
eprint('-'*80)
|
|
1662
|
+
eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
1663
|
+
eprint('-'*80)
|
|
1580
1664
|
if no_start:
|
|
1581
|
-
|
|
1665
|
+
eprint(f"Warning: no_start is set, the command will not be started. As we are in interactive mode, no action will be done.")
|
|
1582
1666
|
else:
|
|
1583
1667
|
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)
|
|
1584
1668
|
return hosts
|
|
@@ -1594,9 +1678,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1594
1678
|
else:
|
|
1595
1679
|
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1596
1680
|
if not __global_suppress_printout and len(commands) > 1:
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1681
|
+
eprint('-'*80)
|
|
1682
|
+
eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
1683
|
+
eprint('-'*80)
|
|
1600
1684
|
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)
|
|
1601
1685
|
allHosts += hosts
|
|
1602
1686
|
return allHosts
|
|
@@ -1710,41 +1794,41 @@ def main():
|
|
|
1710
1794
|
if args.store_config_file:
|
|
1711
1795
|
try:
|
|
1712
1796
|
if os.path.exists(CONFIG_FILE):
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1797
|
+
eprint(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
|
|
1798
|
+
eprint(f"o: Overwrite the file")
|
|
1799
|
+
eprint(f"b: Rename the current config file at {CONFIG_FILE}.bak forcefully and write the new config file (default)")
|
|
1800
|
+
eprint(f"n: Do nothing")
|
|
1717
1801
|
inStr = input_with_timeout_and_countdown(10)
|
|
1718
1802
|
if (not inStr) or inStr.lower().strip().startswith('b'):
|
|
1719
1803
|
write_default_config(args,CONFIG_FILE,backup = True)
|
|
1720
|
-
|
|
1804
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1721
1805
|
elif inStr.lower().strip().startswith('o'):
|
|
1722
1806
|
write_default_config(args,CONFIG_FILE,backup = False)
|
|
1723
|
-
|
|
1807
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1724
1808
|
else:
|
|
1725
1809
|
write_default_config(args,CONFIG_FILE,backup = True)
|
|
1726
|
-
|
|
1810
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1727
1811
|
except Exception as e:
|
|
1728
|
-
|
|
1812
|
+
eprint(f"Error while writing config file: {e}")
|
|
1729
1813
|
if not args.commands:
|
|
1730
1814
|
with open(CONFIG_FILE,'r') as f:
|
|
1731
|
-
|
|
1815
|
+
eprint(f"Config file content: \n{f.read()}")
|
|
1732
1816
|
sys.exit(0)
|
|
1733
1817
|
|
|
1734
1818
|
_env_file = args.env_file
|
|
1735
1819
|
# if there are more than 1 commands, and every command only consists of one word,
|
|
1736
1820
|
# we will ask the user to confirm if they want to run multiple commands or just one command.
|
|
1737
1821
|
if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1822
|
+
eprint(f"Multiple one word command detected, what to do? (1/m/n)")
|
|
1823
|
+
eprint(f"1: Run 1 command [{' '.join(args.commands)}] on all hosts ( default )")
|
|
1824
|
+
eprint(f"m: Run multiple commands [{', '.join(args.commands)}] on all hosts")
|
|
1825
|
+
eprint(f"n: Exit")
|
|
1742
1826
|
inStr = input_with_timeout_and_countdown(3)
|
|
1743
1827
|
if (not inStr) or inStr.lower().strip().startswith('1'):
|
|
1744
1828
|
args.commands = [" ".join(args.commands)]
|
|
1745
|
-
|
|
1829
|
+
eprint(f"\nRunning 1 command: {args.commands[0]} on all hosts")
|
|
1746
1830
|
elif inStr.lower().strip().startswith('m'):
|
|
1747
|
-
|
|
1831
|
+
eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
|
|
1748
1832
|
else:
|
|
1749
1833
|
sys.exit(0)
|
|
1750
1834
|
|
|
@@ -1754,7 +1838,7 @@ def main():
|
|
|
1754
1838
|
__global_suppress_printout = False
|
|
1755
1839
|
|
|
1756
1840
|
if not __global_suppress_printout:
|
|
1757
|
-
|
|
1841
|
+
eprint('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1758
1842
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
1759
1843
|
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,
|
|
1760
1844
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
@@ -1764,10 +1848,10 @@ def main():
|
|
|
1764
1848
|
|
|
1765
1849
|
for i in range(args.repeat):
|
|
1766
1850
|
if args.interval > 0 and i < args.repeat - 1:
|
|
1767
|
-
|
|
1851
|
+
eprint(f"Sleeping for {args.interval} seconds")
|
|
1768
1852
|
time.sleep(args.interval)
|
|
1769
1853
|
|
|
1770
|
-
if not __global_suppress_printout:
|
|
1854
|
+
if not __global_suppress_printout: eprint(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
|
|
1771
1855
|
hosts = run_command_on_hosts(args.hosts,args.commands,
|
|
1772
1856
|
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1773
1857
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
@@ -1776,7 +1860,7 @@ def main():
|
|
|
1776
1860
|
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only)
|
|
1777
1861
|
#print('*'*80)
|
|
1778
1862
|
|
|
1779
|
-
if not __global_suppress_printout:
|
|
1863
|
+
if not __global_suppress_printout: eprint('-'*80)
|
|
1780
1864
|
|
|
1781
1865
|
succeededHosts = set()
|
|
1782
1866
|
for host in hosts:
|
|
@@ -1790,18 +1874,18 @@ def main():
|
|
|
1790
1874
|
__failedHosts = sorted(__failedHosts)
|
|
1791
1875
|
succeededHosts = sorted(succeededHosts)
|
|
1792
1876
|
if __mainReturnCode > 0:
|
|
1793
|
-
if not __global_suppress_printout:
|
|
1877
|
+
if not __global_suppress_printout: eprint(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
|
|
1794
1878
|
# with open('/tmp/bashcmd.stdin','w') as f:
|
|
1795
1879
|
# f.write(f"export failed_hosts={__failedHosts}\n")
|
|
1796
|
-
if not __global_suppress_printout:
|
|
1880
|
+
if not __global_suppress_printout: eprint(f'failed_hosts: {",".join(__failedHosts)}')
|
|
1797
1881
|
else:
|
|
1798
|
-
if not __global_suppress_printout:
|
|
1882
|
+
if not __global_suppress_printout: eprint('Complete. All hosts returned 0.')
|
|
1799
1883
|
|
|
1800
1884
|
if args.success_hosts and not __global_suppress_printout:
|
|
1801
|
-
|
|
1885
|
+
eprint(f'succeeded_hosts: {",".join(succeededHosts)}')
|
|
1802
1886
|
|
|
1803
1887
|
if threading.active_count() > 1:
|
|
1804
|
-
if not __global_suppress_printout:
|
|
1888
|
+
if not __global_suppress_printout: eprint(f'Remaining active thread: {threading.active_count()}')
|
|
1805
1889
|
# os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
1806
1890
|
# os._exit(mainReturnCode)
|
|
1807
1891
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|