From 7b8e7371db32258a9dbbd35cf28714e74cf8e397 Mon Sep 17 00:00:00 2001 From: Sage Weil Date: Wed, 18 Dec 2019 11:01:33 -0600 Subject: [PATCH] cephadm: add FileLock class This is an abbreviated version of https://github.com/benediktschmitt/py-filelock Signed-off-by: Sage Weil --- src/cephadm/cephadm | 193 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/src/cephadm/cephadm b/src/cephadm/cephadm index 526f9a0cefe..b8961e543bc 100755 --- a/src/cephadm/cephadm +++ b/src/cephadm/cephadm @@ -3,6 +3,7 @@ DEFAULT_IMAGE='ceph/daemon-base:latest-master-devel' # FIXME when octopus is ready!!! DATA_DIR='/var/lib/ceph' LOG_DIR='/var/log/ceph' +LOCK_DIR='/run/cephadm' LOGROTATE_DIR='/etc/logrotate.d' UNIT_DIR='/etc/systemd/system' LOG_DIR_MODE=0o770 @@ -122,6 +123,198 @@ def port_in_use(port_num): return False +################################## + +# this is an abbreviated version of +# https://github.com/benediktschmitt/py-filelock/blob/master/filelock.py +# that drops all of the compatibility (this is Unix/Linux only). + +try: + TimeoutError +except NameError: + TimeoutError = OSError + +class Timeout(TimeoutError): + """ + Raised when the lock could not be acquired in *timeout* + seconds. + """ + + def __init__(self, lock_file): + """ + """ + #: The path of the file lock. + self.lock_file = lock_file + return None + + def __str__(self): + temp = "The file lock '{}' could not be acquired."\ + .format(self.lock_file) + return temp + + +class _Acquire_ReturnProxy(object): + def __init__(self, lock): + self.lock = lock + return None + + def __enter__(self): + return self.lock + + def __exit__(self, exc_type, exc_value, traceback): + self.lock.release() + return None + + +class FileLock(object): + def __init__(self, name, timeout = -1): + if not os.path.exists(LOCK_DIR): + os.mkdir(LOCK_DIR, 0o700) + self._lock_file = os.path.join(LOCK_DIR, name + '.lock') + + # The file descriptor for the *_lock_file* as it is returned by the + # os.open() function. + # This file lock is only NOT None, if the object currently holds the + # lock. + self._lock_file_fd = None + self.timeout = timeout + # The lock counter is used for implementing the nested locking + # mechanism. Whenever the lock is acquired, the counter is increased and + # the lock is only released, when this value is 0 again. + self._lock_counter = 0 + return None + + @property + def is_locked(self): + return self._lock_file_fd is not None + + def acquire(self, timeout=None, poll_intervall=0.05): + """ + Acquires the file lock or fails with a :exc:`Timeout` error. + .. code-block:: python + # You can use this method in the context manager (recommended) + with lock.acquire(): + pass + # Or use an equivalent try-finally construct: + lock.acquire() + try: + pass + finally: + lock.release() + :arg float timeout: + The maximum time waited for the file lock. + If ``timeout < 0``, there is no timeout and this method will + block until the lock could be acquired. + If ``timeout`` is None, the default :attr:`~timeout` is used. + :arg float poll_intervall: + We check once in *poll_intervall* seconds if we can acquire the + file lock. + :raises Timeout: + if the lock could not be acquired in *timeout* seconds. + .. versionchanged:: 2.0.0 + This method returns now a *proxy* object instead of *self*, + so that it can be used in a with statement without side effects. + """ + # Use the default timeout, if no timeout is provided. + if timeout is None: + timeout = self.timeout + + # Increment the number right at the beginning. + # We can still undo it, if something fails. + self._lock_counter += 1 + + lock_id = id(self) + lock_filename = self._lock_file + start_time = time.time() + try: + while True: + if not self.is_locked: + logger.debug('Acquiring lock %s on %s', lock_id, + lock_filename) + self._acquire() + + if self.is_locked: + logger.debug('Lock %s acquired on %s', lock_id, + lock_filename) + break + elif timeout >= 0 and time.time() - start_time > timeout: + logger.warning('Timeout acquiring lock %s on %s', lock_id, + lock_filename) + raise Timeout(self._lock_file) + else: + logger.debug( + 'Lock %s not acquired on %s, waiting %s seconds ...', + lock_id, lock_filename, poll_intervall + ) + time.sleep(poll_intervall) + except: + # Something did go wrong, so decrement the counter. + self._lock_counter = max(0, self._lock_counter - 1) + + raise + return _Acquire_ReturnProxy(lock = self) + + def release(self, force = False): + """ + Releases the file lock. + Please note, that the lock is only completly released, if the lock + counter is 0. + Also note, that the lock file itself is not automatically deleted. + :arg bool force: + If true, the lock counter is ignored and the lock is released in + every case. + """ + if self.is_locked: + self._lock_counter -= 1 + + if self._lock_counter == 0 or force: + lock_id = id(self) + lock_filename = self._lock_file + + logger.debug('Releasing lock %s on %s', lock_id, lock_filename) + self._release() + self._lock_counter = 0 + logger.debug('Lock %s released on %s', lock_id, lock_filename) + + return None + + def __enter__(self): + self.acquire() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.release() + return None + + def __del__(self): + self.release(force = True) + return None + + + def _acquire(self): + open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC + fd = os.open(self._lock_file, open_mode) + + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except (IOError, OSError): + os.close(fd) + else: + self._lock_file_fd = fd + return None + + def _release(self): + # Do not remove the lockfile: + # + # https://github.com/benediktschmitt/py-filelock/issues/31 + # https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition + fd = self._lock_file_fd + self._lock_file_fd = None + fcntl.flock(fd, fcntl.LOCK_UN) + os.close(fd) + return None + + ################################## # Popen wrappers, lifted from ceph-volume -- 2.39.5