multiSSH3 4.98__py3-none-any.whl → 4.99__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.98.dist-info → multiSSH3-4.99.dist-info}/METADATA +62 -30
- multiSSH3-4.99.dist-info/RECORD +7 -0
- {multiSSH3-4.98.dist-info → multiSSH3-4.99.dist-info}/WHEEL +1 -1
- multiSSH3.py +91 -24
- multiSSH3-4.98.dist-info/RECORD +0 -7
- {multiSSH3-4.98.dist-info → multiSSH3-4.99.dist-info}/LICENSE +0 -0
- {multiSSH3-4.98.dist-info → multiSSH3-4.99.dist-info}/entry_points.txt +0 -0
- {multiSSH3-4.98.dist-info → multiSSH3-4.99.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.99
|
|
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
|
|
@@ -38,7 +38,7 @@ multissh will read a config file located at ```/etc/multiSSH3.config.json```
|
|
|
38
38
|
To store / generate a config file with the current command line options, you can use
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
mssh --
|
|
41
|
+
mssh --store_config_file
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
You can modify the json file directly after generation and multissh will read from it for loading defaults.
|
|
@@ -47,10 +47,14 @@ Note:
|
|
|
47
47
|
|
|
48
48
|
If you want to store password, it will be a plain text password in this config file. This will be better to supply it everytime as a CLI argument but you should really consider setting up priv-pub key setup.
|
|
49
49
|
|
|
50
|
+
Also Note:
|
|
51
|
+
|
|
52
|
+
On some systems, scp / rsync will require you use a priv-pub key to work
|
|
53
|
+
|
|
50
54
|
This option can also be used to store cli options into the config files. For example.
|
|
51
55
|
|
|
52
56
|
```bash
|
|
53
|
-
mssh --ipmi_interface_ip_prefix 192 --
|
|
57
|
+
mssh --ipmi_interface_ip_prefix 192 --store_config_file
|
|
54
58
|
```
|
|
55
59
|
will store
|
|
56
60
|
```json
|
|
@@ -64,6 +68,22 @@ DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
|
|
|
64
68
|
```
|
|
65
69
|
as hostname aliases.
|
|
66
70
|
|
|
71
|
+
multissh3 will resolve hostname grouping by:
|
|
72
|
+
ipv4 address expansion > local hostname resolution ( like /etc/hosts ) > currrent terminal environment > env from env_file > remote hostname resolution ( socket.gethostbyname() )
|
|
73
|
+
|
|
74
|
+
An example hostname alias file will look like:
|
|
75
|
+
```bash
|
|
76
|
+
us_east='100.100.0.1-3,us_east_prod_[1-5]'
|
|
77
|
+
us_central=""
|
|
78
|
+
us_west="100.101.0.1-2,us_west_prod_[a-c]_[1-3]"
|
|
79
|
+
us="$us_east,$us_central,$us_west"
|
|
80
|
+
asia="100.90.0-1,1-9"
|
|
81
|
+
eu=''
|
|
82
|
+
rhel8="$asia,$us_east"
|
|
83
|
+
all="$us,$asia,$eu"
|
|
84
|
+
```
|
|
85
|
+
( You can use bash replacements for grouping. )
|
|
86
|
+
|
|
67
87
|
For example:
|
|
68
88
|
```bash
|
|
69
89
|
export all='192.168.1-2.1-64'
|
|
@@ -104,61 +124,73 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
|
|
|
104
124
|
Use ```mssh --help``` for more info.
|
|
105
125
|
|
|
106
126
|
```bash
|
|
107
|
-
usage: mssh [-h] [-u USERNAME] [-
|
|
108
|
-
[-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo]
|
|
109
|
-
[--
|
|
110
|
-
|
|
127
|
+
usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL]
|
|
128
|
+
[--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo]
|
|
129
|
+
[-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su] [-sh SKIP_HOSTS]
|
|
130
|
+
[--store_config_file] [--debug] [--copy-id] [-V]
|
|
131
|
+
[hosts] [commands ...]
|
|
111
132
|
|
|
112
|
-
Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command
|
|
133
|
+
Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file: /etc/multiSSH3.config.json
|
|
113
134
|
|
|
114
135
|
positional arguments:
|
|
115
|
-
hosts Hosts to run the command on, use "," to seperate hosts
|
|
116
|
-
commands the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host
|
|
136
|
+
hosts Hosts to run the command on, use "," to seperate hosts. (default: all)
|
|
137
|
+
commands the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host
|
|
138
|
+
name.
|
|
117
139
|
|
|
118
140
|
options:
|
|
119
141
|
-h, --help show this help message and exit
|
|
120
142
|
-u USERNAME, --username USERNAME
|
|
121
|
-
The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified.
|
|
122
|
-
|
|
123
|
-
Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default:
|
|
124
|
-
None)
|
|
143
|
+
The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified.
|
|
144
|
+
(default: None)
|
|
125
145
|
-p PASSWORD, --password PASSWORD
|
|
126
|
-
The password to use to connect to the hosts, (default:
|
|
146
|
+
The password to use to connect to the hosts, (default: )
|
|
147
|
+
-ea EXTRAARGS, --extraargs EXTRAARGS
|
|
148
|
+
Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex.
|
|
149
|
+
-ea="--delete" (default: None)
|
|
127
150
|
-11, --oneonone Run one corresponding command on each host. (default: False)
|
|
128
151
|
-f FILE, --file FILE The file to be copied to the hosts. Use -f multiple times to copy multiple files
|
|
129
|
-
--file_sync
|
|
130
|
-
and destination will be the same in this mode. (default: False)
|
|
152
|
+
-fs, --file_sync Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and
|
|
153
|
+
<COMMANDS> both as source as source and destination will be the same in this mode. (default: False)
|
|
131
154
|
--scp Use scp for copying files instead of rsync. Need to use this on windows. (default: False)
|
|
155
|
+
-gm, --gather_mode Gather files from the hosts instead of sending files to the hosts. Will send remote files specified in <FILE> to
|
|
156
|
+
local path specified in <COMMANDS> (default: False)
|
|
132
157
|
-t TIMEOUT, --timeout TIMEOUT
|
|
133
|
-
Timeout for each command in seconds (default:
|
|
158
|
+
Timeout for each command in seconds (default: 600 (disabled))
|
|
134
159
|
-r REPEAT, --repeat REPEAT
|
|
135
160
|
Repeat the command for a number of times (default: 1)
|
|
136
161
|
-i INTERVAL, --interval INTERVAL
|
|
137
162
|
Interval between repeats in seconds (default: 0)
|
|
138
163
|
--ipmi Use ipmitool to run the command. (default: False)
|
|
164
|
+
-mpre IPMI_INTERFACE_IP_PREFIX, --ipmi_interface_ip_prefix IPMI_INTERFACE_IP_PREFIX
|
|
165
|
+
The prefix of the IPMI interfaces (default: )
|
|
139
166
|
-pre INTERFACE_IP_PREFIX, --interface_ip_prefix INTERFACE_IP_PREFIX
|
|
140
167
|
The prefix of the for the interfaces (default: None)
|
|
141
|
-
-q,
|
|
168
|
+
-q, -nw, --nowatch, --quiet
|
|
169
|
+
Quiet mode, no curses watch, only print the output. (default: False)
|
|
142
170
|
-ww WINDOW_WIDTH, --window_width WINDOW_WIDTH
|
|
143
171
|
The minimum character length of the curses window. (default: 40)
|
|
144
172
|
-wh WINDOW_HEIGHT, --window_height WINDOW_HEIGHT
|
|
145
|
-
The minimum line height of the curses window. (default:
|
|
173
|
+
The minimum line height of the curses window. (default: 5)
|
|
146
174
|
-sw, --single_window Use a single window for all hosts. (default: False)
|
|
147
175
|
-eo, --error_only Only print the error output. (default: False)
|
|
148
|
-
-no, --
|
|
149
|
-
--no_env Do not load the environment variables. (default: False)
|
|
150
|
-
--env_file ENV_FILE The file to load the environment variables from. (default:
|
|
151
|
-
|
|
176
|
+
-no, --no_output Do not print the output. (default: False)
|
|
177
|
+
--no_env Do not load the command line environment variables. (default: False)
|
|
178
|
+
--env_file ENV_FILE The file to load the mssh file based environment variables from. ( Still work with --no_env ) (default:
|
|
179
|
+
/etc/profile.d/hosts.sh)
|
|
180
|
+
-m MAX_CONNECTIONS, --max_connections MAX_CONNECTIONS
|
|
152
181
|
Max number of connections to use (default: 4 * cpu_count)
|
|
153
182
|
-j, --json Output in json format. (default: False)
|
|
154
183
|
--success_hosts Output the hosts that succeeded in summary as wells. (default: False)
|
|
155
184
|
-g, --greppable Output in greppable format. (default: False)
|
|
156
|
-
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
185
|
+
-su, --skip_unreachable
|
|
186
|
+
Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple
|
|
187
|
+
command sequence will still auto skip unreachable hosts. (default: False)
|
|
188
|
+
-sh SKIP_HOSTS, --skip_hosts SKIP_HOSTS
|
|
189
|
+
Skip the hosts in the list. (default: None)
|
|
190
|
+
--store_config_file Store / generate the default config file from command line argument and current config at
|
|
191
|
+
/etc/multiSSH3.config.json
|
|
192
|
+
--debug Print debug information
|
|
193
|
+
--copy-id Copy the ssh id to the hosts
|
|
162
194
|
-V, --version show program's version number and exit
|
|
163
195
|
```
|
|
164
196
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
multiSSH3.py,sha256=MdhvFCCWxQCZkjqFcsCp9nwiYiB1jKg1_YLu6BBrqZ4,96689
|
|
2
|
+
multiSSH3-4.99.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
+
multiSSH3-4.99.dist-info/METADATA,sha256=cLbJhPOIt81YaMyAn5iOocyPvEST-3RRFP-uafmZm30,17517
|
|
4
|
+
multiSSH3-4.99.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
5
|
+
multiSSH3-4.99.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
6
|
+
multiSSH3-4.99.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
7
|
+
multiSSH3-4.99.dist-info/RECORD,,
|
multiSSH3.py
CHANGED
|
@@ -30,7 +30,7 @@ except AttributeError:
|
|
|
30
30
|
# If neither is available, use a dummy decorator
|
|
31
31
|
def cache_decorator(func):
|
|
32
32
|
return func
|
|
33
|
-
version = '4.
|
|
33
|
+
version = '4.99'
|
|
34
34
|
VERSION = version
|
|
35
35
|
|
|
36
36
|
CONFIG_FILE = '/etc/multiSSH3.config.json'
|
|
@@ -68,6 +68,9 @@ __build_in_default_config = {
|
|
|
68
68
|
'DEFAULT_HOSTS': 'all',
|
|
69
69
|
'DEFAULT_USERNAME': None,
|
|
70
70
|
'DEFAULT_PASSWORD': '',
|
|
71
|
+
'DEFAULT_IDENTITY_FILE': None,
|
|
72
|
+
'DEDAULT_SSH_KEY_SEARCH_PATH': '~/.ssh/',
|
|
73
|
+
'DEFAULT_USE_KEY': False,
|
|
71
74
|
'DEFAULT_EXTRA_ARGS': None,
|
|
72
75
|
'DEFAULT_ONE_ON_ONE': False,
|
|
73
76
|
'DEFAULT_SCP': False,
|
|
@@ -125,6 +128,9 @@ DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_conf
|
|
|
125
128
|
DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
|
|
126
129
|
DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
|
|
127
130
|
DEFAULT_PASSWORD = __configs_from_file.get('DEFAULT_PASSWORD', __build_in_default_config['DEFAULT_PASSWORD'])
|
|
131
|
+
DEFAULT_IDENTITY_FILE = __configs_from_file.get('DEFAULT_IDENTITY_FILE', __build_in_default_config['DEFAULT_IDENTITY_FILE'])
|
|
132
|
+
DEDAULT_SSH_KEY_SEARCH_PATH = __configs_from_file.get('DEDAULT_SSH_KEY_SEARCH_PATH', __build_in_default_config['DEDAULT_SSH_KEY_SEARCH_PATH'])
|
|
133
|
+
DEFAULT_USE_KEY = __configs_from_file.get('DEFAULT_USE_KEY', __build_in_default_config['DEFAULT_USE_KEY'])
|
|
128
134
|
DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
|
|
129
135
|
DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
|
|
130
136
|
DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
|
|
@@ -193,7 +199,7 @@ def get_i():
|
|
|
193
199
|
return __host_i_counter
|
|
194
200
|
|
|
195
201
|
class Host:
|
|
196
|
-
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False):
|
|
202
|
+
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None):
|
|
197
203
|
self.name = name # the name of the host (hostname or IP address)
|
|
198
204
|
self.command = command # the command to run on the host
|
|
199
205
|
self.returncode = None # the return code of the command
|
|
@@ -212,12 +218,13 @@ class Host:
|
|
|
212
218
|
# also store a globally unique integer i from 0
|
|
213
219
|
self.i = get_i()
|
|
214
220
|
self.uuid = uuid.uuid4()
|
|
221
|
+
self.identity_file = identity_file
|
|
215
222
|
|
|
216
223
|
def __iter__(self):
|
|
217
224
|
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
218
225
|
def __repr__(self):
|
|
219
226
|
# return the complete data structure
|
|
220
|
-
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid})"
|
|
227
|
+
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), identity_file={self.identity_file}"
|
|
221
228
|
def __str__(self):
|
|
222
229
|
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
|
|
223
230
|
|
|
@@ -255,7 +262,7 @@ def check_path(program_name):
|
|
|
255
262
|
return True
|
|
256
263
|
return False
|
|
257
264
|
|
|
258
|
-
[check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash']]
|
|
265
|
+
[check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash','ssh-copy-id']]
|
|
259
266
|
|
|
260
267
|
|
|
261
268
|
|
|
@@ -678,11 +685,13 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
678
685
|
global _binPaths
|
|
679
686
|
global __DEBUG_MODE
|
|
680
687
|
try:
|
|
681
|
-
|
|
682
|
-
|
|
688
|
+
localExtraArgs = []
|
|
689
|
+
|
|
683
690
|
if not SSH_STRICT_HOST_KEY_CHECKING:
|
|
684
|
-
|
|
685
|
-
|
|
691
|
+
localExtraArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
|
|
692
|
+
if host.identity_file:
|
|
693
|
+
localExtraArgs += ['-i',host.identity_file]
|
|
694
|
+
rsyncLocalExtraArgs = ['--rsh','ssh ' + ' '.join(localExtraArgs)]
|
|
686
695
|
host.username = None
|
|
687
696
|
host.address = host.name
|
|
688
697
|
if '@' in host.name:
|
|
@@ -783,11 +792,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
783
792
|
else:
|
|
784
793
|
fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
|
|
785
794
|
if useScp:
|
|
786
|
-
formatedCMD = [_binPaths['scp'],'-rpB'] +
|
|
795
|
+
formatedCMD = [_binPaths['scp'],'-rpB'] + localExtraArgs + extraargs +['--']+fileArgs
|
|
787
796
|
else:
|
|
788
|
-
formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] +
|
|
797
|
+
formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncLocalExtraArgs + extraargs +['--']+fileArgs
|
|
789
798
|
else:
|
|
790
|
-
formatedCMD = [_binPaths['ssh']] +
|
|
799
|
+
formatedCMD = [_binPaths['ssh']] + localExtraArgs + extraargs +['--']+ [host.resolvedName, host.command]
|
|
791
800
|
if passwds and 'sshpass' in _binPaths:
|
|
792
801
|
formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
|
|
793
802
|
elif passwds:
|
|
@@ -839,7 +848,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
|
|
|
839
848
|
|
|
840
849
|
proc.terminate()
|
|
841
850
|
break
|
|
842
|
-
elif time.time() - host.lastUpdateTime >
|
|
851
|
+
elif time.time() - host.lastUpdateTime > max(1, timeout // 2):
|
|
843
852
|
timeoutLine = f'Timeout in [{timeout - int(time.time() - host.lastUpdateTime)}] seconds!'
|
|
844
853
|
if host.output and not host.output[-1].strip().startswith(timeoutLine):
|
|
845
854
|
# remove last line if it is a countdown
|
|
@@ -1449,12 +1458,13 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
1449
1458
|
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
|
|
1450
1459
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1451
1460
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
|
|
1452
|
-
file_sync = False, error_only = DEFAULT_ERROR_ONLY,
|
|
1461
|
+
file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
1453
1462
|
shortend = False) -> str:
|
|
1454
1463
|
argsList = []
|
|
1455
1464
|
if oneonone: argsList.append('--oneonone' if not shortend else '-11')
|
|
1456
1465
|
if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
|
|
1457
1466
|
if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
|
|
1467
|
+
if identity_file and identity_file != DEFAULT_IDENTITY_FILE: argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
|
|
1458
1468
|
if nowatch: argsList.append('--nowatch' if not shortend else '-q')
|
|
1459
1469
|
if json: argsList.append('--json' if not shortend else '-j')
|
|
1460
1470
|
if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
|
|
@@ -1479,7 +1489,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1479
1489
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1480
1490
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1481
1491
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1482
|
-
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
|
|
1492
|
+
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
1483
1493
|
shortend = False):
|
|
1484
1494
|
hosts = hosts if type(hosts) == str else frozenset(hosts)
|
|
1485
1495
|
hostStr = formHostStr(hosts)
|
|
@@ -1488,7 +1498,8 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
1488
1498
|
nowatch = nowatch,json = json,max_connections=max_connections,
|
|
1489
1499
|
files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
|
|
1490
1500
|
username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
|
|
1491
|
-
greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only,
|
|
1501
|
+
greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, identity_file = identity_file,
|
|
1502
|
+
shortend = shortend)
|
|
1492
1503
|
commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
|
|
1493
1504
|
return f'multissh {argsStr} {hostStr} {commandStr}'
|
|
1494
1505
|
|
|
@@ -1498,7 +1509,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1498
1509
|
scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
|
|
1499
1510
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
|
|
1500
1511
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
1501
|
-
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False):
|
|
1512
|
+
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE):
|
|
1502
1513
|
f'''
|
|
1503
1514
|
Run the command on the hosts, aka multissh. main function
|
|
1504
1515
|
|
|
@@ -1532,6 +1543,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1532
1543
|
file_sync (bool, optional): Whether to use file sync mode to sync directories. Defaults to {DEFAULT_FILE_SYNC}.
|
|
1533
1544
|
error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
|
|
1534
1545
|
quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
|
|
1546
|
+
identity_file (str, optional): The identity file to use for the ssh connection. Defaults to {DEFAULT_IDENTITY_FILE}.
|
|
1535
1547
|
|
|
1536
1548
|
Returns:
|
|
1537
1549
|
list: A list of Host objects
|
|
@@ -1652,9 +1664,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1652
1664
|
continue
|
|
1653
1665
|
if host.strip() in skipHostsList: continue
|
|
1654
1666
|
if file_sync:
|
|
1655
|
-
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))
|
|
1667
|
+
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,identity_file=identity_file))
|
|
1656
1668
|
else:
|
|
1657
|
-
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1669
|
+
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
|
|
1658
1670
|
if not __global_suppress_printout:
|
|
1659
1671
|
eprint(f"Running command: {command} on host: {host}")
|
|
1660
1672
|
if not __global_suppress_printout: print('-'*80)
|
|
@@ -1678,7 +1690,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1678
1690
|
elif ipmi:
|
|
1679
1691
|
eprint(f"Error: ipmi mode is not supported in interactive mode")
|
|
1680
1692
|
else:
|
|
1681
|
-
hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
|
|
1693
|
+
hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,identity_file=identity_file))
|
|
1682
1694
|
if not __global_suppress_printout:
|
|
1683
1695
|
eprint('-'*80)
|
|
1684
1696
|
eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
@@ -1696,9 +1708,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
1696
1708
|
continue
|
|
1697
1709
|
if host.strip() in skipHostsList: continue
|
|
1698
1710
|
if file_sync:
|
|
1699
|
-
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))
|
|
1711
|
+
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,identity_file=identity_file))
|
|
1700
1712
|
else:
|
|
1701
|
-
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
|
|
1713
|
+
hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
|
|
1702
1714
|
if not __global_suppress_printout and len(commands) > 1:
|
|
1703
1715
|
eprint('-'*80)
|
|
1704
1716
|
eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
|
|
@@ -1723,6 +1735,9 @@ def get_default_config(args):
|
|
|
1723
1735
|
'DEFAULT_HOSTS': args.hosts,
|
|
1724
1736
|
'DEFAULT_USERNAME': args.username,
|
|
1725
1737
|
'DEFAULT_PASSWORD': args.password,
|
|
1738
|
+
'DEFAULT_IDENTITY_FILE': args.key if args.key and not os.path.isdir(args.key) else DEFAULT_IDENTITY_FILE,
|
|
1739
|
+
'DEDAULT_SSH_KEY_SEARCH_PATH': args.key if args.key and os.path.isdir(args.key) else DEDAULT_SSH_KEY_SEARCH_PATH,
|
|
1740
|
+
'DEFAULT_USE_KEY': args.use_key,
|
|
1726
1741
|
'DEFAULT_EXTRA_ARGS': args.extraargs,
|
|
1727
1742
|
'DEFAULT_ONE_ON_ONE': args.oneonone,
|
|
1728
1743
|
'DEFAULT_SCP': args.scp,
|
|
@@ -1761,6 +1776,26 @@ def write_default_config(args,CONFIG_FILE,backup = True):
|
|
|
1761
1776
|
with open(CONFIG_FILE,'w') as f:
|
|
1762
1777
|
json.dump(__configs_from_file,f,indent=4)
|
|
1763
1778
|
|
|
1779
|
+
def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
|
|
1780
|
+
'''
|
|
1781
|
+
Find the ssh public key file
|
|
1782
|
+
|
|
1783
|
+
Args:
|
|
1784
|
+
searchPath (str, optional): The path to search. Defaults to DEDAULT_SSH_KEY_SEARCH_PATH.
|
|
1785
|
+
|
|
1786
|
+
Returns:
|
|
1787
|
+
str: The path to the ssh key file
|
|
1788
|
+
'''
|
|
1789
|
+
if searchPath:
|
|
1790
|
+
sshKeyPath = searchPath
|
|
1791
|
+
else:
|
|
1792
|
+
sshKeyPath ='~/.ssh'
|
|
1793
|
+
possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
|
|
1794
|
+
for sshKeyFile in possibleSshKeyFiles:
|
|
1795
|
+
if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
|
|
1796
|
+
return os.path.join(sshKeyPath,sshKeyFile)
|
|
1797
|
+
return None
|
|
1798
|
+
|
|
1764
1799
|
|
|
1765
1800
|
def main():
|
|
1766
1801
|
global _emo
|
|
@@ -1780,6 +1815,8 @@ def main():
|
|
|
1780
1815
|
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.')
|
|
1781
1816
|
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)
|
|
1782
1817
|
parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
|
|
1818
|
+
parser.add_argument('-k','--key','--identity',nargs='?', type=str,help=f'The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a key. Use option without value to use {DEDAULT_SSH_KEY_SEARCH_PATH} (default: {DEFAULT_IDENTITY_FILE})',const=DEDAULT_SSH_KEY_SEARCH_PATH,default=DEFAULT_IDENTITY_FILE)
|
|
1819
|
+
parser.add_argument('-uk','--use_key', action='store_true', help=f'Attempt to use public key file to connect to the hosts. (default: {DEFAULT_USE_KEY})', default=DEFAULT_USE_KEY)
|
|
1783
1820
|
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)
|
|
1784
1821
|
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)
|
|
1785
1822
|
parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
|
|
@@ -1848,6 +1885,8 @@ def main():
|
|
|
1848
1885
|
eprint(f"Config file written to {CONFIG_FILE}")
|
|
1849
1886
|
except Exception as e:
|
|
1850
1887
|
eprint(f"Error while writing config file: {e}")
|
|
1888
|
+
import traceback
|
|
1889
|
+
eprint(traceback.format_exc())
|
|
1851
1890
|
if not args.commands:
|
|
1852
1891
|
with open(CONFIG_FILE,'r') as f:
|
|
1853
1892
|
eprint(f"Config file content: \n{f.read()}")
|
|
@@ -1870,7 +1909,35 @@ def main():
|
|
|
1870
1909
|
eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
|
|
1871
1910
|
else:
|
|
1872
1911
|
sys.exit(0)
|
|
1873
|
-
|
|
1912
|
+
|
|
1913
|
+
if args.key or args.use_key:
|
|
1914
|
+
if not args.key:
|
|
1915
|
+
args.key = find_ssh_key_file()
|
|
1916
|
+
else:
|
|
1917
|
+
if os.path.isdir(os.path.expanduser(args.key)):
|
|
1918
|
+
args.key = find_ssh_key_file(args.key)
|
|
1919
|
+
elif not os.path.exists(args.key):
|
|
1920
|
+
eprint(f"Warning: Identity file {args.key} not found. Passing to ssh anyway. Proceed with caution.")
|
|
1921
|
+
|
|
1922
|
+
if args.copy_id:
|
|
1923
|
+
if 'ssh-copy-id' in _binPaths:
|
|
1924
|
+
# we will copy the id to the hosts
|
|
1925
|
+
for host in formHostStr(args.hosts).split(','):
|
|
1926
|
+
command = f"{_binPaths['ssh-copy-id']} "
|
|
1927
|
+
if args.key:
|
|
1928
|
+
command = f"{command}-i {args.key} "
|
|
1929
|
+
if args.username:
|
|
1930
|
+
command = f"{command} {args.username}@"
|
|
1931
|
+
command = f"{command}{host}"
|
|
1932
|
+
if args.password and 'sshpass' in _binPaths:
|
|
1933
|
+
command = f"{_binPaths['sshpass']} -p {args.password} {command}"
|
|
1934
|
+
eprint(f"> {command}")
|
|
1935
|
+
os.system(command)
|
|
1936
|
+
else:
|
|
1937
|
+
eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
|
|
1938
|
+
if not args.commands:
|
|
1939
|
+
sys.exit(0)
|
|
1940
|
+
|
|
1874
1941
|
__ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
|
|
1875
1942
|
|
|
1876
1943
|
if not args.greppable and not args.json and not args.no_output:
|
|
@@ -1881,7 +1948,7 @@ def main():
|
|
|
1881
1948
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
1882
1949
|
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,
|
|
1883
1950
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
1884
|
-
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only))
|
|
1951
|
+
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key))
|
|
1885
1952
|
if args.error_only:
|
|
1886
1953
|
__global_suppress_printout = True
|
|
1887
1954
|
|
|
@@ -1896,7 +1963,7 @@ def main():
|
|
|
1896
1963
|
nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
|
|
1897
1964
|
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,
|
|
1898
1965
|
extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
|
|
1899
|
-
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only)
|
|
1966
|
+
curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key)
|
|
1900
1967
|
#print('*'*80)
|
|
1901
1968
|
|
|
1902
1969
|
if not __global_suppress_printout: eprint('-'*80)
|
multiSSH3-4.98.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=Dm7lML6_oR8yedBdlZCsR-J7Et2Xh5r149y1V666Avw,93039
|
|
2
|
-
multiSSH3-4.98.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
-
multiSSH3-4.98.dist-info/METADATA,sha256=hkcvqm3j6w0i24rVinCJMhoxax1btQbHoCaqG-SnJx0,16043
|
|
4
|
-
multiSSH3-4.98.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
5
|
-
multiSSH3-4.98.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
6
|
-
multiSSH3-4.98.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
7
|
-
multiSSH3-4.98.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|