multiSSH3 4.92__tar.gz → 4.97__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of multiSSH3 might be problematic. Click here for more details.

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