sshreader 6.2.0__py3-none-any.whl → 7.0.0__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.
- sshreader/__init__.py +1 -1
- sshreader/customtypes.py +10 -10
- sshreader/scripts/__init__.py +1 -1
- sshreader/scripts/pydsh.py +245 -108
- sshreader/ssh.py +65 -27
- sshreader/utils.py +184 -104
- sshreader-7.0.0.dist-info/METADATA +131 -0
- sshreader-7.0.0.dist-info/RECORD +12 -0
- {sshreader-6.2.0.dist-info → sshreader-7.0.0.dist-info}/WHEEL +1 -1
- sshreader-6.2.0.dist-info/METADATA +0 -86
- sshreader-6.2.0.dist-info/RECORD +0 -12
- {sshreader-6.2.0.dist-info → sshreader-7.0.0.dist-info}/entry_points.txt +0 -0
- {sshreader-6.2.0.dist-info → sshreader-7.0.0.dist-info}/licenses/LICENSE +0 -0
- {sshreader-6.2.0.dist-info → sshreader-7.0.0.dist-info}/licenses/LICENSE.lesser +0 -0
sshreader/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""A Python Package for parallelizing ssh connections via threading and multiprocessing.
|
|
2
2
|
"""
|
|
3
|
-
# Copyright (C) 2015-
|
|
3
|
+
# Copyright (C) 2015-2026 Jesse Almanrode
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU Lesser General Public License as published by
|
sshreader/customtypes.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
""" Typing module for sshreader
|
|
2
2
|
"""
|
|
3
|
-
# Copyright (C) 2015-
|
|
3
|
+
# Copyright (C) 2015-2026 Jesse Almanrode
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
@@ -14,23 +14,23 @@
|
|
|
14
14
|
#
|
|
15
15
|
# You should have received a copy of the GNU Lesser General Public License
|
|
16
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
-
from typing import NamedTuple
|
|
17
|
+
from typing import NamedTuple
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class Command(NamedTuple):
|
|
21
21
|
cmd: str
|
|
22
|
-
stdout:
|
|
23
|
-
stderr:
|
|
22
|
+
stdout: str | bytes | None
|
|
23
|
+
stderr: str | bytes | None
|
|
24
24
|
return_code: int
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class EnvVars(NamedTuple):
|
|
28
|
-
username:
|
|
29
|
-
agent_keys:
|
|
30
|
-
dsa_key:
|
|
31
|
-
ecdsa_key:
|
|
32
|
-
rsa_key:
|
|
28
|
+
username: str | None
|
|
29
|
+
agent_keys: list[object] | None
|
|
30
|
+
dsa_key: str | None
|
|
31
|
+
ecdsa_key: str | None
|
|
32
|
+
rsa_key: str | None
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
Timeout =
|
|
35
|
+
Timeout = int | float
|
|
36
36
|
TimeoutTuple = tuple[Timeout, Timeout]
|
sshreader/scripts/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
""" Scripts portion of sshreader package
|
|
2
2
|
"""
|
|
3
|
-
# Copyright (C) 2015-
|
|
3
|
+
# Copyright (C) 2015-2026 Jesse Almanrode
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU Lesser General Public License as published by
|
sshreader/scripts/pydsh.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
""" A Pythonic implementation of pdsh powered by sshreader
|
|
2
2
|
"""
|
|
3
|
-
# Copyright (C) 2015-
|
|
3
|
+
# Copyright (C) 2015-2026 Jesse Almanrode
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
@@ -19,7 +19,8 @@ import logging
|
|
|
19
19
|
import os
|
|
20
20
|
import sys
|
|
21
21
|
from collections import defaultdict
|
|
22
|
-
from
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Protocol
|
|
23
24
|
|
|
24
25
|
import click
|
|
25
26
|
from hostlist import collect_hostlist, expand_hostlist
|
|
@@ -28,7 +29,7 @@ import sshreader
|
|
|
28
29
|
|
|
29
30
|
# GLOBALS
|
|
30
31
|
__author__ = 'Jesse Almanrode'
|
|
31
|
-
__version__ = '
|
|
32
|
+
__version__ = '4.0.0'
|
|
32
33
|
__examples__ = """\b
|
|
33
34
|
Examples:
|
|
34
35
|
pydsh -w host1,host2,host3 "uname -r"
|
|
@@ -45,7 +46,24 @@ log.setLevel(logging.WARNING)
|
|
|
45
46
|
log.propagate = False # Keeps our messages out of the root logger.
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
def
|
|
49
|
+
def _job_output(job: sshreader.ServerJob) -> str:
|
|
50
|
+
result = job.results[0]
|
|
51
|
+
output = result.stderr if job.status == 255 else result.stdout
|
|
52
|
+
if output is None:
|
|
53
|
+
return ''
|
|
54
|
+
return output.decode() if isinstance(output, bytes) else output
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _SFTPUploader(Protocol):
|
|
58
|
+
def sftp_put(self, srcfile: str, dstfile: str) -> Any: ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _ScriptJob(Protocol):
|
|
62
|
+
_conn: _SFTPUploader
|
|
63
|
+
name: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def copy_script(scriptfile: str, job: _ScriptJob) -> bool:
|
|
49
67
|
""" Copy script to remote host using SFTP connection
|
|
50
68
|
|
|
51
69
|
:param scriptfile: Path to script file
|
|
@@ -56,23 +74,25 @@ def copy_script(scriptfile, job):
|
|
|
56
74
|
try:
|
|
57
75
|
job._conn.sftp_put(scriptfile, '/tmp/' + script_name)
|
|
58
76
|
except Exception as err:
|
|
59
|
-
|
|
77
|
+
log.exception('Failed to copy script %s to %s: %s', scriptfile, getattr(job, 'name', '<unknown>'), err)
|
|
78
|
+
return False
|
|
79
|
+
return True
|
|
60
80
|
|
|
61
81
|
|
|
62
|
-
def output(thisjob):
|
|
82
|
+
def output(thisjob: sshreader.ServerJob) -> None:
|
|
63
83
|
""" Print output from jobs as they complete in a format that could be piped to dshbak
|
|
64
84
|
|
|
65
85
|
:param thisjob: <ServerJob> object
|
|
66
86
|
:return: None
|
|
67
87
|
"""
|
|
68
|
-
result = thisjob
|
|
88
|
+
result = _job_output(thisjob)
|
|
69
89
|
if len(result) != 0:
|
|
70
90
|
for line in result.split('\n'):
|
|
71
91
|
sshreader.echo(str(thisjob.name) + ': ' + str(line))
|
|
72
92
|
return None
|
|
73
93
|
|
|
74
94
|
|
|
75
|
-
def dshbak(jobresults):
|
|
95
|
+
def dshbak(jobresults: list[sshreader.ServerJob]) -> None:
|
|
76
96
|
""" Output the results of the jobs grouped by hosts
|
|
77
97
|
|
|
78
98
|
Similar to piping output to `dshbak`
|
|
@@ -81,17 +101,14 @@ def dshbak(jobresults):
|
|
|
81
101
|
:return: None
|
|
82
102
|
"""
|
|
83
103
|
for thisjob in jobresults:
|
|
84
|
-
|
|
85
|
-
result = thisjob.results[0]
|
|
86
|
-
else:
|
|
87
|
-
result = thisjob.results[0].stdout
|
|
104
|
+
result = _job_output(thisjob)
|
|
88
105
|
if len(result) != 0:
|
|
89
106
|
click.echo(str('-' * 16) + '\n' + str(thisjob.name) + '\n' + str('-' * 16))
|
|
90
107
|
click.echo(result)
|
|
91
108
|
return None
|
|
92
109
|
|
|
93
110
|
|
|
94
|
-
def coalesce(jobresults):
|
|
111
|
+
def coalesce(jobresults: list[sshreader.ServerJob]) -> None:
|
|
95
112
|
""" Output the results of jobs coalescing identical output from hosts
|
|
96
113
|
|
|
97
114
|
Similar to piping output to `dshbak -c`
|
|
@@ -99,23 +116,150 @@ def coalesce(jobresults):
|
|
|
99
116
|
:param jobresults: List of <ServerJob> objects
|
|
100
117
|
:return: None
|
|
101
118
|
"""
|
|
102
|
-
|
|
103
|
-
output_hashes = dict()
|
|
119
|
+
output_groups: dict[str, list[str]] = defaultdict(list)
|
|
104
120
|
for job in jobresults:
|
|
105
|
-
|
|
106
|
-
md5sum = md5(result.encode()).hexdigest()
|
|
107
|
-
job_hashes[md5sum].append(job.name)
|
|
108
|
-
if md5sum not in output_hashes:
|
|
109
|
-
output_hashes[md5sum] = result
|
|
121
|
+
output_groups[_job_output(job)].append(job.name)
|
|
110
122
|
|
|
111
|
-
for
|
|
123
|
+
for stdout, hosts in output_groups.items():
|
|
112
124
|
if len(stdout) != 0:
|
|
113
|
-
click.echo(str('-' * 16) + '\n' + collect_hostlist(
|
|
125
|
+
click.echo(str('-' * 16) + '\n' + collect_hostlist(hosts) + '\n' + str('-' * 16))
|
|
114
126
|
click.echo(stdout)
|
|
115
127
|
return None
|
|
116
128
|
|
|
117
129
|
|
|
118
|
-
def
|
|
130
|
+
def setup_script_file(cmd: str, file_flag: bool, confirm_flag: bool) -> tuple[str | list[str], sshreader.Hook | None]:
|
|
131
|
+
""" Prepare script file for remote execution
|
|
132
|
+
|
|
133
|
+
Validates script file, extracts shebang, and creates pre-hook for copying.
|
|
134
|
+
Prompts for confirmation unless --confirm flag is set.
|
|
135
|
+
|
|
136
|
+
:param cmd: Command string or script path
|
|
137
|
+
:param file_flag: Boolean indicating if cmd is a file path
|
|
138
|
+
:param confirm_flag: Boolean indicating if confirmation should be skipped
|
|
139
|
+
:return: Tuple of (cmd_list, prehook) or (cmd, None) if not a file
|
|
140
|
+
"""
|
|
141
|
+
if not file_flag:
|
|
142
|
+
return cmd, None
|
|
143
|
+
|
|
144
|
+
script_path = Path(cmd)
|
|
145
|
+
if not script_path.exists() or not script_path.is_file():
|
|
146
|
+
raise click.BadParameter('Script file not found: ' + str(script_path))
|
|
147
|
+
|
|
148
|
+
script_name = script_path.name
|
|
149
|
+
log.info('Creating copy_script prehook for: ' + script_name)
|
|
150
|
+
prehook = sshreader.Hook(copy_script, args=[str(script_path)], ssh_established=True)
|
|
151
|
+
|
|
152
|
+
content = script_path.read_text(encoding='utf-8', errors='replace')
|
|
153
|
+
first_line = content.splitlines()[0] if content.splitlines() else ''
|
|
154
|
+
if not first_line.startswith('#!'):
|
|
155
|
+
raise click.UsageError('Script must start with #!')
|
|
156
|
+
|
|
157
|
+
# Request confirmation unless --confirm flag is set
|
|
158
|
+
if not confirm_flag:
|
|
159
|
+
click.echo(f'\nScript: {script_path.resolve()}')
|
|
160
|
+
click.echo(f'Interpreter: {first_line[2:].strip()}')
|
|
161
|
+
if not click.confirm('Execute this script on selected hosts?'):
|
|
162
|
+
raise click.Abort()
|
|
163
|
+
click.echo()
|
|
164
|
+
|
|
165
|
+
cmd_list = [first_line.split('#!').pop().strip() + ' /tmp/' + script_name, 'rm /tmp/' + script_name]
|
|
166
|
+
return cmd_list, prehook
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def create_jobs(hostlist: list[str], cmd: str | list[str], username: str, password: str | None,
|
|
170
|
+
keyfile: str | None, keypass: str | None, port: int, timeout: int, sha2: bool,
|
|
171
|
+
prehook: sshreader.Hook | None, posthook: sshreader.Hook | None,
|
|
172
|
+
file_flag: bool) -> list[sshreader.ServerJob]:
|
|
173
|
+
""" Create ServerJob objects for each host
|
|
174
|
+
|
|
175
|
+
:param hostlist: List of hosts from expanded hostlist expression
|
|
176
|
+
:param cmd: Command(s) to run on each host
|
|
177
|
+
:param username: SSH username
|
|
178
|
+
:param password: SSH password
|
|
179
|
+
:param keyfile: Path to SSH private key
|
|
180
|
+
:param keypass: Passphrase for private key
|
|
181
|
+
:param port: Default SSH port
|
|
182
|
+
:param timeout: SSH timeout in seconds
|
|
183
|
+
:param sha2: Use SHA2 for RSA keys
|
|
184
|
+
:param prehook: Pre-execution hook (if any)
|
|
185
|
+
:param posthook: Post-execution hook (if any)
|
|
186
|
+
:param file_flag: Boolean indicating if cmd is from a script file
|
|
187
|
+
:return: List of ServerJob objects
|
|
188
|
+
"""
|
|
189
|
+
jobs = []
|
|
190
|
+
for host in hostlist:
|
|
191
|
+
# Handle port specified in host string
|
|
192
|
+
if ':' in host:
|
|
193
|
+
log.info('SSH Port declared in host: ' + host)
|
|
194
|
+
host, port_str = host.split(':', 1)
|
|
195
|
+
try:
|
|
196
|
+
port = int(port_str)
|
|
197
|
+
except Exception:
|
|
198
|
+
raise click.BadParameter('Invalid port in host: ' + host) from Exception
|
|
199
|
+
log.debug((host, port))
|
|
200
|
+
else:
|
|
201
|
+
port = port # Use default port from args
|
|
202
|
+
|
|
203
|
+
# Create job with appropriate authentication
|
|
204
|
+
if keyfile:
|
|
205
|
+
job = sshreader.ServerJob(
|
|
206
|
+
host, cmd, username=username, keyfile=keyfile,
|
|
207
|
+
key_pass=keypass, combine_output=True, rsa_sha2=sha2,
|
|
208
|
+
timeout=(0.5, timeout)
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
job = sshreader.ServerJob(
|
|
212
|
+
host, cmd, username=username, password=password,
|
|
213
|
+
combine_output=True, rsa_sha2=sha2,
|
|
214
|
+
timeout=(0.5, timeout)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
job.ssh_port = port
|
|
218
|
+
if posthook:
|
|
219
|
+
log.info('Adding posthook to ServerJob for: ' + host)
|
|
220
|
+
job.post_hook = posthook
|
|
221
|
+
if file_flag and prehook:
|
|
222
|
+
log.info('Adding prehook to ServerJob for: ' + host)
|
|
223
|
+
job.pre_hook = prehook
|
|
224
|
+
jobs.append(job)
|
|
225
|
+
|
|
226
|
+
return jobs
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def execute_jobs(jobs: list[sshreader.ServerJob], dshbak_flag: bool, coalesce_flag: bool,
|
|
230
|
+
redline_flag: bool) -> list[sshreader.ServerJob]:
|
|
231
|
+
""" Execute jobs and handle output formatting
|
|
232
|
+
|
|
233
|
+
:param jobs: List of ServerJob objects
|
|
234
|
+
:param dshbak_flag: Format output like dshbak
|
|
235
|
+
:param coalesce_flag: Coalesce similar output
|
|
236
|
+
:param redline_flag: Run without thread/process overhead
|
|
237
|
+
:return: List of completed jobs
|
|
238
|
+
"""
|
|
239
|
+
log.info(f'Sending {len(jobs)} ServerJobs to sshreader module')
|
|
240
|
+
|
|
241
|
+
if dshbak_flag or coalesce_flag:
|
|
242
|
+
# With output aggregation, use progress bar
|
|
243
|
+
if redline_flag:
|
|
244
|
+
jobs_finished = sshreader.sshread(jobs, pcount=0, tcount=0, progress_bar=True)
|
|
245
|
+
else:
|
|
246
|
+
jobs_finished = sshreader.sshread(jobs, tcount=0, progress_bar=True)
|
|
247
|
+
|
|
248
|
+
if coalesce_flag:
|
|
249
|
+
coalesce(jobs_finished)
|
|
250
|
+
else:
|
|
251
|
+
dshbak(jobs_finished)
|
|
252
|
+
else:
|
|
253
|
+
# Without aggregation, output as jobs complete
|
|
254
|
+
if redline_flag:
|
|
255
|
+
sshreader.sshread(jobs, pcount=0, tcount=0, print_lock=True)
|
|
256
|
+
else:
|
|
257
|
+
sshreader.sshread(jobs, tcount=0, print_lock=True)
|
|
258
|
+
|
|
259
|
+
return jobs
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def validate_hostlist(ctx: click.Context | None, param: click.Parameter | str, value: str) -> list[str]:
|
|
119
263
|
""" Callback for click to expand hostlist expressions or error
|
|
120
264
|
|
|
121
265
|
:param ctx: Click context
|
|
@@ -126,56 +270,20 @@ def validate_hostlist(ctx, param, value):
|
|
|
126
270
|
try:
|
|
127
271
|
return expand_hostlist(value)
|
|
128
272
|
except Exception:
|
|
129
|
-
|
|
273
|
+
option_name = param if isinstance(param, str) else param.name
|
|
274
|
+
raise click.BadOptionUsage(option_name, 'Invalid hostlist expression') from None
|
|
130
275
|
|
|
131
276
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@click.option('--hostlist', '-w', metavar='EXPR', required=True, callback=validate_hostlist,
|
|
135
|
-
help='Hostlist expression')
|
|
136
|
-
@click.option('--username', '-u', help='Override ssh username')
|
|
137
|
-
@click.option('--keyfile', '-k', type=click.Path(exists=True, dir_okay=False), help='Private key file')
|
|
138
|
-
@click.option('--keypass', '-K', is_flag=True, help='Prompt for private key password')
|
|
139
|
-
@click.option('--prompt', '-p', is_flag=True, help='Prompt for ssh password')
|
|
140
|
-
@click.option('--password', '-P', help='Supply ssh password')
|
|
141
|
-
@click.option('--dshbak', '-D', is_flag=True, help='Group output by host')
|
|
142
|
-
@click.option('--coalesce', '-C', is_flag=True, help='Coalesce similar output from hosts')
|
|
143
|
-
@click.option('--file', '-F', is_flag=True, help='Treat CMD as a script file')
|
|
144
|
-
@click.option('--debug', '-d', is_flag=True, help='Enable debug output')
|
|
145
|
-
@click.option('--verbose', '-v', count=True, help='Increase debug verbosity')
|
|
146
|
-
@click.option('--redline', is_flag=True, help='Run pydsh faster')
|
|
147
|
-
@click.option('--port', default=22, help='SSH Port')
|
|
148
|
-
@click.option('--sha2', is_flag=True, default=True, help='Use SHA2 Hash Algorithm for Keys')
|
|
149
|
-
@click.argument('cmd', nargs=1)
|
|
150
|
-
def cli(**kwargs):
|
|
151
|
-
""" Run ssh commands in parallel across hosts
|
|
152
|
-
"""
|
|
153
|
-
if kwargs['debug']:
|
|
154
|
-
log.setLevel(logging.INFO)
|
|
155
|
-
if kwargs['verbose']:
|
|
156
|
-
logging.getLogger('sshreader').setLevel(logging.INFO)
|
|
157
|
-
if kwargs['verbose'] > 1:
|
|
158
|
-
log.setLevel(logging.DEBUG)
|
|
159
|
-
if kwargs['verbose'] > 2:
|
|
160
|
-
logging.getLogger('sshreader').setLevel(logging.DEBUG)
|
|
161
|
-
log.debug(kwargs)
|
|
162
|
-
if kwargs['file']:
|
|
163
|
-
mkpath = click.Path(exists=True, dir_okay=False)
|
|
164
|
-
script_path = mkpath(kwargs['cmd'])
|
|
165
|
-
script_name = os.path.split(script_path)[1]
|
|
166
|
-
log.info('Creating copy_script prehook for: ' + script_name)
|
|
167
|
-
prehook = sshreader.Hook(copy_script, args=[script_path], ssh_established=True)
|
|
168
|
-
with open(script_path) as s:
|
|
169
|
-
script = s.readline()
|
|
170
|
-
if script.startswith('#!') is False:
|
|
171
|
-
raise click.UsageError('Script must start with #!')
|
|
172
|
-
kwargs['cmd'] = [script.split('#!').pop().strip() + ' /tmp/' + script_name, 'rm /tmp/' + script_name]
|
|
173
|
-
sshenv = sshreader.envvars()
|
|
174
|
-
log.debug(sshenv)
|
|
277
|
+
def setup_authentication(kwargs: dict[str, Any], sshenv: sshreader.EnvVars) -> dict[str, Any]:
|
|
278
|
+
""" Configure SSH authentication credentials
|
|
175
279
|
|
|
176
|
-
|
|
177
|
-
|
|
280
|
+
Handles the logic for determining which authentication method to use
|
|
281
|
+
(keyfile, password, SSH agent, or prompt for password).
|
|
178
282
|
|
|
283
|
+
:param kwargs: Dictionary of command-line arguments
|
|
284
|
+
:param sshenv: sshreader.EnvVars object with environment SSH info
|
|
285
|
+
:return: Updated kwargs with authentication credentials configured
|
|
286
|
+
"""
|
|
179
287
|
if kwargs['username'] is None:
|
|
180
288
|
if sshenv.username is None:
|
|
181
289
|
raise click.ClickException('Unable to determine ssh username. Please provide one using --username')
|
|
@@ -196,7 +304,7 @@ def cli(**kwargs):
|
|
|
196
304
|
kwargs['keyfile'] = sshenv.dsa_key
|
|
197
305
|
log.info('Using DSA private key file')
|
|
198
306
|
else:
|
|
199
|
-
if len(sshenv.agent_keys) == 0:
|
|
307
|
+
if sshenv.agent_keys is None or len(sshenv.agent_keys) == 0:
|
|
200
308
|
if not all((kwargs['username'], kwargs['password'])):
|
|
201
309
|
raise click.ClickException('Unable to find ssh key to use and password not supplied.')
|
|
202
310
|
else:
|
|
@@ -214,7 +322,7 @@ def cli(**kwargs):
|
|
|
214
322
|
# If you specify a password or prompt for one it overrides the ssh key
|
|
215
323
|
if not kwargs['password']:
|
|
216
324
|
if not kwargs['prompt']:
|
|
217
|
-
if not kwargs['keyfile'] and len(sshenv.agent_keys) == 0:
|
|
325
|
+
if not kwargs['keyfile'] and (sshenv.agent_keys is None or len(sshenv.agent_keys) == 0):
|
|
218
326
|
raise click.ClickException('Unable to find ssh key to use and password not supplied or prompt enabled.')
|
|
219
327
|
else:
|
|
220
328
|
log.info('Prompting for password and disabling discovered SSH keyfiles')
|
|
@@ -226,46 +334,75 @@ def cli(**kwargs):
|
|
|
226
334
|
# You provided a password, ignore the SSH key
|
|
227
335
|
kwargs['keyfile'] = None
|
|
228
336
|
|
|
337
|
+
return kwargs
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@click.command(epilog=__examples__)
|
|
341
|
+
@click.version_option(version=__version__)
|
|
342
|
+
@click.option('--hostlist', '-w', metavar='EXPR', required=True, callback=validate_hostlist,
|
|
343
|
+
help='Hostlist expression')
|
|
344
|
+
@click.option('--username', '-u', help='Override ssh username')
|
|
345
|
+
@click.option('--keyfile', '-k', type=click.Path(exists=True, dir_okay=False), help='Private key file')
|
|
346
|
+
@click.option('--keypass', '-K', is_flag=True, help='Prompt for private key password')
|
|
347
|
+
@click.option('--prompt', '-p', is_flag=True, help='Prompt for ssh password')
|
|
348
|
+
@click.option('--password', '-P', help='Supply ssh password')
|
|
349
|
+
@click.option('--dshbak', '-D', is_flag=True, help='Group output by host')
|
|
350
|
+
@click.option('--coalesce', '-C', is_flag=True, help='Coalesce similar output from hosts')
|
|
351
|
+
@click.option('--file', '-F', is_flag=True, help='Treat CMD as a script file')
|
|
352
|
+
@click.option('--confirm', is_flag=True, help='Skip confirmation prompt when executing scripts')
|
|
353
|
+
@click.option('--debug', '-d', is_flag=True, help='Enable debug output')
|
|
354
|
+
@click.option('--verbose', '-v', count=True, help='Increase debug verbosity')
|
|
355
|
+
@click.option('--redline', is_flag=True, help='Run pydsh faster')
|
|
356
|
+
@click.option('--port', default=22, type=click.IntRange(1, 65535), help='SSH Port')
|
|
357
|
+
@click.option('--timeout', default=30, type=click.IntRange(1, 3600), help='SSH connection timeout in seconds')
|
|
358
|
+
@click.option('--sha2', is_flag=True, default=True, help='Use SHA2 Hash Algorithm for Keys')
|
|
359
|
+
@click.argument('cmd', nargs=1)
|
|
360
|
+
def cli(**kwargs: Any) -> None:
|
|
361
|
+
""" Run ssh commands in parallel across hosts
|
|
362
|
+
"""
|
|
363
|
+
# Setup logging
|
|
364
|
+
if kwargs['debug']:
|
|
365
|
+
log.setLevel(logging.INFO)
|
|
366
|
+
if kwargs['verbose']:
|
|
367
|
+
logging.getLogger('sshreader').setLevel(logging.INFO)
|
|
368
|
+
if kwargs['verbose'] > 1:
|
|
369
|
+
log.setLevel(logging.DEBUG)
|
|
370
|
+
if kwargs['verbose'] > 2:
|
|
371
|
+
logging.getLogger('sshreader').setLevel(logging.DEBUG)
|
|
229
372
|
log.debug(kwargs)
|
|
230
|
-
posthook = sshreader.Hook(target=output)
|
|
231
|
-
jobs = list()
|
|
232
|
-
for host in kwargs['hostlist']:
|
|
233
|
-
if ':' in host:
|
|
234
|
-
log.info('SSH Port declared in host: ' + host)
|
|
235
|
-
host, port = host.split(':')
|
|
236
|
-
log.debug((host, port))
|
|
237
|
-
else:
|
|
238
|
-
port = kwargs['port']
|
|
239
|
-
if kwargs['keyfile']:
|
|
240
|
-
job = sshreader.ServerJob(host, kwargs['cmd'], username=kwargs['username'], keyfile=kwargs['keyfile'],
|
|
241
|
-
key_pass=kwargs['keypass'], combine_output=True, rsa_sha2=kwargs['sha2'])
|
|
242
|
-
else:
|
|
243
|
-
job = sshreader.ServerJob(host, kwargs['cmd'], username=kwargs['username'], password=kwargs['password'],
|
|
244
|
-
combine_output=True, rsa_sha2=kwargs['sha2'])
|
|
245
|
-
job.ssh_port = port
|
|
246
|
-
if kwargs['dshbak'] is False and kwargs['coalesce'] is False:
|
|
247
|
-
log.info('Adding posthook to ServerJob for: ' + host)
|
|
248
|
-
job.post_hook = posthook
|
|
249
|
-
if kwargs['file']:
|
|
250
|
-
log.info('Adding prehook to ServerJob for: ' + host)
|
|
251
|
-
job.pre_hook = prehook
|
|
252
|
-
jobs.append(job)
|
|
253
373
|
|
|
254
|
-
|
|
374
|
+
# Setup script file if provided
|
|
375
|
+
cmd, prehook = setup_script_file(kwargs['cmd'], kwargs['file'], kwargs['confirm'])
|
|
376
|
+
|
|
377
|
+
# Configure authentication
|
|
378
|
+
sshenv = sshreader.envvars()
|
|
379
|
+
log.debug(sshenv)
|
|
380
|
+
kwargs = setup_authentication(kwargs, sshenv)
|
|
381
|
+
log.debug(kwargs)
|
|
382
|
+
|
|
383
|
+
# Setup output formatting
|
|
384
|
+
posthook = None
|
|
255
385
|
if kwargs['dshbak'] is False and kwargs['coalesce'] is False:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
386
|
+
posthook = sshreader.Hook(target=output)
|
|
387
|
+
|
|
388
|
+
# Create jobs for all hosts
|
|
389
|
+
jobs = create_jobs(
|
|
390
|
+
hostlist=kwargs['hostlist'],
|
|
391
|
+
cmd=cmd,
|
|
392
|
+
username=kwargs['username'],
|
|
393
|
+
password=kwargs['password'],
|
|
394
|
+
keyfile=kwargs['keyfile'],
|
|
395
|
+
keypass=kwargs['keypass'],
|
|
396
|
+
port=kwargs['port'],
|
|
397
|
+
timeout=kwargs['timeout'],
|
|
398
|
+
sha2=kwargs['sha2'],
|
|
399
|
+
prehook=prehook,
|
|
400
|
+
posthook=posthook,
|
|
401
|
+
file_flag=kwargs['file']
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Execute jobs
|
|
405
|
+
execute_jobs(jobs, kwargs['dshbak'], kwargs['coalesce'], kwargs['redline'])
|
|
269
406
|
sys.exit(0)
|
|
270
407
|
|
|
271
408
|
|