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/facts.py CHANGED
@@ -1,259 +1,326 @@
1
- '''
1
+ """
2
2
  The pyinfra facts API. Facts enable pyinfra to collect remote server state which
3
3
  is used to "diff" with the desired state, producing the final commands required
4
4
  for a deploy.
5
- '''
6
5
 
7
- from inspect import isfunction, ismethod
8
- from socket import (
9
- error as socket_error,
10
- timeout as timeout_error,
11
- )
6
+ Note that the facts API does *not* use the global currently in context host so
7
+ it's possible to call facts on hosts out of context (ie give me the IP of this
8
+ other host B while I operate on this host A).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import inspect
14
+ import re
15
+ from inspect import getcallargs
16
+ from socket import error as socket_error, timeout as timeout_error
17
+ from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Optional, Type, TypeVar, cast
12
18
 
13
19
  import click
14
20
  import gevent
15
-
16
- from gevent.lock import BoundedSemaphore
17
21
  from paramiko import SSHException
22
+ from typing_extensions import override
18
23
 
19
24
  from pyinfra import logger
25
+ from pyinfra.api import StringCommand
26
+ from pyinfra.api.arguments import all_global_arguments, pop_global_arguments
27
+ from pyinfra.api.exceptions import FactProcessError
20
28
  from pyinfra.api.util import (
21
- get_arg_value,
29
+ get_kwargs_str,
30
+ log_error_or_warning,
22
31
  log_host_command_error,
23
- make_hash,
24
- underscore,
32
+ print_host_combined_output,
25
33
  )
34
+ from pyinfra.connectors.util import CommandOutput
35
+ from pyinfra.context import ctx_host, ctx_state
26
36
  from pyinfra.progress import progress_spinner
27
37
 
38
+ from .arguments import CONNECTOR_ARGUMENT_KEYS
28
39
 
29
- # Index of snake_case facts -> CamelCase classes
30
- FACTS = {}
31
- FACT_LOCK = BoundedSemaphore()
40
+ if TYPE_CHECKING:
41
+ from pyinfra.api import Host, State
32
42
 
43
+ SUDO_REGEX = r"^sudo: unknown user"
44
+ SU_REGEXES = (
45
+ r"^su: user .+ does not exist",
46
+ r"^su: unknown login",
47
+ )
33
48
 
34
- def is_fact(name):
35
- return name in FACTS
36
49
 
50
+ T = TypeVar("T")
37
51
 
38
- def get_fact_names():
39
- '''
40
- Returns a list of available facts in camel_case format.
41
- '''
42
52
 
43
- return list(FACTS.keys())
53
+ class FactBase(Generic[T]):
54
+ name: str
44
55
 
56
+ abstract: bool = True
45
57
 
46
- class FactMeta(type):
47
- '''
48
- Metaclass to dynamically build the facts index.
49
- '''
58
+ shell_executable: str | None = None
50
59
 
51
- def __init__(cls, name, bases, attrs):
52
- if attrs.get('abstract'):
53
- return
60
+ command: Callable[..., str | StringCommand]
54
61
 
55
- fact_name = underscore(name)
56
- cls.name = fact_name
62
+ def requires_command(self, *args, **kwargs) -> str | None:
63
+ return None
57
64
 
58
- # Get the an instance of the fact, attach to facts
59
- FACTS[fact_name] = cls
65
+ @override
66
+ def __init_subclass__(cls) -> None:
67
+ super().__init_subclass__()
68
+ module_name = cls.__module__.replace("pyinfra.facts.", "")
69
+ cls.name = f"{module_name}.{cls.__name__}"
60
70
 
71
+ # Check that fact's `command` method does not inadvertently take a global
72
+ # argument, most commonly `name`.
73
+ if hasattr(cls, "command") and callable(cls.command):
74
+ command_args = set(inspect.signature(cls.command).parameters.keys())
75
+ global_args = set([name for name, _ in all_global_arguments()])
76
+ command_global_args = command_args & global_args
61
77
 
