pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.2__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.
Files changed (148) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +115 -97
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +139 -39
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/deploy.py +19 -19
  8. pyinfra/api/exceptions.py +35 -4
  9. pyinfra/api/facts.py +62 -86
  10. pyinfra/api/host.py +102 -15
  11. pyinfra/api/inventory.py +4 -0
  12. pyinfra/api/operation.py +188 -120
  13. pyinfra/api/operations.py +66 -113
  14. pyinfra/api/state.py +53 -34
  15. pyinfra/api/util.py +64 -33
  16. pyinfra/connectors/base.py +65 -20
  17. pyinfra/connectors/chroot.py +15 -13
  18. pyinfra/connectors/docker.py +62 -72
  19. pyinfra/connectors/dockerssh.py +20 -19
  20. pyinfra/connectors/local.py +32 -22
  21. pyinfra/connectors/ssh.py +162 -86
  22. pyinfra/connectors/sshuserclient/client.py +1 -1
  23. pyinfra/connectors/terraform.py +57 -39
  24. pyinfra/connectors/util.py +26 -27
  25. pyinfra/connectors/vagrant.py +27 -26
  26. pyinfra/context.py +1 -0
  27. pyinfra/facts/apk.py +7 -2
  28. pyinfra/facts/apt.py +15 -7
  29. pyinfra/facts/brew.py +28 -13
  30. pyinfra/facts/bsdinit.py +9 -6
  31. pyinfra/facts/cargo.py +6 -3
  32. pyinfra/facts/choco.py +8 -4
  33. pyinfra/facts/deb.py +21 -9
  34. pyinfra/facts/dnf.py +11 -6
  35. pyinfra/facts/docker.py +30 -5
  36. pyinfra/facts/files.py +49 -33
  37. pyinfra/facts/gem.py +7 -2
  38. pyinfra/facts/git.py +14 -21
  39. pyinfra/facts/gpg.py +4 -1
  40. pyinfra/facts/hardware.py +186 -138
  41. pyinfra/facts/launchd.py +7 -2
  42. pyinfra/facts/lxd.py +8 -2
  43. pyinfra/facts/mysql.py +19 -12
  44. pyinfra/facts/npm.py +3 -1
  45. pyinfra/facts/openrc.py +8 -2
  46. pyinfra/facts/pacman.py +13 -5
  47. pyinfra/facts/pip.py +2 -0
  48. pyinfra/facts/pkg.py +5 -1
  49. pyinfra/facts/pkgin.py +7 -2
  50. pyinfra/facts/postgres.py +170 -0
  51. pyinfra/facts/postgresql.py +5 -162
  52. pyinfra/facts/rpm.py +21 -15
  53. pyinfra/facts/runit.py +70 -0
  54. pyinfra/facts/selinux.py +12 -4
  55. pyinfra/facts/server.py +240 -82
  56. pyinfra/facts/snap.py +8 -2
  57. pyinfra/facts/systemd.py +37 -13
  58. pyinfra/facts/sysvinit.py +7 -4
  59. pyinfra/facts/upstart.py +7 -2
  60. pyinfra/facts/util/packaging.py +3 -2
  61. pyinfra/facts/vzctl.py +8 -4
  62. pyinfra/facts/xbps.py +7 -2
  63. pyinfra/facts/yum.py +10 -5
  64. pyinfra/facts/zypper.py +9 -4
  65. pyinfra/operations/apk.py +5 -3
  66. pyinfra/operations/apt.py +28 -25
  67. pyinfra/operations/brew.py +60 -29
  68. pyinfra/operations/bsdinit.py +6 -4
  69. pyinfra/operations/cargo.py +3 -1
  70. pyinfra/operations/choco.py +3 -1
  71. pyinfra/operations/dnf.py +16 -20
  72. pyinfra/operations/docker.py +339 -0
  73. pyinfra/operations/files.py +187 -168
  74. pyinfra/operations/gem.py +3 -1
  75. pyinfra/operations/git.py +23 -25
  76. pyinfra/operations/iptables.py +33 -25
  77. pyinfra/operations/launchd.py +5 -6
  78. pyinfra/operations/lxd.py +7 -4
  79. pyinfra/operations/mysql.py +59 -55
  80. pyinfra/operations/npm.py +8 -1
  81. pyinfra/operations/openrc.py +5 -3
  82. pyinfra/operations/pacman.py +6 -7
  83. pyinfra/operations/pip.py +19 -12
  84. pyinfra/operations/pkg.py +3 -1
  85. pyinfra/operations/pkgin.py +5 -3
  86. pyinfra/operations/postgres.py +349 -0
  87. pyinfra/operations/postgresql.py +18 -335
  88. pyinfra/operations/puppet.py +3 -1
  89. pyinfra/operations/python.py +8 -19
  90. pyinfra/operations/runit.py +182 -0
  91. pyinfra/operations/selinux.py +47 -29
  92. pyinfra/operations/server.py +138 -67
  93. pyinfra/operations/snap.py +3 -1
  94. pyinfra/operations/ssh.py +18 -16
  95. pyinfra/operations/systemd.py +18 -12
  96. pyinfra/operations/sysvinit.py +7 -5
  97. pyinfra/operations/upstart.py +7 -5
  98. pyinfra/operations/util/__init__.py +12 -0
  99. pyinfra/operations/util/docker.py +177 -0
  100. pyinfra/operations/util/files.py +24 -16
  101. pyinfra/operations/util/packaging.py +54 -38
  102. pyinfra/operations/util/service.py +39 -47
  103. pyinfra/operations/vzctl.py +12 -10
  104. pyinfra/operations/xbps.py +5 -3
  105. pyinfra/operations/yum.py +15 -19
  106. pyinfra/operations/zypper.py +9 -10
  107. pyinfra/version.py +5 -2
  108. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/METADATA +51 -58
  109. pyinfra-3.0.2.dist-info/RECORD +168 -0
  110. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/WHEEL +1 -1
  111. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/entry_points.txt +0 -3
  112. pyinfra_cli/__main__.py +4 -3
  113. pyinfra_cli/commands.py +3 -2
  114. pyinfra_cli/exceptions.py +75 -43
  115. pyinfra_cli/inventory.py +52 -31
  116. pyinfra_cli/log.py +10 -2
  117. pyinfra_cli/main.py +88 -65
  118. pyinfra_cli/prints.py +37 -109
  119. pyinfra_cli/util.py +15 -10
  120. tests/test_api/test_api.py +2 -0
  121. tests/test_api/test_api_arguments.py +9 -9
  122. tests/test_api/test_api_deploys.py +15 -19
  123. tests/test_api/test_api_facts.py +4 -5
  124. tests/test_api/test_api_operations.py +18 -20
  125. tests/test_api/test_api_util.py +41 -2
  126. tests/test_cli/test_cli.py +14 -50
  127. tests/test_cli/test_cli_deploy.py +17 -14
  128. tests/test_cli/test_cli_exceptions.py +50 -19
  129. tests/test_cli/test_cli_inventory.py +66 -0
  130. tests/test_cli/util.py +1 -1
  131. tests/test_connectors/test_dockerssh.py +11 -8
  132. tests/test_connectors/test_ssh.py +88 -23
  133. tests/test_connectors/test_sshuserclient.py +1 -1
  134. tests/test_connectors/test_terraform.py +11 -8
  135. tests/test_connectors/test_vagrant.py +6 -6
  136. pyinfra/connectors/ansible.py +0 -175
  137. pyinfra/connectors/mech.py +0 -189
  138. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  139. pyinfra/connectors/winrm.py +0 -312
  140. pyinfra/facts/windows.py +0 -366
  141. pyinfra/facts/windows_files.py +0 -90
  142. pyinfra/operations/windows.py +0 -59
  143. pyinfra/operations/windows_files.py +0 -538
  144. pyinfra-3.0.dev0.dist-info/RECORD +0 -170
  145. tests/test_connectors/test_ansible.py +0 -64
  146. tests/test_connectors/test_mech.py +0 -126
  147. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/LICENSE.md +0 -0
  148. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/top_level.txt +0 -0
