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.
- ansible/__main__.py +2 -17
- ansible/cli/__init__.py +3 -15
- ansible/cli/config.py +187 -24
- ansible/cli/console.py +1 -1
- ansible/cli/doc.py +38 -16
- ansible/cli/galaxy.py +3 -49
- ansible/cli/inventory.py +2 -2
- ansible/cli/pull.py +2 -2
- ansible/cli/scripts/ansible_connection_cli_stub.py +1 -10
- ansible/config/base.yml +127 -57
- ansible/config/manager.py +89 -11
- ansible/constants.py +32 -9
- ansible/errors/__init__.py +5 -0
- ansible/executor/interpreter_discovery.py +1 -1
- ansible/executor/play_iterator.py +16 -0
- ansible/executor/playbook_executor.py +1 -4
- ansible/executor/powershell/become_wrapper.ps1 +4 -5
- ansible/executor/powershell/bootstrap_wrapper.ps1 +2 -3
- ansible/executor/powershell/exec_wrapper.ps1 +1 -1
- ansible/executor/powershell/module_manifest.py +2 -2
- ansible/executor/task_executor.py +50 -39
- ansible/executor/task_queue_manager.py +1 -1
- ansible/executor/task_result.py +1 -1
- ansible/galaxy/api.py +3 -4
- ansible/galaxy/collection/__init__.py +21 -10
- ansible/galaxy/collection/concrete_artifact_manager.py +2 -2
- ansible/galaxy/collection/galaxy_api_proxy.py +10 -16
- ansible/galaxy/collection/gpg.py +17 -23
- ansible/galaxy/data/COPYING +7 -0
- ansible/galaxy/data/apb/Dockerfile.j2 +1 -0
- ansible/galaxy/data/apb/Makefile.j2 +1 -0
- ansible/galaxy/data/apb/README.md +7 -3
- ansible/galaxy/data/apb/apb.yml.j2 +1 -0
- ansible/galaxy/data/apb/defaults/main.yml.j2 +1 -0
- ansible/galaxy/data/apb/handlers/main.yml.j2 +1 -0
- ansible/galaxy/data/apb/meta/main.yml.j2 +1 -0
- ansible/galaxy/data/apb/playbooks/deprovision.yml.j2 +1 -0
- ansible/galaxy/data/apb/playbooks/provision.yml.j2 +1 -0
- ansible/galaxy/data/apb/tasks/main.yml.j2 +1 -0
- ansible/galaxy/data/apb/tests/ansible.cfg +1 -0
- ansible/galaxy/data/apb/tests/inventory +1 -0
- ansible/galaxy/data/apb/tests/test.yml.j2 +1 -0
- ansible/galaxy/data/apb/vars/main.yml.j2 +1 -0
- ansible/galaxy/data/collections_galaxy_meta.yml +1 -0
- ansible/galaxy/data/container/defaults/main.yml.j2 +1 -0
- ansible/galaxy/data/container/handlers/main.yml.j2 +1 -0
- ansible/galaxy/data/container/meta/container.yml.j2 +1 -0
- ansible/galaxy/data/container/meta/main.yml.j2 +1 -0
- ansible/galaxy/data/container/tasks/main.yml.j2 +1 -0
- ansible/galaxy/data/container/tests/ansible.cfg +1 -0
- ansible/galaxy/data/container/tests/inventory +1 -0
- ansible/galaxy/data/container/tests/test.yml.j2 +1 -0
- ansible/galaxy/data/container/vars/main.yml.j2 +1 -0
- ansible/galaxy/data/default/collection/README.md.j2 +1 -0
- ansible/galaxy/data/default/collection/galaxy.yml.j2 +1 -0
- ansible/galaxy/data/default/collection/meta/runtime.yml +1 -0
- ansible/galaxy/data/default/collection/plugins/README.md.j2 +1 -0
- ansible/galaxy/data/default/role/defaults/main.yml.j2 +1 -0
- ansible/galaxy/data/default/role/handlers/main.yml.j2 +1 -0
- ansible/galaxy/data/default/role/meta/main.yml.j2 +1 -0
- ansible/galaxy/data/default/role/tasks/main.yml.j2 +1 -0
- ansible/galaxy/data/default/role/tests/inventory +1 -0
- ansible/galaxy/data/default/role/tests/test.yml.j2 +1 -0
- ansible/galaxy/data/default/role/vars/main.yml.j2 +1 -0
- ansible/galaxy/data/network/cliconf_plugins/example.py.j2 +1 -0
- ansible/galaxy/data/network/defaults/main.yml.j2 +1 -0
- ansible/galaxy/data/network/library/example_command.py.j2 +1 -0
- ansible/galaxy/data/network/library/example_config.py.j2 +1 -0
- ansible/galaxy/data/network/library/example_facts.py.j2 +1 -0
- ansible/galaxy/data/network/meta/main.yml.j2 +1 -0
- ansible/galaxy/data/network/module_utils/example.py.j2 +1 -0
- ansible/galaxy/data/network/netconf_plugins/example.py.j2 +1 -0
- ansible/galaxy/data/network/tasks/main.yml.j2 +1 -0
- ansible/galaxy/data/network/terminal_plugins/example.py.j2 +1 -0
- ansible/galaxy/data/network/tests/inventory +1 -0
- ansible/galaxy/data/network/tests/test.yml.j2 +1 -0
- ansible/galaxy/data/network/vars/main.yml.j2 +1 -0
- ansible/galaxy/dependency_resolution/providers.py +3 -3
- ansible/galaxy/role.py +1 -1
- ansible/galaxy/token.py +20 -8
- ansible/keyword_desc.yml +1 -1
- ansible/module_utils/_internal/__init__.py +0 -0
- ansible/module_utils/_internal/_concurrent/__init__.py +0 -0
- ansible/module_utils/_internal/_concurrent/_daemon_threading.py +28 -0
- ansible/module_utils/_internal/_concurrent/_futures.py +21 -0
- ansible/module_utils/ansible_release.py +2 -2
- ansible/module_utils/api.py +2 -2
- ansible/module_utils/basic.py +8 -8
- ansible/module_utils/common/collections.py +1 -1
- ansible/module_utils/common/file.py +0 -6
- ansible/module_utils/common/process.py +22 -9
- ansible/module_utils/common/text/converters.py +5 -8
- ansible/module_utils/common/text/formatters.py +20 -4
- ansible/module_utils/common/validation.py +33 -25
- ansible/module_utils/compat/paramiko.py +6 -1
- ansible/module_utils/compat/selinux.py +2 -2
- ansible/module_utils/connection.py +8 -24
- ansible/module_utils/csharp/Ansible.Become.cs +14 -25
- ansible/module_utils/csharp/Ansible.Process.cs +1 -1
- ansible/module_utils/distro/__init__.py +1 -1
- ansible/module_utils/distro/_distro.py +8 -4
- ansible/module_utils/facts/collector.py +2 -0
- ansible/module_utils/facts/default_collectors.py +3 -1
- ansible/module_utils/facts/hardware/aix.py +54 -52
- ansible/module_utils/facts/hardware/darwin.py +37 -34
- ansible/module_utils/facts/hardware/freebsd.py +55 -15
- ansible/module_utils/facts/hardware/hpux.py +3 -0
- ansible/module_utils/facts/hardware/linux.py +101 -57
- ansible/module_utils/facts/hardware/netbsd.py +3 -0
- ansible/module_utils/facts/hardware/openbsd.py +4 -1
- ansible/module_utils/facts/hardware/sunos.py +7 -1
- ansible/module_utils/facts/network/aix.py +16 -17
- ansible/module_utils/facts/network/fc_wwn.py +4 -1
- ansible/module_utils/facts/network/hpux.py +21 -4
- ansible/module_utils/facts/network/iscsi.py +7 -8
- ansible/module_utils/facts/network/linux.py +0 -2
- ansible/module_utils/facts/other/facter.py +9 -4
- ansible/module_utils/facts/other/ohai.py +5 -5
- ansible/module_utils/facts/packages.py +49 -7
- ansible/module_utils/facts/sysctl.py +33 -31
- ansible/module_utils/facts/system/distribution.py +1 -1
- ansible/module_utils/facts/system/local.py +12 -22
- ansible/module_utils/facts/system/service_mgr.py +3 -1
- ansible/module_utils/facts/system/systemd.py +47 -0
- ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
- ansible/module_utils/splitter.py +1 -1
- ansible/modules/add_host.py +1 -1
- ansible/modules/apt.py +43 -32
- ansible/modules/apt_key.py +6 -6
- ansible/modules/apt_repository.py +23 -14
- ansible/modules/assemble.py +7 -2
- ansible/modules/assert.py +4 -4
- ansible/modules/blockinfile.py +3 -6
- ansible/modules/command.py +1 -1
- ansible/modules/copy.py +4 -4
- ansible/modules/cron.py +13 -10
- ansible/modules/deb822_repository.py +16 -17
- ansible/modules/debconf.py +9 -9
- ansible/modules/debug.py +1 -1
- ansible/modules/dnf.py +79 -164
- ansible/modules/dnf5.py +54 -29
- ansible/modules/dpkg_selections.py +2 -2
- ansible/modules/expect.py +2 -2
- ansible/modules/fetch.py +2 -2
- ansible/modules/file.py +5 -3
- ansible/modules/find.py +40 -12
- ansible/modules/gather_facts.py +4 -2
- ansible/modules/get_url.py +29 -24
- ansible/modules/git.py +35 -35
- ansible/modules/group.py +71 -1
- ansible/modules/hostname.py +2 -4
- ansible/modules/include_vars.py +5 -5
- ansible/modules/iptables.py +13 -16
- ansible/modules/known_hosts.py +16 -13
- ansible/modules/lineinfile.py +1 -4
- ansible/modules/meta.py +6 -1
- ansible/modules/mount_facts.py +651 -0
- ansible/modules/package_facts.py +63 -80
- ansible/modules/pause.py +4 -3
- ansible/modules/pip.py +14 -14
- ansible/modules/replace.py +1 -4
- ansible/modules/rpm_key.py +31 -11
- ansible/modules/service.py +8 -8
- ansible/modules/service_facts.py +20 -5
- ansible/modules/set_stats.py +1 -1
- ansible/modules/setup.py +3 -3
- ansible/modules/stat.py +3 -3
- ansible/modules/subversion.py +1 -1
- ansible/modules/systemd.py +16 -10
- ansible/modules/systemd_service.py +16 -10
- ansible/modules/sysvinit.py +4 -4
- ansible/modules/unarchive.py +35 -22
- ansible/modules/uri.py +24 -18
- ansible/modules/user.py +145 -12
- ansible/modules/validate_argument_spec.py +3 -3
- ansible/modules/wait_for_connection.py +2 -1
- ansible/modules/yum_repository.py +136 -179
- ansible/parsing/dataloader.py +2 -2
- ansible/parsing/mod_args.py +11 -10
- ansible/parsing/vault/__init__.py +8 -3
- ansible/parsing/yaml/constructor.py +10 -8
- ansible/parsing/yaml/objects.py +1 -1
- ansible/playbook/base.py +12 -23
- ansible/playbook/helpers.py +4 -0
- ansible/playbook/loop_control.py +8 -0
- ansible/playbook/play.py +4 -22
- ansible/playbook/play_context.py +0 -16
- ansible/playbook/playbook_include.py +2 -2
- ansible/playbook/role/__init__.py +2 -2
- ansible/plugins/__init__.py +2 -0
- ansible/plugins/action/__init__.py +7 -9
- ansible/plugins/action/dnf.py +7 -5
- ansible/plugins/action/package.py +5 -4
- ansible/plugins/action/reboot.py +2 -2
- ansible/plugins/become/__init__.py +1 -1
- ansible/plugins/callback/__init__.py +44 -3
- ansible/plugins/callback/default.py +1 -1
- ansible/plugins/cliconf/__init__.py +1 -1
- ansible/plugins/connection/paramiko_ssh.py +2 -80
- ansible/plugins/connection/psrp.py +33 -82
- ansible/plugins/connection/ssh.py +0 -8
- ansible/plugins/connection/winrm.py +46 -1
- ansible/plugins/doc_fragments/connection_pipelining.py +2 -2
- ansible/plugins/doc_fragments/constructed.py +10 -10
- ansible/plugins/doc_fragments/default_callback.py +8 -8
- ansible/plugins/doc_fragments/files.py +5 -5
- ansible/plugins/doc_fragments/inventory_cache.py +2 -2
- ansible/plugins/doc_fragments/result_format_callback.py +6 -6
- ansible/plugins/doc_fragments/return_common.py +1 -1
- ansible/plugins/doc_fragments/shell_common.py +2 -10
- ansible/plugins/doc_fragments/shell_windows.py +0 -9
- ansible/plugins/doc_fragments/url.py +2 -2
- ansible/plugins/doc_fragments/url_windows.py +4 -5
- ansible/plugins/doc_fragments/validate.py +1 -1
- ansible/plugins/filter/core.py +2 -0
- ansible/plugins/filter/human_to_bytes.yml +9 -0
- ansible/plugins/filter/password_hash.yml +1 -1
- ansible/plugins/filter/strftime.yml +1 -1
- ansible/plugins/filter/to_nice_json.yml +7 -3
- ansible/plugins/filter/to_uuid.yml +1 -1
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +1 -1
- ansible/plugins/loader.py +0 -11
- ansible/plugins/lookup/config.py +1 -1
- ansible/plugins/lookup/csvfile.py +21 -9
- ansible/plugins/lookup/env.py +8 -9
- ansible/plugins/lookup/ini.py +10 -1
- ansible/plugins/lookup/random_choice.py +2 -2
- ansible/plugins/lookup/url.py +7 -2
- ansible/plugins/shell/__init__.py +15 -20
- ansible/plugins/shell/powershell.py +9 -6
- ansible/plugins/strategy/__init__.py +16 -7
- ansible/plugins/test/core.py +23 -1
- ansible/plugins/test/issubset.yml +1 -1
- ansible/plugins/test/subset.yml +1 -1
- ansible/plugins/test/timedout.yml +20 -0
- ansible/plugins/test/vault_encrypted.yml +6 -6
- ansible/plugins/test/vaulted_file.yml +19 -0
- ansible/release.py +2 -2
- ansible/template/__init__.py +3 -8
- ansible/utils/collection_loader/_collection_finder.py +23 -55
- ansible/utils/display.py +44 -31
- ansible/utils/jsonrpc.py +1 -1
- ansible/utils/listify.py +1 -5
- ansible/utils/path.py +3 -0
- ansible/utils/vars.py +18 -27
- ansible/vars/manager.py +7 -150
- ansible/vars/plugins.py +1 -1
- ansible_core-2.18.0.dist-info/Apache-License.txt +202 -0
- {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/METADATA +36 -23
- ansible_core-2.18.0.dist-info/MIT-license.txt +14 -0
- ansible_core-2.18.0.dist-info/PSF-license.txt +48 -0
- {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/RECORD +316 -311
- {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/entry_points.txt +1 -1
- ansible_core-2.18.0.dist-info/simplified_bsd.txt +8 -0
- ansible_test/_data/completion/docker.txt +7 -7
- ansible_test/_data/completion/remote.txt +5 -4
- ansible_test/_data/completion/windows.txt +4 -4
- ansible_test/_data/requirements/ansible-test.txt +1 -2
- ansible_test/_data/requirements/constraints.txt +1 -2
- ansible_test/_data/requirements/sanity.ansible-doc.txt +3 -3
- ansible_test/_data/requirements/sanity.changelog.in +1 -1
- ansible_test/_data/requirements/sanity.changelog.txt +4 -4
- ansible_test/_data/requirements/sanity.import.plugin.txt +2 -2
- ansible_test/_data/requirements/sanity.import.txt +1 -1
- ansible_test/_data/requirements/sanity.integration-aliases.txt +1 -1
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +6 -8
- ansible_test/_data/requirements/sanity.runtime-metadata.txt +2 -2
- ansible_test/_data/requirements/sanity.validate-modules.txt +3 -3
- ansible_test/_data/requirements/sanity.yamllint.in +1 -0
- ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
- ansible_test/_internal/ansible_util.py +8 -35
- ansible_test/_internal/ci/azp.py +1 -1
- ansible_test/_internal/classification/__init__.py +0 -2
- ansible_test/_internal/cli/parsers/key_value_parsers.py +3 -0
- ansible_test/_internal/commands/integration/cloud/hcloud.py +1 -1
- ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
- ansible_test/_internal/commands/integration/cloud/nios.py +1 -1
- ansible_test/_internal/commands/sanity/__init__.py +96 -19
- ansible_test/_internal/commands/sanity/pylint.py +20 -24
- ansible_test/_internal/completion.py +2 -0
- ansible_test/_internal/constants.py +0 -1
- ansible_test/_internal/coverage_util.py +1 -2
- ansible_test/_internal/docker_util.py +1 -1
- ansible_test/_internal/encoding.py +4 -4
- ansible_test/_internal/host_configs.py +10 -0
- ansible_test/_internal/host_profiles.py +9 -13
- ansible_test/_internal/pypi_proxy.py +1 -1
- ansible_test/_internal/python_requirements.py +5 -14
- ansible_test/_internal/timeout.py +1 -1
- ansible_test/_internal/util.py +40 -0
- ansible_test/_internal/util_common.py +5 -1
- ansible_test/_util/controller/sanity/code-smell/action-plugin-docs.json +3 -1
- ansible_test/_util/controller/sanity/code-smell/action-plugin-docs.py +6 -3
- ansible_test/_util/controller/sanity/code-smell/empty-init.json +0 -2
- ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +5 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +5 -0
- ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +5 -0
- ansible_test/_util/controller/sanity/pylint/config/collection.cfg +6 -0
- ansible_test/_util/controller/sanity/pylint/config/default.cfg +6 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +1 -19
- ansible_test/_util/controller/sanity/shellcheck/exclude.txt +1 -0
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +67 -2
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +27 -5
- ansible_test/_util/target/cli/ansible_test_cli_stub.py +0 -0
- ansible_test/_util/target/common/constants.py +2 -2
- ansible_test/_util/target/injector/python.py +5 -0
- ansible_test/_util/target/pytest/plugins/ansible_pytest_coverage.py +6 -0
- ansible_test/_util/target/sanity/import/importer.py +1 -1
- ansible_test/_util/target/setup/bootstrap.sh +6 -17
- ansible_test/_util/target/setup/requirements.py +18 -24
- ansible_test/config/config.yml +1 -1
- ansible_core-2.17.6rc1.data/scripts/ansible-test +0 -44
- ansible_test/_data/requirements/sanity.mypy.in +0 -10
- ansible_test/_data/requirements/sanity.mypy.txt +0 -18
- ansible_test/_internal/commands/sanity/mypy.py +0 -274
- ansible_test/_util/controller/sanity/mypy/ansible-core.ini +0 -116
- ansible_test/_util/controller/sanity/mypy/ansible-test.ini +0 -27
- ansible_test/_util/controller/sanity/mypy/modules.ini +0 -92
- ansible_test/_util/controller/sanity/mypy/packaging.ini +0 -20
- {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/COPYING +0 -0
- {ansible_core-2.17.6rc1.dist-info → ansible_core-2.18.0.dist-info}/WHEEL +0 -0
- {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()
|