#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2012, Red Hat, inc
# Written by Seth Vidal
# based on the mount modules from salt and puppet
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = r'''
---
module: mount
short_description: Control active and configured mount points
description:
  - This module controls active and configured mount points in C(/etc/fstab).
author:
  - Ansible Core Team
  - Seth Vidal (@skvidal)
version_added: "1.0.0"
options:
  path:
    description:
      - Path to the mount point (e.g. C(/mnt/files)).
      - Before Ansible 2.3 this option was only usable as O(ignore:dest), O(ignore:destfile), and O(name).
    type: path
    required: true
    aliases: [ name ]
  src:
    description:
      - Device (or NFS volume, or something else) to be mounted on I(path).
      - Required when O(state) set to V(present), V(mounted), or V(ephemeral).
      - Ignored when O(state) set to V(absent) or V(unmounted).
    type: path
  fstype:
    description:
      - Filesystem type.
      - Required when O(state) is V(present), V(mounted), or V(ephemeral).
    type: str
  opts:
    description:
      - Mount options (see fstab(5), or vfstab(4) on Solaris).
    type: str
  opts_no_log:
    description:
      - Do not log opts.
    type: bool
    default: false
  dump:
    description:
      - Dump (see fstab(5)).
      - Note that if set to C(null) and O(state=present),
        it will cease to work and duplicate entries will be made
        with subsequent runs.
      - Has no effect on Solaris systems or when used with O(state=ephemeral).
    type: str
    default: '0'
  passno:
    description:
      - Passno (see fstab(5)).
      - Note that if set to C(null) and O(state=present),
        it will cease to work and duplicate entries will be made
        with subsequent runs.
      - Deprecated on Solaris systems. Has no effect when used with O(state=ephemeral).
    type: str
    default: '0'
  state:
    description:
      - If V(mounted), the device will be actively mounted and appropriately
        configured in I(fstab). If the mount point is not present, the mount
        point will be created.
      - If V(unmounted), the device will be unmounted without changing I(fstab).
      - V(present) only specifies that the device is to be configured in
        I(fstab) and does not trigger or require a mount.
      - V(ephemeral) only specifies that the device is to be mounted, without changing
        I(fstab). If it is already mounted, a remount will be triggered.
        This will always return RV(ignore:changed=true). If the mount point O(path)
        has already a device mounted on, and its source is different than O(src),
        the module will fail to avoid unexpected unmount or mount point override.
        If the mount point is not present, the mount point will be created.
        The I(fstab) is completely ignored. This option is added in version 1.5.0.
      - V(absent) specifies that the mount point entry O(path) will be removed
        from I(fstab) and will also unmount the mounted device and remove the
        mount point. A mounted device will be unmounted regardless of O(src) or its
        real source. V(absent) does not unmount recursively, and the module will
        fail if multiple devices are mounted on the same mount point. Using
        V(absent) with a mount point that is not registered in the I(fstab) has
        no effect, use V(unmounted) instead.
      - V(remounted) specifies that the device will be remounted for when you
        want to force a refresh on the mount itself (added in 2.9). This will
        always return RV(ignore:changed=true). If O(opts) is set, the options will be
        applied to the remount, but will not change I(fstab).  Additionally,
        if O(opts) is set, and the remount command fails, the module will
        error to prevent unexpected mount changes.  Try using V(mounted)
        instead to work around this issue.  V(remounted) expects the mount point
        to be present in the I(fstab). To remount a mount point not registered
        in I(fstab), use V(ephemeral) instead, especially with BSD nodes.
      - V(absent_from_fstab) specifies that the device mount's entry will be
        removed from I(fstab). This option does not unmount it or delete the
        mountpoint.
    type: str
    required: true
    choices: [ absent, absent_from_fstab, mounted, present, unmounted, remounted, ephemeral ]
  fstab:
    description:
      - File to use instead of C(/etc/fstab).
      - You should not use this option unless you really know what you are doing.
      - This might be useful if you need to configure mountpoints in a chroot environment.
      - OpenBSD does not allow specifying alternate fstab files with mount so do not
        use this on OpenBSD with any state that operates on the live filesystem.
      - This parameter defaults to C(/etc/fstab) or C(/etc/vfstab) on Solaris.
      - This parameter is ignored when O(state=ephemeral).
    type: str
  boot:
    description:
      - Determines if the filesystem should be mounted on boot.
      - Only applies to Solaris and Linux systems.
      - For Solaris systems, C(true) will set C(yes) as the value of mount at boot
        in C(/etc/vfstab).
      - For Linux, FreeBSD, NetBSD and OpenBSD systems, C(false) will add C(noauto)
        to mount options in C(/etc/fstab).
      - To avoid mount option conflicts, if C(noauto) specified in O(opts),
        mount module will ignore O(boot).
      - This parameter is ignored when O(state=ephemeral).
    type: bool
    default: true
  backup:
    description:
      - Create a backup file including the timestamp information so you can get
        the original file back if you somehow clobbered it incorrectly.
    type: bool
    default: false
notes:
  - As of Ansible 2.3, the O(name) option has been changed to O(path) as
    default, but O(name) still works as well.
  - Using O(state=remounted) with O(opts) set may create unexpected results based on
    the existing options already defined on mount, so care should be taken to
    ensure that conflicting options are not present before hand.
'''

