pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +18 -3
  4. pyinfra/api/arguments.py +406 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +67 -18
  12. pyinfra/api/facts.py +253 -202
  13. pyinfra/api/host.py +413 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/operation.py +432 -262
  16. pyinfra/api/operations.py +273 -260
  17. pyinfra/api/state.py +302 -248
  18. pyinfra/api/util.py +291 -368
  19. pyinfra/connectors/base.py +173 -0
  20. pyinfra/connectors/chroot.py +212 -0
  21. pyinfra/connectors/docker.py +381 -0
  22. pyinfra/connectors/dockerssh.py +297 -0
  23. pyinfra/connectors/local.py +238 -0
  24. pyinfra/connectors/scp/__init__.py +1 -0
  25. pyinfra/connectors/scp/client.py +204 -0
  26. pyinfra/connectors/ssh.py +670 -0
  27. pyinfra/connectors/ssh_util.py +114 -0
  28. pyinfra/connectors/sshuserclient/client.py +309 -0
  29. pyinfra/connectors/sshuserclient/config.py +102 -0
  30. pyinfra/connectors/terraform.py +135 -0
  31. pyinfra/connectors/util.py +410 -0
  32. pyinfra/connectors/vagrant.py +183 -0
  33. pyinfra/context.py +145 -0
  34. pyinfra/facts/__init__.py +7 -6
  35. pyinfra/facts/apk.py +22 -7
  36. pyinfra/facts/apt.py +117 -60
  37. pyinfra/facts/brew.py +100 -15
  38. pyinfra/facts/bsdinit.py +23 -0
  39. pyinfra/facts/cargo.py +37 -0
  40. pyinfra/facts/choco.py +47 -0
  41. pyinfra/facts/crontab.py +195 -0
  42. pyinfra/facts/deb.py +94 -0
  43. pyinfra/facts/dnf.py +48 -0
  44. pyinfra/facts/docker.py +96 -23
  45. pyinfra/facts/efibootmgr.py +113 -0
  46. pyinfra/facts/files.py +630 -58
  47. pyinfra/facts/flatpak.py +77 -0
  48. pyinfra/facts/freebsd.py +70 -0
  49. pyinfra/facts/gem.py +19 -6
  50. pyinfra/facts/git.py +59 -14
  51. pyinfra/facts/gpg.py +150 -0
  52. pyinfra/facts/hardware.py +313 -167
  53. pyinfra/facts/iptables.py +72 -62
  54. pyinfra/facts/launchd.py +44 -0
  55. pyinfra/facts/lxd.py +17 -4
  56. pyinfra/facts/mysql.py +122 -86
  57. pyinfra/facts/npm.py +17 -9
  58. pyinfra/facts/openrc.py +71 -0
  59. pyinfra/facts/opkg.py +246 -0
  60. pyinfra/facts/pacman.py +50 -7
  61. pyinfra/facts/pip.py +24 -7
  62. pyinfra/facts/pipx.py +82 -0
  63. pyinfra/facts/pkg.py +15 -6
  64. pyinfra/facts/pkgin.py +35 -0
  65. pyinfra/facts/podman.py +54 -0
  66. pyinfra/facts/postgres.py +178 -0
  67. pyinfra/facts/postgresql.py +6 -147
  68. pyinfra/facts/rpm.py +105 -0
  69. pyinfra/facts/runit.py +77 -0
  70. pyinfra/facts/selinux.py +161 -0
  71. pyinfra/facts/server.py +746 -285
  72. pyinfra/facts/snap.py +88 -0
  73. pyinfra/facts/systemd.py +139 -0
  74. pyinfra/facts/sysvinit.py +59 -0
  75. pyinfra/facts/upstart.py +35 -0
  76. pyinfra/facts/util/__init__.py +17 -0
  77. pyinfra/facts/util/databases.py +4 -6
  78. pyinfra/facts/util/packaging.py +37 -6
  79. pyinfra/facts/util/units.py +30 -0
  80. pyinfra/facts/util/win_files.py +99 -0
  81. pyinfra/facts/vzctl.py +20 -13
  82. pyinfra/facts/xbps.py +35 -0
  83. pyinfra/facts/yum.py +34 -40
  84. pyinfra/facts/zfs.py +77 -0
  85. pyinfra/facts/zypper.py +42 -0
  86. pyinfra/local.py +45 -83
  87. pyinfra/operations/__init__.py +12 -0
  88. pyinfra/operations/apk.py +98 -0
  89. pyinfra/operations/apt.py +488 -0
  90. pyinfra/operations/brew.py +231 -0
  91. pyinfra/operations/bsdinit.py +59 -0
  92. pyinfra/operations/cargo.py +45 -0
  93. pyinfra/operations/choco.py +61 -0
  94. pyinfra/operations/crontab.py +191 -0
  95. pyinfra/operations/dnf.py +210 -0
  96. pyinfra/operations/docker.py +446 -0
  97. pyinfra/operations/files.py +1939 -0
  98. pyinfra/operations/flatpak.py +94 -0
  99. pyinfra/operations/freebsd/__init__.py +12 -0
  100. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  101. pyinfra/operations/freebsd/pkg.py +219 -0
  102. pyinfra/operations/freebsd/service.py +116 -0
  103. pyinfra/operations/freebsd/sysrc.py +92 -0
  104. pyinfra/operations/gem.py +47 -0
  105. pyinfra/operations/git.py +419 -0
  106. pyinfra/operations/iptables.py +311 -0
  107. pyinfra/operations/launchd.py +45 -0
  108. pyinfra/operations/lxd.py +68 -0
  109. pyinfra/operations/mysql.py +609 -0
  110. pyinfra/operations/npm.py +57 -0
  111. pyinfra/operations/openrc.py +63 -0
  112. pyinfra/operations/opkg.py +88 -0
  113. pyinfra/operations/pacman.py +81 -0
  114. pyinfra/operations/pip.py +205 -0
  115. pyinfra/operations/pipx.py +102 -0
  116. pyinfra/operations/pkg.py +70 -0
  117. pyinfra/operations/pkgin.py +91 -0
  118. pyinfra/operations/postgres.py +436 -0
  119. pyinfra/operations/postgresql.py +30 -0
  120. pyinfra/operations/puppet.py +40 -0
  121. pyinfra/operations/python.py +72 -0
  122. pyinfra/operations/runit.py +184 -0
  123. pyinfra/operations/selinux.py +189 -0
  124. pyinfra/operations/server.py +1099 -0
  125. pyinfra/operations/snap.py +117 -0
  126. pyinfra/operations/ssh.py +216 -0
  127. pyinfra/operations/systemd.py +149 -0
  128. pyinfra/operations/sysvinit.py +141 -0
  129. pyinfra/operations/upstart.py +68 -0
  130. pyinfra/operations/util/__init__.py +12 -0
  131. pyinfra/operations/util/docker.py +251 -0
  132. pyinfra/operations/util/files.py +247 -0
  133. pyinfra/operations/util/packaging.py +336 -0
  134. pyinfra/operations/util/service.py +46 -0
  135. pyinfra/operations/vzctl.py +137 -0
  136. pyinfra/operations/xbps.py +77 -0
  137. pyinfra/operations/yum.py +210 -0
  138. pyinfra/operations/zfs.py +175 -0
  139. pyinfra/operations/zypper.py +192 -0
  140. pyinfra/progress.py +44 -32
  141. pyinfra/py.typed +0 -0
  142. pyinfra/version.py +9 -1
  143. pyinfra-3.5.1.dist-info/METADATA +141 -0
  144. pyinfra-3.5.1.dist-info/RECORD +159 -0
  145. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
  146. pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
  147. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
  148. pyinfra_cli/__init__.py +1 -0
  149. pyinfra_cli/cli.py +780 -0
  150. pyinfra_cli/commands.py +66 -0
  151. pyinfra_cli/exceptions.py +155 -65
  152. pyinfra_cli/inventory.py +233 -89
  153. pyinfra_cli/log.py +39 -43
  154. pyinfra_cli/main.py +26 -495
  155. pyinfra_cli/prints.py +215 -156
  156. pyinfra_cli/util.py +172 -105
  157. pyinfra_cli/virtualenv.py +25 -20
  158. pyinfra/api/connectors/__init__.py +0 -21
  159. pyinfra/api/connectors/ansible.py +0 -99
  160. pyinfra/api/connectors/docker.py +0 -178
  161. pyinfra/api/connectors/local.py +0 -169
  162. pyinfra/api/connectors/ssh.py +0 -402
  163. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  164. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  165. pyinfra/api/connectors/util.py +0 -63
  166. pyinfra/api/connectors/vagrant.py +0 -155
  167. pyinfra/facts/init.py +0 -176
  168. pyinfra/facts/util/files.py +0 -102
  169. pyinfra/hook.py +0 -41
  170. pyinfra/modules/__init__.py +0 -11
  171. pyinfra/modules/apk.py +0 -64
  172. pyinfra/modules/apt.py +0 -272
  173. pyinfra/modules/brew.py +0 -122
  174. pyinfra/modules/files.py +0 -711
  175. pyinfra/modules/gem.py +0 -30
  176. pyinfra/modules/git.py +0 -115
  177. pyinfra/modules/init.py +0 -344
  178. pyinfra/modules/iptables.py +0 -271
  179. pyinfra/modules/lxd.py +0 -45
  180. pyinfra/modules/mysql.py +0 -347
  181. pyinfra/modules/npm.py +0 -47
  182. pyinfra/modules/pacman.py +0 -60
  183. pyinfra/modules/pip.py +0 -99
  184. pyinfra/modules/pkg.py +0 -43
  185. pyinfra/modules/postgresql.py +0 -245
  186. pyinfra/modules/puppet.py +0 -20
  187. pyinfra/modules/python.py +0 -37
  188. pyinfra/modules/server.py +0 -524
  189. pyinfra/modules/ssh.py +0 -150
  190. pyinfra/modules/util/files.py +0 -52
  191. pyinfra/modules/util/packaging.py +0 -118
  192. pyinfra/modules/vzctl.py +0 -133
  193. pyinfra/modules/yum.py +0 -171
  194. pyinfra/pseudo_modules.py +0 -64
  195. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  196. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  197. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  198. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  199. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  200. pyinfra_cli/__main__.py +0 -40
  201. pyinfra_cli/config.py +0 -92
  202. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  203. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/api/util.py CHANGED
