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.
Files changed (203) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +18 -3
  4. pyinfra/api/arguments.py +406 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +67 -18
  12. pyinfra/api/facts.py +253 -202
  13. pyinfra/api/host.py +413 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/operation.py +432 -262
  16. pyinfra/api/operations.py +273 -260
  17. pyinfra/api/state.py +302 -248
  18. pyinfra/api/util.py +291 -368
  19. pyinfra/connectors/base.py +173 -0
  20. pyinfra/connectors/chroot.py +212 -0
  21. pyinfra/connectors/docker.py +381 -0
  22. pyinfra/connectors/dockerssh.py +297 -0
  23. pyinfra/connectors/local.py +238 -0
  24. pyinfra/connectors/scp/__init__.py +1 -0
  25. pyinfra/connectors/scp/client.py +204 -0
  26. pyinfra/connectors/ssh.py +670 -0
  27. pyinfra/connectors/ssh_util.py +114 -0
  28. pyinfra/connectors/sshuserclient/client.py +309 -0
  29. pyinfra/connectors/sshuserclient/config.py +102 -0
  30. pyinfra/connectors/terraform.py +135 -0
  31. pyinfra/connectors/util.py +410 -0
  32. pyinfra/connectors/vagrant.py +183 -0
  33. pyinfra/context.py +145 -0
  34. pyinfra/facts/__init__.py +7 -6
  35. pyinfra/facts/apk.py +22 -7
  36. pyinfra/facts/apt.py +117 -60
  37. pyinfra/facts/brew.py +100 -15
  38. pyinfra/facts/bsdinit.py +23 -0
  39. pyinfra/facts/cargo.py +37 -0
  40. pyinfra/facts/choco.py +47 -0
  41. pyinfra/facts/crontab.py +195 -0
  42. pyinfra/facts/deb.py +94 -0
  43. pyinfra/facts/dnf.py +48 -0
  44. pyinfra/facts/docker.py +96 -23
  45. pyinfra/facts/efibootmgr.py +113 -0
  46. pyinfra/facts/files.py +630 -58
  47. pyinfra/facts/flatpak.py +77 -0
  48. pyinfra/facts/freebsd.py +70 -0
  49. pyinfra/facts/gem.py +19 -6
  50. pyinfra/facts/git.py +59 -14
  51. pyinfra/facts/gpg.py +150 -0
  52. pyinfra/facts/hardware.py +313 -167
  53. pyinfra/facts/iptables.py +72 -62
  54. pyinfra/facts/launchd.py +44 -0
  55. pyinfra/facts/lxd.py +17 -4
  56. pyinfra/facts/mysql.py +122 -86
  57. pyinfra/facts/npm.py +17 -9
  58. pyinfra/facts/openrc.py +71 -0
  59. pyinfra/facts/opkg.py +246 -0
  60. pyinfra/facts/pacman.py +50 -7
  61. pyinfra/facts/pip.py +24 -7
  62. pyinfra/facts/pipx.py +82 -0
  63. pyinfra/facts/pkg.py +15 -6
  64. pyinfra/facts/pkgin.py +35 -0
  65. pyinfra/facts/podman.py +54 -0
  66. pyinfra/facts/postgres.py +178 -0
  67. pyinfra/facts/postgresql.py +6 -147
  68. pyinfra/facts/rpm.py +105 -0
  69. pyinfra/facts/runit.py +77 -0
  70. pyinfra/facts/selinux.py +161 -0
  71. pyinfra/facts/server.py +746 -285
  72. pyinfra/facts/snap.py +88 -0
  73. pyinfra/facts/systemd.py +139 -0
  74. pyinfra/facts/sysvinit.py +59 -0
  75. pyinfra/facts/upstart.py +35 -0
  76. pyinfra/facts/util/__init__.py +17 -0
  77. pyinfra/facts/util/databases.py +4 -6
  78. pyinfra/facts/util/packaging.py +37 -6
  79. pyinfra/facts/util/units.py +30 -0
  80. pyinfra/facts/util/win_files.py +99 -0
  81. pyinfra/facts/vzctl.py +20 -13
  82. pyinfra/facts/xbps.py +35 -0
  83. pyinfra/facts/yum.py +34 -40
  84. pyinfra/facts/zfs.py +77 -0
  85. pyinfra/facts/zypper.py +42 -0
  86. pyinfra/local.py +45 -83
  87. pyinfra/operations/__init__.py +12 -0
  88. pyinfra/operations/apk.py +98 -0
  89. pyinfra/operations/apt.py +488 -0
  90. pyinfra/operations/brew.py +231 -0
  91. pyinfra/operations/bsdinit.py +59 -0
  92. pyinfra/operations/cargo.py +45 -0
  93. pyinfra/operations/choco.py +61 -0
  94. pyinfra/operations/crontab.py +191 -0
  95. pyinfra/operations/dnf.py +210 -0
  96. pyinfra/operations/docker.py +446 -0
  97. pyinfra/operations/files.py +1939 -0
  98. pyinfra/operations/flatpak.py +94 -0
  99. pyinfra/operations/freebsd/__init__.py +12 -0
  100. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  101. pyinfra/operations/freebsd/pkg.py +219 -0
  102. pyinfra/operations/freebsd/service.py +116 -0
  103. pyinfra/operations/freebsd/sysrc.py +92 -0
  104. pyinfra/operations/gem.py +47 -0
  105. pyinfra/operations/git.py +419 -0
  106. pyinfra/operations/iptables.py +311 -0
  107. pyinfra/operations/launchd.py +45 -0
  108. pyinfra/operations/lxd.py +68 -0
  109. pyinfra/operations/mysql.py +609 -0
  110. pyinfra/operations/npm.py +57 -0
  111. pyinfra/operations/openrc.py +63 -0
  112. pyinfra/operations/opkg.py +88 -0
  113. pyinfra/operations/pacman.py +81 -0
  114. pyinfra/operations/pip.py +205 -0
  115. pyinfra/operations/pipx.py +102 -0
  116. pyinfra/operations/pkg.py +70 -0
  117. pyinfra/operations/pkgin.py +91 -0
  118. pyinfra/operations/postgres.py +436 -0
  119. pyinfra/operations/postgresql.py +30 -0
  120. pyinfra/operations/puppet.py +40 -0
  121. pyinfra/operations/python.py +72 -0
  122. pyinfra/operations/runit.py +184 -0
  123. pyinfra/operations/selinux.py +189 -0
  124. pyinfra/operations/server.py +1099 -0
  125. pyinfra/operations/snap.py +117 -0
  126. pyinfra/operations/ssh.py +216 -0
  127. pyinfra/operations/systemd.py +149 -0
  128. pyinfra/operations/sysvinit.py +141 -0
  129. pyinfra/operations/upstart.py +68 -0
  130. pyinfra/operations/util/__init__.py +12 -0
  131. pyinfra/operations/util/docker.py +251 -0
  132. pyinfra/operations/util/files.py +247 -0
  133. pyinfra/operations/util/packaging.py +336 -0
  134. pyinfra/operations/util/service.py +46 -0
  135. pyinfra/operations/vzctl.py +137 -0
  136. pyinfra/operations/xbps.py +77 -0
  137. pyinfra/operations/yum.py +210 -0
  138. pyinfra/operations/zfs.py +175 -0
  139. pyinfra/operations/zypper.py +192 -0
  140. pyinfra/progress.py +44 -32
  141. pyinfra/py.typed +0 -0
  142. pyinfra/version.py +9 -1
  143. pyinfra-3.5.1.dist-info/METADATA +141 -0
  144. pyinfra-3.5.1.dist-info/RECORD +159 -0
  145. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
  146. pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
  147. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
  148. pyinfra_cli/__init__.py +1 -0
  149. pyinfra_cli/cli.py +780 -0
  150. pyinfra_cli/commands.py +66 -0
  151. pyinfra_cli/exceptions.py +155 -65
  152. pyinfra_cli/inventory.py +233 -89
  153. pyinfra_cli/log.py +39 -43
  154. pyinfra_cli/main.py +26 -495
  155. pyinfra_cli/prints.py +215 -156
  156. pyinfra_cli/util.py +172 -105
  157. pyinfra_cli/virtualenv.py +25 -20
  158. pyinfra/api/connectors/__init__.py +0 -21
  159. pyinfra/api/connectors/ansible.py +0 -99
  160. pyinfra/api/connectors/docker.py +0 -178
  161. pyinfra/api/connectors/local.py +0 -169
  162. pyinfra/api/connectors/ssh.py +0 -402
  163. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  164. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  165. pyinfra/api/connectors/util.py +0 -63
  166. pyinfra/api/connectors/vagrant.py +0 -155
  167. pyinfra/facts/init.py +0 -176
  168. pyinfra/facts/util/files.py +0 -102
  169. pyinfra/hook.py +0 -41
  170. pyinfra/modules/__init__.py +0 -11
  171. pyinfra/modules/apk.py +0 -64
  172. pyinfra/modules/apt.py +0 -272
  173. pyinfra/modules/brew.py +0 -122
  174. pyinfra/modules/files.py +0 -711
  175. pyinfra/modules/gem.py +0 -30
  176. pyinfra/modules/git.py +0 -115
  177. pyinfra/modules/init.py +0 -344
  178. pyinfra/modules/iptables.py +0 -271
  179. pyinfra/modules/lxd.py +0 -45
  180. pyinfra/modules/mysql.py +0 -347
  181. pyinfra/modules/npm.py +0 -47
  182. pyinfra/modules/pacman.py +0 -60
  183. pyinfra/modules/pip.py +0 -99
  184. pyinfra/modules/pkg.py +0 -43
  185. pyinfra/modules/postgresql.py +0 -245
  186. pyinfra/modules/puppet.py +0 -20
  187. pyinfra/modules/python.py +0 -37
  188. pyinfra/modules/server.py +0 -524
  189. pyinfra/modules/ssh.py +0 -150
  190. pyinfra/modules/util/files.py +0 -52
  191. pyinfra/modules/util/packaging.py +0 -118
  192. pyinfra/modules/vzctl.py +0 -133
  193. pyinfra/modules/yum.py +0 -171
  194. pyinfra/pseudo_modules.py +0 -64
  195. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  196. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  197. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  198. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  199. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  200. pyinfra_cli/__main__.py +0 -40
  201. pyinfra_cli/config.py +0 -92
  202. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  203. /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)