EXAMPLES = r'''
# Before 2.3, option 'name' was used instead of 'path'
- name: Mount DVD read-only
  ansible.posix.mount:
    path: /mnt/dvd
    src: /dev/sr0
    fstype: iso9660
    opts: ro,noauto
    state: present

- name: Mount up device by label
  ansible.posix.mount:
    path: /srv/disk
    src: LABEL=SOME_LABEL
    fstype: ext4
    state: present

- name: Mount up device by UUID
  ansible.posix.mount:
    path: /home
    src: UUID=b3e48f45-f933-4c8e-a700-22a159ec9077
    fstype: xfs
    opts: noatime
    state: present

- name: Unmount a mounted volume
  ansible.posix.mount:
    path: /tmp/mnt-pnt
    state: unmounted

- name: Remount a mounted volume
  ansible.posix.mount:
    path: /tmp/mnt-pnt
    state: remounted

# The following will not save changes to fstab, and only be temporary until
# a reboot, or until calling "state: unmounted" followed by "state: mounted"
# on the same "path"
- name: Remount a mounted volume and append exec to the existing options
  ansible.posix.mount:
    path: /tmp
    state: remounted
    opts: exec

- name: Mount and bind a volume
  ansible.posix.mount:
    path: /system/new_volume/boot
    src: /boot
    opts: bind
    state: mounted
    fstype: none

- name: Mount an NFS volume
  ansible.posix.mount:
    src: 192.168.1.100:/nfs/ssd/shared_data
    path: /mnt/shared_data
    opts: rw,sync,hard
    state: mounted
    fstype: nfs

- name: Mount NFS volumes with noauto according to boot option
  ansible.posix.mount:
    src: 192.168.1.100:/nfs/ssd/shared_data
    path: /mnt/shared_data
    opts: rw,sync,hard
    boot: false
    state: mounted
    fstype: nfs

- name: Mount ephemeral SMB volume
  ansible.posix.mount:
    src: //192.168.1.200/share
    path: /mnt/smb_share
    opts: "rw,vers=3,file_mode=0600,dir_mode=0700,dom={{ ad_domain }},username={{ ad_username }},password={{ ad_password }}"
    opts_no_log: true
    fstype: cifs
    state: ephemeral
'''

import errno
import os
import platform

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.ansible.posix.plugins.module_utils.mount import ismount
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.parsing.convert_bool import boolean


