pyinfra 0.11.dev3__py3-none-any.whl → 3.6__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 (204) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +19 -3
  4. pyinfra/api/arguments.py +413 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +73 -18
  12. pyinfra/api/facts.py +267 -200
  13. pyinfra/api/host.py +416 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/metadata.py +69 -0
  16. pyinfra/api/operation.py +432 -262
  17. pyinfra/api/operations.py +273 -260
  18. pyinfra/api/state.py +302 -248
  19. pyinfra/api/util.py +309 -369
  20. pyinfra/connectors/base.py +173 -0
  21. pyinfra/connectors/chroot.py +212 -0
  22. pyinfra/connectors/docker.py +405 -0
  23. pyinfra/connectors/dockerssh.py +297 -0
  24. pyinfra/connectors/local.py +238 -0
  25. pyinfra/connectors/scp/__init__.py +1 -0
  26. pyinfra/connectors/scp/client.py +204 -0
  27. pyinfra/connectors/ssh.py +727 -0
  28. pyinfra/connectors/ssh_util.py +114 -0
  29. pyinfra/connectors/sshuserclient/client.py +309 -0
  30. pyinfra/connectors/sshuserclient/config.py +102 -0
  31. pyinfra/connectors/terraform.py +135 -0
  32. pyinfra/connectors/util.py +417 -0
  33. pyinfra/connectors/vagrant.py +183 -0
  34. pyinfra/context.py +145 -0
  35. pyinfra/facts/__init__.py +7 -6
  36. pyinfra/facts/apk.py +22 -7
  37. pyinfra/facts/apt.py +117 -60
  38. pyinfra/facts/brew.py +100 -15
  39. pyinfra/facts/bsdinit.py +23 -0
  40. pyinfra/facts/cargo.py +37 -0
  41. pyinfra/facts/choco.py +47 -0
  42. pyinfra/facts/crontab.py +195 -0
  43. pyinfra/facts/deb.py +94 -0
  44. pyinfra/facts/dnf.py +48 -0
  45. pyinfra/facts/docker.py +96 -23
  46. pyinfra/facts/efibootmgr.py +113 -0
  47. pyinfra/facts/files.py +629 -58
  48. pyinfra/facts/flatpak.py +77 -0
  49. pyinfra/facts/freebsd.py +70 -0
  50. pyinfra/facts/gem.py +19 -6
  51. pyinfra/facts/git.py +59 -14
  52. pyinfra/facts/gpg.py +150 -0
  53. pyinfra/facts/hardware.py +313 -167
  54. pyinfra/facts/iptables.py +72 -62
  55. pyinfra/facts/launchd.py +44 -0
  56. pyinfra/facts/lxd.py +17 -4
  57. pyinfra/facts/mysql.py +122 -86
  58. pyinfra/facts/npm.py +17 -9
  59. pyinfra/facts/openrc.py +71 -0
  60. pyinfra/facts/opkg.py +246 -0
  61. pyinfra/facts/pacman.py +50 -7
  62. pyinfra/facts/pip.py +24 -7
  63. pyinfra/facts/pipx.py +82 -0
  64. pyinfra/facts/pkg.py +15 -6
  65. pyinfra/facts/pkgin.py +35 -0
  66. pyinfra/facts/podman.py +54 -0
  67. pyinfra/facts/postgres.py +178 -0
  68. pyinfra/facts/postgresql.py +6 -147
  69. pyinfra/facts/rpm.py +105 -0
  70. pyinfra/facts/runit.py +77 -0
  71. pyinfra/facts/selinux.py +161 -0
  72. pyinfra/facts/server.py +762 -285
  73. pyinfra/facts/snap.py +88 -0
  74. pyinfra/facts/systemd.py +139 -0
  75. pyinfra/facts/sysvinit.py +59 -0
  76. pyinfra/facts/upstart.py +35 -0
  77. pyinfra/facts/util/__init__.py +17 -0
  78. pyinfra/facts/util/databases.py +4 -6
  79. pyinfra/facts/util/packaging.py +37 -6
  80. pyinfra/facts/util/units.py +30 -0
  81. pyinfra/facts/util/win_files.py +99 -0
  82. pyinfra/facts/vzctl.py +20 -13
  83. pyinfra/facts/xbps.py +35 -0
  84. pyinfra/facts/yum.py +34 -40
  85. pyinfra/facts/zfs.py +77 -0
  86. pyinfra/facts/zypper.py +42 -0
  87. pyinfra/local.py +45 -83
  88. pyinfra/operations/__init__.py +12 -0
  89. pyinfra/operations/apk.py +99 -0
  90. pyinfra/operations/apt.py +496 -0
  91. pyinfra/operations/brew.py +232 -0
  92. pyinfra/operations/bsdinit.py +59 -0
  93. pyinfra/operations/cargo.py +45 -0
  94. pyinfra/operations/choco.py +61 -0
  95. pyinfra/operations/crontab.py +194 -0
  96. pyinfra/operations/dnf.py +213 -0
  97. pyinfra/operations/docker.py +492 -0
  98. pyinfra/operations/files.py +2014 -0
  99. pyinfra/operations/flatpak.py +95 -0
  100. pyinfra/operations/freebsd/__init__.py +12 -0
  101. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  102. pyinfra/operations/freebsd/pkg.py +219 -0
  103. pyinfra/operations/freebsd/service.py +116 -0
  104. pyinfra/operations/freebsd/sysrc.py +92 -0
  105. pyinfra/operations/gem.py +48 -0
  106. pyinfra/operations/git.py +420 -0
  107. pyinfra/operations/iptables.py +312 -0
  108. pyinfra/operations/launchd.py +45 -0
  109. pyinfra/operations/lxd.py +69 -0
  110. pyinfra/operations/mysql.py +610 -0
  111. pyinfra/operations/npm.py +57 -0
  112. pyinfra/operations/openrc.py +63 -0
  113. pyinfra/operations/opkg.py +89 -0
  114. pyinfra/operations/pacman.py +82 -0
  115. pyinfra/operations/pip.py +206 -0
  116. pyinfra/operations/pipx.py +103 -0
  117. pyinfra/operations/pkg.py +71 -0
  118. pyinfra/operations/pkgin.py +92 -0
  119. pyinfra/operations/postgres.py +437 -0
  120. pyinfra/operations/postgresql.py +30 -0
  121. pyinfra/operations/puppet.py +41 -0
  122. pyinfra/operations/python.py +73 -0
  123. pyinfra/operations/runit.py +184 -0
  124. pyinfra/operations/selinux.py +190 -0
  125. pyinfra/operations/server.py +1100 -0
  126. pyinfra/operations/snap.py +118 -0
  127. pyinfra/operations/ssh.py +217 -0
  128. pyinfra/operations/systemd.py +150 -0
  129. pyinfra/operations/sysvinit.py +142 -0
  130. pyinfra/operations/upstart.py +68 -0
  131. pyinfra/operations/util/__init__.py +12 -0
  132. pyinfra/operations/util/docker.py +407 -0
  133. pyinfra/operations/util/files.py +247 -0
  134. pyinfra/operations/util/packaging.py +338 -0
  135. pyinfra/operations/util/service.py +46 -0
  136. pyinfra/operations/vzctl.py +137 -0
  137. pyinfra/operations/xbps.py +78 -0
  138. pyinfra/operations/yum.py +213 -0
  139. pyinfra/operations/zfs.py +176 -0
  140. pyinfra/operations/zypper.py +193 -0
  141. pyinfra/progress.py +44 -32
  142. pyinfra/py.typed +0 -0
  143. pyinfra/version.py +9 -1
  144. pyinfra-3.6.dist-info/METADATA +142 -0
  145. pyinfra-3.6.dist-info/RECORD +160 -0
  146. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
  147. pyinfra-3.6.dist-info/entry_points.txt +12 -0
  148. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
  149. pyinfra_cli/__init__.py +1 -0
  150. pyinfra_cli/cli.py +793 -0
  151. pyinfra_cli/commands.py +66 -0
  152. pyinfra_cli/exceptions.py +155 -65
  153. pyinfra_cli/inventory.py +233 -89
  154. pyinfra_cli/log.py +39 -43
  155. pyinfra_cli/main.py +26 -495
  156. pyinfra_cli/prints.py +215 -156
  157. pyinfra_cli/util.py +172 -105
  158. pyinfra_cli/virtualenv.py +25 -20
  159. pyinfra/api/connectors/__init__.py +0 -21
  160. pyinfra/api/connectors/ansible.py +0 -99
  161. pyinfra/api/connectors/docker.py +0 -178
  162. pyinfra/api/connectors/local.py +0 -169
  163. pyinfra/api/connectors/ssh.py +0 -402
  164. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  165. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  166. pyinfra/api/connectors/util.py +0 -63
  167. pyinfra/api/connectors/vagrant.py +0 -155
  168. pyinfra/facts/init.py +0 -176
  169. pyinfra/facts/util/files.py +0 -102
  170. pyinfra/hook.py +0 -41
  171. pyinfra/modules/__init__.py +0 -11
  172. pyinfra/modules/apk.py +0 -64
  173. pyinfra/modules/apt.py +0 -272
  174. pyinfra/modules/brew.py +0 -122
  175. pyinfra/modules/files.py +0 -711
  176. pyinfra/modules/gem.py +0 -30
  177. pyinfra/modules/git.py +0 -115
  178. pyinfra/modules/init.py +0 -344
  179. pyinfra/modules/iptables.py +0 -271
  180. pyinfra/modules/lxd.py +0 -45
  181. pyinfra/modules/mysql.py +0 -347
  182. pyinfra/modules/npm.py +0 -47
  183. pyinfra/modules/pacman.py +0 -60
  184. pyinfra/modules/pip.py +0 -99
  185. pyinfra/modules/pkg.py +0 -43
  186. pyinfra/modules/postgresql.py +0 -245
  187. pyinfra/modules/puppet.py +0 -20
  188. pyinfra/modules/python.py +0 -37
  189. pyinfra/modules/server.py +0 -524
  190. pyinfra/modules/ssh.py +0 -150
  191. pyinfra/modules/util/files.py +0 -52
  192. pyinfra/modules/util/packaging.py +0 -118
  193. pyinfra/modules/vzctl.py +0 -133
  194. pyinfra/modules/yum.py +0 -171
  195. pyinfra/pseudo_modules.py +0 -64
  196. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  197. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  198. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  199. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  200. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  201. pyinfra_cli/__main__.py +0 -40
  202. pyinfra_cli/config.py +0 -92
  203. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  204. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/api/command.py ADDED
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+ from inspect import getfullargspec
5
+ from string import Formatter
6
+ from typing import IO, TYPE_CHECKING, Callable, Union
7
+
8
+ import gevent
9
+ from typing_extensions import Unpack, override
10
+
11
+ from pyinfra.context import LocalContextObject, ctx_config, ctx_host
12
+
13
+ from .arguments import ConnectorArguments
14
+
15
+ if TYPE_CHECKING:
16
+ from pyinfra.api.host import Host
17
+ from pyinfra.api.state import State
18
+
19
+
20
+ def make_formatted_string_command(string: str, *args, **kwargs) -> "StringCommand":
21
+ """
22
+ Helper function that takes a shell command or script as a string, splits it
23
+ using ``shlex.split`` and then formats each bit, returning a ``StringCommand``
24
+ instance with each bit.
25
+
26
+ Useful to enable string formatted commands/scripts, for example:
27
+
28
+ .. code:: python
29
+
30
+ curl_command = make_formatted_string_command(
31
+ 'curl -sSLf {0} -o {1}',
32
+ QuoteString(src),
33
+ QuoteString(dest),
34
+ )
35
+ """
36
+
37
+ formatter = Formatter()
38
+ string_bits = []
39
+
40
+ for bit in shlex.split(string):
41
+ for item in formatter.parse(bit):
42
+ if item[0]:
43
+ string_bits.append(item[0])
44
+ if item[1]:
45
+ value, _ = formatter.get_field(item[1], args, kwargs)
46
+ string_bits.append(value)
47
+
48
+ return StringCommand(*string_bits)
49
+
50
+
51
+ class MaskString(str):
52
+ pass
53
+
54
+
55
+ class QuoteString:
56
+ obj: Union[str, "StringCommand"]
57
+
58
+ def __init__(self, obj: Union[str, "StringCommand"]):
59
+ self.obj = obj
60
+
61
+ @override
62
+ def __repr__(self) -> str:
63
+ return f"QuoteString({self.obj})"
64
+
65
+
66
+ class PyinfraCommand:
67
+ connector_arguments: ConnectorArguments
68
+
69
+ def __init__(self, **arguments: Unpack[ConnectorArguments]):
70
+ self.connector_arguments = arguments
71
+
72
+ @override
73
+ def __eq__(self, other) -> bool:
74
+ if isinstance(other, self.__class__) and repr(self) == repr(other):
75
+ return True
76
+ return False
77
+
78
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
79
+ raise NotImplementedError
80
+
81
+
82
+ class StringCommand(PyinfraCommand):
83
+ def __init__(
84
+ self,
85
+ *bits,
86
+ _separator=" ",
87
+ **arguments: Unpack[ConnectorArguments],
88
+ ):
89
+ super().__init__(**arguments)
90
+ self.bits = bits
91
+ self.separator = _separator
92
+
93
+ @override
94
+ def __str__(self) -> str:
95
+ return self.get_masked_value()
96
+
97
+ @override
98
+ def __repr__(self) -> str:
99
+ return f"StringCommand({self.get_masked_value()})"
100
+
101
+ def _get_all_bits(self, bit_accessor):
102
+ all_bits = []
103
+
104
+ for bit in self.bits:
105
+ quote = False
106
+ if isinstance(bit, QuoteString):
107
+ quote = True
108
+ bit = bit.obj
109
+
110
+ if isinstance(bit, StringCommand):
111
+ bit = bit_accessor(bit)
112
+
113
+ if not isinstance(bit, str):
114
+ bit = "{0}".format(bit)
115
+
116
+ if quote:
117
+ bit = shlex.quote(bit)
118
+
119
+ all_bits.append(bit)
120
+
121
+ return all_bits
122
+
123
+ def get_raw_value(self) -> str:
124
+ return self.separator.join(
125
+ self._get_all_bits(
126
+ lambda bit: bit.get_raw_value(),
127
+ ),
128
+ )
129
+
130
+ def get_masked_value(self) -> str:
131
+ return self.separator.join(
132
+ [
133
+ "***" if isinstance(bit, MaskString) else bit
134
+ for bit in self._get_all_bits(lambda bit: bit.get_masked_value())
135
+ ],
136
+ )
137
+
138
+ @override
139
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
140
+ connector_arguments.update(self.connector_arguments)
141
+
142
+ return host.run_shell_command(
143
+ self,
144
+ print_output=state.print_output,
145
+ print_input=state.print_input,
146
+ **connector_arguments,
147
+ )
148
+
149
+
150
+ class FileUploadCommand(PyinfraCommand):
151
+ def __init__(
152
+ self,
153
+ src: str | IO,
154
+ dest: str,
155
+ remote_temp_filename=None,
156
+ **kwargs: Unpack[ConnectorArguments],
157
+ ):
158
+ super().__init__(**kwargs)
159
+ self.src = src
160
+ self.dest = dest
161
+ self.remote_temp_filename = remote_temp_filename
162
+
163
+ @override
164
+ def __repr__(self):
165
+ return "FileUploadCommand({0}, {1})".format(self.src, self.dest)
166
+
167
+ @override
168
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
169
+ connector_arguments.update(self.connector_arguments)
170
+
171
+ return host.put_file(
172
+ self.src,
173
+ self.dest,
174
+ remote_temp_filename=self.remote_temp_filename,
175
+ print_output=state.print_output,
176
+ print_input=state.print_input,
177
+ **connector_arguments,
178
+ )
179
+
180
+
181
+ class FileDownloadCommand(PyinfraCommand):
182
+ def __init__(
183
+ self,
184
+ src: str,
185
+ dest: str | IO,
186
+ remote_temp_filename=None,
187
+ **kwargs: Unpack[ConnectorArguments],
188
+ ):
189
+ super().__init__(**kwargs)
190
+ self.src = src
191
+ self.dest = dest
192
+ self.remote_temp_filename = remote_temp_filename
193
+
194
+ @override
195
+ def __repr__(self):
196
+ return "FileDownloadCommand({0}, {1})".format(self.src, self.dest)
197
+
198
+ @override
199
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
200
+ connector_arguments.update(self.connector_arguments)
201
+
202
+ return host.get_file(
203
+ self.src,
204
+ self.dest,
205
+ remote_temp_filename=self.remote_temp_filename,
206
+ print_output=state.print_output,
207
+ print_input=state.print_input,
208
+ **connector_arguments,
209
+ )
210
+
211
+
212
+ class FunctionCommand(PyinfraCommand):
213
+ def __init__(
214
+ self,
215
+ function: Callable,
216
+ args,
217
+ func_kwargs,
218
+ **kwargs: Unpack[ConnectorArguments],
219
+ ):
220
+ super().__init__(**kwargs)
221
+ self.function = function
222
+ self.args = args
223
+ self.kwargs = func_kwargs
224
+
225
+ @override
226
+ def __repr__(self):
227
+ return "FunctionCommand({0}, {1}, {2})".format(
228
+ self.function.__name__,
229
+ self.args,
230
+ self.kwargs,
231
+ )
232
+
233
+ @override
234
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
235
+ argspec = getfullargspec(self.function)
236
+ if "state" in argspec.args and "host" in argspec.args:
237
+ return self.function(state, host, *self.args, **self.kwargs)
238
+
239
+ # If we're already running inside a greenlet (ie a nested callback) just execute the func
240
+ # without any gevent.spawn which will break the local host object.
241
+ if isinstance(host, LocalContextObject):
242
+ self.function(*self.args, **self.kwargs)
243
+ return
244
+
245
+ def execute_function() -> None:
246
+ with ctx_config.use(state.config.copy()):
247
+ with ctx_host.use(host):
248
+ self.function(*self.args, **self.kwargs)
249
+
250
+ greenlet = gevent.spawn(execute_function)
251
+ return greenlet.get()
252
+
253
+
254
+ class RsyncCommand(PyinfraCommand):
255
+ def __init__(self, src: str, dest: str, flags, **kwargs: Unpack[ConnectorArguments]):
256
+ super().__init__(**kwargs)
257
+ self.src = src
258
+ self.dest = dest
259
+ self.flags = flags
260
+
261
+ @override
262
+ def __repr__(self):
263
+ return "RsyncCommand({0}, {1}, {2})".format(self.src, self.dest, self.flags)
264
+
265
+ @override
266
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
267
+ return host.rsync(
268
+ self.src,
269
+ self.dest,
270
+ self.flags,
271
+ print_output=state.print_output,
272
+ print_input=state.print_input,
273
+ **connector_arguments,
274
+ )
pyinfra/api/config.py CHANGED
@@ -1,46 +1,240 @@
1
- class Config(object):
2
- '''
3
- The default/base configuration options for a pyinfra deploy.
4
- '''
1
+ try:
2
+ import importlib_metadata
3
+ except ImportError:
4
+ import importlib.metadata as importlib_metadata # type: ignore[no-redef]
5
5
 
