Home | History | Annotate | Download | only in toolchain-utils
      1 #!/usr/bin/env python2
      2 #
      3 # Copyright 2010 Google Inc. All Rights Reserved.
      4 """Script to lock/unlock machines."""
      5 
      6 from __future__ import print_function
      7 
      8 __author__ = 'asharif (at] google.com (Ahmad Sharif)'
      9 
     10 import argparse
     11 import datetime
     12 import fcntl
     13 import getpass
     14 import glob
     15 import json
     16 import os
     17 import socket
     18 import sys
     19 import time
     20 
     21 from cros_utils import logger
     22 
     23 LOCK_SUFFIX = '_check_lock_liveness'
     24 
     25 # The locks file directory REQUIRES that 'group' only has read/write
     26 # privileges and 'world' has no privileges.  So the mask must be
     27 # '0027': 0777 - 0027 = 0750.
     28 LOCK_MASK = 0027
     29 
     30 
     31 def FileCheckName(name):
     32   return name + LOCK_SUFFIX
     33 
     34 
     35 def OpenLiveCheck(file_name):
     36   with FileCreationMask(LOCK_MASK):
     37     fd = open(file_name, 'a')
     38   try:
     39     fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
     40   except IOError:
     41     raise
     42   return fd
     43 
     44 
     45 class FileCreationMask(object):
     46   """Class for the file creation mask."""
     47 
     48   def __init__(self, mask):
     49     self._mask = mask
     50     self._old_mask = None
     51 
     52   def __enter__(self):
     53     self._old_mask = os.umask(self._mask)
     54 
     55   def __exit__(self, typ, value, traceback):
     56     os.umask(self._old_mask)
     57 
     58 
     59 class LockDescription(object):
     60   """The description of the lock."""
     61 
     62   def __init__(self, desc=None):
     63     try:
     64       self.owner = desc['owner']
     65       self.exclusive = desc['exclusive']
     66       self.counter = desc['counter']
     67       self.time = desc['time']
     68       self.reason = desc['reason']
     69       self.auto = desc['auto']
     70     except (KeyError, TypeError):
     71       self.owner = ''
     72       self.exclusive = False
     73       self.counter = 0
     74       self.time = 0
     75       self.reason = ''
     76       self.auto = False
     77 
     78   def IsLocked(self):
     79     return self.counter or self.exclusive
     80 
     81   def __str__(self):
     82     return ' '.join([
     83         'Owner: %s' % self.owner, 'Exclusive: %s' % self.exclusive,
     84         'Counter: %s' % self.counter, 'Time: %s' % self.time,
     85         'Reason: %s' % self.reason, 'Auto: %s' % self.auto
     86     ])
     87 
     88 
     89 class FileLock(object):
     90   """File lock operation class."""
     91   FILE_OPS = []
     92 
     93   def __init__(self, lock_filename):
     94     self._filepath = lock_filename
     95     lock_dir = os.path.dirname(lock_filename)
     96     assert os.path.isdir(lock_dir), ("Locks dir: %s doesn't exist!" % lock_dir)
     97     self._file = None
     98     self._description = None
     99 
    100   def getDescription(self):
    101     return self._description
    102 
    103   def getFilePath(self):
    104     return self._filepath
    105 
    106   def setDescription(self, desc):
    107     self._description = desc
    108 
    109   @classmethod
    110   def AsString(cls, file_locks):
    111     stringify_fmt = '%-30s %-15s %-4s %-4s %-15s %-40s %-4s'
    112     header = stringify_fmt % ('machine', 'owner', 'excl', 'ctr', 'elapsed',
    113                               'reason', 'auto')
    114     lock_strings = []
    115     for file_lock in file_locks:
    116 
    117       elapsed_time = datetime.timedelta(
    118           seconds=int(time.time() - file_lock.getDescription().time))
    119       elapsed_time = '%s ago' % elapsed_time
    120       lock_strings.append(
    121           stringify_fmt %
    122           (os.path.basename(file_lock.getFilePath),
    123            file_lock.getDescription().owner,
    124            file_lock.getDescription().exclusive,
    125            file_lock.getDescription().counter, elapsed_time,
    126            file_lock.getDescription().reason, file_lock.getDescription().auto))
    127     table = '\n'.join(lock_strings)
    128     return '\n'.join([header, table])
    129 
    130   @classmethod
    131   def ListLock(cls, pattern, locks_dir):
    132     if not locks_dir:
    133       locks_dir = Machine.LOCKS_DIR
    134     full_pattern = os.path.join(locks_dir, pattern)
    135     file_locks = []
    136     for lock_filename in glob.glob(full_pattern):
    137       if LOCK_SUFFIX in lock_filename:
    138         continue
    139       file_lock = FileLock(lock_filename)
    140       with file_lock as lock:
    141         if lock.IsLocked():
    142           file_locks.append(file_lock)
    143     logger.GetLogger().LogOutput('\n%s' % cls.AsString(file_locks))
    144 
    145   def __enter__(self):
    146     with FileCreationMask(LOCK_MASK):
    147       try:
    148         self._file = open(self._filepath, 'a+')
    149         self._file.seek(0, os.SEEK_SET)
    150 
    151         if fcntl.flock(self._file.fileno(), fcntl.LOCK_EX) == -1:
    152           raise IOError('flock(%s, LOCK_EX) failed!' % self._filepath)
    153 
    154         try:
    155           desc = json.load(self._file)
    156         except (EOFError, ValueError):
    157           desc = None
    158         self._description = LockDescription(desc)
    159 
    160         if self._description.exclusive and self._description.auto:
    161           locked_byself = False
    162           for fd in self.FILE_OPS:
    163             if fd.name == FileCheckName(self._filepath):
    164               locked_byself = True
    165               break
    166           if not locked_byself:
    167             try:
    168               fp = OpenLiveCheck(FileCheckName(self._filepath))
    169             except IOError:
    170               pass
    171             else:
    172               self._description = LockDescription()
    173               fcntl.lockf(fp, fcntl.LOCK_UN)
    174               fp.close()
    175         return self._description
    176       # Check this differently?
    177       except IOError as ex:
    178         logger.GetLogger().LogError(ex)
    179         return None
    180 
    181   def __exit__(self, typ, value, traceback):
    182     self._file.truncate(0)
    183     self._file.write(json.dumps(self._description.__dict__, skipkeys=True))
    184     self._file.close()
    185 
    186   def __str__(self):
    187     return self.AsString([self])
    188 
    189 
    190 class Lock(object):
    191   """Lock class"""
    192 
    193   def __init__(self, lock_file, auto=True):
    194     self._to_lock = os.path.basename(lock_file)
    195     self._lock_file = lock_file
    196     self._logger = logger.GetLogger()
    197     self._auto = auto
    198 
    199   def NonBlockingLock(self, exclusive, reason=''):
    200     with FileLock(self._lock_file) as lock:
    201       if lock.exclusive:
    202         self._logger.LogError(
    203             'Exclusive lock already acquired by %s. Reason: %s' % (lock.owner,
    204                                                                    lock.reason))
    205         return False
    206 
    207       if exclusive:
    208         if lock.counter:
    209           self._logger.LogError('Shared lock already acquired')
    210           return False
    211         lock_file_check = FileCheckName(self._lock_file)
    212         fd = OpenLiveCheck(lock_file_check)
    213         FileLock.FILE_OPS.append(fd)
    214 
    215         lock.exclusive = True
    216         lock.reason = reason
    217         lock.owner = getpass.getuser()
    218         lock.time = time.time()
    219         lock.auto = self._auto
    220       else:
    221         lock.counter += 1
    222     self._logger.LogOutput('Successfully locked: %s' % self._to_lock)
    223     return True
    224 
    225   def Unlock(self, exclusive, force=False):
    226     with FileLock(self._lock_file) as lock:
    227       if not lock.IsLocked():
    228         self._logger.LogWarning("Can't unlock unlocked machine!")
    229         return True
    230 
    231       if lock.exclusive != exclusive:
    232         self._logger.LogError('shared locks must be unlocked with --shared')
    233         return False
    234 
    235       if lock.exclusive:
    236         if lock.owner != getpass.getuser() and not force:
    237           self._logger.LogError("%s can't unlock lock owned by: %s" %
    238                                 (getpass.getuser(), lock.owner))
    239           return False
    240         if lock.auto != self._auto:
    241           self._logger.LogError("Can't unlock lock with different -a"
    242                                 ' parameter.')
    243           return False
    244         lock.exclusive = False
    245         lock.reason = ''
    246         lock.owner = ''
    247 
    248         if self._auto:
    249           del_list = [
    250               i for i in FileLock.FILE_OPS
    251               if i.name == FileCheckName(self._lock_file)
    252           ]
    253           for i in del_list:
    254             FileLock.FILE_OPS.remove(i)
    255           for f in del_list:
    256             fcntl.lockf(f, fcntl.LOCK_UN)
    257             f.close()
    258           del del_list
    259           os.remove(FileCheckName(self._lock_file))
    260 
    261       else:
    262         lock.counter -= 1
    263     return True
    264 
    265 
    266 class Machine(object):
    267   """Machine class"""
    268 
    269   LOCKS_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/locks'
    270 
    271   def __init__(self, name, locks_dir=LOCKS_DIR, auto=True):
    272     self._name = name
    273     self._auto = auto
    274     try:
    275       self._full_name = socket.gethostbyaddr(name)[0]
    276     except socket.error:
    277       self._full_name = self._name
    278     self._full_name = os.path.join(locks_dir, self._full_name)
    279 
    280   def Lock(self, exclusive=False, reason=''):
    281     lock = Lock(self._full_name, self._auto)
    282     return lock.NonBlockingLock(exclusive, reason)
    283 
    284   def TryLock(self, timeout=300, exclusive=False, reason=''):
    285     locked = False
    286     sleep = timeout / 10
    287     while True:
    288       locked = self.Lock(exclusive, reason)
    289       if locked or not timeout >= 0:
    290         break
    291       print('Lock not acquired for {0}, wait {1} seconds ...'.format(
    292           self._name, sleep))
    293       time.sleep(sleep)
    294       timeout -= sleep
    295     return locked
    296 
    297   def Unlock(self, exclusive=False, ignore_ownership=False):
    298     lock = Lock(self._full_name, self._auto)
    299     return lock.Unlock(exclusive, ignore_ownership)
    300 
    301 
    302 def Main(argv):
    303   """The main function."""
    304 
    305   parser = argparse.ArgumentParser()
    306   parser.add_argument(
    307       '-r', '--reason', dest='reason', default='', help='The lock reason.')
    308   parser.add_argument(
    309       '-u',
    310       '--unlock',
    311       dest='unlock',
    312       action='store_true',
    313       default=False,
    314       help='Use this to unlock.')
    315   parser.add_argument(
    316       '-l',
    317       '--list_locks',
    318       dest='list_locks',
    319       action='store_true',
    320       default=False,
    321       help='Use this to list locks.')
    322   parser.add_argument(
    323       '-f',
    324       '--ignore_ownership',
    325       dest='ignore_ownership',
    326       action='store_true',
    327       default=False,
    328       help="Use this to force unlock on a lock you don't own.")
    329   parser.add_argument(
    330       '-s',
    331       '--shared',
    332       dest='shared',
    333       action='store_true',
    334       default=False,
    335       help='Use this for a shared (non-exclusive) lock.')
    336   parser.add_argument(
    337       '-d',
    338       '--dir',
    339       dest='locks_dir',
    340       action='store',
    341       default=Machine.LOCKS_DIR,
    342       help='Use this to set different locks_dir')
    343   parser.add_argument('args', nargs='*', help='Machine arg.')
    344 
    345   options = parser.parse_args(argv)
    346 
    347   options.locks_dir = os.path.abspath(options.locks_dir)
    348   exclusive = not options.shared
    349 
    350   if not options.list_locks and len(options.args) != 2:
    351     logger.GetLogger().LogError(
    352         'Either --list_locks or a machine arg is needed.')
    353     return 1
    354 
    355   if len(options.args) > 1:
    356     machine = Machine(options.args[1], options.locks_dir, auto=False)
    357   else:
    358     machine = None
    359 
    360   if options.list_locks:
    361     FileLock.ListLock('*', options.locks_dir)
    362     retval = True
    363   elif options.unlock:
    364     retval = machine.Unlock(exclusive, options.ignore_ownership)
    365   else:
    366     retval = machine.Lock(exclusive, options.reason)
    367 
    368   if retval:
    369     return 0
    370   else:
    371     return 1
    372 
    373 
    374 if __name__ == '__main__':
    375   sys.exit(Main(sys.argv[1:]))
    376