pyinfra 2.9.1__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.
Files changed (156) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +265 -253
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +68 -53
  5. pyinfra/api/config.py +139 -32
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +7 -26
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +102 -137
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +21 -25
  13. pyinfra/api/operation.py +240 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +79 -86
  17. pyinfra/connectors/base.py +147 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +220 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +196 -208
  22. pyinfra/connectors/ssh.py +530 -613
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +211 -137
  27. pyinfra/connectors/vagrant.py +60 -53
  28. pyinfra/context.py +4 -2
  29. pyinfra/facts/apk.py +2 -0
  30. pyinfra/facts/apt.py +2 -0
  31. pyinfra/facts/brew.py +2 -0
  32. pyinfra/facts/bsdinit.py +2 -0
  33. pyinfra/facts/cargo.py +2 -0
  34. pyinfra/facts/choco.py +2 -0
  35. pyinfra/facts/deb.py +7 -2
  36. pyinfra/facts/dnf.py +2 -0
  37. pyinfra/facts/docker.py +19 -0
  38. pyinfra/facts/files.py +47 -32
  39. pyinfra/facts/gem.py +2 -0
  40. pyinfra/facts/git.py +3 -1
  41. pyinfra/facts/gpg.py +3 -1
  42. pyinfra/facts/hardware.py +34 -24
  43. pyinfra/facts/iptables.py +5 -3
  44. pyinfra/facts/launchd.py +2 -0
  45. pyinfra/facts/lxd.py +2 -0
  46. pyinfra/facts/mysql.py +13 -6
  47. pyinfra/facts/npm.py +1 -0
  48. pyinfra/facts/openrc.py +2 -0
  49. pyinfra/facts/pacman.py +6 -2
  50. pyinfra/facts/pip.py +2 -0
  51. pyinfra/facts/pkg.py +2 -0
  52. pyinfra/facts/pkgin.py +2 -0
  53. pyinfra/facts/postgres.py +168 -0
  54. pyinfra/facts/postgresql.py +6 -160
  55. pyinfra/facts/rpm.py +12 -9
  56. pyinfra/facts/runit.py +68 -0
  57. pyinfra/facts/selinux.py +3 -1
  58. pyinfra/facts/server.py +80 -36
  59. pyinfra/facts/snap.py +2 -0
  60. pyinfra/facts/systemd.py +31 -12
  61. pyinfra/facts/sysvinit.py +10 -10
  62. pyinfra/facts/upstart.py +2 -0
  63. pyinfra/facts/util/packaging.py +7 -4
  64. pyinfra/facts/vzctl.py +2 -0
  65. pyinfra/facts/xbps.py +2 -0
  66. pyinfra/facts/yum.py +2 -0
  67. pyinfra/facts/zypper.py +2 -0
  68. pyinfra/local.py +4 -5
  69. pyinfra/operations/apk.py +6 -4
  70. pyinfra/operations/apt.py +46 -65
  71. pyinfra/operations/brew.py +17 -22
  72. pyinfra/operations/bsdinit.py +9 -7
  73. pyinfra/operations/cargo.py +4 -2
  74. pyinfra/operations/choco.py +4 -2
  75. pyinfra/operations/dnf.py +19 -23
  76. pyinfra/operations/docker.py +339 -0
  77. pyinfra/operations/files.py +188 -386
  78. pyinfra/operations/gem.py +4 -2
  79. pyinfra/operations/git.py +24 -53
  80. pyinfra/operations/iptables.py +29 -35
  81. pyinfra/operations/launchd.py +6 -7
  82. pyinfra/operations/lxd.py +8 -13
  83. pyinfra/operations/mysql.py +62 -81
  84. pyinfra/operations/npm.py +9 -2
  85. pyinfra/operations/openrc.py +6 -4
  86. pyinfra/operations/pacman.py +7 -8
  87. pyinfra/operations/pip.py +25 -24
  88. pyinfra/operations/pkg.py +4 -2
  89. pyinfra/operations/pkgin.py +6 -4
  90. pyinfra/operations/postgres.py +349 -0
  91. pyinfra/operations/postgresql.py +18 -379
  92. pyinfra/operations/puppet.py +3 -1
  93. pyinfra/operations/python.py +8 -19
  94. pyinfra/operations/runit.py +182 -0
  95. pyinfra/operations/selinux.py +47 -44
  96. pyinfra/operations/server.py +111 -127
  97. pyinfra/operations/snap.py +4 -4
  98. pyinfra/operations/ssh.py +20 -33
  99. pyinfra/operations/systemd.py +19 -15
  100. pyinfra/operations/sysvinit.py +9 -16
  101. pyinfra/operations/upstart.py +9 -7
  102. pyinfra/operations/util/__init__.py +12 -0
  103. pyinfra/operations/util/docker.py +177 -0
  104. pyinfra/operations/util/files.py +24 -16
  105. pyinfra/operations/util/packaging.py +55 -57
  106. pyinfra/operations/util/service.py +39 -51
  107. pyinfra/operations/vzctl.py +12 -10
  108. pyinfra/operations/xbps.py +6 -4
  109. pyinfra/operations/yum.py +18 -22
  110. pyinfra/operations/zypper.py +12 -13
  111. pyinfra/version.py +5 -2
  112. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
  113. pyinfra-3.0.dist-info/RECORD +167 -0
  114. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
  115. pyinfra-3.0.dist-info/entry_points.txt +11 -0
  116. pyinfra_cli/__main__.py +4 -3
  117. pyinfra_cli/commands.py +7 -2
  118. pyinfra_cli/exceptions.py +78 -42
  119. pyinfra_cli/inventory.py +40 -6
  120. pyinfra_cli/log.py +17 -3
  121. pyinfra_cli/main.py +133 -90
  122. pyinfra_cli/prints.py +95 -127
  123. pyinfra_cli/util.py +62 -29
  124. tests/test_api/test_api.py +2 -0
  125. tests/test_api/test_api_arguments.py +13 -13
  126. tests/test_api/test_api_deploys.py +28 -29
  127. tests/test_api/test_api_facts.py +60 -98
  128. tests/test_api/test_api_operations.py +101 -201
  129. tests/test_cli/test_cli.py +18 -49
  130. tests/test_cli/test_cli_deploy.py +11 -37
  131. tests/test_cli/test_cli_exceptions.py +50 -19
  132. tests/test_cli/util.py +1 -1
  133. tests/test_connectors/test_chroot.py +6 -6
  134. tests/test_connectors/test_docker.py +4 -4
  135. tests/test_connectors/test_dockerssh.py +38 -50
  136. tests/test_connectors/test_local.py +11 -12
  137. tests/test_connectors/test_ssh.py +105 -93
  138. tests/test_connectors/test_terraform.py +9 -15
  139. tests/test_connectors/test_util.py +24 -46
  140. tests/test_connectors/test_vagrant.py +7 -7
  141. pyinfra/api/operation.pyi +0 -117
  142. pyinfra/connectors/ansible.py +0 -171
  143. pyinfra/connectors/mech.py +0 -186
  144. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  145. pyinfra/connectors/winrm.py +0 -320
  146. pyinfra/facts/windows.py +0 -366
  147. pyinfra/facts/windows_files.py +0 -90
  148. pyinfra/operations/windows.py +0 -59
  149. pyinfra/operations/windows_files.py +0 -551
  150. pyinfra-2.9.1.dist-info/RECORD +0 -170
  151. pyinfra-2.9.1.dist-info/entry_points.txt +0 -14
  152. tests/test_connectors/test_ansible.py +0 -64
  153. tests/test_connectors/test_mech.py +0 -126
  154. tests/test_connectors/test_winrm.py +0 -76
  155. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
  156. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