pyinfra_cli/inventory.py CHANGED
@@ -1,4 +1,3 @@
1
- import re
2
1
  import socket
3
2
  from collections import defaultdict
4
3
  from os import listdir, path
@@ -37,32 +36,33 @@ def _is_inventory_group(key: str, value: Any):
37
36
  return all(isinstance(item, ALLOWED_HOST_TYPES) for item in value)
38
37
 
39
38
 
40
- def _get_group_data(dirname: str):
39
+ def _get_group_data(dirname_or_filename: str):
41
40
  group_data = {}
42
- group_data_directory = path.join(dirname, "group_data")
43
41
 
44
- logger.debug("Checking possible group_data directory: %s", dirname)
42
+ logger.debug("Checking possible group_data at: %s", dirname_or_filename)
45
43
 
46
- if path.exists(group_data_directory):
47
- files = listdir(group_data_directory)
44
+ if path.exists(dirname_or_filename):
45
+ if path.isfile(dirname_or_filename):
46
+ files = [dirname_or_filename]
47
+ else:
48
+ files = [path.join(dirname_or_filename, file) for file in listdir(dirname_or_filename)]
48
49
 
49
50
  for file in files:
50
51
  if not file.endswith(".py"):
51
52
  continue
52
53
 
53
- group_data_file = path.join(group_data_directory, file)
54
54
  group_name = path.basename(file)[:-3]
