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,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
|