pyinfra 2.9.1__py2.py3-none-any.whl → 3.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyinfra/api/__init__.py +3 -0
- pyinfra/api/arguments.py +265 -253
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +68 -53
- pyinfra/api/config.py +139 -32
- pyinfra/api/connect.py +1 -1
- pyinfra/api/connectors.py +7 -26
- pyinfra/api/deploy.py +21 -52
- pyinfra/api/exceptions.py +33 -8
- pyinfra/api/facts.py +102 -137
- pyinfra/api/host.py +150 -82
- pyinfra/api/inventory.py +21 -25
- pyinfra/api/operation.py +240 -198
- pyinfra/api/operations.py +102 -148
- pyinfra/api/state.py +137 -79
- pyinfra/api/util.py +79 -86
- pyinfra/connectors/base.py +147 -0
- pyinfra/connectors/chroot.py +160 -169
- pyinfra/connectors/docker.py +220 -237
- pyinfra/connectors/dockerssh.py +231 -253
- pyinfra/connectors/local.py +196 -208
- pyinfra/connectors/ssh.py +530 -613
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +5 -3
- pyinfra/connectors/terraform.py +86 -65
- pyinfra/connectors/util.py +211 -137
- pyinfra/connectors/vagrant.py +60 -53
- pyinfra/context.py +4 -2
- pyinfra/facts/apk.py +2 -0
- pyinfra/facts/apt.py +2 -0
- pyinfra/facts/brew.py +2 -0
- pyinfra/facts/bsdinit.py +2 -0
- pyinfra/facts/cargo.py +2 -0
- pyinfra/facts/choco.py +2 -0
- pyinfra/facts/deb.py +7 -2
- pyinfra/facts/dnf.py +2 -0
- pyinfra/facts/docker.py +19 -0
- pyinfra/facts/files.py +47 -32
- pyinfra/facts/gem.py +2 -0
- pyinfra/facts/git.py +3 -1
- pyinfra/facts/gpg.py +3 -1
- pyinfra/facts/hardware.py +34 -24
- pyinfra/facts/iptables.py +5 -3
- pyinfra/facts/launchd.py +2 -0
- pyinfra/facts/lxd.py +2 -0
- pyinfra/facts/mysql.py +13 -6
- pyinfra/facts/npm.py +1 -0
- pyinfra/facts/openrc.py +2 -0
- pyinfra/facts/pacman.py +6 -2
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +2 -0
- pyinfra/facts/pkgin.py +2 -0
- pyinfra/facts/postgres.py +168 -0
- pyinfra/facts/postgresql.py +6 -160
- pyinfra/facts/rpm.py +12 -9
- pyinfra/facts/runit.py +68 -0
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +80 -36
- pyinfra/facts/snap.py +2 -0
- pyinfra/facts/systemd.py +31 -12
- pyinfra/facts/sysvinit.py +10 -10
- pyinfra/facts/upstart.py +2 -0
- pyinfra/facts/util/packaging.py +7 -4
- pyinfra/facts/vzctl.py +2 -0
- pyinfra/facts/xbps.py +2 -0
- pyinfra/facts/yum.py +2 -0
- pyinfra/facts/zypper.py +2 -0
- pyinfra/local.py +4 -5
- pyinfra/operations/apk.py +6 -4
- pyinfra/operations/apt.py +46 -65
- pyinfra/operations/brew.py +17 -22
- pyinfra/operations/bsdinit.py +9 -7
- pyinfra/operations/cargo.py +4 -2
- pyinfra/operations/choco.py +4 -2
- pyinfra/operations/dnf.py +19 -23
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +188 -386
- pyinfra/operations/gem.py +4 -2
- pyinfra/operations/git.py +24 -53
- pyinfra/operations/iptables.py +29 -35
- pyinfra/operations/launchd.py +6 -7
- pyinfra/operations/lxd.py +8 -13
- pyinfra/operations/mysql.py +62 -81
- pyinfra/operations/npm.py +9 -2
- pyinfra/operations/openrc.py +6 -4
- pyinfra/operations/pacman.py +7 -8
- pyinfra/operations/pip.py +25 -24
- pyinfra/operations/pkg.py +4 -2
- pyinfra/operations/pkgin.py +6 -4
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -379
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -44
- pyinfra/operations/server.py +111 -127
- pyinfra/operations/snap.py +4 -4
- pyinfra/operations/ssh.py +20 -33
- pyinfra/operations/systemd.py +19 -15
- pyinfra/operations/sysvinit.py +9 -16
- pyinfra/operations/upstart.py +9 -7
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +177 -0
- pyinfra/operations/util/files.py +24 -16
- pyinfra/operations/util/packaging.py +55 -57
- pyinfra/operations/util/service.py +39 -51
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +6 -4
- pyinfra/operations/yum.py +18 -22
- pyinfra/operations/zypper.py +12 -13
- pyinfra/version.py +5 -2
- {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
- pyinfra-3.0.dist-info/RECORD +167 -0
- {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
- pyinfra-3.0.dist-info/entry_points.txt +11 -0
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +7 -2
- pyinfra_cli/exceptions.py +78 -42
- pyinfra_cli/inventory.py +40 -6
- pyinfra_cli/log.py +17 -3
- pyinfra_cli/main.py +133 -90
- pyinfra_cli/prints.py +95 -127
- pyinfra_cli/util.py +62 -29
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +13 -13
- tests/test_api/test_api_deploys.py +28 -29
- tests/test_api/test_api_facts.py +60 -98
- tests/test_api/test_api_operations.py +101 -201
- tests/test_cli/test_cli.py +18 -49
- tests/test_cli/test_cli_deploy.py +11 -37
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_chroot.py +6 -6
- tests/test_connectors/test_docker.py +4 -4
- tests/test_connectors/test_dockerssh.py +38 -50
- tests/test_connectors/test_local.py +11 -12
- tests/test_connectors/test_ssh.py +105 -93
- tests/test_connectors/test_terraform.py +9 -15
- tests/test_connectors/test_util.py +24 -46
- tests/test_connectors/test_vagrant.py +7 -7
- pyinfra/api/operation.pyi +0 -117
- pyinfra/connectors/ansible.py +0 -171
- pyinfra/connectors/mech.py +0 -186
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -320
- pyinfra/facts/windows.py +0 -366
- pyinfra/facts/windows_files.py +0 -90
- pyinfra/operations/windows.py +0 -59
- pyinfra/operations/windows_files.py +0 -551
- pyinfra-2.9.1.dist-info/RECORD +0 -170
- pyinfra-2.9.1.dist-info/entry_points.txt +0 -14
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- tests/test_connectors/test_winrm.py +0 -76
- {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
- {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
pyinfra/api/command.py
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import shlex
|
|
2
4
|
from inspect import getfullargspec
|
|
3
5
|
from string import Formatter
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import IO, TYPE_CHECKING, Callable, Union
|
|
5
7
|
|
|
6
8
|
import gevent
|
|
9
|
+
from typing_extensions import Unpack
|
|
7
10
|
|
|
8
11
|
from pyinfra.context import ctx_config, ctx_host
|
|
9
12
|
|
|
10
|
-
from .arguments import
|
|
13
|
+
from .arguments import ConnectorArguments
|
|
11
14
|
|
|
12
15
|
if TYPE_CHECKING:
|
|
13
16
|
from pyinfra.api.host import Host
|
|
14
17
|
from pyinfra.api.state import State
|
|
15
18
|
|
|
16
19
|
|
|
17
|
-
def make_formatted_string_command(string: str, *args, **kwargs):
|
|
20
|
+
def make_formatted_string_command(string: str, *args, **kwargs) -> "StringCommand":
|
|
18
21
|
"""
|
|
19
22
|
Helper function that takes a shell command or script as a string, splits it
|
|
20
23
|
using ``shlex.split`` and then formats each bit, returning a ``StringCommand``
|
|
@@ -50,39 +53,46 @@ class MaskString(str):
|
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
class QuoteString:
|
|
53
|
-
|
|
54
|
-
self.object = obj
|
|
56
|
+
obj: Union[str, "StringCommand"]
|
|
55
57
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
+
def __init__(self, obj: Union[str, "StringCommand"]):
|
|
59
|
+
self.obj = obj
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return f"QuoteString({self.obj})"
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
class PyinfraCommand:
|
|
61
|
-
|
|
62
|
-
self.executor_kwargs = {
|
|
63
|
-
key: kwargs[key] for key in get_executor_kwarg_keys() if key in kwargs
|
|
64
|
-
}
|
|
66
|
+
connector_arguments: ConnectorArguments
|
|
65
67
|
|
|
66
|
-
def
|
|
68
|
+
def __init__(self, **arguments: Unpack[ConnectorArguments]):
|
|
69
|
+
self.connector_arguments = arguments
|
|
70
|
+
|
|
71
|
+
def __eq__(self, other) -> bool:
|
|
67
72
|
if isinstance(other, self.__class__) and repr(self) == repr(other):
|
|
68
73
|
return True
|
|
69
74
|
return False
|
|
70
75
|
|
|
71
|
-
def execute(self, state: "State", host: "Host",
|
|
76
|
+
def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
|
|
72
77
|
raise NotImplementedError
|
|
73
78
|
|
|
74
79
|
|
|
75
80
|
class StringCommand(PyinfraCommand):
|
|
76
|
-
def __init__(
|
|
77
|
-
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
*bits,
|
|
84
|
+
_separator=" ",
|
|
85
|
+
**arguments: Unpack[ConnectorArguments],
|
|
86
|
+
):
|
|
87
|
+
super().__init__(**arguments)
|
|
78
88
|
self.bits = bits
|
|
79
|
-
self.separator =
|
|
89
|
+
self.separator = _separator
|
|
80
90
|
|
|
81
|
-
def __str__(self):
|
|
91
|
+
def __str__(self) -> str:
|
|
82
92
|
return self.get_masked_value()
|
|
83
93
|
|
|
84
|
-
def __repr__(self):
|
|
85
|
-
return "StringCommand({
|
|
94
|
+
def __repr__(self) -> str:
|
|
95
|
+
return f"StringCommand({self.get_masked_value()})"
|
|
86
96
|
|
|
87
97
|
def _get_all_bits(self, bit_accessor):
|
|
88
98
|
all_bits = []
|
|
@@ -91,7 +101,7 @@ class StringCommand(PyinfraCommand):
|
|
|
91
101
|
quote = False
|
|
92
102
|
if isinstance(bit, QuoteString):
|
|
93
103
|
quote = True
|
|
94
|
-
bit = bit.
|
|
104
|
+
bit = bit.obj
|
|
95
105
|
|
|
96
106
|
if isinstance(bit, StringCommand):
|
|
97
107
|
bit = bit_accessor(bit)
|
|
@@ -106,14 +116,14 @@ class StringCommand(PyinfraCommand):
|
|
|
106
116
|
|
|
107
117
|
return all_bits
|
|
108
118
|
|
|
109
|
-
def get_raw_value(self):
|
|
119
|
+
def get_raw_value(self) -> str:
|
|
110
120
|
return self.separator.join(
|
|
111
121
|
self._get_all_bits(
|
|
112
122
|
lambda bit: bit.get_raw_value(),
|
|
113
123
|
),
|
|
114
124
|
)
|
|
115
125
|
|
|
116
|
-
def get_masked_value(self):
|
|
126
|
+
def get_masked_value(self) -> str:
|
|
117
127
|
return self.separator.join(
|
|
118
128
|
[
|
|
119
129
|
"***" if isinstance(bit, MaskString) else bit
|
|
@@ -121,20 +131,25 @@ class StringCommand(PyinfraCommand):
|
|
|
121
131
|
],
|
|
122
132
|
)
|
|
123
133
|
|
|
124
|
-
def execute(self, state: "State", host: "Host",
|
|
125
|
-
|
|
134
|
+
def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
|
|
135
|
+
connector_arguments.update(self.connector_arguments)
|
|
126
136
|
|
|
127
137
|
return host.run_shell_command(
|
|
128
138
|
self,
|
|
129
139
|
print_output=state.print_output,
|
|
130
140
|
print_input=state.print_input,
|
|
131
|
-
|
|
132
|
-
**executor_kwargs,
|
|
141
|
+
**connector_arguments,
|
|
133
142
|
)
|
|
134
143
|
|
|
135
144
|
|
|
136
145
|
class FileUploadCommand(PyinfraCommand):
|
|
137
|
-
def __init__(
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
src: str | IO,
|
|
149
|
+
dest: str,
|
|
150
|
+
remote_temp_filename=None,
|
|
151
|
+
**kwargs: Unpack[ConnectorArguments],
|
|
152
|
+
):
|
|
138
153
|
super().__init__(**kwargs)
|
|
139
154
|
self.src = src
|
|
140
155
|
self.dest = dest
|
|
@@ -143,8 +158,8 @@ class FileUploadCommand(PyinfraCommand):
|
|
|
143
158
|
def __repr__(self):
|
|
144
159
|
return "FileUploadCommand({0}, {1})".format(self.src, self.dest)
|
|
145
160
|
|
|
146
|
-
def execute(self, state: "State", host: "Host",
|
|
147
|
-
|
|
161
|
+
def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
|
|
162
|
+
connector_arguments.update(self.connector_arguments)
|
|
148
163
|
|
|
149
164
|
return host.put_file(
|
|
150
165
|
self.src,
|
|
@@ -152,12 +167,18 @@ class FileUploadCommand(PyinfraCommand):
|
|
|
152
167
|
remote_temp_filename=self.remote_temp_filename,
|
|
153
168
|
print_output=state.print_output,
|
|
154
169
|
print_input=state.print_input,
|
|
155
|
-
**
|
|
170
|
+
**connector_arguments,
|
|
156
171
|
)
|
|
157
172
|
|
|
158
173
|
|
|
159
174
|
class FileDownloadCommand(PyinfraCommand):
|
|
160
|
-
def __init__(
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
src: str,
|
|
178
|
+
dest: str | IO,
|
|
179
|
+
remote_temp_filename=None,
|
|
180
|
+
**kwargs: Unpack[ConnectorArguments],
|
|
181
|
+
):
|
|
161
182
|
super().__init__(**kwargs)
|
|
162
183
|
self.src = src
|
|
163
184
|
self.dest = dest
|
|
@@ -166,8 +187,8 @@ class FileDownloadCommand(PyinfraCommand):
|
|
|
166
187
|
def __repr__(self):
|
|
167
188
|
return "FileDownloadCommand({0}, {1})".format(self.src, self.dest)
|
|
168
189
|
|
|
169
|
-
def execute(self, state: "State", host: "Host",
|
|
170
|
-
|
|
190
|
+
def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
|
|
191
|
+
connector_arguments.update(self.connector_arguments)
|
|
171
192
|
|
|
172
193
|
return host.get_file(
|
|
173
194
|
self.src,
|
|
@@ -175,12 +196,18 @@ class FileDownloadCommand(PyinfraCommand):
|
|
|
175
196
|
remote_temp_filename=self.remote_temp_filename,
|
|
176
197
|
print_output=state.print_output,
|
|
177
198
|
print_input=state.print_input,
|
|
178
|
-
**
|
|
199
|
+
**connector_arguments,
|
|
179
200
|
)
|
|
180
201
|
|
|
181
202
|
|
|
182
203
|
class FunctionCommand(PyinfraCommand):
|
|
183
|
-
def __init__(
|
|
204
|
+
def __init__(
|
|
205
|
+
self,
|
|
206
|
+
function: Callable,
|
|
207
|
+
args,
|
|
208
|
+
func_kwargs,
|
|
209
|
+
**kwargs: Unpack[ConnectorArguments],
|
|
210
|
+
):
|
|
184
211
|
super().__init__(**kwargs)
|
|
185
212
|
self.function = function
|
|
186
213
|
self.args = args
|
|
@@ -193,34 +220,22 @@ class FunctionCommand(PyinfraCommand):
|
|
|
193
220
|
self.kwargs,
|
|
194
221
|
)
|
|
195
222
|
|
|
196
|
-
def execute(self, state: "State", host: "Host",
|
|
223
|
+
def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
|
|
197
224
|
argspec = getfullargspec(self.function)
|
|
198
225
|
if "state" in argspec.args and "host" in argspec.args:
|
|
199
226
|
return self.function(state, host, *self.args, **self.kwargs)
|
|
200
227
|
|
|
201
|
-
# Note: we use try/except here to catch any exception inside the greenlet
|
|
202
|
-
# and bubble that back out as a return value before re-raising outside.
|
|
203
|
-
# This is because gevent dumps stack traces when raised within a greenlet
|
|
204
|
-
# and we want to handle that ourselves.
|
|
205
228
|
def execute_function():
|
|
206
229
|
with ctx_config.use(state.config.copy()):
|
|
207
230
|
with ctx_host.use(host):
|
|
208
|
-
|
|
209
|
-
self.function(*self.args, **self.kwargs)
|
|
210
|
-
except Exception as e:
|
|
211
|
-
return e
|
|
212
|
-
else:
|
|
213
|
-
return True
|
|
231
|
+
self.function(*self.args, **self.kwargs)
|
|
214
232
|
|
|
215
233
|
greenlet = gevent.spawn(execute_function)
|
|
216
|
-
|
|
217
|
-
if isinstance(res, Exception):
|
|
218
|
-
raise res
|
|
219
|
-
return res
|
|
234
|
+
return greenlet.get()
|
|
220
235
|
|
|
221
236
|
|
|
222
237
|
class RsyncCommand(PyinfraCommand):
|
|
223
|
-
def __init__(self, src: str, dest: str, flags, **kwargs):
|
|
238
|
+
def __init__(self, src: str, dest: str, flags, **kwargs: Unpack[ConnectorArguments]):
|
|
224
239
|
super().__init__(**kwargs)
|
|
225
240
|
self.src = src
|
|
226
241
|
self.dest = dest
|
|
@@ -229,12 +244,12 @@ class RsyncCommand(PyinfraCommand):
|
|
|
229
244
|
def __repr__(self):
|
|
230
245
|
return "RsyncCommand({0}, {1}, {2})".format(self.src, self.dest, self.flags)
|
|
231
246
|
|
|
232
|
-
def execute(self, state: "State", host: "Host",
|
|
247
|
+
def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
|
|
233
248
|
return host.rsync(
|
|
234
249
|
self.src,
|
|
235
250
|
self.dest,
|
|
236
251
|
self.flags,
|
|
237
252
|
print_output=state.print_output,
|
|
238
253
|
print_input=state.print_input,
|
|
239
|
-
**
|
|
254
|
+
**connector_arguments,
|
|
240
255
|
)
|
pyinfra/api/config.py
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import importlib_metadata
|
|
3
|
+
except ImportError:
|
|
4
|
+
import importlib.metadata as importlib_metadata # type: ignore[no-redef]
|
|
1
5
|
from os import path
|
|
6
|
+
from typing import Iterable, Optional, Set
|
|
2
7
|
|
|
3
|
-
from
|
|
8
|
+
from packaging.markers import Marker
|
|
9
|
+
from packaging.requirements import Requirement
|
|
10
|
+
from packaging.specifiers import SpecifierSet
|
|
11
|
+
from packaging.version import Version
|
|
4
12
|
|
|
5
13
|
from pyinfra import __version__, state
|
|
6
14
|
|
|
@@ -9,37 +17,40 @@ from .exceptions import PyinfraError
|
|
|
9
17
|
|
|
10
18
|
class ConfigDefaults:
|
|
11
19
|
# % of hosts which have to fail for all operations to stop
|
|
12
|
-
FAIL_PERCENT = None
|
|
20
|
+
FAIL_PERCENT: Optional[int] = None
|
|
13
21
|
# Seconds to timeout SSH connections
|
|
14
|
-
CONNECT_TIMEOUT = 10
|
|
15
|
-
# Temporary directory (on the remote side) to use for caching any files/downloads
|
|
16
|
-
|
|
22
|
+
CONNECT_TIMEOUT: int = 10
|
|
23
|
+
# Temporary directory (on the remote side) to use for caching any files/downloads, the default
|
|
24
|
+
# None value first tries to load the hosts' temporary directory configured via "TMPDIR" env
|
|
25
|
+
# variable, falling back to DEFAULT_TEMP_DIR if not set.
|
|
26
|
+
TEMP_DIR: Optional[str] = None
|
|
27
|
+
DEFAULT_TEMP_DIR: str = "/tmp"
|
|
17
28
|
# Gevent pool size (defaults to #of target hosts)
|
|
18
|
-
PARALLEL = 0
|
|
29
|
+
PARALLEL: int = 0
|
|
19
30
|
# Specify the required pyinfra version (using PEP 440 setuptools specifier)
|
|
20
|
-
REQUIRE_PYINFRA_VERSION = None
|
|
31
|
+
REQUIRE_PYINFRA_VERSION: Optional[str] = None
|
|
21
32
|
# Specify any required packages (either using PEP 440 or a requirements file)
|
|
22
33
|
# Note: this can also include pyinfra potentially replacing REQUIRE_PYINFRA_VERSION
|
|
23
|
-
REQUIRE_PACKAGES = None
|
|
34
|
+
REQUIRE_PACKAGES: Optional[str] = None
|
|
24
35
|
# All these can be overridden inside individual operation calls:
|
|
25
36
|
# Switch to this user (from ssh_user) using su before executing operations
|
|
26
|
-
SU_USER = None
|
|
27
|
-
USE_SU_LOGIN = False
|
|
28
|
-
SU_SHELL =
|
|
29
|
-
PRESERVE_SU_ENV = False
|
|
37
|
+
SU_USER: Optional[str] = None
|
|
38
|
+
USE_SU_LOGIN: bool = False
|
|
39
|
+
SU_SHELL: bool = False
|
|
40
|
+
PRESERVE_SU_ENV: bool = False
|
|
30
41
|
# Use sudo and optional user
|
|
31
|
-
SUDO = False
|
|
32
|
-
SUDO_USER = None
|
|
33
|
-
PRESERVE_SUDO_ENV = False
|
|
34
|
-
USE_SUDO_LOGIN = False
|
|
35
|
-
|
|
42
|
+
SUDO: bool = False
|
|
43
|
+
SUDO_USER: Optional[str] = None
|
|
44
|
+
PRESERVE_SUDO_ENV: bool = False
|
|
45
|
+
USE_SUDO_LOGIN: bool = False
|
|
46
|
+
SUDO_PASSWORD: Optional[str] = None
|
|
36
47
|
# Use doas and optional user
|
|
37
|
-
DOAS = False
|
|
38
|
-
DOAS_USER = None
|
|
48
|
+
DOAS: bool = False
|
|
49
|
+
DOAS_USER: Optional[str] = None
|
|
39
50
|
# Only show errors but don't count as failure
|
|
40
|
-
IGNORE_ERRORS = False
|
|
51
|
+
IGNORE_ERRORS: bool = False
|
|
41
52
|
# Shell to use to execute commands
|
|
42
|
-
SHELL = "sh"
|
|
53
|
+
SHELL: str = "sh"
|
|
43
54
|
|
|
44
55
|
|
|
45
56
|
config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items() if key.isupper()}
|
|
@@ -48,15 +59,113 @@ config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items()
|
|
|
48
59
|
def check_pyinfra_version(version: str):
|
|
49
60
|
if not version:
|
|
50
61
|
return
|
|
51
|
-
running_version =
|
|
52
|
-
required_versions =
|
|
62
|
+
running_version = Version(__version__)
|
|
63
|
+
required_versions = SpecifierSet(version)
|
|
53
64
|
|
|
54
|
-
if not required_versions
|
|
65
|
+
if running_version not in required_versions:
|
|
55
66
|
raise PyinfraError(
|
|
56
67
|
f"pyinfra version requirement not met (requires {version}, running {__version__})"
|
|
57
68
|
)
|
|
58
69
|
|
|
59
70
|
|
|
71
|
+
def _check_requirements(requirements: Iterable[str]) -> Set[Requirement]:
|
|
72
|
+
"""
|
|
73
|
+
Check whether each of the given requirements and all their dependencies are
|
|
74
|
+
installed.
|
|
75
|
+
|
|
76
|
+
Or more precisely, this checks that each of the given *requirements* is
|
|
77
|
+
satisfied by some installed *distribution package*, and so on recursively
|
|
78
|
+
for each of the dependencies of those distribution packages. The terminology
|
|
79
|
+
here is as follows:
|
|
80
|
+
|
|
81
|
+
* A *distribution package* is essentially a thing that can be installed with
|
|
82
|
+
``pip``, from an sdist or wheel or Git repo or so on.
|
|
83
|
+
* A *requirement* is the expectation that a distribution package satisfying
|
|
84
|
+
some constraint is installed.
|
|
85
|
+
* A *dependency* is a requirement specified by a distribution package (as
|
|
86
|
+
opposed to the requirements passed in to this function).
|
|
87
|
+
|
|
88
|
+
So what this function does is start from the given requirements, for each
|
|
89
|
+
one check that it is satisfied by some installed distribution package, and
|
|
90
|
+
if so recursively perform the same check on all the dependencies of that
|
|
91
|
+
distribution package. In short, it's traversing the graph of package
|
|
92
|
+
requirements. It stops whenever it finds a requirement that is not satisfied
|
|
93
|
+
(i.e. a required package that is not installed), or when it runs out of
|
|
94
|
+
requirements to check.
|
|
95
|
+
|
|
96
|
+
.. note::
|
|
97
|
+
This is basically equivalent to ``pkg_resources.require()`` except that
|
|
98
|
+
when ``require()`` succeeds, it will return the list of distribution
|
|
99
|
+
packages that satisfy the given requirements and their dependencies, and
|
|
100
|
+
when it fails, it will raise an exception. This function just returns
|
|
101
|
+
the requirements which were not satisfied instead.
|
|
102
|
+
|
|
103
|
+
:param requirements: The requirements to check for in the set of installed
|
|
104
|
+
packages (along with their dependencies).
|
|
105
|
+
:return: The set of requirements that were not satisfied, which will be
|
|
106
|
+
an empty set if all requirements (recursively) were satisfied.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
# Based on pkg_resources.require() from setuptools. The implementation of
|
|
110
|
+
# hbutils.system.check_reqs() from the hbutils package was also helpful in
|
|
111
|
+
# clarifying what this is supposed to do.
|
|
112
|
+
|
|
113
|
+
reqs_to_check: Set[Requirement] = set(Requirement(r) for r in requirements)
|
|
114
|
+
reqs_satisfied: Set[Requirement] = set()
|
|
115
|
+
reqs_not_satisfied: Set[Requirement] = set()
|
|
116
|
+
|
|
117
|
+
while reqs_to_check:
|
|
118
|
+
req = reqs_to_check.pop()
|
|
119
|
+
assert req not in reqs_satisfied and req not in reqs_not_satisfied
|
|
120
|
+
|
|
121
|
+
# Check for an installed distribution package with the right name and version
|
|
122
|
+
try:
|
|
123
|
+
dist = importlib_metadata.distribution(req.name)
|
|
124
|
+
except importlib_metadata.PackageNotFoundError:
|
|
125
|
+
# No installed package with the right name
|
|
126
|
+
# This would raise a DistributionNotFound error from pkg_resources.require()
|
|
127
|
+
reqs_not_satisfied.add(req)
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if dist.version not in req.specifier:
|
|
131
|
+
# There is a distribution with the right name but wrong version
|
|
132
|
+
# This would raise a VersionConflict error from pkg_resources.require()
|
|
133
|
+
reqs_not_satisfied.add(req)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
reqs_satisfied.add(req)
|
|
137
|
+
|
|
138
|
+
# If the distribution package has dependencies of its own, go through
|
|
139
|
+
# those dependencies and for each one add it to the set to be checked if
|
|
140
|
+
# - it's unconditional (no marker)
|
|
141
|
+
# - or it's conditional and the condition is satisfied (the marker
|
|
142
|
+
# evaluates to true) in the current environment
|
|
143
|
+
# Markers can check things like the Python version and system version
|
|
144
|
+
# etc., and/or they can check which extras of the distribution package
|
|
145
|
+
# were required. To facilitate checking extras we have to pass the extra
|
|
146
|
+
# in the environment when calling Marker.evaluate().
|
|
147
|
+
if dist.requires:
|
|
148
|
+
if req.extras:
|
|
149
|
+
extras_envs = [{"extra": extra} for extra in req.extras]
|
|
150
|
+
|
|
151
|
+
def evaluate_marker(marker: Marker) -> bool:
|
|
152
|
+
return any(map(marker.evaluate, extras_envs))
|
|
153
|
+
|
|
154
|
+
else:
|
|
155
|
+
|
|
156
|
+
def evaluate_marker(marker: Marker) -> bool:
|
|
157
|
+
return marker.evaluate()
|
|
158
|
+
|
|
159
|
+
for dist_req_str in dist.requires:
|
|
160
|
+
dist_req = Requirement(dist_req_str)
|
|
161
|
+
if dist_req in reqs_satisfied or dist_req in reqs_not_satisfied:
|
|
162
|
+
continue
|
|
163
|
+
if (not dist_req.marker) or evaluate_marker(dist_req.marker):
|
|
164
|
+
reqs_to_check.add(dist_req)
|
|
165
|
+
|
|
166
|
+
return reqs_not_satisfied
|
|
167
|
+
|
|
168
|
+
|
|
60
169
|
def check_require_packages(requirements_config):
|
|
61
170
|
if not requirements_config:
|
|
62
171
|
return
|
|
@@ -64,17 +173,15 @@ def check_require_packages(requirements_config):
|
|
|
64
173
|
if isinstance(requirements_config, (list, tuple)):
|
|
65
174
|
requirements = requirements_config
|
|
66
175
|
else:
|
|
67
|
-
with open(path.join(state.cwd, requirements_config), encoding="utf-8") as f:
|
|
176
|
+
with open(path.join(state.cwd or "", requirements_config), encoding="utf-8") as f:
|
|
68
177
|
requirements = [line.split("#egg=")[-1] for line in f.read().splitlines()]
|
|
69
178
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
except ResolutionError as e:
|
|
179
|
+
requirements_not_met = _check_requirements(requirements)
|
|
180
|
+
if requirements_not_met:
|
|
73
181
|
raise PyinfraError(
|
|
74
|
-
"Deploy requirements ({0}) not met: {1}".format(
|
|
75
|
-
requirements_config,
|
|
76
|
-
|
|
77
|
-
),
|
|
182
|
+
"Deploy requirements ({0}) not met: missing {1}".format(
|
|
183
|
+
requirements_config, ", ".join(str(r) for r in requirements_not_met)
|
|
184
|
+
)
|
|
78
185
|
)
|
|
79
186
|
|
|
80
187
|
|
pyinfra/api/connect.py
CHANGED
pyinfra/api/connectors.py
CHANGED
|
@@ -1,36 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
handles_execution = False
|
|
6
|
-
keys_prefix = ""
|
|
7
|
-
|
|
8
|
-
class DataKeys:
|
|
9
|
-
pass
|
|
10
|
-
|
|
11
|
-
@classmethod
|
|
12
|
-
def keys(cls):
|
|
13
|
-
class Keys:
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
for key in cls.DataKeys.__dict__:
|
|
17
|
-
if not key.startswith("_"):
|
|
18
|
-
setattr(Keys, key, f"{cls.keys_prefix}_{key}")
|
|
19
|
-
|
|
20
|
-
return Keys
|
|
1
|
+
try:
|
|
2
|
+
from importlib_metadata import entry_points
|
|
3
|
+
except ImportError:
|
|
4
|
+
from importlib.metadata import entry_points # type: ignore[assignment]
|
|
21
5
|
|
|
22
6
|
|
|
23
7
|
def _load_connector(entrypoint):
|
|
24
|
-
|
|
25
|
-
if not getattr(connector, "Meta", None):
|
|
26
|
-
connector.Meta = BaseConnectorMeta
|
|
27
|
-
return connector
|
|
8
|
+
return entrypoint.load()
|
|
28
9
|
|
|
29
10
|
|
|
30
11
|
def get_all_connectors():
|
|
31
12
|
return {
|
|
32
13
|
entrypoint.name: _load_connector(entrypoint)
|
|
33
|
-
for entrypoint in
|
|
14
|
+
for entrypoint in entry_points(group="pyinfra.connectors")
|
|
34
15
|
}
|
|
35
16
|
|
|
36
17
|
|
|
@@ -38,7 +19,7 @@ def get_execution_connectors():
|
|
|
38
19
|
return {
|
|
39
20
|
connector: connector_mod
|
|
40
21
|
for connector, connector_mod in get_all_connectors().items()
|
|
41
|
-
if connector_mod.
|
|
22
|
+
if connector_mod.handles_execution
|
|
42
23
|
}
|
|
43
24
|
|
|
44
25
|
|
pyinfra/api/deploy.py
CHANGED
|
@@ -5,31 +5,24 @@ creation (eg pyinfra-openstack).
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from functools import wraps
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Callable,
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
|
|
9
|
+
|
|
10
|
+
from typing_extensions import ParamSpec
|
|
9
11
|
|
|
10
12
|
import pyinfra
|
|
11
|
-
from pyinfra import context
|
|
13
|
+
from pyinfra import context
|
|
12
14
|
from pyinfra.context import ctx_host, ctx_state
|
|
13
15
|
|
|
14
16
|
from .arguments import pop_global_arguments
|
|
17
|
+
from .arguments_typed import PyinfraOperation
|
|
15
18
|
from .exceptions import PyinfraError
|
|
16
19
|
from .host import Host
|
|
17
|
-
from .util import
|
|
20
|
+
from .util import get_call_location
|
|
18
21
|
|
|
19
22
|
if TYPE_CHECKING:
|
|
20
23
|
from pyinfra.api.state import State
|
|
21
24
|
|
|
22
25
|
|
|
23
|
-
@memoize
|
|
24
|
-
def show_state_host_arguments_warning(call_location):
|
|
25
|
-
logger.warning(
|
|
26
|
-
(
|
|
27
|
-
"{0}:\n\tLegacy deploy function detected! Deploys should no longer define "
|
|
28
|
-
"`state` and `host` arguments."
|
|
29
|
-
).format(call_location),
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
|
|
33
26
|
def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs):
|
|
34
27
|
"""
|
|
35
28
|
Prepare & add an deploy to pyinfra.state by executing it on all hosts.
|
|
@@ -58,61 +51,37 @@ def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs)
|
|
|
58
51
|
deploy_func(*args, **kwargs)
|
|
59
52
|
|
|
60
53
|
|
|
61
|
-
|
|
54
|
+
P = ParamSpec("P")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def deploy(name: Optional[str] = None, data_defaults=None):
|
|
62
58
|
"""
|
|
63
59
|
Decorator that takes a deploy function (normally from a pyinfra_* package)
|
|
64
60
|
and wraps any operations called inside with any deploy-wide kwargs/data.
|
|
65
61
|
"""
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
f.deploy_name = name
|
|
73
|
-
if data_defaults:
|
|
74
|
-
f.deploy_data = data_defaults
|
|
75
|
-
return deploy(f, _call_location=get_call_location())
|
|
76
|
-
|
|
77
|
-
return decorator
|
|
63
|
+
def decorator(func: Callable[P, Any]) -> PyinfraOperation[P]:
|
|
64
|
+
func.deploy_name = name or func.__name__ # type: ignore[attr-defined]
|
|
65
|
+
if data_defaults:
|
|
66
|
+
func.deploy_data = data_defaults # type: ignore[attr-defined]
|
|
67
|
+
return _wrap_deploy(func)
|
|
78
68
|
|
|
79
|
-
|
|
80
|
-
func = func_or_name
|
|
69
|
+
return decorator
|
|
81
70
|
|
|
82
|
-
# Check whether an operation is "legacy" - ie contains state=None, host=None kwargs
|
|
83
|
-
# TODO: remove this in v3
|
|
84
|
-
is_legacy = False
|
|
85
|
-
args, kwargs = get_args_kwargs_spec(func)
|
|
86
|
-
if all(key in kwargs and kwargs[key] is None for key in ("state", "host")):
|
|
87
|
-
show_state_host_arguments_warning(_call_location or get_call_location())
|
|
88
|
-
is_legacy = True
|
|
89
|
-
func.is_legacy = is_legacy # type: ignore
|
|
90
71
|
|
|
72
|
+
def _wrap_deploy(func: Callable[P, Any]) -> PyinfraOperation[P]:
|
|
91
73
|
@wraps(func)
|
|
92
|
-
def decorated_func(*args, **kwargs):
|
|
74
|
+
def decorated_func(*args: P.args, **kwargs: P.kwargs) -> Any:
|
|
93
75
|
deploy_kwargs, _ = pop_global_arguments(kwargs)
|
|
94
76
|
|
|
95
|
-
# If this is a legacy operation function (ie - state & host arg kwargs), ensure that state
|
|
96
|
-
# and host are included as kwargs.
|
|
97
|
-
if func.is_legacy:
|
|
98
|
-
if "state" not in kwargs:
|
|
99
|
-
kwargs["state"] = context.state
|
|
100
|
-
if "host" not in kwargs:
|
|
101
|
-
kwargs["host"] = context.host
|
|
102
|
-
# If not legacy, pop off any state/host kwargs that may come from legacy @deploy functions
|
|
103
|
-
else:
|
|
104
|
-
kwargs.pop("state", None)
|
|
105
|
-
kwargs.pop("host", None)
|
|
106
|
-
|
|
107
|
-
# Name the deploy
|
|
108
|
-
deploy_name = getattr(func, "deploy_name", func.__name__)
|
|
109
77
|
deploy_data = getattr(func, "deploy_data", None)
|
|
110
78
|
|
|
111
79
|
with context.host.deploy(
|
|
112
|
-
name=deploy_name,
|
|
80
|
+
name=func.deploy_name, # type: ignore[attr-defined]
|
|
113
81
|
kwargs=deploy_kwargs,
|
|
114
82
|
data=deploy_data,
|
|
115
83
|
):
|
|
116
84
|
return func(*args, **kwargs)
|
|
117
85
|
|
|
118
|
-
|
|
86
|
+
decorated_func._inner = func # type: ignore[attr-defined]
|
|
87
|
+
return cast(PyinfraOperation[P], decorated_func)
|