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.
Files changed (204) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +19 -3
  4. pyinfra/api/arguments.py +413 -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 +73 -18
  12. pyinfra/api/facts.py +267 -200
  13. pyinfra/api/host.py +416 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/metadata.py +69 -0
  16. pyinfra/api/operation.py +432 -262
  17. pyinfra/api/operations.py +273 -260
  18. pyinfra/api/state.py +302 -248
  19. pyinfra/api/util.py +309 -369
  20. pyinfra/connectors/base.py +173 -0
  21. pyinfra/connectors/chroot.py +212 -0
  22. pyinfra/connectors/docker.py +405 -0
  23. pyinfra/connectors/dockerssh.py +297 -0
  24. pyinfra/connectors/local.py +238 -0
  25. pyinfra/connectors/scp/__init__.py +1 -0
  26. pyinfra/connectors/scp/client.py +204 -0
  27. pyinfra/connectors/ssh.py +727 -0
  28. pyinfra/connectors/ssh_util.py +114 -0
  29. pyinfra/connectors/sshuserclient/client.py +309 -0
  30. pyinfra/connectors/sshuserclient/config.py +102 -0
  31. pyinfra/connectors/terraform.py +135 -0
  32. pyinfra/connectors/util.py +417 -0
  33. pyinfra/connectors/vagrant.py +183 -0
  34. pyinfra/context.py +145 -0
  35. pyinfra/facts/__init__.py +7 -6
  36. pyinfra/facts/apk.py +22 -7
  37. pyinfra/facts/apt.py +117 -60
  38. pyinfra/facts/brew.py +100 -15
  39. pyinfra/facts/bsdinit.py +23 -0
  40. pyinfra/facts/cargo.py +37 -0
  41. pyinfra/facts/choco.py +47 -0
  42. pyinfra/facts/crontab.py +195 -0
  43. pyinfra/facts/deb.py +94 -0
  44. pyinfra/facts/dnf.py +48 -0
  45. pyinfra/facts/docker.py +96 -23
  46. pyinfra/facts/efibootmgr.py +113 -0
  47. pyinfra/facts/files.py +629 -58
  48. pyinfra/facts/flatpak.py +77 -0
  49. pyinfra/facts/freebsd.py +70 -0
  50. pyinfra/facts/gem.py +19 -6
  51. pyinfra/facts/git.py +59 -14
  52. pyinfra/facts/gpg.py +150 -0
  53. pyinfra/facts/hardware.py +313 -167
  54. pyinfra/facts/iptables.py +72 -62
  55. pyinfra/facts/launchd.py +44 -0
  56. pyinfra/facts/lxd.py +17 -4
  57. pyinfra/facts/mysql.py +122 -86
  58. pyinfra/facts/npm.py +17 -9
  59. pyinfra/facts/openrc.py +71 -0
  60. pyinfra/facts/opkg.py +246 -0
  61. pyinfra/facts/pacman.py +50 -7
  62. pyinfra/facts/pip.py +24 -7
  63. pyinfra/facts/pipx.py +82 -0
  64. pyinfra/facts/pkg.py +15 -6
  65. pyinfra/facts/pkgin.py +35 -0
  66. pyinfra/facts/podman.py +54 -0
  67. pyinfra/facts/postgres.py +178 -0
  68. pyinfra/facts/postgresql.py +6 -147
  69. pyinfra/facts/rpm.py +105 -0
  70. pyinfra/facts/runit.py +77 -0
  71. pyinfra/facts/selinux.py +161 -0
  72. pyinfra/facts/server.py +762 -285
  73. pyinfra/facts/snap.py +88 -0
  74. pyinfra/facts/systemd.py +139 -0
  75. pyinfra/facts/sysvinit.py +59 -0
  76. pyinfra/facts/upstart.py +35 -0
  77. pyinfra/facts/util/__init__.py +17 -0
  78. pyinfra/facts/util/databases.py +4 -6
  79. pyinfra/facts/util/packaging.py +37 -6
  80. pyinfra/facts/util/units.py +30 -0
  81. pyinfra/facts/util/win_files.py +99 -0
  82. pyinfra/facts/vzctl.py +20 -13
  83. pyinfra/facts/xbps.py +35 -0
  84. pyinfra/facts/yum.py +34 -40
  85. pyinfra/facts/zfs.py +77 -0
  86. pyinfra/facts/zypper.py +42 -0
  87. pyinfra/local.py +45 -83
  88. pyinfra/operations/__init__.py +12 -0
  89. pyinfra/operations/apk.py +99 -0
  90. pyinfra/operations/apt.py +496 -0
  91. pyinfra/operations/brew.py +232 -0
  92. pyinfra/operations/bsdinit.py +59 -0
  93. pyinfra/operations/cargo.py +45 -0
  94. pyinfra/operations/choco.py +61 -0
  95. pyinfra/operations/crontab.py +194 -0
  96. pyinfra/operations/dnf.py +213 -0
  97. pyinfra/operations/docker.py +492 -0
  98. pyinfra/operations/files.py +2014 -0
  99. pyinfra/operations/flatpak.py +95 -0
  100. pyinfra/operations/freebsd/__init__.py +12 -0
  101. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  102. pyinfra/operations/freebsd/pkg.py +219 -0
  103. pyinfra/operations/freebsd/service.py +116 -0
  104. pyinfra/operations/freebsd/sysrc.py +92 -0
  105. pyinfra/operations/gem.py +48 -0
  106. pyinfra/operations/git.py +420 -0
  107. pyinfra/operations/iptables.py +312 -0
  108. pyinfra/operations/launchd.py +45 -0
  109. pyinfra/operations/lxd.py +69 -0
  110. pyinfra/operations/mysql.py +610 -0
  111. pyinfra/operations/npm.py +57 -0
  112. pyinfra/operations/openrc.py +63 -0
  113. pyinfra/operations/opkg.py +89 -0
  114. pyinfra/operations/pacman.py +82 -0
  115. pyinfra/operations/pip.py +206 -0
  116. pyinfra/operations/pipx.py +103 -0
  117. pyinfra/operations/pkg.py +71 -0
  118. pyinfra/operations/pkgin.py +92 -0
  119. pyinfra/operations/postgres.py +437 -0
  120. pyinfra/operations/postgresql.py +30 -0
  121. pyinfra/operations/puppet.py +41 -0
  122. pyinfra/operations/python.py +73 -0
  123. pyinfra/operations/runit.py +184 -0
  124. pyinfra/operations/selinux.py +190 -0
  125. pyinfra/operations/server.py +1100 -0
  126. pyinfra/operations/snap.py +118 -0
  127. pyinfra/operations/ssh.py +217 -0
  128. pyinfra/operations/systemd.py +150 -0
  129. pyinfra/operations/sysvinit.py +142 -0
  130. pyinfra/operations/upstart.py +68 -0
  131. pyinfra/operations/util/__init__.py +12 -0
  132. pyinfra/operations/util/docker.py +407 -0
  133. pyinfra/operations/util/files.py +247 -0
  134. pyinfra/operations/util/packaging.py +338 -0
  135. pyinfra/operations/util/service.py +46 -0
  136. pyinfra/operations/vzctl.py +137 -0
  137. pyinfra/operations/xbps.py +78 -0
  138. pyinfra/operations/yum.py +213 -0
  139. pyinfra/operations/zfs.py +176 -0
  140. pyinfra/operations/zypper.py +193 -0
  141. pyinfra/progress.py +44 -32
  142. pyinfra/py.typed +0 -0
  143. pyinfra/version.py +9 -1
  144. pyinfra-3.6.dist-info/METADATA +142 -0
  145. pyinfra-3.6.dist-info/RECORD +160 -0
  146. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
  147. pyinfra-3.6.dist-info/entry_points.txt +12 -0
  148. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
  149. pyinfra_cli/__init__.py +1 -0
  150. pyinfra_cli/cli.py +793 -0
  151. pyinfra_cli/commands.py +66 -0
  152. pyinfra_cli/exceptions.py +155 -65
  153. pyinfra_cli/inventory.py +233 -89
  154. pyinfra_cli/log.py +39 -43
  155. pyinfra_cli/main.py +26 -495
  156. pyinfra_cli/prints.py +215 -156
  157. pyinfra_cli/util.py +172 -105
  158. pyinfra_cli/virtualenv.py +25 -20
  159. pyinfra/api/connectors/__init__.py +0 -21
  160. pyinfra/api/connectors/ansible.py +0 -99
  161. pyinfra/api/connectors/docker.py +0 -178
  162. pyinfra/api/connectors/local.py +0 -169
  163. pyinfra/api/connectors/ssh.py +0 -402
  164. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  165. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  166. pyinfra/api/connectors/util.py +0 -63
  167. pyinfra/api/connectors/vagrant.py +0 -155
  168. pyinfra/facts/init.py +0 -176
  169. pyinfra/facts/util/files.py +0 -102
  170. pyinfra/hook.py +0 -41
  171. pyinfra/modules/__init__.py +0 -11
  172. pyinfra/modules/apk.py +0 -64
  173. pyinfra/modules/apt.py +0 -272
  174. pyinfra/modules/brew.py +0 -122
  175. pyinfra/modules/files.py +0 -711
  176. pyinfra/modules/gem.py +0 -30
  177. pyinfra/modules/git.py +0 -115
  178. pyinfra/modules/init.py +0 -344
  179. pyinfra/modules/iptables.py +0 -271
  180. pyinfra/modules/lxd.py +0 -45
  181. pyinfra/modules/mysql.py +0 -347
  182. pyinfra/modules/npm.py +0 -47
  183. pyinfra/modules/pacman.py +0 -60
  184. pyinfra/modules/pip.py +0 -99
  185. pyinfra/modules/pkg.py +0 -43
  186. pyinfra/modules/postgresql.py +0 -245
  187. pyinfra/modules/puppet.py +0 -20
  188. pyinfra/modules/python.py +0 -37
  189. pyinfra/modules/server.py +0 -524
  190. pyinfra/modules/ssh.py +0 -150
  191. pyinfra/modules/util/files.py +0 -52
  192. pyinfra/modules/util/packaging.py +0 -118
  193. pyinfra/modules/vzctl.py +0 -133
  194. pyinfra/modules/yum.py +0 -171
  195. pyinfra/pseudo_modules.py +0 -64
  196. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  197. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  198. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  199. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  200. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  201. pyinfra_cli/__main__.py +0 -40
  202. pyinfra_cli/config.py +0 -92
  203. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  204. /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, 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
