pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyinfra/api/__init__.py +3 -0
- pyinfra/api/arguments.py +115 -97
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +5 -3
- pyinfra/api/config.py +139 -39
- pyinfra/api/connectors.py +5 -2
- pyinfra/api/deploy.py +19 -19
- pyinfra/api/exceptions.py +35 -4
- pyinfra/api/facts.py +62 -86
- pyinfra/api/host.py +102 -15
- pyinfra/api/inventory.py +4 -0
- pyinfra/api/operation.py +188 -120
- pyinfra/api/operations.py +66 -113
- pyinfra/api/state.py +53 -34
- pyinfra/api/util.py +64 -33
- pyinfra/connectors/base.py +65 -20
- pyinfra/connectors/chroot.py +15 -13
- pyinfra/connectors/docker.py +62 -72
- pyinfra/connectors/dockerssh.py +20 -19
- pyinfra/connectors/local.py +32 -22
- pyinfra/connectors/ssh.py +162 -86
- pyinfra/connectors/sshuserclient/client.py +1 -1
- pyinfra/connectors/terraform.py +57 -39
- pyinfra/connectors/util.py +26 -27
- pyinfra/connectors/vagrant.py +27 -26
- pyinfra/context.py +1 -0
- pyinfra/facts/apk.py +7 -2
- pyinfra/facts/apt.py +15 -7
- pyinfra/facts/brew.py +28 -13
- pyinfra/facts/bsdinit.py +9 -6
- pyinfra/facts/cargo.py +6 -3
- pyinfra/facts/choco.py +8 -4
- pyinfra/facts/deb.py +21 -9
- pyinfra/facts/dnf.py +11 -6
- pyinfra/facts/docker.py +30 -5
- pyinfra/facts/files.py +49 -33
- pyinfra/facts/gem.py +7 -2
- pyinfra/facts/git.py +14 -21
- pyinfra/facts/gpg.py +4 -1
- pyinfra/facts/hardware.py +186 -138
- pyinfra/facts/launchd.py +7 -2
- pyinfra/facts/lxd.py +8 -2
- pyinfra/facts/mysql.py +19 -12
- pyinfra/facts/npm.py +3 -1
- pyinfra/facts/openrc.py +8 -2
- pyinfra/facts/pacman.py +13 -5
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +5 -1
- pyinfra/facts/pkgin.py +7 -2
- pyinfra/facts/postgres.py +170 -0
- pyinfra/facts/postgresql.py +5 -162
- pyinfra/facts/rpm.py +21 -15
- pyinfra/facts/runit.py +70 -0
- pyinfra/facts/selinux.py +12 -4
- pyinfra/facts/server.py +240 -82
- pyinfra/facts/snap.py +8 -2
- pyinfra/facts/systemd.py +37 -13
- pyinfra/facts/sysvinit.py +7 -4
- pyinfra/facts/upstart.py +7 -2
- pyinfra/facts/util/packaging.py +3 -2
- pyinfra/facts/vzctl.py +8 -4
- pyinfra/facts/xbps.py +7 -2
- pyinfra/facts/yum.py +10 -5
- pyinfra/facts/zypper.py +9 -4
- pyinfra/operations/apk.py +5 -3
- pyinfra/operations/apt.py +28 -25
- pyinfra/operations/brew.py +60 -29
- pyinfra/operations/bsdinit.py +6 -4
- pyinfra/operations/cargo.py +3 -1
- pyinfra/operations/choco.py +3 -1
- pyinfra/operations/dnf.py +16 -20
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +187 -168
- pyinfra/operations/gem.py +3 -1
- pyinfra/operations/git.py +23 -25
- pyinfra/operations/iptables.py +33 -25
- pyinfra/operations/launchd.py +5 -6
- pyinfra/operations/lxd.py +7 -4
- pyinfra/operations/mysql.py +59 -55
- pyinfra/operations/npm.py +8 -1
- pyinfra/operations/openrc.py +5 -3
- pyinfra/operations/pacman.py +6 -7
- pyinfra/operations/pip.py +19 -12
- pyinfra/operations/pkg.py +3 -1
- pyinfra/operations/pkgin.py +5 -3
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -335
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -29
- pyinfra/operations/server.py +138 -67
- pyinfra/operations/snap.py +3 -1
- pyinfra/operations/ssh.py +18 -16
- pyinfra/operations/systemd.py +18 -12
- pyinfra/operations/sysvinit.py +7 -5
- pyinfra/operations/upstart.py +7 -5
- 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 +54 -38
- pyinfra/operations/util/service.py +39 -47
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +5 -3
- pyinfra/operations/yum.py +15 -19
- pyinfra/operations/zypper.py +9 -10
- pyinfra/version.py +5 -2
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/METADATA +51 -58
- pyinfra-3.0.2.dist-info/RECORD +168 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/WHEEL +1 -1
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/entry_points.txt +0 -3
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +3 -2
- pyinfra_cli/exceptions.py +75 -43
- pyinfra_cli/inventory.py +52 -31
- pyinfra_cli/log.py +10 -2
- pyinfra_cli/main.py +88 -65
- pyinfra_cli/prints.py +37 -109
- pyinfra_cli/util.py +15 -10
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +9 -9
- tests/test_api/test_api_deploys.py +15 -19
- tests/test_api/test_api_facts.py +4 -5
- tests/test_api/test_api_operations.py +18 -20
- tests/test_api/test_api_util.py +41 -2
- tests/test_cli/test_cli.py +14 -50
- tests/test_cli/test_cli_deploy.py +17 -14
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/test_cli_inventory.py +66 -0
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_dockerssh.py +11 -8
- tests/test_connectors/test_ssh.py +88 -23
- tests/test_connectors/test_sshuserclient.py +1 -1
- tests/test_connectors/test_terraform.py +11 -8
- tests/test_connectors/test_vagrant.py +6 -6
- pyinfra/connectors/ansible.py +0 -175
- pyinfra/connectors/mech.py +0 -189
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -312
- 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 -538
- pyinfra-3.0.dev0.dist-info/RECORD +0 -170
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/top_level.txt +0 -0
pyinfra/api/host.py
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import (
|
|
5
|
+
TYPE_CHECKING,
|
|
6
|
+
Any,
|
|
7
|
+
Callable,
|
|
8
|
+
Generator,
|
|
9
|
+
Optional,
|
|
10
|
+
Type,
|
|
11
|
+
TypeVar,
|
|
12
|
+
Union,
|
|
13
|
+
cast,
|
|
14
|
+
overload,
|
|
15
|
+
)
|
|
16
|
+
from uuid import uuid4
|
|
5
17
|
|
|
6
18
|
import click
|
|
19
|
+
from typing_extensions import Unpack
|
|
7
20
|
|
|
8
21
|
from pyinfra import logger
|
|
9
22
|
from pyinfra.connectors.base import BaseConnector
|
|
@@ -11,7 +24,8 @@ from pyinfra.connectors.util import CommandOutput, remove_any_sudo_askpass_file
|
|
|
11
24
|
|
|
12
25
|
from .connectors import get_execution_connector
|
|
13
26
|
from .exceptions import ConnectError
|
|
14
|
-
from .facts import get_host_fact
|
|
27
|
+
from .facts import FactBase, ShortFactBase, get_host_fact
|
|
28
|
+
from .util import memoize, sha1_hash
|
|
15
29
|
|
|
16
30
|
if TYPE_CHECKING:
|
|
17
31
|
from pyinfra.api.arguments import AllArguments
|
|
@@ -19,7 +33,9 @@ if TYPE_CHECKING:
|
|
|
19
33
|
from pyinfra.api.state import State
|
|
20
34
|
|
|
21
35
|
|
|
22
|
-
def extract_callable_datas(
|
|
36
|
+
def extract_callable_datas(
|
|
37
|
+
datas: list[Union[Callable[..., Any], Any]],
|
|
38
|
+
) -> Generator[Any, Any, Any]:
|
|
23
39
|
for data in datas:
|
|
24
40
|
# Support for dynamic data, ie @deploy wrapped data defaults where
|
|
25
41
|
# the data is stored on the state temporarily.
|
|
@@ -94,17 +110,22 @@ class Host:
|
|
|
94
110
|
in_op: bool = False
|
|
95
111
|
in_callback_op: bool = False
|
|
96
112
|
current_op_hash: Optional[str] = None
|
|
97
|
-
current_op_global_arguments: Optional["AllArguments"]
|
|
113
|
+
current_op_global_arguments: Optional["AllArguments"] = None
|
|
98
114
|
|
|
99
|
-
# Current context inside a @deploy function
|
|
115
|
+
# Current context inside a @deploy function which become part of the op data
|
|
100
116
|
in_deploy: bool = False
|
|
101
117
|
current_deploy_name: Optional[str] = None
|
|
102
118
|
current_deploy_kwargs = None
|
|
103
|
-
|
|
119
|
+
|
|
120
|
+
# @deploy decorator data is a bit different - we need to handle the case
|
|
121
|
+
# where we're evaluating an operation at runtime (current_op_) but also
|
|
122
|
+
# when ordering operations (current_) outside of an operation context.
|
|
123
|
+
current_op_deploy_data: Optional[dict[str, Any]] = None
|
|
124
|
+
current_deploy_data: Optional[dict[str, Any]] = None
|
|
104
125
|
|
|
105
126
|
# Current context during operation execution
|
|
106
|
-
executing_op_hash = None
|
|
107
|
-
nested_executing_op_hash = None
|
|
127
|
+
executing_op_hash: Optional[str] = None
|
|
128
|
+
nested_executing_op_hash: Optional[str] = None
|
|
108
129
|
|
|
109
130
|
loop_position: list[int]
|
|
110
131
|
|
|
@@ -133,7 +154,6 @@ class Host:
|
|
|
133
154
|
self.loop_position = []
|
|
134
155
|
|
|
135
156
|
self.connector_data = {}
|
|
136
|
-
self.current_op_global_arguments = {}
|
|
137
157
|
|
|
138
158
|
# Append only list of operation hashes as called on this host, used to
|
|
139
159
|
# generate a DAG to create the final operation order.
|
|
@@ -195,11 +215,15 @@ class Host:
|
|
|
195
215
|
self.print_prefix_padding,
|
|
196
216
|
)
|
|
197
217
|
|
|
198
|
-
def
|
|
199
|
-
|
|
200
|
-
return self.current_deploy_data
|
|
218
|
+
def log(self, message, log_func=logger.info):
|
|
219
|
+
log_func(f"{self.print_prefix}{message}")
|
|
201
220
|
|
|
202
|
-
|
|
221
|
+
def log_styled(self, message, log_func=logger.info, **kwargs):
|
|
222
|
+
message_styled = click.style(message, **kwargs)
|
|
223
|
+
self.log(message_styled, log_func=log_func)
|
|
224
|
+
|
|
225
|
+
def get_deploy_data(self):
|
|
226
|
+
return self.current_op_deploy_data or self.current_deploy_data or {}
|
|
203
227
|
|
|
204
228
|
def noop(self, description):
|
|
205
229
|
"""
|
|
@@ -209,8 +233,25 @@ class Host:
|
|
|
209
233
|
handler = logger.info if self.state.print_noop_info else logger.debug
|
|
210
234
|
handler("{0}noop: {1}".format(self.print_prefix, description))
|
|
211
235
|
|
|
236
|
+
def when(self, condition: Callable[[], bool]):
|
|
237
|
+
return self.deploy(
|
|
238
|
+
"",
|
|
239
|
+
cast("AllArguments", {"_if": [condition]}),
|
|
240
|
+
{},
|
|
241
|
+
in_deploy=False,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def arguments(self, **arguments: Unpack["AllArguments"]):
|
|
245
|
+
return self.deploy("", arguments, {}, in_deploy=False)
|
|
246
|
+
|
|
212
247
|
@contextmanager
|
|
213
|
-
def deploy(
|
|
248
|
+
def deploy(
|
|
249
|
+
self,
|
|
250
|
+
name: str,
|
|
251
|
+
kwargs: Optional["AllArguments"],
|
|
252
|
+
data: Optional[dict],
|
|
253
|
+
in_deploy: bool = True,
|
|
254
|
+
):
|
|
214
255
|
"""
|
|
215
256
|
Wraps a group of operations as a deploy, this should not be used
|
|
216
257
|
directly, instead use ``pyinfra.api.deploy.deploy``.
|
|
@@ -227,6 +268,13 @@ class Host:
|
|
|
227
268
|
old_deploy_data = self.current_deploy_data
|
|
228
269
|
self.in_deploy = in_deploy
|
|
229
270
|
|
|
271
|
+
# Combine any old _ifs with the new ones
|
|
272
|
+
if old_deploy_kwargs and kwargs:
|
|
273
|
+
old_ifs = old_deploy_kwargs["_if"]
|
|
274
|
+
new_ifs = kwargs["_if"]
|
|
275
|
+
if old_ifs and new_ifs:
|
|
276
|
+
kwargs["_if"] = old_ifs + new_ifs
|
|
277
|
+
|
|
230
278
|
# Set the new values
|
|
231
279
|
self.current_deploy_name = name
|
|
232
280
|
self.current_deploy_kwargs = kwargs
|
|
@@ -253,9 +301,48 @@ class Host:
|
|
|
253
301
|
old_deploy_data,
|
|
254
302
|
)
|
|
255
303
|
|
|
304
|
+
@memoize
|
|
305
|
+
def _get_temp_directory(self):
|
|
306
|
+
temp_directory = self.state.config.TEMP_DIR
|
|
307
|
+
|
|
308
|
+
if temp_directory is None:
|
|
309
|
+
# Unfortunate, but very hard to avoid, circular dependency, this method is memoized so
|
|
310
|
+
# performance isn't a concern.
|
|
311
|
+
from pyinfra.facts.server import TmpDir
|
|
312
|
+
|
|
313
|
+
temp_directory = self.get_fact(TmpDir)
|
|
314
|
+
|
|
315
|
+
if not temp_directory:
|
|
316
|
+
temp_directory = self.state.config.DEFAULT_TEMP_DIR
|
|
317
|
+
|
|
318
|
+
return temp_directory
|
|
319
|
+
|
|
320
|
+
def get_temp_filename(self, hash_key: Optional[str] = None, hash_filename: bool = True):
|
|
321
|
+
"""
|
|
322
|
+
Generate a temporary filename for this deploy.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
temp_directory = self._get_temp_directory()
|
|
326
|
+
|
|
327
|
+
if not hash_key:
|
|
328
|
+
hash_key = str(uuid4())
|
|
329
|
+
|
|
330
|
+
if hash_filename:
|
|
331
|
+
hash_key = sha1_hash(hash_key)
|
|
332
|
+
|
|
333
|
+
return "{0}/pyinfra-{1}".format(temp_directory, hash_key)
|
|
334
|
+
|
|
256
335
|
# Host facts
|
|
257
336
|
#
|
|
258
337
|
|
|
338
|
+
T = TypeVar("T")
|
|
339
|
+
|
|
340
|
+
@overload
|
|
341
|
+
def get_fact(self, name_or_cls: Type[FactBase[T]], *args, **kwargs) -> T: ...
|
|
342
|
+
|
|
343
|
+
@overload
|
|
344
|
+
def get_fact(self, name_or_cls: Type[ShortFactBase[T]], *args, **kwargs) -> T: ...
|
|
345
|
+
|
|
259
346
|
def get_fact(self, name_or_cls, *args, **kwargs):
|
|
260
347
|
"""
|
|
261
348
|
Get a fact for this host, reading from the cache if present.
|
|
@@ -323,7 +410,7 @@ class Host:
|
|
|
323
410
|
|
|
324
411
|
self.state.trigger_callbacks("host_disconnect", self)
|
|
325
412
|
|
|
326
|
-
def run_shell_command(self, *args, **kwargs) ->
|
|
413
|
+
def run_shell_command(self, *args, **kwargs) -> tuple[bool, CommandOutput]:
|
|
327
414
|
"""
|
|
328
415
|
Low level method to execute a shell command on the host via it's configured connector.
|
|
329
416
|
"""
|
pyinfra/api/inventory.py
CHANGED
pyinfra/api/operation.py
CHANGED
|
@@ -8,15 +8,19 @@ to the deploy state. This is then run later by pyinfra's ``__main__`` or the
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from functools import wraps
|
|
11
|
+
from inspect import signature
|
|
11
12
|
from io import StringIO
|
|
12
13
|
from types import FunctionType
|
|
13
|
-
from typing import Any, Iterator, Optional,
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Optional, cast
|
|
15
|
+
|
|
16
|
+
from typing_extensions import ParamSpec
|
|
14
17
|
|
|
15
18
|
import pyinfra
|
|
16
19
|
from pyinfra import context, logger
|
|
17
20
|
from pyinfra.context import ctx_host, ctx_state
|
|
18
21
|
|
|
19
|
-
from .arguments import
|
|
22
|
+
from .arguments import EXECUTION_KWARG_KEYS, AllArguments, pop_global_arguments
|
|
23
|
+
from .arguments_typed import PyinfraOperation
|
|
20
24
|
from .command import PyinfraCommand, StringCommand
|
|
21
25
|
from .exceptions import OperationValueError, PyinfraError
|
|
22
26
|
from .host import Host
|
|
@@ -32,53 +36,117 @@ from .util import (
|
|
|
32
36
|
|
|
33
37
|
op_meta_default = object()
|
|
34
38
|
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from pyinfra.connectors.util import CommandOutput
|
|
41
|
+
|
|
35
42
|
|
|
36
43
|
class OperationMeta:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
_hash: str
|
|
45
|
+
|
|
46
|
+
_combined_output: Optional["CommandOutput"] = None
|
|
47
|
+
_commands: Optional[list[Any]] = None
|
|
48
|
+
_maybe_is_change: Optional[bool] = None
|
|
49
|
+
_success: Optional[bool] = None
|
|
41
50
|
|
|
42
|
-
def __init__(self, hash
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
51
|
+
def __init__(self, hash, is_change: Optional[bool]):
|
|
52
|
+
self._hash = hash
|
|
53
|
+
self._maybe_is_change = is_change
|
|
45
54
|
|
|
46
55
|
def __repr__(self) -> str:
|
|
47
56
|
"""
|
|
48
57
|
Return Operation object as a string.
|
|
49
58
|
"""
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
if self._commands is not None:
|
|
61
|
+
return (
|
|
62
|
+
"OperationMeta(executed=True, "
|
|
63
|
+
f"success={self.did_succeed()}, hash={self._hash}, commands={len(self._commands)})"
|
|
64
|
+
)
|
|
65
|
+
return (
|
|
66
|
+
"OperationMeta(executed=False, "
|
|
67
|
+
f"maybeChange={self._maybe_is_change}, hash={self._hash})"
|
|
68
|
+
)
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
# Completion & status checks
|
|
71
|
+
def set_complete(
|
|
72
|
+
self,
|
|
73
|
+
success: bool,
|
|
74
|
+
commands: list[Any],
|
|
75
|
+
combined_output: "CommandOutput",
|
|
76
|
+
) -> None:
|
|
77
|
+
if self.is_complete():
|
|
78
|
+
raise RuntimeError("Cannot complete an already complete operation")
|
|
79
|
+
self._success = success
|
|
80
|
+
self._commands = commands
|
|
81
|
+
self._combined_output = combined_output
|
|
82
|
+
|
|
83
|
+
def is_complete(self) -> bool:
|
|
84
|
+
return self._success is not None
|
|
85
|
+
|
|
86
|
+
def _raise_if_not_complete(self) -> None:
|
|
87
|
+
if not self.is_complete():
|
|
88
|
+
raise RuntimeError("Cannot evaluate operation result before execution")
|
|
61
89
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
@property
|
|
91
|
+
def executed(self) -> bool:
|
|
92
|
+
if self._commands is None:
|
|
93
|
+
return False
|
|
94
|
+
return len(self._commands) > 0
|
|
65
95
|
|
|
66
|
-
|
|
96
|
+
@property
|
|
97
|
+
def will_change(self) -> bool:
|
|
98
|
+
if self._maybe_is_change is not None:
|
|
99
|
+
return self._maybe_is_change
|
|
100
|
+
|
|
101
|
+
op_data = context.state.get_op_data_for_host(context.host, self._hash)
|
|
102
|
+
cmd_gen = op_data.command_generator
|
|
103
|
+
for _ in cmd_gen():
|
|
104
|
+
self._maybe_is_change = True
|
|
105
|
+
return True
|
|
106
|
+
self._maybe_is_change = False
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def did_change(self) -> bool:
|
|
110
|
+
self._raise_if_not_complete()
|
|
111
|
+
return bool(self._success and len(self._commands or []) > 0)
|
|
112
|
+
|
|
113
|
+
def did_not_change(self) -> bool:
|
|
114
|
+
return not self.did_change()
|
|
115
|
+
|
|
116
|
+
def did_succeed(self, _raise_if_not_complete=True) -> bool:
|
|
117
|
+
if _raise_if_not_complete:
|
|
118
|
+
self._raise_if_not_complete()
|
|
119
|
+
return self._success is True
|
|
120
|
+
|
|
121
|
+
def did_error(self) -> bool:
|
|
122
|
+
self._raise_if_not_complete()
|
|
123
|
+
return self._success is False
|
|
124
|
+
|
|
125
|
+
# TODO: deprecated, remove in v4
|
|
126
|
+
@property
|
|
127
|
+
def changed(self) -> bool:
|
|
128
|
+
if self.is_complete():
|
|
129
|
+
return self.did_change()
|
|
130
|
+
return self.will_change
|
|
67
131
|
|
|
68
132
|
@property
|
|
69
|
-
def stdout_lines(self):
|
|
70
|
-
|
|
133
|
+
def stdout_lines(self) -> list[str]:
|
|
134
|
+
self._raise_if_not_complete()
|
|
135
|
+
assert self._combined_output is not None
|
|
136
|
+
return self._combined_output.stdout_lines
|
|
71
137
|
|
|
72
138
|
@property
|
|
73
|
-
def stderr_lines(self):
|
|
74
|
-
|
|
139
|
+
def stderr_lines(self) -> list[str]:
|
|
140
|
+
self._raise_if_not_complete()
|
|
141
|
+
assert self._combined_output is not None
|
|
142
|
+
return self._combined_output.stderr_lines
|
|
75
143
|
|
|
76
144
|
@property
|
|
77
|
-
def stdout(self):
|
|
145
|
+
def stdout(self) -> str:
|
|
78
146
|
return "\n".join(self.stdout_lines)
|
|
79
147
|
|
|
80
148
|
@property
|
|
81
|
-
def stderr(self):
|
|
149
|
+
def stderr(self) -> str:
|
|
82
150
|
return "\n".join(self.stderr_lines)
|
|
83
151
|
|
|
84
152
|
|
|
@@ -113,57 +181,67 @@ def add_op(state: State, op_func, *args, **kwargs):
|
|
|
113
181
|
return results
|
|
114
182
|
|
|
115
183
|
|
|
184
|
+
P = ParamSpec("P")
|
|
185
|
+
|
|
186
|
+
|
|
116
187
|
def operation(
|
|
117
|
-
pipeline_facts=None,
|
|
118
188
|
is_idempotent: bool = True,
|
|
119
|
-
idempotent_notice=None,
|
|
120
|
-
|
|
189
|
+
idempotent_notice: Optional[str] = None,
|
|
190
|
+
is_deprecated: bool = False,
|
|
191
|
+
deprecated_for: Optional[str] = None,
|
|
192
|
+
_set_in_op: bool = True,
|
|
193
|
+
) -> Callable[[Callable[P, Generator]], PyinfraOperation[P]]:
|
|
121
194
|
"""
|
|
122
195
|
Decorator that takes a simple module function and turn it into the internal
|
|
123
196
|
operation representation that consists of a list of commands + options
|
|
124
197
|
(sudo, (sudo|su)_user, env).
|
|
125
198
|
"""
|
|
126
199
|
|
|
127
|
-
def decorator(f):
|
|
128
|
-
f.
|
|
129
|
-
f.
|
|
130
|
-
f.
|
|
131
|
-
|
|
200
|
+
def decorator(f: Callable[P, Generator]) -> PyinfraOperation[P]:
|
|
201
|
+
f.is_idempotent = is_idempotent # type: ignore[attr-defined]
|
|
202
|
+
f.idempotent_notice = idempotent_notice # type: ignore[attr-defined]
|
|
203
|
+
f.is_deprecated = is_deprecated # type: ignore[attr-defined]
|
|
204
|
+
f.deprecated_for = deprecated_for # type: ignore[attr-defined]
|
|
205
|
+
return _wrap_operation(f, _set_in_op=_set_in_op)
|
|
132
206
|
|
|
133
207
|
return decorator
|
|
134
208
|
|
|
135
209
|
|
|
136
|
-
def _wrap_operation(func):
|
|
210
|
+
def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> PyinfraOperation[P]:
|
|
137
211
|
@wraps(func)
|
|
138
|
-
def decorated_func(*args, **kwargs):
|
|
212
|
+
def decorated_func(*args: P.args, **kwargs: P.kwargs) -> OperationMeta:
|
|
139
213
|
state = context.state
|
|
140
214
|
host = context.host
|
|
141
215
|
|
|
216
|
+
if host.in_op:
|
|
217
|
+
raise Exception(
|
|
218
|
+
"Operation called within another operation, this is not allowed! Use the `_inner` "
|
|
219
|
+
+ "function to call the underlying operation."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if func.is_deprecated: # type: ignore[attr-defined]
|
|
223
|
+
if func.deprecated_for: # type: ignore[attr-defined]
|
|
224
|
+
logger.warning(
|
|
225
|
+
f"The {get_operation_name_from_func(func)} operation is "
|
|
226
|
+
+ f"deprecated, please use: {func.deprecated_for}", # type: ignore[attr-defined] # noqa
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
logger.warning(f"The {get_operation_name_from_func(func)} operation is deprecated")
|
|
230
|
+
|
|
142
231
|
# Configure operation
|
|
143
232
|
#
|
|
144
233
|
# Get the meta kwargs (globals that apply to all hosts)
|
|
145
234
|
global_arguments, global_argument_keys = pop_global_arguments(kwargs)
|
|
146
235
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if host.in_op and not host.in_callback_op:
|
|
150
|
-
if global_argument_keys:
|
|
151
|
-
_error_msg = "Nested operation called with global arguments: {0} ({1})".format(
|
|
152
|
-
global_argument_keys,
|
|
153
|
-
get_call_location(),
|
|
154
|
-
)
|
|
155
|
-
raise PyinfraError(_error_msg)
|
|
156
|
-
return func(*args, **kwargs) or []
|
|
157
|
-
|
|
158
|
-
names, add_args = _generate_operation_name(func, host, kwargs, global_arguments)
|
|
159
|
-
op_order, op_hash = _solve_operation_consistency(names, state, host)
|
|
236
|
+
names, add_args = generate_operation_name(func, host, kwargs, global_arguments)
|
|
237
|
+
op_order, op_hash = solve_operation_consistency(names, state, host)
|
|
160
238
|
|
|
161
239
|
# Ensure shared (between servers) operation meta, mutates state
|
|
162
|
-
op_meta =
|
|
240
|
+
op_meta = ensure_shared_op_meta(state, op_hash, op_order, global_arguments, names)
|
|
163
241
|
|
|
164
242
|
# Attach normal args, if we're auto-naming this operation
|
|
165
243
|
if add_args:
|
|
166
|
-
op_meta =
|
|
244
|
+
op_meta = attach_args(op_meta, args, kwargs)
|
|
167
245
|
|
|
168
246
|
# Check if we're actually running the operation on this host
|
|
169
247
|
# Run once and we've already added meta for this op? Stop here.
|
|
@@ -175,20 +253,30 @@ def _wrap_operation(func):
|
|
|
175
253
|
break
|
|
176
254
|
|
|
177
255
|
if has_run:
|
|
178
|
-
return OperationMeta(op_hash)
|
|
256
|
+
return OperationMeta(op_hash, is_change=False)
|
|
257
|
+
|
|
258
|
+
# Grab a reference to any *current* deploy data as this may change when
|
|
259
|
+
# we later evaluate the operation at runtime.This means we put back the
|
|
260
|
+
# expected deploy data.
|
|
261
|
+
current_deploy_data = host.current_deploy_data
|
|
179
262
|
|
|
180
263
|
# "Run" operation - here we make a generator that will yield out actual commands to execute
|
|
181
264
|
# and, if we're diff-ing, we then iterate the generator now to determine if any changes
|
|
182
265
|
# *would* be made based on the *current* remote state.
|
|
183
266
|
|
|
184
267
|
def command_generator() -> Iterator[PyinfraCommand]:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
268
|
+
# Check global _if argument function and do nothing if returns False
|
|
269
|
+
if state.is_executing:
|
|
270
|
+
_ifs = global_arguments.get("_if")
|
|
271
|
+
if isinstance(_ifs, list) and not all(_if() for _if in _ifs):
|
|
272
|
+
return
|
|
273
|
+
elif callable(_ifs) and not _ifs():
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
host.in_op = _set_in_op
|
|
190
277
|
host.current_op_hash = op_hash
|
|
191
278
|
host.current_op_global_arguments = global_arguments
|
|
279
|
+
host.current_op_deploy_data = current_deploy_data
|
|
192
280
|
|
|
193
281
|
try:
|
|
194
282
|
for command in func(*args, **kwargs):
|
|
@@ -199,35 +287,54 @@ def _wrap_operation(func):
|
|
|
199
287
|
host.in_op = False
|
|
200
288
|
host.current_op_hash = None
|
|
201
289
|
host.current_op_global_arguments = None
|
|
290
|
+
host.current_op_deploy_data = None
|
|
202
291
|
|
|
203
|
-
op_is_change =
|
|
292
|
+
op_is_change = None
|
|
204
293
|
if state.should_check_for_changes():
|
|
205
|
-
|
|
294
|
+
op_is_change = False
|
|
295
|
+
for _ in command_generator():
|
|
206
296
|
op_is_change = True
|
|
207
297
|
break
|
|
298
|
+
else:
|
|
299
|
+
# If not calling the op function to check for change we still want to ensure the args
|
|
300
|
+
# are valid, so use Signature.bind to trigger any TypeError.
|
|
301
|
+
signature(func).bind(*args, **kwargs)
|
|
208
302
|
|
|
209
303
|
# Add host-specific operation data to state, this mutates state
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
)
|
|
304
|
+
host_meta = state.get_meta_for_host(host)
|
|
305
|
+
host_meta.ops += 1
|
|
306
|
+
if op_is_change:
|
|
307
|
+
host_meta.ops_change += 1
|
|
308
|
+
else:
|
|
309
|
+
host_meta.ops_no_change += 1
|
|
310
|
+
|
|
311
|
+
operation_meta = OperationMeta(op_hash, op_is_change)
|
|
312
|
+
|
|
313
|
+
# Add the server-relevant commands
|
|
314
|
+
op_data = StateOperationHostData(command_generator, global_arguments, operation_meta)
|
|
315
|
+
state.set_op_data_for_host(host, op_hash, op_data)
|
|
218
316
|
|
|
219
317
|
# If we're already in the execution phase, execute this operation immediately
|
|
220
318
|
if state.is_executing:
|
|
221
|
-
|
|
319
|
+
execute_immediately(state, host, op_hash)
|
|
222
320
|
|
|
223
321
|
# Return result meta for use in deploy scripts
|
|
224
322
|
return operation_meta
|
|
225
323
|
|
|
226
|
-
decorated_func.
|
|
227
|
-
return decorated_func
|
|
324
|
+
decorated_func._inner = func # type: ignore[attr-defined]
|
|
325
|
+
return cast(PyinfraOperation[P], decorated_func)
|
|
228
326
|
|
|
229
327
|
|
|
230
|
-
def
|
|
328
|
+
def get_operation_name_from_func(func):
|
|
329
|
+
if func.__module__:
|
|
330
|
+
module_bits = func.__module__.split(".")
|
|
331
|
+
module_name = module_bits[-1]
|
|
332
|
+
return "{0}.{1}".format(module_name, func.__name__)
|
|
333
|
+
else:
|
|
334
|
+
return func.__name__
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def generate_operation_name(func, host, kwargs, global_arguments):
|
|
231
338
|
# Generate an operation name if needed (Module/Operation format)
|
|
232
339
|
name = global_arguments.get("name")
|
|
233
340
|
add_args = False
|
|
@@ -235,14 +342,7 @@ def _generate_operation_name(func, host, kwargs, global_arguments):
|
|
|
235
342
|
names = {name}
|
|
236
343
|
else:
|
|
237
344
|
add_args = True
|
|
238
|
-
|
|
239
|
-
if func.__module__:
|
|
240
|
-
module_bits = func.__module__.split(".")
|
|
241
|
-
module_name = module_bits[-1]
|
|
242
|
-
name = "{0}/{1}".format(module_name.title(), func.__name__.title())
|
|
243
|
-
else:
|
|
244
|
-
name = func.__name__
|
|
245
|
-
|
|
345
|
+
name = get_operation_name_from_func(func)
|
|
246
346
|
names = {name}
|
|
247
347
|
|
|
248
348
|
if host.current_deploy_name:
|
|
@@ -251,7 +351,7 @@ def _generate_operation_name(func, host, kwargs, global_arguments):
|
|
|
251
351
|
return names, add_args
|
|
252
352
|
|
|
253
353
|
|
|
254
|
-
def
|
|
354
|
+
def solve_operation_consistency(names, state, host):
|
|
255
355
|
# Operation order is used to tie-break available nodes in the operation DAG, in CLI mode
|
|
256
356
|
# we use stack call order so this matches as defined by the user deploy code.
|
|
257
357
|
if pyinfra.is_cli:
|
|
@@ -286,16 +386,16 @@ def _solve_operation_consistency(names, state, host):
|
|
|
286
386
|
|
|
287
387
|
|
|
288
388
|
# NOTE: this function mutates state.op_meta for this hash
|
|
289
|
-
def
|
|
389
|
+
def ensure_shared_op_meta(
|
|
290
390
|
state: State,
|
|
291
391
|
op_hash: str,
|
|
292
|
-
op_order:
|
|
392
|
+
op_order: tuple[int, ...],
|
|
293
393
|
global_arguments: AllArguments,
|
|
294
|
-
names:
|
|
394
|
+
names: set[str],
|
|
295
395
|
):
|
|
296
396
|
op_meta = state.op_meta.setdefault(op_hash, StateOperationMeta(op_order))
|
|
297
397
|
|
|
298
|
-
for key in
|
|
398
|
+
for key in EXECUTION_KWARG_KEYS:
|
|
299
399
|
global_value = global_arguments.pop(key) # type: ignore[misc]
|
|
300
400
|
op_meta_value = op_meta.global_arguments.get(key, op_meta_default)
|
|
301
401
|
|
|
@@ -310,12 +410,7 @@ def _ensure_shared_op_meta(
|
|
|
310
410
|
return op_meta
|
|
311
411
|
|
|
312
412
|
|
|
313
|
-
def
|
|
314
|
-
logger.warning(
|
|
315
|
-
f"Note: nested operations are currently in beta ({get_call_location()})\n"
|
|
316
|
-
" More information: "
|
|
317
|
-
"https://docs.pyinfra.com/en/2.x/using-operations.html#nested-operations",
|
|
318
|
-
)
|
|
413
|
+
def execute_immediately(state, host, op_hash):
|
|
319
414
|
op_meta = state.get_op_meta(op_hash)
|
|
320
415
|
op_data = state.get_op_data_for_host(host, op_hash)
|
|
321
416
|
op_data.parent_op_hash = host.executing_op_hash
|
|
@@ -331,7 +426,7 @@ def _get_arg_value(arg):
|
|
|
331
426
|
return arg
|
|
332
427
|
|
|
333
428
|
|
|
334
|
-
def
|
|
429
|
+
def attach_args(op_meta, args, kwargs):
|
|
335
430
|
for arg in args:
|
|
336
431
|
if arg not in op_meta.args:
|
|
337
432
|
op_meta.args.append(str(_get_arg_value(arg)))
|
|
@@ -343,30 +438,3 @@ def _attach_args(op_meta, args, kwargs):
|
|
|
343
438
|
op_meta.args.append(arg)
|
|
344
439
|
|
|
345
440
|
return op_meta
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
# NOTE: this function mutates state.meta for this host
|
|
349
|
-
def _add_host_op_to_state(
|
|
350
|
-
state: State,
|
|
351
|
-
host: Host,
|
|
352
|
-
op_hash: str,
|
|
353
|
-
is_change: bool,
|
|
354
|
-
command_generator,
|
|
355
|
-
global_arguments,
|
|
356
|
-
) -> OperationMeta:
|
|
357
|
-
host_meta = state.get_meta_for_host(host)
|
|
358
|
-
|
|
359
|
-
host_meta.ops += 1
|
|
360
|
-
|
|
361
|
-
if is_change:
|
|
362
|
-
host_meta.ops_change += 1
|
|
363
|
-
else:
|
|
364
|
-
host_meta.ops_no_change += 1
|
|
365
|
-
|
|
366
|
-
operation_meta = OperationMeta(op_hash, is_change)
|
|
367
|
-
|
|
368
|
-
# Add the server-relevant commands
|
|
369
|
-
op_data = StateOperationHostData(command_generator, global_arguments, operation_meta)
|
|
370
|
-
state.set_op_data_for_host(host, op_hash, op_data)
|
|
371
|
-
|
|
372
|
-
return operation_meta
|