pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.2__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 (148) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +115 -97
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +139 -39
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/deploy.py +19 -19
  8. pyinfra/api/exceptions.py +35 -4
  9. pyinfra/api/facts.py +62 -86
  10. pyinfra/api/host.py +102 -15
  11. pyinfra/api/inventory.py +4 -0
  12. pyinfra/api/operation.py +188 -120
  13. pyinfra/api/operations.py +66 -113
  14. pyinfra/api/state.py +53 -34
  15. pyinfra/api/util.py +64 -33
  16. pyinfra/connectors/base.py +65 -20
  17. pyinfra/connectors/chroot.py +15 -13
  18. pyinfra/connectors/docker.py +62 -72
  19. pyinfra/connectors/dockerssh.py +20 -19
  20. pyinfra/connectors/local.py +32 -22
  21. pyinfra/connectors/ssh.py +162 -86
  22. pyinfra/connectors/sshuserclient/client.py +1 -1
  23. pyinfra/connectors/terraform.py +57 -39
  24. pyinfra/connectors/util.py +26 -27
  25. pyinfra/connectors/vagrant.py +27 -26
  26. pyinfra/context.py +1 -0
  27. pyinfra/facts/apk.py +7 -2
  28. pyinfra/facts/apt.py +15 -7
  29. pyinfra/facts/brew.py +28 -13
  30. pyinfra/facts/bsdinit.py +9 -6
  31. pyinfra/facts/cargo.py +6 -3
  32. pyinfra/facts/choco.py +8 -4
  33. pyinfra/facts/deb.py +21 -9
  34. pyinfra/facts/dnf.py +11 -6
  35. pyinfra/facts/docker.py +30 -5
  36. pyinfra/facts/files.py +49 -33
  37. pyinfra/facts/gem.py +7 -2
  38. pyinfra/facts/git.py +14 -21
  39. pyinfra/facts/gpg.py +4 -1
  40. pyinfra/facts/hardware.py +186 -138
  41. pyinfra/facts/launchd.py +7 -2
  42. pyinfra/facts/lxd.py +8 -2
  43. pyinfra/facts/mysql.py +19 -12
  44. pyinfra/facts/npm.py +3 -1
  45. pyinfra/facts/openrc.py +8 -2
  46. pyinfra/facts/pacman.py +13 -5
  47. pyinfra/facts/pip.py +2 -0
  48. pyinfra/facts/pkg.py +5 -1
  49. pyinfra/facts/pkgin.py +7 -2
  50. pyinfra/facts/postgres.py +170 -0
  51. pyinfra/facts/postgresql.py +5 -162
  52. pyinfra/facts/rpm.py +21 -15
  53. pyinfra/facts/runit.py +70 -0
  54. pyinfra/facts/selinux.py +12 -4
  55. pyinfra/facts/server.py +240 -82
  56. pyinfra/facts/snap.py +8 -2
  57. pyinfra/facts/systemd.py +37 -13
  58. pyinfra/facts/sysvinit.py +7 -4
  59. pyinfra/facts/upstart.py +7 -2
  60. pyinfra/facts/util/packaging.py +3 -2
  61. pyinfra/facts/vzctl.py +8 -4
  62. pyinfra/facts/xbps.py +7 -2
  63. pyinfra/facts/yum.py +10 -5
  64. pyinfra/facts/zypper.py +9 -4
  65. pyinfra/operations/apk.py +5 -3
  66. pyinfra/operations/apt.py +28 -25
  67. pyinfra/operations/brew.py +60 -29
  68. pyinfra/operations/bsdinit.py +6 -4
  69. pyinfra/operations/cargo.py +3 -1
  70. pyinfra/operations/choco.py +3 -1
  71. pyinfra/operations/dnf.py +16 -20
  72. pyinfra/operations/docker.py +339 -0
  73. pyinfra/operations/files.py +187 -168
  74. pyinfra/operations/gem.py +3 -1
  75. pyinfra/operations/git.py +23 -25
  76. pyinfra/operations/iptables.py +33 -25
  77. pyinfra/operations/launchd.py +5 -6
  78. pyinfra/operations/lxd.py +7 -4
  79. pyinfra/operations/mysql.py +59 -55
  80. pyinfra/operations/npm.py +8 -1
  81. pyinfra/operations/openrc.py +5 -3
  82. pyinfra/operations/pacman.py +6 -7
  83. pyinfra/operations/pip.py +19 -12
  84. pyinfra/operations/pkg.py +3 -1
  85. pyinfra/operations/pkgin.py +5 -3
  86. pyinfra/operations/postgres.py +349 -0
  87. pyinfra/operations/postgresql.py +18 -335
  88. pyinfra/operations/puppet.py +3 -1
  89. pyinfra/operations/python.py +8 -19
  90. pyinfra/operations/runit.py +182 -0
  91. pyinfra/operations/selinux.py +47 -29
  92. pyinfra/operations/server.py +138 -67
  93. pyinfra/operations/snap.py +3 -1
  94. pyinfra/operations/ssh.py +18 -16
  95. pyinfra/operations/systemd.py +18 -12
  96. pyinfra/operations/sysvinit.py +7 -5
  97. pyinfra/operations/upstart.py +7 -5
  98. pyinfra/operations/util/__init__.py +12 -0
  99. pyinfra/operations/util/docker.py +177 -0
  100. pyinfra/operations/util/files.py +24 -16
  101. pyinfra/operations/util/packaging.py +54 -38
  102. pyinfra/operations/util/service.py +39 -47
  103. pyinfra/operations/vzctl.py +12 -10
  104. pyinfra/operations/xbps.py +5 -3
  105. pyinfra/operations/yum.py +15 -19
  106. pyinfra/operations/zypper.py +9 -10
  107. pyinfra/version.py +5 -2
  108. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/METADATA +51 -58
  109. pyinfra-3.0.2.dist-info/RECORD +168 -0
  110. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/WHEEL +1 -1
  111. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/entry_points.txt +0 -3
  112. pyinfra_cli/__main__.py +4 -3
  113. pyinfra_cli/commands.py +3 -2
  114. pyinfra_cli/exceptions.py +75 -43
  115. pyinfra_cli/inventory.py +52 -31
  116. pyinfra_cli/log.py +10 -2
  117. pyinfra_cli/main.py +88 -65
  118. pyinfra_cli/prints.py +37 -109
  119. pyinfra_cli/util.py +15 -10
  120. tests/test_api/test_api.py +2 -0
  121. tests/test_api/test_api_arguments.py +9 -9
  122. tests/test_api/test_api_deploys.py +15 -19
  123. tests/test_api/test_api_facts.py +4 -5
  124. tests/test_api/test_api_operations.py +18 -20
  125. tests/test_api/test_api_util.py +41 -2
  126. tests/test_cli/test_cli.py +14 -50
  127. tests/test_cli/test_cli_deploy.py +17 -14
  128. tests/test_cli/test_cli_exceptions.py +50 -19
  129. tests/test_cli/test_cli_inventory.py +66 -0
  130. tests/test_cli/util.py +1 -1
  131. tests/test_connectors/test_dockerssh.py +11 -8
  132. tests/test_connectors/test_ssh.py +88 -23
  133. tests/test_connectors/test_sshuserclient.py +1 -1
  134. tests/test_connectors/test_terraform.py +11 -8
  135. tests/test_connectors/test_vagrant.py +6 -6
  136. pyinfra/connectors/ansible.py +0 -175
  137. pyinfra/connectors/mech.py +0 -189
  138. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  139. pyinfra/connectors/winrm.py +0 -312
  140. pyinfra/facts/windows.py +0 -366
  141. pyinfra/facts/windows_files.py +0 -90
  142. pyinfra/operations/windows.py +0 -59
  143. pyinfra/operations/windows_files.py +0 -538
  144. pyinfra-3.0.dev0.dist-info/RECORD +0 -170
  145. tests/test_connectors/test_ansible.py +0 -64
  146. tests/test_connectors/test_mech.py +0 -126
  147. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/LICENSE.md +0 -0
  148. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/top_level.txt +0 -0