@@ -1,70 +1,99 @@
1
- import re
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 socket import (
8
- error as socket_error,
9
- timeout as timeout_error,
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
- from .exceptions import PyinfraError
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
- def ensure_host_list(hosts, inventory):
39
- if hosts is None:
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
- # If passed a string, treat as group name and get any hosts from inventory
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
- return hosts
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
- def memoize(func):
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 = '{0}{1}'.format(args, kwargs)
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 get_caller_frameinfo(frame_offset=0):
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 extract_callable_datas(datas):
84
- for data in datas:
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
- yield data
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
- class FallbackDict(object):
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
- override_datas = None
129
+ for stack_item in stack_items[i:]:
130
+ frame = getframeinfo(stack_item[0])
99
131
 
100
- def __init__(self, *datas):
101
- datas = list(datas)
132
+ if frame.filename.startswith(PYINFRA_INSTALL_DIR):
133
+ continue
102
134
 
103
- # Inject an empty override data so we can assign during deploy
104
- self.__dict__['override_datas'] = {}
105
- datas.insert(0, self.override_datas)
135
+ line_numbers.append(frame.lineno)
106
136
 
107
- self.__dict__['datas'] = tuple(datas)
137
+ del stack_items
108
138
 
109
- def __getattr__(self, key):
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
- def __str__(self):
118
- return str(self.datas)
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
- def dict(self):
121
- out = {}
152
+ if cache_key and cache_key in TEMPLATES:
153
+ return TEMPLATES[cache_key]
122
154
 
123
- # Copy and reverse data objects (such that the first items override
124
- # the last, matching __getattr__ output).
125
- datas = list(self.datas)
126
- datas.reverse()
155
+ with file_data as file_io:
156
+ template_string = file_io.read()
127
157
 
128
- for data in extract_callable_datas(datas):
129
- out.update(data)
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
- return out
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
- meta_kwargs = state.deploy_kwargs or {}
171
+ def sha1_hash(string: str) -> str:
172
+ """
173
+ Return the SHA1 of the input string.
174
+ """
140
175
 
141
- def get_kwarg(key, default=None):
142
- return kwargs.pop(key, meta_kwargs.get(key, default))
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
- hosts = get_kwarg('hosts')
149
- hosts = ensure_host_list(hosts, inventory=state.inventory)
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
- 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))
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
- items.append(item)
214
-
215
- return items
216
-
217
-
218
- def get_template(filename_or_string, is_string=False):
219
- '''
220
- Gets a jinja2 ``Template`` object for the input filename or string, with caching
221
- based on the filename of the template, or the SHA1 of the input string.
222
- '''
223
-
224
- # Cache against string sha or just the filename
225
- cache_key = sha1_hash(filename_or_string) if is_string else filename_or_string
226
-
227
- if cache_key in TEMPLATES:
228
- return TEMPLATES[cache_key]
229
-
230
- if is_string:
231
- # Set the input string as our template
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]
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
- hasher = sha1()
258
- hasher.update(string.encode())
259
- return hasher.hexdigest()
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
- def format_exception(e):
263
- return '{0}{1}'.format(e.__class__.__name__, e.args)
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('{0}{1}'.format(
269
- host.print_prefix,
270
- click.style('Command timed out after {0}s'.format(
271
- timeout,
272
- ), 'red'),
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('{0}{1}'.format(
277
- host.print_prefix,
278
- click.style('Command socket/SSH error: {0}'.format(
279
- format_exception(e)), 'red',
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('{0}{1}'.format(
285
- host.print_prefix,
286
- click.style('Command IO error: {0}'.format(
287
- format_exception(e)), 'red',
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 = ''.join([make_hash(e) for e in obj])
294
+ hash_string = "".join([make_hash(e) for e in obj])
405
295
 
406
296
  elif isinstance(obj, dict):
407
- hash_string = ''.join(
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
- '_PYINFRA_CONSTANT' if obj in (True, False, None)
417
- # Plain strings
418
- else obj if isinstance(obj, str)
419
- # Objects with __name__s
420
- else obj.__name__ if hasattr(obj, '__name__')
421
- # Objects with names
422
- else obj.name if hasattr(obj, 'name')
423
- # Repr anything else
424
- else repr(obj)
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(object):
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
- close = False
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, 'read')
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('Invalid filename or IO object: {0}'.format(
446
- filename_or_io,
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
- # If we have a read attribute, just use the object as-is
453
- if hasattr(self.filename_or_io, 'read'):
454
- file_io = self.filename_or_io
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 = open(self.filename_or_io, 'rb')
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.close:
470
- self.file_io.close()
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 get_file_sha1(filename_or_io):
481
- '''
482
- Calculates the SHA1 of a file or file object using a buffer to handle larger files.
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('utf-8')
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 read_buffer(type_, io, output_queue, print_output=False, print_func=None):
511
- '''
512
- Reads a file-like buffer object into lines and optionally prints the output.
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
- print(line)
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
- line = line.strip()
527
- output_queue.put((type_, line))
528
-
529
- if print_output:
530
- _print(line)
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}")