pyinfra 3.0b0__py2.py3-none-any.whl → 3.0b2__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 +12 -5
- pyinfra/api/arguments_typed.py +19 -6
- pyinfra/api/command.py +5 -3
- pyinfra/api/config.py +115 -13
- pyinfra/api/connectors.py +5 -2
- pyinfra/api/exceptions.py +19 -0
- pyinfra/api/facts.py +34 -33
- pyinfra/api/host.py +51 -12
- pyinfra/api/inventory.py +4 -0
- pyinfra/api/operation.py +88 -42
- pyinfra/api/operations.py +10 -11
- pyinfra/api/state.py +11 -2
- pyinfra/api/util.py +24 -16
- pyinfra/connectors/base.py +4 -7
- pyinfra/connectors/chroot.py +5 -6
- pyinfra/connectors/docker.py +13 -19
- pyinfra/connectors/dockerssh.py +5 -4
- pyinfra/connectors/local.py +7 -7
- pyinfra/connectors/ssh.py +46 -25
- pyinfra/connectors/terraform.py +9 -6
- pyinfra/connectors/util.py +7 -8
- pyinfra/connectors/vagrant.py +11 -10
- pyinfra/context.py +1 -0
- 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 +3 -1
- pyinfra/facts/deb.py +9 -4
- pyinfra/facts/dnf.py +2 -0
- pyinfra/facts/docker.py +2 -0
- pyinfra/facts/files.py +2 -0
- pyinfra/facts/gem.py +2 -0
- pyinfra/facts/gpg.py +2 -0
- pyinfra/facts/hardware.py +30 -22
- pyinfra/facts/launchd.py +2 -0
- pyinfra/facts/lxd.py +2 -0
- pyinfra/facts/mysql.py +12 -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 +5 -162
- pyinfra/facts/rpm.py +12 -9
- pyinfra/facts/server.py +10 -13
- pyinfra/facts/snap.py +2 -0
- pyinfra/facts/systemd.py +28 -10
- pyinfra/facts/upstart.py +2 -0
- pyinfra/facts/util/packaging.py +3 -2
- 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/operations/apk.py +3 -1
- pyinfra/operations/apt.py +16 -18
- pyinfra/operations/brew.py +10 -8
- pyinfra/operations/bsdinit.py +5 -3
- pyinfra/operations/cargo.py +3 -1
- pyinfra/operations/choco.py +3 -1
- pyinfra/operations/dnf.py +15 -19
- pyinfra/operations/files.py +86 -69
- pyinfra/operations/gem.py +3 -1
- pyinfra/operations/git.py +18 -16
- pyinfra/operations/iptables.py +33 -25
- pyinfra/operations/launchd.py +5 -6
- pyinfra/operations/lxd.py +7 -4
- pyinfra/operations/mysql.py +57 -53
- pyinfra/operations/npm.py +8 -1
- pyinfra/operations/openrc.py +5 -3
- pyinfra/operations/pacman.py +4 -5
- pyinfra/operations/pip.py +16 -9
- pyinfra/operations/pkg.py +3 -1
- pyinfra/operations/pkgin.py +3 -1
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -335
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +7 -3
- pyinfra/operations/selinux.py +42 -16
- pyinfra/operations/server.py +48 -43
- pyinfra/operations/snap.py +3 -1
- pyinfra/operations/ssh.py +12 -10
- pyinfra/operations/systemd.py +13 -9
- pyinfra/operations/sysvinit.py +6 -4
- pyinfra/operations/upstart.py +5 -3
- pyinfra/operations/util/files.py +24 -16
- pyinfra/operations/util/packaging.py +53 -37
- pyinfra/operations/util/service.py +18 -13
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +3 -1
- pyinfra/operations/yum.py +14 -18
- pyinfra/operations/zypper.py +8 -9
- pyinfra/version.py +5 -2
- {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/METADATA +31 -29
- pyinfra-3.0b2.dist-info/RECORD +163 -0
- {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/WHEEL +1 -1
- pyinfra_cli/commands.py +3 -2
- pyinfra_cli/inventory.py +38 -19
- pyinfra_cli/main.py +2 -0
- pyinfra_cli/prints.py +27 -105
- pyinfra_cli/util.py +3 -1
- tests/test_api/test_api_deploys.py +5 -5
- tests/test_api/test_api_operations.py +5 -5
- tests/test_connectors/test_ssh.py +105 -0
- tests/test_connectors/test_terraform.py +11 -8
- tests/test_connectors/test_vagrant.py +6 -6
- pyinfra-3.0b0.dist-info/RECORD +0 -162
- pyinfra_cli/inventory_dsl.py +0 -23
- {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/top_level.txt +0 -0
pyinfra/api/operation.py
CHANGED
|
@@ -11,7 +11,7 @@ from functools import wraps
|
|
|
11
11
|
from inspect import signature
|
|
12
12
|
from io import StringIO
|
|
13
13
|
from types import FunctionType
|
|
14
|
-
from typing import Any, Callable, Generator, Iterator, Optional, cast
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Optional, cast
|
|
15
15
|
|
|
16
16
|
from typing_extensions import ParamSpec
|
|
17
17
|
|
|
@@ -36,16 +36,19 @@ from .util import (
|
|
|
36
36
|
|
|
37
37
|
op_meta_default = object()
|
|
38
38
|
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from pyinfra.connectors.util import CommandOutput
|
|
41
|
+
|
|
39
42
|
|
|
40
43
|
class OperationMeta:
|
|
41
44
|
_hash: str
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
_combined_output: Optional["CommandOutput"] = None
|
|
44
47
|
_commands: Optional[list[Any]] = None
|
|
45
|
-
_maybe_is_change: bool =
|
|
48
|
+
_maybe_is_change: Optional[bool] = None
|
|
46
49
|
_success: Optional[bool] = None
|
|
47
50
|
|
|
48
|
-
def __init__(self, hash, is_change
|
|
51
|
+
def __init__(self, hash, is_change: Optional[bool]):
|
|
49
52
|
self._hash = hash
|
|
50
53
|
self._maybe_is_change = is_change
|
|
51
54
|
|
|
@@ -57,7 +60,7 @@ class OperationMeta:
|
|
|
57
60
|
if self._commands is not None:
|
|
58
61
|
return (
|
|
59
62
|
"OperationMeta(executed=True, "
|
|
60
|
-
f"
|
|
63
|
+
f"success={self.did_succeed}, hash={self._hash}, commands={len(self._commands)})"
|
|
61
64
|
)
|
|
62
65
|
return (
|
|
63
66
|
"OperationMeta(executed=False, "
|
|
@@ -69,13 +72,13 @@ class OperationMeta:
|
|
|
69
72
|
self,
|
|
70
73
|
success: bool,
|
|
71
74
|
commands: list[Any],
|
|
72
|
-
|
|
75
|
+
combined_output: "CommandOutput",
|
|
73
76
|
) -> None:
|
|
74
77
|
if self.is_complete():
|
|
75
78
|
raise RuntimeError("Cannot complete an already complete operation")
|
|
76
79
|
self._success = success
|
|
77
80
|
self._commands = commands
|
|
78
|
-
self.
|
|
81
|
+
self._combined_output = combined_output
|
|
79
82
|
|
|
80
83
|
def is_complete(self) -> bool:
|
|
81
84
|
return self._success is not None
|
|
@@ -84,31 +87,57 @@ class OperationMeta:
|
|
|
84
87
|
if not self.is_complete():
|
|
85
88
|
raise RuntimeError("Cannot evaluate operation result before execution")
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
@property
|
|
91
|
+
def will_change(self) -> bool:
|
|
92
|
+
if self._maybe_is_change is not None:
|
|
93
|
+
return self._maybe_is_change
|
|
94
|
+
|
|
95
|
+
op_data = context.state.get_op_data_for_host(context.host, self._hash)
|
|
96
|
+
cmd_gen = op_data.command_generator
|
|
97
|
+
for _ in cmd_gen():
|
|
98
|
+
self._maybe_is_change = True
|
|
99
|
+
return True
|
|
100
|
+
self._maybe_is_change = False
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def _did_change(self) -> bool:
|
|
89
104
|
return bool(self._success and len(self._commands or []) > 0)
|
|
90
105
|
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
@property
|
|
107
|
+
def did_change(self):
|
|
108
|
+
return context.host.when(self._did_change)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def did_not_change(self):
|
|
112
|
+
return context.host.when(lambda: not self._did_change())
|
|
113
|
+
|
|
114
|
+
def did_succeed(self, _raise_if_not_complete=True) -> bool:
|
|
115
|
+
if _raise_if_not_complete:
|
|
116
|
+
self._raise_if_not_complete()
|
|
93
117
|
return self._success is True
|
|
94
118
|
|
|
95
119
|
def did_error(self) -> bool:
|
|
96
120
|
self._raise_if_not_complete()
|
|
97
121
|
return self._success is False
|
|
98
122
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
# TODO: deprecated, remove in v4
|
|
124
|
+
@property
|
|
125
|
+
def changed(self) -> bool:
|
|
126
|
+
if self.is_complete():
|
|
127
|
+
return self._did_change()
|
|
128
|
+
return self.will_change
|
|
104
129
|
|
|
105
130
|
@property
|
|
106
|
-
def stdout_lines(self):
|
|
107
|
-
|
|
131
|
+
def stdout_lines(self) -> list[str]:
|
|
132
|
+
self._raise_if_not_complete()
|
|
133
|
+
assert self._combined_output is not None
|
|
134
|
+
return self._combined_output.stdout_lines
|
|
108
135
|
|
|
109
136
|
@property
|
|
110
|
-
def stderr_lines(self):
|
|
111
|
-
|
|
137
|
+
def stderr_lines(self) -> list[str]:
|
|
138
|
+
self._raise_if_not_complete()
|
|
139
|
+
assert self._combined_output is not None
|
|
140
|
+
return self._combined_output.stderr_lines
|
|
112
141
|
|
|
113
142
|
@property
|
|
114
143
|
def stdout(self) -> str:
|
|
@@ -118,14 +147,6 @@ class OperationMeta:
|
|
|
118
147
|
def stderr(self) -> str:
|
|
119
148
|
return "\n".join(self.stderr_lines)
|
|
120
149
|
|
|
121
|
-
# TODO: deprecated, remove in v4
|
|
122
|
-
@property
|
|
123
|
-
def changed(self) -> int:
|
|
124
|
-
if not self.is_complete():
|
|
125
|
-
logger.warning("Checking changed before execution can give unexpected results")
|
|
126
|
-
return self._maybe_is_change
|
|
127
|
-
return self.did_change()
|
|
128
|
-
|
|
129
150
|
|
|
130
151
|
def add_op(state: State, op_func, *args, **kwargs):
|
|
131
152
|
"""
|
|
@@ -164,6 +185,8 @@ P = ParamSpec("P")
|
|
|
164
185
|
def operation(
|
|
165
186
|
is_idempotent: bool = True,
|
|
166
187
|
idempotent_notice: Optional[str] = None,
|
|
188
|
+
is_deprecated: bool = False,
|
|
189
|
+
deprecated_for: Optional[str] = None,
|
|
167
190
|
_set_in_op: bool = True,
|
|
168
191
|
) -> Callable[[Callable[P, Generator]], PyinfraOperation[P]]:
|
|
169
192
|
"""
|
|
@@ -175,6 +198,8 @@ def operation(
|
|
|
175
198
|
def decorator(f: Callable[P, Generator]) -> PyinfraOperation[P]:
|
|
176
199
|
f.is_idempotent = is_idempotent # type: ignore[attr-defined]
|
|
177
200
|
f.idempotent_notice = idempotent_notice # type: ignore[attr-defined]
|
|
201
|
+
f.is_deprecated = is_deprecated # type: ignore[attr-defined]
|
|
202
|
+
f.deprecated_for = deprecated_for # type: ignore[attr-defined]
|
|
178
203
|
return _wrap_operation(f, _set_in_op=_set_in_op)
|
|
179
204
|
|
|
180
205
|
return decorator
|
|
@@ -192,6 +217,15 @@ def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> Py
|
|
|
192
217
|
+ "function to call the underlying operation."
|
|
193
218
|
)
|
|
194
219
|
|
|
220
|
+
if func.is_deprecated: # type: ignore[attr-defined]
|
|
221
|
+
if func.deprecated_for: # type: ignore[attr-defined]
|
|
222
|
+
logger.warning(
|
|
223
|
+
f"The {get_operation_name_from_func(func)} operation is "
|
|
224
|
+
+ f"deprecated, please use: {func.deprecated_for}", # type: ignore[attr-defined] # noqa
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
logger.warning(f"The {get_operation_name_from_func(func)} operation is deprecated")
|
|
228
|
+
|
|
195
229
|
# Configure operation
|
|
196
230
|
#
|
|
197
231
|
# Get the meta kwargs (globals that apply to all hosts)
|
|
@@ -217,22 +251,30 @@ def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> Py
|
|
|
217
251
|
break
|
|
218
252
|
|
|
219
253
|
if has_run:
|
|
220
|
-
return OperationMeta(op_hash)
|
|
254
|
+
return OperationMeta(op_hash, is_change=False)
|
|
255
|
+
|
|
256
|
+
# Grab a reference to any *current* deploy data as this may change when
|
|
257
|
+
# we later evaluate the operation at runtime.This means we put back the
|
|
258
|
+
# expected deploy data.
|
|
259
|
+
current_deploy_data = host.current_deploy_data
|
|
221
260
|
|
|
222
261
|
# "Run" operation - here we make a generator that will yield out actual commands to execute
|
|
223
262
|
# and, if we're diff-ing, we then iterate the generator now to determine if any changes
|
|
224
263
|
# *would* be made based on the *current* remote state.
|
|
225
264
|
|
|
226
265
|
def command_generator() -> Iterator[PyinfraCommand]:
|
|
227
|
-
# Check global
|
|
266
|
+
# Check global _if argument function and do nothing if returns False
|
|
228
267
|
if state.is_executing:
|
|
229
|
-
|
|
230
|
-
if
|
|
268
|
+
_ifs = global_arguments.get("_if")
|
|
269
|
+
if isinstance(_ifs, list) and not all(_if() for _if in _ifs):
|
|
270
|
+
return
|
|
271
|
+
elif callable(_ifs) and not _ifs():
|
|
231
272
|
return
|
|
232
273
|
|
|
233
274
|
host.in_op = _set_in_op
|
|
234
275
|
host.current_op_hash = op_hash
|
|
235
276
|
host.current_op_global_arguments = global_arguments
|
|
277
|
+
host.current_op_deploy_data = current_deploy_data
|
|
236
278
|
|
|
237
279
|
try:
|
|
238
280
|
for command in func(*args, **kwargs):
|
|
@@ -243,10 +285,12 @@ def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> Py
|
|
|
243
285
|
host.in_op = False
|
|
244
286
|
host.current_op_hash = None
|
|
245
287
|
host.current_op_global_arguments = None
|
|
288
|
+
host.current_op_deploy_data = None
|
|
246
289
|
|
|
247
|
-
op_is_change =
|
|
290
|
+
op_is_change = None
|
|
248
291
|
if state.should_check_for_changes():
|
|
249
|
-
|
|
292
|
+
op_is_change = False
|
|
293
|
+
for _ in command_generator():
|
|
250
294
|
op_is_change = True
|
|
251
295
|
break
|
|
252
296
|
else:
|
|
@@ -279,6 +323,15 @@ def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> Py
|
|
|
279
323
|
return cast(PyinfraOperation[P], decorated_func)
|
|
280
324
|
|
|
281
325
|
|
|
326
|
+
def get_operation_name_from_func(func):
|
|
327
|
+
if func.__module__:
|
|
328
|
+
module_bits = func.__module__.split(".")
|
|
329
|
+
module_name = module_bits[-1]
|
|
330
|
+
return "{0}.{1}".format(module_name, func.__name__)
|
|
331
|
+
else:
|
|
332
|
+
return func.__name__
|
|
333
|
+
|
|
334
|
+
|
|
282
335
|
def generate_operation_name(func, host, kwargs, global_arguments):
|
|
283
336
|
# Generate an operation name if needed (Module/Operation format)
|
|
284
337
|
name = global_arguments.get("name")
|
|
@@ -287,14 +340,7 @@ def generate_operation_name(func, host, kwargs, global_arguments):
|
|
|
287
340
|
names = {name}
|
|
288
341
|
else:
|
|
289
342
|
add_args = True
|
|
290
|
-
|
|
291
|
-
if func.__module__:
|
|
292
|
-
module_bits = func.__module__.split(".")
|
|
293
|
-
module_name = module_bits[-1]
|
|
294
|
-
name = "{0}/{1}".format(module_name.title(), func.__name__.title())
|
|
295
|
-
else:
|
|
296
|
-
name = func.__name__
|
|
297
|
-
|
|
343
|
+
name = get_operation_name_from_func(func)
|
|
298
344
|
names = {name}
|
|
299
345
|
|
|
300
346
|
if host.current_deploy_name:
|
pyinfra/api/operations.py
CHANGED
|
@@ -67,17 +67,16 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
|
|
|
67
67
|
timeout = global_arguments.get("_timeout", 0)
|
|
68
68
|
|
|
69
69
|
executor_kwarg_keys = CONNECTOR_ARGUMENT_KEYS
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
70
|
+
# See: https://github.com/python/mypy/issues/10371
|
|
71
|
+
base_connector_arguments: ConnectorArguments = cast(
|
|
72
|
+
ConnectorArguments,
|
|
73
|
+
{key: global_arguments[key] for key in executor_kwarg_keys if key in global_arguments}, # type: ignore[literal-required] # noqa
|
|
75
74
|
)
|
|
76
75
|
|
|
77
76
|
did_error = False
|
|
78
77
|
executed_commands = 0
|
|
79
78
|
commands = []
|
|
80
|
-
|
|
79
|
+
all_output_lines: list[OutputLine] = []
|
|
81
80
|
|
|
82
81
|
for command in op_data.command_generator():
|
|
83
82
|
commands.append(command)
|
|
@@ -103,19 +102,19 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
|
|
|
103
102
|
)
|
|
104
103
|
|
|
105
104
|
elif isinstance(command, StringCommand):
|
|
106
|
-
|
|
105
|
+
output_lines = CommandOutput([])
|
|
107
106
|
try:
|
|
108
|
-
status,
|
|
107
|
+
status, output_lines = command.execute(
|
|
109
108
|
state,
|
|
110
109
|
host,
|
|
111
110
|
connector_arguments,
|
|
112
111
|
)
|
|
113
112
|
except (timeout_error, socket_error, SSHException) as e:
|
|
114
113
|
log_host_command_error(host, e, timeout=timeout)
|
|
115
|
-
|
|
114
|
+
all_output_lines.extend(output_lines)
|
|
116
115
|
# If we failed and have not already printed the stderr, print it
|
|
117
116
|
if status is False and not state.print_output:
|
|
118
|
-
print_host_combined_output(host,
|
|
117
|
+
print_host_combined_output(host, output_lines)
|
|
119
118
|
|
|
120
119
|
else:
|
|
121
120
|
try:
|
|
@@ -170,7 +169,7 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
|
|
|
170
169
|
op_data.operation_meta.set_complete(
|
|
171
170
|
op_success,
|
|
172
171
|
commands,
|
|
173
|
-
|
|
172
|
+
CommandOutput(all_output_lines),
|
|
174
173
|
)
|
|
175
174
|
|
|
176
175
|
return return_status
|
pyinfra/api/state.py
CHANGED
|
@@ -344,10 +344,19 @@ class State:
|
|
|
344
344
|
def get_results_for_host(self, host: "Host") -> StateHostResults:
|
|
345
345
|
return self.results[host]
|
|
346
346
|
|
|
347
|
-
def get_op_data_for_host(
|
|
347
|
+
def get_op_data_for_host(
|
|
348
|
+
self,
|
|
349
|
+
host: "Host",
|
|
350
|
+
op_hash: str,
|
|
351
|
+
) -> StateOperationHostData:
|
|
348
352
|
return self.ops[host][op_hash]
|
|
349
353
|
|
|
350
|
-
def set_op_data_for_host(
|
|
354
|
+
def set_op_data_for_host(
|
|
355
|
+
self,
|
|
356
|
+
host: "Host",
|
|
357
|
+
op_hash: str,
|
|
358
|
+
op_data: StateOperationHostData,
|
|
359
|
+
):
|
|
351
360
|
self.ops[host][op_hash] = op_data
|
|
352
361
|
|
|
353
362
|
def activate_host(self, host: "Host"):
|
pyinfra/api/util.py
CHANGED
|
@@ -109,16 +109,16 @@ def get_caller_frameinfo(frame_offset: int = 0):
|
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
def get_operation_order_from_stack(state: "State"):
|
|
112
|
+
|
|
112
113
|
stack_items = list(reversed(stack()))
|
|
113
114
|
|
|
115
|
+
i = 0
|
|
114
116
|
# Find the *first* occurrence of our deploy file in the reversed stack
|
|
115
117
|
if state.current_deploy_filename:
|
|
116
118
|
for i, stack_item in enumerate(stack_items):
|
|
117
119
|
frame = getframeinfo(stack_item[0])
|
|
118
120
|
if frame.filename == state.current_deploy_filename:
|
|
119
121
|
break
|
|
120
|
-
else:
|
|
121
|
-
i = 0
|
|
122
122
|
|
|
123
123
|
# Now generate a list of line numbers *following that file*
|
|
124
124
|
line_numbers = []
|
|
@@ -139,7 +139,7 @@ def get_operation_order_from_stack(state: "State"):
|
|
|
139
139
|
return line_numbers
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
def get_template(filename_or_io: str):
|
|
142
|
+
def get_template(filename_or_io: str | IO):
|
|
143
143
|
"""
|
|
144
144
|
Gets a jinja2 ``Template`` object for the input filename or string, with caching
|
|
145
145
|
based on the filename of the template, or the SHA1 of the input string.
|
|
@@ -301,19 +301,27 @@ def make_hash(obj):
|
|
|
301
301
|
if isinstance(obj, int)
|
|
302
302
|
# Constants - the values can change between hosts but we should still
|
|
303
303
|
# group them under the same operation hash.
|
|
304
|
-
else
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
304
|
+
else (
|
|
305
|
+
"_PYINFRA_CONSTANT"
|
|
306
|
+
if obj in (True, False, None)
|
|
307
|
+
# Plain strings
|
|
308
|
+
else (
|
|
309
|
+
obj
|
|
310
|
+
if isinstance(obj, str)
|
|
311
|
+
# Objects with __name__s
|
|
312
|
+
else (
|
|
313
|
+
obj.__name__
|
|
314
|
+
if hasattr(obj, "__name__")
|
|
315
|
+
# Objects with names
|
|
316
|
+
else (
|
|
317
|
+
obj.name
|
|
318
|
+
if hasattr(obj, "name")
|
|
319
|
+
# Repr anything else
|
|
320
|
+
else repr(obj)
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
)
|
|
317
325
|
)
|
|
318
326
|
|
|
319
327
|
return sha1_hash(hash_string)
|
pyinfra/connectors/base.py
CHANGED
|
@@ -84,7 +84,7 @@ class BaseConnector(abc.ABC):
|
|
|
84
84
|
|
|
85
85
|
@staticmethod
|
|
86
86
|
@abc.abstractmethod
|
|
87
|
-
def make_names_data(
|
|
87
|
+
def make_names_data(name: str) -> Iterator[tuple[str, dict, list[str]]]:
|
|
88
88
|
"""
|
|
89
89
|
Generates hosts/data/groups information for inventory. This allows a
|
|
90
90
|
single connector reference to generate multiple target hosts.
|
|
@@ -108,8 +108,7 @@ class BaseConnector(abc.ABC):
|
|
|
108
108
|
print_output: bool,
|
|
109
109
|
print_input: bool,
|
|
110
110
|
**arguments: Unpack["ConnectorArguments"],
|
|
111
|
-
) -> tuple[bool, "CommandOutput"]:
|
|
112
|
-
...
|
|
111
|
+
) -> tuple[bool, "CommandOutput"]: ...
|
|
113
112
|
|
|
114
113
|
@abc.abstractmethod
|
|
115
114
|
def put_file(
|
|
@@ -120,8 +119,7 @@ class BaseConnector(abc.ABC):
|
|
|
120
119
|
print_output: bool = False,
|
|
121
120
|
print_input: bool = False,
|
|
122
121
|
**arguments: Unpack["ConnectorArguments"],
|
|
123
|
-
) -> bool:
|
|
124
|
-
...
|
|
122
|
+
) -> bool: ...
|
|
125
123
|
|
|
126
124
|
@abc.abstractmethod
|
|
127
125
|
def get_file(
|
|
@@ -132,8 +130,7 @@ class BaseConnector(abc.ABC):
|
|
|
132
130
|
print_output: bool = False,
|
|
133
131
|
print_input: bool = False,
|
|
134
132
|
**arguments: Unpack["ConnectorArguments"],
|
|
135
|
-
) -> bool:
|
|
136
|
-
...
|
|
133
|
+
) -> bool: ...
|
|
137
134
|
|
|
138
135
|
def check_can_rsync(self):
|
|
139
136
|
raise NotImplementedError("This connector does not support rsync")
|
pyinfra/connectors/chroot.py
CHANGED
|
@@ -40,17 +40,17 @@ class ChrootConnector(BaseConnector):
|
|
|
40
40
|
self.local = LocalConnector(state, host)
|
|
41
41
|
|
|
42
42
|
@staticmethod
|
|
43
|
-
def make_names_data(
|
|
44
|
-
if not
|
|
43
|
+
def make_names_data(name: Optional[str] = None):
|
|
44
|
+
if not name:
|
|
45
45
|
raise InventoryError("No directory provided!")
|
|
46
46
|
|
|
47
47
|
show_warning()
|
|
48
48
|
|
|
49
|
-
yield "@chroot/{0}".format(
|
|
50
|
-
"chroot_directory": "/{0}".format(
|
|
49
|
+
yield "@chroot/{0}".format(name), {
|
|
50
|
+
"chroot_directory": "/{0}".format(name.lstrip("/")),
|
|
51
51
|
}, ["@chroot"]
|
|
52
52
|
|
|
53
|
-
def connect(self):
|
|
53
|
+
def connect(self) -> None:
|
|
54
54
|
self.local.connect()
|
|
55
55
|
|
|
56
56
|
chroot_directory = self.host.data.chroot_directory
|
|
@@ -65,7 +65,6 @@ class ChrootConnector(BaseConnector):
|
|
|
65
65
|
raise ConnectError(e.args[0])
|
|
66
66
|
|
|
67
67
|
self.host.connector_data["chroot_directory"] = chroot_directory
|
|
68
|
-
return True
|
|
69
68
|
|
|
70
69
|
def run_shell_command(
|
|
71
70
|
self,
|
pyinfra/connectors/docker.py
CHANGED
|
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
|
|
24
24
|
from pyinfra.api.state import State
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class ConnectorData(TypedDict
|
|
27
|
+
class ConnectorData(TypedDict):
|
|
28
28
|
docker_identifier: str
|
|
29
29
|
|
|
30
30
|
|
|
@@ -35,6 +35,7 @@ connector_data_meta: dict[str, DataMeta] = {
|
|
|
35
35
|
|
|
36
36
|
def _find_start_docker_container(container_id) -> tuple[str, bool]:
|
|
37
37
|
docker_info = local.shell("docker container inspect {0}".format(container_id))
|
|
38
|
+
assert isinstance(docker_info, str)
|
|
38
39
|
docker_info = json.loads(docker_info)[0]
|
|
39
40
|
if docker_info["State"]["Running"] is False:
|
|
40
41
|
logger.info("Starting stopped container: {0}".format(container_id))
|
|
@@ -60,8 +61,10 @@ class DockerConnector(BaseConnector):
|
|
|
60
61
|
The docker connector allows you to build Docker images or modify running
|
|
61
62
|
Docker containers. You can pass either an image name or existing container ID:
|
|
62
63
|
|
|
63
|
-
+ Image - will create a new container from the image, execute operations
|
|
64
|
-
|
|
64
|
+
+ Image - will create a new container from the image, execute operations \
|
|
65
|
+
against it, save into a new Docker image and remove the container
|
|
66
|
+
+ Existing container ID - will execute operations against the running \
|
|
67
|
+
container, leaving it running
|
|
65
68
|
|
|
66
69
|
.. code:: shell
|
|
67
70
|
|
|
@@ -91,17 +94,17 @@ class DockerConnector(BaseConnector):
|
|
|
91
94
|
self.local = LocalConnector(state, host)
|
|
92
95
|
|
|
93
96
|
@staticmethod
|
|
94
|
-
def make_names_data(
|
|
95
|
-
if not
|
|
97
|
+
def make_names_data(name=None):
|
|
98
|
+
if not name:
|
|
96
99
|
raise InventoryError("No docker base ID provided!")
|
|
97
100
|
|
|
98
101
|
yield (
|
|
99
|
-
"@docker/{0}".format(
|
|
100
|
-
{"docker_identifier":
|
|
102
|
+
"@docker/{0}".format(name),
|
|
103
|
+
{"docker_identifier": name},
|
|
101
104
|
["@docker"],
|
|
102
105
|
)
|
|
103
106
|
|
|
104
|
-
def connect(self):
|
|
107
|
+
def connect(self) -> None:
|
|
105
108
|
self.local.connect()
|
|
106
109
|
|
|
107
110
|
docker_identifier = self.data["docker_identifier"]
|
|
@@ -113,8 +116,6 @@ class DockerConnector(BaseConnector):
|
|
|
113
116
|
except PyinfraError:
|
|
114
117
|
self.container_id = _start_docker_image(docker_identifier)
|
|
115
118
|
|
|
116
|
-
return True
|
|
117
|
-
|
|
118
119
|
def disconnect(self):
|
|
119
120
|
container_id = self.container_id
|
|
120
121
|
|
|
@@ -264,17 +265,10 @@ class DockerConnector(BaseConnector):
|
|
|
264
265
|
)
|
|
265
266
|
|
|
266
267
|
# Load the temporary file and write it to our file or IO object
|
|
267
|
-
with open(temp_filename,
|
|
268
|
+
with open(temp_filename, "rb") as temp_f:
|
|
268
269
|
with get_file_io(filename_or_io, "wb") as file_io:
|
|
269
270
|
data = temp_f.read()
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if isinstance(data, str):
|
|
273
|
-
data_bytes = data.encode()
|
|
274
|
-
else:
|
|
275
|
-
data_bytes = data
|
|
276
|
-
|
|
277
|
-
file_io.write(data_bytes)
|
|
271
|
+
file_io.write(data)
|
|
278
272
|
finally:
|
|
279
273
|
os.close(fd)
|
|
280
274
|
os.remove(temp_filename)
|
pyinfra/connectors/dockerssh.py
CHANGED
|
@@ -30,7 +30,8 @@ class DockerSSHConnector(BaseConnector):
|
|
|
30
30
|
"""
|
|
31
31
|
**Note**: this connector is in beta!
|
|
32
32
|
|
|
33
|
-
The ``@dockerssh`` connector allows you to run commands on Docker containers
|
|
33
|
+
The ``@dockerssh`` connector allows you to run commands on Docker containers \
|
|
34
|
+
on a remote machine.
|
|
34
35
|
|
|
35
36
|
.. code:: shell
|
|
36
37
|
|
|
@@ -50,10 +51,10 @@ class DockerSSHConnector(BaseConnector):
|
|
|
50
51
|
self.ssh = SSHConnector(state, host)
|
|
51
52
|
|
|
52
53
|
@staticmethod
|
|
53
|
-
def make_names_data(
|
|
54
|
+
def make_names_data(name):
|
|
54
55
|
try:
|
|
55
|
-
hostname, image =
|
|
56
|
-
except (AttributeError, ValueError): # failure to parse the
|
|
56
|
+
hostname, image = name.split(":", 1)
|
|
57
|
+
except (AttributeError, ValueError): # failure to parse the name
|
|
57
58
|
raise InventoryError("No ssh host or docker base image provided!")
|
|
58
59
|
|
|
59
60
|
if not image:
|
pyinfra/connectors/local.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from
|
|
2
|
+
from shutil import which
|
|
3
3
|
from tempfile import mkstemp
|
|
4
4
|
from typing import TYPE_CHECKING, Tuple
|
|
5
5
|
|
|
@@ -25,7 +25,8 @@ if TYPE_CHECKING:
|
|
|
25
25
|
|
|
26
26
|
class LocalConnector(BaseConnector):
|
|
27
27
|
"""
|
|
28
|
-
The ``@local`` connector executes changes on the local machine using
|
|
28
|
+
The ``@local`` connector executes changes on the local machine using
|
|
29
|
+
subprocesses. **This connector is only compatible with MacOS & Linux hosts**.
|
|
29
30
|
|
|
30
31
|
Examples:
|
|
31
32
|
|
|
@@ -38,8 +39,8 @@ class LocalConnector(BaseConnector):
|
|
|
38
39
|
handles_execution = True
|
|
39
40
|
|
|
40
41
|
@staticmethod
|
|
41
|
-
def make_names_data(
|
|
42
|
-
if
|
|
42
|
+
def make_names_data(name=None):
|
|
43
|
+
if name is not None:
|
|
43
44
|
raise InventoryError("Cannot have more than one @local")
|
|
44
45
|
|
|
45
46
|
yield "@local", {}, ["@local"]
|
|
@@ -205,9 +206,8 @@ class LocalConnector(BaseConnector):
|
|
|
205
206
|
|
|
206
207
|
return True
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if not find_executable("rsync"):
|
|
209
|
+
def check_can_rsync(self):
|
|
210
|
+
if not which("rsync"):
|
|
211
211
|
raise NotImplementedError("The `rsync` binary is not available on this system.")
|
|
212
212
|
|
|
213
213
|
def rsync(
|