853180dbcf
- Minor improvement of the summary overview in the console output. - Store metadata about the LEAPP plugins inside leapp audit db. - Prevent leapp from starting if an instance is already runnning. - Resolves: RHEL-27848, RHEL-25407
176 lines
5.2 KiB
Diff
176 lines
5.2 KiB
Diff
From b64c44bfb741e17650c7c0d65f25fc4ef67fdf19 Mon Sep 17 00:00:00 2001
|
|
From: David Kubek <dkubek@redhat.com>
|
|
Date: Thu, 22 Feb 2024 12:51:04 +0100
|
|
Subject: [PATCH 6/7] Add process lock
|
|
|
|
This commit addresses the potential risk of running multiple instances
|
|
of Leapp simultaneously on a single system. It implements a simple lock
|
|
mechanism to prevent concurrent executions on a single system using a
|
|
simple BSD lock (`flock(2)`).
|
|
|
|
Lock is acquired at the start of the execution and a PID number is
|
|
stored in lockfile. The PID in lockfile currently has purely
|
|
informational character.
|
|
---
|
|
leapp/cli/__init__.py | 10 ++++--
|
|
leapp/config.py | 3 ++
|
|
leapp/exceptions.py | 4 +++
|
|
leapp/utils/lock.py | 83 +++++++++++++++++++++++++++++++++++++++++++
|
|
4 files changed, 97 insertions(+), 3 deletions(-)
|
|
create mode 100644 leapp/utils/lock.py
|
|
|
|
diff --git a/leapp/cli/__init__.py b/leapp/cli/__init__.py
|
|
index fe997d4..ab16198 100644
|
|
--- a/leapp/cli/__init__.py
|
|
+++ b/leapp/cli/__init__.py
|
|
@@ -2,12 +2,12 @@ import os
|
|
import pkgutil
|
|
import socket
|
|
import sys
|
|
-import textwrap
|
|
|
|
from leapp import VERSION
|
|
from leapp.cli import commands
|
|
-from leapp.exceptions import UnknownCommandError
|
|
+from leapp.exceptions import UnknownCommandError, ProcessLockError
|
|
from leapp.utils.clicmd import command
|
|
+from leapp.utils.lock import leapp_lock
|
|
|
|
|
|
@command('')
|
|
@@ -42,7 +42,8 @@ def main():
|
|
os.environ['LEAPP_HOSTNAME'] = socket.getfqdn()
|
|
_load_commands(cli.command)
|
|
try:
|
|
- cli.command.execute('leapp version {}'.format(VERSION))
|
|
+ with leapp_lock():
|
|
+ cli.command.execute('leapp version {}'.format(VERSION))
|
|
except UnknownCommandError as e:
|
|
bad_cmd = (
|
|
"Command \"{CMD}\" is unknown.\nMost likely there is a typo in the command or particular "
|
|
@@ -54,3 +55,6 @@ def main():
|
|
bad_cmd = "No such argument {CMD}"
|
|
print(bad_cmd.format(CMD=e.requested))
|
|
sys.exit(1)
|
|
+ except ProcessLockError as e:
|
|
+ sys.stderr.write('{}\nAborting.\n'.format(e.message))
|
|
+ sys.exit(1)
|
|
diff --git a/leapp/config.py b/leapp/config.py
|
|
index 18c357d..2487e0f 100644
|
|
--- a/leapp/config.py
|
|
+++ b/leapp/config.py
|
|
@@ -40,6 +40,9 @@ _CONFIG_DEFAULTS = {
|
|
'dir': '/var/log/leapp/',
|
|
'files': ','.join(_FILES_TO_ARCHIVE),
|
|
},
|
|
+ 'lock': {
|
|
+ 'path': '/var/run/leapp.pid'
|
|
+ },
|
|
'logs': {
|
|
'dir': '/var/log/leapp/',
|
|
'files': ','.join(_LOGS),
|
|
diff --git a/leapp/exceptions.py b/leapp/exceptions.py
|
|
index 43c5905..9463a5c 100644
|
|
--- a/leapp/exceptions.py
|
|
+++ b/leapp/exceptions.py
|
|
@@ -148,3 +148,7 @@ class RequestStopAfterPhase(LeappError):
|
|
|
|
def __init__(self):
|
|
super(RequestStopAfterPhase, self).__init__('Stop after phase has been requested.')
|
|
+
|
|
+
|
|
+class ProcessLockError(LeappError):
|
|
+ """ This exception is used to represent an error within the process locking mechanism. """
|
|
diff --git a/leapp/utils/lock.py b/leapp/utils/lock.py
|
|
new file mode 100644
|
|
index 0000000..33e825c
|
|
--- /dev/null
|
|
+++ b/leapp/utils/lock.py
|
|
@@ -0,0 +1,83 @@
|
|
+import os
|
|
+import fcntl
|
|
+import logging
|
|
+
|
|
+from leapp.config import get_config
|
|
+from leapp.exceptions import ProcessLockError
|
|
+
|
|
+
|
|
+def leapp_lock(lockfile=None):
|
|
+ return ProcessLock(lockfile=lockfile)
|
|
+
|
|
+
|
|
+def _acquire_lock(fd):
|
|
+ try:
|
|
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
+ return True
|
|
+ except OSError:
|
|
+ return False
|
|
+
|
|
+
|
|
+def _clear_lock(fd):
|
|
+ os.lseek(fd, 0, os.SEEK_SET)
|
|
+ os.ftruncate(fd, 0)
|
|
+
|
|
+
|
|
+def _read_pid(fd):
|
|
+ return os.read(fd, 20)
|
|
+
|
|
+
|
|
+def _write_pid(fd, pid):
|
|
+ _clear_lock(fd)
|
|
+ os.write(fd, str(pid).encode('utf-8'))
|
|
+
|
|
+
|
|
+class ProcessLock(object):
|
|
+
|
|
+ def __init__(self, lockfile=None):
|
|
+ self.log = logging.getLogger('leapp.utils.lock')
|
|
+ self.lockfile = lockfile if lockfile else get_config().get('lock', 'path')
|
|
+
|
|
+ self.fd = None
|
|
+
|
|
+ def _get_pid_from_lockfile(self):
|
|
+ running_pid = _read_pid(self.fd)
|
|
+ self.log.debug("_get_pid_from_lockfile: running_pid=%s", running_pid)
|
|
+ running_pid = int(running_pid)
|
|
+
|
|
+ return running_pid
|
|
+
|
|
+ def _try_lock(self, pid):
|
|
+ if not _acquire_lock(self.fd):
|
|
+ try:
|
|
+ running_pid = self._get_pid_from_lockfile()
|
|
+ except ValueError:
|
|
+ process_msg = ''
|
|
+ else:
|
|
+ process_msg = ' by process with PID {}'.format(running_pid)
|
|
+
|
|
+ msg = (
|
|
+ 'Leapp is currently locked{} and cannot be started.\n'
|
|
+ 'Please ensure no other instance of leapp is running and then delete the lockfile at {} and try again.'
|
|
+ ).format(process_msg, self.lockfile)
|
|
+ raise ProcessLockError(msg)
|
|
+
|
|
+ try:
|
|
+ _write_pid(self.fd, pid)
|
|
+ except OSError:
|
|
+ raise ProcessLockError('Could not write PID to lockfile.')
|
|
+
|
|
+ def __enter__(self):
|
|
+ my_pid = os.getpid()
|
|
+
|
|
+ self.fd = os.open(self.lockfile, os.O_CREAT | os.O_RDWR, 0o600)
|
|
+ try:
|
|
+ self._try_lock(my_pid)
|
|
+ except ProcessLockError:
|
|
+ os.close(self.fd)
|
|
+ raise
|
|
+
|
|
+ def __exit__(self, *exc_args):
|
|
+ _clear_lock(self.fd)
|
|
+ os.close(self.fd)
|
|
+ os.unlink(self.lockfile)
|
|
--
|
|
2.42.0
|
|
|