multiSSH3 4.89__py3-none-any.whl → 4.97__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multiSSH3
3
- Version: 4.89
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
@@ -0,0 +1,7 @@
1
+ multiSSH3.py,sha256=WCp2vui9NMeviCPe39CVu9WY0-lOn3YEIzFGO8i8m7o,91417
2
+ multiSSH3-4.97.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-4.97.dist-info/METADATA,sha256=ndgCxl2LHcQ3x88vqxJhTOd6ah7U46-Xva-Uj74oBzU,16043
4
+ multiSSH3-4.97.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
+ multiSSH3-4.97.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-4.97.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-4.97.dist-info/RECORD,,
multiSSH3.py CHANGED
@@ -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.89'
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
@@ -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
- 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!")
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
- print(f"{prompt} [{timeout}s]: ", end='', flush=True)
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
- print(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
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
- 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!")
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
- if "#HOST#" in host.command.upper() or "#HOSTNAME#" in host.command.upper():
627
- host.command = host.command.replace("#HOST#",host.address).replace("#HOSTNAME#",host.address).replace("#host#",host.address).replace("#hostname#",host.address)
628
- if "#USER#" in host.command.upper() or "#USERNAME#" in host.command.upper():
629
- if host.username:
630
- host.command = host.command.replace("#USER#",host.username).replace("#USERNAME#",host.username).replace("#user#",host.username).replace("#username#",host.username)
631
- else:
632
- current_user = getpass.getuser()
633
- 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)
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
- start_time = time.time()
754
- outLength = len(host.output)
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() - start_time > timeout:
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() - start_time > min(10, timeout // 2):
769
- timeoutLine = f'Timeout in [{timeout - int(time.time() - start_time)}] seconds!'
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
- outLength = len(host.output)
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, 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):
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
- # raise zero division error if the terminal is too small
972
+ # return True if the terminal is too small
906
973
  if max_x < 2 or max_y < 2:
907
- raise ZeroDivisionError
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
- raise ZeroDivisionError
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
- raise ZeroDivisionError
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
- raise ZeroDivisionError
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
- raise ZeroDivisionError
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
- 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)
983
1067
  # We handle positional keys
984
- # uparrow: 259; downarrow: 258; leftarrow: 260; rightarrow: 261
985
- # pageup: 339; pagedown: 338; home: 262; end: 360
986
- elif key in [259, 258, 260, 261, 339, 338, 262, 360]:
987
- # if the key is up arrow, we will move the line to display up
988
- if key == 259: # 259 is the key code for up arrow
989
- lineToDisplay = max(lineToDisplay - 1, -len(__keyPressesIn))
990
- # if the key is down arrow, we will move the line to display down
991
- elif key == 258: # 258 is the key code for down arrow
992
- lineToDisplay = min(lineToDisplay + 1, -1)
993
- # if the key is left arrow, we will move the cursor left
994
- elif key == 260: # 260 is the key code for left arrow
995
- curserPosition = min(max(curserPosition - 1, 0), len(__keyPressesIn[lineToDisplay]) -1)
996
- # if the key is right arrow, we will move the cursor right
997
- elif key == 261: # 261 is the key code for right arrow
998
- curserPosition = max(min(curserPosition + 1, len(__keyPressesIn[lineToDisplay])), 0)
999
- # if the key is page up, we will move the line to display up by 5 lines
1000
- elif key == 339: # 339 is the key code for page up
1001
- lineToDisplay = max(lineToDisplay - 5, -len(__keyPressesIn))
1002
- # if the key is page down, we will move the line to display down by 5 lines
1003
- elif key == 338: # 338 is the key code for page down
1004
- lineToDisplay = min(lineToDisplay + 5, -1)
1005
- # if the key is home, we will move the cursor to the beginning of the line
1006
- elif key == 262: # 262 is the key code for home
1007
- curserPosition = 0
1008
- # if the key is end, we will move the cursor to the end of the line
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
- raise Exception('Terminal size changed. Please reconfigure window.')
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)} 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, "━")
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)} 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, "─")
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
- raise Exception('Terminal size changed. Please reconfigure window.')
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
- stdscr.clear()
1119
- stdscr.refresh()
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
- 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
+
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'Error returncode produced by {hosts}:\n'
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
- print('Ctrl C caught, exiting...')
1372
+ eprint('Ctrl C caught, exiting...')
1284
1373
  _emo = True
