mirror of
https://github.com/ansible-collections/ansible.posix.git
synced 2026-01-11 07:05:27 +01:00
the CI failures are unrelated and shouldn't even be showing up ... I'm going to sort that out separately but that doesn't need to prevent this merge, all relevant CI tests passed
1008 lines
34 KiB
Python
1008 lines
34 KiB
Python
#!/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 I(dest), I(destfile) and I(name).
|
|
type: path
|
|
required: true
|
|
aliases: [ name ]
|
|
src:
|
|
description:
|
|
- Device (or NFS volume, or something else) to be mounted on I(path).
|
|
- Required when I(state) set to C(present), C(mounted) or C(ephemeral).
|
|
type: path
|
|
fstype:
|
|
description:
|
|
- Filesystem type.
|
|
- Required when I(state) is C(present), C(mounted) or C(ephemeral).
|
|
type: str
|
|
opts:
|
|
description:
|
|
- Mount options (see fstab(5), or vfstab(4) on Solaris).
|
|
type: str
|
|
dump:
|
|
description:
|
|
- Dump (see fstab(5)).
|
|
- Note that if set to C(null) and I(state) set to C(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 C(ephemeral).
|
|
type: str
|
|
default: '0'
|
|
passno:
|
|
description:
|
|
- Passno (see fstab(5)).
|
|
- Note that if set to C(null) and I(state) set to C(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 C(ephemeral).
|
|
type: str
|
|
default: '0'
|
|
state:
|
|
description:
|
|
- If C(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 C(unmounted), the device will be unmounted without changing I(fstab).
|
|
- C(present) only specifies that the device is to be configured in
|
|
I(fstab) and does not trigger or require a mount.
|
|
- C(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 changed=True. If the mount point I(path)
|
|
has already a device mounted on, and its source is different than I(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.
|
|
- C(absent) specifies that the device mount's entry will be removed from
|
|
I(fstab) and will also unmount the device and remove the mount
|
|
point.
|
|
- C(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 changed=true. If I(opts) is set, the options will be
|
|
applied to the remount, but will not change I(fstab). Additionally,
|
|
if I(opts) is set, and the remount command fails, the module will
|
|
error to prevent unexpected mount changes. Try using C(mounted)
|
|
instead to work around this issue. C(remounted) expects the mount point
|
|
to be present in the I(fstab). To remount a mount point not registered
|
|
in I(fstab), use C(ephemeral) instead, especially with BSD nodes.
|
|
- C(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 /etc/fstab or /etc/vfstab on Solaris.
|
|
- This parameter is ignored when I(state) is set to C(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 I(/etc/vfstab).
|
|
- For Linux, FreeBSD, NetBSD and OpenBSD systems, C(false) will add C(noauto)
|
|
to mount options in I(/etc/fstab).
|
|
- To avoid mount option conflicts, if C(noauto) specified in C(opts),
|
|
mount module will ignore C(boot).
|
|
- This parameter is ignored when I(state) is set to C(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 I(name) option has been changed to I(path) as
|
|
default, but I(name) still works as well.
|
|
- Using C(remounted) with I(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 }}"
|
|
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.six import iteritems
|
|
from ansible.module_utils._text 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 iteritems(args) if k != 'warnings'])
|
|
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()
|
|
|
|
# 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'),
|
|
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']]
|
|
),
|
|
)
|
|
|
|
# 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',
|
|
warnings=[]
|
|
)
|
|
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',
|
|
warnings=[]
|
|
)
|
|
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:
|
|
args['warnings'].append('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:
|
|
args['warnings'].append("Ignore the 'boot' due to 'opts' contains 'noauto'.")
|
|
elif not module.params['boot']:
|
|
args['boot'] = 'no'
|
|
if 'defaults' in opts:
|
|
args['warnings'].append("Ignore the 'boot' due to 'opts' contains 'defaults'.")
|
|
else:
|
|
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()
|