62
- class FactBase(object, metaclass=FactMeta):
63
- abstract = True
78
+ if len(command_global_args) > 0:
79
+ names = ", ".join(command_global_args)
80
+ raise TypeError(f"{cls.name}'s arguments {names} are reserved for global arguments")
64
81
 
65
82
  @staticmethod
66
- def default():
67
- '''
83
+ def default() -> T:
84
+ """
68
85
  Set the default attribute to be a type (eg list/dict).
69
- '''
86
+ """
70
87
 
71
- @staticmethod
72
- def process(output):
73
- return '\n'.join(output)
88
+ return cast(T, None)
74
89
 
75
- def process_pipeline(self, args, output):
76
- return {
77
- arg: self.process([output[i]])
78
- for i, arg in enumerate(args)
79
- }
90
+ def process(self, output: Iterable[str]) -> T:
91
+ # NOTE: TypeVar does not support a default, so we have to cast this str -> T
92
+ return cast(T, "\n".join(output))
80
93
 
94
+ def process_pipeline(self, args, output):
95
+ return {arg: self.process([output[i]]) for i, arg in enumerate(args)}
81
96
 
82
- class ShortFactBase(object, metaclass=FactMeta):
83
- fact = None
84
97
 
98
+ class ShortFactBase(Generic[T]):
99
+ name: str
100
+ fact: Type[FactBase]
85
101
 
86
- def get_short_facts(state, short_fact, **kwargs):
87
- facts = get_facts(state, short_fact.fact.name, **kwargs)
102
+ @override
103
+ def __init_subclass__(cls) -> None:
104
+ super().__init_subclass__()
105
+ module_name = cls.__module__.replace("pyinfra.facts.", "")
106
+ cls.name = f"{module_name}.{cls.__name__}"
88
107
 
89
- return {
90
- host: short_fact.process_data(data)
91
- for host, data in facts.items()
92
- }
108
+ def process_data(self, data):
109
+ return data
93
110
 
94
111
 
95
- def get_facts(state, name, args=None, ensure_hosts=None):
96
- '''
97
- Get a single fact for all hosts in the state.
98
- '''
112
+ def get_short_facts(state: "State", host: "Host", short_fact, **kwargs):
113
+ fact_data = get_fact(state, host, short_fact.fact, **kwargs)
114
+ return short_fact().process_data(fact_data)
99
115
 
100
- # Create an instance of the fact
101
- fact = FACTS[name]()
102
116
 
103
- if isinstance(fact, ShortFactBase):
104
- return get_short_facts(state, fact, args=args, ensure_hosts=ensure_hosts)
117
+ def _make_command(command_attribute, host_args):
118
+ if callable(command_attribute):
119
+ host_args.pop("self", None)
120
+ return command_attribute(**host_args)
121
+ return command_attribute
105
122
 
106
- logger.debug('Getting fact: {0} (ensure_hosts: {1})'.format(
107
- name, ensure_hosts,
108
- ))
109
123
 
124
+ def _handle_fact_kwargs(state: "State", host: "Host", cls, args, kwargs):
110
125
  args = args or []
126
+ kwargs = kwargs or {}
111
127
 
112
- # Apply args or defaults
113
- sudo = state.config.SUDO
114
- sudo_user = state.config.SUDO_USER
115
- su_user = state.config.SU_USER
116
- ignore_errors = state.config.IGNORE_ERRORS
117
-
118
- # Timeout for operations !== timeout for connect (config.CONNECT_TIMEOUT)
119
- timeout = None
120
-
121
- # Get the current op meta
122
- current_op_hash = state.current_op_hash
123
- current_op_meta = state.op_meta.get(current_op_hash)
124
-
125
- # If inside an operation, fetch config meta
126
- if current_op_meta:
127
- sudo = current_op_meta['sudo']
128
- sudo_user = current_op_meta['sudo_user']
129
- su_user = current_op_meta['su_user']
130
- ignore_errors = current_op_meta['ignore_errors']
131
- timeout = current_op_meta['timeout']
132
-
133
- # Make a hash which keeps facts unique - but usable cross-deploy/threads.
134
- # Locks are used to maintain order.
135
- fact_hash = make_hash((name, args, sudo, sudo_user, su_user, ignore_errors))
136
-
137
- # Already got this fact? Unlock and return 'em
138
- current_facts = state.facts.get(fact_hash, {})
139
- if current_facts:
140
- if not ensure_hosts or all(
141
- host in current_facts for host in ensure_hosts
142
- ):
143
- return current_facts
144
-
145
- with FACT_LOCK:
146
- # Add any hosts we must have, whether considered in the inventory or not
147
- # (these hosts might be outside the --limit or current op limit_hosts).
148
- hosts = set(state.inventory)
149
- if ensure_hosts:
150
- hosts.update(ensure_hosts)
151
-
152
- # Execute the command for each state inventory in a greenlet
153
- greenlet_to_host = {}
154
-
155
- for host in hosts:
156
- if host in current_facts:
157
- continue
158
-
159
- # if host in state.ready_hosts:
160
- # continue
161
-
162
- # Work out the command
163
- command = fact.command
164
-
165
- if ismethod(command) or isfunction(command):
166
- # Generate actual arguments by pasing strings as jinja2 templates
167
- host_args = [get_arg_value(state, host, arg) for arg in args]
168
-
169
- command = command(*host_args)
170
-
171
- greenlet = state.fact_pool.spawn(
172
- host.run_shell_command, state, command,
173
- sudo=sudo, sudo_user=sudo_user,
174
- su_user=su_user, timeout=timeout,
175
- print_output=state.print_fact_output,
176
- )
177
- greenlet_to_host[greenlet] = host
178
-
179
- # Wait for all the commands to execute
180
- progress_prefix = 'fact: {0}'.format(name)
181
- if args:
182
- progress_prefix = '{0}{1}'.format(progress_prefix, args)
183
-
184
- with progress_spinner(
185
- greenlet_to_host.values(),
186
- prefix_message=progress_prefix,
187
- ) as progress:
188
- for greenlet in gevent.iwait(greenlet_to_host.keys()):
189
- host = greenlet_to_host[greenlet]
190
- progress(host)
191
-
192
- hostname_facts = {}
193
- failed_hosts = set()
194
-
195
- # Collect the facts and any failures
196
- for greenlet, host in greenlet_to_host.items():
197
- status = False
198
- stdout = []
128
+ # Start with a (shallow) copy of current operation kwargs if any
129
+ ctx_kwargs: dict[str, Any] = (
130
+ cast(dict[str, Any], host.current_op_global_arguments) or {}
131
+ ).copy()
132
+ # Update with the input kwargs (overrides)
133
+ ctx_kwargs.update(kwargs)
199
134
 
200
- try:
201
- status, stdout, _ = greenlet.get()
135
+ # Pop executor kwargs, pass remaining
136
+ global_kwargs, _ = pop_global_arguments(state, host, cast(dict[str, Any], ctx_kwargs))
202
137
 
203
- except (timeout_error, socket_error, SSHException) as e:
204
- failed_hosts.add(host)
205
- log_host_command_error(
206
- host, e,
207
- timeout=timeout,
208
- )
209
-
210
- data = fact.default()
138
+ fact_kwargs = {key: value for key, value in kwargs.items() if key not in global_kwargs}
211
139
 
212
- if status and stdout:
213
- data = fact.process(stdout)
140
+ if args or fact_kwargs:
141
+ # Merges args & kwargs into a single kwargs dictionary
142
+ fact_kwargs = getcallargs(cls().command, *args, **fact_kwargs)
214
143
 
215
- hostname_facts[host] = data
144
+ return fact_kwargs, global_kwargs
216
145
 
217
- log_name = click.style(name, bold=True)
218
146
 
219
- filtered_args = list(filter(None, args))
220
- if filtered_args:
221
- log = 'Loaded fact {0}: {1}'.format(log_name, tuple(filtered_args))
222
- else:
223
- log = 'Loaded fact {0}'.format(log_name)
147
+ def get_facts(state, *args, **kwargs):
148
+ def get_host_fact(host, *args, **kwargs):
149
+ with ctx_host.use(host):
150
+ return get_fact(state, host, *args, **kwargs)
224
151
 
225
- if state.print_fact_info:
226
- logger.info(log)
227
- else:
228
- logger.debug(log)
229
-
230
- # Check we've not failed
231
- if not ignore_errors:
232
- state.fail_hosts(failed_hosts)
233
-
234
- # Assign the facts
235
- state.facts.setdefault(fact_hash, {}).update(hostname_facts)
236
-
237
- return state.facts[fact_hash]
152
+ with ctx_state.use(state):
153
+ greenlet_to_host = {
154
+ state.pool.spawn(get_host_fact, host, *args, **kwargs): host
155
+ for host in state.inventory.iter_active_hosts()
156
+ }
238
157
 
158
+ results = {}
159
+
160
+ with progress_spinner(greenlet_to_host.values()) as progress:
161
+ for greenlet in gevent.iwait(greenlet_to_host.keys()):
162
+ host = greenlet_to_host[greenlet]
163
+ results[host] = greenlet.get()
164
+ progress(host)
165
+
166
+ return results
167
+
168
+
169
+ def get_fact(
170
+ state: "State",
171
+ host: "Host",
172
+ cls: type[FactBase],
173
+ args: Optional[Any] = None,
174
+ kwargs: Optional[Any] = None,
175
+ ensure_hosts: Optional[Any] = None,
176
+ apply_failed_hosts: bool = True,
177
+ ) -> Any:
178
+ if issubclass(cls, ShortFactBase):
179
+ return get_short_facts(
180
+ state,
181
+ host,
182
+ cls,
183
+ args=args,
184
+ kwargs=kwargs,
185
+ ensure_hosts=ensure_hosts,
186
+ apply_failed_hosts=apply_failed_hosts,
187
+ )
188
+
189
+ return _get_fact(
190
+ state,
191
+ host,
192
+ cls,
193
+ args,
194
+ kwargs,
195
+ ensure_hosts,
196
+ apply_failed_hosts,
197
+ )
198
+
199
+
200
+ def _get_fact(
201
+ state: "State",
202
+ host: "Host",
203
+ cls: type[FactBase],
204
+ args: Optional[list] = None,
205
+ kwargs: Optional[dict] = None,
206
+ ensure_hosts: Optional[Any] = None,
207
+ apply_failed_hosts: bool = True,
208
+ ) -> Any:
209
+ fact = cls()
210
+ name = fact.name
211
+
212
+ fact_kwargs, global_kwargs = _handle_fact_kwargs(state, host, cls, args, kwargs)
213
+
214
+ kwargs_str = get_kwargs_str(fact_kwargs)
215
+ logger.debug(
216
+ "Getting fact: %s (%s) (ensure_hosts: %r)",
217
+ name,
218
+ kwargs_str,
219
+ ensure_hosts,
220
+ )
221
+
222
+ if not host.connected:
223
+ host.connect(
224
+ reason=f"to load fact: {name} ({kwargs_str})",
225
+ raise_exceptions=True,
226
+ )
227
+
228
+ # Facts can override the shell (winrm powershell vs cmd support)
229
+ if fact.shell_executable:
230
+ global_kwargs["_shell_executable"] = fact.shell_executable
231
+
232
+ command = _make_command(fact.command, fact_kwargs)
233
+ requires_command = _make_command(fact.requires_command, fact_kwargs)
234
+ if requires_command:
235
+ command = StringCommand(
236
+ # Command doesn't exist, return 0 *or* run & return fact command
237
+ "!",
238
+ "command",
239
+ "-v",
240
+ requires_command,
241
+ ">/dev/null",
242
+ "||",
243
+ command,
244
+ )
245
+
246
+ status = False
247
+ output = CommandOutput([])
248
+
249
+ executor_kwargs = {
250
+ key: value for key, value in global_kwargs.items() if key in CONNECTOR_ARGUMENT_KEYS
251
+ }
239
252
 
240
- def get_fact(state, host, name):
241
- '''
242
- Wrapper around ``get_facts`` returning facts for one host or a function
243
- that does.
244
- '''
253
+ try:
254
+ status, output = host.run_shell_command(
255
+ command,
256
+ print_output=state.print_fact_output,
257
+ print_input=state.print_fact_input,
258
+ **executor_kwargs,
259
+ )
260
+ except (timeout_error, socket_error, SSHException) as e:
261
+ log_host_command_error(
262
+ host,
263
+ e,
264
+ timeout=global_kwargs.get("_timeout"),
265
+ )
266
+
267
+ stdout_lines, stderr_lines = output.stdout_lines, output.stderr_lines
268
+
269
+ data = fact.default()
270
+
271
+ if status:
272
+ if stdout_lines:
273
+ try:
274
+ data = fact.process(stdout_lines)
275
+ except FactProcessError as e:
276
+ log_error_or_warning(
277
+ host,
278
+ global_kwargs["_ignore_errors"],
279
+ description=("could not process fact: {0} {1}").format(
280
+ name, get_kwargs_str(fact_kwargs)
281
+ ),
282
+ exception=e,
283
+ )
245
284
 
246
- # Expecting a function to return
247
- if callable(getattr(FACTS[name], 'command', None)):
248
- def wrapper(*args):
249
- fact_data = get_facts(state, name, args=args, ensure_hosts=(host,))
285
+ # Check we've not failed
286
+ if apply_failed_hosts and not global_kwargs["_ignore_errors"]:
287
+ state.fail_hosts({host})
288
+
289
+ elif stderr_lines:
290
+ # If we have error output and that error is sudo or su stating the user
291
+ # does not exist, do not fail but instead return the default fact value.
292
+ # This allows for users that don't currently but may be created during
293
+ # other operations.
294
+ first_line = stderr_lines[0]
295
+ if executor_kwargs["_sudo_user"] and re.match(SUDO_REGEX, first_line):
296
+ status = True
297
+ if executor_kwargs["_su_user"] and any(re.match(regex, first_line) for regex in SU_REGEXES):
298
+ status = True
299
+
300
+ if status:
301
+ log_message = "{0}{1}".format(
302
+ host.print_prefix,
303
+ "Loaded fact {0}{1}".format(
304
+ click.style(name, bold=True),
305
+ f" ({get_kwargs_str(kwargs)})" if kwargs else "",
306
+ ),
307
+ )
308
+ if state.print_fact_info:
309
+ logger.info(log_message)
310
+ else:
311
+ logger.debug(log_message)
312
+ else:
313
+ if not state.print_fact_output:
314
+ print_host_combined_output(host, output)
250
315
 
251
- return fact_data.get(host)
252
- return wrapper
316
+ log_error_or_warning(
317
+ host,
318
+ global_kwargs["_ignore_errors"],
319
+ description=("could not load fact: {0} {1}").format(name, get_kwargs_str(fact_kwargs)),
320
+ )
253
321
 
254
- # Expecting the fact as a return value
255
- else:
256
- # Get the fact
257
- fact_data = get_facts(state, name, ensure_hosts=(host,))
322
+ # Check we've not failed
323
+ if apply_failed_hosts and not status and not global_kwargs["_ignore_errors"]:
324
+ state.fail_hosts({host})
258
325
 
259
- return fact_data.get(host)
326
+ return data