multiSSH3 5.10__py3-none-any.whl → 5.24__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
@@ -36,13 +36,12 @@ except AttributeError:
36
36
  # If neither is available, use a dummy decorator
37
37
  def cache_decorator(func):
38
38
  return func
39
- version = '5.10'
39
+ version = '5.24'
40
40
  VERSION = version
41
41
 
42
42
  CONFIG_FILE = '/etc/multiSSH3.config.json'
43
43
 
44
- import sys
45
-
44
+ # ------------ Pre Helper Functions ----------------
46
45
  def eprint(*args, **kwargs):
47
46
  try:
48
47
  print(*args, file=sys.stderr, **kwargs)
@@ -50,6 +49,146 @@ def eprint(*args, **kwargs):
50
49
  print(f"Error: Cannot print to stderr: {e}")
51
50
  print(*args, **kwargs)
52
51
 
52
+ def signal_handler(sig, frame):
53
+ '''
54
+ Handle the Ctrl C signal
55
+
56
+ Args:
57
+ sig (int): The signal
58
+ frame (frame): The frame
59
+
60
+ Returns:
61
+ None
62
+ '''
63
+ global _emo
64
+ if not _emo:
65
+ eprint('Ctrl C caught, exiting...')
66
+ _emo = True
67
+ else:
68
+ eprint('Ctrl C caught again, exiting immediately!')
69
+ # wait for 0.1 seconds to allow the threads to exit
70
+ time.sleep(0.1)
71
+ os.system(f'pkill -ef {os.path.basename(__file__)}')
72
+ sys.exit(0)
73
+
74
+ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
75
+ """
76
+ Read an input from the user with a timeout and a countdown.
77
+
78
+ Parameters:
79
+ timeout (int): The timeout value in seconds.
80
+ prompt (str): The prompt message to display to the user. Default is 'Please enter your selection'.
81
+
82
+ Returns:
83
+ str or None: The user input if received within the timeout, or None if no input is received.
84
+ """
85
+ import select
86
+ # Print the initial prompt with the countdown
87
+ eprint(f"{prompt} [{timeout}s]: ", end='', flush=True)
88
+ # Loop until the timeout
89
+ for remaining in range(timeout, 0, -1):
90
+ # If there is an input, return it
91
+ if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
92
+ return input().strip()
93
+ # Print the remaining time
94
+ eprint(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
95
+ # Wait a second
96
+ time.sleep(1)
97
+ # If there is no input, return None
98
+ return None
99
+
100
+ @cache_decorator
101
+ def getIP(hostname: str,local=False):
102
+ '''
103
+ Get the IP address of the hostname
104
+
105
+ Args:
106
+ hostname (str): The hostname
107
+
108
+ Returns:
109
+ str: The IP address of the hostname
110
+ '''
111
+ global _etc_hosts
112
+ if '@' in hostname:
113
+ _, hostname = hostname.rsplit('@',1)
114
+ # First we check if the hostname is an IP address
115
+ try:
116
+ ipaddress.ip_address(hostname)
117
+ return hostname
118
+ except ValueError:
119
+ pass
120
+ # Then we check /etc/hosts
121
+ if not _etc_hosts and os.path.exists('/etc/hosts'):
122
+ with open('/etc/hosts','r') as f:
123
+ for line in f:
124
+ if line.startswith('#') or not line.strip():
125
+ continue
126
+ #ip, host = line.split()[:2]
127
+ chunks = line.split()
128
+ if len(chunks) < 2:
129
+ continue
130
+ ip = chunks[0]
131
+ for host in chunks[1:]:
132
+ _etc_hosts[host] = ip
133
+ if hostname in _etc_hosts:
134
+ return _etc_hosts[hostname]
135
+ if local:
136
+ return None
137
+ # Then we check the DNS
138
+ try:
139
+ return socket.gethostbyname(hostname)
140
+ except:
141
+ return None
142
+
143
+ __host_i_lock = threading.Lock()
144
+ __host_i_counter = -1
145
+ def _get_i():
146
+ '''
147
+ Get the global counter for the host objects
148
+
149
+ Returns:
150
+ int: The global counter for the host objects
151
+ '''
152
+ global __host_i_counter
153
+ global __host_i_lock
154
+ with __host_i_lock:
155
+ __host_i_counter += 1
156
+ return __host_i_counter
157
+
158
+ # ------------ Host Object ----------------
159
+ class Host:
160
+ def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,bash=False,i = _get_i(),uuid=uuid.uuid4(),ip = None):
161
+ self.name = name # the name of the host (hostname or IP address)
162
+ self.command = command # the command to run on the host
163
+ self.returncode = None # the return code of the command
164
+ self.output = [] # the output of the command for curses
165
+ self.stdout = [] # the stdout of the command
166
+ self.stderr = [] # the stderr of the command
167
+ self.printedLines = -1 # the number of lines printed on the screen
168
+ self.lastUpdateTime = time.time() # the last time the output was updated
169
+ self.files = files # the files to be copied to the host
170
+ self.ipmi = ipmi # whether to use ipmi to connect to the host
171
+ self.bash = bash # whether to use bash to run the command
172
+ self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
173
+ self.scp = scp # whether to use scp to copy files to the host
174
+ self.gatherMode = gatherMode # whether the host is in gather mode
175
+ self.extraargs = extraargs # extra arguments to be passed to ssh
176
+ self.resolvedName = None # the resolved IP address of the host
177
+ # also store a globally unique integer i from 0
178
+ self.i = i
179
+ self.uuid = uuid
180
+ self.identity_file = identity_file
181
+ self.ip = ip if ip else getIP(name)
182
+
183
+ def __iter__(self):
184
+ return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
185
+ def __repr__(self):
186
+ # return the complete data structure
187
+ return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), identity_file={self.identity_file}"
188
+ def __str__(self):
189
+ return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
190
+
191
+ # ------------ Load Defaults ( Config ) File ----------------
53
192
  def load_config_file(config_file):