6
- state = None
6
+ from os import path
7
+ from typing import Iterable, Optional, Set
7
8
 
8
- # % of hosts which have to fail for all operations to stop
9
- FAIL_PERCENT = None
9
+ from packaging.markers import Marker
10
+ from packaging.requirements import Requirement
11
+ from packaging.specifiers import SpecifierSet
12
+ from packaging.version import Version
13
+ from typing_extensions import override
10
14
 
11
- # Seconds to timeout SSH connections
12
- CONNECT_TIMEOUT = 10
15
+ from pyinfra import __version__, state
16
+
17
+ from .exceptions import PyinfraError
13
18
 
14
- # Temporary directory (on the remote side) to use for caching any files/downloads
15
- TEMP_DIR = '/tmp'
16
19
 
20
+ class ConfigDefaults:
21
+ # % of hosts which have to fail for all operations to stop
22
+ FAIL_PERCENT: Optional[int] = None
23
+ # Seconds to timeout SSH connections
24
+ CONNECT_TIMEOUT: int = 10
25
+ # Temporary directory (on the remote side) to use for caching any files/downloads, the default
26
+ # None value first tries to load the hosts' temporary directory configured via "TMPDIR" env
27
+ # variable, falling back to DEFAULT_TEMP_DIR if not set.
28
+ TEMP_DIR: Optional[str] = None
29
+ DEFAULT_TEMP_DIR: str = "/tmp"
17
30
  # Gevent pool size (defaults to #of target hosts)
