pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.1__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 +184 -118
- 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.1.dist-info}/METADATA +51 -58
- pyinfra-3.0.1.dist-info/RECORD +168 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/WHEEL +1 -1
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.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 +10 -12
- 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.1.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.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,115 @@ 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
|
-
|
|
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
|
+
)
|
|
69
|
+
|
|
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")
|
|
52
89
|
|
|
53
|
-
|
|
54
|
-
|
|
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:
|
|
104
|
+
return bool(self._success and len(self._commands or []) > 0)
|
|
55
105
|
|
|
56
|
-
|
|
57
|
-
|
|
106
|
+
@property
|
|
107
|
+
def did_change(self):
|
|
108
|
+
return context.host.when(self._did_change)
|
|
58
109
|
|
|
59
|
-
|
|
60
|
-
|
|
110
|
+
@property
|
|
111
|
+
def did_not_change(self):
|
|
112
|
+
return context.host.when(lambda: not self._did_change())
|
|
61
113
|
|
|
62
|
-
def
|
|
63
|
-
if
|
|
64
|
-
|
|
114
|
+
def did_succeed(self, _raise_if_not_complete=True) -> bool:
|
|
115
|
+
if _raise_if_not_complete:
|
|
116
|
+
self._raise_if_not_complete()
|
|
117
|
+
return self._success is True
|
|
65
118
|
|
|
66
|
-
|
|
119
|
+
def did_error(self) -> bool:
|
|
120
|
+
self._raise_if_not_complete()
|
|
121
|
+
return self._success is False
|
|
67
122
|
|
|
123
|
+
# TODO: deprecated, remove in v4
|
|
68
124
|
@property
|
|
69
|
-
def
|
|
70
|
-
|
|
125
|
+
def changed(self) -> bool:
|
|
126
|
+
if self.is_complete():
|
|
127
|
+
return self._did_change()
|
|
128
|
+
return self.will_change
|
|
71
129
|
|
|
72
130
|
@property
|
|
73
|
-
def
|
|
74
|
-
|
|
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
|
|
75
135
|
|
|
76
136
|
@property
|
|
77
|
-
def
|
|
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
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def stdout(self) -> str:
|
|
78
144
|
return "\n".join(self.stdout_lines)
|
|
79
145
|
|
|
80
146
|
@property
|
|
81
|
-
def stderr(self):
|
|
147
|
+
def stderr(self) -> str:
|
|
82
148
|
return "\n".join(self.stderr_lines)
|
|
83
149
|
|
|
84
150
|
|
|
@@ -113,57 +179,67 @@ def add_op(state: State, op_func, *args, **kwargs):
|
|
|
113
179
|
return results
|
|
114
180
|
|
|
115
181
|
|
|
182
|
+
P = ParamSpec("P")
|
|
183
|
+
|
|
184
|
+
|
|
116
185
|
def operation(
|
|
117
|
-
pipeline_facts=None,
|
|
118
186
|
is_idempotent: bool = True,
|
|
119
|
-
idempotent_notice=None,
|
|
120
|
-
|
|
187
|
+
idempotent_notice: Optional[str] = None,
|
|
188
|
+
is_deprecated: bool = False,
|
|
189
|
+
deprecated_for: Optional[str] = None,
|
|
190
|
+
_set_in_op: bool = True,
|
|
191
|
+
) -> Callable[[Callable[P, Generator]], PyinfraOperation[P]]:
|
|
121
192
|
"""
|
|
122
193
|
Decorator that takes a simple module function and turn it into the internal
|
|
123
194
|
operation representation that consists of a list of commands + options
|
|
124
195
|
(sudo, (sudo|su)_user, env).
|
|
125
196
|
"""
|
|
126
197
|
|
|
127
|
-
def decorator(f):
|
|
128
|
-
f.
|
|
129
|
-
f.
|
|
130
|
-
f.
|
|
131
|
-
|
|
198
|
+
def decorator(f: Callable[P, Generator]) -> PyinfraOperation[P]:
|
|
199
|
+
f.is_idempotent = is_idempotent # type: ignore[attr-defined]
|
|
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]
|
|
203
|
+
return _wrap_operation(f, _set_in_op=_set_in_op)
|
|
132
204
|
|
|
133
205
|
return decorator
|
|
134
206
|
|
|
135
207
|
|
|
136
|
-
def _wrap_operation(func):
|
|
208
|
+
def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> PyinfraOperation[P]:
|
|
137
209
|
@wraps(func)
|
|
138
|
-
def decorated_func(*args, **kwargs):
|
|
210
|
+
def decorated_func(*args: P.args, **kwargs: P.kwargs) -> OperationMeta:
|
|
139
211
|
state = context.state
|
|
140
212
|
host = context.host
|
|
141
213
|
|
|
214
|
+
if host.in_op:
|
|
215
|
+
raise Exception(
|
|
216
|
+
"Operation called within another operation, this is not allowed! Use the `_inner` "
|
|
217
|
+
+ "function to call the underlying operation."
|
|
218
|
+
)
|
|
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
|
+
|
|
142
229
|
# Configure operation
|
|
143
230
|
#
|
|
144
231
|
# Get the meta kwargs (globals that apply to all hosts)
|
|
145
232
|
global_arguments, global_argument_keys = pop_global_arguments(kwargs)
|
|
146
233
|
|
|
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)
|
|
234
|
+
names, add_args = generate_operation_name(func, host, kwargs, global_arguments)
|
|
235
|
+
op_order, op_hash = solve_operation_consistency(names, state, host)
|
|
160
236
|
|
|
161
237
|
# Ensure shared (between servers) operation meta, mutates state
|
|
162
|
-
op_meta =
|
|
238
|
+
op_meta = ensure_shared_op_meta(state, op_hash, op_order, global_arguments, names)
|
|
163
239
|
|
|
164
240
|
# Attach normal args, if we're auto-naming this operation
|
|
165
241
|
if add_args:
|
|
166
|
-
op_meta =
|
|
242
|
+
op_meta = attach_args(op_meta, args, kwargs)
|
|
167
243
|
|
|
168
244
|
# Check if we're actually running the operation on this host
|
|
169
245
|
# Run once and we've already added meta for this op? Stop here.
|
|
@@ -175,20 +251,30 @@ def _wrap_operation(func):
|
|
|
175
251
|
break
|
|
176
252
|
|
|
177
253
|
if has_run:
|
|
178
|
-
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
|
|
179
260
|
|
|
180
261
|
# "Run" operation - here we make a generator that will yield out actual commands to execute
|
|
181
262
|
# and, if we're diff-ing, we then iterate the generator now to determine if any changes
|
|
182
263
|
# *would* be made based on the *current* remote state.
|
|
183
264
|
|
|
184
265
|
def command_generator() -> Iterator[PyinfraCommand]:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
266
|
+
# Check global _if argument function and do nothing if returns False
|
|
267
|
+
if state.is_executing:
|
|
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():
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
host.in_op = _set_in_op
|
|
190
275
|
host.current_op_hash = op_hash
|
|
191
276
|
host.current_op_global_arguments = global_arguments
|
|
277
|
+
host.current_op_deploy_data = current_deploy_data
|
|
192
278
|
|
|
193
279
|
try:
|
|
194
280
|
for command in func(*args, **kwargs):
|
|
@@ -199,35 +285,54 @@ def _wrap_operation(func):
|
|
|
199
285
|
host.in_op = False
|
|
200
286
|
host.current_op_hash = None
|
|
201
287
|
host.current_op_global_arguments = None
|
|
288
|
+
host.current_op_deploy_data = None
|
|
202
289
|
|
|
203
|
-
op_is_change =
|
|
290
|
+
op_is_change = None
|
|
204
291
|
if state.should_check_for_changes():
|
|
205
|
-
|
|
292
|
+
op_is_change = False
|
|
293
|
+
for _ in command_generator():
|
|
206
294
|
op_is_change = True
|
|
207
295
|
break
|
|
296
|
+
else:
|
|
297
|
+
# If not calling the op function to check for change we still want to ensure the args
|
|
298
|
+
# are valid, so use Signature.bind to trigger any TypeError.
|
|
299
|
+
signature(func).bind(*args, **kwargs)
|
|
208
300
|
|
|
209
301
|
# Add host-specific operation data to state, this mutates state
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
)
|
|
302
|
+
host_meta = state.get_meta_for_host(host)
|
|
303
|
+
host_meta.ops += 1
|
|
304
|
+
if op_is_change:
|
|
305
|
+
host_meta.ops_change += 1
|
|
306
|
+
else:
|
|
307
|
+
host_meta.ops_no_change += 1
|
|
308
|
+
|
|
309
|
+
operation_meta = OperationMeta(op_hash, op_is_change)
|
|
310
|
+
|
|
311
|
+
# Add the server-relevant commands
|
|
312
|
+
op_data = StateOperationHostData(command_generator, global_arguments, operation_meta)
|
|
313
|
+
state.set_op_data_for_host(host, op_hash, op_data)
|
|
218
314
|
|
|
219
315
|
# If we're already in the execution phase, execute this operation immediately
|
|
220
316
|
if state.is_executing:
|
|
221
|
-
|
|
317
|
+
execute_immediately(state, host, op_hash)
|
|
222
318
|
|
|
223
319
|
# Return result meta for use in deploy scripts
|
|
224
320
|
return operation_meta
|
|
225
321
|
|
|
226
|
-
decorated_func.
|
|
227
|
-
return decorated_func
|
|
322
|
+
decorated_func._inner = func # type: ignore[attr-defined]
|
|
323
|
+
return cast(PyinfraOperation[P], decorated_func)
|
|
228
324
|
|
|
229
325
|
|
|
230
|
-
def
|
|
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
|
+
|
|
335
|
+
def generate_operation_name(func, host, kwargs, global_arguments):
|
|
231
336
|
# Generate an operation name if needed (Module/Operation format)
|
|
232
337
|
name = global_arguments.get("name")
|
|
233
338
|
add_args = False
|
|
@@ -235,14 +340,7 @@ def _generate_operation_name(func, host, kwargs, global_arguments):
|
|
|
235
340
|
names = {name}
|
|
236
341
|
else:
|
|
237
342
|
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
|
-
|
|
343
|
+
name = get_operation_name_from_func(func)
|
|
246
344
|
names = {name}
|
|
247
345
|
|
|
248
346
|
if host.current_deploy_name:
|
|
@@ -251,7 +349,7 @@ def _generate_operation_name(func, host, kwargs, global_arguments):
|
|
|
251
349
|
return names, add_args
|
|
252
350
|
|
|
253
351
|
|
|
254
|
-
def
|
|
352
|
+
def solve_operation_consistency(names, state, host):
|
|
255
353
|
# Operation order is used to tie-break available nodes in the operation DAG, in CLI mode
|
|
256
354
|
# we use stack call order so this matches as defined by the user deploy code.
|
|
257
355
|
if pyinfra.is_cli:
|
|
@@ -286,16 +384,16 @@ def _solve_operation_consistency(names, state, host):
|
|
|
286
384
|
|
|
287
385
|
|
|
288
386
|
# NOTE: this function mutates state.op_meta for this hash
|
|
289
|
-
def
|
|
387
|
+
def ensure_shared_op_meta(
|
|
290
388
|
state: State,
|
|
291
389
|
op_hash: str,
|
|
292
|
-
op_order:
|
|
390
|
+
op_order: tuple[int, ...],
|
|
293
391
|
global_arguments: AllArguments,
|
|
294
|
-
names:
|
|
392
|
+
names: set[str],
|
|
295
393
|
):
|
|
296
394
|
op_meta = state.op_meta.setdefault(op_hash, StateOperationMeta(op_order))
|
|
297
395
|
|
|
298
|
-
for key in
|
|
396
|
+
for key in EXECUTION_KWARG_KEYS:
|
|
299
397
|
global_value = global_arguments.pop(key) # type: ignore[misc]
|
|
300
398
|
op_meta_value = op_meta.global_arguments.get(key, op_meta_default)
|
|
301
399
|
|
|
@@ -310,12 +408,7 @@ def _ensure_shared_op_meta(
|
|
|
310
408
|
return op_meta
|
|
311
409
|
|
|
312
410
|
|
|
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
|
-
)
|
|
411
|
+
def execute_immediately(state, host, op_hash):
|
|
319
412
|
op_meta = state.get_op_meta(op_hash)
|
|
320
413
|
op_data = state.get_op_data_for_host(host, op_hash)
|
|
321
414
|
op_data.parent_op_hash = host.executing_op_hash
|
|
@@ -331,7 +424,7 @@ def _get_arg_value(arg):
|
|
|
331
424
|
return arg
|
|
332
425
|
|
|
333
426
|
|
|
334
|
-
def
|
|
427
|
+
def attach_args(op_meta, args, kwargs):
|
|
335
428
|
for arg in args:
|
|
336
429
|
if arg not in op_meta.args:
|
|
337
430
|
op_meta.args.append(str(_get_arg_value(arg)))
|
|
@@ -343,30 +436,3 @@ def _attach_args(op_meta, args, kwargs):
|
|
|
343
436
|
op_meta.args.append(arg)
|
|
344
437
|
|
|
345
438
|
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
|