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.
Files changed (204) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +19 -3
  4. pyinfra/api/arguments.py +413 -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 +73 -18
  12. pyinfra/api/facts.py +267 -200
  13. pyinfra/api/host.py +416 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/metadata.py +69 -0
  16. pyinfra/api/operation.py +432 -262
  17. pyinfra/api/operations.py +273 -260
  18. pyinfra/api/state.py +302 -248
  19. pyinfra/api/util.py +309 -369
  20. pyinfra/connectors/base.py +173 -0
  21. pyinfra/connectors/chroot.py +212 -0
  22. pyinfra/connectors/docker.py +405 -0
  23. pyinfra/connectors/dockerssh.py +297 -0
  24. pyinfra/connectors/local.py +238 -0
  25. pyinfra/connectors/scp/__init__.py +1 -0
  26. pyinfra/connectors/scp/client.py +204 -0
  27. pyinfra/connectors/ssh.py +727 -0
  28. pyinfra/connectors/ssh_util.py +114 -0
  29. pyinfra/connectors/sshuserclient/client.py +309 -0
  30. pyinfra/connectors/sshuserclient/config.py +102 -0
  31. pyinfra/connectors/terraform.py +135 -0
  32. pyinfra/connectors/util.py +417 -0
  33. pyinfra/connectors/vagrant.py +183 -0
  34. pyinfra/context.py +145 -0
  35. pyinfra/facts/__init__.py +7 -6
  36. pyinfra/facts/apk.py +22 -7
  37. pyinfra/facts/apt.py +117 -60
  38. pyinfra/facts/brew.py +100 -15
  39. pyinfra/facts/bsdinit.py +23 -0
  40. pyinfra/facts/cargo.py +37 -0
  41. pyinfra/facts/choco.py +47 -0
  42. pyinfra/facts/crontab.py +195 -0
  43. pyinfra/facts/deb.py +94 -0
  44. pyinfra/facts/dnf.py +48 -0
  45. pyinfra/facts/docker.py +96 -23
  46. pyinfra/facts/efibootmgr.py +113 -0
  47. pyinfra/facts/files.py +629 -58
  48. pyinfra/facts/flatpak.py +77 -0
  49. pyinfra/facts/freebsd.py +70 -0
  50. pyinfra/facts/gem.py +19 -6
  51. pyinfra/facts/git.py +59 -14
  52. pyinfra/facts/gpg.py +150 -0
  53. pyinfra/facts/hardware.py +313 -167
  54. pyinfra/facts/iptables.py +72 -62
  55. pyinfra/facts/launchd.py +44 -0
  56. pyinfra/facts/lxd.py +17 -4
  57. pyinfra/facts/mysql.py +122 -86
  58. pyinfra/facts/npm.py +17 -9
  59. pyinfra/facts/openrc.py +71 -0
  60. pyinfra/facts/opkg.py +246 -0
  61. pyinfra/facts/pacman.py +50 -7
  62. pyinfra/facts/pip.py +24 -7
  63. pyinfra/facts/pipx.py +82 -0
  64. pyinfra/facts/pkg.py +15 -6
  65. pyinfra/facts/pkgin.py +35 -0
  66. pyinfra/facts/podman.py +54 -0
  67. pyinfra/facts/postgres.py +178 -0
  68. pyinfra/facts/postgresql.py +6 -147
  69. pyinfra/facts/rpm.py +105 -0
  70. pyinfra/facts/runit.py +77 -0
  71. pyinfra/facts/selinux.py +161 -0
  72. pyinfra/facts/server.py +762 -285
  73. pyinfra/facts/snap.py +88 -0
  74. pyinfra/facts/systemd.py +139 -0
  75. pyinfra/facts/sysvinit.py +59 -0
  76. pyinfra/facts/upstart.py +35 -0
  77. pyinfra/facts/util/__init__.py +17 -0
  78. pyinfra/facts/util/databases.py +4 -6
  79. pyinfra/facts/util/packaging.py +37 -6
  80. pyinfra/facts/util/units.py +30 -0
  81. pyinfra/facts/util/win_files.py +99 -0
  82. pyinfra/facts/vzctl.py +20 -13
  83. pyinfra/facts/xbps.py +35 -0
  84. pyinfra/facts/yum.py +34 -40
  85. pyinfra/facts/zfs.py +77 -0
  86. pyinfra/facts/zypper.py +42 -0
  87. pyinfra/local.py +45 -83
  88. pyinfra/operations/__init__.py +12 -0
  89. pyinfra/operations/apk.py +99 -0
  90. pyinfra/operations/apt.py +496 -0
  91. pyinfra/operations/brew.py +232 -0
  92. pyinfra/operations/bsdinit.py +59 -0
  93. pyinfra/operations/cargo.py +45 -0
  94. pyinfra/operations/choco.py +61 -0
  95. pyinfra/operations/crontab.py +194 -0
  96. pyinfra/operations/dnf.py +213 -0
  97. pyinfra/operations/docker.py +492 -0
  98. pyinfra/operations/files.py +2014 -0
  99. pyinfra/operations/flatpak.py +95 -0
  100. pyinfra/operations/freebsd/__init__.py +12 -0
  101. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  102. pyinfra/operations/freebsd/pkg.py +219 -0
  103. pyinfra/operations/freebsd/service.py +116 -0
  104. pyinfra/operations/freebsd/sysrc.py +92 -0
  105. pyinfra/operations/gem.py +48 -0
  106. pyinfra/operations/git.py +420 -0
  107. pyinfra/operations/iptables.py +312 -0
  108. pyinfra/operations/launchd.py +45 -0
  109. pyinfra/operations/lxd.py +69 -0
  110. pyinfra/operations/mysql.py +610 -0
  111. pyinfra/operations/npm.py +57 -0
  112. pyinfra/operations/openrc.py +63 -0
  113. pyinfra/operations/opkg.py +89 -0
  114. pyinfra/operations/pacman.py +82 -0
  115. pyinfra/operations/pip.py +206 -0
  116. pyinfra/operations/pipx.py +103 -0
  117. pyinfra/operations/pkg.py +71 -0
  118. pyinfra/operations/pkgin.py +92 -0
  119. pyinfra/operations/postgres.py +437 -0
  120. pyinfra/operations/postgresql.py +30 -0
  121. pyinfra/operations/puppet.py +41 -0
  122. pyinfra/operations/python.py +73 -0
  123. pyinfra/operations/runit.py +184 -0
  124. pyinfra/operations/selinux.py +190 -0
  125. pyinfra/operations/server.py +1100 -0
  126. pyinfra/operations/snap.py +118 -0
  127. pyinfra/operations/ssh.py +217 -0
  128. pyinfra/operations/systemd.py +150 -0
  129. pyinfra/operations/sysvinit.py +142 -0
  130. pyinfra/operations/upstart.py +68 -0
  131. pyinfra/operations/util/__init__.py +12 -0
  132. pyinfra/operations/util/docker.py +407 -0
  133. pyinfra/operations/util/files.py +247 -0
  134. pyinfra/operations/util/packaging.py +338 -0
  135. pyinfra/operations/util/service.py +46 -0
  136. pyinfra/operations/vzctl.py +137 -0
  137. pyinfra/operations/xbps.py +78 -0
  138. pyinfra/operations/yum.py +213 -0
  139. pyinfra/operations/zfs.py +176 -0
  140. pyinfra/operations/zypper.py +193 -0
  141. pyinfra/progress.py +44 -32
  142. pyinfra/py.typed +0 -0
  143. pyinfra/version.py +9 -1
  144. pyinfra-3.6.dist-info/METADATA +142 -0
  145. pyinfra-3.6.dist-info/RECORD +160 -0
  146. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
  147. pyinfra-3.6.dist-info/entry_points.txt +12 -0
  148. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
  149. pyinfra_cli/__init__.py +1 -0
  150. pyinfra_cli/cli.py +793 -0
  151. pyinfra_cli/commands.py +66 -0
  152. pyinfra_cli/exceptions.py +155 -65
  153. pyinfra_cli/inventory.py +233 -89
  154. pyinfra_cli/log.py +39 -43
  155. pyinfra_cli/main.py +26 -495
  156. pyinfra_cli/prints.py +215 -156
  157. pyinfra_cli/util.py +172 -105
  158. pyinfra_cli/virtualenv.py +25 -20
  159. pyinfra/api/connectors/__init__.py +0 -21
  160. pyinfra/api/connectors/ansible.py +0 -99
  161. pyinfra/api/connectors/docker.py +0 -178
  162. pyinfra/api/connectors/local.py +0 -169
  163. pyinfra/api/connectors/ssh.py +0 -402
  164. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  165. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  166. pyinfra/api/connectors/util.py +0 -63
  167. pyinfra/api/connectors/vagrant.py +0 -155
  168. pyinfra/facts/init.py +0 -176
  169. pyinfra/facts/util/files.py +0 -102
  170. pyinfra/hook.py +0 -41
  171. pyinfra/modules/__init__.py +0 -11
  172. pyinfra/modules/apk.py +0 -64
  173. pyinfra/modules/apt.py +0 -272
  174. pyinfra/modules/brew.py +0 -122
  175. pyinfra/modules/files.py +0 -711
  176. pyinfra/modules/gem.py +0 -30
  177. pyinfra/modules/git.py +0 -115
  178. pyinfra/modules/init.py +0 -344
  179. pyinfra/modules/iptables.py +0 -271
  180. pyinfra/modules/lxd.py +0 -45
  181. pyinfra/modules/mysql.py +0 -347
  182. pyinfra/modules/npm.py +0 -47
  183. pyinfra/modules/pacman.py +0 -60
  184. pyinfra/modules/pip.py +0 -99
  185. pyinfra/modules/pkg.py +0 -43
  186. pyinfra/modules/postgresql.py +0 -245
  187. pyinfra/modules/puppet.py +0 -20
  188. pyinfra/modules/python.py +0 -37
  189. pyinfra/modules/server.py +0 -524
  190. pyinfra/modules/ssh.py +0 -150
  191. pyinfra/modules/util/files.py +0 -52
  192. pyinfra/modules/util/packaging.py +0 -118
  193. pyinfra/modules/vzctl.py +0 -133
  194. pyinfra/modules/yum.py +0 -171
  195. pyinfra/pseudo_modules.py +0 -64
  196. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  197. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  198. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  199. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  200. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  201. pyinfra_cli/__main__.py +0 -40
  202. pyinfra_cli/config.py +0 -92
  203. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  204. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
