multiSSH3 4.81__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.81.dist-info → multiSSH3-4.89.dist-info}/METADATA +31 -51
- multiSSH3-4.89.dist-info/RECORD +7 -0
- multiSSH3.py +292 -176
- multiSSH3-4.81.dist-info/RECORD +0 -7
- {multiSSH3-4.81.dist-info → multiSSH3-4.89.dist-info}/LICENSE +0 -0
- {multiSSH3-4.81.dist-info → multiSSH3-4.89.dist-info}/WHEEL +0 -0
- {multiSSH3-4.81.dist-info → multiSSH3-4.89.dist-info}/entry_points.txt +0 -0
- {multiSSH3-4.81.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]
|
|
@@ -164,8 +174,6 @@ Following document is generated courtesy of Mr.ChatGPT-o1 Preview:
|
|
|
164
174
|
- [multissh](#multissh)
|
|
165
175
|
- [Table of Contents](#table-of-contents)
|
|
166
176
|
- [Features](#features)
|
|
167
|
-
- [Installation](#installation)
|
|
168
|
-
- [Usage](#usage)
|
|
169
177
|
- [Basic Syntax](#basic-syntax)
|
|
170
178
|
- [Command-Line Options](#command-line-options)
|
|
171
179
|
- [Examples](#examples)
|
|
@@ -194,36 +202,10 @@ Following document is generated courtesy of Mr.ChatGPT-o1 Preview:
|
|
|
194
202
|
- **Interactive Mode**: Run interactive commands with curses-based UI for monitoring.
|
|
195
203
|
- **Quiet Mode**: Suppress output for cleaner automation scripts.
|
|
196
204
|
|
|
197
|
-
## Installation
|
|
198
|
-
|
|
199
|
-
1. **Clone the Repository**
|
|
200
|
-
|
|
201
|
-
```bash
|
|
202
|
-
git clone https://github.com/yourusername/multissh.git
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
2. **Navigate to the Directory**
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
cd multissh
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
3. **Make the Script Executable**
|
|
212
|
-
|
|
213
|
-
```bash
|
|
214
|
-
chmod +x multissh.py
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
4. **Install Dependencies**
|
|
218
|
-
|
|
219
|
-
Ensure you have Python 3 and the required modules installed. You may need to install `curses` and `ipaddress` modules if they are not already available.
|
|
220
|
-
|
|
221
|
-
## Usage
|
|
222
|
-
|
|
223
205
|
### Basic Syntax
|
|
224
206
|
|
|
225
207
|
```bash
|
|
226
|
-
|
|
208
|
+
mssh [options] <hosts> <commands>
|
|
227
209
|
```
|
|
228
210
|
|
|
229
211
|
- `<hosts>`: Comma-separated list of target hosts. Supports ranges and wildcards.
|
|
@@ -267,7 +249,7 @@ Following document is generated courtesy of Mr.ChatGPT-o1 Preview:
|
|
|
267
249
|
### Running a Command on Multiple Hosts
|
|
268
250
|
|
|
269
251
|
```bash
|
|
270
|
-
|
|
252
|
+
mssh "host1,host2,host3" "uptime"
|
|
271
253
|
```
|
|
272
254
|
|
|
273
255
|
This command runs `uptime` on `host1`, `host2`, and `host3`.
|
|
@@ -275,7 +257,7 @@ This command runs `uptime` on `host1`, `host2`, and `host3`.
|
|
|
275
257
|
### Copying Files to Multiple Hosts
|
|
276
258
|
|
|
277
259
|
```bash
|
|
278
|
-
|
|
260
|
+
mssh -f "/path/to/local/file.txt" "host1,host2,host3" "/remote/path/"
|
|
279
261
|
```
|
|
280
262
|
|
|
281
263
|
This command copies `file.txt` to `/remote/path/` on the specified hosts.
|
|
@@ -283,7 +265,7 @@ This command copies `file.txt` to `/remote/path/` on the specified hosts.
|
|
|
283
265
|
### Using Hostname Ranges
|
|
284
266
|
|
|
285
267
|
```bash
|
|
286
|
-
|
|
268
|
+
mssh "host[01-05]" "hostname"
|
|
287
269
|
```
|
|
288
270
|
|
|
289
271
|
This expands to `host01`, `host02`, `host03`, `host04`, `host05` and runs `hostname` on each.
|
|
@@ -291,7 +273,7 @@ This expands to `host01`, `host02`, `host03`, `host04`, `host05` and runs `hostn
|
|
|
291
273
|
### Using IPMI
|
|
292
274
|
|
|
293
275
|
```bash
|
|
294
|
-
|
|
276
|
+
mssh --ipmi "192.168.1.[100-105]" "chassis power status"
|
|
295
277
|
```
|
|
296
278
|
|
|
297
279
|
Runs `ipmitool chassis power status` on the specified IPMI interfaces.
|
|
@@ -299,7 +281,7 @@ Runs `ipmitool chassis power status` on the specified IPMI interfaces.
|
|
|
299
281
|
### Using Password Authentication
|
|
300
282
|
|
|
301
283
|
```bash
|
|
302
|
-
|
|
284
|
+
mssh -p "yourpassword" "host1,host2" "whoami"
|
|
303
285
|
```
|
|
304
286
|
|
|
305
287
|
Uses `sshpass` to provide the password for SSH authentication.
|
|
@@ -307,7 +289,7 @@ Uses `sshpass` to provide the password for SSH authentication.
|
|
|
307
289
|
### Skipping Unreachable Hosts
|
|
308
290
|
|
|
309
291
|
```bash
|
|
310
|
-
|
|
292
|
+
mssh -su "host1,host2,host3" "date"
|
|
311
293
|
```
|
|
312
294
|
|
|
313
295
|
Skips hosts that are unreachable during execution.
|
|
@@ -315,7 +297,7 @@ Skips hosts that are unreachable during execution.
|
|
|
315
297
|
### JSON Output
|
|
316
298
|
|
|
317
299
|
```bash
|
|
318
|
-
|
|
300
|
+
mssh -j "host1,host2" "uname -a"
|
|
319
301
|
```
|
|
320
302
|
|
|
321
303
|
Outputs the results in JSON format, suitable for parsing.
|
|
@@ -323,7 +305,7 @@ Outputs the results in JSON format, suitable for parsing.
|
|
|
323
305
|
### Quiet Mode
|
|
324
306
|
|
|
325
307
|
```bash
|
|
326
|
-
|
|
308
|
+
mssh -q "host1,host2" "ls /nonexistent"
|
|
327
309
|
```
|
|
328
310
|
|
|
329
311
|
Suppresses all output, useful for scripts where you only care about exit codes.
|
|
@@ -335,8 +317,6 @@ Suppresses all output, useful for scripts where you only care about exit codes.
|
|
|
335
317
|
- Use `--no_env` to prevent loading any environment variables from files.
|
|
336
318
|
|
|
337
319
|
## Notes
|
|
338
|
-
|
|
339
|
-
- **SSH Configuration**: The script modifies `~/.ssh/config` to disable `StrictHostKeyChecking`. Ensure this is acceptable in your environment.
|
|
340
320
|
- **Dependencies**: Requires Python 3, `sshpass` (if using password authentication), and standard Unix utilities like `ssh`, `scp`, and `rsync`.
|
|
341
321
|
- **Signal Handling**: Supports graceful termination with `Ctrl+C`.
|
|
342
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
|
@@ -15,7 +15,9 @@ import io
|
|
|
15
15
|
import signal
|
|
16
16
|
import functools
|
|
17
17
|
import glob
|
|
18
|
-
|
|
18
|
+
import shutil
|
|
19
|
+
import getpass
|
|
20
|
+
|
|
19
21
|
try:
|
|
20
22
|
# Check if functiools.cache is available
|
|
21
23
|
cache_decorator = functools.cache
|
|
@@ -27,7 +29,7 @@ except AttributeError:
|
|
|
27
29
|
# If neither is available, use a dummy decorator
|
|
28
30
|
def cache_decorator(func):
|
|
29
31
|
return func
|
|
30
|
-
version = '4.
|
|
32
|
+
version = '4.89'
|
|
31
33
|
VERSION = version
|
|
32
34
|
|
|
33
35
|
CONFIG_FILE = '/etc/multiSSH3.config.json'
|
|
@@ -81,19 +83,27 @@ __build_in_default_config = {
|
|
|
81
83
|
'DEFAULT_GREPPABLE_MODE': False,
|
|
82
84
|
'DEFAULT_SKIP_UNREACHABLE': False,
|
|
83
85
|
'DEFAULT_SKIP_HOSTS': '',
|
|
86
|
+
'SSH_STRICT_HOST_KEY_CHECKING': False,
|
|
84
87
|
'ERROR_MESSAGES_TO_IGNORE': [
|
|
85
88
|
'Pseudo-terminal will not be allocated because stdin is not a terminal',
|
|
86
89
|
'Connection to .* closed',
|
|
87
90
|
'Warning: Permanently added',
|
|
88
91
|
'mux_client_request_session',
|
|
89
92
|
'disabling multiplexing',
|
|
93
|
+
'Killed by signal',
|
|
94
|
+
'Connection reset by peer',
|
|
90
95
|
],
|
|
91
96
|
'_DEFAULT_CALLED': True,
|
|
92
97
|
'_DEFAULT_RETURN_UNFINISHED': False,
|
|
93
98
|
'_DEFAULT_UPDATE_UNREACHABLE_HOSTS': True,
|
|
94
99
|
'_DEFAULT_NO_START': False,
|
|
95
100
|
'_etc_hosts': {},
|
|
96
|
-
'
|
|
101
|
+
'_sshpassPath': None,
|
|
102
|
+
'_sshPath': None,
|
|
103
|
+
'_scpPath': None,
|
|
104
|
+
'_ipmitoolPath': None,
|
|
105
|
+
'_rsyncPath': None,
|
|
106
|
+
'_bashPath': None,
|
|
97
107
|
'__ERROR_MESSAGES_TO_IGNORE_REGEX':None,
|
|
98
108
|
}
|
|
99
109
|
|
|
@@ -131,6 +141,8 @@ DEFAULT_GREPPABLE_MODE = __configs_from_file.get('DEFAULT_GREPPABLE_MODE', __bui
|
|
|
131
141
|
DEFAULT_SKIP_UNREACHABLE = __configs_from_file.get('DEFAULT_SKIP_UNREACHABLE', __build_in_default_config['DEFAULT_SKIP_UNREACHABLE'])
|
|
132
142
|
DEFAULT_SKIP_HOSTS = __configs_from_file.get('DEFAULT_SKIP_HOSTS', __build_in_default_config['DEFAULT_SKIP_HOSTS'])
|
|
133
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
|
+
|
|
134
146
|
ERROR_MESSAGES_TO_IGNORE = __configs_from_file.get('ERROR_MESSAGES_TO_IGNORE', __build_in_default_config['ERROR_MESSAGES_TO_IGNORE'])
|
|
135
147
|
|
|
136
148
|
_DEFAULT_CALLED = __configs_from_file.get('_DEFAULT_CALLED', __build_in_default_config['_DEFAULT_CALLED'])
|
|
@@ -139,7 +151,8 @@ _DEFAULT_UPDATE_UNREACHABLE_HOSTS = __configs_from_file.get('_DEFAULT_UPDATE_UNR
|
|
|
139
151
|
_DEFAULT_NO_START = __configs_from_file.get('_DEFAULT_NO_START', __build_in_default_config['_DEFAULT_NO_START'])
|
|
140
152
|
|
|
141
153
|
# form the regex from the list
|
|
142
|
-
|
|
154
|
+
__ERROR_MESSAGES_TO_IGNORE_REGEX = __configs_from_file.get('__ERROR_MESSAGES_TO_IGNORE_REGEX', __build_in_default_config['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
|
|
155
|
+
if __ERROR_MESSAGES_TO_IGNORE_REGEX:
|
|
143
156
|
print('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
|
|
144
157
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__configs_from_file['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
|
|
145
158
|
else:
|
|
@@ -152,7 +165,7 @@ __global_suppress_printout = True
|
|
|
152
165
|
__mainReturnCode = 0
|
|
153
166
|
__failedHosts = set()
|
|
154
167
|
class Host:
|
|
155
|
-
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None):
|
|
168
|
+
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False):
|
|
156
169
|
self.name = name # the name of the host (hostname or IP address)
|
|
157
170
|
self.command = command # the command to run on the host
|
|
158
171
|
self.returncode = None # the return code of the command
|
|
@@ -164,19 +177,24 @@ class Host:
|
|
|
164
177
|
self.ipmi = ipmi # whether to use ipmi to connect to the host
|
|
165
178
|
self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
|
|
166
179
|
self.scp = scp # whether to use scp to copy files to the host
|
|
180
|
+
self.gatherMode = gatherMode # whether the host is in gather mode
|
|
167
181
|
self.extraargs = extraargs # extra arguments to be passed to ssh
|
|
168
182
|
self.resolvedName = None # the resolved IP address of the host
|
|
169
183
|
def __iter__(self):
|
|
170
184
|
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
171
185
|
def __repr__(self):
|
|
172
186
|
# return the complete data structure
|
|
173
|
-
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}, extraargs={self.extraargs}, resolvedName={self.resolvedName})"
|
|
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})"
|
|
174
188
|
def __str__(self):
|
|
175
189
|
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
|
|
176
190
|
|
|
177
191
|
__wildCharacters = ['*','?','x']
|
|
178
192
|
|
|
179
|
-
|
|
193
|
+
_no_env = DEFAULT_NO_ENV
|
|
194
|
+
|
|
195
|
+
_env_file = DEFAULT_ENV_FILE
|
|
196
|
+
|
|
197
|
+
__globalUnavailableHosts = set()
|
|
180
198
|
|
|
181
199
|
__ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
|
|
182
200
|
|
|
@@ -186,15 +204,26 @@ _emo = False
|
|
|
186
204
|
|
|
187
205
|
_etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
|
|
188
206
|
|
|
189
|
-
_env_file = DEFAULT_ENV_FILE
|
|
190
207
|
|
|
191
208
|
# check if command sshpass is available
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
209
|
+
_binPaths = {}
|
|
210
|
+
def check_path(program_name):
|
|
211
|
+
global __configs_from_file
|
|
212
|
+
global __build_in_default_config
|
|
213
|
+
global _binPaths
|
|
214
|
+
config_key = f'_{program_name}Path'
|
|
215
|
+
program_path = (
|
|
216
|
+
__configs_from_file.get(config_key) or
|
|
217
|
+
__build_in_default_config.get(config_key) or
|
|
218
|
+
shutil.which(program_name)
|
|
219
|
+
)
|
|
220
|
+
if program_path:
|
|
221
|
+
_binPaths[program_name] = program_path
|
|
222
|
+
return True
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
[check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash']]
|
|
226
|
+
|
|
198
227
|
|
|
199
228
|
|
|
200
229
|
@cache_decorator
|
|
@@ -242,37 +271,6 @@ def expandIPv4Address(hosts):
|
|
|
242
271
|
expandedHosts.extend(expandedHost)
|
|
243
272
|
return expandedHosts
|
|
244
273
|
|
|
245
|
-
@cache_decorator
|
|
246
|
-
def readEnvFromFile(environemnt_file = ''):
|
|
247
|
-
'''
|
|
248
|
-
Read the environment variables from env_file
|
|
249
|
-
Returns:
|
|
250
|
-
dict: A dictionary of environment variables
|
|
251
|
-
'''
|
|
252
|
-
global env
|
|
253
|
-
try:
|
|
254
|
-
if env:
|
|
255
|
-
return env
|
|
256
|
-
except:
|
|
257
|
-
env = {}
|
|
258
|
-
global _env_file
|
|
259
|
-
if environemnt_file:
|
|
260
|
-
envf = environemnt_file
|
|
261
|
-
else:
|
|
262
|
-
envf = _env_file if _env_file else DEFAULT_ENV_FILE
|
|
263
|
-
if os.path.exists(envf):
|
|
264
|
-
with open(envf,'r') as f:
|
|
265
|
-
for line in f:
|
|
266
|
-
if line.startswith('#') or not line.strip():
|
|
267
|
-
continue
|
|
268
|
-
key, value = line.replace('export ', '', 1).strip().split('=', 1)
|
|
269
|
-
key = key.strip().strip('"').strip("'")
|
|
270
|
-
value = value.strip().strip('"').strip("'")
|
|
271
|
-
# avoid infinite recursion
|
|
272
|
-
if key != value:
|
|
273
|
-
env[key] = value.strip('"').strip("'")
|
|
274
|
-
return env
|
|
275
|
-
|
|
276
274
|
@cache_decorator
|
|
277
275
|
def getIP(hostname,local=False):
|
|
278
276
|
'''
|
|
@@ -313,9 +311,40 @@ def getIP(hostname,local=False):
|
|
|
313
311
|
return socket.gethostbyname(hostname)
|
|
314
312
|
except:
|
|
315
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
|
|
316
345
|
|
|
317
346
|
@cache_decorator
|
|
318
|
-
def expand_hostname(text,validate=True
|
|
347
|
+
def expand_hostname(text,validate=True):
|
|
319
348
|
'''
|
|
320
349
|
Expand the hostname range in the text.
|
|
321
350
|
Will search the string for a range ( [] encloused and non enclosed number ranges).
|
|
@@ -336,12 +365,12 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
336
365
|
hostname = expandinghosts.pop()
|
|
337
366
|
match = re.search(r'\[(.*?-.*?)\]', hostname)
|
|
338
367
|
if not match:
|
|
339
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
368
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
340
369
|
continue
|
|
341
370
|
try:
|
|
342
371
|
range_start, range_end = match.group(1).split('-')
|
|
343
372
|
except ValueError:
|
|
344
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
373
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
345
374
|
continue
|
|
346
375
|
range_start = range_start.strip()
|
|
347
376
|
range_end = range_end.strip()
|
|
@@ -353,7 +382,7 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
353
382
|
elif range_start.isalpha() and range_start.isupper():
|
|
354
383
|
range_end = 'Z'
|
|
355
384
|
else:
|
|
356
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
385
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
357
386
|
continue
|
|
358
387
|
if not range_start:
|
|
359
388
|
if range_end.isdigit():
|
|
@@ -363,7 +392,7 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
363
392
|
elif range_end.isalpha() and range_end.isupper():
|
|
364
393
|
range_start = 'A'
|
|
365
394
|
else:
|
|
366
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
395
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
367
396
|
continue
|
|
368
397
|
if range_start.isdigit() and range_end.isdigit():
|
|
369
398
|
padding_length = min(len(range_start), len(range_end))
|
|
@@ -373,14 +402,14 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
373
402
|
if '[' in hostname:
|
|
374
403
|
expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
|
|
375
404
|
else:
|
|
376
|
-
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])
|
|
377
406
|
else:
|
|
378
407
|
if all(c in string.hexdigits for c in range_start + range_end):
|
|
379
408
|
for i in range(int(range_start, 16), int(range_end, 16)+1):
|
|
380
409
|
if '[' in hostname:
|
|
381
410
|
expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
|
|
382
411
|
else:
|
|
383
|
-
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])
|
|
384
413
|
else:
|
|
385
414
|
try:
|
|
386
415
|
start_index = alphanumeric.index(range_start)
|
|
@@ -389,13 +418,13 @@ def expand_hostname(text,validate=True,no_env=False):
|
|
|
389
418
|
if '[' in hostname:
|
|
390
419
|
expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
|
|
391
420
|
else:
|
|
392
|
-
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])
|
|
393
422
|
except ValueError:
|
|
394
|
-
expandedhosts.update(validate_expand_hostname(hostname
|
|
423
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
395
424
|
return expandedhosts
|
|
396
425
|
|
|
397
426
|
@cache_decorator
|
|
398
|
-
def expand_hostnames(hosts
|
|
427
|
+
def expand_hostnames(hosts):
|
|
399
428
|
'''
|
|
400
429
|
Expand the hostnames in the hosts list
|
|
401
430
|
|
|
@@ -424,17 +453,17 @@ def expand_hostnames(hosts,no_env=False):
|
|
|
424
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):
|
|
425
454
|
hostSetToAdd = sorted(expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
|
|
426
455
|
else:
|
|
427
|
-
hostSetToAdd = sorted(expand_hostname(host
|
|
456
|
+
hostSetToAdd = sorted(expand_hostname(host))
|
|
428
457
|
if username:
|
|
429
458
|
# we expand the username
|
|
430
|
-
username = sorted(expand_hostname(username,validate=False
|
|
459
|
+
username = sorted(expand_hostname(username,validate=False))
|
|
431
460
|
# we combine the username and hostname
|
|
432
461
|
hostSetToAdd = [u+'@'+h for u,h in product(username,hostSetToAdd)]
|
|
433
462
|
expandedhosts.extend(hostSetToAdd)
|
|
434
463
|
return expandedhosts
|
|
435
464
|
|
|
436
465
|
@cache_decorator
|
|
437
|
-
def validate_expand_hostname(hostname
|
|
466
|
+
def validate_expand_hostname(hostname):
|
|
438
467
|
'''
|
|
439
468
|
Validate the hostname and expand it if it is a range of IP addresses
|
|
440
469
|
|
|
@@ -444,17 +473,18 @@ def validate_expand_hostname(hostname,no_env=False):
|
|
|
444
473
|
Returns:
|
|
445
474
|
list: A list of valid hostnames
|
|
446
475
|
'''
|
|
476
|
+
global _no_env
|
|
447
477
|
# maybe it is just defined in ./target_files/hosts.sh and exported to the environment
|
|
448
478
|
# we will try to get the valid host name from the environment
|
|
449
479
|
hostname = hostname.strip('$')
|
|
450
480
|
if getIP(hostname,local=True):
|
|
451
481
|
return [hostname]
|
|
452
|
-
elif not
|
|
482
|
+
elif not _no_env and hostname in os.environ:
|
|
453
483
|
# we will expand these hostnames again
|
|
454
|
-
return expand_hostnames(frozenset(os.environ[hostname].split(','))
|
|
484
|
+
return expand_hostnames(frozenset(os.environ[hostname].split(',')))
|
|
455
485
|
elif hostname in readEnvFromFile():
|
|
456
486
|
# we will expand these hostnames again
|
|
457
|
-
return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(','))
|
|
487
|
+
return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
|
|
458
488
|
elif getIP(hostname,local=False):
|
|
459
489
|
return [hostname]
|
|
460
490
|
else:
|
|
@@ -582,64 +612,129 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
582
612
|
global _emo
|
|
583
613
|
global __ERROR_MESSAGES_TO_IGNORE_REGEX
|
|
584
614
|
global __ipmiiInterfaceIPPrefix
|
|
585
|
-
global
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
extraargs = host.extraargs.split()
|
|
602
|
-
else:
|
|
603
|
-
extraargs = []
|
|
604
|
-
if __ipmiiInterfaceIPPrefix:
|
|
605
|
-
host.interface_ip_prefix = __ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
|
|
606
|
-
if host.interface_ip_prefix:
|
|
607
|
-
try:
|
|
608
|
-
hostOctets = getIP(host.address,local=False).split('.')
|
|
609
|
-
prefixOctets = host.interface_ip_prefix.split('.')
|
|
610
|
-
host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
|
|
611
|
-
host.resolvedName = host.username + '@' if host.username else ''
|
|
612
|
-
host.resolvedName += host.address
|
|
613
|
-
except:
|
|
614
|
-
host.resolvedName = host.name
|
|
615
|
+
global _binPaths
|
|
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']
|
|
622
|
+
host.username = None
|
|
623
|
+
host.address = host.name
|
|
624
|
+
if '@' in host.name:
|
|
625
|
+
host.username, host.address = host.name.rsplit('@',1)
|
|
626
|
+
if "#HOST#" in host.command.upper() or "#HOSTNAME#" in host.command.upper():
|
|
627
|
+
host.command = host.command.replace("#HOST#",host.address).replace("#HOSTNAME#",host.address).replace("#host#",host.address).replace("#hostname#",host.address)
|
|
628
|
+
if "#USER#" in host.command.upper() or "#USERNAME#" in host.command.upper():
|
|
629
|
+
if host.username:
|
|
630
|
+
host.command = host.command.replace("#USER#",host.username).replace("#USERNAME#",host.username).replace("#user#",host.username).replace("#username#",host.username)
|
|
615
631
|
else:
|
|
632
|
+
current_user = getpass.getuser()
|
|
633
|
+
host.command = host.command.replace("#USER#",current_user).replace("#USERNAME#",current_user).replace("#user#",current_user).replace("#username#",current_user)
|
|
634
|
+
formatedCMD = []
|
|
635
|
+
if host.extraargs and type(host.extraargs) == str:
|
|
636
|
+
extraargs = host.extraargs.split()
|
|
637
|
+
elif host.extraargs and type(host.extraargs) == list:
|
|
638
|
+
extraargs = [str(arg) for arg in host.extraargs]
|
|
639
|
+
else:
|
|
640
|
+
extraargs = []
|
|
641
|
+
if __ipmiiInterfaceIPPrefix:
|
|
642
|
+
host.interface_ip_prefix = __ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
|
|
643
|
+
if host.interface_ip_prefix:
|
|
644
|
+
try:
|
|
645
|
+
hostOctets = getIP(host.address,local=False).split('.')
|
|
646
|
+
prefixOctets = host.interface_ip_prefix.split('.')
|
|
647
|
+
host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
|
|
648
|
+
host.resolvedName = host.username + '@' if host.username else ''
|
|
649
|
+
host.resolvedName += host.address
|
|
650
|
+
except:
|
|
616
651
|
host.resolvedName = host.name
|
|
617
|
-
|
|
652
|
+
else:
|
|
653
|
+
host.resolvedName = host.name
|
|
654
|
+
if host.ipmi:
|
|
655
|
+
if 'ipmitool' in _binPaths:
|
|
618
656
|
if host.command.startswith('ipmitool '):
|
|
619
657
|
host.command = host.command.replace('ipmitool ','')
|
|
658
|
+
elif host.command.startswith(_binPaths['ipmitool']):
|
|
659
|
+
host.command = host.command.replace(_binPaths['ipmitool'],'')
|
|
620
660
|
if not host.username:
|
|
621
661
|
host.username = 'admin'
|
|
622
|
-
if
|
|
623
|
-
|
|
662
|
+
if 'bash' in _binPaths:
|
|
663
|
+
if passwds:
|
|
664
|
+
formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
|
|
665
|
+
else:
|
|
666
|
+
formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} {" ".join(extraargs)} {host.command}']
|
|
624
667
|
else:
|
|
625
|
-
|
|
668
|
+
if passwds:
|
|
669
|
+
formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P {passwds}'] + extraargs + [host.command]
|
|
670
|
+
else:
|
|
671
|
+
formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}'] + extraargs + [host.command]
|
|
672
|
+
elif 'ssh' in _binPaths:
|
|
673
|
+
host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
|
|
674
|
+
host.ipmi = False
|
|
675
|
+
host.interface_ip_prefix = None
|
|
676
|
+
host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
|
|
677
|
+
ssh_command(host,sem,timeout,passwds)
|
|
678
|
+
return
|
|
626
679
|
else:
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
680
|
+
host.output.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
|
|
681
|
+
host.stderr.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
|
|
682
|
+
host.returncode = 1
|
|
683
|
+
return
|
|
684
|
+
else:
|
|
685
|
+
if host.files:
|
|
686
|
+
if host.scp:
|
|
687
|
+
if 'scp' in _binPaths:
|
|
688
|
+
useScp = True
|
|
689
|
+
elif 'rsync' in _binPaths:
|
|
690
|
+
host.output.append('scp not found on the local machine! Trying to use rsync...')
|
|
691
|
+
useScp = False
|
|
630
692
|
else:
|
|
631
|
-
|
|
693
|
+
host.output.append('scp not found on the local machine! Please install scp or rsync to use file sync mode.')
|
|
694
|
+
host.stderr.append('scp not found on the local machine! Please install scp or rsync to use file sync mode.')
|
|
695
|
+
host.returncode = 1
|
|
696
|
+
return
|
|
697
|
+
elif 'rsync' in _binPaths:
|
|
698
|
+
useScp = False
|
|
699
|
+
elif 'scp' in _binPaths:
|
|
700
|
+
host.output.append('rsync not found on the local machine! Trying to use scp...')
|
|
701
|
+
useScp = True
|
|
632
702
|
else:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
703
|
+
host.output.append('rsync not found on the local machine! Please install rsync or scp to use file sync mode.')
|
|
704
|
+
host.stderr.append('rsync not found on the local machine! Please install rsync or scp to use file sync mode.')
|
|
705
|
+
host.returncode = 1
|
|
706
|
+
return
|
|
707
|
+
if host.gatherMode:
|
|
708
|
+
fileArgs = [f'{host.resolvedName}:{file}' for file in host.files] + [host.command]
|
|
709
|
+
else:
|
|
710
|
+
fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
|
|
711
|
+
if useScp:
|
|
712
|
+
formatedCMD = [_binPaths['scp'],'-rpB'] + keyCheckArgs + extraargs +['--']+fileArgs
|
|
713
|
+
else:
|
|
714
|
+
formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncKeyCheckArgs + extraargs +['--']+fileArgs
|
|
715
|
+
else:
|
|
716
|
+
formatedCMD = [_binPaths['ssh']] + keyCheckArgs + extraargs +['--']+ [host.resolvedName, host.command]
|
|
717
|
+
if passwds and 'sshpass' in _binPaths:
|
|
718
|
+
formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
|
|
719
|
+
elif passwds:
|
|
720
|
+
host.output.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
|
|
721
|
+
#host.stderr.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
|
|
722
|
+
host.output.append('Please provide password via live input or use ssh key authentication.')
|
|
723
|
+
# # try to send the password via __keyPressesIn
|
|
724
|
+
# __keyPressesIn[-1] = list(passwds) + ['\n']
|
|
725
|
+
# __keyPressesIn.append([])
|
|
726
|
+
except Exception as e:
|
|
727
|
+
import traceback
|
|
728
|
+
host.output.append(f'Error occurred while formatting the command : {host.command}!')
|
|
729
|
+
host.stderr.append(f'Error occurred while formatting the command : {host.command}!')
|
|
730
|
+
host.stderr.extend(str(e).split('\n'))
|
|
731
|
+
host.output.extend(str(e).split('\n'))
|
|
732
|
+
host.stderr.extend(traceback.format_exc().split('\n'))
|
|
733
|
+
host.output.extend(traceback.format_exc().split('\n'))
|
|
734
|
+
host.returncode = -1
|
|
735
|
+
return
|
|
736
|
+
with sem:
|
|
737
|
+
try:
|
|
643
738
|
host.output.append('Running command: '+' '.join(formatedCMD))
|
|
644
739
|
#host.stdout = []
|
|
645
740
|
proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
|
|
@@ -733,7 +828,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
733
828
|
host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
|
|
734
829
|
ssh_command(host,sem,timeout,passwds)
|
|
735
830
|
# If transfering files, we will try again using scp if rsync connection is not successful
|
|
736
|
-
if host.files and not host.scp and host.returncode != 0 and host.stderr:
|
|
831
|
+
if host.files and not host.scp and not useScp and host.returncode != 0 and host.stderr:
|
|
737
832
|
host.stderr = []
|
|
738
833
|
host.stdout = []
|
|
739
834
|
host.output.append('Rsync connection failed! Trying SCP connection...')
|
|
@@ -1143,34 +1238,34 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1143
1238
|
print(rtnStr)
|
|
1144
1239
|
return rtnStr
|
|
1145
1240
|
|
|
1146
|
-
sshConfigged = False
|
|
1147
|
-
def verify_ssh_config():
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
|
1174
1269
|
|
|
1175
1270
|
def signal_handler(sig, frame):
|
|
1176
1271
|
'''
|
|
@@ -1194,9 +1289,9 @@ def signal_handler(sig, frame):
|
|
|
1194
1289
|
os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
1195
1290
|
sys.exit(0)
|
|
1196
1291
|
|
|
1197
|
-
|
|
1198
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):
|
|
1199
|
-
global
|
|
1293
|
+
global __globalUnavailableHosts
|
|
1294
|
+
global _no_env
|
|
1200
1295
|
threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
|
|
1201
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:
|
|
1202
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)
|
|
@@ -1209,7 +1304,11 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
|
|
|
1209
1304
|
# update the unavailable hosts and global unavailable hosts
|
|
1210
1305
|
if willUpdateUnreachableHosts:
|
|
1211
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!'))])
|
|
1212
|
-
|
|
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
|
+
|
|
1213
1312
|
# print the output, if the output of multiple hosts are the same, we aggragate them
|
|
1214
1313
|
if not called:
|
|
1215
1314
|
print_output(hosts,json,greppable=greppable)
|
|
@@ -1242,7 +1341,7 @@ def formHostStr(host) -> str:
|
|
|
1242
1341
|
def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
1243
1342
|
nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
1244
1343
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
|
|
1245
|
-
scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1344
|
+
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1246
1345
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
|
|
1247
1346
|
file_sync = False, error_only = DEFAULT_ERROR_ONLY,
|
|
1248
1347
|
shortend = False) -> str:
|
|
@@ -1257,6 +1356,7 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
1257
1356
|
if ipmi: argsList.append('--ipmi')
|
|
1258
1357
|
if interface_ip_prefix and interface_ip_prefix != DEFAULT_INTERFACE_IP_PREFIX: argsList.append(f'--interface_ip_prefix="{interface_ip_prefix}"' if not shortend else f'-pre="{interface_ip_prefix}"')
|
|
1259
1358
|
if scp: argsList.append('--scp')
|
|
1359
|
+
if gather_mode: argsList.append('--gather_mode' if not shortend else '-gm')
|
|
1260
1360
|
if username and username != DEFAULT_USERNAME: argsList.append(f'--username="{username}"' if not shortend else f'-u="{username}"')
|
|
1261
1361
|
if extraargs and extraargs != DEFAULT_EXTRA_ARGS: argsList.append(f'--extraargs="{extraargs}"' if not shortend else f'-ea="{extraargs}"')
|
|
1262
1362
|
if skipUnreachable: argsList.append('--skipUnreachable' if not shortend else '-su')
|
|
@@ -1270,7 +1370,7 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
1270
1370
|
def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
1271
1371
|
nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
1272
1372
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
|
|
1273
|
-
scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1373
|
+
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1274
1374
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1275
1375
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1276
1376
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, shortend = False):
|
|
@@ -1279,7 +1379,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1279
1379
|
files = frozenset(files) if files else None
|
|
1280
1380
|
argsStr = __formCommandArgStr(oneonone = oneonone, timeout = timeout,password = password,
|
|
1281
1381
|
nowatch = nowatch,json = json,max_connections=max_connections,
|
|
1282
|
-
files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,
|
|
1382
|
+
files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
|
|
1283
1383
|
username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
|
|
1284
1384
|
greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, shortend = shortend)
|
|
1285
1385
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
@@ -1288,7 +1388,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1288
1388
|
def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
|
|
1289
1389
|
nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
|
|
1290
1390
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
|
|
1291
|
-
scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1391
|
+
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1292
1392
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1293
1393
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1294
1394
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY):
|
|
@@ -1310,6 +1410,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1310
1410
|
interface_ip_prefix (str, optional): The prefix of the IPMI interface. Defaults to {DEFAULT_INTERFACE_IP_PREFIX}.
|
|
1311
1411
|
returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {_DEFAULT_RETURN_UNFINISHED}.
|
|
1312
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.
|
|
1313
1414
|
username (str, optional): The username to use to connect to the hosts. Defaults to {DEFAULT_USERNAME}.
|
|
1314
1415
|
extraargs (str, optional): Extra arguments to pass to the ssh / rsync / scp command. Defaults to {DEFAULT_EXTRA_ARGS}.
|
|
1315
1416
|
skipUnreachable (bool, optional): Whether to skip unreachable hosts. Defaults to {DEFAULT_SKIP_UNREACHABLE}.
|
|
@@ -1326,8 +1427,16 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1326
1427
|
Returns:
|
|
1327
1428
|
list: A list of Host objects
|
|
1328
1429
|
'''
|
|
1329
|
-
global
|
|
1430
|
+
global __globalUnavailableHosts
|
|
1330
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(','))
|
|
1331
1440
|
if not max_connections:
|
|
1332
1441
|
max_connections = 4 * os.cpu_count()
|
|
1333
1442
|
elif max_connections == 0:
|
|
@@ -1336,7 +1445,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1336
1445
|
max_connections = (-max_connections) * os.cpu_count()
|
|
1337
1446
|
if not commands:
|
|
1338
1447
|
commands = []
|
|
1339
|
-
verify_ssh_config()
|
|
1448
|
+
#verify_ssh_config()
|
|
1340
1449
|
# load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
|
|
1341
1450
|
if called:
|
|
1342
1451
|
# if called,
|
|
@@ -1345,18 +1454,17 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1345
1454
|
if skipUnreachable is None:
|
|
1346
1455
|
skipUnreachable = True
|
|
1347
1456
|
if skipUnreachable:
|
|
1348
|
-
unavailableHosts =
|
|
1457
|
+
unavailableHosts = __globalUnavailableHosts
|
|
1349
1458
|
else:
|
|
1350
1459
|
unavailableHosts = set()
|
|
1351
1460
|
else:
|
|
1352
1461
|
# if run in command line ( or emulating running in command line, we default to skip unreachable hosts within one command call )
|
|
1353
1462
|
if skipUnreachable:
|
|
1354
|
-
unavailableHosts =
|
|
1463
|
+
unavailableHosts = __globalUnavailableHosts
|
|
1355
1464
|
else:
|
|
1356
1465
|
unavailableHosts = set()
|
|
1357
1466
|
skipUnreachable = True
|
|
1358
|
-
|
|
1359
|
-
_emo = False
|
|
1467
|
+
|
|
1360
1468
|
# We create the hosts
|
|
1361
1469
|
hostStr = formHostStr(hosts)
|
|
1362
1470
|
skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
|
|
@@ -1375,8 +1483,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1375
1483
|
if '@' not in host:
|
|
1376
1484
|
skipHostStr[i] = userStr + host
|
|
1377
1485
|
skipHostStr = ','.join(skipHostStr)
|
|
1378
|
-
targetHostsList = expand_hostnames(frozenset(hostStr.split(','))
|
|
1379
|
-
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(','))
|
|
1486
|
+
targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
|
|
1487
|
+
skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
|
|
1380
1488
|
if skipHostsList:
|
|
1381
1489
|
if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
|
|
1382
1490
|
if files and not commands:
|
|
@@ -1387,15 +1495,18 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1387
1495
|
files = set(files+commands) if files else set(commands)
|
|
1388
1496
|
if files:
|
|
1389
1497
|
# try to resolve files first (like * etc)
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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)
|
|
1399
1510
|
if file_sync:
|
|
1400
1511
|
# use abosolute path for file sync
|
|
1401
1512
|
commands = [os.path.abspath(file) for file in pathSet]
|
|
@@ -1418,9 +1529,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1418
1529
|
continue
|
|
1419
1530
|
if host.strip() in skipHostsList: continue
|
|
1420
1531
|
if file_sync:
|
|
1421
|
-
hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
|
|
1532
|
+
hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1422
1533
|
else:
|
|
1423
|
-
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
|
|
1534
|
+
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1424
1535
|
if not __global_suppress_printout:
|
|
1425
1536
|
print(f"Running command: {command} on host: {host}")
|
|
1426
1537
|
if not __global_suppress_printout: print('-'*80)
|
|
@@ -1462,9 +1573,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1462
1573
|
continue
|
|
1463
1574
|
if host.strip() in skipHostsList: continue
|
|
1464
1575
|
if file_sync:
|
|
1465
|
-
hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
|
|
1576
|
+
hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1466
1577
|
else:
|
|
1467
|
-
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
|
|
1578
|
+
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1468
1579
|
if not __global_suppress_printout and len(commands) > 1:
|
|
1469
1580
|
print('-'*80)
|
|
1470
1581
|
print(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
@@ -1514,6 +1625,7 @@ def get_default_config(args):
|
|
|
1514
1625
|
'DEFAULT_GREPPABLE_MODE': args.greppable,
|
|
1515
1626
|
'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
|
|
1516
1627
|
'DEFAULT_SKIP_HOSTS': args.skip_hosts,
|
|
1628
|
+
'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
|
|
1517
1629
|
'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
|
|
1518
1630
|
}
|
|
1519
1631
|
|
|
@@ -1521,18 +1633,19 @@ def write_default_config(args,CONFIG_FILE,backup = True):
|
|
|
1521
1633
|
if backup and os.path.exists(CONFIG_FILE):
|
|
1522
1634
|
os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
|
|
1523
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)
|
|
1524
1638
|
with open(CONFIG_FILE,'w') as f:
|
|
1525
|
-
json.dump(
|
|
1639
|
+
json.dump(__configs_from_file,f,indent=4)
|
|
1526
1640
|
|
|
1527
1641
|
|
|
1528
1642
|
def main():
|
|
1529
1643
|
global _emo
|
|
1530
1644
|
global __global_suppress_printout
|
|
1531
|
-
global __gloablUnavailableHosts
|
|
1532
1645
|
global __mainReturnCode
|
|
1533
1646
|
global __failedHosts
|
|
1534
1647
|
global __ipmiiInterfaceIPPrefix
|
|
1535
|
-
global
|
|
1648
|
+
global _binPaths
|
|
1536
1649
|
global _env_file
|
|
1537
1650
|
_emo = False
|
|
1538
1651
|
# We handle the signal
|
|
@@ -1548,6 +1661,7 @@ def main():
|
|
|
1548
1661
|
parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
|
|
1549
1662
|
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)
|
|
1550
1663
|
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)
|
|
1664
|
+
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)
|
|
1551
1665
|
#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")
|
|
1552
1666
|
parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds (default: {DEFAULT_CLI_TIMEOUT} (disabled))", default=DEFAULT_CLI_TIMEOUT)
|
|
1553
1667
|
parser.add_argument("-r","--repeat", type=int, help=f"Repeat the command for a number of times (default: {DEFAULT_REPEAT})", default=DEFAULT_REPEAT)
|
|
@@ -1561,22 +1675,22 @@ def main():
|
|
|
1561
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)
|
|
1562
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)
|
|
1563
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)
|
|
1564
|
-
parser.add_argument('--no_env', action='store_true', help=f'Do not load the environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
|
|
1565
|
-
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)
|
|
1566
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)
|
|
1567
1681
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
1568
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)
|
|
1569
1683
|
parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
|
|
1570
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)
|
|
1571
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)
|
|
1572
|
-
parser.add_argument('--
|
|
1573
|
-
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version}
|
|
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}')
|
|
1687
|
+
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
|
|
1574
1688
|
|
|
1575
1689
|
# parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
|
|
1576
1690
|
# help='the user to use to connect to the hosts')
|
|
1577
1691
|
args = parser.parse_args()
|
|
1578
1692
|
|
|
1579
|
-
if args.
|
|
1693
|
+
if args.store_config_file:
|
|
1580
1694
|
try:
|
|
1581
1695
|
if os.path.exists(CONFIG_FILE):
|
|
1582
1696
|
print(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
|
|
@@ -1596,6 +1710,8 @@ def main():
|
|
|
1596
1710
|
except Exception as e:
|
|
1597
1711
|
print(f"Error while writing config file: {e}")
|
|
1598
1712
|
if not args.commands:
|
|
1713
|
+
with open(CONFIG_FILE,'r') as f:
|
|
1714
|
+
print(f"Config file content: \n{f.read()}")
|
|
1599
1715
|
sys.exit(0)
|
|
1600
1716
|
|
|
1601
1717
|
_env_file = args.env_file
|
|
@@ -1623,7 +1739,7 @@ def main():
|
|
|
1623
1739
|
if not __global_suppress_printout:
|
|
1624
1740
|
print('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1625
1741
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
1626
|
-
files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,username=args.username,
|
|
1742
|
+
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,
|
|
1627
1743
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
1628
1744
|
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only))
|
|
1629
1745
|
if args.error_only:
|
|
@@ -1638,7 +1754,7 @@ def main():
|
|
|
1638
1754
|
hosts = run_command_on_hosts(args.hosts,args.commands,
|
|
1639
1755
|
oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1640
1756
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
1641
|
-
files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,username=args.username,
|
|
1757
|
+
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,
|
|
1642
1758
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
1643
1759
|
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only)
|
|
1644
1760
|
#print('*'*80)
|
multiSSH3-4.81.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=Nerzo1GexP3GoZv0uhjydtu2zSpw4vY2k4Rx0F3yD8M,81707
|
|
2
|
-
multiSSH3-4.81.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
-
multiSSH3-4.81.dist-info/METADATA,sha256=GnSUBBxKq9cvqK4Z2cMVsOqWMgjqL_vpEO5XOZ0mNV8,16515
|
|
4
|
-
multiSSH3-4.81.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
5
|
-
multiSSH3-4.81.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
6
|
-
multiSSH3-4.81.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
7
|
-
multiSSH3-4.81.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|