def write_fstab(module, lines, path):

    if module.params['backup']:
        backup_file = module.backup_local(path)
    else:
        backup_file = ""

    fs_w = open(path, 'w')

    for l in lines:
        fs_w.write(l)

    fs_w.flush()
    fs_w.close()

    return backup_file


def _escape_fstab(v):
    """Escape invalid characters in fstab fields.

    space (040)
    ampersand (046)
    backslash (134)
    """

    if isinstance(v, int):
        return v
    else:
        return (
            v.
            replace('\\', '\\134').
            replace(' ', '\\040').
            replace('&', '\\046'))


def set_mount(module, args):
    """Set/change a mount point location in fstab."""
    name, backup_lines, changed = _set_mount_save_old(module, args)
    return name, changed


def _set_mount_save_old(module, args):
    """Set/change a mount point location in fstab. Save the old fstab contents."""

    to_write = []
    old_lines = []
    exists = False
    changed = False
    escaped_args = dict([(k, _escape_fstab(v)) for k, v in args.items()])
    new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n'

    if platform.system() == 'SunOS':
        new_line = (
            '%(src)s - %(name)s %(fstype)s %(passno)s %(boot)s %(opts)s\n')

    for line in open(args['fstab'], 'r').readlines():
        # Append newline if the line in fstab does not finished with newline.
        if not line.endswith('\n'):
            line += '\n'

        old_lines.append(line)

        if not line.strip():
            to_write.append(line)

            continue

        if line.strip().startswith('#'):
            to_write.append(line)

            continue

        fields = line.split('#')[0].split()

        # Check if we got a valid line for splitting
        # (on Linux the 5th and the 6th field is optional)
        if (
                platform.system() == 'SunOS' and len(fields) != 7 or
                platform.system() == 'Linux' and len(fields) not in [4, 5, 6] or
                platform.system() not in ['SunOS', 'Linux'] and len(fields) != 6):
            to_write.append(line)

            continue

        ld = {}

        if platform.system() == 'SunOS':
            (
                ld['src'],
                dash,
                ld['name'],
                ld['fstype'],
                ld['passno'],
                ld['boot'],
                ld['opts']
            ) = fields
        else:
            fields_labels = ['src', 'name', 'fstype', 'opts', 'dump', 'passno']

            # The last two fields are optional on Linux so we fill in default values
            ld['dump'] = 0
            ld['passno'] = 0

            # Fill in the rest of the available fields
            for i, field in enumerate(fields):
                ld[fields_labels[i]] = field

        # Check if we found the correct line
        if (
                ld['name'] != escaped_args['name'] or (
                    # In the case of swap, check the src instead
                    'src' in args and
                    ld['name'] == 'none' and
                    ld['fstype'] == 'swap' and
                    ld['src'] != args['src'])):
            to_write.append(line)

            continue

        # If we got here we found a match - let's check if there is any
        # difference
        exists = True
        args_to_check = ('src', 'fstype', 'opts', 'dump', 'passno')

        if platform.system() == 'SunOS':
            args_to_check = ('src', 'fstype', 'passno', 'boot', 'opts')

        for t in args_to_check:
            if ld[t] != escaped_args[t]:
                ld[t] = escaped_args[t]
                changed = True

        if changed:
            to_write.append(new_line % ld)
        else:
            to_write.append(line)

    if not exists:
        to_write.append(new_line % escaped_args)
        changed = True

    if changed and not module.check_mode:
        args['backup_file'] = write_fstab(module, to_write, args['fstab'])

    return (args['name'], old_lines, changed)


