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