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/operation.py
CHANGED
|
@@ -5,83 +5,150 @@ to the deploy state. This is then run later by pyinfra's ``__main__`` or the
|
|
|
5
5
|
:doc:`./pyinfra.api.operations` module.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
8
10
|
from functools import wraps
|
|
11
|
+
from inspect import signature
|
|
12
|
+
from io import StringIO
|
|
9
13
|
from types import FunctionType
|
|
10
|
-
from typing import TYPE_CHECKING
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Optional, cast
|
|
15
|
+
|
|
16
|
+
from typing_extensions import ParamSpec
|
|
11
17
|
|
|
12
18
|
import pyinfra
|
|
13
19
|
from pyinfra import context, logger
|
|
14
20
|
from pyinfra.context import ctx_host, ctx_state
|
|
15
21
|
|
|
16
|
-
from .arguments import
|
|
17
|
-
from .
|
|
22
|
+
from .arguments import EXECUTION_KWARG_KEYS, AllArguments, pop_global_arguments
|
|
23
|
+
from .arguments_typed import PyinfraOperation
|
|
24
|
+
from .command import PyinfraCommand, StringCommand
|
|
18
25
|
from .exceptions import OperationValueError, PyinfraError
|
|
19
26
|
from .host import Host
|
|
20
27
|
from .operations import run_host_op
|
|
28
|
+
from .state import State, StateOperationHostData, StateOperationMeta
|
|
21
29
|
from .util import (
|
|
22
|
-
get_args_kwargs_spec,
|
|
23
30
|
get_call_location,
|
|
31
|
+
get_file_sha1,
|
|
24
32
|
get_operation_order_from_stack,
|
|
25
33
|
log_operation_start,
|
|
26
34
|
make_hash,
|
|
27
|
-
memoize,
|
|
28
35
|
)
|
|
29
36
|
|
|
30
|
-
if TYPE_CHECKING:
|
|
31
|
-
from pyinfra.api.state import State
|
|
32
|
-
|
|
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
|
-
|
|
44
|
+
_hash: str
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
self.hash = hash
|
|
46
|
+
_combined_output: Optional["CommandOutput"] = None
|
|
47
|
+
_commands: Optional[list[Any]] = None
|
|
48
|
+
_maybe_is_change: Optional[bool] = None
|
|
49
|
+
_success: Optional[bool] = None
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
self.
|
|
51
|
+
def __init__(self, hash, is_change: Optional[bool]):
|
|
52
|
+
self._hash = hash
|
|
53
|
+
self._maybe_is_change = is_change
|
|
47
54
|
|
|
48
|
-
def __repr__(self):
|
|
55
|
+
def __repr__(self) -> str:
|
|
49
56
|
"""
|
|
50
57
|
Return Operation object as a string.
|
|
51
58
|
"""
|
|
52
59
|
|
|
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
|
+
)
|
|
53
65
|
return (
|
|
54
|
-
|
|
55
|
-
f"
|
|
66
|
+
"OperationMeta(executed=False, "
|
|
67
|
+
f"maybeChange={self._maybe_is_change}, hash={self._hash})"
|
|
56
68
|
)
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
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")
|
|
89
|
+
|
|
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)
|
|
105
|
+
|
|
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()
|
|
117
|
+
return self._success is True
|
|
60
118
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
119
|
+
def did_error(self) -> bool:
|
|
120
|
+
self._raise_if_not_complete()
|
|
121
|
+
return self._success is False
|
|
64
122
|
|
|
65
|
-
|
|
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
|
|
66
129
|
|
|
67
130
|
@property
|
|
68
|
-
def stdout_lines(self):
|
|
69
|
-
|
|
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
|
|
70
135
|
|
|
71
136
|
@property
|
|
72
|
-
def stderr_lines(self):
|
|
73
|
-
|
|
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
|
|
74
141
|
|
|
75
142
|
@property
|
|
76
|
-
def stdout(self):
|
|
143
|
+
def stdout(self) -> str:
|
|
77
144
|
return "\n".join(self.stdout_lines)
|
|
78
145
|
|
|
79
146
|
@property
|
|
80
|
-
def stderr(self):
|
|
147
|
+
def stderr(self) -> str:
|
|
81
148
|
return "\n".join(self.stderr_lines)
|
|
82
149
|
|
|
83
150
|
|
|
84
|
-
def add_op(state:
|
|
151
|
+
def add_op(state: State, op_func, *args, **kwargs):
|
|
85
152
|
"""
|
|
86
153
|
Prepare & add an operation to ``pyinfra.state`` by executing it on all hosts.
|
|
87
154
|
|
|
@@ -112,85 +179,71 @@ def add_op(state: "State", op_func, *args, **kwargs):
|
|
|
112
179
|
return results
|
|
113
180
|
|
|
114
181
|
|
|
115
|
-
|
|
116
|
-
def show_state_host_arguments_warning(call_location):
|
|
117
|
-
logger.warning(
|
|
118
|
-
(
|
|
119
|
-
"{0}:\n\tLegacy operation function detected! Operations should no longer define "
|
|
120
|
-
"`state` and `host` arguments."
|
|
121
|
-
).format(call_location),
|
|
122
|
-
)
|
|
182
|
+
P = ParamSpec("P")
|
|
123
183
|
|
|
124
184
|
|
|
125
185
|
def operation(
|
|
126
|
-
func=None,
|
|
127
|
-
pipeline_facts=None,
|
|
128
186
|
is_idempotent: bool = True,
|
|
129
|
-
idempotent_notice=None,
|
|
130
|
-
|
|
131
|
-
|
|
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]]:
|
|
132
192
|
"""
|
|
133
193
|
Decorator that takes a simple module function and turn it into the internal
|
|
134
194
|
operation representation that consists of a list of commands + options
|
|
135
195
|
(sudo, (sudo|su)_user, env).
|
|
136
196
|
"""
|
|
137
197
|
|
|
138
|
-
|
|
139
|
-
|
|
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)
|
|
140
204
|
|
|
141
|
-
|
|
142
|
-
f.pipeline_facts = pipeline_facts
|
|
143
|
-
f.is_idempotent = is_idempotent
|
|
144
|
-
f.idempotent_notice = idempotent_notice
|
|
145
|
-
return operation(f, frame_offset=2)
|
|
205
|
+
return decorator
|
|
146
206
|
|
|
147
|
-
return decorator
|
|
148
207
|
|
|
149
|
-
|
|
150
|
-
# TODO: remove this in v3
|
|
151
|
-
is_legacy = False
|
|
152
|
-
args, kwargs = get_args_kwargs_spec(func)
|
|
153
|
-
if all(key in kwargs and kwargs[key] is None for key in ("state", "host")):
|
|
154
|
-
show_state_host_arguments_warning(get_call_location(frame_offset=frame_offset))
|
|
155
|
-
is_legacy = True
|
|
156
|
-
func.is_legacy = is_legacy
|
|
157
|
-
|
|
158
|
-
# Actually decorate!
|
|
208
|
+
def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> PyinfraOperation[P]:
|
|
159
209
|
@wraps(func)
|
|
160
|
-
def decorated_func(*args, **kwargs):
|
|
210
|
+
def decorated_func(*args: P.args, **kwargs: P.kwargs) -> OperationMeta:
|
|
161
211
|
state = context.state
|
|
162
212
|
host = context.host
|
|
163
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
|
+
|
|
164
229
|
# Configure operation
|
|
165
230
|
#
|
|
166
231
|
# Get the meta kwargs (globals that apply to all hosts)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
# If this op is being called inside another, just return here
|
|
170
|
-
# (any unwanted/op-related kwargs removed above).
|
|
171
|
-
if host.in_op:
|
|
172
|
-
if global_kwarg_keys:
|
|
173
|
-
_error_msg = "Nested operation called with global arguments: {0} ({1})".format(
|
|
174
|
-
global_kwarg_keys,
|
|
175
|
-
get_call_location(),
|
|
176
|
-
)
|
|
177
|
-
raise PyinfraError(_error_msg)
|
|
178
|
-
return func(*args, **kwargs) or []
|
|
232
|
+
global_arguments, global_argument_keys = pop_global_arguments(kwargs)
|
|
179
233
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
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)
|
|
183
236
|
|
|
184
237
|
# Ensure shared (between servers) operation meta, mutates state
|
|
185
|
-
op_meta =
|
|
238
|
+
op_meta = ensure_shared_op_meta(state, op_hash, op_order, global_arguments, names)
|
|
186
239
|
|
|
187
240
|
# Attach normal args, if we're auto-naming this operation
|
|
188
241
|
if add_args:
|
|
189
|
-
op_meta =
|
|
242
|
+
op_meta = attach_args(op_meta, args, kwargs)
|
|
190
243
|
|
|
191
244
|
# Check if we're actually running the operation on this host
|
|
192
245
|
# Run once and we've already added meta for this op? Stop here.
|
|
193
|
-
if op_meta["
|
|
246
|
+
if op_meta.global_arguments["_run_once"]:
|
|
194
247
|
has_run = False
|
|
195
248
|
for ops in state.ops.values():
|
|
196
249
|
if op_hash in ops:
|
|
@@ -198,75 +251,96 @@ def operation(
|
|
|
198
251
|
break
|
|
199
252
|
|
|
200
253
|
if has_run:
|
|
201
|
-
return OperationMeta(op_hash)
|
|
202
|
-
|
|
203
|
-
#
|
|
204
|
-
#
|
|
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
|
|
260
|
+
|
|
261
|
+
# "Run" operation - here we make a generator that will yield out actual commands to execute
|
|
262
|
+
# and, if we're diff-ing, we then iterate the generator now to determine if any changes
|
|
263
|
+
# *would* be made based on the *current* remote state.
|
|
264
|
+
|
|
265
|
+
def command_generator() -> Iterator[PyinfraCommand]:
|
|
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
|
|
275
|
+
host.current_op_hash = op_hash
|
|
276
|
+
host.current_op_global_arguments = global_arguments
|
|
277
|
+
host.current_op_deploy_data = current_deploy_data
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
for command in func(*args, **kwargs):
|
|
281
|
+
if isinstance(command, str):
|
|
282
|
+
command = StringCommand(command.strip())
|
|
283
|
+
yield command
|
|
284
|
+
finally:
|
|
285
|
+
host.in_op = False
|
|
286
|
+
host.current_op_hash = None
|
|
287
|
+
host.current_op_global_arguments = None
|
|
288
|
+
host.current_op_deploy_data = None
|
|
289
|
+
|
|
290
|
+
op_is_change = None
|
|
291
|
+
if state.should_check_for_changes():
|
|
292
|
+
op_is_change = False
|
|
293
|
+
for _ in command_generator():
|
|
294
|
+
op_is_change = True
|
|
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)
|
|
205
300
|
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
301
|
+
# Add host-specific operation data to state, this mutates state
|
|
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
|
|
210
308
|
|
|
211
|
-
|
|
212
|
-
commands = func(*args, **kwargs)
|
|
213
|
-
commands = [ # convert any strings -> StringCommand's
|
|
214
|
-
StringCommand(command.strip()) if isinstance(command, str) else command
|
|
215
|
-
for command in commands
|
|
216
|
-
]
|
|
309
|
+
operation_meta = OperationMeta(op_hash, op_is_change)
|
|
217
310
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
host
|
|
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)
|
|
221
314
|
|
|
222
|
-
#
|
|
223
|
-
|
|
315
|
+
# If we're already in the execution phase, execute this operation immediately
|
|
316
|
+
if state.is_executing:
|
|
317
|
+
execute_immediately(state, host, op_hash)
|
|
224
318
|
|
|
225
319
|
# Return result meta for use in deploy scripts
|
|
226
320
|
return operation_meta
|
|
227
321
|
|
|
228
|
-
decorated_func.
|
|
229
|
-
return decorated_func
|
|
322
|
+
decorated_func._inner = func # type: ignore[attr-defined]
|
|
323
|
+
return cast(PyinfraOperation[P], decorated_func)
|
|
230
324
|
|
|
231
325
|
|
|
232
|
-
def
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
# If this is a legacy operation function (ie - state & host arg kwargs), ensure that state
|
|
238
|
-
# and host are included as kwargs.
|
|
239
|
-
|
|
240
|
-
# Legacy operation arguments
|
|
241
|
-
if op_func.is_legacy:
|
|
242
|
-
if "state" not in kwargs:
|
|
243
|
-
kwargs["state"] = state
|
|
244
|
-
if "host" not in kwargs:
|
|
245
|
-
kwargs["host"] = host
|
|
246
|
-
# If not legacy, pop off any state/host kwargs that may come from legacy @deploy functions
|
|
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__)
|
|
247
331
|
else:
|
|
248
|
-
|
|
249
|
-
kwargs.pop("host", None)
|
|
250
|
-
|
|
251
|
-
return kwargs
|
|
332
|
+
return func.__name__
|
|
252
333
|
|
|
253
334
|
|
|
254
|
-
def
|
|
335
|
+
def generate_operation_name(func, host, kwargs, global_arguments):
|
|
255
336
|
# Generate an operation name if needed (Module/Operation format)
|
|
256
|
-
name =
|
|
337
|
+
name = global_arguments.get("name")
|
|
257
338
|
add_args = False
|
|
258
339
|
if name:
|
|
259
340
|
names = {name}
|
|
260
341
|
else:
|
|
261
342
|
add_args = True
|
|
262
|
-
|
|
263
|
-
if func.__module__:
|
|
264
|
-
module_bits = func.__module__.split(".")
|
|
265
|
-
module_name = module_bits[-1]
|
|
266
|
-
name = "{0}/{1}".format(module_name.title(), func.__name__.title())
|
|
267
|
-
else:
|
|
268
|
-
name = func.__name__
|
|
269
|
-
|
|
343
|
+
name = get_operation_name_from_func(func)
|
|
270
344
|
names = {name}
|
|
271
345
|
|
|
272
346
|
if host.current_deploy_name:
|
|
@@ -275,7 +349,7 @@ def _generate_operation_name(func, host, kwargs, global_kwargs):
|
|
|
275
349
|
return names, add_args
|
|
276
350
|
|
|
277
351
|
|
|
278
|
-
def
|
|
352
|
+
def solve_operation_consistency(names, state, host):
|
|
279
353
|
# Operation order is used to tie-break available nodes in the operation DAG, in CLI mode
|
|
280
354
|
# we use stack call order so this matches as defined by the user deploy code.
|
|
281
355
|
if pyinfra.is_cli:
|
|
@@ -310,87 +384,55 @@ def _solve_operation_consistency(names, state, host):
|
|
|
310
384
|
|
|
311
385
|
|
|
312
386
|
# NOTE: this function mutates state.op_meta for this hash
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
op_meta_value = op_meta.get(key, op_meta_default)
|
|
387
|
+
def ensure_shared_op_meta(
|
|
388
|
+
state: State,
|
|
389
|
+
op_hash: str,
|
|
390
|
+
op_order: tuple[int, ...],
|
|
391
|
+
global_arguments: AllArguments,
|
|
392
|
+
names: set[str],
|
|
393
|
+
):
|
|
394
|
+
op_meta = state.op_meta.setdefault(op_hash, StateOperationMeta(op_order))
|
|
395
|
+
|
|
396
|
+
for key in EXECUTION_KWARG_KEYS:
|
|
397
|
+
global_value = global_arguments.pop(key) # type: ignore[misc]
|
|
398
|
+
op_meta_value = op_meta.global_arguments.get(key, op_meta_default)
|
|
326
399
|
|
|
327
400
|
if op_meta_value is not op_meta_default and global_value != op_meta_value:
|
|
328
401
|
raise OperationValueError("Cannot have different values for `{0}`.".format(key))
|
|
329
402
|
|
|
330
|
-
op_meta[key] = global_value
|
|
403
|
+
op_meta.global_arguments[key] = global_value # type: ignore[literal-required]
|
|
331
404
|
|
|
332
405
|
# Add any new names to the set
|
|
333
|
-
op_meta
|
|
406
|
+
op_meta.names.update(names)
|
|
334
407
|
|
|
335
408
|
return op_meta
|
|
336
409
|
|
|
337
410
|
|
|
338
|
-
def
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"https://docs.pyinfra.com/en/2.x/using-operations.html#nested-operations",
|
|
343
|
-
)
|
|
344
|
-
op_data["parent_op_hash"] = host.executing_op_hash
|
|
411
|
+
def execute_immediately(state, host, op_hash):
|
|
412
|
+
op_meta = state.get_op_meta(op_hash)
|
|
413
|
+
op_data = state.get_op_data_for_host(host, op_hash)
|
|
414
|
+
op_data.parent_op_hash = host.executing_op_hash
|
|
345
415
|
log_operation_start(op_meta, op_types=["nested"], prefix="")
|
|
346
|
-
|
|
347
|
-
if status is False:
|
|
348
|
-
state.fail_hosts({host})
|
|
416
|
+
run_host_op(state, host, op_hash)
|
|
349
417
|
|
|
350
418
|
|
|
351
|
-
def
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
419
|
+
def _get_arg_value(arg):
|
|
420
|
+
if isinstance(arg, FunctionType):
|
|
421
|
+
return arg.__name__
|
|
422
|
+
if isinstance(arg, StringIO):
|
|
423
|
+
return f"StringIO(hash={get_file_sha1(arg)})"
|
|
424
|
+
return arg
|
|
355
425
|
|
|
356
|
-
|
|
357
|
-
|
|
426
|
+
|
|
427
|
+
def attach_args(op_meta, args, kwargs):
|
|
428
|
+
for arg in args:
|
|
429
|
+
if arg not in op_meta.args:
|
|
430
|
+
op_meta.args.append(str(_get_arg_value(arg)))
|
|
358
431
|
|
|
359
432
|
# Attach keyword args
|
|
360
433
|
for key, value in kwargs.items():
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
arg = "=".join((str(key), str(value)))
|
|
365
|
-
if arg not in op_meta["args"]:
|
|
366
|
-
op_meta["args"].append(arg)
|
|
434
|
+
arg = "=".join((str(key), str(_get_arg_value(value))))
|
|
435
|
+
if arg not in op_meta.args:
|
|
436
|
+
op_meta.args.append(arg)
|
|
367
437
|
|
|
368
438
|
return op_meta
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
# NOTE: this function mutates state.meta for this host
|
|
372
|
-
def _update_state_meta(state, host, commands, op_hash, op_meta, global_kwargs):
|
|
373
|
-
# We're doing some commands, meta/ops++
|
|
374
|
-
state.meta[host]["ops"] += 1
|
|
375
|
-
state.meta[host]["commands"] += len(commands)
|
|
376
|
-
|
|
377
|
-
if commands:
|
|
378
|
-
state.meta[host]["ops_change"] += 1
|
|
379
|
-
else:
|
|
380
|
-
state.meta[host]["ops_no_change"] += 1
|
|
381
|
-
|
|
382
|
-
operation_meta = OperationMeta(op_hash, commands)
|
|
383
|
-
|
|
384
|
-
# Add the server-relevant commands
|
|
385
|
-
op_data = {
|
|
386
|
-
"commands": commands,
|
|
387
|
-
"global_kwargs": global_kwargs,
|
|
388
|
-
"operation_meta": operation_meta,
|
|
389
|
-
}
|
|
390
|
-
state.set_op_data(host, op_hash, op_data)
|
|
391
|
-
|
|
392
|
-
# If we're already in the execution phase, execute this operation immediately
|
|
393
|
-
if state.is_executing:
|
|
394
|
-
_execute_immediately(state, host, op_data, op_meta, op_hash)
|
|
395
|
-
|
|
396
|
-
return operation_meta
|