multiSSH3 4.83__py3-none-any.whl → 4.89__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-4.83.dist-info → multiSSH3-4.89.dist-info}/METADATA +22 -14
- multiSSH3-4.89.dist-info/RECORD +7 -0
- multiSSH3.py +138 -107
- multiSSH3-4.83.dist-info/RECORD +0 -7
- {multiSSH3-4.83.dist-info → multiSSH3-4.89.dist-info}/LICENSE +0 -0
- {multiSSH3-4.83.dist-info → multiSSH3-4.89.dist-info}/WHEEL +0 -0
- {multiSSH3-4.83.dist-info → multiSSH3-4.89.dist-info}/entry_points.txt +0 -0
- {multiSSH3-4.83.dist-info → multiSSH3-4.89.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: multiSSH3
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.89
|
|
4
4
|
Summary: Run commands on multiple hosts via SSH
|
|
5
5
|
Home-page: https://github.com/yufei-pan/multiSSH3
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -20,12 +20,12 @@ Can be used in bash scripts for automation actions.
|
|
|
20
20
|
Also able to be imported and / or use with Flexec SSH Backend to perform cluster automation actions.
|
|
21
21
|
|
|
22
22
|
Install via
|
|
23
|
-
```
|
|
23
|
+
```bash
|
|
24
24
|
pip install multiSSH3
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
multiSSH3 will be available as
|
|
28
|
-
```
|
|
28
|
+
```bash
|
|
29
29
|
mssh
|
|
30
30
|
mssh3
|
|
31
31
|
multissh
|
|
@@ -37,7 +37,7 @@ multissh will read a config file located at ```/etc/multiSSH3.config.json```
|
|
|
37
37
|
|
|
38
38
|
To store / generate a config file with the current command line options, you can use
|
|
39
39
|
|
|
40
|
-
```
|
|
40
|
+
```bash
|
|
41
41
|
mssh --generate_default_config_file
|
|
42
42
|
```
|
|
43
43
|
|
|
@@ -49,42 +49,52 @@ If you want to store password, it will be a plain text password in this config f
|
|
|
49
49
|
|
|
50
50
|
This option can also be used to store cli options into the config files. For example.
|
|
51
51
|
|
|
52
|
-
```
|
|
52
|
+
```bash
|
|
53
53
|
mssh --ipmi_interface_ip_prefix 192 --generate_default_config_file
|
|
54
54
|
```
|
|
55
55
|
will store
|
|
56
|
-
```
|
|
56
|
+
```json
|
|
57
57
|
"DEFAULT_IPMI_INTERFACE_IP_PREFIX": "192"
|
|
58
58
|
```
|
|
59
59
|
into the json file.
|
|
60
60
|
|
|
61
61
|
By defualt reads bash env variables for hostname aliases. Also able to read
|
|
62
|
-
```
|
|
62
|
+
```bash
|
|
63
63
|
DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
|
|
64
64
|
```
|
|
65
65
|
as hostname aliases.
|
|
66
66
|
|
|
67
67
|
For example:
|
|
68
|
-
```
|
|
68
|
+
```bash
|
|
69
69
|
export all='192.168.1-2.1-64'
|
|
70
70
|
mssh all 'echo hi'
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Note: you probably want to set presistent ssh connections to speed up each connection events.
|
|
74
|
+
An example .ssh/config:
|
|
75
|
+
```bash
|
|
76
|
+
Host *
|
|
77
|
+
StrictHostKeyChecking no
|
|
78
|
+
ControlMaster auto
|
|
79
|
+
ControlPath /run/ssh_sockets_%r@%h-%p
|
|
80
|
+
ControlPersist 3600
|
|
81
|
+
```
|
|
82
|
+
|
|
73
83
|
It is also able to recognize ip blocks / number blocks / hex blocks / character blocks directly.
|
|
74
84
|
|
|
75
85
|
For example:
|
|
76
|
-
```
|
|
86
|
+
```bash
|
|
77
87
|
mssh testrig[1-10] lsblk
|
|
78
88
|
mssh ww[a-c],10.100.0.* 'cat /etc/fstab' 'sed -i "/lustre/d' /etc/fstab' 'cat /etc/fstab'
|
|
79
89
|
```
|
|
80
90
|
|
|
81
91
|
It also supports interactive inputs. ( and able to async boardcast to all supplied hosts )
|
|
82
|
-
```
|
|
92
|
+
```bash
|
|
83
93
|
mssh www bash
|
|
84
94
|
```
|
|
85
95
|
|
|
86
96
|
By default, it will try to fit everything inside your window.
|
|
87
|
-
```
|
|
97
|
+
```bash
|
|
88
98
|
DEFAULT_CURSES_MINIMUM_CHAR_LEN = 40
|
|
89
99
|
DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
|
|
90
100
|
```
|
|
@@ -93,7 +103,7 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
|
|
|
93
103
|
|
|
94
104
|
Use ```mssh --help``` for more info.
|
|
95
105
|
|
|
96
|
-
```
|
|
106
|
+
```bash
|
|
97
107
|
usage: mssh [-h] [-u USERNAME] [-ea EXTRAARGS] [-p PASSWORD] [-11] [-f FILE] [--file_sync] [--scp] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL] [--ipmi]
|
|
98
108
|
[-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo] [-no] [--no_env] [--env_file ENV_FILE] [-m MAXCONNECTIONS] [-j]
|
|
99
109
|
[--success_hosts] [-g] [-nw] [-su] [-sh SKIPHOSTS] [-V]
|
|
@@ -307,8 +317,6 @@ Suppresses all output, useful for scripts where you only care about exit codes.
|
|
|
307
317
|
- Use `--no_env` to prevent loading any environment variables from files.
|
|
308
318
|
|
|
309
319
|
## Notes
|
|
310
|
-
|
|
311
|
-
- **SSH Configuration**: The script modifies `~/.ssh/config` to disable `StrictHostKeyChecking`. Ensure this is acceptable in your environment.
|
|
312
320
|
- **Dependencies**: Requires Python 3, `sshpass` (if using password authentication), and standard Unix utilities like `ssh`, `scp`, and `rsync`.
|
|
313
321
|
- **Signal Handling**: Supports graceful termination with `Ctrl+C`.
|
|
314
322
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
multiSSH3.py,sha256=g_U5Kk6pDABART5Tb4yoViPZ-5fiARpb_8Irvs7f2QA,86990
|
|
2
|
+
multiSSH3-4.89.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
+
multiSSH3-4.89.dist-info/METADATA,sha256=J9K4EyDzcP7Pvj4p779YAkYz7vbDGfQoxz9hmkWRra8,16043
|
|
4
|
+
multiSSH3-4.89.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
5
|
+
multiSSH3-4.89.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
6
|
+
multiSSH3-4.89.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
7
|
+
multiSSH3-4.89.dist-info/RECORD,,
|
multiSSH3.py
CHANGED
|
@@ -29,7 +29,7 @@ except AttributeError:
|
|
|
29
29
|
# If neither is available, use a dummy decorator
|
|
30
30
|
def cache_decorator(func):
|
|
31
31
|
return func
|
|
32
|
-
version = '4.
|
|
32
|
+
version = '4.89'
|
|
33
33
|
VERSION = version
|
|
34
34
|
|
|
35
35
|
CONFIG_FILE = '/etc/multiSSH3.config.json'
|
|
@@ -83,6 +83,7 @@ __build_in_default_config = {
|
|
|
83
83
|
'DEFAULT_GREPPABLE_MODE': False,
|
|
84
84
|
'DEFAULT_SKIP_UNREACHABLE': False,
|
|
85
85
|
'DEFAULT_SKIP_HOSTS': '',
|
|
86
|
+
'SSH_STRICT_HOST_KEY_CHECKING': False,
|
|
86
87
|
'ERROR_MESSAGES_TO_IGNORE': [
|
|
87
88
|
'Pseudo-terminal will not be allocated because stdin is not a terminal',
|
|
88
89
|
'Connection to .* closed',
|
|
@@ -140,6 +141,8 @@ DEFAULT_GREPPABLE_MODE = __configs_from_file.get('DEFAULT_GREPPABLE_MODE', __bui
|
|
|
140
141
|
DEFAULT_SKIP_UNREACHABLE = __configs_from_file.get('DEFAULT_SKIP_UNREACHABLE', __build_in_default_config['DEFAULT_SKIP_UNREACHABLE'])
|
|
141
142
|
DEFAULT_SKIP_HOSTS = __configs_from_file.get('DEFAULT_SKIP_HOSTS', __build_in_default_config['DEFAULT_SKIP_HOSTS'])
|
|
142
143
|
|
|
144
|
+
SSH_STRICT_HOST_KEY_CHECKING = __configs_from_file.get('SSH_STRICT_HOST_KEY_CHECKING', __build_in_default_config['SSH_STRICT_HOST_KEY_CHECKING'])
|
|
145
|
+
|
|
143
146
|
ERROR_MESSAGES_TO_IGNORE = __configs_from_file.get('ERROR_MESSAGES_TO_IGNORE', __build_in_default_config['ERROR_MESSAGES_TO_IGNORE'])
|
|
144
147
|
|
|
145
148
|
_DEFAULT_CALLED = __configs_from_file.get('_DEFAULT_CALLED', __build_in_default_config['_DEFAULT_CALLED'])
|
|
@@ -187,7 +190,11 @@ class Host:
|
|
|
187
190
|
|
|
188
191
|
__wildCharacters = ['*','?','x']
|
|
189
192
|
|
|
190
|
-
|
|
193
|
+
_no_env = DEFAULT_NO_ENV
|
|
194
|
+
|
|
195
|
+
_env_file = DEFAULT_ENV_FILE
|
|
196
|
+
|
|
197
|
+
__globalUnavailableHosts = set()
|
|
191
198
|
|
|
192
199
|
__ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
|
|
193
200
|
|
|
@@ -197,7 +204,6 @@ _emo = False
|
|
|
197
204
|
|
|
198
205
|
_etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
|
|
199
206
|
|
|
200
|
-
_env_file = DEFAULT_ENV_FILE
|
|
201
207
|
|
|
202
208
|
# check if command sshpass is available
|
|
203
209
|
_binPaths = {}
|
|
@@ -265,37 +271,6 @@ def expandIPv4Address(hosts):
|
|
|
265
271
|
expandedHosts.extend(expandedHost)
|
|
266
272
|
return expandedHosts
|
|
267
273
|
|
|
268
|
-
@cache_decorator
|
|
269
|
-
def readEnvFromFile(environemnt_file = ''):
|
|
270
|
-
'''
|
|
271
|
-
Read the environment variables from env_file
|
|
272
|
-
Returns:
|
|
273
|
-
dict: A dictionary of environment variables
|
|
274
|
-
'''
|
|
275
|
-
global env
|
|
276
|
-
try:
|
|
277
|
-
if env:
|
|
278
|
-
return env
|
|
279
|
-
except:
|
|
280
|
-
env = {}
|
|
281
|
-
global _env_file
|
|
282
|
-
if environemnt_file:
|
|
283
|
-
envf = environemnt_file
|
|
284
|
-
else:
|
|
285
|
-
envf = _env_file if _env_file else DEFAULT_ENV_FILE
|
|
286
|
-
if os.path.exists(envf):
|
|
287
|
-
with open(envf,'r') as f:
|
|
288
|
-
for line in f:
|
|
289
|
-
if line.startswith('#') or not line.strip():
|
|
290
|
-
continue
|
|
291
|
-
key, value = line.replace('export ', '', 1).strip().split('=', 1)
|
|
292
|
-
key = key.strip().strip('"').strip("'")
|
|
293
|
-
value = value.strip().strip('"').strip("'")
|
|
294
|
-
# avoid infinite recursion
|
|
295
|
-
if key != value:
|
|
296
|
-
env[key] = value.strip('"').strip("'")
|
|
297
|
-
return env
|
|
298
|
-
|
|
299
274
|
@cache_decorator
|
|
300
275
|
def getIP(hostname,local=False):
|
|
301
276
|
'''
|
|
@@ -336,9 +311,40 @@ def getIP(hostname,local=False):
|
|
|
336
311
|
return socket.gethostbyname(hostname)
|
|
337
312
|
except:
|
|
338
313
|
return None
|
|
314
|
+
|
|
315
|
+
@cache_decorator
|
|
316
|
+
def readEnvFromFile(environemnt_file = ''):
|
|
317
|
+
'''
|
|
318
|
+
Read the environment variables from env_file
|
|
319
|
+
Returns:
|
|
320
|
+
dict: A dictionary of environment variables
|
|
321
|
+
'''
|
|
322
|
+
global env
|
|
323
|
+
try:
|
|
324
|
+
if env:
|
|
325
|
+
return env
|
|
326
|
+
except:
|
|
327
|
+
env = {}
|
|
328
|
+
global _env_file
|
|
329
|
+
if environemnt_file:
|
|
330
|
+
envf = environemnt_file
|
|
331
|
+
else:
|
|
332
|
+
envf = _env_file if _env_file else DEFAULT_ENV_FILE
|
|
333
|
+
if os.path.exists(envf):
|
|
334
|
+
with open(envf,'r') as f:
|
|
335
|
+
for line in f:
|
|
336
|
+
if line.startswith('#') or not line.strip():
|
|
337
|
+
continue
|
|
338
|
+
key, value = line.replace('export ', '', 1).strip().split('=', 1)
|
|
339
|
+
key = key.strip().strip('"').strip("'")
|
|
340
|
+
value = value.strip().strip('"').strip("'")
|
|
341
|
+
# avoid infinite recursion
|
|
342
|
+
if key != value:
|
|
343
|
+
env[key] = value.strip('"').strip("'")
|
|
344
|
+
return env
|
|
339
345
|
|
|
340
346
|
@cache_decorator
|
|
341
|
-
def expand_hostname(text,validate=True
|
|
347
|
+
def expand_hostname(text,validate=True):
|
|
342
348
|
'''
|
|
343
349
|
Expand the hostname range in the text.
|
|
344
350
|
Will search the string for a range ( [] encloused and non enclosed number ranges).
|
|
@@ -359,12 +365,12 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
359
365
|
hostname = expandinghosts.pop()
|
|
360
366
|
match = re.search(r'\[(.*?-.*?)\]', hostname)
|
|
361
367
|
if not match:
|
|
362
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
368
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
363
369
|
continue
|
|
364
370
|
try:
|
|
365
371
|
range_start, range_end = match.group(1).split('-')
|
|
366
372
|
except ValueError:
|
|
367
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
373
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
368
374
|
continue
|
|
369
375
|
range_start = range_start.strip()
|
|
370
376
|
range_end = range_end.strip()
|
|
@@ -376,7 +382,7 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
376
382
|
elif range_start.isalpha() and range_start.isupper():
|
|
377
383
|
range_end = 'Z'
|
|
378
384
|
else:
|
|
379
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
385
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
380
386
|
continue
|
|
381
387
|
if not range_start:
|
|
382
388
|
if range_end.isdigit():
|
|
@@ -386,7 +392,7 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
386
392
|
elif range_end.isalpha() and range_end.isupper():
|
|
387
393
|
range_start = 'A'
|
|
388
394
|
else:
|
|
389
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
395
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
390
396
|
continue
|
|
391
397
|
if range_start.isdigit() and range_end.isdigit():
|
|
392
398
|
padding_length = min(len(range_start), len(range_end))
|
|
@@ -396,14 +402,14 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
396
402
|
if '[' in hostname:
|
|
397
403
|
expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
|
|
398
404
|
else:
|
|
399
|
-
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)
|
|
405
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)) if validate else [hostname])
|
|
400
406
|
else:
|
|
401
407
|
if all(c in string.hexdigits for c in range_start + range_end):
|
|
402
408
|
for i in range(int(range_start, 16), int(range_end, 16)+1):
|
|
403
409
|
if '[' in hostname:
|
|
404
410
|
expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
|
|
405
411
|
else:
|
|
406
|
-
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)
|
|
412
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)) if validate else [hostname])
|
|
407
413
|
else:
|
|
408
414
|
try:
|
|
409
415
|
start_index = alphanumeric.index(range_start)
|
|
@@ -412,13 +418,13 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
412
418
|
if '[' in hostname:
|
|
413
419
|
expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
|
|
414
420
|
else:
|
|
415
|
-
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)
|
|
421
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)) if validate else [hostname])
|
|
416
422
|
except ValueError:
|
|
417
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
423
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
418
424
|
return expandedhosts
|
|
419
425
|
|
|
420
426
|
@cache_decorator
|
|
421
|
-
def expand_hostnames(hosts
|
|
427
|
+
def expand_hostnames(hosts):
|
|
422
428
|
'''
|
|
423
429
|
Expand the hostnames in the hosts list
|
|
424
430
|
|
|
@@ -447,17 +453,17 @@ def expand_hostnames(hosts,no_env=False):
|
|
|
447
453
|
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):
|
|
448
454
|
hostSetToAdd = sorted(expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
|
|
449
455
|
else:
|
|
450
|
-
hostSetToAdd = sorted(expand_hostname(host
|
|
456
|
+
hostSetToAdd = sorted(expand_hostname(host))
|
|
451
457
|
if username:
|
|
452
458
|
# we expand the username
|
|
453
|
-
username = sorted(expand_hostname(username,validate=False
|
|
459
|
+
username = sorted(expand_hostname(username,validate=False))
|
|
454
460
|
# we combine the username and hostname
|
|
455
461
|
hostSetToAdd = [u+'@'+h for u,h in product(username,hostSetToAdd)]
|
|
456
462
|
expandedhosts.extend(hostSetToAdd)
|
|
457
463
|
return expandedhosts
|
|
458
464
|
|
|
459
465
|
@cache_decorator
|
|
460
|
-
def validate_expand_hostname(hostname
|
|
466
|
+
def validate_expand_hostname(hostname):
|
|
461
467
|
'''
|
|
462
468
|
Validate the hostname and expand it if it is a range of IP addresses
|
|
463
469
|
|
|
@@ -467,17 +473,18 @@ def validate_expand_hostname(hostname,no_env=False):
|
|
|
467
473
|
Returns:
|
|
468
474
|
list: A list of valid hostnames
|
|
469
475
|
'''
|
|
476
|
+
global _no_env
|
|
470
477
|
# maybe it is just defined in ./target_files/hosts.sh and exported to the environment
|
|
471
478
|
# we will try to get the valid host name from the environment
|
|
472
479
|
hostname = hostname.strip('$')
|
|
473
480
|
if getIP(hostname,local=True):
|
|
474
481
|
return [hostname]
|
|
475
|
-
elif not
|
|
482
|
+
elif not _no_env and hostname in os.environ:
|
|
476
483
|
# we will expand these hostnames again
|
|
477
|
-
return expand_hostnames(frozenset(os.environ[hostname].split(','))
|
|
484
|
+
return expand_hostnames(frozenset(os.environ[hostname].split(',')))
|
|
478
485
|
elif hostname in readEnvFromFile():
|
|
479
486
|
# we will expand these hostnames again
|
|
480
|
-
return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(','))
|
|
487
|
+
return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
|
|
481
488
|
elif getIP(hostname,local=False):
|
|
482
489
|
return [hostname]
|
|
483
490
|
else:
|
|
@@ -607,6 +614,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
607
614
|
global __ipmiiInterfaceIPPrefix
|
|
608
615
|
global _binPaths
|
|
609
616
|
try:
|
|
617
|
+
keyCheckArgs = []
|
|
618
|
+
rsyncKeyCheckArgs = []
|
|
619
|
+
if not SSH_STRICT_HOST_KEY_CHECKING:
|
|
620
|
+
keyCheckArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
|
|
621
|
+
rsyncKeyCheckArgs = ['--rsh','ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null']
|
|
610
622
|
host.username = None
|
|
611
623
|
host.address = host.name
|
|
612
624
|
if '@' in host.name:
|
|
@@ -697,11 +709,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
697
709
|
else:
|
|
698
710
|
fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
|
|
699
711
|
if useScp:
|
|
700
|
-
formatedCMD = [_binPaths['scp'],'-rpB'] + extraargs +['--']+fileArgs
|
|
712
|
+
formatedCMD = [_binPaths['scp'],'-rpB'] + keyCheckArgs + extraargs +['--']+fileArgs
|
|
701
713
|
else:
|
|
702
|
-
formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + extraargs +['--']+fileArgs
|
|
714
|
+
formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncKeyCheckArgs + extraargs +['--']+fileArgs
|
|
703
715
|
else:
|
|
704
|
-
formatedCMD = [_binPaths['ssh']] + extraargs +['--']+ [host.resolvedName, host.command]
|
|
716
|
+
formatedCMD = [_binPaths['ssh']] + keyCheckArgs + extraargs +['--']+ [host.resolvedName, host.command]
|
|
705
717
|
if passwds and 'sshpass' in _binPaths:
|
|
706
718
|
formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
|
|
707
719
|
elif passwds:
|
|
@@ -1226,34 +1238,34 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1226
1238
|
print(rtnStr)
|
|
1227
1239
|
return rtnStr
|
|
1228
1240
|
|
|
1229
|
-
sshConfigged = False
|
|
1230
|
-
def verify_ssh_config():
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1241
|
+
# sshConfigged = False
|
|
1242
|
+
# def verify_ssh_config():
|
|
1243
|
+
# '''
|
|
1244
|
+
# Verify that ~/.ssh/config exists and contains the line "StrictHostKeyChecking no"
|
|
1245
|
+
|
|
1246
|
+
# Args:
|
|
1247
|
+
# None
|
|
1248
|
+
|
|
1249
|
+
# Returns:
|
|
1250
|
+
# None
|
|
1251
|
+
# '''
|
|
1252
|
+
# global sshConfigged
|
|
1253
|
+
# if not sshConfigged:
|
|
1254
|
+
# # first we make sure ~/.ssh/config exists
|
|
1255
|
+
# config = ''
|
|
1256
|
+
# if not os.path.exists(os.path.expanduser('~/.ssh')):
|
|
1257
|
+
# os.makedirs(os.path.expanduser('~/.ssh'))
|
|
1258
|
+
# if os.path.exists(os.path.expanduser('~/.ssh/config')):
|
|
1259
|
+
# with open(os.path.expanduser('~/.ssh/config'),'r') as f:
|
|
1260
|
+
# config = f.read()
|
|
1261
|
+
# if config:
|
|
1262
|
+
# if 'StrictHostKeyChecking no' not in config:
|
|
1263
|
+
# with open(os.path.expanduser('~/.ssh/config'),'a') as f:
|
|
1264
|
+
# f.write('\nHost *\n\tStrictHostKeyChecking no\n')
|
|
1265
|
+
# else:
|
|
1266
|
+
# with open(os.path.expanduser('~/.ssh/config'),'w') as f:
|
|
1267
|
+
# f.write('Host *\n\tStrictHostKeyChecking no\n')
|
|
1268
|
+
# sshConfigged = True
|
|
1257
1269
|
|
|
1258
1270
|
def signal_handler(sig, frame):
|
|
1259
1271
|
'''
|
|
@@ -1277,9 +1289,9 @@ def signal_handler(sig, frame):
|
|
|
1277
1289
|
os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
1278
1290
|
sys.exit(0)
|
|
1279
1291
|
|
|
1280
|
-
|
|
1281
1292
|
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):
|
|
1282
|
-
global
|
|
1293
|
+
global __globalUnavailableHosts
|
|
1294
|
+
global _no_env
|
|
1283
1295
|
threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
|
|
1284
1296
|
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:
|
|
1285
1297
|
curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
|
|
@@ -1292,7 +1304,11 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
1292
1304
|
# update the unavailable hosts and global unavailable hosts
|
|
1293
1305
|
if willUpdateUnreachableHosts:
|
|
1294
1306
|
unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or host.stderr[0].strip().startswith('Timeout!'))])
|
|
1295
|
-
|
|
1307
|
+
__globalUnavailableHosts.update(unavailableHosts)
|
|
1308
|
+
# update the os environment variable if not _no_env
|
|
1309
|
+
if not _no_env:
|
|
1310
|
+
os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
|
|
1311
|
+
|
|
1296
1312
|
# print the output, if the output of multiple hosts are the same, we aggragate them
|
|
1297
1313
|
if not called:
|
|
1298
1314
|
print_output(hosts,json,greppable=greppable)
|
|
@@ -1394,6 +1410,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1394
1410
|
interface_ip_prefix (str, optional): The prefix of the IPMI interface. Defaults to {DEFAULT_INTERFACE_IP_PREFIX}.
|
|
1395
1411
|
returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {_DEFAULT_RETURN_UNFINISHED}.
|
|
1396
1412
|
scp (bool, optional): Whether to use scp instead of rsync. Defaults to {DEFAULT_SCP}.
|
|
1413
|
+
gather_mode (bool, optional): Whether to use gather mode. Defaults to False.
|
|
1397
1414
|
username (str, optional): The username to use to connect to the hosts. Defaults to {DEFAULT_USERNAME}.
|
|
1398
1415
|
extraargs (str, optional): Extra arguments to pass to the ssh / rsync / scp command. Defaults to {DEFAULT_EXTRA_ARGS}.
|
|
1399
1416
|
skipUnreachable (bool, optional): Whether to skip unreachable hosts. Defaults to {DEFAULT_SKIP_UNREACHABLE}.
|
|
@@ -1410,8 +1427,16 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1410
1427
|
Returns:
|
|
1411
1428
|
list: A list of Host objects
|
|
1412
1429
|
'''
|
|
1413
|
-
global
|
|
1430
|
+
global __globalUnavailableHosts
|
|
1414
1431
|
global __global_suppress_printout
|
|
1432
|
+
global _no_env
|
|
1433
|
+
global _emo
|
|
1434
|
+
_emo = False
|
|
1435
|
+
_no_env = no_env
|
|
1436
|
+
if not no_env and '__multiSSH3_UNAVAILABLE_HOSTS' in os.environ:
|
|
1437
|
+
__globalUnavailableHosts = set(os.environ['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
|
|
1438
|
+
elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
|
|
1439
|
+
__globalUnavailableHosts = set(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
|
|
1415
1440
|
if not max_connections:
|
|
1416
1441
|
max_connections = 4 * os.cpu_count()
|
|
1417
1442
|
elif max_connections == 0:
|
|
@@ -1420,7 +1445,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1420
1445
|
max_connections = (-max_connections) * os.cpu_count()
|
|
1421
1446
|
if not commands:
|
|
1422
1447
|
commands = []
|
|
1423
|
-
verify_ssh_config()
|
|
1448
|
+
#verify_ssh_config()
|
|
1424
1449
|
# load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
|
|
1425
1450
|
if called:
|
|
1426
1451
|
# if called,
|
|
@@ -1429,18 +1454,17 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1429
1454
|
if skipUnreachable is None:
|
|
1430
1455
|
skipUnreachable = True
|
|
1431
1456
|
if skipUnreachable:
|
|
1432
|
-
unavailableHosts =
|
|
1457
|
+
unavailableHosts = __globalUnavailableHosts
|
|
1433
1458
|
else:
|
|
1434
1459
|
unavailableHosts = set()
|
|
1435
1460
|
else:
|
|
1436
1461
|
# if run in command line ( or emulating running in command line, we default to skip unreachable hosts within one command call )
|
|
1437
1462
|
if skipUnreachable:
|
|
1438
|
-
unavailableHosts =
|
|
1463
|
+
unavailableHosts = __globalUnavailableHosts
|
|
1439
1464
|
else:
|
|
1440
1465
|
unavailableHosts = set()
|
|
1441
1466
|
skipUnreachable = True
|
|
1442
|
-
|
|
1443
|
-
_emo = False
|
|
1467
|
+
|
|
1444
1468
|
# We create the hosts
|
|
1445
1469
|
hostStr = formHostStr(hosts)
|
|
1446
1470
|
skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
|
|
@@ -1459,8 +1483,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1459
1483
|
if '@' not in host:
|
|
1460
1484
|
skipHostStr[i] = userStr + host
|
|
1461
1485
|
skipHostStr = ','.join(skipHostStr)
|
|
1462
|
-
targetHostsList = expand_hostnames(frozenset(hostStr.split(','))
|
|
1463
|
-
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(','))
|
|
1486
|
+
targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
|
|
1487
|
+
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
|
|
1464
1488
|
if skipHostsList:
|
|
1465
1489
|
if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
|
|
1466
1490
|
if files and not commands:
|
|
@@ -1471,15 +1495,18 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1471
1495
|
files = set(files+commands) if files else set(commands)
|
|
1472
1496
|
if files:
|
|
1473
1497
|
# try to resolve files first (like * etc)
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1498
|
+
if not gather_mode:
|
|
1499
|
+
pathSet = set()
|
|
1500
|
+
for file in files:
|
|
1501
|
+
try:
|
|
1502
|
+
pathSet.update(glob.glob(file,include_hidden=True,recursive=True))
|
|
1503
|
+
except:
|
|
1504
|
+
pathSet.update(glob.glob(file,recursive=True))
|
|
1505
|
+
if not pathSet:
|
|
1506
|
+
print(f'Warning: No source files at {files} are found after resolving globs!')
|
|
1507
|
+
sys.exit(66)
|
|
1508
|
+
else:
|
|
1509
|
+
pathSet = set(files)
|
|
1483
1510
|
if file_sync:
|
|
1484
1511
|
# use abosolute path for file sync
|
|
1485
1512
|
commands = [os.path.abspath(file) for file in pathSet]
|
|
@@ -1598,6 +1625,7 @@ def get_default_config(args):
|
|
|
1598
1625
|
'DEFAULT_GREPPABLE_MODE': args.greppable,
|
|
1599
1626
|
'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
|
|
1600
1627
|
'DEFAULT_SKIP_HOSTS': args.skip_hosts,
|
|
1628
|
+
'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
|
|
1601
1629
|
'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
|
|
1602
1630
|
}
|
|
1603
1631
|
|
|
@@ -1605,14 +1633,15 @@ def write_default_config(args,CONFIG_FILE,backup = True):
|
|
|
1605
1633
|
if backup and os.path.exists(CONFIG_FILE):
|
|
1606
1634
|
os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
|
|
1607
1635
|
default_config = get_default_config(args)
|
|
1636
|
+
# apply the updated defualt_config to __configs_from_file and write that to file
|
|
1637
|
+
__configs_from_file.update(default_config)
|
|
1608
1638
|
with open(CONFIG_FILE,'w') as f:
|
|
1609
|
-
json.dump(
|
|
1639
|
+
json.dump(__configs_from_file,f,indent=4)
|
|
1610
1640
|
|
|
1611
1641
|
|
|
1612
1642
|
def main():
|
|
1613
1643
|
global _emo
|
|
1614
1644
|
global __global_suppress_printout
|
|
1615
|
-
global __gloablUnavailableHosts
|
|
1616
1645
|
global __mainReturnCode
|
|
1617
1646
|
global __failedHosts
|
|
1618
1647
|
global __ipmiiInterfaceIPPrefix
|
|
@@ -1646,22 +1675,22 @@ def main():
|
|
|
1646
1675
|
parser.add_argument('-sw','--single_window', action='store_true', help=f'Use a single window for all hosts. (default: {DEFAULT_SINGLE_WINDOW})', default=DEFAULT_SINGLE_WINDOW)
|
|
1647
1676
|
parser.add_argument('-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
|
|
1648
1677
|
parser.add_argument("-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
|
|
1649
|
-
parser.add_argument('--no_env', action='store_true', help=f'Do not load the environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
|
|
1650
|
-
parser.add_argument("--env_file", type=str, help=f"The file to load the environment variables from. (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
|
|
1678
|
+
parser.add_argument('--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
|
|
1679
|
+
parser.add_argument("--env_file", type=str, help=f"The file to load the mssh file based environment variables from. ( Still work with --no_env ) (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
|
|
1651
1680
|
parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
|
|
1652
1681
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
1653
1682
|
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)
|
|
1654
1683
|
parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
1655
1684
|
parser.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
|
|
1656
1685
|
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)
|
|
1657
|
-
parser.add_argument('--
|
|
1686
|
+
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}')
|
|
1658
1687
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
1659
1688
|
|
|
1660
1689
|
# parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
|
|
1661
1690
|
# help='the user to use to connect to the hosts')
|
|
1662
1691
|
args = parser.parse_args()
|
|
1663
1692
|
|
|
1664
|
-
if args.
|
|
1693
|
+
if args.store_config_file:
|
|
1665
1694
|
try:
|
|
1666
1695
|
if os.path.exists(CONFIG_FILE):
|
|
1667
1696
|
print(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
|
|
@@ -1681,6 +1710,8 @@ def main():
|
|
|
1681
1710
|
except Exception as e:
|
|
1682
1711
|
print(f"Error while writing config file: {e}")
|
|
1683
1712
|
if not args.commands:
|
|
1713
|
+
with open(CONFIG_FILE,'r') as f:
|
|
1714
|
+
print(f"Config file content: \n{f.read()}")
|
|
1684
1715
|
sys.exit(0)
|
|
1685
1716
|
|
|
1686
1717
|
_env_file = args.env_file
|
multiSSH3-4.83.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=1Un4-afx2wCyXWKgy1-Y55fpth0_L8zdb-qa_Cm3U-s,85689
|
|
2
|
-
multiSSH3-4.83.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
-
multiSSH3-4.83.dist-info/METADATA,sha256=A3OVJ44Q0Oi8_1na-AredxReylobi6DX2m87o1z7-4s,15887
|
|
4
|
-
multiSSH3-4.83.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
5
|
-
multiSSH3-4.83.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
6
|
-
multiSSH3-4.83.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
7
|
-
multiSSH3-4.83.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|