54
193
  '''
55
194
  Load the config file to global variables
@@ -131,131 +270,79 @@ __build_in_default_config = {
131
270
  '__DEBUG_MODE': False,
132
271
  }
133
272
 
134
- AUTHOR = __configs_from_file.get('AUTHOR', __build_in_default_config['AUTHOR'])
135
- AUTHOR_EMAIL = __configs_from_file.get('AUTHOR_EMAIL', __build_in_default_config['AUTHOR_EMAIL'])
136
-
137
- DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_config['DEFAULT_HOSTS'])
138
- DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
139
- DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
140
- DEFAULT_PASSWORD = __configs_from_file.get('DEFAULT_PASSWORD', __build_in_default_config['DEFAULT_PASSWORD'])
141
- DEFAULT_IDENTITY_FILE = __configs_from_file.get('DEFAULT_IDENTITY_FILE', __build_in_default_config['DEFAULT_IDENTITY_FILE'])
142
- DEDAULT_SSH_KEY_SEARCH_PATH = __configs_from_file.get('DEDAULT_SSH_KEY_SEARCH_PATH', __build_in_default_config['DEDAULT_SSH_KEY_SEARCH_PATH'])
143
- DEFAULT_USE_KEY = __configs_from_file.get('DEFAULT_USE_KEY', __build_in_default_config['DEFAULT_USE_KEY'])
144
- DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
145
- DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
146
- DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
147
- DEFAULT_FILE_SYNC = __configs_from_file.get('DEFAULT_FILE_SYNC', __build_in_default_config['DEFAULT_FILE_SYNC'])
148
- DEFAULT_TIMEOUT = __configs_from_file.get('DEFAULT_TIMEOUT', __build_in_default_config['DEFAULT_TIMEOUT'])
149
- DEFAULT_CLI_TIMEOUT = __configs_from_file.get('DEFAULT_CLI_TIMEOUT', __build_in_default_config['DEFAULT_CLI_TIMEOUT'])
150
- DEFAULT_REPEAT = __configs_from_file.get('DEFAULT_REPEAT', __build_in_default_config['DEFAULT_REPEAT'])
151
- DEFAULT_INTERVAL = __configs_from_file.get('DEFAULT_INTERVAL', __build_in_default_config['DEFAULT_INTERVAL'])
152
- DEFAULT_IPMI = __configs_from_file.get('DEFAULT_IPMI', __build_in_default_config['DEFAULT_IPMI'])
153
- DEFAULT_IPMI_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_IPMI_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_IPMI_INTERFACE_IP_PREFIX'])
154
- DEFAULT_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_INTERFACE_IP_PREFIX'])
155
- DEFAULT_NO_WATCH = __configs_from_file.get('DEFAULT_NO_WATCH', __build_in_default_config['DEFAULT_NO_WATCH'])
156
- DEFAULT_CURSES_MINIMUM_CHAR_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_CHAR_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_CHAR_LEN'])
157
- DEFAULT_CURSES_MINIMUM_LINE_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_LINE_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_LINE_LEN'])
158
- DEFAULT_SINGLE_WINDOW = __configs_from_file.get('DEFAULT_SINGLE_WINDOW', __build_in_default_config['DEFAULT_SINGLE_WINDOW'])
159
- DEFAULT_ERROR_ONLY = __configs_from_file.get('DEFAULT_ERROR_ONLY', __build_in_default_config['DEFAULT_ERROR_ONLY'])
160
- DEFAULT_NO_OUTPUT = __configs_from_file.get('DEFAULT_NO_OUTPUT', __build_in_default_config['DEFAULT_NO_OUTPUT'])
161
- DEFAULT_NO_ENV = __configs_from_file.get('DEFAULT_NO_ENV', __build_in_default_config['DEFAULT_NO_ENV'])
162
- DEFAULT_MAX_CONNECTIONS = __configs_from_file.get('DEFAULT_MAX_CONNECTIONS', __build_in_default_config['DEFAULT_MAX_CONNECTIONS'])
163
- if not DEFAULT_MAX_CONNECTIONS:
164
- DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
165
- DEFAULT_JSON_MODE = __configs_from_file.get('DEFAULT_JSON_MODE', __build_in_default_config['DEFAULT_JSON_MODE'])
166
- DEFAULT_PRINT_SUCCESS_HOSTS = __configs_from_file.get('DEFAULT_PRINT_SUCCESS_HOSTS', __build_in_default_config['DEFAULT_PRINT_SUCCESS_HOSTS'])
167
- DEFAULT_GREPPABLE_MODE = __configs_from_file.get('DEFAULT_GREPPABLE_MODE', __build_in_default_config['DEFAULT_GREPPABLE_MODE'])
168
- DEFAULT_SKIP_UNREACHABLE = __configs_from_file.get('DEFAULT_SKIP_UNREACHABLE', __build_in_default_config['DEFAULT_SKIP_UNREACHABLE'])
169
- DEFAULT_SKIP_HOSTS = __configs_from_file.get('DEFAULT_SKIP_HOSTS', __build_in_default_config['DEFAULT_SKIP_HOSTS'])
170
-
171
- SSH_STRICT_HOST_KEY_CHECKING = __configs_from_file.get('SSH_STRICT_HOST_KEY_CHECKING', __build_in_default_config['SSH_STRICT_HOST_KEY_CHECKING'])
172
-
173
- ERROR_MESSAGES_TO_IGNORE = __configs_from_file.get('ERROR_MESSAGES_TO_IGNORE', __build_in_default_config['ERROR_MESSAGES_TO_IGNORE'])
174
-
175
- _DEFAULT_CALLED = __configs_from_file.get('_DEFAULT_CALLED', __build_in_default_config['_DEFAULT_CALLED'])
176
- _DEFAULT_RETURN_UNFINISHED = __configs_from_file.get('_DEFAULT_RETURN_UNFINISHED', __build_in_default_config['_DEFAULT_RETURN_UNFINISHED'])
177
- _DEFAULT_UPDATE_UNREACHABLE_HOSTS = __configs_from_file.get('_DEFAULT_UPDATE_UNREACHABLE_HOSTS', __build_in_default_config['_DEFAULT_UPDATE_UNREACHABLE_HOSTS'])
178
- _DEFAULT_NO_START = __configs_from_file.get('_DEFAULT_NO_START', __build_in_default_config['_DEFAULT_NO_START'])
179
-
180
- # form the regex from the list
181
- __ERROR_MESSAGES_TO_IGNORE_REGEX = __configs_from_file.get('__ERROR_MESSAGES_TO_IGNORE_REGEX', __build_in_default_config['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
182
- if __ERROR_MESSAGES_TO_IGNORE_REGEX:
183
- eprint('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
184
- __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__configs_from_file['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
185
- else:
186
- __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
187
-
188
- __DEBUG_MODE = __configs_from_file.get('__DEBUG_MODE', __build_in_default_config['__DEBUG_MODE'])
189
-
190
-
191
-
192
- __global_suppress_printout = False
193
-
194
- __mainReturnCode = 0
195
- __failedHosts = set()
196
- __host_i_lock = threading.Lock()
197
- __host_i_counter = -1
198
- def get_i():
199
- '''
200
- Get the global counter for the host objects
201
-
202
- Returns:
203
- int: The global counter for the host objects
204
- '''
205
- global __host_i_counter
206
- global __host_i_lock
207
- with __host_i_lock:
208
- __host_i_counter += 1
209
- return __host_i_counter
210
-
211
- class Host:
212
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,bash=False,i = get_i(),uuid=uuid.uuid4()):
213
- self.name = name # the name of the host (hostname or IP address)
214
- self.command = command # the command to run on the host
215
- self.returncode = None # the return code of the command
216
- self.output = [] # the output of the command for curses
217
- self.stdout = [] # the stdout of the command
218
- self.stderr = [] # the stderr of the command
219
- self.printedLines = -1 # the number of lines printed on the screen
220
- self.lastUpdateTime = time.time() # the last time the output was updated
221
- self.files = files # the files to be copied to the host
222
- self.ipmi = ipmi # whether to use ipmi to connect to the host
223
- self.bash = bash # whether to use bash to run the command
224
- self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
225
- self.scp = scp # whether to use scp to copy files to the host
226
- self.gatherMode = gatherMode # whether the host is in gather mode
227
- self.extraargs = extraargs # extra arguments to be passed to ssh
228
- self.resolvedName = None # the resolved IP address of the host
229
- # also store a globally unique integer i from 0
230
- self.i = i
231
- self.uuid = uuid
232
- self.identity_file = identity_file
233
-
234
- def __iter__(self):
235
- return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
236
- def __repr__(self):
237
- # return the complete data structure
238
- 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}), identity_file={self.identity_file}"
239
- def __str__(self):
240
- return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
241
-
242
- __wildCharacters = ['*','?','x']
243
-
244
- _no_env = DEFAULT_NO_ENV
245
-
246
- _env_file = DEFAULT_ENV_FILE
247
-
248
- __globalUnavailableHosts = set()
249
-
250
- __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
251
-
252
- __keyPressesIn = [[]]
253
-
254
- _emo = False
255
-
256
- _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
257
-
273
+ # Load Config Based Default Global variables
274
+ if True:
275
+ AUTHOR = __configs_from_file.get('AUTHOR', __build_in_default_config['AUTHOR'])
276
+ AUTHOR_EMAIL = __configs_from_file.get('AUTHOR_EMAIL', __build_in_default_config['AUTHOR_EMAIL'])
277
+
278
+ DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_config['DEFAULT_HOSTS'])
279
+ DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
280
+ DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
281
+ DEFAULT_PASSWORD = __configs_from_file.get('DEFAULT_PASSWORD', __build_in_default_config['DEFAULT_PASSWORD'])
282
+ DEFAULT_IDENTITY_FILE = __configs_from_file.get('DEFAULT_IDENTITY_FILE', __build_in_default_config['DEFAULT_IDENTITY_FILE'])
283
+ DEDAULT_SSH_KEY_SEARCH_PATH = __configs_from_file.get('DEDAULT_SSH_KEY_SEARCH_PATH', __build_in_default_config['DEDAULT_SSH_KEY_SEARCH_PATH'])
284
+ DEFAULT_USE_KEY = __configs_from_file.get('DEFAULT_USE_KEY', __build_in_default_config['DEFAULT_USE_KEY'])
285
+ DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
286
+ DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
287
+ DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
288
+ DEFAULT_FILE_SYNC = __configs_from_file.get('DEFAULT_FILE_SYNC', __build_in_default_config['DEFAULT_FILE_SYNC'])
289
+ DEFAULT_TIMEOUT = __configs_from_file.get('DEFAULT_TIMEOUT', __build_in_default_config['DEFAULT_TIMEOUT'])
290
+ DEFAULT_CLI_TIMEOUT = __configs_from_file.get('DEFAULT_CLI_TIMEOUT', __build_in_default_config['DEFAULT_CLI_TIMEOUT'])
291
+ DEFAULT_REPEAT = __configs_from_file.get('DEFAULT_REPEAT', __build_in_default_config['DEFAULT_REPEAT'])
292
+ DEFAULT_INTERVAL = __configs_from_file.get('DEFAULT_INTERVAL', __build_in_default_config['DEFAULT_INTERVAL'])
293
+ DEFAULT_IPMI = __configs_from_file.get('DEFAULT_IPMI', __build_in_default_config['DEFAULT_IPMI'])
294
+ DEFAULT_IPMI_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_IPMI_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_IPMI_INTERFACE_IP_PREFIX'])
295
+ DEFAULT_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_INTERFACE_IP_PREFIX'])
296
+ DEFAULT_NO_WATCH = __configs_from_file.get('DEFAULT_NO_WATCH', __build_in_default_config['DEFAULT_NO_WATCH'])
297
+ DEFAULT_CURSES_MINIMUM_CHAR_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_CHAR_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_CHAR_LEN'])
298
+ DEFAULT_CURSES_MINIMUM_LINE_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_LINE_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_LINE_LEN'])
299
+ DEFAULT_SINGLE_WINDOW = __configs_from_file.get('DEFAULT_SINGLE_WINDOW', __build_in_default_config['DEFAULT_SINGLE_WINDOW'])
300
+ DEFAULT_ERROR_ONLY = __configs_from_file.get('DEFAULT_ERROR_ONLY', __build_in_default_config['DEFAULT_ERROR_ONLY'])
301
+ DEFAULT_NO_OUTPUT = __configs_from_file.get('DEFAULT_NO_OUTPUT', __build_in_default_config['DEFAULT_NO_OUTPUT'])
302
+ DEFAULT_NO_ENV = __configs_from_file.get('DEFAULT_NO_ENV', __build_in_default_config['DEFAULT_NO_ENV'])
303
+ DEFAULT_MAX_CONNECTIONS = __configs_from_file.get('DEFAULT_MAX_CONNECTIONS', __build_in_default_config['DEFAULT_MAX_CONNECTIONS'])
304
+ if not DEFAULT_MAX_CONNECTIONS:
305
+ DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
306
+ DEFAULT_JSON_MODE = __configs_from_file.get('DEFAULT_JSON_MODE', __build_in_default_config['DEFAULT_JSON_MODE'])
307
+ DEFAULT_PRINT_SUCCESS_HOSTS = __configs_from_file.get('DEFAULT_PRINT_SUCCESS_HOSTS', __build_in_default_config['DEFAULT_PRINT_SUCCESS_HOSTS'])
308
+ DEFAULT_GREPPABLE_MODE = __configs_from_file.get('DEFAULT_GREPPABLE_MODE', __build_in_default_config['DEFAULT_GREPPABLE_MODE'])
309
+ DEFAULT_SKIP_UNREACHABLE = __configs_from_file.get('DEFAULT_SKIP_UNREACHABLE', __build_in_default_config['DEFAULT_SKIP_UNREACHABLE'])
310
+ DEFAULT_SKIP_HOSTS = __configs_from_file.get('DEFAULT_SKIP_HOSTS', __build_in_default_config['DEFAULT_SKIP_HOSTS'])
311
+
312
+ SSH_STRICT_HOST_KEY_CHECKING = __configs_from_file.get('SSH_STRICT_HOST_KEY_CHECKING', __build_in_default_config['SSH_STRICT_HOST_KEY_CHECKING'])
313
+
314
+ ERROR_MESSAGES_TO_IGNORE = __configs_from_file.get('ERROR_MESSAGES_TO_IGNORE', __build_in_default_config['ERROR_MESSAGES_TO_IGNORE'])
315
+
316
+ _DEFAULT_CALLED = __configs_from_file.get('_DEFAULT_CALLED', __build_in_default_config['_DEFAULT_CALLED'])
317
+ _DEFAULT_RETURN_UNFINISHED = __configs_from_file.get('_DEFAULT_RETURN_UNFINISHED', __build_in_default_config['_DEFAULT_RETURN_UNFINISHED'])
318
+ _DEFAULT_UPDATE_UNREACHABLE_HOSTS = __configs_from_file.get('_DEFAULT_UPDATE_UNREACHABLE_HOSTS', __build_in_default_config['_DEFAULT_UPDATE_UNREACHABLE_HOSTS'])
319
+ _DEFAULT_NO_START = __configs_from_file.get('_DEFAULT_NO_START', __build_in_default_config['_DEFAULT_NO_START'])
320
+
321
+ # form the regex from the list
322
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = __configs_from_file.get('__ERROR_MESSAGES_TO_IGNORE_REGEX', __build_in_default_config['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
323
+ if __ERROR_MESSAGES_TO_IGNORE_REGEX:
324
+ eprint('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
325
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__configs_from_file['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
326
+ else:
327
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
328
+
329
+ __DEBUG_MODE = __configs_from_file.get('__DEBUG_MODE', __build_in_default_config['__DEBUG_MODE'])
330
+
331
+ # Load mssh Functional Global Variables
332
+ if True:
333
+ __global_suppress_printout = False
334
+ __mainReturnCode = 0
335
+ __failedHosts = set()
336
+ __wildCharacters = ['*','?','x']
337
+ _no_env = DEFAULT_NO_ENV
338
+ _env_file = DEFAULT_ENV_FILE
339
+ __globalUnavailableHosts = set()
340
+ __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
341
+ __keyPressesIn = [[]]
342
+ _emo = False
343
+ _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
258
344
 
345
+ # ------------ Exportable Help Functions ----------------
259
346
  # check if command sshpass is available
260
347
  _binPaths = {}
261
348
  def check_path(program_name):
@@ -275,40 +362,162 @@ def check_path(program_name):
275
362
 
276
363
  [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash','ssh-copy-id']]
277
364
 
278
- #%%
279
- def tokenize_hostname(hostname):
280
- """
281
- Tokenize the hostname into a list of tokens.
282
- Tokens will be separated by symbols or numbers.
283
-
284
- Args:
285
- hostname (str): The hostname to tokenize.
286
-
287
- Returns:
288
- list: A list of tokens.
289
-
290
- Example:
291
- >>> tokenize_hostname('www.example.com')
292
- ('www', '.', 'example', '.', 'com')
293
- >>> tokenize_hostname('localhost')
294
- ('localhost',)
295
- >>> tokenize_hostname('Sub-S1')
296
- ('Sub', '-', 'S', '1')
297
- >>> tokenize_hostname('Sub-S10')
298
- ('Sub', '-', 'S', '10')
299
- >>> tokenize_hostname('Process-Client10-1')
300
- ('Process', '-', 'Client', '10', '-', '1')
301
- >>> tokenize_hostname('Process-C5-15')
302
- ('Process', '-', 'C', '5', '-', '15')
303
- >>> tokenize_hostname('192.168.1.1')
304
- ('192', '.', '168', '.', '1', '.', '1')
305
- """
306
- # Regular expression to match sequences of letters, digits, or symbols
307
- tokens = re.findall(r'[A-Za-z]+|\d+|[^A-Za-z0-9]', hostname)
308
- return tuple(tokens)
365
+ def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
366
+ '''
367
+ Find the ssh public key file
368
+
369
+ Args:
370
+ searchPath (str, optional): The path to search. Defaults to DEDAULT_SSH_KEY_SEARCH_PATH.
371
+
372
+ Returns:
373
+ str: The path to the ssh key file
374
+ '''
375
+ if searchPath:
376
+ sshKeyPath = searchPath
377
+ else:
378
+ sshKeyPath ='~/.ssh'
379
+ possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
380
+ for sshKeyFile in possibleSshKeyFiles:
381
+ if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
382
+ return os.path.join(sshKeyPath,sshKeyFile)
383
+ return None
309
384
 
310
385
  @cache_decorator
311
- def hashTokens(tokens):
386
+ def readEnvFromFile(environemnt_file = ''):
387
+ '''
388
+ Read the environment variables from env_file
389
+ Returns:
390
+ dict: A dictionary of environment variables
391
+ '''
392
+ global env
393
+ try:
394
+ if env:
395
+ return env
396
+ except:
397
+ env = {}
398
+ global _env_file
399
+ if environemnt_file:
400
+ envf = environemnt_file
401
+ else:
402
+ envf = _env_file if _env_file else DEFAULT_ENV_FILE
403
+ if os.path.exists(envf):
404
+ with open(envf,'r') as f:
405
+ for line in f:
406
+ if line.startswith('#') or not line.strip():
407
+ continue
408
+ key, value = line.replace('export ', '', 1).strip().split('=', 1)
409
+ key = key.strip().strip('"').strip("'")
410
+ value = value.strip().strip('"').strip("'")
411
+ # avoid infinite recursion
412
+ if key != value:
413
+ env[key] = value.strip('"').strip("'")
414
+ return env
415
+
416
+ def replace_magic_strings(string,keys,value,case_sensitive=False):
417
+ '''
418
+ Replace the magic strings in the host object
419
+
420
+ Args:
421
+ string (str): The string to replace the magic strings
422
+ keys (list): Search for keys to replace
423
+ value (str): The value to replace the key
424
+ case_sensitive (bool, optional): Whether to search for the keys in a case sensitive way. Defaults to False.
425
+
426
+ Returns:
427
+ str: The string with the magic strings replaced
428
+ '''
429
+ # verify magic strings have # at the beginning and end
430
+ newKeys = []
431
+ for key in keys:
432
+ if key.startswith('#') and key.endswith('#'):
433
+ newKeys.append(key)
434
+ else:
435
+ newKeys.append('#'+key.strip('#')+'#')
436
+ # replace the magic strings
437
+ for key in newKeys:
438
+ if case_sensitive:
439
+ string = string.replace(key,value)
440
+ else:
441
+ string = re.sub(re.escape(key),value,string,flags=re.IGNORECASE)
442
+ return string
443
+
444
+ def pretty_format_table(data):
445
+ if not data:
446
+ return ''
447
+ if type(data) == str:
448
+ data = data.strip('\n').split('\n')
449
+ data = [line.split('\t') for line in data]
450
+ elif isinstance(data, dict):
451
+ # flatten the 2D dict to a list of lists
452
+ if isinstance(next(iter(data.values())), dict):
453
+ tempData = [['key'] + list(next(iter(data.values())).keys())]
454
+ tempData.extend( [[key] + list(value.values()) for key, value in data.items()])
455
+ data = tempData
456
+ else:
457
+ # it is a dict of lists
458
+ data = [[key] + list(value) for key, value in data.items()]
459
+ elif type(data) != list:
460
+ data = list(data)
461
+ # TODO : add support for list of dictionaries
462
+ # format the list into 2d list of list of strings
463
+ if isinstance(data[0], dict):
464
+ tempData = [data[0].keys()]
465
+ tempData.extend([list(item.values()) for item in data])
466
+ data = tempData
467
+ data = [[str(item) for item in row] for row in data]
468
+ num_cols = len(data[0])
469
+ col_widths = [0] * num_cols
470
+ # Calculate the maximum width of each column
471
+ for c in range(num_cols):
472
+ col_widths[c] = max(len(row[c]) for row in data)
473
+ # Build the row format string
474
+ row_format = ' | '.join('{{:<{}}}'.format(width) for width in col_widths)
475
+ # Print the header
476
+ header = data[0]
477
+ outTable = []
478
+ outTable.append(row_format.format(*header))
479
+ outTable.append('-+-'.join('-' * width for width in col_widths))
480
+ for row in data[1:]:
481
+ # if the row is empty, print an divider
482
+ if not any(row):
483
+ outTable.append('-+-'.join('-' * width for width in col_widths))
484
+ else:
485
+ outTable.append(row_format.format(*row))
486
+ return '\n'.join(outTable) + '\n'
487
+
488
+ # ------------ Compacting Hostnames ----------------
489
+ def __tokenize_hostname(hostname):
490
+ """
491
+ Tokenize the hostname into a list of tokens.
492
+ Tokens will be separated by symbols or numbers.
493
+
494
+ Args:
495
+ hostname (str): The hostname to tokenize.
496
+
497
+ Returns:
498
+ list: A list of tokens.
499
+
500
+ Example:
501
+ >>> tokenize_hostname('www.example.com')
502
+ ('www', '.', 'example', '.', 'com')
503
+ >>> tokenize_hostname('localhost')
504
+ ('localhost',)
505
+ >>> tokenize_hostname('Sub-S1')
506
+ ('Sub', '-', 'S', '1')
507
+ >>> tokenize_hostname('Sub-S10')
508
+ ('Sub', '-', 'S', '10')
509
+ >>> tokenize_hostname('Process-Client10-1')
510
+ ('Process', '-', 'Client', '10', '-', '1')
511
+ >>> tokenize_hostname('Process-C5-15')
512
+ ('Process', '-', 'C', '5', '-', '15')
513
+ >>> tokenize_hostname('192.168.1.1')
514
+ ('192', '.', '168', '.', '1', '.', '1')
515
+ """
516
+ # Regular expression to match sequences of letters, digits, or symbols
517
+ tokens = re.findall(r'[A-Za-z]+|\d+|[^A-Za-z0-9]', hostname)
518
+ return tuple(tokens)
519
+
520
+ def __hashTokens(tokens):
312
521
  """
313
522
  Translate a list of tokens in string to a list of integers with positional information.
314
523
 
@@ -332,7 +541,7 @@ def hashTokens(tokens):
332
541
  """
333
542
  return tuple(int(token) if token.isdigit() else hash(token) for token in tokens)
334
543
 
335
- def findDiffIndex(token1, token2):
544
+ def __findDiffIndex(token1, token2):
336
545
  """
337
546
  Find the index of the first difference between two lists of tokens.
338
547
  If there is more than one difference, return -1.
@@ -377,7 +586,7 @@ def findDiffIndex(token1, token2):
377
586
  return -1
378
587
  return rtn
379
588
 
380
- def generateSumDic(Hostnames):
589
+ def __generateSumDic(Hostnames):
381
590
  """
382
591
  Generate a dictionary of sums of tokens for a list of hostnames.
383
592
 
@@ -396,12 +605,12 @@ def generateSumDic(Hostnames):
396
605
  """
397
606
  sumDic = {}
398
607
  for hostname in reversed(sorted(Hostnames)):
399
- tokens = tokenize_hostname(hostname)
400
- sumHash = sum(hashTokens(tokens))
608
+ tokens = __tokenize_hostname(hostname)
609
+ sumHash = sum(__hashTokens(tokens))
401
610
  sumDic.setdefault(sumHash, {})[tokens] = {}
402
611
  return sumDic
403
612
 
404
- def filterSumDic(sumDic):
613
+ def __filterSumDic(sumDic):
405
614
  """
406
615
  Filter the sumDic to do one order of grouping.
407
616
 
@@ -465,78 +674,80 @@ def filterSumDic(sumDic):
465
674
  # 3. the two hostnames have the same tokens except for one token
466
675
  # 4. the two hostnames have the same token groups
467
676
  if len(hostnameTokens) == len(lastHostnameTokens) and \
468
- lastSumHash in newSumDic and lastHostnameTokens in newSumDic[lastSumHash] and \
469
- (diffIndex:=findDiffIndex(hostnameTokens, lastHostnameTokens)) != -1 and \
470
- sumDic[sumHash][hostnameTokens] == sumDic[lastSumHash][lastHostnameTokens]:
471
- # the sumDic[sumHash][hostnameTokens] will ba a dic of 2 element value lists with 2 element key representing:
472
- # (token position that got grouped, the amount of zero padding (length) ):
473
- # [ the start int token, the end int token]
474
- # if we entered here, this means we are able to group the two hostnames together
475
-
476
- if not diffIndex:
477
- # should never happen, but just in case, we skip grouping
478
- continue
479
- tokenToGroup = hostnameTokens[diffIndex]
480
- try:
481
- tokenLength = len(tokenToGroup)
482
- tokenToGroup = int(tokenToGroup)
483
- except ValueError:
484
- # if the token is not an int, we skip grouping
485
- continue
486
- # group(09 , 10) -> (x, 2): [9, 10]
487
- # group(9 , 10) -> (x, 0): [9, 10]
488
- # group(9 , 010) -> not able to group
489
- # group(009 , 10) -> not able to group
490
- # group(08, 09) -> (x, 2): [8, 9]
491
- # group(08, 9) -> not able to group
492
- # group(8, 09) -> not able to group
493
- # group(0099, 0100) -> (x, 4): [99, 100]
494
- # group(0099, 100) -> not able to groups
495
- # group(099, 100) -> (x, 3): [99, 100]
496
- # group(99, 100) -> (x, 0): [99, 100]
497
- lastTokenToGroup = lastHostnameTokens[diffIndex]
498
- try:
499
- minimumTokenLength = 0
500
- lastTokenLength = len(lastTokenToGroup)
501
- if lastTokenLength > tokenLength:
502
- raise ValueError('The last token is longer than the current token.')
503
- elif lastTokenLength < tokenLength:
504
- if tokenLength - lastTokenLength != 1:
505
- raise ValueError('The last token is not one less than the current token.')
506
- # if the last token is not made out of all 9s, we cannot group
507
- if any(c != '9' for c in lastTokenToGroup):
508
- raise ValueError('The last token is not made out of all 9s.')
509
- elif lastTokenToGroup[0] == '0' and lastTokenLength > 1:
510
- # we have encoutered a padded last token, will set this as the minimum token length
511
- minimumTokenLength = lastTokenLength
512
- lastTokenToGroup = int(lastTokenToGroup)
513
- except ValueError:
514
- # if the token is not an int, we skip grouping
515
- continue
516
- assert lastTokenToGroup + 1 == tokenToGroup, 'Error! The two tokens are not one apart.'
517
- # we take the last hostname tokens grouped dic out from the newSumDic
518
- hostnameGroupDic = newSumDic[lastSumHash][lastHostnameTokens].copy()
519
- if (diffIndex, minimumTokenLength) in hostnameGroupDic and hostnameGroupDic[(diffIndex, minimumTokenLength)][1] + 1 == tokenToGroup:
520
- # if the token is already grouped, we just update the end token
521
- hostnameGroupDic[(diffIndex, minimumTokenLength)][1] = tokenToGroup
522
- elif (diffIndex, tokenLength) in hostnameGroupDic and hostnameGroupDic[(diffIndex, tokenLength)][1] + 1 == tokenToGroup:
523
- # alternatively, there is already an exact length padded token grouped
524
- hostnameGroupDic[(diffIndex, tokenLength)][1] = tokenToGroup
525
- elif sumDic[lastSumHash][lastHostnameTokens] == newSumDic[lastSumHash][lastHostnameTokens]:
526
- # only when there are no new groups added to this token group this iter, we can add the new group
527
- hostnameGroupDic[(diffIndex, minimumTokenLength)] = [lastTokenToGroup, tokenToGroup]
528
- else:
529
- # skip grouping if there are new groups added to this token group this iter
530
- continue
531
- # move the grouped dic under the new hostname / sum hash
532
- del newSumDic[lastSumHash][lastHostnameTokens]
533
- del sumDic[lastSumHash][lastHostnameTokens]
534
- if not newSumDic[lastSumHash]:
535
- del newSumDic[lastSumHash]
536
- newSumDic.setdefault(sumHash, {})[hostnameTokens] = hostnameGroupDic
537
- # we add the new group to the newSumDic
538
- added = True
539
- break
677
+ lastSumHash in newSumDic and lastHostnameTokens in newSumDic[lastSumHash]:
678
+ #(diffIndex:=findDiffIndex(hostnameTokens, lastHostnameTokens)) != -1 and \
679
+ diffIndex=__findDiffIndex(hostnameTokens, lastHostnameTokens)
680
+ if diffIndex != -1 and \
681
+ sumDic[sumHash][hostnameTokens] == sumDic[lastSumHash][lastHostnameTokens]:
682
+ # the sumDic[sumHash][hostnameTokens] will ba a dic of 2 element value lists with 2 element key representing:
683
+ # (token position that got grouped, the amount of zero padding (length) ):
684
+ # [ the start int token, the end int token]
685
+ # if we entered here, this means we are able to group the two hostnames together
686
+
687
+ if not diffIndex:
688
+ # should never happen, but just in case, we skip grouping
689
+ continue
690
+ tokenToGroup = hostnameTokens[diffIndex]
691
+ try:
692
+ tokenLength = len(tokenToGroup)
693
+ tokenToGroup = int(tokenToGroup)
694
+ except ValueError:
695
+ # if the token is not an int, we skip grouping
696
+ continue
697
+ # group(09 , 10) -> (x, 2): [9, 10]
698
+ # group(9 , 10) -> (x, 0): [9, 10]
699
+ # group(9 , 010) -> not able to group
700
+ # group(009 , 10) -> not able to group
701
+ # group(08, 09) -> (x, 2): [8, 9]
702
+ # group(08, 9) -> not able to group
703
+ # group(8, 09) -> not able to group
704
+ # group(0099, 0100) -> (x, 4): [99, 100]
705
+ # group(0099, 100) -> not able to groups
706
+ # group(099, 100) -> (x, 3): [99, 100]
707
+ # group(99, 100) -> (x, 0): [99, 100]
708
+ lastTokenToGroup = lastHostnameTokens[diffIndex]
709
+ try:
710
+ minimumTokenLength = 0
711
+ lastTokenLength = len(lastTokenToGroup)
712
+ if lastTokenLength > tokenLength:
713
+ raise ValueError('The last token is longer than the current token.')
714
+ elif lastTokenLength < tokenLength:
715
+ if tokenLength - lastTokenLength != 1:
716
+ raise ValueError('The last token is not one less than the current token.')
717
+ # if the last token is not made out of all 9s, we cannot group
718
+ if any(c != '9' for c in lastTokenToGroup):
719
+ raise ValueError('The last token is not made out of all 9s.')
720
+ elif lastTokenToGroup[0] == '0' and lastTokenLength > 1:
721
+ # we have encoutered a padded last token, will set this as the minimum token length
722
+ minimumTokenLength = lastTokenLength
723
+ lastTokenToGroup = int(lastTokenToGroup)
724
+ except ValueError:
725
+ # if the token is not an int, we skip grouping
726
+ continue
727
+ assert lastTokenToGroup + 1 == tokenToGroup, 'Error! The two tokens are not one apart.'
728
+ # we take the last hostname tokens grouped dic out from the newSumDic
729
+ hostnameGroupDic = newSumDic[lastSumHash][lastHostnameTokens].copy()
730
+ if (diffIndex, minimumTokenLength) in hostnameGroupDic and hostnameGroupDic[(diffIndex, minimumTokenLength)][1] + 1 == tokenToGroup:
731
+ # if the token is already grouped, we just update the end token
732
+ hostnameGroupDic[(diffIndex, minimumTokenLength)][1] = tokenToGroup
733
+ elif (diffIndex, tokenLength) in hostnameGroupDic and hostnameGroupDic[(diffIndex, tokenLength)][1] + 1 == tokenToGroup:
734
+ # alternatively, there is already an exact length padded token grouped
735
+ hostnameGroupDic[(diffIndex, tokenLength)][1] = tokenToGroup
736
+ elif sumDic[lastSumHash][lastHostnameTokens] == newSumDic[lastSumHash][lastHostnameTokens]:
737
+ # only when there are no new groups added to this token group this iter, we can add the new group
738
+ hostnameGroupDic[(diffIndex, minimumTokenLength)] = [lastTokenToGroup, tokenToGroup]
739
+ else:
740
+ # skip grouping if there are new groups added to this token group this iter
741
+ continue
742
+ # move the grouped dic under the new hostname / sum hash
743
+ del newSumDic[lastSumHash][lastHostnameTokens]
744
+ del sumDic[lastSumHash][lastHostnameTokens]
745
+ if not newSumDic[lastSumHash]:
746
+ del newSumDic[lastSumHash]
747
+ newSumDic.setdefault(sumHash, {})[hostnameTokens] = hostnameGroupDic
748
+ # we add the new group to the newSumDic
749
+ added = True
750
+ break
540
751
  if not added:
541
752
  # if the two hostnames are not able to group, we just add the last group to the newSumDic
542
753
  newSumDic.setdefault(sumHash, {})[hostnameTokens] = sumDic[sumHash][hostnameTokens].copy()
@@ -546,6 +757,7 @@ def filterSumDic(sumDic):
546
757
  lastSumHash = sumHash
547
758
  return newSumDic
548
759
 
760
+ @cache_decorator
549
761
  def compact_hostnames(Hostnames):
550
762
  """
551
763
  Compact a list of hostnames.
@@ -571,12 +783,12 @@ def compact_hostnames(Hostnames):
571
783
  >>> compact_hostnames(['sub-s1', 'sub-s2'])
572
784
  ['sub-s[1-2]']
573
785
  """
574
- sumDic = generateSumDic(Hostnames)
575
- filteredSumDic = filterSumDic(sumDic)
786
+ sumDic = __generateSumDic(Hostnames)
787
+ filteredSumDic = __filterSumDic(sumDic)
576
788
  lastFilteredSumDicLen = len(filteredSumDic) + 1
577
789
  while lastFilteredSumDicLen > len(filteredSumDic):
578
790
  lastFilteredSumDicLen = len(filteredSumDic)
579
- filteredSumDic = filterSumDic(filteredSumDic)
791
+ filteredSumDic = __filterSumDic(filteredSumDic)
580
792
  rtnSet = set()
581
793
  for sumHash in filteredSumDic:
582
794
  for hostnameTokens in filteredSumDic[sumHash]:
@@ -589,210 +801,89 @@ def compact_hostnames(Hostnames):
589
801
  else:
590
802
  hostnameList[tokenIndex] = f'[{startToken}-{endToken}]'
591
803
  rtnSet.add(''.join(hostnameList))
592
- return rtnSet
804
+ return frozenset(rtnSet)
593
805
 
594
- #%%
806
+ # ------------ Expanding Hostnames ----------------
595
807
  @cache_decorator
596
- def expandIPv4Address(hosts):
597
- '''
598
- Expand the IP address range in the hosts list
599
-
600
- Args:
601
- hosts (list): A list of IP addresses or IP address ranges
602
-
603
- Returns:
604
- list: A list of expanded IP addresses
808
+ def __validate_expand_hostname(hostname):
605
809
  '''
606
- expandedHosts = []
607
- expandedHost = []
608
- for host in hosts:
609
- host = host.replace('[','').replace(']','')
610
- octets = host.split('.')
611
- expandedOctets = []
612
- for octet in octets:
613
- if '-' in octet:
614
- # Handle wildcards
615
- octetRange = octet.split('-')
616
- for i in range(len(octetRange)):
617
- if not octetRange[i] or octetRange[i] in __wildCharacters:
618
- if i == 0:
619
- octetRange[i] = '0'
620
- elif i == 1:
621
- octetRange[i] = '255'
622
-
623
- expandedOctets.append([str(i) for i in range(int(octetRange[0]),int(octetRange[1])+1)])
624
- elif octet in __wildCharacters:
625
- expandedOctets.append([str(i) for i in range(0,256)])
626
- else:
627
- expandedOctets.append([octet])
628
- # handle the first and last subnet addresses
629
- if '0' in expandedOctets[-1]:
630
- expandedOctets[-1].remove('0')
631
- if '255' in expandedOctets[-1]:
632
- expandedOctets[-1].remove('255')
633
- #print(expandedOctets)
634
- # Generate the expanded hosts
635
- for ip in list(product(expandedOctets[0],expandedOctets[1],expandedOctets[2],expandedOctets[3])):
636
- expandedHost.append('.'.join(ip))
637
- expandedHosts.extend(expandedHost)
638
- return expandedHosts
639
-
640
- @cache_decorator
641
- def getIP(hostname,local=False):
642
- '''
643
- Get the IP address of the hostname
810
+ Validate the hostname and expand it if it is a range of IP addresses
644
811
 
