pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__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.
- pyinfra/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +18 -3
- pyinfra/api/arguments.py +406 -0
- pyinfra/api/arguments_typed.py +79 -0
- pyinfra/api/command.py +274 -0
- pyinfra/api/config.py +222 -28
- pyinfra/api/connect.py +33 -13
- pyinfra/api/connectors.py +27 -0
- pyinfra/api/deploy.py +65 -66
- pyinfra/api/exceptions.py +67 -18
- pyinfra/api/facts.py +253 -202
- pyinfra/api/host.py +413 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +291 -368
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +381 -0
- pyinfra/connectors/dockerssh.py +297 -0
- pyinfra/connectors/local.py +238 -0
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +670 -0
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +309 -0
- pyinfra/connectors/sshuserclient/config.py +102 -0
- pyinfra/connectors/terraform.py +135 -0
- pyinfra/connectors/util.py +410 -0
- pyinfra/connectors/vagrant.py +183 -0
- pyinfra/context.py +145 -0
- pyinfra/facts/__init__.py +7 -6
- pyinfra/facts/apk.py +22 -7
- pyinfra/facts/apt.py +117 -60
- pyinfra/facts/brew.py +100 -15
- pyinfra/facts/bsdinit.py +23 -0
- pyinfra/facts/cargo.py +37 -0
- pyinfra/facts/choco.py +47 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +94 -0
- pyinfra/facts/dnf.py +48 -0
- pyinfra/facts/docker.py +96 -23
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +630 -58
- pyinfra/facts/flatpak.py +77 -0
- pyinfra/facts/freebsd.py +70 -0
- pyinfra/facts/gem.py +19 -6
- pyinfra/facts/git.py +59 -14
- pyinfra/facts/gpg.py +150 -0
- pyinfra/facts/hardware.py +313 -167
- pyinfra/facts/iptables.py +72 -62
- pyinfra/facts/launchd.py +44 -0
- pyinfra/facts/lxd.py +17 -4
- pyinfra/facts/mysql.py +122 -86
- pyinfra/facts/npm.py +17 -9
- pyinfra/facts/openrc.py +71 -0
- pyinfra/facts/opkg.py +246 -0
- pyinfra/facts/pacman.py +50 -7
- pyinfra/facts/pip.py +24 -7
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +15 -6
- pyinfra/facts/pkgin.py +35 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +178 -0
- pyinfra/facts/postgresql.py +6 -147
- pyinfra/facts/rpm.py +105 -0
- pyinfra/facts/runit.py +77 -0
- pyinfra/facts/selinux.py +161 -0
- pyinfra/facts/server.py +746 -285
- pyinfra/facts/snap.py +88 -0
- pyinfra/facts/systemd.py +139 -0
- pyinfra/facts/sysvinit.py +59 -0
- pyinfra/facts/upstart.py +35 -0
- pyinfra/facts/util/__init__.py +17 -0
- pyinfra/facts/util/databases.py +4 -6
- pyinfra/facts/util/packaging.py +37 -6
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/util/win_files.py +99 -0
- pyinfra/facts/vzctl.py +20 -13
- pyinfra/facts/xbps.py +35 -0
- pyinfra/facts/yum.py +34 -40
- pyinfra/facts/zfs.py +77 -0
- pyinfra/facts/zypper.py +42 -0
- pyinfra/local.py +45 -83
- pyinfra/operations/__init__.py +12 -0
- pyinfra/operations/apk.py +98 -0
- pyinfra/operations/apt.py +488 -0
- pyinfra/operations/brew.py +231 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +191 -0
- pyinfra/operations/dnf.py +210 -0
- pyinfra/operations/docker.py +446 -0
- pyinfra/operations/files.py +1939 -0
- pyinfra/operations/flatpak.py +94 -0
- pyinfra/operations/freebsd/__init__.py +12 -0
- pyinfra/operations/freebsd/freebsd_update.py +70 -0
- pyinfra/operations/freebsd/pkg.py +219 -0
- pyinfra/operations/freebsd/service.py +116 -0
- pyinfra/operations/freebsd/sysrc.py +92 -0
- pyinfra/operations/gem.py +47 -0
- pyinfra/operations/git.py +419 -0
- pyinfra/operations/iptables.py +311 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +68 -0
- pyinfra/operations/mysql.py +609 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pacman.py +81 -0
- pyinfra/operations/pip.py +205 -0
- pyinfra/operations/pipx.py +102 -0
- pyinfra/operations/pkg.py +70 -0
- pyinfra/operations/pkgin.py +91 -0
- pyinfra/operations/postgres.py +436 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +40 -0
- pyinfra/operations/python.py +72 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +189 -0
- pyinfra/operations/server.py +1099 -0
- pyinfra/operations/snap.py +117 -0
- pyinfra/operations/ssh.py +216 -0
- pyinfra/operations/systemd.py +149 -0
- pyinfra/operations/sysvinit.py +141 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +251 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +336 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +77 -0
- pyinfra/operations/yum.py +210 -0
- pyinfra/operations/zfs.py +175 -0
- pyinfra/operations/zypper.py +192 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.5.1.dist-info/METADATA +141 -0
- pyinfra-3.5.1.dist-info/RECORD +159 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
- pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +780 -0
- pyinfra_cli/commands.py +66 -0
- pyinfra_cli/exceptions.py +155 -65
- pyinfra_cli/inventory.py +233 -89
- pyinfra_cli/log.py +39 -43
- pyinfra_cli/main.py +26 -495
- pyinfra_cli/prints.py +215 -156
- pyinfra_cli/util.py +172 -105
- pyinfra_cli/virtualenv.py +25 -20
- pyinfra/api/connectors/__init__.py +0 -21
- pyinfra/api/connectors/ansible.py +0 -99
- pyinfra/api/connectors/docker.py +0 -178
- pyinfra/api/connectors/local.py +0 -169
- pyinfra/api/connectors/ssh.py +0 -402
- pyinfra/api/connectors/sshuserclient/client.py +0 -105
- pyinfra/api/connectors/sshuserclient/config.py +0 -90
- pyinfra/api/connectors/util.py +0 -63
- pyinfra/api/connectors/vagrant.py +0 -155
- pyinfra/facts/init.py +0 -176
- pyinfra/facts/util/files.py +0 -102
- pyinfra/hook.py +0 -41
- pyinfra/modules/__init__.py +0 -11
- pyinfra/modules/apk.py +0 -64
- pyinfra/modules/apt.py +0 -272
- pyinfra/modules/brew.py +0 -122
- pyinfra/modules/files.py +0 -711
- pyinfra/modules/gem.py +0 -30
- pyinfra/modules/git.py +0 -115
- pyinfra/modules/init.py +0 -344
- pyinfra/modules/iptables.py +0 -271
- pyinfra/modules/lxd.py +0 -45
- pyinfra/modules/mysql.py +0 -347
- pyinfra/modules/npm.py +0 -47
- pyinfra/modules/pacman.py +0 -60
- pyinfra/modules/pip.py +0 -99
- pyinfra/modules/pkg.py +0 -43
- pyinfra/modules/postgresql.py +0 -245
- pyinfra/modules/puppet.py +0 -20
- pyinfra/modules/python.py +0 -37
- pyinfra/modules/server.py +0 -524
- pyinfra/modules/ssh.py +0 -150
- pyinfra/modules/util/files.py +0 -52
- pyinfra/modules/util/packaging.py +0 -118
- pyinfra/modules/vzctl.py +0 -133
- pyinfra/modules/yum.py +0 -171
- pyinfra/pseudo_modules.py +0 -64
- pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
- pyinfra-0.11.dev3.dist-info/METADATA +0 -135
- pyinfra-0.11.dev3.dist-info/RECORD +0 -95
- pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
- pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
- pyinfra_cli/__main__.py +0 -40
- pyinfra_cli/config.py +0 -92
- /pyinfra/{modules/util → connectors}/__init__.py +0 -0
- /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/api/connectors/ssh.py
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
from getpass import getpass
|
|
2
|
-
from os import path
|
|
3
|
-
from socket import (
|
|
4
|
-
error as socket_error,
|
|
5
|
-
gaierror,
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
import click
|
|
9
|
-
|
|
10
|
-
from paramiko import (
|
|
11
|
-
AuthenticationException,
|
|
12
|
-
MissingHostKeyPolicy,
|
|
13
|
-
PasswordRequiredException,
|
|
14
|
-
RSAKey,
|
|
15
|
-
SFTPClient,
|
|
16
|
-
SSHException,
|
|
17
|
-
)
|
|
18
|
-
from paramiko.agent import AgentRequestHandler
|
|
19
|
-
|
|
20
|
-
import pyinfra
|
|
21
|
-
|
|
22
|
-
from pyinfra import logger
|
|
23
|
-
from pyinfra.api.exceptions import PyinfraError
|
|
24
|
-
from pyinfra.api.util import get_file_io, make_command, memoize
|
|
25
|
-
|
|
26
|
-
from .sshuserclient import SSHClient
|
|
27
|
-
from .util import read_buffers_into_queue, split_combined_output
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _log_connect_error(host, message, data):
|
|
31
|
-
logger.error('{0}{1} ({2})'.format(
|
|
32
|
-
host.print_prefix,
|
|
33
|
-
click.style(message, 'red'),
|
|
34
|
-
data,
|
|
35
|
-
))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _get_private_key(state, key_filename, key_password):
|
|
39
|
-
if key_filename in state.private_keys:
|
|
40
|
-
return state.private_keys[key_filename]
|
|
41
|
-
|
|
42
|
-
ssh_key_filenames = [
|
|
43
|
-
# Global from executed directory
|
|
44
|
-
path.expanduser(key_filename),
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
# Relative to the deploy
|
|
48
|
-
if state.deploy_dir:
|
|
49
|
-
ssh_key_filenames.append(
|
|
50
|
-
path.join(state.deploy_dir, key_filename),
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
for filename in ssh_key_filenames:
|
|
54
|
-
if not path.isfile(filename):
|
|
55
|
-
continue
|
|
56
|
-
|
|
57
|
-
# First, lets try the key without a password
|
|
58
|
-
try:
|
|
59
|
-
key = RSAKey.from_private_key_file(
|
|
60
|
-
filename=filename,
|
|
61
|
-
)
|
|
62
|
-
break
|
|
63
|
-
|
|
64
|
-
# Key is encrypted!
|
|
65
|
-
except PasswordRequiredException:
|
|
66
|
-
# If password is not provided, but we're in CLI mode, ask for it. I'm not a
|
|
67
|
-
# huge fan of having CLI specific code in here, but it doesn't really fit
|
|
68
|
-
# anywhere else without duplicating lots of key related code into cli.py.
|
|
69
|
-
if not key_password:
|
|
70
|
-
if pyinfra.is_cli:
|
|
71
|
-
key_password = getpass(
|
|
72
|
-
'Enter password for private key: {0}: '.format(
|
|
73
|
-
key_filename,
|
|
74
|
-
),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
# API mode and no password? We can't continue!
|
|
78
|
-
else:
|
|
79
|
-
raise PyinfraError(
|
|
80
|
-
'Private key file ({0}) is encrypted, set ssh_key_password to '
|
|
81
|
-
'use this key'.format(key_filename),
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
# Now, try opening the key with the password
|
|
85
|
-
try:
|
|
86
|
-
key = RSAKey.from_private_key_file(
|
|
87
|
-
filename=filename,
|
|
88
|
-
password=key_password,
|
|
89
|
-
)
|
|
90
|
-
break
|
|
91
|
-
|
|
92
|
-
except SSHException:
|
|
93
|
-
raise PyinfraError(
|
|
94
|
-
'Incorrect password for private key: {0}'.format(
|
|
95
|
-
key_filename,
|
|
96
|
-
),
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# No break, so no key found
|
|
100
|
-
else:
|
|
101
|
-
raise IOError('No such private key file: {0}'.format(key_filename))
|
|
102
|
-
|
|
103
|
-
state.private_keys[key_filename] = key
|
|
104
|
-
return key
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _make_paramiko_kwargs(state, host):
|
|
108
|
-
kwargs = {
|
|
109
|
-
'allow_agent': False,
|
|
110
|
-
'look_for_keys': False,
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
for key, value in (
|
|
114
|
-
('username', host.data.ssh_user),
|
|
115
|
-
('port', int(host.data.ssh_port or 0)),
|
|
116
|
-
('timeout', state.config.CONNECT_TIMEOUT),
|
|
117
|
-
):
|
|
118
|
-
if value:
|
|
119
|
-
kwargs[key] = value
|
|
120
|
-
|
|
121
|
-
# Password auth (boo!)
|
|
122
|
-
if host.data.ssh_password:
|
|
123
|
-
kwargs['password'] = host.data.ssh_password
|
|
124
|
-
|
|
125
|
-
# Key auth!
|
|
126
|
-
elif host.data.ssh_key:
|
|
127
|
-
kwargs['pkey'] = _get_private_key(
|
|
128
|
-
state,
|
|
129
|
-
key_filename=host.data.ssh_key,
|
|
130
|
-
key_password=host.data.ssh_key_password,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# No key or password, so let's have paramiko look for SSH agents and user keys
|
|
134
|
-
else:
|
|
135
|
-
kwargs['allow_agent'] = True
|
|
136
|
-
kwargs['look_for_keys'] = True
|
|
137
|
-
|
|
138
|
-
return kwargs
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def connect(state, host, for_fact=None):
|
|
142
|
-
'''
|
|
143
|
-
Connect to a single host. Returns the SSH client if succesful. Stateless by
|
|
144
|
-
design so can be run in parallel.
|
|
145
|
-
'''
|
|
146
|
-
|
|
147
|
-
kwargs = _make_paramiko_kwargs(state, host)
|
|
148
|
-
logger.debug('Connecting to: {0} ({1})'.format(host.name, kwargs))
|
|
149
|
-
|
|
150
|
-
# Hostname can be provided via SSH config (alias), data, or the hosts name
|
|
151
|
-
hostname = kwargs.pop(
|
|
152
|
-
'hostname',
|
|
153
|
-
host.data.ssh_hostname or host.name,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
try:
|
|
157
|
-
# Create new client & connect to the host
|
|
158
|
-
client = SSHClient()
|
|
159
|
-
client.set_missing_host_key_policy(MissingHostKeyPolicy())
|
|
160
|
-
client.connect(hostname, **kwargs)
|
|
161
|
-
|
|
162
|
-
# Enable SSH forwarding
|
|
163
|
-
session = client.get_transport().open_session()
|
|
164
|
-
AgentRequestHandler(session)
|
|
165
|
-
|
|
166
|
-
# Log
|
|
167
|
-
log_message = '{0}{1}'.format(
|
|
168
|
-
host.print_prefix,
|
|
169
|
-
click.style('Connected', 'green'),
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
if for_fact:
|
|
173
|
-
log_message = '{0}{1}'.format(
|
|
174
|
-
log_message,
|
|
175
|
-
' (for {0} fact)'.format(for_fact),
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
logger.info(log_message)
|
|
179
|
-
|
|
180
|
-
return client
|
|
181
|
-
|
|
182
|
-
except AuthenticationException:
|
|
183
|
-
auth_kwargs = {}
|
|
184
|
-
|
|
185
|
-
for key, value in kwargs.items():
|
|
186
|
-
if key in ('username', 'password'):
|
|
187
|
-
auth_kwargs[key] = value
|
|
188
|
-
continue
|
|
189
|
-
|
|
190
|
-
if key == 'pkey' and value:
|
|
191
|
-
auth_kwargs['key'] = host.data.ssh_key
|
|
192
|
-
|
|
193
|
-
auth_args = ', '.join(
|
|
194
|
-
'{0}={1}'.format(key, value)
|
|
195
|
-
for key, value in auth_kwargs.items()
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
_log_connect_error(host, 'Authentication error', auth_args)
|
|
199
|
-
|
|
200
|
-
except SSHException as e:
|
|
201
|
-
_log_connect_error(host, 'SSH error', e)
|
|
202
|
-
|
|
203
|
-
except gaierror:
|
|
204
|
-
_log_connect_error(host, 'Could not resolve hostname', hostname)
|
|
205
|
-
|
|
206
|
-
except socket_error as e:
|
|
207
|
-
_log_connect_error(host, 'Could not connect', e)
|
|
208
|
-
|
|
209
|
-
except EOFError as e:
|
|
210
|
-
_log_connect_error(host, 'EOF error', e)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def run_shell_command(
|
|
214
|
-
state, host, command,
|
|
215
|
-
get_pty=False, timeout=None, print_output=False,
|
|
216
|
-
return_combined_output=False,
|
|
217
|
-
**command_kwargs
|
|
218
|
-
):
|
|
219
|
-
'''
|
|
220
|
-
Execute a command on the specified host.
|
|
221
|
-
|
|
222
|
-
Args:
|
|
223
|
-
state (``pyinfra.api.State`` obj): state object for this command
|
|
224
|
-
hostname (string): hostname of the target
|
|
225
|
-
command (string): actual command to execute
|
|
226
|
-
sudo (boolean): whether to wrap the command with sudo
|
|
227
|
-
sudo_user (string): user to sudo to
|
|
228
|
-
get_pty (boolean): whether to get a PTY before executing the command
|
|
229
|
-
env (dict): envrionment variables to set
|
|
230
|
-
timeout (int): timeout for this command to complete before erroring
|
|
231
|
-
|
|
232
|
-
Returns:
|
|
233
|
-
tuple: (exit_code, stdout, stderr)
|
|
234
|
-
stdout and stderr are both lists of strings from each buffer.
|
|
235
|
-
'''
|
|
236
|
-
|
|
237
|
-
command = make_command(command, **command_kwargs)
|
|
238
|
-
|
|
239
|
-
logger.debug('Running command on {0}: (pty={1}) {2}'.format(
|
|
240
|
-
host.name, get_pty, command,
|
|
241
|
-
))
|
|
242
|
-
|
|
243
|
-
if print_output:
|
|
244
|
-
print('{0}>>> {1}'.format(host.print_prefix, command))
|
|
245
|
-
|
|
246
|
-
# Run it! Get stdout, stderr & the underlying channel
|
|
247
|
-
_, stdout_buffer, stderr_buffer = host.connection.exec_command(
|
|
248
|
-
command,
|
|
249
|
-
get_pty=get_pty,
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
combined_output = read_buffers_into_queue(
|
|
253
|
-
host,
|
|
254
|
-
stdout_buffer,
|
|
255
|
-
stderr_buffer,
|
|
256
|
-
timeout=timeout,
|
|
257
|
-
print_output=print_output,
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
logger.debug('Waiting for exit status...')
|
|
261
|
-
exit_status = stdout_buffer.channel.recv_exit_status()
|
|
262
|
-
logger.debug('Command exit status: {0}'.format(exit_status))
|
|
263
|
-
|
|
264
|
-
status = exit_status == 0
|
|
265
|
-
|
|
266
|
-
if return_combined_output:
|
|
267
|
-
return status, combined_output
|
|
268
|
-
|
|
269
|
-
stdout, stderr = split_combined_output(combined_output)
|
|
270
|
-
return status, stdout, stderr
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
@memoize
|
|
274
|
-
def _get_sftp_connection(host):
|
|
275
|
-
transport = host.connection.get_transport()
|
|
276
|
-
return SFTPClient.from_transport(transport)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def _get_file(host, remote_filename, filename_or_io):
|
|
280
|
-
with get_file_io(filename_or_io) as file_io:
|
|
281
|
-
sftp = _get_sftp_connection(host)
|
|
282
|
-
sftp.getfo(remote_filename, file_io)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def get_file(
|
|
286
|
-
state, host, remote_filename, filename_or_io,
|
|
287
|
-
sudo=False, sudo_user=None, su_user=None, print_output=False,
|
|
288
|
-
**command_kwargs
|
|
289
|
-
):
|
|
290
|
-
'''
|
|
291
|
-
Download a file from the remote host using SFTP. Supports download files
|
|
292
|
-
with sudo by copying to a temporary directory with read permissions,
|
|
293
|
-
downloading and then removing the copy.
|
|
294
|
-
'''
|
|
295
|
-
|
|
296
|
-
if sudo or su_user:
|
|
297
|
-
# Get temp file location
|
|
298
|
-
temp_file = state.get_temp_filename(remote_filename)
|
|
299
|
-
|
|
300
|
-
# Copy the file to the tempfile location and add read permissions
|
|
301
|
-
command = 'cp {0} {1} && chmod +r {0}'.format(remote_filename, temp_file)
|
|
302
|
-
|
|
303
|
-
copy_status, _, stderr = run_shell_command(
|
|
304
|
-
state, host, command,
|
|
305
|
-
sudo=sudo, sudo_user=sudo_user, su_user=su_user,
|
|
306
|
-
print_output=print_output,
|
|
307
|
-
**command_kwargs
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
if copy_status is False:
|
|
311
|
-
logger.error('File download copy temp error: {0}'.format('\n'.join(stderr)))
|
|
312
|
-
return False
|
|
313
|
-
|
|
314
|
-
try:
|
|
315
|
-
_get_file(host, temp_file, filename_or_io)
|
|
316
|
-
|
|
317
|
-
# Ensure that, even if we encounter an error, we (attempt to) remove the
|
|
318
|
-
# temporary copy of the file.
|
|
319
|
-
finally:
|
|
320
|
-
remove_status, _, stderr = run_shell_command(
|
|
321
|
-
state, host, 'rm -f {0}'.format(temp_file),
|
|
322
|
-
sudo=sudo, sudo_user=sudo_user, su_user=su_user,
|
|
323
|
-
print_output=print_output,
|
|
324
|
-
**command_kwargs
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
if remove_status is False:
|
|
328
|
-
logger.error('File download remove temp error: {0}'.format('\n'.join(stderr)))
|
|
329
|
-
return False
|
|
330
|
-
|
|
331
|
-
else:
|
|
332
|
-
_get_file(host, remote_filename, filename_or_io)
|
|
333
|
-
|
|
334
|
-
if print_output:
|
|
335
|
-
print('{0}file downloaded: {1}'.format(host.print_prefix, remote_filename))
|
|
336
|
-
|
|
337
|
-
return True
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def _put_file(host, filename_or_io, remote_location):
|
|
341
|
-
with get_file_io(filename_or_io) as file_io:
|
|
342
|
-
# Upload it via SFTP
|
|
343
|
-
sftp = _get_sftp_connection(host)
|
|
344
|
-
|
|
345
|
-
try:
|
|
346
|
-
sftp.putfo(file_io, remote_location)
|
|
347
|
-
except IOError as e:
|
|
348
|
-
# IO mismatch errors might indicate full disks
|
|
349
|
-
message = getattr(e, 'message', None)
|
|
350
|
-
if message and message.startswith('size mismatch in put! 0 !='):
|
|
351
|
-
raise IOError('{0} (disk may be full)'.format(e.message))
|
|
352
|
-
|
|
353
|
-
raise
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
def put_file(
|
|
357
|
-
state, host, filename_or_io, remote_filename,
|
|
358
|
-
sudo=False, sudo_user=None, su_user=None, print_output=False,
|
|
359
|
-
**command_kwargs
|
|
360
|
-
):
|
|
361
|
-
'''
|
|
362
|
-
Upload file-ios to the specified host using SFTP. Supports uploading files
|
|
363
|
-
with sudo by uploading to a temporary directory then moving & chowning.
|
|
364
|
-
'''
|
|
365
|
-
|
|
366
|
-
# sudo/su are a little more complicated, as you can only sftp with the SSH
|
|
367
|
-
# user connected, so upload to tmp and copy/chown w/sudo and/or su_user
|
|
368
|
-
if sudo or su_user:
|
|
369
|
-
# Get temp file location
|
|
370
|
-
temp_file = state.get_temp_filename(remote_filename)
|
|
371
|
-
_put_file(host, filename_or_io, temp_file)
|
|
372
|
-
|
|
373
|
-
# Execute run_shell_command w/sudo and/or su_user
|
|
374
|
-
command = 'mv {0} {1}'.format(temp_file, remote_filename)
|
|
375
|
-
|
|
376
|
-
# Move it to the su_user if present
|
|
377
|
-
if su_user:
|
|
378
|
-
command = '{0} && chown {1} {2}'.format(command, su_user, remote_filename)
|
|
379
|
-
|
|
380
|
-
# Otherwise any sudo_user
|
|
381
|
-
elif sudo_user:
|
|
382
|
-
command = '{0} && chown {1} {2}'.format(command, sudo_user, remote_filename)
|
|
383
|
-
|
|
384
|
-
status, _, stderr = run_shell_command(
|
|
385
|
-
state, host, command,
|
|
386
|
-
sudo=sudo, sudo_user=sudo_user, su_user=su_user,
|
|
387
|
-
print_output=print_output,
|
|
388
|
-
**command_kwargs
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
if status is False:
|
|
392
|
-
logger.error('File upload error: {0}'.format('\n'.join(stderr)))
|
|
393
|
-
return False
|
|
394
|
-
|
|
395
|
-
# No sudo and no su_user, so just upload it!
|
|
396
|
-
else:
|
|
397
|
-
_put_file(host, filename_or_io, remote_filename)
|
|
398
|
-
|
|
399
|
-
if print_output:
|
|
400
|
-
print('{0}file uploaded: {1}'.format(host.print_prefix, remote_filename))
|
|
401
|
-
|
|
402
|
-
return True
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
'''
|
|
2
|
-
This file as originally part of the "sshuserclient" pypi package. The GitHub
|
|
3
|
-
source has now vanished (https://github.com/tobald/sshuserclient).
|
|
4
|
-
'''
|
|
5
|
-
|
|
6
|
-
import os
|
|
7
|
-
|
|
8
|
-
from paramiko import (
|
|
9
|
-
AutoAddPolicy,
|
|
10
|
-
ProxyCommand,
|
|
11
|
-
SSHClient as ParamikoClient,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
from pyinfra.api.util import memoize
|
|
15
|
-
|
|
16
|
-
from .config import SSHConfig
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@memoize
|
|
20
|
-
def get_ssh_config():
|
|
21
|
-
user_config_file = os.path.expanduser('~/.ssh/config')
|
|
22
|
-
if os.path.exists(user_config_file):
|
|
23
|
-
with open(user_config_file) as f:
|
|
24
|
-
ssh_config = SSHConfig()
|
|
25
|
-
ssh_config.parse(f)
|
|
26
|
-
return ssh_config
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class SSHClient(ParamikoClient):
|
|
30
|
-
'''
|
|
31
|
-
An SSHClient which honors ssh_config and supports proxyjumping
|
|
32
|
-
original idea at http://bitprophet.org/blog/2012/11/05/gateway-solutions/
|
|
33
|
-
'''
|
|
34
|
-
|
|
35
|
-
def connect(self, hostname, **kwargs):
|
|
36
|
-
self.hostname, self.config = self.parse_config(hostname)
|
|
37
|
-
self.config.update(kwargs)
|
|
38
|
-
super(SSHClient, self).connect(hostname, **self.config)
|
|
39
|
-
|
|
40
|
-
def gateway(self, target, target_port):
|
|
41
|
-
transport = self.get_transport()
|
|
42
|
-
return transport.open_channel(
|
|
43
|
-
'direct-tcpip',
|
|
44
|
-
(target, target_port),
|
|
45
|
-
(self.hostname, self.config['port']))
|
|
46
|
-
|
|
47
|
-
def parse_config(self, hostname):
|
|
48
|
-
cfg = {'port': 22}
|
|
49
|
-
|
|
50
|
-
ssh_config = get_ssh_config()
|
|
51
|
-
host_config = ssh_config.lookup(hostname)
|
|
52
|
-
|
|
53
|
-
if 'hostname' in host_config:
|
|
54
|
-
hostname = host_config['hostname']
|
|
55
|
-
if 'user' in host_config:
|
|
56
|
-
cfg['username'] = host_config['user']
|
|
57
|
-
if 'identityfile' in host_config:
|
|
58
|
-
cfg['key_filename'] = host_config['identityfile']
|
|
59
|
-
if 'port' in host_config:
|
|
60
|
-
cfg['port'] = int(host_config['port'])
|
|
61
|
-
if 'proxycommand' in host_config:
|
|
62
|
-
cfg['sock'] = ProxyCommand(host_config['proxycommand'])
|
|
63
|
-
elif 'proxyjump' in host_config:
|
|
64
|
-
hops = host_config['proxyjump'].split(',')
|
|
65
|
-
sock = None
|
|
66
|
-
for i, hop in enumerate(hops):
|
|
67
|
-
hop_hostname, hop_config = self.derive_shorthand(hop)
|
|
68
|
-
c = SSHClient()
|
|
69
|
-
c.set_missing_host_key_policy(AutoAddPolicy())
|
|
70
|
-
c.connect(hop_hostname, sock=sock, **hop_config)
|
|
71
|
-
if i == len(hops) - 1:
|
|
72
|
-
target = hostname
|
|
73
|
-
target_config = {'port': cfg['port']}
|
|
74
|
-
else:
|
|
75
|
-
target, target_config = self.derive_shorthand(
|
|
76
|
-
hops[i + 1])
|
|
77
|
-
sock = c.gateway(target, target_config['port'])
|
|
78
|
-
cfg['sock'] = sock
|
|
79
|
-
|
|
80
|
-
return hostname, cfg
|
|
81
|
-
|
|
82
|
-
def derive_shorthand(self, host_string):
|
|
83
|
-
config = {}
|
|
84
|
-
user_hostport = host_string.rsplit('@', 1)
|
|
85
|
-
hostport = user_hostport.pop()
|
|
86
|
-
user = user_hostport[0] if user_hostport and user_hostport[0] else None
|
|
87
|
-
if user:
|
|
88
|
-
config['username'] = user
|
|
89
|
-
|
|
90
|
-
# IPv6: can't reliably tell where addr ends and port begins, so don't
|
|
91
|
-
# try (and don't bother adding special syntax either, user should avoid
|
|
92
|
-
# this situation by using port=).
|
|
93
|
-
if hostport.count(':') > 1:
|
|
94
|
-
host = hostport
|
|
95
|
-
config['port'] = 22
|
|
96
|
-
# IPv4: can split on ':' reliably.
|
|
97
|
-
else:
|
|
98
|
-
host_port = hostport.rsplit(':', 1)
|
|
99
|
-
host = host_port.pop(0) or None
|
|
100
|
-
if host_port and host_port[0]:
|
|
101
|
-
config['port'] = int(host_port[0])
|
|
102
|
-
else:
|
|
103
|
-
config['port'] = 22
|
|
104
|
-
|
|
105
|
-
return host, config
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
'''
|
|
2
|
-
This file as originally part of the "sshuserclient" pypi package. The GitHub
|
|
3
|
-
source has now vanished (https://github.com/tobald/sshuserclient).
|
|
4
|
-
'''
|
|
5
|
-
|
|
6
|
-
import glob
|
|
7
|
-
import os
|
|
8
|
-
import re
|
|
9
|
-
|
|
10
|
-
from paramiko import SSHConfig as ParamkoSSHConfig
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class SSHConfig(ParamkoSSHConfig):
|
|
14
|
-
'''
|
|
15
|
-
an SSHConfig that supports includes directives
|
|
16
|
-
https://github.com/paramiko/paramiko/pull/1194
|
|
17
|
-
'''
|
|
18
|
-
|
|
19
|
-
SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)')
|
|
20
|
-
|
|
21
|
-
def parse(self, file_obj, parsed_files=None):
|
|
22
|
-
'''
|
|
23
|
-
Read an OpenSSH config from the given file object.
|
|
24
|
-
|
|
25
|
-
:param file_obj: a file-like object to read the config file from
|
|
26
|
-
'''
|
|
27
|
-
host = {'host': ['*'], 'config': {}}
|
|
28
|
-
for line in file_obj:
|
|
29
|
-
# Strip any leading or trailing whitespace from the line.
|
|
30
|
-
# Refer to https://github.com/paramiko/paramiko/issues/499
|
|
31
|
-
line = line.strip()
|
|
32
|
-
if not line or line.startswith('#'):
|
|
33
|
-
continue
|
|
34
|
-
|
|
35
|
-
match = re.match(self.SETTINGS_REGEX, line)
|
|
36
|
-
if not match:
|
|
37
|
-
raise Exception('Unparsable line {}'.format(line))
|
|
38
|
-
key = match.group(1).lower()
|
|
39
|
-
value = match.group(2)
|
|
40
|
-
|
|
41
|
-
if key == 'host':
|
|
42
|
-
self._config.append(host)
|
|
43
|
-
host = {
|
|
44
|
-
'host': self._get_hosts(value),
|
|
45
|
-
'config': {},
|
|
46
|
-
}
|
|
47
|
-
elif key == 'proxycommand' and value.lower() == 'none':
|
|
48
|
-
# Store 'none' as None; prior to 3.x, it will get stripped out
|
|
49
|
-
# at the end (for compatibility with issue #415). After 3.x, it
|
|
50
|
-
# will simply not get stripped, leaving a nice explicit marker.
|
|
51
|
-
host['config'][key] = None
|
|
52
|
-
elif key == 'include':
|
|
53
|
-
# support for Include directive in ssh_config
|
|
54
|
-
path = value
|
|
55
|
-
# the path can be relative to its parent configuration file
|
|
56
|
-
if os.path.isabs(path) is False and path[0] != '~':
|
|
57
|
-
folder = os.path.dirname(file_obj.name)
|
|
58
|
-
path = os.path.join(folder, path)
|
|
59
|
-
|
|
60
|
-
# expand the user home path
|
|
61
|
-
path = os.path.expanduser(path)
|
|
62
|
-
if parsed_files is None:
|
|
63
|
-
parsed_files = []
|
|
64
|
-
|
|
65
|
-
# parse every included file
|
|
66
|
-
for filename in glob.iglob(path):
|
|
67
|
-
if os.path.isfile(filename):
|
|
68
|
-
if filename in parsed_files:
|
|
69
|
-
raise Exception(
|
|
70
|
-
'Include loop detected in ssh config file: %s' % filename,
|
|
71
|
-
)
|
|
72
|
-
with open(filename) as fd:
|
|
73
|
-
parsed_files.append(filename)
|
|
74
|
-
self.parse(fd, parsed_files)
|
|
75
|
-
|
|
76
|
-
else:
|
|
77
|
-
if value.startswith('"') and value.endswith('"'):
|
|
78
|
-
value = value[1:-1]
|
|
79
|
-
|
|
80
|
-
# identityfile, localforward, remoteforward keys are special
|
|
81
|
-
# cases, since they are allowed to be specified multiple times
|
|
82
|
-
# and they should be tried in order of specification.
|
|
83
|
-
if key in ['identityfile', 'localforward', 'remoteforward']:
|
|
84
|
-
if key in host['config']:
|
|
85
|
-
host['config'][key].append(value)
|
|
86
|
-
else:
|
|
87
|
-
host['config'][key] = [value]
|
|
88
|
-
elif key not in host['config']:
|
|
89
|
-
host['config'][key] = value
|
|
90
|
-
self._config.append(host)
|
pyinfra/api/connectors/util.py
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
from socket import timeout as timeout_error
|
|
2
|
-
|
|
3
|
-
import click
|
|
4
|
-
import gevent
|
|
5
|
-
|
|
6
|
-
from gevent.queue import Queue
|
|
7
|
-
|
|
8
|
-
from pyinfra.api.util import read_buffer
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def read_buffers_into_queue(host, stdout_buffer, stderr_buffer, timeout, print_output):
|
|
12
|
-
output_queue = Queue()
|
|
13
|
-
|
|
14
|
-
# Iterate through outputs to get an exit status and generate desired list
|
|
15
|
-
# output, done in two greenlets so stdout isn't printed before stderr. Not
|
|
16
|
-
# attached to state.pool to avoid blocking it with 2x n-hosts greenlets.
|
|
17
|
-
stdout_reader = gevent.spawn(
|
|
18
|
-
read_buffer,
|
|
19
|
-
'stdout',
|
|
20
|
-
stdout_buffer,
|
|
21
|
-
output_queue,
|
|
22
|
-
print_output=print_output,
|
|
23
|
-
print_func=lambda line: '{0}{1}'.format(host.print_prefix, line),
|
|
24
|
-
)
|
|
25
|
-
stderr_reader = gevent.spawn(
|
|
26
|
-
read_buffer,
|
|
27
|
-
'stderr',
|
|
28
|
-
stderr_buffer,
|
|
29
|
-
output_queue,
|
|
30
|
-
print_output=print_output,
|
|
31
|
-
print_func=lambda line: '{0}{1}'.format(
|
|
32
|
-
host.print_prefix, click.style(line, 'red'),
|
|
33
|
-
),
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
# Wait on output, with our timeout (or None)
|
|
37
|
-
greenlets = gevent.wait((stdout_reader, stderr_reader), timeout=timeout)
|
|
38
|
-
|
|
39
|
-
# Timeout doesn't raise an exception, but gevent.wait returns the greenlets
|
|
40
|
-
# which did complete. So if both haven't completed, we kill them and fail
|
|
41
|
-
# with a timeout.
|
|
42
|
-
if len(greenlets) != 2:
|
|
43
|
-
stdout_reader.kill()
|
|
44
|
-
stderr_reader.kill()
|
|
45
|
-
|
|
46
|
-
raise timeout_error()
|
|
47
|
-
|
|
48
|
-
return list(output_queue.queue)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def split_combined_output(combined_output):
|
|
52
|
-
stdout = []
|
|
53
|
-
stderr = []
|
|
54
|
-
|
|
55
|
-
for type_, line in combined_output:
|
|
56
|
-
if type_ == 'stdout':
|
|
57
|
-
stdout.append(line)
|
|
58
|
-
elif type_ == 'stderr':
|
|
59
|
-
stderr.append(line)
|
|
60
|
-
else: # pragma: no cover
|
|
61
|
-
raise ValueError('Incorrect output line type: {0}'.format(type_))
|
|
62
|
-
|
|
63
|
-
return stdout, stderr
|