From 40ade1f84b8bb10a63576b0ac320c13f57c87d34 Mon Sep 17 00:00:00 2001 From: Sloane Hertel <19572925+s-hertel@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:05:05 -0400 Subject: [PATCH] Add mount_facts module (#83508) * Add a mount_facts module capable of gathering mounts skipped by default fact gathering * By default, collect mount facts from standard locations including /etc/mtab, /proc/mounts, /etc/fstab, /etc/mnttab, /etc/vfstab, and on AIX, /etc/filesystems. When no file-based source for the current mounts can be found (like /proc/mounts), the module falls back to using mount as a source. This allows BSD and AIX to collect the existing mounts by default, without causing Linux hosts to use both /proc/mounts and mount output. * Non-standard locations and "mount" can be configured as a sources. * Support returning an aggregate list of mount points in addition to first found. When there are multiple mounts for the same mount point in an individual source, a warning is given if the include_aggregate_mounts option is not configured. * Add options to filter on fstypes and devices (supporting UNIX shell wildcards). * Support configuring a timeout and timeout behavior to make it easier to use the module as a default facts module without risking a hang. * Include the source and line(s) corresponding to a mount for easier debugging. Co-authored-by: Brian Coca Co-authored-by: Matt Clay Co-authored-by: Matt Davis <6775756+nitzmahone@users.noreply.github.com> --- changelogs/fragments/83508_mount_facts.yml | 2 + lib/ansible/modules/mount_facts.py | 651 ++++++++++++++++++ test/integration/targets/mount_facts/aliases | 5 + .../targets/mount_facts/meta/main.yml | 2 + .../targets/mount_facts/tasks/main.yml | 193 ++++++ test/units/modules/mount_facts_data.py | 600 ++++++++++++++++ test/units/modules/test_mount_facts.py | 394 +++++++++++ 7 files changed, 1847 insertions(+) create mode 100644 changelogs/fragments/83508_mount_facts.yml create mode 100644 lib/ansible/modules/mount_facts.py create mode 100644 test/integration/targets/mount_facts/aliases create mode 100644 test/integration/targets/mount_facts/meta/main.yml create mode 100644 test/integration/targets/mount_facts/tasks/main.yml create mode 100644 test/units/modules/mount_facts_data.py create mode 100644 test/units/modules/test_mount_facts.py diff --git a/changelogs/fragments/83508_mount_facts.yml b/changelogs/fragments/83508_mount_facts.yml new file mode 100644 index 00000000000..baa7e592b18 --- /dev/null +++ b/changelogs/fragments/83508_mount_facts.yml @@ -0,0 +1,2 @@ +minor_changes: + - Add a new mount_facts module to support gathering information about mounts that are excluded by default fact gathering. diff --git a/lib/ansible/modules/mount_facts.py b/lib/ansible/modules/mount_facts.py new file mode 100644 index 00000000000..5982ae580ae --- /dev/null +++ b/lib/ansible/modules/mount_facts.py @@ -0,0 +1,651 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import annotations + + +DOCUMENTATION = """ +--- +module: mount_facts +version_added: 2.18 +short_description: Retrieve mount information. +description: + - Retrieve information about mounts from preferred sources and filter the results based on the filesystem type and device. +options: + devices: + description: A list of fnmatch patterns to filter mounts by the special device or remote file system. + default: ~ + type: list + elements: str + fstypes: + description: A list of fnmatch patterns to filter mounts by the type of the file system. + default: ~ + type: list + elements: str + sources: + description: + - A list of sources used to determine the mounts. Missing file sources (or empty files) are skipped. Repeat sources, including symlinks, are skipped. + - The C(mount_points) return value contains the first definition found for a mount point. + - Additional mounts to the same mount point are available from C(aggregate_mounts) (if enabled). + - By default, mounts are retrieved from all of the standard locations, which have the predefined aliases V(all)/V(static)/V(dynamic). + - V(all) contains V(dynamic) and V(static). + - V(dynamic) contains V(/etc/mtab), V(/proc/mounts), V(/etc/mnttab), and the value of O(mount_binary) if it is not None. + This allows platforms like BSD or AIX, which don't have an equivalent to V(/proc/mounts), to collect the current mounts by default. + See the O(mount_binary) option to disable the fall back or configure a different executable. + - V(static) contains V(/etc/fstab), V(/etc/vfstab), and V(/etc/filesystems). + Note that V(/etc/filesystems) is specific to AIX. The Linux file by this name has a different format/purpose and is ignored. + - The value of O(mount_binary) can be configured as a source, which will cause it to always execute. + Depending on the other sources configured, this could be inefficient/redundant. + For example, if V(/proc/mounts) and V(mount) are listed as O(sources), Linux hosts will retrieve the same mounts twice. + default: ~ + type: list + elements: str + mount_binary: + description: + - The O(mount_binary) is used if O(sources) contain the value "mount", or if O(sources) contains a dynamic + source, and none were found (as can be expected on BSD or AIX hosts). + - Set to V(null) to stop after no dynamic file source is found instead. + type: raw + default: mount + timeout: + description: + - This is the maximum number of seconds to wait for each mount to complete. When this is V(null), wait indefinitely. + - Configure in conjunction with O(on_timeout) to skip unresponsive mounts. + - This timeout also applies to the O(mount_binary) command to list mounts. + - 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). + type: float + on_timeout: + description: + - The action to take when gathering mount information exceeds O(timeout). + type: str + default: error + choices: + - error + - warn + - ignore + include_aggregate_mounts: + description: + - Whether or not the module should return the C(aggregate_mounts) list in C(ansible_facts). + - When this is V(null), a warning will be emitted if multiple mounts for the same mount point are found. + default: ~ + type: bool +extends_documentation_fragment: + - action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: none + platform: + platforms: posix +author: + - Ansible Core Team + - Sloane Hertel (@s-hertel) +""" + +EXAMPLES = """ +- name: Get non-local devices + mount_facts: + devices: "[!/]*" + +- name: Get FUSE subtype mounts + mount_facts: + fstypes: + - "fuse.*" + +- name: Get NFS mounts during gather_facts with timeout + hosts: all + gather_facts: true + vars: + ansible_facts_modules: + - ansible.builtin.mount_facts + module_default: + ansible.builtin.mount_facts: + timeout: 10 + fstypes: + - nfs + - nfs4 + +- name: Get mounts from a non-default location + mount_facts: + sources: + - /usr/etc/fstab + +- name: Get mounts from the mount binary + mount_facts: + sources: + - mount + mount_binary: /sbin/mount +""" + +RETURN = """ +ansible_facts: + description: + - An ansible_facts dictionary containing a dictionary of C(mount_points) and list of C(aggregate_mounts) when enabled. + - Each key in C(mount_points) is a mount point, and the value contains mount information (similar to C(ansible_facts["mounts"])). + Each value also contains the key C(ansible_context), with details about the source and line(s) corresponding to the parsed mount point. + - When C(aggregate_mounts) are included, the containing dictionaries are the same format as the C(mount_point) values. + returned: on success + type: dict + sample: + mount_points: + /proc/sys/fs/binfmt_misc: + ansible_context: + source: /proc/mounts + 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" + block_available: 0 + block_size: 4096 + block_total: 0 + block_used: 0 + device: "systemd-1" + dump: 0 + fstype: "autofs" + inode_available: 0 + inode_total: 0 + inode_used: 0 + mount: "/proc/sys/fs/binfmt_misc" + options: "rw,relatime,fd=33,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=33850" + passno: 0 + size_available: 0 + size_total: 0 + uuid: null + aggregate_mounts: + - ansible_context: + source: /proc/mounts + 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" + block_available: 0 + block_size: 4096 + block_total: 0 + block_used: 0 + device: "systemd-1" + dump: 0 + fstype: "autofs" + inode_available: 0 + inode_total: 0 + inode_used: 0 + mount: "/proc/sys/fs/binfmt_misc" + options: "rw,relatime,fd=33,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=33850" + passno: 0 + size_available: 0 + size_total: 0 + uuid: null + - ansible_context: + source: /proc/mounts + source_data: "binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc rw,nosuid,nodev,noexec,relatime 0 0" + block_available: 0 + block_size: 4096 + block_total: 0 + block_used: 0 + device: binfmt_misc + dump: 0 + fstype: binfmt_misc + inode_available: 0 + inode_total: 0 + inode_used: 0 + mount: "/proc/sys/fs/binfmt_misc" + options: "rw,nosuid,nodev,noexec,relatime" + passno: 0 + size_available: 0 + size_total: 0 + uuid: null +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.facts import timeout as _timeout +from ansible.module_utils.facts.utils import get_mount_size, get_file_content + +from contextlib import suppress +from dataclasses import astuple, dataclass +from fnmatch import fnmatch + +import codecs +import datetime +import functools +import os +import re +import subprocess +import typing as t + +STATIC_SOURCES = ["/etc/fstab", "/etc/vfstab", "/etc/filesystems"] +DYNAMIC_SOURCES = ["/etc/mtab", "/proc/mounts", "/etc/mnttab"] + +# AIX and BSD don't have a file-based dynamic source, so the module also supports running a mount binary to collect these. +# Pattern for Linux, including OpenBSD and NetBSD +LINUX_MOUNT_RE = re.compile(r"^(?P\S+) on (?P\S+) type (?P\S+) \((?P.+)\)$") +# Pattern for other BSD including FreeBSD, DragonFlyBSD, and MacOS +BSD_MOUNT_RE = re.compile(r"^(?P\S+) on (?P\S+) \((?P.+)\)$") +# Pattern for AIX, example in https://www.ibm.com/docs/en/aix/7.2?topic=m-mount-command +AIX_MOUNT_RE = re.compile(r"^(?P\S*)\s+(?P\S+)\s+(?P\S+)\s+(?P\S+)\s+(?P