645
812
  Args:
646
- hostname (str): The hostname
813
+ hostname (str): The hostname to be validated and expanded
647
814
 
648
815
  Returns:
649
- str: The IP address of the hostname
650
- '''
651
- global _etc_hosts
652
- if '@' in hostname:
653
- _, hostname = hostname.rsplit('@',1)
654
- # First we check if the hostname is an IP address
655
- try:
656
- ipaddress.ip_address(hostname)
657
- return hostname
658
- except ValueError:
659
- pass
660
- # Then we check /etc/hosts
661
- if not _etc_hosts and os.path.exists('/etc/hosts'):
662
- with open('/etc/hosts','r') as f:
663
- for line in f:
664
- if line.startswith('#') or not line.strip():
665
- continue
666
- #ip, host = line.split()[:2]
667
- chunks = line.split()
668
- if len(chunks) < 2:
669
- continue
670
- ip = chunks[0]
671
- for host in chunks[1:]:
672
- _etc_hosts[host] = ip
673
- if hostname in _etc_hosts:
674
- return _etc_hosts[hostname]
675
- if local:
676
- return None
677
- # Then we check the DNS
678
- try:
679
- return socket.gethostbyname(hostname)
680
- except:
681
- return None
682
-
683
- @cache_decorator
684
- def readEnvFromFile(environemnt_file = ''):
685
- '''
686
- Read the environment variables from env_file
687
- Returns:
688
- dict: A dictionary of environment variables
816
+ list: A list of valid hostnames
689
817
  '''
690
- global env
691
- try:
692
- if env:
693
- return env
694
- except:
695
- env = {}
696
- global _env_file
697
- if environemnt_file:
698
- envf = environemnt_file
818
+ global _no_env
819
+ # maybe it is just defined in ./target_files/hosts.sh and exported to the environment
820
+ # we will try to get the valid host name from the environment
821
+ hostname = hostname.strip().strip('$')
822
+ if getIP(hostname,local=True):
823
+ return [hostname]
824
+ elif not _no_env and hostname in os.environ:
825
+ # we will expand these hostnames again
826
+ return expand_hostnames(frozenset(os.environ[hostname].split(',')))
827
+ elif hostname in readEnvFromFile():
828
+ # we will expand these hostnames again
829
+ return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
830
+ elif getIP(hostname,local=False):
831
+ return [hostname]
699
832
  else:
700
- envf = _env_file if _env_file else DEFAULT_ENV_FILE
701
- if os.path.exists(envf):
702
- with open(envf,'r') as f:
703
- for line in f:
704
- if line.startswith('#') or not line.strip():
705
- continue
706
- key, value = line.replace('export ', '', 1).strip().split('=', 1)
707
- key = key.strip().strip('"').strip("'")
708
- value = value.strip().strip('"').strip("'")
709
- # avoid infinite recursion
710
- if key != value:
711
- env[key] = value.strip('"').strip("'")
712
- return env
833
+ eprint(f"Error: {hostname!r} is not a valid hostname or IP address!")
834
+ global __mainReturnCode
835
+ __mainReturnCode += 1
836
+ global __failedHosts
837
+ __failedHosts.add(hostname)
838
+ return []
713
839
 
714
- @cache_decorator
715
- def old_expand_hostname(text,validate=True):
840
+ @cache_decorator
841
+ def __expandIPv4Address(hosts):
716
842
  '''
