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.
- pyinfra/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +18 -3
- pyinfra/api/arguments.py +406 -0
- pyinfra/api/arguments_typed.py +79 -0
- pyinfra/api/command.py +274 -0
- pyinfra/api/config.py +222 -28
- pyinfra/api/connect.py +33 -13
- pyinfra/api/connectors.py +27 -0
- pyinfra/api/deploy.py +65 -66
- pyinfra/api/exceptions.py +67 -18
- pyinfra/api/facts.py +253 -202
- pyinfra/api/host.py +413 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +291 -368
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +381 -0
- pyinfra/connectors/dockerssh.py +297 -0
- pyinfra/connectors/local.py +238 -0
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +670 -0
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +309 -0
- pyinfra/connectors/sshuserclient/config.py +102 -0
- pyinfra/connectors/terraform.py +135 -0
- pyinfra/connectors/util.py +410 -0
- pyinfra/connectors/vagrant.py +183 -0
- pyinfra/context.py +145 -0
- pyinfra/facts/__init__.py +7 -6
- pyinfra/facts/apk.py +22 -7
- pyinfra/facts/apt.py +117 -60
- pyinfra/facts/brew.py +100 -15
- pyinfra/facts/bsdinit.py +23 -0
- pyinfra/facts/cargo.py +37 -0
- pyinfra/facts/choco.py +47 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +94 -0
- pyinfra/facts/dnf.py +48 -0
- pyinfra/facts/docker.py +96 -23
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +630 -58
- pyinfra/facts/flatpak.py +77 -0
- pyinfra/facts/freebsd.py +70 -0
- pyinfra/facts/gem.py +19 -6
- pyinfra/facts/git.py +59 -14
- pyinfra/facts/gpg.py +150 -0
- pyinfra/facts/hardware.py +313 -167
- pyinfra/facts/iptables.py +72 -62
- pyinfra/facts/launchd.py +44 -0
- pyinfra/facts/lxd.py +17 -4
- pyinfra/facts/mysql.py +122 -86
- pyinfra/facts/npm.py +17 -9
- pyinfra/facts/openrc.py +71 -0
- pyinfra/facts/opkg.py +246 -0
- pyinfra/facts/pacman.py +50 -7
- pyinfra/facts/pip.py +24 -7
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +15 -6
- pyinfra/facts/pkgin.py +35 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +178 -0
- pyinfra/facts/postgresql.py +6 -147
- pyinfra/facts/rpm.py +105 -0
- pyinfra/facts/runit.py +77 -0
- pyinfra/facts/selinux.py +161 -0
- pyinfra/facts/server.py +746 -285
- pyinfra/facts/snap.py +88 -0
- pyinfra/facts/systemd.py +139 -0
- pyinfra/facts/sysvinit.py +59 -0
- pyinfra/facts/upstart.py +35 -0
- pyinfra/facts/util/__init__.py +17 -0
- pyinfra/facts/util/databases.py +4 -6
- pyinfra/facts/util/packaging.py +37 -6
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/util/win_files.py +99 -0
- pyinfra/facts/vzctl.py +20 -13
- pyinfra/facts/xbps.py +35 -0
- pyinfra/facts/yum.py +34 -40
- pyinfra/facts/zfs.py +77 -0
- pyinfra/facts/zypper.py +42 -0
- pyinfra/local.py +45 -83
- pyinfra/operations/__init__.py +12 -0
- pyinfra/operations/apk.py +98 -0
- pyinfra/operations/apt.py +488 -0
- pyinfra/operations/brew.py +231 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +191 -0
- pyinfra/operations/dnf.py +210 -0
- pyinfra/operations/docker.py +446 -0
- pyinfra/operations/files.py +1939 -0
- pyinfra/operations/flatpak.py +94 -0
- pyinfra/operations/freebsd/__init__.py +12 -0
- pyinfra/operations/freebsd/freebsd_update.py +70 -0
- pyinfra/operations/freebsd/pkg.py +219 -0
- pyinfra/operations/freebsd/service.py +116 -0
- pyinfra/operations/freebsd/sysrc.py +92 -0
- pyinfra/operations/gem.py +47 -0
- pyinfra/operations/git.py +419 -0
- pyinfra/operations/iptables.py +311 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +68 -0
- pyinfra/operations/mysql.py +609 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pacman.py +81 -0
- pyinfra/operations/pip.py +205 -0
- pyinfra/operations/pipx.py +102 -0
- pyinfra/operations/pkg.py +70 -0
- pyinfra/operations/pkgin.py +91 -0
- pyinfra/operations/postgres.py +436 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +40 -0
- pyinfra/operations/python.py +72 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +189 -0
- pyinfra/operations/server.py +1099 -0
- pyinfra/operations/snap.py +117 -0
- pyinfra/operations/ssh.py +216 -0
- pyinfra/operations/systemd.py +149 -0
- pyinfra/operations/sysvinit.py +141 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +251 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +336 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +77 -0
- pyinfra/operations/yum.py +210 -0
- pyinfra/operations/zfs.py +175 -0
- pyinfra/operations/zypper.py +192 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.5.1.dist-info/METADATA +141 -0
- pyinfra-3.5.1.dist-info/RECORD +159 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
- pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +780 -0
- pyinfra_cli/commands.py +66 -0
- pyinfra_cli/exceptions.py +155 -65
- pyinfra_cli/inventory.py +233 -89
- pyinfra_cli/log.py +39 -43
- pyinfra_cli/main.py +26 -495
- pyinfra_cli/prints.py +215 -156
- pyinfra_cli/util.py +172 -105
- pyinfra_cli/virtualenv.py +25 -20
- pyinfra/api/connectors/__init__.py +0 -21
- pyinfra/api/connectors/ansible.py +0 -99
- pyinfra/api/connectors/docker.py +0 -178
- pyinfra/api/connectors/local.py +0 -169
- pyinfra/api/connectors/ssh.py +0 -402
- pyinfra/api/connectors/sshuserclient/client.py +0 -105
- pyinfra/api/connectors/sshuserclient/config.py +0 -90
- pyinfra/api/connectors/util.py +0 -63
- pyinfra/api/connectors/vagrant.py +0 -155
- pyinfra/facts/init.py +0 -176
- pyinfra/facts/util/files.py +0 -102
- pyinfra/hook.py +0 -41
- pyinfra/modules/__init__.py +0 -11
- pyinfra/modules/apk.py +0 -64
- pyinfra/modules/apt.py +0 -272
- pyinfra/modules/brew.py +0 -122
- pyinfra/modules/files.py +0 -711
- pyinfra/modules/gem.py +0 -30
- pyinfra/modules/git.py +0 -115
- pyinfra/modules/init.py +0 -344
- pyinfra/modules/iptables.py +0 -271
- pyinfra/modules/lxd.py +0 -45
- pyinfra/modules/mysql.py +0 -347
- pyinfra/modules/npm.py +0 -47
- pyinfra/modules/pacman.py +0 -60
- pyinfra/modules/pip.py +0 -99
- pyinfra/modules/pkg.py +0 -43
- pyinfra/modules/postgresql.py +0 -245
- pyinfra/modules/puppet.py +0 -20
- pyinfra/modules/python.py +0 -37
- pyinfra/modules/server.py +0 -524
- pyinfra/modules/ssh.py +0 -150
- pyinfra/modules/util/files.py +0 -52
- pyinfra/modules/util/packaging.py +0 -118
- pyinfra/modules/vzctl.py +0 -133
- pyinfra/modules/yum.py +0 -171
- pyinfra/pseudo_modules.py +0 -64
- pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
- pyinfra-0.11.dev3.dist-info/METADATA +0 -135
- pyinfra-0.11.dev3.dist-info/RECORD +0 -95
- pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
- pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
- pyinfra_cli/__main__.py +0 -40
- pyinfra_cli/config.py +0 -92
- /pyinfra/{modules/util → connectors}/__init__.py +0 -0
- /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
|