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/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
|
|
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[Any, Any] = {}
|
|
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 ""
|
|
50
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)
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
|
|
60
|
+
def try_int(value):
|
|
61
|
+
try:
|
|
62
|
+
return int(value)
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
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,277 @@ 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()
|
|
112
|
+
def get_operation_order_from_stack(state: "State"):
|
|
113
|
+
stack_items = list(reversed(stack()))
|
|
89
114
|
|
|
90
|
-
|
|
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
|
|
91
122
|
|
|
123
|
+
# Now generate a list of line numbers *following that file*
|
|
124
|
+
line_numbers = []
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Combines multiple AttrData's to search for attributes.
|
|
96
|
-
'''
|
|
126
|
+
if pyinfra.is_cli:
|
|
127
|
+
line_numbers.append(state.current_op_file_number)
|
|
97
128
|
|
|
98
|
-
|
|
129
|
+
for stack_item in stack_items[i:]:
|
|
130
|
+
frame = getframeinfo(stack_item[0])
|
|
99
131
|
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
if frame.filename.startswith(PYINFRA_INSTALL_DIR):
|
|
133
|
+
continue
|
|
102
134
|
|
|
103
|
-
|
|
104
|
-
self.__dict__['override_datas'] = {}
|
|
105
|
-
datas.insert(0, self.override_datas)
|
|
135
|
+
line_numbers.append(frame.lineno)
|
|
106
136
|
|
|
107
|
-
|
|
137
|
+
del stack_items
|
|
108
138
|
|
|
109
|
-
|
|
110
|
-
for data in extract_callable_datas(self.datas):
|
|
111
|
-
if key in data:
|
|
112
|
-
return data[key]
|
|
139
|
+
return line_numbers
|
|
113
140
|
|
|
114
|
-
def __setattr__(self, key, value):
|
|
115
|
-
self.override_datas[key] = value
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
def get_template(filename_or_io: str | IO, jinja_env_kwargs: dict[str, Any] | None = None):
|
|
143
|
+
"""
|
|
144
|
+
Gets a jinja2 ``Template`` object for the input filename or string, with caching
|
|
145
|
+
based on the filename of the template, or the SHA1 of the input string.
|
|
146
|
+
"""
|
|
147
|
+
if jinja_env_kwargs is None:
|
|
148
|
+
jinja_env_kwargs = {}
|
|
149
|
+
file_data = get_file_io(filename_or_io, mode="r")
|
|
150
|
+
cache_key = file_data.cache_key
|
|
119
151
|
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
if cache_key and cache_key in TEMPLATES:
|
|
153
|
+
return TEMPLATES[cache_key]
|
|
122
154
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
datas = list(self.datas)
|
|
126
|
-
datas.reverse()
|
|
155
|
+
with file_data as file_io:
|
|
156
|
+
template_string = file_io.read()
|
|
127
157
|
|
|
128
|
-
|
|
129
|
-
|
|
158
|
+
template = Environment(
|
|
159
|
+
undefined=StrictUndefined,
|
|
160
|
+
keep_trailing_newline=True,
|
|
161
|
+
loader=FileSystemLoader(getcwd()),
|
|
162
|
+
**jinja_env_kwargs,
|
|
163
|
+
).from_string(template_string)
|
|
130
164
|
|
|
131
|
-
|
|
165
|
+
if cache_key:
|
|
166
|
+
TEMPLATES[cache_key] = template
|
|
132
167
|
|
|
168
|
+
return template
|
|
133
169
|
|
|
134
|
-
def pop_op_kwargs(state, kwargs):
|
|
135
|
-
'''
|
|
136
|
-
Pop and return operation global keyword arguments.
|
|
137
|
-
'''
|
|
138
170
|
|
|
139
|
-
|
|
171
|
+
def sha1_hash(string: str) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Return the SHA1 of the input string.
|
|
174
|
+
"""
|
|
140
175
|
|
|
141
|
-
|
|
142
|
-
|
|
176
|
+
hasher = sha1()
|
|
177
|
+
hasher.update(string.encode("utf-8"))
|
|
178
|
+
return hasher.hexdigest()
|
|
143
179
|
|
|
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
180
|
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
def format_exception(e: Exception) -> str:
|
|
182
|
+
return f"{e.__class__.__name__}{e.args}"
|
|
150
183
|
|
|
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
184
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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))
|
|
185
|
+
def print_host_combined_output(host: "Host", output: "CommandOutput") -> None:
|
|
186
|
+
for line in output:
|
|
187
|
+
if line.buffer_name == "stderr":
|
|
188
|
+
logger.error(f"{host.print_prefix}{click.style(line.line, 'red')}")
|
|
212
189
|
else:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def underscore(name):
|
|
244
|
-
'''
|
|
245
|
-
Transform CamelCase -> snake_case.
|
|
246
|
-
'''
|
|
247
|
-
|
|
248
|
-
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
|
249
|
-
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
250
|
-
|
|
190
|
+
logger.error(f"{host.print_prefix}{line.line}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def log_operation_start(
|
|
194
|
+
op_meta: "StateOperationMeta", op_types: Optional[List] = None, prefix: str = "--> "
|
|
195
|
+
) -> None:
|
|
196
|
+
op_types = op_types or []
|
|
197
|
+
if op_meta.global_arguments["_serial"]:
|
|
198
|
+
op_types.append("serial")
|
|
199
|
+
if op_meta.global_arguments["_run_once"]:
|
|
200
|
+
op_types.append("run once")
|
|
201
|
+
|
|
202
|
+
args = ""
|
|
203
|
+
if op_meta.args:
|
|
204
|
+
args = "({0})".format(", ".join(str(arg) for arg in op_meta.args))
|
|
205
|
+
|
|
206
|
+
logger.info(
|
|
207
|
+
"{0} {1} {2}".format(
|
|
208
|
+
click.style(
|
|
209
|
+
"{0}Starting{1}operation:".format(
|
|
210
|
+
prefix,
|
|
211
|
+
" {0} ".format(", ".join(op_types)) if op_types else " ",
|
|
212
|
+
),
|
|
213
|
+
"blue",
|
|
214
|
+
),
|
|
215
|
+
click.style(", ".join(op_meta.names), bold=True),
|
|
216
|
+
args,
|
|
217
|
+
),
|
|
218
|
+
)
|
|
251
219
|
|
|
252
|
-
def sha1_hash(string):
|
|
253
|
-
'''
|
|
254
|
-
Return the SHA1 of the input string.
|
|
255
|
-
'''
|
|
256
220
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
221
|
+
def log_error_or_warning(
|
|
222
|
+
host: "Host", ignore_errors: bool, description: str = "", continue_on_error: bool = False
|
|
223
|
+
) -> None:
|
|
224
|
+
log_func = logger.error
|
|
225
|
+
log_color = "red"
|
|
226
|
+
log_text = "Error: " if description else "Error"
|
|
260
227
|
|
|
228
|
+
if ignore_errors:
|
|
229
|
+
log_func = logger.warning
|
|
230
|
+
log_color = "yellow"
|
|
231
|
+
log_text = (
|
|
232
|
+
"Error (ignored, execution continued)" if continue_on_error else "Error (ignored)"
|
|
233
|
+
)
|
|
234
|
+
if description:
|
|
235
|
+
log_text = f"{log_text}: "
|
|
261
236
|
|
|
262
|
-
|
|
263
|
-
|
|
237
|
+
log_func(
|
|
238
|
+
"{0}{1}{2}".format(
|
|
239
|
+
host.print_prefix,
|
|
240
|
+
click.style(log_text, log_color),
|
|
241
|
+
description,
|
|
242
|
+
),
|
|
243
|
+
)
|
|
264
244
|
|
|
265
245
|
|
|
266
|
-
def log_host_command_error(host, e, timeout=0):
|
|
246
|
+
def log_host_command_error(host: "Host", e: Exception, timeout: int | None = 0) -> None:
|
|
267
247
|
if isinstance(e, timeout_error):
|
|
268
|
-
logger.error(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
248
|
+
logger.error(
|
|
249
|
+
"{0}{1}".format(
|
|
250
|
+
host.print_prefix,
|
|
251
|
+
click.style(
|
|
252
|
+
"Command timed out after {0}s".format(
|
|
253
|
+
timeout,
|
|
254
|
+
),
|
|
255
|
+
"red",
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
)
|
|
274
259
|
|
|
275
260
|
elif isinstance(e, (socket_error, SSHException)):
|
|
276
|
-
logger.error(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
261
|
+
logger.error(
|
|
262
|
+
"{0}{1}".format(
|
|
263
|
+
host.print_prefix,
|
|
264
|
+
click.style(
|
|
265
|
+
"Command socket/SSH error: {0}".format(format_exception(e)),
|
|
266
|
+
"red",
|
|
267
|
+
),
|
|
280
268
|
),
|
|
281
|
-
)
|
|
269
|
+
)
|
|
282
270
|
|
|
283
271
|
elif isinstance(e, IOError):
|
|
284
|
-
logger.error(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
272
|
+
logger.error(
|
|
273
|
+
"{0}{1}".format(
|
|
274
|
+
host.print_prefix,
|
|
275
|
+
click.style(
|
|
276
|
+
"Command IO error: {0}".format(format_exception(e)),
|
|
277
|
+
"red",
|
|
278
|
+
),
|
|
288
279
|
),
|
|
289
|
-
)
|
|
280
|
+
)
|
|
290
281
|
|
|
291
282
|
# Still here? Re-raise!
|
|
292
283
|
else:
|
|
293
284
|
raise e
|
|
294
285
|
|
|
295
286
|
|
|
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
287
|
def make_hash(obj):
|
|
398
|
-
|
|
288
|
+
"""
|
|
399
289
|
Make a hash from an arbitrary nested dictionary, list, tuple or set, used to generate
|
|
400
290
|
ID's for operations based on their name & arguments.
|
|
401
|
-
|
|
291
|
+
"""
|
|
402
292
|
|
|
403
293
|
if isinstance(obj, (set, tuple, list)):
|
|
404
|
-
hash_string =
|
|
294
|
+
hash_string = "".join([make_hash(e) for e in obj])
|
|
405
295
|
|
|
406
296
|
elif isinstance(obj, dict):
|
|
407
|
-
hash_string =
|
|
408
|
-
''.join((key, make_hash(value)))
|
|
409
|
-
for key, value in obj.items()
|
|
410
|
-
)
|
|
297
|
+
hash_string = "".join("".join((key, make_hash(value))) for key, value in obj.items())
|
|
411
298
|
|
|
412
299
|
else:
|
|
413
300
|
hash_string = (
|
|
301
|
+
# Capture integers first (as 1 == True)
|
|
302
|
+
"{0}".format(obj)
|
|
303
|
+
if isinstance(obj, int)
|
|
414
304
|
# Constants - the values can change between hosts but we should still
|
|
415
305
|
# group them under the same operation hash.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
306
|
+
else (
|
|
307
|
+
"_PYINFRA_CONSTANT"
|
|
308
|
+
if obj in (True, False, None)
|
|
309
|
+
# Plain strings
|
|
310
|
+
else (
|
|
311
|
+
obj
|
|
312
|
+
if isinstance(obj, str)
|
|
313
|
+
# Objects with __name__s
|
|
314
|
+
else (
|
|
315
|
+
obj.__name__
|
|
316
|
+
if hasattr(obj, "__name__")
|
|
317
|
+
# Objects with names
|
|
318
|
+
else (
|
|
319
|
+
obj.name
|
|
320
|
+
if hasattr(obj, "name")
|
|
321
|
+
# Repr anything else
|
|
322
|
+
else repr(obj)
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
)
|
|
425
327
|
)
|
|
426
328
|
|
|
427
329
|
return sha1_hash(hash_string)
|
|
428
330
|
|
|
429
331
|
|
|
430
|
-
class get_file_io
|
|
431
|
-
|
|
332
|
+
class get_file_io:
|
|
333
|
+
"""
|
|
432
334
|
Given either a filename or an existing IO object, this context processor
|
|
433
335
|
will open and close filenames, and leave IO objects alone.
|
|
434
|
-
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
filename_or_io: Union[str, IO[Any]]
|
|
339
|
+
mode: str
|
|
435
340
|
|
|
436
|
-
|
|
341
|
+
_close: bool = False
|
|
342
|
+
_file_io: IO[Any]
|
|
437
343
|
|
|
438
|
-
def __init__(self, filename_or_io):
|
|
344
|
+
def __init__(self, filename_or_io: str | IO, mode: str = "rb"):
|
|
439
345
|
if not (
|
|
440
346
|
# Check we can be read
|
|
441
|
-
hasattr(filename_or_io,
|
|
347
|
+
hasattr(filename_or_io, "read")
|
|
442
348
|
# Or we're a filename
|
|
443
349
|
or isinstance(filename_or_io, str)
|
|
444
350
|
):
|
|
445
|
-
raise TypeError(
|
|
446
|
-
|
|
447
|
-
|
|
351
|
+
raise TypeError(
|
|
352
|
+
"Invalid filename or IO object: {0}".format(
|
|
353
|
+
filename_or_io,
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Convert any StringIO/BytesIO to the other to match the desired mode
|
|
358
|
+
if isinstance(filename_or_io, StringIO) and mode == "rb":
|
|
359
|
+
filename_or_io.seek(0)
|
|
360
|
+
filename_or_io = BytesIO(filename_or_io.read().encode())
|
|
361
|
+
if isinstance(filename_or_io, BytesIO) and mode == "r":
|
|
362
|
+
filename_or_io.seek(0)
|
|
363
|
+
filename_or_io = StringIO(filename_or_io.read().decode())
|
|
448
364
|
|
|
449
365
|
self.filename_or_io = filename_or_io
|
|
366
|
+
self.mode = mode
|
|
450
367
|
|
|
451
368
|
def __enter__(self):
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
# Otherwise, assume a filename and open it up
|
|
369
|
+
if isinstance(self.filename_or_io, str):
|
|
370
|
+
file_io = open(self.filename_or_io, self.mode)
|
|
371
|
+
self._file_io = file_io
|
|
372
|
+
self._close = True
|
|
457
373
|
else:
|
|
458
|
-
file_io =
|
|
459
|
-
|
|
460
|
-
# Attach to self for closing on __exit__
|
|
461
|
-
self.file_io = file_io
|
|
462
|
-
self.close = True
|
|
374
|
+
file_io = self.filename_or_io
|
|
463
375
|
|
|
464
376
|
# Ensure we're at the start of the file
|
|
465
377
|
file_io.seek(0)
|
|
466
378
|
return file_io
|
|
467
379
|
|
|
468
380
|
def __exit__(self, type, value, traceback):
|
|
469
|
-
if self.
|
|
470
|
-
self.
|
|
381
|
+
if self._close:
|
|
382
|
+
self._file_io.close()
|
|
471
383
|
|
|
472
384
|
@property
|
|
473
385
|
def cache_key(self):
|
|
@@ -477,24 +389,37 @@ class get_file_io(object):
|
|
|
477
389
|
return self.filename_or_io
|
|
478
390
|
|
|
479
391
|
|
|
480
|
-
def
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
392
|
+
def get_file_md5(filename_or_io: str | IO):
|
|
393
|
+
return _get_file_digest(filename_or_io, md5())
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def get_file_sha1(filename_or_io: str | IO):
|
|
397
|
+
return _get_file_digest(filename_or_io, sha1())
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def get_file_sha256(filename_or_io: str | IO):
|
|
401
|
+
return _get_file_digest(filename_or_io, sha256())
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _get_file_digest(filename_or_io: str | IO, hasher: hashlib._Hash):
|
|
405
|
+
"""
|
|
406
|
+
Calculates the hash of a file or file object using a buffer to handle larger files.
|
|
407
|
+
"""
|
|
484
408
|
|
|
485
409
|
file_data = get_file_io(filename_or_io)
|
|
486
410
|
cache_key = file_data.cache_key
|
|
411
|
+
if cache_key:
|
|
412
|
+
cache_key = f"{cache_key}_{hasher.name}"
|
|
487
413
|
|
|
488
414
|
if cache_key and cache_key in FILE_SHAS:
|
|
489
415
|
return FILE_SHAS[cache_key]
|
|
490
416
|
|
|
491
417
|
with file_data as file_io:
|
|
492
|
-
hasher = sha1()
|
|
493
418
|
buff = file_io.read(BLOCKSIZE)
|
|
494
419
|
|
|
495
420
|
while len(buff) > 0:
|
|
496
421
|
if isinstance(buff, str):
|
|
497
|
-
buff = buff.encode(
|
|
422
|
+
buff = buff.encode("utf-8")
|
|
498
423
|
|
|
499
424
|
hasher.update(buff)
|
|
500
425
|
buff = file_io.read(BLOCKSIZE)
|
|
@@ -507,24 +432,22 @@ def get_file_sha1(filename_or_io):
|
|
|
507
432
|
return digest
|
|
508
433
|
|
|
509
434
|
|
|
510
|
-
def
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def _print(line):
|
|
516
|
-
if print_func:
|
|
517
|
-
line = print_func(line)
|
|
435
|
+
def get_path_permissions_mode(pathname: str):
|
|
436
|
+
"""
|
|
437
|
+
Get the permissions (bits) of a path as an integer.
|
|
438
|
+
"""
|
|
518
439
|
|
|
519
|
-
|
|
440
|
+
mode_octal = oct(stat(pathname).st_mode)
|
|
441
|
+
return int(mode_octal[-3:])
|
|
520
442
|
|
|
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
443
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
444
|
+
def raise_if_bad_type(
|
|
445
|
+
value: Any,
|
|
446
|
+
type_: Type,
|
|
447
|
+
exception: type[Exception],
|
|
448
|
+
message_prefix: str,
|
|
449
|
+
):
|
|
450
|
+
try:
|
|
451
|
+
check_type(value, type_)
|
|
452
|
+
except TypeCheckError as e:
|
|
453
|
+
raise exception(f"{message_prefix}: {e}")
|