pyinfra 2.9.2__py2.py3-none-any.whl → 3.0__py2.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/api/__init__.py +3 -0
- pyinfra/api/arguments.py +265 -253
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +68 -53
- pyinfra/api/config.py +139 -32
- pyinfra/api/connect.py +1 -1
- pyinfra/api/connectors.py +7 -26
- pyinfra/api/deploy.py +21 -52
- pyinfra/api/exceptions.py +33 -8
- pyinfra/api/facts.py +102 -137
- pyinfra/api/host.py +150 -82
- pyinfra/api/inventory.py +21 -25
- pyinfra/api/operation.py +240 -198
- pyinfra/api/operations.py +102 -148
- pyinfra/api/state.py +137 -79
- pyinfra/api/util.py +79 -86
- pyinfra/connectors/base.py +147 -0
- pyinfra/connectors/chroot.py +160 -169
- pyinfra/connectors/docker.py +220 -237
- pyinfra/connectors/dockerssh.py +231 -253
- pyinfra/connectors/local.py +196 -208
- pyinfra/connectors/ssh.py +530 -613
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +5 -3
- pyinfra/connectors/terraform.py +86 -65
- pyinfra/connectors/util.py +211 -137
- pyinfra/connectors/vagrant.py +60 -53
- pyinfra/context.py +4 -2
- pyinfra/facts/apk.py +2 -0
- pyinfra/facts/apt.py +2 -0
- pyinfra/facts/brew.py +2 -0
- pyinfra/facts/bsdinit.py +2 -0
- pyinfra/facts/cargo.py +2 -0
- pyinfra/facts/choco.py +2 -0
- pyinfra/facts/deb.py +7 -2
- pyinfra/facts/dnf.py +2 -0
- pyinfra/facts/docker.py +19 -0
- pyinfra/facts/files.py +47 -32
- pyinfra/facts/gem.py +2 -0
- pyinfra/facts/git.py +3 -1
- pyinfra/facts/gpg.py +3 -1
- pyinfra/facts/hardware.py +34 -24
- pyinfra/facts/iptables.py +5 -3
- pyinfra/facts/launchd.py +2 -0
- pyinfra/facts/lxd.py +2 -0
- pyinfra/facts/mysql.py +13 -6
- pyinfra/facts/npm.py +1 -0
- pyinfra/facts/openrc.py +2 -0
- pyinfra/facts/pacman.py +6 -2
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +2 -0
- pyinfra/facts/pkgin.py +2 -0
- pyinfra/facts/postgres.py +168 -0
- pyinfra/facts/postgresql.py +6 -160
- pyinfra/facts/rpm.py +12 -9
- pyinfra/facts/runit.py +68 -0
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +80 -36
- pyinfra/facts/snap.py +2 -0
- pyinfra/facts/systemd.py +31 -12
- pyinfra/facts/sysvinit.py +10 -10
- pyinfra/facts/upstart.py +2 -0
- pyinfra/facts/util/packaging.py +7 -4
- pyinfra/facts/vzctl.py +2 -0
- pyinfra/facts/xbps.py +2 -0
- pyinfra/facts/yum.py +2 -0
- pyinfra/facts/zypper.py +2 -0
- pyinfra/local.py +4 -5
- pyinfra/operations/apk.py +6 -4
- pyinfra/operations/apt.py +46 -65
- pyinfra/operations/brew.py +17 -22
- pyinfra/operations/bsdinit.py +9 -7
- pyinfra/operations/cargo.py +4 -2
- pyinfra/operations/choco.py +4 -2
- pyinfra/operations/dnf.py +19 -23
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +188 -386
- pyinfra/operations/gem.py +4 -2
- pyinfra/operations/git.py +24 -53
- pyinfra/operations/iptables.py +29 -35
- pyinfra/operations/launchd.py +6 -7
- pyinfra/operations/lxd.py +8 -13
- pyinfra/operations/mysql.py +62 -81
- pyinfra/operations/npm.py +9 -2
- pyinfra/operations/openrc.py +6 -4
- pyinfra/operations/pacman.py +7 -8
- pyinfra/operations/pip.py +25 -24
- pyinfra/operations/pkg.py +4 -2
- pyinfra/operations/pkgin.py +6 -4
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -379
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -44
- pyinfra/operations/server.py +111 -127
- pyinfra/operations/snap.py +4 -4
- pyinfra/operations/ssh.py +20 -33
- pyinfra/operations/systemd.py +19 -15
- pyinfra/operations/sysvinit.py +9 -16
- pyinfra/operations/upstart.py +9 -7
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +177 -0
- pyinfra/operations/util/files.py +24 -16
- pyinfra/operations/util/packaging.py +55 -57
- pyinfra/operations/util/service.py +39 -51
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +6 -4
- pyinfra/operations/yum.py +18 -22
- pyinfra/operations/zypper.py +12 -13
- pyinfra/version.py +5 -2
- {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
- pyinfra-3.0.dist-info/RECORD +167 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
- pyinfra-3.0.dist-info/entry_points.txt +11 -0
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +7 -2
- pyinfra_cli/exceptions.py +78 -42
- pyinfra_cli/inventory.py +40 -6
- pyinfra_cli/log.py +17 -3
- pyinfra_cli/main.py +133 -90
- pyinfra_cli/prints.py +95 -127
- pyinfra_cli/util.py +62 -29
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +13 -13
- tests/test_api/test_api_deploys.py +28 -29
- tests/test_api/test_api_facts.py +60 -98
- tests/test_api/test_api_operations.py +101 -201
- tests/test_cli/test_cli.py +18 -49
- tests/test_cli/test_cli_deploy.py +11 -37
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_chroot.py +6 -6
- tests/test_connectors/test_docker.py +4 -4
- tests/test_connectors/test_dockerssh.py +38 -50
- tests/test_connectors/test_local.py +11 -12
- tests/test_connectors/test_ssh.py +105 -93
- tests/test_connectors/test_terraform.py +9 -15
- tests/test_connectors/test_util.py +24 -46
- tests/test_connectors/test_vagrant.py +7 -7
- pyinfra/api/operation.pyi +0 -117
- pyinfra/connectors/ansible.py +0 -171
- pyinfra/connectors/mech.py +0 -186
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -320
- pyinfra/facts/windows.py +0 -366
- pyinfra/facts/windows_files.py +0 -90
- pyinfra/operations/windows.py +0 -59
- pyinfra/operations/windows_files.py +0 -551
- pyinfra-2.9.2.dist-info/RECORD +0 -170
- pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- tests/test_connectors/test_winrm.py +0 -76
- {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
pyinfra/connectors/util.py
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import shlex
|
|
4
|
+
from dataclasses import dataclass
|
|
2
5
|
from getpass import getpass
|
|
6
|
+
from queue import Queue
|
|
3
7
|
from socket import timeout as timeout_error
|
|
4
8
|
from subprocess import PIPE, Popen
|
|
9
|
+
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Union
|
|
5
10
|
|
|
6
11
|
import click
|
|
7
12
|
import gevent
|
|
8
|
-
from gevent.queue import Queue
|
|
9
13
|
|
|
10
14
|
from pyinfra import logger
|
|
11
15
|
from pyinfra.api import MaskString, QuoteString, StringCommand
|
|
12
16
|
from pyinfra.api.util import memoize
|
|
13
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
|
+
|
|
14
24
|
SUDO_ASKPASS_ENV_VAR = "PYINFRA_SUDO_PASSWORD"
|
|
15
25
|
SUDO_ASKPASS_COMMAND = r"""
|
|
16
26
|
temp=$(mktemp "${{TMPDIR:=/tmp}}/pyinfra-sudo-askpass-XXXXXXXXXXXX")
|
|
@@ -25,54 +35,22 @@ echo "$temp"
|
|
|
25
35
|
)
|
|
26
36
|
|
|
27
37
|
|
|
28
|
-
def read_buffer(type_, io, output_queue, print_output=False, print_func=None):
|
|
29
|
-
"""
|
|
30
|
-
Reads a file-like buffer object into lines and optionally prints the output.
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
def _print(line):
|
|
34
|
-
if print_func:
|
|
35
|
-
line = print_func(line)
|
|
36
|
-
|
|
37
|
-
click.echo(line, err=True)
|
|
38
|
-
|
|
39
|
-
for line in io:
|
|
40
|
-
# Handle local Popen shells returning list of bytes, not strings
|
|
41
|
-
if not isinstance(line, str):
|
|
42
|
-
line = line.decode("utf-8")
|
|
43
|
-
|
|
44
|
-
line = line.rstrip("\n")
|
|
45
|
-
output_queue.put((type_, line))
|
|
46
|
-
|
|
47
|
-
if print_output:
|
|
48
|
-
_print(line)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def execute_command_with_sudo_retry(host, command_kwargs, execute_command):
|
|
52
|
-
return_code, combined_output = execute_command()
|
|
53
|
-
|
|
54
|
-
if return_code != 0 and combined_output:
|
|
55
|
-
last_line = combined_output[-1][1]
|
|
56
|
-
if last_line.strip() == "sudo: a password is required":
|
|
57
|
-
command_kwargs["use_sudo_password"] = True # ask for the password
|
|
58
|
-
return_code, combined_output = execute_command()
|
|
59
|
-
|
|
60
|
-
return return_code, combined_output
|
|
61
|
-
|
|
62
|
-
|
|
63
38
|
def run_local_process(
|
|
64
|
-
command,
|
|
39
|
+
command: str,
|
|
65
40
|
stdin=None,
|
|
66
|
-
timeout=None,
|
|
67
|
-
print_output=False,
|
|
68
|
-
print_prefix=
|
|
69
|
-
):
|
|
41
|
+
timeout: Optional[int] = None,
|
|
42
|
+
print_output: bool = False,
|
|
43
|
+
print_prefix: str = "",
|
|
44
|
+
) -> tuple[int, "CommandOutput"]:
|
|
70
45
|
process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
|
71
46
|
|
|
72
47
|
if stdin:
|
|
73
48
|
write_stdin(stdin, process.stdin)
|
|
74
49
|
|
|
75
|
-
|
|
50
|
+
assert process.stdout is not None
|
|
51
|
+
assert process.stderr is not None
|
|
52
|
+
|
|
53
|
+
combined_output = read_output_buffers(
|
|
76
54
|
process.stdout,
|
|
77
55
|
process.stderr,
|
|
78
56
|
timeout=timeout,
|
|
@@ -91,14 +69,85 @@ def run_local_process(
|
|
|
91
69
|
return process.returncode, combined_output
|
|
92
70
|
|
|
93
71
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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()
|
|
102
151
|
|
|
103
152
|
# Iterate through outputs to get an exit status and generate desired list
|
|
104
153
|
# output, done in two greenlets so stdout isn't printed before stderr. Not
|
|
@@ -135,22 +184,30 @@ def read_buffers_into_queue(
|
|
|
135
184
|
|
|
136
185
|
raise timeout_error()
|
|
137
186
|
|
|
138
|
-
return list(output_queue.queue)
|
|
187
|
+
return CommandOutput(list(output_queue.queue))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Connector execution control
|
|
191
|
+
#
|
|
139
192
|
|
|
140
193
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
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()
|
|
144
200
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
201
|
+
if return_code != 0 and output and output.combined_lines:
|
|
202
|
+
last_line = output.combined_lines[-1].line
|
|
203
|
+
if last_line.strip() == "sudo: a password is required":
|
|
204
|
+
# If we need a password, ask the user for it and attach to the host
|
|
205
|
+
# internal connector data for use when executing future commands.
|
|
206
|
+
sudo_password = getpass("{0}sudo password: ".format(host.print_prefix))
|
|
207
|
+
host.connector_data["prompted_sudo_password"] = sudo_password
|
|
208
|
+
return_code, output = execute_command()
|
|
152
209
|
|
|
153
|
-
return
|
|
210
|
+
return return_code, output
|
|
154
211
|
|
|
155
212
|
|
|
156
213
|
def write_stdin(stdin, buffer):
|
|
@@ -167,26 +224,7 @@ def write_stdin(stdin, buffer):
|
|
|
167
224
|
buffer.close()
|
|
168
225
|
|
|
169
226
|
|
|
170
|
-
def
|
|
171
|
-
if not host.connector_data.get("sudo_askpass_path"):
|
|
172
|
-
_, stdout, _ = host.run_shell_command(SUDO_ASKPASS_COMMAND)
|
|
173
|
-
host.connector_data["sudo_askpass_path"] = shlex.quote(stdout[0])
|
|
174
|
-
|
|
175
|
-
if use_sudo_password is True:
|
|
176
|
-
sudo_password = host.connector_data.get("sudo_password")
|
|
177
|
-
if not sudo_password:
|
|
178
|
-
sudo_password = getpass("{0}sudo password: ".format(host.print_prefix))
|
|
179
|
-
host.connector_data["sudo_password"] = sudo_password
|
|
180
|
-
sudo_password = sudo_password
|
|
181
|
-
elif callable(use_sudo_password):
|
|
182
|
-
sudo_password = use_sudo_password()
|
|
183
|
-
else:
|
|
184
|
-
sudo_password = use_sudo_password
|
|
185
|
-
|
|
186
|
-
return shlex.quote(sudo_password)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def remove_any_sudo_askpass_file(host):
|
|
227
|
+
def remove_any_sudo_askpass_file(host) -> None:
|
|
190
228
|
sudo_askpass_path = host.connector_data.get("sudo_askpass_path")
|
|
191
229
|
if sudo_askpass_path:
|
|
192
230
|
host.run_shell_command("rm -f {0}".format(sudo_askpass_path))
|
|
@@ -204,112 +242,148 @@ def _show_use_su_login_warning():
|
|
|
204
242
|
)
|
|
205
243
|
|
|
206
244
|
|
|
207
|
-
def
|
|
208
|
-
|
|
209
|
-
if use_sudo_password:
|
|
210
|
-
command_kwargs["sudo_password"] = _get_sudo_password(host, use_sudo_password)
|
|
211
|
-
command_kwargs["sudo_askpass_path"] = host.connector_data.get("sudo_askpass_path")
|
|
245
|
+
def extract_control_arguments(arguments: "ConnectorArguments") -> "ConnectorArguments":
|
|
246
|
+
control_arguments: "ConnectorArguments" = {}
|
|
212
247
|
|
|
213
|
-
|
|
248
|
+
if "_success_exit_codes" in arguments:
|
|
249
|
+
control_arguments["_success_exit_codes"] = arguments.pop("_success_exit_codes")
|
|
250
|
+
if "_timeout" in arguments:
|
|
251
|
+
control_arguments["_timeout"] = arguments.pop("_timeout")
|
|
252
|
+
if "_get_pty" in arguments:
|
|
253
|
+
control_arguments["_get_pty"] = arguments.pop("_get_pty")
|
|
254
|
+
if "_stdin" in arguments:
|
|
255
|
+
control_arguments["_stdin"] = arguments.pop("_stdin")
|
|
256
|
+
|
|
257
|
+
return control_arguments
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _ensure_sudo_askpass_set_for_host(host: "Host"):
|
|
261
|
+
if host.connector_data.get("sudo_askpass_path"):
|
|
262
|
+
return
|
|
263
|
+
_, output = host.run_shell_command(SUDO_ASKPASS_COMMAND)
|
|
264
|
+
host.connector_data["sudo_askpass_path"] = shlex.quote(output.stdout_lines[0])
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def make_unix_command_for_host(
|
|
268
|
+
state: "State",
|
|
269
|
+
host: "Host",
|
|
270
|
+
command: StringCommand,
|
|
271
|
+
**command_arguments,
|
|
272
|
+
) -> StringCommand:
|
|
273
|
+
if not command_arguments.get("_sudo"):
|
|
274
|
+
# If no sudo, we've nothing to do here
|
|
275
|
+
return make_unix_command(command, **command_arguments)
|
|
276
|
+
|
|
277
|
+
# If the sudo password is not set in the direct arguments,
|
|
278
|
+
# set it from the connector data value.
|
|
279
|
+
if "_sudo_password" not in command_arguments or not command_arguments["_sudo_password"]:
|
|
280
|
+
command_arguments["_sudo_password"] = host.connector_data.get("prompted_sudo_password")
|
|
281
|
+
|
|
282
|
+
if command_arguments["_sudo_password"]:
|
|
283
|
+
# Ensure the askpass path is correctly set and passed through
|
|
284
|
+
_ensure_sudo_askpass_set_for_host(host)
|
|
285
|
+
command_arguments["_sudo_askpass_path"] = host.connector_data["sudo_askpass_path"]
|
|
286
|
+
return make_unix_command(command, **command_arguments)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# Connector command generation
|
|
290
|
+
#
|
|
214
291
|
|
|
215
292
|
|
|
216
293
|
def make_unix_command(
|
|
217
|
-
command,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
294
|
+
command: StringCommand,
|
|
295
|
+
_env=None,
|
|
296
|
+
_chdir=None,
|
|
297
|
+
_shell_executable="sh",
|
|
221
298
|
# Su config
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
299
|
+
_su_user=None,
|
|
300
|
+
_use_su_login=False,
|
|
301
|
+
_su_shell=None,
|
|
302
|
+
_preserve_su_env=False,
|
|
226
303
|
# Sudo config
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
304
|
+
_sudo=False,
|
|
305
|
+
_sudo_user=None,
|
|
306
|
+
_use_sudo_login=False,
|
|
307
|
+
_sudo_password=False,
|
|
308
|
+
_sudo_askpass_path=None,
|
|
309
|
+
_preserve_sudo_env=False,
|
|
233
310
|
# Doas config
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
):
|
|
311
|
+
_doas=False,
|
|
312
|
+
_doas_user=None,
|
|
313
|
+
) -> StringCommand:
|
|
237
314
|
"""
|
|
238
315
|
Builds a shell command with various kwargs.
|
|
239
316
|
"""
|
|
240
317
|
|
|
241
|
-
if
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if isinstance(command, bytes):
|
|
245
|
-
command = command.decode("utf-8")
|
|
318
|
+
if _shell_executable is not None and not isinstance(_shell_executable, str):
|
|
319
|
+
_shell_executable = "sh"
|
|
246
320
|
|
|
247
|
-
if
|
|
248
|
-
env_string = " ".join(['"{0}={1}"'.format(key, value) for key, value in
|
|
321
|
+
if _env:
|
|
322
|
+
env_string = " ".join(['"{0}={1}"'.format(key, value) for key, value in _env.items()])
|
|
249
323
|
command = StringCommand("export", env_string, "&&", command)
|
|
250
324
|
|
|
251
|
-
if
|
|
252
|
-
command = StringCommand("cd",
|
|
325
|
+
if _chdir:
|
|
326
|
+
command = StringCommand("cd", _chdir, "&&", command)
|
|
253
327
|
|
|
254
|
-
command_bits = []
|
|
328
|
+
command_bits: list[Union[str, StringCommand, QuoteString]] = []
|
|
255
329
|
|
|
256
|
-
if
|
|
330
|
+
if _doas:
|
|
257
331
|
command_bits.extend(["doas", "-n"])
|
|
258
332
|
|
|
259
|
-
if
|
|
260
|
-
command_bits.extend(["-u",
|
|
333
|
+
if _doas_user:
|
|
334
|
+
command_bits.extend(["-u", _doas_user])
|
|
261
335
|
|
|
262
|
-
if
|
|
336
|
+
if _sudo_password and _sudo_askpass_path:
|
|
263
337
|
command_bits.extend(
|
|
264
338
|
[
|
|
265
339
|
"env",
|
|
266
|
-
"SUDO_ASKPASS={0}".format(
|
|
267
|
-
MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR,
|
|
340
|
+
"SUDO_ASKPASS={0}".format(_sudo_askpass_path),
|
|
341
|
+
MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, shlex.quote(_sudo_password))),
|
|
268
342
|
],
|
|
269
343
|
)
|
|
270
344
|
|
|
271
|
-
if
|
|
345
|
+
if _sudo:
|
|
272
346
|
command_bits.extend(["sudo", "-H"])
|
|
273
347
|
|
|
274
|
-
if
|
|
348
|
+
if _sudo_password:
|
|
275
349
|
command_bits.extend(["-A", "-k"]) # use askpass, disable cache
|
|
276
350
|
else:
|
|
277
351
|
command_bits.append("-n") # disable prompt/interactivity
|
|
278
352
|
|
|
279
|
-
if
|
|
353
|
+
if _use_sudo_login:
|
|
280
354
|
command_bits.append("-i")
|
|
281
355
|
|
|
282
|
-
if
|
|
356
|
+
if _preserve_sudo_env:
|
|
283
357
|
command_bits.append("-E")
|
|
284
358
|
|
|
285
|
-
if
|
|
286
|
-
command_bits.extend(("-u",
|
|
359
|
+
if _sudo_user:
|
|
360
|
+
command_bits.extend(("-u", _sudo_user))
|
|
287
361
|
|
|
288
|
-
if
|
|
362
|
+
if _su_user:
|
|
289
363
|
command_bits.append("su")
|
|
290
364
|
|
|
291
|
-
if
|
|
365
|
+
if _use_su_login:
|
|
292
366
|
_show_use_su_login_warning()
|
|
293
367
|
command_bits.append("-l")
|
|
294
368
|
|
|
295
|
-
if
|
|
369
|
+
if _preserve_su_env:
|
|
296
370
|
command_bits.append("-m")
|
|
297
371
|
|
|
298
|
-
if
|
|
299
|
-
command_bits.extend(["-s", "`which {0}`".format(
|
|
372
|
+
if _su_shell:
|
|
373
|
+
command_bits.extend(["-s", "`which {0}`".format(_su_shell)])
|
|
300
374
|
|
|
301
|
-
command_bits.extend([
|
|
375
|
+
command_bits.extend([_su_user, "-c"])
|
|
302
376
|
|
|
303
|
-
if
|
|
377
|
+
if _shell_executable is not None:
|
|
304
378
|
# Quote the whole shell -c 'command' as BSD `su` does not have a shell option
|
|
305
379
|
command_bits.append(
|
|
306
|
-
QuoteString(StringCommand(
|
|
380
|
+
QuoteString(StringCommand(_shell_executable, "-c", QuoteString(command))),
|
|
307
381
|
)
|
|
308
382
|
else:
|
|
309
383
|
command_bits.append(QuoteString(StringCommand(command)))
|
|
310
384
|
else:
|
|
311
|
-
if
|
|
312
|
-
command_bits.extend([
|
|
385
|
+
if _shell_executable is not None:
|
|
386
|
+
command_bits.extend([_shell_executable, "-c", QuoteString(command)])
|
|
313
387
|
else:
|
|
314
388
|
command_bits.extend([command])
|
|
315
389
|
|
pyinfra/connectors/vagrant.py
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
The ``@vagrant`` connector reads the current Vagrant status and generates an
|
|
3
|
-
inventory for any running VMs.
|
|
4
|
-
|
|
5
|
-
.. code:: shell
|
|
6
|
-
|
|
7
|
-
# Run on all hosts
|
|
8
|
-
pyinfra @vagrant ...
|
|
9
|
-
|
|
10
|
-
# Run on a specific VM
|
|
11
|
-
pyinfra @vagrant/my-vm-name ...
|
|
12
|
-
|
|
13
|
-
# Run on multiple named VMs
|
|
14
|
-
pyinfra @vagrant/my-vm-name,@vagrant/another-vm-name ...
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
1
|
import json
|
|
18
2
|
from os import path
|
|
19
3
|
from queue import Queue
|
|
@@ -24,6 +8,8 @@ from pyinfra.api.exceptions import InventoryError
|
|
|
24
8
|
from pyinfra.api.util import memoize
|
|
25
9
|
from pyinfra.progress import progress_spinner
|
|
26
10
|
|
|
11
|
+
from .base import BaseConnector
|
|
12
|
+
|
|
27
13
|
|
|
28
14
|
def _get_vagrant_ssh_config(queue, progress, target):
|
|
29
15
|
logger.debug("Loading SSH config for %s", target)
|
|
@@ -68,7 +54,7 @@ def get_vagrant_config(limit=None):
|
|
|
68
54
|
targets.append(target)
|
|
69
55
|
|
|
70
56
|
threads = []
|
|
71
|
-
config_queue = Queue()
|
|
57
|
+
config_queue = Queue() # type: ignore
|
|
72
58
|
|
|
73
59
|
with progress_spinner(targets) as progress:
|
|
74
60
|
for target in targets:
|
|
@@ -107,13 +93,13 @@ def _make_name_data(host):
|
|
|
107
93
|
"ssh_hostname": host["HostName"],
|
|
108
94
|
}
|
|
109
95
|
|
|
110
|
-
for config_key, data_key in (
|
|
111
|
-
("Port", "ssh_port"),
|
|
112
|
-
("User", "ssh_user"),
|
|
113
|
-
("IdentityFile", "ssh_key"),
|
|
96
|
+
for config_key, data_key, data_cast in (
|
|
97
|
+
("Port", "ssh_port", int),
|
|
98
|
+
("User", "ssh_user", str),
|
|
99
|
+
("IdentityFile", "ssh_key", str),
|
|
114
100
|
):
|
|
115
101
|
if config_key in host:
|
|
116
|
-
data[data_key] = host[config_key]
|
|
102
|
+
data[data_key] = data_cast(host[config_key])
|
|
117
103
|
|
|
118
104
|
# Update any configured JSON data
|
|
119
105
|
if vagrant_host in vagrant_options.get("data", {}):
|
|
@@ -128,46 +114,67 @@ def _make_name_data(host):
|
|
|
128
114
|
return "@vagrant/{0}".format(host["Host"]), data, groups
|
|
129
115
|
|
|
130
116
|
|
|
131
|
-
|
|
132
|
-
|
|
117
|
+
class VagrantInventoryConnector(BaseConnector):
|
|
118
|
+
"""
|
|
119
|
+
The ``@vagrant`` connector reads the current Vagrant status and generates an
|
|
120
|
+
inventory for any running VMs.
|
|
133
121
|
|
|
134
|
-
|
|
122
|
+
.. code:: shell
|
|
135
123
|
|
|
136
|
-
|
|
137
|
-
|
|
124
|
+
# Run on all hosts
|
|
125
|
+
pyinfra @vagrant ...
|
|
138
126
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if not line:
|
|
142
|
-
if current_host:
|
|
143
|
-
hosts.append(_make_name_data(current_host))
|
|
127
|
+
# Run on a specific VM
|
|
128
|
+
pyinfra @vagrant/my-vm-name ...
|
|
144
129
|
|
|
145
|
-
|
|
146
|
-
|
|
130
|
+
# Run on multiple named VMs
|
|
131
|
+
pyinfra @vagrant/my-vm-name,@vagrant/another-vm-name ...
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def make_names_data(name=None):
|
|
136
|
+
vagrant_ssh_info = get_vagrant_config(name)
|
|
137
|
+
|
|
138
|
+
logger.debug("Got Vagrant SSH info: \n%s", vagrant_ssh_info)
|
|
139
|
+
|
|
140
|
+
hosts = []
|
|
141
|
+
current_host = None
|
|
142
|
+
|
|
143
|
+
for line in vagrant_ssh_info:
|
|
144
|
+
# Vagrant outputs an empty line between each host
|
|
145
|
+
if not line:
|
|
146
|
+
if current_host:
|
|
147
|
+
hosts.append(_make_name_data(current_host))
|
|
148
|
+
|
|
149
|
+
current_host = None
|
|
150
|
+
continue
|
|
147
151
|
|
|
148
|
-
|
|
152
|
+
key, value = line.split(" ", 1)
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
if key == "Host":
|
|
155
|
+
if current_host:
|
|
156
|
+
hosts.append(_make_name_data(current_host))
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
# Set the new host
|
|
159
|
+
current_host = {
|
|
160
|
+
key: value,
|
|
161
|
+
}
|
|
158
162
|
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
elif current_host:
|
|
164
|
+
current_host[key] = value
|
|
161
165
|
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
else:
|
|
167
|
+
logger.debug("Extra Vagrant SSH key/value (%s=%s)", key, value)
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
if current_host:
|
|
170
|
+
hosts.append(_make_name_data(current_host))
|
|
167
171
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
if not hosts:
|
|
173
|
+
if name:
|
|
174
|
+
raise InventoryError(
|
|
175
|
+
"No running Vagrant instances matching `{0}` found!".format(name)
|
|
176
|
+
)
|
|
177
|
+
raise InventoryError("No running Vagrant instances found!")
|
|
172
178
|
|
|
173
|
-
|
|
179
|
+
for host in hosts:
|
|
180
|
+
yield host
|
pyinfra/context.py
CHANGED
|
@@ -4,7 +4,9 @@ are imported and used throughout pyinfra and end user deploy code (CLI mode).
|
|
|
4
4
|
|
|
5
5
|
These variables always represent the current executing pyinfra context.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from contextlib import contextmanager
|
|
9
|
+
from types import ModuleType
|
|
8
10
|
from typing import TYPE_CHECKING
|
|
9
11
|
|
|
10
12
|
from gevent.local import local
|
|
@@ -17,12 +19,12 @@ if TYPE_CHECKING:
|
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class container:
|
|
20
|
-
|
|
22
|
+
module = None
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class ContextObject:
|
|
24
26
|
_container_cls = container
|
|
25
|
-
_base_cls
|
|
27
|
+
_base_cls: ModuleType
|
|
26
28
|
|
|
27
29
|
def __init__(self):
|
|
28
30
|
self._container = self._container_cls()
|
pyinfra/facts/apk.py
CHANGED
pyinfra/facts/apt.py
CHANGED
pyinfra/facts/brew.py
CHANGED
pyinfra/facts/bsdinit.py
CHANGED
pyinfra/facts/cargo.py
CHANGED