multiSSH3 5.6__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
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- import curses
2
+ __curses_available = False
3
+ try:
4
+ import curses
5
+ __curses_available = True
6
+ except ImportError:
7
+ pass
3
8
  import subprocess
4
9
  import threading
5
10
  import time,os
@@ -31,16 +36,159 @@ except AttributeError:
31
36
  # If neither is available, use a dummy decorator
32
37
  def cache_decorator(func):
33
38
  return func
34
- version = '5.06'
39
+ version = '5.24'
35
40
  VERSION = version
36
41
 
37
42
  CONFIG_FILE = '/etc/multiSSH3.config.json'
38
43
 
39
- import sys
40
-
44
+ # ------------ Pre Helper Functions ----------------
41
45
  def eprint(*args, **kwargs):
42
- print(*args, file=sys.stderr, **kwargs)
46
+ try:
47
+ print(*args, file=sys.stderr, **kwargs)
48
+ except Exception as e:
49
+ print(f"Error: Cannot print to stderr: {e}")
50
+ print(*args, **kwargs)
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
43
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 ----------------
44
192
  def load_config_file(config_file):
45
193
  '''
46
194
  Load the config file to global variables
@@ -57,7 +205,7 @@ def load_config_file(config_file):
57
205
  with open(config_file,'r') as f:
58
206
  config = json.load(f)
59
207
  except:
60
- eprint(f"Error: Cannot load config file {config_file}")
208
+ eprint(f"Error: Cannot load config file {config_file!r}")
61
209
  return {}
62
210
  return config
63
211
 
@@ -122,130 +270,79 @@ __build_in_default_config = {
122
270
  '__DEBUG_MODE': False,
123
271
  }
124
272
 
125
- AUTHOR = __configs_from_file.get('AUTHOR', __build_in_default_config['AUTHOR'])
126
- AUTHOR_EMAIL = __configs_from_file.get('AUTHOR_EMAIL', __build_in_default_config['AUTHOR_EMAIL'])
127
-
128
- DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_config['DEFAULT_HOSTS'])
129
- DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
130
- DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
131
- DEFAULT_PASSWORD = __configs_from_file.get('DEFAULT_PASSWORD', __build_in_default_config['DEFAULT_PASSWORD'])
132
- DEFAULT_IDENTITY_FILE = __configs_from_file.get('DEFAULT_IDENTITY_FILE', __build_in_default_config['DEFAULT_IDENTITY_FILE'])
133
- DEDAULT_SSH_KEY_SEARCH_PATH = __configs_from_file.get('DEDAULT_SSH_KEY_SEARCH_PATH', __build_in_default_config['DEDAULT_SSH_KEY_SEARCH_PATH'])
134
- DEFAULT_USE_KEY = __configs_from_file.get('DEFAULT_USE_KEY', __build_in_default_config['DEFAULT_USE_KEY'])
135
- DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
136
- DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
137
- DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
138
- DEFAULT_FILE_SYNC = __configs_from_file.get('DEFAULT_FILE_SYNC', __build_in_default_config['DEFAULT_FILE_SYNC'])
139
- DEFAULT_TIMEOUT = __configs_from_file.get('DEFAULT_TIMEOUT', __build_in_default_config['DEFAULT_TIMEOUT'])
140
- DEFAULT_CLI_TIMEOUT = __configs_from_file.get('DEFAULT_CLI_TIMEOUT', __build_in_default_config['DEFAULT_CLI_TIMEOUT'])
141
- DEFAULT_REPEAT = __configs_from_file.get('DEFAULT_REPEAT', __build_in_default_config['DEFAULT_REPEAT'])
142
- DEFAULT_INTERVAL = __configs_from_file.get('DEFAULT_INTERVAL', __build_in_default_config['DEFAULT_INTERVAL'])
143
- DEFAULT_IPMI = __configs_from_file.get('DEFAULT_IPMI', __build_in_default_config['DEFAULT_IPMI'])
144
- DEFAULT_IPMI_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_IPMI_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_IPMI_INTERFACE_IP_PREFIX'])
145
- DEFAULT_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_INTERFACE_IP_PREFIX'])
146
- DEFAULT_NO_WATCH = __configs_from_file.get('DEFAULT_NO_WATCH', __build_in_default_config['DEFAULT_NO_WATCH'])
147
- DEFAULT_CURSES_MINIMUM_CHAR_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_CHAR_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_CHAR_LEN'])
148
- DEFAULT_CURSES_MINIMUM_LINE_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_LINE_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_LINE_LEN'])
149
- DEFAULT_SINGLE_WINDOW = __configs_from_file.get('DEFAULT_SINGLE_WINDOW', __build_in_default_config['DEFAULT_SINGLE_WINDOW'])
150
- DEFAULT_ERROR_ONLY = __configs_from_file.get('DEFAULT_ERROR_ONLY', __build_in_default_config['DEFAULT_ERROR_ONLY'])
151
- DEFAULT_NO_OUTPUT = __configs_from_file.get('DEFAULT_NO_OUTPUT', __build_in_default_config['DEFAULT_NO_OUTPUT'])
152
- DEFAULT_NO_ENV = __configs_from_file.get('DEFAULT_NO_ENV', __build_in_default_config['DEFAULT_NO_ENV'])
153
- DEFAULT_MAX_CONNECTIONS = __configs_from_file.get('DEFAULT_MAX_CONNECTIONS', __build_in_default_config['DEFAULT_MAX_CONNECTIONS'])
154
- if not DEFAULT_MAX_CONNECTIONS:
155
- DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
156
- DEFAULT_JSON_MODE = __configs_from_file.get('DEFAULT_JSON_MODE', __build_in_default_config['DEFAULT_JSON_MODE'])
157
- DEFAULT_PRINT_SUCCESS_HOSTS = __configs_from_file.get('DEFAULT_PRINT_SUCCESS_HOSTS', __build_in_default_config['DEFAULT_PRINT_SUCCESS_HOSTS'])
158
- DEFAULT_GREPPABLE_MODE = __configs_from_file.get('DEFAULT_GREPPABLE_MODE', __build_in_default_config['DEFAULT_GREPPABLE_MODE'])
159
- DEFAULT_SKIP_UNREACHABLE = __configs_from_file.get('DEFAULT_SKIP_UNREACHABLE', __build_in_default_config['DEFAULT_SKIP_UNREACHABLE'])
160
- DEFAULT_SKIP_HOSTS = __configs_from_file.get('DEFAULT_SKIP_HOSTS', __build_in_default_config['DEFAULT_SKIP_HOSTS'])
161
-
162
- SSH_STRICT_HOST_KEY_CHECKING = __configs_from_file.get('SSH_STRICT_HOST_KEY_CHECKING', __build_in_default_config['SSH_STRICT_HOST_KEY_CHECKING'])
163
-
164
- ERROR_MESSAGES_TO_IGNORE = __configs_from_file.get('ERROR_MESSAGES_TO_IGNORE', __build_in_default_config['ERROR_MESSAGES_TO_IGNORE'])
165
-
166
- _DEFAULT_CALLED = __configs_from_file.get('_DEFAULT_CALLED', __build_in_default_config['_DEFAULT_CALLED'])
167
- _DEFAULT_RETURN_UNFINISHED = __configs_from_file.get('_DEFAULT_RETURN_UNFINISHED', __build_in_default_config['_DEFAULT_RETURN_UNFINISHED'])
168
- _DEFAULT_UPDATE_UNREACHABLE_HOSTS = __configs_from_file.get('_DEFAULT_UPDATE_UNREACHABLE_HOSTS', __build_in_default_config['_DEFAULT_UPDATE_UNREACHABLE_HOSTS'])
169
- _DEFAULT_NO_START = __configs_from_file.get('_DEFAULT_NO_START', __build_in_default_config['_DEFAULT_NO_START'])
170
-
171
- # form the regex from the list
172
- __ERROR_MESSAGES_TO_IGNORE_REGEX = __configs_from_file.get('__ERROR_MESSAGES_TO_IGNORE_REGEX', __build_in_default_config['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
173
- if __ERROR_MESSAGES_TO_IGNORE_REGEX:
174
- eprint('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
175
- __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__configs_from_file['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
176
- else:
177
- __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
178
-
179
- __DEBUG_MODE = __configs_from_file.get('__DEBUG_MODE', __build_in_default_config['__DEBUG_MODE'])
180
-
181
-
182
-
183
- __global_suppress_printout = False
184
-
185
- __mainReturnCode = 0
186
- __failedHosts = set()
187
- __host_i_lock = threading.Lock()
188
- __host_i_counter = -1
189
- def get_i():
190
- '''
191
- Get the global counter for the host objects
192
-
193
- Returns:
194
- int: The global counter for the host objects
195
- '''
196
- global __host_i_counter
197
- global __host_i_lock
198
- with __host_i_lock:
199
- __host_i_counter += 1
200
- return __host_i_counter
201
-
202
- class Host:
203
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None):
204
- self.name = name # the name of the host (hostname or IP address)
205
- self.command = command # the command to run on the host
206
- self.returncode = None # the return code of the command
207
- self.output = [] # the output of the command for curses
208
- self.stdout = [] # the stdout of the command
209
- self.stderr = [] # the stderr of the command
210
- self.printedLines = -1 # the number of lines printed on the screen
211
- self.lastUpdateTime = time.time() # the last time the output was updated
212
- self.files = files # the files to be copied to the host
213
- self.ipmi = ipmi # whether to use ipmi to connect to the host
214
- self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
215
- self.scp = scp # whether to use scp to copy files to the host
216
- self.gatherMode = gatherMode # whether the host is in gather mode
217
- self.extraargs = extraargs # extra arguments to be passed to ssh
218
- self.resolvedName = None # the resolved IP address of the host
219
- # also store a globally unique integer i from 0
220
- self.i = get_i()
221
- self.uuid = uuid.uuid4()
222
- self.identity_file = identity_file
223
-
224
- def __iter__(self):
225
- return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
226
- def __repr__(self):
227
- # return the complete data structure
228
- 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}"
229
- def __str__(self):
230
- return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
231
-
232
- __wildCharacters = ['*','?','x']
233
-
234
- _no_env = DEFAULT_NO_ENV
235
-
236
- _env_file = DEFAULT_ENV_FILE
237
-
238
- __globalUnavailableHosts = set()
239
-
240
- __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
241
-
242
- __keyPressesIn = [[]]
243
-
244
- _emo = False
245
-
246
- _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
247
-
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'])
248
344
 
345
+ # ------------ Exportable Help Functions ----------------
249
346
  # check if command sshpass is available
250
347
  _binPaths = {}
251
348
  def check_path(program_name):
@@ -265,130 +362,531 @@ def check_path(program_name):
265
362
 
266
363
  [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash','ssh-copy-id']]
267
364
 
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.
268
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
269
384
 
270
385
  @cache_decorator
271
- def expandIPv4Address(hosts):
386
+ def readEnvFromFile(environemnt_file = ''):
272
387
  '''