def unset_mount(module, args):
    """Remove a mount point from fstab."""

    to_write = []
    changed = False
    escaped_name = _escape_fstab(args['name'])

    for line in open(args['fstab'], 'r').readlines():
        if not line.strip():
            to_write.append(line)

            continue

        if line.strip().startswith('#'):
            to_write.append(line)

            continue

        # Check if we got a valid line for splitting
        if (
                platform.system() == 'SunOS' and len(line.split()) != 7 or
                platform.system() != 'SunOS' and len(line.split()) != 6):
            to_write.append(line)

            continue

        ld = {}

        if platform.system() == 'SunOS':
            (
                ld['src'],
                dash,
                ld['name'],
                ld['fstype'],
                ld['passno'],
                ld['boot'],
                ld['opts']
            ) = line.split()
        else:
            (
                ld['src'],
                ld['name'],
                ld['fstype'],
                ld['opts'],
                ld['dump'],
                ld['passno']
            ) = line.split()

        if (
                ld['name'] != escaped_name or (
                    # In the case of swap, check the src instead
                    'src' in args and
                    ld['name'] == 'none' and
                    ld['fstype'] == 'swap' and
                    ld['src'] != args['src'])):
            to_write.append(line)

            continue

        # If we got here we found a match - continue and mark changed
        changed = True

    if changed and not module.check_mode:
        write_fstab(module, to_write, args['fstab'])

    return (args['name'], changed)


def _set_fstab_args(fstab_file):
    result = []

    if (
            fstab_file and
            fstab_file != '/etc/fstab' and
            platform.system().lower() != 'sunos'):
        if platform.system().lower().endswith('bsd'):
            result.append('-F')
        else:
            result.append('-T')

        result.append(fstab_file)

    return result


def _set_ephemeral_args(args):
    result = []
    # Set fstype switch according to platform. SunOS/Solaris use -F
    if platform.system().lower() == 'sunos':
        result.append('-F')
    else:
        result.append('-t')
    result.append(args['fstype'])

    # Even if '-o remount' is already set, specifying multiple -o is valid
    if args['opts'] != 'defaults':
        result += ['-o', args['opts']]

    result.append(args['src'])

    return result


def mount(module, args):
    """Mount up a path or remount if needed."""

    mount_bin = module.get_bin_path('mount', required=True)
    name = args['name']
    cmd = [mount_bin]

    if platform.system().lower() == 'openbsd':
        # Use module.params['fstab'] here as args['fstab'] has been set to the
        # default value.
        if module.params['fstab'] is not None:
            module.fail_json(
                msg=(
                    'OpenBSD does not support alternate fstab files. Do not '
                    'specify the fstab parameter for OpenBSD hosts'))
    else:
        if module.params['state'] != 'ephemeral':
            cmd += _set_fstab_args(args['fstab'])

    if module.params['state'] == 'ephemeral':
        cmd += _set_ephemeral_args(args)

    cmd += [name]

    rc, out, err = module.run_command(cmd)

    if rc == 0:
        return 0, ''
    else:
        return rc, out + err


def umount(module, path):
    """Unmount a path."""

    umount_bin = module.get_bin_path('umount', required=True)
    cmd = [umount_bin, path]

    rc, out, err = module.run_command(cmd)

    if rc == 0:
        return 0, ''
    else:
        return rc, out + err