18
- PARALLEL = None
31
+ PARALLEL: int = 0
32
+ # Specify the required pyinfra version (using PEP 440 setuptools specifier)
33
+ REQUIRE_PYINFRA_VERSION: Optional[str] = None
34
+ # Specify any required packages (either using PEP 440 or a requirements file)
35
+ # Note: this can also include pyinfra potentially replacing REQUIRE_PYINFRA_VERSION
36
+ REQUIRE_PACKAGES: Optional[str] = None
37
+ # All these can be overridden inside individual operation calls:
38
+ # Switch to this user (from ssh_user) using su before executing operations
39
+ SU_USER: Optional[str] = None
40
+ USE_SU_LOGIN: bool = False
41
+ SU_SHELL: bool = False
42
+ PRESERVE_SU_ENV: bool = False
43
+ # Use sudo and optional user
44
+ SUDO: bool = False
45
+ SUDO_USER: Optional[str] = None
46
+ PRESERVE_SUDO_ENV: bool = False
47
+ USE_SUDO_LOGIN: bool = False
48
+ SUDO_PASSWORD: Optional[str] = None
49
+ # Use doas and optional user
50
+ DOAS: bool = False
51
+ DOAS_USER: Optional[str] = None
52
+ # Only show errors but don't count as failure
53
+ IGNORE_ERRORS: bool = False
54
+ # Shell to use to execute commands
55
+ SHELL: str = "sh"
56
+ # Whether to display full diffs for files
57
+ DIFF: bool = False
58
+ # Number of times to retry failed operations
59
+ RETRY: int = 0
60
+ # Delay in seconds between retry attempts
61
+ RETRY_DELAY: int = 5
19
62
 