@@ -1,42 +1,90 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import abc
4
- from dataclasses import dataclass
5
4
  from io import IOBase
6
- from typing import TYPE_CHECKING, Iterable, Iterator, Optional, Union
7
-
8
- from typing_extensions import Unpack
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Iterable,
9
+ Iterator,
10
+ Optional,
11
+ Type,
12
+ TypeVar,
13
+ Union,
14
+ cast,
15
+ get_type_hints,
16
+ )
17
+
18
+ from typing_extensions import TypedDict, Unpack
19
+
20
+ from pyinfra.api.exceptions import ConnectorDataTypeError
21
+ from pyinfra.api.util import raise_if_bad_type
9
22
 
10
23
  if TYPE_CHECKING:
11
24
  from pyinfra.api.arguments import ConnectorArguments
12
25
  from pyinfra.api.command import StringCommand
13
- from pyinfra.api.host import Host
26
+ from pyinfra.api.host import Host, HostData
14
27
  from pyinfra.api.state import State
15
28
 
16
29
  from .util import CommandOutput
17
30
 
18
31
 
19
- def make_keys(prefix: str, cls):
20
- class Keys:
21
- pass
32
+ T = TypeVar("T")
33
+ default_sentinel = object()
34
+
35
+
36
+ def host_to_connector_data(
37
+ connector_data: Type[T],
38
+ connector_data_meta: dict[str, DataMeta],
39
+ host_data: "HostData",
40
+ ) -> T:
41
+ data: T = cast(T, {})
42
+ for key, type_ in get_type_hints(connector_data).items():
43
+ value = host_data.get(key, default_sentinel)
44
+ if value is default_sentinel:
45
+ value = connector_data_meta[key].default
46
+ else:
47
+ raise_if_bad_type(
48
+ value,
49
+ type_,
50
+ ConnectorDataTypeError,
51
+ f"Invalid connector data `{key}`:",
52
+ )
53
+
54
+ data[key] = value # type: ignore
55
+ return data
56
+
57
+
58
+ class DataMeta:
59
+ description: str
60
+ default: Any
61
+
62
+ def __init__(self, description, default=None) -> None:
63
+ self.description = description
64
+ self.default = default
22
65
 