- 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[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
- 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 ""
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 = '{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,294 @@ 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()
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
- self.__dict__['datas'] = tuple(datas)
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
- def __getattr__(self, key):
110
- for data in extract_callable_datas(self.datas):
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
- def __setattr__(self, key, value):
115
- self.override_datas[key] = value
126
+ if pyinfra.is_cli:
127
+ line_numbers.append(state.current_op_file_number)
116
128
 
117
- def __str__(self):
118
- return str(self.datas)
129
+ for stack_item in stack_items[i:]:
130
+ frame = getframeinfo(stack_item[0])
119
131
 
120
- def dict(self):
121
- out = {}
132
+ if frame.filename.startswith(PYINFRA_INSTALL_DIR):
133
+ continue
122
134
 
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()
135
+ line_numbers.append(frame.lineno)
127
136
 
128
- for data in extract_callable_datas(datas):
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 items
139
+ return line_numbers
216
140
 
217
141
 
218
- def get_template(filename_or_string, is_string=False):
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
- # Cache against string sha or just the filename
225
- cache_key = sha1_hash(filename_or_string) if is_string else filename_or_string
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
- 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]
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
- def underscore(name):
244
- '''
245
- Transform CamelCase -> snake_case.
246
- '''
168
+ if cache_key:
169
+ TEMPLATES[cache_key] = template
247
170
 
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()
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 '{0}{1}'.format(e.__class__.__name__, e.args)
184
+ def format_exception(e: Exception) -> str:
185
+ return f"{e.__class__.__name__}{e.args}"
264
186
 
