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,410 @@
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
+ SUDO_ASKPASS_COMMAND = r"""
26
+ temp=$(mktemp "${{TMPDIR:=/tmp}}/pyinfra-sudo-askpass-XXXXXXXXXXXX")
27
+ cat >"$temp"<<'__EOF__'
28
+ #!/bin/sh
29
+ printf '%s\n' "${0}"
30
+ __EOF__
31
+ chmod 755 "$temp"
32
+ echo "$temp"
33
+ """.format(
34
+ SUDO_ASKPASS_ENV_VAR,
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
+ if stdin:
48
+ write_stdin(stdin, process.stdin)
49
+
50
+ assert process.stdout is not None
51
+ assert process.stderr is not None
52
+
53
+ combined_output = read_output_buffers(
54
+ process.stdout,
55
+ process.stderr,
56
+ timeout=timeout,
57
+ print_output=print_output,
58
+ print_prefix=print_prefix,
59
+ )
60
+
61
+ logger.debug("--> Waiting for exit status...")
62
+ process.wait()
63
+ logger.debug("--> Command exit status: %i", process.returncode)
64
+
65
+ # Close any open file descriptors
66
+ process.stdout.close()
67
+ process.stderr.close()
68
+
69
+ return process.returncode, combined_output
70
+
71
+
72
+ # Command output buffer handling
73
+ #
74
+
75
+
76
+ @dataclass
77
+ class OutputLine:
78
+ buffer_name: str
79
+ line: str
80
+
81
+
82
+ @dataclass
83
+ class CommandOutput:
84
+ combined_lines: list[OutputLine]
85
+
86
+ def __iter__(self):
87
+ yield from self.combined_lines
88
+
89
+ @property
90
+ def output_lines(self) -> list[str]:
91
+ return [line.line for line in self.combined_lines]
92
+
93
+ @property
94
+ def output(self) -> str:
95
+ return "\n".join(self.output_lines)
96
+
97
+ @property
98
+ def stdout_lines(self) -> list[str]:
99
+ return [line.line for line in self.combined_lines if line.buffer_name == "stdout"]
100
+
101
+ @property
102
+ def stdout(self) -> str:
103
+ return "\n".join(self.stdout_lines)
104
+
105
+ @property
106
+ def stderr_lines(self) -> list[str]:
107
+ return [line.line for line in self.combined_lines if line.buffer_name == "stderr"]
108
+
109
+ @property
110
+ def stderr(self) -> str:
111
+ return "\n".join(self.stderr_lines)
112
+
113
+
114
+ def read_buffer(
115
+ name: str,
116
+ io: Iterable,
117
+ output_queue: Queue[OutputLine],
118
+ print_output=False,
119
+ print_func=None,
120
+ ) -> None:
121
+ """
122
+ Reads a file-like buffer object into lines and optionally prints the output.
123
+ """
124
+
125
+ def _print(line):
126
+ if print_func:
127
+ line = print_func(line)
128
+
129
+ click.echo(line, err=True)
130
+
131
+ for line in io:
132
+ # Handle local Popen shells returning list of bytes, not strings
133
+ if not isinstance(line, str):
134
+ line = line.decode("utf-8")
135
+
136
+ line = line.rstrip("\n")
137
+ output_queue.put(OutputLine(name, line))
138
+
139
+ if print_output:
140
+ _print(line)
141
+
142
+
143
+ def read_output_buffers(
144
+ stdout_buffer: Iterable,
145
+ stderr_buffer: Iterable,
146
+ timeout: Optional[int],
147
+ print_output: bool,
148
+ print_prefix: str,
149
+ ) -> CommandOutput:
150
+ output_queue: Queue[OutputLine] = Queue()
151
+
152
+ # Iterate through outputs to get an exit status and generate desired list
153
+ # output, done in two greenlets so stdout isn't printed before stderr. Not
154
+ # attached to state.pool to avoid blocking it with 2x n-hosts greenlets.
155
+ stdout_reader = gevent.spawn(
156
+ read_buffer,
157
+ "stdout",
158
+ stdout_buffer,
159
+ output_queue,
160
+ print_output=print_output,
161
+ print_func=lambda line: "{0}{1}".format(print_prefix, line),
162
+ )
163
+ stderr_reader = gevent.spawn(
164
+ read_buffer,
165
+ "stderr",
166
+ stderr_buffer,
167
+ output_queue,
168
+ print_output=print_output,
169
+ print_func=lambda line: "{0}{1}".format(
170
+ print_prefix,
171
+ click.style(line, "red"),
172
+ ),
173
+ )
174
+
175
+ # Wait on output, with our timeout (or None)
176
+ greenlets = gevent.wait((stdout_reader, stderr_reader), timeout=timeout)
177
+
178
+ # Timeout doesn't raise an exception, but gevent.wait returns the greenlets
179
+ # which did complete. So if both haven't completed, we kill them and fail
180
+ # with a timeout.
181
+ if len(greenlets) != 2:
182
+ stdout_reader.kill()
183
+ stderr_reader.kill()
184
+
185
+ raise timeout_error()
186
+
187
+ return CommandOutput(list(output_queue.queue))
188
+
189
+
190
+ # Connector execution control
191
+ #
192
+
193
+
194
+ def execute_command_with_sudo_retry(
195
+ host: "Host",
196
+ command_arguments: "ConnectorArguments",
197
+ execute_command: Callable[..., tuple[int, CommandOutput]],
198
+ ) -> tuple[int, CommandOutput]:
199
+ return_code, output = execute_command()
200
+
201
+ # If we failed look for a sudo password prompt line and re-submit using the sudo password. Look
202
+ # at all lines here in case anything else gets printed, eg in:
203
+ # https://github.com/pyinfra-dev/pyinfra/issues/1292
204
+ if return_code != 0 and output and output.combined_lines:
205
+ for line in reversed(output.combined_lines):
206
+ if line.line.strip() == "sudo: a password is required":
207
+ # If we need a password, ask the user for it and attach to the host
208
+ # internal connector data for use when executing future commands.
209
+ sudo_password = getpass("{0}sudo password: ".format(host.print_prefix))
210
+ host.connector_data["prompted_sudo_password"] = sudo_password
211
+ return_code, output = execute_command()
212
+ break
213
+
214
+ return return_code, output
215
+
216
+
217
+ def write_stdin(stdin, buffer):
218
+ if hasattr(stdin, "readlines"):
219
+ stdin = stdin.readlines()
220
+ if not isinstance(stdin, (list, tuple)):
221
+ stdin = [stdin]
222
+
223
+ for line in stdin:
224
+ if not line.endswith("\n"):
225
+ line = "{0}\n".format(line)
226
+ line = line.encode()
227
+ buffer.write(line)
228
+ buffer.close()
229
+
230
+
231
+ def remove_any_sudo_askpass_file(host) -> None:
232
+ sudo_askpass_path = host.connector_data.get("sudo_askpass_path")
233
+ if sudo_askpass_path:
234
+ host.run_shell_command("rm -f {0}".format(sudo_askpass_path))
235
+ host.connector_data["sudo_askpass_path"] = None
236
+
237
+
238
+ @memoize
239
+ def _show_use_su_login_warning() -> None:
240
+ logger.warning(
241
+ (
242
+ "Using `use_su_login` may not work: "
243
+ "some systems (MacOS, OpenBSD) ignore the flag when executing a command, "
244
+ "use `sudo` + `use_sudo_login` instead."
245
+ ),
246
+ )
247
+
248
+
249
+ def extract_control_arguments(arguments: "ConnectorArguments") -> "ConnectorArguments":
250
+ control_arguments: "ConnectorArguments" = {}
251
+
252
+ if "_success_exit_codes" in arguments:
253
+ control_arguments["_success_exit_codes"] = arguments.pop("_success_exit_codes")
254
+ if "_timeout" in arguments:
255
+ control_arguments["_timeout"] = arguments.pop("_timeout")
256
+ if "_get_pty" in arguments:
257
+ control_arguments["_get_pty"] = arguments.pop("_get_pty")
258
+ if "_stdin" in arguments:
259
+ control_arguments["_stdin"] = arguments.pop("_stdin")
260
+
261
+ return control_arguments
262
+
263
+
264
+ def _ensure_sudo_askpass_set_for_host(host: "Host"):
265
+ if host.connector_data.get("sudo_askpass_path"):
266
+ return
267
+ _, output = host.run_shell_command(SUDO_ASKPASS_COMMAND)
268
+ host.connector_data["sudo_askpass_path"] = shlex.quote(output.stdout_lines[0])
269
+
270
+
271
+ def make_unix_command_for_host(
272
+ state: "State",
273
+ host: "Host",
274
+ command: StringCommand,
275
+ **command_arguments,
276
+ ) -> StringCommand:
277
+ if not command_arguments.get("_sudo"):
278
+ # If no sudo, we've nothing to do here
279
+ return make_unix_command(command, **command_arguments)
280
+
281
+ # If the sudo password is not set in the direct arguments,
282
+ # set it from the connector data value.
283
+ if "_sudo_password" not in command_arguments or not command_arguments["_sudo_password"]:
284
+ command_arguments["_sudo_password"] = host.connector_data.get("prompted_sudo_password")
285
+
286
+ if command_arguments["_sudo_password"]:
287
+ # Ensure the askpass path is correctly set and passed through
288
+ _ensure_sudo_askpass_set_for_host(host)
289
+ command_arguments["_sudo_askpass_path"] = host.connector_data["sudo_askpass_path"]
290
+ return make_unix_command(command, **command_arguments)
291
+
292
+
293
+ # Connector command generation
294
+ #
295
+
296
+
297
+ def make_unix_command(
298
+ command: StringCommand,
299
+ _env=None,
300
+ _chdir=None,
301
+ _shell_executable="sh",
302
+ # Su config
303
+ _su_user=None,
304
+ _use_su_login=False,
305
+ _su_shell=None,
306
+ _preserve_su_env=False,
307
+ # Sudo config
308
+ _sudo=False,
309
+ _sudo_user=None,
310
+ _use_sudo_login=False,
311
+ _sudo_password="",
312
+ _sudo_askpass_path=None,
313
+ _preserve_sudo_env=False,
314
+ # Doas config
315
+ _doas=False,
316
+ _doas_user=None,
317
+ # Retry config (ignored in command generation but passed through)
318
+ _retries=0,
319
+ _retry_delay=0,
320
+ _retry_until=None,
321
+ ) -> StringCommand:
322
+ """
323
+ Builds a shell command with various kwargs.
324
+ """
325
+
326
+ if _shell_executable is not None and not isinstance(_shell_executable, str):
327
+ _shell_executable = "sh"
328
+
329
+ if _env:
330
+ env_string = " ".join(['"{0}={1}"'.format(key, value) for key, value in _env.items()])
331
+ command = StringCommand("export", env_string, "&&", command)
332
+
333
+ if _chdir:
334
+ command = StringCommand("cd", _chdir, "&&", command)
335
+
336
+ command_bits: list[Union[str, StringCommand, QuoteString]] = []
337
+
338
+ if _doas:
339
+ command_bits.extend(["doas", "-n"])
340
+
341
+ if _doas_user:
342
+ command_bits.extend(["-u", _doas_user])
343
+
344
+ if _sudo_password and _sudo_askpass_path:
345
+ command_bits.extend(
346
+ [
347
+ "env",
348
+ "SUDO_ASKPASS={0}".format(_sudo_askpass_path),
349
+ MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, shlex.quote(_sudo_password))),
350
+ ],
351
+ )
352
+
353
+ if _sudo:
354
+ command_bits.extend(["sudo", "-H"])
355
+
356
+ if _sudo_password:
357
+ command_bits.extend(["-A", "-k"]) # use askpass, disable cache
358
+ else:
359
+ command_bits.append("-n") # disable prompt/interactivity
360
+
361
+ if _use_sudo_login:
362
+ command_bits.append("-i")
363
+
364
+ if _preserve_sudo_env:
365
+ command_bits.append("-E")
366
+
367
+ if _sudo_user:
368
+ command_bits.extend(("-u", _sudo_user))
369
+
370
+ if _su_user:
371
+ command_bits.append("su")
372
+
373
+ if _use_su_login:
374
+ _show_use_su_login_warning()
375
+ command_bits.append("-l")
376
+
377
+ if _preserve_su_env:
378
+ command_bits.append("-m")
379
+
380
+ if _su_shell:
381
+ command_bits.extend(["-s", "`which {0}`".format(_su_shell)])
382
+
383
+ command_bits.extend([_su_user, "-c"])
384
+
385
+ if _shell_executable is not None:
386
+ # Quote the whole shell -c 'command' as BSD `su` does not have a shell option
387
+ command_bits.append(
388
+ QuoteString(StringCommand(_shell_executable, "-c", QuoteString(command))),
389
+ )
390
+ else:
391
+ command_bits.append(QuoteString(StringCommand(command)))
392
+ else:
393
+ if _shell_executable is not None:
394
+ command_bits.extend([_shell_executable, "-c", QuoteString(command)])
395
+ else:
396
+ command_bits.extend([command])
397
+
398
+ return StringCommand(*command_bits)
399
+
400
+
401
+ def make_win_command(command):
402
+ """
403
+ Builds a windows command with various kwargs.
404
+ """
405
+
406
+ # Quote the command as a string
407
+ command = shlex.quote(str(command))
408
+ command = "{0}".format(command)
409
+
410
+ 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