pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__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/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +18 -3
- pyinfra/api/arguments.py +406 -0
- pyinfra/api/arguments_typed.py +79 -0
- pyinfra/api/command.py +274 -0
- pyinfra/api/config.py +222 -28
- pyinfra/api/connect.py +33 -13
- pyinfra/api/connectors.py +27 -0
- pyinfra/api/deploy.py +65 -66
- pyinfra/api/exceptions.py +67 -18
- pyinfra/api/facts.py +253 -202
- pyinfra/api/host.py +413 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +291 -368
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +381 -0
- pyinfra/connectors/dockerssh.py +297 -0
- pyinfra/connectors/local.py +238 -0
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +670 -0
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +309 -0
- pyinfra/connectors/sshuserclient/config.py +102 -0
- pyinfra/connectors/terraform.py +135 -0
- pyinfra/connectors/util.py +410 -0
- pyinfra/connectors/vagrant.py +183 -0
- pyinfra/context.py +145 -0
- pyinfra/facts/__init__.py +7 -6
- pyinfra/facts/apk.py +22 -7
- pyinfra/facts/apt.py +117 -60
- pyinfra/facts/brew.py +100 -15
- pyinfra/facts/bsdinit.py +23 -0
- pyinfra/facts/cargo.py +37 -0
- pyinfra/facts/choco.py +47 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +94 -0
- pyinfra/facts/dnf.py +48 -0
- pyinfra/facts/docker.py +96 -23
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +630 -58
- pyinfra/facts/flatpak.py +77 -0
- pyinfra/facts/freebsd.py +70 -0
- pyinfra/facts/gem.py +19 -6
- pyinfra/facts/git.py +59 -14
- pyinfra/facts/gpg.py +150 -0
- pyinfra/facts/hardware.py +313 -167
- pyinfra/facts/iptables.py +72 -62
- pyinfra/facts/launchd.py +44 -0
- pyinfra/facts/lxd.py +17 -4
- pyinfra/facts/mysql.py +122 -86
- pyinfra/facts/npm.py +17 -9
- pyinfra/facts/openrc.py +71 -0
- pyinfra/facts/opkg.py +246 -0
- pyinfra/facts/pacman.py +50 -7
- pyinfra/facts/pip.py +24 -7
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +15 -6
- pyinfra/facts/pkgin.py +35 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +178 -0
- pyinfra/facts/postgresql.py +6 -147
- pyinfra/facts/rpm.py +105 -0
- pyinfra/facts/runit.py +77 -0
- pyinfra/facts/selinux.py +161 -0
- pyinfra/facts/server.py +746 -285
- pyinfra/facts/snap.py +88 -0
- pyinfra/facts/systemd.py +139 -0
- pyinfra/facts/sysvinit.py +59 -0
- pyinfra/facts/upstart.py +35 -0
- pyinfra/facts/util/__init__.py +17 -0
- pyinfra/facts/util/databases.py +4 -6
- pyinfra/facts/util/packaging.py +37 -6
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/util/win_files.py +99 -0
- pyinfra/facts/vzctl.py +20 -13
- pyinfra/facts/xbps.py +35 -0
- pyinfra/facts/yum.py +34 -40
- pyinfra/facts/zfs.py +77 -0
- pyinfra/facts/zypper.py +42 -0
- pyinfra/local.py +45 -83
- pyinfra/operations/__init__.py +12 -0
- pyinfra/operations/apk.py +98 -0
- pyinfra/operations/apt.py +488 -0
- pyinfra/operations/brew.py +231 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +191 -0
- pyinfra/operations/dnf.py +210 -0
- pyinfra/operations/docker.py +446 -0
- pyinfra/operations/files.py +1939 -0
- pyinfra/operations/flatpak.py +94 -0
- pyinfra/operations/freebsd/__init__.py +12 -0
- pyinfra/operations/freebsd/freebsd_update.py +70 -0
- pyinfra/operations/freebsd/pkg.py +219 -0
- pyinfra/operations/freebsd/service.py +116 -0
- pyinfra/operations/freebsd/sysrc.py +92 -0
- pyinfra/operations/gem.py +47 -0
- pyinfra/operations/git.py +419 -0
- pyinfra/operations/iptables.py +311 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +68 -0
- pyinfra/operations/mysql.py +609 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pacman.py +81 -0
- pyinfra/operations/pip.py +205 -0
- pyinfra/operations/pipx.py +102 -0
- pyinfra/operations/pkg.py +70 -0
- pyinfra/operations/pkgin.py +91 -0
- pyinfra/operations/postgres.py +436 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +40 -0
- pyinfra/operations/python.py +72 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +189 -0
- pyinfra/operations/server.py +1099 -0
- pyinfra/operations/snap.py +117 -0
- pyinfra/operations/ssh.py +216 -0
- pyinfra/operations/systemd.py +149 -0
- pyinfra/operations/sysvinit.py +141 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +251 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +336 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +77 -0
- pyinfra/operations/yum.py +210 -0
- pyinfra/operations/zfs.py +175 -0
- pyinfra/operations/zypper.py +192 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.5.1.dist-info/METADATA +141 -0
- pyinfra-3.5.1.dist-info/RECORD +159 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
- pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +780 -0
- pyinfra_cli/commands.py +66 -0
- pyinfra_cli/exceptions.py +155 -65
- pyinfra_cli/inventory.py +233 -89
- pyinfra_cli/log.py +39 -43
- pyinfra_cli/main.py +26 -495
- pyinfra_cli/prints.py +215 -156
- pyinfra_cli/util.py +172 -105
- pyinfra_cli/virtualenv.py +25 -20
- pyinfra/api/connectors/__init__.py +0 -21
- pyinfra/api/connectors/ansible.py +0 -99
- pyinfra/api/connectors/docker.py +0 -178
- pyinfra/api/connectors/local.py +0 -169
- pyinfra/api/connectors/ssh.py +0 -402
- pyinfra/api/connectors/sshuserclient/client.py +0 -105
- pyinfra/api/connectors/sshuserclient/config.py +0 -90
- pyinfra/api/connectors/util.py +0 -63
- pyinfra/api/connectors/vagrant.py +0 -155
- pyinfra/facts/init.py +0 -176
- pyinfra/facts/util/files.py +0 -102
- pyinfra/hook.py +0 -41
- pyinfra/modules/__init__.py +0 -11
- pyinfra/modules/apk.py +0 -64
- pyinfra/modules/apt.py +0 -272
- pyinfra/modules/brew.py +0 -122
- pyinfra/modules/files.py +0 -711
- pyinfra/modules/gem.py +0 -30
- pyinfra/modules/git.py +0 -115
- pyinfra/modules/init.py +0 -344
- pyinfra/modules/iptables.py +0 -271
- pyinfra/modules/lxd.py +0 -45
- pyinfra/modules/mysql.py +0 -347
- pyinfra/modules/npm.py +0 -47
- pyinfra/modules/pacman.py +0 -60
- pyinfra/modules/pip.py +0 -99
- pyinfra/modules/pkg.py +0 -43
- pyinfra/modules/postgresql.py +0 -245
- pyinfra/modules/puppet.py +0 -20
- pyinfra/modules/python.py +0 -37
- pyinfra/modules/server.py +0 -524
- pyinfra/modules/ssh.py +0 -150
- pyinfra/modules/util/files.py +0 -52
- pyinfra/modules/util/packaging.py +0 -118
- pyinfra/modules/vzctl.py +0 -133
- pyinfra/modules/yum.py +0 -171
- pyinfra/pseudo_modules.py +0 -64
- pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
- pyinfra-0.11.dev3.dist-info/METADATA +0 -135
- pyinfra-0.11.dev3.dist-info/RECORD +0 -95
- pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
- pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
- pyinfra_cli/__main__.py +0 -40
- pyinfra_cli/config.py +0 -92
- /pyinfra/{modules/util → connectors}/__init__.py +0 -0
- /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/api/operation.py
CHANGED
|
@@ -1,55 +1,210 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
Operations are the core of pyinfra. The ``@operation`` wrapper intercepts calls
|
|
3
3
|
to the function and instead diff against the remote server, outputting commands
|
|
4
4
|
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
|
+
|
|
8
|
+
from __future__ import annotations
|
|
7
9
|
|
|
8
10
|
from functools import wraps
|
|
9
|
-
from inspect import
|
|
10
|
-
from
|
|
11
|
+
from inspect import signature
|
|
12
|
+
from io import StringIO
|
|
11
13
|
from types import FunctionType
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Optional, cast
|
|
15
|
+
|
|
16
|
+
from typing_extensions import ParamSpec, override
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
from pyinfra
|
|
18
|
+
import pyinfra
|
|
19
|
+
from pyinfra import context, logger
|
|
20
|
+
from pyinfra.context import ctx_host, ctx_state
|
|
15
21
|
|
|
16
|
-
from .
|
|
22
|
+
from .arguments import EXECUTION_KWARG_KEYS, AllArguments, pop_global_arguments
|
|
23
|
+
from .arguments_typed import PyinfraOperation
|
|
24
|
+
from .command import PyinfraCommand, StringCommand
|
|
25
|
+
from .exceptions import OperationValueError, PyinfraError
|
|
17
26
|
from .host import Host
|
|
18
|
-
from .
|
|
27
|
+
from .operations import run_host_op
|
|
28
|
+
from .state import State, StateOperationHostData, StateOperationMeta, StateStage
|
|
19
29
|
from .util import (
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
get_call_location,
|
|
31
|
+
get_file_sha1,
|
|
32
|
+
get_operation_order_from_stack,
|
|
33
|
+
log_operation_start,
|
|
22
34
|
make_hash,
|
|
23
|
-
pop_op_kwargs,
|
|
24
|
-
unroll_generators,
|
|
25
35
|
)
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
self.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
op_meta_default = object()
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from pyinfra.connectors.util import CommandOutput
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OperationMeta:
|
|
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
|
|
50
|
+
_retry_attempts: int = 0
|
|
51
|
+
_max_retries: int = 0
|
|
52
|
+
_retry_succeeded: Optional[bool] = None
|
|
53
|
+
|
|
54
|
+
def __init__(self, hash, is_change: Optional[bool]):
|
|
55
|
+
self._hash = hash
|
|
56
|
+
self._maybe_is_change = is_change
|
|
57
|
+
|
|
58
|
+
@override
|
|
59
|
+
def __repr__(self) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Return Operation object as a string.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if self._commands is not None:
|
|
65
|
+
retry_info = ""
|
|
66
|
+
if self._retry_attempts > 0:
|
|
67
|
+
retry_result = "succeeded" if self._retry_succeeded else "failed"
|
|
68
|
+
retry_info = (
|
|
69
|
+
f", retries={self._retry_attempts}/{self._max_retries} ({retry_result})"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
"OperationMeta(executed=True, "
|
|
74
|
+
f"success={self.did_succeed()}, hash={self._hash}, "
|
|
75
|
+
f"commands={len(self._commands)}{retry_info})"
|
|
76
|
+
)
|
|
77
|
+
return (
|
|
78
|
+
f"OperationMeta(executed=False, maybeChange={self._maybe_is_change}, hash={self._hash})"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Completion & status checks
|
|
82
|
+
def set_complete(
|
|
83
|
+
self,
|
|
84
|
+
success: bool,
|
|
85
|
+
commands: list[Any],
|
|
86
|
+
combined_output: "CommandOutput",
|
|
87
|
+
retry_attempts: int = 0,
|
|
88
|
+
max_retries: int = 0,
|
|
89
|
+
) -> None:
|
|
90
|
+
if self.is_complete():
|
|
91
|
+
raise RuntimeError("Cannot complete an already complete operation")
|
|
92
|
+
self._success = success
|
|
93
|
+
self._commands = commands
|
|
94
|
+
self._combined_output = combined_output
|
|
95
|
+
self._retry_attempts = retry_attempts
|
|
96
|
+
self._max_retries = max_retries
|
|
97
|
+
|
|
98
|
+
# Determine if operation succeeded after retries
|
|
99
|
+
if retry_attempts > 0:
|
|
100
|
+
self._retry_succeeded = success
|
|
101
|
+
|
|
102
|
+
def is_complete(self) -> bool:
|
|
103
|
+
return self._success is not None
|
|
104
|
+
|
|
105
|
+
def _raise_if_not_complete(self) -> None:
|
|
106
|
+
if not self.is_complete():
|
|
107
|
+
raise RuntimeError("Cannot evaluate operation result before execution")
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def executed(self) -> bool:
|
|
111
|
+
if self._commands is None:
|
|
112
|
+
return False
|
|
113
|
+
return len(self._commands) > 0
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def will_change(self) -> bool:
|
|
117
|
+
if self._maybe_is_change is not None:
|
|
118
|
+
return self._maybe_is_change
|
|
119
|
+
|
|
120
|
+
op_data = context.state.get_op_data_for_host(context.host, self._hash)
|
|
121
|
+
cmd_gen = op_data.command_generator
|
|
122
|
+
for _ in cmd_gen():
|
|
123
|
+
self._maybe_is_change = True
|
|
124
|
+
return True
|
|
125
|
+
self._maybe_is_change = False
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def did_change(self) -> bool:
|
|
129
|
+
self._raise_if_not_complete()
|
|
130
|
+
return bool(self._success and len(self._commands or []) > 0)
|
|
131
|
+
|
|
132
|
+
def did_not_change(self) -> bool:
|
|
133
|
+
return not self.did_change()
|
|
134
|
+
|
|
135
|
+
def did_succeed(self, _raise_if_not_complete=True) -> bool:
|
|
136
|
+
if _raise_if_not_complete:
|
|
137
|
+
self._raise_if_not_complete()
|
|
138
|
+
return self._success is True
|
|
139
|
+
|
|
140
|
+
def did_error(self) -> bool:
|
|
141
|
+
self._raise_if_not_complete()
|
|
142
|
+
return self._success is False
|
|
143
|
+
|
|
144
|
+
# TODO: deprecated, remove in v4
|
|
145
|
+
@property
|
|
146
|
+
def changed(self) -> bool:
|
|
147
|
+
if self.is_complete():
|
|
148
|
+
return self.did_change()
|
|
149
|
+
return self.will_change
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def stdout_lines(self) -> list[str]:
|
|
153
|
+
self._raise_if_not_complete()
|
|
154
|
+
assert self._combined_output is not None
|
|
155
|
+
return self._combined_output.stdout_lines
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def stderr_lines(self) -> list[str]:
|
|
159
|
+
self._raise_if_not_complete()
|
|
160
|
+
assert self._combined_output is not None
|
|
161
|
+
return self._combined_output.stderr_lines
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def stdout(self) -> str:
|
|
165
|
+
return "\n".join(self.stdout_lines)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def stderr(self) -> str:
|
|
169
|
+
return "\n".join(self.stderr_lines)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def retry_attempts(self) -> int:
|
|
173
|
+
return self._retry_attempts
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def max_retries(self) -> int:
|
|
177
|
+
return self._max_retries
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def was_retried(self) -> bool:
|
|
181
|
+
"""
|
|
182
|
+
Returns whether this operation was retried at least once.
|
|
183
|
+
"""
|
|
184
|
+
return self._retry_attempts > 0
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def retry_succeeded(self) -> Optional[bool]:
|
|
188
|
+
"""
|
|
189
|
+
Returns whether this operation succeeded after retries.
|
|
190
|
+
Returns None if the operation was not retried.
|
|
191
|
+
"""
|
|
192
|
+
return self._retry_succeeded
|
|
193
|
+
|
|
194
|
+
def get_retry_info(self) -> dict[str, Any]:
|
|
195
|
+
"""
|
|
196
|
+
Returns a dictionary with all retry-related information.
|
|
197
|
+
"""
|
|
198
|
+
return {
|
|
199
|
+
"retry_attempts": self._retry_attempts,
|
|
200
|
+
"max_retries": self._max_retries,
|
|
201
|
+
"was_retried": self.was_retried,
|
|
202
|
+
"retry_succeeded": self._retry_succeeded,
|
|
203
|
+
}
|
|
49
204
|
|
|
50
205
|
|
|
51
|
-
def add_op(state, op_func, *args, **kwargs):
|
|
52
|
-
|
|
206
|
+
def add_op(state: State, op_func, *args, **kwargs):
|
|
207
|
+
"""
|
|
53
208
|
Prepare & add an operation to ``pyinfra.state`` by executing it on all hosts.
|
|
54
209
|
|
|
55
210
|
Args:
|
|
@@ -57,198 +212,96 @@ def add_op(state, op_func, *args, **kwargs):
|
|
|
57
212
|
to op_func (function): the operation function from one of the modules,
|
|
58
213
|
ie ``server.user``
|
|
59
214
|
args/kwargs: passed to the operation function
|
|
60
|
-
|
|
215
|
+
"""
|
|
61
216
|
|
|
62
|
-
|
|
63
|
-
|
|
217
|
+
if pyinfra.is_cli:
|
|
218
|
+
raise PyinfraError(
|
|
219
|
+
("`add_op` should not be called when pyinfra is executing in CLI mode! ({0})").format(
|
|
220
|
+
get_call_location(),
|
|
221
|
+
),
|
|
222
|
+
)
|
|
64
223
|
|
|
65
|
-
|
|
66
|
-
|
|
224
|
+
hosts = kwargs.pop("host", state.inventory.iter_active_hosts())
|
|
225
|
+
if isinstance(hosts, Host):
|
|
226
|
+
hosts = [hosts]
|
|
67
227
|
|
|
228
|
+
with ctx_state.use(state):
|
|
229
|
+
results = {}
|
|
230
|
+
for op_host in hosts:
|
|
231
|
+
with ctx_host.use(op_host):
|
|
232
|
+
results[op_host] = op_func(*args, **kwargs)
|
|
68
233
|
|
|
69
|
-
|
|
70
|
-
frames = stack()
|
|
234
|
+
return results
|
|
71
235
|
|
|
72
|
-
# Frist two frames are this and the caller below, so get the third item on
|
|
73
|
-
# the frame list, which should be the call to the actual operation.
|
|
74
|
-
frame = getframeinfo(frames[2][0])
|
|
75
236
|
|
|
76
|
-
|
|
77
|
-
frame.lineno,
|
|
78
|
-
path.relpath(frame.filename),
|
|
79
|
-
)
|
|
237
|
+
P = ParamSpec("P")
|
|
80
238
|
|
|
81
239
|
|
|
82
|
-
def operation(
|
|
83
|
-
|
|
240
|
+
def operation(
|
|
241
|
+
is_idempotent: bool = True,
|
|
242
|
+
idempotent_notice: Optional[str] = None,
|
|
243
|
+
is_deprecated: bool = False,
|
|
244
|
+
deprecated_for: Optional[str] = None,
|
|
245
|
+
_set_in_op: bool = True,
|
|
246
|
+
) -> Callable[[Callable[P, Generator]], PyinfraOperation[P]]:
|
|
247
|
+
"""
|
|
84
248
|
Decorator that takes a simple module function and turn it into the internal
|
|
85
249
|
operation representation that consists of a list of commands + options
|
|
86
250
|
(sudo, (sudo|su)_user, env).
|
|
87
|
-
|
|
251
|
+
"""
|
|
88
252
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
253
|
+
def decorator(f: Callable[P, Generator]) -> PyinfraOperation[P]:
|
|
254
|
+
f.is_idempotent = is_idempotent # type: ignore[attr-defined]
|
|
255
|
+
f.idempotent_notice = idempotent_notice # type: ignore[attr-defined]
|
|
256
|
+
f.is_deprecated = is_deprecated # type: ignore[attr-defined]
|
|
257
|
+
f.deprecated_for = deprecated_for # type: ignore[attr-defined]
|
|
258
|
+
return _wrap_operation(f, _set_in_op=_set_in_op)
|
|
94
259
|
|
|
95
|
-
|
|
260
|
+
return decorator
|
|
96
261
|
|
|
97
|
-
# Index the operation!
|
|
98
|
-
module_bits = func.__module__.split('.')
|
|
99
|
-
module_name = module_bits[-1]
|
|
100
|
-
op_name = '.'.join((module_name, func.__name__))
|
|
101
|
-
OPERATIONS.append(op_name)
|
|
102
262
|
|
|
103
|
-
|
|
263
|
+
def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> PyinfraOperation[P]:
|
|
104
264
|
@wraps(func)
|
|
105
|
-
def decorated_func(*args, **kwargs):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
).format(op_name, _get_call_location()))
|
|
127
|
-
|
|
128
|
-
# Otherwise (API mode) we just trim off the commands
|
|
129
|
-
else:
|
|
130
|
-
args_copy = list(args)
|
|
131
|
-
state, host = args[0], args[1]
|
|
132
|
-
args = args_copy[2:]
|
|
133
|
-
|
|
134
|
-
# In API mode we have the kwarg - if a nested operation call we have
|
|
135
|
-
# current_frameinfo.
|
|
136
|
-
frameinfo = kwargs.pop('frameinfo', get_caller_frameinfo())
|
|
265
|
+
def decorated_func(*args: P.args, **kwargs: P.kwargs) -> OperationMeta:
|
|
266
|
+
state = context.state
|
|
267
|
+
host = context.host
|
|
268
|
+
|
|
269
|
+
if state.current_stage < StateStage.Prepare or state.current_stage > StateStage.Execute:
|
|
270
|
+
raise Exception("Cannot call operations outside of Prepare/Execute stages")
|
|
271
|
+
|
|
272
|
+
if host.in_op:
|
|
273
|
+
raise Exception(
|
|
274
|
+
"Operation called within another operation, this is not allowed! Use the `_inner` "
|
|
275
|
+
+ "function to call the underlying operation."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if func.is_deprecated: # type: ignore[attr-defined]
|
|
279
|
+
if func.deprecated_for: # type: ignore[attr-defined]
|
|
280
|
+
logger.warning(
|
|
281
|
+
f"The {get_operation_name_from_func(func)} operation is "
|
|
282
|
+
+ f"deprecated, please use: {func.deprecated_for}", # type: ignore[attr-defined] # noqa
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
logger.warning(f"The {get_operation_name_from_func(func)} operation is deprecated")
|
|
137
286
|
|
|
138
287
|
# Configure operation
|
|
139
288
|
#
|
|
140
|
-
|
|
141
|
-
# Name the operation
|
|
142
|
-
names = None
|
|
143
|
-
autoname = False
|
|
144
|
-
|
|
145
|
-
# Look for a set as the first argument
|
|
146
|
-
if len(args) > 0 and isinstance(args[0], set):
|
|
147
|
-
names = args[0]
|
|
148
|
-
args_copy = list(args)
|
|
149
|
-
args = args[1:]
|
|
150
|
-
|
|
151
|
-
# Generate an operation name if needed (Module/Operation format)
|
|
152
|
-
else:
|
|
153
|
-
autoname = True
|
|
154
|
-
module_bits = func.__module__.split('.')
|
|
155
|
-
module_name = module_bits[-1]
|
|
156
|
-
names = {
|
|
157
|
-
'{0}/{1}'.format(module_name.title(), func.__name__.title()),
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if state.deploy_name:
|
|
161
|
-
names = {
|
|
162
|
-
'{0} | {1}'.format(state.deploy_name, name)
|
|
163
|
-
for name in names
|
|
164
|
-
}
|
|
165
|
-
|
|
166
289
|
# 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 state.in_op:
|
|
172
|
-
return func(state, host, *args, **kwargs) or []
|
|
173
|
-
|
|
174
|
-
line_number = frameinfo.lineno
|
|
175
|
-
|
|
176
|
-
# Inject the current op file number (only incremented in CLI mode)
|
|
177
|
-
op_lines = [state.current_op_file]
|
|
178
|
-
|
|
179
|
-
# Add any current @deploy line numbers
|
|
180
|
-
if state.deploy_line_numbers:
|
|
181
|
-
op_lines.extend(state.deploy_line_numbers)
|
|
182
|
-
|
|
183
|
-
# Add any current loop count
|
|
184
|
-
if state.loop_line:
|
|
185
|
-
op_lines.extend([state.loop_line, state.loop_counter])
|
|
186
|
-
|
|
187
|
-
# Add the line number that called this operation
|
|
188
|
-
op_lines.append(line_number)
|
|
189
|
-
|
|
190
|
-
# Make a hash from the call stack lines
|
|
191
|
-
op_hash = make_hash(op_lines)
|
|
192
|
-
|
|
193
|
-
# Avoid adding duplicates! This happens if an operation is called within
|
|
194
|
-
# a loop - such that the filename/lineno/code _are_ the same, but the
|
|
195
|
-
# arguments might be different. We just append an increasing number to
|
|
196
|
-
# the op hash and also handle below with the op order.
|
|
197
|
-
host_op_hashes = state.meta[host]['op_hashes']
|
|
198
|
-
duplicate_op_count = 0
|
|
199
|
-
while op_hash in host_op_hashes:
|
|
200
|
-
logger.debug('Duplicate hash ({0}) detected!'.format(op_hash))
|
|
201
|
-
op_hash = '{0}-{1}'.format(op_hash, duplicate_op_count)
|
|
202
|
-
duplicate_op_count += 1
|
|
203
|
-
|
|
204
|
-
host_op_hashes.add(op_hash)
|
|
205
|
-
|
|
206
|
-
if duplicate_op_count:
|
|
207
|
-
op_lines.append(duplicate_op_count)
|
|
208
|
-
|
|
209
|
-
op_lines = tuple(op_lines)
|
|
210
|
-
state.op_line_numbers_to_hash[op_lines] = op_hash
|
|
211
|
-
logger.debug('Adding operation, {0}, called @ {1}:{2}, opLines={3}, opHash={4}'.format(
|
|
212
|
-
names, frameinfo.filename, line_number, op_lines, op_hash,
|
|
213
|
-
))
|
|
214
|
-
|
|
215
|
-
# Ensure shared (between servers) operation meta
|
|
216
|
-
op_meta = state.op_meta.setdefault(op_hash, {
|
|
217
|
-
'names': set(),
|
|
218
|
-
'args': [],
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
# Add any meta kwargs (sudo, etc) to the meta - first parse any strings
|
|
222
|
-
# as jinja templates.
|
|
223
|
-
actual_op_meta_kwargs = {
|
|
224
|
-
key: get_arg_value(state, host, a)
|
|
225
|
-
for key, a in op_meta_kwargs.items()
|
|
226
|
-
}
|
|
227
|
-
op_meta.update(actual_op_meta_kwargs)
|
|
228
|
-
|
|
229
|
-
# Add any new names to the set
|
|
230
|
-
op_meta['names'].update(names)
|
|
290
|
+
global_arguments, global_argument_keys = pop_global_arguments(state, host, kwargs)
|
|
231
291
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
for arg in args:
|
|
235
|
-
if isinstance(arg, FunctionType):
|
|
236
|
-
arg = arg.__name__
|
|
292
|
+
names, add_args = generate_operation_name(func, host, kwargs, global_arguments)
|
|
293
|
+
op_order, op_hash = solve_operation_consistency(names, state, host)
|
|
237
294
|
|
|
238
|
-
|
|
239
|
-
|
|
295
|
+
# Ensure shared (between servers) operation meta, mutates state
|
|
296
|
+
op_meta = ensure_shared_op_meta(state, op_hash, op_order, global_arguments, names)
|
|
240
297
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if arg not in op_meta['args']:
|
|
245
|
-
op_meta['args'].append(arg)
|
|
298
|
+
# Attach normal args, if we're auto-naming this operation
|
|
299
|
+
if add_args:
|
|
300
|
+
op_meta = attach_args(op_meta, args, kwargs)
|
|
246
301
|
|
|
247
302
|
# Check if we're actually running the operation on this host
|
|
248
|
-
#
|
|
249
|
-
|
|
250
303
|
# Run once and we've already added meta for this op? Stop here.
|
|
251
|
-
if
|
|
304
|
+
if op_meta.global_arguments["_run_once"]:
|
|
252
305
|
has_run = False
|
|
253
306
|
for ops in state.ops.values():
|
|
254
307
|
if op_hash in ops:
|
|
@@ -256,71 +309,188 @@ def operation(func=None, pipeline_facts=None):
|
|
|
256
309
|
break
|
|
257
310
|
|
|
258
311
|
if has_run:
|
|
259
|
-
return OperationMeta(op_hash)
|
|
260
|
-
|
|
261
|
-
#
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
312
|
+
return OperationMeta(op_hash, is_change=False)
|
|
313
|
+
|
|
314
|
+
# Grab a reference to any *current* deploy data as this may change when
|
|
315
|
+
# we later evaluate the operation at runtime.This means we put back the
|
|
316
|
+
# expected deploy data.
|
|
317
|
+
current_deploy_data = host.current_deploy_data
|
|
318
|
+
|
|
319
|
+
# "Run" operation - here we make a generator that will yield out actual commands to execute
|
|
320
|
+
# and, if we're diff-ing, we then iterate the generator now to determine if any changes
|
|
321
|
+
# *would* be made based on the *current* remote state.
|
|
322
|
+
|
|
323
|
+
def command_generator() -> Iterator[PyinfraCommand]:
|
|
324
|
+
# Check global _if argument function and do nothing if returns False
|
|
325
|
+
if state.is_executing:
|
|
326
|
+
_ifs = global_arguments.get("_if")
|
|
327
|
+
if isinstance(_ifs, list) and not all(_if() for _if in _ifs):
|
|
328
|
+
return
|
|
329
|
+
elif callable(_ifs) and not _ifs():
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
host.in_op = _set_in_op
|
|
333
|
+
host.current_op_hash = op_hash
|
|
334
|
+
host.current_op_global_arguments = global_arguments
|
|
335
|
+
host.current_op_deploy_data = current_deploy_data
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
for command in func(*args, **kwargs):
|
|
339
|
+
if isinstance(command, str):
|
|
340
|
+
command = StringCommand(command.strip())
|
|
341
|
+
yield command
|
|
342
|
+
finally:
|
|
343
|
+
host.in_op = False
|
|
344
|
+
host.current_op_hash = None
|
|
345
|
+
host.current_op_global_arguments = None
|
|
346
|
+
host.current_op_deploy_data = None
|
|
347
|
+
|
|
348
|
+
op_is_change = None
|
|
349
|
+
if state.should_check_for_changes():
|
|
350
|
+
op_is_change = False
|
|
351
|
+
for _ in command_generator():
|
|
352
|
+
op_is_change = True
|
|
353
|
+
break
|
|
354
|
+
else:
|
|
355
|
+
# If not calling the op function to check for change we still want to ensure the args
|
|
356
|
+
# are valid, so use Signature.bind to trigger any TypeError.
|
|
357
|
+
signature(func).bind(*args, **kwargs)
|
|
358
|
+
|
|
359
|
+
# Add host-specific operation data to state, this mutates state
|
|
360
|
+
host_meta = state.get_meta_for_host(host)
|
|
361
|
+
host_meta.ops += 1
|
|
362
|
+
if op_is_change:
|
|
363
|
+
host_meta.ops_change += 1
|
|
364
|
+
else:
|
|
365
|
+
host_meta.ops_no_change += 1
|
|
281
366
|
|
|
282
|
-
|
|
283
|
-
state.in_op = True
|
|
284
|
-
state.current_op_hash = op_hash
|
|
285
|
-
|
|
286
|
-
# Generate actual arguments by parsing strings as jinja2 templates. This
|
|
287
|
-
# means you can string format arguments w/o generating multiple
|
|
288
|
-
# operations. Only affects top level operations, as must be run "in_op"
|
|
289
|
-
# so facts are gathered correctly.
|
|
290
|
-
actual_args = [
|
|
291
|
-
get_arg_value(state, host, a)
|
|
292
|
-
for a in args
|
|
293
|
-
]
|
|
294
|
-
|
|
295
|
-
actual_kwargs = {
|
|
296
|
-
key: get_arg_value(state, host, a)
|
|
297
|
-
for key, a in kwargs.items()
|
|
298
|
-
}
|
|
367
|
+
operation_meta = OperationMeta(op_hash, op_is_change)
|
|
299
368
|
|
|
300
|
-
#
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
*actual_args,
|
|
304
|
-
**actual_kwargs
|
|
305
|
-
))
|
|
369
|
+
# Add the server-relevant commands
|
|
370
|
+
op_data = StateOperationHostData(command_generator, global_arguments, operation_meta)
|
|
371
|
+
state.set_op_data_for_host(host, op_hash, op_data)
|
|
306
372
|
|
|
307
|
-
|
|
308
|
-
state.
|
|
373
|
+
# If we're already in the execution phase, execute this operation immediately
|
|
374
|
+
if state.is_executing:
|
|
375
|
+
execute_immediately(state, host, op_hash)
|
|
309
376
|
|
|
310
|
-
#
|
|
311
|
-
|
|
377
|
+
# Return result meta for use in deploy scripts
|
|
378
|
+
return operation_meta
|
|
379
|
+
|
|
380
|
+
decorated_func._inner = func # type: ignore[attr-defined]
|
|
381
|
+
return cast(PyinfraOperation[P], decorated_func)
|
|
312
382
|
|
|
313
|
-
# We're doing some commands, meta/ops++
|
|
314
|
-
state.meta[host]['ops'] += 1
|
|
315
|
-
state.meta[host]['commands'] += len(commands)
|
|
316
383
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
384
|
+
def get_operation_name_from_func(func):
|
|
385
|
+
if func.__module__:
|
|
386
|
+
module_bits = func.__module__.split(".")
|
|
387
|
+
module_name = module_bits[-1]
|
|
388
|
+
return "{0}.{1}".format(module_name, func.__name__)
|
|
389
|
+
else:
|
|
390
|
+
return func.__name__
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def generate_operation_name(func, host, kwargs, global_arguments):
|
|
394
|
+
# Generate an operation name if needed (Module/Operation format)
|
|
395
|
+
name = global_arguments.get("name")
|
|
396
|
+
add_args = False
|
|
397
|
+
if name:
|
|
398
|
+
names = {name}
|
|
399
|
+
else:
|
|
400
|
+
add_args = True
|
|
401
|
+
name = get_operation_name_from_func(func)
|
|
402
|
+
names = {name}
|
|
403
|
+
|
|
404
|
+
if host.current_deploy_name:
|
|
405
|
+
names = {"{0} | {1}".format(host.current_deploy_name, name) for name in names}
|
|
406
|
+
|
|
407
|
+
return names, add_args
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def solve_operation_consistency(names, state, host):
|
|
411
|
+
# Operation order is used to tie-break available nodes in the operation DAG, in CLI mode
|
|
412
|
+
# we use stack call order so this matches as defined by the user deploy code.
|
|
413
|
+
if pyinfra.is_cli:
|
|
414
|
+
op_order = get_operation_order_from_stack(state)
|
|
415
|
+
# In API mode we just increase the order for each host
|
|
416
|
+
else:
|
|
417
|
+
op_order = [len(host.op_hash_order)]
|
|
418
|
+
|
|
419
|
+
if host.loop_position:
|
|
420
|
+
op_order.extend(host.loop_position)
|
|
421
|
+
|
|
422
|
+
# Make a hash from the call stack lines
|
|
423
|
+
op_hash = make_hash(op_order)
|
|
424
|
+
|
|
425
|
+
# Avoid adding duplicates! This happens if an operation is called within
|
|
426
|
+
# a loop - such that the filename/lineno/code _are_ the same, but the
|
|
427
|
+
# arguments might be different. We just append an increasing number to
|
|
428
|
+
# the op hash and also handle below with the op order.
|
|
429
|
+
duplicate_op_count = 0
|
|
430
|
+
while op_hash in host.op_hash_order:
|
|
431
|
+
logger.debug("Duplicate hash ({0}) detected!".format(op_hash))
|
|
432
|
+
op_hash = "{0}-{1}".format(op_hash, duplicate_op_count)
|
|
433
|
+
duplicate_op_count += 1
|
|
434
|
+
|
|
435
|
+
host.op_hash_order.append(op_hash)
|
|
436
|
+
if duplicate_op_count:
|
|
437
|
+
op_order.append(duplicate_op_count)
|
|
438
|
+
|
|
439
|
+
op_order = tuple(op_order)
|
|
440
|
+
logger.debug(f"Adding operation, {names}, opOrder={op_order}, opHash={op_hash}")
|
|
441
|
+
return op_order, op_hash
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# NOTE: this function mutates state.op_meta for this hash
|
|
445
|
+
def ensure_shared_op_meta(
|
|
446
|
+
state: State,
|
|
447
|
+
op_hash: str,
|
|
448
|
+
op_order: tuple[int, ...],
|
|
449
|
+
global_arguments: AllArguments,
|
|
450
|
+
names: set[str],
|
|
451
|
+
):
|
|
452
|
+
op_meta = state.op_meta.setdefault(op_hash, StateOperationMeta(op_order))
|
|
453
|
+
|
|
454
|
+
for key in EXECUTION_KWARG_KEYS:
|
|
455
|
+
global_value = global_arguments.pop(key) # type: ignore[misc]
|
|
456
|
+
op_meta_value = op_meta.global_arguments.get(key, op_meta_default)
|
|
457
|
+
|
|
458
|
+
if op_meta_value is not op_meta_default and global_value != op_meta_value:
|
|
459
|
+
raise OperationValueError("Cannot have different values for `{0}`.".format(key))
|
|
460
|
+
|
|
461
|
+
op_meta.global_arguments[key] = global_value # type: ignore[literal-required]
|
|
462
|
+
|
|
463
|
+
# Add any new names to the set
|
|
464
|
+
op_meta.names.update(names)
|
|
465
|
+
|
|
466
|
+
return op_meta
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def execute_immediately(state, host, op_hash):
|
|
470
|
+
op_meta = state.get_op_meta(op_hash)
|
|
471
|
+
op_data = state.get_op_data_for_host(host, op_hash)
|
|
472
|
+
op_data.parent_op_hash = host.executing_op_hash
|
|
473
|
+
log_operation_start(op_meta, op_types=["nested"], prefix="")
|
|
474
|
+
run_host_op(state, host, op_hash)
|
|
321
475
|
|
|
322
|
-
# Return result meta for use in deploy scripts
|
|
323
|
-
return OperationMeta(op_hash, commands)
|
|
324
476
|
|
|
325
|
-
|
|
326
|
-
|
|
477
|
+
def _get_arg_value(arg):
|
|
478
|
+
if isinstance(arg, FunctionType):
|
|
479
|
+
return arg.__name__
|
|
480
|
+
if isinstance(arg, StringIO):
|
|
481
|
+
return f"StringIO(hash={get_file_sha1(arg)})"
|
|
482
|
+
return arg
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def attach_args(op_meta, args, kwargs):
|
|
486
|
+
for arg in args:
|
|
487
|
+
if arg not in op_meta.args:
|
|
488
|
+
op_meta.args.append(str(_get_arg_value(arg)))
|
|
489
|
+
|
|
490
|
+
# Attach keyword args
|
|
491
|
+
for key, value in kwargs.items():
|
|
492
|
+
arg = "=".join((str(key), str(_get_arg_value(value))))
|
|
493
|
+
if arg not in op_meta.args:
|
|
494
|
+
op_meta.args.append(arg)
|
|
495
|
+
|
|
496
|
+
return op_meta
|