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