multiSSH3 4.92__tar.gz → 4.98__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.98}/PKG-INFO +1 -1
- {multissh3-4.92 → multissh3-4.98}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-4.92 → multissh3-4.98}/multiSSH3.py +238 -115
- {multissh3-4.92 → multissh3-4.98}/setup.py +1 -1
- {multissh3-4.92 → multissh3-4.98}/LICENSE +0 -0
- {multissh3-4.92 → multissh3-4.98}/README.md +0 -0
- {multissh3-4.92 → multissh3-4.98}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-4.92 → multissh3-4.98}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-4.92 → multissh3-4.98}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-4.92 → multissh3-4.98}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-4.92 → multissh3-4.98}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-4.92 → multissh3-4.98}/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.98'
|
|
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)
|
|
@@ -105,6 +115,7 @@ __build_in_default_config = {
|
|
|
105
115
|
'_rsyncPath': None,
|
|
106
116
|
'_bashPath': None,
|
|
107
117
|
'__ERROR_MESSAGES_TO_IGNORE_REGEX':None,
|
|
118
|
+
'__DEBUG_MODE': False,
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
AUTHOR = __configs_from_file.get('AUTHOR', __build_in_default_config['AUTHOR'])
|
|
@@ -153,17 +164,34 @@ _DEFAULT_NO_START = __configs_from_file.get('_DEFAULT_NO_START', __build_in_defa
|
|
|
153
164
|
# form the regex from the list
|
|
154
165
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = __configs_from_file.get('__ERROR_MESSAGES_TO_IGNORE_REGEX', __build_in_default_config['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
|
|
155
166
|
if __ERROR_MESSAGES_TO_IGNORE_REGEX:
|
|
156
|
-
|
|
167
|
+
eprint('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
|
|
157
168
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__configs_from_file['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
|
|
158
169
|
else:
|
|
159
170
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
|
|
160
171
|
|
|
172
|
+
__DEBUG_MODE = __configs_from_file.get('__DEBUG_MODE', __build_in_default_config['__DEBUG_MODE'])
|
|
173
|
+
|
|
161
174
|
|
|
162
175
|
|
|
163
176
|
__global_suppress_printout = True
|
|
164
177
|
|
|
165
178
|
__mainReturnCode = 0
|
|
166
179
|
__failedHosts = set()
|
|
180
|
+
__host_i_lock = threading.Lock()
|
|
181
|
+
__host_i_counter = -1
|
|
182
|
+
def get_i():
|
|
183
|
+
'''
|
|
184
|
+
Get the global counter for the host objects
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
int: The global counter for the host objects
|
|
188
|
+
'''
|
|
189
|
+
global __host_i_counter
|
|
190
|
+
global __host_i_lock
|
|
191
|
+
with __host_i_lock:
|
|
192
|
+
__host_i_counter += 1
|
|
193
|
+
return __host_i_counter
|
|
194
|
+
|
|
167
195
|
class Host:
|
|
168
196
|
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False):
|
|
169
197
|
self.name = name # the name of the host (hostname or IP address)
|
|
@@ -181,11 +209,15 @@ class Host:
|
|
|
181
209
|
self.gatherMode = gatherMode # whether the host is in gather mode
|
|
182
210
|
self.extraargs = extraargs # extra arguments to be passed to ssh
|
|
183
211
|
self.resolvedName = None # the resolved IP address of the host
|
|
212
|
+
# also store a globally unique integer i from 0
|
|
213
|
+
self.i = get_i()
|
|
214
|
+
self.uuid = uuid.uuid4()
|
|
215
|
+
|
|
184
216
|
def __iter__(self):
|
|
185
217
|
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
186
218
|
def __repr__(self):
|
|
187
219
|
# 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})"
|
|
220
|
+
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
221
|
def __str__(self):
|
|
190
222
|
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
|
|
191
223
|
|
|
@@ -489,7 +521,7 @@ def validate_expand_hostname(hostname):
|
|
|
489
521
|
elif getIP(hostname,local=False):
|
|
490
522
|
return [hostname]
|
|
491
523
|
else:
|
|
492
|
-
|
|
524
|
+
eprint(f"Error: {hostname} is not a valid hostname or IP address!")
|
|
493
525
|
global __mainReturnCode
|
|
494
526
|
__mainReturnCode += 1
|
|
495
527
|
global __failedHosts
|
|
@@ -509,14 +541,14 @@ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selectio
|
|
|
509
541
|
"""
|
|
510
542
|
import select
|
|
511
543
|
# Print the initial prompt with the countdown
|
|
512
|
-
|
|
544
|
+
eprint(f"{prompt} [{timeout}s]: ", end='', flush=True)
|
|
513
545
|
# Loop until the timeout
|
|
514
546
|
for remaining in range(timeout, 0, -1):
|
|
515
547
|
# If there is an input, return it
|
|
516
548
|
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
|
|
517
549
|
return input().strip()
|
|
518
550
|
# Print the remaining time
|
|
519
|
-
|
|
551
|
+
eprint(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
|
|
520
552
|
# Wait a second
|
|
521
553
|
time.sleep(1)
|
|
522
554
|
# If there is no input, return None
|
|
@@ -590,7 +622,7 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
590
622
|
else:
|
|
591
623
|
time.sleep(0.1)
|
|
592
624
|
if sentInput < len(__keyPressesIn) - 1 :
|
|
593
|
-
|
|
625
|
+
eprint(f"Warning: {len(__keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
|
|
594
626
|
# # send the last line
|
|
595
627
|
# if __keyPressesIn and __keyPressesIn[-1]:
|
|
596
628
|
# stream.write(''.join(__keyPressesIn[-1]).encode())
|
|
@@ -598,6 +630,34 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
598
630
|
# host.output.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
599
631
|
# host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
600
632
|
return sentInput
|
|
633
|
+
|
|
634
|
+
def replace_magic_strings(string,keys,value,case_sensitive=False):
|
|
635
|
+
'''
|
|
636
|
+
Replace the magic strings in the host object
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
string (str): The string to replace the magic strings
|
|
640
|
+
keys (list): Search for keys to replace
|
|
641
|
+
value (str): The value to replace the key
|
|
642
|
+
case_sensitive (bool, optional): Whether to search for the keys in a case sensitive way. Defaults to False.
|
|
643
|
+
|
|
644
|
+
Returns:
|
|
645
|
+
str: The string with the magic strings replaced
|
|
646
|
+
'''
|
|
647
|
+
# verify magic strings have # at the beginning and end
|
|
648
|
+
newKeys = []
|
|
649
|
+
for key in keys:
|
|
650
|
+
if key.startswith('#') and key.endswith('#'):
|
|
651
|
+
newKeys.append(key)
|
|
652
|
+
else:
|
|
653
|
+
newKeys.append('#'+key.strip('#')+'#')
|
|
654
|
+
# replace the magic strings
|
|
655
|
+
for key in newKeys:
|
|
656
|
+
if case_sensitive:
|
|
657
|
+
string = string.replace(key,value)
|
|
658
|
+
else:
|
|
659
|
+
string = re.sub(re.escape(key),value,string,flags=re.IGNORECASE)
|
|
660
|
+
return string
|
|
601
661
|
|
|
602
662
|
def ssh_command(host, sem, timeout=60,passwds=None):
|
|
603
663
|
'''
|
|
@@ -616,6 +676,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
616
676
|
global __ERROR_MESSAGES_TO_IGNORE_REGEX
|
|
617
677
|
global __ipmiiInterfaceIPPrefix
|
|
618
678
|
global _binPaths
|
|
679
|
+
global __DEBUG_MODE
|
|
619
680
|
try:
|
|
620
681
|
keyCheckArgs = []
|
|
621
682
|
rsyncKeyCheckArgs = []
|
|
@@ -626,14 +687,18 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
626
687
|
host.address = host.name
|
|
627
688
|
if '@' in host.name:
|
|
628
689
|
host.username, host.address = host.name.rsplit('@',1)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
690
|
+
host.command = replace_magic_strings(host.command,['#HOST#','#HOSTNAME#'],host.address,case_sensitive=False)
|
|
691
|
+
if host.username:
|
|
692
|
+
host.command = replace_magic_strings(host.command,['#USER#','#USERNAME#'],host.username,case_sensitive=False)
|
|
693
|
+
else:
|
|
694
|
+
current_user = getpass.getuser()
|
|
695
|
+
host.command = replace_magic_strings(host.command,['#USER#','#USERNAME#'],current_user,case_sensitive=False)
|
|
696
|
+
host.command = replace_magic_strings(host.command,['#ID#'],str(id(host)),case_sensitive=False)
|
|
697
|
+
host.command = replace_magic_strings(host.command,['#I#'],str(host.i),case_sensitive=False)
|
|
698
|
+
host.command = replace_magic_strings(host.command,['#PASSWD#','#PASSWORD#'],passwds,case_sensitive=False)
|
|
699
|
+
if host.resolvedName:
|
|
700
|
+
host.command = replace_magic_strings(host.command,['#RESOLVEDNAME#','#RESOLVED#'],host.resolvedName,case_sensitive=False)
|
|
701
|
+
host.command = replace_magic_strings(host.command,['#UUID#'],str(host.uuid),case_sensitive=False)
|
|
637
702
|
formatedCMD = []
|
|
638
703
|
if host.extraargs and type(host.extraargs) == str:
|
|
639
704
|
extraargs = host.extraargs.split()
|
|
@@ -674,6 +739,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
674
739
|
formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}'] + extraargs + [host.command]
|
|
675
740
|
elif 'ssh' in _binPaths:
|
|
676
741
|
host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
|
|
742
|
+
if __DEBUG_MODE:
|
|
743
|
+
host.stderr.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
|
|
677
744
|
host.ipmi = False
|
|
678
745
|
host.interface_ip_prefix = None
|
|
679
746
|
host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
|
|
@@ -691,6 +758,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
691
758
|
useScp = True
|
|
692
759
|
elif 'rsync' in _binPaths:
|
|
693
760
|
host.output.append('scp not found on the local machine! Trying to use rsync...')
|
|
761
|
+
if __DEBUG_MODE:
|
|
762
|
+
host.stderr.append('scp not found on the local machine! Trying to use rsync...')
|
|
694
763
|
useScp = False
|
|
695
764
|
else:
|
|
696
765
|
host.output.append('scp not found on the local machine! Please install scp or rsync to use file sync mode.')
|
|
@@ -701,6 +770,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
701
770
|
useScp = False
|
|
702
771
|
elif 'scp' in _binPaths:
|
|
703
772
|
host.output.append('rsync not found on the local machine! Trying to use scp...')
|
|
773
|
+
if __DEBUG_MODE:
|
|
774
|
+
host.stderr.append('rsync not found on the local machine! Trying to use scp...')
|
|
704
775
|
useScp = True
|
|
705
776
|
else:
|
|
706
777
|
host.output.append('rsync not found on the local machine! Please install rsync or scp to use file sync mode.')
|
|
@@ -721,7 +792,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
721
792
|
formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
|
|
722
793
|
elif passwds:
|
|
723
794
|
host.output.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
|
|
724
|
-
|
|
795
|
+
if __DEBUG_MODE:
|
|
796
|
+
host.stderr.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
|
|
725
797
|
host.output.append('Please provide password via live input or use ssh key authentication.')
|
|
726
798
|
# # try to send the password via __keyPressesIn
|
|
727
799
|
# __keyPressesIn[-1] = list(passwds) + ['\n']
|
|
@@ -739,6 +811,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
739
811
|
with sem:
|
|
740
812
|
try:
|
|
741
813
|
host.output.append('Running command: '+' '.join(formatedCMD))
|
|
814
|
+
if __DEBUG_MODE:
|
|
815
|
+
host.stderr.append('Running command: '+' '.join(formatedCMD))
|
|
742
816
|
#host.stdout = []
|
|
743
817
|
proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
|
|
744
818
|
# create a thread to handle stdout
|
|
@@ -827,6 +901,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
827
901
|
if host.ipmi and host.returncode != 0 and any(['Unable to establish IPMI' in line for line in host.stderr]):
|
|
828
902
|
host.stderr = []
|
|
829
903
|
host.output.append('IPMI connection failed! Trying SSH connection...')
|
|
904
|
+
if __DEBUG_MODE:
|
|
905
|
+
host.stderr.append('IPMI connection failed! Trying SSH connection...')
|
|
830
906
|
host.ipmi = False
|
|
831
907
|
host.interface_ip_prefix = None
|
|
832
908
|
host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
|
|
@@ -836,6 +912,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
836
912
|
host.stderr = []
|
|
837
913
|
host.stdout = []
|
|
838
914
|
host.output.append('Rsync connection failed! Trying SCP connection...')
|
|
915
|
+
if __DEBUG_MODE:
|
|
916
|
+
host.stderr.append('Rsync connection failed! Trying SCP connection...')
|
|
839
917
|
host.scp = True
|
|
840
918
|
ssh_command(host,sem,timeout,passwds)
|
|
841
919
|
|
|
@@ -892,7 +970,7 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
|
892
970
|
host.printedLines = 0
|
|
893
971
|
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
894
972
|
|
|
895
|
-
def generate_display(stdscr, hosts,
|
|
973
|
+
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
974
|
try:
|
|
897
975
|
org_dim = stdscr.getmaxyx()
|
|
898
976
|
new_configured = True
|
|
@@ -906,11 +984,11 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
906
984
|
if single_window:
|
|
907
985
|
min_char_len_local = max_x-1
|
|
908
986
|
min_line_len_local = max_y-1
|
|
909
|
-
#
|
|
987
|
+
# return True if the terminal is too small
|
|
910
988
|
if max_x < 2 or max_y < 2:
|
|
911
|
-
|
|
989
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
912
990
|
if min_char_len_local < 1 or min_line_len_local < 1:
|
|
913
|
-
|
|
991
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
914
992
|
# We need to figure out how many hosts we can fit in the terminal
|
|
915
993
|
# We will need at least 2 lines per host, one for its name, one for its output
|
|
916
994
|
# Each line will be at least 61 characters long (60 for the output, 1 for the borders)
|
|
@@ -918,10 +996,10 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
918
996
|
max_num_hosts_y = max_y // (min_line_len_local + 1)
|
|
919
997
|
max_num_hosts = max_num_hosts_x * max_num_hosts_y
|
|
920
998
|
if max_num_hosts < 1:
|
|
921
|
-
|
|
999
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
922
1000
|
hosts_to_display , host_stats = get_hosts_to_display(hosts, max_num_hosts)
|
|
923
1001
|
if len(hosts_to_display) == 0:
|
|
924
|
-
|
|
1002
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
925
1003
|
# Now we calculate the actual number of hosts we will display for x and y
|
|
926
1004
|
optimal_len_x = max(min_char_len_local, 80)
|
|
927
1005
|
num_hosts_x = max(min(max_num_hosts_x, max_x // optimal_len_x),1)
|
|
@@ -942,7 +1020,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
942
1020
|
host_window_height = max_y // num_hosts_y
|
|
943
1021
|
host_window_width = max_x // num_hosts_x
|
|
944
1022
|
if host_window_height < 1 or host_window_width < 1:
|
|
945
|
-
|
|
1023
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
946
1024
|
|
|
947
1025
|
old_stat = ''
|
|
948
1026
|
old_bottom_stat = ''
|
|
@@ -983,36 +1061,50 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
983
1061
|
# with open('keylog.txt','a') as f:
|
|
984
1062
|
# f.write(str(key)+'\n')
|
|
985
1063
|
if key == 410: # 410 is the key code for resize
|
|
986
|
-
|
|
1064
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1065
|
+
elif key == 95 and not __keyPressesIn[-1]: # 95 is the key code for _
|
|
1066
|
+
# if last line is empty, we will reconfigure the wh to be smaller
|
|
1067
|
+
if min_line_len != 1:
|
|
1068
|
+
return (lineToDisplay,curserPosition , min_char_len , max(min_line_len -1,1), single_window)
|
|
1069
|
+
elif key == 43 and not __keyPressesIn[-1]: # 43 is the key code for +
|
|
1070
|
+
# if last line is empty, we will reconfigure the wh to be larger
|
|
1071
|
+
return (lineToDisplay,curserPosition , min_char_len , min_line_len +1, single_window)
|
|
1072
|
+
elif key == 123 and not __keyPressesIn[-1]: # 123 is the key code for {
|
|
1073
|
+
# if last line is empty, we will reconfigure the ww to be smaller
|
|
1074
|
+
if min_char_len != 1:
|
|
1075
|
+
return (lineToDisplay,curserPosition , max(min_char_len -1,1), min_line_len, single_window)
|
|
1076
|
+
elif key == 124 and not __keyPressesIn[-1]: # 124 is the key code for |
|
|
1077
|
+
# if last line is empty, we will toggle the single window mode
|
|
1078
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, not single_window)
|
|
1079
|
+
elif key == 125 and not __keyPressesIn[-1]: # 125 is the key code for }
|
|
1080
|
+
# if last line is empty, we will reconfigure the ww to be larger
|
|
1081
|
+
return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window)
|
|
987
1082
|
# 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.
|
|
1083
|
+
# if the key is up arrow, we will move the line to display up
|
|
1084
|
+
elif key == 259: # 259 is the key code for up arrow
|
|
1085
|
+
lineToDisplay = max(lineToDisplay - 1, -len(__keyPressesIn))
|
|
1086
|
+
# if the key is down arrow, we will move the line to display down
|
|
1087
|
+
elif key == 258: # 258 is the key code for down arrow
|
|
1088
|
+
lineToDisplay = min(lineToDisplay + 1, -1)
|
|
1089
|
+
# if the key is left arrow, we will move the cursor left
|
|
1090
|
+
elif key == 260: # 260 is the key code for left arrow
|
|
1091
|
+
curserPosition = min(max(curserPosition - 1, 0), len(__keyPressesIn[lineToDisplay]) -1)
|
|
1092
|
+
# if the key is right arrow, we will move the cursor right
|
|
1093
|
+
elif key == 261: # 261 is the key code for right arrow
|
|
1094
|
+
curserPosition = max(min(curserPosition + 1, len(__keyPressesIn[lineToDisplay])), 0)
|
|
1095
|
+
# if the key is page up, we will move the line to display up by 5 lines
|
|
1096
|
+
elif key == 339: # 339 is the key code for page up
|
|
1097
|
+
lineToDisplay = max(lineToDisplay - 5, -len(__keyPressesIn))
|
|
1098
|
+
# if the key is page down, we will move the line to display down by 5 lines
|
|
1099
|
+
elif key == 338: # 338 is the key code for page down
|
|
1100
|
+
lineToDisplay = min(lineToDisplay + 5, -1)
|
|
1101
|
+
# if the key is home, we will move the cursor to the beginning of the line
|
|
1102
|
+
elif key == 262: # 262 is the key code for home
|
|
1103
|
+
curserPosition = 0
|
|
1104
|
+
# if the key is end, we will move the cursor to the end of the line
|
|
1105
|
+
elif key == 360: # 360 is the key code for end
|
|
1106
|
+
curserPosition = len(__keyPressesIn[lineToDisplay])
|
|
1107
|
+
# We are left with these are keys that mofidy the current line.
|
|
1016
1108
|
else:
|
|
1017
1109
|
# This means the user have done scrolling and is committing to modify the current line.
|
|
1018
1110
|
if lineToDisplay < -1:
|
|
@@ -1044,12 +1136,11 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1044
1136
|
__keyPressesIn[lineToDisplay].insert(curserPosition, chr(key))
|
|
1045
1137
|
curserPosition += 1
|
|
1046
1138
|
# reconfigure when the terminal size changes
|
|
1047
|
-
# raise Exception when max_y or max_x is changed, let parent handle reconfigure
|
|
1048
1139
|
if org_dim != stdscr.getmaxyx():
|
|
1049
|
-
|
|
1140
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1050
1141
|
# We generate the aggregated stats if user did not input anything
|
|
1051
1142
|
if not __keyPressesIn[lineToDisplay]:
|
|
1052
|
-
stats = '┍'+ f"Total: {len(hosts)}
|
|
1143
|
+
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
1144
|
else:
|
|
1054
1145
|
# we use the stat bar to display the key presses
|
|
1055
1146
|
encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
|
|
@@ -1060,7 +1151,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1060
1151
|
# encodedLine = encodedLine[:curserPosition] + ' ' + encodedLine[curserPosition:]
|
|
1061
1152
|
stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
|
|
1062
1153
|
if bottom_border:
|
|
1063
|
-
bottom_stats = '└'+ f"Total: {len(hosts)}
|
|
1154
|
+
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
1155
|
if bottom_stats != old_bottom_stat:
|
|
1065
1156
|
old_bottom_stat = bottom_stats
|
|
1066
1157
|
#bottom_border.clear()
|
|
@@ -1111,17 +1202,12 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1111
1202
|
# print(str(e).strip())
|
|
1112
1203
|
# print(traceback.format_exc().strip())
|
|
1113
1204
|
if org_dim != stdscr.getmaxyx():
|
|
1114
|
-
|
|
1205
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1115
1206
|
new_configured = False
|
|
1116
1207
|
last_refresh_time = time.perf_counter()
|
|
1117
|
-
|
|
1118
|
-
except ZeroDivisionError:
|
|
1119
|
-
# terminial is too small, we skip the display
|
|
1120
|
-
pass
|
|
1121
1208
|
except Exception as e:
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
generate_display(stdscr, hosts, threads, lineToDisplay, curserPosition, min_char_len, min_line_len, single_window)
|
|
1209
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window)
|
|
1210
|
+
return None
|
|
1125
1211
|
|
|
1126
1212
|
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
1213
|
'''
|
|
@@ -1159,7 +1245,20 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
1159
1245
|
curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
|
|
1160
1246
|
curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
|
|
1161
1247
|
curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
|
1162
|
-
|
|
1248
|
+
params = (-1,0 , min_char_len, min_line_len, single_window)
|
|
1249
|
+
while params:
|
|
1250
|
+
params = generate_display(stdscr, hosts, *params)
|
|
1251
|
+
if not params:
|
|
1252
|
+
break
|
|
1253
|
+
if not any([host.returncode is None for host in hosts]):
|
|
1254
|
+
# this means no hosts are running
|
|
1255
|
+
break
|
|
1256
|
+
# print the current configuration
|
|
1257
|
+
stdscr.clear()
|
|
1258
|
+
stdscr.addstr(0, 0, f"Loading Configuration: min_char_len={params[2]}, min_line_len={params[3]}, single_window={params[4]}")
|
|
1259
|
+
stdscr.refresh()
|
|
1260
|
+
#time.sleep(0.25)
|
|
1261
|
+
|
|
1163
1262
|
|
|
1164
1263
|
def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
1165
1264
|
'''
|
|
@@ -1285,10 +1384,10 @@ def signal_handler(sig, frame):
|
|
|
1285
1384
|
'''
|
|
1286
1385
|
global _emo
|
|
1287
1386
|
if not _emo:
|
|
1288
|
-
|
|
1387
|
+
eprint('Ctrl C caught, exiting...')
|
|
1289
1388
|
_emo = True
|
|
1290
1389
|
else:
|
|
1291
|
-
|
|
1390
|
+
eprint('Ctrl C caught again, exiting immediately!')
|
|
1292
1391
|
# wait for 0.1 seconds to allow the threads to exit
|
|
1293
1392
|
time.sleep(0.1)
|
|
1294
1393
|
os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
@@ -1309,6 +1408,8 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
1309
1408
|
# update the unavailable hosts and global unavailable hosts
|
|
1310
1409
|
if willUpdateUnreachableHosts:
|
|
1311
1410
|
unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or host.stderr[0].strip().startswith('Timeout!'))])
|
|
1411
|
+
if __DEBUG_MODE:
|
|
1412
|
+
print(f'Unreachable hosts: {unavailableHosts}')
|
|
1312
1413
|
__globalUnavailableHosts.update(unavailableHosts)
|
|
1313
1414
|
# update the os environment variable if not _no_env
|
|
1314
1415
|
if not _no_env:
|
|
@@ -1439,6 +1540,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1439
1540
|
global __global_suppress_printout
|
|
1440
1541
|
global _no_env
|
|
1441
1542
|
global _emo
|
|
1543
|
+
global __DEBUG_MODE
|
|
1442
1544
|
_emo = False
|
|
1443
1545
|
_no_env = no_env
|
|
1444
1546
|
if not no_env and '__multiSSH3_UNAVAILABLE_HOSTS' in os.environ:
|
|
@@ -1460,7 +1562,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1460
1562
|
commands = [' '.join(command) if not type(command) == str else command for command in commands]
|
|
1461
1563
|
except:
|
|
1462
1564
|
pass
|
|
1463
|
-
|
|
1565
|
+
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
1566
|
#verify_ssh_config()
|
|
1465
1567
|
# load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
|
|
1466
1568
|
if called:
|
|
@@ -1501,9 +1603,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1501
1603
|
skipHostStr[i] = userStr + host
|
|
1502
1604
|
skipHostStr = ','.join(skipHostStr)
|
|
1503
1605
|
targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
|
|
1606
|
+
if __DEBUG_MODE:
|
|
1607
|
+
eprint(f"Target hosts: {targetHostsList}")
|
|
1504
1608
|
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
|
|
1505
1609
|
if skipHostsList:
|
|
1506
|
-
|
|
1610
|
+
eprint(f"Skipping hosts: {skipHostsList}")
|
|
1507
1611
|
if files and not commands:
|
|
1508
1612
|
# if files are specified but not target dir, we default to file sync mode
|
|
1509
1613
|
file_sync = True
|
|
@@ -1520,7 +1624,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1520
1624
|
except:
|
|
1521
1625
|
pathSet.update(glob.glob(file,recursive=True))
|
|
1522
1626
|
if not pathSet:
|
|
1523
|
-
|
|
1627
|
+
eprint(f'Warning: No source files at {files} are found after resolving globs!')
|
|
1524
1628
|
sys.exit(66)
|
|
1525
1629
|
else:
|
|
1526
1630
|
pathSet = set(files)
|
|
@@ -1530,19 +1634,21 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1530
1634
|
files = []
|
|
1531
1635
|
else:
|
|
1532
1636
|
files = list(pathSet)
|
|
1637
|
+
if __DEBUG_MODE:
|
|
1638
|
+
eprint(f"Files: {files}")
|
|
1533
1639
|
if oneonone:
|
|
1534
1640
|
hosts = []
|
|
1535
1641
|
if len(commands) != len(targetHostsList) - len(skipHostsList):
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1642
|
+
eprint("Error: the number of commands must be the same as the number of hosts")
|
|
1643
|
+
eprint(f"Number of commands: {len(commands)}")
|
|
1644
|
+
eprint(f"Number of hosts: {len(targetHostsList - skipHostsList)}")
|
|
1539
1645
|
sys.exit(255)
|
|
1540
1646
|
if not __global_suppress_printout:
|
|
1541
|
-
|
|
1542
|
-
|
|
1647
|
+
eprint('-'*80)
|
|
1648
|
+
eprint("Running in one on one mode")
|
|
1543
1649
|
for host, command in zip(targetHostsList, commands):
|
|
1544
1650
|
if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
|
|
1545
|
-
|
|
1651
|
+
eprint(f"Skipping unavailable host: {host}")
|
|
1546
1652
|
continue
|
|
1547
1653
|
if host.strip() in skipHostsList: continue
|
|
1548
1654
|
if file_sync:
|
|
@@ -1550,7 +1656,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1550
1656
|
else:
|
|
1551
1657
|
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1552
1658
|
if not __global_suppress_printout:
|
|
1553
|
-
|
|
1659
|
+
eprint(f"Running command: {command} on host: {host}")
|
|
1554
1660
|
if not __global_suppress_printout: print('-'*80)
|
|
1555
1661
|
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
1662
|
return hosts
|
|
@@ -1565,20 +1671,20 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1565
1671
|
continue
|
|
1566
1672
|
if host.strip() in skipHostsList: continue
|
|
1567
1673
|
if file_sync:
|
|
1568
|
-
|
|
1674
|
+
eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
|
|
1569
1675
|
return []
|
|
1570
1676
|
elif files:
|
|
1571
|
-
|
|
1677
|
+
eprint(f"Error: files need to be specified with at least one path to sync")
|
|
1572
1678
|
elif ipmi:
|
|
1573
|
-
|
|
1679
|
+
eprint(f"Error: ipmi mode is not supported in interactive mode")
|
|
1574
1680
|
else:
|
|
1575
1681
|
hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
|
|
1576
1682
|
if not __global_suppress_printout:
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1683
|
+
eprint('-'*80)
|
|
1684
|
+
eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
1685
|
+
eprint('-'*80)
|
|
1580
1686
|
if no_start:
|
|
1581
|
-
|
|
1687
|
+
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
1688
|
else:
|
|
1583
1689
|
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
1690
|
return hosts
|
|
@@ -1594,9 +1700,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1594
1700
|
else:
|
|
1595
1701
|
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1596
1702
|
if not __global_suppress_printout and len(commands) > 1:
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1703
|
+
eprint('-'*80)
|
|
1704
|
+
eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
1705
|
+
eprint('-'*80)
|
|
1600
1706
|
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
1707
|
allHosts += hosts
|
|
1602
1708
|
return allHosts
|
|
@@ -1664,13 +1770,14 @@ def main():
|
|
|
1664
1770
|
global __ipmiiInterfaceIPPrefix
|
|
1665
1771
|
global _binPaths
|
|
1666
1772
|
global _env_file
|
|
1773
|
+
global __DEBUG_MODE
|
|
1667
1774
|
_emo = False
|
|
1668
1775
|
# We handle the signal
|
|
1669
1776
|
signal.signal(signal.SIGINT, signal_handler)
|
|
1670
1777
|
# We parse the arguments
|
|
1671
1778
|
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}')
|
|
1672
1779
|
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)
|
|
1673
|
-
parser.add_argument('commands', metavar='commands', type=str, nargs='
|
|
1780
|
+
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.')
|
|
1674
1781
|
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)
|
|
1675
1782
|
parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
|
|
1676
1783
|
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)
|
|
@@ -1701,50 +1808,66 @@ def main():
|
|
|
1701
1808
|
parser.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
|
|
1702
1809
|
parser.add_argument("-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
|
|
1703
1810
|
parser.add_argument('--store_config_file', action='store_true', help=f'Store / generate the default config file from command line argument and current config at {CONFIG_FILE}')
|
|
1811
|
+
parser.add_argument('--debug', action='store_true', help='Print debug information')
|
|
1812
|
+
parser.add_argument('--copy-id', action='store_true', help='Copy the ssh id to the hosts')
|
|
1704
1813
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
1705
1814
|
|
|
1706
1815
|
# parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
|
|
1707
1816
|
# help='the user to use to connect to the hosts')
|
|
1708
|
-
args = parser.parse_args()
|
|
1817
|
+
#args = parser.parse_args()
|
|
1818
|
+
|
|
1819
|
+
# if python version is 3.7 or higher, use parse_intermixed_args
|
|
1820
|
+
if sys.version_info >= (3,7):
|
|
1821
|
+
args = parser.parse_intermixed_args()
|
|
1822
|
+
else:
|
|
1823
|
+
# try to parse the arguments using parse_known_args
|
|
1824
|
+
args, unknown = parser.parse_known_args()
|
|
1825
|
+
# if there are unknown arguments, we will try to parse them again using parse_args
|
|
1826
|
+
if unknown:
|
|
1827
|
+
eprint(f"Warning: Unknown arguments, treating all as commands: {unknown}")
|
|
1828
|
+
args.commands += unknown
|
|
1829
|
+
|
|
1830
|
+
|
|
1709
1831
|
|
|
1710
1832
|
if args.store_config_file:
|
|
1711
1833
|
try:
|
|
1712
1834
|
if os.path.exists(CONFIG_FILE):
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1835
|
+
eprint(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
|
|
1836
|
+
eprint(f"o: Overwrite the file")
|
|
1837
|
+
eprint(f"b: Rename the current config file at {CONFIG_FILE}.bak forcefully and write the new config file (default)")
|
|
1838
|
+
eprint(f"n: Do nothing")
|
|
1717
1839
|
inStr = input_with_timeout_and_countdown(10)
|
|
1718
1840
|
if (not inStr) or inStr.lower().strip().startswith('b'):
|
|
1719
1841
|
write_default_config(args,CONFIG_FILE,backup = True)
|
|
1720
|
-
|
|
1842
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1721
1843
|
elif inStr.lower().strip().startswith('o'):
|
|
1722
1844
|
write_default_config(args,CONFIG_FILE,backup = False)
|
|
1723
|
-
|
|
1845
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1724
1846
|
else:
|
|
1725
1847
|
write_default_config(args,CONFIG_FILE,backup = True)
|
|
1726
|
-
|
|
1848
|
+
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1727
1849
|
except Exception as e:
|
|
1728
|
-
|
|
1850
|
+
eprint(f"Error while writing config file: {e}")
|
|
1729
1851
|
if not args.commands:
|
|
1730
1852
|
with open(CONFIG_FILE,'r') as f:
|
|
1731
|
-
|
|
1853
|
+
eprint(f"Config file content: \n{f.read()}")
|
|
1732
1854
|
sys.exit(0)
|
|
1733
1855
|
|
|
1734
1856
|
_env_file = args.env_file
|
|
1857
|
+
__DEBUG_MODE = args.debug
|
|
1735
1858
|
# if there are more than 1 commands, and every command only consists of one word,
|
|
1736
1859
|
# we will ask the user to confirm if they want to run multiple commands or just one command.
|
|
1737
1860
|
if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1861
|
+
eprint(f"Multiple one word command detected, what to do? (1/m/n)")
|
|
1862
|
+
eprint(f"1: Run 1 command [{' '.join(args.commands)}] on all hosts ( default )")
|
|
1863
|
+
eprint(f"m: Run multiple commands [{', '.join(args.commands)}] on all hosts")
|
|
1864
|
+
eprint(f"n: Exit")
|
|
1742
1865
|
inStr = input_with_timeout_and_countdown(3)
|
|
1743
1866
|
if (not inStr) or inStr.lower().strip().startswith('1'):
|
|
1744
1867
|
args.commands = [" ".join(args.commands)]
|
|
1745
|
-
|
|
1868
|
+
eprint(f"\nRunning 1 command: {args.commands[0]} on all hosts")
|
|
1746
1869
|
elif inStr.lower().strip().startswith('m'):
|
|
1747
|
-
|
|
1870
|
+
eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
|
|
1748
1871
|
else:
|
|
1749
1872
|
sys.exit(0)
|
|
1750
1873
|
|
|
@@ -1754,7 +1877,7 @@ def main():
|
|
|
1754
1877
|
__global_suppress_printout = False
|
|
1755
1878
|
|
|
1756
1879
|
if not __global_suppress_printout:
|
|
1757
|
-
|
|
1880
|
+
eprint('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1758
1881
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
1759
1882
|
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
1883
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
@@ -1764,10 +1887,10 @@ def main():
|
|
|
1764
1887
|
|
|
1765
1888
|
for i in range(args.repeat):
|
|
1766
1889
|
if args.interval > 0 and i < args.repeat - 1:
|
|
1767
|
-
|
|
1890
|
+
eprint(f"Sleeping for {args.interval} seconds")
|
|
1768
1891
|
time.sleep(args.interval)
|
|
1769
1892
|
|
|
1770
|
-
if not __global_suppress_printout:
|
|
1893
|
+
if not __global_suppress_printout: eprint(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
|
|
1771
1894
|
hosts = run_command_on_hosts(args.hosts,args.commands,
|
|
1772
1895
|
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1773
1896
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
@@ -1776,7 +1899,7 @@ def main():
|
|
|
1776
1899
|
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
1900
|
#print('*'*80)
|
|
1778
1901
|
|
|
1779
|
-
if not __global_suppress_printout:
|
|
1902
|
+
if not __global_suppress_printout: eprint('-'*80)
|
|
1780
1903
|
|
|
1781
1904
|
succeededHosts = set()
|
|
1782
1905
|
for host in hosts:
|
|
@@ -1790,18 +1913,18 @@ def main():
|
|
|
1790
1913
|
__failedHosts = sorted(__failedHosts)
|
|
1791
1914
|
succeededHosts = sorted(succeededHosts)
|
|
1792
1915
|
if __mainReturnCode > 0:
|
|
1793
|
-
if not __global_suppress_printout:
|
|
1916
|
+
if not __global_suppress_printout: eprint(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
|
|
1794
1917
|
# with open('/tmp/bashcmd.stdin','w') as f:
|
|
1795
1918
|
# f.write(f"export failed_hosts={__failedHosts}\n")
|
|
1796
|
-
if not __global_suppress_printout:
|
|
1919
|
+
if not __global_suppress_printout: eprint(f'failed_hosts: {",".join(__failedHosts)}')
|
|
1797
1920
|
else:
|
|
1798
|
-
if not __global_suppress_printout:
|
|
1921
|
+
if not __global_suppress_printout: eprint('Complete. All hosts returned 0.')
|
|
1799
1922
|
|
|
1800
1923
|
if args.success_hosts and not __global_suppress_printout:
|
|
1801
|
-
|
|
1924
|
+
eprint(f'succeeded_hosts: {",".join(succeededHosts)}')
|
|
1802
1925
|
|
|
1803
1926
|
if threading.active_count() > 1:
|
|
1804
|
-
if not __global_suppress_printout:
|
|
1927
|
+
if not __global_suppress_printout: eprint(f'Remaining active thread: {threading.active_count()}')
|
|
1805
1928
|
# os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
1806
1929
|
# os._exit(mainReturnCode)
|
|
1807
1930
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|