717
- Expand the hostname range in the text.
718
- Will search the string for a range ( [] encloused and non enclosed number ranges).
719
- Will expand the range, validate them using validate_expand_hostname and return a list of expanded hostnames
843
+ Expand the IP address range in the hosts list
720
844
 
721
845
  Args:
722
- text (str): The text to be expanded
723
- validate (bool, optional): Whether to validate the hostname. Defaults to True.
846
+ hosts (list): A list of IP addresses or IP address ranges
724
847
 
725
848
  Returns:
726
- set: A set of expanded hostnames
849
+ list: A list of expanded IP addresses
727
850
  '''
728
- expandinghosts = [text]
729
- expandedhosts = set()
730
- # all valid alphanumeric characters
731
- alphanumeric = string.digits + string.ascii_letters
732
- while len(expandinghosts) > 0:
733
- hostname = expandinghosts.pop()
734
- match = re.search(r'\[(.*?-.*?)\]', hostname)
735
- if not match:
736
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
737
- continue
738
- try:
739
- range_start, range_end = match.group(1).split('-')
740
- except ValueError:
741
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
742
- continue
743
- range_start = range_start.strip()
744
- range_end = range_end.strip()
745
- if not range_end:
746
- if range_start.isdigit():
747
- range_end = '9'
748
- elif range_start.isalpha() and range_start.islower():
749
- range_end = 'z'
750
- elif range_start.isalpha() and range_start.isupper():
751
- range_end = 'Z'
752
- else:
753
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
754
- continue
755
- if not range_start:
756
- if range_end.isdigit():
757
- range_start = '0'
758
- elif range_end.isalpha() and range_end.islower():
759
- range_start = 'a'
760
- elif range_end.isalpha() and range_end.isupper():
761
- range_start = 'A'
762
- else:
763
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
764
- continue
765
- if range_start.isdigit() and range_end.isdigit():
766
- padding_length = min(len(range_start), len(range_end))
767
- format_str = "{:0" + str(padding_length) + "d}"
768
- for i in range(int(range_start), int(range_end) + 1):
769
- formatted_i = format_str.format(i)
770
- if '[' in hostname:
771
- expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
772
- else:
773
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)) if validate else [hostname])
774
- else:
775
- if all(c in string.hexdigits for c in range_start + range_end):
776
- for i in range(int(range_start, 16), int(range_end, 16)+1):
777
- if '[' in hostname:
778
- expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
779
- else:
780
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)) if validate else [hostname])
851
+ expandedHosts = []
852
+ expandedHost = []
853
+ for host in hosts:
854
+ host = host.replace('[','').replace(']','').strip()
855
+ octets = host.split('.')
856
+ expandedOctets = []
857
+ for octet in octets:
858
+ if '-' in octet:
859
+ # Handle wildcards
860
+ octetRange = octet.split('-')
861
+ for i in range(len(octetRange)):
862
+ if not octetRange[i] or octetRange[i] in __wildCharacters:
863
+ if i == 0:
864
+ octetRange[i] = '0'
865
+ elif i == 1:
866
+ octetRange[i] = '255'
867
+
868
+ expandedOctets.append([str(i) for i in range(int(octetRange[0]),int(octetRange[1])+1)])
869
+ elif octet in __wildCharacters:
870
+ expandedOctets.append([str(i) for i in range(0,256)])
781
871
  else:
782
- try:
783
- start_index = alphanumeric.index(range_start)
784
- end_index = alphanumeric.index(range_end)
785
- for i in range(start_index, end_index + 1):
786
- if '[' in hostname:
787
- expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
788
- else:
789
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)) if validate else [hostname])
790
- except ValueError:
791
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
792
- return expandedhosts
872
+ expandedOctets.append([octet])
873
+ # handle the first and last subnet addresses
874
+ if '0' in expandedOctets[-1]:
875
+ expandedOctets[-1].remove('0')
876
+ if '255' in expandedOctets[-1]:
877
+ expandedOctets[-1].remove('255')
878
+ #print(expandedOctets)
879
+ # Generate the expanded hosts
880
+ for ip in list(product(expandedOctets[0],expandedOctets[1],expandedOctets[2],expandedOctets[3])):
881
+ expandedHost.append('.'.join(ip))
882
+ expandedHosts.extend(expandedHost)
883
+ return expandedHosts
793
884
 
794
885
  @cache_decorator
795
- def expand_hostname(text, validate=True):
886
+ def __expand_hostname(text, validate=True):# -> set:
796
887
  '''
