multiSSH3 4.83__py3-none-any.whl → 4.92__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.92.dist-info}/METADATA +22 -14
- multiSSH3-4.92.dist-info/RECORD +7 -0
- multiSSH3.py +176 -128
- multiSSH3-4.83.dist-info/RECORD +0 -7
- {multiSSH3-4.83.dist-info → multiSSH3-4.92.dist-info}/LICENSE +0 -0
- {multiSSH3-4.83.dist-info → multiSSH3-4.92.dist-info}/WHEEL +0 -0
- {multiSSH3-4.83.dist-info → multiSSH3-4.92.dist-info}/entry_points.txt +0 -0
- {multiSSH3-4.83.dist-info → multiSSH3-4.92.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.92
|
|
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=XGiNVv-ZsYMGFVrZ8zpeK4wz170Fi8YRtltcRxrQQCI,88186
|
|
2
|
+
multiSSH3-4.92.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
+
multiSSH3-4.92.dist-info/METADATA,sha256=lesBhivYV4dahRGYUWvo-jbxcOEhEl6VHhSjQ1U-7-M,16043
|
|
4
|
+
multiSSH3-4.92.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
5
|
+
multiSSH3-4.92.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
6
|
+
multiSSH3-4.92.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
7
|
+
multiSSH3-4.92.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.92'
|
|
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'])
|
|
@@ -170,6 +173,7 @@ class Host:
|
|
|
170
173
|
self.stdout = [] # the stdout of the command
|
|
171
174
|
self.stderr = [] # the stderr of the command
|
|
172
175
|
self.printedLines = -1 # the number of lines printed on the screen
|
|
176
|
+
self.lastUpdateTime = time.time() # the last time the output was updated
|
|
173
177
|
self.files = files # the files to be copied to the host
|
|
174
178
|
self.ipmi = ipmi # whether to use ipmi to connect to the host
|
|
175
179
|
self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
|
|
@@ -187,7 +191,11 @@ class Host:
|
|
|
187
191
|
|
|
188
192
|
__wildCharacters = ['*','?','x']
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
_no_env = DEFAULT_NO_ENV
|
|
195
|
+
|
|
196
|
+
_env_file = DEFAULT_ENV_FILE
|
|
197
|
+
|
|
198
|
+
__globalUnavailableHosts = set()
|
|
191
199
|
|
|
192
200
|
__ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
|
|
193
201
|
|
|
@@ -197,7 +205,6 @@ _emo = False
|
|
|
197
205
|
|
|
198
206
|
_etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
|
|
199
207
|
|
|
200
|
-
_env_file = DEFAULT_ENV_FILE
|
|
201
208
|
|
|
202
209
|
# check if command sshpass is available
|
|
203
210
|
_binPaths = {}
|
|
@@ -265,37 +272,6 @@ def expandIPv4Address(hosts):
|
|
|
265
272
|
expandedHosts.extend(expandedHost)
|
|
266
273
|
return expandedHosts
|
|
267
274
|
|
|
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
275
|
@cache_decorator
|
|
300
276
|
def getIP(hostname,local=False):
|
|
301
277
|
'''
|
|
@@ -336,9 +312,40 @@ def getIP(hostname,local=False):
|
|
|
336
312
|
return socket.gethostbyname(hostname)
|
|
337
313
|
except:
|
|
338
314
|
return None
|
|
315
|
+
|
|
316
|
+
@cache_decorator
|
|
317
|
+
def readEnvFromFile(environemnt_file = ''):
|
|
318
|
+
'''
|
|
319
|
+
Read the environment variables from env_file
|
|
320
|
+
Returns:
|
|
321
|
+
dict: A dictionary of environment variables
|
|
322
|
+
'''
|
|
323
|
+
global env
|
|
324
|
+
try:
|
|
325
|
+
if env:
|
|
326
|
+
return env
|
|
327
|
+
except:
|
|
328
|
+
env = {}
|
|
329
|
+
global _env_file
|
|
330
|
+
if environemnt_file:
|
|
331
|
+
envf = environemnt_file
|
|
332
|
+
else:
|
|
333
|
+
envf = _env_file if _env_file else DEFAULT_ENV_FILE
|
|
334
|
+
if os.path.exists(envf):
|
|
335
|
+
with open(envf,'r') as f:
|
|
336
|
+
for line in f:
|
|
337
|
+
if line.startswith('#') or not line.strip():
|
|
338
|
+
continue
|
|
339
|
+
key, value = line.replace('export ', '', 1).strip().split('=', 1)
|
|
340
|
+
key = key.strip().strip('"').strip("'")
|
|
341
|
+
value = value.strip().strip('"').strip("'")
|
|
342
|
+
# avoid infinite recursion
|
|
343
|
+
if key != value:
|
|
344
|
+
env[key] = value.strip('"').strip("'")
|
|
345
|
+
return env
|
|
339
346
|
|
|
340
347
|
@cache_decorator
|
|
341
|
-
def expand_hostname(text,validate=True
|
|
348
|
+
def expand_hostname(text,validate=True):
|
|
342
349
|
'''
|
|
343
350
|
Expand the hostname range in the text.
|
|
344
351
|
Will search the string for a range ( [] encloused and non enclosed number ranges).
|
|
@@ -359,12 +366,12 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
359
366
|
hostname = expandinghosts.pop()
|
|
360
367
|
match = re.search(r'\[(.*?-.*?)\]', hostname)
|
|
361
368
|
if not match:
|
|
362
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
369
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
363
370
|
continue
|
|
364
371
|
try:
|
|
365
372
|
range_start, range_end = match.group(1).split('-')
|
|
366
373
|
except ValueError:
|
|
367
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
374
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
368
375
|
continue
|
|
369
376
|
range_start = range_start.strip()
|
|
370
377
|
range_end = range_end.strip()
|
|
@@ -376,7 +383,7 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
376
383
|
elif range_start.isalpha() and range_start.isupper():
|
|
377
384
|
range_end = 'Z'
|
|
378
385
|
else:
|
|
379
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
386
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
380
387
|
continue
|
|
381
388
|
if not range_start:
|
|
382
389
|
if range_end.isdigit():
|
|
@@ -386,7 +393,7 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
386
393
|
elif range_end.isalpha() and range_end.isupper():
|
|
387
394
|
range_start = 'A'
|
|
388
395
|
else:
|
|
389
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
396
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
390
397
|
continue
|
|
391
398
|
if range_start.isdigit() and range_end.isdigit():
|
|
392
399
|
padding_length = min(len(range_start), len(range_end))
|
|
@@ -396,14 +403,14 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
396
403
|
if '[' in hostname:
|
|
397
404
|
expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
|
|
398
405
|
else:
|
|
399
|
-
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)
|
|
406
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)) if validate else [hostname])
|
|
400
407
|
else:
|
|
401
408
|
if all(c in string.hexdigits for c in range_start + range_end):
|
|
402
409
|
for i in range(int(range_start, 16), int(range_end, 16)+1):
|
|
403
410
|
if '[' in hostname:
|
|
404
411
|
expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
|
|
405
412
|
else:
|
|
406
|
-
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)
|
|
413
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)) if validate else [hostname])
|
|
407
414
|
else:
|
|
408
415
|
try:
|
|
409
416
|
start_index = alphanumeric.index(range_start)
|
|
@@ -412,13 +419,13 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
412
419
|
if '[' in hostname:
|
|
413
420
|
expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
|
|
414
421
|
else:
|
|
415
|
-
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)
|
|
422
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)) if validate else [hostname])
|
|
416
423
|
except ValueError:
|
|
417
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
424
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
418
425
|
return expandedhosts
|
|
419
426
|
|
|
420
427
|
@cache_decorator
|
|
421
|
-
def expand_hostnames(hosts
|
|
428
|
+
def expand_hostnames(hosts):
|
|
422
429
|
'''
|
|
423
430
|
Expand the hostnames in the hosts list
|
|
424
431
|
|
|
@@ -447,17 +454,17 @@ def expand_hostnames(hosts,no_env=False):
|
|
|
447
454
|
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
455
|
hostSetToAdd = sorted(expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
|
|
449
456
|
else:
|
|
450
|
-
hostSetToAdd = sorted(expand_hostname(host
|
|
457
|
+
hostSetToAdd = sorted(expand_hostname(host))
|
|
451
458
|
if username:
|
|
452
459
|
# we expand the username
|
|
453
|
-
username = sorted(expand_hostname(username,validate=False
|
|
460
|
+
username = sorted(expand_hostname(username,validate=False))
|
|
454
461
|
# we combine the username and hostname
|
|
455
462
|
hostSetToAdd = [u+'@'+h for u,h in product(username,hostSetToAdd)]
|
|
456
463
|
expandedhosts.extend(hostSetToAdd)
|
|
457
464
|
return expandedhosts
|
|
458
465
|
|
|
459
466
|
@cache_decorator
|
|
460
|
-
def validate_expand_hostname(hostname
|
|
467
|
+
def validate_expand_hostname(hostname):
|
|
461
468
|
'''
|
|
462
469
|
Validate the hostname and expand it if it is a range of IP addresses
|
|
463
470
|
|
|
@@ -467,17 +474,18 @@ def validate_expand_hostname(hostname,no_env=False):
|
|
|
467
474
|
Returns:
|
|
468
475
|
list: A list of valid hostnames
|
|
469
476
|
'''
|
|
477
|
+
global _no_env
|
|
470
478
|
# maybe it is just defined in ./target_files/hosts.sh and exported to the environment
|
|
471
479
|
# we will try to get the valid host name from the environment
|
|
472
480
|
hostname = hostname.strip('$')
|
|
473
481
|
if getIP(hostname,local=True):
|
|
474
482
|
return [hostname]
|
|
475
|
-
elif not
|
|
483
|
+
elif not _no_env and hostname in os.environ:
|
|
476
484
|
# we will expand these hostnames again
|
|
477
|
-
return expand_hostnames(frozenset(os.environ[hostname].split(','))
|
|
485
|
+
return expand_hostnames(frozenset(os.environ[hostname].split(',')))
|
|
478
486
|
elif hostname in readEnvFromFile():
|
|
479
487
|
# we will expand these hostnames again
|
|
480
|
-
return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(','))
|
|
488
|
+
return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
|
|
481
489
|
elif getIP(hostname,local=False):
|
|
482
490
|
return [hostname]
|
|
483
491
|
else:
|
|
@@ -534,6 +542,7 @@ def handle_reading_stream(stream,target, host):
|
|
|
534
542
|
current_line_str = current_line.decode('utf-8',errors='backslashreplace')
|
|
535
543
|
target.append(current_line_str)
|
|
536
544
|
host.output.append(current_line_str)
|
|
545
|
+
host.lastUpdateTime = time.time()
|
|
537
546
|
current_line = bytearray()
|
|
538
547
|
lastLineCommited = True
|
|
539
548
|
for char in iter(lambda:stream.read(1), b''):
|
|
@@ -577,6 +586,7 @@ def handle_writing_stream(stream,stop_event,host):
|
|
|
577
586
|
host.output.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
|
|
578
587
|
host.stdout.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
|
|
579
588
|
sentInput += 1
|
|
589
|
+
host.lastUpdateTime = time.time()
|
|
580
590
|
else:
|
|
581
591
|
time.sleep(0.1)
|
|
582
592
|
if sentInput < len(__keyPressesIn) - 1 :
|
|
@@ -607,6 +617,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
607
617
|
global __ipmiiInterfaceIPPrefix
|
|
608
618
|
global _binPaths
|
|
609
619
|
try:
|
|
620
|
+
keyCheckArgs = []
|
|
621
|
+
rsyncKeyCheckArgs = []
|
|
622
|
+
if not SSH_STRICT_HOST_KEY_CHECKING:
|
|
623
|
+
keyCheckArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
|
|
624
|
+
rsyncKeyCheckArgs = ['--rsh','ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null']
|
|
610
625
|
host.username = None
|
|
611
626
|
host.address = host.name
|
|
612
627
|
if '@' in host.name:
|
|
@@ -697,11 +712,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
697
712
|
else:
|
|
698
713
|
fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
|
|
699
714
|
if useScp:
|
|
700
|
-
formatedCMD = [_binPaths['scp'],'-rpB'] + extraargs +['--']+fileArgs
|
|
715
|
+
formatedCMD = [_binPaths['scp'],'-rpB'] + keyCheckArgs + extraargs +['--']+fileArgs
|
|
701
716
|
else:
|
|
702
|
-
formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + extraargs +['--']+fileArgs
|
|
717
|
+
formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncKeyCheckArgs + extraargs +['--']+fileArgs
|
|
703
718
|
else:
|
|
704
|
-
formatedCMD = [_binPaths['ssh']] + extraargs +['--']+ [host.resolvedName, host.command]
|
|
719
|
+
formatedCMD = [_binPaths['ssh']] + keyCheckArgs + extraargs +['--']+ [host.resolvedName, host.command]
|
|
705
720
|
if passwds and 'sshpass' in _binPaths:
|
|
706
721
|
formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
|
|
707
722
|
elif passwds:
|
|
@@ -738,14 +753,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
738
753
|
stdin_thread = threading.Thread(target=handle_writing_stream, args=(proc.stdin,stdin_stop_event, host), daemon=True)
|
|
739
754
|
stdin_thread.start()
|
|
740
755
|
# Monitor the subprocess and terminate it after the timeout
|
|
741
|
-
|
|
742
|
-
|
|
756
|
+
host.lastUpdateTime = time.time()
|
|
757
|
+
timeoutLineAppended = False
|
|
743
758
|
while proc.poll() is None: # while the process is still running
|
|
744
|
-
if len(host.output) > outLength:
|
|
745
|
-
start_time = time.time()
|
|
746
|
-
outLength = len(host.output)
|
|
747
759
|
if timeout > 0:
|
|
748
|
-
if time.time() -
|
|
760
|
+
if time.time() - host.lastUpdateTime > timeout:
|
|
749
761
|
host.stderr.append('Timeout!')
|
|
750
762
|
host.output.append('Timeout!')
|
|
751
763
|
proc.send_signal(signal.SIGINT)
|
|
@@ -753,15 +765,19 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
753
765
|
|
|
754
766
|
proc.terminate()
|
|
755
767
|
break
|
|
756
|
-
elif time.time() -
|
|
757
|
-
timeoutLine = f'Timeout in [{timeout - int(time.time() -
|
|
768
|
+
elif time.time() - host.lastUpdateTime > min(30, timeout // 2):
|
|
769
|
+
timeoutLine = f'Timeout in [{timeout - int(time.time() - host.lastUpdateTime)}] seconds!'
|
|
758
770
|
if host.output and not host.output[-1].strip().startswith(timeoutLine):
|
|
759
771
|
# remove last line if it is a countdown
|
|
760
|
-
if host.output and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
|
|
772
|
+
if host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
|
|
761
773
|
host.output.pop()
|
|
762
774
|
host.printedLines -= 1
|
|
763
775
|
host.output.append(timeoutLine)
|
|
764
|
-
|
|
776
|
+
timeoutLineAppended = True
|
|
777
|
+
elif host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
|
|
778
|
+
host.output.pop()
|
|
779
|
+
host.printedLines -= 1
|
|
780
|
+
timeoutLineAppended = False
|
|
765
781
|
if _emo:
|
|
766
782
|
host.stderr.append('Ctrl C detected, Emergency Stop!')
|
|
767
783
|
host.output.append('Ctrl C detected, Emergency Stop!')
|
|
@@ -954,7 +970,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
954
970
|
bottom_border = None
|
|
955
971
|
if y + host_window_height < org_dim[0]:
|
|
956
972
|
bottom_border = curses.newwin(1, max_x, y + host_window_height, 0)
|
|
957
|
-
bottom_border.clear()
|
|
973
|
+
#bottom_border.clear()
|
|
958
974
|
bottom_border.addstr(0, 0, '-' * (max_x - 1))
|
|
959
975
|
bottom_border.refresh()
|
|
960
976
|
while host_stats['running'] > 0 or host_stats['waiting'] > 0:
|
|
@@ -1047,7 +1063,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1047
1063
|
bottom_stats = '└'+ f"Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']}"[:max_x - 2].center(max_x - 2, "─")
|
|
1048
1064
|
if bottom_stats != old_bottom_stat:
|
|
1049
1065
|
old_bottom_stat = bottom_stats
|
|
1050
|
-
bottom_border.clear()
|
|
1066
|
+
#bottom_border.clear()
|
|
1051
1067
|
bottom_border.addstr(0, 0, bottom_stats)
|
|
1052
1068
|
bottom_border.refresh()
|
|
1053
1069
|
if stats != old_stat or curserPosition != old_cursor_position:
|
|
@@ -1058,7 +1074,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1058
1074
|
curserPositionStats = min(min(curserPosition,len(encodedLine) -1) + stats.find('Send CMD: ')+len('Send CMD: '), max_x -2)
|
|
1059
1075
|
else:
|
|
1060
1076
|
curserPositionStats = max_x -2
|
|
1061
|
-
stat_window.clear()
|
|
1077
|
+
#stat_window.clear()
|
|
1062
1078
|
#stat_window.addstr(0, 0, stats)
|
|
1063
1079
|
# add the line with curser that inverses the color at the curser position
|
|
1064
1080
|
stat_window.addstr(0, 0, stats[:curserPositionStats], curses.color_pair(1))
|
|
@@ -1074,7 +1090,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1074
1090
|
# we will only update the window if there is new output or the window is not fully printed
|
|
1075
1091
|
if new_configured or host.printedLines < len(host.output):
|
|
1076
1092
|
try:
|
|
1077
|
-
host_window.clear()
|
|
1093
|
+
#host_window.clear()
|
|
1078
1094
|
# we will try to center the name of the host with ┼ at the beginning and end and ─ in between
|
|
1079
1095
|
linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
|
|
1080
1096
|
host_window.addstr(0, 0, linePrintOut)
|
|
@@ -1082,12 +1098,12 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
|
|
|
1082
1098
|
for i, line in enumerate(host.output[-(host_window_height - 1):]):
|
|
1083
1099
|
# print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
|
|
1084
1100
|
# time.sleep(10)
|
|
1085
|
-
linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
|
|
1101
|
+
linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip().ljust(host_window_width - 1, ' ')
|
|
1086
1102
|
host_window.addstr(i + 1, 0, linePrintOut)
|
|
1087
1103
|
# we draw the rest of the available lines
|
|
1088
1104
|
for i in range(len(host.output), host_window_height - 1):
|
|
1089
1105
|
# print(f"Printng a line at {i + 1} with length of {len('│')}")
|
|
1090
|
-
host_window.addstr(i + 1, 0, '│')
|
|
1106
|
+
host_window.addstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '))
|
|
1091
1107
|
host.printedLines = len(host.output)
|
|
1092
1108
|
host_window.refresh()
|
|
1093
1109
|
except Exception as e:
|
|
@@ -1122,6 +1138,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
1122
1138
|
# We create all the windows we need
|
|
1123
1139
|
# We initialize the color pair
|
|
1124
1140
|
curses.start_color()
|
|
1141
|
+
curses.curs_set(0)
|
|
1125
1142
|
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
|
1126
1143
|
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
|
1127
1144
|
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
@@ -1204,7 +1221,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1204
1221
|
rtnStr = ''
|
|
1205
1222
|
for output, hosts in outputs.items():
|
|
1206
1223
|
if __global_suppress_printout:
|
|
1207
|
-
rtnStr += f'
|
|
1224
|
+
rtnStr += f'Abnormal returncode produced by {hosts}:\n'
|
|
1208
1225
|
rtnStr += output+'\n'
|
|
1209
1226
|
else:
|
|
1210
1227
|
rtnStr += '*'*80+'\n'
|
|
@@ -1226,34 +1243,34 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1226
1243
|
print(rtnStr)
|
|
1227
1244
|
return rtnStr
|
|
1228
1245
|
|
|
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
|
-
|
|
1246
|
+
# sshConfigged = False
|
|
1247
|
+
# def verify_ssh_config():
|
|
1248
|
+
# '''
|
|
1249
|
+
# Verify that ~/.ssh/config exists and contains the line "StrictHostKeyChecking no"
|
|
1250
|
+
|
|
1251
|
+
# Args:
|
|
1252
|
+
# None
|
|
1253
|
+
|
|
1254
|
+
# Returns:
|
|
1255
|
+
# None
|
|
1256
|
+
# '''
|
|
1257
|
+
# global sshConfigged
|
|
1258
|
+
# if not sshConfigged:
|
|
1259
|
+
# # first we make sure ~/.ssh/config exists
|
|
1260
|
+
# config = ''
|
|
1261
|
+
# if not os.path.exists(os.path.expanduser('~/.ssh')):
|
|
1262
|
+
# os.makedirs(os.path.expanduser('~/.ssh'))
|
|
1263
|
+
# if os.path.exists(os.path.expanduser('~/.ssh/config')):
|
|
1264
|
+
# with open(os.path.expanduser('~/.ssh/config'),'r') as f:
|
|
1265
|
+
# config = f.read()
|
|
1266
|
+
# if config:
|
|
1267
|
+
# if 'StrictHostKeyChecking no' not in config:
|
|
1268
|
+
# with open(os.path.expanduser('~/.ssh/config'),'a') as f:
|
|
1269
|
+
# f.write('\nHost *\n\tStrictHostKeyChecking no\n')
|
|
1270
|
+
# else:
|
|
1271
|
+
# with open(os.path.expanduser('~/.ssh/config'),'w') as f:
|
|
1272
|
+
# f.write('Host *\n\tStrictHostKeyChecking no\n')
|
|
1273
|
+
# sshConfigged = True
|
|
1257
1274
|
|
|
1258
1275
|
def signal_handler(sig, frame):
|
|
1259
1276
|
'''
|
|
@@ -1277,9 +1294,9 @@ def signal_handler(sig, frame):
|
|
|
1277
1294
|
os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
1278
1295
|
sys.exit(0)
|
|
1279
1296
|
|
|
1280
|
-
|
|
1281
1297
|
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
|
|
1298
|
+
global __globalUnavailableHosts
|
|
1299
|
+
global _no_env
|
|
1283
1300
|
threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
|
|
1284
1301
|
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
1302
|
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 +1309,11 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
1292
1309
|
# update the unavailable hosts and global unavailable hosts
|
|
1293
1310
|
if willUpdateUnreachableHosts:
|
|
1294
1311
|
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
|
-
|
|
1312
|
+
__globalUnavailableHosts.update(unavailableHosts)
|
|
1313
|
+
# update the os environment variable if not _no_env
|
|
1314
|
+
if not _no_env:
|
|
1315
|
+
os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
|
|
1316
|
+
|
|
1296
1317
|
# print the output, if the output of multiple hosts are the same, we aggragate them
|
|
1297
1318
|
if not called:
|
|
1298
1319
|
print_output(hosts,json,greppable=greppable)
|
|
@@ -1357,7 +1378,8 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1357
1378
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1358
1379
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1359
1380
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1360
|
-
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
|
|
1381
|
+
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
|
|
1382
|
+
shortend = False):
|
|
1361
1383
|
hosts = hosts if type(hosts) == str else frozenset(hosts)
|
|
1362
1384
|
hostStr = formHostStr(hosts)
|
|
1363
1385
|
files = frozenset(files) if files else None
|
|
@@ -1375,7 +1397,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1375
1397
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1376
1398
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1377
1399
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1378
|
-
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY):
|
|
1400
|
+
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False):
|
|
1379
1401
|
f'''
|
|
1380
1402
|
Run the command on the hosts, aka multissh. main function
|
|
1381
1403
|
|
|
@@ -1394,6 +1416,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1394
1416
|
interface_ip_prefix (str, optional): The prefix of the IPMI interface. Defaults to {DEFAULT_INTERFACE_IP_PREFIX}.
|
|
1395
1417
|
returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {_DEFAULT_RETURN_UNFINISHED}.
|
|
1396
1418
|
scp (bool, optional): Whether to use scp instead of rsync. Defaults to {DEFAULT_SCP}.
|
|
1419
|
+
gather_mode (bool, optional): Whether to use gather mode. Defaults to False.
|
|
1397
1420
|
username (str, optional): The username to use to connect to the hosts. Defaults to {DEFAULT_USERNAME}.
|
|
1398
1421
|
extraargs (str, optional): Extra arguments to pass to the ssh / rsync / scp command. Defaults to {DEFAULT_EXTRA_ARGS}.
|
|
1399
1422
|
skipUnreachable (bool, optional): Whether to skip unreachable hosts. Defaults to {DEFAULT_SKIP_UNREACHABLE}.
|
|
@@ -1406,12 +1429,22 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1406
1429
|
min_line_len (int, optional): The minimum line number for each window of the curses output. Defaults to {DEFAULT_CURSES_MINIMUM_LINE_LEN}.
|
|
1407
1430
|
single_window (bool, optional): Whether to use a single window for the curses output. Defaults to {DEFAULT_SINGLE_WINDOW}.
|
|
1408
1431
|
file_sync (bool, optional): Whether to use file sync mode to sync directories. Defaults to {DEFAULT_FILE_SYNC}.
|
|
1432
|
+
error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
|
|
1433
|
+
quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
|
|
1409
1434
|
|
|
1410
1435
|
Returns:
|
|
1411
1436
|
list: A list of Host objects
|
|
1412
1437
|
'''
|
|
1413
|
-
global
|
|
1438
|
+
global __globalUnavailableHosts
|
|
1414
1439
|
global __global_suppress_printout
|
|
1440
|
+
global _no_env
|
|
1441
|
+
global _emo
|
|
1442
|
+
_emo = False
|
|
1443
|
+
_no_env = no_env
|
|
1444
|
+
if not no_env and '__multiSSH3_UNAVAILABLE_HOSTS' in os.environ:
|
|
1445
|
+
__globalUnavailableHosts = set(os.environ['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
|
|
1446
|
+
elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
|
|
1447
|
+
__globalUnavailableHosts = set(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
|
|
1415
1448
|
if not max_connections:
|
|
1416
1449
|
max_connections = 4 * os.cpu_count()
|
|
1417
1450
|
elif max_connections == 0:
|
|
@@ -1420,7 +1453,15 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1420
1453
|
max_connections = (-max_connections) * os.cpu_count()
|
|
1421
1454
|
if not commands:
|
|
1422
1455
|
commands = []
|
|
1423
|
-
|
|
1456
|
+
else:
|
|
1457
|
+
commands = [commands] if type(commands) == str else commands
|
|
1458
|
+
# reformat commands into a list of strings, join the iterables if they are not strings
|
|
1459
|
+
try:
|
|
1460
|
+
commands = [' '.join(command) if not type(command) == str else command for command in commands]
|
|
1461
|
+
except:
|
|
1462
|
+
pass
|
|
1463
|
+
print(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.")
|
|
1464
|
+
#verify_ssh_config()
|
|
1424
1465
|
# load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
|
|
1425
1466
|
if called:
|
|
1426
1467
|
# if called,
|
|
@@ -1429,18 +1470,18 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1429
1470
|
if skipUnreachable is None:
|
|
1430
1471
|
skipUnreachable = True
|
|
1431
1472
|
if skipUnreachable:
|
|
1432
|
-
unavailableHosts =
|
|
1473
|
+
unavailableHosts = __globalUnavailableHosts
|
|
1433
1474
|
else:
|
|
1434
1475
|
unavailableHosts = set()
|
|
1435
1476
|
else:
|
|
1436
1477
|
# if run in command line ( or emulating running in command line, we default to skip unreachable hosts within one command call )
|
|
1437
1478
|
if skipUnreachable:
|
|
1438
|
-
unavailableHosts =
|
|
1479
|
+
unavailableHosts = __globalUnavailableHosts
|
|
1439
1480
|
else:
|
|
1440
1481
|
unavailableHosts = set()
|
|
1441
1482
|
skipUnreachable = True
|
|
1442
|
-
|
|
1443
|
-
|
|
1483
|
+
if quiet:
|
|
1484
|
+
__global_suppress_printout = True
|
|
1444
1485
|
# We create the hosts
|
|
1445
1486
|
hostStr = formHostStr(hosts)
|
|
1446
1487
|
skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
|
|
@@ -1459,8 +1500,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1459
1500
|
if '@' not in host:
|
|
1460
1501
|
skipHostStr[i] = userStr + host
|
|
1461
1502
|
skipHostStr = ','.join(skipHostStr)
|
|
1462
|
-
targetHostsList = expand_hostnames(frozenset(hostStr.split(','))
|
|
1463
|
-
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(','))
|
|
1503
|
+
targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
|
|
1504
|
+
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
|
|
1464
1505
|
if skipHostsList:
|
|
1465
1506
|
if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
|
|
1466
1507
|
if files and not commands:
|
|
@@ -1471,15 +1512,18 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1471
1512
|
files = set(files+commands) if files else set(commands)
|
|
1472
1513
|
if files:
|
|
1473
1514
|
# try to resolve files first (like * etc)
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1515
|
+
if not gather_mode:
|
|
1516
|
+
pathSet = set()
|
|
1517
|
+
for file in files:
|
|
1518
|
+
try:
|
|
1519
|
+
pathSet.update(glob.glob(file,include_hidden=True,recursive=True))
|
|
1520
|
+
except:
|
|
1521
|
+
pathSet.update(glob.glob(file,recursive=True))
|
|
1522
|
+
if not pathSet:
|
|
1523
|
+
print(f'Warning: No source files at {files} are found after resolving globs!')
|
|
1524
|
+
sys.exit(66)
|
|
1525
|
+
else:
|
|
1526
|
+
pathSet = set(files)
|
|
1483
1527
|
if file_sync:
|
|
1484
1528
|
# use abosolute path for file sync
|
|
1485
1529
|
commands = [os.path.abspath(file) for file in pathSet]
|
|
@@ -1598,6 +1642,7 @@ def get_default_config(args):
|
|
|
1598
1642
|
'DEFAULT_GREPPABLE_MODE': args.greppable,
|
|
1599
1643
|
'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
|
|
1600
1644
|
'DEFAULT_SKIP_HOSTS': args.skip_hosts,
|
|
1645
|
+
'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
|
|
1601
1646
|
'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
|
|
1602
1647
|
}
|
|
1603
1648
|
|
|
@@ -1605,14 +1650,15 @@ def write_default_config(args,CONFIG_FILE,backup = True):
|
|
|
1605
1650
|
if backup and os.path.exists(CONFIG_FILE):
|
|
1606
1651
|
os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
|
|
1607
1652
|
default_config = get_default_config(args)
|
|
1653
|
+
# apply the updated defualt_config to __configs_from_file and write that to file
|
|
1654
|
+
__configs_from_file.update(default_config)
|
|
1608
1655
|
with open(CONFIG_FILE,'w') as f:
|
|
1609
|
-
json.dump(
|
|
1656
|
+
json.dump(__configs_from_file,f,indent=4)
|
|
1610
1657
|
|
|
1611
1658
|
|
|
1612
1659
|
def main():
|
|
1613
1660
|
global _emo
|
|
1614
1661
|
global __global_suppress_printout
|
|
1615
|
-
global __gloablUnavailableHosts
|
|
1616
1662
|
global __mainReturnCode
|
|
1617
1663
|
global __failedHosts
|
|
1618
1664
|
global __ipmiiInterfaceIPPrefix
|
|
@@ -1624,13 +1670,13 @@ def main():
|
|
|
1624
1670
|
# We parse the arguments
|
|
1625
1671
|
parser = argparse.ArgumentParser(description=f'Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file: {CONFIG_FILE}')
|
|
1626
1672
|
parser.add_argument('hosts', metavar='hosts', type=str, nargs='?', help=f'Hosts to run the command on, use "," to seperate hosts. (default: {DEFAULT_HOSTS})',default=DEFAULT_HOSTS)
|
|
1627
|
-
parser.add_argument('commands', metavar='commands', type=str, nargs='
|
|
1673
|
+
parser.add_argument('commands', metavar='commands', type=str, nargs='+',default=None,help='the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host name.')
|
|
1628
1674
|
parser.add_argument('-u','--username', type=str,help=f'The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified. (default: {DEFAULT_USERNAME})',default=DEFAULT_USERNAME)
|
|
1629
1675
|
parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
|
|
1630
1676
|
parser.add_argument('-ea','--extraargs',type=str,help=f'Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: {DEFAULT_EXTRA_ARGS})',default=DEFAULT_EXTRA_ARGS)
|
|
1631
1677
|
parser.add_argument("-11",'--oneonone', action='store_true', help=f"Run one corresponding command on each host. (default: {DEFAULT_ONE_ON_ONE})", default=DEFAULT_ONE_ON_ONE)
|
|
1632
1678
|
parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
|
|
1633
|
-
parser.add_argument('--file_sync', action='store_true', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
|
|
1679
|
+
parser.add_argument('-fs','--file_sync', action='store_true', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
|
|
1634
1680
|
parser.add_argument('--scp', action='store_true', help=f'Use scp for copying files instead of rsync. Need to use this on windows. (default: {DEFAULT_SCP})', default=DEFAULT_SCP)
|
|
1635
1681
|
parser.add_argument('-gm','--gather_mode', action='store_true', help=f'Gather files from the hosts instead of sending files to the hosts. Will send remote files specified in <FILE> to local path specified in <COMMANDS> (default: False)', default=False)
|
|
1636
1682
|
#parser.add_argument("-d",'-c',"--destination", type=str, help="The destination of the files. Same as specify with commands. Added for compatibility. Use #HOST# or #HOSTNAME# to replace the host name in the destination")
|
|
@@ -1646,22 +1692,22 @@ def main():
|
|
|
1646
1692
|
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
1693
|
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
1694
|
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)
|
|
1695
|
+
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)
|
|
1696
|
+
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
1697
|
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
1698
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
1653
1699
|
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
1700
|
parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
1655
1701
|
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
1702
|
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('--
|
|
1703
|
+
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
1704
|
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
1659
1705
|
|
|
1660
1706
|
# parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
|
|
1661
1707
|
# help='the user to use to connect to the hosts')
|
|
1662
1708
|
args = parser.parse_args()
|
|
1663
1709
|
|
|
1664
|
-
if args.
|
|
1710
|
+
if args.store_config_file:
|
|
1665
1711
|
try:
|
|
1666
1712
|
if os.path.exists(CONFIG_FILE):
|
|
1667
1713
|
print(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
|
|
@@ -1681,6 +1727,8 @@ def main():
|
|
|
1681
1727
|
except Exception as e:
|
|
1682
1728
|
print(f"Error while writing config file: {e}")
|
|
1683
1729
|
if not args.commands:
|
|
1730
|
+
with open(CONFIG_FILE,'r') as f:
|
|
1731
|
+
print(f"Config file content: \n{f.read()}")
|
|
1684
1732
|
sys.exit(0)
|
|
1685
1733
|
|
|
1686
1734
|
_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
|