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.
- pyinfra/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +19 -3
- pyinfra/api/arguments.py +413 -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 +73 -18
- pyinfra/api/facts.py +267 -200
- pyinfra/api/host.py +416 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/metadata.py +69 -0
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +309 -369
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +405 -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 +727 -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 +417 -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 +629 -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 +762 -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 +99 -0
- pyinfra/operations/apt.py +496 -0
- pyinfra/operations/brew.py +232 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +194 -0
- pyinfra/operations/dnf.py +213 -0
- pyinfra/operations/docker.py +492 -0
- pyinfra/operations/files.py +2014 -0
- pyinfra/operations/flatpak.py +95 -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 +48 -0
- pyinfra/operations/git.py +420 -0
- pyinfra/operations/iptables.py +312 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +69 -0
- pyinfra/operations/mysql.py +610 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +89 -0
- pyinfra/operations/pacman.py +82 -0
- pyinfra/operations/pip.py +206 -0
- pyinfra/operations/pipx.py +103 -0
- pyinfra/operations/pkg.py +71 -0
- pyinfra/operations/pkgin.py +92 -0
- pyinfra/operations/postgres.py +437 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +41 -0
- pyinfra/operations/python.py +73 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +190 -0
- pyinfra/operations/server.py +1100 -0
- pyinfra/operations/snap.py +118 -0
- pyinfra/operations/ssh.py +217 -0
- pyinfra/operations/systemd.py +150 -0
- pyinfra/operations/sysvinit.py +142 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +407 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +338 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +78 -0
- pyinfra/operations/yum.py +213 -0
- pyinfra/operations/zfs.py +176 -0
- pyinfra/operations/zypper.py +193 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.6.dist-info/METADATA +142 -0
- pyinfra-3.6.dist-info/RECORD +160 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
- pyinfra-3.6.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +793 -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/api/util.py
CHANGED
|
@@ -1,70 +1,99 @@
|
|
|
1
|
-
import
|
|
2
|
-
import shlex
|
|
1
|
+
from __future__ import annotations
|
|
3
2
|
|
|
3
|
+
import hashlib
|
|
4
4
|
from functools import wraps
|
|
5
|
-
from hashlib import sha1
|
|
5
|
+
from hashlib import md5, sha1, sha256
|
|
6
6
|
from inspect import getframeinfo, stack
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from types import GeneratorType
|
|
7
|
+
from io import BytesIO, StringIO
|
|
8
|
+
from os import getcwd, path, stat
|
|
9
|
+
from socket import error as socket_error, timeout as timeout_error
|
|
10
|
+
from typing import IO, TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union
|
|
12
11
|
|
|
13
12
|
import click
|
|
14
|
-
|
|
15
|
-
from jinja2 import Template, TemplateSyntaxError, UndefinedError
|
|
13
|
+
from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template
|
|
16
14
|
from paramiko import SSHException
|
|
15
|
+
from typeguard import TypeCheckError, check_type
|
|
17
16
|
|
|
17
|
+
import pyinfra
|
|
18
18
|
from pyinfra import logger
|
|
19
|
-
from pyinfra.api import Config
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pyinfra.api.host import Host
|
|
22
|
+
from pyinfra.api.state import State, StateOperationMeta
|
|
23
|
+
from pyinfra.connectors.util import CommandOutput
|
|
22
24
|
|
|
23
25
|
# 64kb chunks
|
|
24
26
|
BLOCKSIZE = 65536
|
|
25
27
|
|
|
26
28
|
# Caches
|
|
27
|
-
TEMPLATES = {}
|
|
28
|
-
FILE_SHAS = {}
|
|
29
|
+
TEMPLATES: Dict[str, Template] = {}
|
|
30
|
+
FILE_SHAS: Dict[Any, Any] = {}
|
|
29
31
|
|
|
32
|
+
PYINFRA_INSTALL_DIR = path.normpath(path.join(path.dirname(__file__), ".."))
|
|
30
33
|
|
|
31
|
-
def try_int(value):
|
|
32
|
-
try:
|
|
33
|
-
return int(value)
|
|
34
|
-
except (TypeError, ValueError):
|
|
35
|
-
return value
|
|
36
34
|
|
|
35
|
+
def get_file_path(state: "State", filename: str):
|
|
36
|
+
if path.isabs(filename):
|
|
37
|
+
return filename
|
|
38
|
+
|
|
39
|
+
assert state.cwd is not None, "Cannot use `get_file_path` with no `state.cwd` set"
|
|
40
|
+
relative_to = state.cwd
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return hosts
|
|
42
|
+
if state.current_exec_filename and (filename.startswith("./") or filename.startswith(".\\")):
|
|
43
|
+
relative_to = path.dirname(state.current_exec_filename)
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
if isinstance(hosts, str):
|
|
44
|
-
return inventory.get_group(hosts, [])
|
|
45
|
+
return path.join(relative_to, filename)
|
|
45
46
|
|
|
46
|
-
if not isinstance(hosts, (list, tuple)):
|
|
47
|
-
return [hosts]
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
def get_kwargs_str(kwargs: Dict[Any, Any]):
|
|
49
|
+
if not kwargs:
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
items = [
|
|
53
|
+
"{0}={1}".format(key, value)
|
|
54
|
+
for key, value in sorted(kwargs.items())
|
|
55
|
+
if key not in ("self", "state", "host")
|
|
56
|
+
]
|
|
57
|
+
return ", ".join(items)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def try_int(value):
|
|
61
|
+
try:
|
|
62
|
+
return int(value)
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return value
|
|
50
65
|
|
|
51
66
|
|
|
52
|
-
def memoize(func):
|
|
67
|
+
def memoize(func: Callable[..., Any]):
|
|
53
68
|
@wraps(func)
|
|
54
69
|
def wrapper(*args, **kwargs):
|
|
55
|
-
key =
|
|
56
|
-
if key in wrapper.cache:
|
|
57
|
-
return wrapper.cache[key]
|
|
70
|
+
key = "{0}{1}".format(args, kwargs)
|
|
71
|
+
if key in wrapper.cache: # type: ignore[attr-defined]
|
|
72
|
+
return wrapper.cache[key] # type: ignore[attr-defined]
|
|
58
73
|
|
|
59
74
|
value = func(*args, **kwargs)
|
|
60
|
-
wrapper.cache[key] = value
|
|
75
|
+
wrapper.cache[key] = value # type: ignore[attr-defined]
|
|
61
76
|
return value
|
|
62
77
|
|
|
63
|
-
wrapper.cache = {}
|
|
78
|
+
wrapper.cache = {} # type: ignore[attr-defined]
|
|
64
79
|
return wrapper
|
|
65
80
|
|
|
66
81
|
|
|
67
|
-
def
|
|
82
|
+
def get_call_location(frame_offset: int = 1):
|
|
83
|
+
frame = get_caller_frameinfo(frame_offset=frame_offset) # escape *this* function
|
|
84
|
+
relpath = frame.filename
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# On Windows if pyinfra is on a different drive to the filename here, this will
|
|
88
|
+
# error as there's no way to do relative paths between drives.
|
|
89
|
+
relpath = path.relpath(frame.filename)
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
return "line {0} in {1}".format(frame.lineno, relpath)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_caller_frameinfo(frame_offset: int = 0):
|
|
68
97
|
# Default frames to look at is 2; one for this function call itself
|
|
69
98
|
# in util.py and one for the caller of this function within pyinfra
|
|
70
99
|
# giving the external call frame (ie end user deploy code).
|
|
@@ -80,394 +109,294 @@ def get_caller_frameinfo(frame_offset=0):
|
|
|
80
109
|
return info
|
|
81
110
|
|
|
82
111
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
# Support for dynamic data, ie @deploy wrapped data defaults where
|
|
86
|
-
# the data is stored on the state temporarily.
|
|
87
|
-
if callable(data):
|
|
88
|
-
data = data()
|
|
89
|
-
|
|
90
|
-
yield data
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class FallbackDict(object):
|
|
94
|
-
'''
|
|
95
|
-
Combines multiple AttrData's to search for attributes.
|
|
96
|
-
'''
|
|
97
|
-
|
|
98
|
-
override_datas = None
|
|
99
|
-
|
|
100
|
-
def __init__(self, *datas):
|
|
101
|
-
datas = list(datas)
|
|
102
|
-
|
|
103
|
-
# Inject an empty override data so we can assign during deploy
|
|
104
|
-
self.__dict__['override_datas'] = {}
|
|
105
|
-
datas.insert(0, self.override_datas)
|
|
112
|
+
def get_operation_order_from_stack(state: "State"):
|
|
113
|
+
stack_items = list(reversed(stack()))
|
|
106
114
|
|
|
107
|
-
|
|
115
|
+
i = 0
|
|
116
|
+
# Find the *first* occurrence of our deploy file in the reversed stack
|
|
117
|
+
if state.current_deploy_filename:
|
|
118
|
+
for i, stack_item in enumerate(stack_items):
|
|
119
|
+
frame = getframeinfo(stack_item[0])
|
|
120
|
+
if frame.filename == state.current_deploy_filename:
|
|
121
|
+
break
|
|
108
122
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if key in data:
|
|
112
|
-
return data[key]
|
|
123
|
+
# Now generate a list of line numbers *following that file*
|
|
124
|
+
line_numbers = []
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
if pyinfra.is_cli:
|
|
127
|
+
line_numbers.append(state.current_op_file_number)
|
|
116
128
|
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
for stack_item in stack_items[i:]:
|
|
130
|
+
frame = getframeinfo(stack_item[0])
|
|
119
131
|
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
if frame.filename.startswith(PYINFRA_INSTALL_DIR):
|
|
133
|
+
continue
|
|
122
134
|
|
|
123
|
-
|
|
124
|
-
# the last, matching __getattr__ output).
|
|
125
|
-
datas = list(self.datas)
|
|
126
|
-
datas.reverse()
|
|
135
|
+
line_numbers.append(frame.lineno)
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
out.update(data)
|
|
130
|
-
|
|
131
|
-
return out
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def pop_op_kwargs(state, kwargs):
|
|
135
|
-
'''
|
|
136
|
-
Pop and return operation global keyword arguments.
|
|
137
|
-
'''
|
|
138
|
-
|
|
139
|
-
meta_kwargs = state.deploy_kwargs or {}
|
|
140
|
-
|
|
141
|
-
def get_kwarg(key, default=None):
|
|
142
|
-
return kwargs.pop(key, meta_kwargs.get(key, default))
|
|
143
|
-
|
|
144
|
-
# Get the env for this host: config env followed by command-level env
|
|
145
|
-
env = state.config.ENV.copy()
|
|
146
|
-
env.update(get_kwarg('env', {}))
|
|
147
|
-
|
|
148
|
-
hosts = get_kwarg('hosts')
|
|
149
|
-
hosts = ensure_host_list(hosts, inventory=state.inventory)
|
|
150
|
-
|
|
151
|
-
# Filter out any hosts not in the meta kwargs (nested support)
|
|
152
|
-
if meta_kwargs.get('hosts') is not None:
|
|
153
|
-
hosts = [
|
|
154
|
-
host for host in hosts
|
|
155
|
-
if host in meta_kwargs['hosts']
|
|
156
|
-
]
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
# ENVars for commands in this operation
|
|
160
|
-
'env': env,
|
|
161
|
-
# Hosts to limit the op to
|
|
162
|
-
'hosts': hosts,
|
|
163
|
-
# When to limit the op (default always)
|
|
164
|
-
'when': get_kwarg('when', True),
|
|
165
|
-
# Locally & globally configurable
|
|
166
|
-
'shell_executable': get_kwarg('shell_executable', state.config.SHELL),
|
|
167
|
-
'sudo': get_kwarg('sudo', state.config.SUDO),
|
|
168
|
-
'sudo_user': get_kwarg('sudo_user', state.config.SUDO_USER),
|
|
169
|
-
'su_user': get_kwarg('su_user', state.config.SU_USER),
|
|
170
|
-
# Whether to preserve ENVars when sudoing (eg SSH forward agent socket)
|
|
171
|
-
'preserve_sudo_env': get_kwarg(
|
|
172
|
-
'preserve_sudo_env', state.config.PRESERVE_SUDO_ENV,
|
|
173
|
-
),
|
|
174
|
-
# Ignore any errors during this operation
|
|
175
|
-
'ignore_errors': get_kwarg(
|
|
176
|
-
'ignore_errors', state.config.IGNORE_ERRORS,
|
|
177
|
-
),
|
|
178
|
-
# Timeout on running the command
|
|
179
|
-
'timeout': get_kwarg('timeout'),
|
|
180
|
-
# Get a PTY before executing commands
|
|
181
|
-
'get_pty': get_kwarg('get_pty', False),
|
|
182
|
-
# Forces serial mode for this operation (--serial for one op)
|
|
183
|
-
'serial': get_kwarg('serial', False),
|
|
184
|
-
# Only runs this operation once
|
|
185
|
-
'run_once': get_kwarg('run_once', False),
|
|
186
|
-
# Execute in batches of X hosts rather than all at once
|
|
187
|
-
'parallel': get_kwarg('parallel'),
|
|
188
|
-
# Callbacks
|
|
189
|
-
'on_success': get_kwarg('on_success'),
|
|
190
|
-
'on_error': get_kwarg('on_error'),
|
|
191
|
-
# Operation hash
|
|
192
|
-
'op': get_kwarg('op'),
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def unroll_generators(generator):
|
|
197
|
-
'''
|
|
198
|
-
Take a generator and unroll any sub-generators recursively. This is
|
|
199
|
-
essentially a Python 2 way of doing `yield from` in Python 3 (given
|
|
200
|
-
iterating the entire thing).
|
|
201
|
-
'''
|
|
202
|
-
|
|
203
|
-
# Ensure we have a generator (prevents ccommands returning lists)
|
|
204
|
-
if not isinstance(generator, GeneratorType):
|
|
205
|
-
raise TypeError('{0} is not a generator'.format(generator))
|
|
206
|
-
|
|
207
|
-
items = []
|
|
208
|
-
|
|
209
|
-
for item in generator:
|
|
210
|
-
if isinstance(item, GeneratorType):
|
|
211
|
-
items.extend(unroll_generators(item))
|
|
212
|
-
else:
|
|
213
|
-
items.append(item)
|
|
137
|
+
del stack_items
|
|
214
138
|
|
|
215
|
-
return
|
|
139
|
+
return line_numbers
|
|
216
140
|
|
|
217
141
|
|
|
218
|
-
def get_template(
|
|
219
|
-
|
|
142
|
+
def get_template(
|
|
143
|
+
filename_or_io: str | IO, jinja_env_kwargs: dict[str, Any] | None = None
|
|
144
|
+
) -> Template:
|
|
145
|
+
"""
|
|
220
146
|
Gets a jinja2 ``Template`` object for the input filename or string, with caching
|
|
221
147
|
based on the filename of the template, or the SHA1 of the input string.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
148
|
+
"""
|
|
149
|
+
if jinja_env_kwargs is None:
|
|
150
|
+
jinja_env_kwargs = {}
|
|
151
|
+
file_data = get_file_io(filename_or_io, mode="r")
|
|
152
|
+
cache_key = file_data.cache_key
|
|
226
153
|
|
|
227
|
-
if cache_key in TEMPLATES:
|
|
154
|
+
if cache_key and cache_key in TEMPLATES:
|
|
228
155
|
return TEMPLATES[cache_key]
|
|
229
156
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
template_string = filename_or_string
|
|
233
|
-
|
|
234
|
-
else:
|
|
235
|
-
# Load template data into memory
|
|
236
|
-
with open(filename_or_string, 'r') as file_io:
|
|
237
|
-
template_string = file_io.read()
|
|
238
|
-
|
|
239
|
-
TEMPLATES[cache_key] = Template(template_string, keep_trailing_newline=True)
|
|
240
|
-
return TEMPLATES[cache_key]
|
|
157
|
+
with file_data as file_io:
|
|
158
|
+
template_string = file_io.read()
|
|
241
159
|
|
|
160
|
+
default_loader = FileSystemLoader(getcwd())
|
|
161
|
+
template = Environment(
|
|
162
|
+
undefined=StrictUndefined,
|
|
163
|
+
keep_trailing_newline=True,
|
|
164
|
+
loader=jinja_env_kwargs.pop("loader", default_loader),
|
|
165
|
+
**jinja_env_kwargs,
|
|
166
|
+
).from_string(template_string)
|
|
242
167
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
Transform CamelCase -> snake_case.
|
|
246
|
-
'''
|
|
168
|
+
if cache_key:
|
|
169
|
+
TEMPLATES[cache_key] = template
|
|
247
170
|
|
|
248
|
-
|
|
249
|
-
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
171
|
+
return template
|
|
250
172
|
|
|
251
173
|
|
|
252
|
-
def sha1_hash(string):
|
|
253
|
-
|
|
174
|
+
def sha1_hash(string: str) -> str:
|
|
175
|
+
"""
|
|
254
176
|
Return the SHA1 of the input string.
|
|
255
|
-
|
|
177
|
+
"""
|
|
256
178
|
|
|
257
179
|
hasher = sha1()
|
|
258
|
-
hasher.update(string.encode())
|
|
180
|
+
hasher.update(string.encode("utf-8"))
|
|
259
181
|
return hasher.hexdigest()
|
|
260
182
|
|
|
261
183
|
|
|
262
|
-
def format_exception(e):
|
|
263
|
-
return
|
|
184
|
+
def format_exception(e: Exception) -> str:
|
|
185
|
+
return f"{e.__class__.__name__}{e.args}"
|
|
264
186
|
|
|
265
187
|
|
|
266
|
-
def
|
|
267
|
-
|
|
268
|
-
|
|
188
|
+
def print_host_combined_output(host: "Host", output: "CommandOutput") -> None:
|
|
189
|
+
for line in output:
|
|
190
|
+
if line.buffer_name == "stderr":
|
|
191
|
+
logger.error(f"{host.print_prefix}{click.style(line.line, 'red')}")
|
|
192
|
+
else:
|
|
193
|
+
logger.error(f"{host.print_prefix}{line.line}")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def log_operation_start(
|
|
197
|
+
op_meta: "StateOperationMeta", op_types: Optional[List] = None, prefix: str = "--> "
|
|
198
|
+
) -> None:
|
|
199
|
+
op_types = op_types or []
|
|
200
|
+
if op_meta.global_arguments["_serial"]:
|
|
201
|
+
op_types.append("serial")
|
|
202
|
+
if op_meta.global_arguments["_run_once"]:
|
|
203
|
+
op_types.append("run once")
|
|
204
|
+
|
|
205
|
+
args = ""
|
|
206
|
+
if op_meta.args:
|
|
207
|
+
args = "({0})".format(", ".join(str(arg) for arg in op_meta.args))
|
|
208
|
+
|
|
209
|
+
logger.info(
|
|
210
|
+
"{0} {1} {2}".format(
|
|
211
|
+
click.style(
|
|
212
|
+
"{0}Starting{1}operation:".format(
|
|
213
|
+
prefix,
|
|
214
|
+
" {0} ".format(", ".join(op_types)) if op_types else " ",
|
|
215
|
+
),
|
|
216
|
+
"blue",
|
|
217
|
+
),
|
|
218
|
+
click.style(", ".join(op_meta.names), bold=True),
|
|
219
|
+
args,
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def log_error_or_warning(
|
|
225
|
+
host: "Host",
|
|
226
|
+
ignore_errors: bool,
|
|
227
|
+
description: str = "",
|
|
228
|
+
continue_on_error: bool = False,
|
|
229
|
+
exception: Exception | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
log_func = logger.error
|
|
232
|
+
log_color = "red"
|
|
233
|
+
log_text = "Error: " if description else "Error"
|
|
234
|
+
|
|
235
|
+
if ignore_errors:
|
|
236
|
+
log_func = logger.warning
|
|
237
|
+
log_color = "yellow"
|
|
238
|
+
log_text = (
|
|
239
|
+
"Error (ignored, execution continued)" if continue_on_error else "Error (ignored)"
|
|
240
|
+
)
|
|
241
|
+
if description:
|
|
242
|
+
log_text = f"{log_text}: "
|
|
243
|
+
|
|
244
|
+
if exception:
|
|
245
|
+
exc = exception.__cause__ or exception
|
|
246
|
+
exc_text = "{0}: {1}".format(type(exc).__name__, exc)
|
|
247
|
+
log_func(
|
|
248
|
+
"{0}{1}".format(
|
|
249
|
+
host.print_prefix,
|
|
250
|
+
click.style(exc_text, log_color),
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
log_func(
|
|
255
|
+
"{0}{1}{2}".format(
|
|
269
256
|
host.print_prefix,
|
|
270
|
-
click.style(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
257
|
+
click.style(log_text, log_color),
|
|
258
|
+
description,
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def log_host_command_error(host: "Host", e: Exception, timeout: int | None = 0) -> None:
|
|
264
|
+
if isinstance(e, timeout_error):
|
|
265
|
+
logger.error(
|
|
266
|
+
"{0}{1}".format(
|
|
267
|
+
host.print_prefix,
|
|
268
|
+
click.style(
|
|
269
|
+
"Command timed out after {0}s".format(
|
|
270
|
+
timeout,
|
|
271
|
+
),
|
|
272
|
+
"red",
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
)
|
|
274
276
|
|
|
275
277
|
elif isinstance(e, (socket_error, SSHException)):
|
|
276
|
-
logger.error(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
logger.error(
|
|
279
|
+
"{0}{1}".format(
|
|
280
|
+
host.print_prefix,
|
|
281
|
+
click.style(
|
|
282
|
+
"Command socket/SSH error: {0}".format(format_exception(e)),
|
|
283
|
+
"red",
|
|
284
|
+
),
|
|
280
285
|
),
|
|
281
|
-
)
|
|
286
|
+
)
|
|
282
287
|
|
|
283
288
|
elif isinstance(e, IOError):
|
|
284
|
-
logger.error(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
289
|
+
logger.error(
|
|
290
|
+
"{0}{1}".format(
|
|
291
|
+
host.print_prefix,
|
|
292
|
+
click.style(
|
|
293
|
+
"Command IO error: {0}".format(format_exception(e)),
|
|
294
|
+
"red",
|
|
295
|
+
),
|
|
288
296
|
),
|
|
289
|
-
)
|
|
297
|
+
)
|
|
290
298
|
|
|
291
299
|
# Still here? Re-raise!
|
|
292
300
|
else:
|
|
293
301
|
raise e
|
|
294
302
|
|
|
295
303
|
|
|
296
|
-
def make_command(
|
|
297
|
-
command,
|
|
298
|
-
env=None,
|
|
299
|
-
su_user=None,
|
|
300
|
-
sudo=False,
|
|
301
|
-
sudo_user=None,
|
|
302
|
-
preserve_sudo_env=False,
|
|
303
|
-
shell_executable=Config.SHELL,
|
|
304
|
-
):
|
|
305
|
-
'''
|
|
306
|
-
Builds a shell command with various kwargs.
|
|
307
|
-
'''
|
|
308
|
-
|
|
309
|
-
debug_meta = {}
|
|
310
|
-
|
|
311
|
-
for key, value in (
|
|
312
|
-
('shell_executable', shell_executable),
|
|
313
|
-
('sudo', sudo),
|
|
314
|
-
('sudo_user', sudo_user),
|
|
315
|
-
('su_user', su_user),
|
|
316
|
-
('env', env),
|
|
317
|
-
):
|
|
318
|
-
if value:
|
|
319
|
-
debug_meta[key] = value
|
|
320
|
-
|
|
321
|
-
logger.debug('Building command ({0}): {1}'.format(' '.join(
|
|
322
|
-
'{0}: {1}'.format(key, value)
|
|
323
|
-
for key, value in debug_meta.items()
|
|
324
|
-
), command))
|
|
325
|
-
|
|
326
|
-
# Use env & build our actual command
|
|
327
|
-
if env:
|
|
328
|
-
env_string = ' '.join([
|
|
329
|
-
'{0}={1}'.format(key, value)
|
|
330
|
-
for key, value in env.items()
|
|
331
|
-
])
|
|
332
|
-
command = 'export {0}; {1}'.format(env_string, command)
|
|
333
|
-
|
|
334
|
-
# Quote the command as a string
|
|
335
|
-
command = shlex.quote(command)
|
|
336
|
-
|
|
337
|
-
# Switch user with su
|
|
338
|
-
if su_user:
|
|
339
|
-
# note `which <shell>` usage here - su requires an absolute path
|
|
340
|
-
command = 'su {0} -s `which {1}` -c {2}'.format(
|
|
341
|
-
su_user, shell_executable, command,
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
# Otherwise just sh wrap the command
|
|
345
|
-
else:
|
|
346
|
-
command = '{0} -c {1}'.format(shell_executable, command)
|
|
347
|
-
|
|
348
|
-
# Use sudo (w/user?)
|
|
349
|
-
if sudo:
|
|
350
|
-
sudo_bits = ['sudo', '-H']
|
|
351
|
-
|
|
352
|
-
if preserve_sudo_env:
|
|
353
|
-
sudo_bits.append('-E')
|
|
354
|
-
|
|
355
|
-
if sudo_user:
|
|
356
|
-
sudo_bits.extend(('-u', sudo_user))
|
|
357
|
-
|
|
358
|
-
command = '{0} {1}'.format(' '.join(sudo_bits), command)
|
|
359
|
-
|
|
360
|
-
return command
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def get_arg_value(state, host, arg):
|
|
364
|
-
'''
|
|
365
|
-
Runs string arguments through the jinja2 templating system with a state and
|
|
366
|
-
host. Used to avoid string formatting in deploy operations which result in
|
|
367
|
-
one operation per host/variable. By parsing the commands after we generate
|
|
368
|
-
the ``op_hash``, multiple command variations can fall under one op.
|
|
369
|
-
'''
|
|
370
|
-
|
|
371
|
-
if isinstance(arg, str):
|
|
372
|
-
data = {
|
|
373
|
-
'host': host,
|
|
374
|
-
'inventory': state.inventory,
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
try:
|
|
378
|
-
return get_template(arg, is_string=True).render(data)
|
|
379
|
-
except (TemplateSyntaxError, UndefinedError) as e:
|
|
380
|
-
raise PyinfraError('Error in template string: {0}'.format(e))
|
|
381
|
-
|
|
382
|
-
elif isinstance(arg, list):
|
|
383
|
-
return [get_arg_value(state, host, value) for value in arg]
|
|
384
|
-
|
|
385
|
-
elif isinstance(arg, tuple):
|
|
386
|
-
return tuple(get_arg_value(state, host, value) for value in arg)
|
|
387
|
-
|
|
388
|
-
elif isinstance(arg, dict):
|
|
389
|
-
return {
|
|
390
|
-
key: get_arg_value(state, host, value)
|
|
391
|
-
for key, value in arg.items()
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return arg
|
|
395
|
-
|
|
396
|
-
|
|
397
304
|
def make_hash(obj):
|
|
398
|
-
|
|
305
|
+
"""
|
|
399
306
|
Make a hash from an arbitrary nested dictionary, list, tuple or set, used to generate
|
|
400
307
|
ID's for operations based on their name & arguments.
|
|
401
|
-
|
|
308
|
+
"""
|
|
402
309
|
|
|
403
310
|
if isinstance(obj, (set, tuple, list)):
|
|
404
|
-
hash_string =
|
|
311
|
+
hash_string = "".join([make_hash(e) for e in obj])
|
|
405
312
|
|
|
406
313
|
elif isinstance(obj, dict):
|
|
407
|
-
hash_string =
|
|
408
|
-
''.join((key, make_hash(value)))
|
|
409
|
-
for key, value in obj.items()
|
|
410
|
-
)
|
|
314
|
+
hash_string = "".join("".join((key, make_hash(value))) for key, value in obj.items())
|
|
411
315
|
|
|
412
316
|
else:
|
|
413
317
|
hash_string = (
|
|
318
|
+
# Capture integers first (as 1 == True)
|
|
319
|
+
"{0}".format(obj)
|
|
320
|
+
if isinstance(obj, int)
|
|
414
321
|
# Constants - the values can change between hosts but we should still
|
|
415
322
|
# group them under the same operation hash.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
323
|
+
else (
|
|
324
|
+
"_PYINFRA_CONSTANT"
|
|
325
|
+
if obj in (True, False, None)
|
|
326
|
+
# Plain strings
|
|
327
|
+
else (
|
|
328
|
+
obj
|
|
329
|
+
if isinstance(obj, str)
|
|
330
|
+
# Objects with __name__s
|
|
331
|
+
else (
|
|
332
|
+
obj.__name__
|
|
333
|
+
if hasattr(obj, "__name__")
|
|
334
|
+
# Objects with names
|
|
335
|
+
else (
|
|
336
|
+
obj.name
|
|
337
|
+
if hasattr(obj, "name")
|
|
338
|
+
# Repr anything else
|
|
339
|
+
else repr(obj)
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
)
|
|
425
344
|
)
|
|
426
345
|
|
|
427
346
|
return sha1_hash(hash_string)
|
|
428
347
|
|
|
429
348
|
|
|
430
|
-
class get_file_io
|
|
431
|
-
|
|
349
|
+
class get_file_io:
|
|
350
|
+
"""
|
|
432
351
|
Given either a filename or an existing IO object, this context processor
|
|
433
352
|
will open and close filenames, and leave IO objects alone.
|
|
434
|
-
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
filename_or_io: Union[str, IO[Any]]
|
|
356
|
+
mode: str
|
|
435
357
|
|
|
436
|
-
|
|
358
|
+
_close: bool = False
|
|
359
|
+
_file_io: IO[Any]
|
|
437
360
|
|
|
438
|
-
def __init__(self, filename_or_io):
|
|
361
|
+
def __init__(self, filename_or_io: str | IO, mode: str = "rb"):
|
|
439
362
|
if not (
|
|
440
363
|
# Check we can be read
|
|
441
|
-
hasattr(filename_or_io,
|
|
364
|
+
hasattr(filename_or_io, "read")
|
|
442
365
|
# Or we're a filename
|
|
443
366
|
or isinstance(filename_or_io, str)
|
|
444
367
|
):
|
|
445
|
-
raise TypeError(
|
|
446
|
-
|
|
447
|
-
|
|
368
|
+
raise TypeError(
|
|
369
|
+
"Invalid filename or IO object: {0}".format(
|
|
370
|
+
filename_or_io,
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Convert any StringIO/BytesIO to the other to match the desired mode
|
|
375
|
+
if isinstance(filename_or_io, StringIO) and mode == "rb":
|
|
376
|
+
filename_or_io.seek(0)
|
|
377
|
+
filename_or_io = BytesIO(filename_or_io.read().encode())
|
|
378
|
+
if isinstance(filename_or_io, BytesIO) and mode == "r":
|
|
379
|
+
filename_or_io.seek(0)
|
|
380
|
+
filename_or_io = StringIO(filename_or_io.read().decode())
|
|
448
381
|
|
|
449
382
|
self.filename_or_io = filename_or_io
|
|
383
|
+
self.mode = mode
|
|
450
384
|
|
|
451
385
|
def __enter__(self):
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
# Otherwise, assume a filename and open it up
|
|
386
|
+
if isinstance(self.filename_or_io, str):
|
|
387
|
+
file_io = open(self.filename_or_io, self.mode)
|
|
388
|
+
self._file_io = file_io
|
|
389
|
+
self._close = True
|
|
457
390
|
else:
|
|
458
|
-
file_io =
|
|
459
|
-
|
|
460
|
-
# Attach to self for closing on __exit__
|
|
461
|
-
self.file_io = file_io
|
|
462
|
-
self.close = True
|
|
391
|
+
file_io = self.filename_or_io
|
|
463
392
|
|
|
464
393
|
# Ensure we're at the start of the file
|
|
465
394
|
file_io.seek(0)
|
|
466
395
|
return file_io
|
|
467
396
|
|
|
468
397
|
def __exit__(self, type, value, traceback):
|
|
469
|
-
if self.
|
|
470
|
-
self.
|
|
398
|
+
if self._close:
|
|
399
|
+
self._file_io.close()
|
|
471
400
|
|
|
472
401
|
@property
|
|
473
402
|
def cache_key(self):
|
|
@@ -477,24 +406,37 @@ class get_file_io(object):
|
|
|
477
406
|
return self.filename_or_io
|
|
478
407
|
|
|
479
408
|
|
|
480
|
-
def
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
409
|
+
def get_file_md5(filename_or_io: str | IO):
|
|
410
|
+
return _get_file_digest(filename_or_io, md5())
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def get_file_sha1(filename_or_io: str | IO):
|
|
414
|
+
return _get_file_digest(filename_or_io, sha1())
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_file_sha256(filename_or_io: str | IO):
|
|
418
|
+
return _get_file_digest(filename_or_io, sha256())
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _get_file_digest(filename_or_io: str | IO, hasher: hashlib._Hash):
|
|
422
|
+
"""
|
|
423
|
+
Calculates the hash of a file or file object using a buffer to handle larger files.
|
|
424
|
+
"""
|
|
484
425
|
|
|
485
426
|
file_data = get_file_io(filename_or_io)
|
|
486
427
|
cache_key = file_data.cache_key
|
|
428
|
+
if cache_key:
|
|
429
|
+
cache_key = f"{cache_key}_{hasher.name}"
|
|
487
430
|
|
|
488
431
|
if cache_key and cache_key in FILE_SHAS:
|
|
489
432
|
return FILE_SHAS[cache_key]
|
|
490
433
|
|
|
491
434
|
with file_data as file_io:
|
|
492
|
-
hasher = sha1()
|
|
493
435
|
buff = file_io.read(BLOCKSIZE)
|
|
494
436
|
|
|
495
437
|
while len(buff) > 0:
|
|
496
438
|
if isinstance(buff, str):
|
|
497
|
-
buff = buff.encode(
|
|
439
|
+
buff = buff.encode("utf-8")
|
|
498
440
|
|
|
499
441
|
hasher.update(buff)
|
|
500
442
|
buff = file_io.read(BLOCKSIZE)
|
|
@@ -507,24 +449,22 @@ def get_file_sha1(filename_or_io):
|
|
|
507
449
|
return digest
|
|
508
450
|
|
|
509
451
|
|
|
510
|
-
def
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def _print(line):
|
|
516
|
-
if print_func:
|
|
517
|
-
line = print_func(line)
|
|
452
|
+
def get_path_permissions_mode(pathname: str):
|
|
453
|
+
"""
|
|
454
|
+
Get the permissions (bits) of a path as an integer.
|
|
455
|
+
"""
|
|
518
456
|
|
|
519
|
-
|
|
457
|
+
mode_octal = oct(stat(pathname).st_mode)
|
|
458
|
+
return int(mode_octal[-3:])
|
|
520
459
|
|
|
521
|
-
for line in io:
|
|
522
|
-
# Handle local Popen shells returning list of bytes, not strings
|
|
523
|
-
if not isinstance(line, str):
|
|
524
|
-
line = line.decode('utf-8')
|
|
525
460
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
461
|
+
def raise_if_bad_type(
|
|
462
|
+
value: Any,
|
|
463
|
+
type_: Type,
|
|
464
|
+
exception: type[Exception],
|
|
465
|
+
message_prefix: str,
|
|
466
|
+
):
|
|
467
|
+
try:
|
|
468
|
+
check_type(value, type_)
|
|
469
|
+
except TypeCheckError as e:
|
|
470
|
+
raise exception(f"{message_prefix}: {e}")
|