multiSSH3 4.89__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.89 → multissh3-4.97}/PKG-INFO +1 -1
- {multissh3-4.89 → multissh3-4.97}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-4.89 → multissh3-4.97}/multiSSH3.py +234 -133
- {multissh3-4.89 → multissh3-4.97}/setup.py +1 -1
- {multissh3-4.89 → multissh3-4.97}/LICENSE +0 -0
- {multissh3-4.89 → multissh3-4.97}/README.md +0 -0
- {multissh3-4.89 → multissh3-4.97}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-4.89 → multissh3-4.97}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-4.89 → multissh3-4.97}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-4.89 → multissh3-4.97}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-4.89 → multissh3-4.97}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-4.89 → 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
|
|
@@ -173,6 +200,7 @@ class Host:
|
|
|
173
200
|
self.stdout = [] # the stdout of the command
|
|
174
201
|
self.stderr = [] # the stderr of the command
|
|
175
202
|
self.printedLines = -1 # the number of lines printed on the screen
|
|
203
|
+
self.lastUpdateTime = time.time() # the last time the output was updated
|
|
176
204
|
self.files = files # the files to be copied to the host
|
|
177
205
|
self.ipmi = ipmi # whether to use ipmi to connect to the host
|
|
178
206
|
self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
|
|
@@ -180,11 +208,15 @@ class Host:
|
|
|
180
208
|
self.gatherMode = gatherMode # whether the host is in gather mode
|
|
181
209
|
self.extraargs = extraargs # extra arguments to be passed to ssh
|
|
182
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
|
+
|
|
183
215
|
def __iter__(self):
|
|
184
216
|
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
185
217
|
def __repr__(self):
|
|
186
218
|
# return the complete data structure
|
|
187
|
-
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})"
|
|
188
220
|
def __str__(self):
|
|
189
221
|
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
|
|
190
222
|
|
|
@@ -488,7 +520,7 @@ def validate_expand_hostname(hostname):
|
|
|
488
520
|
elif getIP(hostname,local=False):
|
|
489
521
|
return [hostname]
|
|
490
522
|
else:
|
|
491
|
-
|
|
523
|
+
eprint(f"Error: {hostname} is not a valid hostname or IP address!")
|
|
492
524
|
global __mainReturnCode
|
|
493
525
|
__mainReturnCode += 1
|
|
494
526
|
global __failedHosts
|
|
@@ -508,14 +540,14 @@ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selectio
|
|
|
508
540
|
"""
|
|
509
541
|
import select
|
|
510
542
|
# Print the initial prompt with the countdown
|
|
511
|
-
|
|
543
|
+
eprint(f"{prompt} [{timeout}s]: ", end='', flush=True)
|
|
512
544
|
# Loop until the timeout
|
|
513
545
|
for remaining in range(timeout, 0, -1):
|
|
514
546
|
# If there is an input, return it
|
|
515
547
|
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
|
|
516
548
|
return input().strip()
|
|
517
549
|
# Print the remaining time
|
|
518
|
-
|
|
550
|
+
eprint(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
|
|
519
551
|
# Wait a second
|
|
520
552
|
time.sleep(1)
|
|
521
553
|
# If there is no input, return None
|
|
@@ -541,6 +573,7 @@ def handle_reading_stream(stream,target, host):
|
|
|
541
573
|
current_line_str = current_line.decode('utf-8',errors='backslashreplace')
|
|
542
574
|
target.append(current_line_str)
|
|
543
575
|
host.output.append(current_line_str)
|
|
576
|
+
host.lastUpdateTime = time.time()
|
|
544
577
|
current_line = bytearray()
|
|
545
578
|
lastLineCommited = True
|
|
546
579
|
for char in iter(lambda:stream.read(1), b''):
|
|
@@ -584,10 +617,11 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
584
617
|
host.output.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
|
|
585
618
|
host.stdout.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
|
|
586
619
|
sentInput += 1
|
|
620
|
+
host.lastUpdateTime = time.time()
|
|
587
621
|
else:
|
|
588
622
|
time.sleep(0.1)
|
|
589
623
|
if sentInput < len(__keyPressesIn) - 1 :
|
|
590
|
-
|
|
624
|
+
eprint(f"Warning: {len(__keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
|
|
591
625
|
# # send the last line
|
|
592
626
|
# if __keyPressesIn and __keyPressesIn[-1]:
|
|
593
627
|
# stream.write(''.join(__keyPressesIn[-1]).encode())
|
|
@@ -595,6 +629,34 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
595
629
|
# host.output.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
596
630
|
# host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
597
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
|
|
598
660
|
|
|
599
661
|
def ssh_command(host, sem, timeout=60,passwds=None):
|
|
600
662
|
'''
|
|
@@ -623,14 +685,18 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
623
685
|
host.address = host.name
|
|
624
686
|
if '@' in host.name:
|
|
625
687
|
host.username, host.address = host.name.rsplit('@',1)
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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)
|
|
634
700
|
formatedCMD = []
|
|
635
701
|
if host.extraargs and type(host.extraargs) == str:
|
|
636
702
|
extraargs = host.extraargs.split()
|
|
@@ -750,14 +816,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
750
816
|
stdin_thread = threading.Thread(target=handle_writing_stream, args=(proc.stdin,stdin_stop_event, host), daemon=True)
|
|
751
817
|
stdin_thread.start()
|
|
752
818
|
# Monitor the subprocess and terminate it after the timeout
|
|
753
|
-
|
|
754
|
-
|
|
819
|
+
host.lastUpdateTime = time.time()
|
|
820
|
+
timeoutLineAppended = False
|
|
755
821
|
while proc.poll() is None: # while the process is still running
|
|
756
|
-
if len(host.output) > outLength:
|
|
757
|
-
start_time = time.time()
|
|
758
|
-
outLength = len(host.output)
|
|
759
822
|
if timeout > 0:
|
|
760
|
-
if time.time() -
|
|
823
|
+
if time.time() - host.lastUpdateTime > timeout:
|
|
761
824
|
host.stderr.append('Timeout!')
|
|
762
825
|
host.output.append('Timeout!')
|
|
763
826
|
proc.send_signal(signal.SIGINT)
|
|
@@ -765,15 +828,19 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
765
828
|
|
|
766
829
|
proc.terminate()
|
|
767
830
|
break
|
|
768
|
-
elif time.time() -
|
|
769
|
-
timeoutLine = f'Timeout in [{timeout - int(time.time() -
|
|
831
|
+
elif time.time() - host.lastUpdateTime > min(30, timeout // 2):
|
|
832
|
+
timeoutLine = f'Timeout in [{timeout - int(time.time() - host.lastUpdateTime)}] seconds!'
|
|
770
833
|
if host.output and not host.output[-1].strip().startswith(timeoutLine):
|
|
771
834
|
# remove last line if it is a countdown
|
|
772
|
-
if host.output and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
|
|
835
|
+
if host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
|
|
773
836
|
host.output.pop()
|
|
774
837
|
host.printedLines -= 1
|
|
775
838
|
host.output.append(timeoutLine)
|
|
776
|
-
|
|
839
|
+
timeoutLineAppended = True
|
|
840
|
+
elif host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
|
|
841
|
+
host.output.pop()
|
|
842
|
+
host.printedLines -= 1
|
|
843
|
+
timeoutLineAppended = False
|
|
777
844
|
if _emo:
|
|
778
845
|
host.stderr.append('Ctrl C detected, Emergency Stop!')
|
|
779
846
|
host.output.append('Ctrl C detected, Emergency Stop!')
|
|
@@ -888,7 +955,7 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
|
888
955
|
host.printedLines = 0
|
|
889
956
|
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
890
957
|
|
|
891
|
-
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):
|
|
892
959
|
try:
|
|
893
960
|
org_dim = stdscr.getmaxyx()
|
|
894
961
|
new_configured = True
|
|
@@ -902,11 +969,11 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
902
969
|
if single_window:
|
|
903
970
|
min_char_len_local = max_x-1
|
|
904
971
|
min_line_len_local = max_y-1
|
|
905
|
-
#
|
|
972
|
+
# return True if the terminal is too small
|
|
906
973
|
if max_x < 2 or max_y < 2:
|
|
907
|
-
|
|
974
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
908
975
|
if min_char_len_local < 1 or min_line_len_local < 1:
|
|
909
|
-
|
|
976
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
910
977
|
# We need to figure out how many hosts we can fit in the terminal
|
|
911
978
|
# We will need at least 2 lines per host, one for its name, one for its output
|
|
912
979
|
# Each line will be at least 61 characters long (60 for the output, 1 for the borders)
|
|
@@ -914,10 +981,10 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
914
981
|
max_num_hosts_y = max_y // (min_line_len_local + 1)
|
|
915
982
|
max_num_hosts = max_num_hosts_x * max_num_hosts_y
|
|
916
983
|
if max_num_hosts < 1:
|
|
917
|
-
|
|
984
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
918
985
|
hosts_to_display , host_stats = get_hosts_to_display(hosts, max_num_hosts)
|
|
919
986
|
if len(hosts_to_display) == 0:
|
|
920
|
-
|
|
987
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
921
988
|
# Now we calculate the actual number of hosts we will display for x and y
|
|
922
989
|
optimal_len_x = max(min_char_len_local, 80)
|
|
923
990
|
num_hosts_x = max(min(max_num_hosts_x, max_x // optimal_len_x),1)
|
|
@@ -938,7 +1005,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
938
1005
|
host_window_height = max_y // num_hosts_y
|
|
939
1006
|
host_window_width = max_x // num_hosts_x
|
|
940
1007
|
if host_window_height < 1 or host_window_width < 1:
|
|
941
|
-
|
|
1008
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
942
1009
|
|
|
943
1010
|
old_stat = ''
|
|
944
1011
|
old_bottom_stat = ''
|
|
@@ -966,7 +1033,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
966
1033
|
bottom_border = None
|
|
967
1034
|
if y + host_window_height < org_dim[0]:
|
|
968
1035
|
bottom_border = curses.newwin(1, max_x, y + host_window_height, 0)
|
|
969
|
-
bottom_border.clear()
|
|
1036
|
+
#bottom_border.clear()
|
|
970
1037
|
bottom_border.addstr(0, 0, '-' * (max_x - 1))
|
|
971
1038
|
bottom_border.refresh()
|
|
972
1039
|
while host_stats['running'] > 0 or host_stats['waiting'] > 0:
|
|
@@ -979,36 +1046,50 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
979
1046
|
# with open('keylog.txt','a') as f:
|
|
980
1047
|
# f.write(str(key)+'\n')
|
|
981
1048
|
if key == 410: # 410 is the key code for resize
|
|
982
|
-
|
|
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)
|
|
983
1067
|
# We handle positional keys
|
|
984
|
-
#
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
elif key == 360: # 360 is the key code for end
|
|
1010
|
-
curserPosition = len(__keyPressesIn[lineToDisplay])
|
|
1011
|
-
# 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.
|
|
1012
1093
|
else:
|
|
1013
1094
|
# This means the user have done scrolling and is committing to modify the current line.
|
|
1014
1095
|
if lineToDisplay < -1:
|
|
@@ -1040,12 +1121,11 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1040
1121
|
__keyPressesIn[lineToDisplay].insert(curserPosition, chr(key))
|
|
1041
1122
|
curserPosition += 1
|
|
1042
1123
|
# reconfigure when the terminal size changes
|
|
1043
|
-
# raise Exception when max_y or max_x is changed, let parent handle reconfigure
|
|
1044
1124
|
if org_dim != stdscr.getmaxyx():
|
|
1045
|
-
|
|
1125
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1046
1126
|
# We generate the aggregated stats if user did not input anything
|
|
1047
1127
|
if not __keyPressesIn[lineToDisplay]:
|
|
1048
|
-
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, "━")
|
|
1049
1129
|
else:
|
|
1050
1130
|
# we use the stat bar to display the key presses
|
|
1051
1131
|
encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
|
|
@@ -1056,10 +1136,10 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1056
1136
|
# encodedLine = encodedLine[:curserPosition] + ' ' + encodedLine[curserPosition:]
|
|
1057
1137
|
stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
|
|
1058
1138
|
if bottom_border:
|
|
1059
|
-
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, "─")
|
|
1060
1140
|
if bottom_stats != old_bottom_stat:
|
|
1061
1141
|
old_bottom_stat = bottom_stats
|
|
1062
|
-
bottom_border.clear()
|
|
1142
|
+
#bottom_border.clear()
|
|
1063
1143
|
bottom_border.addstr(0, 0, bottom_stats)
|
|
1064
1144
|
bottom_border.refresh()
|
|
1065
1145
|
if stats != old_stat or curserPosition != old_cursor_position:
|
|
@@ -1070,7 +1150,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1070
1150
|
curserPositionStats = min(min(curserPosition,len(encodedLine) -1) + stats.find('Send CMD: ')+len('Send CMD: '), max_x -2)
|
|
1071
1151
|
else:
|
|
1072
1152
|
curserPositionStats = max_x -2
|
|
1073
|
-
stat_window.clear()
|
|
1153
|
+
#stat_window.clear()
|
|
1074
1154
|
#stat_window.addstr(0, 0, stats)
|
|
1075
1155
|
# add the line with curser that inverses the color at the curser position
|
|
1076
1156
|
stat_window.addstr(0, 0, stats[:curserPositionStats], curses.color_pair(1))
|
|
@@ -1086,7 +1166,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1086
1166
|
# we will only update the window if there is new output or the window is not fully printed
|
|
1087
1167
|
if new_configured or host.printedLines < len(host.output):
|
|
1088
1168
|
try:
|
|
1089
|
-
host_window.clear()
|
|
1169
|
+
#host_window.clear()
|
|
1090
1170
|
# we will try to center the name of the host with ┼ at the beginning and end and ─ in between
|
|
1091
1171
|
linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
|
|
1092
1172
|
host_window.addstr(0, 0, linePrintOut)
|
|
@@ -1094,12 +1174,12 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1094
1174
|
for i, line in enumerate(host.output[-(host_window_height - 1):]):
|
|
1095
1175
|
# print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
|
|
1096
1176
|
# time.sleep(10)
|
|
1097
|
-
linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
|
|
1177
|
+
linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip().ljust(host_window_width - 1, ' ')
|
|
1098
1178
|
host_window.addstr(i + 1, 0, linePrintOut)
|
|
1099
1179
|
# we draw the rest of the available lines
|
|
1100
1180
|
for i in range(len(host.output), host_window_height - 1):
|
|
1101
1181
|
# print(f"Printng a line at {i + 1} with length of {len('│')}")
|
|
1102
|
-
host_window.addstr(i + 1, 0, '│')
|
|
1182
|
+
host_window.addstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '))
|
|
1103
1183
|
host.printedLines = len(host.output)
|
|
1104
1184
|
host_window.refresh()
|
|
1105
1185
|
except Exception as e:
|
|
@@ -1107,17 +1187,12 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1107
1187
|
# print(str(e).strip())
|
|
1108
1188
|
# print(traceback.format_exc().strip())
|
|
1109
1189
|
if org_dim != stdscr.getmaxyx():
|
|
1110
|
-
|
|
1190
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1111
1191
|
new_configured = False
|
|
1112
1192
|
last_refresh_time = time.perf_counter()
|
|
1113
|
-
|
|
1114
|
-
except ZeroDivisionError:
|
|
1115
|
-
# terminial is too small, we skip the display
|
|
1116
|
-
pass
|
|
1117
1193
|
except Exception as e:
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
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
|
|
1121
1196
|
|
|
1122
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):
|
|
1123
1198
|
'''
|
|
@@ -1134,6 +1209,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
1134
1209
|
# We create all the windows we need
|
|
1135
1210
|
# We initialize the color pair
|
|
1136
1211
|
curses.start_color()
|
|
1212
|
+
curses.curs_set(0)
|
|
1137
1213
|
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
|
1138
1214
|
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
|
1139
1215
|
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
@@ -1154,7 +1230,20 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
1154
1230
|
curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
|
|
1155
1231
|
curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
|
|
1156
1232
|
curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
|
1157
|
-
|
|
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
|
+
|
|
1158
1247
|
|
|
1159
1248
|
def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
1160
1249
|
'''
|
|
@@ -1216,7 +1305,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1216
1305
|
rtnStr = ''
|
|
1217
1306
|
for output, hosts in outputs.items():
|
|
1218
1307
|
if __global_suppress_printout:
|
|
1219
|
-
rtnStr += f'
|
|
1308
|
+
rtnStr += f'Abnormal returncode produced by {hosts}:\n'
|
|
1220
1309
|
rtnStr += output+'\n'
|
|
1221
1310
|
else:
|
|
1222
1311
|
rtnStr += '*'*80+'\n'
|
|
@@ -1280,10 +1369,10 @@ def signal_handler(sig, frame):
|
|
|
1280
1369
|
'''
|
|
1281
1370
|
global _emo
|
|
1282
1371
|
if not _emo:
|
|
1283
|
-
|
|
1372
|
+
eprint('Ctrl C caught, exiting...')
|
|
1284
1373
|
_emo = True
|
|
1285
1374
|
else:
|
|
1286
|
-
|
|
1375
|
+
eprint('Ctrl C caught again, exiting immediately!')
|
|
1287
1376
|
# wait for 0.1 seconds to allow the threads to exit
|
|
1288
1377
|
time.sleep(0.1)
|
|
1289
1378
|
os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
@@ -1373,7 +1462,8 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1373
1462
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1374
1463
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1375
1464
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1376
|
-
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
|
|
1465
|
+
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
|
|
1466
|
+
shortend = False):
|
|
1377
1467
|
hosts = hosts if type(hosts) == str else frozenset(hosts)
|
|
1378
1468
|
hostStr = formHostStr(hosts)
|
|
1379
1469
|
files = frozenset(files) if files else None
|
|
@@ -1391,7 +1481,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1391
1481
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1392
1482
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1393
1483
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1394
|
-
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY):
|
|
1484
|
+
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False):
|
|
1395
1485
|
f'''
|
|
1396
1486
|
Run the command on the hosts, aka multissh. main function
|
|
1397
1487
|
|
|
@@ -1423,6 +1513,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1423
1513
|
min_line_len (int, optional): The minimum line number for each window of the curses output. Defaults to {DEFAULT_CURSES_MINIMUM_LINE_LEN}.
|
|
1424
1514
|
single_window (bool, optional): Whether to use a single window for the curses output. Defaults to {DEFAULT_SINGLE_WINDOW}.
|
|
1425
1515
|
file_sync (bool, optional): Whether to use file sync mode to sync directories. Defaults to {DEFAULT_FILE_SYNC}.
|
|
1516
|
+
error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
|
|
1517
|
+
quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
|
|
1426
1518
|
|
|
1427
1519
|
Returns:
|
|
1428
1520
|
list: A list of Host objects
|
|
@@ -1445,6 +1537,14 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1445
1537
|
max_connections = (-max_connections) * os.cpu_count()
|
|
1446
1538
|
if not commands:
|
|
1447
1539
|
commands = []
|
|
1540
|
+
else:
|
|
1541
|
+
commands = [commands] if type(commands) == str else commands
|
|
1542
|
+
# reformat commands into a list of strings, join the iterables if they are not strings
|
|
1543
|
+
try:
|
|
1544
|
+
commands = [' '.join(command) if not type(command) == str else command for command in commands]
|
|
1545
|
+
except:
|
|
1546
|
+
pass
|
|
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.")
|
|
1448
1548
|
#verify_ssh_config()
|
|
1449
1549
|
# load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
|
|
1450
1550
|
if called:
|
|
@@ -1464,7 +1564,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1464
1564
|
else:
|
|
1465
1565
|
unavailableHosts = set()
|
|
1466
1566
|
skipUnreachable = True
|
|
1467
|
-
|
|
1567
|
+
if quiet:
|
|
1568
|
+
__global_suppress_printout = True
|
|
1468
1569
|
# We create the hosts
|
|
1469
1570
|
hostStr = formHostStr(hosts)
|
|
1470
1571
|
skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
|
|
@@ -1486,7 +1587,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1486
1587
|
targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
|
|
1487
1588
|
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
|
|
1488
1589
|
if skipHostsList:
|
|
1489
|
-
|
|
1590
|
+
eprint(f"Skipping hosts: {skipHostsList}")
|
|
1490
1591
|
if files and not commands:
|
|
1491
1592
|
# if files are specified but not target dir, we default to file sync mode
|
|
1492
1593
|
file_sync = True
|
|
@@ -1503,7 +1604,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1503
1604
|
except:
|
|
1504
1605
|
pathSet.update(glob.glob(file,recursive=True))
|
|
1505
1606
|
if not pathSet:
|
|
1506
|
-
|
|
1607
|
+
eprint(f'Warning: No source files at {files} are found after resolving globs!')
|
|
1507
1608
|
sys.exit(66)
|
|
1508
1609
|
else:
|
|
1509
1610
|
pathSet = set(files)
|
|
@@ -1516,16 +1617,16 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1516
1617
|
if oneonone:
|
|
1517
1618
|
hosts = []
|
|
1518
1619
|
if len(commands) != len(targetHostsList) - len(skipHostsList):
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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)}")
|
|
1522
1623
|
sys.exit(255)
|
|
1523
1624
|
if not __global_suppress_printout:
|
|
1524
|
-
|
|
1525
|
-
|
|
1625
|
+
eprint('-'*80)
|
|
1626
|
+
eprint("Running in one on one mode")
|
|
1526
1627
|
for host, command in zip(targetHostsList, commands):
|
|
1527
1628
|
if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
|
|
1528
|
-
|
|
1629
|
+
eprint(f"Skipping unavailable host: {host}")
|
|
1529
1630
|
continue
|
|
1530
1631
|
if host.strip() in skipHostsList: continue
|
|
1531
1632
|
if file_sync:
|
|
@@ -1533,7 +1634,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1533
1634
|
else:
|
|
1534
1635
|
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1535
1636
|
if not __global_suppress_printout:
|
|
1536
|
-
|
|
1637
|
+
eprint(f"Running command: {command} on host: {host}")
|
|
1537
1638
|
if not __global_suppress_printout: print('-'*80)
|
|
1538
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)
|
|
1539
1640
|
return hosts
|
|
@@ -1548,20 +1649,20 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1548
1649
|
continue
|
|
1549
1650
|
if host.strip() in skipHostsList: continue
|
|
1550
1651
|
if file_sync:
|
|
1551
|
-
|
|
1652
|
+
eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
|
|
1552
1653
|
return []
|
|
1553
1654
|
elif files:
|
|
1554
|
-
|
|
1655
|
+
eprint(f"Error: files need to be specified with at least one path to sync")
|
|
1555
1656
|
elif ipmi:
|
|
1556
|
-
|
|
1657
|
+
eprint(f"Error: ipmi mode is not supported in interactive mode")
|
|
1557
1658
|
else:
|
|
1558
1659
|
hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
|
|
1559
1660
|
if not __global_suppress_printout:
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1661
|
+
eprint('-'*80)
|
|
1662
|
+
eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
1663
|
+
eprint('-'*80)
|
|
1563
1664
|
if no_start:
|
|
1564
|
-
|
|
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.")
|
|
1565
1666
|
else:
|
|
1566
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)
|
|
1567
1668
|
return hosts
|
|
@@ -1577,9 +1678,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1577
1678
|
else:
|
|
1578
1679
|
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1579
1680
|
if not __global_suppress_printout and len(commands) > 1:
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1681
|
+
eprint('-'*80)
|
|
1682
|
+
eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
1683
|
+
eprint('-'*80)
|
|
1583
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)
|
|
1584
1685
|
allHosts += hosts
|
|
1585
1686
|
return allHosts
|
|
@@ -1653,13 +1754,13 @@ def main():
|
|
|
1653
1754
|
# We parse the arguments
|
|
1654
1755
|
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: {CONFIG_FILE}')
|
|
1655
1756
|
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)
|
|
1656
|
-
parser.add_argument('commands', metavar='commands', type=str, nargs='
|
|
1757
|
+
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.')
|
|
1657
1758
|
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)
|
|
1658
1759
|
parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
|
|
1659
1760
|
parser.add_argument('-ea','--extraargs',type=str,help=f'Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: {DEFAULT_EXTRA_ARGS})',default=DEFAULT_EXTRA_ARGS)
|
|
1660
1761
|
parser.add_argument("-11",'--oneonone', action='store_true', help=f"Run one corresponding command on each host. (default: {DEFAULT_ONE_ON_ONE})", default=DEFAULT_ONE_ON_ONE)
|
|
1661
1762
|
parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
|
|
1662
|
-
parser.add_argument('--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 as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
|
|
1763
|
+
parser.add_argument('-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 as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
|
|
1663
1764
|
parser.add_argument('--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)
|
|
1664
1765
|
parser.add_argument('-gm','--gather_mode', action='store_true', help=f'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)
|
|
1665
1766
|
#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")
|
|
@@ -1693,41 +1794,41 @@ def main():
|
|
|
1693
1794
|
if args.store_config_file:
|
|
1694
1795
|
try:
|
|
1695
1796
|
if os.path.exists(CONFIG_FILE):
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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")
|
|
1700
1801
|
inStr = input_with_timeout_and_countdown(10)
|
|
1701
1802
|
if (not inStr) or inStr.lower().strip().startswith('b'):
|
|
1702
1803
|
write_default_config(args,CONFIG_FILE,backup = True)
|
|
1703
|
-
|
|
1804
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1704
1805
|
elif inStr.lower().strip().startswith('o'):
|
|
1705
1806
|
write_default_config(args,CONFIG_FILE,backup = False)
|
|
1706
|
-
|
|
1807
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1707
1808
|
else:
|
|
1708
1809
|
write_default_config(args,CONFIG_FILE,backup = True)
|
|
1709
|
-
|
|
1810
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1710
1811
|
except Exception as e:
|
|
1711
|
-
|
|
1812
|
+
eprint(f"Error while writing config file: {e}")
|
|
1712
1813
|
if not args.commands:
|
|
1713
1814
|
with open(CONFIG_FILE,'r') as f:
|
|
1714
|
-
|
|
1815
|
+
eprint(f"Config file content: \n{f.read()}")
|
|
1715
1816
|
sys.exit(0)
|
|
1716
1817
|
|
|
1717
1818
|
_env_file = args.env_file
|
|
1718
1819
|
# if there are more than 1 commands, and every command only consists of one word,
|
|
1719
1820
|
# we will ask the user to confirm if they want to run multiple commands or just one command.
|
|
1720
1821
|
if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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")
|
|
1725
1826
|
inStr = input_with_timeout_and_countdown(3)
|
|
1726
1827
|
if (not inStr) or inStr.lower().strip().startswith('1'):
|
|
1727
1828
|
args.commands = [" ".join(args.commands)]
|
|
1728
|
-
|
|
1829
|
+
eprint(f"\nRunning 1 command: {args.commands[0]} on all hosts")
|
|
1729
1830
|
elif inStr.lower().strip().startswith('m'):
|
|
1730
|
-
|
|
1831
|
+
eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
|
|
1731
1832
|
else:
|
|
1732
1833
|
sys.exit(0)
|
|
1733
1834
|
|
|
@@ -1737,7 +1838,7 @@ def main():
|
|
|
1737
1838
|
__global_suppress_printout = False
|
|
1738
1839
|
|
|
1739
1840
|
if not __global_suppress_printout:
|
|
1740
|
-
|
|
1841
|
+
eprint('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1741
1842
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
1742
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,
|
|
1743
1844
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
@@ -1747,10 +1848,10 @@ def main():
|
|
|
1747
1848
|
|
|
1748
1849
|
for i in range(args.repeat):
|
|
1749
1850
|
if args.interval > 0 and i < args.repeat - 1:
|
|
1750
|
-
|
|
1851
|
+
eprint(f"Sleeping for {args.interval} seconds")
|
|
1751
1852
|
time.sleep(args.interval)
|
|
1752
1853
|
|
|
1753
|
-
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
|
|
1754
1855
|
hosts = run_command_on_hosts(args.hosts,args.commands,
|
|
1755
1856
|
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1756
1857
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
@@ -1759,7 +1860,7 @@ def main():
|
|
|
1759
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)
|
|
1760
1861
|
#print('*'*80)
|
|
1761
1862
|
|
|
1762
|
-
if not __global_suppress_printout:
|
|
1863
|
+
if not __global_suppress_printout: eprint('-'*80)
|
|
1763
1864
|
|
|
1764
1865
|
succeededHosts = set()
|
|
1765
1866
|
for host in hosts:
|
|
@@ -1773,18 +1874,18 @@ def main():
|
|
|
1773
1874
|
__failedHosts = sorted(__failedHosts)
|
|
1774
1875
|
succeededHosts = sorted(succeededHosts)
|
|
1775
1876
|
if __mainReturnCode > 0:
|
|
1776
|
-
if not __global_suppress_printout:
|
|
1877
|
+
if not __global_suppress_printout: eprint(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
|
|
1777
1878
|
# with open('/tmp/bashcmd.stdin','w') as f:
|
|
1778
1879
|
# f.write(f"export failed_hosts={__failedHosts}\n")
|
|
1779
|
-
if not __global_suppress_printout:
|
|
1880
|
+
if not __global_suppress_printout: eprint(f'failed_hosts: {",".join(__failedHosts)}')
|
|
1780
1881
|
else:
|
|
1781
|
-
if not __global_suppress_printout:
|
|
1882
|
+
if not __global_suppress_printout: eprint('Complete. All hosts returned 0.')
|
|
1782
1883
|
|
|
1783
1884
|
if args.success_hosts and not __global_suppress_printout:
|
|
1784
|
-
|
|
1885
|
+
eprint(f'succeeded_hosts: {",".join(succeededHosts)}')
|
|
1785
1886
|
|
|
1786
1887
|
if threading.active_count() > 1:
|
|
1787
|
-
if not __global_suppress_printout:
|
|
1888
|
+
if not __global_suppress_printout: eprint(f'Remaining active thread: {threading.active_count()}')
|
|
1788
1889
|
# os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
1789
1890
|
# os._exit(mainReturnCode)
|
|
1790
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
|