55
55
 
56
- logger.debug("Looking for group data in: %s", group_data_file)
56
+ logger.debug("Looking for group data in: %s", file)
57
57
 
58
58
  # Read the files locals into a dict
59
- attrs = exec_file(group_data_file, return_locals=True)
59
+ attrs = exec_file(file, return_locals=True)
60
60
  keys = attrs.get("__all__", attrs.keys())
61
61
 
62
62
  group_data[group_name] = {
63
63
  key: value
64
64
  for key, value in attrs.items()
65
- if key in keys and not key.startswith("_")
65
+ if key in keys and not key.startswith("__")
66
66
  }
67
67
 
68
68
  return group_data
@@ -81,32 +81,52 @@ def _get_any_tuple_first(item: Union[T, Tuple[T, Any]]) -> T:
81
81
  return item[0] if isinstance(item, tuple) else item
82
82
 
83
83
 
84
+ def _resolves_to_host(maybe_host: str) -> bool:
85
+ """Check if a string resolves to a valid IP address."""
86
+ try:
87
+ # Use getaddrinfo to support IPv6 hosts
88
+ socket.getaddrinfo(maybe_host, port=None)
89
+ return True
90
+ except socket.gaierror:
91
+ return False
92
+
93
+
84
94
  def make_inventory(
85
95
  inventory: str,
86
96
  override_data=None,
87
97
  cwd: Optional[str] = None,
88
98
  group_data_directories=None,
89
99
  ):
90
- inventory_func = None
91
-
92
- # (Un)fortunately the CLI is pretty flexible for inventory inputs; we support a single hostname,
93
- # a Python module.function import or a Python file path. All of these are kind of similar, and
94
- # we want error handling to be a good user experience.
95
- # Thus, we'll check for everything but also drop a warning to the console if the inventory looks
96
- # like either an import or hostname but neither works.
97
- if re.match("[a-zA-Z0-9\\.]+[\\.:][a-zA-Z0-9]+", inventory):
98
- # First, try loading the inventory as if it's a Python import function
99
- inventory_func = try_import_module_attribute(inventory, raise_for_no_module=False)
100
- if inventory_func is None:
101
- try:
102
- socket.gethostbyname(inventory)
103
- except socket.gaierror:
104
- logger.warning(f"{inventory} is neither a valid Python import or hostname.")
105
-
106
- if inventory_func is None:
107
- # If not an import, load as if from the filesystem *or* comma separated list, which also
108
- # loads any all.py group data files (imported functions do not load group data).
100
+ # (Un)fortunately the CLI is pretty flexible for inventory inputs; we support inventory files, a
101
+ # single hostname, list of hosts, connectors, and python module.function or module:function
102
+ # imports.
103
+ #
104
+ # We check first for an inventory file, a list of hosts or anything with a connector, because
105
+ # (1) an inventory file is a common use case and (2) no other option can have a comma or an @
106
+ # symbol in them.
107
+ is_path_or_host_list_or_connector = (
108
+ path.exists(inventory) or "," in inventory or "@" in inventory
109
+ )
110
+ if not is_path_or_host_list_or_connector:
111
+ # Next, try loading the inventory from a python function. This happens before checking for a
112
+ # single-host inventory, so that your command does not stop working because somebody
113
+ # registered the domain `my.module.name`.
114
+ inventory_func = try_import_module_attribute(inventory, raise_for_none=False)
115
+
116
+ # If the inventory does not refer to a module, we finally check if it refers to a reachable
117
+ # host
118
+ if inventory_func is None and _resolves_to_host(inventory):
119
+ is_path_or_host_list_or_connector = True
120
+
121
+ if is_path_or_host_list_or_connector:
122
+ # The inventory is either an inventory file or a (list of) hosts
109
123
  return make_inventory_from_files(inventory, override_data, cwd, group_data_directories)