797
888
  Expand the hostname range in the text.
798
889
  Will search the string for a range ( [] enclosed and non-enclosed number ranges).
@@ -813,7 +904,7 @@ def expand_hostname(text, validate=True):
813
904
  hostname = expandinghosts.pop()
814
905
  match = re.search(r'\[(.*?)]', hostname)
815
906
  if not match:
816
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
907
+ expandedhosts.update(__validate_expand_hostname(hostname) if validate else [hostname])
817
908
  continue
818
909
  group = match.group(1)
819
910
  parts = group.split(',')
@@ -823,7 +914,7 @@ def expand_hostname(text, validate=True):
823
914
  try:
824
915
  range_start,_, range_end = part.partition('-')
825
916
  except ValueError:
826
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
917
+ expandedhosts.update(__validate_expand_hostname(hostname) if validate else [hostname])
827
918
  continue
828
919
  range_start = range_start.strip()
829
920
  range_end = range_end.strip()
@@ -843,24 +934,23 @@ def expand_hostname(text, validate=True):
843
934
  for i in range(start_index, end_index + 1):
844
935
  expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
845
936
  except ValueError:
846
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
937
+ expandedhosts.update(__validate_expand_hostname(hostname) if validate else [hostname])
847
938
  else:
848
939
  expandinghosts.append(hostname.replace(match.group(0), part, 1))
