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.
- pyinfra/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +19 -3
- pyinfra/api/arguments.py +413 -0
- pyinfra/api/arguments_typed.py +79 -0
- pyinfra/api/command.py +274 -0
- pyinfra/api/config.py +222 -28
- pyinfra/api/connect.py +33 -13
- pyinfra/api/connectors.py +27 -0
- pyinfra/api/deploy.py +65 -66
- pyinfra/api/exceptions.py +73 -18
- pyinfra/api/facts.py +267 -200
- pyinfra/api/host.py +416 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/metadata.py +69 -0
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +309 -369
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +405 -0
- pyinfra/connectors/dockerssh.py +297 -0
- pyinfra/connectors/local.py +238 -0
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +727 -0
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +309 -0
- pyinfra/connectors/sshuserclient/config.py +102 -0
- pyinfra/connectors/terraform.py +135 -0
- pyinfra/connectors/util.py +417 -0
- pyinfra/connectors/vagrant.py +183 -0
- pyinfra/context.py +145 -0
- pyinfra/facts/__init__.py +7 -6
- pyinfra/facts/apk.py +22 -7
- pyinfra/facts/apt.py +117 -60
- pyinfra/facts/brew.py +100 -15
- pyinfra/facts/bsdinit.py +23 -0
- pyinfra/facts/cargo.py +37 -0
- pyinfra/facts/choco.py +47 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +94 -0
- pyinfra/facts/dnf.py +48 -0
- pyinfra/facts/docker.py +96 -23
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +629 -58
- pyinfra/facts/flatpak.py +77 -0
- pyinfra/facts/freebsd.py +70 -0
- pyinfra/facts/gem.py +19 -6
- pyinfra/facts/git.py +59 -14
- pyinfra/facts/gpg.py +150 -0
- pyinfra/facts/hardware.py +313 -167
- pyinfra/facts/iptables.py +72 -62
- pyinfra/facts/launchd.py +44 -0
- pyinfra/facts/lxd.py +17 -4
- pyinfra/facts/mysql.py +122 -86
- pyinfra/facts/npm.py +17 -9
- pyinfra/facts/openrc.py +71 -0
- pyinfra/facts/opkg.py +246 -0
- pyinfra/facts/pacman.py +50 -7
- pyinfra/facts/pip.py +24 -7
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +15 -6
- pyinfra/facts/pkgin.py +35 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +178 -0
- pyinfra/facts/postgresql.py +6 -147
- pyinfra/facts/rpm.py +105 -0
- pyinfra/facts/runit.py +77 -0
- pyinfra/facts/selinux.py +161 -0
- pyinfra/facts/server.py +762 -285
- pyinfra/facts/snap.py +88 -0
- pyinfra/facts/systemd.py +139 -0
- pyinfra/facts/sysvinit.py +59 -0
- pyinfra/facts/upstart.py +35 -0
- pyinfra/facts/util/__init__.py +17 -0
- pyinfra/facts/util/databases.py +4 -6
- pyinfra/facts/util/packaging.py +37 -6
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/util/win_files.py +99 -0
- pyinfra/facts/vzctl.py +20 -13
- pyinfra/facts/xbps.py +35 -0
- pyinfra/facts/yum.py +34 -40
- pyinfra/facts/zfs.py +77 -0
- pyinfra/facts/zypper.py +42 -0
- pyinfra/local.py +45 -83
- pyinfra/operations/__init__.py +12 -0
- pyinfra/operations/apk.py +99 -0
- pyinfra/operations/apt.py +496 -0
- pyinfra/operations/brew.py +232 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +194 -0
- pyinfra/operations/dnf.py +213 -0
- pyinfra/operations/docker.py +492 -0
- pyinfra/operations/files.py +2014 -0
- pyinfra/operations/flatpak.py +95 -0
- pyinfra/operations/freebsd/__init__.py +12 -0
- pyinfra/operations/freebsd/freebsd_update.py +70 -0
- pyinfra/operations/freebsd/pkg.py +219 -0
- pyinfra/operations/freebsd/service.py +116 -0
- pyinfra/operations/freebsd/sysrc.py +92 -0
- pyinfra/operations/gem.py +48 -0
- pyinfra/operations/git.py +420 -0
- pyinfra/operations/iptables.py +312 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +69 -0
- pyinfra/operations/mysql.py +610 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +89 -0
- pyinfra/operations/pacman.py +82 -0
- pyinfra/operations/pip.py +206 -0
- pyinfra/operations/pipx.py +103 -0
- pyinfra/operations/pkg.py +71 -0
- pyinfra/operations/pkgin.py +92 -0
- pyinfra/operations/postgres.py +437 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +41 -0
- pyinfra/operations/python.py +73 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +190 -0
- pyinfra/operations/server.py +1100 -0
- pyinfra/operations/snap.py +118 -0
- pyinfra/operations/ssh.py +217 -0
- pyinfra/operations/systemd.py +150 -0
- pyinfra/operations/sysvinit.py +142 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +407 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +338 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +78 -0
- pyinfra/operations/yum.py +213 -0
- pyinfra/operations/zfs.py +176 -0
- pyinfra/operations/zypper.py +193 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.6.dist-info/METADATA +142 -0
- pyinfra-3.6.dist-info/RECORD +160 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
- pyinfra-3.6.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +793 -0
- pyinfra_cli/commands.py +66 -0
- pyinfra_cli/exceptions.py +155 -65
- pyinfra_cli/inventory.py +233 -89
- pyinfra_cli/log.py +39 -43
- pyinfra_cli/main.py +26 -495
- pyinfra_cli/prints.py +215 -156
- pyinfra_cli/util.py +172 -105
- pyinfra_cli/virtualenv.py +25 -20
- pyinfra/api/connectors/__init__.py +0 -21
- pyinfra/api/connectors/ansible.py +0 -99
- pyinfra/api/connectors/docker.py +0 -178
- pyinfra/api/connectors/local.py +0 -169
- pyinfra/api/connectors/ssh.py +0 -402
- pyinfra/api/connectors/sshuserclient/client.py +0 -105
- pyinfra/api/connectors/sshuserclient/config.py +0 -90
- pyinfra/api/connectors/util.py +0 -63
- pyinfra/api/connectors/vagrant.py +0 -155
- pyinfra/facts/init.py +0 -176
- pyinfra/facts/util/files.py +0 -102
- pyinfra/hook.py +0 -41
- pyinfra/modules/__init__.py +0 -11
- pyinfra/modules/apk.py +0 -64
- pyinfra/modules/apt.py +0 -272
- pyinfra/modules/brew.py +0 -122
- pyinfra/modules/files.py +0 -711
- pyinfra/modules/gem.py +0 -30
- pyinfra/modules/git.py +0 -115
- pyinfra/modules/init.py +0 -344
- pyinfra/modules/iptables.py +0 -271
- pyinfra/modules/lxd.py +0 -45
- pyinfra/modules/mysql.py +0 -347
- pyinfra/modules/npm.py +0 -47
- pyinfra/modules/pacman.py +0 -60
- pyinfra/modules/pip.py +0 -99
- pyinfra/modules/pkg.py +0 -43
- pyinfra/modules/postgresql.py +0 -245
- pyinfra/modules/puppet.py +0 -20
- pyinfra/modules/python.py +0 -37
- pyinfra/modules/server.py +0 -524
- pyinfra/modules/ssh.py +0 -150
- pyinfra/modules/util/files.py +0 -52
- pyinfra/modules/util/packaging.py +0 -118
- pyinfra/modules/vzctl.py +0 -133
- pyinfra/modules/yum.py +0 -171
- pyinfra/pseudo_modules.py +0 -64
- pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
- pyinfra-0.11.dev3.dist-info/METADATA +0 -135
- pyinfra-0.11.dev3.dist-info/RECORD +0 -95
- pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
- pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
- pyinfra_cli/__main__.py +0 -40
- pyinfra_cli/config.py +0 -92
- /pyinfra/{modules/util → connectors}/__init__.py +0 -0
- /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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
try:
|
|
2
|
+
import importlib_metadata
|
|
3
|
+
except ImportError:
|
|
4
|
+
import importlib.metadata as importlib_metadata # type: ignore[no-redef]
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
from os import path
|
|
7
|
+
from typing import Iterable, Optional, Set
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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(
|
|
209
|
+
env = kwargs.pop("ENV", {})
|
|
42
210
|
self.ENV = env
|
|
43
211
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
16
|
-
|
|
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.
|
|
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
|
-
|
|
47
|
-
|
|
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]
|