blob: 5bba44304d3299ec7be2f6984850d3c4ef941323 [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Script to lock/unlock machines."""
from __future__ import division
from __future__ import print_function
__author__ = 'asharif@google.com (Ahmad Sharif)'
import argparse
import datetime
import fcntl
import getpass
import glob
import json
import os
import socket
import sys
import time
from cros_utils import logger
LOCK_SUFFIX = '_check_lock_liveness'
# The locks file directory REQUIRES that 'group' only has read/write
# privileges and 'world' has no privileges. So the mask must be
# '0o27': 0o777 - 0o27 = 0o750.
LOCK_MASK = 0o27
def FileCheckName(name):
return name + LOCK_SUFFIX
def OpenLiveCheck(file_name):
with FileCreationMask(LOCK_MASK):
fd = open(file_name, 'a')
try:
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError as e:
logger.GetLogger().LogError(e)
raise
return fd
class FileCreationMask(object):
"""Class for the file creation mask."""
def __init__(self, mask):
self._mask = mask
self._old_mask = None
def __enter__(self):
self._old_mask = os.umask(self._mask)
def __exit__(self, typ, value, traceback):
os.umask(self._old_mask)
class LockDescription(object):
"""The description of the lock."""
def __init__(self, desc=None):
try:
self.owner = desc['owner']
self.exclusive = desc['exclusive']
self.counter = desc['counter']
self.time = desc['time']
self.reason = desc['reason']
self.auto = desc['auto']
except (KeyError, TypeError):
self.owner = ''
self.exclusive = False
self.counter = 0
self.time = 0
self.reason = ''
self.auto = False
def IsLocked(self):
return self.counter or self.exclusive
def __str__(self):
return ' '.join([
'Owner: %s' % self.owner,
'Exclusive: %s' % self.exclusive,
'Counter: %s' % self.counter,
'Time: %s' % self.time,
'Reason: %s' % self.reason,
'Auto: %s' % self.auto,
])
class FileLock(object):
"""File lock operation class."""
FILE_OPS = []
def __init__(self, lock_filename):
self._filepath = lock_filename
lock_dir = os.path.dirname(lock_filename)
assert os.path.isdir(lock_dir), ("Locks dir: %s doesn't exist!" % lock_dir)
self._file = None
self._description = None
self.exclusive = None
self.auto = None
self.reason = None
self.time = None
self.owner = None
def getDescription(self):
return self._description
def getFilePath(self):
return self._filepath
def setDescription(self, desc):
self._description = desc
@classmethod
def AsString(cls, file_locks):
stringify_fmt = '%-30s %-15s %-4s %-4s %-15s %-40s %-4s'
header = stringify_fmt % ('machine', 'owner', 'excl', 'ctr', 'elapsed',
'reason', 'auto')
lock_strings = []
for file_lock in file_locks:
elapsed_time = datetime.timedelta(
seconds=int(time.time() - file_lock.getDescription().time))
elapsed_time = '%s ago' % elapsed_time
lock_strings.append(
stringify_fmt % (os.path.basename(file_lock.getFilePath),
file_lock.getDescription().owner,
file_lock.getDescription().exclusive,
file_lock.getDescription().counter, elapsed_time,
file_lock.getDescription().reason,
file_lock.getDescription().auto))
table = '\n'.join(lock_strings)
return '\n'.join([header, table])
@classmethod
def ListLock(cls, pattern, locks_dir):
if not locks_dir:
locks_dir = Machine.LOCKS_DIR
full_pattern = os.path.join(locks_dir, pattern)
file_locks = []
for lock_filename in glob.glob(full_pattern):
if LOCK_SUFFIX in lock_filename:
continue
file_lock = FileLock(lock_filename)
with file_lock as lock:
if lock.IsLocked():
file_locks.append(file_lock)
logger.GetLogger().LogOutput('\n%s' % cls.AsString(file_locks))
def __enter__(self):
with FileCreationMask(LOCK_MASK):
try:
self._file = open(self._filepath, 'a+')
self._file.seek(0, os.SEEK_SET)
if fcntl.flock(self._file.fileno(), fcntl.LOCK_EX) == -1:
raise IOError('flock(%s, LOCK_EX) failed!' % self._filepath)
try:
desc = json.load(self._file)
except (EOFError, ValueError):
desc = None
self._description = LockDescription(desc)
if self._description.exclusive and self._description.auto:
locked_byself = False
for fd in self.FILE_OPS:
if fd.name == FileCheckName(self._filepath):
locked_byself = True
break
if not locked_byself:
try:
fp = OpenLiveCheck(FileCheckName(self._filepath))
except IOError:
pass
else:
self._description = LockDescription()
fcntl.lockf(fp, fcntl.LOCK_UN)
fp.close()
return self._description
# Check this differently?
except IOError as ex:
logger.GetLogger().LogError(ex)
return None
def __exit__(self, typ, value, traceback):
self._file.truncate(0)
self._file.write(json.dumps(self._description.__dict__, skipkeys=True))
self._file.close()
def __str__(self):
return self.AsString([self])
class Lock(object):
"""Lock class"""
def __init__(self, lock_file, auto=True):
self._to_lock = os.path.basename(lock_file)
self._lock_file = lock_file
self._logger = logger.GetLogger()
self._auto = auto
def NonBlockingLock(self, exclusive, reason=''):
with FileLock(self._lock_file) as lock:
if lock.exclusive:
self._logger.LogError(
'Exclusive lock already acquired by %s. Reason: %s' % (lock.owner,
lock.reason))
return False
if exclusive:
if lock.counter:
self._logger.LogError('Shared lock already acquired')
return False
lock_file_check = FileCheckName(self._lock_file)
fd = OpenLiveCheck(lock_file_check)
FileLock.FILE_OPS.append(fd)
lock.exclusive = True
lock.reason = reason
lock.owner = getpass.getuser()
lock.time = time.time()
lock.auto = self._auto
else:
lock.counter += 1
self._logger.LogOutput('Successfully locked: %s' % self._to_lock)
return True
def Unlock(self, exclusive, force=False):
with FileLock(self._lock_file) as lock:
if not lock.IsLocked():
self._logger.LogWarning("Can't unlock unlocked machine!")
return True
if lock.exclusive != exclusive:
self._logger.LogError('shared locks must be unlocked with --shared')
return False
if lock.exclusive:
if lock.owner != getpass.getuser() and not force:
self._logger.LogError("%s can't unlock lock owned by: %s" %
(getpass.getuser(), lock.owner))
return False
if lock.auto != self._auto:
self._logger.LogError("Can't unlock lock with different -a"
' parameter.')
return False
lock.exclusive = False
lock.reason = ''
lock.owner = ''
if self._auto:
del_list = [
i for i in FileLock.FILE_OPS
if i.name == FileCheckName(self._lock_file)
]
for i in del_list:
FileLock.FILE_OPS.remove(i)
for f in del_list:
fcntl.lockf(f, fcntl.LOCK_UN)
f.close()
del del_list
os.remove(FileCheckName(self._lock_file))
else:
lock.counter -= 1
return True
class Machine(object):
"""Machine class"""
LOCKS_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/locks'
def __init__(self, name, locks_dir=LOCKS_DIR, auto=True):
self._name = name
self._auto = auto
try:
self._full_name = socket.gethostbyaddr(name)[0]
except socket.error:
self._full_name = self._name
self._full_name = os.path.join(locks_dir, self._full_name)
def Lock(self, exclusive=False, reason=''):
lock = Lock(self._full_name, self._auto)
return lock.NonBlockingLock(exclusive, reason)
def TryLock(self, timeout=300, exclusive=False, reason=''):
locked = False
sleep = timeout / 10
while True:
locked = self.Lock(exclusive, reason)
if locked or timeout < 0:
break
print('Lock not acquired for {0}, wait {1} seconds ...'.format(
self._name, sleep))
time.sleep(sleep)
timeout -= sleep
return locked
def Unlock(self, exclusive=False, ignore_ownership=False):
lock = Lock(self._full_name, self._auto)
return lock.Unlock(exclusive, ignore_ownership)
def Main(argv):
"""The main function."""
parser = argparse.ArgumentParser()
parser.add_argument(
'-r', '--reason', dest='reason', default='', help='The lock reason.')
parser.add_argument(
'-u',
'--unlock',
dest='unlock',
action='store_true',
default=False,
help='Use this to unlock.')
parser.add_argument(
'-l',
'--list_locks',
dest='list_locks',
action='store_true',
default=False,
help='Use this to list locks.')
parser.add_argument(
'-f',
'--ignore_ownership',
dest='ignore_ownership',
action='store_true',
default=False,
help="Use this to force unlock on a lock you don't own.")
parser.add_argument(
'-s',
'--shared',
dest='shared',
action='store_true',
default=False,
help='Use this for a shared (non-exclusive) lock.')
parser.add_argument(
'-d',
'--dir',
dest='locks_dir',
action='store',
default=Machine.LOCKS_DIR,
help='Use this to set different locks_dir')
parser.add_argument('args', nargs='*', help='Machine arg.')
options = parser.parse_args(argv)
options.locks_dir = os.path.abspath(options.locks_dir)
exclusive = not options.shared
if not options.list_locks and len(options.args) != 2:
logger.GetLogger().LogError(
'Either --list_locks or a machine arg is needed.')
return 1
if len(options.args) > 1:
machine = Machine(options.args[1], options.locks_dir, auto=False)
else:
machine = None
if options.list_locks:
FileLock.ListLock('*', options.locks_dir)
retval = True
elif options.unlock:
retval = machine.Unlock(exclusive, options.ignore_ownership)
else:
retval = machine.Lock(exclusive, options.reason)
if retval:
return 0
else:
return 1
if __name__ == '__main__':
sys.exit(Main(sys.argv[1:]))