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_cli/prints.py CHANGED
@@ -1,26 +1,33 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
4
+ import platform
2
5
  import re
6
+ import sys
7
+ from typing import TYPE_CHECKING, Callable, Dict, Iterator, List, Tuple, Union
3
8
 
4
9
  import click
5
10
 
6
- from pyinfra import logger
7
- from pyinfra.api.facts import get_fact_names
11
+ from pyinfra import __version__, logger
8
12
  from pyinfra.api.host import Host
9
- from pyinfra.api.operation import get_operation_names
10
13
 
11
14
  from .util import json_encode
12
15
 
13
- ANSI_RE = re.compile('\033\[((?:\d|;)*)([a-zA-Z])') # noqa: W605
16
+ if TYPE_CHECKING:
17
+ from pyinfra.api.state import State
18
+
19
+
20
+ ANSI_RE = re.compile(r"\033\[((?:\d|;)*)([a-zA-Z])")
14
21
 
15
22
 
16
23
  def _strip_ansi(value):
17
- return ANSI_RE.sub('', value)
24
+ return ANSI_RE.sub("", value)
18
25
 
19
26
 
20
- def _get_group_combinations(inventory):
21
- group_combinations = {}
27
+ def _get_group_combinations(inventory: Iterator[Host]):
28
+ group_combinations: Dict[Tuple, List[Host]] = {}
22
29
 
23
- for host in inventory.iter_all_hosts():
30
+ for host in inventory:
24
31
  # Tuple for hashability, set to normalise order
25
32
  host_groups = tuple(set(host.groups))
26
33
 
@@ -45,34 +52,32 @@ def jsonify(data, *args, **kwargs):
45
52
  return json.dumps(data, *args, **kwargs)
46
53
 
47
54
 
48
- def print_state_facts(state):
49
- print()
50
- print('--> Facts:')
51
- print(jsonify(state.facts, indent=4, default=json_encode))
55
+ def print_state_operations(state: "State"):
56
+ state_ops = {host: ops for host, ops in state.ops.items() if state.is_host_in_limit(host)}
52
57
 
58
+ click.echo(err=True)
59
+ click.echo("--> Operations:", err=True)
60
+ click.echo(jsonify(state_ops, indent=4, default=json_encode), err=True)
61
+ click.echo(err=True)
62
+ click.echo("--> Operation meta:", err=True)
63
+ click.echo(jsonify(state.op_meta, indent=4, default=json_encode), err=True)
53
64
 
54
- def print_state_operations(state):
55
- print()
56
- print('--> Operations:')
57
- print(jsonify(state.ops, indent=4, default=json_encode))
58
- print()
59
- print('--> Operation meta:')
60
- print(jsonify(state.op_meta, indent=4, default=json_encode))
61
-
62
- print()
63
- print('--> Operation order:')
64
- print()
65
+ click.echo(err=True)
66
+ click.echo("--> Operation order:", err=True)
67
+ click.echo(err=True)
65
68
  for op_hash in state.get_op_order():
66
69
  meta = state.op_meta[op_hash]
