Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ephemeral state to mount fs without altering fstab #264

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelogs/fragments/264_mount_ephemeral.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
minor_changes:
- mount - Add ``ephemeral`` value for the ``state`` parameter, that allows to mount a filesystem
without altering the ``fstab`` file (https://github.com/ansible-collections/ansible.posix/pull/264).
142 changes: 119 additions & 23 deletions plugins/modules/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@
src:
description:
- Device (or NFS volume, or something else) to be mounted on I(path).
- Required when I(state) set to C(present) or C(mounted).
- 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) or C(mounted).
- Required when I(state) is C(present), C(mounted) or C(ephemeral).
type: str
opts:
description:
Expand All @@ -48,7 +48,7 @@
- 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.
- Has no effect on Solaris systems or when used with C(ephemeral).
type: str
default: 0
passno:
Expand All @@ -57,7 +57,7 @@
- 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.
- Deprecated on Solaris systems. Has no effect when used with C(ephemeral).
type: str
default: 0
state:
Expand All @@ -68,6 +68,13 @@
- 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 C(path)
has already a device mounted on, and its I(src) is different than C(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 value of C(fstab) is ignored.
- 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.
Expand All @@ -80,7 +87,7 @@
instead to work around this issue.
type: str
required: true
choices: [ absent, mounted, present, unmounted, remounted ]
choices: [ absent, mounted, present, unmounted, remounted, ephemeral ]
fstab:
description:
- File to use instead of C(/etc/fstab).
Expand All @@ -89,6 +96,7 @@
- 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:
Expand All @@ -100,6 +108,7 @@
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: yes
backup:
Expand Down Expand Up @@ -184,6 +193,14 @@
boot: no
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples: add an example for the use of state=ephemeral. This example has been tested on my side.

'''

import errno
Expand Down Expand Up @@ -426,6 +443,23 @@ def _set_fstab_args(fstab_file):
return result


def _set_ephemeral_args(args):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_set_ephemeral_args(args): When using ephemeral, we cannot rely on an fstab entry to set the mount options. This function sets the mount options accordingly to what was supplied to the module: fstype, opts and src.

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."""

Expand All @@ -442,7 +476,10 @@ def mount(module, args):
'OpenBSD does not support alternate fstab files. Do not '
'specify the fstab parameter for OpenBSD hosts'))
else:
cmd += _set_fstab_args(args['fstab'])
if module.params['state'] != 'ephemeral':
cmd += _set_fstab_args(args['fstab'])
else:
cmd += _set_ephemeral_args(args)

cmd += [name]

Expand Down Expand Up @@ -494,7 +531,10 @@ def remount(module, args):
'OpenBSD does not support alternate fstab files. Do not '
'specify the fstab parameter for OpenBSD hosts'))
else:
cmd += _set_fstab_args(args['fstab'])
if module.params['state'] != 'ephemeral':
cmd += _set_fstab_args(args['fstab'])
else:
cmd += _set_ephemeral_args(args)

cmd += [args['name']]
out = err = ''
Expand Down Expand Up @@ -587,9 +627,8 @@ def is_bind_mounted(module, linux_mounts, dest, src=None, fstype=None):
return is_mounted


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

def _get_mount_info(module, mntinfo_file="/proc/self/mountinfo"):
"""Return raw mount information"""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New function _get_mount_info and minor change in function get_linux_mounts. As my new function _is_same_mount_src uses the mount info, I created this new function to avoid code redundancy. The function get_linux_mounts has been edited accordingly, and the original behavior is entirely preserved.

This function does exactly what get_linux_mounts used to do to retrieve mount info.

try:
f = open(mntinfo_file)
except IOError:
Expand All @@ -602,6 +641,17 @@ def get_linux_mounts(module, mntinfo_file="/proc/self/mountinfo"):
except IOError:
module.fail_json(msg="Cannot close file %s" % mntinfo_file)

return lines


def get_linux_mounts(module, mntinfo_file="/proc/self/mountinfo"):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As previously noticed, this function has been edited to use the new function _get_mount_info, instead of retrieving "manually" the mount info.

"""Gather mount information"""

lines = _get_mount_info(module)
# Keep same behavior than before
if lines is None:
return

Copy link
Contributor Author

@NeodymiumFerBore NeodymiumFerBore Sep 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition if lines is None: return preserve the function behavior from before the edit.

mntinfo = {}

for line in lines:
Expand Down Expand Up @@ -659,6 +709,24 @@ def get_linux_mounts(module, mntinfo_file="/proc/self/mountinfo"):
return mounts


def _is_same_mount_src(module, args, mntinfo_file="/proc/self/mountinfo"):
"""Return True if the mounted fs on mountpoint is the same source than src"""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New function _is_same_mount_src: This function is used for the ephemeral state: if this function returns False, then it means that the provided mount point has already a volume mounted on it, which is NOT the volume given in src. This leads to a module failure, to avoid an unwanted unmount.

mountpoint = args['name']
src = args['src']
lines = _get_mount_info(module)

# If this function is used and we cannot retrieve mount info, we must fail to avoid unexpected behavior
if lines is None:
module.fail_json(msg="Unable to retrieve mount info from '%s'" % mntinfo_file)

for line in lines:
fields = line.split()
if fields[4] == mountpoint and fields[-2] == src:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fields[4] contains a mountpoint and fields[-2] contains the associated source. See get_linux_mounts for more info about mountinfo fields. The logic behind picking these fields was already implemented, I only copy-pasted after test and review.

return True

# (dst == mountpoint and src == name) was never reached
return False

def main():
module = AnsibleModule(
argument_spec=dict(
Expand All @@ -671,12 +739,13 @@ def main():
passno=dict(type='str', no_log=False),
src=dict(type='path'),
backup=dict(type='bool', default=False),
state=dict(type='str', required=True, choices=['absent', 'mounted', 'present', 'unmounted', 'remounted']),
state=dict(type='str', required=True, choices=['absent', 'mounted', 'present', 'unmounted', 'remounted', 'ephemeral']),
),
supports_check_mode=True,
required_if=(
['state', 'mounted', ['src', 'fstype']],
['state', 'present', ['src', 'fstype']],
['state', 'ephemeral', ['src', 'fstype']]
),
)

Expand Down Expand Up @@ -747,15 +816,17 @@ def main():

# If fstab file does not exist, we first need to create it. This mainly
# happens when fstab option is passed to the module.
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)))
# If state is 'ephemeral', we do not need fstab file
if module.params['state'] != 'ephemeral':
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only this line changed (add if statement and fix block indent). This code block is in charge of creating the provided fstab file. For ephemeral state, the fstab file does not have to be created.

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.
Expand All @@ -766,6 +837,8 @@ def main():
# 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']
Expand Down Expand Up @@ -797,7 +870,7 @@ def main():
msg="Error unmounting %s: %s" % (name, msg))

changed = True
elif state == 'mounted':
elif state == 'mounted' or state == 'ephemeral':
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ephemeral treatment is based on the mounted logic, bypassing the fstab read and write.

dirs_created = []
if not os.path.exists(name) and not module.check_mode:
try:
Expand Down Expand Up @@ -825,7 +898,11 @@ def main():
module.fail_json(
msg="Error making dir %s: %s" % (name, to_native(e)))

name, backup_lines, changed = _set_mount_save_old(module, args)
# 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 (
Expand All @@ -835,7 +912,25 @@ def main():
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):
res, msg = remount(module, args)
changed = True
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.'))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When state is ephemeral or mounted, it tests if the provided mountpoint has already a volume mounted on it.

In the case of ephemeral, there is no fstab file to provide an authoritative answer to the question "What should be mounted here". To avoid an unwanted unmount and a possible service outage, the module fails if the volume already mounted on the mountpoint is not the same as the one provided in src (this is checked with the new function _is_same_mount_src).

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

if not module.check_mode:
Expand All @@ -847,7 +942,8 @@ def main():
# A non-working fstab entry may break the system at the reboot,
# so undo all the changes if possible.
try:
write_fstab(module, backup_lines, args['fstab'])
if state != 'ephemeral':
write_fstab(module, backup_lines, args['fstab'])
except Exception:
pass

Expand Down