pyinfra 3.3__py2.py3-none-any.whl → 3.4__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 (47) hide show
  1. pyinfra/api/arguments.py +8 -16
  2. pyinfra/api/deploy.py +1 -1
  3. pyinfra/api/facts.py +10 -26
  4. pyinfra/api/host.py +10 -4
  5. pyinfra/api/inventory.py +5 -2
  6. pyinfra/api/operation.py +1 -1
  7. pyinfra/api/util.py +20 -6
  8. pyinfra/connectors/docker.py +117 -38
  9. pyinfra/connectors/dockerssh.py +1 -0
  10. pyinfra/connectors/local.py +1 -0
  11. pyinfra/connectors/ssh.py +1 -0
  12. pyinfra/connectors/sshuserclient/client.py +5 -5
  13. pyinfra/connectors/terraform.py +3 -0
  14. pyinfra/connectors/vagrant.py +3 -0
  15. pyinfra/context.py +14 -5
  16. pyinfra/facts/brew.py +1 -0
  17. pyinfra/facts/docker.py +6 -2
  18. pyinfra/facts/git.py +10 -0
  19. pyinfra/facts/hardware.py +1 -1
  20. pyinfra/facts/opkg.py +1 -0
  21. pyinfra/facts/server.py +81 -23
  22. pyinfra/facts/systemd.py +1 -1
  23. pyinfra/operations/crontab.py +7 -5
  24. pyinfra/operations/docker.py +2 -0
  25. pyinfra/operations/files.py +64 -21
  26. pyinfra/operations/flatpak.py +17 -2
  27. pyinfra/operations/git.py +6 -2
  28. pyinfra/operations/server.py +34 -24
  29. pyinfra/operations/util/docker.py +4 -0
  30. pyinfra/operations/util/files.py +44 -3
  31. {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/METADATA +5 -4
  32. {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/RECORD +47 -47
  33. {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/entry_points.txt +1 -0
  34. pyinfra_cli/inventory.py +1 -1
  35. pyinfra_cli/main.py +4 -2
  36. tests/test_api/test_api_arguments.py +25 -20
  37. tests/test_api/test_api_facts.py +28 -15
  38. tests/test_api/test_api_operations.py +43 -44
  39. tests/test_cli/test_cli.py +17 -17
  40. tests/test_cli/test_cli_inventory.py +4 -4
  41. tests/test_cli/test_context_objects.py +26 -26
  42. tests/test_connectors/test_docker.py +83 -43
  43. tests/test_connectors/test_ssh.py +153 -132
  44. tests/test_connectors/test_sshuserclient.py +10 -5
  45. {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/LICENSE.md +0 -0
  46. {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/WHEEL +0 -0
  47. {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/top_level.txt +0 -0
pyinfra/api/arguments.py CHANGED
@@ -18,14 +18,12 @@ from typing import (
18
18
 
19
19
  from typing_extensions import TypedDict
20
20
 
21
- from pyinfra import context
22
21
  from pyinfra.api.exceptions import ArgumentTypeError
23
- from pyinfra.api.state import State
24
22
  from pyinfra.api.util import raise_if_bad_type
23
+ from pyinfra.context import ctx_config
25
24
 
26
25
  if TYPE_CHECKING:
27
- from pyinfra.api.config import Config
28
- from pyinfra.api.host import Host
26
+ from pyinfra.api import Config, Host, State
29
27
 
30
28
  T = TypeVar("T")
31
29
  default_sentinel = object()
@@ -292,10 +290,9 @@ __argument_docs__ = {
292
290
 
293
291
 
294
292
  def pop_global_arguments(
293
+ state: "State",
294
+ host: "Host",
295
295
  kwargs: dict[str, Any],
296
- state: Optional["State"] = None,
297
- host: Optional["Host"] = None,
298
- keys_to_check=None,
299
296
  ) -> tuple[AllArguments, list[str]]:
300
297
  """
301
298
  Pop and return operation global keyword arguments, in preferred order:
@@ -306,22 +303,17 @@ def pop_global_arguments(
306
303
  + From the config variables
307
304
  """
308
305
 
309
- state = state or context.state
310
- host = host or context.host
311
-
312
306
  config = state.config
313
- if context.ctx_config.isset():
314
- config = context.config
307
+ if ctx_config.isset():
308
+ config = config
315
309
 
316
- meta_kwargs: dict[str, Any] = host.current_deploy_kwargs or {} # type: ignore[assignment]
310
+ cdkwargs = host.current_deploy_kwargs
311
+ meta_kwargs: dict[str, Any] = cdkwargs or {} # type: ignore[assignment]
317
312
 
318
313
  arguments: dict[str, Any] = {}
319
314
  found_keys: list[str] = []
320
315
 
321
316
  for key, type_ in all_global_arguments():
322
- if keys_to_check and key not in keys_to_check:
323
- continue
324
-
325
317
  argument_meta = all_argument_meta[key]
326
318
  handler = argument_meta.handler
327
319
  default: Any = argument_meta.default(config)
pyinfra/api/deploy.py CHANGED
@@ -82,7 +82,7 @@ def deploy(
82
82
  def _wrap_deploy(func: Callable[P, Any]) -> PyinfraOperation[P]:
83
83
  @wraps(func)
84
84
  def decorated_func(*args: P.args, **kwargs: P.kwargs) -> Any:
85
- deploy_kwargs, _ = pop_global_arguments(kwargs)
85
+ deploy_kwargs, _ = pop_global_arguments(context.state, context.host, kwargs)
86
86
 
87
87
  deploy_data = getattr(func, "deploy_data", None)
88
88
 
pyinfra/api/facts.py CHANGED
@@ -31,14 +31,12 @@ from pyinfra.api.util import (
31
31
  print_host_combined_output,
32
32
  )
33
33
  from pyinfra.connectors.util import CommandOutput
34
- from pyinfra.context import ctx_host, ctx_state
35
34
  from pyinfra.progress import progress_spinner
36
35
 
37
36
  from .arguments import CONNECTOR_ARGUMENT_KEYS
38
37
 
39
38
  if TYPE_CHECKING:
40
- from pyinfra.api.host import Host
41
- from pyinfra.api.state import State
39
+ from pyinfra.api import Host, State
42
40
 
43
41
  SUDO_REGEX = r"^sudo: unknown user"
44
42
  SU_REGEXES = (
@@ -121,21 +119,19 @@ def _make_command(command_attribute, host_args):
121
119
  return command_attribute
122
120
 
123
121
 
124
- def _handle_fact_kwargs(state, host, cls, args, kwargs):
122
+ def _handle_fact_kwargs(state: "State", host: "Host", cls, args, kwargs):
125
123
  args = args or []
126
124
  kwargs = kwargs or {}
127
125
 
128
126
  # Start with a (shallow) copy of current operation kwargs if any
129
- ctx_kwargs = (host.current_op_global_arguments or {}).copy()
127
+ ctx_kwargs: dict[str, Any] = (
128
+ cast(dict[str, Any], host.current_op_global_arguments) or {}
129
+ ).copy()
130
130
  # Update with the input kwargs (overrides)
131
131
  ctx_kwargs.update(kwargs)
132
132
 
133
133
  # Pop executor kwargs, pass remaining
134
- global_kwargs, _ = pop_global_arguments(
135
- ctx_kwargs,
136
- state=state,
137
- host=host,
138
- )
134
+ global_kwargs, _ = pop_global_arguments(state, host, cast(dict[str, Any], ctx_kwargs))
139
135
 
140
136
  fact_kwargs = {key: value for key, value in kwargs.items() if key not in global_kwargs}
141
137
 
@@ -146,14 +142,12 @@ def _handle_fact_kwargs(state, host, cls, args, kwargs):
146
142
  return fact_kwargs, global_kwargs
147
143
 
148
144
 
149
- def get_facts(state: "State", *args, **kwargs):
150
- def get_fact_with_context(state, host, *args, **kwargs):
151
- with ctx_state.use(state):
152
- with ctx_host.use(host):
153
- return get_fact(state, host, *args, **kwargs)
145
+ def get_facts(state, *args, **kwargs):
146
+ def get_host_fact(host, *args, **kwargs):
147
+ return get_fact(state, host, *args, **kwargs)
154
148
 
155
149
  greenlet_to_host = {
156
- state.pool.spawn(get_fact_with_context, state, host, *args, **kwargs): host
150
+ state.pool.spawn(get_host_fact, host, *args, **kwargs): host
157
151
  for host in state.inventory.iter_active_hosts()
158
152
  }
159
153
 
@@ -311,13 +305,3 @@ def _get_fact(
311
305
  state.fail_hosts({host})
312
306
 
313
307
  return data
314
-
315
-
316
- def get_host_fact(
317
- state: "State",
318
- host: "Host",
319
- cls,
320
- args: Optional[Iterable] = None,
321
- kwargs: Optional[dict] = None,
322
- ) -> Any:
323
- return get_fact(state, host, cls, args=args, kwargs=kwargs)
pyinfra/api/host.py CHANGED
@@ -25,7 +25,7 @@ from pyinfra.connectors.util import CommandOutput, remove_any_sudo_askpass_file
25
25
 
26
26
  from .connectors import get_execution_connector
27
27
  from .exceptions import ConnectError
28
- from .facts import FactBase, ShortFactBase, get_host_fact
28
+ from .facts import FactBase, ShortFactBase, get_fact
29
29
  from .util import memoize, sha1_hash
30
30
 
31
31
  if TYPE_CHECKING:
@@ -328,12 +328,18 @@ class Host:
328
328
 
329
329
  return temp_directory
330
330
 
331
- def get_temp_filename(self, hash_key: Optional[str] = None, hash_filename: bool = True):
331
+ def get_temp_filename(
332
+ self,
333
+ hash_key: Optional[str] = None,
334
+ hash_filename: bool = True,
335
+ *,
336
+ temp_directory: Optional[str] = None,
337
+ ):
332
338
  """
333
339
  Generate a temporary filename for this deploy.
334
340
  """
335
341
 
336
- temp_directory = self._get_temp_directory()
342
+ temp_directory = temp_directory or self._get_temp_directory()
337
343
 
338
344
  if not hash_key:
339
345
  hash_key = str(uuid4())
@@ -358,7 +364,7 @@ class Host:
358
364
  """
359
365
  Get a fact for this host, reading from the cache if present.
360
366
  """
361
- return get_host_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
367
+ return get_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
362
368
 
363
369
  # Connector proxy
364
370
  #
pyinfra/api/inventory.py CHANGED
@@ -36,6 +36,7 @@ class Inventory:
36
36
  """
37
37
 
38
38
  state: "State"
39
+ groups: dict[str, list[Host]]
39
40
 
40
41
  @staticmethod
41
42
  def empty():
@@ -181,7 +182,7 @@ class Inventory:
181
182
  """
182
183
  return len(self.state.activated_hosts)
183
184
 
184
- def get_host(self, name: str, default=NoHostError):
185
+ def get_host(self, name: str, default=NoHostError) -> Host:
185
186
  """
186
187
  Get a single host by name.
187
188
  """
@@ -192,9 +193,10 @@ class Inventory:
192
193
  if default is NoHostError:
193
194
  raise NoHostError("No such host: {0}".format(name))
194
195
 
196
+ # TODO: remove default here?
195
197
  return default
196
198
 
197
- def get_group(self, name: str, default=NoGroupError):
199
+ def get_group(self, name: str, default=NoGroupError) -> list[Host]:
198
200
  """
199
201
  Get a list of hosts belonging to a group.
200
202
  """
@@ -205,6 +207,7 @@ class Inventory:
205
207
  if default is NoGroupError:
206
208
  raise NoGroupError("No such group: {0}".format(name))
207
209
 
210
+ # TODO: remove default here?
208
211
  return default
209
212
 
210
213
  def get_data(self):
pyinfra/api/operation.py CHANGED
@@ -232,7 +232,7 @@ def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> Py
232
232
  # Configure operation
233
233
  #
234
234
  # Get the meta kwargs (globals that apply to all hosts)
235
- global_arguments, global_argument_keys = pop_global_arguments(kwargs)
235
+ global_arguments, global_argument_keys = pop_global_arguments(state, host, kwargs)
236
236
 
237
237
  names, add_args = generate_operation_name(func, host, kwargs, global_arguments)
238
238
  op_order, op_hash = solve_operation_consistency(names, state, host)
pyinfra/api/util.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  from functools import wraps
4
- from hashlib import sha1
5
+ from hashlib import md5, sha1, sha256
5
6
  from inspect import getframeinfo, stack
6
7
  from io import BytesIO, StringIO
7
8
  from os import getcwd, path, stat
@@ -341,7 +342,7 @@ class get_file_io:
341
342
  _close: bool = False
342
343
  _file_io: IO[Any]
343
344
 
344
- def __init__(self, filename_or_io, mode="rb"):
345
+ def __init__(self, filename_or_io: str | IO, mode: str = "rb"):
345
346
  if not (
346
347
  # Check we can be read
347
348
  hasattr(filename_or_io, "read")
@@ -389,19 +390,32 @@ class get_file_io:
389
390
  return self.filename_or_io
390
391
 
391
392
 
392
- def get_file_sha1(filename_or_io):
393
+ def get_file_md5(filename_or_io: str | IO):
394
+ return _get_file_digest(filename_or_io, md5())
395
+
396
+
397
+ def get_file_sha1(filename_or_io: str | IO):
398
+ return _get_file_digest(filename_or_io, sha1())
399
+
400
+
401
+ def get_file_sha256(filename_or_io: str | IO):
402
+ return _get_file_digest(filename_or_io, sha256())
403
+
404
+
405
+ def _get_file_digest(filename_or_io: str | IO, hasher: hashlib._Hash):
393
406
  """
394
- Calculates the SHA1 of a file or file object using a buffer to handle larger files.
407
+ Calculates the hash of a file or file object using a buffer to handle larger files.
395
408
  """
396
409
 
397
410
  file_data = get_file_io(filename_or_io)
398
411
  cache_key = file_data.cache_key
412
+ if cache_key:
413
+ cache_key = f"{cache_key}_{hasher.name}"
399
414
 
400
415
  if cache_key and cache_key in FILE_SHAS:
401
416
  return FILE_SHAS[cache_key]
402
417
 
403
418
  with file_data as file_io:
404
- hasher = sha1()
405
419
  buff = file_io.read(BLOCKSIZE)
406
420
 
407
421
  while len(buff) > 0:
@@ -425,7 +439,7 @@ def get_path_permissions_mode(pathname: str):
425
439
  """
426
440
 
427
441
  mode_octal = oct(stat(pathname).st_mode)
428
- return mode_octal[-3:]
442
+ return int(mode_octal[-3:])
429
443
 
430
444
 
431
445
  def raise_if_bad_type(
@@ -33,29 +33,6 @@ connector_data_meta: dict[str, DataMeta] = {
33
33
  }
34
34
 
35
35
 
36
- def _find_start_docker_container(container_id) -> tuple[str, bool]:
37
- docker_info = local.shell("docker container inspect {0}".format(container_id))
38
- assert isinstance(docker_info, str)
39
- docker_info = json.loads(docker_info)[0]
40
- if docker_info["State"]["Running"] is False:
41
- logger.info("Starting stopped container: {0}".format(container_id))
42
- local.shell("docker container start {0}".format(container_id))
43
- return container_id, False
44
- return container_id, True
45
-
46
-
47
- def _start_docker_image(image_name):
48
- try:
49
- return local.shell(
50
- "docker run -d {0} tail -f /dev/null".format(image_name),
51
- splitlines=True,
52
- )[
53
- -1
54
- ] # last line is the container ID
55
- except PyinfraError as e:
56
- raise ConnectError(e.args[0])
57
-
58
-
59
36
  class DockerConnector(BaseConnector):
60
37
  """
61
38
  The Docker connector allows you to use pyinfra to create new Docker images or modify running
@@ -89,6 +66,9 @@ class DockerConnector(BaseConnector):
89
66
  writing deploys, operations or facts.
90
67
  """
91
68
 
69
+ # enable the use of other docker cli compatible tools like podman
70
+ docker_cmd = "docker"
71
+
92
72
  handles_execution = True
93
73
 
94
74
  data_cls = ConnectorData
@@ -104,29 +84,54 @@ class DockerConnector(BaseConnector):
104
84
  super().__init__(state, host)
105
85
  self.local = LocalConnector(state, host)
106
86
 
87
+ @override
107
88
  @staticmethod
108
89
  def make_names_data(name=None):
109
90
  if not name:
110
91
  raise InventoryError("No docker base ID provided!")
111
92
 
112
93
  yield (
113
- "@docker/{0}".format(name),
94
+ f"@docker/{name}",
114
95
  {"docker_identifier": name},
115
96
  ["@docker"],
116
97
  )
117
98
 
99
+ # 2 helper functions
100
+ def _find_start_docker_container(self, container_id) -> tuple[str, bool]:
101
+ docker_info = local.shell(f"{self.docker_cmd} container inspect {container_id}")
102
+ assert isinstance(docker_info, str)
103
+ docker_info = json.loads(docker_info)[0]
104
+ if docker_info["State"]["Running"] is False:
105
+ logger.info(f"Starting stopped container: {container_id}")
106
+ local.shell(f"{self.docker_cmd} container start {container_id}")
107
+ return container_id, False
108
+ return container_id, True
109
+
110
+ def _start_docker_image(self, image_name):
111
+ try:
112
+ return local.shell(
113
+ f"{self.docker_cmd} run -d {image_name} tail -f /dev/null",
114
+ splitlines=True,
115
+ )[
116
+ -1
117
+ ] # last line is the container ID
118
+ except PyinfraError as e:
119
+ raise ConnectError(e.args[0])
120
+
118
121
  @override
119
122
  def connect(self) -> None:
120
123
  self.local.connect()
121
124
 
122
125
  docker_identifier = self.data["docker_identifier"]
123
- with progress_spinner({"prepare docker container"}):
126
+ with progress_spinner({f"prepare {self.docker_cmd} container"}):
124
127
  try:
125
- self.container_id, was_running = _find_start_docker_container(docker_identifier)
128
+ self.container_id, was_running = self._find_start_docker_container(
129
+ docker_identifier
130
+ )
126
131
  if was_running:
127
132
  self.no_stop = True
128
133
  except PyinfraError:
129
- self.container_id = _start_docker_image(docker_identifier)
134
+ self.container_id = self._start_docker_image(docker_identifier)
130
135
 
131
136
  @override
132
137
  def disconnect(self) -> None:
@@ -134,26 +139,28 @@ class DockerConnector(BaseConnector):
134
139
 
135
140
  if self.no_stop:
136
141
  logger.info(
137
- "{0}docker build complete, container left running: {1}".format(
142
+ "{0}{1} build complete, container left running: {2}".format(
138
143
  self.host.print_prefix,
144
+ self.docker_cmd,
139
145
  click.style(container_id, bold=True),
140
146
  ),
141
147
  )
142
148
  return
143
149
 
144
- with progress_spinner({"docker commit"}):
145
- image_id = local.shell("docker commit {0}".format(container_id), splitlines=True)[-1][
150
+ with progress_spinner({f"{self.docker_cmd} commit"}):
151
+ image_id = local.shell(f"{self.docker_cmd} commit {container_id}", splitlines=True)[-1][
146
152
  7:19
147
153
  ] # last line is the image ID, get sha256:[XXXXXXXXXX]...
148
154
 
149
- with progress_spinner({"docker rm"}):
155
+ with progress_spinner({f"{self.docker_cmd} rm"}):
150
156
  local.shell(
151
- "docker rm -f {0}".format(container_id),
157
+ f"{self.docker_cmd} rm -f {container_id}",
152
158
  )
153
159
 
154
160
  logger.info(
155
- "{0}docker build complete, image ID: {1}".format(
161
+ "{0}{1} build complete, image ID: {2}".format(
156
162
  self.host.print_prefix,
163
+ self.docker_cmd,
157
164
  click.style(image_id, bold=True),
158
165
  ),
159
166
  )
@@ -175,7 +182,7 @@ class DockerConnector(BaseConnector):
175
182
 
176
183
  docker_flags = "-it" if local_arguments.get("_get_pty") else "-i"
177
184
  docker_command = StringCommand(
178
- "docker",
185
+ self.docker_cmd,
179
186
  "exec",
180
187
  docker_flags,
181
188
  container_id,
@@ -202,7 +209,7 @@ class DockerConnector(BaseConnector):
202
209
  **kwargs, # ignored (sudo/etc)
203
210
  ) -> bool:
204
211
  """
205
- Upload a file/IO object to the target Docker container by copying it to a
212
+ Upload a file/IO object to the target container by copying it to a
206
213
  temporary location and then uploading it into the container using ``docker cp``.
207
214
  """
208
215
 
@@ -220,7 +227,7 @@ class DockerConnector(BaseConnector):
220
227
  temp_f.write(data)
221
228
 
222
229
  docker_command = StringCommand(
223
- "docker",
230
+ self.docker_cmd,
224
231
  "cp",
225
232
  temp_filename,
226
233
  f"{self.container_id}:{remote_filename}",
@@ -260,7 +267,7 @@ class DockerConnector(BaseConnector):
260
267
  **kwargs, # ignored (sudo/etc)
261
268
  ) -> bool:
262
269
  """
263
- Download a file from the target Docker container by copying it to a temporary
270
+ Download a file from the target container by copying it to a temporary
264
271
  location and then reading that into our final file/IO object.
265
272
  """
266
273
 
@@ -268,7 +275,7 @@ class DockerConnector(BaseConnector):
268
275
 
269
276
  try:
270
277
  docker_command = StringCommand(
271
- "docker",
278
+ self.docker_cmd,
272
279
  "cp",
273
280
  f"{self.container_id}:{remote_filename}",
274
281
  temp_filename,
@@ -302,3 +309,75 @@ class DockerConnector(BaseConnector):
302
309
  )
303
310
 
304
311
  return status
312
+
313
+
314
+ class PodmanConnector(DockerConnector):
315
+ """
316
+ The Podman connector allows you to use pyinfra to create new Podman images or modify running
317
+ Podman containers.
318
+
319
+ .. note::
320
+
321
+ The Podman connector allows pyinfra to target Podman containers as inventory and is
322
+ unrelated to the :doc:`../operations/docker` & :doc:`../facts/docker`.
323
+
324
+ You can pass either an image name or existing container ID:
325
+
326
+ + Image - will create a new container from the image, execute operations against it, save into \
327
+ a new Podman image and remove the container
328
+ + Existing container ID - will execute operations against the running container, leaving it \
329
+ running
330
+
331
+ .. code:: shell
332
+
333
+ # A Podman base image must be provided
334
+ pyinfra @podman/alpine:3.8 ...
335
+
336
+ # pyinfra can run on multiple Docker images in parallel
337
+ pyinfra @podman/alpine:3.8,@podman/ubuntu:bionic ...
338
+
339
+ # Execute against a running container
340
+ pyinfra @podman/2beb8c15a1b1 ...
341
+
342
+ The Podman connector is great for testing pyinfra operations locally, rather than connecting to
343
+ a remote host over SSH each time. This gives you a fast, local-first devloop to iterate on when
344
+ writing deploys, operations or facts.
345
+ """
346
+
347
+ docker_cmd = "podman"
348
+
349
+ @override
350
+ @staticmethod
351
+ def make_names_data(name=None):
352
+ if not name:
353
+ raise InventoryError("No podman base ID provided!")
354
+
355
+ yield (
356
+ f"@podman/{name}",
357
+ {"docker_identifier": name},
358
+ ["@podman"],
359
+ )
360
+
361
+ # Duplicate function definition to swap the docstring.
362
+ @override
363
+ def put_file(
364
+ self,
365
+ filename_or_io,
366
+ remote_filename,
367
+ remote_temp_filename=None, # ignored
368
+ print_output=False,
369
+ print_input=False,
370
+ **kwargs, # ignored (sudo/etc)
371
+ ) -> bool:
372
+ """
373
+ Upload a file/IO object to the target container by copying it to a
374
+ temporary location and then uploading it into the container using ``podman cp``.
375
+ """
376
+ return super().put_file(
377
+ filename_or_io,
378
+ remote_filename,
379
+ remote_temp_filename, # ignored
380
+ print_output,
381
+ print_input,
382
+ **kwargs, # ignored (sudo/etc)
383
+ )
@@ -50,6 +50,7 @@ class DockerSSHConnector(BaseConnector):
50
50
  super().__init__(state, host)
51
51
  self.ssh = SSHConnector(state, host)
52
52
 
53
+ @override
53
54
  @staticmethod
54
55
  def make_names_data(name):
55
56
  try:
@@ -38,6 +38,7 @@ class LocalConnector(BaseConnector):
38
38
 
39
39
  handles_execution = True
40
40
 
41
+ @override
41
42
  @staticmethod
42
43
  def make_names_data(name=None):
43
44
  if name is not None:
pyinfra/connectors/ssh.py CHANGED
@@ -144,6 +144,7 @@ class SSHConnector(BaseConnector):
144
144
 
145
145
  client: Optional[SSHClient] = None
146
146
 
147
+ @override
147
148
  @staticmethod
148
149
  def make_names_data(name):
149
150
  yield "@ssh/{0}".format(name), {"ssh_hostname": name}, []
@@ -158,6 +158,7 @@ class SSHClient(ParamikoClient):
158
158
  forward_agent,
159
159
  missing_host_key_policy,
160
160
  host_keys_file,
161
+ keep_alive,
161
162
  ) = self.parse_config(
162
163
  hostname,
163
164
  kwargs,
@@ -183,8 +184,6 @@ class SSHClient(ParamikoClient):
183
184
  if _pyinfra_ssh_forward_agent is not None:
184
185
  forward_agent = _pyinfra_ssh_forward_agent
185
186
 
186
- keep_alive = config.get("keep_alive")
187
-
188
187
  if keep_alive:
189
188
  transport = self.get_transport()
190
189
  assert transport is not None, "No transport"
@@ -215,13 +214,14 @@ class SSHClient(ParamikoClient):
215
214
  cfg: dict = {"port": 22}
216
215
  cfg.update(initial_cfg or {})
217
216
 
217
+ keep_alive = 0
218
218
  forward_agent = False
219
219
  missing_host_key_policy = get_missing_host_key_policy(strict_host_key_checking)
220
220
  host_keys_file = path.expanduser("~/.ssh/known_hosts") # OpenSSH default
221
221
 
222
222
  ssh_config = get_ssh_config(ssh_config_file)
223
223
  if not ssh_config:
224
- return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file
224
+ return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive
225
225
 
226
226
  host_config = ssh_config.lookup(hostname)
227
227
  forward_agent = host_config.get("forwardagent") == "yes"
@@ -248,7 +248,7 @@ class SSHClient(ParamikoClient):
248
248
  cfg["port"] = int(host_config["port"])
249
249
 
250
250
  if "serveraliveinterval" in host_config:
251
- cfg["keep_alive"] = int(host_config["serveraliveinterval"])
251
+ keep_alive = int(host_config["serveraliveinterval"])
252
252
 
253
253
  if "proxycommand" in host_config:
254
254
  cfg["sock"] = ProxyCommand(host_config["proxycommand"])
@@ -275,7 +275,7 @@ class SSHClient(ParamikoClient):
275
275
  sock = c.gateway(hostname, cfg["port"], target, target_config["port"])
276
276
  cfg["sock"] = sock
277
277
 
278
- return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file
278
+ return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive
279
279
 
280
280
  @staticmethod
281
281
  def derive_shorthand(ssh_config, host_string):
@@ -1,5 +1,7 @@
1
1
  import json
2
2
 
3
+ from typing_extensions import override
4
+
3
5
  from pyinfra import local, logger
4
6
  from pyinfra.api.exceptions import InventoryError
5
7
  from pyinfra.api.util import memoize
@@ -76,6 +78,7 @@ class TerraformInventoryConnector(BaseConnector):
76
78
 
77
79
  """
78
80
 
81
+ @override
79
82
  @staticmethod
80
83
  def make_names_data(name=None):
81
84
  show_warning()
@@ -3,6 +3,8 @@ from os import path
3
3
  from queue import Queue
4
4
  from threading import Thread
5
5
 
6
+ from typing_extensions import override
7
+
6
8
  from pyinfra import local, logger
7
9
  from pyinfra.api.exceptions import InventoryError
8
10
  from pyinfra.api.util import memoize
@@ -131,6 +133,7 @@ class VagrantInventoryConnector(BaseConnector):
131
133
  pyinfra @vagrant/my-vm-name,@vagrant/another-vm-name ...
132
134
  """
133
135
 
136
+ @override
134
137
  @staticmethod
135
138
  def make_names_data(name=None):
136
139
  vagrant_ssh_info = get_vagrant_config(name)