pyinfra 0.11.dev3__py3-none-any.whl → 3.6__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 (204) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +19 -3
  4. pyinfra/api/arguments.py +413 -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 +73 -18
  12. pyinfra/api/facts.py +267 -200
  13. pyinfra/api/host.py +416 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/metadata.py +69 -0
  16. pyinfra/api/operation.py +432 -262
  17. pyinfra/api/operations.py +273 -260
  18. pyinfra/api/state.py +302 -248
  19. pyinfra/api/util.py +309 -369
  20. pyinfra/connectors/base.py +173 -0
  21. pyinfra/connectors/chroot.py +212 -0
  22. pyinfra/connectors/docker.py +405 -0
  23. pyinfra/connectors/dockerssh.py +297 -0
  24. pyinfra/connectors/local.py +238 -0
  25. pyinfra/connectors/scp/__init__.py +1 -0
  26. pyinfra/connectors/scp/client.py +204 -0
  27. pyinfra/connectors/ssh.py +727 -0
  28. pyinfra/connectors/ssh_util.py +114 -0
  29. pyinfra/connectors/sshuserclient/client.py +309 -0
  30. pyinfra/connectors/sshuserclient/config.py +102 -0
  31. pyinfra/connectors/terraform.py +135 -0
  32. pyinfra/connectors/util.py +417 -0
  33. pyinfra/connectors/vagrant.py +183 -0
  34. pyinfra/context.py +145 -0
  35. pyinfra/facts/__init__.py +7 -6
  36. pyinfra/facts/apk.py +22 -7
  37. pyinfra/facts/apt.py +117 -60
  38. pyinfra/facts/brew.py +100 -15
  39. pyinfra/facts/bsdinit.py +23 -0
  40. pyinfra/facts/cargo.py +37 -0
  41. pyinfra/facts/choco.py +47 -0
  42. pyinfra/facts/crontab.py +195 -0
  43. pyinfra/facts/deb.py +94 -0
  44. pyinfra/facts/dnf.py +48 -0
  45. pyinfra/facts/docker.py +96 -23
  46. pyinfra/facts/efibootmgr.py +113 -0
  47. pyinfra/facts/files.py +629 -58
  48. pyinfra/facts/flatpak.py +77 -0
  49. pyinfra/facts/freebsd.py +70 -0
  50. pyinfra/facts/gem.py +19 -6
  51. pyinfra/facts/git.py +59 -14
  52. pyinfra/facts/gpg.py +150 -0
  53. pyinfra/facts/hardware.py +313 -167
  54. pyinfra/facts/iptables.py +72 -62
  55. pyinfra/facts/launchd.py +44 -0
  56. pyinfra/facts/lxd.py +17 -4
  57. pyinfra/facts/mysql.py +122 -86
  58. pyinfra/facts/npm.py +17 -9
  59. pyinfra/facts/openrc.py +71 -0
  60. pyinfra/facts/opkg.py +246 -0
  61. pyinfra/facts/pacman.py +50 -7
  62. pyinfra/facts/pip.py +24 -7
  63. pyinfra/facts/pipx.py +82 -0
  64. pyinfra/facts/pkg.py +15 -6
  65. pyinfra/facts/pkgin.py +35 -0
  66. pyinfra/facts/podman.py +54 -0
  67. pyinfra/facts/postgres.py +178 -0
  68. pyinfra/facts/postgresql.py +6 -147
  69. pyinfra/facts/rpm.py +105 -0
  70. pyinfra/facts/runit.py +77 -0
  71. pyinfra/facts/selinux.py +161 -0
  72. pyinfra/facts/server.py +762 -285
  73. pyinfra/facts/snap.py +88 -0
  74. pyinfra/facts/systemd.py +139 -0
  75. pyinfra/facts/sysvinit.py +59 -0
  76. pyinfra/facts/upstart.py +35 -0
  77. pyinfra/facts/util/__init__.py +17 -0
  78. pyinfra/facts/util/databases.py +4 -6
  79. pyinfra/facts/util/packaging.py +37 -6
  80. pyinfra/facts/util/units.py +30 -0
  81. pyinfra/facts/util/win_files.py +99 -0
  82. pyinfra/facts/vzctl.py +20 -13
  83. pyinfra/facts/xbps.py +35 -0
  84. pyinfra/facts/yum.py +34 -40
  85. pyinfra/facts/zfs.py +77 -0
  86. pyinfra/facts/zypper.py +42 -0
  87. pyinfra/local.py +45 -83
  88. pyinfra/operations/__init__.py +12 -0
  89. pyinfra/operations/apk.py +99 -0
  90. pyinfra/operations/apt.py +496 -0
  91. pyinfra/operations/brew.py +232 -0
  92. pyinfra/operations/bsdinit.py +59 -0
  93. pyinfra/operations/cargo.py +45 -0
  94. pyinfra/operations/choco.py +61 -0
  95. pyinfra/operations/crontab.py +194 -0
  96. pyinfra/operations/dnf.py +213 -0
  97. pyinfra/operations/docker.py +492 -0
  98. pyinfra/operations/files.py +2014 -0
  99. pyinfra/operations/flatpak.py +95 -0
  100. pyinfra/operations/freebsd/__init__.py +12 -0
  101. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  102. pyinfra/operations/freebsd/pkg.py +219 -0
  103. pyinfra/operations/freebsd/service.py +116 -0
  104. pyinfra/operations/freebsd/sysrc.py +92 -0
  105. pyinfra/operations/gem.py +48 -0
  106. pyinfra/operations/git.py +420 -0
  107. pyinfra/operations/iptables.py +312 -0
  108. pyinfra/operations/launchd.py +45 -0
  109. pyinfra/operations/lxd.py +69 -0
  110. pyinfra/operations/mysql.py +610 -0
  111. pyinfra/operations/npm.py +57 -0
  112. pyinfra/operations/openrc.py +63 -0
  113. pyinfra/operations/opkg.py +89 -0
  114. pyinfra/operations/pacman.py +82 -0
  115. pyinfra/operations/pip.py +206 -0
  116. pyinfra/operations/pipx.py +103 -0
  117. pyinfra/operations/pkg.py +71 -0
  118. pyinfra/operations/pkgin.py +92 -0
  119. pyinfra/operations/postgres.py +437 -0
  120. pyinfra/operations/postgresql.py +30 -0
  121. pyinfra/operations/puppet.py +41 -0
  122. pyinfra/operations/python.py +73 -0
  123. pyinfra/operations/runit.py +184 -0
  124. pyinfra/operations/selinux.py +190 -0
  125. pyinfra/operations/server.py +1100 -0
  126. pyinfra/operations/snap.py +118 -0
  127. pyinfra/operations/ssh.py +217 -0
  128. pyinfra/operations/systemd.py +150 -0
  129. pyinfra/operations/sysvinit.py +142 -0
  130. pyinfra/operations/upstart.py +68 -0
  131. pyinfra/operations/util/__init__.py +12 -0
  132. pyinfra/operations/util/docker.py +407 -0
  133. pyinfra/operations/util/files.py +247 -0
  134. pyinfra/operations/util/packaging.py +338 -0
  135. pyinfra/operations/util/service.py +46 -0
  136. pyinfra/operations/vzctl.py +137 -0
  137. pyinfra/operations/xbps.py +78 -0
  138. pyinfra/operations/yum.py +213 -0
  139. pyinfra/operations/zfs.py +176 -0
  140. pyinfra/operations/zypper.py +193 -0
  141. pyinfra/progress.py +44 -32
  142. pyinfra/py.typed +0 -0
  143. pyinfra/version.py +9 -1
  144. pyinfra-3.6.dist-info/METADATA +142 -0
  145. pyinfra-3.6.dist-info/RECORD +160 -0
  146. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
  147. pyinfra-3.6.dist-info/entry_points.txt +12 -0
  148. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
  149. pyinfra_cli/__init__.py +1 -0
  150. pyinfra_cli/cli.py +793 -0
  151. pyinfra_cli/commands.py +66 -0
  152. pyinfra_cli/exceptions.py +155 -65
  153. pyinfra_cli/inventory.py +233 -89
  154. pyinfra_cli/log.py +39 -43
  155. pyinfra_cli/main.py +26 -495
  156. pyinfra_cli/prints.py +215 -156
  157. pyinfra_cli/util.py +172 -105
  158. pyinfra_cli/virtualenv.py +25 -20
  159. pyinfra/api/connectors/__init__.py +0 -21
  160. pyinfra/api/connectors/ansible.py +0 -99
  161. pyinfra/api/connectors/docker.py +0 -178
  162. pyinfra/api/connectors/local.py +0 -169
  163. pyinfra/api/connectors/ssh.py +0 -402
  164. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  165. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  166. pyinfra/api/connectors/util.py +0 -63
  167. pyinfra/api/connectors/vagrant.py +0 -155
  168. pyinfra/facts/init.py +0 -176
  169. pyinfra/facts/util/files.py +0 -102
  170. pyinfra/hook.py +0 -41
  171. pyinfra/modules/__init__.py +0 -11
  172. pyinfra/modules/apk.py +0 -64
  173. pyinfra/modules/apt.py +0 -272
  174. pyinfra/modules/brew.py +0 -122
  175. pyinfra/modules/files.py +0 -711
  176. pyinfra/modules/gem.py +0 -30
  177. pyinfra/modules/git.py +0 -115
  178. pyinfra/modules/init.py +0 -344
  179. pyinfra/modules/iptables.py +0 -271
  180. pyinfra/modules/lxd.py +0 -45
  181. pyinfra/modules/mysql.py +0 -347
  182. pyinfra/modules/npm.py +0 -47
  183. pyinfra/modules/pacman.py +0 -60
  184. pyinfra/modules/pip.py +0 -99
  185. pyinfra/modules/pkg.py +0 -43
  186. pyinfra/modules/postgresql.py +0 -245
  187. pyinfra/modules/puppet.py +0 -20
  188. pyinfra/modules/python.py +0 -37
  189. pyinfra/modules/server.py +0 -524
  190. pyinfra/modules/ssh.py +0 -150
  191. pyinfra/modules/util/files.py +0 -52
  192. pyinfra/modules/util/packaging.py +0 -118
  193. pyinfra/modules/vzctl.py +0 -133
  194. pyinfra/modules/yum.py +0 -171
  195. pyinfra/pseudo_modules.py +0 -64
  196. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  197. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  198. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  199. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  200. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  201. pyinfra_cli/__main__.py +0 -40
  202. pyinfra_cli/config.py +0 -92
  203. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  204. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra_cli/cli.py ADDED