def remount(module, args):
    """Try to use 'remount' first and fallback to (u)mount if unsupported."""
    mount_bin = module.get_bin_path('mount', required=True)
    cmd = [mount_bin]

    # Multiplatform remount opts
    if platform.system().lower().endswith('bsd'):
        if module.params['state'] == 'remounted' and args['opts'] != 'defaults':
            cmd += ['-u', '-o', args['opts']]
        else:
            cmd += ['-u']
    else:
        if module.params['state'] == 'remounted' and args['opts'] != 'defaults':
            cmd += ['-o', 'remount,' + args['opts']]
        else:
            cmd += ['-o', 'remount']

    if platform.system().lower() == 'openbsd':
        # Use module.params['fstab'] here as args['fstab'] has been set to the
        # default value.
        if module.params['fstab'] is not None:
            module.fail_json(
                msg=(
                    'OpenBSD does not support alternate fstab files. Do not '
                    'specify the fstab parameter for OpenBSD hosts'))
    else:
        if module.params['state'] != 'ephemeral':
            cmd += _set_fstab_args(args['fstab'])

    if module.params['state'] == 'ephemeral':
        cmd += _set_ephemeral_args(args)

    cmd += [args['name']]
    out = err = ''

    try:
        if module.params['state'] != 'ephemeral' and platform.system().lower().endswith('bsd'):
            # Note: Forcing BSDs to do umount/mount due to BSD remount not
            # working as expected (suspect bug in the BSD mount command)
            # Interested contributor could rework this to use mount options on
            # the CLI instead of relying on fstab
            # https://github.com/ansible/ansible-modules-core/issues/5591
            # Note: this does not affect ephemeral state as all options
            # are set on the CLI and fstab is expected to be ignored.
            rc = 1
        else:
            rc, out, err = module.run_command(cmd)
    except Exception:
        rc = 1

    msg = ''

    if rc != 0:
        msg = out + err

        if module.params['state'] == 'remounted' and args['opts'] != 'defaults':
            module.fail_json(
                msg=(
                    'Options were specified with remounted, but the remount '
                    'command failed. Failing in order to prevent an '
                    'unexpected mount result. Try replacing this command with '
                    'a "state: unmounted" followed by a "state: mounted" '
                    'using the full desired mount options instead.'))

        rc, msg = umount(module, args['name'])

        if rc == 0:
            rc, msg = mount(module, args)

    return rc, msg


# Note if we wanted to put this into module_utils we'd have to get permission
# from @jupeter -- https://github.com/ansible/ansible-modules-core/pull/2923
# @jtyr -- https://github.com/ansible/ansible-modules-core/issues/4439
# and @abadger to relicense from GPLv3+
def is_bind_mounted(module, linux_mounts, dest, src=None, fstype=None):
    """Return whether the dest is bind mounted

    :arg module: The AnsibleModule (used for helper functions)
    :arg dest: The directory to be mounted under. This is the primary means
        of identifying whether the destination is mounted.
    :kwarg src: The source directory. If specified, this is used to help
        ensure that we are detecting that the correct source is mounted there.
    :kwarg fstype: The filesystem type. If specified this is also used to
        help ensure that we are detecting the right mount.
    :kwarg linux_mounts: Cached list of mounts for Linux.
    :returns: True if the dest is mounted with src otherwise False.
    """

    is_mounted = False

    if platform.system() == 'Linux' and linux_mounts is not None:
        if src is None:
            # That's for unmounted/absent
            if dest in linux_mounts:
                is_mounted = True
        else:
            if dest in linux_mounts:
                is_mounted = linux_mounts[dest]['src'] == src

    else:
        bin_path = module.get_bin_path('mount', required=True)
        cmd = '%s -l' % bin_path
        rc, out, err = module.run_command(cmd)
        mounts = []

        if len(out):
            mounts = to_native(out).strip().split('\n')

        for mnt in mounts:
            arguments = mnt.split()

            if (
                    (arguments[0] == src or src is None) and
                    arguments[2] == dest and
                    (arguments[4] == fstype or fstype is None)):
                is_mounted = True

            if is_mounted:
                break

    return is_mounted


