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

multiSSH3.py CHANGED
@@ -15,6 +15,9 @@ import io
15
15
  import signal
16
16
  import functools
17
17
  import glob
18
+ import shutil
19
+ import getpass
20
+
18
21
  try:
19
22
  # Check if functiools.cache is available
20
23
  cache_decorator = functools.cache
@@ -26,49 +29,140 @@ except AttributeError:
26
29
  # If neither is available, use a dummy decorator
27
30
  def cache_decorator(func):
28
31
  return func
29
-
30
- version = '4.76'
32
+ version = '4.83'
31
33
  VERSION = version
32
34
 
33
- DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
34
- DEFAULT_USERNAME = None
35
- DEFAULT_EXTRA_ARGS = None
36
- DEFAULT_PASSWORD = ''
37
- DEFAULT_ONE_ON_ONE = False
38
- DEFAULT_FILE_SYNC = False
39
- DEFAULT_SCP = False
40
- DEFAULT_TIMEOUT = 50
41
- DEFAULT_REPEAT = 1
42
- DEFAULT_INTERVAL = 0
43
- DEFAULT_IPMI = False
44
- DEFAULT_INTERFACE_IP_PREFIX = None
45
- DEFAULT_IPMI_INTERFACE_IP_PREFIX = None
46
- DEFAULT_QUIET = False
47
- DEFAULT_ERROR_ONLY = False
48
- DEFAULT_NO_OUTPUT = False
49
- DEFAULT_NO_ENV = False
50
- DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
51
- DEFAULT_JSON_MODE = False
52
- DEFAULT_PRINT_SUCCESS_HOSTS = False
53
- DEFAULT_GREPPABLE_MODE = False
54
- DEFAULT_NO_WATCH = False
55
- DEFAULT_SKIP_UNREACHABLE = False
56
- DEFAULT_SKIP_HOSTS = ''
57
- DEFAULT_CURSES_MINIMUM_CHAR_LEN = 40
58
- DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
59
- DEFAULT_SINGLE_WINDOW = False
60
-
61
- DEFAULT_CALLED = True
62
- DEFAULT_RETURN_UNFINISHED = False
63
- DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
64
- DEFAULT_NO_START = False
65
-
66
- global_suppress_printout = True
67
-
68
- mainReturnCode = 0
69
- failedHosts = set()
35
+ CONFIG_FILE = '/etc/multiSSH3.config.json'
36
+
37
+ def load_config_file(config_file):
38
+ '''
39
+ Load the config file to global variables
40
+
41
+ Args:
42
+ config_file (str): The config file
43
+
44
+ Returns:
45
+ dict: The config
46
+ '''
47
+ if not os.path.exists(config_file):
48
+ return {}
49
+ with open(config_file,'r') as f:
50
+ config = json.load(f)
51
+ return config
52
+
53
+ __configs_from_file = load_config_file(CONFIG_FILE)
54
+
55
+ __build_in_default_config = {
56
+ 'AUTHOR': 'Yufei Pan',
57
+ 'AUTHOR_EMAIL': 'pan@zopyr.us',
58
+ 'DEFAULT_HOSTS': 'all',
59
+ 'DEFAULT_USERNAME': None,
60
+ 'DEFAULT_PASSWORD': '',
61
+ 'DEFAULT_EXTRA_ARGS': None,
62
+ 'DEFAULT_ONE_ON_ONE': False,
63
+ 'DEFAULT_SCP': False,
64
+ 'DEFAULT_FILE_SYNC': False,
65
+ 'DEFAULT_TIMEOUT': 50,
66
+ 'DEFAULT_CLI_TIMEOUT': 0,
67
+ 'DEFAULT_REPEAT': 1,
68
+ 'DEFAULT_INTERVAL': 0,
69
+ 'DEFAULT_IPMI': False,
70
+ 'DEFAULT_IPMI_INTERFACE_IP_PREFIX': '',
71
+ 'DEFAULT_INTERFACE_IP_PREFIX': None,
72
+ 'DEFAULT_NO_WATCH': False,
73
+ 'DEFAULT_CURSES_MINIMUM_CHAR_LEN': 40,
74
+ 'DEFAULT_CURSES_MINIMUM_LINE_LEN': 1,
75
+ 'DEFAULT_SINGLE_WINDOW': False,
76
+ 'DEFAULT_ERROR_ONLY': False,
77
+ 'DEFAULT_NO_OUTPUT': False,
78
+ 'DEFAULT_NO_ENV': False,
79
+ 'DEFAULT_ENV_FILE': '/etc/profile.d/hosts.sh',
80
+ 'DEFAULT_MAX_CONNECTIONS': 4 * os.cpu_count(),
81
+ 'DEFAULT_JSON_MODE': False,
82
+ 'DEFAULT_PRINT_SUCCESS_HOSTS': False,
83
+ 'DEFAULT_GREPPABLE_MODE': False,
84
+ 'DEFAULT_SKIP_UNREACHABLE': False,
85
+ 'DEFAULT_SKIP_HOSTS': '',
86
+ 'ERROR_MESSAGES_TO_IGNORE': [
87
+ 'Pseudo-terminal will not be allocated because stdin is not a terminal',
88
+ 'Connection to .* closed',
89
+ 'Warning: Permanently added',
90
+ 'mux_client_request_session',
91
+ 'disabling multiplexing',
92
+ 'Killed by signal',
93
+ 'Connection reset by peer',
94
+ ],
95
+ '_DEFAULT_CALLED': True,
96
+ '_DEFAULT_RETURN_UNFINISHED': False,
97
+ '_DEFAULT_UPDATE_UNREACHABLE_HOSTS': True,
98
+ '_DEFAULT_NO_START': False,
99
+ '_etc_hosts': {},
100
+ '_sshpassPath': None,
101
+ '_sshPath': None,
102
+ '_scpPath': None,
103
+ '_ipmitoolPath': None,
104
+ '_rsyncPath': None,
105
+ '_bashPath': None,
106
+ '__ERROR_MESSAGES_TO_IGNORE_REGEX':None,
107
+ }
108
+
109
+ AUTHOR = __configs_from_file.get('AUTHOR', __build_in_default_config['AUTHOR'])
110
+ AUTHOR_EMAIL = __configs_from_file.get('AUTHOR_EMAIL', __build_in_default_config['AUTHOR_EMAIL'])
111
+
112
+ DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_config['DEFAULT_HOSTS'])
113
+ DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
114
+ DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
115
+ DEFAULT_PASSWORD = __configs_from_file.get('DEFAULT_PASSWORD', __build_in_default_config['DEFAULT_PASSWORD'])
116
+ DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
117
+ DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
118
+ DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
119
+ DEFAULT_FILE_SYNC = __configs_from_file.get('DEFAULT_FILE_SYNC', __build_in_default_config['DEFAULT_FILE_SYNC'])
120
+ DEFAULT_TIMEOUT = __configs_from_file.get('DEFAULT_TIMEOUT', __build_in_default_config['DEFAULT_TIMEOUT'])
121
+ DEFAULT_CLI_TIMEOUT = __configs_from_file.get('DEFAULT_CLI_TIMEOUT', __build_in_default_config['DEFAULT_CLI_TIMEOUT'])
122
+ DEFAULT_REPEAT = __configs_from_file.get('DEFAULT_REPEAT', __build_in_default_config['DEFAULT_REPEAT'])
123
+ DEFAULT_INTERVAL = __configs_from_file.get('DEFAULT_INTERVAL', __build_in_default_config['DEFAULT_INTERVAL'])
124
+ DEFAULT_IPMI = __configs_from_file.get('DEFAULT_IPMI', __build_in_default_config['DEFAULT_IPMI'])
125
+ DEFAULT_IPMI_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_IPMI_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_IPMI_INTERFACE_IP_PREFIX'])
126
+ DEFAULT_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_INTERFACE_IP_PREFIX'])
127
+ DEFAULT_NO_WATCH = __configs_from_file.get('DEFAULT_NO_WATCH', __build_in_default_config['DEFAULT_NO_WATCH'])
128
+ DEFAULT_CURSES_MINIMUM_CHAR_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_CHAR_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_CHAR_LEN'])
129
+ DEFAULT_CURSES_MINIMUM_LINE_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_LINE_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_LINE_LEN'])
130
+ DEFAULT_SINGLE_WINDOW = __configs_from_file.get('DEFAULT_SINGLE_WINDOW', __build_in_default_config['DEFAULT_SINGLE_WINDOW'])
131
+ DEFAULT_ERROR_ONLY = __configs_from_file.get('DEFAULT_ERROR_ONLY', __build_in_default_config['DEFAULT_ERROR_ONLY'])
132
+ DEFAULT_NO_OUTPUT = __configs_from_file.get('DEFAULT_NO_OUTPUT', __build_in_default_config['DEFAULT_NO_OUTPUT'])
133
+ DEFAULT_NO_ENV = __configs_from_file.get('DEFAULT_NO_ENV', __build_in_default_config['DEFAULT_NO_ENV'])
134
+ DEFAULT_MAX_CONNECTIONS = __configs_from_file.get('DEFAULT_MAX_CONNECTIONS', __build_in_default_config['DEFAULT_MAX_CONNECTIONS'])
135
+ if not DEFAULT_MAX_CONNECTIONS:
136
+ DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
137
+ DEFAULT_JSON_MODE = __configs_from_file.get('DEFAULT_JSON_MODE', __build_in_default_config['DEFAULT_JSON_MODE'])
138
+ DEFAULT_PRINT_SUCCESS_HOSTS = __configs_from_file.get('DEFAULT_PRINT_SUCCESS_HOSTS', __build_in_default_config['DEFAULT_PRINT_SUCCESS_HOSTS'])
139
+ DEFAULT_GREPPABLE_MODE = __configs_from_file.get('DEFAULT_GREPPABLE_MODE', __build_in_default_config['DEFAULT_GREPPABLE_MODE'])
140
+ DEFAULT_SKIP_UNREACHABLE = __configs_from_file.get('DEFAULT_SKIP_UNREACHABLE', __build_in_default_config['DEFAULT_SKIP_UNREACHABLE'])
141
+ DEFAULT_SKIP_HOSTS = __configs_from_file.get('DEFAULT_SKIP_HOSTS', __build_in_default_config['DEFAULT_SKIP_HOSTS'])
142
+
143
+ ERROR_MESSAGES_TO_IGNORE = __configs_from_file.get('ERROR_MESSAGES_TO_IGNORE', __build_in_default_config['ERROR_MESSAGES_TO_IGNORE'])
144
+
145
+ _DEFAULT_CALLED = __configs_from_file.get('_DEFAULT_CALLED', __build_in_default_config['_DEFAULT_CALLED'])
146
+ _DEFAULT_RETURN_UNFINISHED = __configs_from_file.get('_DEFAULT_RETURN_UNFINISHED', __build_in_default_config['_DEFAULT_RETURN_UNFINISHED'])
147
+ _DEFAULT_UPDATE_UNREACHABLE_HOSTS = __configs_from_file.get('_DEFAULT_UPDATE_UNREACHABLE_HOSTS', __build_in_default_config['_DEFAULT_UPDATE_UNREACHABLE_HOSTS'])
148
+ _DEFAULT_NO_START = __configs_from_file.get('_DEFAULT_NO_START', __build_in_default_config['_DEFAULT_NO_START'])
149
+
150
+ # form the regex from the list
151
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = __configs_from_file.get('__ERROR_MESSAGES_TO_IGNORE_REGEX', __build_in_default_config['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
152
+ if __ERROR_MESSAGES_TO_IGNORE_REGEX:
153
+ print('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
154
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__configs_from_file['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
155
+ else:
156
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
157
+
158
+
159
+
160
+ __global_suppress_printout = True
161
+
162
+ __mainReturnCode = 0
163
+ __failedHosts = set()
70
164
  class Host:
71
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None):
165
+ def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False):
72
166
  self.name = name # the name of the host (hostname or IP address)
73
167
  self.command = command # the command to run on the host
74
168
  self.returncode = None # the return code of the command
@@ -80,37 +174,50 @@ class Host:
80
174
  self.ipmi = ipmi # whether to use ipmi to connect to the host
81
175
  self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
82
176
  self.scp = scp # whether to use scp to copy files to the host
177
+ self.gatherMode = gatherMode # whether the host is in gather mode
83
178
  self.extraargs = extraargs # extra arguments to be passed to ssh
84
179
  self.resolvedName = None # the resolved IP address of the host
85
180
  def __iter__(self):
86
181
  return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
87
182
  def __repr__(self):
88
183
  # return the complete data structure
89
- 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}, extraargs={self.extraargs}, resolvedName={self.resolvedName})"
184
+ 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})"
90
185
  def __str__(self):