124
+ elif inventory_func is None:
125
+ logger.warn(
126
+ f"{inventory} is neither an inventory file, a (list of) hosts or connectors "
127
+ "nor refers to a python module"
128
+ )
129
+ return Inventory.empty()
110
130
  else:
111
131
  return make_inventory_from_func(inventory_func, override_data)
112
132
 
@@ -236,10 +256,10 @@ def make_inventory_from_files(
236
256
 
237
257
  possible_group_data_folders = []
238
258
  if cwd:
239
- possible_group_data_folders.append(cwd)
259
+ possible_group_data_folders.append(path.join(cwd, "group_data"))
240
260
  inventory_dirname = path.abspath(path.dirname(inventory_filename))
241
261
  if inventory_dirname != cwd:
242
- possible_group_data_folders.append(inventory_dirname)
262
+ possible_group_data_folders.append(path.join(inventory_dirname, "group_data"))
243
263
 
244
264
  if group_data_directories:
245
265
  possible_group_data_folders.extend(group_data_directories)
@@ -250,6 +270,7 @@ def make_inventory_from_files(
250
270
  for folder in possible_group_data_folders:
251
271
  for group_name, data in _get_group_data(folder).items():
252
272
  group_data[group_name].update(data)
273
+ logger.debug("Adding data to group %s: %r", group_name, data)
253
274
 
254
275
  # For each group load up any data
255
276
  for name, hosts in groups.items():
pyinfra_cli/log.py CHANGED
@@ -2,7 +2,8 @@ import logging
2
2
 
3
3
  import click
4
4
 
5
- from pyinfra import logger
5
+ from pyinfra import logger, state
6
+ from pyinfra.context import ctx_state
6
7
 
7
8
 
8
9
  class LogHandler(logging.Handler):
@@ -41,6 +42,9 @@ class LogFormatter(logging.Formatter):
41
42
 
42
43
  # We only handle strings here
43
44
  if isinstance(message, str):
45
+ if ctx_state.isset() and record.levelno is logging.WARNING:
46
+ state.increment_warning_counter()
47
+
44
48
  if "-->" in message:
45
49
  if not self.previous_was_header:
46
50
  click.echo(err=True)
@@ -57,9 +61,13 @@ class LogFormatter(logging.Formatter):
57
61
  return super().format(record)
58
62
 
59
63
 
60
- def setup_logging(log_level):
64
+ def setup_logging(log_level, other_log_level=None):
65
+ if other_log_level:
66
+ logging.basicConfig(level=other_log_level)
67
+
61
68
  logger.setLevel(log_level)
62
69
  handler = LogHandler()
63
70
  formatter = LogFormatter()
64
71
  handler.setFormatter(formatter)
65
72
  logger.addHandler(handler)
73
+ logger.propagate = False
pyinfra_cli/main.py CHANGED
@@ -13,12 +13,13 @@ from pyinfra.api.connect import connect_all, disconnect_all
13
13
  from pyinfra.api.exceptions import NoGroupError, PyinfraError
14
14
  from pyinfra.api.facts import get_facts
15
15
  from pyinfra.api.operations import run_ops
16
+ from pyinfra.api.state import StateStage
16
17
  from pyinfra.api.util import get_kwargs_str
17
18
  from pyinfra.context import ctx_config, ctx_inventory, ctx_state
18
19
  from pyinfra.operations import server
19
20
 
20
21
  from .commands import get_facts_and_args, get_func_and_args
21
- from .exceptions import CliError, UnexpectedExternalError, UnexpectedInternalError
22
+ from .exceptions import CliError, UnexpectedExternalError, UnexpectedInternalError, WrappedError
22
23
  from .inventory import make_inventory
23
24
  from .log import setup_logging
24
25
  from .prints import (
@@ -69,6 +70,8 @@ def _print_support(ctx, param, value):
69
70
  is_flag=True,
70
71
  default=False,
71
72
  help="Execute operations immediately on hosts without prompt or checking for changes.",
73
+ envvar="PYINFRA_YES",
74
+ show_envvar=True,
72
75
  )
73
76
  @click.option(
74
77
  "--limit",
@@ -138,11 +141,6 @@ def _print_support(ctx, param, value):
138
141
  help="SSH Private key password.",
139
142
  )
140
143
  @click.option("--ssh-password", "--password", "ssh_password", help="SSH password.")
141
- # WinRM connector args
142
- @click.option("--winrm-username", help="WINRM user to connect as.")
143
- @click.option("--winrm-password", help="WINRM password.")
144
- @click.option("--winrm-port", help="WINRM port to connect to.")
145
- @click.option("--winrm-transport", help="WINRM transport for use.")
146
144
  # Eager commands (pyinfra --support)
147
145
  @click.option(
148
146
  "--support",
@@ -153,16 +151,16 @@ def _print_support(ctx, param, value):
153
151
  )
154
152
  # Debug args
155
153
  @click.option(
156
- "--quiet",
154
+ "--debug",
157
155
  is_flag=True,
158
156
  default=False,
159
- help="Hide most pyinfra output.",
157
+ help="Print debug logs from pyinfra.",
160
158
  )
161
159
  @click.option(
162
- "--debug",
160
+ "--debug-all",
163
161
  is_flag=True,
164
162
  default=False,
165
- help="Print debug info.",
163
+ help="Print debug logs from all packages including pyinfra.",
166
164
  )
167
165
  @click.option(
168
166
  "--debug-facts",
@@ -222,24 +220,14 @@ def cli(*args, **kwargs):
222
220
 
223
221
  try:
224
222
  _main(*args, **kwargs)
225
-
226
- except PyinfraError as e:
227
- # Re-raise any internal exceptions that aren't handled by click as
228
- # CliErrors which are.
229
- if not isinstance(e, click.ClickException):
230
- message = getattr(e, "message", e.args[0])
231
- raise CliError(message)
232
-
233
- raise
234
-
235
- except UnexpectedExternalError:
236
- # Pass unexpected external exceptions through as-is
223
+ except (CliError, UnexpectedExternalError):
237
224
  raise
238
-
225
+ except PyinfraError as e:
226
+ # Re-raise "expected" pyinfra exceptions with our click exception wrapper
227
+ raise WrappedError(e)
239
228
  except Exception as e:
240
229
  # Re-raise any unexpected internal exceptions as UnexpectedInternalError
241
230
  raise UnexpectedInternalError(e)
242
-
243
231
  finally:
244
232
  if ctx_state.isset() and state.initialised:
245
233
  logger.info("--> Disconnecting from hosts...")
@@ -265,10 +253,6 @@ def _main(
265
253
  ssh_key,
266
254
  ssh_key_password: str,
267
255
  ssh_password: str,
268
- winrm_username: str,
269
- winrm_password: str,
270
- winrm_port,
271
- winrm_transport,
272
256
  shell_executable,
273
257
  sudo: bool,
274
258
  sudo_user: str,
@@ -284,8 +268,8 @@ def _main(
284
268
  limit: Iterable,
285
269
  no_wait: bool,
286
270
  serial: bool,
287
- quiet: bool,
288
271
  debug: bool,
272
+ debug_all: bool,
289
273
  debug_facts: bool,
290
274
  debug_operations: bool,
291
275
  support: bool = False,
@@ -297,7 +281,7 @@ def _main(
297
281
 
298
282
  # Setup logging & Bootstrap/Venv
299
283
  #
300
- _setup_log_level(debug)
284
+ _setup_log_level(debug, debug_all)
301
285
  init_virtualenv()
302
286
 
303
287
  # Check operations are valid and setup commands
@@ -306,7 +290,7 @@ def _main(
306
290
 
307
291
  # Setup state, config & inventory
308
292
  #
309
- state = _setup_state(verbosity, quiet, check_for_changes=not yes)
293
+ state = _setup_state(verbosity, yes)
310
294
  config = Config()
311
295
  ctx_config.set(config)
312
296
 
@@ -322,7 +306,7 @@ def _main(
322
306
  parallel,
323
307
  shell_executable,
324
308
  fail_percent,
325
- quiet,
309
+ yes,
326
310
  )
327
311
  override_data = _set_override_data(
328
312
  data,
@@ -331,12 +315,11 @@ def _main(
331
315
  ssh_key_password,
332
316
  ssh_port,
333
317
  ssh_password,
334
- winrm_username,
335
- winrm_password,
336
- winrm_port,
337
- winrm_transport,
338
318
  )
339
319
 
320
+ if yes is False:
321
+ _set_fail_prompts(state, config)
322
+
340
323
  # Load up the inventory from the filesystem
341
324
  #
342
325
  logger.info("--> Loading inventory...")
@@ -361,7 +344,11 @@ def _main(
361
344
  # Connect to the hosts & start handling the user commands
362
345
  #
363
346
  logger.info("--> Connecting to hosts...")
347
+ state.set_stage(StateStage.Connect)
364
348
  connect_all(state)
349
+
350
+ logger.info("--> Preparing operations...")
351
+ state.set_stage(StateStage.Prepare)
365
352
  can_diff, state, config = _handle_commands(
366
353
  state, config, command, original_operations, operations
367
354
  )
@@ -374,6 +361,14 @@ def _main(
374
361
  else:
375
362
  logger.info("--> Detected changes:")
376
363
  print_meta(state)
364
+ click.echo(
365
+ """
366
+ Detected changes may not include every change pyinfra will execute.
367
+ Hidden side effects of operations may alter behaviour of future operations,
368
+ this will be shown in the results. The remote state will always be updated
369
+ to reflect the state defined by the input operations.""",
370
+ err=True,
371
+ )
377
372
 
378
373
  # If --debug-facts or --debug-operations, print and exit
379
374
  if debug_facts or debug_operations:
@@ -385,41 +380,64 @@ def _main(
385
380
  if dry:
386
381
  _exit()
387
382
 
388
- if can_diff and not yes:
389
- click.echo(err=True)
390
- confirm_msg = " Operations ready, press enter to execute..."
391
- click.echo(confirm_msg, err=True, nl=False)
392
- v = input()
393
- if v:
394
- click.echo(f"Unexpected user input: {v}", err=True)
395
- _exit()
396
- # Go up, clear the line, go up again - as if the confirmation statement was never here!
397
- click.echo(
398
- "\033[1A{0}\033[1A".format("".join(" " for _ in range(len(confirm_msg)))),
399
- err=True,
400
- nl=False,
401
- )
383
+ if (
384
+ can_diff
385
+ and not yes
386
+ and not _do_confirm("Detected changes displayed above, skip this step with -y")
387
+ ):
388
+ _exit()
402
389
 
403
390
  logger.info("--> Beginning operation run...")
404
-
391
+ state.set_stage(StateStage.Execute)
405
392
  run_ops(state, serial=serial, no_wait=no_wait)
406
393
 
407
394
  logger.info("--> Results:")
395
+ state.set_stage(StateStage.Disconnect)
408
396
  print_results(state)
409
397
  _exit()
410
398
 
411
399
 
400
+ def _do_confirm(msg: str) -> bool:
401
+ click.echo(err=True)
402
+ click.echo(f" {msg}", err=True)
403
+ warning_count = state.get_warning_counter()
404
+ if warning_count > 0:
405
+ click.secho(
406
+ f" {warning_count} warnings shown during change detection, see above",
407
+ fg="yellow",
408
+ err=True,
409
+ )
410
+ confirm_msg = " Press enter to execute..."
411
+ click.echo(confirm_msg, err=True, nl=False)
412
+ v = input()
413
+ if v:
414
+ click.echo(f" Unexpected user input: {v}", err=True)
415
+ return False
416
+ # Go up, clear the line, go up again - as if the confirmation statement was never here!
417
+ click.echo(
418
+ "\033[1A{0}\033[1A".format("".join(" " for _ in range(len(confirm_msg)))),
419
+ err=True,
420
+ nl=False,
421
+ )
422
+ click.echo(err=True)
423
+ return True
424
+
425
+
412
426
  # Setup
413
427
  #
414
- def _setup_log_level(debug):
428
+ def _setup_log_level(debug, debug_all):
415
429
  if not debug and not sys.warnoptions:
416
430
  warnings.simplefilter("ignore")
417
431
 
418
432
  log_level = logging.INFO
419
- if debug:
433
+ if debug or debug_all:
420
434
  log_level = logging.DEBUG
421
435
 
422
- setup_logging(log_level)
436
+ other_log_level = None
437
+ if debug_all:
438
+ other_log_level = logging.DEBUG
439
+
440
+ setup_logging(log_level, other_log_level)
423
441
 
424
442
 
425
443
  def _validate_operations(operations, chdir):
@@ -503,12 +521,12 @@ def _set_verbosity(state, verbosity):
503
521
  return state
504
522
 
505
523
 
506
- def _setup_state(verbosity, quiet, check_for_changes):
524
+ def _setup_state(verbosity, yes):
507
525
  cwd = getcwd()
508
526
  if cwd not in sys.path: # ensure cwd is present in sys.path
509
527
  sys.path.append(cwd)
510
528
 
511
- state = State(check_for_changes=check_for_changes)
529
+ state = State(check_for_changes=not yes)
512
530
  state.cwd = cwd
513
531
  ctx_state.set(state)
514
532
 
@@ -526,7 +544,7 @@ def _set_config(
526
544
  parallel,
527
545
  shell_executable,
528
546
  fail_percent,
529
- quiet,
547
+ yes,
530
548
  ):
531
549
  logger.info("--> Loading config...")
532
550
 
@@ -571,10 +589,6 @@ def _set_override_data(
571
589
  ssh_key_password,
572
590
  ssh_port,
573
591
  ssh_password,
574
- winrm_username,
575
- winrm_password,
576
- winrm_port,
577
- winrm_transport,
578
592
  ):
579
593
  override_data = {}
580
594
 
@@ -590,10 +604,6 @@ def _set_override_data(
590
604
  ("ssh_key_password", ssh_key_password),
591
605
  ("ssh_port", ssh_port),
592
606
  ("ssh_password", ssh_password),
593
- ("winrm_username", winrm_username),
594
- ("winrm_password", winrm_password),
595
- ("winrm_port", winrm_port),
596
- ("winrm_transport", winrm_transport),
597
607
  ):
598
608
  if value:
599
609
  override_data[key] = value
@@ -601,6 +611,19 @@ def _set_override_data(
601
611
  return override_data
602
612
 
603
613
 
614
+ def _set_fail_prompts(state: State, config: Config) -> None:
615
+ # Set fail percent to zero, meaning we'll raise an exception for any fail,
616
+ # and we can capture + prompt the user to continue/exit.
617
+ config.FAIL_PERCENT = 0
618
+
619
+ def should_raise_failed_hosts(state: State) -> bool:
620
+ if state.current_stage == StateStage.Connect:
621
+ return not _do_confirm("One of more hosts failed to connect, continue?")
622
+ return not _do_confirm("One of more hosts failed, continue?")
623
+
624
+ state.should_raise_failed_hosts = should_raise_failed_hosts
625
+
626
+
604
627
  def _apply_inventory_limit(inventory, limit):
605
628
  initial_limit = None
606
629
  if limit:
pyinfra_cli/prints.py CHANGED
@@ -217,28 +217,42 @@ def pretty_op_name(op_meta):
217
217
 
218
218
  def print_meta(state: "State"):
219
219
  rows: List[Tuple[Callable, Union[List[str], str]]] = [
220
- (logger.info, ["Operation", "Hosts"]),
220
+ (logger.info, ["Operation", "Change", "Conditional Change"]),
221
221
  ]
222
222
 
223
223
  for op_hash in state.get_op_order():
224
- hosts_in_op = list()
224
+ hosts_in_op = []
225
+ hosts_maybe_in_op = []
225
226
  for host in state.inventory.iter_activated_hosts():
226
227
  if op_hash in state.ops[host]:
227
- if state.ops[host][op_hash].operation_meta.changed:
228
- hosts_in_op.append(host.name)
228
+ op_data = state.get_op_data_for_host(host, op_hash)
229
+ if op_data.operation_meta._maybe_is_change:
230
+ if op_data.global_arguments["_if"]:
231
+ hosts_maybe_in_op.append(host.name)
232
+ else:
233
+ hosts_in_op.append(host.name)
229
234
 
230
235
  rows.append(
231
236
  (
232
237
  logger.info,
233
238
  [
234
239
  pretty_op_name(state.op_meta[op_hash]),
235
- "{0}/{1} ({2})".format(
236
- len(hosts_in_op),
237
- len(state.inventory),
238
- truncate(", ".join(sorted(hosts_in_op)), 48),
239
- )
240
- if hosts_in_op
241
- else "No hosts with changes at this time",
240
+ (
241
+ "-"
242
+ if len(hosts_in_op) == 0
243
+ else "{0} ({1})".format(
244
+ len(hosts_in_op),
245
+ truncate(", ".join(sorted(hosts_in_op)), 48),
246
+ )
247
+ ),
248
+ (
249
+ "-"
250
+ if len(hosts_maybe_in_op) == 0
251
+ else "{0} ({1})".format(
252
+ len(hosts_maybe_in_op),
253
+ truncate(", ".join(sorted(hosts_maybe_in_op)), 48),
254
+ )
255
+ ),
242
256
  ],
243
257
  )
244
258
  )
@@ -253,25 +267,23 @@ def print_results(state: "State"):
253
267
 
254
268
  for op_hash in state.get_op_order():
255
269
  hosts_in_op = 0
256
- hosts_in_op_success = list()
257
- hosts_in_op_error = list()
258
- hosts_in_op_no_attempt = list()
270
+ hosts_in_op_success: list[str] = []
271
+ hosts_in_op_error: list[str] = []
272
+ hosts_in_op_no_change: list[str] = []
259
273
  for host in state.inventory.iter_activated_hosts():
260
274
  if op_hash not in state.ops[host]:
261
275
  continue
262
276
 
263
277
  hosts_in_op += 1
264
278
 
265
- result = state.ops[host][op_hash].operation_meta.success
266
- if result is True:
267
- hosts_in_op_success.append(host.name)
268
- elif result is False:
269
- hosts_in_op_error.append(host.name)
279
+ op_meta = state.ops[host][op_hash].operation_meta
280
+ if op_meta.did_succeed(_raise_if_not_complete=False):
281
+ if op_meta.did_change():
282
+ hosts_in_op_success.append(host.name)
283
+ else:
284
+ hosts_in_op_no_change.append(host.name)
270
285
  else:
271
- hosts_in_op_no_attempt.append(host.name)
272
-
273
- # if not hosts_in_op:
274
- # continue
286
+ hosts_in_op_error.append(host.name)
275
287
 
276
288
  row = [
277
289
  pretty_op_name(state.op_meta[op_hash]),
@@ -286,95 +298,11 @@ def print_results(state: "State"):
286
298
  row.append(f"{len(hosts_in_op_error)}")
287
299
  else:
288
300
  row.append("-")
289
- if hosts_in_op_no_attempt:
290
- row.append(f"{len(hosts_in_op_no_attempt)}")
301
+ if hosts_in_op_no_change:
302
+ row.append(f"{len(hosts_in_op_no_change)}")
291
303
  else:
292
304
  row.append("-")
293
305
 
294
306
  rows.append((logger.info, row))
295
307
 
296
308
  print_rows(rows)
297
-
298
-
299
- def get_fucked(state: "State"):
300
- group_combinations = _get_group_combinations(state.inventory.iter_activated_hosts())
301
- rows: List[Tuple[Callable, Union[List[str], str]]] = []
302
-
303
- for i, (groups, hosts) in enumerate(group_combinations.items(), 1):
304
- if not hosts:
305
- continue
306
-
307
- if groups:
308
- rows.append(
309
- (
310
- logger.info,
311
- "Groups: {0}".format(
312
- click.style(" / ".join(groups), bold=True),
313
- ),
314
- ),
315
- )
316
- else:
317
- rows.append((logger.info, "Ungrouped:"))
318
-
319
- for host in hosts:
320
- # Didn't connect to this host?
321
- if host not in state.activated_hosts:
322
- rows.append(
323
- (
324
- logger.info,
325
- [
326
- host.style_print_prefix("red", bold=True),
327
- click.style("No connection", "red"),
328
- ],
329
- ),
330
- )
331
- continue
332
-
333
- results = state.results[host]
334
-
335
- meta = state.meta[host]
336
- success_ops = results.success_ops
337
- partial_ops = results.partial_ops
338
- # TODO: type meta object
339
- changed_ops = success_ops - meta.ops_no_change # type: ignore
340
- error_ops = results.error_ops
341
- ignored_error_ops = results.ignored_error_ops
342
-
343
- host_args = ("green",)
344
- host_kwargs = {}
345
-
346
- # If all ops got complete
347
- if results.ops == meta.ops:
348
- # We had some errors - but we ignored them - so "warning" color
349
- if error_ops != 0:
350
- host_args = ("yellow",)
351
-
352
- # Ops did not complete!
353
- else:
354
- host_args = ("red",)
355
- host_kwargs["bold"] = True
356
-
357
- changed_str = "Changed: {0}".format(click.style(f"{changed_ops}", bold=True))
358
- if partial_ops:
359
- changed_str = f"{changed_str} ({partial_ops} partial)"
360
-
361
- error_str = "Errors: {0}".format(click.style(f"{error_ops}", bold=True))
362
- if ignored_error_ops:
363
- error_str = f"{error_str} ({ignored_error_ops} ignored)"
364
-
365
- rows.append(
366
- (
367
- logger.info,
368
- [
369
- host.style_print_prefix(*host_args, **host_kwargs),
370
- changed_str,
371
- "No change: {0}".format(click.style(f"{meta.ops_no_change}", bold=True)),
372
- error_str,
373
- ],
374
- ),
375
- )
376
-
377
- if i != len(group_combinations):
378
- rows.append((lambda m: click.echo(m, err=True), []))
379
-
380
- print_rows(rows)