prevent running second copy of fuelmenu

Create a file and take a lock on it before running main
part of fuel-menu, so running two copies would be impossible.

This patch adds an option -l, --lock-file to have ability to
specify path to lock file.

Change-Id: I8ab41e6e068caa0881e0affcbfb5695a9025b762
Closes-Bug: #1566401
This commit is contained in:
Dmitry Guryanov 2016-04-19 12:50:15 +03:00
parent cc4628c865
commit 3f84153673
4 changed files with 87 additions and 0 deletions

View File

@ -11,11 +11,14 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import fcntl
import logging
import os
import random as _random
import string
import subprocess
import sys
from fuelmenu import consts
@ -86,3 +89,21 @@ def gensalt():
sha512prefix = "$6$"
random_letters = ''.join(random.choice(letters) for _ in range(16))
return sha512prefix + random_letters
def lock_running(lock_file):
"""Tries to acquire app lock
:param lock_file: a path to a file for locking
:returns: True in case of success, False, if lock's already held
"""
global lock_file_obj
lock_file_obj = open(lock_file, "w")
try:
fcntl.lockf(lock_file_obj, fcntl.LOCK_EX | fcntl.LOCK_NB)
return True
except IOError:
print("Another copy of fuelmenu is running. "
"Please exit it and try again.", file=sys.stderr)
return False

View File

@ -21,6 +21,8 @@ PUPPET_LOGFILE = "/var/log/puppet/fuelmenu-puppet.log"
SETTINGS_FILE = "/etc/fuel/astute.yaml"
RELEASE_FILE = "/etc/fuel_release"
DEFAULT_LOCK_FILE = "/var/run/fuelmenu.lock"
PRE_DEPLOYMENT_MODE = "pre"
POST_DEPLOYMENT_MODE = "post"

View File

@ -469,8 +469,17 @@ def main(*args, **kwargs):
parser.add_option("-i", "--iface", dest="iface", metavar="IFACE",
default=default_iface, help="Set IFACE as primary.")
parser.add_option("-l", "--lock-file",
default=consts.DEFAULT_LOCK_FILE,
help="Path to the process lock file. If unspecified, "
"the default {} is used."
.format(consts.DEFAULT_LOCK_FILE))
options, args = parser.parse_args()
if not utils.lock_running(options.lock_file):
sys.exit(1)
if not network.is_interface_has_ip(options.iface):
print("Selected interface '{0}' has no assigned IP. "
"Could not start.".format(options.iface))

View File

@ -14,6 +14,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
import signal
import tempfile
from fuelmenu.common import utils
import mock
@ -73,3 +77,54 @@ class TestUtils(unittest.TestCase):
mocked_open.side_effect = IOError()
data = utils.get_fuel_version()
self.assertEqual("", data)
def test_lock_running(self):
lock_file = tempfile.mktemp()
self.assertTrue(utils.lock_running(lock_file))
def test_lock_running_fail(self):
def handler(signum, frame):
raise Exception("Timeout occured while running unit test "
"test_lock_running_fail")
# set an alarm, because test may hang
signal.signal(signal.SIGALRM, handler)
signal.setitimer(signal.ITIMER_REAL, 3)
lock_file = tempfile.mktemp()
read_fd1, write_fd1 = os.pipe()
read_fd2, write_fd2 = os.pipe()
pid = os.fork()
if pid == 0:
# Run lock_running in child first
os.close(read_fd1)
os.close(write_fd2)
write_f1 = os.fdopen(write_fd1, 'w')
read_f2 = os.fdopen(read_fd2, 'r')
utils.lock_running(lock_file)
write_f1.write('x')
write_f1.close()
read_f2.read()
# exit from child by issuing execve, so that unit
# testing framework will not finish in two instances
os.execlp('true', 'true')
else:
# then in parent
os.close(write_fd1)
os.close(read_fd2)
read_f1 = os.fdopen(read_fd1, 'r')
write_f2 = os.fdopen(write_fd2, 'w')
read_f1.read()
# child is holding lock at this point
self.assertFalse(utils.lock_running(lock_file))
write_f2.write('x')
write_f2.close()
signal.alarm(0)