849
940
  return expandedhosts
850
941
 
851
-
852
942
  @cache_decorator
853
- def expand_hostnames(hosts):
943
+ def expand_hostnames(hosts) -> dict:
854
944
  '''
855
- Expand the hostnames in the hosts list
945
+ Expand the hostnames in the hosts into a dictionary
856
946
 
857
947
  Args:
858
948
  hosts (list): A list of hostnames
859
949
 
860
950
  Returns:
861
- list: A list of expanded hostnames
951
+ dict: A dictionary of expanded hostnames with key: hostname, value: resolved IP address
862
952
  '''
863
- expandedhosts = []
953
+ expandedhosts = {}
864
954
  if isinstance(hosts, str):
865
955
  hosts = [hosts]
866
956
  for host in hosts:
@@ -870,84 +960,32 @@ def expand_hostnames(hosts):
870
960
  # we seperate the username from the hostname
871
961
  username = None
872
962
  if '@' in host:
873
- username, host = host.split('@',1)
963
+ username, host = host.split('@',1).strip()
874
964
  # first we check if the hostname is an range of IP addresses
875
965
  # This is done by checking if the hostname follows four fields of
876
966
  # "(((\d{1,3}|x|\*|\?)(-(\d{1,3}|x|\*|\?))?)|(\[(\d{1,3}|x|\*|\?)(-(\d{1,3}|x|\*|\?))?\]))"
877
967
  # seperated by .
878
968
  # If so, we expand the IP address range
969
+ iplist = []
879
970
  if re.match(r'^((((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])}|x|\*|\?))?\]))(\.((((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?)(-((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?))?)|(\[((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?)(-((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?))?\]))){2}(\.(((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])}|x|\*|\?))?\]))$', host):
