#!/usr/bin/python # Copyright (c) 2017, Kairo Araujo # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations DOCUMENTATION = r""" author: - Kairo Araujo (@kairoaraujo) module: aix_filesystem short_description: Configure LVM and NFS file systems for AIX description: - This module creates, removes, mount and unmount LVM and NFS file system for AIX using C(/etc/filesystems). - For LVM file systems is possible to resize a file system. extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: full diff_mode: support: none options: account_subsystem: description: - Specifies whether the file system is to be processed by the accounting subsystem. type: bool default: false attributes: description: - Specifies attributes for files system separated by comma. type: list elements: str default: - agblksize=4096 - isnapshot=no auto_mount: description: - File system is automatically mounted at system restart. type: bool default: true device: description: - Logical volume (LV) device name or remote export device to create a NFS file system. - It is used to create a file system on an already existing logical volume or the exported NFS file system. - If not mentioned a new logical volume name is created following AIX standards (LVM). type: str fs_type: description: - Specifies the virtual file system type. type: str default: jfs2 permissions: description: - Set file system permissions. V(rw) (read-write) or V(ro) (read-only). type: str choices: [ro, rw] default: rw mount_group: description: - Specifies the mount group. type: str filesystem: description: - Specifies the mount point, which is the directory where the file system will be mounted. type: str required: true nfs_server: description: - Specifies a Network File System (NFS) server. type: str rm_mount_point: description: - Removes the mount point directory when used with state V(absent). type: bool default: false size: description: - Specifies the file system size. - For already present it resizes the filesystem. - 512-byte blocks, megabytes or gigabytes. If the value has M specified it is in megabytes. If the value has G specified it is in gigabytes. - If no M or G the value is 512-byte blocks. - If V(+) is specified in begin of value, the value is added. - If V(-) is specified in begin of value, the value is removed. - If neither V(+) nor V(-) is specified, then the total value is the specified. - Size respects the LVM AIX standards. type: str state: description: - Controls the file system state. - V(present) check if file system exists, creates or resize. - V(absent) removes existing file system if already V(unmounted). - V(mounted) checks if the file system is mounted or mount the file system. - V(unmounted) check if the file system is unmounted or unmount the file system. type: str choices: [absent, mounted, present, unmounted] default: present vg: description: - Specifies an existing volume group (VG). type: str notes: - For more O(attributes), please check C(crfs) AIX manual. """ EXAMPLES = r""" - name: Create filesystem in a previously defined logical volume. community.general.aix_filesystem: device: testlv filesystem: /testfs state: present - name: Creating NFS filesystem from nfshost. community.general.aix_filesystem: device: /home/ftp nfs_server: nfshost filesystem: /home/ftp state: present - name: Creating a new file system without a previously logical volume. community.general.aix_filesystem: filesystem: /newfs size: 1G state: present vg: datavg - name: Unmounting /testfs. community.general.aix_filesystem: filesystem: /testfs state: unmounted - name: Resizing /mksysb to +512M. community.general.aix_filesystem: filesystem: /mksysb size: +512M state: present - name: Resizing /mksysb to 11G. community.general.aix_filesystem: filesystem: /mksysb size: 11G state: present - name: Resizing /mksysb to -2G. community.general.aix_filesystem: filesystem: /mksysb size: -2G state: present - name: Remove NFS filesystem /home/ftp. community.general.aix_filesystem: filesystem: /home/ftp rm_mount_point: true state: absent - name: Remove /newfs. community.general.aix_filesystem: filesystem: /newfs rm_mount_point: true state: absent """ import re from os.path import ismount from ansible.module_utils.basic import AnsibleModule def _fs_exists(module, filesystem): """ Check if file system already exists on /etc/filesystems. :param module: Ansible module. :param community.general.filesystem: filesystem name. :return: True or False. """ lsfs_cmd = module.get_bin_path("lsfs", True) rc, lsfs_out, err = module.run_command([lsfs_cmd, "-l", filesystem]) if rc == 1: if re.findall("No record matching", err): return False else: module.fail_json(msg=f"Failed to run lsfs. Error message: {err}") else: return True def _check_nfs_device(module, nfs_host, device): """ Validate if NFS server is exporting the device (remote export). :param module: Ansible module. :param nfs_host: nfs_host parameter, NFS server. :param device: device parameter, remote export. :return: True or False. """ showmount_cmd = module.get_bin_path("showmount", True) rc, showmount_out, err = module.run_command([showmount_cmd, "-a", nfs_host]) if rc != 0: module.fail_json(msg=f"Failed to run showmount. Error message: {err}") else: showmount_data = showmount_out.splitlines() return any(line.split(":")[1] == device for line in showmount_data) def _validate_vg(module, vg): """ Check the current state of volume group. :param module: Ansible module argument spec. :param vg: Volume Group name. :return: True (VG in varyon state) or False (VG in varyoff state) or None (VG does not exist), message. """ lsvg_cmd = module.get_bin_path("lsvg", True) rc, current_active_vgs, err = module.run_command([lsvg_cmd, "-o"]) if rc != 0: module.fail_json(msg=f"Failed executing {lsvg_cmd} command.") rc, current_all_vgs, err = module.run_command([lsvg_cmd]) if rc != 0: module.fail_json(msg=f"Failed executing {lsvg_cmd} command.") if vg in current_all_vgs and vg not in current_active_vgs: msg = f"Volume group {vg} is in varyoff state." return False, msg elif vg in current_active_vgs: msg = f"Volume group {vg} is in varyon state." return True, msg else: msg = f"Volume group {vg} does not exist." return None, msg def resize_fs(module, filesystem, size): """Resize LVM file system.""" chfs_cmd = module.get_bin_path("chfs", True) if not module.check_mode: rc, chfs_out, err = module.run_command([chfs_cmd, "-a", f"size={size}", filesystem]) if rc == 28: changed = False return changed, chfs_out elif rc != 0: if re.findall("Maximum allocation for logical", err): changed = False return changed, err else: module.fail_json(msg=f"Failed to run chfs. Error message: {err}") else: if re.findall("The filesystem size is already", chfs_out): changed = False else: changed = True return changed, chfs_out else: changed = True msg = "" return changed, msg def create_fs( module, fs_type, filesystem, vg, device, size, mount_group, auto_mount, account_subsystem, permissions, nfs_server, attributes, ): """Create LVM file system or NFS remote mount point.""" attributes = " -a ".join(attributes) # Parameters definition. account_subsys_opt = {True: "-t yes", False: "-t no"} if nfs_server is not None: auto_mount_opt = {True: "-A", False: "-a"} else: auto_mount_opt = {True: "-A yes", False: "-A no"} if size is None: size = "" else: size = f"-a size={size}" if device is None: device = "" else: device = f"-d {device}" if vg is None: vg = "" else: vg_state, msg = _validate_vg(module, vg) if vg_state: vg = f"-g {vg}" else: changed = False return changed, msg if mount_group is None: mount_group = "" else: mount_group = f"-u {mount_group}" auto_mount = auto_mount_opt[auto_mount] account_subsystem = account_subsys_opt[account_subsystem] if nfs_server is not None: # Creates a NFS file system. mknfsmnt_cmd = module.get_bin_path("mknfsmnt", True) if not module.check_mode: rc, mknfsmnt_out, err = module.run_command( [mknfsmnt_cmd, "-f", filesystem, device, "-h", nfs_server, "-t", permissions, auto_mount, "-w", "bg"] ) if rc != 0: module.fail_json(msg=f"Failed to run mknfsmnt. Error message: {err}") else: changed = True msg = f"NFS file system {filesystem} created." return changed, msg else: changed = True msg = "" return changed, msg else: # Creates a LVM file system. crfs_cmd = module.get_bin_path("crfs", True) if not module.check_mode: cmd = [crfs_cmd] cmd.append("-v") cmd.append(fs_type) if vg: (flag, value) = vg.split() cmd.append(flag) cmd.append(value) if device: (flag, value) = device.split() cmd.append(flag) cmd.append(value) cmd.append("-m") cmd.append(filesystem) if mount_group: (flag, value) = mount_group.split() cmd.append(flag) cmd.append(value) if auto_mount: (flag, value) = auto_mount.split() cmd.append(flag) cmd.append(value) if account_subsystem: (flag, value) = account_subsystem.split() cmd.append(flag) cmd.append(value) cmd.append("-p") cmd.append(permissions) if size: (flag, value) = size.split() cmd.append(flag) cmd.append(value) if attributes: splitted_attributes = attributes.split() cmd.append("-a") for value in splitted_attributes: cmd.append(value) rc, crfs_out, err = module.run_command(cmd) if rc == 10: module.exit_json( msg=f"Using a existent previously defined logical volume, volume group needs to be empty. {err}" ) elif rc != 0: module.fail_json(msg=f"Failed to run {cmd}. Error message: {err}") else: changed = True return changed, crfs_out else: changed = True msg = "" return changed, msg def remove_fs(module, filesystem, rm_mount_point): """Remove an LVM file system or NFS entry.""" # Command parameters. rm_mount_point_opt = {True: "-r", False: ""} rm_mount_point = rm_mount_point_opt[rm_mount_point] rmfs_cmd = module.get_bin_path("rmfs", True) if not module.check_mode: cmd = [rmfs_cmd, "-r", rm_mount_point, filesystem] rc, rmfs_out, err = module.run_command(cmd) if rc != 0: module.fail_json(msg=f"Failed to run {cmd}. Error message: {err}") else: changed = True msg = rmfs_out if not rmfs_out: msg = f"File system {filesystem} removed." return changed, msg else: changed = True msg = "" return changed, msg def mount_fs(module, filesystem): """Mount a file system.""" mount_cmd = module.get_bin_path("mount", True) if not module.check_mode: rc, mount_out, err = module.run_command([mount_cmd, filesystem]) if rc != 0: module.fail_json(msg=f"Failed to run mount. Error message: {err}") else: changed = True msg = f"File system {filesystem} mounted." return changed, msg else: changed = True msg = "" return changed, msg def unmount_fs(module, filesystem): """Unmount a file system.""" unmount_cmd = module.get_bin_path("unmount", True) if not module.check_mode: rc, unmount_out, err = module.run_command([unmount_cmd, filesystem]) if rc != 0: module.fail_json(msg=f"Failed to run unmount. Error message: {err}") else: changed = True msg = f"File system {filesystem} unmounted." return changed, msg else: changed = True msg = "" return changed, msg def main(): module = AnsibleModule( argument_spec=dict( account_subsystem=dict(type="bool", default=False), attributes=dict(type="list", elements="str", default=["agblksize=4096", "isnapshot=no"]), auto_mount=dict(type="bool", default=True), device=dict(type="str"), filesystem=dict(type="str", required=True), fs_type=dict(type="str", default="jfs2"), permissions=dict(type="str", default="rw", choices=["rw", "ro"]), mount_group=dict(type="str"), nfs_server=dict(type="str"), rm_mount_point=dict(type="bool", default=False), size=dict(type="str"), state=dict(type="str", default="present", choices=["absent", "mounted", "present", "unmounted"]), vg=dict(type="str"), ), supports_check_mode=True, ) account_subsystem = module.params["account_subsystem"] attributes = module.params["attributes"] auto_mount = module.params["auto_mount"] device = module.params["device"] fs_type = module.params["fs_type"] permissions = module.params["permissions"] mount_group = module.params["mount_group"] filesystem = module.params["filesystem"] nfs_server = module.params["nfs_server"] rm_mount_point = module.params["rm_mount_point"] size = module.params["size"] state = module.params["state"] vg = module.params["vg"] result = dict( changed=False, msg="", ) if state == "present": fs_mounted = ismount(filesystem) fs_exists = _fs_exists(module, filesystem) # Check if fs is mounted or exists. if fs_mounted or fs_exists: result["msg"] = f"File system {filesystem} already exists." result["changed"] = False # If parameter size was passed, resize fs. if size is not None: result["changed"], result["msg"] = resize_fs(module, filesystem, size) # If fs doesn't exist, create it. else: # Check if fs will be a NFS device. if nfs_server is not None: if device is None: result["msg"] = 'Parameter "device" is required when "nfs_server" is defined.' module.fail_json(**result) else: # Create a fs from NFS export. if _check_nfs_device(module, nfs_server, device): result["changed"], result["msg"] = create_fs( module, fs_type, filesystem, vg, device, size, mount_group, auto_mount, account_subsystem, permissions, nfs_server, attributes, ) if device is None: if vg is None: result["msg"] = 'Required parameter "device" and/or "vg" is missing for filesystem creation.' module.fail_json(**result) else: # Create a fs from result["changed"], result["msg"] = create_fs( module, fs_type, filesystem, vg, device, size, mount_group, auto_mount, account_subsystem, permissions, nfs_server, attributes, ) if device is not None and nfs_server is None: # Create a fs from a previously lv device. result["changed"], result["msg"] = create_fs( module, fs_type, filesystem, vg, device, size, mount_group, auto_mount, account_subsystem, permissions, nfs_server, attributes, ) elif state == "absent": if ismount(filesystem): result["msg"] = f"File system {filesystem} mounted." else: fs_status = _fs_exists(module, filesystem) if not fs_status: result["msg"] = f"File system {filesystem} does not exist." else: result["changed"], result["msg"] = remove_fs(module, filesystem, rm_mount_point) elif state == "mounted": if ismount(filesystem): result["changed"] = False result["msg"] = f"File system {filesystem} already mounted." else: result["changed"], result["msg"] = mount_fs(module, filesystem) elif state == "unmounted": if not ismount(filesystem): result["changed"] = False result["msg"] = f"File system {filesystem} already unmounted." else: result["changed"], result["msg"] = unmount_fs(module, filesystem) else: # Unreachable codeblock result["msg"] = f"Unexpected state {state}." module.fail_json(**result) module.exit_json(**result) if __name__ == "__main__": main()