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