273
- Expand the IP address range in the hosts list
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
274
419
 
275
420
  Args:
276
- hosts (list): A list of IP addresses or IP address ranges
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.
277
425
 
278
426
  Returns:
279
- list: A list of expanded IP addresses
427
+ str: The string with the magic strings replaced
280
428
  '''
281
- expandedHosts = []
282
- expandedHost = []
283
- for host in hosts:
284
- host = host.replace('[','').replace(']','')
285
- octets = host.split('.')
286
- expandedOctets = []
287
- for octet in octets:
288
- if '-' in octet:
289
- # Handle wildcards
290
- octetRange = octet.split('-')
291
- for i in range(len(octetRange)):
292
- if not octetRange[i] or octetRange[i] in __wildCharacters:
293
- if i == 0:
294
- octetRange[i] = '0'
295
- elif i == 1:
296
- octetRange[i] = '255'
297
-
298
- expandedOctets.append([str(i) for i in range(int(octetRange[0]),int(octetRange[1])+1)])
299
- elif octet in __wildCharacters:
300
- expandedOctets.append([str(i) for i in range(0,256)])
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):
521
+ """
522
+ Translate a list of tokens in string to a list of integers with positional information.
523
+
524
+ Args:
525
+ tokens (tuple): A tuple of tokens.
526
+
527
+ Returns:
528
+ list: A list of integers.
529
+
530
+ Example:
531
+ >>> tuple(hashTokens(('1')))
532
+ (1,)
533
+ >>> tuple(hashTokens(('1', '2')))
534
+ (1, 2)
535
+ >>> tuple(hashTokens(('1', '.', '2')))
536
+ (1, -5047856122680242044, 2)
537
+ >>> tuple(hashTokens(('Process', '-', 'C', '5', '-', '15')))
538
+ (117396829274297939, 7549860403020794775, 8629208860073383633, 5, 7549860403020794775, 15)
539
+ >>> tuple(hashTokens(('192', '.', '168', '.', '1', '.', '1')))
540
+ (192, -5047856122680242044, 168, -5047856122680242044, 1, -5047856122680242044, 1)
541
+ """
542
+ return tuple(int(token) if token.isdigit() else hash(token) for token in tokens)
543
+
544
+ def __findDiffIndex(token1, token2):
545
+ """
546
+ Find the index of the first difference between two lists of tokens.
547
+ If there is more than one difference, return -1.
548
+
549
+ Args:
550
+ token1 (tuple): A list of tokens.
551
+ token2 (tuple): A list of tokens.
552
+
553
+ Returns:
554
+ int: The index of the first difference between the two lists of tokens.
555
+
556
+ Example:
557
+ >>> findDiffIndex(('1',), ('1',))
558
+ -1
559
+ >>> findDiffIndex(('1','2'), ('1', '1'))
560
+ 1
561
+ >>> findDiffIndex(('1','1'), ('1', '1', '1'))
562
+ Traceback (most recent call last):
563
+ ...
564
+ ValueError: The two lists must have the same length.
565
+ >>> findDiffIndex(('192', '.', '168', '.', '2', '.', '1'), ('192', '.', '168', '.', '1', '.', '1'))
566
+ 4
567
+ >>> findDiffIndex(('192', '.', '168', '.', '2', '.', '1'), ('192', '.', '168', '.', '1', '.', '2'))
568
+ -1
569
+ >>> findDiffIndex(('Process', '-', 'C', '5', '-', '15'), ('Process', '-', 'C', '5', '-', '15'))
570
+ -1
571
+ >>> findDiffIndex(('Process', '-', 'C', '5', '-', '15'), ('Process', '-', 'C', '5', '-', '16'))
572
+ 5
573
+ >>> findDiffIndex(tokenize_hostname('nebulahost3'), tokenize_hostname('nebulaleaf3'))
574
+ -1
575
+ >>> findDiffIndex(tokenize_hostname('nebulaleaf3'), tokenize_hostname('nebulaleaf4'))
576
+ 1
577
+ """
578
+ if len(token1) != len(token2):
579
+ raise ValueError('The two lists must have the same length.')
580
+ rtn = -1
581
+ for i, (subToken1, subToken2) in enumerate(zip(token1, token2)):
582
+ if subToken1 != subToken2:
583
+ if rtn == -1 and subToken1.isdigit() and subToken2.isdigit():
584
+ rtn = i
301
585
  else:
302
- expandedOctets.append([octet])
303
- # handle the first and last subnet addresses
304
- if '0' in expandedOctets[-1]:
305
- expandedOctets[-1].remove('0')
306
- if '255' in expandedOctets[-1]:
307
- expandedOctets[-1].remove('255')
308
- #print(expandedOctets)
309
- # Generate the expanded hosts
310
- for ip in list(product(expandedOctets[0],expandedOctets[1],expandedOctets[2],expandedOctets[3])):
311
- expandedHost.append('.'.join(ip))
312
- expandedHosts.extend(expandedHost)
313
- return expandedHosts
586
+ return -1
587
+ return rtn
588
+
589
+ def __generateSumDic(Hostnames):
590
+ """
591
+ Generate a dictionary of sums of tokens for a list of hostnames.
592
+
593
+ Args:
594
+ Hostnames (list): A list of hostnames.
595
+
596
+ Example:
597
+ >>> generateSumDic(['localhost'])
598
+ {6564370170492138900: {('localhost',): {}}}
599
+ >>> generateSumDic(['1', '2'])
600
+ {1: {('1',): {}}, 2: {('2',): {}}}
601
+ >>> generateSumDic(['1.1','1.2'])
602
+ {3435203479547611399: {('1', '.', '1'): {}}, 3435203479547611400: {('1', '.', '2'): {}}}
603
+ >>> generateSumDic(['1.2','2.1'])
604
+ {3435203479547611400: {('1', '.', '2'): {}, ('2', '.', '1'): {}}}
605
+ """
606
+ sumDic = {}
607
+ for hostname in reversed(sorted(Hostnames)):
608
+ tokens = __tokenize_hostname(hostname)
609
+ sumHash = sum(__hashTokens(tokens))
610
+ sumDic.setdefault(sumHash, {})[tokens] = {}
611
+ return sumDic
612
+
613
+ def __filterSumDic(sumDic):
614
+ """
615
+ Filter the sumDic to do one order of grouping.
314
616
 
617
+ Args:
618
+ sumDic (dict): A dictionary of sums of tokens.
619
+
620
+ Returns:
621
+ dict: A filtered dictionary of sums of tokens.
622
+
623
+ Example:
624
+ >>> filterSumDic(generateSumDic(['server15', 'server16', 'server17']))
625
+ {-6728831096159691241: {('server', '17'): {(1, 0): [15, 17]}}}
626
+ >>> filterSumDic(generateSumDic(['server15', 'server16', 'server17', 'server18']))
627
+ {-6728831096159691240: {('server', '18'): {(1, 0): [15, 18]}}}
628
+ >>> filterSumDic(generateSumDic(['server-1', 'server-2', 'server-3']))
629
+ {1441623239094376437: {('server', '-', '3'): {(2, 0): [1, 3]}}}
630
+ >>> filterSumDic(generateSumDic(['server-1-2', 'server-1-1', 'server-2-1', 'server-2-2']))
631
+ {9612077574348444129: {('server', '-', '1', '-', '2'): {(4, 0): [1, 2]}}, 9612077574348444130: {('server', '-', '2', '-', '2'): {(4, 0): [1, 2]}}}
632
+ >>> filterSumDic(generateSumDic(['server-1-2', 'server-1-1', 'server-2-2']))
633
+ {9612077574348444129: {('server', '-', '1', '-', '2'): {(4, 0): [1, 2]}}, 9612077574348444130: {('server', '-', '2', '-', '2'): {}}}
634
+ >>> filterSumDic(generateSumDic(['test1-a', 'test2-a']))
635
+ {12310874833182455839: {('test', '2', '-', 'a'): {(1, 0): [1, 2]}}}
636
+ >>> filterSumDic(generateSumDic(['sub-s1', 'sub-s2']))
637
+ {15455586825715425366: {('sub', '-', 's', '2'): {(3, 0): [1, 2]}}}
638
+ >>> filterSumDic(generateSumDic(['s9', 's10', 's11']))
639
+ {1169697225593811728: {('s', '11'): {(1, 0): [9, 11]}}}
640
+ >>> filterSumDic(generateSumDic(['s99', 's98', 's100','s101']))
641
+ {1169697225593811818: {('s', '101'): {(1, 0): [98, 101]}}}
642
+ >>> filterSumDic(generateSumDic(['s08', 's09', 's10', 's11']))
643
+ {1169697225593811728: {('s', '11'): {(1, 2): [8, 11]}}}
644
+ >>> filterSumDic(generateSumDic(['s099', 's098', 's100','s101']))
645
+ {1169697225593811818: {('s', '101'): {(1, 3): [98, 101]}}}
646
+ >>> filterSumDic(generateSumDic(['server1', 'server2', 'server3','server04']))
647
+ {-6728831096159691255: {('server', '3'): {(1, 0): [1, 3]}}, -6728831096159691254: {('server', '04'): {}}}
648
+ >>> filterSumDic(generateSumDic(['server9', 'server09', 'server10','server10']))
649
+ {-6728831096159691249: {('server', '09'): {}}, -6728831096159691248: {('server', '10'): {(1, 0): [9, 10]}}}
650
+ >>> filterSumDic(generateSumDic(['server09', 'server9', 'server10']))
651
+ {-6728831096159691249: {('server', '9'): {}}, -6728831096159691248: {('server', '10'): {(1, 2): [9, 10]}}}
652
+ """
653
+ lastSumHash = None
654
+ newSumDic = {}
655
+ for key, value in sumDic.items():
656
+ newSumDic[key] = value.copy()
657
+ sumDic = newSumDic
658
+ newSumDic = {}
659
+ for sumHash in sorted(sumDic):
660
+ if lastSumHash is None:
661
+ lastSumHash = sumHash
662
+ newSumDic[sumHash] = sumDic[sumHash].copy()
663
+ continue
664
+ if sumHash - lastSumHash == 1:
665
+ # this means the distence between these two group of hostnames is 1, thus we try to group them together
666
+ for hostnameTokens in sumDic[sumHash]:
667
+ added = False
668
+ if lastSumHash in newSumDic and sumDic[lastSumHash]:
669
+ for lastHostnameTokens in sumDic[lastSumHash].copy():
670
+ # if the two hostnames are able to group, we group them together
671
+ # the two hostnames are able to group if:
672
+ # 1. the two hostnames have the same amount of tokens
673
+ # 2. the last hostname is not already been grouped
674
+ # 3. the two hostnames have the same tokens except for one token
675
+ # 4. the two hostnames have the same token groups
676
+ if len(hostnameTokens) == len(lastHostnameTokens) and \
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
751
+ if not added:
752
+ # if the two hostnames are not able to group, we just add the last group to the newSumDic
753
+ newSumDic.setdefault(sumHash, {})[hostnameTokens] = sumDic[sumHash][hostnameTokens].copy()
754
+ else:
755
+ # this means the distence between these two group of hostnames is not 1, thus we just add the last group to the newSumDic
756
+ newSumDic[sumHash] = sumDic[sumHash].copy()
757
+ lastSumHash = sumHash
758
+ return newSumDic
759
+
760
+ @cache_decorator
761
+ def compact_hostnames(Hostnames):
762
+ """
763
+ Compact a list of hostnames.
764
+ Compact numeric numbers into ranges.
765
+
766
+ Args:
767
+ Hostnames (list): A list of hostnames.
768
+
769
+ Returns:
770
+ list: A list of comapcted hostname list.
771
+
772
+ Example:
773
+ >>> compact_hostnames(['server15', 'server16', 'server17'])
774
+ ['server[15-17]']
775
+ >>> compact_hostnames(['server-1', 'server-2', 'server-3'])
776
+ ['server-[1-3]']
777
+ >>> compact_hostnames(['server-1-2', 'server-1-1', 'server-2-1', 'server-2-2'])
778
+ ['server-[1-2]-[1-2]']
779
+ >>> compact_hostnames(['server-1-2', 'server-1-1', 'server-2-2'])
780
+ ['server-1-[1-2]', 'server-2-2']
781
+ >>> compact_hostnames(['test1-a', 'test2-a'])
782
+ ['test[1-2]-a']
783
+ >>> compact_hostnames(['sub-s1', 'sub-s2'])
784
+ ['sub-s[1-2]']
785
+ """
786
+ sumDic = __generateSumDic(Hostnames)
787
+ filteredSumDic = __filterSumDic(sumDic)
788
+ lastFilteredSumDicLen = len(filteredSumDic) + 1
789
+ while lastFilteredSumDicLen > len(filteredSumDic):
790
+ lastFilteredSumDicLen = len(filteredSumDic)
791
+ filteredSumDic = __filterSumDic(filteredSumDic)
792
+ rtnSet = set()
793
+ for sumHash in filteredSumDic:
794
+ for hostnameTokens in filteredSumDic[sumHash]:
795
+ hostnameGroupDic = filteredSumDic[sumHash][hostnameTokens]
796
+ hostnameList = list(hostnameTokens)
797
+ for tokenIndex, tokenLength in hostnameGroupDic:
798
+ startToken, endToken = hostnameGroupDic[(tokenIndex, tokenLength)]
799
+ if tokenLength:
800
+ hostnameList[tokenIndex] = f'[{startToken:0{tokenLength}d}-{endToken:0{tokenLength}d}]'
801
+ else:
802
+ hostnameList[tokenIndex] = f'[{startToken}-{endToken}]'
803
+ rtnSet.add(''.join(hostnameList))
804
+ return frozenset(rtnSet)
805
+
806
+ # ------------ Expanding Hostnames ----------------
315
807
  @cache_decorator
316
- def getIP(hostname,local=False):
808
+ def __validate_expand_hostname(hostname):
317
809
  '''
