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
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from tempfile import mkstemp
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from typing_extensions import TypedDict, Unpack, override
|
|
10
|
+
|
|
11
|
+
from pyinfra import local, logger
|
|
12
|
+
from pyinfra.api import QuoteString, StringCommand
|
|
13
|
+
from pyinfra.api.exceptions import ConnectError, InventoryError, PyinfraError
|
|
14
|
+
from pyinfra.api.util import get_file_io
|
|
15
|
+
from pyinfra.progress import progress_spinner
|
|
16
|
+
|
|
17
|
+
from .base import BaseConnector, DataMeta
|
|
18
|
+
from .local import LocalConnector
|
|
19
|
+
from .util import CommandOutput, extract_control_arguments, make_unix_command_for_host
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pyinfra.api.arguments import ConnectorArguments
|
|
23
|
+
from pyinfra.api.host import Host
|
|
24
|
+
from pyinfra.api.state import State
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConnectorData(TypedDict):
|
|
28
|
+
docker_identifier: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
connector_data_meta: dict[str, DataMeta] = {
|
|
32
|
+
"docker_identifier": DataMeta("ID of container or image to start from"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DockerConnector(BaseConnector):
|
|
37
|
+
"""
|
|
38
|
+
The Docker connector allows you to use pyinfra to create new Docker images or modify running
|
|
39
|
+
Docker containers.
|
|
40
|
+
|
|
41
|
+
.. note::
|
|
42
|
+
|
|
43
|
+
The Docker connector allows pyinfra to target Docker containers as inventory and is
|
|
44
|
+
unrelated to the :doc:`../operations/docker` & :doc:`../facts/docker`.
|
|
45
|
+
|
|
46
|
+
You can pass either an image name or existing container ID:
|
|
47
|
+
|
|
48
|
+
+ Image - will create a new container from the image, execute operations against it, save into \
|
|
49
|
+
a new Docker image and remove the container
|
|
50
|
+
+ Existing container ID - will execute operations against the running container, leaving it \
|
|
51
|
+
running
|
|
52
|
+
|
|
53
|
+
.. code:: shell
|
|
54
|
+
|
|
55
|
+
# A Docker base image must be provided
|
|
56
|
+
pyinfra @docker/alpine:3.8 ...
|
|
57
|
+
|
|
58
|
+
# pyinfra can run on multiple Docker images in parallel
|
|
59
|
+
pyinfra @docker/alpine:3.8,@docker/ubuntu:bionic ...
|
|
60
|
+
|
|
61
|
+
# Execute against a running container
|
|
62
|
+
pyinfra @docker/2beb8c15a1b1 ...
|
|
63
|
+
|
|
64
|
+
The Docker connector is great for testing pyinfra operations locally, rather than connecting to
|
|
65
|
+
a remote host over SSH each time. This gives you a fast, local-first devloop to iterate on when
|
|
66
|
+
writing deploys, operations or facts.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# enable the use of other docker cli compatible tools like podman
|
|
70
|
+
docker_cmd = "docker"
|
|
71
|
+
|
|
72
|
+
handles_execution = True
|
|
73
|
+
|
|
74
|
+
data_cls = ConnectorData
|
|
75
|
+
data_meta = connector_data_meta
|
|
76
|
+
data: ConnectorData
|
|
77
|
+
|
|
78
|
+
local: LocalConnector
|
|
79
|
+
|
|
80
|
+
container_id: str
|
|
81
|
+
no_stop: bool = False
|
|
82
|
+
|
|
83
|
+
def __init__(self, state: "State", host: "Host"):
|
|
84
|
+
super().__init__(state, host)
|
|
85
|
+
self.local = LocalConnector(state, host)
|
|
86
|
+
|
|
87
|
+
@override
|
|
88
|
+
@staticmethod
|
|
89
|
+
def make_names_data(name=None):
|
|
90
|
+
if not name:
|
|
91
|
+
raise InventoryError("No docker base ID provided!")
|
|
92
|
+
|
|
93
|
+
yield (
|
|
94
|
+
f"@docker/{name}",
|
|
95
|
+
{"docker_identifier": name},
|
|
96
|
+
["@docker"],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# 2 helper functions
|
|
100
|
+
def _find_start_docker_container(self, container_id) -> tuple[str, bool]:
|
|
101
|
+
docker_info = local.shell(f"{self.docker_cmd} container inspect {container_id}")
|
|
102
|
+
assert isinstance(docker_info, str)
|
|
103
|
+
docker_info = json.loads(docker_info)[0]
|
|
104
|
+
if docker_info["State"]["Running"] is False:
|
|
105
|
+
logger.info(f"Starting stopped container: {container_id}")
|
|
106
|
+
local.shell(f"{self.docker_cmd} container start {container_id}")
|
|
107
|
+
return container_id, False
|
|
108
|
+
return container_id, True
|
|
109
|
+
|
|
110
|
+
def _start_docker_image(self, image_name):
|
|
111
|
+
try:
|
|
112
|
+
return local.shell(
|
|
113
|
+
f"{self.docker_cmd} run -d {image_name} tail -f /dev/null",
|
|
114
|
+
splitlines=True,
|
|
115
|
+
)[-1] # last line is the container ID
|
|
116
|
+
except PyinfraError as e:
|
|
117
|
+
raise ConnectError(e.args[0])
|
|
118
|
+
|
|
119
|
+
@override
|
|
120
|
+
def connect(self) -> None:
|
|
121
|
+
self.local.connect()
|
|
122
|
+
|
|
123
|
+
docker_identifier = self.data["docker_identifier"]
|
|
124
|
+
with progress_spinner({f"prepare {self.docker_cmd} container"}):
|
|
125
|
+
try:
|
|
126
|
+
self.container_id, was_running = self._find_start_docker_container(
|
|
127
|
+
docker_identifier
|
|
128
|
+
)
|
|
129
|
+
if was_running:
|
|
130
|
+
self.no_stop = True
|
|
131
|
+
except PyinfraError:
|
|
132
|
+
self.container_id = self._start_docker_image(docker_identifier)
|
|
133
|
+
|
|
134
|
+
@override
|
|
135
|
+
def disconnect(self) -> None:
|
|
136
|
+
container_id = self.container_id
|
|
137
|
+
|
|
138
|
+
if self.no_stop:
|
|
139
|
+
logger.info(
|
|
140
|
+
"{0}{1} build complete, container left running: {2}".format(
|
|
141
|
+
self.host.print_prefix,
|
|
142
|
+
self.docker_cmd,
|
|
143
|
+
click.style(container_id, bold=True),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
with progress_spinner({f"{self.docker_cmd} commit"}):
|
|
149
|
+
image_id = local.shell(f"{self.docker_cmd} commit {container_id}", splitlines=True)[-1][
|
|
150
|
+
7:19
|
|
151
|
+
] # last line is the image ID, get sha256:[XXXXXXXXXX]...
|
|
152
|
+
|
|
153
|
+
with progress_spinner({f"{self.docker_cmd} rm"}):
|
|
154
|
+
local.shell(
|
|
155
|
+
f"{self.docker_cmd} rm -f {container_id}",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
logger.info(
|
|
159
|
+
"{0}{1} build complete, image ID: {2}".format(
|
|
160
|
+
self.host.print_prefix,
|
|
161
|
+
self.docker_cmd,
|
|
162
|
+
click.style(image_id, bold=True),
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@override
|
|
167
|
+
def run_shell_command(
|
|
168
|
+
self,
|
|
169
|
+
command: StringCommand,
|
|
170
|
+
print_output: bool = False,
|
|
171
|
+
print_input: bool = False,
|
|
172
|
+
**arguments: Unpack["ConnectorArguments"],
|
|
173
|
+
) -> tuple[bool, CommandOutput]:
|
|
174
|
+
local_arguments = extract_control_arguments(arguments)
|
|
175
|
+
|
|
176
|
+
container_id = self.container_id
|
|
177
|
+
|
|
178
|
+
command = make_unix_command_for_host(self.state, self.host, command, **arguments)
|
|
179
|
+
command = StringCommand(QuoteString(command))
|
|
180
|
+
|
|
181
|
+
docker_flags = "-it" if local_arguments.get("_get_pty") else "-i"
|
|
182
|
+
docker_command = StringCommand(
|
|
183
|
+
self.docker_cmd,
|
|
184
|
+
"exec",
|
|
185
|
+
docker_flags,
|
|
186
|
+
container_id,
|
|
187
|
+
"sh",
|
|
188
|
+
"-c",
|
|
189
|
+
command,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return self.local.run_shell_command(
|
|
193
|
+
docker_command,
|
|
194
|
+
print_output=print_output,
|
|
195
|
+
print_input=print_input,
|
|
196
|
+
**local_arguments,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@override
|
|
200
|
+
def put_file(
|
|
201
|
+
self,
|
|
202
|
+
filename_or_io,
|
|
203
|
+
remote_filename,
|
|
204
|
+
remote_temp_filename=None, # ignored
|
|
205
|
+
print_output=False,
|
|
206
|
+
print_input=False,
|
|
207
|
+
**kwargs, # ignored (sudo/etc)
|
|
208
|
+
) -> bool:
|
|
209
|
+
"""
|
|
210
|
+
Upload a file/IO object to the target container by copying it to a
|
|
211
|
+
temporary location and then uploading it into the container using ``docker cp``.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
fd, temp_filename = mkstemp()
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
# Load our file or IO object and write it to the temporary file
|
|
218
|
+
with get_file_io(filename_or_io) as file_io:
|
|
219
|
+
with open(temp_filename, "wb") as temp_f:
|
|
220
|
+
data = file_io.read()
|
|
221
|
+
|
|
222
|
+
if isinstance(data, str):
|
|
223
|
+
data = data.encode()
|
|
224
|
+
|
|
225
|
+
temp_f.write(data)
|
|
226
|
+
|
|
227
|
+
docker_command = StringCommand(
|
|
228
|
+
self.docker_cmd,
|
|
229
|
+
"cp",
|
|
230
|
+
temp_filename,
|
|
231
|
+
f"{self.container_id}:{remote_filename}",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
status, output = self.local.run_shell_command(
|
|
235
|
+
docker_command,
|
|
236
|
+
print_output=print_output,
|
|
237
|
+
print_input=print_input,
|
|
238
|
+
)
|
|
239
|
+
finally:
|
|
240
|
+
os.close(fd)
|
|
241
|
+
os.remove(temp_filename)
|
|
242
|
+
|
|
243
|
+
if not status:
|
|
244
|
+
raise IOError(output.stderr)
|
|
245
|
+
|
|
246
|
+
if print_output:
|
|
247
|
+
click.echo(
|
|
248
|
+
"{0}file uploaded to container: {1}".format(
|
|
249
|
+
self.host.print_prefix,
|
|
250
|
+
remote_filename,
|
|
251
|
+
),
|
|
252
|
+
err=True,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return status
|
|
256
|
+
|
|
257
|
+
@override
|
|
258
|
+
def get_file(
|
|
259
|
+
self,
|
|
260
|
+
remote_filename,
|
|
261
|
+
filename_or_io,
|
|
262
|
+
remote_temp_filename=None, # ignored
|
|
263
|
+
print_output=False,
|
|
264
|
+
print_input=False,
|
|
265
|
+
**kwargs, # ignored (sudo/etc)
|
|
266
|
+
) -> bool:
|
|
267
|
+
"""
|
|
268
|
+
Download a file from the target container by copying it to a temporary
|
|
269
|
+
location and then reading that into our final file/IO object.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
fd, temp_filename = mkstemp()
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
docker_command = StringCommand(
|
|
276
|
+
self.docker_cmd,
|
|
277
|
+
"cp",
|
|
278
|
+
f"{self.container_id}:{remote_filename}",
|
|
279
|
+
temp_filename,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
status, output = self.local.run_shell_command(
|
|
283
|
+
docker_command,
|
|
284
|
+
print_output=print_output,
|
|
285
|
+
print_input=print_input,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Load the temporary file and write it to our file or IO object
|
|
289
|
+
with open(temp_filename, "rb") as temp_f:
|
|
290
|
+
with get_file_io(filename_or_io, "wb") as file_io:
|
|
291
|
+
data = temp_f.read()
|
|
292
|
+
file_io.write(data)
|
|
293
|
+
finally:
|
|
294
|
+
os.close(fd)
|
|
295
|
+
os.remove(temp_filename)
|
|
296
|
+
|
|
297
|
+
if not status:
|
|
298
|
+
raise IOError(output.stderr)
|
|
299
|
+
|
|
300
|
+
if print_output:
|
|
301
|
+
click.echo(
|
|
302
|
+
"{0}file downloaded from container: {1}".format(
|
|
303
|
+
self.host.print_prefix,
|
|
304
|
+
remote_filename,
|
|
305
|
+
),
|
|
306
|
+
err=True,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return status
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class PodmanConnector(DockerConnector):
|
|
313
|
+
"""
|
|
314
|
+
The Podman connector allows you to use pyinfra to create new Podman images or modify running
|
|
315
|
+
Podman containers.
|
|
316
|
+
|
|
317
|
+
.. note::
|
|
318
|
+
|
|
319
|
+
The Podman connector allows pyinfra to target Podman containers as inventory and is
|
|
320
|
+
unrelated to the :doc:`../operations/docker` & :doc:`../facts/docker`.
|
|
321
|
+
|
|
322
|
+
You can pass either an image name or existing container ID:
|
|
323
|
+
|
|
324
|
+
+ Image - will create a new container from the image, execute operations against it, save into \
|
|
325
|
+
a new Podman image and remove the container
|
|
326
|
+
+ Existing container ID - will execute operations against the running container, leaving it \
|
|
327
|
+
running
|
|
328
|
+
|
|
329
|
+
.. code:: shell
|
|
330
|
+
|
|
331
|
+
# A Podman base image must be provided
|
|
332
|
+
pyinfra @podman/alpine:3.8 ...
|
|
333
|
+
|
|
334
|
+
# pyinfra can run on multiple Docker images in parallel
|
|
335
|
+
pyinfra @podman/alpine:3.8,@podman/ubuntu:bionic ...
|
|
336
|
+
|
|
337
|
+
# Execute against a running container
|
|
338
|
+
pyinfra @podman/2beb8c15a1b1 ...
|
|
339
|
+
|
|
340
|
+
The Podman connector is great for testing pyinfra operations locally, rather than connecting to
|
|
341
|
+
a remote host over SSH each time. This gives you a fast, local-first devloop to iterate on when
|
|
342
|
+
writing deploys, operations or facts.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
docker_cmd = "podman"
|
|
346
|
+
|
|
347
|
+
@override
|
|
348
|
+
@staticmethod
|
|
349
|
+
def make_names_data(name=None):
|
|
350
|
+
if not name:
|
|
351
|
+
raise InventoryError("No podman base ID provided!")
|
|
352
|
+
|
|
353
|
+
yield (
|
|
354
|
+
f"@podman/{name}",
|
|
355
|
+
{"docker_identifier": name},
|
|
356
|
+
["@podman"],
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Duplicate function definition to swap the docstring.
|
|
360
|
+
@override
|
|
361
|
+
def put_file(
|
|
362
|
+
self,
|
|
363
|
+
filename_or_io,
|
|
364
|
+
remote_filename,
|
|
365
|
+
remote_temp_filename=None, # ignored
|
|
366
|
+
print_output=False,
|
|
367
|
+
print_input=False,
|
|
368
|
+
**kwargs, # ignored (sudo/etc)
|
|
369
|
+
) -> bool:
|
|
370
|
+
"""
|
|
371
|
+
Upload a file/IO object to the target container by copying it to a
|
|
372
|
+
temporary location and then uploading it into the container using ``podman cp``.
|
|
373
|
+
"""
|
|
374
|
+
return super().put_file(
|
|
375
|
+
filename_or_io,
|
|
376
|
+
remote_filename,
|
|
377
|
+
remote_temp_filename, # ignored
|
|
378
|
+
print_output,
|
|
379
|
+
print_input,
|
|
380
|
+
**kwargs, # ignored (sudo/etc)
|
|
381
|
+
)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from tempfile import mkstemp
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from typing_extensions import Unpack, override
|
|
7
|
+
|
|
8
|
+
from pyinfra import logger
|
|
9
|
+
from pyinfra.api import QuoteString, StringCommand
|
|
10
|
+
from pyinfra.api.exceptions import ConnectError, InventoryError, PyinfraError
|
|
11
|
+
from pyinfra.api.util import get_file_io, memoize
|
|
12
|
+
from pyinfra.progress import progress_spinner
|
|
13
|
+
|
|
14
|
+
from .base import BaseConnector
|
|
15
|
+
from .ssh import SSHConnector
|
|
16
|
+
from .util import extract_control_arguments, make_unix_command_for_host
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pyinfra.api.arguments import ConnectorArguments
|
|
20
|
+
from pyinfra.api.host import Host
|
|
21
|
+
from pyinfra.api.state import State
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@memoize
|
|
25
|
+
def show_warning() -> None:
|
|
26
|
+
logger.warning("The @dockerssh connector is in beta!")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DockerSSHConnector(BaseConnector):
|
|
30
|
+
"""
|
|
31
|
+
**Note**: this connector is in beta!
|
|
32
|
+
|
|
33
|
+
The ``@dockerssh`` connector allows you to run commands on Docker containers \
|
|
34
|
+
on a remote machine.
|
|
35
|
+
|
|
36
|
+
.. code:: shell
|
|
37
|
+
|
|
38
|
+
# A Docker base image must be provided
|
|
39
|
+
pyinfra @dockerssh/remotehost:alpine:3.8 ...
|
|
40
|
+
|
|
41
|
+
# pyinfra can run on multiple Docker images in parallel
|
|
42
|
+
pyinfra @dockerssh/remotehost:alpine:3.8,@dockerssh/remotehost:ubuntu:bionic ...
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
handles_execution = True
|
|
46
|
+
|
|
47
|
+
ssh: SSHConnector
|
|
48
|
+
|
|
49
|
+
def __init__(self, state: "State", host: "Host"):
|
|
50
|
+
super().__init__(state, host)
|
|
51
|
+
self.ssh = SSHConnector(state, host)
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
@staticmethod
|
|
55
|
+
def make_names_data(name):
|
|
56
|
+
try:
|
|
57
|
+
hostname, image = name.split(":", 1)
|
|
58
|
+
except (AttributeError, ValueError): # failure to parse the name
|
|
59
|
+
raise InventoryError("No ssh host or docker base image provided!")
|
|
60
|
+
|
|
61
|
+
if not image:
|
|
62
|
+
raise InventoryError("No docker base image provided!")
|
|
63
|
+
|
|
64
|
+
show_warning()
|
|
65
|
+
|
|
66
|
+
yield (
|
|
67
|
+
"@dockerssh/{0}:{1}".format(hostname, image),
|
|
68
|
+
{"ssh_hostname": hostname, "docker_image": image},
|
|
69
|
+
["@dockerssh"],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
def connect(self) -> None:
|
|
74
|
+
self.ssh.connect()
|
|
75
|
+
|
|
76
|
+
if "docker_container_id" in self.host.host_data: # user can provide a docker_container_id
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
with progress_spinner({"docker run"}):
|
|
81
|
+
# last line is the container ID
|
|
82
|
+
status, output = self.ssh.run_shell_command(
|
|
83
|
+
StringCommand(
|
|
84
|
+
"docker",
|
|
85
|
+
"run",
|
|
86
|
+
"-d",
|
|
87
|
+
self.host.data.docker_image,
|
|
88
|
+
"tail",
|
|
89
|
+
"-f",
|
|
90
|
+
"/dev/null",
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
if not status:
|
|
94
|
+
raise IOError(output.stderr)
|
|
95
|
+
container_id = output.stdout_lines[-1]
|
|
96
|
+
|
|
97
|
+
except PyinfraError as e:
|
|
98
|
+
raise ConnectError(e.args[0])
|
|
99
|
+
|
|
100
|
+
self.host.host_data["docker_container_id"] = container_id
|
|
101
|
+
|
|
102
|
+
@override
|
|
103
|
+
def disconnect(self) -> None:
|
|
104
|
+
container_id = self.host.host_data["docker_container_id"][:12]
|
|
105
|
+
|
|
106
|
+
with progress_spinner({"docker commit"}):
|
|
107
|
+
_, output = self.ssh.run_shell_command(StringCommand("docker", "commit", container_id))
|
|
108
|
+
|
|
109
|
+
# Last line is the image ID, get sha256:[XXXXXXXXXX]...
|
|
110
|
+
image_id = output.stdout_lines[-1][7:19]
|
|
111
|
+
|
|
112
|
+
with progress_spinner({"docker rm"}):
|
|
113
|
+
self.ssh.run_shell_command(
|
|
114
|
+
StringCommand("docker", "rm", "-f", container_id),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
logger.info(
|
|
118
|
+
"{0}docker build complete, image ID: {1}".format(
|
|
119
|
+
self.host.print_prefix,
|
|
120
|
+
click.style(image_id, bold=True),
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@override
|
|
125
|
+
def run_shell_command(
|
|
126
|
+
self,
|
|
127
|
+
command,
|
|
128
|
+
print_output: bool = False,
|
|
129
|
+
print_input: bool = False,
|
|
130
|
+
**arguments: Unpack["ConnectorArguments"],
|
|
131
|
+
):
|
|
132
|
+
local_arguments = extract_control_arguments(arguments)
|
|
133
|
+
|
|
134
|
+
container_id = self.host.host_data["docker_container_id"]
|
|
135
|
+
|
|
136
|
+
command = make_unix_command_for_host(self.state, self.host, command, **arguments)
|
|
137
|
+
command = QuoteString(command)
|
|
138
|
+
|
|
139
|
+
docker_flags = "-it" if local_arguments.get("_get_pty") else "-i"
|
|
140
|
+
docker_command = StringCommand(
|
|
141
|
+
"docker",
|
|
142
|
+
"exec",
|
|
143
|
+
docker_flags,
|
|
144
|
+
container_id,
|
|
145
|
+
"sh",
|
|
146
|
+
"-c",
|
|
147
|
+
command,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return self.ssh.run_shell_command(
|
|
151
|
+
docker_command,
|
|
152
|
+
print_output=print_output,
|
|
153
|
+
print_input=print_input,
|
|
154
|
+
**local_arguments,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@override
|
|
158
|
+
def put_file(
|
|
159
|
+
self,
|
|
160
|
+
filename_or_io,
|
|
161
|
+
remote_filename,
|
|
162
|
+
remote_temp_filename=None,
|
|
163
|
+
print_output: bool = False,
|
|
164
|
+
print_input: bool = False,
|
|
165
|
+
**kwargs, # ignored (sudo/etc)
|
|
166
|
+
):
|
|
167
|
+
"""
|
|
168
|
+
Upload a file/IO object to the target Docker container by copying it to a
|
|
169
|
+
temporary location and then uploading it into the container using ``docker cp``.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
fd, local_temp_filename = mkstemp()
|
|
173
|
+
remote_temp_filename = remote_temp_filename or self.host.get_temp_filename(
|
|
174
|
+
local_temp_filename
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Load our file or IO object and write it to the temporary file
|
|
178
|
+
with get_file_io(filename_or_io) as file_io:
|
|
179
|
+
with open(local_temp_filename, "wb") as temp_f:
|
|
180
|
+
data = file_io.read()
|
|
181
|
+
|
|
182
|
+
if isinstance(data, str):
|
|
183
|
+
data = data.encode()
|
|
184
|
+
|
|
185
|
+
temp_f.write(data)
|
|
186
|
+
|
|
187
|
+
# upload file to remote server
|
|
188
|
+
ssh_status = self.ssh.put_file(local_temp_filename, remote_temp_filename)
|
|
189
|
+
if not ssh_status:
|
|
190
|
+
raise IOError("Failed to copy file over ssh")
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
docker_id = self.host.host_data["docker_container_id"]
|
|
194
|
+
docker_command = StringCommand(
|
|
195
|
+
"docker",
|
|
196
|
+
"cp",
|
|
197
|
+
remote_temp_filename,
|
|
198
|
+
f"{docker_id}:{remote_filename}",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
status, output = self.ssh.run_shell_command(
|
|
202
|
+
docker_command,
|
|
203
|
+
print_output=print_output,
|
|
204
|
+
print_input=print_input,
|
|
205
|
+
)
|
|
206
|
+
finally:
|
|
207
|
+
os.close(fd)
|
|
208
|
+
os.remove(local_temp_filename)
|
|
209
|
+
self.remote_remove(
|
|
210
|
+
local_temp_filename,
|
|
211
|
+
print_output=print_output,
|
|
212
|
+
print_input=print_input,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if not status:
|
|
216
|
+
raise IOError(output.stderr)
|
|
217
|
+
|
|
218
|
+
if print_output:
|
|
219
|
+
click.echo(
|
|
220
|
+
"{0}file uploaded to container: {1}".format(
|
|
221
|
+
self.host.print_prefix,
|
|
222
|
+
remote_filename,
|
|
223
|
+
),
|
|
224
|
+
err=True,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return status
|
|
228
|
+
|
|
229
|
+
@override
|
|
230
|
+
def get_file(
|
|
231
|
+
self,
|
|
232
|
+
remote_filename,
|
|
233
|
+
filename_or_io,
|
|
234
|
+
remote_temp_filename=None,
|
|
235
|
+
print_output: bool = False,
|
|
236
|
+
print_input: bool = False,
|
|
237
|
+
**kwargs, # ignored (sudo/etc)
|
|
238
|
+
):
|
|
239
|
+
"""
|
|
240
|
+
Download a file from the target Docker container by copying it to a temporary
|
|
241
|
+
location and then reading that into our final file/IO object.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
remote_temp_filename = remote_temp_filename or self.host.get_temp_filename(remote_filename)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
docker_id = self.host.host_data["docker_container_id"]
|
|
248
|
+
docker_command = StringCommand(
|
|
249
|
+
"docker",
|
|
250
|
+
"cp",
|
|
251
|
+
f"{docker_id}:{remote_filename}",
|
|
252
|
+
remote_temp_filename,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
status, output = self.ssh.run_shell_command(
|
|
256
|
+
docker_command,
|
|
257
|
+
print_output=print_output,
|
|
258
|
+
print_input=print_input,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
ssh_status = self.ssh.get_file(remote_temp_filename, filename_or_io)
|
|
262
|
+
finally:
|
|
263
|
+
self.remote_remove(
|
|
264
|
+
remote_temp_filename,
|
|
265
|
+
print_output=print_output,
|
|
266
|
+
print_input=print_input,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if not ssh_status:
|
|
270
|
+
raise IOError("failed to copy file over ssh")
|
|
271
|
+
|
|
272
|
+
if not status:
|
|
273
|
+
raise IOError(output.stderr)
|
|
274
|
+
|
|
275
|
+
if print_output:
|
|
276
|
+
click.echo(
|
|
277
|
+
"{0}file downloaded from container: {1}".format(
|
|
278
|
+
self.host.print_prefix,
|
|
279
|
+
remote_filename,
|
|
280
|
+
),
|
|
281
|
+
err=True,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return status
|
|
285
|
+
|
|
286
|
+
def remote_remove(self, filename, print_output: bool = False, print_input: bool = False):
|
|
287
|
+
"""
|
|
288
|
+
Deletes a file on a remote machine over ssh.
|
|
289
|
+
"""
|
|
290
|
+
remove_status, output = self.ssh.run_shell_command(
|
|
291
|
+
StringCommand("rm", "-f", filename),
|
|
292
|
+
print_output=print_output,
|
|
293
|
+
print_input=print_input,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if not remove_status:
|
|
297
|
+
raise IOError(output.stderr)
|