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/host.py CHANGED
@@ -1,55 +1,197 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from copy import copy
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Callable,
9
+ Generator,
10
+ Optional,
11
+ Type,
12
+ TypeVar,
13
+ Union,
14
+ cast,
15
+ overload,
16
+ )
17
+ from uuid import uuid4
18
+
1
19
  import click
20
+ from typing_extensions import Unpack, override
2
21
 
3
- from .connectors import EXECUTION_CONNECTORS
4
- from .exceptions import PyinfraError
5
- from .facts import get_fact, is_fact
22
+ from pyinfra import logger
23
+ from pyinfra.connectors.base import BaseConnector
24
+ from pyinfra.connectors.util import CommandOutput, remove_any_sudo_askpass_file
6
25
 
26
+ from .connectors import get_execution_connector
27
+ from .exceptions import ConnectError
28
+ from .facts import FactBase, ShortFactBase, get_fact
29
+ from .util import memoize, sha1_hash
7
30
 
8
- class HostFacts(object):
9
- def __init__(self, inventory, host):
10
- self.inventory = inventory
11
- self.host = host
31
+ if TYPE_CHECKING:
32
+ from pyinfra.api.arguments import AllArguments
33
+ from pyinfra.api.inventory import Inventory
34
+ from pyinfra.api.state import State
35
+
36
+
37
+ def extract_callable_datas(
38
+ datas: list[Union[Callable[..., Any], Any]],
39
+ ) -> Generator[Any, Any, Any]:
40
+ for data in datas:
41
+ # Support for dynamic data, ie @deploy wrapped data defaults where
42
+ # the data is stored on the state temporarily.
43
+ if callable(data):
44
+ data = data()
45
+ yield data
46
+
47
+
48
+ class HostData:
49
+ """
50
+ Combines multiple AttrData's to search for attributes.
51
+ """
52
+
53
+ override_datas: dict[str, Any]
54
+
55
+ def __init__(self, host: "Host", *datas):
56
+ self.__dict__["host"] = host
57
+
58
+ parsed_datas = list(datas)
12
59
 
13
- def __getattr__(self, key):
14
- if not is_fact(key):
15
- raise AttributeError('No such fact: {0}'.format(key))
60
+ # Inject an empty override data so we can assign during deploy
61
+ self.__dict__["override_datas"] = {}
62
+ parsed_datas.insert(0, self.override_datas)
16
63
 
17
- # Ensure this host is connected
18
- connection = self.host.connect(self.inventory.state, for_fact=key)
64
+ self.__dict__["datas"] = tuple(parsed_datas)
19
65
 
20
- # If we can't connect - fail immediately as we specifically need this
21
- # fact for this host and without it we cannot satisfy the deploy.
22
- if not connection:
23
- raise PyinfraError('Could not connect to {0} for fact {1}!'.format(
24
- self.host, key,
25
- ))
66
+ def __getattr__(self, key: str):
67
+ for data in extract_callable_datas(self.datas):
68
+ try:
69
+ # Take a shallow copy of the object here, we don't want modifications
70
+ # to host.data.<X> to stick, instead setting host.data.<Y> = is the
71
+ # correct way to achieve this (see __setattr__).
72
+ return copy(data[key])
73
+ except KeyError:
74
+ pass
26
75
 
27
- return get_fact(self.inventory.state, self.host, key)
76
+ raise AttributeError(f"Host `{self.host}` has no data `{key}`")
28
77
 
78
+ @override
79
+ def __setattr__(self, key: str, value: Any):
80
+ self.override_datas[key] = value
29
81
 