91
186
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
92
187
 
93
- wildCharacters = ['*','?','x']
188
+ __wildCharacters = ['*','?','x']
94
189
 
95
- gloablUnavailableHosts = set()
190
+ __gloablUnavailableHosts = set()
96
191
 
97
- ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
192
+ __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
98
193
 
99
- keyPressesIn = [[]]
194
+ __keyPressesIn = [[]]
100
195
 
101
- emo = False
196
+ _emo = False
102
197
 
103
- etc_hosts = {}
198
+ _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
104
199
 
105
- env_file = DEFAULT_ENV_FILE
200
+ _env_file = DEFAULT_ENV_FILE
106
201
 
107
202
  # check if command sshpass is available
108
- sshpassAvailable = False
109
- try:
110
- subprocess.run(['which', 'sshpass'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
111
- sshpassAvailable = True
112
- except:
113
- pass
203
+ _binPaths = {}
204
+ def check_path(program_name):
205
+ global __configs_from_file
206
+ global __build_in_default_config
207
+ global _binPaths
208
+ config_key = f'_{program_name}Path'
209
+ program_path = (
210
+ __configs_from_file.get(config_key) or
211
+ __build_in_default_config.get(config_key) or
212
+ shutil.which(program_name)
213
+ )
214
+ if program_path:
215
+ _binPaths[program_name] = program_path
216
+ return True
217
+ return False
218
+
219
+ [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash']]
220
+
114
221
 
115
222
 
116
223
  @cache_decorator
@@ -135,14 +242,14 @@ def expandIPv4Address(hosts):
135
242
  # Handle wildcards
136
243
  octetRange = octet.split('-')
137
244
  for i in range(len(octetRange)):
138
- if not octetRange[i] or octetRange[i] in wildCharacters:
245
+ if not octetRange[i] or octetRange[i] in __wildCharacters:
139
246
  if i == 0:
140
247
  octetRange[i] = '0'
141
248
  elif i == 1:
142
249
  octetRange[i] = '255'
143
250
 
144
251
  expandedOctets.append([str(i) for i in range(int(octetRange[0]),int(octetRange[1])+1)])
145
- elif octet in wildCharacters:
252
+ elif octet in __wildCharacters:
146
253
  expandedOctets.append([str(i) for i in range(0,256)])
147
254
  else:
148
255
  expandedOctets.append([octet])
@@ -171,11 +278,11 @@ def readEnvFromFile(environemnt_file = ''):
171
278
  return env
172
279
  except:
173
280
  env = {}
174
- global env_file
281
+ global _env_file
175
282
  if environemnt_file:
176
283
  envf = environemnt_file
177
284
  else:
178
- envf = env_file if env_file else DEFAULT_ENV_FILE
285
+ envf = _env_file if _env_file else DEFAULT_ENV_FILE
179
286
  if os.path.exists(envf):
180
287
  with open(envf,'r') as f:
181
288
  for line in f:
@@ -200,7 +307,7 @@ def getIP(hostname,local=False):
200
307
  Returns:
201
308
  str: The IP address of the hostname
202
309
  '''
203
- global etc_hosts
310
+ global _etc_hosts
204
311
  # First we check if the hostname is an IP address
205
312
  try:
206
313
  ipaddress.ip_address(hostname)
@@ -208,7 +315,7 @@ def getIP(hostname,local=False):
208
315
  except ValueError:
209
316
  pass
210
317
  # Then we check /etc/hosts
211
- if not etc_hosts and os.path.exists('/etc/hosts'):
318
+ if not _etc_hosts and os.path.exists('/etc/hosts'):
212
319
  with open('/etc/hosts','r') as f:
213
320
  for line in f:
214
321
  if line.startswith('#') or not line.strip():
@@ -219,9 +326,9 @@ def getIP(hostname,local=False):
219
326
  continue
220
327
  ip = chunks[0]
221
328
  for host in chunks[1:]:
222
- etc_hosts[host] = ip
223
- if hostname in etc_hosts:
224
- return etc_hosts[hostname]
329
+ _etc_hosts[host] = ip
330
+ if hostname in _etc_hosts:
331
+ return _etc_hosts[hostname]
225
332
  if local:
226
333
  return None
227
334
  # Then we check the DNS
@@ -375,10 +482,10 @@ def validate_expand_hostname(hostname,no_env=False):
375
482
  return [hostname]
376
483
  else:
377
484
  print(f"Error: {hostname} is not a valid hostname or IP address!")
378
- global mainReturnCode
379
- mainReturnCode += 1
380
- global failedHosts
381
- failedHosts.add(hostname)
485
+ global __mainReturnCode
486
+ __mainReturnCode += 1
487
+ global __failedHosts
488
+ __failedHosts.add(hostname)
382
489
  return []
383
490
 
384
491
  def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
@@ -458,28 +565,28 @@ def handle_writing_stream(stream,stop_event,host):
458
565
  Returns:
459
566
  None
460
567
  '''
461
- global keyPressesIn
462
- # keyPressesIn is a list of lists.
568
+ global __keyPressesIn
569
+ # __keyPressesIn is a list of lists.
463
570
  # Each list is a list of characters to be sent to the stdin of the process at once.
464
571
  # We do not send the last line as it may be incomplete.
465
572
  sentInput = 0
466
573
  while not stop_event.is_set():
467
- if sentInput < len(keyPressesIn) - 1 :
468
- stream.write(''.join(keyPressesIn[sentInput]).encode())
574
+ if sentInput < len(__keyPressesIn) - 1 :
575
+ stream.write(''.join(__keyPressesIn[sentInput]).encode())
469
576
  stream.flush()
470
- host.output.append(' $ ' + ''.join(keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
471
- host.stdout.append(' $ ' + ''.join(keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
577
+ host.output.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
578
+ host.stdout.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
472
579
  sentInput += 1
473
580
  else:
474
581
  time.sleep(0.1)
475
- if sentInput < len(keyPressesIn) - 1 :
476
- print(f"Warning: {len(keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
582
+ if sentInput < len(__keyPressesIn) - 1 :
583
+ print(f"Warning: {len(__keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
477
584
  # # send the last line
478
- # if keyPressesIn and keyPressesIn[-1]:
479
- # stream.write(''.join(keyPressesIn[-1]).encode())
585
+ # if __keyPressesIn and __keyPressesIn[-1]:
586
+ # stream.write(''.join(__keyPressesIn[-1]).encode())
480
587
  # stream.flush()
481
- # host.output.append(' $ ' + ''.join(keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
482
- # host.stdout.append(' $ ' + ''.join(keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
588
+ # host.output.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
589
+ # host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
483
590
  return sentInput
484
591
 
485
592
  def ssh_command(host, sem, timeout=60,passwds=None):
@@ -495,64 +602,127 @@ def ssh_command(host, sem, timeout=60,passwds=None):
495
602
  Returns:
496
603
  None
497
604
  '''
498
- global emo
499
- with sem:
500
- try:
501
- host.username = None
502
- host.address = host.name
503
- if '@' in host.name:
504
- host.username, host.address = host.name.rsplit('@',1)
505
- if "#HOST#" in host.command.upper() or "#HOSTNAME#" in host.command.upper():
506
- host.command = host.command.replace("#HOST#",host.address).replace("#HOSTNAME#",host.address).replace("#host#",host.address).replace("#hostname#",host.address)
507
- if "#USER#" in host.command.upper() or "#USERNAME#" in host.command.upper():
508
- if host.username:
509
- host.command = host.command.replace("#USER#",host.username).replace("#USERNAME#",host.username).replace("#user#",host.username).replace("#username#",host.username)
510
- else:
511
- host.command = host.command.replace("#USER#",'CURRENT_USER').replace("#USERNAME#",'CURRENT_USER').replace("#user#",'CURRENT_USER').replace("#username#",'CURRENT_USER')
512
- formatedCMD = []
513
- if host.extraargs:
514
- extraargs = host.extraargs.split()
515
- else:
516
- extraargs = []
517
- if ipmiiInterfaceIPPrefix:
518
- host.interface_ip_prefix = ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
519
- if host.interface_ip_prefix:
520
- try:
521
- hostOctets = getIP(host.address,local=False).split('.')
522
- prefixOctets = host.interface_ip_prefix.split('.')
523
- host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
524
- host.resolvedName = host.username + '@' if host.username else ''
525
- host.resolvedName += host.address
526
- except:
527
- host.resolvedName = host.name
605
+ global _emo
606
+ global __ERROR_MESSAGES_TO_IGNORE_REGEX
607
+ global __ipmiiInterfaceIPPrefix
608
+ global _binPaths
609
+ try:
610
+ host.username = None
611
+ host.address = host.name
612
+ if '@' in host.name:
613
+ host.username, host.address = host.name.rsplit('@',1)
614
+ if "#HOST#" in host.command.upper() or "#HOSTNAME#" in host.command.upper():
615
+ host.command = host.command.replace("#HOST#",host.address).replace("#HOSTNAME#",host.address).replace("#host#",host.address).replace("#hostname#",host.address)
616
+ if "#USER#" in host.command.upper() or "#USERNAME#" in host.command.upper():
617
+ if host.username:
618
+ host.command = host.command.replace("#USER#",host.username).replace("#USERNAME#",host.username).replace("#user#",host.username).replace("#username#",host.username)
528
619
  else:
620
+ current_user = getpass.getuser()
621
+ host.command = host.command.replace("#USER#",current_user).replace("#USERNAME#",current_user).replace("#user#",current_user).replace("#username#",current_user)
622
+ formatedCMD = []
623
+ if host.extraargs and type(host.extraargs) == str:
624
+ extraargs = host.extraargs.split()
625
+ elif host.extraargs and type(host.extraargs) == list:
626
+ extraargs = [str(arg) for arg in host.extraargs]
627
+ else:
628
+ extraargs = []
629
+ if __ipmiiInterfaceIPPrefix:
630
+ host.interface_ip_prefix = __ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
631
+ if host.interface_ip_prefix:
632
+ try:
633
+ hostOctets = getIP(host.address,local=False).split('.')
634
+ prefixOctets = host.interface_ip_prefix.split('.')
635
+ host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
636
+ host.resolvedName = host.username + '@' if host.username else ''
637
+ host.resolvedName += host.address
638
+ except:
529
639
  host.resolvedName = host.name
530
- if host.ipmi:
640
+ else:
641
+ host.resolvedName = host.name
642
+ if host.ipmi:
643
+ if 'ipmitool' in _binPaths:
531
644
  if host.command.startswith('ipmitool '):
532
645
  host.command = host.command.replace('ipmitool ','')
646
+ elif host.command.startswith(_binPaths['ipmitool']):
647
+ host.command = host.command.replace(_binPaths['ipmitool'],'')
533
648
  if not host.username:
534
649
  host.username = 'admin'
535
- if passwds:
536
- formatedCMD = ['bash','-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
650
+ if 'bash' in _binPaths:
651
+ if passwds:
652
+ formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
653
+ else:
654
+ formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} {" ".join(extraargs)} {host.command}']
537
655
  else:
538
- formatedCMD = ['bash','-c',f'ipmitool -H {host.address} -U {host.username} {" ".join(extraargs)} {host.command}']
656
+ if passwds:
657
+ formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P {passwds}'] + extraargs + [host.command]
658
+ else:
659
+ formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}'] + extraargs + [host.command]
660
+ elif 'ssh' in _binPaths:
661
+ host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
662
+ host.ipmi = False
663
+ host.interface_ip_prefix = None
664
+ host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
665
+ ssh_command(host,sem,timeout,passwds)
666
+ return
539
667
  else:
540
- if host.files:
541
- if host.scp:
542
- formatedCMD = ['scp','-rpB'] + extraargs +['--']+host.files+[f'{host.resolvedName}:{host.command}']
543
- else:
544
- formatedCMD = ['rsync','-ahlX','--partial','--inplace', '--info=name'] + extraargs +['--']+host.files+[f'{host.resolvedName}:{host.command}']
668
+ host.output.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
669
+ host.stderr.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
670
+ host.returncode = 1
671
+ return
672
+ else:
673
+ if host.files:
674
+ if host.scp:
675
+ if 'scp' in _binPaths:
676
+ useScp = True
677
+ elif 'rsync' in _binPaths:
678
+ host.output.append('scp not found on the local machine! Trying to use rsync...')
679
+ useScp = False
680
+ else:
681
+ host.output.append('scp not found on the local machine! Please install scp or rsync to use file sync mode.')
682
+ host.stderr.append('scp not found on the local machine! Please install scp or rsync to use file sync mode.')
683
+ host.returncode = 1
684
+ return
685
+ elif 'rsync' in _binPaths:
686
+ useScp = False
687
+ elif 'scp' in _binPaths:
688
+ host.output.append('rsync not found on the local machine! Trying to use scp...')
689
+ useScp = True
690
+ else:
691
+ host.output.append('rsync not found on the local machine! Please install rsync or scp to use file sync mode.')
692
+ host.stderr.append('rsync not found on the local machine! Please install rsync or scp to use file sync mode.')
693
+ host.returncode = 1
694
+ return
695
+ if host.gatherMode:
696
+ fileArgs = [f'{host.resolvedName}:{file}' for file in host.files] + [host.command]
697
+ else:
698
+ fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
699
+ if useScp:
700
+ formatedCMD = [_binPaths['scp'],'-rpB'] + extraargs +['--']+fileArgs
545
701
  else:
546
- formatedCMD = ['ssh'] + extraargs +['--']+ [host.resolvedName, host.command]
547
- if passwds and sshpassAvailable:
548
- formatedCMD = ['sshpass', '-p', passwds] + formatedCMD
549
- elif passwds:
550
- host.output.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
551
- #host.stderr.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
552
- host.output.append('Please provide password via live input or use ssh key authentication.')
553
- # # try to send the password via keyPressesIn
554
- # keyPressesIn[-1] = list(passwds) + ['\n']
555
- # keyPressesIn.append([])
702
+ formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + extraargs +['--']+fileArgs
703
+ else:
704
+ formatedCMD = [_binPaths['ssh']] + extraargs +['--']+ [host.resolvedName, host.command]
705
+ if passwds and 'sshpass' in _binPaths:
706
+ formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
707
+ elif passwds:
708
+ host.output.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
709
+ #host.stderr.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
710
+ host.output.append('Please provide password via live input or use ssh key authentication.')
711
+ # # try to send the password via __keyPressesIn
712
+ # __keyPressesIn[-1] = list(passwds) + ['\n']
713
+ # __keyPressesIn.append([])
714
+ except Exception as e:
715
+ import traceback
716
+ host.output.append(f'Error occurred while formatting the command : {host.command}!')
717
+ host.stderr.append(f'Error occurred while formatting the command : {host.command}!')
718
+ host.stderr.extend(str(e).split('\n'))
719
+ host.output.extend(str(e).split('\n'))
720
+ host.stderr.extend(traceback.format_exc().split('\n'))
721
+ host.output.extend(traceback.format_exc().split('\n'))
722
+ host.returncode = -1
723
+ return
724
+ with sem:
725
+ try:
556
726
  host.output.append('Running command: '+' '.join(formatedCMD))
557
727
  #host.stdout = []
558
728
  proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
@@ -592,7 +762,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
592
762
  host.printedLines -= 1
593
763
  host.output.append(timeoutLine)
594
764
  outLength = len(host.output)
595
- if emo:
765
+ if _emo:
596
766
  host.stderr.append('Ctrl C detected, Emergency Stop!')
597
767
  host.output.append('Ctrl C detected, Emergency Stop!')
598
768
  proc.send_signal(signal.SIGINT)
@@ -607,7 +777,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
607
777
  stdin_thread.join(timeout=1)
608
778
  # here we handle the rest of the stdout after the subprocess returns
609
779
  host.output.append(f'Pipe Closed. Trying to read the rest of the stdout...')
610
- if not emo:
780
+ if not _emo:
611
781
  stdout = None
612
782
  stderr = None
613
783
  try:
@@ -627,8 +797,9 @@ def ssh_command(host, sem, timeout=60,passwds=None):
627
797
  elif host.stderr and host.stderr[-1].strip().startswith('Ctrl C detected, Emergency Stop!'):
628
798
  host.returncode = 137
629
799
  host.output.append(f'Command finished with return code {host.returncode}')
630
- if host.stderr and host.stderr[-1].strip().startswith('Connection to ') and host.stderr[-1].strip().endswith(' closed.'):
631
- host.stderr.pop()
800
+ if host.stderr:
801
+ # filter out the error messages that we want to ignore
802
+ host.stderr = [line for line in host.stderr if not __ERROR_MESSAGES_TO_IGNORE_REGEX.search(line)]
632
803
  except Exception as e:
633
804
  import traceback
634
805
  host.stderr.extend(str(e).split('\n'))
@@ -645,7 +816,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
645
816
  host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
646
817
  ssh_command(host,sem,timeout,passwds)
647
818
  # If transfering files, we will try again using scp if rsync connection is not successful
648
- if host.files and not host.scp and host.returncode != 0 and host.stderr:
819
+ if host.files and not host.scp and not useScp and host.returncode != 0 and host.stderr:
649
820
  host.stderr = []
650
821
  host.stdout = []
651
822
  host.output.append('Rsync connection failed! Trying SCP connection...')
@@ -764,7 +935,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
764
935
  last_refresh_time = time.perf_counter()
765
936
  stdscr.clear()
766
937
  #host_window.refresh()
767
- global keyPressesIn
938
+ global __keyPressesIn
768
939
  stdscr.nodelay(True)
769
940
  # we generate a stats window at the top of the screen
770
941
  stat_window = curses.newwin(1, max_x, 0, 0)
@@ -803,19 +974,19 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
803
974
  elif key in [259, 258, 260, 261, 339, 338, 262, 360]:
804
975
  # if the key is up arrow, we will move the line to display up
805
976
  if key == 259: # 259 is the key code for up arrow
806
- lineToDisplay = max(lineToDisplay - 1, -len(keyPressesIn))
977
+ lineToDisplay = max(lineToDisplay - 1, -len(__keyPressesIn))
807
978
  # if the key is down arrow, we will move the line to display down
808
979
  elif key == 258: # 258 is the key code for down arrow
809
980
  lineToDisplay = min(lineToDisplay + 1, -1)
810
981
  # if the key is left arrow, we will move the cursor left
811
982
  elif key == 260: # 260 is the key code for left arrow
812
- curserPosition = min(max(curserPosition - 1, 0), len(keyPressesIn[lineToDisplay]) -1)
983
+ curserPosition = min(max(curserPosition - 1, 0), len(__keyPressesIn[lineToDisplay]) -1)
813
984
  # if the key is right arrow, we will move the cursor right
814
985
  elif key == 261: # 261 is the key code for right arrow
815
- curserPosition = max(min(curserPosition + 1, len(keyPressesIn[lineToDisplay])), 0)
986
+ curserPosition = max(min(curserPosition + 1, len(__keyPressesIn[lineToDisplay])), 0)
816
987
  # if the key is page up, we will move the line to display up by 5 lines
817
988
  elif key == 339: # 339 is the key code for page up
818
- lineToDisplay = max(lineToDisplay - 5, -len(keyPressesIn))
989
+ lineToDisplay = max(lineToDisplay - 5, -len(__keyPressesIn))
819
990
  # if the key is page down, we will move the line to display down by 5 lines
820
991
  elif key == 338: # 338 is the key code for page down
821
992
  lineToDisplay = min(lineToDisplay + 5, -1)
@@ -824,48 +995,48 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
824
995
  curserPosition = 0
825
996
  # if the key is end, we will move the cursor to the end of the line
826
997
  elif key == 360: # 360 is the key code for end
827
- curserPosition = len(keyPressesIn[lineToDisplay])
998
+ curserPosition = len(__keyPressesIn[lineToDisplay])
828
999
  # We are left with these are keys that mofidy the current line.
829
1000
  else:
830
1001
  # This means the user have done scrolling and is committing to modify the current line.
831
1002
  if lineToDisplay < -1:
832
1003
  # We overwrite the last line (current working line) with the line to display, removing the newline at the end
833
- keyPressesIn[-1] = keyPressesIn[lineToDisplay][:-1]
1004
+ __keyPressesIn[-1] = __keyPressesIn[lineToDisplay][:-1]
834
1005
  lineToDisplay = -1
835
- curserPosition = max(0, min(curserPosition, len(keyPressesIn[lineToDisplay])))
1006
+ curserPosition = max(0, min(curserPosition, len(__keyPressesIn[lineToDisplay])))
836
1007
  if key == 10: # 10 is the key code for newline
837
- keyPressesIn[-1].append(chr(key))
838
- keyPressesIn.append([])
1008
+ __keyPressesIn[-1].append(chr(key))
1009
+ __keyPressesIn.append([])
839
1010
  lineToDisplay = -1
840
1011
  curserPosition = 0
841
1012
  # if the key is backspace, we will remove the last character from the last list
842
1013
  elif key in [8,263]: # 8 is the key code for backspace
843
1014
  if curserPosition > 0:
844
- keyPressesIn[lineToDisplay].pop(curserPosition - 1)
1015
+ __keyPressesIn[lineToDisplay].pop(curserPosition - 1)
845
1016
  curserPosition -= 1
846
1017
  # if the key is ESC, we will clear the last list
847
1018
  elif key == 27: # 27 is the key code for ESC
848
- keyPressesIn[-1] = []
1019
+ __keyPressesIn[-1] = []
849
1020
  curserPosition = 0
850
1021
  # ignore delete key
851
1022
  elif key in [127, 330]: # 330 is the key code for delete key
852
1023
  # delete the character at the cursor position
853
- if curserPosition < len(keyPressesIn[lineToDisplay]):
854
- keyPressesIn[lineToDisplay].pop(curserPosition)
1024
+ if curserPosition < len(__keyPressesIn[lineToDisplay]):
1025
+ __keyPressesIn[lineToDisplay].pop(curserPosition)
855
1026
  else:
856
1027
  # if the key is not a special key, we will add it
857
- keyPressesIn[lineToDisplay].insert(curserPosition, chr(key))
1028
+ __keyPressesIn[lineToDisplay].insert(curserPosition, chr(key))
858
1029
  curserPosition += 1
859
1030
  # reconfigure when the terminal size changes
860
1031
  # raise Exception when max_y or max_x is changed, let parent handle reconfigure
861
1032
  if org_dim != stdscr.getmaxyx():
862
1033
  raise Exception('Terminal size changed. Please reconfigure window.')
863
1034
  # We generate the aggregated stats if user did not input anything
864
- if not keyPressesIn[lineToDisplay]:
1035
+ if not __keyPressesIn[lineToDisplay]:
865
1036
  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, "━")
866
1037
  else:
867
1038
  # we use the stat bar to display the key presses
868
- encodedLine = ''.join(keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
1039
+ encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
869
1040
  # # add the flashing indicator at the curse position
870
1041
  # if time.perf_counter() % 1 > 0.5:
871
1042
  # encodedLine = encodedLine[:curserPosition] + '█' + encodedLine[curserPosition:]
@@ -985,8 +1156,8 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
985
1156
  Returns:
986
1157
  str: The pretty output generated
987
1158
  '''
988
- global keyPressesIn
989
- global global_suppress_printout
1159
+ global __keyPressesIn
1160
+ global __global_suppress_printout
990
1161
  hosts = [dict(host) for host in hosts]
991
1162
  if usejson:
992
1163
  # [print(dict(host)) for host in hosts]
@@ -1009,14 +1180,14 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1009
1180
  rtnStr = ''
1010
1181
  for output, hosts in outputs.items():
1011
1182
  rtnStr += f"{','.join(hosts)}{output}\n"
1012
- if keyPressesIn[-1]:
1013
- CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in keyPressesIn if cmd]
1183
+ if __keyPressesIn[-1]:
1184
+ CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
1014
1185
  rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
1015
1186
  #rtnStr += '\n'
1016
1187
  else:
1017
1188
  outputs = {}
1018
1189
  for host in hosts:
1019
- if global_suppress_printout:
1190
+ if __global_suppress_printout:
1020
1191
  if host['returncode'] == 0:
1021
1192
  continue
1022
1193
  hostPrintOut = f" Command:\n {host['command']}\n"
@@ -1032,24 +1203,24 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1032
1203
  outputs[hostPrintOut].append(host['name'])
1033
1204
  rtnStr = ''
1034
1205
  for output, hosts in outputs.items():
1035
- if global_suppress_printout:
1206
+ if __global_suppress_printout:
1036
1207
  rtnStr += f'Error returncode produced by {hosts}:\n'
1037
1208
  rtnStr += output+'\n'
1038
1209
  else:
1039
1210
  rtnStr += '*'*80+'\n'
1040
1211
  rtnStr += f"These hosts: {hosts} have a response of:\n"
1041
1212
  rtnStr += output+'\n'
1042
- if not global_suppress_printout or outputs:
1213
+ if not __global_suppress_printout or outputs:
1043
1214
  rtnStr += '*'*80+'\n'
1044
- if keyPressesIn[-1]:
1045
- CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in keyPressesIn if cmd]
1046
- #rtnStr += f"Key presses: {''.join(keyPressesIn).encode('unicode_escape').decode()}\n"
1047
- #rtnStr += f"Key presses: {keyPressesIn}\n"
1215
+ if __keyPressesIn[-1]:
1216
+ CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
1217
+ #rtnStr += f"Key presses: {''.join(__keyPressesIn).encode('unicode_escape').decode()}\n"
1218
+ #rtnStr += f"Key presses: {__keyPressesIn}\n"
1048
1219
  rtnStr += "User Inputs: \n "
1049
1220
  rtnStr += '\n '.join(CMDsOut)
1050
1221
  rtnStr += '\n'
1051
- keyPressesIn = [[]]
1052
- if global_suppress_printout and not outputs:
1222
+ __keyPressesIn = [[]]
1223
+ if __global_suppress_printout and not outputs:
1053
1224
  rtnStr += 'Success'
1054
1225
  if not quiet:
1055
1226
  print(rtnStr)
@@ -1095,10 +1266,10 @@ def signal_handler(sig, frame):
1095
1266
  Returns:
1096
1267
  None
1097
1268
  '''
1098
- global emo
1099
- if not emo:
1269
+ global _emo
1270
+ if not _emo:
1100
1271
  print('Ctrl C caught, exiting...')
1101
- emo = True
1272
+ _emo = True
1102
1273
  else:
1103
1274
  print('Ctrl C caught again, exiting immediately!')
1104
1275
  # wait for 0.1 seconds to allow the threads to exit
@@ -1107,10 +1278,10 @@ def signal_handler(sig, frame):
1107
1278
  sys.exit(0)
1108
1279
 
1109
1280
 
1110
- def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, quiet, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
1111
- global gloablUnavailableHosts
1281
+ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
1282
+ global __gloablUnavailableHosts
1112
1283
  threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
1113
- if not quiet and threads and not returnUnfinished and any([thread.is_alive() for thread in threads]) and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
1284
+ if not nowatch and threads and not returnUnfinished and any([thread.is_alive() for thread in threads]) and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
1114
1285
  curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
1115
1286
  if not returnUnfinished:
1116
1287
  # wait until all hosts have a return code
@@ -1121,7 +1292,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1121
1292
  # update the unavailable hosts and global unavailable hosts
1122
1293
  if willUpdateUnreachableHosts:
1123
1294
  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!'))])
1124
- gloablUnavailableHosts.update(unavailableHosts)
1295
+ __gloablUnavailableHosts.update(unavailableHosts)
1125
1296
  # print the output, if the output of multiple hosts are the same, we aggragate them
1126
1297
  if not called:
1127
1298
  print_output(hosts,json,greppable=greppable)
@@ -1152,9 +1323,9 @@ def formHostStr(host) -> str:
1152
1323
 
1153
1324
  @cache_decorator
1154
1325
  def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1155
- quiet = DEFAULT_QUIET,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
1326
+ nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
1156
1327
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
1157
- scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1328
+ scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1158
1329
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
1159
1330
  file_sync = False, error_only = DEFAULT_ERROR_ONLY,
1160
1331
  shortend = False) -> str:
@@ -1162,13 +1333,14 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
1162
1333
  if oneonone: argsList.append('--oneonone' if not shortend else '-11')
1163
1334
  if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
1164
1335
  if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
1165
- if quiet: argsList.append('--quiet' if not shortend else '-q')
1336
+ if nowatch: argsList.append('--nowatch' if not shortend else '-q')
1166
1337
  if json: argsList.append('--json' if not shortend else '-j')
1167
- if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--maxconnections={max_connections}' if not shortend else f'-m={max_connections}')
1338
+ if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
1168
1339
  if files: argsList.extend([f'--file="{file}"' for file in files] if not shortend else [f'-f="{file}"' for file in files])
1169
1340
  if ipmi: argsList.append('--ipmi')
1170
1341
  if interface_ip_prefix and interface_ip_prefix != DEFAULT_INTERFACE_IP_PREFIX: argsList.append(f'--interface_ip_prefix="{interface_ip_prefix}"' if not shortend else f'-pre="{interface_ip_prefix}"')
1171
1342
  if scp: argsList.append('--scp')
1343
+ if gather_mode: argsList.append('--gather_mode' if not shortend else '-gm')
1172
1344
  if username and username != DEFAULT_USERNAME: argsList.append(f'--username="{username}"' if not shortend else f'-u="{username}"')
1173
1345
  if extraargs and extraargs != DEFAULT_EXTRA_ARGS: argsList.append(f'--extraargs="{extraargs}"' if not shortend else f'-ea="{extraargs}"')
1174
1346
  if skipUnreachable: argsList.append('--skipUnreachable' if not shortend else '-su')
@@ -1179,56 +1351,56 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
1179
1351
  if file_sync: argsList.append('--file_sync' if not shortend else '-fs')
1180
1352
  return ' '.join(argsList)
1181
1353
 
1182
- def getStrCommand(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1183
- quiet = DEFAULT_QUIET,json = DEFAULT_JSON_MODE,called = DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1184
- files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = DEFAULT_RETURN_UNFINISHED,
1185
- scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1186
- no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=DEFAULT_NO_START,
1354
+ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1355
+ nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1356
+ files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
1357
+ scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1358
+ no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1187
1359
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1188
1360
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, shortend = False):
1189
1361
  hosts = hosts if type(hosts) == str else frozenset(hosts)
1190
1362
  hostStr = formHostStr(hosts)
1191
1363
  files = frozenset(files) if files else None
1192
1364
  argsStr = __formCommandArgStr(oneonone = oneonone, timeout = timeout,password = password,
1193
- quiet = quiet,json = json,max_connections=max_connections,
1194
- files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,
1365
+ nowatch = nowatch,json = json,max_connections=max_connections,
1366
+ files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
1195
1367
  username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
1196
1368
  greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, shortend = shortend)
1197
1369
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
1198
1370
  return f'multissh {argsStr} {hostStr} {commandStr}'
1199
1371
 
1200
- def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1201
- quiet = DEFAULT_QUIET,json = DEFAULT_JSON_MODE,called = DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1202
- files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = DEFAULT_RETURN_UNFINISHED,
1203
- scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1204
- no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=DEFAULT_NO_START,
1372
+ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1373
+ nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1374
+ files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
1375
+ scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1376
+ no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1205
1377
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1206
1378
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY):
1207
1379
  f'''
1208
1380
  Run the command on the hosts, aka multissh. main function
1209
1381
 
1210
1382
  Args:
1211
- hosts (str/iterable): A string of hosts seperated by space or comma / iterable of hosts.
1212
- commands (list): A list of commands to run on the hosts. When using files, defines the destination of the files.
1383
+ hosts (str/iterable): A string of hosts seperated by space or comma / iterable of hosts. Default to {DEFAULT_HOSTS}.
1384
+ commands (list): A list of commands to run on the hosts. When using files, defines the destination of the files. Defaults to None.
1213
1385
  oneonone (bool, optional): Whether to run the commands one on one. Defaults to {DEFAULT_ONE_ON_ONE}.
1214
1386
  timeout (int, optional): The timeout for the command. Defaults to {DEFAULT_TIMEOUT}.
1215
1387
  password (str, optional): The password for the hosts. Defaults to {DEFAULT_PASSWORD}.
1216
- quiet (bool, optional): Whether to print the output. Defaults to {DEFAULT_QUIET}.
1388
+ nowatch (bool, optional): Whether to print the output. Defaults to {DEFAULT_NO_WATCH}.
1217
1389
  json (bool, optional): Whether to print the output in JSON format. Defaults to {DEFAULT_JSON_MODE}.
1218
- called (bool, optional): Whether the function is called by another function. Defaults to {DEFAULT_CALLED}.
1390
+ called (bool, optional): Whether the function is called by another function. Defaults to {_DEFAULT_CALLED}.
1219
1391
  max_connections (int, optional): The maximum number of concurrent SSH sessions. Defaults to 4 * os.cpu_count().
1220
1392
  files (list, optional): A list of files to be copied to the hosts. Defaults to None.
1221
1393
  ipmi (bool, optional): Whether to use IPMI to connect to the hosts. Defaults to {DEFAULT_IPMI}.
1222
1394
  interface_ip_prefix (str, optional): The prefix of the IPMI interface. Defaults to {DEFAULT_INTERFACE_IP_PREFIX}.
1223
- returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {DEFAULT_RETURN_UNFINISHED}.
1395
+ returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {_DEFAULT_RETURN_UNFINISHED}.
1224
1396
  scp (bool, optional): Whether to use scp instead of rsync. Defaults to {DEFAULT_SCP}.
1225
1397
  username (str, optional): The username to use to connect to the hosts. Defaults to {DEFAULT_USERNAME}.
1226
1398
  extraargs (str, optional): Extra arguments to pass to the ssh / rsync / scp command. Defaults to {DEFAULT_EXTRA_ARGS}.
1227
1399
  skipUnreachable (bool, optional): Whether to skip unreachable hosts. Defaults to {DEFAULT_SKIP_UNREACHABLE}.
1228
1400
  no_env (bool, optional): Whether to not read the current sat system environment variables. (Will still read from files) Defaults to {DEFAULT_NO_ENV}.
1229
1401
  greppable (bool, optional): Whether to print the output in greppable format. Defaults to {DEFAULT_GREPPABLE_MODE}.
1230
- willUpdateUnreachableHosts (bool, optional): Whether to update the global unavailable hosts. Defaults to {DEFAULT_UPDATE_UNREACHABLE_HOSTS}.
1231
- no_start (bool, optional): Whether to return the hosts without starting the command. Defaults to {DEFAULT_NO_START}.
1402
+ willUpdateUnreachableHosts (bool, optional): Whether to update the global unavailable hosts. Defaults to {_DEFAULT_UPDATE_UNREACHABLE_HOSTS}.
1403
+ no_start (bool, optional): Whether to return the hosts without starting the command. Defaults to {_DEFAULT_NO_START}.
1232
1404
  skip_hosts (str, optional): The hosts to skip. Defaults to {DEFAULT_SKIP_HOSTS}.
1233
1405
  min_char_len (int, optional): The minimum character per line of the curses output. Defaults to {DEFAULT_CURSES_MINIMUM_CHAR_LEN}.
1234
1406
  min_line_len (int, optional): The minimum line number for each window of the curses output. Defaults to {DEFAULT_CURSES_MINIMUM_LINE_LEN}.
@@ -1238,8 +1410,8 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1238
1410
  Returns:
1239
1411
  list: A list of Host objects
1240
1412
  '''
1241
- global gloablUnavailableHosts
1242
- global global_suppress_printout
1413
+ global __gloablUnavailableHosts
1414
+ global __global_suppress_printout
1243
1415
  if not max_connections:
1244
1416
  max_connections = 4 * os.cpu_count()
1245
1417
  elif max_connections == 0:
@@ -1253,22 +1425,22 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1253
1425
  if called:
1254
1426
  # if called,
1255
1427
  # if skipUnreachable is not set, we default to skip unreachable hosts within one command call
1256
- global_suppress_printout = True
1428
+ __global_suppress_printout = True
1257
1429
  if skipUnreachable is None:
1258
1430
  skipUnreachable = True
1259
1431
  if skipUnreachable:
1260
- unavailableHosts = gloablUnavailableHosts
1432
+ unavailableHosts = __gloablUnavailableHosts
1261
1433
  else:
1262
1434
  unavailableHosts = set()
1263
1435
  else:
1264
1436
  # if run in command line ( or emulating running in command line, we default to skip unreachable hosts within one command call )
1265
1437
  if skipUnreachable:
1266
- unavailableHosts = gloablUnavailableHosts
1438
+ unavailableHosts = __gloablUnavailableHosts
1267
1439
  else:
1268
1440
  unavailableHosts = set()
1269
1441
  skipUnreachable = True
1270
- global emo
1271
- emo = False
1442
+ global _emo
1443
+ _emo = False
1272
1444
  # We create the hosts
1273
1445
  hostStr = formHostStr(hosts)
1274
1446
  skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
@@ -1290,7 +1462,7 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1290
1462
  targetHostsList = expand_hostnames(frozenset(hostStr.split(',')),no_env=no_env)
1291
1463
  skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')),no_env=no_env)
1292
1464
  if skipHostsList:
1293
- if not global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
1465
+ if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
1294
1466
  if files and not commands:
1295
1467
  # if files are specified but not target dir, we default to file sync mode
1296
1468
  file_sync = True
@@ -1321,22 +1493,22 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1321
1493
  print(f"Number of commands: {len(commands)}")
1322
1494
  print(f"Number of hosts: {len(targetHostsList - skipHostsList)}")
1323
1495
  sys.exit(255)
1324
- if not global_suppress_printout:
1496
+ if not __global_suppress_printout:
1325
1497
  print('-'*80)
1326
1498
  print("Running in one on one mode")
1327
1499
  for host, command in zip(targetHostsList, commands):
1328
1500
  if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
1329
- if not global_suppress_printout: print(f"Skipping unavailable host: {host}")
1501
+ if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
1330
1502
  continue
1331
1503
  if host.strip() in skipHostsList: continue
1332
1504
  if file_sync:
1333
- hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1505
+ hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1334
1506
  else:
1335
- hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1336
- if not global_suppress_printout:
1507
+ hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1508
+ if not __global_suppress_printout:
1337
1509
  print(f"Running command: {command} on host: {host}")
1338
- if not global_suppress_printout: print('-'*80)
1339
- if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, quiet, 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)
1510
+ if not __global_suppress_printout: print('-'*80)
1511
+ 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)
1340
1512
  return hosts
1341
1513
  else:
1342
1514
  allHosts = []
@@ -1345,7 +1517,7 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1345
1517
  hosts = []
1346
1518
  for host in targetHostsList:
1347
1519
  if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
1348
- if not global_suppress_printout: print(f"Skipping unavailable host: {host}")
1520
+ if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
1349
1521
  continue
1350
1522
  if host.strip() in skipHostsList: continue
1351
1523
  if file_sync:
@@ -1357,93 +1529,165 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1357
1529
  print(f"Error: ipmi mode is not supported in interactive mode")
1358
1530
  else:
1359
1531
  hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1360
- if not global_suppress_printout:
1532
+ if not __global_suppress_printout:
1361
1533
  print('-'*80)
1362
1534
  print(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
1363
1535
  print('-'*80)
1364
1536
  if no_start:
1365
1537
  print(f"Warning: no_start is set, the command will not be started. As we are in interactive mode, no action will be done.")
1366
1538
  else:
1367
- processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, quiet, 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
+ 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)
1368
1540
  return hosts
1369
1541
  for command in commands:
1370
1542
  hosts = []
1371
1543
  for host in targetHostsList:
1372
1544
  if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
1373
- if not global_suppress_printout: print(f"Skipping unavailable host: {host}")
1545
+ if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
1374
1546
  continue
1375
1547
  if host.strip() in skipHostsList: continue
1376
1548
  if file_sync:
1377
- hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1549
+ hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1378
1550
  else:
1379
- hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1380
- if not global_suppress_printout and len(commands) > 1:
1551
+ hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1552
+ if not __global_suppress_printout and len(commands) > 1:
1381
1553
  print('-'*80)
1382
1554
  print(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
1383
1555
  print('-'*80)
1384
- if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, quiet, 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
+ 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)
1385
1557
  allHosts += hosts
1386
1558
  return allHosts
1387
1559
 
1388
- def main():
1389
- global emo
1390
- global global_suppress_printout
1391
- global gloablUnavailableHosts
1392
- global mainReturnCode
1393
- global failedHosts
1394
- global keyPressesIn
1395
- global ipmiiInterfaceIPPrefix
1396
- global sshpassAvailable
1397
- global env_file
1398
- emo = False
1560
+ def get_default_config(args):
1561
+ '''
1562
+ Get the default config
1563
+
1564
+ Args:
1565
+ args (argparse.Namespace): The arguments
1566
+
1567
+ Returns:
1568
+ dict: The default config
1569
+ '''
1570
+ return {
1571
+ 'AUTHOR': AUTHOR,
1572
+ 'AUTHOR_EMAIL': AUTHOR_EMAIL,
1573
+ 'DEFAULT_HOSTS': args.hosts,
1574
+ 'DEFAULT_USERNAME': args.username,
1575
+ 'DEFAULT_PASSWORD': args.password,
1576
+ 'DEFAULT_EXTRA_ARGS': args.extraargs,
1577
+ 'DEFAULT_ONE_ON_ONE': args.oneonone,
1578
+ 'DEFAULT_SCP': args.scp,
1579
+ 'DEFAULT_FILE_SYNC': args.file_sync,
1580
+ 'DEFAULT_TIMEOUT': DEFAULT_TIMEOUT,
1581
+ 'DEFAULT_CLI_TIMEOUT': args.timeout,
1582
+ 'DEFAULT_REPEAT': args.repeat,
1583
+ 'DEFAULT_INTERVAL': args.interval,
1584
+ 'DEFAULT_IPMI': args.ipmi,
1585
+ 'DEFAULT_IPMI_INTERFACE_IP_PREFIX': args.ipmi_interface_ip_prefix,
1586
+ 'DEFAULT_INTERFACE_IP_PREFIX': args.interface_ip_prefix,
1587
+ 'DEFAULT_NO_WATCH': args.nowatch,
1588
+ 'DEFAULT_CURSES_MINIMUM_CHAR_LEN': args.window_width,
1589
+ 'DEFAULT_CURSES_MINIMUM_LINE_LEN': args.window_height,
1590
+ 'DEFAULT_SINGLE_WINDOW': args.single_window,
1591
+ 'DEFAULT_ERROR_ONLY': args.error_only,
1592
+ 'DEFAULT_NO_OUTPUT': args.no_output,
1593
+ 'DEFAULT_NO_ENV': args.no_env,
1594
+ 'DEFAULT_ENV_FILE': args.env_file,
1595
+ 'DEFAULT_MAX_CONNECTIONS': args.max_connections if args.max_connections != 4 * os.cpu_count() else None,
1596
+ 'DEFAULT_JSON_MODE': args.json,
1597
+ 'DEFAULT_PRINT_SUCCESS_HOSTS': args.success_hosts,
1598
+ 'DEFAULT_GREPPABLE_MODE': args.greppable,
1599
+ 'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
1600
+ 'DEFAULT_SKIP_HOSTS': args.skip_hosts,
1601
+ 'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
1602
+ }
1603
+
1604
+ def write_default_config(args,CONFIG_FILE,backup = True):
1605
+ if backup and os.path.exists(CONFIG_FILE):
1606
+ os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
1607
+ default_config = get_default_config(args)
1608
+ with open(CONFIG_FILE,'w') as f:
1609
+ json.dump(default_config,f,indent=4)
1399
1610
 
1400
1611
 
1612
+ def main():
1613
+ global _emo
1614
+ global __global_suppress_printout
1615
+ global __gloablUnavailableHosts
1616
+ global __mainReturnCode
1617
+ global __failedHosts
1618
+ global __ipmiiInterfaceIPPrefix
1619
+ global _binPaths
1620
+ global _env_file
1621
+ _emo = False
1401
1622
  # We handle the signal
1402
1623
  signal.signal(signal.SIGINT, signal_handler)
1403
1624
  # We parse the arguments
1404
- parser = argparse.ArgumentParser(description='Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command')
1405
- parser.add_argument('hosts', metavar='hosts', type=str, help='Hosts to run the command on, use "," to seperate hosts')
1406
- parser.add_argument('commands', metavar='commands', type=str, nargs='+',help='the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host name.')
1625
+ 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}')
1626
+ 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)
1627
+ 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.')
1407
1628
  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)
1408
- 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)
1409
1629
  parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
1630
+ 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)
1410
1631
  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)
1411
1632
  parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
1412
1633
  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)
1413
1634
  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)
1635
+ 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)
1414
1636
  #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")
1415
- parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds (default: 0 (disabled))", default=0)
1637
+ parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds (default: {DEFAULT_CLI_TIMEOUT} (disabled))", default=DEFAULT_CLI_TIMEOUT)
1416
1638
  parser.add_argument("-r","--repeat", type=int, help=f"Repeat the command for a number of times (default: {DEFAULT_REPEAT})", default=DEFAULT_REPEAT)
1417
1639
  parser.add_argument("-i","--interval", type=int, help=f"Interval between repeats in seconds (default: {DEFAULT_INTERVAL})", default=DEFAULT_INTERVAL)
1418
1640
  parser.add_argument("--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
1419
1641
  parser.add_argument("-mpre","--ipmi_interface_ip_prefix", type=str, help=f"The prefix of the IPMI interfaces (default: {DEFAULT_IPMI_INTERFACE_IP_PREFIX})", default=DEFAULT_IPMI_INTERFACE_IP_PREFIX)
1420
1642
  parser.add_argument("-pre","--interface_ip_prefix", type=str, help=f"The prefix of the for the interfaces (default: {DEFAULT_INTERFACE_IP_PREFIX})", default=DEFAULT_INTERFACE_IP_PREFIX)
1421
- parser.add_argument("-q","--quiet", action='store_true', help=f"Quiet mode, no curses, only print the output. (default: {DEFAULT_QUIET})", default=DEFAULT_QUIET)
1643
+ parser.add_argument("-q","-nw","--nowatch","--quiet", action='store_true', help=f"Quiet mode, no curses watch, only print the output. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
1422
1644
  parser.add_argument("-ww",'--window_width', type=int, help=f"The minimum character length of the curses window. (default: {DEFAULT_CURSES_MINIMUM_CHAR_LEN})", default=DEFAULT_CURSES_MINIMUM_CHAR_LEN)
1423
1645
  parser.add_argument("-wh",'--window_height', type=int, help=f"The minimum line height of the curses window. (default: {DEFAULT_CURSES_MINIMUM_LINE_LEN})", default=DEFAULT_CURSES_MINIMUM_LINE_LEN)
1424
1646
  parser.add_argument('-sw','--single_window', action='store_true', help=f'Use a single window for all hosts. (default: {DEFAULT_SINGLE_WINDOW})', default=DEFAULT_SINGLE_WINDOW)
1425
1647
  parser.add_argument('-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
1426
- parser.add_argument("-no","--nooutput", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
1648
+ parser.add_argument("-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
1427
1649
  parser.add_argument('--no_env', action='store_true', help=f'Do not load the environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
1428
1650
  parser.add_argument("--env_file", type=str, help=f"The file to load the environment variables from. (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
1429
- parser.add_argument("-m","--maxconnections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
1651
+ parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
1430
1652
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
1431
1653
  parser.add_argument("--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as wells. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
1432
1654
  parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
1433
- parser.add_argument("-nw","--nowatch", action='store_true', help=f"Do not watch the output in curses modem, Use \\r. Not implemented yet. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
1434
- parser.add_argument("-su","--skipunreachable", 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)
1435
- parser.add_argument("-sh","--skiphosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS})", default=DEFAULT_SKIP_HOSTS)
1436
- parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} {("with sshpass " if sshpassAvailable else "")}by pan@zopyr.us')
1655
+ 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)
1656
+ 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)
1657
+ parser.add_argument('--generate_default_config_file', action='store_true', help=f'Generate / store the default config file from command line argument and current config at {CONFIG_FILE}')
1658
+ parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
1437
1659
 
1438
1660
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
1439
1661
  # help='the user to use to connect to the hosts')
1440
1662
  args = parser.parse_args()
1441
1663
 
1442
- env_file = args.env_file
1664
+ if args.generate_default_config_file:
1665
+ try:
1666
+ if os.path.exists(CONFIG_FILE):
1667
+ print(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
1668
+ print(f"o: Overwrite the file")
1669
+ print(f"b: Rename the current config file at {CONFIG_FILE}.bak forcefully and write the new config file (default)")
1670
+ print(f"n: Do nothing")
1671
+ inStr = input_with_timeout_and_countdown(10)
1672
+ if (not inStr) or inStr.lower().strip().startswith('b'):
1673
+ write_default_config(args,CONFIG_FILE,backup = True)
1674
+ print(f"Config file written to {CONFIG_FILE}")
1675
+ elif inStr.lower().strip().startswith('o'):
1676
+ write_default_config(args,CONFIG_FILE,backup = False)
1677
+ print(f"Config file written to {CONFIG_FILE}")
1678
+ else:
1679
+ write_default_config(args,CONFIG_FILE,backup = True)
1680
+ print(f"Config file written to {CONFIG_FILE}")
1681
+ except Exception as e:
1682
+ print(f"Error while writing config file: {e}")
1683
+ if not args.commands:
1684
+ sys.exit(0)
1685
+
1686
+ _env_file = args.env_file
1443
1687
  # if there are more than 1 commands, and every command only consists of one word,
1444
1688
  # we will ask the user to confirm if they want to run multiple commands or just one command.
1445
1689
  if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
1446
- print(f"Multiple one word command detected, what to do? (s/f/n)")
1690
+ print(f"Multiple one word command detected, what to do? (1/m/n)")
1447
1691
  print(f"1: Run 1 command [{' '.join(args.commands)}] on all hosts ( default )")
1448
1692
  print(f"m: Run multiple commands [{', '.join(args.commands)}] on all hosts")
1449
1693
  print(f"n: Exit")
@@ -1456,66 +1700,64 @@ def main():
1456
1700
  else:
1457
1701
  sys.exit(0)
1458
1702
 
1459
- ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
1703
+ __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
1460
1704
 
1461
- if not args.greppable and not args.json and not args.nooutput:
1462
- global_suppress_printout = False
1705
+ if not args.greppable and not args.json and not args.no_output:
1706
+ __global_suppress_printout = False
1463
1707
 
1464
- if not global_suppress_printout:
1708
+ if not __global_suppress_printout:
1465
1709
  print('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
1466
- quiet=args.quiet,json=args.json,called=args.nooutput,max_connections=args.maxconnections,
1467
- files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,username=args.username,
1468
- extraargs=args.extraargs,skipUnreachable=args.skipunreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skiphosts,
1710
+ nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1711
+ 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,
1712
+ extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1469
1713
  curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only))
1470
1714
  if args.error_only:
1471
- global_suppress_printout = True
1715
+ __global_suppress_printout = True
1472
1716
 
1473
1717
  for i in range(args.repeat):
1474
1718
  if args.interval > 0 and i < args.repeat - 1:
1475
1719
  print(f"Sleeping for {args.interval} seconds")
1476
1720
  time.sleep(args.interval)
1477
1721
 
1478
- if not global_suppress_printout: print(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
1722
+ if not __global_suppress_printout: print(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
1479
1723
  hosts = run_command_on_hosts(args.hosts,args.commands,
1480
1724
  oneonone=args.oneonone,timeout=args.timeout,password=args.password,
1481
- quiet=args.quiet,json=args.json,called=args.nooutput,max_connections=args.maxconnections,
1482
- files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,username=args.username,
1483
- extraargs=args.extraargs,skipUnreachable=args.skipunreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skiphosts,
1725
+ nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1726
+ 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,
1727
+ extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1484
1728
  curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only)
1485
1729
  #print('*'*80)
1486
1730
 
1487
- if not global_suppress_printout: print('-'*80)
1731
+ if not __global_suppress_printout: print('-'*80)
1488
1732
 
1489
1733
  succeededHosts = set()
1490
1734
  for host in hosts:
1491
1735
  if host.returncode and host.returncode != 0:
1492
- mainReturnCode += 1
1493
- failedHosts.add(host.name)
1736
+ __mainReturnCode += 1
1737
+ __failedHosts.add(host.name)
1494
1738
  else:
1495
1739
  succeededHosts.add(host.name)
1496
- succeededHosts -= failedHosts
1740
+ succeededHosts -= __failedHosts
1497
1741
  # sort the failed hosts and succeeded hosts
1498
- failedHosts = sorted(failedHosts)
1742
+ __failedHosts = sorted(__failedHosts)
1499
1743
  succeededHosts = sorted(succeededHosts)
1500
- if mainReturnCode > 0:
1501
- if not global_suppress_printout: print(f'Complete. Failed hosts (Return Code not 0) count: {mainReturnCode}')
1744
+ if __mainReturnCode > 0:
1745
+ if not __global_suppress_printout: print(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
1502
1746
  # with open('/tmp/bashcmd.stdin','w') as f:
1503
- # f.write(f"export failed_hosts={failedHosts}\n")
1504
- if not global_suppress_printout: print(f'failed_hosts: {",".join(failedHosts)}')
1747
+ # f.write(f"export failed_hosts={__failedHosts}\n")
1748
+ if not __global_suppress_printout: print(f'failed_hosts: {",".join(__failedHosts)}')
1505
1749
  else:
1506
- if not global_suppress_printout: print('Complete. All hosts returned 0.')
1750
+ if not __global_suppress_printout: print('Complete. All hosts returned 0.')
1507
1751
 
1508
- if args.success_hosts and not global_suppress_printout:
1752
+ if args.success_hosts and not __global_suppress_printout:
1509
1753
  print(f'succeeded_hosts: {",".join(succeededHosts)}')
1510
1754
 
1511
1755
  if threading.active_count() > 1:
1512
- if not global_suppress_printout: print(f'Remaining active thread: {threading.active_count()}')
1756
+ if not __global_suppress_printout: print(f'Remaining active thread: {threading.active_count()}')
1513
1757
  # os.system(f'pkill -ef {os.path.basename(__file__)}')
1514
1758
  # os._exit(mainReturnCode)
1515
1759
 
1516
- sys.exit(mainReturnCode)
1517
-
1518
-
1760
+ sys.exit(__mainReturnCode)
1519
1761
 
1520
1762
  if __name__ == "__main__":
1521
1763
  main()