pyinfra 0.11.dev3__py3-none-any.whl → 3.6__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 +19 -3
- pyinfra/api/arguments.py +413 -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 +73 -18
- pyinfra/api/facts.py +267 -200
- pyinfra/api/host.py +416 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/metadata.py +69 -0
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +309 -369
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +405 -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 +727 -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 +417 -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 +629 -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 +762 -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 +99 -0
- pyinfra/operations/apt.py +496 -0
- pyinfra/operations/brew.py +232 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +194 -0
- pyinfra/operations/dnf.py +213 -0
- pyinfra/operations/docker.py +492 -0
- pyinfra/operations/files.py +2014 -0
- pyinfra/operations/flatpak.py +95 -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 +48 -0
- pyinfra/operations/git.py +420 -0
- pyinfra/operations/iptables.py +312 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +69 -0
- pyinfra/operations/mysql.py +610 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +89 -0
- pyinfra/operations/pacman.py +82 -0
- pyinfra/operations/pip.py +206 -0
- pyinfra/operations/pipx.py +103 -0
- pyinfra/operations/pkg.py +71 -0
- pyinfra/operations/pkgin.py +92 -0
- pyinfra/operations/postgres.py +437 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +41 -0
- pyinfra/operations/python.py +73 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +190 -0
- pyinfra/operations/server.py +1100 -0
- pyinfra/operations/snap.py +118 -0
- pyinfra/operations/ssh.py +217 -0
- pyinfra/operations/systemd.py +150 -0
- pyinfra/operations/sysvinit.py +142 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +407 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +338 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +78 -0
- pyinfra/operations/yum.py +213 -0
- pyinfra/operations/zfs.py +176 -0
- pyinfra/operations/zypper.py +193 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.6.dist-info/METADATA +142 -0
- pyinfra-3.6.dist-info/RECORD +160 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
- pyinfra-3.6.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +793 -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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from shutil import which
|
|
3
|
+
from tempfile import mkstemp
|
|
4
|
+
from typing import TYPE_CHECKING, Tuple
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from typing_extensions import Unpack, override
|
|
8
|
+
|
|
9
|
+
from pyinfra import logger
|
|
10
|
+
from pyinfra.api.command import QuoteString, StringCommand
|
|
11
|
+
from pyinfra.api.exceptions import InventoryError
|
|
12
|
+
from pyinfra.api.util import get_file_io
|
|
13
|
+
|
|
14
|
+
from .base import BaseConnector
|
|
15
|
+
from .util import (
|
|
16
|
+
CommandOutput,
|
|
17
|
+
execute_command_with_sudo_retry,
|
|
18
|
+
make_unix_command_for_host,
|
|
19
|
+
run_local_process,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pyinfra.api.arguments import ConnectorArguments
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LocalConnector(BaseConnector):
|
|
27
|
+
"""
|
|
28
|
+
The ``@local`` connector executes changes on the local machine using
|
|
29
|
+
subprocesses. **This connector is only compatible with MacOS & Linux hosts**.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
|
|
33
|
+
.. code::
|
|
34
|
+
|
|
35
|
+
# Install nginx
|
|
36
|
+
pyinfra inventory.py apt.packages nginx update=true _sudo=true
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
handles_execution = True
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
@staticmethod
|
|
43
|
+
def make_names_data(name=None):
|
|
44
|
+
if name is not None:
|
|
45
|
+
raise InventoryError("Cannot have more than one @local")
|
|
46
|
+
|
|
47
|
+
yield "@local", {}, ["@local"]
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
def run_shell_command(
|
|
51
|
+
self,
|
|
52
|
+
command: StringCommand,
|
|
53
|
+
print_output: bool = False,
|
|
54
|
+
print_input: bool = False,
|
|
55
|
+
**arguments: Unpack["ConnectorArguments"],
|
|
56
|
+
) -> Tuple[bool, CommandOutput]:
|
|
57
|
+
"""
|
|
58
|
+
Execute a command on the local machine.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
command (StringCommand): actual command to execute
|
|
62
|
+
print_output (bool): whether to print command output
|
|
63
|
+
print_input (bool): whether to print command input
|
|
64
|
+
arguments: (ConnectorArguments): connector global arguments
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
tuple: (bool, CommandOutput)
|
|
68
|
+
Bool indicating success and CommandOutput with stdout/stderr lines.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
arguments.pop("_get_pty", False)
|
|
72
|
+
_timeout = arguments.pop("_timeout", None)
|
|
73
|
+
_stdin = arguments.pop("_stdin", None)
|
|
74
|
+
_success_exit_codes = arguments.pop("_success_exit_codes", None)
|
|
75
|
+
|
|
76
|
+
def execute_command() -> Tuple[int, CommandOutput]:
|
|
77
|
+
unix_command = make_unix_command_for_host(self.state, self.host, command, **arguments)
|
|
78
|
+
actual_command = unix_command.get_raw_value()
|
|
79
|
+
|
|
80
|
+
logger.debug("--> Running command on localhost: %s", unix_command)
|
|
81
|
+
|
|
82
|
+
if print_input:
|
|
83
|
+
click.echo("{0}>>> {1}".format(self.host.print_prefix, unix_command), err=True)
|
|
84
|
+
|
|
85
|
+
return run_local_process(
|
|
86
|
+
actual_command,
|
|
87
|
+
stdin=_stdin,
|
|
88
|
+
timeout=_timeout,
|
|
89
|
+
print_output=print_output,
|
|
90
|
+
print_prefix=self.host.print_prefix,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return_code, combined_output = execute_command_with_sudo_retry(
|
|
94
|
+
self.host,
|
|
95
|
+
arguments,
|
|
96
|
+
execute_command,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if _success_exit_codes:
|
|
100
|
+
status = return_code in _success_exit_codes
|
|
101
|
+
else:
|
|
102
|
+
status = return_code == 0
|
|
103
|
+
|
|
104
|
+
return status, combined_output
|
|
105
|
+
|
|
106
|
+
@override
|
|
107
|
+
def put_file(
|
|
108
|
+
self,
|
|
109
|
+
filename_or_io,
|
|
110
|
+
remote_filename,
|
|
111
|
+
remote_temp_filename=None, # ignored
|
|
112
|
+
print_output: bool = False,
|
|
113
|
+
print_input: bool = False,
|
|
114
|
+
**arguments,
|
|
115
|
+
) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Upload a local file or IO object by copying it to a temporary directory
|
|
118
|
+
and then writing it to the upload location.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
bool: Indicating success or failure
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
_, temp_filename = mkstemp()
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
# Load our file or IO object and write it to the temporary file
|
|
128
|
+
with get_file_io(filename_or_io) as file_io:
|
|
129
|
+
with open(temp_filename, "wb") as temp_f:
|
|
130
|
+
data = file_io.read()
|
|
131
|
+
|
|
132
|
+
if isinstance(data, str):
|
|
133
|
+
data = data.encode()
|
|
134
|
+
|
|
135
|
+
temp_f.write(data)
|
|
136
|
+
|
|
137
|
+
# Copy the file using `cp` such that we support sudo/su
|
|
138
|
+
status, output = self.run_shell_command(
|
|
139
|
+
StringCommand("cp", temp_filename, QuoteString(remote_filename)),
|
|
140
|
+
print_output=print_output,
|
|
141
|
+
print_input=print_input,
|
|
142
|
+
**arguments,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not status:
|
|
146
|
+
raise IOError(output.stderr)
|
|
147
|
+
finally:
|
|
148
|
+
os.remove(temp_filename)
|
|
149
|
+
|
|
150
|
+
if print_output:
|
|
151
|
+
click.echo(
|
|
152
|
+
"{0}file copied: {1}".format(self.host.print_prefix, remote_filename),
|
|
153
|
+
err=True,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return status
|
|
157
|
+
|
|
158
|
+
@override
|
|
159
|
+
def get_file(
|
|
160
|
+
self,
|
|
161
|
+
remote_filename,
|
|
162
|
+
filename_or_io,
|
|
163
|
+
remote_temp_filename=None, # ignored
|
|
164
|
+
print_output: bool = False,
|
|
165
|
+
print_input: bool = False,
|
|
166
|
+
**arguments,
|
|
167
|
+
) -> bool:
|
|
168
|
+
"""
|
|
169
|
+
Download a local file by copying it to a temporary location and then writing
|
|
170
|
+
it to our filename or IO object.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
bool: Indicating success or failure
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
_, temp_filename = mkstemp()
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Copy the file using `cp` such that we support sudo/su
|
|
180
|
+
status, output = self.run_shell_command(
|
|
181
|
+
StringCommand("cp", remote_filename, temp_filename),
|
|
182
|
+
print_output=print_output,
|
|
183
|
+
print_input=print_input,
|
|
184
|
+
**arguments,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not status:
|
|
188
|
+
raise IOError(output.stderr)
|
|
189
|
+
|
|
190
|
+
# Load our file or IO object and write it to the temporary file
|
|
191
|
+
with open(temp_filename, "rb") as temp_f:
|
|
192
|
+
with get_file_io(filename_or_io, "wb") as file_io:
|
|
193
|
+
data_bytes: bytes
|
|
194
|
+
|
|
195
|
+
data = temp_f.read()
|
|
196
|
+
if isinstance(data, str):
|
|
197
|
+
data_bytes = data.encode()
|
|
198
|
+
else:
|
|
199
|
+
data_bytes = data
|
|
200
|
+
|
|
201
|
+
file_io.write(data_bytes)
|
|
202
|
+
finally:
|
|
203
|
+
os.remove(temp_filename)
|
|
204
|
+
|
|
205
|
+
if print_output:
|
|
206
|
+
click.echo(
|
|
207
|
+
"{0}file copied: {1}".format(self.host.print_prefix, remote_filename),
|
|
208
|
+
err=True,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
@override
|
|
214
|
+
def check_can_rsync(self) -> None:
|
|
215
|
+
if not which("rsync"):
|
|
216
|
+
raise NotImplementedError("The `rsync` binary is not available on this system.")
|
|
217
|
+
|
|
218
|
+
@override
|
|
219
|
+
def rsync(
|
|
220
|
+
self,
|
|
221
|
+
src,
|
|
222
|
+
dest,
|
|
223
|
+
flags,
|
|
224
|
+
print_output: bool = False,
|
|
225
|
+
print_input: bool = False,
|
|
226
|
+
**arguments,
|
|
227
|
+
) -> bool:
|
|
228
|
+
status, output = self.run_shell_command(
|
|
229
|
+
StringCommand("rsync", " ".join(flags), src, dest),
|
|
230
|
+
print_output=print_output,
|
|
231
|
+
print_input=print_input,
|
|
232
|
+
**arguments,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if not status:
|
|
236
|
+
raise IOError(output.stderr)
|
|
237
|
+
|
|
238
|
+
return True
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .client import SCPClient # noqa: F401
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ntpath
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import PurePath
|
|
6
|
+
from shlex import quote
|
|
7
|
+
from socket import timeout as SocketTimeoutError
|
|
8
|
+
from typing import IO, AnyStr
|
|
9
|
+
|
|
10
|
+
from paramiko import Channel
|
|
11
|
+
from paramiko.transport import Transport
|
|
12
|
+
|
|
13
|
+
SCP_COMMAND = b"scp"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Unicode conversion functions; assume UTF-8
|
|
17
|
+
def asbytes(s: bytes | str | PurePath) -> bytes:
|
|
18
|
+
"""Turns unicode into bytes, if needed.
|
|
19
|
+
|
|
20
|
+
Assumes UTF-8.
|
|
21
|
+
"""
|
|
22
|
+
if isinstance(s, bytes):
|
|
23
|
+
return s
|
|
24
|
+
elif isinstance(s, PurePath):
|
|
25
|
+
return bytes(s)
|
|
26
|
+
else:
|
|
27
|
+
return s.encode("utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def asunicode(s: bytes | str) -> str:
|
|
31
|
+
"""Turns bytes into unicode, if needed.
|
|
32
|
+
|
|
33
|
+
Uses UTF-8.
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(s, bytes):
|
|
36
|
+
return s.decode("utf-8", "replace")
|
|
37
|
+
else:
|
|
38
|
+
return s
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SCPClient:
|
|
42
|
+
"""
|
|
43
|
+
An scp1 implementation, compatible with openssh scp.
|
|
44
|
+
Raises SCPException for all transport related errors. Local filesystem
|
|
45
|
+
and OS errors pass through.
|
|
46
|
+
|
|
47
|
+
Main public methods are .putfo and .getfo
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
transport: Transport,
|
|
53
|
+
buff_size: int = 16384,
|
|
54
|
+
socket_timeout: float = 10.0,
|
|
55
|
+
):
|
|
56
|
+
self.transport = transport
|
|
57
|
+
self.buff_size = buff_size
|
|
58
|
+
self.socket_timeout = socket_timeout
|
|
59
|
+
self._channel: Channel | None = None
|
|
60
|
+
self.scp_command = SCP_COMMAND
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def channel(self) -> Channel:
|
|
64
|
+
"""Return an open Channel, (re)opening if needed."""
|
|
65
|
+
if self._channel is None or self._channel.closed:
|
|
66
|
+
self._channel = self.transport.open_session()
|
|
67
|
+
return self._channel
|
|
68
|
+
|
|
69
|
+
def __enter__(self):
|
|
70
|
+
_ = self.channel # triggers opening if not already open
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(self, type, value, traceback):
|
|
74
|
+
self.close()
|
|
75
|
+
|
|
76
|
+
def putfo(
|
|
77
|
+
self,
|
|
78
|
+
fl: IO[AnyStr],
|
|
79
|
+
remote_path: str | bytes,
|
|
80
|
+
mode: str | bytes = "0644",
|
|
81
|
+
size: int | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
if size is None:
|
|
84
|
+
pos = fl.tell()
|
|
85
|
+
fl.seek(0, os.SEEK_END) # Seek to end
|
|
86
|
+
size = fl.tell() - pos
|
|
87
|
+
fl.seek(pos, os.SEEK_SET) # Seek back
|
|
88
|
+
|
|
89
|
+
self.channel.settimeout(self.socket_timeout)
|
|
90
|
+
self.channel.exec_command(
|
|
91
|
+
self.scp_command + b" -t " + asbytes(quote(asunicode(remote_path)))
|
|
92
|
+
)
|
|
93
|
+
self._recv_confirm()
|
|
94
|
+
self._send_file(fl, remote_path, mode, size=size)
|
|
95
|
+
self.close()
|
|
96
|
+
|
|
97
|
+
def getfo(self, remote_path: str, fl: IO):
|
|
98
|
+
remote_path_sanitized = quote(remote_path)
|
|
99
|
+
if os.name == "nt":
|
|
100
|
+
remote_file_name = ntpath.basename(remote_path_sanitized)
|
|
101
|
+
else:
|
|
102
|
+
remote_file_name = os.path.basename(remote_path_sanitized)
|
|
103
|
+
self.channel.settimeout(self.socket_timeout)
|
|
104
|
+
self.channel.exec_command(self.scp_command + b" -f " + asbytes(remote_path_sanitized))
|
|
105
|
+
self._recv_all(fl, remote_file_name)
|
|
106
|
+
self.close()
|
|
107
|
+
return fl
|
|
108
|
+
|
|
109
|
+
def close(self):
|
|
110
|
+
"""close scp channel"""
|
|
111
|
+
if self._channel is not None:
|
|
112
|
+
self._channel.close()
|
|
113
|
+
self._channel = None
|
|
114
|
+
|
|
115
|
+
def _send_file(self, fl, name, mode, size):
|
|
116
|
+
basename = asbytes(os.path.basename(name))
|
|
117
|
+
# The protocol can't handle \n in the filename.
|
|
118
|
+
# Quote them as the control sequence \^J for now,
|
|
119
|
+
# which is how openssh handles it.
|
|
120
|
+
self.channel.sendall(
|
|
121
|
+
("C%s %d " % (mode, size)).encode("ascii") + basename.replace(b"\n", b"\\^J") + b"\n"
|
|
122
|
+
)
|
|
123
|
+
self._recv_confirm()
|
|
124
|
+
file_pos = 0
|
|
125
|
+
buff_size = self.buff_size
|
|
126
|
+
chan = self.channel
|
|
127
|
+
while file_pos < size:
|
|
128
|
+
chan.sendall(fl.read(buff_size))
|
|
129
|
+
file_pos = fl.tell()
|
|
130
|
+
chan.sendall(b"\x00")
|
|
131
|
+
self._recv_confirm()
|
|
132
|
+
|
|
133
|
+
def _recv_confirm(self):
|
|
134
|
+
# read scp response
|
|
135
|
+
msg = b""
|
|
136
|
+
try:
|
|
137
|
+
msg = self.channel.recv(512)
|
|
138
|
+
except SocketTimeoutError:
|
|
139
|
+
raise SCPException("Timeout waiting for scp response")
|
|
140
|
+
# slice off the first byte, so this compare will work in py2 and py3
|
|
141
|
+
if msg and msg[0:1] == b"\x00":
|
|
142
|
+
return
|
|
143
|
+
elif msg and msg[0:1] == b"\x01":
|
|
144
|
+
raise SCPException(asunicode(msg[1:]))
|
|
145
|
+
elif self.channel.recv_stderr_ready():
|
|
146
|
+
msg = self.channel.recv_stderr(512)
|
|
147
|
+
raise SCPException(asunicode(msg))
|
|
148
|
+
elif not msg:
|
|
149
|
+
raise SCPException("No response from server")
|
|
150
|
+
else:
|
|
151
|
+
raise SCPException("Invalid response from server", msg)
|
|
152
|
+
|
|
153
|
+
def _recv_all(self, fh: IO, remote_file_name: str) -> None:
|
|
154
|
+
# loop over scp commands, and receive as necessary
|
|
155
|
+
commands = (b"C",)
|
|
156
|
+
while not self.channel.closed:
|
|
157
|
+
# wait for command as long as we're open
|
|
158
|
+
self.channel.sendall(b"\x00")
|
|
159
|
+
msg = self.channel.recv(1024)
|
|
160
|
+
if not msg: # chan closed while receiving
|
|
161
|
+
break
|
|
162
|
+
assert msg[-1:] == b"\n"
|
|
163
|
+
msg = msg[:-1]
|
|
164
|
+
code = msg[0:1]
|
|
165
|
+
if code not in commands:
|
|
166
|
+
raise SCPException(asunicode(msg[1:]))
|
|
167
|
+
self._recv_file(msg[1:], fh, remote_file_name)
|
|
168
|
+
|
|
169
|
+
def _recv_file(self, cmd: bytes, fh: IO, remote_file_name: str) -> None:
|
|
170
|
+
chan = self.channel
|
|
171
|
+
parts = cmd.strip().split(b" ", 2)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
size = int(parts[1])
|
|
175
|
+
except (ValueError, IndexError):
|
|
176
|
+
chan.send(b"\x01")
|
|
177
|
+
chan.close()
|
|
178
|
+
raise SCPException("Bad file format")
|
|
179
|
+
|
|
180
|
+
buff_size = self.buff_size
|
|
181
|
+
pos = 0
|
|
182
|
+
chan.send(b"\x00")
|
|
183
|
+
try:
|
|
184
|
+
while pos < size:
|
|
185
|
+
# we have to make sure we don't read the final byte
|
|
186
|
+
if size - pos <= buff_size:
|
|
187
|
+
buff_size = size - pos
|
|
188
|
+
data = chan.recv(buff_size)
|
|
189
|
+
if not data:
|
|
190
|
+
raise SCPException("Underlying channel was closed")
|
|
191
|
+
fh.write(data)
|
|
192
|
+
pos = fh.tell()
|
|
193
|
+
msg = chan.recv(512)
|
|
194
|
+
if msg and msg[0:1] != b"\x00":
|
|
195
|
+
raise SCPException(asunicode(msg[1:]))
|
|
196
|
+
except SocketTimeoutError:
|
|
197
|
+
chan.close()
|
|
198
|
+
raise SCPException("Error receiving, socket.timeout")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class SCPException(Exception):
|
|
202
|
+
"""SCP exception class"""
|
|
203
|
+
|
|
204
|
+
pass
|