@@ -0,0 +1,793 @@
1
+ import logging
2
+ import sys
3
+ import warnings
4
+ from fnmatch import fnmatch
5
+ from getpass import getpass
6
+ from os import chdir as os_chdir, getcwd, path
7
+ from typing import Iterable, List, Tuple, Union
8
+
9
+ import click
10
+
11
+ from pyinfra import __version__, logger, state
12
+ from pyinfra.api import Config, State
13
+ from pyinfra.api.connect import connect_all, disconnect_all
14
+ from pyinfra.api.exceptions import NoGroupError, PyinfraError
15
+ from pyinfra.api.facts import get_facts
16
+ from pyinfra.api.operations import run_ops
17
+ from pyinfra.api.state import StateStage
18
+ from pyinfra.api.util import get_kwargs_str
19
+ from pyinfra.context import ctx_config, ctx_inventory, ctx_state
20
+ from pyinfra.operations import server
21
+
22
+ from .commands import get_facts_and_args, get_func_and_args
23
+ from .exceptions import CliError, UnexpectedExternalError, UnexpectedInternalError, WrappedError
24
+ from .inventory import make_inventory
25
+ from .log import setup_logging
26
+ from .prints import (
27
+ print_facts,
28
+ print_inventory,
29
+ print_meta,
30
+ print_results,
31
+ print_state_operations,
32
+ print_support_info,
33
+ )
34
+ from .util import exec_file, load_deploy_file, load_func, parse_cli_arg
35
+ from .virtualenv import init_virtualenv
36
+
37
+
38
+ def _exit() -> None:
39
+ if ctx_state.isset() and state.failed_hosts:
40
+ sys.exit(1)
41
+ sys.exit(0)
42
+
43
+
44
+ def _print_support(ctx, param, value):
45
+ if not value:
46
+ return
47
+
48
+ logger.info("--> Support information:")
49
+ print_support_info()
50
+ ctx.exit()
51
+
52
+
53
+ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
54
+
55
+
56
+ @click.command(context_settings=CONTEXT_SETTINGS)
57
+ @click.argument("inventory", nargs=1, type=click.Path(exists=False))
58
+ @click.argument("operations", nargs=-1, required=True, type=click.Path(exists=False))
59
+ @click.option(
60
+ "verbosity",
61
+ "-v",
62
+ count=True,
63
+ help="Print meta (-v), input (-vv) and output (-vvv).",
64
+ )
65
+ @click.option(
66
+ "--dry",
67
+ is_flag=True,
68
+ default=False,
69
+ help="Don't execute operations on the target hosts.",
70
+ )
71
+ @click.option(
72
+ "--diff",
73
+ is_flag=True,
74
+ default=False,
75
+ help="Show the differences when changing text files and templates.",
76
+ )
77
+ @click.option(
78
+ "-y",
79
+ "--yes",
80
+ is_flag=True,
81
+ default=False,
82
+ help="Execute operations immediately on hosts without prompt or checking for changes.",
83
+ envvar="PYINFRA_YES",
84
+ show_envvar=True,
85
+ )
86
+ @click.option(
87
+ "--limit",
88
+ help="Restrict the target hosts by name and group name.",
89
+ multiple=True,
90
+ )
91
+ @click.option("--fail-percent", type=int, help="% of hosts that need to fail before exiting early.")
92
+ @click.option(
93
+ "--data",
94
+ multiple=True,
95
+ help="Override data values, format key=value.",
96
+ )
97
+ @click.option(
98
+ "--group-data",
99
+ multiple=True,
100
+ help="Paths to load additional group data from (overrides matching keys).",
101
+ )
102
+ @click.option(
103
+ "--config",
104
+ "config_filename",
105
+ help="Specify config file to use (default: config.py).",
106
+ default="config.py",
107
+ )
108
+ @click.option(
109
+ "--chdir",
110
+ help="Set the working directory before executing.",
111
+ )
112
+ # Auth args
113
+ @click.option(
114
+ "--sudo",
115
+ is_flag=True,
116
+ default=False,
117
+ help="Whether to execute operations with sudo.",
118
+ )
119
+ @click.option("--sudo-user", help="Which user to sudo when sudoing.")
120
+ @click.option(
121
+ "--same-sudo-password",
122
+ is_flag=True,
123
+ default=False,
124
+ help="All hosts have the same sudo password, so ask only once.",
125
+ )
126
+ @click.option(
127
+ "--use-sudo-password",
128
+ is_flag=True,
129
+ default=False,
130
+ help="Whether to use a password with sudo.",
131
+ )
132
+ @click.option("--su-user", help="Which user to su to.")
133
+ @click.option("--shell-executable", help='Shell to use (ex: "sh", "cmd", "ps").')
134
+ # Operation flow args
135
+ @click.option("--parallel", type=int, help="Number of operations to run in parallel.")
136
+ @click.option(
137
+ "--no-wait",
138
+ is_flag=True,
139
+ default=False,
140
+ help="Don't wait between operations for hosts.",
141
+ )
142
+ @click.option(
143
+ "--serial",
144
+ is_flag=True,
145
+ default=False,
146
+ help="Run operations in serial, host by host.",
147
+ )
148
+ @click.option(
149
+ "--retry",
150
+ type=int,
151
+ default=0,
152
+ help="Number of times to retry failed operations.",
153
+ )
154
+ @click.option(
155
+ "--retry-delay",
156
+ type=int,
157
+ default=5,
158
+ help="Delay in seconds between retry attempts.",
159
+ )
160
+ # SSH connector args
161
+ # TODO: remove the non-ssh-prefixed variants
162
+ @click.option("--ssh-user", "--user", "ssh_user", help="SSH user to connect as.")
163
+ @click.option("--ssh-port", "--port", "ssh_port", type=int, help="SSH port to connect to.")
164
+ @click.option("--ssh-key", "--key", "ssh_key", type=click.Path(), help="SSH Private key filename.")
165
+ @click.option(
166
+ "--ssh-key-password",
167
+ "--key-password",
168
+ "ssh_key_password",
169
+ help="SSH Private key password.",
170
+ )
171
+ @click.option("--ssh-password", "--password", "ssh_password", help="SSH password.")
172
+ # Eager commands (pyinfra --support)
173
+ @click.option(
174
+ "--support",
175
+ is_flag=True,
176
+ is_eager=True,
177
+ callback=_print_support,
178
+ help="Print useful information for support and exit.",
179
+ )
180
+ # Debug args
181
+ @click.option(
182
+ "--debug",
183
+ is_flag=True,
184
+ default=False,
185
+ help="Print debug logs from pyinfra.",
186
+ )
187
+ @click.option(
188
+ "--debug-all",
189
+ is_flag=True,
190
+ default=False,
191
+ help="Print debug logs from all packages including pyinfra.",
192
+ )
193
+ @click.option(
194
+ "--debug-facts",
195
+ is_flag=True,
196
+ default=False,
197
+ help="Print facts after generating operations and exit.",
198
+ )
199
+ @click.option(
200
+ "--debug-operations",
201
+ is_flag=True,
202
+ default=False,
203
+ help="Print operations after generating and exit.",
204
+ )
205
+ @click.version_option(
206
+ version=__version__,
207
+ prog_name="pyinfra",
208
+ message="%(prog)s: v%(version)s",
209
+ )
210
+ def cli(*args, **kwargs):
211
+ """
212
+ pyinfra manages the state of one or more servers. It can be used for
213
+ app/service deployment, config management and ad-hoc command execution.
214
+
215
+ Documentation: docs.pyinfra.com
216
+
217
+ # INVENTORY
218
+
219
+ \b
220
+ + a file (inventory.py)
221
+ + hostname (host.net)
222
+ + Comma separated hostnames:
223
+ host-1.net,host-2.net,@local
224
+
225
+ # OPERATIONS
226
+
227
+ \b
228
+ # Run one or more deploys against the inventory
229
+ pyinfra INVENTORY deploy_web.py [deploy_db.py]...
230
+
231
+ \b
232
+ # Run a single operation against the inventory
233
+ pyinfra INVENTORY server.user pyinfra home=/home/pyinfra
234
+
235
+ \b
236
+ # Execute an arbitrary command against the inventory
237
+ pyinfra INVENTORY exec -- echo "hello world"
238
+
239
+ \b
240
+ # Run one or more facts against the inventory
241
+ pyinfra INVENTORY fact server.LinuxName [server.Users]...
242
+ pyinfra INVENTORY fact files.File path=/path/to/file...
243
+
244
+ \b
245
+ # Debug the inventory hosts and data
246
+ pyinfra INVENTORY debug-inventory
247
+ """
248
+
249
+ try:
250
+ _main(*args, **kwargs)
251
+ except (CliError, UnexpectedExternalError):
252
+ raise
253
+ except PyinfraError as e:
254
+ # Re-raise "expected" pyinfra exceptions with our click exception wrapper
255
+ raise WrappedError(e)
256
+ except Exception as e:
257
+ # Re-raise any unexpected internal exceptions as UnexpectedInternalError
258
+ raise UnexpectedInternalError(e)
259
+ finally:
260
+ if ctx_state.isset() and state.initialised:
261
+ logger.info("--> Disconnecting from hosts...")
262
+ # Triggers any executor disconnect requirements
263
+ disconnect_all(state)
264
+
265
+
266
+ class CliCommands:
267
+ DEBUG_INVENTORY = "DEBUG_INVENTORY"
268
+ FACT = "FACT"
269
+ SHELL = "SHELL"
270
+ DEPLOY_FILES = "DEPLOY_FILES"
271
+ FUNC = "FUNC"
272
+
273
+
274
+ def _main(
275
+ inventory,
276
+ operations: Union[List, Tuple],
277
+ verbosity: int,
278
+ chdir: str,
279
+ ssh_user,
280
+ ssh_port: int,
281
+ ssh_key,
282
+ ssh_key_password: str,
283
+ ssh_password: str,
284
+ same_sudo_password: bool,
285
+ shell_executable,
286
+ sudo: bool,
287
+ sudo_user: str,
288
+ use_sudo_password: bool,
289
+ su_user: str,
290
+ parallel: int,
291
+ fail_percent: int,
292
+ data,
293
+ group_data,
294
+ config_filename: str,
295
+ dry: bool,
296
+ diff: bool,
297
+ yes: bool,
298
+ limit: Iterable,
299
+ no_wait: bool,
300
+ serial: bool,
301
+ retry: int,
302
+ retry_delay: int,
303
+ debug: bool,
304
+ debug_all: bool,
305
+ debug_facts: bool,
306
+ debug_operations: bool,
307
+ support: bool = False,
308
+ ):
309
+ # Setup working directory
310
+ #
311
+ if chdir:
312
+ os_chdir(chdir)
313
+
314
+ # Setup logging & Bootstrap/Venv
315
+ #
316
+ _setup_log_level(debug, debug_all)
317
+ init_virtualenv()
318
+
319
+ # Check operations are valid and setup commands
320
+ #
321
+ original_operations, operations, command, chdir = _validate_operations(operations, chdir)
322
+
323
+ # Setup state, config & inventory
324
+ #
325
+ state = _setup_state(verbosity, yes)
326
+ config = Config()
327
+ ctx_config.set(config)
328
+
329
+ # Update Config & Override Data
330
+ #
331
+ config = _set_config(
332
+ config,
333
+ config_filename,
334
+ sudo,
335
+ sudo_user,
336
+ use_sudo_password,
337
+ same_sudo_password,
338
+ su_user,
339
+ parallel,
340
+ shell_executable,
341
+ fail_percent,
342
+ yes,
343
+ diff,
344
+ retry,
345
+ retry_delay,
346
+ )
347
+ override_data = _set_override_data(
348
+ data,
349
+ ssh_user,
350
+ ssh_key,
351
+ ssh_key_password,
352
+ ssh_port,
353
+ ssh_password,
354
+ )
355
+
356
+ if yes is False:
357
+ _set_fail_prompts(state, config)
358
+
359
+ # Load up the inventory from the filesystem
360
+ #
361
+ logger.info("--> Loading inventory...")
362
+ inventory = make_inventory(
363
+ inventory,
364
+ cwd=state.cwd,
365
+ override_data=override_data,
366
+ group_data_directories=group_data,
367
+ )
368
+ ctx_inventory.set(inventory)
369
+
370
+ # Now that we have inventory, apply --limit config override
371
+ initial_limit = _apply_inventory_limit(inventory, limit)
372
+
373
+ # Initialise the state
374
+ state.init(inventory, config, initial_limit=initial_limit)
375
+
376
+ if command == CliCommands.DEBUG_INVENTORY:
377
+ print_inventory(state)
378
+ _exit()
379
+
380
+ # Connect to the hosts & start handling the user commands
381
+ #
382
+ logger.info("--> Connecting to hosts...")
383
+ state.set_stage(StateStage.Connect)
384
+ connect_all(state)
385
+
386
+ state.set_stage(StateStage.Prepare)
387
+ can_diff, state, config = _handle_commands(
388
+ state, config, command, original_operations, operations
389
+ )
390
+
391
+ # Print proposed changes, execute unless --dry, and exit
392
+ #
393
+ if can_diff:
394
+ if yes:
395
+ logger.info("--> Skipping change detection")
396
+ else:
397
+ logger.info("--> Detected changes:")
398
+ print_meta(state)
399
+ click.echo(
400
+ """
401
+ Detected changes may not include every change pyinfra will execute.
402
+ Hidden side effects of operations may alter behaviour of future operations,
403
+ this will be shown in the results. The remote state will always be updated
404
+ to reflect the state defined by the input operations.""",
405
+ err=True,
406
+ )
407
+
408
+ # If --debug-facts or --debug-operations, print and exit
409
+ if debug_facts or debug_operations:
410
+ if debug_operations:
411
+ print_state_operations(state)
412
+
413
+ _exit()
414
+
415
+ if dry:
416
+ _exit()
417
+
418
+ if (
419
+ can_diff
420
+ and not yes
421
+ and not _do_confirm("Detected changes displayed above, skip this step with -y")
422
+ ):
423
+ _exit()
424
+
425
+ logger.info("--> Beginning operation run...")
426
+ state.set_stage(StateStage.Execute)
427
+ run_ops(state, serial=serial, no_wait=no_wait)
428
+
429
+ logger.info("--> Results:")
430
+ state.set_stage(StateStage.Disconnect)
431
+ print_results(state)
432
+ _exit()
433
+
434
+
435
+ def _do_confirm(msg: str) -> bool:
436
+ click.echo(err=True)
437
+ click.echo(f" {msg}", err=True)
438
+ warning_count = state.get_warning_counter()
439
+ if warning_count > 0:
440
+ click.secho(
441
+ f" {warning_count} warnings shown during change detection, see above",
442
+ fg="yellow",
443
+ err=True,
444
+ )
445
+ confirm_msg = " Press enter to execute..."
446
+ click.echo(confirm_msg, err=True, nl=False)
447
+ v = input()
448
+ if v:
449
+ click.echo(f" Unexpected user input: {v}", err=True)
450
+ return False
451
+ # Go up, clear the line, go up again - as if the confirmation statement was never here!
452
+ click.echo(
453
+ "\033[1A{0}\033[1A".format("".join(" " for _ in range(len(confirm_msg)))),
454
+ err=True,
455
+ nl=False,
456
+ )
457
+ click.echo(err=True)
458
+ return True
459
+
460
+
461
+ # Setup
462
+ #
463
+ def _setup_log_level(debug, debug_all):
464
+ if not debug and not sys.warnoptions:
465
+ warnings.simplefilter("ignore")
466
+
467
+ log_level = logging.INFO
468
+ if debug or debug_all:
469
+ log_level = logging.DEBUG
470
+
471
+ other_log_level = None
472
+ if debug_all:
473
+ other_log_level = logging.DEBUG
474
+
475
+ setup_logging(log_level, other_log_level)
476
+
477
+
478
+ def _validate_operations(operations, chdir):
479
+ # Make a copy before we overwrite
480
+ original_operations = operations
481
+
482
+ # Debug (print) inventory + group data
483
+ if operations[0] == "debug-inventory":
484
+ command = CliCommands.DEBUG_INVENTORY
485
+
486
+ # Get one or more facts
487
+ elif operations[0] == "fact":
488
+ command = CliCommands.FACT
489
+ operations = get_facts_and_args(operations[1:])
490
+
491
+ # Execute a raw command with server.shell
492
+ elif operations[0] == "exec":
493
+ command = CliCommands.SHELL
494
+ operations = operations[1:]
495
+
496
+ # Execute one or more deploy files
497
+ elif all(cmd.endswith(".py") for cmd in operations):
498
+ command = CliCommands.DEPLOY_FILES
499
+
500
+ filenames = []
501
+
502
+ for filename in operations[0:]:
503
+ if path.exists(filename):
504
+ filenames.append(filename)
505
+ continue
506
+ if chdir and filename.startswith(chdir):
507
+ correct_filename = path.relpath(filename, chdir)
508
+ logger.warning(
509
+ (
510
+ "Fixing deploy filename under `--chdir` argument: "
511
+ f"{filename} -> {correct_filename}"
512
+ ),
513
+ )
514
+ filenames.append(correct_filename)
515
+ continue
516
+ raise CliError(
517
+ "No deploy file: {0}".format(
518
+ path.join(chdir, filename) if chdir else filename,
519
+ ),
520
+ )
521
+
522
+ operations = filenames
523
+
524
+ # Load a function (op or deploy) directly with arguments
525
+ elif "." in operations[0]:
526
+ command = CliCommands.FUNC
527
+ operations = get_func_and_args(operations)
528
+
529
+ else:
530
+ raise CliError(
531
+ """Invalid operations: {0}
532
+
533
+ Operation usage:
534
+ pyinfra INVENTORY deploy_web.py [deploy_db.py]...
535
+ pyinfra INVENTORY server.user pyinfra home=/home/pyinfra
536
+ pyinfra INVENTORY exec -- echo "hello world"
537
+ pyinfra INVENTORY fact os [users]...""".format(
538
+ operations,
539
+ ),
540
+ )
541
+
542
+ return original_operations, operations, command, chdir
543
+
544
+
545
+ def _set_verbosity(state, verbosity):
546
+ if verbosity > 0:
547
+ state.print_fact_info = True
548
+ state.print_noop_info = True
549
+
550
+ if verbosity > 1:
551
+ state.print_input = True
552
+ state.print_fact_input = True
553
+
554
+ if verbosity > 2:
555
+ state.print_output = True
556
+ state.print_fact_output = True
557
+
558
+ return state
559
+
560
+
561
+ def _setup_state(verbosity, yes):
562
+ cwd = getcwd()
563
+ if cwd not in sys.path: # ensure cwd is present in sys.path
564
+ sys.path.append(cwd)
565
+
566
+ state = State(check_for_changes=not yes)
567
+ state.cwd = cwd
568
+ ctx_state.set(state)
569
+
570
+ state = _set_verbosity(state, verbosity)
571
+ return state
572
+
573
+
574
+ def _set_config(
575
+ config,
576
+ config_filename,
577
+ sudo,
578
+ sudo_user,
579
+ use_sudo_password,
580
+ same_sudo_password,
581
+ su_user,
582
+ parallel,
583
+ shell_executable,
584
+ fail_percent,
585
+ yes,
586
+ diff,
587
+ retry,
588
+ retry_delay,
589
+ ):
590
+ logger.info("--> Loading config...")
591
+
592
+ # Load up any config.py from the filesystem
593
+ if state.cwd:
594
+ config_filename = path.join(state.cwd, config_filename)
595
+ if path.exists(config_filename):
596
+ exec_file(config_filename)
597
+
598
+ # Lock the current config, this allows us to restore this version after
599
+ # executing deploy files that may alter them.
600
+ config.lock_current_state()
601
+
602
+ # Arg based config overrides
603
+ if sudo:
604
+ config.SUDO = True
605
+ if sudo_user:
606
+ config.SUDO_USER = sudo_user
607
+
608
+ if use_sudo_password:
609
+ config.USE_SUDO_PASSWORD = use_sudo_password
610
+
611
+ if same_sudo_password:
612
+ config.SUDO_PASSWORD = getpass("sudo password: ")
613
+
614
+ if su_user:
615
+ config.SU_USER = su_user
616
+
617
+ if parallel:
618
+ config.PARALLEL = parallel
619
+
620
+ if shell_executable:
621
+ config.SHELL = None if shell_executable in ("None", "null") else shell_executable
622
+
623
+ if fail_percent is not None:
624
+ config.FAIL_PERCENT = fail_percent
625
+
626
+ if diff:
627
+ config.DIFF = True
628
+
629
+ if retry is not None:
630
+ config.RETRY = retry
631
+
632
+ if retry_delay is not None:
633
+ config.RETRY_DELAY = retry_delay
634
+
635
+ return config
636
+
637
+
638
+ def _set_override_data(
639
+ data,
640
+ ssh_user,
641
+ ssh_key,
642
+ ssh_key_password,
643
+ ssh_port,
644
+ ssh_password,
645
+ ):
646
+ override_data = {}
647
+
648
+ for arg in data:
649
+ key, value = arg.split("=", 1)
650
+ override_data[key] = value
651
+
652
+ override_data = {key: parse_cli_arg(value) for key, value in override_data.items()}
653
+
654
+ for key, value in (
655
+ ("ssh_user", ssh_user),
656
+ ("ssh_key", ssh_key),
657
+ ("ssh_key_password", ssh_key_password),
658
+ ("ssh_port", ssh_port),
659
+ ("ssh_password", ssh_password),
660
+ ):
661
+ if value:
662
+ override_data[key] = value
663
+
664
+ return override_data
665
+
666
+
667
+ def _set_fail_prompts(state: State, config: Config) -> None:
668
+ # Set fail percent to zero, meaning we'll raise an exception for any fail,
669
+ # and we can capture + prompt the user to continue/exit.
670
+ config.FAIL_PERCENT = 0
671
+
672
+ def should_raise_failed_hosts(state: State) -> bool:
673
+ if state.current_stage == StateStage.Connect:
674
+ return not _do_confirm("One of more hosts failed to connect, continue?")
675
+ return not _do_confirm("One of more hosts failed, continue?")
676
+
677
+ state.should_raise_failed_hosts = should_raise_failed_hosts
678
+
679
+
680
+ def _apply_inventory_limit(inventory, limit):
681
+ initial_limit = None
682
+ if limit:
683
+ all_limit_hosts = []
684
+
685
+ for limiter in limit:
686
+ try:
687
+ limit_hosts = inventory.get_group(limiter)
688
+ except NoGroupError:
689
+ limit_hosts = [host for host in inventory if fnmatch(host.name, limiter)]
690
+
691
+ if not limit_hosts:
692
+ logger.warning("No host matches found for --limit pattern: {0}".format(limiter))
693
+
694
+ all_limit_hosts.extend(limit_hosts)
695
+ initial_limit = list(set(all_limit_hosts))
696
+
697
+ return initial_limit
698
+
699
+
700
+ # Operations Execution
701
+ #
702
+ def _handle_commands(state, config, command, original_operations, operations):
703
+ if command is CliCommands.FACT:
704
+ logger.info("--> Gathering facts...")
705
+ state, fact_data = _run_fact_operations(state, config, operations)
706
+ print_facts(fact_data)
707
+ _exit()
708
+
709
+ can_diff = True
710
+
711
+ if command == CliCommands.SHELL:
712
+ logger.info("--> Preparing exec operation...")
713
+ state = _prepare_exec_operations(state, config, operations)
714
+ can_diff = False
715
+
716
+ elif command == CliCommands.DEPLOY_FILES:
717
+ logger.info("--> Preparing operation files...")
718
+ state, config, operations = _prepare_deploy_operations(state, config, operations)
719
+
720
+ elif command == CliCommands.FUNC:
721
+ logger.info("--> Preparing operation func...")
722
+ state, kwargs = _prepare_func_operations(
723
+ state,
724
+ config,
725
+ operations,
726
+ original_operations,
727
+ )
728
+
729
+ return can_diff, state, config
730
+
731
+
732
+ def _run_fact_operations(state, config, operations):
733
+ state.print_fact_info = True
734
+ fact_data = {}
735
+
736
+ for i, command in enumerate(operations):
737
+ fact_cls, args, kwargs = command
738
+ fact_key = fact_cls.name
739
+
740
+ if args or kwargs:
741
+ _fact_args = args or ""
742
+ _fact_details = " ({0})".format(get_kwargs_str(kwargs)) if kwargs else ""
743
+ fact_key = "{0}{1}{2}".format(fact_cls.name, _fact_args, _fact_details)
744
+
745
+ try:
746
+ fact_data[fact_key] = get_facts(
747
+ state,
748
+ fact_cls,
749
+ args=args,
750
+ kwargs=kwargs,
751
+ apply_failed_hosts=False,
752
+ )
753
+ except PyinfraError:
754
+ pass
755
+
756
+ return state, fact_data
757
+
758
+
759
+ def _prepare_exec_operations(state, config, operations):
760
+ state.print_output = True
761
+ # Pass the retry settings from config to the shell operation
762
+ load_func(
763
+ state,
764
+ server.shell,
765
+ " ".join(operations),
766
+ _retries=config.RETRY,
767
+ _retry_delay=config.RETRY_DELAY,
768
+ )
769
+ return state
770
+
771
+
772
+ def _prepare_deploy_operations(state, config, operations):
773
+ # Number of "steps" to make = number of files * number of hosts
774
+ for i, filename in enumerate(operations):
775
+ _log_styled_msg = click.style(filename, bold=True)
776
+ logger.info("Loading: {0}".format(_log_styled_msg))
777
+
778
+ state.current_op_file_number = i
779
+ load_deploy_file(state, filename)
780
+
781
+ # Remove any config changes introduced by the deploy file & any includes
782
+ config.reset_locked_state()
783
+
784
+ return state, config, operations
785
+
786
+
787
+ def _prepare_func_operations(state, config, operations, original_operations):
788
+ op, args = operations
789
+ args, kwargs = args
790
+
791
+ load_func(state, op, *args, **kwargs)
792
+
793
+ return state, kwargs