lock fstab with ansible's FileLock to avoid race condition

This commit is contained in:
quidame 2021-03-30 06:13:44 +02:00
parent ceddb849b8
commit 6181b4d784
2 changed files with 106 additions and 88 deletions

View file

@ -0,0 +1,6 @@
---
bugfixes:
- mount - fix a race condition that might result in unknown/unexpected fstab
state when running the module on several inventory hostnames adressing the
same host. Lock it with ansible.module_utils.common.file.FileLock.lock_file()
(https://github.com/ansible-collections/ansible.posix/issues/115).

View file

@ -193,6 +193,7 @@ from ansible_collections.ansible.posix.plugins.module_utils.mount import ismount
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_bytes, to_native from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils.common.file import FileLock, LockTimeout
def write_fstab(module, lines, path): def write_fstab(module, lines, path):
@ -763,105 +764,116 @@ def main():
name = module.params['path'] name = module.params['path']
changed = False changed = False
if state == 'absent': lock_timeout = 2
name, changed = unset_mount(module, args) lock_tempdir = '/tmp'
lock_message = 'another instance of the module holds the %s lock' % os.path.basename(args['fstab'])
if changed and not module.check_mode: _fstab = FileLock()
if ismount(name) or is_bind_mounted(module, linux_mounts, name): try:
res, msg = umount(module, name) with _fstab.lock_file(args['fstab'], lock_tempdir, lock_timeout):
if res: if state == 'absent':
module.fail_json( name, changed = unset_mount(module, args)
msg="Error unmounting %s: %s" % (name, msg))
if os.path.exists(name): if changed and not module.check_mode:
try: if ismount(name) or is_bind_mounted(module, linux_mounts, name):
os.rmdir(name) res, msg = umount(module, 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: if res:
module.fail_json( module.fail_json(
msg="Error unmounting %s: %s" % (name, msg)) msg="Error unmounting %s: %s" % (name, msg))
changed = True if os.path.exists(name):
elif state == 'mounted':
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: try:
os.mkdir(b_curpath) os.rmdir(name)
dirs_created.append(b_curpath) except (OSError, IOError) as e:
except OSError as ex: module.fail_json(msg="Error rmdir %s: %s" % (name, to_native(e)))
# Possibly something else created the dir since the os.path.exists elif state == 'unmounted':
# check above. As long as it's a dir, we don't need to error out. if ismount(name) or is_bind_mounted(module, linux_mounts, name):
if not (ex.errno == errno.EEXIST and os.path.isdir(b_curpath)): if not module.check_mode:
raise res, msg = umount(module, name)
except (OSError, IOError) as e: if res:
module.fail_json( module.fail_json(
msg="Error making dir %s: %s" % (name, to_native(e))) msg="Error unmounting %s: %s" % (name, msg))
name, backup_lines, changed = _set_mount_save_old(module, args) changed = True
res = 0 elif state == 'mounted':
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)))
name, backup_lines, changed = _set_mount_save_old(module, args)
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
else:
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:
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))
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 changed = True
else: else:
changed = True module.fail_json(msg='Unexpected position reached')
if not module.check_mode: except LockTimeout:
res, msg = mount(module, args) module.fail_json(msg=lock_message)
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:
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 # If the managed node is Solaris, convert the boot value type to Boolean
# to match the type of return value with the module argument. # to match the type of return value with the module argument.