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/operations.py
CHANGED
|
@@ -10,11 +10,11 @@ import gevent
|
|
|
10
10
|
from paramiko import SSHException
|
|
11
11
|
|
|
12
12
|
from pyinfra import logger
|
|
13
|
-
from pyinfra.connectors.util import CommandOutput
|
|
13
|
+
from pyinfra.connectors.util import CommandOutput, OutputLine
|
|
14
14
|
from pyinfra.context import ctx_host, ctx_state
|
|
15
15
|
from pyinfra.progress import progress_spinner
|
|
16
16
|
|
|
17
|
-
from .arguments import
|
|
17
|
+
from .arguments import CONNECTOR_ARGUMENT_KEYS, ConnectorArguments
|
|
18
18
|
from .command import FunctionCommand, PyinfraCommand, StringCommand
|
|
19
19
|
from .exceptions import PyinfraError
|
|
20
20
|
from .util import (
|
|
@@ -22,7 +22,6 @@ from .util import (
|
|
|
22
22
|
log_error_or_warning,
|
|
23
23
|
log_host_command_error,
|
|
24
24
|
log_operation_start,
|
|
25
|
-
memoize,
|
|
26
25
|
print_host_combined_output,
|
|
27
26
|
)
|
|
28
27
|
|
|
@@ -31,92 +30,53 @@ if TYPE_CHECKING:
|
|
|
31
30
|
from .state import State
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
logger.warning("The `{0}` argument is in beta!".format(condition_name))
|
|
33
|
+
# Run a single host operation
|
|
34
|
+
#
|
|
37
35
|
|
|
38
36
|
|
|
39
|
-
def run_host_op(state: "State", host: "Host", op_hash) -> Optional[bool]:
|
|
37
|
+
def run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
|
|
40
38
|
state.trigger_callbacks("operation_host_start", host, op_hash)
|
|
41
39
|
|
|
42
40
|
if op_hash not in state.ops[host]:
|
|
43
41
|
logger.info("{0}{1}".format(host.print_prefix, click.style("Skipped", "blue")))
|
|
44
42
|
return True
|
|
45
43
|
|
|
46
|
-
op_data = state.get_op_data_for_host(host, op_hash)
|
|
47
|
-
global_arguments = op_data.global_arguments
|
|
48
|
-
|
|
49
44
|
op_meta = state.get_op_meta(op_hash)
|
|
50
|
-
|
|
51
|
-
ignore_errors = global_arguments["_ignore_errors"]
|
|
52
|
-
continue_on_error = global_arguments["_continue_on_error"]
|
|
53
|
-
|
|
54
45
|
logger.debug("Starting operation %r on %s", op_meta.names, host)
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{key: global_arguments[key] for key in executor_kwarg_keys if key in global_arguments},
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
def _run_shell_command(command, connector_arguments):
|
|
65
|
-
status = False
|
|
66
|
-
combined_output_lines = CommandOutput([])
|
|
67
|
-
|
|
68
|
-
try:
|
|
69
|
-
status, combined_output_lines = command.execute(state, host, connector_arguments)
|
|
70
|
-
except (timeout_error, socket_error, SSHException) as e:
|
|
71
|
-
log_host_command_error(
|
|
72
|
-
host,
|
|
73
|
-
e,
|
|
74
|
-
timeout=global_arguments["_timeout"],
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
# If we failed and have no already printed the stderr, print it
|
|
78
|
-
if status is False and not state.print_output:
|
|
79
|
-
print_host_combined_output(host, combined_output_lines)
|
|
80
|
-
|
|
81
|
-
return status, combined_output_lines
|
|
82
|
-
|
|
83
|
-
def run_condition(condition_name: str) -> bool:
|
|
84
|
-
condition_value = global_arguments[condition_name]
|
|
85
|
-
if not condition_value:
|
|
86
|
-
return True
|
|
87
|
-
|
|
88
|
-
show_pre_or_post_condition_warning(condition_name)
|
|
89
|
-
|
|
90
|
-
_shell_command_status, _ = _run_shell_command(
|
|
91
|
-
StringCommand(condition_value),
|
|
92
|
-
base_connector_arguments,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
if _shell_command_status:
|
|
96
|
-
return True
|
|
47
|
+
if host.executing_op_hash is None:
|
|
48
|
+
host.executing_op_hash = op_hash
|
|
49
|
+
else:
|
|
50
|
+
host.nested_executing_op_hash = op_hash
|
|
97
51
|
|
|
98
|
-
|
|
99
|
-
|
|
52
|
+
try:
|
|
53
|
+
return _run_host_op(state, host, op_hash)
|
|
54
|
+
finally:
|
|
55
|
+
if host.nested_executing_op_hash:
|
|
56
|
+
host.nested_executing_op_hash = None
|
|
57
|
+
else:
|
|
58
|
+
host.executing_op_hash = None
|
|
100
59
|
|
|
101
|
-
if ignore_errors:
|
|
102
|
-
return True
|
|
103
60
|
|
|
104
|
-
|
|
105
|
-
|
|
61
|
+
def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
|
|
62
|
+
op_data = state.get_op_data_for_host(host, op_hash)
|
|
63
|
+
global_arguments = op_data.global_arguments
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
65
|
+
ignore_errors = global_arguments["_ignore_errors"]
|
|
66
|
+
continue_on_error = global_arguments["_continue_on_error"]
|
|
67
|
+
timeout = global_arguments.get("_timeout", 0)
|
|
109
68
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
69
|
+
executor_kwarg_keys = CONNECTOR_ARGUMENT_KEYS
|
|
70
|
+
# See: https://github.com/python/mypy/issues/10371
|
|
71
|
+
base_connector_arguments: ConnectorArguments = cast(
|
|
72
|
+
ConnectorArguments,
|
|
73
|
+
{key: global_arguments[key] for key in executor_kwarg_keys if key in global_arguments}, # type: ignore[literal-required] # noqa
|
|
74
|
+
)
|
|
114
75
|
|
|
115
|
-
return_status: Optional[bool] = False
|
|
116
76
|
did_error = False
|
|
117
77
|
executed_commands = 0
|
|
118
78
|
commands = []
|
|
119
|
-
|
|
79
|
+
all_output_lines: list[OutputLine] = []
|
|
120
80
|
|
|
121
81
|
for command in op_data.command_generator():
|
|
122
82
|
commands.append(command)
|
|
@@ -126,54 +86,58 @@ def run_host_op(state: "State", host: "Host", op_hash) -> Optional[bool]:
|
|
|
126
86
|
connector_arguments = base_connector_arguments.copy()
|
|
127
87
|
connector_arguments.update(command.connector_arguments)
|
|
128
88
|
|
|
129
|
-
# Now we attempt to execute the command
|
|
130
|
-
#
|
|
131
|
-
|
|
132
89
|
if not isinstance(command, PyinfraCommand):
|
|
133
90
|
raise TypeError("{0} is an invalid pyinfra command!".format(command))
|
|
134
91
|
|
|
135
92
|
if isinstance(command, FunctionCommand):
|
|
136
93
|
try:
|
|
137
94
|
status = command.execute(state, host, connector_arguments)
|
|
138
|
-
except Exception as e:
|
|
139
|
-
|
|
140
|
-
_error_msg = "Unexpected error in Python callback: {0}".format(_formatted_exc)
|
|
141
|
-
_error_msg_styled = click.style(_error_msg, "red")
|
|
142
|
-
_error_log = "{0}{1}".format(host.print_prefix, _error_msg_styled)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
# Custom functions could do anything, so expect anything!
|
|
143
97
|
logger.warning(traceback.format_exc())
|
|
144
|
-
|
|
98
|
+
host.log_styled(
|
|
99
|
+
f"Unexpected error in Python callback: {format_exception(e)}",
|
|
100
|
+
fg="red",
|
|
101
|
+
log_func=logger.warning,
|
|
102
|
+
)
|
|
145
103
|
|
|
146
104
|
elif isinstance(command, StringCommand):
|
|
147
|
-
|
|
148
|
-
|
|
105
|
+
output_lines = CommandOutput([])
|
|
106
|
+
try:
|
|
107
|
+
status, output_lines = command.execute(
|
|
108
|
+
state,
|
|
109
|
+
host,
|
|
110
|
+
connector_arguments,
|
|
111
|
+
)
|
|
112
|
+
except (timeout_error, socket_error, SSHException) as e:
|
|
113
|
+
log_host_command_error(host, e, timeout=timeout)
|
|
114
|
+
all_output_lines.extend(output_lines)
|
|
115
|
+
# If we failed and have not already printed the stderr, print it
|
|
116
|
+
if status is False and not state.print_output:
|
|
117
|
+
print_host_combined_output(host, output_lines)
|
|
149
118
|
|
|
150
119
|
else:
|
|
151
120
|
try:
|
|
152
121
|
status = command.execute(state, host, connector_arguments)
|
|
153
122
|
except (timeout_error, socket_error, SSHException, IOError) as e:
|
|
154
|
-
|
|
155
|
-
log_host_command_error(host, e, timeout=_timeout)
|
|
123
|
+
log_host_command_error(host, e, timeout=timeout)
|
|
156
124
|
|
|
157
125
|
# Break the loop to trigger a failure
|
|
158
126
|
if status is False:
|
|
127
|
+
did_error = True
|
|
159
128
|
if continue_on_error is True:
|
|
160
|
-
did_error = True
|
|
161
129
|
continue
|
|
162
130
|
break
|
|
163
131
|
|
|
164
132
|
executed_commands += 1
|
|
165
133
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
if not run_condition("_postcondition"):
|
|
169
|
-
return False
|
|
170
|
-
|
|
171
|
-
if not did_error:
|
|
172
|
-
return_status = True
|
|
134
|
+
# Handle results
|
|
135
|
+
#
|
|
173
136
|
|
|
137
|
+
op_success = return_status = not did_error
|
|
174
138
|
host_results = state.get_results_for_host(host)
|
|
175
139
|
|
|
176
|
-
if
|
|
140
|
+
if did_error is False:
|
|
177
141
|
host_results.ops += 1
|
|
178
142
|
host_results.success_ops += 1
|
|
179
143
|
|
|
@@ -181,10 +145,6 @@ def run_host_op(state: "State", host: "Host", op_hash) -> Optional[bool]:
|
|
|
181
145
|
_click_log_status = click.style(_status_log, "green")
|
|
182
146
|
logger.info("{0}{1}".format(host.print_prefix, _click_log_status))
|
|
183
147
|
|
|
184
|
-
# Trigger any success handler
|
|
185
|
-
if global_arguments["_on_success"]:
|
|
186
|
-
global_arguments["_on_success"](state, host, op_hash)
|
|
187
|
-
|
|
188
148
|
state.trigger_callbacks("operation_host_success", host, op_hash)
|
|
189
149
|
else:
|
|
190
150
|
if ignore_errors:
|
|
@@ -198,31 +158,25 @@ def run_host_op(state: "State", host: "Host", op_hash) -> Optional[bool]:
|
|
|
198
158
|
_command_description = f"executed {executed_commands} commands"
|
|
199
159
|
log_error_or_warning(host, ignore_errors, _command_description, continue_on_error)
|
|
200
160
|
|
|
201
|
-
# Always trigger any error handler
|
|
202
|
-
if global_arguments["_on_error"]:
|
|
203
|
-
global_arguments["_on_error"](state, host, op_hash)
|
|
204
|
-
|
|
205
161
|
# Ignored, op "completes" w/ ignored error
|
|
206
162
|
if ignore_errors:
|
|
207
163
|
host_results.ops += 1
|
|
164
|
+
return_status = True
|
|
208
165
|
|
|
209
166
|
# Unignored error -> False
|
|
210
167
|
state.trigger_callbacks("operation_host_error", host, op_hash)
|
|
211
168
|
|
|
212
|
-
|
|
213
|
-
|
|
169
|
+
op_data.operation_meta.set_complete(
|
|
170
|
+
op_success,
|
|
171
|
+
commands,
|
|
172
|
+
CommandOutput(all_output_lines),
|
|
173
|
+
)
|
|
214
174
|
|
|
215
|
-
|
|
216
|
-
op_data.operation_meta.set_result(return_status)
|
|
217
|
-
op_data.operation_meta.set_commands(commands)
|
|
218
|
-
op_data.operation_meta.set_combined_output_lines(all_combined_output_lines)
|
|
175
|
+
return return_status
|
|
219
176
|
|
|
220
|
-
if host.nested_executing_op_hash:
|
|
221
|
-
host.nested_executing_op_hash = None
|
|
222
|
-
else:
|
|
223
|
-
host.executing_op_hash = None
|
|
224
177
|
|
|
225
|
-
|
|
178
|
+
# Run all operations strategies
|
|
179
|
+
#
|
|
226
180
|
|
|
227
181
|
|
|
228
182
|
def _run_host_op_with_context(state: "State", host: "Host", op_hash: str):
|
|
@@ -323,10 +277,9 @@ def _run_single_op(state: "State", op_hash: str):
|
|
|
323
277
|
batches = [list(state.inventory.iter_active_hosts())]
|
|
324
278
|
|
|
325
279
|
# If parallel set break up the inventory into a series of batches
|
|
326
|
-
|
|
327
|
-
|
|
280
|
+
parallel = op_meta.global_arguments["_parallel"]
|
|
281
|
+
if parallel:
|
|
328
282
|
hosts = list(state.inventory.iter_active_hosts())
|
|
329
|
-
|
|
330
283
|
batches = [hosts[i : i + parallel] for i in range(0, len(hosts), parallel)]
|
|
331
284
|
|
|
332
285
|
for batch in batches:
|
pyinfra/api/state.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections import defaultdict
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from enum import IntEnum
|
|
5
6
|
from graphlib import CycleError, TopologicalSorter
|
|
6
7
|
from multiprocessing import cpu_count
|
|
7
|
-
from typing import TYPE_CHECKING, Iterator, Optional
|
|
8
|
-
from uuid import uuid4
|
|
8
|
+
from typing import TYPE_CHECKING, Callable, Iterator, Optional
|
|
9
9
|
|
|
10
10
|
from gevent.pool import Pool
|
|
11
11
|
from paramiko import PKey
|
|
@@ -14,7 +14,6 @@ from pyinfra import logger
|
|
|
14
14
|
|
|
15
15
|
from .config import Config
|
|
16
16
|
from .exceptions import PyinfraError
|
|
17
|
-
from .util import sha1_hash
|
|
18
17
|
|
|
19
18
|
if TYPE_CHECKING:
|
|
20
19
|
from pyinfra.api.arguments import AllArguments
|
|
@@ -83,22 +82,35 @@ class BaseStateCallback:
|
|
|
83
82
|
pass
|
|
84
83
|
|
|
85
84
|
|
|
85
|
+
class StateStage(IntEnum):
|
|
86
|
+
# Setup - collect inventory & data
|
|
87
|
+
Setup = 1
|
|
88
|
+
# Connect - connect to the inventory
|
|
89
|
+
Connect = 2
|
|
90
|
+
# Prepare - detect operation changes
|
|
91
|
+
Prepare = 3
|
|
92
|
+
# Execute - execute operations
|
|
93
|
+
Execute = 4
|
|
94
|
+
# Disconnect - disconnect from the inventory
|
|
95
|
+
Disconnect = 5
|
|
96
|
+
|
|
97
|
+
|
|
86
98
|
class StateOperationMeta:
|
|
87
99
|
names: set[str]
|
|
88
100
|
args: list[str]
|
|
89
|
-
op_order: tuple[int]
|
|
101
|
+
op_order: tuple[int, ...]
|
|
90
102
|
global_arguments: "AllArguments"
|
|
91
103
|
|
|
92
|
-
def __init__(self, op_order: tuple[int]):
|
|
104
|
+
def __init__(self, op_order: tuple[int, ...]):
|
|
93
105
|
self.op_order = op_order
|
|
94
106
|
self.names = set()
|
|
95
107
|
self.args = []
|
|
96
|
-
self.global_arguments = {}
|
|
108
|
+
self.global_arguments = {} # type: ignore
|
|
97
109
|
|
|
98
110
|
|
|
99
111
|
@dataclass
|
|
100
112
|
class StateOperationHostData:
|
|
101
|
-
command_generator: Iterator["PyinfraCommand"]
|
|
113
|
+
command_generator: Callable[[], Iterator["PyinfraCommand"]]
|
|
102
114
|
global_arguments: "AllArguments"
|
|
103
115
|
operation_meta: "OperationMeta"
|
|
104
116
|
parent_op_hash: Optional[str] = None
|
|
@@ -138,6 +150,11 @@ class State:
|
|
|
138
150
|
# Main gevent pool
|
|
139
151
|
pool: "Pool"
|
|
140
152
|
|
|
153
|
+
# Current stage this state is in
|
|
154
|
+
current_stage: StateStage = StateStage.Setup
|
|
155
|
+
# Warning counters by stage
|
|
156
|
+
stage_warnings: dict[StateStage, int] = defaultdict(int)
|
|
157
|
+
|
|
141
158
|
# Whether we are executing operations (ie hosts are all ready)
|
|
142
159
|
is_executing: bool = False
|
|
143
160
|
|
|
@@ -157,6 +174,7 @@ class State:
|
|
|
157
174
|
current_deploy_filename: Optional[str] = None
|
|
158
175
|
current_exec_filename: Optional[str] = None
|
|
159
176
|
current_op_file_number: int = 0
|
|
177
|
+
should_raise_failed_hosts: Optional[Callable[["State"], bool]] = None
|
|
160
178
|
|
|
161
179
|
def __init__(
|
|
162
180
|
self,
|
|
@@ -165,7 +183,8 @@ class State:
|
|
|
165
183
|
check_for_changes: bool = True,
|
|
166
184
|
**kwargs,
|
|
167
185
|
):
|
|
168
|
-
"""
|
|
186
|
+
"""
|
|
187
|
+
Initializes the state, the main Pyinfra
|
|
169
188
|
|
|
170
189
|
Args:
|
|
171
190
|
inventory (Optional[Inventory], optional): The inventory. Defaults to None.
|
|
@@ -254,6 +273,17 @@ class State:
|
|
|
254
273
|
|
|
255
274
|
self.initialised = True
|
|
256
275
|
|
|
276
|
+
def set_stage(self, stage: StateStage) -> None:
|
|
277
|
+
if stage < self.current_stage:
|
|
278
|
+
raise Exception("State stage cannot go backwards!")
|
|
279
|
+
self.current_stage = stage
|
|
280
|
+
|
|
281
|
+
def increment_warning_counter(self) -> None:
|
|
282
|
+
self.stage_warnings[self.current_stage] += 1
|
|
283
|
+
|
|
284
|
+
def get_warning_counter(self) -> int:
|
|
285
|
+
return self.stage_warnings[self.current_stage]
|
|
286
|
+
|
|
257
287
|
def should_check_for_changes(self):
|
|
258
288
|
return self.check_for_changes
|
|
259
289
|
|
|
@@ -269,16 +299,6 @@ class State:
|
|
|
269
299
|
func = getattr(handler, method_name)
|
|
270
300
|
func(self, *args, **kwargs)
|
|
271
301
|
|
|
272
|
-
@contextmanager
|
|
273
|
-
def preserve_loop_order(self, items):
|
|
274
|
-
logger.warning(
|
|
275
|
-
(
|
|
276
|
-
"Using `state.preserve_loop_order` is not longer required for operations to be "
|
|
277
|
-
"executed in correct loop order and can be safely removed."
|
|
278
|
-
),
|
|
279
|
-
)
|
|
280
|
-
yield lambda: items
|
|
281
|
-
|
|
282
302
|
def get_op_order(self):
|
|
283
303
|
ts: TopologicalSorter = TopologicalSorter()
|
|
284
304
|
|
|
@@ -324,10 +344,19 @@ class State:
|
|
|
324
344
|
def get_results_for_host(self, host: "Host") -> StateHostResults:
|
|
325
345
|
return self.results[host]
|
|
326
346
|
|
|
327
|
-
def get_op_data_for_host(
|
|
347
|
+
def get_op_data_for_host(
|
|
348
|
+
self,
|
|
349
|
+
host: "Host",
|
|
350
|
+
op_hash: str,
|
|
351
|
+
) -> StateOperationHostData:
|
|
328
352
|
return self.ops[host][op_hash]
|
|
329
353
|
|
|
330
|
-
def set_op_data_for_host(
|
|
354
|
+
def set_op_data_for_host(
|
|
355
|
+
self,
|
|
356
|
+
host: "Host",
|
|
357
|
+
op_hash: str,
|
|
358
|
+
op_data: StateOperationHostData,
|
|
359
|
+
):
|
|
331
360
|
self.ops[host][op_hash] = op_data
|
|
332
361
|
|
|
333
362
|
def activate_host(self, host: "Host"):
|
|
@@ -375,6 +404,9 @@ class State:
|
|
|
375
404
|
percent_failed = (1 - len(active_hosts) / activated_count) * 100
|
|
376
405
|
|
|
377
406
|
if percent_failed > self.config.FAIL_PERCENT:
|
|
407
|
+
if self.should_raise_failed_hosts and self.should_raise_failed_hosts(self) is False:
|
|
408
|
+
return
|
|
409
|
+
|
|
378
410
|
raise PyinfraError(
|
|
379
411
|
"Over {0}% of hosts failed ({1}%)".format(
|
|
380
412
|
self.config.FAIL_PERCENT,
|
|
@@ -392,16 +424,3 @@ class State:
|
|
|
392
424
|
if not isinstance(limit_hosts, list):
|
|
393
425
|
return True
|
|
394
426
|
return host in limit_hosts
|
|
395
|
-
|
|
396
|
-
def get_temp_filename(self, hash_key: Optional[str] = None, hash_filename: bool = True):
|
|
397
|
-
"""
|
|
398
|
-
Generate a temporary filename for this deploy.
|
|
399
|
-
"""
|
|
400
|
-
|
|
401
|
-
if not hash_key:
|
|
402
|
-
hash_key = str(uuid4())
|
|
403
|
-
|
|
404
|
-
if hash_filename:
|
|
405
|
-
hash_key = sha1_hash(hash_key)
|
|
406
|
-
|
|
407
|
-
return "{0}/pyinfra-{1}".format(self.config.TEMP_DIR, hash_key)
|
pyinfra/api/util.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from functools import wraps
|
|
2
4
|
from hashlib import sha1
|
|
3
5
|
from inspect import getframeinfo, stack
|
|
6
|
+
from io import BytesIO, StringIO
|
|
4
7
|
from os import getcwd, path, stat
|
|
5
8
|
from socket import error as socket_error, timeout as timeout_error
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
|
9
|
+
from typing import IO, TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union
|
|
7
10
|
|
|
8
11
|
import click
|
|
9
12
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
10
13
|
from paramiko import SSHException
|
|
14
|
+
from typeguard import TypeCheckError, check_type
|
|
11
15
|
|
|
12
16
|
import pyinfra
|
|
13
17
|
from pyinfra import logger
|
|
@@ -24,7 +28,7 @@ BLOCKSIZE = 65536
|
|
|
24
28
|
TEMPLATES: Dict[Any, Any] = {}
|
|
25
29
|
FILE_SHAS: Dict[Any, Any] = {}
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
PYINFRA_INSTALL_DIR = path.normpath(path.join(path.dirname(__file__), ".."))
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
def get_file_path(state: "State", filename: str):
|
|
@@ -105,16 +109,16 @@ def get_caller_frameinfo(frame_offset: int = 0):
|
|
|
105
109
|
|
|
106
110
|
|
|
107
111
|
def get_operation_order_from_stack(state: "State"):
|
|
112
|
+
|
|
108
113
|
stack_items = list(reversed(stack()))
|
|
109
114
|
|
|
115
|
+
i = 0
|
|
110
116
|
# Find the *first* occurrence of our deploy file in the reversed stack
|
|
111
117
|
if state.current_deploy_filename:
|
|
112
118
|
for i, stack_item in enumerate(stack_items):
|
|
113
119
|
frame = getframeinfo(stack_item[0])
|
|
114
120
|
if frame.filename == state.current_deploy_filename:
|
|
115
121
|
break
|
|
116
|
-
else:
|
|
117
|
-
i = 0
|
|
118
122
|
|
|
119
123
|
# Now generate a list of line numbers *following that file*
|
|
120
124
|
line_numbers = []
|
|
@@ -125,7 +129,7 @@ def get_operation_order_from_stack(state: "State"):
|
|
|
125
129
|
for stack_item in stack_items[i:]:
|
|
126
130
|
frame = getframeinfo(stack_item[0])
|
|
127
131
|
|
|
128
|
-
if frame.filename.startswith(
|
|
132
|
+
if frame.filename.startswith(PYINFRA_INSTALL_DIR):
|
|
129
133
|
continue
|
|
130
134
|
|
|
131
135
|
line_numbers.append(frame.lineno)
|
|
@@ -135,7 +139,7 @@ def get_operation_order_from_stack(state: "State"):
|
|
|
135
139
|
return line_numbers
|
|
136
140
|
|
|
137
141
|
|
|
138
|
-
def get_template(filename_or_io: str):
|
|
142
|
+
def get_template(filename_or_io: str | IO):
|
|
139
143
|
"""
|
|
140
144
|
Gets a jinja2 ``Template`` object for the input filename or string, with caching
|
|
141
145
|
based on the filename of the template, or the SHA1 of the input string.
|
|
@@ -237,7 +241,7 @@ def log_error_or_warning(
|
|
|
237
241
|
)
|
|
238
242
|
|
|
239
243
|
|
|
240
|
-
def log_host_command_error(host: "Host", e: Exception, timeout: int = 0) -> None:
|
|
244
|
+
def log_host_command_error(host: "Host", e: Exception, timeout: int | None = 0) -> None:
|
|
241
245
|
if isinstance(e, timeout_error):
|
|
242
246
|
logger.error(
|
|
243
247
|
"{0}{1}".format(
|
|
@@ -297,19 +301,27 @@ def make_hash(obj):
|
|
|
297
301
|
if isinstance(obj, int)
|
|
298
302
|
# Constants - the values can change between hosts but we should still
|
|
299
303
|
# group them under the same operation hash.
|
|
300
|
-
else
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
304
|
+
else (
|
|
305
|
+
"_PYINFRA_CONSTANT"
|
|
306
|
+
if obj in (True, False, None)
|
|
307
|
+
# Plain strings
|
|
308
|
+
else (
|
|
309
|
+
obj
|
|
310
|
+
if isinstance(obj, str)
|
|
311
|
+
# Objects with __name__s
|
|
312
|
+
else (
|
|
313
|
+
obj.__name__
|
|
314
|
+
if hasattr(obj, "__name__")
|
|
315
|
+
# Objects with names
|
|
316
|
+
else (
|
|
317
|
+
obj.name
|
|
318
|
+
if hasattr(obj, "name")
|
|
319
|
+
# Repr anything else
|
|
320
|
+
else repr(obj)
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
)
|
|
313
325
|
)
|
|
314
326
|
|
|
315
327
|
return sha1_hash(hash_string)
|
|
@@ -321,7 +333,11 @@ class get_file_io:
|
|
|
321
333
|
will open and close filenames, and leave IO objects alone.
|
|
322
334
|
"""
|
|
323
335
|
|
|
324
|
-
|
|
336
|
+
filename_or_io: Union[str, IO[Any]]
|
|
337
|
+
mode: str
|
|
338
|
+
|
|
339
|
+
_close: bool = False
|
|
340
|
+
_file_io: IO[Any]
|
|
325
341
|
|
|
326
342
|
def __init__(self, filename_or_io, mode="rb"):
|
|
327
343
|
if not (
|
|
@@ -336,29 +352,32 @@ class get_file_io:
|
|
|
336
352
|
),
|
|
337
353
|
)
|
|
338
354
|
|
|
355
|
+
# Convert any StringIO/BytesIO to the other to match the desired mode
|
|
356
|
+
if isinstance(filename_or_io, StringIO) and mode == "rb":
|
|
357
|
+
filename_or_io.seek(0)
|
|
358
|
+
filename_or_io = BytesIO(filename_or_io.read().encode())
|
|
359
|
+
if isinstance(filename_or_io, BytesIO) and mode == "r":
|
|
360
|
+
filename_or_io.seek(0)
|
|
361
|
+
filename_or_io = StringIO(filename_or_io.read().decode())
|
|
362
|
+
|
|
339
363
|
self.filename_or_io = filename_or_io
|
|
340
364
|
self.mode = mode
|
|
341
365
|
|
|
342
366
|
def __enter__(self):
|
|
343
|
-
|
|
344
|
-
if hasattr(self.filename_or_io, "read"):
|
|
345
|
-
file_io = self.filename_or_io
|
|
346
|
-
|
|
347
|
-
# Otherwise, assume a filename and open it up
|
|
348
|
-
else:
|
|
367
|
+
if isinstance(self.filename_or_io, str):
|
|
349
368
|
file_io = open(self.filename_or_io, self.mode)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
369
|
+
self._file_io = file_io
|
|
370
|
+
self._close = True
|
|
371
|
+
else:
|
|
372
|
+
file_io = self.filename_or_io
|
|
354
373
|
|
|
355
374
|
# Ensure we're at the start of the file
|
|
356
375
|
file_io.seek(0)
|
|
357
376
|
return file_io
|
|
358
377
|
|
|
359
378
|
def __exit__(self, type, value, traceback):
|
|
360
|
-
if self.
|
|
361
|
-
self.
|
|
379
|
+
if self._close:
|
|
380
|
+
self._file_io.close()
|
|
362
381
|
|
|
363
382
|
@property
|
|
364
383
|
def cache_key(self):
|
|
@@ -405,3 +424,15 @@ def get_path_permissions_mode(pathname: str):
|
|
|
405
424
|
|
|
406
425
|
mode_octal = oct(stat(pathname).st_mode)
|
|
407
426
|
return mode_octal[-3:]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def raise_if_bad_type(
|
|
430
|
+
value: Any,
|
|
431
|
+
type_: Type,
|
|
432
|
+
exception: type[Exception],
|
|
433
|
+
message_prefix: str,
|
|
434
|
+
):
|
|
435
|
+
try:
|
|
436
|
+
check_type(value, type_)
|
|
437
|
+
except TypeCheckError as e:
|
|
438
|
+
raise exception(f"{message_prefix}: {e}")
|