@@ -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=None,
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
- combined_output = read_buffers_into_queue(
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
- def read_buffers_into_queue(
95
- stdout_buffer,
96
- stderr_buffer,
97
- timeout,
98
- print_output,
99
- print_prefix,
100
- ):
101
- output_queue = Queue()
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 split_combined_output(combined_output):
142
- stdout = []
143
- stderr = []
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
- for type_, line in combined_output:
146
- if type_ == "stdout":
147
- stdout.append(line)
148
- elif type_ == "stderr":
149
- stderr.append(line)
150
- else: # pragma: no cover
151
- raise ValueError("Incorrect output line type: {0}".format(type_))
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 stdout, stderr
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 _get_sudo_password(host, use_sudo_password):
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 make_unix_command_for_host(state, host, *command_args, **command_kwargs):
208
- use_sudo_password = command_kwargs.pop("use_sudo_password", None)
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
- return make_unix_command(*command_args, **command_kwargs)
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
- env=None,
219
- chdir=None,
220
- shell_executable="sh",
294
+ command: StringCommand,
295
+ _env=None,
296
+ _chdir=None,
297
+ _shell_executable="sh",
221
298
  # Su config
222
- su_user=None,
223
- use_su_login=False,
224
- su_shell=None,
225
- preserve_su_env=False,
299
+ _su_user=None,
300
+ _use_su_login=False,
301
+ _su_shell=None,
302
+ _preserve_su_env=False,
226
303
  # Sudo config
227
- sudo=False,
228
- sudo_user=None,
229
- use_sudo_login=False,
230
- sudo_password=False,
231
- sudo_askpass_path=None,
232
- preserve_sudo_env=False,
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
- doas=False,
235
- doas_user=None,
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 shell_executable is not None and not isinstance(shell_executable, str):
242
- shell_executable = "sh"
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 env:
248
- env_string = " ".join(['"{0}={1}"'.format(key, value) for key, value in env.items()])
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 chdir:
252
- command = StringCommand("cd", chdir, "&&", command)
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 doas:
330
+ if _doas:
257
331
  command_bits.extend(["doas", "-n"])
258
332
 
259
- if doas_user:
260
- command_bits.extend(["-u", doas_user])
333
+ if _doas_user:
334
+ command_bits.extend(["-u", _doas_user])
261
335
 
262
- if sudo_password and sudo_askpass_path:
336
+ if _sudo_password and _sudo_askpass_path:
263
337
  command_bits.extend(
264
338
  [
265
339
  "env",
266
- "SUDO_ASKPASS={0}".format(sudo_askpass_path),
267
- MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, sudo_password)),
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 sudo:
345
+ if _sudo:
272
346
  command_bits.extend(["sudo", "-H"])
273
347
 
274
- if sudo_password:
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 use_sudo_login:
353
+ if _use_sudo_login:
280
354
  command_bits.append("-i")
281
355
 
282
- if preserve_sudo_env:
356
+ if _preserve_sudo_env:
283
357
  command_bits.append("-E")
284
358
 
285
- if sudo_user:
286
- command_bits.extend(("-u", sudo_user))
359
+ if _sudo_user:
360
+ command_bits.extend(("-u", _sudo_user))
287
361
 
288
- if su_user:
362
+ if _su_user:
289
363
  command_bits.append("su")
290
364
 
291
- if use_su_login:
365
+ if _use_su_login:
292
366
  _show_use_su_login_warning()
293
367
  command_bits.append("-l")
294
368
 
295
- if preserve_su_env:
369
+ if _preserve_su_env:
296
370
  command_bits.append("-m")
297
371
 
298
- if su_shell:
299
- command_bits.extend(["-s", "`which {0}`".format(su_shell)])
372
+ if _su_shell:
373
+ command_bits.extend(["-s", "`which {0}`".format(_su_shell)])
300
374
 
301
- command_bits.extend([su_user, "-c"])
375
+ command_bits.extend([_su_user, "-c"])
302
376
 
303
- if shell_executable is not None:
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(shell_executable, "-c", QuoteString(command))),
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 shell_executable is not None:
312
- command_bits.extend([shell_executable, "-c", QuoteString(command)])
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
 