20
- # Specify a minimum required pyinfra version for a deploy
21
- MIN_PYINFRA_VERSION = None
22
63
 
23
- # All these can be overridden inside individual operation calls:
64
+ config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items() if key.isupper()}
24
65
 
25
- # Switch to this user (from ssh_user) using su before executing operations
26
- SU_USER = None
27
66
 
28
- # Use sudo and optional user
29
- SUDO = False
30
- SUDO_USER = None
31
- PRESERVE_SUDO_ENV = False
67
+ def check_pyinfra_version(version: str):
68
+ if not version:
69
+ return
70
+ running_version = Version(__version__)
71
+ required_versions = SpecifierSet(version)
32
72
 
33
- # Only show errors, but don't count as failure
34
- IGNORE_ERRORS = False
73
+ if running_version not in required_versions:
74
+ raise PyinfraError(
75
+ f"pyinfra version requirement not met (requires {version}, running {__version__})"
76
+ )
35
77
 
36
- # Shell to use to execute commands
37
- SHELL = 'sh'
78
+
79
+ def _check_requirements(requirements: Iterable[str]) -> Set[Requirement]:
80
+ """
81
+ Check whether each of the given requirements and all their dependencies are
82
+ installed.
83
+
84
+ Or more precisely, this checks that each of the given *requirements* is
85
+ satisfied by some installed *distribution package*, and so on recursively
86
+ for each of the dependencies of those distribution packages. The terminology
87
+ here is as follows:
88
+
89
+ * A *distribution package* is essentially a thing that can be installed with
90
+ ``pip``, from an sdist or wheel or Git repo or so on.
91
+ * A *requirement* is the expectation that a distribution package satisfying
92
+ some constraint is installed.
93
+ * A *dependency* is a requirement specified by a distribution package (as
94
+ opposed to the requirements passed in to this function).
95
+
96
+ So what this function does is start from the given requirements, for each
97
+ one check that it is satisfied by some installed distribution package, and
98
+ if so recursively perform the same check on all the dependencies of that
99
+ distribution package. In short, it's traversing the graph of package
100
+ requirements. It stops whenever it finds a requirement that is not satisfied
101
+ (i.e. a required package that is not installed), or when it runs out of
102
+ requirements to check.
103
+
104
+ .. note::
105
+ This is basically equivalent to ``pkg_resources.require()`` except that
106
+ when ``require()`` succeeds, it will return the list of distribution
107
+ packages that satisfy the given requirements and their dependencies, and
108
+ when it fails, it will raise an exception. This function just returns
109
+ the requirements which were not satisfied instead.
110
+
111
+ :param requirements: The requirements to check for in the set of installed
112
+ packages (along with their dependencies).
113
+ :return: The set of requirements that were not satisfied, which will be
114
+ an empty set if all requirements (recursively) were satisfied.
115
+ """
116
+
117
+ # Based on pkg_resources.require() from setuptools. The implementation of
118
+ # hbutils.system.check_reqs() from the hbutils package was also helpful in
119
+ # clarifying what this is supposed to do.
120
+
121
+ reqs_to_check: Set[Requirement] = set(Requirement(r) for r in requirements)
122
+ reqs_satisfied: Set[Requirement] = set()
123
+ reqs_not_satisfied: Set[Requirement] = set()
124
+
125
+ while reqs_to_check:
126
+ req = reqs_to_check.pop()
127
+ assert req not in reqs_satisfied and req not in reqs_not_satisfied
128
+
129
+ # Check for an installed distribution package with the right name and version
130
+ try:
131
+ dist = importlib_metadata.distribution(req.name)
132
+ except importlib_metadata.PackageNotFoundError:
133
+ # No installed package with the right name
134
+ # This would raise a DistributionNotFound error from pkg_resources.require()
135
+ reqs_not_satisfied.add(req)
136
+ continue
137
+
138
+ if dist.version not in req.specifier:
139
+ # There is a distribution with the right name but wrong version
140
+ # This would raise a VersionConflict error from pkg_resources.require()
141
+ reqs_not_satisfied.add(req)
142
+ continue
143
+
144
+ reqs_satisfied.add(req)
145
+
146
+ # If the distribution package has dependencies of its own, go through
147
+ # those dependencies and for each one add it to the set to be checked if
148
+ # - it's unconditional (no marker)
149
+ # - or it's conditional and the condition is satisfied (the marker
150
+ # evaluates to true) in the current environment
151
+ # Markers can check things like the Python version and system version
152
+ # etc., and/or they can check which extras of the distribution package
153
+ # were required. To facilitate checking extras we have to pass the extra
154
+ # in the environment when calling Marker.evaluate().
155
+ if dist.requires:
156
+ if req.extras:
157
+ extras_envs = [{"extra": extra} for extra in req.extras]
158
+
159
+ def evaluate_marker(marker: Marker) -> bool:
160
+ return any(map(marker.evaluate, extras_envs))
161
+
162
+ else:
163
+
164
+ def evaluate_marker(marker: Marker) -> bool:
165
+ return marker.evaluate()
166
+
167
+ for dist_req_str in dist.requires:
168
+ dist_req = Requirement(dist_req_str)
169
+ if dist_req in reqs_satisfied or dist_req in reqs_not_satisfied:
170
+ continue
171
+ if (not dist_req.marker) or evaluate_marker(dist_req.marker):
172
+ reqs_to_check.add(dist_req)
173
+
174
+ return reqs_not_satisfied
175
+
176
+
177
+ def check_require_packages(requirements_config):
178
+ if not requirements_config:
179
+ return
180
+
181
+ if isinstance(requirements_config, (list, tuple)):
182
+ requirements = requirements_config
183
+ else:
184
+ with open(path.join(state.cwd or "", requirements_config), encoding="utf-8") as f:
185
+ requirements = [line.split("#egg=")[-1] for line in f.read().splitlines()]
186
+
187
+ requirements_not_met = _check_requirements(requirements)
188
+ if requirements_not_met:
189
+ raise PyinfraError(
190
+ "Deploy requirements ({0}) not met: missing {1}".format(
191
+ requirements_config, ", ".join(str(r) for r in requirements_not_met)
192
+ )
193
+ )
194
+
195
+
196
+ config_checkers = {
197
+ "REQUIRE_PYINFRA_VERSION": check_pyinfra_version,
198
+ "REQUIRE_PACKAGES": check_require_packages,
199
+ }
200
+
201
+
202
+ class Config(ConfigDefaults):
203
+ """
204
+ The default/base configuration options for a pyinfra deploy.
205
+ """
38
206
 