265
187
 
266
- def log_host_command_error(host, e, timeout=0):
267
- if isinstance(e, timeout_error):
268
- logger.error('{0}{1}'.format(
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('Command timed out after {0}s'.format(
271
- timeout,
272
- ), 'red'),
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('{0}{1}'.format(
277
- host.print_prefix,
278
- click.style('Command socket/SSH error: {0}'.format(
279
- format_exception(e)), 'red',
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('{0}{1}'.format(
285
- host.print_prefix,
286
- click.style('Command IO error: {0}'.format(
287
- format_exception(e)), 'red',
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 = ''.join([make_hash(e) for e in obj])
311
+ hash_string = "".join([make_hash(e) for e in obj])
405
312
 
406
313
  elif isinstance(obj, dict):
407
- hash_string = ''.join(
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
- '_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)
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(object):
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
- close = False
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, 'read')
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('Invalid filename or IO object: {0}'.format(
446
- filename_or_io,
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
- # 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
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 = 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
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.close:
470
- self.file_io.close()
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 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
- '''
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('utf-8')
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 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)
452
+ def get_path_permissions_mode(pathname: str):
453
+ """
454
+ Get the permissions (bits) of a path as an integer.
455
+ """
518
456
 
519
- print(line)
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
- line = line.strip()
527
- output_queue.put((type_, line))
528
-
529
- if print_output:
530
- _print(line)
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}")