67
- hosts = set(
68
- host for host, operations in state.ops.items()
69
- if op_hash in operations
70
+ hosts = set(host for host, operations in state.ops.items() if op_hash in operations)
71
+
72
+ click.echo(
73
+ " {0} (names={1}, hosts={2})".format(
74
+ op_hash,
75
+ meta.names,
76
+ hosts,
77
+ ),
78
+ err=True,
70
79
  )
71
80
 
72
- print(' {0} (names={1}, hosts={2})'.format(
73
- op_hash, meta['names'], hosts,
74
- ))
75
-
76
81
 
77
82
  def print_groups_by_comparison(print_items, comparator=lambda item: item[0]):
78
83
  items = []
@@ -84,79 +89,110 @@ def print_groups_by_comparison(print_items, comparator=lambda item: item[0]):
84
89
  items.append(name)
85
90
 
86
91
  else:
87
- print(' {0}'.format(', '.join((
88
- click.style(name, bold=True)
89
- for name in items
90
- ))))
92
+ click.echo(
93
+ " {0}".format(", ".join((click.style(name, bold=True) for name in items))),
94
+ err=True,
95
+ )
91
96
 
92
97
  items = [name]
93
98
 
94
99
  last_name = name
95
100
 
96
101
  if items:
97
- print(' {0}'.format(', '.join((
98
- click.style(name, bold=True)
99
- for name in items
100
- ))))
101
-
102
-
103
- def print_facts_list():
104
- fact_names = sorted(get_fact_names())
105
- print_groups_by_comparison(fact_names)
106
-
107
-
108
- def print_operations_list():
109
- operation_names = sorted(get_operation_names())
110
- print_groups_by_comparison(
111
- operation_names,
112
- comparator=lambda item: item.split('.')[0],
113
- )
102
+ click.echo(
103
+ " {0}".format(", ".join((click.style(name, bold=True) for name in items))),
104
+ err=True,
105
+ )
114
106
 
115
107
 
116
108
  def print_fact(fact_data):
117
- print(jsonify(fact_data, indent=4, default=json_encode))
109
+ click.echo(jsonify(fact_data, indent=4, default=json_encode), err=True)
118
110
 
119
111
 
120
- def print_inventory(state):
112
+ def print_inventory(state: "State"):
121
113
  for host in state.inventory:
122
- if not state.is_host_in_limit(host):
123
- continue
124
-
125
- print()
126
- print('--> Data for: {0}'.format(click.style(host.name, bold=True)))
127
- print(jsonify(host.data, indent=4, default=json_encode))
114
+ click.echo(err=True)
115
+ click.echo(host.print_prefix, err=True)
116
+ click.echo("--> Groups: {0}".format(", ".join(host.groups)), err=True)
117
+ click.echo("--> Data:", err=True)
118
+ click.echo(jsonify(host.data, indent=4, default=json_encode), err=True)
128
119
 
129
120
 
130
121
  def print_facts(facts):
131
122
  for name, data in facts.items():
132
- print()
133
- print('--> Fact data for: {0}'.format(
134
- click.style(name, bold=True),
135
- ))
123
+ click.echo(err=True)
124
+ click.echo(
125
+ "--> Fact data for: {0}".format(
126
+ click.style(name, bold=True),
127
+ ),
128
+ err=True,
129
+ )
136
130
  print_fact(data)
137
131
 
138
132
 
133
+ def print_support_info() -> None:
134
+ from importlib.metadata import PackageNotFoundError, requires, version
135
+
136
+ from packaging.requirements import Requirement
137
+
138
+ click.echo(
139
+ """
140
+ If you are having issues with pyinfra or wish to make feature requests, please
141
+ check out the GitHub issues at https://github.com/Fizzadar/pyinfra/issues .
142
+ When adding an issue, be sure to include the following:
143
+ """,
144
+ )
145
+
146
+ click.echo(" System: {0}".format(platform.system()), err=True)
147
+ click.echo(" Platform: {0}".format(platform.platform()), err=True)
148
+ click.echo(" Release: {0}".format(platform.uname()[2]), err=True)
149
+ click.echo(" Machine: {0}".format(platform.uname()[4]), err=True)
150
+ click.echo(" pyinfra: v{0}".format(__version__), err=True)
151
+
152
+ seen_reqs: set[str] = set()
153
+ for requirement_string in sorted(requires("pyinfra") or []):
154
+ requirement = Requirement(requirement_string)
155
+ if requirement.name in seen_reqs:
156
+ continue
157
+ seen_reqs.add(requirement.name)
158
+ try:
159
+ click.echo(
160
+ " {0}: v{1}".format(requirement.name, version(requirement.name)),
161
+ err=True,
162
+ )
163
+ except PackageNotFoundError:
164
+ # package not installed in this environment
165
+ continue
166
+
167
+ click.echo(" Executable: {0}".format(sys.argv[0]), err=True)
168
+ click.echo(
169
+ " Python: {0} ({1}, {2})".format(
170
+ platform.python_version(),
171
+ platform.python_implementation(),
172
+ platform.python_compiler(),
173
+ ),
174
+ err=True,
175
+ )
176
+
177
+
139
178
  def print_rows(rows):
140
179
  # Go through the rows and work out all the widths in each column
141
- column_widths = []
180
+ row_column_widths: list[list[int]] = []
142
181
 
143
182
  for _, columns in rows:
144
183
  if isinstance(columns, str):
145
184
  continue
146
185
 
147
186
  for i, column in enumerate(columns):
148
- if i >= len(column_widths):
149
- column_widths.append([])
187
+ if i >= len(row_column_widths):
188
+ row_column_widths.append([])
150
189
 
151
190
  # Length of the column (with ansi codes removed)
152
191
  width = len(_strip_ansi(column.strip()))
153
- column_widths[i].append(width)
192
+ row_column_widths[i].append(width)
154
193
 
155
194
  # Get the max width of each column and add 4 padding spaces
156
- column_widths = [
157
- max(widths) + 4
158
- for widths in column_widths
159
- ]
195
+ column_widths = [max(widths) + 4 for widths in row_column_widths]
160
196
 
161
197
  # Now print each column, keeping text justified to the widths above
162
198
  for func, columns in rows:
@@ -170,115 +206,138 @@ def print_rows(rows):
170
206
  desired_width = column_widths[i]
171
207
  padding = desired_width - len(stripped)
172
208
 
173
- justified.append('{0}{1}'.format(
174
- column, ' '.join('' for _ in range(padding)),
175
- ))
209
+ justified.append(
210
+ "{0}{1}".format(
211
+ column,
212
+ " ".join("" for _ in range(padding)),
213
+ ),
214
+ )
176
215
 
177
- line = ''.join(justified)
216
+ line = "".join(justified)
178
217
 
179
218
  func(line)
180
219
 
181
220
 
182
- def print_meta(state):
183
- group_combinations = _get_group_combinations(state.inventory)
184
- rows = []
221
+ def truncate(text, max_length):
222
+ if len(text) <= max_length:
223
+ return text
185
224
 
186
- for i, (groups, hosts) in enumerate(group_combinations.items(), 1):
187
- hosts = [
188
- host for host in hosts
189
- if state.is_host_in_limit(host)
190
- ]
225
+ text = text[: max_length - 3]
226
+ return f"{text}..."
191
227
 
192
- if not hosts:
193
- continue # pragma: no cover
194
228
 
195
- if groups:
196
- rows.append((logger.info, 'Groups: {0}'.format(
197
- click.style(' / '.join(groups), bold=True),
198
- )))
199
- else:
200
- rows.append((logger.info, 'Ungrouped:'))
229
+ def pretty_op_name(op_meta):
230
+ name = list(op_meta.names)[0]
201
231
 
202
- for host in hosts:
203
- meta = state.meta[host]
232
+ if op_meta.args:
233
+ name = "{0} ({1})".format(name, ", ".join(str(arg) for arg in op_meta.args))
234
+
235
+ return name
204
236
 
205
- # Didn't connect to this host?
206
- if host not in state.activated_hosts:
207
- rows.append((logger.info, [
208
- host.style_print_prefix('red', bold=True),
209
- click.style('No connection', 'red'),
210
- ]))
211
- continue
212
237
 
213
- rows.append((logger.info, [
214
- host.print_prefix,
215
- 'Operations: {0}'.format(meta['ops']),
216
- 'Commands: {0}'.format(meta['commands']),
217
- ]))
238
+ def print_meta(state: "State"):
239
+ rows: List[Tuple[Callable, Union[List[str], str]]] = [
240
+ (logger.info, ["Operation", "Change", "Conditional Change"]),
241
+ ]
218
242
 
219
- if i != len(group_combinations):
220
- rows.append((print, []))
243
+ for op_hash in state.get_op_order():
244
+ hosts_in_op = []
245
+ hosts_maybe_in_op = []
246
+ for host in state.inventory.iter_activated_hosts():
247
+ if op_hash in state.ops[host]:
248
+ op_data = state.get_op_data_for_host(host, op_hash)
249
+ if op_data.operation_meta._maybe_is_change:
250
+ if op_data.global_arguments["_if"]:
251
+ hosts_maybe_in_op.append(host.name)
252
+ else:
253
+ hosts_in_op.append(host.name)
254
+
255
+ rows.append(
256
+ (
257
+ logger.info,
258
+ [
259
+ pretty_op_name(state.op_meta[op_hash]),
260
+ (
261
+ "-"
262
+ if len(hosts_in_op) == 0
263
+ else "{0} ({1})".format(
264
+ len(hosts_in_op),
265
+ truncate(", ".join(sorted(hosts_in_op)), 48),
266
+ )
267
+ ),
268
+ (
269
+ "-"
270
+ if len(hosts_maybe_in_op) == 0
271
+ else "{0} ({1})".format(
272
+ len(hosts_maybe_in_op),
273
+ truncate(", ".join(sorted(hosts_maybe_in_op)), 48),
274
+ )
275
+ ),
276
+ ],
277
+ )
278
+ )
221
279
 
222
280
  print_rows(rows)
223
281
 
224
282
 
225
- def print_results(state):
226
- group_combinations = _get_group_combinations(state.inventory)
227
- rows = []
283
+ def print_results(state: "State"):
284
+ rows: List[Tuple[Callable, Union[List[str], str]]] = [
285
+ (logger.info, ["Operation", "Hosts", "Success", "Error", "No Change"]),
286
+ ]
287
+
288
+ totals = {"hosts": 0, "success": 0, "error": 0, "no_change": 0}
228
289
 
229
- for i, (groups, hosts) in enumerate(group_combinations.items(), 1):
230
- hosts = [
231
- host for host in hosts
232
- if state.is_host_in_limit(host)
290
+ for op_hash in state.get_op_order():
291
+ hosts_in_op = 0
292
+ hosts_in_op_success: list[str] = []
293
+ hosts_in_op_error: list[str] = []
294
+ hosts_in_op_no_change: list[str] = []
295
+ for host in state.inventory.iter_activated_hosts():
296
+ if op_hash not in state.ops[host]:
297
+ continue
298
+
299
+ hosts_in_op += 1
300
+
301
+ op_meta = state.ops[host][op_hash].operation_meta
302
+ if op_meta.did_succeed(_raise_if_not_complete=False):
303
+ if op_meta.did_change():
304
+ hosts_in_op_success.append(host.name)
305
+ else:
306
+ hosts_in_op_no_change.append(host.name)
307
+ else:
308
+ hosts_in_op_error.append(host.name)
309
+
310
+ row = [
311
+ pretty_op_name(state.op_meta[op_hash]),
312
+ str(hosts_in_op),
233
313
  ]
234
314
 
235
- if not hosts:
236
- continue
315
+ totals["hosts"] += hosts_in_op
237
316
 
238
- if groups:
239
- rows.append((logger.info, 'Groups: {0}'.format(
240
- click.style(' / '.join(groups), bold=True),
241
- )))
317
+ if hosts_in_op_success:
318
+ num_hosts_in_op_success = len(hosts_in_op_success)
319
+ row.append(str(num_hosts_in_op_success))
320
+ totals["success"] += num_hosts_in_op_success
242
321
  else:
243
- rows.append((logger.info, 'Ungrouped:'))
244
-
245
- for host in hosts:
246
- # Didn't conenct to this host?
247
- if host not in state.activated_hosts:
248
- rows.append((logger.info, [
249
- host.style_print_prefix('red', bold=True),
250
- click.style('No connection', 'red'),
251
- ]))
252
- continue
253
-
254
- results = state.results[host]
322
+ row.append("-")
255
323
 
256
- meta = state.meta[host]
257
- success_ops = results['success_ops']
258
- error_ops = results['error_ops']
324
+ if hosts_in_op_error:
325
+ num_hosts_in_op_error = len(hosts_in_op_error)
326
+ row.append(str(num_hosts_in_op_error))
327
+ totals["error"] += num_hosts_in_op_error
328
+ else:
329
+ row.append("-")
259
330
 
260
- host_args = ('green',)
261
- host_kwargs = {}
331
+ if hosts_in_op_no_change:
332
+ num_hosts_in_op_no_change = len(hosts_in_op_no_change)
333
+ row.append(str(num_hosts_in_op_no_change))
334
+ totals["no_change"] += num_hosts_in_op_no_change
335
+ else:
336
+ row.append("-")
262
337
 
263
- # If all ops got complete
264
- if results['ops'] == meta['ops']:
265
- # We had some errors - but we ignored them - so "warning" color
266
- if error_ops != 0:
267
- host_args = ('yellow',)
338
+ rows.append((logger.info, row))
268
339
 
269
- # Ops did not complete!
270
- else:
271
- host_args = ('red',)
272
- host_kwargs['bold'] = True
273
-
274
- rows.append((logger.info, [
275
- host.style_print_prefix(*host_args, **host_kwargs),
276
- 'Successful: {0}'.format(click.style(str(success_ops), bold=True)),
277
- 'Errors: {0}'.format(click.style(str(error_ops), bold=True)),
278
- 'Commands: {0}/{1}'.format(results['commands'], meta['commands']),
279
- ]))
280
-
281
- if i != len(group_combinations):
282
- rows.append((print, []))
340
+ totals_row = ["Grand total"] + [str(i) if i else "-" for i in totals.values()]
341
+ rows.append((logger.info, totals_row))
283
342
 
284
343
  print_rows(rows)