318
- Get the IP address of the hostname
810
+ Validate the hostname and expand it if it is a range of IP addresses
319
811
 
320
812
  Args:
321
- hostname (str): The hostname
813
+ hostname (str): The hostname to be validated and expanded
322
814
 
323
815
  Returns:
324
- str: The IP address of the hostname
816
+ list: A list of valid hostnames
325
817
  '''
326
- global _etc_hosts
327
- # First we check if the hostname is an IP address
328
- try:
329
- ipaddress.ip_address(hostname)
330
- return hostname
331
- except ValueError:
332
- pass
333
- # Then we check /etc/hosts
334
- if not _etc_hosts and os.path.exists('/etc/hosts'):
335
- with open('/etc/hosts','r') as f:
336
- for line in f:
337
- if line.startswith('#') or not line.strip():
338
- continue
339
- #ip, host = line.split()[:2]
340
- chunks = line.split()
341
- if len(chunks) < 2:
342
- continue
343
- ip = chunks[0]
344
- for host in chunks[1:]:
345
- _etc_hosts[host] = ip
346
- if hostname in _etc_hosts:
347
- return _etc_hosts[hostname]
348
- if local:
349
- return None
350
- # Then we check the DNS
351
- try:
352
- return socket.gethostbyname(hostname)
353
- except:
354
- return None
355
-
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]
832
+ else:
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 []
839
+
356
840
  @cache_decorator
357
- def readEnvFromFile(environemnt_file = ''):
841
+ def __expandIPv4Address(hosts):
358
842
  '''
359
- Read the environment variables from env_file
843
+ Expand the IP address range in the hosts list
844
+
845
+ Args:
846
+ hosts (list): A list of IP addresses or IP address ranges
847
+
360
848
  Returns:
361
- dict: A dictionary of environment variables
849
+ list: A list of expanded IP addresses
362
850
  '''
363
- global env
364
- try:
365
- if env:
366
- return env
367
- except:
368
- env = {}
369
- global _env_file
370
- if environemnt_file:
371
- envf = environemnt_file
372
- else:
373
- envf = _env_file if _env_file else DEFAULT_ENV_FILE
374
- if os.path.exists(envf):
375
- with open(envf,'r') as f:
376
- for line in f:
377
- if line.startswith('#') or not line.strip():
378
- continue
379
- key, value = line.replace('export ', '', 1).strip().split('=', 1)
380
- key = key.strip().strip('"').strip("'")
381
- value = value.strip().strip('"').strip("'")
382
- # avoid infinite recursion
383
- if key != value:
384
- env[key] = value.strip('"').strip("'")
385
- return env
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)])
871
+ else:
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
386
884
 
387
885
  @cache_decorator
388
- def expand_hostname(text,validate=True):
886
+ def __expand_hostname(text, validate=True):# -> set:
389
887
  '''