def get_linux_mounts(module, mntinfo_file="/proc/self/mountinfo"):
    """Gather mount information"""

    try:
        f = open(mntinfo_file)
    except IOError:
        return

    lines = map(str.strip, f.readlines())

    try:
        f.close()
    except IOError:
        module.fail_json(msg="Cannot close file %s" % mntinfo_file)

    mntinfo = {}

    for line in lines:
        fields = line.split()

        record = {
            'id': int(fields[0]),
            'parent_id': int(fields[1]),
            'root': fields[3],
            'dst': fields[4],
            'opts': fields[5],
            'fs': fields[-3],
            'src': fields[-2]
        }

        mntinfo[record['id']] = record

    mounts = {}

    for mnt in mntinfo.values():
        if mnt['parent_id'] != 1 and mnt['parent_id'] in mntinfo:
            m = mntinfo[mnt['parent_id']]
            if (
                    len(m['root']) > 1 and
                    mnt['root'].startswith("%s/" % m['root'])):
                # Omit the parent's root in the child's root
                # == Example:
                # 140 136 253:2 /rootfs / rw - ext4 /dev/sdb2 rw
                # 141 140 253:2 /rootfs/tmp/aaa /tmp/bbb rw - ext4 /dev/sdb2 rw
                # == Expected result:
                # src=/tmp/aaa
                mnt['root'] = mnt['root'][len(m['root']):]

            # Prepend the parent's dst to the child's root
            # == Example:
            # 42 60 0:35 / /tmp rw - tmpfs tmpfs rw
            # 78 42 0:35 /aaa /tmp/bbb rw - tmpfs tmpfs rw
            # == Expected result:
            # src=/tmp/aaa
            if m['dst'] != '/':
                mnt['root'] = "%s%s" % (m['dst'], mnt['root'])
            src = mnt['root']
        else:
            src = mnt['src']

        record = {
            'dst': mnt['dst'],
            'src': src,
            'opts': mnt['opts'],
            'fs': mnt['fs']
        }

        mounts[mnt['dst']] = record

    return mounts


def _is_same_mount_src(module, src, mountpoint, linux_mounts):
    """Return True if the mounted fs on mountpoint is the same source than src. Return False if mountpoint is not a mountpoint"""
    # If the provided mountpoint is not a mountpoint, don't waste time
    if (
            not ismount(mountpoint) and
            not is_bind_mounted(module, linux_mounts, mountpoint)):
        return False

    # Treat Linux bind mounts
    if platform.system() == 'Linux' and linux_mounts is not None:
        # For Linux bind mounts only: the mount command does not return
        # the actual source for bind mounts, but the device of the source.
        # is_bind_mounted() called with the 'src' parameter will return True if
        # the mountpoint is a bind mount AND the source FS is the same than 'src'.
        # is_bind_mounted() is not reliable on Solaris, NetBSD and OpenBSD.
        # But we can rely on 'mount -v' on all other platforms, and Linux non-bind mounts.
        if is_bind_mounted(module, linux_mounts, mountpoint, src):
            return True

    # mount with parameter -v has a close behavior on Linux, *BSD, SunOS
    # Requires -v with SunOS. Without -v, source and destination are reversed
    # Output format differs from a system to another, but field[0:3] are consistent: [src, 'on', dest]
    cmd = '%s -v' % module.get_bin_path('mount', required=True)
    rc, out, err = module.run_command(cmd)
    mounts = []

    if len(out):
        mounts = to_native(out).strip().split('\n')
    else:
        module.fail_json(msg="Unable to retrieve mount info with command '%s'" % cmd)

    for mnt in mounts:
        fields = mnt.split()
        mp_src = fields[0]
        mp_dst = fields[2]
        if mp_src == src and mp_dst == mountpoint:
            return True

    return False