@@ -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
- def make_names_data(limit=None):
132
- vagrant_ssh_info = get_vagrant_config(limit)
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
- logger.debug("Got Vagrant SSH info: \n%s", vagrant_ssh_info)
122
+ .. code:: shell
135
123
 
136
- hosts = []
137
- current_host = None
124
+ # Run on all hosts
125
+ pyinfra @vagrant ...
138
126
 
139
- for line in vagrant_ssh_info:
140
- # Vagrant outputs an empty line between each host
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
- current_host = None
146
- continue
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
- key, value = line.split(" ", 1)
152
+ key, value = line.split(" ", 1)
149
153
 
150
- if key == "Host":
151
- if current_host:
152
- hosts.append(_make_name_data(current_host))
154
+ if key == "Host":
155
+ if current_host:
156
+ hosts.append(_make_name_data(current_host))
153
157
 
154
- # Set the new host
155
- current_host = {
156
- key: value,
157
- }
158
+ # Set the new host
159
+ current_host = {
160
+ key: value,
161
+ }
158
162
 
159
- elif current_host:
160
- current_host[key] = value
163
+ elif current_host:
164
+ current_host[key] = value
161
165
 
162
- else:
163
- logger.debug("Extra Vagrant SSH key/value (%s=%s)", key, value)
166
+ else:
167
+ logger.debug("Extra Vagrant SSH key/value (%s=%s)", key, value)
164
168
 
165
- if current_host:
166
- hosts.append(_make_name_data(current_host))
169
+ if current_host:
170
+ hosts.append(_make_name_data(current_host))
167
171
 
168
- if not hosts:
169
- if limit:
170
- raise InventoryError("No running Vagrant instances matching `{0}` found!".format(limit))
171
- raise InventoryError("No running Vagrant instances found!")
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
- return hosts
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
- pass
22
+ module = None
21
23
 
22
24
 
23
25
  class ContextObject:
24
26
  _container_cls = container
25
- _base_cls = None
27
+ _base_cls: ModuleType
26
28
 
27
29
  def __init__(self):
28
30
  self._container = self._container_cls()
pyinfra/facts/apk.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
pyinfra/facts/apt.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra.api import FactBase
pyinfra/facts/brew.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra import logger
pyinfra/facts/bsdinit.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from .sysvinit import InitdStatus
2
4
 
3
5
 
pyinfra/facts/cargo.py CHANGED
@@ -1,5 +1,7 @@
1
1
  # encoding: utf8
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from pyinfra.api import FactBase
4
6
 
5
7
  from .util.packaging import parse_packages
pyinfra/facts/choco.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages