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.
Files changed (203) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +18 -3
  4. pyinfra/api/arguments.py +406 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +67 -18
  12. pyinfra/api/facts.py +253 -202
  13. pyinfra/api/host.py +413 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/operation.py +432 -262
  16. pyinfra/api/operations.py +273 -260
  17. pyinfra/api/state.py +302 -248
  18. pyinfra/api/util.py +291 -368
  19. pyinfra/connectors/base.py +173 -0
  20. pyinfra/connectors/chroot.py +212 -0
  21. pyinfra/connectors/docker.py +381 -0
  22. pyinfra/connectors/dockerssh.py +297 -0
  23. pyinfra/connectors/local.py +238 -0
  24. pyinfra/connectors/scp/__init__.py +1 -0
  25. pyinfra/connectors/scp/client.py +204 -0
  26. pyinfra/connectors/ssh.py +670 -0
  27. pyinfra/connectors/ssh_util.py +114 -0
  28. pyinfra/connectors/sshuserclient/client.py +309 -0
  29. pyinfra/connectors/sshuserclient/config.py +102 -0
  30. pyinfra/connectors/terraform.py +135 -0
  31. pyinfra/connectors/util.py +410 -0
  32. pyinfra/connectors/vagrant.py +183 -0
  33. pyinfra/context.py +145 -0
  34. pyinfra/facts/__init__.py +7 -6
  35. pyinfra/facts/apk.py +22 -7
  36. pyinfra/facts/apt.py +117 -60
  37. pyinfra/facts/brew.py +100 -15
  38. pyinfra/facts/bsdinit.py +23 -0
  39. pyinfra/facts/cargo.py +37 -0
  40. pyinfra/facts/choco.py +47 -0
  41. pyinfra/facts/crontab.py +195 -0
  42. pyinfra/facts/deb.py +94 -0
  43. pyinfra/facts/dnf.py +48 -0
  44. pyinfra/facts/docker.py +96 -23
  45. pyinfra/facts/efibootmgr.py +113 -0
  46. pyinfra/facts/files.py +630 -58
  47. pyinfra/facts/flatpak.py +77 -0
  48. pyinfra/facts/freebsd.py +70 -0
  49. pyinfra/facts/gem.py +19 -6
  50. pyinfra/facts/git.py +59 -14
  51. pyinfra/facts/gpg.py +150 -0
  52. pyinfra/facts/hardware.py +313 -167
  53. pyinfra/facts/iptables.py +72 -62
  54. pyinfra/facts/launchd.py +44 -0
  55. pyinfra/facts/lxd.py +17 -4
  56. pyinfra/facts/mysql.py +122 -86
  57. pyinfra/facts/npm.py +17 -9
  58. pyinfra/facts/openrc.py +71 -0
  59. pyinfra/facts/opkg.py +246 -0
  60. pyinfra/facts/pacman.py +50 -7
  61. pyinfra/facts/pip.py +24 -7
  62. pyinfra/facts/pipx.py +82 -0
  63. pyinfra/facts/pkg.py +15 -6
  64. pyinfra/facts/pkgin.py +35 -0
  65. pyinfra/facts/podman.py +54 -0
  66. pyinfra/facts/postgres.py +178 -0
  67. pyinfra/facts/postgresql.py +6 -147
  68. pyinfra/facts/rpm.py +105 -0
  69. pyinfra/facts/runit.py +77 -0
  70. pyinfra/facts/selinux.py +161 -0
  71. pyinfra/facts/server.py +746 -285
  72. pyinfra/facts/snap.py +88 -0
  73. pyinfra/facts/systemd.py +139 -0
  74. pyinfra/facts/sysvinit.py +59 -0
  75. pyinfra/facts/upstart.py +35 -0
  76. pyinfra/facts/util/__init__.py +17 -0
  77. pyinfra/facts/util/databases.py +4 -6
  78. pyinfra/facts/util/packaging.py +37 -6
  79. pyinfra/facts/util/units.py +30 -0
  80. pyinfra/facts/util/win_files.py +99 -0
  81. pyinfra/facts/vzctl.py +20 -13
  82. pyinfra/facts/xbps.py +35 -0
  83. pyinfra/facts/yum.py +34 -40
  84. pyinfra/facts/zfs.py +77 -0
  85. pyinfra/facts/zypper.py +42 -0
  86. pyinfra/local.py +45 -83
  87. pyinfra/operations/__init__.py +12 -0
  88. pyinfra/operations/apk.py +98 -0
  89. pyinfra/operations/apt.py +488 -0
  90. pyinfra/operations/brew.py +231 -0
  91. pyinfra/operations/bsdinit.py +59 -0
  92. pyinfra/operations/cargo.py +45 -0
  93. pyinfra/operations/choco.py +61 -0
  94. pyinfra/operations/crontab.py +191 -0
  95. pyinfra/operations/dnf.py +210 -0
  96. pyinfra/operations/docker.py +446 -0
  97. pyinfra/operations/files.py +1939 -0
  98. pyinfra/operations/flatpak.py +94 -0
  99. pyinfra/operations/freebsd/__init__.py +12 -0
  100. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  101. pyinfra/operations/freebsd/pkg.py +219 -0
  102. pyinfra/operations/freebsd/service.py +116 -0
  103. pyinfra/operations/freebsd/sysrc.py +92 -0
  104. pyinfra/operations/gem.py +47 -0
  105. pyinfra/operations/git.py +419 -0
  106. pyinfra/operations/iptables.py +311 -0
  107. pyinfra/operations/launchd.py +45 -0
  108. pyinfra/operations/lxd.py +68 -0
  109. pyinfra/operations/mysql.py +609 -0
  110. pyinfra/operations/npm.py +57 -0
  111. pyinfra/operations/openrc.py +63 -0
  112. pyinfra/operations/opkg.py +88 -0
  113. pyinfra/operations/pacman.py +81 -0
  114. pyinfra/operations/pip.py +205 -0
  115. pyinfra/operations/pipx.py +102 -0
  116. pyinfra/operations/pkg.py +70 -0
  117. pyinfra/operations/pkgin.py +91 -0
  118. pyinfra/operations/postgres.py +436 -0
  119. pyinfra/operations/postgresql.py +30 -0
  120. pyinfra/operations/puppet.py +40 -0
  121. pyinfra/operations/python.py +72 -0
  122. pyinfra/operations/runit.py +184 -0
  123. pyinfra/operations/selinux.py +189 -0
  124. pyinfra/operations/server.py +1099 -0
  125. pyinfra/operations/snap.py +117 -0
  126. pyinfra/operations/ssh.py +216 -0
  127. pyinfra/operations/systemd.py +149 -0
  128. pyinfra/operations/sysvinit.py +141 -0
  129. pyinfra/operations/upstart.py +68 -0
  130. pyinfra/operations/util/__init__.py +12 -0
  131. pyinfra/operations/util/docker.py +251 -0
  132. pyinfra/operations/util/files.py +247 -0
  133. pyinfra/operations/util/packaging.py +336 -0
  134. pyinfra/operations/util/service.py +46 -0
  135. pyinfra/operations/vzctl.py +137 -0
  136. pyinfra/operations/xbps.py +77 -0
  137. pyinfra/operations/yum.py +210 -0
  138. pyinfra/operations/zfs.py +175 -0
  139. pyinfra/operations/zypper.py +192 -0
  140. pyinfra/progress.py +44 -32
  141. pyinfra/py.typed +0 -0
  142. pyinfra/version.py +9 -1
  143. pyinfra-3.5.1.dist-info/METADATA +141 -0
  144. pyinfra-3.5.1.dist-info/RECORD +159 -0
  145. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
  146. pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
  147. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
  148. pyinfra_cli/__init__.py +1 -0
  149. pyinfra_cli/cli.py +780 -0
  150. pyinfra_cli/commands.py +66 -0
  151. pyinfra_cli/exceptions.py +155 -65
  152. pyinfra_cli/inventory.py +233 -89
  153. pyinfra_cli/log.py +39 -43
  154. pyinfra_cli/main.py +26 -495
  155. pyinfra_cli/prints.py +215 -156
  156. pyinfra_cli/util.py +172 -105
  157. pyinfra_cli/virtualenv.py +25 -20
  158. pyinfra/api/connectors/__init__.py +0 -21
  159. pyinfra/api/connectors/ansible.py +0 -99
  160. pyinfra/api/connectors/docker.py +0 -178
  161. pyinfra/api/connectors/local.py +0 -169
  162. pyinfra/api/connectors/ssh.py +0 -402
  163. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  164. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  165. pyinfra/api/connectors/util.py +0 -63
  166. pyinfra/api/connectors/vagrant.py +0 -155
  167. pyinfra/facts/init.py +0 -176
  168. pyinfra/facts/util/files.py +0 -102
  169. pyinfra/hook.py +0 -41
  170. pyinfra/modules/__init__.py +0 -11
  171. pyinfra/modules/apk.py +0 -64
  172. pyinfra/modules/apt.py +0 -272
  173. pyinfra/modules/brew.py +0 -122
  174. pyinfra/modules/files.py +0 -711
  175. pyinfra/modules/gem.py +0 -30
  176. pyinfra/modules/git.py +0 -115
  177. pyinfra/modules/init.py +0 -344
  178. pyinfra/modules/iptables.py +0 -271
  179. pyinfra/modules/lxd.py +0 -45
  180. pyinfra/modules/mysql.py +0 -347
  181. pyinfra/modules/npm.py +0 -47
  182. pyinfra/modules/pacman.py +0 -60
  183. pyinfra/modules/pip.py +0 -99
  184. pyinfra/modules/pkg.py +0 -43
  185. pyinfra/modules/postgresql.py +0 -245
  186. pyinfra/modules/puppet.py +0 -20
  187. pyinfra/modules/python.py +0 -37
  188. pyinfra/modules/server.py +0 -524
  189. pyinfra/modules/ssh.py +0 -150
  190. pyinfra/modules/util/files.py +0 -52
  191. pyinfra/modules/util/packaging.py +0 -118
  192. pyinfra/modules/vzctl.py +0 -133
  193. pyinfra/modules/yum.py +0 -171
  194. pyinfra/pseudo_modules.py +0 -64
  195. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  196. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  197. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  198. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  199. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  200. pyinfra_cli/__main__.py +0 -40
  201. pyinfra_cli/config.py +0 -92
  202. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  203. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/api/operations.py CHANGED
@@ -1,318 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import traceback
1
5
  from itertools import product
2
- from socket import (
3
- error as socket_error,
4
- timeout as timeout_error,
5
- )
6
- from types import FunctionType
6
+ from socket import error as socket_error, timeout as timeout_error
7
+ from typing import TYPE_CHECKING, Optional, cast
7
8
 
8
9
  import click
9
10
  import gevent
10
-
11
11
  from paramiko import SSHException
12
12
 
13
- import pyinfra
14
-
15
13
  from pyinfra import logger
16
- from pyinfra.api.exceptions import PyinfraError
17
- from pyinfra.api.util import format_exception, log_host_command_error
14
+ from pyinfra.connectors.util import CommandOutput, OutputLine
15
+ from pyinfra.context import ctx_host, ctx_state
18
16
  from pyinfra.progress import progress_spinner
19
17
 
18
+ from .arguments import CONNECTOR_ARGUMENT_KEYS, ConnectorArguments
19
+ from .command import FunctionCommand, PyinfraCommand, StringCommand
20
+ from .exceptions import PyinfraError
21
+ from .util import (
22
+ format_exception,
23
+ log_error_or_warning,
24
+ log_host_command_error,
25
+ log_operation_start,
26
+ print_host_combined_output,
27
+ )
20
28
 
21
- def _run_server_op(state, host, op_hash):
22
- # Noop for this host?
23
- if op_hash not in state.ops[host]:
24
- logger.info('{0}{1}'.format(
25
- host.print_prefix,
26
- click.style(
27
- 'Skipped',
28
- 'blue',
29
- ),
30
- ))
31
- return True
32
-
33
- op_data = state.ops[host][op_hash]
34
- op_meta = state.op_meta[op_hash]
35
-
36
- logger.debug('Starting operation {0} on {1}'.format(
37
- ', '.join(op_meta['names']), host,
38
- ))
39
-
40
- state.ops_run.add(op_hash)
41
-
42
- # ...loop through each command
43
- for i, command in enumerate(op_data['commands']):
44
- status = False
45
-
46
- shell_executable = op_meta['shell_executable']
47
- sudo = op_meta['sudo']
48
- sudo_user = op_meta['sudo_user']
49
- su_user = op_meta['su_user']
50
- preserve_sudo_env = op_meta['preserve_sudo_env']
29
+ if TYPE_CHECKING:
30
+ from .inventory import Host
31
+ from .state import State
51
32
 
52
- # As dicts, individual commands can override meta settings (ie on a
53
- # per-host basis generated during deploy).
54
- if isinstance(command, dict):
55
- if 'sudo' in command:
56
- sudo = command['sudo']
57
33
 
58
- if 'sudo_user' in command:
59
- sudo_user = command['sudo_user']
34
+ # Run a single host operation
35
+ #
60
36
 
61
- if 'su_user' in command:
62
- su_user = command['su_user']
63
37
 
64
- if 'shell_executable' in command:
65
- shell_executable = command['shell_executable']
38
+ def run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
39
+ state.trigger_callbacks("operation_host_start", host, op_hash)
66
40
 
67
- command = command['command']
41
+ if op_hash not in state.ops[host]:
42
+ logger.info("{0}{1}".format(host.print_prefix, click.style("Skipped", "blue")))
43
+ return True
68
44
 
69
- # Now we attempt to execute the command
45
+ op_meta = state.get_op_meta(op_hash)
46
+ logger.debug("Starting operation %r on %s", op_meta.names, host)
70
47
 
71
- # Tuples stand for callbacks & file uploads
72
- if isinstance(command, tuple):
73
- # If first element is function, it's a callback
74
- if isinstance(command[0], FunctionType):
75
- func, args, kwargs = command
48
+ if host.executing_op_hash is None:
49
+ host.executing_op_hash = op_hash
50
+ else:
51
+ host.nested_executing_op_hash = op_hash
76
52
 
53
+ try:
54
+ return _run_host_op(state, host, op_hash)
55
+ finally:
56
+ if host.nested_executing_op_hash:
57
+ host.nested_executing_op_hash = None
58
+ else:
59
+ host.executing_op_hash = None
60
+
61
+
62
+ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]:
63
+ op_data = state.get_op_data_for_host(host, op_hash)
64
+ global_arguments = op_data.global_arguments
65
+
66
+ ignore_errors = global_arguments["_ignore_errors"]
67
+ continue_on_error = global_arguments["_continue_on_error"]
68
+ timeout = global_arguments.get("_timeout", 0)
69
+
70
+ # Extract retry arguments
71
+ retries = global_arguments.get("_retries", 0)
72
+ retry_delay = global_arguments.get("_retry_delay", 5)
73
+ retry_until = global_arguments.get("_retry_until", None)
74
+
75
+ executor_kwarg_keys = CONNECTOR_ARGUMENT_KEYS
76
+ # See: https://github.com/python/mypy/issues/10371
77
+ base_connector_arguments: ConnectorArguments = cast(
78
+ ConnectorArguments,
79
+ {key: global_arguments[key] for key in executor_kwarg_keys if key in global_arguments}, # type: ignore[literal-required] # noqa
80
+ )
81
+
82
+ retry_attempt = 0
83
+ did_error = False
84
+ executed_commands = 0
85
+ commands: list[PyinfraCommand] = []
86
+ all_output_lines: list[OutputLine] = []
87
+
88
+ # Retry loop
89
+ while retry_attempt <= retries:
90
+ did_error = False
91
+ executed_commands = 0
92
+ commands = []
93
+ all_output_lines = []
94
+
95
+ for command in op_data.command_generator():
96
+ commands.append(command)
97
+ status = False
98
+ connector_arguments = base_connector_arguments.copy()
99
+ connector_arguments.update(command.connector_arguments)
100
+
101
+ if not isinstance(command, PyinfraCommand):
102
+ raise TypeError("{0} is an invalid pyinfra command!".format(command))
103
+
104
+ if isinstance(command, FunctionCommand):
77
105
  try:
78
- status = func(
79
- state, host,
80
- *args, **kwargs
81
- )
82
-
83
- # Custom functions could do anything, so expect anything!
106
+ status = command.execute(state, host, connector_arguments)
84
107
  except Exception as e:
85
- logger.error('{0}{1}'.format(
86
- host.print_prefix,
87
- click.style(
88
- 'Unexpected error in Python callback: {0}'.format(
89
- format_exception(e),
90
- ),
91
- 'red',
92
- ),
93
- ))
94
-
95
- # Non-function mean files to copy
96
- else:
97
- method_type, first_file, second_file = command
98
-
99
- if method_type == 'upload':
100
- method = host.put_file
101
- elif method_type == 'download':
102
- method = host.get_file
103
- else:
104
- raise TypeError('{0} is an invalid pyinfra command!'.format(command))
108
+ # Custom functions could do anything, so expect anything!
109
+ logger.warning(traceback.format_exc())
110
+ host.log_styled(
111
+ f"Unexpected error in Python callback: {format_exception(e)}",
112
+ fg="red",
113
+ log_func=logger.warning,
114
+ )
105
115
 
116
+ elif isinstance(command, StringCommand):
117
+ output_lines = CommandOutput([])
106
118
  try:
107
- status = method(
119
+ status, output_lines = command.execute(
108
120
  state,
109
- first_file,
110
- second_file,
111
- sudo=sudo,
112
- sudo_user=sudo_user,
113
- su_user=su_user,
114
- shell_executable=shell_executable,
115
- print_output=state.print_output,
121
+ host,
122
+ connector_arguments,
116
123
  )
124
+ except (timeout_error, socket_error, SSHException) as e:
125
+ log_host_command_error(host, e, timeout=timeout)
126
+ all_output_lines.extend(output_lines)
127
+ # If we failed and have not already printed the stderr, print it
128
+ if status is False and not state.print_output:
129
+ print_host_combined_output(host, output_lines)
117
130
 
131
+ else:
132
+ try:
133
+ status = command.execute(state, host, connector_arguments)
118
134
  except (timeout_error, socket_error, SSHException, IOError) as e:
119
- log_host_command_error(
120
- host,
121
- e,
122
- timeout=op_meta['timeout'],
135
+ log_host_command_error(host, e, timeout=timeout)
136
+
137
+ # Break the loop to trigger a failure
138
+ if status is False:
139
+ did_error = True
140
+ if continue_on_error is True:
141
+ continue
142
+ break
143
+
144
+ executed_commands += 1
145
+
146
+ # Check if we should retry
147
+ should_retry = False
148
+ if retry_attempt < retries:
149
+ # Retry on error
150
+ if did_error:
151
+ should_retry = True
152
+ # Retry on condition if no error
153
+ elif retry_until and not did_error:
154
+ try:
155
+ output_data = {
156
+ "stdout_lines": [
157
+ line.line for line in all_output_lines if line.buffer_name == "stdout"
158
+ ],
159
+ "stderr_lines": [
160
+ line.line for line in all_output_lines if line.buffer_name == "stderr"
161
+ ],
162
+ "commands": [str(command) for command in commands],
163
+ "executed_commands": executed_commands,
164
+ "host": host.name,
165
+ "operation": ", ".join(state.get_op_meta(op_hash).names) or "Operation",
166
+ }
167
+ should_retry = retry_until(output_data)
168
+ except Exception as e:
169
+ host.log_styled(
170
+ f"Error in retry_until function: {format_exception(e)}",
171
+ fg="red",
172
+ log_func=logger.warning,
123
173
  )
124
174
 
125
- # Must be a string/shell command: execute it on the server w/op-level preferences
126
- elif isinstance(command, str):
127
- combined_output_lines = []
175
+ if should_retry:
176
+ retry_attempt += 1
177
+ state.trigger_callbacks("operation_host_retry", host, op_hash, retry_attempt, retries)
178
+ op_name = ", ".join(state.get_op_meta(op_hash).names) or "Operation"
179
+ host.log_styled(
180
+ f"Retrying {op_name} (attempt {retry_attempt}/{retries}) after {retry_delay}s...",
181
+ fg="yellow",
182
+ log_func=logger.info,
183
+ )
184
+ time.sleep(retry_delay)
185
+ continue
128
186
 
129
- try:
130
- status, combined_output_lines = host.run_shell_command(
131
- state,
132
- command.strip(),
133
- sudo=sudo,
134
- sudo_user=sudo_user,
135
- su_user=su_user,
136
- preserve_sudo_env=preserve_sudo_env,
137
- shell_executable=shell_executable,
138
- timeout=op_meta['timeout'],
139
- get_pty=op_meta['get_pty'],
140
- env=op_meta['env'],
141
- print_output=state.print_output,
142
- return_combined_output=True,
143
- )
187
+ break
144
188
 
145
- except (timeout_error, socket_error, SSHException) as e:
146
- log_host_command_error(
147
- host,
148
- e,
149
- timeout=op_meta['timeout'],
150
- )
189
+ # Handle results
190
+ op_success = return_status = not did_error
191
+ host_results = state.get_results_for_host(host)
151
192
 
152
- # If we failed and have no already printed the stderr, print it
153
- if status is False and not state.print_output:
154
- for type_, line in combined_output_lines:
155
- if type_ == 'stderr':
156
- logger.error('{0}{1}'.format(
157
- host.print_prefix,
158
- click.style(line, 'red'),
159
- ))
160
- else:
161
- logger.error('{0}{1}'.format(
162
- host.print_prefix,
163
- line,
164
- ))
165
- else:
166
- raise TypeError('{0} is an invalid pyinfra command!'.format(command))
193
+ if did_error is False:
194
+ host_results.ops += 1
195
+ host_results.success_ops += 1
167
196
 
168
- # Break the loop to trigger a failure
169
- if status is False:
170
- break
171
- else:
172
- state.results[host]['commands'] += 1
197
+ _status_log = "Success" if executed_commands > 0 else "No changes"
198
+ if retry_attempt > 0:
199
+ _status_log = f"{_status_log} on retry {retry_attempt}"
200
+
201
+ _click_log_status = click.style(_status_log, "green")
202
+ logger.info("{0}{1}".format(host.print_prefix, _click_log_status))
173
203
 
174
- # Commands didn't break, so count our successes & return True!
204
+ state.trigger_callbacks("operation_host_success", host, op_hash, retry_attempt)
175
205
  else:
176
- # Count success
177
- state.results[host]['ops'] += 1
178
- state.results[host]['success_ops'] += 1
179
-
180
- logger.info('{0}{1}'.format(
181
- host.print_prefix,
182
- click.style(
183
- 'Success' if len(op_data['commands']) > 0 else 'No changes',
184
- 'green',
185
- ),
186
- ))
187
-
188
- # Trigger any success handler
189
- if op_meta['on_success']:
190
- op_meta['on_success'](state, host, op_hash)
206
+ if ignore_errors:
207
+ host_results.ignored_error_ops += 1
208
+ else:
209
+ host_results.error_ops += 1
191
210
 
192
- return True
211
+ if executed_commands:
212
+ host_results.partial_ops += 1
193
213
 
194
- # Up error_ops & log
195
- state.results[host]['error_ops'] += 1
214
+ _command_description = f"executed {executed_commands} commands"
215
+ if retry_attempt > 0:
216
+ _command_description = (
217
+ f"{_command_description} (failed after {retry_attempt}/{retries} retries)"
218
+ )
219
+
220
+ log_error_or_warning(host, ignore_errors, _command_description, continue_on_error)
221
+
222
+ # Ignored, op "completes" w/ ignored error
223
+ if ignore_errors:
224
+ host_results.ops += 1
225
+ return_status = True
226
+
227
+ # Unignored error -> False
228
+ state.trigger_callbacks("operation_host_error", host, op_hash, retry_attempt, retries)
229
+
230
+ op_data.operation_meta.set_complete(
231
+ op_success,
232
+ commands,
233
+ CommandOutput(all_output_lines),
234
+ retry_attempts=retry_attempt,
235
+ max_retries=retries,
236
+ )
237
+
238
+ return return_status
196
239
 
197
- if op_meta['ignore_errors']:
198
- logger.warning('{0}{1}'.format(
199
- host.print_prefix,
200
- click.style('Error (ignored)', 'yellow'),
201
- ))
202
- else:
203
- logger.error('{0}{1}'.format(
204
- host.print_prefix,
205
- click.style('Error', 'red'),
206
- ))
207
240
 
208
- # Always trigger any error handler
209
- if op_meta['on_error']:
210
- op_meta['on_error'](state, host, op_hash)
241
+ # Run all operations strategies
242
+ #
211
243
 
212
- # Ignored, op "completes" w/ ignored error
213
- if op_meta['ignore_errors']:
214
- state.results[host]['ops'] += 1
215
244
 
216
- # Unignored error -> False
217
- return False
245
+ def _run_host_op_with_context(state: "State", host: "Host", op_hash: str):
246
+ with ctx_host.use(host):
247
+ return run_host_op(state, host, op_hash)
218
248
 
219
249
 
220
- def _run_server_ops(state, host, progress=None):
221
- '''
250
+ def _run_host_ops(state: "State", host: "Host", progress=None):
251
+ """
222
252
  Run all ops for a single server.
223
- '''
253
+ """
224
254
 
225
- logger.debug('Running all ops on {0}'.format(host))
255
+ logger.debug("Running all ops on %s", host)
226
256
 
227
257
  for op_hash in state.get_op_order():
228
- op_meta = state.op_meta[op_hash]
258
+ op_meta = state.get_op_meta(op_hash)
259
+ log_operation_start(op_meta)
229
260
 
230
- logger.info('--> {0} {1} on {2}'.format(
231
- click.style('--> Starting operation:', 'blue'),
232
- click.style(', '.join(op_meta['names']), bold=True),
233
- click.style(host.name, bold=True),
234
- ))
235
-
236
- result = _run_server_op(state, host, op_hash)
261
+ result = _run_host_op_with_context(state, host, op_hash)
237
262
 
238
263
  # Trigger CLI progress if provided
239
264
  if progress:
240
265
  progress((host, op_hash))
241
266
 
242
267
  if result is False:
243
- raise PyinfraError('Error in operation {0} on {1}'.format(
244
- ', '.join(op_meta['names']), host,
245
- ))
246
-
247
- if pyinfra.is_cli:
248
- print()
268
+ raise PyinfraError(
269
+ "Error in operation {0} on {1}".format(
270
+ ", ".join(op_meta.names),
271
+ host,
272
+ ),
273
+ )
249
274
 
250
275
 
251
- def _run_serial_ops(state):
252
- '''
276
+ def _run_serial_ops(state: "State"):
277
+ """
253
278
  Run all ops for all servers, one server at a time.
254
- '''
279
+ """
255
280
 
256
- for host in list(state.inventory):
281
+ for host in list(state.inventory.iter_active_hosts()):
257
282
  host_operations = product([host], state.get_op_order())
258
283
  with progress_spinner(host_operations) as progress:
259
284
  try:
260
- _run_server_ops(
261
- state, host,
285
+ _run_host_ops(
286
+ state,
287
+ host,
262
288
  progress=progress,
263
289
  )
264
290
  except PyinfraError:
265
291
  state.fail_hosts({host})
266
292
 
267
293
 
268
- def _run_no_wait_ops(state):
269
- '''
294
+ def _run_no_wait_ops(state: "State"):
295
+ """
270
296
  Run all ops for all servers at once.
271
- '''
297
+ """
272
298
 
273
- hosts_operations = product(state.inventory, state.get_op_order())
299
+ hosts_operations = product(state.inventory.iter_active_hosts(), state.get_op_order())
274
300
  with progress_spinner(hosts_operations) as progress:
275
301
  # Spawn greenlet for each host to run *all* ops
302
+ if state.pool is None:
303
+ raise PyinfraError("No pool found on state.")
276
304
  greenlets = [
277
305
  state.pool.spawn(
278
- _run_server_ops, state, host,
306
+ _run_host_ops,
307
+ state,
308
+ host,
279
309
  progress=progress,
280
310
  )
281
- for host in state.inventory
311
+ for host in state.inventory.iter_active_hosts()
282
312
  ]
283
313
  gevent.joinall(greenlets)
284
314
 
285
315
 
286
- def _run_single_op(state, op_hash):
287
- '''
316
+ def _run_single_op(state: "State", op_hash: str):
317
+ """
288
318
  Run a single operation for all servers. Can be configured to run in serial.
289
- '''
290
-
291
- op_meta = state.op_meta[op_hash]
292
-
293
- op_types = []
319
+ """
294
320
 
295
- if op_meta['serial']:
296
- op_types.append('serial')
321
+ state.trigger_callbacks("operation_start", op_hash)
297
322
 
298
- if op_meta['run_once']:
299
- op_types.append('run once')
300
-
301
- logger.info('{0} {1} {2}'.format(
302
- click.style('--> Starting{0}operation:'.format(
303
- ' {0} '.format(', '.join(op_types)) if op_types else ' ',
304
- ), 'blue'),
305
- click.style(', '.join(op_meta['names']), bold=True),
306
- tuple(op_meta['args']) if op_meta['args'] else '',
307
- ))
323
+ op_meta = state.get_op_meta(op_hash)
324
+ log_operation_start(op_meta)
308
325
 
309
326
  failed_hosts = set()
310
327
 
311
- if op_meta['serial']:
312
- with progress_spinner(state.inventory) as progress:
328
+ if op_meta.global_arguments["_serial"]:
329
+ with progress_spinner(state.inventory.iter_active_hosts()) as progress:
313
330
  # For each host, run the op
314
- for host in state.inventory:
315
- result = _run_server_op(state, host, op_hash)
331
+ for host in state.inventory.iter_active_hosts():
332
+ result = _run_host_op_with_context(state, host, op_hash)
316
333
  progress(host)
317
334
 
318
335
  if not result:
@@ -320,23 +337,21 @@ def _run_single_op(state, op_hash):
320
337
 
321
338
  else:
322
339
  # Start with the whole inventory in one batch
323
- batches = [state.inventory]
340
+ batches = [list(state.inventory.iter_active_hosts())]
324
341
 
325
342
  # If parallel set break up the inventory into a series of batches
326
- if op_meta['parallel']:
327
- parallel = op_meta['parallel']
328
- hosts = list(state.inventory)
329
-
330
- batches = [
331
- hosts[i:i + parallel]
332
- for i in range(0, len(hosts), parallel)
333
- ]
343
+ parallel = op_meta.global_arguments["_parallel"]
344
+ if parallel:
345
+ hosts = list(state.inventory.iter_active_hosts())
346
+ batches = [hosts[i : i + parallel] for i in range(0, len(hosts), parallel)]
334
347
 
335
348
  for batch in batches:
336
349
  with progress_spinner(batch) as progress:
337
350
  # Spawn greenlet for each host
351
+ if state.pool is None:
352
+ raise PyinfraError("No pool found on state.")
338
353
  greenlet_to_host = {
339
- state.pool.spawn(_run_server_op, state, host, op_hash): host
354
+ state.pool.spawn(_run_host_op_with_context, state, host, op_hash): host
340
355
  for host in batch
341
356
  }
342
357
 
@@ -351,34 +366,32 @@ def _run_single_op(state, op_hash):
351
366
  failed_hosts.add(host)
352
367
 
353
368
  # Now all the batches/hosts are complete, fail any failures
354
- if not op_meta['ignore_errors']:
355
- state.fail_hosts(failed_hosts)
369
+ state.fail_hosts(failed_hosts)
356
370
 
357
- if pyinfra.is_cli:
358
- print()
371
+ state.trigger_callbacks("operation_end", op_hash)
359
372
 
360
373
 
361
- def run_ops(state, serial=False, no_wait=False):
362
- '''
374
+ def run_ops(state: "State", serial: bool = False, no_wait: bool = False):
375
+ """
363
376
  Runs all operations across all servers in a configurable manner.
364
377
 
365
378
  Args:
366
379
  state (``pyinfra.api.State`` obj): the deploy state to execute
367
380
  serial (boolean): whether to run operations host by host
368
381
  no_wait (boolean): whether to wait for all hosts between operations
369
- '''
382
+ """
370
383
 
371
384
  # Flag state as deploy in process
372
- state.deploying = True
373
-
374
- # Run all ops, but server by server
375
- if serial:
376
- _run_serial_ops(state)
377
-
378
- # Run all the ops on each server in parallel (not waiting at each operation)
379
- elif no_wait:
380
- _run_no_wait_ops(state)
381
-
382
- # Default: run all ops in order, waiting at each for all servers to complete
383
- for op_hash in state.get_op_order():
384
- _run_single_op(state, op_hash)
385
+ state.is_executing = True
386
+
387
+ with ctx_state.use(state):
388
+ # Run all ops, but server by server
389
+ if serial:
390
+ _run_serial_ops(state)
391
+ # Run all the ops on each server in parallel (not waiting at each operation)
392
+ elif no_wait:
393
+ _run_no_wait_ops(state)
394
+ # Default: run all ops in order, waiting at each for all servers to complete
395
+ else:
396
+ for op_hash in state.get_op_order():
397
+ _run_single_op(state, op_hash)