390
888
  Expand the hostname range in the text.
391
- Will search the string for a range ( [] encloused and non enclosed number ranges).
889
+ Will search the string for a range ( [] enclosed and non-enclosed number ranges).
392
890
  Will expand the range, validate them using validate_expand_hostname and return a list of expanded hostnames
393
891
 
394
892
  Args:
@@ -404,78 +902,55 @@ def expand_hostname(text,validate=True):
404
902
  alphanumeric = string.digits + string.ascii_letters
405
903
  while len(expandinghosts) > 0:
406
904
  hostname = expandinghosts.pop()
407
- match = re.search(r'\[(.*?-.*?)\]', hostname)
905
+ match = re.search(r'\[(.*?)]', hostname)
408
906
  if not match:
409
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
410
- continue
411
- try:
412
- range_start, range_end = match.group(1).split('-')
413
- except ValueError:
414
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
907
+ expandedhosts.update(__validate_expand_hostname(hostname) if validate else [hostname])
415
908
  continue
416
- range_start = range_start.strip()
417
- range_end = range_end.strip()
418
- if not range_end:
419
- if range_start.isdigit():
420
- range_end = '9'
421
- elif range_start.isalpha() and range_start.islower():
422
- range_end = 'z'
423
- elif range_start.isalpha() and range_start.isupper():
424
- range_end = 'Z'
425
- else:
426
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
427
- continue
428
- if not range_start:
429
- if range_end.isdigit():
430
- range_start = '0'
431
- elif range_end.isalpha() and range_end.islower():
432
- range_start = 'a'
433
- elif range_end.isalpha() and range_end.isupper():
434
- range_start = 'A'
435
- else:
436
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
437
- continue
438
- if range_start.isdigit() and range_end.isdigit():
439
- padding_length = min(len(range_start), len(range_end))
440
- format_str = "{:0" + str(padding_length) + "d}"
441
- for i in range(int(range_start), int(range_end) + 1):
442
- formatted_i = format_str.format(i)
443
- if '[' in hostname:
444
- expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
445
- else:
446
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)) if validate else [hostname])
447
- else:
448
- if all(c in string.hexdigits for c in range_start + range_end):
449
- for i in range(int(range_start, 16), int(range_end, 16)+1):
450
- if '[' in hostname:
451
- expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
452
- else:
453
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)) if validate else [hostname])
454
- else:
909
+ group = match.group(1)
910
+ parts = group.split(',')
911
+ for part in parts:
912
+ part = part.strip()
913
+ if '-' in part:
455
914
  try:
456
- start_index = alphanumeric.index(range_start)
457
- end_index = alphanumeric.index(range_end)
458
- for i in range(start_index, end_index + 1):
459
- if '[' in hostname:
460
- expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
461
- else:
462
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)) if validate else [hostname])
915
+ range_start,_, range_end = part.partition('-')
463
916
  except ValueError:
464
- expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
917
+ expandedhosts.update(__validate_expand_hostname(hostname) if validate else [hostname])
918
+ continue
919
+ range_start = range_start.strip()
920
+ range_end = range_end.strip()
921
+ if range_start.isdigit() and range_end.isdigit():
922
+ padding_length = min(len(range_start), len(range_end))
923
+ format_str = "{:0" + str(padding_length) + "d}"
924
+ for i in range(int(range_start), int(range_end) + 1):
925
+ formatted_i = format_str.format(i)
926
+ expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
927
+ elif all(c in string.hexdigits for c in range_start + range_end):
928
+ for i in range(int(range_start, 16), int(range_end, 16) + 1):
929
+ expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
930
+ else:
931
+ try:
932
+ start_index = alphanumeric.index(range_start)
933
+ end_index = alphanumeric.index(range_end)
934
+ for i in range(start_index, end_index + 1):
935
+ expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
936
+ except ValueError:
937
+ expandedhosts.update(__validate_expand_hostname(hostname) if validate else [hostname])
938
+ else:
939
+ expandinghosts.append(hostname.replace(match.group(0), part, 1))
465
940
  return expandedhosts
466
941
 
467
942
  @cache_decorator
468
- def expand_hostnames(hosts):
943
+ def expand_hostnames(hosts) -> dict:
469
944
  '''
470
- Expand the hostnames in the hosts list
945
+ Expand the hostnames in the hosts into a dictionary
471
946
 
472
947
  Args:
473
948
  hosts (list): A list of hostnames
474
949
 
475
950
  Returns:
476
- list: A list of expanded hostnames
951
+ dict: A dictionary of expanded hostnames with key: hostname, value: resolved IP address
477
952
  '''
478
- expandedhosts = []
953
+ expandedhosts = {}
479
954
  if isinstance(hosts, str):
480
955
  hosts = [hosts]
481
956
  for host in hosts:
@@ -485,84 +960,32 @@ def expand_hostnames(hosts):
485
960
  # we seperate the username from the hostname
486
961
  username = None
487
962
  if '@' in host:
488
- username, host = host.split('@',1)
963
+ username, host = host.split('@',1).strip()
489
964
  # first we check if the hostname is an range of IP addresses
490
965
  # This is done by checking if the hostname follows four fields of
491
966
  # "(((\d{1,3}|x|\*|\?)(-(\d{1,3}|x|\*|\?))?)|(\[(\d{1,3}|x|\*|\?)(-(\d{1,3}|x|\*|\?))?\]))"
492
967
  # seperated by .
493
968
  # If so, we expand the IP address range
969
+ iplist = []
494
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):
495
- hostSetToAdd = sorted(expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
971
+ hostSetToAdd = sorted(__expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
972
+ iplist = hostSetToAdd
496
973
  else:
497
- hostSetToAdd = sorted(expand_hostname(host))
974
+ hostSetToAdd = sorted(__expand_hostname(host))
975
+ for host in hostSetToAdd:
976
+ iplist.append(getIP(host,local=False))
498
977
  if username:
499
978
  # we expand the username
500
- username = sorted(expand_hostname(username,validate=False))
979
+ username = sorted(__expand_hostname(username,validate=False))
501
980
  # we combine the username and hostname
502
- hostSetToAdd = [u+'@'+h for u,h in product(username,hostSetToAdd)]
503
- 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)]
504
985
  return expandedhosts
505
986
 
506
- @cache_decorator
507
- def validate_expand_hostname(hostname):
508
- '''
509
- Validate the hostname and expand it if it is a range of IP addresses
510
-
511
- Args:
512
- hostname (str): The hostname to be validated and expanded
513
-
514
- Returns:
515
- list: A list of valid hostnames
516
- '''
517
- global _no_env
518
- # maybe it is just defined in ./target_files/hosts.sh and exported to the environment
519
- # we will try to get the valid host name from the environment
520
- hostname = hostname.strip('$')
521
- if getIP(hostname,local=True):
522
- return [hostname]
523
- elif not _no_env and hostname in os.environ:
524
- # we will expand these hostnames again
525
- return expand_hostnames(frozenset(os.environ[hostname].split(',')))
526
- elif hostname in readEnvFromFile():
527
- # we will expand these hostnames again
528
- return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
529
- elif getIP(hostname,local=False):
530
- return [hostname]
531
- else:
532
- eprint(f"Error: {hostname} is not a valid hostname or IP address!")
533
- global __mainReturnCode
534
- __mainReturnCode += 1
535
- global __failedHosts
536
- __failedHosts.add(hostname)
537
- return []
538
-
539
- def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
540
- """
541
- Read an input from the user with a timeout and a countdown.
542
-
543
- Parameters:
544
- timeout (int): The timeout value in seconds.
545
- prompt (str): The prompt message to display to the user. Default is 'Please enter your selection'.
546
-
547
- Returns:
548
- str or None: The user input if received within the timeout, or None if no input is received.
549
- """
550
- import select
551
- # Print the initial prompt with the countdown
552
- eprint(f"{prompt} [{timeout}s]: ", end='', flush=True)
553
- # Loop until the timeout
554
- for remaining in range(timeout, 0, -1):
555
- # If there is an input, return it
556
- if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
557
- return input().strip()
558
- # Print the remaining time
559
- eprint(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
560
- # Wait a second
561
- time.sleep(1)
562
- # If there is no input, return None
563
- return None
564
-
565
- def handle_reading_stream(stream,target, host):
987
+ # ------------ Run Command Block ----------------
988
+ def __handle_reading_stream(stream,target, host):
566
989
  '''
567
990
  Read the stream and append the lines to the target list
568
991
 
@@ -602,7 +1025,7 @@ def handle_reading_stream(stream,target, host):
602
1025
  if current_line:
603
1026
  add_line(current_line,target, host, keepLastLine=lastLineCommited)
604
1027
 
605
- def handle_writing_stream(stream,stop_event,host):
1028
+ def __handle_writing_stream(stream,stop_event,host):
606
1029
  '''
607
1030
  Write the key presses to the stream
608
1031
 
@@ -623,8 +1046,9 @@ def handle_writing_stream(stream,stop_event,host):
623
1046
  if sentInput < len(__keyPressesIn) - 1 :
624
1047
  stream.write(''.join(__keyPressesIn[sentInput]).encode())
625
1048
  stream.flush()
626
- host.output.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
627
- host.stdout.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
1049
+ line = '> ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵')
1050
+ host.output.append(line)
1051
+ host.stdout.append(line)
628
1052
  sentInput += 1