39
207
  def __init__(self, **kwargs):
40
208
  # Always apply some env
41
- env = kwargs.pop('ENV', {})
209
+ env = kwargs.pop("ENV", {})
42
210
  self.ENV = env
43
211
 
44
- # Apply kwargs
45
- for key, value in kwargs.items():
212
+ config = config_defaults.copy()
213
+ config.update(kwargs)
214
+
215
+ for key, value in config.items():
216
+ setattr(self, key, value)
217
+
218
+ @override
219
+ def __setattr__(self, key, value):
220
+ super().__setattr__(key, value)
221
+
222
+ checker = config_checkers.get(key)
223
+ if checker:
224
+ checker(value)
225
+
226
+ def get_current_state(self):
227
+ return [(key, getattr(self, key)) for key in config_defaults.keys()]
228
+
229
+ def set_current_state(self, config_state):
230
+ for key, value in config_state:
46
231
  setattr(self, key, value)
232
+
233
+ def lock_current_state(self) -> None:
234
+ self._locked_config = self.get_current_state()
235
+
236
+ def reset_locked_state(self) -> None:
237
+ self.set_current_state(self._locked_config)
238
+
239
+ def copy(self) -> "Config":
240
+ return Config(**dict(self.get_current_state()))
pyinfra/api/connect.py CHANGED
@@ -1,25 +1,28 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  import gevent
2
4
 