880
- hostSetToAdd = sorted(expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
971
+ hostSetToAdd = sorted(__expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
972
+ iplist = hostSetToAdd
881
973
  else:
882
- hostSetToAdd = sorted(expand_hostname(host))
974
+ hostSetToAdd = sorted(__expand_hostname(host))
975
+ for host in hostSetToAdd:
976
+ iplist.append(getIP(host,local=False))
883
977
  if username:
884
978
  # we expand the username
885
- username = sorted(expand_hostname(username,validate=False))
979
+ username = sorted(__expand_hostname(username,validate=False))
886
980
  # we combine the username and hostname
887
- hostSetToAdd = [u+'@'+h for u,h in product(username,hostSetToAdd)]
888
- expandedhosts.extend(hostSetToAdd)
981
+ for user in username:
982
+ [expandedhosts.update({f'{user}@{host}':ip}) for host,ip in zip(hostSetToAdd,iplist)]
983
+ else:
984
+ [expandedhosts.update({host:ip}) for host,ip in zip(hostSetToAdd,iplist)]
889
985
  return expandedhosts
890
986
 
891
- @cache_decorator
892
- def validate_expand_hostname(hostname):
893
- '''
894
- Validate the hostname and expand it if it is a range of IP addresses
895
-
896
- Args:
897
- hostname (str): The hostname to be validated and expanded
898
-
899
- Returns:
900
- list: A list of valid hostnames
901
- '''
902
- global _no_env
903
- # maybe it is just defined in ./target_files/hosts.sh and exported to the environment
904
- # we will try to get the valid host name from the environment
905
- hostname = hostname.strip('$')
906
- if getIP(hostname,local=True):
907
- return [hostname]
908
- elif not _no_env and hostname in os.environ:
909
- # we will expand these hostnames again
910
- return expand_hostnames(frozenset(os.environ[hostname].split(',')))
911
- elif hostname in readEnvFromFile():
912
- # we will expand these hostnames again
913
- return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
914
- elif getIP(hostname,local=False):
915
- return [hostname]
916
- else:
917
- eprint(f"Error: {hostname!r} is not a valid hostname or IP address!")
918
- global __mainReturnCode
919
- __mainReturnCode += 1
920
- global __failedHosts
921
- __failedHosts.add(hostname)
922
- return []
923
-
924
- def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
925
- """
926
- Read an input from the user with a timeout and a countdown.
927
-
928
- Parameters:
929
- timeout (int): The timeout value in seconds.
930
- prompt (str): The prompt message to display to the user. Default is 'Please enter your selection'.
931
-
932
- Returns:
933
- str or None: The user input if received within the timeout, or None if no input is received.
934
- """
935
- import select
936
- # Print the initial prompt with the countdown
937
- eprint(f"{prompt} [{timeout}s]: ", end='', flush=True)
938
- # Loop until the timeout
939
- for remaining in range(timeout, 0, -1):
940
- # If there is an input, return it
941
- if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
942
- return input().strip()
943
- # Print the remaining time
944
- eprint(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
945
- # Wait a second
946
- time.sleep(1)
947
- # If there is no input, return None
948
- return None
949
-
950
- def handle_reading_stream(stream,target, host):
987
+ # ------------ Run Command Block ----------------
988
+ def __handle_reading_stream(stream,target, host):
951
989
  '''
952
990
  Read the stream and append the lines to the target list
953
991
 
@@ -987,7 +1025,7 @@ def handle_reading_stream(stream,target, host):
987
1025
  if current_line:
988
1026
  add_line(current_line,target, host, keepLastLine=lastLineCommited)
989
1027
 
990
- def handle_writing_stream(stream,stop_event,host):
1028
+ def __handle_writing_stream(stream,stop_event,host):
991
1029
  '''
992
1030
  Write the key presses to the stream
993
1031
 
@@ -1025,34 +1063,6 @@ def handle_writing_stream(stream,stop_event,host):
1025
1063
  # host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
1026
1064
  return sentInput
1027
1065
 
1028
- def replace_magic_strings(string,keys,value,case_sensitive=False):
1029
- '''
1030
- Replace the magic strings in the host object
1031
-
1032
- Args:
1033
- string (str): The string to replace the magic strings
1034
- keys (list): Search for keys to replace
1035
- value (str): The value to replace the key
1036
- case_sensitive (bool, optional): Whether to search for the keys in a case sensitive way. Defaults to False.
1037
-
1038
- Returns:
1039
- str: The string with the magic strings replaced
1040
- '''
1041
- # verify magic strings have # at the beginning and end
1042
- newKeys = []
1043
- for key in keys:
1044
- if key.startswith('#') and key.endswith('#'):
1045
- newKeys.append(key)
1046
- else:
1047
- newKeys.append('#'+key.strip('#')+'#')
1048
- # replace the magic strings
1049
- for key in newKeys:
1050
- if case_sensitive:
1051
- string = string.replace(key,value)
1052
- else:
1053
- string = re.sub(re.escape(key),value,string,flags=re.IGNORECASE)
1054
- return string
1055
-
1056
1066
  def run_command(host, sem, timeout=60,passwds=None):
1057
1067
  '''
1058
1068
  Run the command on the host. Will format the commands accordingly. Main execution function.
@@ -1104,7 +1114,7 @@ def run_command(host, sem, timeout=60,passwds=None):
1104
1114
  host.interface_ip_prefix = __ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
1105
1115
  if host.interface_ip_prefix:
1106
1116
  try:
1107
- hostOctets = getIP(host.address,local=False).split('.')
1117
+ hostOctets = host.ip.split('.')
1108
1118
  prefixOctets = host.interface_ip_prefix.split('.')
1109
1119
  host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
1110
1120
  host.resolvedName = host.username + '@' if host.username else ''
@@ -1232,15 +1242,15 @@ def run_command(host, sem, timeout=60,passwds=None):
1232
1242
  #host.stdout = []
1233
1243
  proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
1234
1244
  # create a thread to handle stdout
1235
- stdout_thread = threading.Thread(target=handle_reading_stream, args=(proc.stdout,host.stdout, host), daemon=True)
1245
+ stdout_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stdout,host.stdout, host), daemon=True)
1236
1246
  stdout_thread.start()
1237
1247
  # create a thread to handle stderr
1238
1248
  #host.stderr = []
1239
- stderr_thread = threading.Thread(target=handle_reading_stream, args=(proc.stderr,host.stderr, host), daemon=True)
1249
+ stderr_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stderr,host.stderr, host), daemon=True)
1240
1250
  stderr_thread.start()
1241
1251
  # create a thread to handle stdin
1242
1252
  stdin_stop_event = threading.Event()
1243
- stdin_thread = threading.Thread(target=handle_writing_stream, args=(proc.stdin,stdin_stop_event, host), daemon=True)
1253
+ stdin_thread = threading.Thread(target=__handle_writing_stream, args=(proc.stdin,stdin_stop_event, host), daemon=True)
1244
1254
  stdin_thread.start()
1245
1255
  # Monitor the subprocess and terminate it after the timeout
1246
1256
  host.lastUpdateTime = time.time()
@@ -1291,9 +1301,9 @@ def run_command(host, sem, timeout=60,passwds=None):
1291
1301
  except subprocess.TimeoutExpired:
1292
1302
  pass
1293
1303
  if stdout:
1294
- handle_reading_stream(io.BytesIO(stdout),host.stdout, host)
1304
+ __handle_reading_stream(io.BytesIO(stdout),host.stdout, host)
1295
1305
  if stderr:
1296
- handle_reading_stream(io.BytesIO(stderr),host.stderr, host)
1306
+ __handle_reading_stream(io.BytesIO(stderr),host.stderr, host)
1297
1307
  # if the last line in host.stderr is Connection to * closed., we will remove it
1298
1308
  host.returncode = proc.poll()
1299
1309
  if not host.returncode:
@@ -1333,6 +1343,7 @@ def run_command(host, sem, timeout=60,passwds=None):
1333
1343
  host.scp = True
1334
1344
  run_command(host,sem,timeout,passwds)
1335
1345
 
1346
+ # ------------ Start Threading Block ----------------
1336
1347
  def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cpu_count()):
1337
1348
  '''
1338
1349
  Start running the command on the hosts. Wrapper function for run_command
@@ -1354,7 +1365,8 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
1354
1365
  thread.start()
1355
1366
  return threads
1356
1367
 
1357
- def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1368
+ # ------------ Display Block ----------------
1369
+ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1358
1370
  '''
1359
1371
  Generate a list for the hosts to be displayed on the screen. This is used to display as much relevant information as possible.
1360
1372
 
@@ -1386,7 +1398,7 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1386
1398
  host.printedLines = 0
1387
1399
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
1388
1400
 
1389
- 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, config_reason= 'New Configuration'):
1401
+ 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, config_reason= 'New Configuration'):
1390
1402
  try:
1391
1403
  org_dim = stdscr.getmaxyx()
1392
1404
  new_configured = True
@@ -1413,7 +1425,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1413
1425
  max_num_hosts = max_num_hosts_x * max_num_hosts_y
1414
1426
  if max_num_hosts < 1:
1415
1427
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small to display any hosts')
1416
- hosts_to_display , host_stats = get_hosts_to_display(hosts, max_num_hosts)
1428
+ hosts_to_display , host_stats = _get_hosts_to_display(hosts, max_num_hosts)
1417
1429
  if len(hosts_to_display) == 0:
1418
1430
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'No hosts to display')
1419
1431
  # Now we calculate the actual number of hosts we will display for x and y
@@ -1592,7 +1604,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1592
1604
  if time.perf_counter() - last_refresh_time < 0.01:
1593
1605
  time.sleep(max(0,0.01 - time.perf_counter() + last_refresh_time))
1594
1606
  #stdscr.clear()
1595
- hosts_to_display, host_stats = get_hosts_to_display(hosts, max_num_hosts,hosts_to_display)
1607
+ hosts_to_display, host_stats = _get_hosts_to_display(hosts, max_num_hosts,hosts_to_display)
1596
1608
  for host_window, host in zip(host_windows, hosts_to_display):
1597
1609
  # we will only update the window if there is new output or the window is not fully printed
1598
1610
  if new_configured or host.printedLines < len(host.output):
@@ -1669,7 +1681,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1669
1681
 
1670
1682
  params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
1671
1683
  while params:
1672
- params = generate_display(stdscr, hosts, *params)
1684
+ params = __generate_display(stdscr, hosts, *params)
1673
1685
  if not params:
1674
1686
  break
1675
1687
  if not any([host.returncode is None for host in hosts]):
@@ -1692,7 +1704,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1692
1704
  time.sleep(0.01)
1693
1705
  #time.sleep(0.25)
1694
1706
 
1695
-
1707
+ # ------------ Generate Output Block ----------------
1696
1708
  def print_output(hosts,usejson = False,quiet = False,greppable = False):
1697
1709
  '''
1698
1710
  Print / generate the output of the hosts to the terminal
@@ -1713,29 +1725,18 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1713
1725
  #print(json.dumps([dict(host) for host in hosts],indent=4))
1714
1726
  rtnStr = json.dumps(hosts,indent=4)
1715
1727
  elif greppable:
1716
- outputs = {}
1717
- # transform hosts to dictionaries
1718
- for host in hosts:
1719
- hostPrintOut = f" | cmd: {host['command']} | stdout: "+'↵ '.join(host['stdout'])
1720
- if host['stderr']:
1721
- if host['stderr'][0].strip().startswith('ssh: connect to host '):
1722
- host['stderr'][0] = 'SSH not reachable!'
1723
- elif host['stderr'][-1].strip().endswith('Connection timed out'):
1724
- host['stderr'][-1] = 'SSH connection timed out!'
1725
- elif host['stderr'][-1].strip().endswith('No route to host'):
1726
- host['stderr'][-1] = 'Cannot find host!'
1727
- hostPrintOut += " | stderr: "+'↵ '.join(host['stderr'])
1728
- hostPrintOut += f" | return_code: {host['returncode']}"
1729
- outputs.setdefault(hostPrintOut, set()).add(host['name'])
1728
+ # transform hosts to a 2d list
1730
1729
  rtnStr = '*'*80+'\n'
1731
- for output, hostSet in outputs.items():
1732
- compact_hosts = compact_hostnames(hostSet)
1733
- if expand_hostnames(frozenset(compact_hosts)) != expand_hostnames(frozenset(hostSet)):
1734
- eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
1735
- rtnStr += f"{','.join(hostSet)}{output}\n"
1736
- else:
1737
- rtnStr += f"{','.join(compact_hosts)}{output}\n"
1738
- rtnStr += '*'*80+'\n'
1730
+ rtnList = [['host_name','return_code','output_type','output']]
1731
+ for host in hosts:
1732
+ #header = f"{host['name']} | rc: {host['returncode']} | "
1733
+ for line in host['stdout']:
1734
+ rtnList.append([host['name'],f"rc: {host['returncode']}",'stdout',line])
1735
+ for line in host['stderr']:
1736
+ rtnList.append([host['name'],f"rc: {host['returncode']}",'stderr',line])
1737
+ rtnList.append(['','','',''])
1738
+ rtnStr += pretty_format_table(rtnList)
1739
+ rtnStr += '*'*80+'\n'
1739
1740
  if __keyPressesIn[-1]:
1740
1741
  CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
1741
1742
  rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
@@ -1760,8 +1761,9 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1760
1761
  outputs.setdefault(hostPrintOut, set()).add(host['name'])
1761
1762
  rtnStr = ''
1762
1763
  for output, hostSet in outputs.items():
1764
+ hostSet = frozenset(hostSet)
1763
1765
  compact_hosts = compact_hostnames(hostSet)
1764
- if expand_hostnames(frozenset(compact_hosts)) != expand_hostnames(frozenset(hostSet)):
1766
+ if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
1765
1767
  eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
1766
1768
  compact_hosts = hostSet
1767
1769
  if __global_suppress_printout:
@@ -1769,7 +1771,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1769
1771
  rtnStr += output+'\n'
1770
1772
  else:
1771
1773
  rtnStr += '*'*80+'\n'
1772
- rtnStr += f'These hosts: {",".join(compact_hosts)} have a response of:\n'
1774
+ rtnStr += f'These hosts: "{",".join(sorted(compact_hosts))}" have a response of:\n'
1773
1775
  rtnStr += output+'\n'
1774
1776
  if not __global_suppress_printout or outputs:
1775
1777
  rtnStr += '*'*80+'\n'
@@ -1787,28 +1789,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1787
1789
  print(rtnStr)
1788
1790
  return rtnStr
1789
1791
 
1790
- def signal_handler(sig, frame):
1791
- '''
1792
- Handle the Ctrl C signal
1793
-
1794
- Args:
1795
- sig (int): The signal
1796
- frame (frame): The frame
1797
-
1798
- Returns:
1799
- None
1800
- '''
1801
- global _emo
1802
- if not _emo:
1803
- eprint('Ctrl C caught, exiting...')
1804
- _emo = True
1805
- else:
1806
- eprint('Ctrl C caught again, exiting immediately!')
1807
- # wait for 0.1 seconds to allow the threads to exit
1808
- time.sleep(0.1)
1809
- os.system(f'pkill -ef {os.path.basename(__file__)}')
1810
- sys.exit(0)
1811
-
1792
+ # ------------ Run / Process Hosts Block ----------------
1812
1793
  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):
1813
1794
  global __globalUnavailableHosts
1814
1795
  global _no_env
@@ -1867,6 +1848,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1867
1848
  if not called:
1868
1849
  print_output(hosts,json,greppable=greppable)
1869
1850
 
1851
+ # ------------ Stringfy Block ----------------
1870
1852
  @cache_decorator
1871
1853
  def formHostStr(host) -> str:
1872
1854
  """
@@ -1890,7 +1872,6 @@ def formHostStr(host) -> str:
1890
1872
  host = ','.join(host)
1891
1873
  return host
1892
1874
 
1893
-
1894
1875
  @cache_decorator
1895
1876
  def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1896
1877
  nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
@@ -1946,6 +1927,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1946
1927
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
1947
1928
  return f'multissh {argsStr} {hostStr} {commandStr}'
1948
1929
 
1930
+ # ------------ Main Block ----------------
1949
1931
  def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1950
1932
  nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1951
1933
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
@@ -2081,18 +2063,19 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2081
2063
  if '@' not in host:
2082
2064
  skipHostStr[i] = userStr + host
2083
2065
  skipHostStr = ','.join(skipHostStr)
2084
- targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
2066
+ targetHostDic = expand_hostnames(frozenset(hostStr.split(',')))
2085
2067
  if __DEBUG_MODE:
2086
- eprint(f"Target hosts: {targetHostsList!r}")
2087
- skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
2088
- if skipHostsList:
2089
- eprint(f"Skipping hosts: {skipHostsList!r}")
2068
+ eprint(f"Target hosts: {targetHostDic!r}")
2069
+ skipHostsDic = expand_hostnames(frozenset(skipHostStr.split(',')))
2070
+ if skipHostsDic:
2071
+ eprint(f"Skipping hosts: {skipHostsDic!r}")
2072
+ skipHostSet = set(skipHostsDic).union(skipHostsDic.values())
2090
2073
  if copy_id:
2091
2074
  if 'ssh-copy-id' in _binPaths:
2092
2075
  # we will copy the id to the hosts
2093
2076
  hosts = []
2094
- for host in targetHostsList:
2095
- if host.strip() in skipHostsList: continue
2077
+ for host in targetHostDic:
2078
+ if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
2096
2079
  command = f"{_binPaths['ssh-copy-id']} "
2097
2080
  if identity_file:
2098
2081
  command = f"{command}-i {identity_file} "
@@ -2101,7 +2084,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2101
2084
  command = f"{command}{host}"
2102
2085
  if password and 'sshpass' in _binPaths:
2103
2086
  command = f"{_binPaths['sshpass']} -p {password} {command}"
2104
- hosts.append(Host(host, command,identity_file=identity_file,bash=True))
2087
+ hosts.append(Host(host, command,identity_file=identity_file,bash=True,ip = targetHostDic[host]))
2105
2088
  else:
2106
2089
  eprint(f"> {command}")
2107
2090
  os.system(command)
@@ -2141,23 +2124,23 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2141
2124
  eprint(f"Files: {files!r}")
2142
2125
  if oneonone:
2143
2126
  hosts = []
2144
- if len(commands) != len(set(targetHostsList) - set(skipHostsList)):
2127
+ if len(commands) != len(set(targetHostDic) - set(skipHostSet)):
2145
2128
  eprint("Error: the number of commands must be the same as the number of hosts")
2146
2129
  eprint(f"Number of commands: {len(commands)}")
2147
- eprint(f"Number of hosts: {len(set(targetHostsList) - set(skipHostsList))}")
2130
+ eprint(f"Number of hosts: {len(set(targetHostDic) - set(skipHostSet))}")
2148
2131
  sys.exit(255)
2149
2132
  if not __global_suppress_printout:
2150
2133
  eprint('-'*80)
2151
2134
  eprint("Running in one on one mode")
2152
- for host, command in zip(targetHostsList, commands):
2153
- if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
2135
+ for host, command in zip(targetHostDic, commands):
2136
+ if not ipmi and skipUnreachable and host in unavailableHosts:
2154
2137
  eprint(f"Skipping unavailable host: {host}")
2155
2138
  continue
2156
- if host.strip() in skipHostsList: continue
2139
+ if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
2157
2140
  if file_sync:
2158
- 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,identity_file=identity_file))
2141
+ hosts.append(Host(host, os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip = targetHostDic[host]))
2159
2142
  else:
2160
- hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
2143
+ hosts.append(Host(host, command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip=targetHostDic[host]))
2161
2144
  if not __global_suppress_printout:
2162
2145
  eprint(f"Running command: {command!r} on host: {host!r}")
2163
2146
  if not __global_suppress_printout: eprint('-'*80)
@@ -2168,11 +2151,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2168
2151
  if not commands:
2169
2152
  # run in interactive mode ssh mode
2170
2153
  hosts = []
2171
- for host in targetHostsList:
2172
- if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
2154
+ for host in targetHostDic:
2155
+ if not ipmi and skipUnreachable and host in unavailableHosts:
2173
2156
  if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
2174
2157
  continue
2175
- if host.strip() in skipHostsList: continue
2158
+ if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
2176
2159
  # TODO: use ip to determine if we skip the host or not, also for unavailable hosts
2177
2160
  if file_sync:
2178
2161
  eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
@@ -2180,7 +2163,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2180
2163
  elif files:
2181
2164
  eprint(f"Error: files need to be specified with at least one path to sync")
2182
2165
  else:
2183
- hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,identity_file=identity_file))
2166
+ hosts.append(Host(host, '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,identity_file=identity_file,ip=targetHostDic[host]))
2184
2167
  if not __global_suppress_printout:
2185
2168
  eprint('-'*80)
2186
2169
  eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -2192,15 +2175,15 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2192
2175
  return hosts
2193
2176
  for command in commands:
2194
2177
  hosts = []
2195
- for host in targetHostsList:
2196
- if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
2178
+ for host in targetHostDic:
2179
+ if not ipmi and skipUnreachable and host in unavailableHosts:
2197
2180
  if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
2198
2181
  continue
2199
- if host.strip() in skipHostsList: continue
2182
+ if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
2200
2183
  if file_sync:
2201
- 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,identity_file=identity_file))
2184
+ hosts.append(Host(host, os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip=targetHostDic[host]))
2202
2185
  else:
2203
- hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
2186
+ hosts.append(Host(host, command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip=targetHostDic[host]))
2204
2187
  if not __global_suppress_printout and len(commands) > 1:
2205
2188
  eprint('-'*80)
2206
2189
  eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -2209,7 +2192,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2209
2192
  allHosts += hosts
2210
2193
  return allHosts
2211
2194
 
2212
- def get_default_config(args):
2195
+ # ------------ Default Config Functions ----------------
2196
+ def generate_default_config(args):
2213
2197
  '''
2214
2198
  Get the default config
2215
2199
 
@@ -2260,33 +2244,13 @@ def get_default_config(args):
2260
2244
  def write_default_config(args,CONFIG_FILE,backup = True):
2261
2245
  if backup and os.path.exists(CONFIG_FILE):
2262
2246
  os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
2263
- default_config = get_default_config(args)
2247
+ default_config = generate_default_config(args)
2264
2248
  # apply the updated defualt_config to __configs_from_file and write that to file
2265
2249
  __configs_from_file.update(default_config)
2266
2250
  with open(CONFIG_FILE,'w') as f:
2267
2251
  json.dump(__configs_from_file,f,indent=4)
2268
2252
 
2269
- def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
2270
- '''
2271
- Find the ssh public key file
2272
-
2273
- Args:
2274
- searchPath (str, optional): The path to search. Defaults to DEDAULT_SSH_KEY_SEARCH_PATH.
2275
-
2276
- Returns:
2277
- str: The path to the ssh key file
2278
- '''
2279
- if searchPath:
2280
- sshKeyPath = searchPath
2281
- else:
2282
- sshKeyPath ='~/.ssh'
2283
- possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
2284
- for sshKeyFile in possibleSshKeyFiles:
2285
- if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
2286
- return os.path.join(sshKeyPath,sshKeyFile)
2287
- return None
2288
-
2289
-
2253
+ # ------------ Wrapper Block ----------------
2290
2254
  def main():
2291
2255
  global _emo
2292
2256
  global __global_suppress_printout
@@ -2331,7 +2295,7 @@ def main():
2331
2295
  parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
2332
2296
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
2333
2297
  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)
2334
- parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
2298
+ parser.add_argument("-g","--greppable",'--table', action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
2335
2299
  group = parser.add_mutually_exclusive_group()
2336
2300
  group.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
2337
2301
  group.add_argument("-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
@@ -2350,7 +2314,7 @@ def main():
2350
2314
  try:
2351
2315
  args = parser.parse_intermixed_args()
2352
2316
  except Exception as e:
2353
- eprint(f"Error while parsing arguments: {e!r}")
2317
+ #eprint(f"Error while parsing arguments: {e!r}")
2354
2318
  # try to parse the arguments using parse_known_args
2355
2319
  args, unknown = parser.parse_known_args()
2356
2320
  # if there are unknown arguments, we will try to parse them again using parse_args
@@ -2444,7 +2408,7 @@ def main():
2444
2408
  copy_id=args.copy_id)
2445
2409
  #print('*'*80)
2446
2410
 
2447
- if not __global_suppress_printout: eprint('-'*80)
2411
+ #if not __global_suppress_printout: eprint('-'*80)
2448
2412
 
2449
2413
  succeededHosts = set()
2450
2414
  for host in hosts:
@@ -2477,6 +2441,3 @@ def main():
2477
2441
 
2478
2442
  if __name__ == "__main__":
2479
2443
  main()
2480
-
2481
- # %%
2482
-