629
1053
  host.lastUpdateTime = time.time()
630
1054
  else:
@@ -639,35 +1063,7 @@ def handle_writing_stream(stream,stop_event,host):
639
1063
  # host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
640
1064
  return sentInput
641
1065
 
642
- def replace_magic_strings(string,keys,value,case_sensitive=False):
643
- '''
644
- Replace the magic strings in the host object
645
-
646
- Args:
647
- string (str): The string to replace the magic strings
648
- keys (list): Search for keys to replace
649
- value (str): The value to replace the key
650
- case_sensitive (bool, optional): Whether to search for the keys in a case sensitive way. Defaults to False.
651
-
652
- Returns:
653
- str: The string with the magic strings replaced
654
- '''
655
- # verify magic strings have # at the beginning and end
656
- newKeys = []
657
- for key in keys:
658
- if key.startswith('#') and key.endswith('#'):
659
- newKeys.append(key)
660
- else:
661
- newKeys.append('#'+key.strip('#')+'#')
662
- # replace the magic strings
663
- for key in newKeys:
664
- if case_sensitive:
665
- string = string.replace(key,value)
666
- else:
667
- string = re.sub(re.escape(key),value,string,flags=re.IGNORECASE)
668
- return string
669
-
670
- def ssh_command(host, sem, timeout=60,passwds=None):
1066
+ def run_command(host, sem, timeout=60,passwds=None):
671
1067
  '''
672
1068
  Run the command on the host. Will format the commands accordingly. Main execution function.
673
1069
 
@@ -706,8 +1102,6 @@ def ssh_command(host, sem, timeout=60,passwds=None):
706
1102
  host.command = replace_magic_strings(host.command,['#ID#'],str(id(host)),case_sensitive=False)
707
1103
  host.command = replace_magic_strings(host.command,['#I#'],str(host.i),case_sensitive=False)
708
1104
  host.command = replace_magic_strings(host.command,['#PASSWD#','#PASSWORD#'],passwds,case_sensitive=False)
709
- if host.resolvedName:
710
- host.command = replace_magic_strings(host.command,['#RESOLVEDNAME#','#RESOLVED#'],host.resolvedName,case_sensitive=False)
711
1105
  host.command = replace_magic_strings(host.command,['#UUID#'],str(host.uuid),case_sensitive=False)
712
1106
  formatedCMD = []
713
1107
  if host.extraargs and type(host.extraargs) == str:
@@ -720,7 +1114,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
720
1114
  host.interface_ip_prefix = __ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
721
1115
  if host.interface_ip_prefix:
722
1116
  try:
723
- hostOctets = getIP(host.address,local=False).split('.')
1117
+ hostOctets = host.ip.split('.')
724
1118
  prefixOctets = host.interface_ip_prefix.split('.')
725
1119
  host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
726
1120
  host.resolvedName = host.username + '@' if host.username else ''
@@ -729,6 +1123,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
729
1123
  host.resolvedName = host.name
730
1124
  else:
731
1125
  host.resolvedName = host.name
1126
+ if host.resolvedName:
1127
+ host.command = replace_magic_strings(host.command,['#RESOLVEDNAME#','#RESOLVED#'],host.resolvedName,case_sensitive=False)
732
1128
  if host.ipmi:
733
1129
  if 'ipmitool' in _binPaths:
734
1130
  if host.command.startswith('ipmitool '):
@@ -737,6 +1133,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
737
1133
  host.command = host.command.replace(_binPaths['ipmitool'],'')
738
1134
  if not host.username:
739
1135
  host.username = 'admin'
1136
+ if not host.command:
1137
+ host.command = 'power status'
740
1138
  if 'bash' in _binPaths:
741
1139
  if passwds:
742
1140
  formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
@@ -755,14 +1153,30 @@ def ssh_command(host, sem, timeout=60,passwds=None):
755
1153
  host.stderr.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
756
1154
  host.ipmi = False
757
1155
  host.interface_ip_prefix = None
758
- host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
759
- ssh_command(host,sem,timeout,passwds)
1156
+ if not host.command:
1157
+ host.command = 'ipmitool power status'
1158
+ else:
1159
+ host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
1160
+ run_command(host,sem,timeout,passwds)
760
1161
  return
761
1162
  else:
762
1163
  host.output.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
763
1164
  host.stderr.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
764
1165
  host.returncode = 1
765
1166
  return
1167
+ elif host.bash:
1168
+ if 'bash' in _binPaths:
1169
+ host.output.append('Running command in bash mode, ignoring the hosts...')
1170
+ if __DEBUG_MODE:
1171
+ host.stderr.append('Running command in bash mode, ignoring the hosts...')
1172
+ formatedCMD = [_binPaths['bash'],'-c',host.command]
1173
+ else:
1174
+ host.output.append('Bash not found on the local machine! Using ssh localhost instead...')
1175
+ if __DEBUG_MODE:
1176
+ host.stderr.append('Bash not found on the local machine! Using ssh localhost instead...')
1177
+ host.bash = False
1178
+ host.name = 'localhost'
1179
+ run_command(host,sem,timeout,passwds)
766
1180
  else:
767
1181
  if host.files:
768
1182
  if host.scp:
@@ -828,15 +1242,15 @@ def ssh_command(host, sem, timeout=60,passwds=None):
828
1242
  #host.stdout = []
829
1243
  proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
830
1244
  # create a thread to handle stdout
831
- 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)
832
1246
  stdout_thread.start()
833
1247
  # create a thread to handle stderr
834
1248
  #host.stderr = []
835
- 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)
836
1250
  stderr_thread.start()
837
1251
  # create a thread to handle stdin
838
1252
  stdin_stop_event = threading.Event()
839
- 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)
840
1254
  stdin_thread.start()
841
1255
  # Monitor the subprocess and terminate it after the timeout
842
1256
  host.lastUpdateTime = time.time()
@@ -887,9 +1301,9 @@ def ssh_command(host, sem, timeout=60,passwds=None):
887
1301
  except subprocess.TimeoutExpired:
888
1302
  pass
889
1303
  if stdout:
890
- handle_reading_stream(io.BytesIO(stdout),host.stdout, host)
1304
+ __handle_reading_stream(io.BytesIO(stdout),host.stdout, host)
891
1305
  if stderr:
892
- handle_reading_stream(io.BytesIO(stderr),host.stderr, host)
1306
+ __handle_reading_stream(io.BytesIO(stderr),host.stderr, host)
893
1307
  # if the last line in host.stderr is Connection to * closed., we will remove it
894
1308
  host.returncode = proc.poll()
895
1309
  if not host.returncode:
@@ -918,7 +1332,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
918
1332
  host.ipmi = False
919
1333
  host.interface_ip_prefix = None
920
1334
  host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
921
- ssh_command(host,sem,timeout,passwds)
1335
+ run_command(host,sem,timeout,passwds)
922
1336
  # If transfering files, we will try again using scp if rsync connection is not successful
923
1337
  if host.files and not host.scp and not useScp and host.returncode != 0 and host.stderr:
924
1338
  host.stderr = []
@@ -927,11 +1341,12 @@ def ssh_command(host, sem, timeout=60,passwds=None):
927
1341
  if __DEBUG_MODE:
928
1342
  host.stderr.append('Rsync connection failed! Trying SCP connection...')
929
1343
  host.scp = True
930
- ssh_command(host,sem,timeout,passwds)
1344
+ run_command(host,sem,timeout,passwds)
931
1345
 
1346
+ # ------------ Start Threading Block ----------------
932
1347
  def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cpu_count()):
933
1348
  '''
934
- Start running the command on the hosts. Wrapper function for ssh_command
1349
+ Start running the command on the hosts. Wrapper function for run_command
935
1350
 
936
1351
  Args:
937
1352
  hosts (list): A list of Host objects
@@ -945,12 +1360,13 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
945
1360
  if len(hosts) == 0:
946
1361
  return []
947
1362
  sem = threading.Semaphore(max_connections) # Limit concurrent SSH sessions
948
- threads = [threading.Thread(target=ssh_command, args=(host, sem,timeout,password), daemon=True) for host in hosts]
1363
+ threads = [threading.Thread(target=run_command, args=(host, sem,timeout,password), daemon=True) for host in hosts]
949
1364
  for thread in threads:
950
1365
  thread.start()
951
1366
  return threads
952
1367
 