23
- for key in cls.__dict__:
24
- if not key.startswith("_"):
25
- setattr(Keys, key, f"{prefix}_{key}")
26
66
 
27
- return Keys
67
+ class ConnectorData(TypedDict, total=False):
68
+ pass
28
69
 
29
70
 
30
- @dataclass
31
71
  class BaseConnector(abc.ABC):
32
72
  state: "State"
33
73
  host: "Host"
34
74
 
35
75
  handles_execution = False
36
76
 
77
+ data_cls: Type = ConnectorData
78
+ data_meta: dict[str, DataMeta] = {}
79
+
80
+ def __init__(self, state: "State", host: "Host"):
81
+ self.state = state
82
+ self.host = host
83
+ self.data = host_to_connector_data(self.data_cls, self.data_meta, host.data)
84
+
37
85
  @staticmethod
38
86
  @abc.abstractmethod
39
- def make_names_data(id: str) -> Iterator[tuple[str, dict, list[str]]]:
87
+ def make_names_data(name: str) -> Iterator[tuple[str, dict, list[str]]]:
40
88
  """
41
89
  Generates hosts/data/groups information for inventory. This allows a
42
90
  single connector reference to generate multiple target hosts.
@@ -60,8 +108,7 @@ class BaseConnector(abc.ABC):
60
108
  print_output: bool,
61
109
  print_input: bool,
62
110
  **arguments: Unpack["ConnectorArguments"],
63
- ) -> tuple[bool, "CommandOutput"]:
64
- ...
111
+ ) -> tuple[bool, "CommandOutput"]: ...
65
112
 
66
113
  @abc.abstractmethod
67
114
  def put_file(
@@ -72,8 +119,7 @@ class BaseConnector(abc.ABC):
72
119
  print_output: bool = False,
73
120
  print_input: bool = False,
74
121
  **arguments: Unpack["ConnectorArguments"],
75
- ) -> bool:
76
- ...
122
+ ) -> bool: ...
77
123
 
78
124
  @abc.abstractmethod
79
125
  def get_file(
@@ -84,8 +130,7 @@ class BaseConnector(abc.ABC):
84
130
  print_output: bool = False,
85
131
  print_input: bool = False,
86
132
  **arguments: Unpack["ConnectorArguments"],
87
- ) -> bool:
88
- ...
133
+ ) -> bool: ...
89
134
 
90
135
  def check_can_rsync(self):
91
136
  raise NotImplementedError("This connector does not support rsync")
@@ -27,6 +27,10 @@ def show_warning():
27
27
 
28
28
 
29
29
  class ChrootConnector(BaseConnector):
30
+ """
31
+ The chroot connector allows you to execute operations within another root.
32
+ """
33
+
30
34
  handles_execution = True
31
35
 
32
36
  local: LocalConnector
@@ -36,17 +40,17 @@ class ChrootConnector(BaseConnector):
36
40
  self.local = LocalConnector(state, host)
37
41
 
38
42
  @staticmethod
39
- def make_names_data(directory: Optional[str] = None):
40
- if not directory:
43
+ def make_names_data(name: Optional[str] = None):
44
+ if not name:
41
45
  raise InventoryError("No directory provided!")
42
46
 
43
47
  show_warning()
44
48
 
45
- yield "@chroot/{0}".format(directory), {
46
- "chroot_directory": "/{0}".format(directory.lstrip("/")),
49
+ yield "@chroot/{0}".format(name), {
50
+ "chroot_directory": "/{0}".format(name.lstrip("/")),
47
51
  }, ["@chroot"]
48
52
 
49
- def connect(self):
53
+ def connect(self) -> None:
50
54
  self.local.connect()
51
55
 
52
56
  chroot_directory = self.host.data.chroot_directory
@@ -61,7 +65,6 @@ class ChrootConnector(BaseConnector):
61
65
  raise ConnectError(e.args[0])
62
66
 
63
67
  self.host.connector_data["chroot_directory"] = chroot_directory
64
- return True
65
68
 
66
69
  def run_shell_command(
67
70
  self,
@@ -117,11 +120,10 @@ class ChrootConnector(BaseConnector):
117
120
  temp_f.write(data)
118
121
 
119
122
  chroot_directory = self.host.connector_data["chroot_directory"]
120
-
121
- chroot_command = "cp {0} {1}/{2}".format(
123
+ chroot_command = StringCommand(
124
+ "cp",
122
125
  temp_filename,
123
- chroot_directory,
124
- remote_filename,
126
+ f"{chroot_directory}/{remote_filename}",
125
127
  )
126
128
 
127
129
  status, output = self.local.run_shell_command(
@@ -159,9 +161,9 @@ class ChrootConnector(BaseConnector):
159
161
 
160
162
  try:
161
163
  chroot_directory = self.host.connector_data["chroot_directory"]
162
- chroot_command = "cp {0}/{1} {2}".format(
163
- chroot_directory,
164
- remote_filename,
164
+ chroot_command = StringCommand(
165
+ "cp",
166
+ f"{chroot_directory}/{remote_filename}",
165
167
  temp_filename,
166
168
  )
167
169
 
@@ -1,26 +1,3 @@
1
- """
2
- The ``@docker`` connector allows you to build Docker images, or modify running
3
- Docker containers, using ``pyinfra``. You can pass either an image name or
4
- existing container ID:
5
-
6
- + Image - will create a container from the image, execute operations and save
7
- into a new image
8
- + Existing container ID - will simply execute operations against the container,
9
- leaving it up afterwards
10
-
11
-
12
- .. code:: shell
13
-
14
- # A Docker base image must be provided
15
- pyinfra @docker/alpine:3.8 ...
16
-
17
- # pyinfra can run on multiple Docker images in parallel
18
- pyinfra @docker/alpine:3.8,@docker/ubuntu:bionic ...
19
-
20
- # Execute against a running container
21
- pyinfra @docker/2beb8c15a1b1 ...
22
- """
23
-
24
1
  from __future__ import annotations
25
2
 
26
3
  import json
@@ -29,7 +6,7 @@ from tempfile import mkstemp
29
6
  from typing import TYPE_CHECKING
30
7
 
31
8
  import click
32
- from typing_extensions import Unpack
9
+ from typing_extensions import TypedDict, Unpack
33
10
 
34
11
  from pyinfra import local, logger
35
12
  from pyinfra.api import QuoteString, StringCommand
@@ -37,7 +14,7 @@ from pyinfra.api.exceptions import ConnectError, InventoryError, PyinfraError
37
14
  from pyinfra.api.util import get_file_io
38
15
  from pyinfra.progress import progress_spinner
39
16
 
40
- from .base import BaseConnector, make_keys
17
+ from .base import BaseConnector, DataMeta
41
18
  from .local import LocalConnector
42
19
  from .util import CommandOutput, extract_control_arguments, make_unix_command_for_host
43
20
 
@@ -47,20 +24,24 @@ if TYPE_CHECKING:
47
24
  from pyinfra.api.state import State
48
25
 
49
26
 
50
- class DataKeys:
51
- identifier = "ID of container or image to target"
52
- container_id = "ID of container to target, overrides ``docker_identifier``"
27
+ class ConnectorData(TypedDict):
28
+ docker_identifier: str
53
29
 
54
30
 
55
- DATA_KEYS = make_keys("docker", DataKeys)
31
+ connector_data_meta: dict[str, DataMeta] = {
32
+ "docker_identifier": DataMeta("ID of container or image to start from"),
33
+ }
56
34
 
57
35
 
58
- def _find_start_docker_container(container_id):
36
+ def _find_start_docker_container(container_id) -> tuple[str, bool]:
59
37
  docker_info = local.shell("docker container inspect {0}".format(container_id))
38
+ assert isinstance(docker_info, str)
60
39
  docker_info = json.loads(docker_info)[0]
61
40
  if docker_info["State"]["Running"] is False:
62
41
  logger.info("Starting stopped container: {0}".format(container_id))
63
42
  local.shell("docker container start {0}".format(container_id))
43
+ return container_id, False
44
+ return container_id, True
64
45
 
65
46
 
66
47
  def _start_docker_image(image_name):
@@ -76,53 +57,69 @@ def _start_docker_image(image_name):
76
57
 
77
58
 
78
59
  class DockerConnector(BaseConnector):
60
+ """
61
+ The docker connector allows you to build Docker images or modify running
62
+ Docker containers. You can pass either an image name or existing container ID:
63
+
64
+ + Image - will create a new container from the image, execute operations \
65
+ against it, save into a new Docker image and remove the container
66
+ + Existing container ID - will execute operations against the running \
67
+ container, leaving it running
68
+
69
+ .. code:: shell
70
+
71
+ # A Docker base image must be provided
72
+ pyinfra @docker/alpine:3.8 ...
73
+
74
+ # pyinfra can run on multiple Docker images in parallel
75
+ pyinfra @docker/alpine:3.8,@docker/ubuntu:bionic ...
76
+
77
+ # Execute against a running container
78
+ pyinfra @docker/2beb8c15a1b1 ...
79
+ """
80
+
79
81
  handles_execution = True
80
- data_keys = DATA_KEYS
82
+
83
+ data_cls = ConnectorData
84
+ data_meta = connector_data_meta
85
+ data: ConnectorData
81
86
 
82
87
  local: LocalConnector
83
88
 
89
+ container_id: str
90
+ no_stop: bool = False
91
+
84
92
  def __init__(self, state: "State", host: "Host"):
85
93
  super().__init__(state, host)
86
94
  self.local = LocalConnector(state, host)
87
95
 
88
96
  @staticmethod
89
- def make_names_data(identifier=None):
90
- if not identifier:
97
+ def make_names_data(name=None):
98
+ if not name:
91
99
  raise InventoryError("No docker base ID provided!")
92
100
 
93
101
  yield (
94
- "@docker/{0}".format(identifier),
95
- {DATA_KEYS.identifier: identifier},
102
+ "@docker/{0}".format(name),
103
+ {"docker_identifier": name},
96
104
  ["@docker"],
97
105
  )
98
106
 
99
- def connect(self):
107
+ def connect(self) -> None:
100
108
  self.local.connect()
101
109
 
102
- docker_container_id = self.host.data.get(DATA_KEYS.container_id)
103
- if docker_container_id: # user can provide a docker_container_id
104
- self.host.connector_data["docker_container_no_disconnect"] = True
105
- self.host.connector_data["docker_container_id"] = docker_container_id
106
- return True
107
-
108
- docker_identifier = getattr(self.host.data, DATA_KEYS.identifier)
110
+ docker_identifier = self.data["docker_identifier"]
109
111
  with progress_spinner({"prepare docker container"}):
110
112
  try:
111
- # Check if the provided @docker/X is an existing container ID
112
- _find_start_docker_container(docker_identifier)
113
+ self.container_id, was_running = _find_start_docker_container(docker_identifier)
114
+ if was_running:
115
+ self.no_stop = True
113
116
  except PyinfraError:
114
- container_id = _start_docker_image(docker_identifier)
115
- else:
116
- container_id = docker_identifier
117
- self.host.connector_data["docker_container_no_disconnect"] = True
118
-
119
- self.host.connector_data["docker_container_id"] = container_id
120
- return True
117
+ self.container_id = _start_docker_image(docker_identifier)
121
118
 
122
119
  def disconnect(self):
123
- container_id = self.host.connector_data["docker_container_id"]
120
+ container_id = self.container_id
124
121
 
125
- if self.host.connector_data.get("docker_container_no_disconnect"):
122
+ if self.no_stop:
126
123
  logger.info(
127
124
  "{0}docker build complete, container left running: {1}".format(
128
125
  self.host.print_prefix,
@@ -157,7 +154,7 @@ class DockerConnector(BaseConnector):
157
154
  ) -> tuple[bool, CommandOutput]:
158
155
  local_arguments = extract_control_arguments(arguments)
159
156
 
160
- container_id = self.host.connector_data["docker_container_id"]
157
+ container_id = self.container_id
161
158
 
162
159
  command = make_unix_command_for_host(self.state, self.host, command, **arguments)
163
160
  command = StringCommand(QuoteString(command))
@@ -207,11 +204,11 @@ class DockerConnector(BaseConnector):
207
204
 
208
205
  temp_f.write(data)
209
206
 
210
- docker_id = self.host.connector_data["docker_container_id"]
211
- docker_command = "docker cp {0} {1}:{2}".format(
207
+ docker_command = StringCommand(
208
+ "docker",
209
+ "cp",
212
210
  temp_filename,
213
- docker_id,
214
- remote_filename,
211
+ f"{self.container_id}:{remote_filename}",
215
212
  )
216
213
 
217
214
  status, output = self.local.run_shell_command(
@@ -254,10 +251,10 @@ class DockerConnector(BaseConnector):
254
251
  fd, temp_filename = mkstemp()
255
252
 
256
253
  try:
257
- docker_id = self.host.connector_data["docker_container_id"]
258
- docker_command = "docker cp {0}:{1} {2}".format(
259
- docker_id,
260
- remote_filename,
254
+ docker_command = StringCommand(
255
+ "docker",
256
+ "cp",
257
+ f"{self.container_id}:{remote_filename}",
261
258
  temp_filename,
262
259
  )
263
260
 
@@ -268,17 +265,10 @@ class DockerConnector(BaseConnector):
268
265
  )
269
266
 
270
267
  # Load the temporary file and write it to our file or IO object
271
- with open(temp_filename, encoding="utf-8") as temp_f:
268
+ with open(temp_filename, "rb") as temp_f:
272
269
  with get_file_io(filename_or_io, "wb") as file_io:
273
270
  data = temp_f.read()
274
- data_bytes: bytes
275
-
276
- if isinstance(data, str):
277
- data_bytes = data.encode()
278
- else:
279
- data_bytes = data
280
-
281
- file_io.write(data_bytes)
271
+ file_io.write(data)
282
272
  finally:
283
273
  os.close(fd)
284
274
  os.remove(temp_filename)
@@ -1,17 +1,3 @@
1
- """
2
- **Note**: this connector is in beta!
3
-
4
- The ``@dockerssh`` connector allows you to run commands on Docker containers on a remote machine.
5
-
6
- .. code:: shell
7
-
8
- # A Docker base image must be provided
9
- pyinfra @dockerssh/remotehost:alpine:3.8 ...
10
-
11
- # pyinfra can run on multiple Docker images in parallel
12
- pyinfra @dockerssh/remotehost:alpine:3.8,@dockerssh/remotehost:ubuntu:bionic ...
13
- """
14
-
15
1
  import os
16
2
  from tempfile import mkstemp
17
3
  from typing import TYPE_CHECKING
@@ -41,6 +27,21 @@ def show_warning():
41
27
 
42
28
 
43
29
  class DockerSSHConnector(BaseConnector):
30
+ """
31
+ **Note**: this connector is in beta!
32
+
33
+ The ``@dockerssh`` connector allows you to run commands on Docker containers \
34
+ on a remote machine.
35
+
36
+ .. code:: shell
37
+
38
+ # A Docker base image must be provided
39
+ pyinfra @dockerssh/remotehost:alpine:3.8 ...
40
+
41
+ # pyinfra can run on multiple Docker images in parallel
42
+ pyinfra @dockerssh/remotehost:alpine:3.8,@dockerssh/remotehost:ubuntu:bionic ...
43
+ """
44
+
44
45
  handles_execution = True
45
46
 
46
47
  ssh: SSHConnector
@@ -50,10 +51,10 @@ class DockerSSHConnector(BaseConnector):
50
51
  self.ssh = SSHConnector(state, host)
51
52
 
52
53
  @staticmethod
53
- def make_names_data(host_image_str):
54
+ def make_names_data(name):
54
55
  try:
55
- hostname, image = host_image_str.split(":", 1)
56
- except (AttributeError, ValueError): # failure to parse the host_image_str
56
+ hostname, image = name.split(":", 1)
57
+ except (AttributeError, ValueError): # failure to parse the name
57
58
  raise InventoryError("No ssh host or docker base image provided!")
58
59
 
59
60
  if not image:
@@ -164,7 +165,7 @@ class DockerSSHConnector(BaseConnector):
164
165
  """
