multiSSH3 4.92__py3-none-any.whl → 4.98__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.

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.98
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=Dm7lML6_oR8yedBdlZCsR-J7Et2Xh5r149y1V666Avw,93039
2
+ multiSSH3-4.98.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-4.98.dist-info/METADATA,sha256=hkcvqm3j6w0i24rVinCJMhoxax1btQbHoCaqG-SnJx0,16043
4
+ multiSSH3-4.98.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
+ multiSSH3-4.98.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-4.98.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-4.98.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.92'
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
- 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)
@@ -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
- print('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
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
- print(f"Error: {hostname} is not a valid hostname or IP address!")
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
- print(f"{prompt} [{timeout}s]: ", end='', flush=True)
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
- print(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
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
- print(f"Warning: {len(__keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
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
- 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)
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
- #host.stderr.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
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, 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):
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
- # raise zero division error if the terminal is too small
987
+ # return True if the terminal is too small
910
988
  if max_x < 2 or max_y < 2:
911
- raise ZeroDivisionError
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
- raise ZeroDivisionError
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
- raise ZeroDivisionError
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
- raise ZeroDivisionError
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
- raise ZeroDivisionError
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
- raise Exception('Terminal size changed. Please reconfigure window.')
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
- # 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.
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
- raise Exception('Terminal size changed. Please reconfigure window.')
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)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']}"[:max_x - 2].center(max_x - 2, "━")
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)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']}"[:max_x - 2].center(max_x - 2, "─")
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
- raise Exception('Terminal size changed. Please reconfigure window.')
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
- stdscr.clear()
1123
- stdscr.refresh()
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
- generate_display(stdscr, hosts, threads,min_char_len = min_char_len, min_line_len = min_line_len, single_window = single_window)
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
- print('Ctrl C caught, exiting...')
1387
+ eprint('Ctrl C caught, exiting...')
1289
1388
  _emo = True
1290
1389
  else:
1291
- print('Ctrl C caught again, exiting immediately!')
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
- 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.")
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
- if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
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
- print(f'Warning: No source files at {files} are found after resolving globs!')
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
- 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)}")
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
- print('-'*80)
1542
- print("Running in one on one mode")
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
- if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
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
- print(f"Running command: {command} on host: {host}")
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
- print(f"Error: file sync mode need to be specified with at least one path to sync.")
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
- print(f"Error: files need to be specified with at least one path to sync")
1677
+ eprint(f"Error: files need to be specified with at least one path to sync")
1572
1678
  elif ipmi:
1573
- print(f"Error: ipmi mode is not supported in interactive mode")
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
- print('-'*80)
1578
- print(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
1579
- print('-'*80)
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
- print(f"Warning: no_start is set, the command will not be started. As we are in interactive mode, no action will be done.")
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
- print('-'*80)
1598
- print(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
1599
- print('-'*80)
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='+',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.')
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
- 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")
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
- print(f"Config file written to {CONFIG_FILE}")
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
- print(f"Config file written to {CONFIG_FILE}")
1845
+ eprint(f"Config file written to {CONFIG_FILE}")
1724
1846
  else:
1725
1847
  write_default_config(args,CONFIG_FILE,backup = True)
1726
- print(f"Config file written to {CONFIG_FILE}")
1848
+ eprint(f"Config file written to {CONFIG_FILE}")
1727
1849
  except Exception as e:
1728
- print(f"Error while writing config file: {e}")
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
- print(f"Config file content: \n{f.read()}")
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
- 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")
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
- print(f"\nRunning 1 command: {args.commands[0]} on all hosts")
1868
+ eprint(f"\nRunning 1 command: {args.commands[0]} on all hosts")
1746
1869
  elif inStr.lower().strip().startswith('m'):
1747
- print(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
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
- print('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
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
- print(f"Sleeping for {args.interval} seconds")
1890
+ eprint(f"Sleeping for {args.interval} seconds")
1768
1891
  time.sleep(args.interval)
1769
1892
 
1770
- if not __global_suppress_printout: print(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
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: print('-'*80)
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: print(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
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: print(f'failed_hosts: {",".join(__failedHosts)}')
1919
+ if not __global_suppress_printout: eprint(f'failed_hosts: {",".join(__failedHosts)}')
1797
1920
  else:
1798
- if not __global_suppress_printout: print('Complete. All hosts returned 0.')
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
- print(f'succeeded_hosts: {",".join(succeededHosts)}')
1924
+ eprint(f'succeeded_hosts: {",".join(succeededHosts)}')
1802
1925
 
1803
1926
  if threading.active_count() > 1:
1804
- if not __global_suppress_printout: print(f'Remaining active thread: {threading.active_count()}')
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
 
@@ -1,7 +0,0 @@
1
- multiSSH3.py,sha256=XGiNVv-ZsYMGFVrZ8zpeK4wz170Fi8YRtltcRxrQQCI,88186
2
- multiSSH3-4.92.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- multiSSH3-4.92.dist-info/METADATA,sha256=lesBhivYV4dahRGYUWvo-jbxcOEhEl6VHhSjQ1U-7-M,16043
4
- multiSSH3-4.92.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
- multiSSH3-4.92.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
- multiSSH3-4.92.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
- multiSSH3-4.92.dist-info/RECORD,,