def main():
    module = AnsibleModule(
        argument_spec=dict(
            boot=dict(type='bool', default=True),
            dump=dict(type='str', default='0'),
            fstab=dict(type='str'),
            fstype=dict(type='str'),
            path=dict(type='path', required=True, aliases=['name']),
            opts=dict(type='str'),
            opts_no_log=dict(type='bool', default=False),
            passno=dict(type='str', no_log=False, default='0'),
            src=dict(type='path'),
            backup=dict(type='bool', default=False),
            state=dict(type='str', required=True, choices=['absent', 'absent_from_fstab', 'mounted', 'present', 'unmounted', 'remounted', 'ephemeral']),
        ),
        supports_check_mode=True,
        required_if=(
            ['state', 'mounted', ['src', 'fstype']],
            ['state', 'present', ['src', 'fstype']],
            ['state', 'ephemeral', ['src', 'fstype']]
        ),
    )

    if module.params['opts_no_log']:
        module.no_log_values.add(module.params['opts'])

    # solaris args:
    #   name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab
    # linux args:
    #   name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
    # Note: Do not modify module.params['fstab'] as we need to know if the user
    # explicitly specified it in mount() and remount()
    if platform.system().lower() == 'sunos':
        args = dict(
            name=module.params['path'],
            opts='-',
            passno='-',
            fstab=module.params['fstab'],
            boot='yes' if module.params['boot'] else 'no',
        )
        if args['fstab'] is None:
            args['fstab'] = '/etc/vfstab'
    else:
        args = dict(
            name=module.params['path'],
            opts='defaults',
            dump='0',
            passno='0',
            fstab=module.params['fstab'],
            boot='yes',
        )
        if args['fstab'] is None:
            args['fstab'] = '/etc/fstab'

        # FreeBSD doesn't have any 'default' so set 'rw' instead
        if platform.system() == 'FreeBSD':
            args['opts'] = 'rw'

    args['backup_file'] = ""
    linux_mounts = []

    # Cache all mounts here in order we have consistent results if we need to
    # call is_bind_mounted() multiple times
    if platform.system() == 'Linux':
        linux_mounts = get_linux_mounts(module)

        if linux_mounts is None:
            module.warn('Cannot open file /proc/self/mountinfo. Bind mounts might be misinterpreted.')

    # Override defaults with user specified params
    for key in ('src', 'fstype', 'passno', 'opts', 'dump', 'fstab'):
        if module.params[key] is not None:
            args[key] = module.params[key]
    if platform.system().lower() == 'linux' or platform.system().lower().endswith('bsd'):
        # Linux, FreeBSD, NetBSD and OpenBSD have 'noauto' as mount option to
        # handle mount on boot.  To avoid mount option conflicts, if 'noauto'
        # specified in 'opts',  mount module will ignore 'boot'.
        opts = args['opts'].split(',')
        if module.params['boot'] and 'noauto' in opts:
            module.warn("Ignore the 'boot' due to 'opts' contains 'noauto'.")
        elif not module.params['boot']:
            args['boot'] = 'no'
            opts.append('noauto')
            args['opts'] = ','.join(opts)

    # If fstab file does not exist, we first need to create it. This mainly
    # happens when fstab option is passed to the module.
    # If state is 'ephemeral', we do not need fstab file
    if module.params['state'] != 'ephemeral':
        if not os.path.exists(args['fstab']):
            if not os.path.exists(os.path.dirname(args['fstab'])):
                os.makedirs(os.path.dirname(args['fstab']))
            try:
                open(args['fstab'], 'a').close()
            except PermissionError as e:
                module.fail_json(msg="Failed to open %s due to permission issue" % args['fstab'])
            except Exception as e:
                module.fail_json(msg="Failed to open %s due to %s" % (args['fstab'], to_native(e)))

    # absent:
    #   Remove from fstab and unmounted.
    # unmounted:
    #   Do not change fstab state, but unmount.
    # present:
    #   Add to fstab, do not change mount state.
    # mounted:
    #   Add to fstab if not there and make sure it is mounted. If it has
    #   changed in fstab then remount it.
    # ephemeral:
    #   Do not change fstab state, but mount.

    state = module.params['state']
    name = module.params['path']
    changed = False

    if state == 'absent_from_fstab':
        name, changed = unset_mount(module, args)
    elif state == 'absent':
        name, changed = unset_mount(module, args)

        if changed and not module.check_mode:
            if ismount(name) or is_bind_mounted(module, linux_mounts, name):
                res, msg = umount(module, name)

                if res:
                    module.fail_json(
                        msg="Error unmounting %s: %s" % (name, msg))

            if os.path.exists(name):
                try:
                    os.rmdir(name)
                except (OSError, IOError) as e:
                    module.fail_json(msg="Error rmdir %s: %s" % (name, to_native(e)))
    elif state == 'unmounted':
        if ismount(name) or is_bind_mounted(module, linux_mounts, name):
            if not module.check_mode:
                res, msg = umount(module, name)

                if res:
                    module.fail_json(
                        msg="Error unmounting %s: %s" % (name, msg))

            changed = True
    elif state == 'mounted' or state == 'ephemeral':
        dirs_created = []
        if not os.path.exists(name) and not module.check_mode:
            try:
                # Something like mkdir -p but with the possibility to undo.
                # Based on some copy-paste from the "file" module.
                curpath = ''
                for dirname in name.strip('/').split('/'):
                    curpath = '/'.join([curpath, dirname])
                    # Remove leading slash if we're creating a relative path
                    if not os.path.isabs(name):
                        curpath = curpath.lstrip('/')

                    b_curpath = to_bytes(curpath, errors='surrogate_or_strict')
                    if not os.path.exists(b_curpath):
                        try:
                            os.mkdir(b_curpath)
                            dirs_created.append(b_curpath)
                        except OSError as ex:
                            # Possibly something else created the dir since the os.path.exists
                            # check above. As long as it's a dir, we don't need to error out.
                            if not (ex.errno == errno.EEXIST and os.path.isdir(b_curpath)):
                                raise

            except (OSError, IOError) as e:
                module.fail_json(
                    msg="Error making dir %s: %s" % (name, to_native(e)))

        # ephemeral: completely ignore fstab
        if state != 'ephemeral':
            name, backup_lines, changed = _set_mount_save_old(module, args)
        else:
            name, backup_lines, changed = args['name'], [], False
        res = 0

        if (
                ismount(name) or
                is_bind_mounted(
                    module, linux_mounts, name, args['src'], args['fstype'])):
            if changed and not module.check_mode:
                res, msg = remount(module, args)
                changed = True

            # When 'state' == 'ephemeral', we don't know what is in fstab, and 'changed' is always False
            if state == 'ephemeral':
                # If state == 'ephemeral', check if the mountpoint src == module.params['src']
                # If it doesn't, fail to prevent unwanted unmount or unwanted mountpoint override
                if _is_same_mount_src(module, args['src'], args['name'], linux_mounts):
                    changed = True
                    if not module.check_mode:
                        res, msg = remount(module, args)
                else:
                    module.fail_json(
                        msg=(
                            'Ephemeral mount point is already mounted with a different '
                            'source than the specified one. Failing in order to prevent an '
                            'unwanted unmount or override operation. Try replacing this command with '
                            'a "state: unmounted" followed by a "state: ephemeral", or use '
                            'a different destination path.'))

        else:
            # If not already mounted, mount it
            changed = True

            if not module.check_mode:
                res, msg = mount(module, args)

        if res:
            # Not restoring fstab after a failed mount was reported as a bug,
            # ansible/ansible#59183
            # A non-working fstab entry may break the system at the reboot,
            # so undo all the changes if possible.
            try:
                if state != 'ephemeral':
                    write_fstab(module, backup_lines, args['fstab'])
            except Exception:
                pass

            try:
                for dirname in dirs_created[::-1]:
                    os.rmdir(dirname)
            except Exception:
                pass

            module.fail_json(msg="Error mounting %s: %s" % (name, msg))
    elif state == 'present':
        name, changed = set_mount(module, args)
    elif state == 'remounted':
        if not module.check_mode:
            res, msg = remount(module, args)

            if res:
                module.fail_json(msg="Error remounting %s: %s" % (name, msg))

        changed = True
    else:
        module.fail_json(msg='Unexpected position reached')

    # If the managed node is Solaris, convert the boot value type to Boolean
    #  to match the type of return value with the module argument.
    if platform.system().lower() == 'sunos':
        args['boot'] = boolean(args['boot'])
    module.exit_json(changed=changed, **args)


if __name__ == '__main__':
    main()