@@ -0,0 +1,417 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+ from dataclasses import dataclass
5
+ from getpass import getpass
6
+ from queue import Queue
7
+ from socket import timeout as timeout_error
8
+ from subprocess import PIPE, Popen
9
+ from typing import TYPE_CHECKING, Callable, Iterable, Optional, Union
10
+
11
+ import click
12
+ import gevent
13
+
14
+ from pyinfra import logger
15
+ from pyinfra.api import MaskString, QuoteString, StringCommand
16
+ from pyinfra.api.util import memoize
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
+ SUDO_ASKPASS_ENV_VAR = "PYINFRA_SUDO_PASSWORD"
25
+
26
+
27
+ SUDO_ASKPASS_COMMAND = r"""
28
+ temp=$(mktemp "${{TMPDIR:={0}}}/pyinfra-sudo-askpass-XXXXXXXXXXXX")
29
+ cat >"$temp"<<'__EOF__'
30
+ #!/bin/sh
31
+ printf '%s\n' "${1}"
32
+ __EOF__
33
+ chmod 755 "$temp"
34
+ echo "$temp"
35
+ """
36
+
37
+
38
+ def run_local_process(
39
+ command: str,
40
+ stdin=None,
41
+ timeout: Optional[int] = None,
42
+ print_output: bool = False,
43
+ print_prefix: str = "",
44
+ ) -> tuple[int, "CommandOutput"]:
45
+ process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE)
46
+
47
+ assert process.stdout is not None
48
+ assert process.stderr is not None
49
+ assert process.stdin is not None
50
+
51
+ # Write any stdin and then close it
52
+ if stdin:
53
+ write_stdin(stdin, process.stdin)
54
+ process.stdin.close()
55
+
56
+ combined_output = read_output_buffers(
57
+ process.stdout,
58
+ process.stderr,
59
+ timeout=timeout,
60
+ print_output=print_output,
61
+ print_prefix=print_prefix,
62
+ )
63
+
64
+ logger.debug("--> Waiting for exit status...")
65
+ process.wait()
66
+ logger.debug("--> Command exit status: %i", process.returncode)
67
+
68
+ # Close any open file descriptors
69
+ process.stdout.close()
70
+ process.stderr.close()
71
+
72
+ return process.returncode, combined_output
73
+
74
+
75
+ # Command output buffer handling
76
+ #
77
+
78
+
79
+ @dataclass
80
+ class OutputLine:
81
+ buffer_name: str
82
+ line: str
83
+
84
+
85
+ @dataclass
86
+ class CommandOutput:
87
+ combined_lines: list[OutputLine]
88
+
89
+ def __iter__(self):
90
+ yield from self.combined_lines
91
+
92
+ @property
93
+ def output_lines(self) -> list[str]:
94
+ return [line.line for line in self.combined_lines]
95
+
96
+ @property
97
+ def output(self) -> str:
98
+ return "\n".join(self.output_lines)
99
+
100
+ @property
101
+ def stdout_lines(self) -> list[str]:
102
+ return [line.line for line in self.combined_lines if line.buffer_name == "stdout"]
103
+
104
+ @property
105
+ def stdout(self) -> str:
106
+ return "\n".join(self.stdout_lines)
107
+
108
+ @property
109
+ def stderr_lines(self) -> list[str]:
110
+ return [line.line for line in self.combined_lines if line.buffer_name == "stderr"]
111
+
112
+ @property
113
+ def stderr(self) -> str:
114
+ return "\n".join(self.stderr_lines)
115
+
116
+
117
+ def read_buffer(
118
+ name: str,
119
+ io: Iterable,
120
+ output_queue: Queue[OutputLine],
121
+ print_output=False,
122
+ print_func=None,
123
+ ) -> None:
124
+ """
125
+ Reads a file-like buffer object into lines and optionally prints the output.
126
+ """
127
+
128
+ def _print(line):
129
+ if print_func:
130
+ line = print_func(line)
131
+
132
+ click.echo(line, err=True)
133
+
134
+ for line in io:
135
+ # Handle local Popen shells returning list of bytes, not strings
136
+ if not isinstance(line, str):
137
+ line = line.decode("utf-8")
138
+
139
+ line = line.rstrip("\n")
140
+ output_queue.put(OutputLine(name, line))
141
+
142
+ if print_output:
143
+ _print(line)
144
+
145
+
146
+ def read_output_buffers(
147
+ stdout_buffer: Iterable,
148
+ stderr_buffer: Iterable,
149
+ timeout: Optional[int],
150
+ print_output: bool,
151
+ print_prefix: str,
152
+ ) -> CommandOutput:
153
+ output_queue: Queue[OutputLine] = Queue()
154
+
155
+ # Iterate through outputs to get an exit status and generate desired list
156
+ # output, done in two greenlets so stdout isn't printed before stderr. Not
157
+ # attached to state.pool to avoid blocking it with 2x n-hosts greenlets.
158
+ stdout_reader = gevent.spawn(
159
+ read_buffer,
160
+ "stdout",
161
+ stdout_buffer,
162
+ output_queue,
163
+ print_output=print_output,
164
+ print_func=lambda line: "{0}{1}".format(print_prefix, line),
165
+ )
166
+ stderr_reader = gevent.spawn(
167
+ read_buffer,
168
+ "stderr",
169
+ stderr_buffer,
170
+ output_queue,
171
+ print_output=print_output,
172
+ print_func=lambda line: "{0}{1}".format(
173
+ print_prefix,
174
+ click.style(line, "red"),
175
+ ),
176
+ )
177
+
178
+ # Wait on output, with our timeout (or None)
179
+ greenlets = gevent.wait((stdout_reader, stderr_reader), timeout=timeout)
180
+
181
+ # Timeout doesn't raise an exception, but gevent.wait returns the greenlets
182
+ # which did complete. So if both haven't completed, we kill them and fail
183
+ # with a timeout.
184
+ if len(greenlets) != 2:
185
+ stdout_reader.kill()
186
+ stderr_reader.kill()
187
+
188
+ raise timeout_error()
189
+
190
+ return CommandOutput(list(output_queue.queue))
191
+
192
+
193
+ # Connector execution control
194
+ #
195
+
196
+
197
+ def execute_command_with_sudo_retry(
198
+ host: "Host",
199
+ command_arguments: "ConnectorArguments",
200
+ execute_command: Callable[..., tuple[int, CommandOutput]],
201
+ ) -> tuple[int, CommandOutput]:
202
+ return_code, output = execute_command()
203
+
204
+ # If we failed look for a sudo password prompt line and re-submit using the sudo password. Look
205
+ # at all lines here in case anything else gets printed, eg in:
206
+ # https://github.com/pyinfra-dev/pyinfra/issues/1292
207
+ if return_code != 0 and output and output.combined_lines:
208
+ for line in reversed(output.combined_lines):
209
+ if line.line.strip() == "sudo: a password is required":
210
+ # If we need a password, ask the user for it and attach to the host
211
+ # internal connector data for use when executing future commands.
212
+ sudo_password = getpass("{0}sudo password: ".format(host.print_prefix))
213
+ host.connector_data["prompted_sudo_password"] = sudo_password
214
+ return_code, output = execute_command()
215
+ break
216
+
217
+ return return_code, output
218
+
219
+
220
+ def write_stdin(stdin, buffer):
221
+ if hasattr(stdin, "readlines"):
222
+ stdin = stdin.readlines()
223
+ if not isinstance(stdin, (list, tuple)):
224
+ stdin = [stdin]
225
+
226
+ for line in stdin:
227
+ if not line.endswith("\n"):
228
+ line = "{0}\n".format(line)
229
+ line = line.encode()
230
+ buffer.write(line)
231
+ buffer.close()
232
+
233
+
234
+ def remove_any_sudo_askpass_file(host) -> None:
235
+ sudo_askpass_path = host.connector_data.get("sudo_askpass_path")
236
+ if sudo_askpass_path:
237
+ host.run_shell_command("rm -f {0}".format(sudo_askpass_path))
238
+ host.connector_data["sudo_askpass_path"] = None
239
+
240
+
241
+ @memoize
242
+ def _show_use_su_login_warning() -> None:
243
+ logger.warning(
244
+ (
245
+ "Using `use_su_login` may not work: "
246
+ "some systems (MacOS, OpenBSD) ignore the flag when executing a command, "
247
+ "use `sudo` + `use_sudo_login` instead."
248
+ ),
249
+ )
250
+
251
+
252
+ def extract_control_arguments(arguments: "ConnectorArguments") -> "ConnectorArguments":
253
+ control_arguments: "ConnectorArguments" = {}
254
+
255
+ if "_success_exit_codes" in arguments:
256
+ control_arguments["_success_exit_codes"] = arguments.pop("_success_exit_codes")
257
+ if "_timeout" in arguments:
258
+ control_arguments["_timeout"] = arguments.pop("_timeout")
259
+ if "_get_pty" in arguments:
260
+ control_arguments["_get_pty"] = arguments.pop("_get_pty")
261
+ if "_stdin" in arguments:
262
+ control_arguments["_stdin"] = arguments.pop("_stdin")
263
+
264
+ return control_arguments
265
+
266
+
267
+ def _ensure_sudo_askpass_set_for_host(host: "Host"):
268
+ if host.connector_data.get("sudo_askpass_path"):
269
+ return
270
+ _, output = host.run_shell_command(
271
+ SUDO_ASKPASS_COMMAND.format(host.get_temp_dir_config(), SUDO_ASKPASS_ENV_VAR)
272
+ )
273
+ host.connector_data["sudo_askpass_path"] = shlex.quote(output.stdout_lines[0])
274
+
275
+
276
+ def make_unix_command_for_host(
277
+ state: "State",
278
+ host: "Host",
279
+ command: StringCommand,
280
+ **command_arguments,
281
+ ) -> StringCommand:
282
+ if not command_arguments.get("_sudo"):
283
+ # If no sudo, we've nothing to do here
284
+ return make_unix_command(command, **command_arguments)
285
+
286
+ # If the sudo password is not set in the direct arguments,
287
+ # set it from the connector data value.
288
+ if "_sudo_password" not in command_arguments or not command_arguments["_sudo_password"]:
289
+ command_arguments["_sudo_password"] = host.connector_data.get("prompted_sudo_password")
290
+
291
+ if command_arguments["_sudo_password"]:
292
+ # Ensure the askpass path is correctly set and passed through
293
+ _ensure_sudo_askpass_set_for_host(host)
294
+ command_arguments["_sudo_askpass_path"] = host.connector_data["sudo_askpass_path"]
295
+ return make_unix_command(command, **command_arguments)
296
+
297
+
298
+ # Connector command generation
299
+ #
300
+
301
+
302
+ def make_unix_command(
303
+ command: StringCommand,
304
+ _env=None,
305
+ _chdir=None,
306
+ _shell_executable="sh",
307
+ # Su config
308
+ _su_user=None,
309
+ _use_su_login=False,
310
+ _su_shell=None,
311
+ _preserve_su_env=False,
312
+ # Sudo config
313
+ _sudo=False,
314
+ _sudo_user=None,
315
+ _use_sudo_login=False,
316
+ _sudo_password="",
317
+ _sudo_askpass_path=None,
318
+ _preserve_sudo_env=False,
319
+ # Doas config
320
+ _doas=False,
321
+ _doas_user=None,
322
+ # Retry config (ignored in command generation but passed through)
323
+ _retries=0,
324
+ _retry_delay=0,
325
+ _retry_until=None,
326
+ # Temp dir config (ignored in command generation, used for temp file path generation)
327
+ _temp_dir=None,
328
+ ) -> StringCommand:
329
+ """
330
+ Builds a shell command with various kwargs.
331
+ """
332
+
333
+ if _shell_executable is not None and not isinstance(_shell_executable, str):
334
+ _shell_executable = "sh"
335
+
336
+ if _env:
337
+ env_string = " ".join(['"{0}={1}"'.format(key, value) for key, value in _env.items()])
338
+ command = StringCommand("export", env_string, "&&", command)
339
+
340
+ if _chdir:
341
+ command = StringCommand("cd", _chdir, "&&", command)
342
+
343
+ command_bits: list[Union[str, StringCommand, QuoteString]] = []
344
+
345
+ if _doas:
346
+ command_bits.extend(["doas", "-n"])
347
+
348
+ if _doas_user:
349
+ command_bits.extend(["-u", _doas_user])
350
+
351
+ if _sudo_password and _sudo_askpass_path:
352
+ command_bits.extend(
353
+ [
354
+ "env",
355
+ "SUDO_ASKPASS={0}".format(_sudo_askpass_path),
356
+ MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, shlex.quote(_sudo_password))),
357
+ ],
358
+ )
359
+
360
+ if _sudo:
361
+ command_bits.extend(["sudo", "-H"])
362
+
363
+ if _sudo_password:
364
+ command_bits.extend(["-A", "-k"]) # use askpass, disable cache
365
+ else:
366
+ command_bits.append("-n") # disable prompt/interactivity
367
+
368
+ if _use_sudo_login:
369
+ command_bits.append("-i")
370
+
371
+ if _preserve_sudo_env:
372
+ command_bits.append("-E")
373
+
374
+ if _sudo_user:
375
+ command_bits.extend(("-u", _sudo_user))
376
+
377
+ if _su_user:
378
+ command_bits.append("su")
379
+
380
+ if _use_su_login:
381
+ _show_use_su_login_warning()
382
+ command_bits.append("-l")
383
+
384
+ if _preserve_su_env:
385
+ command_bits.append("-m")
386
+
387
+ if _su_shell:
388
+ command_bits.extend(["-s", "`which {0}`".format(_su_shell)])
389
+
390
+ command_bits.extend([_su_user, "-c"])
391
+
392
+ if _shell_executable is not None:
393
+ # Quote the whole shell -c 'command' as BSD `su` does not have a shell option
394
+ command_bits.append(
395
+ QuoteString(StringCommand(_shell_executable, "-c", QuoteString(command))),
396
+ )
397
+ else:
398
+ command_bits.append(QuoteString(StringCommand(command)))
399
+ else:
400
+ if _shell_executable is not None:
401
+ command_bits.extend([_shell_executable, "-c", QuoteString(command)])
402
+ else:
403
+ command_bits.extend([command])
404
+
405
+ return StringCommand(*command_bits)
406
+
407
+
408
+ def make_win_command(command):
409
+ """
410
+ Builds a windows command with various kwargs.
411
+ """
412
+
413
+ # Quote the command as a string
414
+ command = shlex.quote(str(command))
415
+ command = "{0}".format(command)
416
+
417
+ return command
@@ -0,0 +1,183 @@
1
+ import json
2
+ from os import path
3
+ from queue import Queue
4
+ from threading import Thread
5
+
6
+ from typing_extensions import override
7
+
8
+ from pyinfra import local, logger
9
+ from pyinfra.api.exceptions import InventoryError
10
+ from pyinfra.api.util import memoize
11
+ from pyinfra.progress import progress_spinner
12
+
13
+ from .base import BaseConnector
14
+
15
+
16
+ def _get_vagrant_ssh_config(queue, progress, target):
17
+ logger.debug("Loading SSH config for %s", target)
18
+
19
+ queue.put(
20
+ local.shell(
21
+ "vagrant ssh-config {0}".format(target),
22
+ splitlines=True,
23
+ ),
24
+ )
25
+
26
+ progress(target)
27
+
28
+
29
+ @memoize
30
+ def get_vagrant_config(limit=None):
31
+ logger.info("Getting Vagrant config...")
32
+
33
+ if limit and not isinstance(limit, (list, tuple)):
34
+ limit = [limit]
35
+
36
+ with progress_spinner({"vagrant status"}) as progress:
37
+ output = local.shell(
38
+ "vagrant status --machine-readable",
39
+ splitlines=True,
40
+ )
41
+ progress("vagrant status")
42
+
43
+ targets = []
44
+
45
+ for line in output:
46
+ line = line.strip()
47
+ _, target, type_, data = line.split(",", 3)
48
+
49
+ # Skip anything not in the limit
50
+ if limit is not None and target not in limit:
51
+ continue
52
+
53
+ # For each running container - fetch it's SSH config in a thread - this
54
+ # is because Vagrant *really* slow to run each command.
55
+ if type_ == "state" and data == "running":
56
+ targets.append(target)
57
+
58
+ threads = []
59
+ config_queue = Queue() # type: ignore
60
+
61
+ with progress_spinner(targets) as progress:
62
+ for target in targets:
63
+ thread = Thread(
64
+ target=_get_vagrant_ssh_config,
65
+ args=(config_queue, progress, target),
66
+ )
67
+ threads.append(thread)
68
+ thread.start()
69
+
70
+ for thread in threads:
71
+ thread.join()
72
+
73
+ queue_items = list(config_queue.queue)
74
+
75
+ lines = []
76
+ for output in queue_items:
77
+ lines.extend([ln.strip() for ln in output])
78
+
79
+ return lines
80
+
81
+
82
+ @memoize
83
+ def get_vagrant_options():
84
+ if path.exists("@vagrant.json"):
85
+ with open("@vagrant.json", "r", encoding="utf-8") as f:
86
+ return json.loads(f.read())
87
+ return {}
88
+
89
+
90
+ def _make_name_data(host):
91
+ vagrant_options = get_vagrant_options()
92
+ vagrant_host = host["Host"]
93
+
94
+ data = {
95
+ "ssh_hostname": host["HostName"],
96
+ }
97
+
98
+ for config_key, data_key, data_cast in (
99
+ ("Port", "ssh_port", int),
100
+ ("User", "ssh_user", str),
101
+ ("IdentityFile", "ssh_key", str),
102
+ ):
103
+ if config_key in host:
104
+ data[data_key] = data_cast(host[config_key])
105
+
106
+ # Update any configured JSON data
107
+ if vagrant_host in vagrant_options.get("data", {}):
108
+ data.update(vagrant_options["data"][vagrant_host])
109
+
110
+ # Work out groups
111
+ groups = vagrant_options.get("groups", {}).get(vagrant_host, [])
112
+
113
+ if "@vagrant" not in groups:
114
+ groups.append("@vagrant")
115
+
116
+ return "@vagrant/{0}".format(host["Host"]), data, groups
117
+
118
+
119
+ class VagrantInventoryConnector(BaseConnector):
120
+ """
121
+ The ``@vagrant`` connector reads the current Vagrant status and generates an
122
+ inventory for any running VMs.
123
+
124
+ .. code:: shell
125
+
126
+ # Run on all hosts
127
+ pyinfra @vagrant ...
128
+
129
+ # Run on a specific VM
130
+ pyinfra @vagrant/my-vm-name ...
131
+
132
+ # Run on multiple named VMs
133
+ pyinfra @vagrant/my-vm-name,@vagrant/another-vm-name ...
134
+ """
135
+
136
+ @override
137
+ @staticmethod
138
+ def make_names_data(name=None):
139
+ vagrant_ssh_info = get_vagrant_config(name)
140
+
141
+ logger.debug("Got Vagrant SSH info: \n%s", vagrant_ssh_info)
142
+
143
+ hosts = []
144
+ current_host = None
145
+
146
+ for line in vagrant_ssh_info:
147
+ # Vagrant outputs an empty line between each host
148
+ if not line:
149
+ if current_host:
150
+ hosts.append(_make_name_data(current_host))
151
+
152
+ current_host = None
153
+ continue
154
+
155
+ key, value = line.split(" ", 1)
156
+
157
+ if key == "Host":
158
+ if current_host:
159
+ hosts.append(_make_name_data(current_host))
160
+
161
+ # Set the new host
162
+ current_host = {
163
+ key: value,
164
+ }
165
+
166
+ elif current_host:
167
+ current_host[key] = value
168
+
169
+ else:
170
+ logger.debug("Extra Vagrant SSH key/value (%s=%s)", key, value)
171
+
172
+ if current_host:
173
+ hosts.append(_make_name_data(current_host))
174
+
175
+ if not hosts:
176
+ if name:
177
+ raise InventoryError(
178
+ "No running Vagrant instances matching `{0}` found!".format(name)
179
+ )
180
+ raise InventoryError("No running Vagrant instances found!")
181
+
182
+ for host in hosts:
183
+ yield host