pyinfra 2.9.2__py2.py3-none-any.whl → 3.0b1__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 +261 -255
- pyinfra/api/arguments_typed.py +77 -0
- pyinfra/api/command.py +66 -53
- pyinfra/api/config.py +27 -22
- pyinfra/api/connect.py +1 -1
- pyinfra/api/connectors.py +2 -24
- pyinfra/api/deploy.py +21 -52
- pyinfra/api/exceptions.py +33 -8
- pyinfra/api/facts.py +77 -113
- pyinfra/api/host.py +150 -82
- pyinfra/api/inventory.py +17 -25
- pyinfra/api/operation.py +232 -198
- pyinfra/api/operations.py +102 -148
- pyinfra/api/state.py +137 -79
- pyinfra/api/util.py +55 -70
- pyinfra/connectors/base.py +150 -0
- pyinfra/connectors/chroot.py +160 -169
- pyinfra/connectors/docker.py +227 -237
- pyinfra/connectors/dockerssh.py +231 -253
- pyinfra/connectors/local.py +195 -207
- pyinfra/connectors/ssh.py +528 -615
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +5 -3
- pyinfra/connectors/terraform.py +86 -65
- pyinfra/connectors/util.py +212 -137
- pyinfra/connectors/vagrant.py +55 -48
- pyinfra/context.py +3 -2
- pyinfra/facts/docker.py +1 -0
- pyinfra/facts/files.py +45 -32
- pyinfra/facts/git.py +3 -1
- pyinfra/facts/gpg.py +1 -1
- pyinfra/facts/hardware.py +4 -2
- pyinfra/facts/iptables.py +5 -3
- pyinfra/facts/mysql.py +1 -0
- pyinfra/facts/postgres.py +168 -0
- pyinfra/facts/postgresql.py +5 -161
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +77 -30
- pyinfra/facts/systemd.py +29 -12
- pyinfra/facts/sysvinit.py +10 -10
- pyinfra/facts/util/packaging.py +4 -2
- pyinfra/local.py +4 -5
- pyinfra/operations/apk.py +3 -3
- pyinfra/operations/apt.py +25 -47
- pyinfra/operations/brew.py +7 -14
- pyinfra/operations/bsdinit.py +4 -4
- pyinfra/operations/cargo.py +1 -1
- pyinfra/operations/choco.py +1 -1
- pyinfra/operations/dnf.py +4 -4
- pyinfra/operations/files.py +108 -321
- pyinfra/operations/gem.py +1 -1
- pyinfra/operations/git.py +6 -37
- pyinfra/operations/iptables.py +2 -10
- pyinfra/operations/launchd.py +1 -1
- pyinfra/operations/lxd.py +1 -9
- pyinfra/operations/mysql.py +5 -28
- pyinfra/operations/npm.py +1 -1
- pyinfra/operations/openrc.py +1 -1
- pyinfra/operations/pacman.py +3 -3
- pyinfra/operations/pip.py +14 -15
- pyinfra/operations/pkg.py +1 -1
- pyinfra/operations/pkgin.py +3 -3
- pyinfra/operations/postgres.py +347 -0
- pyinfra/operations/postgresql.py +17 -380
- pyinfra/operations/python.py +2 -17
- pyinfra/operations/selinux.py +5 -28
- pyinfra/operations/server.py +59 -84
- pyinfra/operations/snap.py +1 -3
- pyinfra/operations/ssh.py +8 -23
- pyinfra/operations/systemd.py +7 -7
- pyinfra/operations/sysvinit.py +3 -12
- pyinfra/operations/upstart.py +4 -4
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/files.py +2 -2
- pyinfra/operations/util/packaging.py +6 -24
- pyinfra/operations/util/service.py +18 -37
- pyinfra/operations/vzctl.py +2 -2
- pyinfra/operations/xbps.py +3 -3
- pyinfra/operations/yum.py +4 -4
- pyinfra/operations/zypper.py +4 -4
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/METADATA +19 -22
- pyinfra-3.0b1.dist-info/RECORD +163 -0
- pyinfra-3.0b1.dist-info/entry_points.txt +11 -0
- pyinfra_cli/__main__.py +2 -0
- pyinfra_cli/commands.py +7 -2
- pyinfra_cli/exceptions.py +83 -42
- pyinfra_cli/inventory.py +19 -4
- pyinfra_cli/log.py +17 -3
- pyinfra_cli/main.py +133 -90
- pyinfra_cli/prints.py +93 -129
- pyinfra_cli/util.py +60 -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 +100 -200
- 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 +66 -107
- tests/test_connectors/test_terraform.py +9 -15
- tests/test_connectors/test_util.py +24 -46
- tests/test_connectors/test_vagrant.py +4 -4
- 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.2.dist-info/RECORD +0 -170
- pyinfra-2.9.2.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.2.dist-info → pyinfra-3.0b1.dist-info}/LICENSE.md +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/WHEEL +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/top_level.txt +0 -0
pyinfra/api/operation.py
CHANGED
|
@@ -5,83 +5,144 @@ 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] = False
|
|
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
|
+
def _did_change(self) -> bool:
|
|
91
|
+
return bool(self._success and len(self._commands or []) > 0)
|
|
60
92
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
93
|
+
@property
|
|
94
|
+
def did_change(self):
|
|
95
|
+
return context.host.when(self._did_change)
|
|
64
96
|
|
|
65
|
-
|
|
97
|
+
@property
|
|
98
|
+
def did_not_change(self):
|
|
99
|
+
return context.host.when(lambda: not self._did_change())
|
|
100
|
+
|
|
101
|
+
def did_succeed(self) -> bool:
|
|
102
|
+
self._raise_if_not_complete()
|
|
103
|
+
return self._success is True
|
|
104
|
+
|
|
105
|
+
def did_error(self) -> bool:
|
|
106
|
+
self._raise_if_not_complete()
|
|
107
|
+
return self._success is False
|
|
66
108
|
|
|
109
|
+
# TODO: deprecated, remove in v4
|
|
67
110
|
@property
|
|
68
|
-
def
|
|
69
|
-
|
|
111
|
+
def changed(self) -> bool:
|
|
112
|
+
if self.is_complete():
|
|
113
|
+
return self._did_change()
|
|
114
|
+
|
|
115
|
+
if self._maybe_is_change is not None:
|
|
116
|
+
return self._maybe_is_change
|
|
117
|
+
|
|
118
|
+
op_data = context.state.get_op_data_for_host(context.host, self._hash)
|
|
119
|
+
cmd_gen = op_data.command_generator
|
|
120
|
+
for _ in cmd_gen():
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
70
123
|
|
|
71
124
|
@property
|
|
72
|
-
def
|
|
73
|
-
|
|
125
|
+
def stdout_lines(self) -> list[str]:
|
|
126
|
+
self._raise_if_not_complete()
|
|
127
|
+
assert self._combined_output is not None
|
|
128
|
+
return self._combined_output.stdout_lines
|
|
74
129
|
|
|
75
130
|
@property
|
|
76
|
-
def
|
|
131
|
+
def stderr_lines(self) -> list[str]:
|
|
132
|
+
self._raise_if_not_complete()
|
|
133
|
+
assert self._combined_output is not None
|
|
134
|
+
return self._combined_output.stderr_lines
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def stdout(self) -> str:
|
|
77
138
|
return "\n".join(self.stdout_lines)
|
|
78
139
|
|
|
79
140
|
@property
|
|
80
|
-
def stderr(self):
|
|
141
|
+
def stderr(self) -> str:
|
|
81
142
|
return "\n".join(self.stderr_lines)
|
|
82
143
|
|
|
83
144
|
|
|
84
|
-
def add_op(state:
|
|
145
|
+
def add_op(state: State, op_func, *args, **kwargs):
|
|
85
146
|
"""
|
|
86
147
|
Prepare & add an operation to ``pyinfra.state`` by executing it on all hosts.
|
|
87
148
|
|
|
@@ -112,85 +173,71 @@ def add_op(state: "State", op_func, *args, **kwargs):
|
|
|
112
173
|
return results
|
|
113
174
|
|
|
114
175
|
|
|
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
|
-
)
|
|
176
|
+
P = ParamSpec("P")
|
|
123
177
|
|
|
124
178
|
|
|
125
179
|
def operation(
|
|
126
|
-
func=None,
|
|
127
|
-
pipeline_facts=None,
|
|
128
180
|
is_idempotent: bool = True,
|
|
129
|
-
idempotent_notice=None,
|
|
130
|
-
|
|
131
|
-
|
|
181
|
+
idempotent_notice: Optional[str] = None,
|
|
182
|
+
is_deprecated: bool = False,
|
|
183
|
+
deprecated_for: Optional[str] = None,
|
|
184
|
+
_set_in_op: bool = True,
|
|
185
|
+
) -> Callable[[Callable[P, Generator]], PyinfraOperation[P]]:
|
|
132
186
|
"""
|
|
133
187
|
Decorator that takes a simple module function and turn it into the internal
|
|
134
188
|
operation representation that consists of a list of commands + options
|
|
135
189
|
(sudo, (sudo|su)_user, env).
|
|
136
190
|
"""
|
|
137
191
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
f.idempotent_notice = idempotent_notice
|
|
145
|
-
return operation(f, frame_offset=2)
|
|
192
|
+
def decorator(f: Callable[P, Generator]) -> PyinfraOperation[P]:
|
|
193
|
+
f.is_idempotent = is_idempotent # type: ignore[attr-defined]
|
|
194
|
+
f.idempotent_notice = idempotent_notice # type: ignore[attr-defined]
|
|
195
|
+
f.is_deprecated = is_deprecated # type: ignore[attr-defined]
|
|
196
|
+
f.deprecated_for = deprecated_for # type: ignore[attr-defined]
|
|
197
|
+
return _wrap_operation(f, _set_in_op=_set_in_op)
|
|
146
198
|
|
|
147
|
-
|
|
199
|
+
return decorator
|
|
148
200
|
|
|
149
|
-
# Check whether an operation is "legacy" - ie contains state=None, host=None kwargs
|
|
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
201
|
|
|
158
|
-
|
|
202
|
+
def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> PyinfraOperation[P]:
|
|
159
203
|
@wraps(func)
|
|
160
|
-
def decorated_func(*args, **kwargs):
|
|
204
|
+
def decorated_func(*args: P.args, **kwargs: P.kwargs) -> OperationMeta:
|
|
161
205
|
state = context.state
|
|
162
206
|
host = context.host
|
|
163
207
|
|
|
208
|
+
if host.in_op:
|
|
209
|
+
raise Exception(
|
|
210
|
+
"Operation called within another operation, this is not allowed! Use the `_inner` "
|
|
211
|
+
+ "function to call the underlying operation."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if func.is_deprecated: # type: ignore[attr-defined]
|
|
215
|
+
if func.deprecated_for: # type: ignore[attr-defined]
|
|
216
|
+
logger.warning(
|
|
217
|
+
f"The {get_operation_name_from_func(func)} operation is "
|
|
218
|
+
+ f"deprecated, please use: {func.deprecated_for}", # type: ignore[attr-defined] # noqa
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
logger.warning(f"The {get_operation_name_from_func(func)} operation is deprecated")
|
|
222
|
+
|
|
164
223
|
# Configure operation
|
|
165
224
|
#
|
|
166
225
|
# Get the meta kwargs (globals that apply to all hosts)
|
|
167
|
-
|
|
226
|
+
global_arguments, global_argument_keys = pop_global_arguments(kwargs)
|
|
168
227
|
|
|
169
|
-
|
|
170
|
-
|
|
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 []
|
|
179
|
-
|
|
180
|
-
kwargs = _solve_legacy_operation_arguments(func, state, host, kwargs)
|
|
181
|
-
names, add_args = _generate_operation_name(func, host, kwargs, global_kwargs)
|
|
182
|
-
op_order, op_hash = _solve_operation_consistency(names, state, host)
|
|
228
|
+
names, add_args = generate_operation_name(func, host, kwargs, global_arguments)
|
|
229
|
+
op_order, op_hash = solve_operation_consistency(names, state, host)
|
|
183
230
|
|
|
184
231
|
# Ensure shared (between servers) operation meta, mutates state
|
|
185
|
-
op_meta =
|
|
232
|
+
op_meta = ensure_shared_op_meta(state, op_hash, op_order, global_arguments, names)
|
|
186
233
|
|
|
187
234
|
# Attach normal args, if we're auto-naming this operation
|
|
188
235
|
if add_args:
|
|
189
|
-
op_meta =
|
|
236
|
+
op_meta = attach_args(op_meta, args, kwargs)
|
|
190
237
|
|
|
191
238
|
# Check if we're actually running the operation on this host
|
|
192
239
|
# Run once and we've already added meta for this op? Stop here.
|
|
193
|
-
if op_meta["
|
|
240
|
+
if op_meta.global_arguments["_run_once"]:
|
|
194
241
|
has_run = False
|
|
195
242
|
for ops in state.ops.values():
|
|
196
243
|
if op_hash in ops:
|
|
@@ -198,75 +245,94 @@ def operation(
|
|
|
198
245
|
break
|
|
199
246
|
|
|
200
247
|
if has_run:
|
|
201
|
-
return OperationMeta(op_hash)
|
|
202
|
-
|
|
203
|
-
#
|
|
204
|
-
#
|
|
248
|
+
return OperationMeta(op_hash, is_change=False)
|
|
249
|
+
|
|
250
|
+
# Grab a reference to any *current* deploy data as this may change when
|
|
251
|
+
# we later evaluate the operation at runtime.This means we put back the
|
|
252
|
+
# expected deploy data.
|
|
253
|
+
current_deploy_data = host.current_deploy_data
|
|
254
|
+
|
|
255
|
+
# "Run" operation - here we make a generator that will yield out actual commands to execute
|
|
256
|
+
# and, if we're diff-ing, we then iterate the generator now to determine if any changes
|
|
257
|
+
# *would* be made based on the *current* remote state.
|
|
258
|
+
|
|
259
|
+
def command_generator() -> Iterator[PyinfraCommand]:
|
|
260
|
+
# Check global _if_ argument function and do nothing if returns False
|
|
261
|
+
if state.is_executing:
|
|
262
|
+
_ifs = global_arguments.get("_if")
|
|
263
|
+
if _ifs and not all(_if() for _if in _ifs):
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
host.in_op = _set_in_op
|
|
267
|
+
host.current_op_hash = op_hash
|
|
268
|
+
host.current_op_global_arguments = global_arguments
|
|
269
|
+
host.current_op_deploy_data = current_deploy_data
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
for command in func(*args, **kwargs):
|
|
273
|
+
if isinstance(command, str):
|
|
274
|
+
command = StringCommand(command.strip())
|
|
275
|
+
yield command
|
|
276
|
+
finally:
|
|
277
|
+
host.in_op = False
|
|
278
|
+
host.current_op_hash = None
|
|
279
|
+
host.current_op_global_arguments = None
|
|
280
|
+
host.current_op_deploy_data = None
|
|
281
|
+
|
|
282
|
+
op_is_change = None
|
|
283
|
+
if state.should_check_for_changes():
|
|
284
|
+
op_is_change = False
|
|
285
|
+
for _ in command_generator():
|
|
286
|
+
op_is_change = True
|
|
287
|
+
break
|
|
288
|
+
else:
|
|
289
|
+
# If not calling the op function to check for change we still want to ensure the args
|
|
290
|
+
# are valid, so use Signature.bind to trigger any TypeError.
|
|
291
|
+
signature(func).bind(*args, **kwargs)
|
|
205
292
|
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
293
|
+
# Add host-specific operation data to state, this mutates state
|
|
294
|
+
host_meta = state.get_meta_for_host(host)
|
|
295
|
+
host_meta.ops += 1
|
|
296
|
+
if op_is_change:
|
|
297
|
+
host_meta.ops_change += 1
|
|
298
|
+
else:
|
|
299
|
+
host_meta.ops_no_change += 1
|
|
210
300
|
|
|
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
|
-
]
|
|
301
|
+
operation_meta = OperationMeta(op_hash, op_is_change)
|
|
217
302
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
host
|
|
303
|
+
# Add the server-relevant commands
|
|
304
|
+
op_data = StateOperationHostData(command_generator, global_arguments, operation_meta)
|
|
305
|
+
state.set_op_data_for_host(host, op_hash, op_data)
|
|
221
306
|
|
|
222
|
-
#
|
|
223
|
-
|
|
307
|
+
# If we're already in the execution phase, execute this operation immediately
|
|
308
|
+
if state.is_executing:
|
|
309
|
+
execute_immediately(state, host, op_hash)
|
|
224
310
|
|
|
225
311
|
# Return result meta for use in deploy scripts
|
|
226
312
|
return operation_meta
|
|
227
313
|
|
|
228
|
-
decorated_func.
|
|
229
|
-
return decorated_func
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _solve_legacy_operation_arguments(op_func, state, host, kwargs):
|
|
233
|
-
"""
|
|
234
|
-
Solve legacy operation arguments.
|
|
235
|
-
"""
|
|
314
|
+
decorated_func._inner = func # type: ignore[attr-defined]
|
|
315
|
+
return cast(PyinfraOperation[P], decorated_func)
|
|
236
316
|
|
|
237
|
-
# If this is a legacy operation function (ie - state & host arg kwargs), ensure that state
|
|
238
|
-
# and host are included as kwargs.
|
|
239
317
|
|
|
240
|
-
|
|
241
|
-
if
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
kwargs["host"] = host
|
|
246
|
-
# If not legacy, pop off any state/host kwargs that may come from legacy @deploy functions
|
|
318
|
+
def get_operation_name_from_func(func):
|
|
319
|
+
if func.__module__:
|
|
320
|
+
module_bits = func.__module__.split(".")
|
|
321
|
+
module_name = module_bits[-1]
|
|
322
|
+
return "{0}.{1}".format(module_name, func.__name__)
|
|
247
323
|
else:
|
|
248
|
-
|
|
249
|
-
kwargs.pop("host", None)
|
|
324
|
+
return func.__name__
|
|
250
325
|
|
|
251
|
-
return kwargs
|
|
252
326
|
|
|
253
|
-
|
|
254
|
-
def _generate_operation_name(func, host, kwargs, global_kwargs):
|
|
327
|
+
def generate_operation_name(func, host, kwargs, global_arguments):
|
|
255
328
|
# Generate an operation name if needed (Module/Operation format)
|
|
256
|
-
name =
|
|
329
|
+
name = global_arguments.get("name")
|
|
257
330
|
add_args = False
|
|
258
331
|
if name:
|
|
259
332
|
names = {name}
|
|
260
333
|
else:
|
|
261
334
|
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
|
-
|
|
335
|
+
name = get_operation_name_from_func(func)
|
|
270
336
|
names = {name}
|
|
271
337
|
|
|
272
338
|
if host.current_deploy_name:
|
|
@@ -275,7 +341,7 @@ def _generate_operation_name(func, host, kwargs, global_kwargs):
|
|
|
275
341
|
return names, add_args
|
|
276
342
|
|
|
277
343
|
|
|
278
|
-
def
|
|
344
|
+
def solve_operation_consistency(names, state, host):
|
|
279
345
|
# Operation order is used to tie-break available nodes in the operation DAG, in CLI mode
|
|
280
346
|
# we use stack call order so this matches as defined by the user deploy code.
|
|
281
347
|
if pyinfra.is_cli:
|
|
@@ -310,87 +376,55 @@ def _solve_operation_consistency(names, state, host):
|
|
|
310
376
|
|
|
311
377
|
|
|
312
378
|
# 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)
|
|
379
|
+
def ensure_shared_op_meta(
|
|
380
|
+
state: State,
|
|
381
|
+
op_hash: str,
|
|
382
|
+
op_order: tuple[int, ...],
|
|
383
|
+
global_arguments: AllArguments,
|
|
384
|
+
names: set[str],
|
|
385
|
+
):
|
|
386
|
+
op_meta = state.op_meta.setdefault(op_hash, StateOperationMeta(op_order))
|
|
387
|
+
|
|
388
|
+
for key in EXECUTION_KWARG_KEYS:
|
|
389
|
+
global_value = global_arguments.pop(key) # type: ignore[misc]
|
|
390
|
+
op_meta_value = op_meta.global_arguments.get(key, op_meta_default)
|
|
326
391
|
|
|
327
392
|
if op_meta_value is not op_meta_default and global_value != op_meta_value:
|
|
328
393
|
raise OperationValueError("Cannot have different values for `{0}`.".format(key))
|
|
329
394
|
|
|
330
|
-
op_meta[key] = global_value
|
|
395
|
+
op_meta.global_arguments[key] = global_value # type: ignore[literal-required]
|
|
331
396
|
|
|
332
397
|
# Add any new names to the set
|
|
333
|
-
op_meta
|
|
398
|
+
op_meta.names.update(names)
|
|
334
399
|
|
|
335
400
|
return op_meta
|
|
336
401
|
|
|
337
402
|
|
|
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
|
|
403
|
+
def execute_immediately(state, host, op_hash):
|
|
404
|
+
op_meta = state.get_op_meta(op_hash)
|
|
405
|
+
op_data = state.get_op_data_for_host(host, op_hash)
|
|
406
|
+
op_data.parent_op_hash = host.executing_op_hash
|
|
345
407
|
log_operation_start(op_meta, op_types=["nested"], prefix="")
|
|
346
|
-
|
|
347
|
-
if status is False:
|
|
348
|
-
state.fail_hosts({host})
|
|
408
|
+
run_host_op(state, host, op_hash)
|
|
349
409
|
|
|
350
410
|
|
|
351
|
-
def
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
411
|
+
def _get_arg_value(arg):
|
|
412
|
+
if isinstance(arg, FunctionType):
|
|
413
|
+
return arg.__name__
|
|
414
|
+
if isinstance(arg, StringIO):
|
|
415
|
+
return f"StringIO(hash={get_file_sha1(arg)})"
|
|
416
|
+
return arg
|
|
417
|
+
|
|
355
418
|
|
|
356
|
-
|
|
357
|
-
|
|
419
|
+
def attach_args(op_meta, args, kwargs):
|
|
420
|
+
for arg in args:
|
|
421
|
+
if arg not in op_meta.args:
|
|
422
|
+
op_meta.args.append(str(_get_arg_value(arg)))
|
|
358
423
|
|
|
359
424
|
# Attach keyword args
|
|
360
425
|
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)
|
|
426
|
+
arg = "=".join((str(key), str(_get_arg_value(value))))
|
|
427
|
+
if arg not in op_meta.args:
|
|
428
|
+
op_meta.args.append(arg)
|
|
367
429
|
|
|
368
430
|
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
|