pyinfra 2.9.2__py2.py3-none-any.whl → 3.0b1__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 +261 -255
- pyinfra/api/arguments_typed.py +77 -0
- pyinfra/api/command.py +66 -53
- pyinfra/api/config.py +27 -22
- pyinfra/api/connect.py +1 -1
- pyinfra/api/connectors.py +2 -24
- pyinfra/api/deploy.py +21 -52
- pyinfra/api/exceptions.py +33 -8
- pyinfra/api/facts.py +77 -113
- pyinfra/api/host.py +150 -82
- pyinfra/api/inventory.py +17 -25
- pyinfra/api/operation.py +232 -198
- pyinfra/api/operations.py +102 -148
- pyinfra/api/state.py +137 -79
- pyinfra/api/util.py +55 -70
- pyinfra/connectors/base.py +150 -0
- pyinfra/connectors/chroot.py +160 -169
- pyinfra/connectors/docker.py +227 -237
- pyinfra/connectors/dockerssh.py +231 -253
- pyinfra/connectors/local.py +195 -207
- pyinfra/connectors/ssh.py +528 -615
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +5 -3
- pyinfra/connectors/terraform.py +86 -65
- pyinfra/connectors/util.py +212 -137
- pyinfra/connectors/vagrant.py +55 -48
- pyinfra/context.py +3 -2
- pyinfra/facts/docker.py +1 -0
- pyinfra/facts/files.py +45 -32
- pyinfra/facts/git.py +3 -1
- pyinfra/facts/gpg.py +1 -1
- pyinfra/facts/hardware.py +4 -2
- pyinfra/facts/iptables.py +5 -3
- pyinfra/facts/mysql.py +1 -0
- pyinfra/facts/postgres.py +168 -0
- pyinfra/facts/postgresql.py +5 -161
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +77 -30
- pyinfra/facts/systemd.py +29 -12
- pyinfra/facts/sysvinit.py +10 -10
- pyinfra/facts/util/packaging.py +4 -2
- pyinfra/local.py +4 -5
- pyinfra/operations/apk.py +3 -3
- pyinfra/operations/apt.py +25 -47
- pyinfra/operations/brew.py +7 -14
- pyinfra/operations/bsdinit.py +4 -4
- pyinfra/operations/cargo.py +1 -1
- pyinfra/operations/choco.py +1 -1
- pyinfra/operations/dnf.py +4 -4
- pyinfra/operations/files.py +108 -321
- pyinfra/operations/gem.py +1 -1
- pyinfra/operations/git.py +6 -37
- pyinfra/operations/iptables.py +2 -10
- pyinfra/operations/launchd.py +1 -1
- pyinfra/operations/lxd.py +1 -9
- pyinfra/operations/mysql.py +5 -28
- pyinfra/operations/npm.py +1 -1
- pyinfra/operations/openrc.py +1 -1
- pyinfra/operations/pacman.py +3 -3
- pyinfra/operations/pip.py +14 -15
- pyinfra/operations/pkg.py +1 -1
- pyinfra/operations/pkgin.py +3 -3
- pyinfra/operations/postgres.py +347 -0
- pyinfra/operations/postgresql.py +17 -380
- pyinfra/operations/python.py +2 -17
- pyinfra/operations/selinux.py +5 -28
- pyinfra/operations/server.py +59 -84
- pyinfra/operations/snap.py +1 -3
- pyinfra/operations/ssh.py +8 -23
- pyinfra/operations/systemd.py +7 -7
- pyinfra/operations/sysvinit.py +3 -12
- pyinfra/operations/upstart.py +4 -4
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/files.py +2 -2
- pyinfra/operations/util/packaging.py +6 -24
- pyinfra/operations/util/service.py +18 -37
- pyinfra/operations/vzctl.py +2 -2
- pyinfra/operations/xbps.py +3 -3
- pyinfra/operations/yum.py +4 -4
- pyinfra/operations/zypper.py +4 -4
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/METADATA +19 -22
- pyinfra-3.0b1.dist-info/RECORD +163 -0
- pyinfra-3.0b1.dist-info/entry_points.txt +11 -0
- pyinfra_cli/__main__.py +2 -0
- pyinfra_cli/commands.py +7 -2
- pyinfra_cli/exceptions.py +83 -42
- pyinfra_cli/inventory.py +19 -4
- pyinfra_cli/log.py +17 -3
- pyinfra_cli/main.py +133 -90
- pyinfra_cli/prints.py +93 -129
- pyinfra_cli/util.py +60 -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 +100 -200
- 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 +66 -107
- tests/test_connectors/test_terraform.py +9 -15
- tests/test_connectors/test_util.py +24 -46
- tests/test_connectors/test_vagrant.py +4 -4
- 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.0b1.dist-info}/LICENSE.md +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/WHEEL +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.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,149 @@ 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
|
+
# Ensure the sudo password is set from either the direct arguments or any
|
|
278
|
+
# connector data value.
|
|
279
|
+
command_arguments["_sudo_password"] = command_arguments.get(
|
|
280
|
+
"_sudo_password",
|
|
281
|
+
host.connector_data.get("prompted_sudo_password"),
|
|
282
|
+
)
|
|
283
|
+
if command_arguments["_sudo_password"]:
|
|
284
|
+
# Ensure the askpass path is correctly set and passed through
|
|
285
|
+
_ensure_sudo_askpass_set_for_host(host)
|
|
286
|
+
command_arguments["_sudo_askpass_path"] = host.connector_data["sudo_askpass_path"]
|
|
287
|
+
return make_unix_command(command, **command_arguments)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# Connector command generation
|
|
291
|
+
#
|
|
214
292
|
|
|
215
293
|
|
|
216
294
|
def make_unix_command(
|
|
217
|
-
command,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
295
|
+
command: StringCommand,
|
|
296
|
+
_env=None,
|
|
297
|
+
_chdir=None,
|
|
298
|
+
_shell_executable="sh",
|
|
221
299
|
# Su config
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
300
|
+
_su_user=None,
|
|
301
|
+
_use_su_login=False,
|
|
302
|
+
_su_shell=None,
|
|
303
|
+
_preserve_su_env=False,
|
|
226
304
|
# Sudo config
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
305
|
+
_sudo=False,
|
|
306
|
+
_sudo_user=None,
|
|
307
|
+
_use_sudo_login=False,
|
|
308
|
+
_sudo_password=False,
|
|
309
|
+
_sudo_askpass_path=None,
|
|
310
|
+
_preserve_sudo_env=False,
|
|
233
311
|
# Doas config
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
):
|
|
312
|
+
_doas=False,
|
|
313
|
+
_doas_user=None,
|
|
314
|
+
) -> StringCommand:
|
|
237
315
|
"""
|
|
238
316
|
Builds a shell command with various kwargs.
|
|
239
317
|
"""
|
|
240
318
|
|
|
241
|
-
if
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if isinstance(command, bytes):
|
|
245
|
-
command = command.decode("utf-8")
|
|
319
|
+
if _shell_executable is not None and not isinstance(_shell_executable, str):
|
|
320
|
+
_shell_executable = "sh"
|
|
246
321
|
|
|
247
|
-
if
|
|
248
|
-
env_string = " ".join(['"{0}={1}"'.format(key, value) for key, value in
|
|
322
|
+
if _env:
|
|
323
|
+
env_string = " ".join(['"{0}={1}"'.format(key, value) for key, value in _env.items()])
|
|
249
324
|
command = StringCommand("export", env_string, "&&", command)
|
|
250
325
|
|
|
251
|
-
if
|
|
252
|
-
command = StringCommand("cd",
|
|
326
|
+
if _chdir:
|
|
327
|
+
command = StringCommand("cd", _chdir, "&&", command)
|
|
253
328
|
|
|
254
|
-
command_bits = []
|
|
329
|
+
command_bits: list[Union[str, StringCommand, QuoteString]] = []
|
|
255
330
|
|
|
256
|
-
if
|
|
331
|
+
if _doas:
|
|
257
332
|
command_bits.extend(["doas", "-n"])
|
|
258
333
|
|
|
259
|
-
if
|
|
260
|
-
command_bits.extend(["-u",
|
|
334
|
+
if _doas_user:
|
|
335
|
+
command_bits.extend(["-u", _doas_user])
|
|
261
336
|
|
|
262
|
-
if
|
|
337
|
+
if _sudo_password and _sudo_askpass_path:
|
|
263
338
|
command_bits.extend(
|
|
264
339
|
[
|
|
265
340
|
"env",
|
|
266
|
-
"SUDO_ASKPASS={0}".format(
|
|
267
|
-
MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR,
|
|
341
|
+
"SUDO_ASKPASS={0}".format(_sudo_askpass_path),
|
|
342
|
+
MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, _sudo_password)),
|
|
268
343
|
],
|
|
269
344
|
)
|
|
270
345
|
|
|
271
|
-
if
|
|
346
|
+
if _sudo:
|
|
272
347
|
command_bits.extend(["sudo", "-H"])
|
|
273
348
|
|
|
274
|
-
if
|
|
349
|
+
if _sudo_password:
|
|
275
350
|
command_bits.extend(["-A", "-k"]) # use askpass, disable cache
|
|
276
351
|
else:
|
|
277
352
|
command_bits.append("-n") # disable prompt/interactivity
|
|
278
353
|
|
|
279
|
-
if
|
|
354
|
+
if _use_sudo_login:
|
|
280
355
|
command_bits.append("-i")
|
|
281
356
|
|
|
282
|
-
if
|
|
357
|
+
if _preserve_sudo_env:
|
|
283
358
|
command_bits.append("-E")
|
|
284
359
|
|
|
285
|
-
if
|
|
286
|
-
command_bits.extend(("-u",
|
|
360
|
+
if _sudo_user:
|
|
361
|
+
command_bits.extend(("-u", _sudo_user))
|
|
287
362
|
|
|
288
|
-
if
|
|
363
|
+
if _su_user:
|
|
289
364
|
command_bits.append("su")
|
|
290
365
|
|
|
291
|
-
if
|
|
366
|
+
if _use_su_login:
|
|
292
367
|
_show_use_su_login_warning()
|
|
293
368
|
command_bits.append("-l")
|
|
294
369
|
|
|
295
|
-
if
|
|
370
|
+
if _preserve_su_env:
|
|
296
371
|
command_bits.append("-m")
|
|
297
372
|
|
|
298
|
-
if
|
|
299
|
-
command_bits.extend(["-s", "`which {0}`".format(
|
|
373
|
+
if _su_shell:
|
|
374
|
+
command_bits.extend(["-s", "`which {0}`".format(_su_shell)])
|
|
300
375
|
|
|
301
|
-
command_bits.extend([
|
|
376
|
+
command_bits.extend([_su_user, "-c"])
|
|
302
377
|
|
|
303
|
-
if
|
|
378
|
+
if _shell_executable is not None:
|
|
304
379
|
# Quote the whole shell -c 'command' as BSD `su` does not have a shell option
|
|
305
380
|
command_bits.append(
|
|
306
|
-
QuoteString(StringCommand(
|
|
381
|
+
QuoteString(StringCommand(_shell_executable, "-c", QuoteString(command))),
|
|
307
382
|
)
|
|
308
383
|
else:
|
|
309
384
|
command_bits.append(QuoteString(StringCommand(command)))
|
|
310
385
|
else:
|
|
311
|
-
if
|
|
312
|
-
command_bits.extend([
|
|
386
|
+
if _shell_executable is not None:
|
|
387
|
+
command_bits.extend([_shell_executable, "-c", QuoteString(command)])
|
|
313
388
|
else:
|
|
314
389
|
command_bits.extend([command])
|
|
315
390
|
|
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:
|
|
@@ -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
|
@@ -5,6 +5,7 @@ are imported and used throughout pyinfra and end user deploy code (CLI mode).
|
|
|
5
5
|
These variables always represent the current executing pyinfra context.
|
|
6
6
|
"""
|
|
7
7
|
from contextlib import contextmanager
|
|
8
|
+
from types import ModuleType
|
|
8
9
|
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
10
11
|
from gevent.local import local
|
|
@@ -17,12 +18,12 @@ if TYPE_CHECKING:
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class container:
|
|
20
|
-
|
|
21
|
+
module = None
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class ContextObject:
|
|
24
25
|
_container_cls = container
|
|
25
|
-
_base_cls
|
|
26
|
+
_base_cls: ModuleType
|
|
26
27
|
|
|
27
28
|
def __init__(self):
|
|
28
29
|
self._container = self._container_cls()
|