multiSSH3 5.10__py3-none-any.whl → 5.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of multiSSH3 might be problematic. Click here for more details.
- {multiSSH3-5.10.dist-info → multiSSH3-5.24.dist-info}/METADATA +1 -1
- multiSSH3-5.24.dist-info/RECORD +7 -0
- {multiSSH3-5.10.dist-info → multiSSH3-5.24.dist-info}/WHEEL +1 -1
- multiSSH3.py +600 -639
- multiSSH3-5.10.dist-info/RECORD +0 -7
- {multiSSH3-5.10.dist-info → multiSSH3-5.24.dist-info}/LICENSE +0 -0
- {multiSSH3-5.10.dist-info → multiSSH3-5.24.dist-info}/entry_points.txt +0 -0
- {multiSSH3-5.10.dist-info → multiSSH3-5.24.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.24'
|
|
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:
|
|
@@ -870,84 +960,32 @@ def expand_hostnames(hosts):
|
|
|
870
960
|
# we seperate the username from the hostname
|
|
871
961
|
username = None
|
|
872
962
|
if '@' in host:
|
|
873
|
-
username, host = host.split('@',1)
|
|
963
|
+
username, host = host.split('@',1).strip()
|
|
874
964
|
# first we check if the hostname is an range of IP addresses
|
|
875
965
|
# This is done by checking if the hostname follows four fields of
|
|
876
966
|
# "(((\d{1,3}|x|\*|\?)(-(\d{1,3}|x|\*|\?))?)|(\[(\d{1,3}|x|\*|\?)(-(\d{1,3}|x|\*|\?))?\]))"
|
|
877
967
|
# seperated by .
|
|
878
968
|
# If so, we expand the IP address range
|
|
969
|
+
iplist = []
|
|
879
970
|
if re.match(r'^((((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])}|x|\*|\?))?\]))(\.((((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?)(-((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?))?)|(\[((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?)(-((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?))?\]))){2}(\.(((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])}|x|\*|\?))?\]))$', host):
|
|
880
|
-
hostSetToAdd = sorted(
|
|
971
|
+
hostSetToAdd = sorted(__expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
|
|
972
|
+
iplist = hostSetToAdd
|
|
881
973
|
else:
|
|
882
|
-
hostSetToAdd = sorted(
|
|
974
|
+
hostSetToAdd = sorted(__expand_hostname(host))
|
|
975
|
+
for host in hostSetToAdd:
|
|
976
|
+
iplist.append(getIP(host,local=False))
|
|
883
977
|
if username:
|
|
884
978
|
# we expand the username
|
|
885
|
-
username = sorted(
|
|
979
|
+
username = sorted(__expand_hostname(username,validate=False))
|
|
886
980
|
# we combine the username and hostname
|
|
887
|
-
|
|
888
|
-
|
|
981
|
+
for user in username:
|
|
982
|
+
[expandedhosts.update({f'{user}@{host}':ip}) for host,ip in zip(hostSetToAdd,iplist)]
|
|
983
|
+
else:
|
|
984
|
+
[expandedhosts.update({host:ip}) for host,ip in zip(hostSetToAdd,iplist)]
|
|
889
985
|
return expandedhosts
|
|
890
986
|
|
|
891
|
-
|
|
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):
|
|
987
|
+
# ------------ Run Command Block ----------------
|
|
988
|
+
def __handle_reading_stream(stream,target, host):
|
|
951
989
|
'''
|
|
952
990
|
Read the stream and append the lines to the target list
|
|
953
991
|
|
|
@@ -987,7 +1025,7 @@ def handle_reading_stream(stream,target, host):
|
|
|
987
1025
|
if current_line:
|
|
988
1026
|
add_line(current_line,target, host, keepLastLine=lastLineCommited)
|
|
989
1027
|
|
|
990
|
-
def
|
|
1028
|
+
def __handle_writing_stream(stream,stop_event,host):
|
|
991
1029
|
'''
|
|
992
1030
|
Write the key presses to the stream
|
|
993
1031
|
|
|
@@ -1025,34 +1063,6 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
1025
1063
|
# host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
|
|
1026
1064
|
return sentInput
|
|
1027
1065
|
|
|
1028
|
-
def replace_magic_strings(string,keys,value,case_sensitive=False):
|
|
1029
|
-
'''
|
|
1030
|
-
Replace the magic strings in the host object
|
|
1031
|
-
|
|
1032
|
-
Args:
|
|
1033
|
-
string (str): The string to replace the magic strings
|
|
1034
|
-
keys (list): Search for keys to replace
|
|
1035
|
-
value (str): The value to replace the key
|
|
1036
|
-
case_sensitive (bool, optional): Whether to search for the keys in a case sensitive way. Defaults to False.
|
|
1037
|
-
|
|
1038
|
-
Returns:
|
|
1039
|
-
str: The string with the magic strings replaced
|
|
1040
|
-
'''
|
|
1041
|
-
# verify magic strings have # at the beginning and end
|
|
1042
|
-
newKeys = []
|
|
1043
|
-
for key in keys:
|
|
1044
|
-
if key.startswith('#') and key.endswith('#'):
|
|
1045
|
-
newKeys.append(key)
|
|
1046
|
-
else:
|
|
1047
|
-
newKeys.append('#'+key.strip('#')+'#')
|
|
1048
|
-
# replace the magic strings
|
|
1049
|
-
for key in newKeys:
|
|
1050
|
-
if case_sensitive:
|
|
1051
|
-
string = string.replace(key,value)
|
|
1052
|
-
else:
|
|
1053
|
-
string = re.sub(re.escape(key),value,string,flags=re.IGNORECASE)
|
|
1054
|
-
return string
|
|
1055
|
-
|
|
1056
1066
|
def run_command(host, sem, timeout=60,passwds=None):
|
|
1057
1067
|
'''
|
|
1058
1068
|
Run the command on the host. Will format the commands accordingly. Main execution function.
|
|
@@ -1104,7 +1114,7 @@ def run_command(host, sem, timeout=60,passwds=None):
|
|
|
1104
1114
|
host.interface_ip_prefix = __ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
|
|
1105
1115
|
if host.interface_ip_prefix:
|
|
1106
1116
|
try:
|
|
1107
|
-
hostOctets =
|
|
1117
|
+
hostOctets = host.ip.split('.')
|
|
1108
1118
|
prefixOctets = host.interface_ip_prefix.split('.')
|
|
1109
1119
|
host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
|
|
1110
1120
|
host.resolvedName = host.username + '@' if host.username else ''
|
|
@@ -1232,15 +1242,15 @@ def run_command(host, sem, timeout=60,passwds=None):
|
|
|
1232
1242
|
#host.stdout = []
|
|
1233
1243
|
proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
|
|
1234
1244
|
# create a thread to handle stdout
|
|
1235
|
-
stdout_thread = threading.Thread(target=
|
|
1245
|
+
stdout_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stdout,host.stdout, host), daemon=True)
|
|
1236
1246
|
stdout_thread.start()
|
|
1237
1247
|
# create a thread to handle stderr
|
|
1238
1248
|
#host.stderr = []
|
|
1239
|
-
stderr_thread = threading.Thread(target=
|
|
1249
|
+
stderr_thread = threading.Thread(target=__handle_reading_stream, args=(proc.stderr,host.stderr, host), daemon=True)
|
|
1240
1250
|
stderr_thread.start()
|
|
1241
1251
|
# create a thread to handle stdin
|
|
1242
1252
|
stdin_stop_event = threading.Event()
|
|
1243
|
-
stdin_thread = threading.Thread(target=
|
|
1253
|
+
stdin_thread = threading.Thread(target=__handle_writing_stream, args=(proc.stdin,stdin_stop_event, host), daemon=True)
|
|
1244
1254
|
stdin_thread.start()
|
|
1245
1255
|
# Monitor the subprocess and terminate it after the timeout
|
|
1246
1256
|
host.lastUpdateTime = time.time()
|
|
@@ -1291,9 +1301,9 @@ def run_command(host, sem, timeout=60,passwds=None):
|
|
|
1291
1301
|
except subprocess.TimeoutExpired:
|
|
1292
1302
|
pass
|
|
1293
1303
|
if stdout:
|
|
1294
|
-
|
|
1304
|
+
__handle_reading_stream(io.BytesIO(stdout),host.stdout, host)
|
|
1295
1305
|
if stderr:
|
|
1296
|
-
|
|
1306
|
+
__handle_reading_stream(io.BytesIO(stderr),host.stderr, host)
|
|
1297
1307
|
# if the last line in host.stderr is Connection to * closed., we will remove it
|
|
1298
1308
|
host.returncode = proc.poll()
|
|
1299
1309
|
if not host.returncode:
|
|
@@ -1333,6 +1343,7 @@ def run_command(host, sem, timeout=60,passwds=None):
|
|
|
1333
1343
|
host.scp = True
|
|
1334
1344
|
run_command(host,sem,timeout,passwds)
|
|
1335
1345
|
|
|
1346
|
+
# ------------ Start Threading Block ----------------
|
|
1336
1347
|
def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cpu_count()):
|
|
1337
1348
|
'''
|
|
1338
1349
|
Start running the command on the hosts. Wrapper function for run_command
|
|
@@ -1354,7 +1365,8 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
|
|
|
1354
1365
|
thread.start()
|
|
1355
1366
|
return threads
|
|
1356
1367
|
|
|
1357
|
-
|
|
1368
|
+
# ------------ Display Block ----------------
|
|
1369
|
+
def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
1358
1370
|
'''
|
|
1359
1371
|
Generate a list for the hosts to be displayed on the screen. This is used to display as much relevant information as possible.
|
|
1360
1372
|
|
|
@@ -1386,7 +1398,7 @@ def get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
|
1386
1398
|
host.printedLines = 0
|
|
1387
1399
|
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
1388
1400
|
|
|
1389
|
-
def
|
|
1401
|
+
def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window=DEFAULT_SINGLE_WINDOW, config_reason= 'New Configuration'):
|
|
1390
1402
|
try:
|
|
1391
1403
|
org_dim = stdscr.getmaxyx()
|
|
1392
1404
|
new_configured = True
|
|
@@ -1413,7 +1425,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
|
|
|
1413
1425
|
max_num_hosts = max_num_hosts_x * max_num_hosts_y
|
|
1414
1426
|
if max_num_hosts < 1:
|
|
1415
1427
|
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal too small to display any hosts')
|
|
1416
|
-
hosts_to_display , host_stats =
|
|
1428
|
+
hosts_to_display , host_stats = _get_hosts_to_display(hosts, max_num_hosts)
|
|
1417
1429
|
if len(hosts_to_display) == 0:
|
|
1418
1430
|
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'No hosts to display')
|
|
1419
1431
|
# Now we calculate the actual number of hosts we will display for x and y
|
|
@@ -1592,7 +1604,7 @@ def generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_c
|
|
|
1592
1604
|
if time.perf_counter() - last_refresh_time < 0.01:
|
|
1593
1605
|
time.sleep(max(0,0.01 - time.perf_counter() + last_refresh_time))
|
|
1594
1606
|
#stdscr.clear()
|
|
1595
|
-
hosts_to_display, host_stats =
|
|
1607
|
+
hosts_to_display, host_stats = _get_hosts_to_display(hosts, max_num_hosts,hosts_to_display)
|
|
1596
1608
|
for host_window, host in zip(host_windows, hosts_to_display):
|
|
1597
1609
|
# we will only update the window if there is new output or the window is not fully printed
|
|
1598
1610
|
if new_configured or host.printedLines < len(host.output):
|
|
@@ -1669,7 +1681,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
1669
1681
|
|
|
1670
1682
|
params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
|
|
1671
1683
|
while params:
|
|
1672
|
-
params =
|
|
1684
|
+
params = __generate_display(stdscr, hosts, *params)
|
|
1673
1685
|
if not params:
|
|
1674
1686
|
break
|
|
1675
1687
|
if not any([host.returncode is None for host in hosts]):
|
|
@@ -1692,7 +1704,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
1692
1704
|
time.sleep(0.01)
|
|
1693
1705
|
#time.sleep(0.25)
|
|
1694
1706
|
|
|
1695
|
-
|
|
1707
|
+
# ------------ Generate Output Block ----------------
|
|
1696
1708
|
def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
1697
1709
|
'''
|
|
1698
1710
|
Print / generate the output of the hosts to the terminal
|
|
@@ -1713,29 +1725,18 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1713
1725
|
#print(json.dumps([dict(host) for host in hosts],indent=4))
|
|
1714
1726
|
rtnStr = json.dumps(hosts,indent=4)
|
|
1715
1727
|
elif greppable:
|
|
1716
|
-
|
|
1717
|
-
# transform hosts to dictionaries
|
|
1718
|
-
for host in hosts:
|
|
1719
|
-
hostPrintOut = f" | cmd: {host['command']} | stdout: "+'↵ '.join(host['stdout'])
|
|
1720
|
-
if host['stderr']:
|
|
1721
|
-
if host['stderr'][0].strip().startswith('ssh: connect to host '):
|
|
1722
|
-
host['stderr'][0] = 'SSH not reachable!'
|
|
1723
|
-
elif host['stderr'][-1].strip().endswith('Connection timed out'):
|
|
1724
|
-
host['stderr'][-1] = 'SSH connection timed out!'
|
|
1725
|
-
elif host['stderr'][-1].strip().endswith('No route to host'):
|
|
1726
|
-
host['stderr'][-1] = 'Cannot find host!'
|
|
1727
|
-
hostPrintOut += " | stderr: "+'↵ '.join(host['stderr'])
|
|
1728
|
-
hostPrintOut += f" | return_code: {host['returncode']}"
|
|
1729
|
-
outputs.setdefault(hostPrintOut, set()).add(host['name'])
|
|
1728
|
+
# transform hosts to a 2d list
|
|
1730
1729
|
rtnStr = '*'*80+'\n'
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1730
|
+
rtnList = [['host_name','return_code','output_type','output']]
|
|
1731
|
+
for host in hosts:
|
|
1732
|
+
#header = f"{host['name']} | rc: {host['returncode']} | "
|
|
1733
|
+
for line in host['stdout']:
|
|
1734
|
+
rtnList.append([host['name'],f"rc: {host['returncode']}",'stdout',line])
|
|
1735
|
+
for line in host['stderr']:
|
|
1736
|
+
rtnList.append([host['name'],f"rc: {host['returncode']}",'stderr',line])
|
|
1737
|
+
rtnList.append(['','','',''])
|
|
1738
|
+
rtnStr += pretty_format_table(rtnList)
|
|
1739
|
+
rtnStr += '*'*80+'\n'
|
|
1739
1740
|
if __keyPressesIn[-1]:
|
|
1740
1741
|
CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
|
|
1741
1742
|
rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
|
|
@@ -1760,8 +1761,9 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1760
1761
|
outputs.setdefault(hostPrintOut, set()).add(host['name'])
|
|
1761
1762
|
rtnStr = ''
|
|
1762
1763
|
for output, hostSet in outputs.items():
|
|
1764
|
+
hostSet = frozenset(hostSet)
|
|
1763
1765
|
compact_hosts = compact_hostnames(hostSet)
|
|
1764
|
-
if expand_hostnames(
|
|
1766
|
+
if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
|
|
1765
1767
|
eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
|
|
1766
1768
|
compact_hosts = hostSet
|
|
1767
1769
|
if __global_suppress_printout:
|
|
@@ -1769,7 +1771,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1769
1771
|
rtnStr += output+'\n'
|
|
1770
1772
|
else:
|
|
1771
1773
|
rtnStr += '*'*80+'\n'
|
|
1772
|
-
rtnStr += f'These hosts: {",".join(compact_hosts)} have a response of:\n'
|
|
1774
|
+
rtnStr += f'These hosts: "{",".join(sorted(compact_hosts))}" have a response of:\n'
|
|
1773
1775
|
rtnStr += output+'\n'
|
|
1774
1776
|
if not __global_suppress_printout or outputs:
|
|
1775
1777
|
rtnStr += '*'*80+'\n'
|
|
@@ -1787,28 +1789,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1787
1789
|
print(rtnStr)
|
|
1788
1790
|
return rtnStr
|
|
1789
1791
|
|
|
1790
|
-
|
|
1791
|
-
'''
|
|
1792
|
-
Handle the Ctrl C signal
|
|
1793
|
-
|
|
1794
|
-
Args:
|
|
1795
|
-
sig (int): The signal
|
|
1796
|
-
frame (frame): The frame
|
|
1797
|
-
|
|
1798
|
-
Returns:
|
|
1799
|
-
None
|
|
1800
|
-
'''
|
|
1801
|
-
global _emo
|
|
1802
|
-
if not _emo:
|
|
1803
|
-
eprint('Ctrl C caught, exiting...')
|
|
1804
|
-
_emo = True
|
|
1805
|
-
else:
|
|
1806
|
-
eprint('Ctrl C caught again, exiting immediately!')
|
|
1807
|
-
# wait for 0.1 seconds to allow the threads to exit
|
|
1808
|
-
time.sleep(0.1)
|
|
1809
|
-
os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
1810
|
-
sys.exit(0)
|
|
1811
|
-
|
|
1792
|
+
# ------------ Run / Process Hosts Block ----------------
|
|
1812
1793
|
def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
|
|
1813
1794
|
global __globalUnavailableHosts
|
|
1814
1795
|
global _no_env
|
|
@@ -1867,6 +1848,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
1867
1848
|
if not called:
|
|
1868
1849
|
print_output(hosts,json,greppable=greppable)
|
|
1869
1850
|
|
|
1851
|
+
# ------------ Stringfy Block ----------------
|
|
1870
1852
|
@cache_decorator
|
|
1871
1853
|
def formHostStr(host) -> str:
|
|
1872
1854
|
"""
|
|
@@ -1890,7 +1872,6 @@ def formHostStr(host) -> str:
|
|
|
1890
1872
|
host = ','.join(host)
|
|
1891
1873
|
return host
|
|
1892
1874
|
|
|
1893
|
-
|
|
1894
1875
|
@cache_decorator
|
|
1895
1876
|
def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
1896
1877
|
nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
@@ -1946,6 +1927,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1946
1927
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
1947
1928
|
return f'multissh {argsStr} {hostStr} {commandStr}'
|
|
1948
1929
|
|
|
1930
|
+
# ------------ Main Block ----------------
|
|
1949
1931
|
def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
1950
1932
|
nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
1951
1933
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
|
|
@@ -2081,18 +2063,19 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2081
2063
|
if '@' not in host:
|
|
2082
2064
|
skipHostStr[i] = userStr + host
|
|
2083
2065
|
skipHostStr = ','.join(skipHostStr)
|
|
2084
|
-
|
|
2066
|
+
targetHostDic = expand_hostnames(frozenset(hostStr.split(',')))
|
|
2085
2067
|
if __DEBUG_MODE:
|
|
2086
|
-
eprint(f"Target hosts: {
|
|
2087
|
-
|
|
2088
|
-
if
|
|
2089
|
-
eprint(f"Skipping hosts: {
|
|
2068
|
+
eprint(f"Target hosts: {targetHostDic!r}")
|
|
2069
|
+
skipHostsDic = expand_hostnames(frozenset(skipHostStr.split(',')))
|
|
2070
|
+
if skipHostsDic:
|
|
2071
|
+
eprint(f"Skipping hosts: {skipHostsDic!r}")
|
|
2072
|
+
skipHostSet = set(skipHostsDic).union(skipHostsDic.values())
|
|
2090
2073
|
if copy_id:
|
|
2091
2074
|
if 'ssh-copy-id' in _binPaths:
|
|
2092
2075
|
# we will copy the id to the hosts
|
|
2093
2076
|
hosts = []
|
|
2094
|
-
for host in
|
|
2095
|
-
if host
|
|
2077
|
+
for host in targetHostDic:
|
|
2078
|
+
if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
|
|
2096
2079
|
command = f"{_binPaths['ssh-copy-id']} "
|
|
2097
2080
|
if identity_file:
|
|
2098
2081
|
command = f"{command}-i {identity_file} "
|
|
@@ -2101,7 +2084,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2101
2084
|
command = f"{command}{host}"
|
|
2102
2085
|
if password and 'sshpass' in _binPaths:
|
|
2103
2086
|
command = f"{_binPaths['sshpass']} -p {password} {command}"
|
|
2104
|
-
hosts.append(Host(host, command,identity_file=identity_file,bash=True))
|
|
2087
|
+
hosts.append(Host(host, command,identity_file=identity_file,bash=True,ip = targetHostDic[host]))
|
|
2105
2088
|
else:
|
|
2106
2089
|
eprint(f"> {command}")
|
|
2107
2090
|
os.system(command)
|
|
@@ -2141,23 +2124,23 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2141
2124
|
eprint(f"Files: {files!r}")
|
|
2142
2125
|
if oneonone:
|
|
2143
2126
|
hosts = []
|
|
2144
|
-
if len(commands) != len(set(
|
|
2127
|
+
if len(commands) != len(set(targetHostDic) - set(skipHostSet)):
|
|
2145
2128
|
eprint("Error: the number of commands must be the same as the number of hosts")
|
|
2146
2129
|
eprint(f"Number of commands: {len(commands)}")
|
|
2147
|
-
eprint(f"Number of hosts: {len(set(
|
|
2130
|
+
eprint(f"Number of hosts: {len(set(targetHostDic) - set(skipHostSet))}")
|
|
2148
2131
|
sys.exit(255)
|
|
2149
2132
|
if not __global_suppress_printout:
|
|
2150
2133
|
eprint('-'*80)
|
|
2151
2134
|
eprint("Running in one on one mode")
|
|
2152
|
-
for host, command in zip(
|
|
2153
|
-
if not ipmi and skipUnreachable and host
|
|
2135
|
+
for host, command in zip(targetHostDic, commands):
|
|
2136
|
+
if not ipmi and skipUnreachable and host in unavailableHosts:
|
|
2154
2137
|
eprint(f"Skipping unavailable host: {host}")
|
|
2155
2138
|
continue
|
|
2156
|
-
if host
|
|
2139
|
+
if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
|
|
2157
2140
|
if file_sync:
|
|
2158
|
-
hosts.append(Host(host
|
|
2141
|
+
hosts.append(Host(host, os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip = targetHostDic[host]))
|
|
2159
2142
|
else:
|
|
2160
|
-
hosts.append(Host(host
|
|
2143
|
+
hosts.append(Host(host, command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip=targetHostDic[host]))
|
|
2161
2144
|
if not __global_suppress_printout:
|
|
2162
2145
|
eprint(f"Running command: {command!r} on host: {host!r}")
|
|
2163
2146
|
if not __global_suppress_printout: eprint('-'*80)
|
|
@@ -2168,11 +2151,11 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2168
2151
|
if not commands:
|
|
2169
2152
|
# run in interactive mode ssh mode
|
|
2170
2153
|
hosts = []
|
|
2171
|
-
for host in
|
|
2172
|
-
if not ipmi and skipUnreachable and host
|
|
2154
|
+
for host in targetHostDic:
|
|
2155
|
+
if not ipmi and skipUnreachable and host in unavailableHosts:
|
|
2173
2156
|
if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
|
|
2174
2157
|
continue
|
|
2175
|
-
if host
|
|
2158
|
+
if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
|
|
2176
2159
|
# TODO: use ip to determine if we skip the host or not, also for unavailable hosts
|
|
2177
2160
|
if file_sync:
|
|
2178
2161
|
eprint(f"Error: file sync mode need to be specified with at least one path to sync.")
|
|
@@ -2180,7 +2163,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2180
2163
|
elif files:
|
|
2181
2164
|
eprint(f"Error: files need to be specified with at least one path to sync")
|
|
2182
2165
|
else:
|
|
2183
|
-
hosts.append(Host(host
|
|
2166
|
+
hosts.append(Host(host, '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,identity_file=identity_file,ip=targetHostDic[host]))
|
|
2184
2167
|
if not __global_suppress_printout:
|
|
2185
2168
|
eprint('-'*80)
|
|
2186
2169
|
eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
@@ -2192,15 +2175,15 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2192
2175
|
return hosts
|
|
2193
2176
|
for command in commands:
|
|
2194
2177
|
hosts = []
|
|
2195
|
-
for host in
|
|
2196
|
-
if not ipmi and skipUnreachable and host
|
|
2178
|
+
for host in targetHostDic:
|
|
2179
|
+
if not ipmi and skipUnreachable and host in unavailableHosts:
|
|
2197
2180
|
if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
|
|
2198
2181
|
continue
|
|
2199
|
-
if host
|
|
2182
|
+
if host in skipHostSet or targetHostDic[host] in skipHostSet: continue
|
|
2200
2183
|
if file_sync:
|
|
2201
|
-
hosts.append(Host(host
|
|
2184
|
+
hosts.append(Host(host, os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip=targetHostDic[host]))
|
|
2202
2185
|
else:
|
|
2203
|
-
hosts.append(Host(host
|
|
2186
|
+
hosts.append(Host(host, command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file,ip=targetHostDic[host]))
|
|
2204
2187
|
if not __global_suppress_printout and len(commands) > 1:
|
|
2205
2188
|
eprint('-'*80)
|
|
2206
2189
|
eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
@@ -2209,7 +2192,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2209
2192
|
allHosts += hosts
|
|
2210
2193
|
return allHosts
|
|
2211
2194
|
|
|
2212
|
-
|
|
2195
|
+
# ------------ Default Config Functions ----------------
|
|
2196
|
+
def generate_default_config(args):
|
|
2213
2197
|
'''
|
|
2214
2198
|
Get the default config
|
|
2215
2199
|
|
|
@@ -2260,33 +2244,13 @@ def get_default_config(args):
|
|
|
2260
2244
|
def write_default_config(args,CONFIG_FILE,backup = True):
|
|
2261
2245
|
if backup and os.path.exists(CONFIG_FILE):
|
|
2262
2246
|
os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
|
|
2263
|
-
default_config =
|
|
2247
|
+
default_config = generate_default_config(args)
|
|
2264
2248
|
# apply the updated defualt_config to __configs_from_file and write that to file
|
|
2265
2249
|
__configs_from_file.update(default_config)
|
|
2266
2250
|
with open(CONFIG_FILE,'w') as f:
|
|
2267
2251
|
json.dump(__configs_from_file,f,indent=4)
|
|
2268
2252
|
|
|
2269
|
-
|
|
2270
|
-
'''
|
|
2271
|
-
Find the ssh public key file
|
|
2272
|
-
|
|
2273
|
-
Args:
|
|
2274
|
-
searchPath (str, optional): The path to search. Defaults to DEDAULT_SSH_KEY_SEARCH_PATH.
|
|
2275
|
-
|
|
2276
|
-
Returns:
|
|
2277
|
-
str: The path to the ssh key file
|
|
2278
|
-
'''
|
|
2279
|
-
if searchPath:
|
|
2280
|
-
sshKeyPath = searchPath
|
|
2281
|
-
else:
|
|
2282
|
-
sshKeyPath ='~/.ssh'
|
|
2283
|
-
possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
|
|
2284
|
-
for sshKeyFile in possibleSshKeyFiles:
|
|
2285
|
-
if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
|
|
2286
|
-
return os.path.join(sshKeyPath,sshKeyFile)
|
|
2287
|
-
return None
|
|
2288
|
-
|
|
2289
|
-
|
|
2253
|
+
# ------------ Wrapper Block ----------------
|
|
2290
2254
|
def main():
|
|
2291
2255
|
global _emo
|
|
2292
2256
|
global __global_suppress_printout
|
|
@@ -2331,7 +2295,7 @@ def main():
|
|
|
2331
2295
|
parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
|
|
2332
2296
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
2333
2297
|
parser.add_argument("--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as wells. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
|
|
2334
|
-
parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
2298
|
+
parser.add_argument("-g","--greppable",'--table', action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
2335
2299
|
group = parser.add_mutually_exclusive_group()
|
|
2336
2300
|
group.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
|
|
2337
2301
|
group.add_argument("-nsu","--no_skip_unreachable",dest = 'skip_unreachable', action='store_false', help=f"Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {not DEFAULT_SKIP_UNREACHABLE})", default=not DEFAULT_SKIP_UNREACHABLE)
|
|
@@ -2350,7 +2314,7 @@ def main():
|
|
|
2350
2314
|
try:
|
|
2351
2315
|
args = parser.parse_intermixed_args()
|
|
2352
2316
|
except Exception as e:
|
|
2353
|
-
eprint(f"Error while parsing arguments: {e!r}")
|
|
2317
|
+
#eprint(f"Error while parsing arguments: {e!r}")
|
|
2354
2318
|
# try to parse the arguments using parse_known_args
|
|
2355
2319
|
args, unknown = parser.parse_known_args()
|
|
2356
2320
|
# if there are unknown arguments, we will try to parse them again using parse_args
|
|
@@ -2444,7 +2408,7 @@ def main():
|
|
|
2444
2408
|
copy_id=args.copy_id)
|
|
2445
2409
|
#print('*'*80)
|
|
2446
2410
|
|
|
2447
|
-
if not __global_suppress_printout: eprint('-'*80)
|
|
2411
|
+
#if not __global_suppress_printout: eprint('-'*80)
|
|
2448
2412
|
|
|
2449
2413
|
succeededHosts = set()
|
|
2450
2414
|
for host in hosts:
|
|
@@ -2477,6 +2441,3 @@ def main():
|
|
|
2477
2441
|
|
|
2478
2442
|
if __name__ == "__main__":
|
|
2479
2443
|
main()
|
|
2480
|
-
|
|
2481
|
-
# %%
|
|
2482
|
-
|