165
166
 
166
167
  fd, local_temp_filename = mkstemp()
167
- remote_temp_filename = remote_temp_filename or self.state.get_temp_filename(
168
+ remote_temp_filename = remote_temp_filename or self.host.get_temp_filename(
168
169
  local_temp_filename
169
170
  )
170
171
 
@@ -234,7 +235,7 @@ class DockerSSHConnector(BaseConnector):
234
235
  location and then reading that into our final file/IO object.
235
236
  """
236
237
 
237
- remote_temp_filename = remote_temp_filename or self.state.get_temp_filename(remote_filename)
238
+ remote_temp_filename = remote_temp_filename or self.host.get_temp_filename(remote_filename)
238
239
 
239
240
  try:
240
241
  docker_id = self.host.host_data["docker_container_id"]
@@ -1,9 +1,5 @@
1
- """
2
- The ``@local`` connector executes changes on the local machine using subprocesses.
3
- """
4
-
5
1
  import os
6
- from distutils.spawn import find_executable
2
+ from shutil import which
7
3
  from tempfile import mkstemp
8
4
  from typing import TYPE_CHECKING, Tuple
9
5
 
@@ -28,18 +24,30 @@ if TYPE_CHECKING:
28
24
 
29
25
 
30
26
  class LocalConnector(BaseConnector):
27
+ """
28
+ The ``@local`` connector executes changes on the local machine using
29
+ subprocesses. **This connector is only compatible with MacOS & Linux hosts**.
30
+
31
+ Examples:
32
+
33
+ .. code::
34
+
35
+ # Install nginx
36
+ pyinfra inventory.py apt.packages nginx update=true _sudo=true
37
+ """
38
+
31
39
  handles_execution = True
32
40
 
33
41
  @staticmethod
34
- def make_names_data(_=None):
35
- if _ is not None:
42
+ def make_names_data(name=None):
43
+ if name is not None:
36
44
  raise InventoryError("Cannot have more than one @local")
37
45
 
38
46
  yield "@local", {}, ["@local"]
39
47
 
40
48
  def run_shell_command(
41
49
  self,
42
- command,
50
+ command: StringCommand,
43
51
  print_output: bool = False,
44
52
  print_input: bool = False,
45
53
  **arguments: Unpack["ConnectorArguments"],
@@ -48,17 +56,14 @@ class LocalConnector(BaseConnector):
48
56
  Execute a command on the local machine.
49
57
 
50
58
  Args:
51
- state (``pyinfra.api.State`` object): state object for this command
52
- host (``pyinfra.api.Host`` object): the target host
53
- command (string): actual command to execute
54
- sudo (boolean): whether to wrap the command with sudo
55
- sudo_user (string): user to sudo to
56
- env (dict): environment variables to set
57
- timeout (int): timeout for this command to complete before erroring
59
+ command (StringCommand): actual command to execute
60
+ print_output (bool): whether to print command output
61
+ print_input (bool): whether to print command input
62
+ arguments: (ConnectorArguments): connector global arguments
58
63
 
59
64
  Returns:
60
- tuple: (exit_code, stdout, stderr)
61
- stdout and stderr are both lists of strings from each buffer.
65
+ tuple: (bool, CommandOutput)
66
+ Bool indicating success and CommandOutput with stdout/stderr lines.
62
67
  """
63
68
 
64
69
  arguments.pop("_get_pty", False)
@@ -108,6 +113,9 @@ class LocalConnector(BaseConnector):
108
113
  """
109
114
  Upload a local file or IO object by copying it to a temporary directory
110
115
  and then writing it to the upload location.
116
+
117
+ Returns:
118
+ bool: Indicating success or failure
111
119
  """
112
120
 
113
121
  _, temp_filename = mkstemp()
@@ -156,6 +164,9 @@ class LocalConnector(BaseConnector):
156
164
  """
157
165
  Download a local file by copying it to a temporary location and then writing
158
166
  it to our filename or IO object.
167
+
168
+ Returns:
169
+ bool: Indicating success or failure
159
170
  """
160
171
 
161
172
  _, temp_filename = mkstemp()
@@ -163,7 +174,7 @@ class LocalConnector(BaseConnector):
163
174
  try:
164
175
  # Copy the file using `cp` such that we support sudo/su
165
176
  status, output = self.run_shell_command(
166
- "cp {0} {1}".format(remote_filename, temp_filename),
177
+ StringCommand("cp", remote_filename, temp_filename),
167
178
  print_output=print_output,
168
179
  print_input=print_input,
169
180
  **arguments,
@@ -195,9 +206,8 @@ class LocalConnector(BaseConnector):
195
206
 
196
207
  return True
197
208
 
198
- @staticmethod
199
- def check_can_rsync(host):
200
- if not find_executable("rsync"):
209
+ def check_can_rsync(self):
210
+ if not which("rsync"):
201
211
  raise NotImplementedError("The `rsync` binary is not available on this system.")
202
212
 
203
213
  def rsync(
@@ -210,7 +220,7 @@ class LocalConnector(BaseConnector):
210
220
  **arguments,
211
221
  ) -> bool:
212
222
  status, output = self.run_shell_command(
213
- "rsync {0} {1} {2}".format(" ".join(flags), src, dest),
223
+ StringCommand("rsync", " ".join(flags), src, dest),
214
224
  print_output=print_output,
215
225
  print_input=print_input,
216
226
  **arguments,