mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-24 03:11:24 -07:00
* Use locking for concurrent file access This implements locking to be used for modules that are used for concurrent file access, like lineinfile or known_hosts. * Reinstate lock_timeout This commit includes: - New file locking infrastructure for modules - Enable timeout tests - Madifications to support concurrency with lineinfile * Rebase, update changelog and tests We need to specify ansible_python_interpreter to avoid running interpreter discovery and selecting the incorrect interpreter. Remove the import of lock in known_hosts since it is not used.
205 lines
5.9 KiB
Python
205 lines
5.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright: (c) 2018, Ansible Project
|
|
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
import fcntl
|
|
import os
|
|
import re
|
|
import stat
|
|
import sys
|
|
import time
|
|
|
|
from contextlib import contextmanager
|
|
from ansible.module_utils._text import to_bytes
|
|
from ansible.module_utils.six import PY3
|
|
|
|
try:
|
|
import selinux
|
|
HAVE_SELINUX = True
|
|
except ImportError:
|
|
HAVE_SELINUX = False
|
|
|
|
|
|
FILE_ATTRIBUTES = {
|
|
'A': 'noatime',
|
|
'a': 'append',
|
|
'c': 'compressed',
|
|
'C': 'nocow',
|
|
'd': 'nodump',
|
|
'D': 'dirsync',
|
|
'e': 'extents',
|
|
'E': 'encrypted',
|
|
'h': 'blocksize',
|
|
'i': 'immutable',
|
|
'I': 'indexed',
|
|
'j': 'journalled',
|
|
'N': 'inline',
|
|
's': 'zero',
|
|
'S': 'synchronous',
|
|
't': 'notail',
|
|
'T': 'blockroot',
|
|
'u': 'undelete',
|
|
'X': 'compressedraw',
|
|
'Z': 'compresseddirty',
|
|
}
|
|
|
|
|
|
# Used for parsing symbolic file perms
|
|
MODE_OPERATOR_RE = re.compile(r'[+=-]')
|
|
USERS_RE = re.compile(r'[^ugo]')
|
|
PERMS_RE = re.compile(r'[^rwxXstugo]')
|
|
|
|
|
|
_PERM_BITS = 0o7777 # file mode permission bits
|
|
_EXEC_PERM_BITS = 0o0111 # execute permission bits
|
|
_DEFAULT_PERM = 0o0666 # default file permission bits
|
|
|
|
|
|
# Ensure we use flock on e.g. FreeBSD, MacOSX and Solaris
|
|
if sys.platform.startswith('linux'):
|
|
filelock = fcntl.lockf
|
|
else:
|
|
filelock = fcntl.flock
|
|
|
|
|
|
def is_executable(path):
|
|
# This function's signature needs to be repeated
|
|
# as the first line of its docstring.
|
|
# This method is reused by the basic module,
|
|
# the repetion helps the basic module's html documentation come out right.
|
|
# http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_docstring_signature
|
|
'''is_executable(path)
|
|
|
|
is the given path executable?
|
|
|
|
:arg path: The path of the file to check.
|
|
|
|
Limitations:
|
|
|
|
* Does not account for FSACLs.
|
|
* Most times we really want to know "Can the current user execute this
|
|
file". This function does not tell us that, only if any execute bit is set.
|
|
'''
|
|
# These are all bitfields so first bitwise-or all the permissions we're
|
|
# looking for, then bitwise-and with the file's mode to determine if any
|
|
# execute bits are set.
|
|
return ((stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) & os.stat(path)[stat.ST_MODE])
|
|
|
|
|
|
def format_attributes(attributes):
|
|
attribute_list = [FILE_ATTRIBUTES.get(attr) for attr in attributes if attr in FILE_ATTRIBUTES]
|
|
return attribute_list
|
|
|
|
|
|
def get_flags_from_attributes(attributes):
|
|
flags = [key for key, attr in FILE_ATTRIBUTES.items() if attr in attributes]
|
|
return ''.join(flags)
|
|
|
|
|
|
def get_file_arg_spec():
|
|
arg_spec = dict(
|
|
mode=dict(type='raw'),
|
|
owner=dict(),
|
|
group=dict(),
|
|
seuser=dict(),
|
|
serole=dict(),
|
|
selevel=dict(),
|
|
setype=dict(),
|
|
attributes=dict(aliases=['attr']),
|
|
)
|
|
return arg_spec
|
|
|
|
|
|
class LockTimeout(Exception):
|
|
pass
|
|
|
|
|
|
# NOTE: Using the open_locked() context manager it is absolutely mandatory
|
|
# to not open or close the same file within the existing context.
|
|
# It is essential to reuse the returned file descriptor only.
|
|
@contextmanager
|
|
def open_locked(path, check_mode=False, lock_timeout=15):
|
|
'''
|
|
Context managed for opening files with lock acquisition
|
|
|
|
:kw path: Path (file) to lock
|
|
:kw lock_timeout:
|
|
Wait n seconds for lock acquisition, fail if timeout is reached.
|
|
0 = Do not wait, fail if lock cannot be acquired immediately,
|
|
Less than 0 or None = wait indefinitely until lock is released
|
|
Default is wait 15s.
|
|
:returns: file descriptor
|
|
'''
|
|
if check_mode:
|
|
b_path = to_bytes(path, errors='surrogate_or_strict')
|
|
fd = open(b_path, 'ab+')
|
|
fd.seek(0) # Due to a difference in behavior between PY2 and PY3 we need to seek(0) on PY3
|
|
else:
|
|
fd = lock(path, check_mode, lock_timeout)
|
|
yield fd
|
|
fd.close()
|
|
|
|
|
|
def lock(path, check_mode=False, lock_timeout=15):
|
|
'''
|
|
Set lock on given path via fcntl.flock(), note that using
|
|
locks does not guarantee exclusiveness unless all accessing
|
|
processes honor locks.
|
|
|
|
:kw path: Path (file) to lock
|
|
:kw lock_timeout:
|
|
Wait n seconds for lock acquisition, fail if timeout is reached.
|
|
0 = Do not wait, fail if lock cannot be acquired immediately,
|
|
Less than 0 or None = wait indefinitely until lock is released
|
|
Default is wait 15s.
|
|
:returns: file descriptor
|
|
'''
|
|
b_path = to_bytes(path, errors='surrogate_or_strict')
|
|
wait = 0.1
|
|
|
|
lock_exception = IOError
|
|
if PY3:
|
|
lock_exception = OSError
|
|
|
|
if not os.path.exists(b_path):
|
|
raise IOError('{0} does not exist'.format(path))
|
|
|
|
if lock_timeout is None or lock_timeout < 0:
|
|
fd = open(b_path, 'ab+')
|
|
fd.seek(0) # Due to a difference in behavior between PY2 and PY3 we need to seek(0) on PY3
|
|
filelock(fd, fcntl.LOCK_EX)
|
|
return fd
|
|
|
|
if lock_timeout >= 0:
|
|
total_wait = 0
|
|
while total_wait <= lock_timeout:
|
|
fd = open(b_path, 'ab+')
|
|
fd.seek(0) # Due to a difference in behavior between PY2 and PY3 we need to seek(0) on PY3
|
|
try:
|
|
filelock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
return fd
|
|
except lock_exception:
|
|
fd.close()
|
|
time.sleep(wait)
|
|
total_wait += wait
|
|
continue
|
|
|
|
fd.close()
|
|
raise LockTimeout('Waited {0} seconds for lock on {1}'.format(total_wait, path))
|
|
|
|
|
|
def unlock(fd):
|
|
'''
|
|
Make sure lock file is available for everyone and Unlock the file descriptor
|
|
locked by set_lock
|
|
|
|
:kw fd: File descriptor of file to unlock
|
|
'''
|
|
try:
|
|
filelock(fd, fcntl.LOCK_UN)
|
|
except ValueError: # File was not opened, let context manager fail gracefully
|
|
pass
|