1285
1374
  else:
1286
- print('Ctrl C caught again, exiting immediately!')
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, shortend = False):
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
- if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
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
- 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!')
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
- print("Error: the number of commands must be the same as the number of hosts")
1520
- print(f"Number of commands: {len(commands)}")
1521
- 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)}")
1522
1623
  sys.exit(255)
1523
1624
  if not __global_suppress_printout:
1524
- print('-'*80)
1525
- print("Running in one on one mode")
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
- if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
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
- print(f"Running command: {command} on host: {host}")
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
- 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.")
1552
1653
  return []
1553
1654
  elif files:
1554
- 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")
1555
1656
  elif ipmi:
1556
- print(f"Error: ipmi mode is not supported in interactive mode")
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
- print('-'*80)
1561
- print(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
1562
- print('-'*80)
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
- 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.")
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
- print('-'*80)
1581
- print(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
1582
- print('-'*80)
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='*',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.')
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
- print(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
1697
- print(f"o: Overwrite the file")
1698
- print(f"b: Rename the current config file at {CONFIG_FILE}.bak forcefully and write the new config file (default)")
1699
- 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")
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
- print(f"Config file written to {CONFIG_FILE}")
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
- print(f"Config file written to {CONFIG_FILE}")
1807
+ eprint(f"Config file written to {CONFIG_FILE}")
1707
1808
  else:
1708
1809
  write_default_config(args,CONFIG_FILE,backup = True)
1709
- print(f"Config file written to {CONFIG_FILE}")
1810
+ eprint(f"Config file written to {CONFIG_FILE}")
1710
1811
  except Exception as e:
1711
- print(f"Error while writing config file: {e}")
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
- print(f"Config file content: \n{f.read()}")
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
- print(f"Multiple one word command detected, what to do? (1/m/n)")
1722
- print(f"1: Run 1 command [{' '.join(args.commands)}] on all hosts ( default )")
1723
- print(f"m: Run multiple commands [{', '.join(args.commands)}] on all hosts")
1724
- 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")
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
- print(f"\nRunning 1 command: {args.commands[0]} on all hosts")
1829
+ eprint(f"\nRunning 1 command: {args.commands[0]} on all hosts")
1729
1830
  elif inStr.lower().strip().startswith('m'):
1730
- print(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
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
- 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,
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
- print(f"Sleeping for {args.interval} seconds")
1851
+ eprint(f"Sleeping for {args.interval} seconds")
1751
1852
  time.sleep(args.interval)
1752
1853
 
1753
- 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
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: print('-'*80)
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: 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}')
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: print(f'failed_hosts: {",".join(__failedHosts)}')
1880
+ if not __global_suppress_printout: eprint(f'failed_hosts: {",".join(__failedHosts)}')
1780
1881
  else:
1781
- if not __global_suppress_printout: print('Complete. All hosts returned 0.')
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
- print(f'succeeded_hosts: {",".join(succeededHosts)}')
1885
+ eprint(f'succeeded_hosts: {",".join(succeededHosts)}')
1785
1886
 
1786
1887
  if threading.active_count() > 1:
1787
- 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()}')
1788
1889
  # os.system(f'pkill -ef {os.path.basename(__file__)}')
1789
1890
  # os._exit(mainReturnCode)
1790
1891
 
@@ -1,7 +0,0 @@
1
- multiSSH3.py,sha256=g_U5Kk6pDABART5Tb4yoViPZ-5fiARpb_8Irvs7f2QA,86990
2
- multiSSH3-4.89.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- multiSSH3-4.89.dist-info/METADATA,sha256=J9K4EyDzcP7Pvj4p779YAkYz7vbDGfQoxz9hmkWRra8,16043
4
- multiSSH3-4.89.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
- multiSSH3-4.89.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
- multiSSH3-4.89.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
- multiSSH3-4.89.dist-info/RECORD,,