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

[master] Add fallback for when "remounting" NFS or FUSE initially fails #65351

Merged
1 change: 1 addition & 0 deletions changelog/18907.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When an NFS or FUSE mount fails to unmount when mount options have changed, try again with a lazy umount before mounting again.
10 changes: 7 additions & 3 deletions salt/modules/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -1383,7 +1383,7 @@ def remount(name, device, mkmnt=False, fstype="", opts="defaults", user=None):
return mount(name, device, mkmnt, fstype, opts, user=user)


def umount(name, device=None, user=None, util="mount"):
def umount(name, device=None, user=None, util="mount", lazy=False):
"""
Attempt to unmount a device by specifying the directory it is mounted on

Expand Down Expand Up @@ -1413,10 +1413,14 @@ def umount(name, device=None, user=None, util="mount"):
if name not in mnts:
return f"{name} does not have anything mounted"

cmd = "umount"
if lazy:
cmd = f"{cmd} -l"
if not device:
cmd = f"umount '{name}'"
cmd = f"{cmd} '{name}'"
else:
cmd = f"umount '{device}'"
cmd = f"{cmd}'{device}'"

out = __salt__["cmd.run_all"](cmd, runas=user, python_shell=False)
if out["retcode"]:
return out["stderr"]
Expand Down
118 changes: 78 additions & 40 deletions salt/states/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def mounted(
extra_mount_translate_options=None,
hidden_opts=None,
bind_mount_copy_active_opts=True,
**kwargs
**kwargs,
):
"""
Verify that a device is mounted
Expand Down Expand Up @@ -282,10 +282,10 @@ def mounted(
real_device = device.split("=")[1].strip('"').lower()
elif device.upper().startswith("LABEL="):
_label = device.split("=")[1]
cmd = "blkid -t LABEL={}".format(_label)
res = __salt__["cmd.run_all"]("{}".format(cmd))
cmd = f"blkid -t LABEL={_label}"
res = __salt__["cmd.run_all"](f"{cmd}")
if res["retcode"] > 0:
ret["comment"] = "Unable to find device with label {}.".format(_label)
ret["comment"] = f"Unable to find device with label {_label}."
ret["result"] = False
return ret
else:
Expand Down Expand Up @@ -432,7 +432,7 @@ def mounted(
)
if size_match:
converted_size = _size_convert(size_match)
opt = "size={}k".format(converted_size)
opt = f"size={converted_size}k"
# make cifs option user synonym for option username which is reported by /proc/mounts
if fstype in ["cifs"] and opt.split("=")[0] == "user":
opt = "username={}".format(opt.split("=")[1])
Expand Down Expand Up @@ -460,9 +460,9 @@ def mounted(
)
if size_match:
converted_size = _size_convert(size_match)
opt = "size={}k".format(converted_size)
opt = f"size={converted_size}k"
_active_superopts.remove(_active_opt)
_active_opt = "size={}k".format(converted_size)
_active_opt = f"size={converted_size}k"
_active_superopts.append(_active_opt)

if (
Expand Down Expand Up @@ -504,11 +504,34 @@ def mounted(
)
ret["result"] = mount_result
else:
ret["result"] = False
ret["comment"] = "Unable to unmount {}: {}.".format(
real_name, unmount_result
# If the first attempt at unmounting fails,
# run again as a lazy umount.
ret["changes"]["umount"] = (
"Forced a lazy unmount and mount "
+ "because the previous unmount failed "
+ "and because the "
+ "options ({}) changed".format(
",".join(sorted(trigger_remount))
)
)
unmount_result = __salt__["mount.umount"](
real_name, lazy=True
)
return ret
if unmount_result is True:
mount_result = __salt__["mount.mount"](
real_name,
device,
mkmnt=mkmnt,
fstype=fstype,
opts=opts,
)
ret["result"] = mount_result
else:
ret["result"] = False
ret["comment"] = "Unable to unmount {}: {}.".format(
real_name, unmount_result
)
return ret
else:
ret["changes"]["umount"] = (
"Forced remount because "
Expand Down Expand Up @@ -551,7 +574,7 @@ def mounted(
if fstype in ["nfs", "cvfs"] or fstype.startswith("fuse"):
ret["changes"]["umount"] = (
"Forced unmount and mount because "
+ "options ({}) changed".format(opt)
+ f"options ({opt}) changed"
)
unmount_result = __salt__["mount.umount"](real_name)
if unmount_result is True:
Expand All @@ -564,15 +587,32 @@ def mounted(
)
ret["result"] = mount_result
else:
ret["result"] = False
ret["comment"] = "Unable to unmount {}: {}.".format(
real_name, unmount_result
# If the first attempt at unmounting fails,
# run again as a lazy umount.
unmount_result = __salt__["mount.umount"](
real_name, lazy=True
)
return ret
if unmount_result is True:
mount_result = __salt__["mount.mount"](
real_name,
device,
mkmnt=mkmnt,
fstype=fstype,
opts=opts,
)
ret["result"] = mount_result
else:
ret["result"] = False
ret[
"comment"
] = "Unable to unmount {}: {}.".format(
real_name, unmount_result
)
return ret
else:
ret["changes"]["umount"] = (
"Forced remount because "
+ "options ({}) changed".format(opt)
+ f"options ({opt}) changed"
)
remount_result = __salt__["mount.remount"](
real_name,
Expand Down Expand Up @@ -638,13 +678,11 @@ def mounted(
if __opts__["test"]:
ret["result"] = None
if os.path.exists(name):
ret["comment"] = "{} would be mounted".format(name)
ret["comment"] = f"{name} would be mounted"
elif mkmnt:
ret["comment"] = "{} would be created and mounted".format(name)
ret["comment"] = f"{name} would be created and mounted"
else:
ret[
"comment"
] = "{} does not exist and would not be created".format(name)
ret["comment"] = f"{name} does not exist and would not be created"
return ret

if not os.path.exists(name) and not mkmnt:
Expand All @@ -668,7 +706,7 @@ def mounted(
if __opts__["test"]:
ret["result"] = None
if mkmnt:
ret["comment"] = "{} would be created, but not mounted".format(name)
ret["comment"] = f"{name} would be created, but not mounted"
else:
ret[
"comment"
Expand All @@ -677,14 +715,14 @@ def mounted(
)
elif mkmnt:
__salt__["file.mkdir"](name, user=user)
ret["comment"] = "{} was created, not mounted".format(name)
ret["comment"] = f"{name} was created, not mounted"
else:
ret["comment"] = "{} not present and not mounted".format(name)
ret["comment"] = f"{name} not present and not mounted"
else:
if __opts__["test"]:
ret["comment"] = "{} would not be mounted".format(name)
ret["comment"] = f"{name} would not be mounted"
else:
ret["comment"] = "{} not mounted".format(name)
ret["comment"] = f"{name} not mounted"

if persist:
if "/etc/fstab" == config:
Expand Down Expand Up @@ -745,7 +783,7 @@ def mounted(
name
)
else:
comment = "The {} fstab entry must be updated.".format(name)
comment = f"The {name} fstab entry must be updated."
else:
ret["result"] = False
comment = (
Expand Down Expand Up @@ -824,25 +862,25 @@ def swap(name, persist=True, config="/etc/fstab"):
if __salt__["file.is_link"](name):
real_swap_device = __salt__["file.readlink"](name)
if not real_swap_device.startswith("/"):
real_swap_device = "/dev/{}".format(os.path.basename(real_swap_device))
real_swap_device = f"/dev/{os.path.basename(real_swap_device)}"
else:
real_swap_device = name

if real_swap_device in on_:
ret["comment"] = "Swap {} already active".format(name)
ret["comment"] = f"Swap {name} already active"
elif __opts__["test"]:
ret["result"] = None
ret["comment"] = "Swap {} is set to be activated".format(name)
ret["comment"] = f"Swap {name} is set to be activated"
else:
__salt__["mount.swapon"](real_swap_device)

on_ = __salt__["mount.swaps"]()

if real_swap_device in on_:
ret["comment"] = "Swap {} activated".format(name)
ret["comment"] = f"Swap {name} activated"
ret["changes"] = on_[real_swap_device]
else:
ret["comment"] = "Swap {} failed to activate".format(name)
ret["comment"] = f"Swap {name} failed to activate"
ret["result"] = False

if persist:
Expand Down Expand Up @@ -949,7 +987,7 @@ def unmounted(
# The mount is present! Unmount it
if __opts__["test"]:
ret["result"] = None
ret["comment"] = "Mount point {} is mounted but should not be".format(name)
ret["comment"] = f"Mount point {name} is mounted but should not be"
return ret
if device:
out = __salt__["mount.umount"](name, device, user=user)
Expand Down Expand Up @@ -1041,10 +1079,10 @@ def mod_watch(name, user=None, **kwargs):
name, kwargs["device"], False, kwargs["fstype"], kwargs["opts"], user=user
)
if out:
ret["comment"] = "{} remounted".format(name)
ret["comment"] = f"{name} remounted"
else:
ret["result"] = False
ret["comment"] = "{} failed to remount: {}".format(name, out)
ret["comment"] = f"{name} failed to remount: {out}"
else:
ret["comment"] = "Watch not supported in {} at this time".format(kwargs["sfun"])
return ret
Expand All @@ -1064,7 +1102,7 @@ def _convert_to(maybe_device, convert_to):
if (
not convert_to
or (convert_to == "device" and maybe_device.startswith("/"))
or maybe_device.startswith("{}=".format(convert_to.upper()))
or maybe_device.startswith(f"{convert_to.upper()}=")
):
return maybe_device

Expand All @@ -1080,7 +1118,7 @@ def _convert_to(maybe_device, convert_to):
result = next(iter(blkid))
else:
key = convert_to.upper()
result = "{}={}".format(key, next(iter(blkid.values()))[key])
result = f"{key}={next(iter(blkid.values()))[key]}"

return result

Expand Down Expand Up @@ -1232,7 +1270,7 @@ def fstab_present(
msg = "{} entry will be written in {}."
ret["comment"].append(msg.format(fs_file, config))
if mount:
msg = "Will mount {} on {}".format(name, fs_file)
msg = f"Will mount {name} on {fs_file}"
ret["comment"].append(msg)
elif out == "change":
msg = "{} entry will be updated in {}."
Expand Down Expand Up @@ -1290,7 +1328,7 @@ def fstab_present(
ret["result"] = False
msg = "Error while mounting {}".format(out.split(":", maxsplit=1)[1])
else:
msg = "Mounted {} on {}".format(name, fs_file)
msg = f"Mounted {name} on {fs_file}"
ret["comment"].append(msg)
elif out == "change":
ret["changes"]["persist"] = out
Expand Down
15 changes: 15 additions & 0 deletions tests/pytests/unit/modules/test_mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,21 @@ def test_umount():
mock.assert_called_once_with("/mountpoint", disk="/path/to/my.qcow")


def test_umount_lazy_true():
"""
Attempt to lazy unmount a device by specifying the
directory it is mounted on
"""
mock_mount_active = MagicMock(return_value={"name": "name"})
with patch.object(mount, "active", mock_mount_active):
mock_cmd = MagicMock(return_value={"retcode": True, "stderr": True})
with patch.dict(mount.__salt__, {"cmd.run_all": mock_cmd}):
mount.umount("name", lazy=True)
mock_cmd.assert_called_once_with(
"umount -l 'name'", runas=None, python_shell=False
)


def test_is_fuse_exec():
"""
Returns true if the command passed is a fuse mountable application
Expand Down
Loading
Loading