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