3
5
  from pyinfra.progress import progress_spinner
4
6
 
7
+ if TYPE_CHECKING:
8
+ from pyinfra.api.state import State
9
+
5
10
 
6
- def connect_all(state):
7
- '''
11
+ def connect_all(state: "State"):
12
+ """
8
13
  Connect to all the configured servers in parallel. Reads/writes state.inventory.
9
14
 
10
15
  Args:
11
16
  state (``pyinfra.api.State`` obj): the state containing an inventory to connect to
12
- '''
17
+ """
13
18
 
14
19
  hosts = [
15
- host for host in state.inventory
16
- if state.is_host_in_limit(host)
20
+ host
21
+ for host in state.inventory
22
+ if state.is_host_in_limit(host) # these are the hosts to activate ("initially connect to")
17
23
  ]
18
24
 
19
- greenlet_to_host = {
20
- state.pool.spawn(host.connect, state): host
21
- for host in hosts
22
- }
25
+ greenlet_to_host = {state.pool.spawn(host.connect): host for host in hosts}
23
26
 
24
27
  with progress_spinner(greenlet_to_host.values()) as progress:
25
28
  for greenlet in gevent.iwait(greenlet_to_host.keys()):
@@ -33,7 +36,7 @@ def connect_all(state):
33
36
  # Raise any unexpected exception
34
37
  greenlet.get()
35
38
 
36
- if host.connection:
39
+ if host.connected:
37
40
  state.activate_host(host)
38
41
  else:
39
42
  failed_hosts.add(host)
@@ -42,6 +45,23 @@ def connect_all(state):
42
45
  state.fail_hosts(failed_hosts, activated_count=len(hosts))
43
46
 
44
47
 
45
- def disconnect_all(state):
46
- for host in state.activated_hosts: # only hosts we connected to please!
47
- host.disconnect(state) # normally a noop
48
+ def disconnect_all(state: "State"):
49
+ """
50
+ Disconnect from all of the configured servers in parallel. Reads/writes state.inventory.
51
+
52
+ Args:
53
+ state (``pyinfra.api.State`` obj): the state containing an inventory to connect to
54
+ """
55
+ greenlet_to_host = {
56
+ state.pool.spawn(host.disconnect): host
57
+ for host in state.activated_hosts # only hosts we connected to please!
58
+ }
59
+
60
+ with progress_spinner(greenlet_to_host.values()) as progress:
61
+ for greenlet in gevent.iwait(greenlet_to_host.keys()):
62
+ host = greenlet_to_host[greenlet]
63
+ progress(host)
64
+
65
+ for greenlet, host in greenlet_to_host.items():
66
+ # Raise any unexpected exception
67
+ greenlet.get()
@@ -0,0 +1,27 @@
1
+ try:
2
+ from importlib_metadata import entry_points
3
+ except ImportError:
4
+ from importlib.metadata import entry_points # type: ignore[assignment]
5
+
6
+
7
+ def _load_connector(entrypoint):
8
+ return entrypoint.load()
9
+
10
+
11
+ def get_all_connectors():
12
+ return {
13
+ entrypoint.name: _load_connector(entrypoint)
14
+ for entrypoint in entry_points(group="pyinfra.connectors")
15
+ }
16
+
17
+
18
+ def get_execution_connectors():
19
+ return {
20
+ connector: connector_mod
21
+ for connector, connector_mod in get_all_connectors().items()
22
+ if connector_mod.handles_execution
23
+ }
24
+
25
+
26
+ def get_execution_connector(name):
27
+ return get_execution_connectors()[name]