953
- 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):
954
1370
  '''
955
1371
  Generate a list for the hosts to be displayed on the screen. This is used to display as much relevant information as possible.
956
1372
 
@@ -982,14 +1398,7 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
982
1398
  host.printedLines = 0
983
1399
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
984
1400
 
985
- # Error: integer division or modulo by zero, Reloading Configuration: min_char_len=40, min_line_len=1, single_window=False with window size (61, 186) and 1 hosts...
986
- # Traceback (most recent call last):
987
- # File "/usr/local/lib/python3.11/site-packages/multiSSH3.py", line 1030, in generate_display
988
- # host_window_height = max_y // num_hosts_y
989
- # ~~~~~~^^~~~~~~~~~~~~
990
- # ZeroDivisionError: integer division or modulo by zero
991
-
992
- 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'):
993
1402
  try:
994
1403
  org_dim = stdscr.getmaxyx()
995
1404
  new_configured = True
@@ -1016,7 +1425,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1016
1425
  max_num_hosts = max_num_hosts_x * max_num_hosts_y
1017
1426
  if max_num_hosts < 1:
1018
1427
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small to display any hosts')
1019
- 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)
1020
1429
  if len(hosts_to_display) == 0:
1021
1430
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'No hosts to display')
1022
1431
  # Now we calculate the actual number of hosts we will display for x and y
@@ -1195,7 +1604,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
1195
1604
  if time.perf_counter() - last_refresh_time < 0.01:
1196
1605
  time.sleep(max(0,0.01 - time.perf_counter() + last_refresh_time))
1197
1606
  #stdscr.clear()
1198
- 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)
1199
1608
  for host_window, host in zip(host_windows, hosts_to_display):
1200
1609
  # we will only update the window if there is new output or the window is not fully printed
1201
1610
  if new_configured or host.printedLines < len(host.output):
@@ -1272,7 +1681,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1272
1681
 
1273
1682
  params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
1274
1683
  while params:
1275
- params = generate_display(stdscr, hosts, *params)
1684
+ params = __generate_display(stdscr, hosts, *params)
1276
1685
  if not params:
1277
1686
  break
1278
1687
  if not any([host.returncode is None for host in hosts]):
@@ -1295,7 +1704,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1295
1704
  time.sleep(0.01)
1296
1705
  #time.sleep(0.25)
1297
1706
 
1298
-
1707
+ # ------------ Generate Output Block ----------------
1299
1708
  def print_output(hosts,usejson = False,quiet = False,greppable = False):
1300
1709
  '''
1301
1710
  Print / generate the output of the hosts to the terminal
@@ -1316,23 +1725,18 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1316
1725
  #print(json.dumps([dict(host) for host in hosts],indent=4))
1317
1726
  rtnStr = json.dumps(hosts,indent=4)
1318
1727
  elif greppable:
1319
- outputs = {}
1320
- # transform hosts to dictionaries
1321
- for host in hosts:
1322
- hostPrintOut = f" | cmd: {host['command']} | stdout: "+'↵ '.join(host['stdout'])
1323
- if host['stderr']:
1324
- if host['stderr'][0].strip().startswith('ssh: connect to host '):
1325
- host['stderr'][0] = 'SSH not reachable!'
1326
- hostPrintOut += " | stderr: "+'↵ '.join(host['stderr'])
1327
- hostPrintOut += f" | return_code: {host['returncode']}"
1328
- if hostPrintOut not in outputs:
1329
- outputs[hostPrintOut] = [host['name']]
1330
- else:
1331
- outputs[hostPrintOut].append(host['name'])
1728
+ # transform hosts to a 2d list
1332
1729
  rtnStr = '*'*80+'\n'
1333
- for output, hosts in outputs.items():
1334
- rtnStr += f"{','.join(hosts)}{output}\n"
1335
- 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'
1336
1740
  if __keyPressesIn[-1]:
1337
1741
  CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
1338
1742
  rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
@@ -1348,20 +1752,26 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1348
1752
  if host['stderr']:
1349
1753
  if host['stderr'][0].strip().startswith('ssh: connect to host '):
1350
1754
  host['stderr'][0] = 'SSH not reachable!'
1755
+ elif host['stderr'][-1].strip().endswith('Connection timed out'):
1756
+ host['stderr'][-1] = 'SSH connection timed out!'
1757
+ elif host['stderr'][-1].strip().endswith('No route to host'):
1758
+ host['stderr'][-1] = 'Cannot find host!'
1351
1759
  hostPrintOut += "\n stderr:\n "+'\n '.join(host['stderr'])
1352
1760
  hostPrintOut += f"\n return_code: {host['returncode']}"
1353
- if hostPrintOut not in outputs:
1354
- outputs[hostPrintOut] = [host['name']]
1355
- else:
1356
- outputs[hostPrintOut].append(host['name'])
1761
+ outputs.setdefault(hostPrintOut, set()).add(host['name'])
1357
1762
  rtnStr = ''
1358
- for output, hosts in outputs.items():
1763
+ for output, hostSet in outputs.items():
1764
+ hostSet = frozenset(hostSet)
1765
+ compact_hosts = compact_hostnames(hostSet)
1766
+ if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
1767
+ eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
1768
+ compact_hosts = hostSet
1359
1769
  if __global_suppress_printout:
1360
- rtnStr += f'Abnormal returncode produced by {hosts}:\n'
1770
+ rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
1361
1771
  rtnStr += output+'\n'
1362
1772
  else:
1363
1773
  rtnStr += '*'*80+'\n'
1364
- rtnStr += f"These hosts: {hosts} have a response of:\n"
1774
+ rtnStr += f'These hosts: "{",".join(sorted(compact_hosts))}" have a response of:\n'
1365
1775
  rtnStr += output+'\n'
1366
1776
  if not __global_suppress_printout or outputs:
1367
1777
  rtnStr += '*'*80+'\n'
@@ -1379,62 +1789,12 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1379
1789
  print(rtnStr)
1380
1790
  return rtnStr
1381
1791
 
1382
- # sshConfigged = False
1383
- # def verify_ssh_config():
1384
- # '''
1385
- # Verify that ~/.ssh/config exists and contains the line "StrictHostKeyChecking no"
1386
-
1387
- # Args:
1388
- # None
1389
-
1390
- # Returns:
1391
- # None
1392
- # '''
1393
- # global sshConfigged
1394
- # if not sshConfigged:
1395
- # # first we make sure ~/.ssh/config exists
1396
- # config = ''
1397
- # if not os.path.exists(os.path.expanduser('~/.ssh')):
1398
- # os.makedirs(os.path.expanduser('~/.ssh'))
1399
- # if os.path.exists(os.path.expanduser('~/.ssh/config')):
1400
- # with open(os.path.expanduser('~/.ssh/config'),'r') as f:
1401
- # config = f.read()
1402
- # if config:
1403
- # if 'StrictHostKeyChecking no' not in config:
1404
- # with open(os.path.expanduser('~/.ssh/config'),'a') as f:
1405
- # f.write('\nHost *\n\tStrictHostKeyChecking no\n')
1406
- # else:
1407
- # with open(os.path.expanduser('~/.ssh/config'),'w') as f:
1408
- # f.write('Host *\n\tStrictHostKeyChecking no\n')
1409
- # sshConfigged = True
1410
-
1411
- def signal_handler(sig, frame):
1412
- '''
1413
- Handle the Ctrl C signal
1414
-
1415
- Args:
1416
- sig (int): The signal
1417
- frame (frame): The frame
1418
-
1419
- Returns:
1420
- None
1421
- '''
1422
- global _emo
1423
- if not _emo:
1424
- eprint('Ctrl C caught, exiting...')
1425
- _emo = True
1426
- else:
1427
- eprint('Ctrl C caught again, exiting immediately!')
1428
- # wait for 0.1 seconds to allow the threads to exit
1429
- time.sleep(0.1)
1430
- os.system(f'pkill -ef {os.path.basename(__file__)}')
1431
- sys.exit(0)
1432
-
1792
+ # ------------ Run / Process Hosts Block ----------------
1433
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):
1434
1794
  global __globalUnavailableHosts
1435
1795
  global _no_env
1436
1796
  threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
1437
- 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:
1797
+ if __curses_available and 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:
1438
1798
  curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
1439
1799
  if not returnUnfinished:
1440
1800
  # wait until all hosts have a return code
@@ -1445,7 +1805,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1445
1805
  # update the unavailable hosts and global unavailable hosts
1446
1806
  if willUpdateUnreachableHosts:
1447
1807
  unavailableHosts = set(unavailableHosts)
1448
- 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!'))])
1808
+ unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or (host.stderr[-1].strip().startswith('Timeout!') and host.returncode == 124))])
1449
1809
  # reachable hosts = all hosts - unreachable hosts
1450
1810
  reachableHosts = set([host.name for host in hosts]) - unavailableHosts
1451
1811
  if __DEBUG_MODE:
@@ -1482,12 +1842,13 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1482
1842
  os.replace(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv.new'),os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'))
1483
1843
 
1484
1844
  except Exception as e:
1485
- eprint(f'Error writing to temporary file: {e}')
1845
+ eprint(f'Error writing to temporary file: {e!r}')
1486
1846
 