30
- class Host(object):
31
- '''
82
+ @override
83
+ def __str__(self):
84
+ return str(self.datas)
85
+
86
+ def get(self, key: str, default=None):
87
+ return getattr(self, key, default)
88
+
89
+ def dict(self):
90
+ out = {}
91
+
92
+ # Copy and reverse data objects (such that the first items override
93
+ # the last, matching __getattr__ output).
94
+ datas = list(self.datas)
95
+ datas.reverse()
96
+
97
+ for data in extract_callable_datas(datas):
98
+ out.update(data)
99
+
100
+ return out
101
+
102
+
103
+ class Host:
104
+ """
32
105
  Represents a target host. Thin class that links up to facts and host/group
33
106
  data.
34
- '''
107
+ """
108
+
109
+ state: "State"
110
+ connector_cls: type[BaseConnector]
111
+ connector: BaseConnector
112
+ connected: bool = False
113
+
114
+ # Current context inside an @operation function (op gen stage)
115
+ in_op: bool = False
116
+ in_callback_op: bool = False
117
+ current_op_hash: Optional[str] = None
118
+ current_op_global_arguments: Optional["AllArguments"] = None
119
+
120
+ # Current context inside a @deploy function which become part of the op data
121
+ in_deploy: bool = False
122
+ current_deploy_name: Optional[str] = None
123
+ current_deploy_kwargs = None
35
124
 
36
- connection = None
125
+ # @deploy decorator data is a bit different - we need to handle the case
126
+ # where we're evaluating an operation at runtime (current_op_) but also
127
+ # when ordering operations (current_) outside of an operation context.
128
+ current_op_deploy_data: Optional[dict[str, Any]] = None
129
+ current_deploy_data: Optional[dict[str, Any]] = None
130
+
131
+ # Current context during operation execution
132
+ executing_op_hash: Optional[str] = None
133
+ nested_executing_op_hash: Optional[str] = None
134
+
135
+ loop_position: list[int]
136
+
137
+ # Arbitrary data dictionary connectors may use
138
+ connector_data: dict[str, Any]
139
+
140
+ def loop(self, iterable):
141
+ self.loop_position.append(0)
142
+ for i, item in enumerate(iterable):
143
+ self.loop_position[-1] = i
144
+ yield item
145
+ self.loop_position.pop()
37
146
 
38
147
  def __init__(
39
- self, name, inventory, groups, data,
40
- executor=EXECUTION_CONNECTORS['ssh'],
148
+ self,
149
+ name: str,
150
+ inventory: "Inventory",
151
+ groups,
152
+ connector_cls=None,
41
153
  ):
154
+ if connector_cls is None:
155
+ connector_cls = get_execution_connector("ssh")
42
156
  self.inventory = inventory
43
157
  self.groups = groups
44
- self.data = data
45
- self.executor = executor
158
+ self.connector_cls = connector_cls
46
159
  self.name = name
47
160
 
48
- # Attach the fact proxy
49
- self.fact = HostFacts(inventory, self)
161
+ self.loop_position = []
162
+
163
+ self.connector_data = {}
164
+
165
+ # Append only list of operation hashes as called on this host, used to
166
+ # generate a DAG to create the final operation order.
167
+ self.op_hash_order: list[str] = []
168
+
169
+ # Create the (waterfall data: override, host, group, global)
170
+ self.data = HostData(
171
+ self,
172
+ lambda: inventory.get_override_data(),
173
+ lambda: inventory.get_host_data(name),
174
+ lambda: inventory.get_groups_data(groups),
175
+ lambda: inventory.get_data(),
176
+ # @deploy function data are default values, so come last
177
+ self.get_deploy_data,
178
+ )
179
+
180
+ def init(self, state: "State") -> None:
181
+ self.state = state
182
+ self.connector = self.connector_cls(state, self)
183
+
184
+ longest_name_len = max([len(host.name) for host in self.inventory])
185
+ padding_diff = longest_name_len - len(self.name)
186
+ self.print_prefix_padding = "".join(" " for _ in range(0, padding_diff))
50
187
 
188
+ @override
189
+ def __str__(self):
190
+ return "{0}".format(self.name)
191
+
192
+ @override
51
193
  def __repr__(self):
52
- return self.name
194
+ return "Host({0})".format(self.name)
53
195
 
54
196
  @property
55
197
  def host_data(self):
@@ -60,38 +202,259 @@ class Host(object):
60
202
  return self.inventory.get_groups_data(self.groups)
61
203
 
62
204
  @property
63
- def print_prefix(self):
64
- return '{0}[{1}] '.format(
65
- click.style(''), # reset
205
+ def print_prefix(self) -> str:
206
+ if self.nested_executing_op_hash:
207
+ return "{0}[{1}] {2}{3} ".format(
208
+ click.style(""), # reset
209
+ click.style(self.name, bold=True),
210
+ click.style("nested", "blue"),
211
+ self.print_prefix_padding,
212
+ )
213
+
214
+ return "{0}[{1}]{2} ".format(
215
+ click.style(""), # reset
66
216
  click.style(self.name, bold=True),
217
+ self.print_prefix_padding,
67
218
  )
68
219
 
69
- def style_print_prefix(self, *args, **kwargs):
70
- return '{0}[{1}] '.format(
71
- click.style(''), # reset
220
+ def style_print_prefix(self, *args, **kwargs) -> str:
221
+ return "{0}[{1}]{2} ".format(
222
+ click.style(""), # reset
72
223
  click.style(self.name, *args, **kwargs),
224
+ self.print_prefix_padding,
225
+ )
226
+
227
+ def log(self, message: str, log_func: Callable[[str], Any] = logger.info) -> None:
228
+ log_func(f"{self.print_prefix}{message}")
229
+
230
+ def log_styled(
231
+ self, message: str, log_func: Callable[[str], Any] = logger.info, **kwargs
232
+ ) -> None:
233
+ message_styled = click.style(message, **kwargs)
234
+ self.log(message_styled, log_func=log_func)
235
+
236
+ def get_deploy_data(self):
237
+ return self.current_op_deploy_data or self.current_deploy_data or {}
238
+
239
+ def noop(self, description: str) -> None:
240
+ """
241
+ Log a description for a noop operation.
242
+ """
243
+
244
+ handler = logger.info if self.state.print_noop_info else logger.debug
245
+ handler("{0}noop: {1}".format(self.print_prefix, description))
246
+
247
+ def when(self, condition: Callable[[], bool]):
248
+ return self.deploy(
249
+ "",
250
+ cast("AllArguments", {"_if": [condition]}),
251
+ {},
252
+ in_deploy=False,
73
253
  )
74
254
 
255
+ def arguments(self, **arguments: Unpack["AllArguments"]):
256
+ return self.deploy("", arguments, {}, in_deploy=False)
257
+
258
+ @contextmanager
259
+ def deploy(
260
+ self,
261
+ name: str,
262
+ kwargs: Optional["AllArguments"],
263
+ data: Optional[dict],
264
+ in_deploy: bool = True,
265
+ ):
266
+ """
267
+ Wraps a group of operations as a deploy, this should not be used
268
+ directly, instead use ``pyinfra.api.deploy.deploy``.
269
+ """
270
+
271
+ # Handle nested deploy names
272
+ if self.current_deploy_name:
273
+ name = "{0} | {1}".format(self.current_deploy_name, name)
274
+
275
+ # Store the previous values
276
+ old_in_deploy = self.in_deploy
277
+ old_deploy_name = self.current_deploy_name
278
+ old_deploy_kwargs = self.current_deploy_kwargs
279
+ old_deploy_data = self.current_deploy_data
280
+ self.in_deploy = in_deploy
281
+
282
+ # Combine any old _ifs with the new ones
283
+ if old_deploy_kwargs and kwargs:
284
+ old_ifs = old_deploy_kwargs["_if"]
285
+ new_ifs = kwargs["_if"]
286
+ if old_ifs and new_ifs:
287
+ kwargs["_if"] = old_ifs + new_ifs
288
+
289
+ # Set the new values
290
+ self.current_deploy_name = name
291
+ self.current_deploy_kwargs = kwargs
292
+ self.current_deploy_data = data
293
+ logger.debug(
294
+ "Starting deploy %s (args=%r, data=%r)",
295
+ name,
296
+ kwargs,
297
+ data,
298
+ )
299
+
300
+ yield
301
+
302
+ # Restore the previous values
303
+ self.in_deploy = old_in_deploy
304
+ self.current_deploy_name = old_deploy_name
305
+ self.current_deploy_kwargs = old_deploy_kwargs
306
+ self.current_deploy_data = old_deploy_data
307
+
308
+ logger.debug(
309
+ "Reset deploy to %s (args=%r, data=%r)",
310
+ old_deploy_name,
311
+ old_deploy_kwargs,
312
+ old_deploy_data,
313
+ )
314
+
315
+ @memoize
316
+ def _get_temp_directory(self):
317
+ temp_directory = self.state.config.TEMP_DIR
318
+
319
+ if temp_directory is None:
320
+ # Unfortunate, but very hard to avoid, circular dependency, this method is memoized so
321
+ # performance isn't a concern.
322
+ from pyinfra.facts.server import TmpDir
323
+
324
+ temp_directory = self.get_fact(TmpDir)
325
+
326
+ if not temp_directory:
327
+ temp_directory = self.state.config.DEFAULT_TEMP_DIR
328
+
329
+ return temp_directory
330
+
331
+ def get_temp_filename(
332
+ self,
333
+ hash_key: Optional[str] = None,
334
+ hash_filename: bool = True,
335
+ *,
336
+ temp_directory: Optional[str] = None,
337
+ ):
338
+ """
339
+ Generate a temporary filename for this deploy.
340
+ """
341
+
342
+ temp_directory = temp_directory or self._get_temp_directory()
343
+
344
+ if not hash_key:
345
+ hash_key = str(uuid4())
346
+
347
+ if hash_filename:
348
+ hash_key = sha1_hash(hash_key)
349
+
350
+ return "{0}/pyinfra-{1}".format(temp_directory, hash_key)
351
+
352
+ # Host facts
353
+ #
354
+
355
+ T = TypeVar("T")
356
+
357
+ @overload
358
+ def get_fact(self, name_or_cls: Type[FactBase[T]], *args, **kwargs) -> T: ...
359
+
360
+ @overload
361
+ def get_fact(self, name_or_cls: Type[ShortFactBase[T]], *args, **kwargs) -> T: ...
362
+
363
+ def get_fact(self, name_or_cls, *args, **kwargs):
364
+ """
365
+ Get a fact for this host, reading from the cache if present.
366
+ """
367
+ return get_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
368
+
75
369
  # Connector proxy
76
370
  #
77
371
 
78
- def connect(self, state, *args, **kwargs):
79
- if not self.connection:
80
- self.connection = self.executor.connect(state, self, *args, **kwargs)
372
+ def _check_state(self) -> None:
373
+ if not self.state:
374
+ raise TypeError("Cannot call this function with no state!")
375
+
376
+ def connect(self, reason=None, show_errors: bool = True, raise_exceptions: bool = False):
377
+ """
378
+ Connect to the host using it's configured connector.
379
+ """
380
+
381
+ self._check_state()
382
+ if not self.connected:
383
+ self.state.trigger_callbacks("host_before_connect", self)
81
384
 
82
- return self.connection
385
+ try:
386
+ self.connector.connect()
387
+ except ConnectError as e:
388
+ if show_errors:
389
+ log_message = "{0}{1}".format(
390
+ self.print_prefix,
391
+ click.style(e.args[0], "red"),
392
+ )
393
+ logger.error(log_message)
83
394
 
84
- def disconnect(self, state):
85
- # Disconnect is an optional function for executors if needed
86
- disconnect_func = getattr(self.executor, 'disconnect', None)
395
+ self.state.trigger_callbacks("host_connect_error", self, e)
396
+
397
+ if raise_exceptions:
398
+ raise
399
+ else:
400
+ log_message = "{0}{1}".format(
401
+ self.print_prefix,
402
+ click.style("Connected", "green"),
403
+ )
404
+ if reason:
405
+ log_message = "{0}{1}".format(
406
+ log_message,
407
+ " ({0})".format(reason),
408
+ )
409
+
410
+ logger.info(log_message)
411
+ self.state.trigger_callbacks("host_connect", self)
412
+ self.connected = True
413
+
414
+ def disconnect(self) -> None:
415
+ """
416
+ Disconnect from the host using it's configured connector.
417
+ """
418
+ self._check_state()
419
+
420
+ # Disconnect is an optional function for connectors if needed
421
+ disconnect_func = getattr(self.connector, "disconnect", None)
87
422
  if disconnect_func:
88
- return disconnect_func(state, self)
423
+ disconnect_func()
424
+
425
+ # TODO: consider whether this should be here!
426
+ remove_any_sudo_askpass_file(self)
427
+
428
+ self.state.trigger_callbacks("host_disconnect", self)
429
+ self.connected = False
430
+
431
+ def run_shell_command(self, *args, **kwargs) -> tuple[bool, CommandOutput]:
432
+ """
433
+ Low level method to execute a shell command on the host via it's configured connector.
434
+ """
435
+ self._check_state()
436
+ return self.connector.run_shell_command(*args, **kwargs)
437
+
438
+ def put_file(self, *args, **kwargs) -> bool:
439
+ """
440
+ Low level method to upload a file to the host via it's configured connector.
441
+ """
442
+ self._check_state()
443
+ return self.connector.put_file(*args, **kwargs)
444
+
445
+ def get_file(self, *args, **kwargs) -> bool:
446
+ """
447
+ Low level method to download a file from the host via it's configured connector.
448
+ """
449
+ self._check_state()
450
+ return self.connector.get_file(*args, **kwargs)
89
451
 
90
- def run_shell_command(self, state, *args, **kwargs):
91
- return self.executor.run_shell_command(state, self, *args, **kwargs)
452
+ # Rsync - optional connector specific ability
92
453
 
93
- def put_file(self, state, *args, **kwargs):
94
- return self.executor.put_file(state, self, *args, **kwargs)
454
+ def check_can_rsync(self) -> None:
455
+ self._check_state()
456
+ return self.connector.check_can_rsync()
95
457
 
96
- def get_file(self, state, *args, **kwargs):
97
- return self.executor.get_file(state, self, *args, **kwargs)
458
+ def rsync(self, *args, **kwargs) -> bool:
459
+ self._check_state()
460
+ return self.connector.rsync(*args, **kwargs)