ansible-core 2.17.6rc1__py3-none-any.whl → 2.18.0__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.

Potentially problematic release.


This version of ansible-core might be problematic. Click here for more details.

Files changed (325) hide show
  1. ansible/__main__.py +2 -17
  2. ansible/cli/__init__.py +3 -15
  3. ansible/cli/config.py +187 -24
  4. ansible/cli/console.py +1 -1
  5. ansible/cli/doc.py +38 -16
  6. ansible/cli/galaxy.py +3 -49
  7. ansible/cli/inventory.py +2 -2
  8. ansible/cli/pull.py +2 -2
  9. ansible/cli/scripts/ansible_connection_cli_stub.py +1 -10
  10. ansible/config/base.yml +127 -57
  11. ansible/config/manager.py +89 -11
  12. ansible/constants.py +32 -9
  13. ansible/errors/__init__.py +5 -0
  14. ansible/executor/interpreter_discovery.py +1 -1
  15. ansible/executor/play_iterator.py +16 -0
  16. ansible/executor/playbook_executor.py +1 -4
  17. ansible/executor/powershell/become_wrapper.ps1 +4 -5
  18. ansible/executor/powershell/bootstrap_wrapper.ps1 +2 -3
  19. ansible/executor/powershell/exec_wrapper.ps1 +1 -1
  20. ansible/executor/powershell/module_manifest.py +2 -2
  21. ansible/executor/task_executor.py +50 -39
  22. ansible/executor/task_queue_manager.py +1 -1
  23. ansible/executor/task_result.py +1 -1
  24. ansible/galaxy/api.py +3 -4
  25. ansible/galaxy/collection/__init__.py +21 -10
  26. ansible/galaxy/collection/concrete_artifact_manager.py +2 -2
  27. ansible/galaxy/collection/galaxy_api_proxy.py +10 -16
  28. ansible/galaxy/collection/gpg.py +17 -23
  29. ansible/galaxy/data/COPYING +7 -0
  30. ansible/galaxy/data/apb/Dockerfile.j2 +1 -0
  31. ansible/galaxy/data/apb/Makefile.j2 +1 -0
  32. ansible/galaxy/data/apb/README.md +7 -3
  33. ansible/galaxy/data/apb/apb.yml.j2 +1 -0
  34. ansible/galaxy/data/apb/defaults/main.yml.j2 +1 -0
  35. ansible/galaxy/data/apb/handlers/main.yml.j2 +1 -0
  36. ansible/galaxy/data/apb/meta/main.yml.j2 +1 -0
  37. ansible/galaxy/data/apb/playbooks/deprovision.yml.j2 +1 -0
  38. ansible/galaxy/data/apb/playbooks/provision.yml.j2 +1 -0
  39. ansible/galaxy/data/apb/tasks/main.yml.j2 +1 -0
  40. ansible/galaxy/data/apb/tests/ansible.cfg +1 -0
  41. ansible/galaxy/data/apb/tests/inventory +1 -0
  42. ansible/galaxy/data/apb/tests/test.yml.j2 +1 -0
  43. ansible/galaxy/data/apb/vars/main.yml.j2 +1 -0
  44. ansible/galaxy/data/collections_galaxy_meta.yml +1 -0
  45. ansible/galaxy/data/container/defaults/main.yml.j2 +1 -0
  46. ansible/galaxy/data/container/handlers/main.yml.j2 +1 -0
  47. ansible/galaxy/data/container/meta/container.yml.j2 +1 -0
  48. ansible/galaxy/data/container/meta/main.yml.j2 +1 -0
  49. ansible/galaxy/data/container/tasks/main.yml.j2 +1 -0
  50. ansible/galaxy/data/container/tests/ansible.cfg +1 -0
  51. ansible/galaxy/data/container/tests/inventory +1 -0
  52. ansible/galaxy/data/container/tests/test.yml.j2 +1 -0
  53. ansible/galaxy/data/container/vars/main.yml.j2 +1 -0
  54. ansible/galaxy/data/default/collection/README.md.j2 +1 -0
  55. ansible/galaxy/data/default/collection/galaxy.yml.j2 +1 -0
  56. ansible/galaxy/data/default/collection/meta/runtime.yml +1 -0
  57. ansible/galaxy/data/default/collection/plugins/README.md.j2 +1 -0
  58. ansible/galaxy/data/default/role/defaults/main.yml.j2 +1 -0
  59. ansible/galaxy/data/default/role/handlers/main.yml.j2 +1 -0
  60. ansible/galaxy/data/default/role/meta/main.yml.j2 +1 -0
  61. ansible/galaxy/data/default/role/tasks/main.yml.j2 +1 -0
  62. ansible/galaxy/data/default/role/tests/inventory +1 -0
  63. ansible/galaxy/data/default/role/tests/test.yml.j2 +1 -0
  64. ansible/galaxy/data/default/role/vars/main.yml.j2 +1 -0
  65. ansible/galaxy/data/network/cliconf_plugins/example.py.j2 +1 -0
  66. ansible/galaxy/data/network/defaults/main.yml.j2 +1 -0
  67. ansible/galaxy/data/network/library/example_command.py.j2 +1 -0
  68. ansible/galaxy/data/network/library/example_config.py.j2 +1 -0
  69. ansible/galaxy/data/network/library/example_facts.py.j2 +1 -0
  70. ansible/galaxy/data/network/meta/main.yml.j2 +1 -0
  71. ansible/galaxy/data/network/module_utils/example.py.j2 +1 -0
  72. ansible/galaxy/data/network/netconf_plugins/example.py.j2 +1 -0
  73. ansible/galaxy/data/network/tasks/main.yml.j2 +1 -0
  74. ansible/galaxy/data/network/terminal_plugins/example.py.j2 +1 -0
  75. ansible/galaxy/data/network/tests/inventory +1 -0
  76. ansible/galaxy/data/network/tests/test.yml.j2 +1 -0
  77. ansible/galaxy/data/network/vars/main.yml.j2 +1 -0
  78. ansible/galaxy/dependency_resolution/providers.py +3 -3
  79. ansible/galaxy/role.py +1 -1
  80. ansible/galaxy/token.py +20 -8
  81. ansible/keyword_desc.yml +1 -1
  82. ansible/module_utils/_internal/__init__.py +0 -0
  83. ansible/module_utils/_internal/_concurrent/__init__.py +0 -0
  84. ansible/module_utils/_internal/_concurrent/_daemon_threading.py +28 -0
  85. ansible/module_utils/_internal/_concurrent/_futures.py +21 -0
  86. ansible/module_utils/ansible_release.py +2 -2
  87. ansible/module_utils/api.py +2 -2
  88. ansible/module_utils/basic.py +8 -8
  89. ansible/module_utils/common/collections.py +1 -1
  90. ansible/module_utils/common/file.py +0 -6
  91. ansible/module_utils/common/process.py +22 -9
  92. ansible/module_utils/common/text/converters.py +5 -8
  93. ansible/module_utils/common/text/formatters.py +20 -4
  94. ansible/module_utils/common/validation.py +33 -25
  95. ansible/module_utils/compat/paramiko.py +6 -1
  96. ansible/module_utils/compat/selinux.py +2 -2
  97. ansible/module_utils/connection.py +8 -24
  98. ansible/module_utils/csharp/Ansible.Become.cs +14 -25
  99. ansible/module_utils/csharp/Ansible.Process.cs +1 -1
  100. ansible/module_utils/distro/__init__.py +1 -1
  101. ansible/module_utils/distro/_distro.py +8 -4
  102. ansible/module_utils/facts/collector.py +2 -0
  103. ansible/module_utils/facts/default_collectors.py +3 -1
  104. ansible/module_utils/facts/hardware/aix.py +54 -52
  105. ansible/module_utils/facts/hardware/darwin.py +37 -34
  106. ansible/module_utils/facts/hardware/freebsd.py +55 -15
  107. ansible/module_utils/facts/hardware/hpux.py +3 -0
  108. ansible/module_utils/facts/hardware/linux.py +101 -57
  109. ansible/module_utils/facts/hardware/netbsd.py +3 -0
  110. ansible/module_utils/facts/hardware/openbsd.py +4 -1
  111. ansible/module_utils/facts/hardware/sunos.py +7 -1
  112. ansible/module_utils/facts/network/aix.py +16 -17
  113. ansible/module_utils/facts/network/fc_wwn.py +4 -1
  114. ansible/module_utils/facts/network/hpux.py +21 -4
  115. ansible/module_utils/facts/network/iscsi.py +7 -8
  116. ansible/module_utils/facts/network/linux.py +0 -2
  117. ansible/module_utils/facts/other/facter.py +9 -4
  118. ansible/module_utils/facts/other/ohai.py +5 -5
  119. ansible/module_utils/facts/packages.py +49 -7
  120. ansible/module_utils/facts/sysctl.py +33 -31
  121. ansible/module_utils/facts/system/distribution.py +1 -1
  122. ansible/module_utils/facts/system/local.py +12 -22
  123. ansible/module_utils/facts/system/service_mgr.py +3 -1
  124. ansible/module_utils/facts/system/systemd.py +47 -0
  125. ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
  126. ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
  127. ansible/module_utils/splitter.py +1 -1
  128. ansible/modules/add_host.py +1 -1
  129. ansible/modules/apt.py +43 -32
  130. ansible/modules/apt_key.py +6 -6
  131. ansible/modules/apt_repository.py +23 -14
  132. ansible/modules/assemble.py +7 -2
  133. ansible/modules/assert.py +4 -4
  134. ansible/modules/blockinfile.py +3 -6
  135. ansible/modules/command.py +1 -1
  136. ansible/modules/copy.py +4 -4
  137. ansible/modules/cron.py +13 -10
  138. ansible/modules/deb822_repository.py +16 -17
  139. ansible/modules/debconf.py +9 -9
  140. ansible/modules/debug.py +1 -1
  141. ansible/modules/dnf.py +79 -164
  142. ansible/modules/dnf5.py +54 -29
  143. ansible/modules/dpkg_selections.py +2 -2
  144. ansible/modules/expect.py +2 -2
  145. ansible/modules/fetch.py +2 -2
  146. ansible/modules/file.py +5 -3
  147. ansible/modules/find.py +40 -12
  148. ansible/modules/gather_facts.py +4 -2
  149. ansible/modules/get_url.py +29 -24
  150. ansible/modules/git.py +35 -35
  151. ansible/modules/group.py +71 -1
  152. ansible/modules/hostname.py +2 -4
  153. ansible/modules/include_vars.py +5 -5
  154. ansible/modules/iptables.py +13 -16
  155. ansible/modules/known_hosts.py +16 -13
  156. ansible/modules/lineinfile.py +1 -4
  157. ansible/modules/meta.py +6 -1
  158. ansible/modules/mount_facts.py +651 -0
  159. ansible/modules/package_facts.py +63 -80
  160. ansible/modules/pause.py +4 -3
  161. ansible/modules/pip.py +14 -14
  162. ansible/modules/replace.py +1 -4
  163. ansible/modules/rpm_key.py +31 -11
  164. ansible/modules/service.py +8 -8
  165. ansible/modules/service_facts.py +20 -5
  166. ansible/modules/set_stats.py +1 -1
  167. ansible/modules/setup.py +3 -3
  168. ansible/modules/stat.py +3 -3
  169. ansible/modules/subversion.py +1 -1
  170. ansible/modules/systemd.py +16 -10
  171. ansible/modules/systemd_service.py +16 -10
  172. ansible/modules/sysvinit.py +4 -4
  173. ansible/modules/unarchive.py +35 -22
  174. ansible/modules/uri.py +24 -18
  175. ansible/modules/user.py +145 -12
  176. ansible/modules/validate_argument_spec.py +3 -3
  177. ansible/modules/wait_for_connection.py +2 -1
  178. ansible/modules/yum_repository.py +136 -179
  179. ansible/parsing/dataloader.py +2 -2
  180. ansible/parsing/mod_args.py +11 -10
  181. ansible/parsing/vault/__init__.py +8 -3
  182. ansible/parsing/yaml/constructor.py +10 -8
  183. ansible/parsing/yaml/objects.py +1 -1
  184. ansible/playbook/base.py +12 -23
  185. ansible/playbook/helpers.py +4 -0
  186. ansible/playbook/loop_control.py +8 -0
  187. ansible/playbook/play.py +4 -22
  188. ansible/playbook/play_context.py +0 -16
  189. ansible/playbook/playbook_include.py +2 -2
  190. ansible/playbook/role/__init__.py +2 -2
  191. ansible/plugins/__init__.py +2 -0
  192. ansible/plugins/action/__init__.py +7 -9
  193. ansible/plugins/action/dnf.py +7 -5
  194. ansible/plugins/action/package.py +5 -4
  195. ansible/plugins/action/reboot.py +2 -2
  196. ansible/plugins/become/__init__.py +1 -1
  197. ansible/plugins/callback/__init__.py +44 -3
  198. ansible/plugins/callback/default.py +1 -1
  199. ansible/plugins/cliconf/__init__.py +1 -1
  200. ansible/plugins/connection/paramiko_ssh.py +2 -80
  201. ansible/plugins/connection/psrp.py +33 -82
  202. ansible/plugins/connection/ssh.py +0 -8
  203. ansible/plugins/connection/winrm.py +46 -1
  204. ansible/plugins/doc_fragments/connection_pipelining.py +2 -2
  205. ansible/plugins/doc_fragments/constructed.py +10 -10
  206. ansible/plugins/doc_fragments/default_callback.py +8 -8
  207. ansible/plugins/doc_fragments/files.py +5 -5
  208. ansible/plugins/doc_fragments/inventory_cache.py +2 -2
  209. ansible/plugins/doc_fragments/result_format_callback.py +6 -6
  210. ansible/plugins/doc_fragments/return_common.py +1 -1
  211. ansible/plugins/doc_fragments/shell_common.py +2 -10
  212. ansible/plugins/doc_fragments/shell_windows.py +0 -9
  213. ansible/plugins/doc_fragments/url.py +2 -2
  214. ansible/plugins/doc_fragments/url_windows.py +4 -5
  215. ansible/plugins/doc_fragments/validate.py +1 -1
  216. ansible/plugins/filter/core.py +2 -0
  217. ansible/plugins/filter/human_to_bytes.yml +9 -0
  218. ansible/plugins/filter/password_hash.yml +1 -1
  219. ansible/plugins/filter/strftime.yml +1 -1
  220. ansible/plugins/filter/to_nice_json.yml +7 -3
  221. ansible/plugins/filter/to_uuid.yml +1 -1
  222. ansible/plugins/inventory/script.py +1 -1
  223. ansible/plugins/list.py +1 -1
  224. ansible/plugins/loader.py +0 -11
  225. ansible/plugins/lookup/config.py +1 -1
  226. ansible/plugins/lookup/csvfile.py +21 -9
  227. ansible/plugins/lookup/env.py +8 -9
  228. ansible/plugins/lookup/ini.py +10 -1
  229. ansible/plugins/lookup/random_choice.py +2 -2
  230. ansible/plugins/lookup/url.py +7 -2
  231. ansible/plugins/shell/__init__.py +15 -20
  232. ansible/plugins/shell/powershell.py +9 -6
  233. ansible/plugins/strategy/__init__.py +16 -7
  234. ansible/plugins/test/core.py +23 -1
  235. ansible/plugins/test/issubset.yml +1 -1
  236. ansible/plugins/test/subset.yml +1 -1
  237. ansible/plugins/test/timedout.yml +20 -0
  238. ansible/plugins/test/vault_encrypted.yml +6 -6
  239. ansible/plugins/test/vaulted_file.yml +19 -0
  240. ansible/release.py +2 -2
  241. ansible/template/__init__.py +3 -8
  242. ansible/utils/collection_loader/_collection_finder.py +23 -55
  243. ansible/utils/display.py +44 -31
  244. ansible/utils/jsonrpc.py +1 -1
  245. ansible/utils/listify.py +1 -5
  246. ansible/utils/path.py +3 -0
  247. ansible/utils/vars.py +18 -27
  248. ansible/vars/manager.py +7 -150
  249. ansible/vars/plugins.py +1 -1
  250. ansible_core-2.18.0.dist-info/Apache-License.txt +202 -0
  251. {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/METADATA +36 -23
  252. ansible_core-2.18.0.dist-info/MIT-license.txt +14 -0
  253. ansible_core-2.18.0.dist-info/PSF-license.txt +48 -0
  254. {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/RECORD +316 -311
  255. {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/entry_points.txt +1 -1
  256. ansible_core-2.18.0.dist-info/simplified_bsd.txt +8 -0
  257. ansible_test/_data/completion/docker.txt +7 -7
  258. ansible_test/_data/completion/remote.txt +5 -4
  259. ansible_test/_data/completion/windows.txt +4 -4
  260. ansible_test/_data/requirements/ansible-test.txt +1 -2
  261. ansible_test/_data/requirements/constraints.txt +1 -2
  262. ansible_test/_data/requirements/sanity.ansible-doc.txt +3 -3
  263. ansible_test/_data/requirements/sanity.changelog.in +1 -1
  264. ansible_test/_data/requirements/sanity.changelog.txt +4 -4
  265. ansible_test/_data/requirements/sanity.import.plugin.txt +2 -2
  266. ansible_test/_data/requirements/sanity.import.txt +1 -1
  267. ansible_test/_data/requirements/sanity.integration-aliases.txt +1 -1
  268. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  269. ansible_test/_data/requirements/sanity.pylint.txt +6 -8
  270. ansible_test/_data/requirements/sanity.runtime-metadata.txt +2 -2
  271. ansible_test/_data/requirements/sanity.validate-modules.txt +3 -3
  272. ansible_test/_data/requirements/sanity.yamllint.in +1 -0
  273. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  274. ansible_test/_internal/ansible_util.py +8 -35
  275. ansible_test/_internal/ci/azp.py +1 -1
  276. ansible_test/_internal/classification/__init__.py +0 -2
  277. ansible_test/_internal/cli/parsers/key_value_parsers.py +3 -0
  278. ansible_test/_internal/commands/integration/cloud/hcloud.py +1 -1
  279. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  280. ansible_test/_internal/commands/integration/cloud/nios.py +1 -1
  281. ansible_test/_internal/commands/sanity/__init__.py +96 -19
  282. ansible_test/_internal/commands/sanity/pylint.py +20 -24
  283. ansible_test/_internal/completion.py +2 -0
  284. ansible_test/_internal/constants.py +0 -1
  285. ansible_test/_internal/coverage_util.py +1 -2
  286. ansible_test/_internal/docker_util.py +1 -1
  287. ansible_test/_internal/encoding.py +4 -4
  288. ansible_test/_internal/host_configs.py +10 -0
  289. ansible_test/_internal/host_profiles.py +9 -13
  290. ansible_test/_internal/pypi_proxy.py +1 -1
  291. ansible_test/_internal/python_requirements.py +5 -14
  292. ansible_test/_internal/timeout.py +1 -1
  293. ansible_test/_internal/util.py +40 -0
  294. ansible_test/_internal/util_common.py +5 -1
  295. ansible_test/_util/controller/sanity/code-smell/action-plugin-docs.json +3 -1
  296. ansible_test/_util/controller/sanity/code-smell/action-plugin-docs.py +6 -3
  297. ansible_test/_util/controller/sanity/code-smell/empty-init.json +0 -2
  298. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +5 -0
  299. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +5 -0
  300. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +5 -0
  301. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +6 -0
  302. ansible_test/_util/controller/sanity/pylint/config/default.cfg +6 -0
  303. ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +1 -19
  304. ansible_test/_util/controller/sanity/shellcheck/exclude.txt +1 -0
  305. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +67 -2
  306. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +27 -5
  307. ansible_test/_util/target/cli/ansible_test_cli_stub.py +0 -0
  308. ansible_test/_util/target/common/constants.py +2 -2
  309. ansible_test/_util/target/injector/python.py +5 -0
  310. ansible_test/_util/target/pytest/plugins/ansible_pytest_coverage.py +6 -0
  311. ansible_test/_util/target/sanity/import/importer.py +1 -1
  312. ansible_test/_util/target/setup/bootstrap.sh +6 -17
  313. ansible_test/_util/target/setup/requirements.py +18 -24
  314. ansible_test/config/config.yml +1 -1
  315. ansible_core-2.17.6rc1.data/scripts/ansible-test +0 -44
  316. ansible_test/_data/requirements/sanity.mypy.in +0 -10
  317. ansible_test/_data/requirements/sanity.mypy.txt +0 -18
  318. ansible_test/_internal/commands/sanity/mypy.py +0 -274
  319. ansible_test/_util/controller/sanity/mypy/ansible-core.ini +0 -116
  320. ansible_test/_util/controller/sanity/mypy/ansible-test.ini +0 -27
  321. ansible_test/_util/controller/sanity/mypy/modules.ini +0 -92
  322. ansible_test/_util/controller/sanity/mypy/packaging.ini +0 -20
  323. {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/COPYING +0 -0
  324. {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/WHEEL +0 -0
  325. {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,651 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (c) 2024 Ansible Project
3
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
+
5
+ from __future__ import annotations
6
+
7
+
8
+ DOCUMENTATION = """
9
+ ---
10
+ module: mount_facts
11
+ version_added: 2.18
12
+ short_description: Retrieve mount information.
13
+ description:
14
+ - Retrieve information about mounts from preferred sources and filter the results based on the filesystem type and device.
15
+ options:
16
+ devices:
17
+ description: A list of fnmatch patterns to filter mounts by the special device or remote file system.
18
+ default: ~
19
+ type: list
20
+ elements: str
21
+ fstypes:
22
+ description: A list of fnmatch patterns to filter mounts by the type of the file system.
23
+ default: ~
24
+ type: list
25
+ elements: str
26
+ sources:
27
+ description:
28
+ - A list of sources used to determine the mounts. Missing file sources (or empty files) are skipped. Repeat sources, including symlinks, are skipped.
29
+ - The C(mount_points) return value contains the first definition found for a mount point.
30
+ - Additional mounts to the same mount point are available from C(aggregate_mounts) (if enabled).
31
+ - By default, mounts are retrieved from all of the standard locations, which have the predefined aliases V(all)/V(static)/V(dynamic).
32
+ - V(all) contains V(dynamic) and V(static).
33
+ - V(dynamic) contains V(/etc/mtab), V(/proc/mounts), V(/etc/mnttab), and the value of O(mount_binary) if it is not None.
34
+ This allows platforms like BSD or AIX, which don't have an equivalent to V(/proc/mounts), to collect the current mounts by default.
35
+ See the O(mount_binary) option to disable the fall back or configure a different executable.
36
+ - V(static) contains V(/etc/fstab), V(/etc/vfstab), and V(/etc/filesystems).
37
+ Note that V(/etc/filesystems) is specific to AIX. The Linux file by this name has a different format/purpose and is ignored.
38
+ - The value of O(mount_binary) can be configured as a source, which will cause it to always execute.
39
+ Depending on the other sources configured, this could be inefficient/redundant.
40
+ For example, if V(/proc/mounts) and V(mount) are listed as O(sources), Linux hosts will retrieve the same mounts twice.
41
+ default: ~
42
+ type: list
43
+ elements: str
44
+ mount_binary:
45
+ description:
46
+ - The O(mount_binary) is used if O(sources) contain the value "mount", or if O(sources) contains a dynamic
47
+ source, and none were found (as can be expected on BSD or AIX hosts).
48
+ - Set to V(null) to stop after no dynamic file source is found instead.
49
+ type: raw
50
+ default: mount
51
+ timeout:
52
+ description:
53
+ - This is the maximum number of seconds to wait for each mount to complete. When this is V(null), wait indefinitely.
54
+ - Configure in conjunction with O(on_timeout) to skip unresponsive mounts.
55
+ - This timeout also applies to the O(mount_binary) command to list mounts.
56
+ - If the module is configured to run during the play's fact gathering stage, set a timeout using module_defaults to prevent a hang (see example).
57
+ type: float
58
+ on_timeout:
59
+ description:
60
+ - The action to take when gathering mount information exceeds O(timeout).
61
+ type: str
62
+ default: error
63
+ choices:
64
+ - error
65
+ - warn
66
+ - ignore
67
+ include_aggregate_mounts:
68
+ description:
69
+ - Whether or not the module should return the C(aggregate_mounts) list in C(ansible_facts).
70
+ - When this is V(null), a warning will be emitted if multiple mounts for the same mount point are found.
71
+ default: ~
72
+ type: bool
73
+ extends_documentation_fragment:
74
+ - action_common_attributes
75
+ attributes:
76
+ check_mode:
77
+ support: full
78
+ diff_mode:
79
+ support: none
80
+ platform:
81
+ platforms: posix
82
+ author:
83
+ - Ansible Core Team
84
+ - Sloane Hertel (@s-hertel)
85
+ """
86
+
87
+ EXAMPLES = """
88
+ - name: Get non-local devices
89
+ mount_facts:
90
+ devices: "[!/]*"
91
+
92
+ - name: Get FUSE subtype mounts
93
+ mount_facts:
94
+ fstypes:
95
+ - "fuse.*"
96
+
97
+ - name: Get NFS mounts during gather_facts with timeout
98
+ hosts: all
99
+ gather_facts: true
100
+ vars:
101
+ ansible_facts_modules:
102
+ - ansible.builtin.mount_facts
103
+ module_default:
104
+ ansible.builtin.mount_facts:
105
+ timeout: 10
106
+ fstypes:
107
+ - nfs
108
+ - nfs4
109
+
110
+ - name: Get mounts from a non-default location
111
+ mount_facts:
112
+ sources:
113
+ - /usr/etc/fstab
114
+
115
+ - name: Get mounts from the mount binary
116
+ mount_facts:
117
+ sources:
118
+ - mount
119
+ mount_binary: /sbin/mount
120
+ """
121
+
122
+ RETURN = """
123
+ ansible_facts:
124
+ description:
125
+ - An ansible_facts dictionary containing a dictionary of C(mount_points) and list of C(aggregate_mounts) when enabled.
126
+ - Each key in C(mount_points) is a mount point, and the value contains mount information (similar to C(ansible_facts["mounts"])).
127
+ Each value also contains the key C(ansible_context), with details about the source and line(s) corresponding to the parsed mount point.
128
+ - When C(aggregate_mounts) are included, the containing dictionaries are the same format as the C(mount_point) values.
129
+ returned: on success
130
+ type: dict
131
+ sample:
132
+ mount_points:
133
+ /proc/sys/fs/binfmt_misc:
134
+ ansible_context:
135
+ source: /proc/mounts
136
+ source_data: "systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=33,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=33850 0 0"
137
+ block_available: 0
138
+ block_size: 4096
139
+ block_total: 0
140
+ block_used: 0
141
+ device: "systemd-1"
142
+ dump: 0
143
+ fstype: "autofs"
144
+ inode_available: 0
145
+ inode_total: 0
146
+ inode_used: 0
147
+ mount: "/proc/sys/fs/binfmt_misc"
148
+ options: "rw,relatime,fd=33,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=33850"
149
+ passno: 0
150
+ size_available: 0
151
+ size_total: 0
152
+ uuid: null
153
+ aggregate_mounts:
154
+ - ansible_context:
155
+ source: /proc/mounts
156
+ source_data: "systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=33,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=33850 0 0"
157
+ block_available: 0
158
+ block_size: 4096
159
+ block_total: 0
160
+ block_used: 0
161
+ device: "systemd-1"
162
+ dump: 0
163
+ fstype: "autofs"
164
+ inode_available: 0
165
+ inode_total: 0
166
+ inode_used: 0
167
+ mount: "/proc/sys/fs/binfmt_misc"
168
+ options: "rw,relatime,fd=33,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=33850"
169
+ passno: 0
170
+ size_available: 0
171
+ size_total: 0
172
+ uuid: null
173
+ - ansible_context:
174
+ source: /proc/mounts
175
+ source_data: "binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc rw,nosuid,nodev,noexec,relatime 0 0"
176
+ block_available: 0
177
+ block_size: 4096
178
+ block_total: 0
179
+ block_used: 0
180
+ device: binfmt_misc
181
+ dump: 0
182
+ fstype: binfmt_misc
183
+ inode_available: 0
184
+ inode_total: 0
185
+ inode_used: 0
186
+ mount: "/proc/sys/fs/binfmt_misc"
187
+ options: "rw,nosuid,nodev,noexec,relatime"
188
+ passno: 0
189
+ size_available: 0
190
+ size_total: 0
191
+ uuid: null
192
+ """
193
+
194
+ from ansible.module_utils.basic import AnsibleModule
195
+ from ansible.module_utils.facts import timeout as _timeout
196
+ from ansible.module_utils.facts.utils import get_mount_size, get_file_content
197
+
198
+ from contextlib import suppress
199
+ from dataclasses import astuple, dataclass
200
+ from fnmatch import fnmatch
201
+
202
+ import codecs
203
+ import datetime
204
+ import functools
205
+ import os
206
+ import re
207
+ import subprocess
208
+ import typing as t
209
+
210
+ STATIC_SOURCES = ["/etc/fstab", "/etc/vfstab", "/etc/filesystems"]
211
+ DYNAMIC_SOURCES = ["/etc/mtab", "/proc/mounts", "/etc/mnttab"]
212
+
213
+ # AIX and BSD don't have a file-based dynamic source, so the module also supports running a mount binary to collect these.
214
+ # Pattern for Linux, including OpenBSD and NetBSD
215
+ LINUX_MOUNT_RE = re.compile(r"^(?P<device>\S+) on (?P<mount>\S+) type (?P<fstype>\S+) \((?P<options>.+)\)$")
216
+ # Pattern for other BSD including FreeBSD, DragonFlyBSD, and MacOS
217
+ BSD_MOUNT_RE = re.compile(r"^(?P<device>\S+) on (?P<mount>\S+) \((?P<fstype>.+)\)$")
218
+ # Pattern for AIX, example in https://www.ibm.com/docs/en/aix/7.2?topic=m-mount-command
219
+ AIX_MOUNT_RE = re.compile(r"^(?P<node>\S*)\s+(?P<mounted>\S+)\s+(?P<mount>\S+)\s+(?P<fstype>\S+)\s+(?P<time>\S+\s+\d+\s+\d+:\d+)\s+(?P<options>.*)$")
220
+
221
+
222
+ @dataclass
223
+ class MountInfo:
224
+ mount_point: str
225
+ line: str
226
+ fields: dict[str, str | int]
227
+
228
+
229
+ @dataclass
230
+ class MountInfoOptions:
231
+ mount_point: str
232
+ line: str
233
+ fields: dict[str, str | dict[str, str]]
234
+
235
+
236
+ def replace_octal_escapes(value: str) -> str:
237
+ return re.sub(r"(\\[0-7]{3})", lambda m: codecs.decode(m.group(0), "unicode_escape"), value)
238
+
239
+
240
+ @functools.lru_cache(maxsize=None)
241
+ def get_device_by_uuid(module: AnsibleModule, uuid : str) -> str | None:
242
+ """Get device information by UUID."""
243
+ blkid_output = None
244
+ if (blkid_binary := module.get_bin_path("blkid")):
245
+ cmd = [blkid_binary, "--uuid", uuid]
246
+ with suppress(subprocess.CalledProcessError):
247
+ blkid_output = handle_timeout(module)(subprocess.check_output)(cmd, text=True, timeout=module.params["timeout"])
248
+ return blkid_output
249
+
250
+
251
+ @functools.lru_cache(maxsize=None)
252
+ def list_uuids_linux() -> list[str]:
253
+ """List UUIDs from the system."""
254
+ with suppress(OSError):
255
+ return os.listdir("/dev/disk/by-uuid")
256
+ return []
257
+
258
+
259
+ @functools.lru_cache(maxsize=None)
260
+ def run_lsblk(module : AnsibleModule) -> list[list[str]]:
261
+ """Return device, UUID pairs from lsblk."""
262
+ lsblk_output = ""
263
+ if (lsblk_binary := module.get_bin_path("lsblk")):
264
+ cmd = [lsblk_binary, "--list", "--noheadings", "--paths", "--output", "NAME,UUID", "--exclude", "2"]
265
+ lsblk_output = subprocess.check_output(cmd, text=True, timeout=module.params["timeout"])
266
+ return [line.split() for line in lsblk_output.splitlines() if len(line.split()) == 2]
267
+
268
+
269
+ @functools.lru_cache(maxsize=None)
270
+ def get_udevadm_device_uuid(module : AnsibleModule, device : str) -> str | None:
271
+ """Fallback to get the device's UUID for lsblk <= 2.23 which doesn't have the --paths option."""
272
+ udevadm_output = ""
273
+ if (udevadm_binary := module.get_bin_path("udevadm")):
274
+ cmd = [udevadm_binary, "info", "--query", "property", "--name", device]
275
+ udevadm_output = subprocess.check_output(cmd, text=True, timeout=module.params["timeout"])
276
+ uuid = None
277
+ for line in udevadm_output.splitlines():
278
+ # a snippet of the output of the udevadm command below will be:
279
+ # ...
280
+ # ID_FS_TYPE=ext4
281
+ # ID_FS_USAGE=filesystem
282
+ # ID_FS_UUID=57b1a3e7-9019-4747-9809-7ec52bba9179
283
+ # ...
284
+ if line.startswith("ID_FS_UUID="):
285
+ uuid = line.split("=", 1)[1]
286
+ break
287
+ return uuid
288
+
289
+
290
+ def get_partition_uuid(module: AnsibleModule, partname : str) -> str | None:
291
+ """Get the UUID of a partition by its name."""
292
+ # TODO: NetBSD and FreeBSD can have UUIDs in /etc/fstab,
293
+ # but none of these methods work (mount always displays the label though)
294
+ for uuid in list_uuids_linux():
295
+ dev = os.path.realpath(os.path.join("/dev/disk/by-uuid", uuid))
296
+ if partname == dev:
297
+ return uuid
298
+ for dev, uuid in handle_timeout(module, default=[])(run_lsblk)(module):
299
+ if partname == dev:
300
+ return uuid
301
+ return handle_timeout(module)(get_udevadm_device_uuid)(module, partname)
302
+
303
+
304
+ def handle_timeout(module, default=None):
305
+ """Decorator to catch timeout exceptions and handle failing, warning, and ignoring the timeout."""
306
+ def decorator(func):
307
+ @functools.wraps(func)
308
+ def wrapper(*args, **kwargs):
309
+ try:
310
+ return func(*args, **kwargs)
311
+ except (subprocess.TimeoutExpired, _timeout.TimeoutError) as e:
312
+ if module.params["on_timeout"] == "error":
313
+ module.fail_json(msg=str(e))
314
+ elif module.params["on_timeout"] == "warn":
315
+ module.warn(str(e))
316
+ return default
317
+ return wrapper
318
+ return decorator
319
+
320
+
321
+ def run_mount_bin(module: AnsibleModule, mount_bin: str) -> str: # type: ignore # Missing return statement
322
+ """Execute the specified mount binary with optional timeout."""
323
+ mount_bin = module.get_bin_path(mount_bin, required=True)
324
+ try:
325
+ return handle_timeout(module, default="")(subprocess.check_output)(
326
+ mount_bin, text=True, timeout=module.params["timeout"]
327
+ )
328
+ except subprocess.CalledProcessError as e:
329
+ module.fail_json(msg=f"Failed to execute {mount_bin}: {str(e)}")
330
+
331
+
332
+ def get_mount_pattern(stdout: str):
333
+ lines = stdout.splitlines()
334
+ pattern = None
335
+ if all(LINUX_MOUNT_RE.match(line) for line in lines):
336
+ pattern = LINUX_MOUNT_RE
337
+ elif all(BSD_MOUNT_RE.match(line) for line in lines if not line.startswith("map ")):
338
+ pattern = BSD_MOUNT_RE
339
+ elif len(lines) > 2 and all(AIX_MOUNT_RE.match(line) for line in lines[2:]):
340
+ pattern = AIX_MOUNT_RE
341
+ return pattern
342
+
343
+
344
+ def gen_mounts_from_stdout(stdout: str) -> t.Iterable[MountInfo]:
345
+ """List mount dictionaries from mount stdout."""
346
+ if not (pattern := get_mount_pattern(stdout)):
347
+ stdout = ""
348
+
349
+ for line in stdout.splitlines():
350
+ if not (match := pattern.match(line)):
351
+ # AIX has a couple header lines for some reason
352
+ # MacOS "map" lines are skipped (e.g. "map auto_home on /System/Volumes/Data/home (autofs, automounted, nobrowse)")
353
+ # TODO: include MacOS lines
354
+ continue
355
+
356
+ mount = match.groupdict()["mount"]
357
+ if pattern is LINUX_MOUNT_RE:
358
+ mount_info = match.groupdict()
359
+ elif pattern is BSD_MOUNT_RE:
360
+ # the group containing fstype is comma separated, and may include whitespace
361
+ mount_info = match.groupdict()
362
+ parts = re.split(r"\s*,\s*", match.group("fstype"), 1)
363
+ if len(parts) == 1:
364
+ mount_info["fstype"] = parts[0]
365
+ else:
366
+ mount_info.update({"fstype": parts[0], "options": parts[1]})
367
+ elif pattern is AIX_MOUNT_RE:
368
+ mount_info = match.groupdict()
369
+ device = mount_info.pop("mounted")
370
+ node = mount_info.pop("node")
371
+ if device and node:
372
+ device = f"{node}:{device}"
373
+ mount_info["device"] = device
374
+
375
+ yield MountInfo(mount, line, mount_info)
376
+
377
+
378
+ def gen_fstab_entries(lines: list[str]) -> t.Iterable[MountInfo]:
379
+ """Yield tuples from /etc/fstab https://man7.org/linux/man-pages/man5/fstab.5.html.
380
+
381
+ Each tuple contains the mount point, line of origin, and the dictionary of the parsed line.
382
+ """
383
+ for line in lines:
384
+ if not (line := line.strip()) or line.startswith("#"):
385
+ continue
386
+ fields = [replace_octal_escapes(field) for field in line.split()]
387
+ mount_info: dict[str, str | int] = {
388
+ "device": fields[0],
389
+ "mount": fields[1],
390
+ "fstype": fields[2],
391
+ "options": fields[3],
392
+ }
393
+ with suppress(IndexError):
394
+ # the last two fields are optional
395
+ mount_info["dump"] = int(fields[4])
396
+ mount_info["passno"] = int(fields[5])
397
+ yield MountInfo(fields[1], line, mount_info)
398
+
399
+
400
+ def gen_vfstab_entries(lines: list[str]) -> t.Iterable[MountInfo]:
401
+ """Yield tuples from /etc/vfstab https://docs.oracle.com/cd/E36784_01/html/E36882/vfstab-4.html.
402
+
403
+ Each tuple contains the mount point, line of origin, and the dictionary of the parsed line.
404
+ """
405
+ for line in lines:
406
+ if not line.strip() or line.strip().startswith("#"):
407
+ continue
408
+ fields = line.split()
409
+ passno: str | int = fields[4]
410
+ with suppress(ValueError):
411
+ passno = int(passno)
412
+ mount_info: dict[str, str | int] = {
413
+ "device": fields[0],
414
+ "device_to_fsck": fields[1],
415
+ "mount": fields[2],
416
+ "fstype": fields[3],
417
+ "passno": passno,
418
+ "mount_at_boot": fields[5],
419
+ "options": fields[6],
420
+ }
421
+ yield MountInfo(fields[2], line, mount_info)
422
+
423
+
424
+ def list_aix_filesystems_stanzas(lines: list[str]) -> list[list[str]]:
425
+ """Parse stanzas from /etc/filesystems according to https://www.ibm.com/docs/hu/aix/7.2?topic=files-filesystems-file."""
426
+ stanzas = []
427
+ for line in lines:
428
+ if line.startswith("*") or not line.strip():
429
+ continue
430
+ if line.rstrip().endswith(":"):
431
+ stanzas.append([line])
432
+ else:
433
+ if "=" not in line:
434
+ # Expected for Linux, return an empty list since this doesn't appear to be AIX /etc/filesystems
435
+ stanzas = []
436
+ break
437
+ stanzas[-1].append(line)
438
+ return stanzas
439
+
440
+
441
+ def gen_aix_filesystems_entries(lines: list[str]) -> t.Iterable[MountInfoOptions]:
442
+ """Yield tuples from /etc/filesystems https://www.ibm.com/docs/hu/aix/7.2?topic=files-filesystems-file.
443
+
444
+ Each tuple contains the mount point, lines of origin, and the dictionary of the parsed lines.
445
+ """
446
+ for stanza in list_aix_filesystems_stanzas(lines):
447
+ original = "\n".join(stanza)
448
+ mount = stanza.pop(0)[:-1] # snip trailing :
449
+ mount_info: dict[str, str] = {}
450
+ for line in stanza:
451
+ attr, value = line.split("=", 1)
452
+ mount_info[attr.strip()] = value.strip()
453
+
454
+ device = ""
455
+ if (nodename := mount_info.get("nodename")):
456
+ device = nodename
457
+ if (dev := mount_info.get("dev")):
458
+ if device:
459
+ device += ":"
460
+ device += dev
461
+
462
+ normalized_fields: dict[str, str | dict[str, str]] = {
463
+ "mount": mount,
464
+ "device": device or "unknown",
465
+ "fstype": mount_info.get("vfs") or "unknown",
466
+ # avoid clobbering the mount point with the AIX mount option "mount"
467
+ "attributes": mount_info,
468
+ }
469
+ yield MountInfoOptions(mount, original, normalized_fields)
470
+
471
+
472
+ def gen_mnttab_entries(lines: list[str]) -> t.Iterable[MountInfo]:
473
+ """Yield tuples from /etc/mnttab columns https://docs.oracle.com/cd/E36784_01/html/E36882/mnttab-4.html.
474
+
475
+ Each tuple contains the mount point, line of origin, and the dictionary of the parsed line.
476
+ """
477
+ if not any(len(fields[4]) == 10 for line in lines for fields in [line.split()]):
478
+ raise ValueError
479
+ for line in lines:
480
+ fields = line.split()
481
+ datetime.date.fromtimestamp(int(fields[4]))
482
+ mount_info: dict[str, str | int] = {
483
+ "device": fields[0],
484
+ "mount": fields[1],
485
+ "fstype": fields[2],
486
+ "options": fields[3],
487
+ "time": int(fields[4]),
488
+ }
489
+ yield MountInfo(fields[1], line, mount_info)
490
+
491
+
492
+ def gen_mounts_by_file(file: str) -> t.Iterable[MountInfo | MountInfoOptions]:
493
+ """Yield parsed mount entries from the first successful generator.
494
+
495
+ Generators are tried in the following order to minimize false positives:
496
+ - /etc/vfstab: 7 columns
497
+ - /etc/mnttab: 5 columns (mnttab[4] must contain UNIX timestamp)
498
+ - /etc/fstab: 4-6 columns (fstab[4] is optional and historically 0-9, but can be any int)
499
+ - /etc/filesystems: multi-line, not column-based, and specific to AIX
500
+ """
501
+ if (lines := get_file_content(file, "").splitlines()):
502
+ for gen_mounts in [gen_vfstab_entries, gen_mnttab_entries, gen_fstab_entries, gen_aix_filesystems_entries]:
503
+ with suppress(IndexError, ValueError):
504
+ # mpypy error: misc: Incompatible types in "yield from" (actual type "object", expected type "Union[MountInfo, MountInfoOptions]
505
+ # only works if either
506
+ # * the list of functions excludes gen_aix_filesystems_entries
507
+ # * the list of functions only contains gen_aix_filesystems_entries
508
+ yield from list(gen_mounts(lines)) # type: ignore[misc]
509
+ break
510
+
511
+
512
+ def get_sources(module: AnsibleModule) -> list[str]:
513
+ """Return a list of filenames from the requested sources."""
514
+ sources: list[str] = []
515
+ for source in module.params["sources"] or ["all"]:
516
+ if not source:
517
+ module.fail_json(msg="sources contains an empty string")
518
+
519
+ if source in {"dynamic", "all"}:
520
+ sources.extend(DYNAMIC_SOURCES)
521
+ if source in {"static", "all"}:
522
+ sources.extend(STATIC_SOURCES)
523
+
524
+ elif source not in {"static", "dynamic", "all"}:
525
+ sources.append(source)
526
+ return sources
527
+
528
+
529
+ def gen_mounts_by_source(module: AnsibleModule):
530
+ """Iterate over the sources and yield tuples containing the source, mount point, source line(s), and the parsed result."""
531
+ sources = get_sources(module)
532
+
533
+ if len(set(sources)) < len(sources):
534
+ module.warn(f"mount_facts option 'sources' contains duplicate entries, repeat sources will be ignored: {sources}")
535
+
536
+ mount_fallback = module.params["mount_binary"] and set(sources).intersection(DYNAMIC_SOURCES)
537
+
538
+ seen = set()
539
+ for source in sources:
540
+ if source in seen or (real_source := os.path.realpath(source)) in seen:
541
+ continue
542
+
543
+ if source == "mount":
544
+ seen.add(source)
545
+ stdout = run_mount_bin(module, module.params["mount_binary"])
546
+ results = [(source, *astuple(mount_info)) for mount_info in gen_mounts_from_stdout(stdout)]
547
+ else:
548
+ seen.add(real_source)
549
+ results = [(source, *astuple(mount_info)) for mount_info in gen_mounts_by_file(source)]
550
+
551
+ if results and source in ("mount", *DYNAMIC_SOURCES):
552
+ mount_fallback = False
553
+
554
+ yield from results
555
+
556
+ if mount_fallback:
557
+ stdout = run_mount_bin(module, module.params["mount_binary"])
558
+ yield from [("mount", *astuple(mount_info)) for mount_info in gen_mounts_from_stdout(stdout)]
559
+
560
+
561
+ def get_mount_facts(module: AnsibleModule):
562
+ """List and filter mounts, returning all mounts for each unique source."""
563
+ seconds = module.params["timeout"]
564
+ mounts = []
565
+ for source, mount, origin, fields in gen_mounts_by_source(module):
566
+ device = fields["device"]
567
+ fstype = fields["fstype"]
568
+
569
+ # Convert UUIDs in Linux /etc/fstab to device paths
570
+ # TODO need similar for OpenBSD which lists UUIDS (without the UUID= prefix) in /etc/fstab, needs another approach though.
571
+ uuid = None
572
+ if device.startswith("UUID="):
573
+ uuid = device.split("=", 1)[1]
574
+ device = get_device_by_uuid(module, uuid) or device
575
+
576
+ if not any(fnmatch(device, pattern) for pattern in module.params["devices"] or ["*"]):
577
+ continue
578
+ if not any(fnmatch(fstype, pattern) for pattern in module.params["fstypes"] or ["*"]):
579
+ continue
580
+
581
+ timed_func = _timeout.timeout(seconds, f"Timed out getting mount size for mount {mount} (type {fstype})")(get_mount_size)
582
+ if mount_size := handle_timeout(module)(timed_func)(mount):
583
+ fields.update(mount_size)
584
+
585
+ if uuid is None:
586
+ with suppress(subprocess.CalledProcessError):
587
+ uuid = get_partition_uuid(module, device)
588
+
589
+ fields.update({"ansible_context": {"source": source, "source_data": origin}, "uuid": uuid})
590
+ mounts.append(fields)
591
+
592
+ return mounts
593
+
594
+
595
+ def handle_deduplication(module, mounts):
596
+ """Return the unique mount points from the complete list of mounts, and handle the optional aggregate results."""
597
+ mount_points = {}
598
+ mounts_by_source = {}
599
+ for mount in mounts:
600
+ mount_point = mount["mount"]
601
+ source = mount["ansible_context"]["source"]
602
+ if mount_point not in mount_points:
603
+ mount_points[mount_point] = mount
604
+ mounts_by_source.setdefault(source, []).append(mount_point)
605
+
606
+ duplicates_by_src = {src: mnts for src, mnts in mounts_by_source.items() if len(set(mnts)) != len(mnts)}
607
+ if duplicates_by_src and module.params["include_aggregate_mounts"] is None:
608
+ duplicates_by_src = {src: mnts for src, mnts in mounts_by_source.items() if len(set(mnts)) != len(mnts)}
609
+ duplicates_str = ", ".join([f"{src} ({duplicates})" for src, duplicates in duplicates_by_src.items()])
610
+ module.warn(f"mount_facts: ignoring repeat mounts in the following sources: {duplicates_str}. "
611
+ "You can disable this warning by configuring the 'include_aggregate_mounts' option as True or False.")
612
+
613
+ if module.params["include_aggregate_mounts"]:
614
+ aggregate_mounts = mounts
615
+ else:
616
+ aggregate_mounts = []
617
+
618
+ return mount_points, aggregate_mounts
619
+
620
+
621
+ def get_argument_spec():
622
+ """Helper returning the argument spec."""
623
+ return dict(
624
+ sources=dict(type="list", elements="str", default=None),
625
+ mount_binary=dict(default="mount", type="raw"),
626
+ devices=dict(type="list", elements="str", default=None),
627
+ fstypes=dict(type="list", elements="str", default=None),
628
+ timeout=dict(type="float"),
629
+ on_timeout=dict(choices=["error", "warn", "ignore"], default="error"),
630
+ include_aggregate_mounts=dict(default=None, type="bool"),
631
+ )
632
+
633
+
634
+ def main():
635
+ module = AnsibleModule(
636
+ argument_spec=get_argument_spec(),
637
+ supports_check_mode=True,
638
+ )
639
+ if (seconds := module.params["timeout"]) is not None and seconds <= 0:
640
+ module.fail_json(msg=f"argument 'timeout' must be a positive number or null, not {seconds}")
641
+ if (mount_binary := module.params["mount_binary"]) is not None and not isinstance(mount_binary, str):
642
+ module.fail_json(msg=f"argument 'mount_binary' must be a string or null, not {mount_binary}")
643
+
644
+ mounts = get_mount_facts(module)
645
+ mount_points, aggregate_mounts = handle_deduplication(module, mounts)
646
+
647
+ module.exit_json(ansible_facts={"mount_points": mount_points, "aggregate_mounts": aggregate_mounts})
648
+
649
+
650
+ if __name__ == "__main__":
651
+ main()