1487
1847
  # print the output, if the output of multiple hosts are the same, we aggragate them
1488
1848
  if not called:
1489
1849
  print_output(hosts,json,greppable=greppable)
1490
1850
 
1851
+ # ------------ Stringfy Block ----------------
1491
1852
  @cache_decorator
1492
1853
  def formHostStr(host) -> str:
1493
1854
  """
@@ -1511,7 +1872,6 @@ def formHostStr(host) -> str:
1511
1872
  host = ','.join(host)
1512
1873
  return host
1513
1874
 
1514
-
1515
1875
  @cache_decorator
1516
1876
  def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1517
1877
  nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
@@ -1519,12 +1879,14 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
1519
1879
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1520
1880
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
1521
1881
  file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
1882
+ copy_id = False,
1522
1883
  shortend = False) -> str:
1523
1884
  argsList = []
1524
1885
  if oneonone: argsList.append('--oneonone' if not shortend else '-11')
1525
1886
  if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
1526
1887
  if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
1527
1888
  if identity_file and identity_file != DEFAULT_IDENTITY_FILE: argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
1889
+ if copy_id: argsList.append('--copy_id' if not shortend else '-ci')
1528
1890
  if nowatch: argsList.append('--nowatch' if not shortend else '-q')
1529
1891
  if json: argsList.append('--json' if not shortend else '-j')
1530
1892
  if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
@@ -1550,6 +1912,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1550
1912
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1551
1913
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1552
1914
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
1915
+ copy_id = False,
1553
1916
  shortend = False):
1554
1917
  hosts = hosts if type(hosts) == str else frozenset(hosts)
1555
1918
  hostStr = formHostStr(hosts)
@@ -1559,17 +1922,20 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1559
1922
  files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
1560
1923
  username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
1561
1924
  greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, identity_file = identity_file,
1925
+ copy_id = copy_id,
1562
1926
  shortend = shortend)
1563
1927
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
1564
1928
  return f'multissh {argsStr} {hostStr} {commandStr}'
1565
1929
 
1930
+ # ------------ Main Block ----------------
1566
1931
  def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1567
1932
  nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1568
1933
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
1569
1934
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1570
1935
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1571
1936
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1572
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE):
1937
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE,
1938
+ copy_id = False):
1573
1939
  f'''
1574
1940
  Run the command on the hosts, aka multissh. main function
1575
1941
 
@@ -1604,6 +1970,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1604
1970
  error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
1605
1971
  quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
1606
1972
  identity_file (str, optional): The identity file to use for the ssh connection. Defaults to {DEFAULT_IDENTITY_FILE}.
1973
+ copy_id (bool, optional): Whether to copy the id to the hosts. Defaults to False.
1607
1974
 
1608
1975
  Returns:
1609
1976
  list: A list of Host objects
@@ -1625,17 +1992,20 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1625
1992
  elif checkTime > 3600:
1626
1993
  checkTime = 3600
1627
1994
  try:
1995
+ readed = False
1628
1996
  if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')) < checkTime:
1629
- if not __global_suppress_printout:
1630
- eprint(f"Reading unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
1997
+
1631
1998
  with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv'),'r') as f:
1632
1999
  for line in f:
1633
2000
  line = line.strip()
1634
2001
  if line and ',' in line and len(line.split(',')) >= 2 and line.split(',')[0] and line.split(',')[1].isdigit():
1635
2002
  if int(line.split(',')[1]) > time.time() - checkTime:
1636
2003
  __globalUnavailableHosts.add(line.split(',')[0])
2004
+ readed = True
2005
+ if readed and not __global_suppress_printout:
2006
+ eprint(f"Read unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
1637
2007
  except Exception as e:
1638
- eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')}")
2008
+ eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS.csv')!r}")
1639
2009
  eprint(str(e))
1640
2010
  elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
1641
2011
  __globalUnavailableHosts.update(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
@@ -1654,13 +2024,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1654
2024
  commands = [' '.join(command) if not type(command) == str else command for command in commands]
1655
2025
  except:
1656
2026
  pass
1657
- eprint(f"Warning: commands should ideally be a list of strings. Now mssh had failed to convert {commands} to a list of strings. Continuing anyway but expect failures.")
2027
+ eprint(f"Warning: commands should ideally be a list of strings. Now mssh had failed to convert {commands!r} to a list of strings. Continuing anyway but expect failures.")
1658
2028
  #verify_ssh_config()
1659
2029
  # load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
1660
2030
  if called:
1661
2031
  # if called,
1662
2032
  # if skipUnreachable is not set, we default to skip unreachable hosts within one command call
1663
- __global_suppress_printout = True
1664
2033
  if skipUnreachable is None:
1665
2034
  skipUnreachable = True
1666
2035
  if skipUnreachable:
@@ -1694,12 +2063,37 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1694
2063
  if '@' not in host:
1695
2064
  skipHostStr[i] = userStr + host
1696
2065
  skipHostStr = ','.join(skipHostStr)
1697
- targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
2066
+ targetHostDic = expand_hostnames(frozenset(hostStr.split(',')))
1698
2067
  if __DEBUG_MODE:
1699
- eprint(f"Target hosts: {targetHostsList}")
1700
- skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
1701
- if skipHostsList:
1702
- eprint(f"Skipping hosts: {skipHostsList}")
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())
2073
+ if copy_id:
2074
+ if 'ssh-copy-id' in _binPaths:
2075
+ # we will copy the id to the hosts
2076
+ hosts = []
2077
+ for host in targetHostDic:
2078
+ if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
2079
+ command = f"{_binPaths['ssh-copy-id']} "
2080
+ if identity_file:
2081
+ command = f"{command}-i {identity_file} "
2082
+ if username:
2083
+ command = f"{command} {username}@"
2084
+ command = f"{command}{host}"
2085
+ if password and 'sshpass' in _binPaths:
2086
+ command = f"{_binPaths['sshpass']} -p {password} {command}"
2087
+ hosts.append(Host(host, command,identity_file=identity_file,bash=True,ip = targetHostDic[host]))
2088
+ else:
2089
+ eprint(f"> {command}")
2090
+ os.system(command)
2091
+ if hosts:
2092
+ 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)
2093
+ else:
2094
+ eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
2095
+ if not commands:
2096
+ sys.exit(0)
1703
2097
  if files and not commands:
1704
2098
  # if files are specified but not target dir, we default to file sync mode
1705
2099
  file_sync = True
@@ -1716,7 +2110,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1716
2110
  except:
1717
2111
  pathSet.update(glob.glob(file,recursive=True))
1718
2112
  if not pathSet:
1719
- eprint(f'Warning: No source files at {files} are found after resolving globs!')
2113
+ eprint(f'Warning: No source files at {files!r} are found after resolving globs!')
1720
2114
  sys.exit(66)
1721
2115
  else:
1722
2116
  pathSet = set(files)
@@ -1727,29 +2121,29 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1727
2121
  else:
1728
2122
  files = list(pathSet)
1729
2123
  if __DEBUG_MODE:
1730
- eprint(f"Files: {files}")
2124
+ eprint(f"Files: {files!r}")
1731
2125
  if oneonone:
1732
2126
  hosts = []
1733
- if len(commands) != len(targetHostsList) - len(skipHostsList):
2127
+ if len(commands) != len(set(targetHostDic) - set(skipHostSet)):
1734
2128
  eprint("Error: the number of commands must be the same as the number of hosts")
1735
2129
  eprint(f"Number of commands: {len(commands)}")
1736
- eprint(f"Number of hosts: {len(targetHostsList - skipHostsList)}")
2130
+ eprint(f"Number of hosts: {len(set(targetHostDic) - set(skipHostSet))}")
1737
2131
  sys.exit(255)
1738
2132
  if not __global_suppress_printout:
1739
2133
  eprint('-'*80)
1740
2134
  eprint("Running in one on one mode")
1741
- for host, command in zip(targetHostsList, commands):
1742
- 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:
1743
2137
  eprint(f"Skipping unavailable host: {host}")
1744
2138
  continue
1745
- if host.strip() in skipHostsList: continue
2139
+ if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
1746
2140
  if file_sync:
1747
- 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]))
1748
2142
  else:
1749
- 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]))
1750
2144
  if not __global_suppress_printout:
1751
- eprint(f"Running command: {command} on host: {host}")
1752
- if not __global_suppress_printout: print('-'*80)
2145
+ eprint(f"Running command: {command!r} on host: {host!r}")
2146
+ if not __global_suppress_printout: eprint('-'*80)
1753
2147
  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)
1754
2148
  return hosts
1755
2149
  else:
@@ -1757,20 +2151,19 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1757
2151
  if not commands:
1758
2152
  # run in interactive mode ssh mode
1759
2153
  hosts = []
1760
- for host in targetHostsList:
1761
- 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:
1762
2156
  if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
1763
2157
  continue
1764
- if host.strip() in skipHostsList: continue
2158
+ if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
2159
+ # TODO: use ip to determine if we skip the host or not, also for unavailable hosts
1765
2160
  if file_sync:
1766
2161
  eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
1767
2162
  return []
1768
2163
  elif files:
1769
2164
  eprint(f"Error: files need to be specified with at least one path to sync")
1770
- elif ipmi:
1771
- eprint(f"Error: ipmi mode is not supported in interactive mode")
1772
2165
  else:
1773
- 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]))
1774
2167
  if not __global_suppress_printout:
1775
2168
  eprint('-'*80)
1776
2169
  eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -1782,15 +2175,15 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1782
2175
  return hosts
1783
2176
  for command in commands:
1784
2177
  hosts = []
1785
- for host in targetHostsList:
1786
- 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:
1787
2180
  if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
1788
2181
  continue
1789
- if host.strip() in skipHostsList: continue
2182
+ if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
1790
2183
  if file_sync:
1791
- 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]))
1792
2185
  else:
1793
- 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]))
1794
2187
  if not __global_suppress_printout and len(commands) > 1:
1795
2188
  eprint('-'*80)
1796
2189
  eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -1799,7 +2192,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1799
2192
  allHosts += hosts
1800
2193
  return allHosts
1801
2194
 
1802
- def get_default_config(args):
2195
+ # ------------ Default Config Functions ----------------
2196
+ def generate_default_config(args):
1803
2197
  '''
1804
2198
  Get the default config
1805
2199
 
@@ -1850,33 +2244,13 @@ def get_default_config(args):
1850
2244
  def write_default_config(args,CONFIG_FILE,backup = True):
1851
2245
  if backup and os.path.exists(CONFIG_FILE):
1852
2246
  os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
1853
- default_config = get_default_config(args)
2247
+ default_config = generate_default_config(args)
1854
2248
  # apply the updated defualt_config to __configs_from_file and write that to file
1855
2249
  __configs_from_file.update(default_config)
1856
2250
  with open(CONFIG_FILE,'w') as f:
1857
2251
  json.dump(__configs_from_file,f,indent=4)
1858
2252
 
1859
- def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
1860
- '''
1861
- Find the ssh public key file
1862
-
1863
- Args:
1864
- searchPath (str, optional): The path to search. Defaults to DEDAULT_SSH_KEY_SEARCH_PATH.
1865
-
1866
- Returns:
1867
- str: The path to the ssh key file
1868
- '''
1869
- if searchPath:
1870
- sshKeyPath = searchPath
1871
- else:
1872
- sshKeyPath ='~/.ssh'
1873
- possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
1874
- for sshKeyFile in possibleSshKeyFiles:
1875
- if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
1876
- return os.path.join(sshKeyPath,sshKeyFile)
1877
- return None
1878
-
1879
-
2253
+ # ------------ Wrapper Block ----------------
1880
2254
  def main():
1881
2255
  global _emo
1882
2256
  global __global_suppress_printout
@@ -1921,12 +2295,15 @@ def main():
1921
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)
1922
2296
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
1923
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)
1924
- parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
1925
- parser.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)
2298
+ parser.add_argument("-g","--greppable",'--table', action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
2299
+ group = parser.add_mutually_exclusive_group()
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)
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)
2302
+
1926
2303
  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)
1927
2304
  parser.add_argument('--store_config_file', action='store_true', help=f'Store / generate the default config file from command line argument and current config at {CONFIG_FILE}')
1928
2305
  parser.add_argument('--debug', action='store_true', help='Print debug information')
1929
- parser.add_argument('--copy-id', action='store_true', help='Copy the ssh id to the hosts')
2306
+ parser.add_argument('-ci','--copy_id', action='store_true', help='Copy the ssh id to the hosts')
1930
2307
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
1931
2308
 
1932
2309
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
@@ -1936,12 +2313,13 @@ def main():
1936
2313
  # if python version is 3.7 or higher, use parse_intermixed_args
1937
2314
  try:
1938
2315
  args = parser.parse_intermixed_args()
1939
- except:
2316
+ except Exception as e:
2317
+ #eprint(f"Error while parsing arguments: {e!r}")
1940
2318
  # try to parse the arguments using parse_known_args
1941
2319
  args, unknown = parser.parse_known_args()
1942
2320
  # if there are unknown arguments, we will try to parse them again using parse_args
1943
2321
  if unknown:
1944
- eprint(f"Warning: Unknown arguments, treating all as commands: {unknown}")
2322
+ eprint(f"Warning: Unknown arguments, treating all as commands: {unknown!r}")
1945
2323
  args.commands += unknown
1946
2324
 
1947
2325
 
@@ -1949,22 +2327,22 @@ def main():
1949
2327
  if args.store_config_file:
1950
2328
  try:
1951
2329
  if os.path.exists(CONFIG_FILE):
1952
- eprint(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
2330
+ eprint(f"Warning: {CONFIG_FILE!r} already exists, what to do? (o/b/n)")
1953
2331
  eprint(f"o: Overwrite the file")
1954
- eprint(f"b: Rename the current config file at {CONFIG_FILE}.bak forcefully and write the new config file (default)")
2332
+ eprint(f"b: Rename the current config file at {CONFIG_FILE!r}.bak forcefully and write the new config file (default)")
1955
2333
  eprint(f"n: Do nothing")
1956
2334
  inStr = input_with_timeout_and_countdown(10)
1957
2335
  if (not inStr) or inStr.lower().strip().startswith('b'):
1958
2336
  write_default_config(args,CONFIG_FILE,backup = True)
1959
- eprint(f"Config file written to {CONFIG_FILE}")
2337
+ eprint(f"Config file written to {CONFIG_FILE!r}")
1960
2338
  elif inStr.lower().strip().startswith('o'):
1961
2339
  write_default_config(args,CONFIG_FILE,backup = False)
1962
- eprint(f"Config file written to {CONFIG_FILE}")
2340
+ eprint(f"Config file written to {CONFIG_FILE!r}")
1963
2341
  else:
1964
2342
  write_default_config(args,CONFIG_FILE,backup = True)
1965
- eprint(f"Config file written to {CONFIG_FILE}")
2343
+ eprint(f"Config file written to {CONFIG_FILE!r}")
1966
2344
  except Exception as e:
1967
- eprint(f"Error while writing config file: {e}")
2345
+ eprint(f"Error while writing config file: {e!r}")
1968
2346
  import traceback
1969
2347
  eprint(traceback.format_exc())
1970
2348
  if not args.commands:
@@ -1984,9 +2362,9 @@ def main():
1984
2362
  inStr = input_with_timeout_and_countdown(3)
1985
2363
  if (not inStr) or inStr.lower().strip().startswith('1'):
1986
2364
  args.commands = [" ".join(args.commands)]
1987
- eprint(f"\nRunning 1 command: {args.commands[0]} on all hosts")
2365
+ eprint(f"\nRunning 1 command: {args.commands[0]!r} on all hosts")
1988
2366
  elif inStr.lower().strip().startswith('m'):
1989
- eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
2367
+ eprint(f"\nRunning multiple commands: {', '.join(args.commands)!r} on all hosts")
1990
2368
  else:
1991
2369
  sys.exit(0)
1992
2370
 
@@ -1997,26 +2375,7 @@ def main():
1997
2375
  if os.path.isdir(os.path.expanduser(args.key)):
1998
2376
  args.key = find_ssh_key_file(args.key)
1999
2377
  elif not os.path.exists(args.key):
2000
- eprint(f"Warning: Identity file {args.key} not found. Passing to ssh anyway. Proceed with caution.")
2001
-
2002
- if args.copy_id:
2003
- if 'ssh-copy-id' in _binPaths:
2004
- # we will copy the id to the hosts
2005
- for host in formHostStr(args.hosts).split(','):
2006
- command = f"{_binPaths['ssh-copy-id']} "
2007
- if args.key:
2008
- command = f"{command}-i {args.key} "
2009
- if args.username:
2010
- command = f"{command} {args.username}@"
2011
- command = f"{command}{host}"
2012
- if args.password and 'sshpass' in _binPaths:
2013
- command = f"{_binPaths['sshpass']} -p {args.password} {command}"
2014
- eprint(f"> {command}")
2015
- os.system(command)
2016
- else:
2017
- eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
2018
- if not args.commands:
2019
- sys.exit(0)
2378
+ eprint(f"Warning: Identity file {args.key!r} not found. Passing to ssh anyway. Proceed with caution.")
2020
2379
 
2021
2380
  __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
2022
2381
 
@@ -2028,7 +2387,8 @@ def main():
2028
2387
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
2029
2388
  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,
2030
2389
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
2031
- curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key)
2390
+ curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key,
2391
+ copy_id=args.copy_id)
2032
2392
  eprint('> ' + cmdStr)
2033
2393
  if args.error_only:
2034
2394
  __global_suppress_printout = True
@@ -2044,10 +2404,11 @@ def main():
2044
2404
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
2045
2405
  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,
2046
2406
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
2047
- curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key)
2407
+ curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key,
2408
+ copy_id=args.copy_id)
2048
2409
  #print('*'*80)
2049
2410
 
2050
- if not __global_suppress_printout: eprint('-'*80)
2411
+ #if not __global_suppress_printout: eprint('-'*80)
2051
2412
 
2052
2413
  succeededHosts = set()
2053
2414
  for host in hosts: