blob: 5e5c17ca7b32173952a03ad7cc59f32509e46685 [file] [log] [blame]
yunlian8a3bdcb2013-02-19 20:42:48 +00001#!/usr/bin/python
asharifb237bca2013-02-15 20:54:57 +00002#
3# Copyright 2010 Google Inc. All Rights Reserved.
yunlian52a6c572013-02-19 20:42:07 +00004"""Script to lock/unlock machines."""
asharifb237bca2013-02-15 20:54:57 +00005
Luis Lozanof2a3ef42015-12-15 13:49:30 -08006__author__ = 'asharif@google.com (Ahmad Sharif)'
asharifb237bca2013-02-15 20:54:57 +00007
8import datetime
asharif455157b2013-02-15 21:15:05 +00009import fcntl
asharifda8e93b2013-02-15 22:49:27 +000010import getpass
asharifb237bca2013-02-15 20:54:57 +000011import glob
yunlian8a3bdcb2013-02-19 20:42:48 +000012import json
asharifb237bca2013-02-15 20:54:57 +000013import optparse
14import os
asharifb237bca2013-02-15 20:54:57 +000015import socket
asharif455157b2013-02-15 21:15:05 +000016import sys
17import time
yunlian52a6c572013-02-19 20:42:07 +000018
asharifb237bca2013-02-15 20:54:57 +000019from utils import logger
20
Luis Lozanof2a3ef42015-12-15 13:49:30 -080021LOCK_SUFFIX = '_check_lock_liveness'
yunlian8c9419b2013-02-19 21:11:29 +000022
Caroline Tice6a00af92015-10-14 11:24:09 -070023# The locks file directory REQUIRES that 'group' only has read/write
24# privileges and 'world' has no privileges. So the mask must be
25# '0027': 0777 - 0027 = 0750.
26LOCK_MASK = 0027
yunlian8c9419b2013-02-19 21:11:29 +000027
Luis Lozanof2a3ef42015-12-15 13:49:30 -080028
yunlian8c9419b2013-02-19 21:11:29 +000029def FileCheckName(name):
30 return name + LOCK_SUFFIX
31
32
33def OpenLiveCheck(file_name):
Caroline Tice6a00af92015-10-14 11:24:09 -070034 with FileCreationMask(LOCK_MASK):
Luis Lozanof2a3ef42015-12-15 13:49:30 -080035 fd = open(file_name, 'a+w')
yunlian8c9419b2013-02-19 21:11:29 +000036 try:
37 fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
38 except IOError:
39 raise
40 return fd
41
asharifb237bca2013-02-15 20:54:57 +000042
asharif455157b2013-02-15 21:15:05 +000043class FileCreationMask(object):
Luis Lozanof2a3ef42015-12-15 13:49:30 -080044
asharif455157b2013-02-15 21:15:05 +000045 def __init__(self, mask):
46 self._mask = mask
47
48 def __enter__(self):
49 self._old_mask = os.umask(self._mask)
50
51 def __exit__(self, type, value, traceback):
52 os.umask(self._old_mask)
asharifb237bca2013-02-15 20:54:57 +000053
54
asharif455157b2013-02-15 21:15:05 +000055class LockDescription(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000056 """The description of the lock."""
57
yunlian8c9419b2013-02-19 21:11:29 +000058 def __init__(self, desc=None):
yunlian8a3bdcb2013-02-19 20:42:48 +000059 try:
Luis Lozanof2a3ef42015-12-15 13:49:30 -080060 self.owner = desc['owner']
61 self.exclusive = desc['exclusive']
62 self.counter = desc['counter']
63 self.time = desc['time']
64 self.reason = desc['reason']
65 self.auto = desc['auto']
yunlian8a3bdcb2013-02-19 20:42:48 +000066 except (KeyError, TypeError):
Luis Lozanof2a3ef42015-12-15 13:49:30 -080067 self.owner = ''
yunlian8a3bdcb2013-02-19 20:42:48 +000068 self.exclusive = False
69 self.counter = 0
70 self.time = 0
Luis Lozanof2a3ef42015-12-15 13:49:30 -080071 self.reason = ''
yunlian8c9419b2013-02-19 21:11:29 +000072 self.auto = False
asharifb237bca2013-02-15 20:54:57 +000073
asharif455157b2013-02-15 21:15:05 +000074 def IsLocked(self):
75 return self.counter or self.exclusive
asharifb237bca2013-02-15 20:54:57 +000076
asharif455157b2013-02-15 21:15:05 +000077 def __str__(self):
Luis Lozanof2a3ef42015-12-15 13:49:30 -080078 return ' '.join(['Owner: %s' % self.owner, 'Exclusive: %s' % self.exclusive,
79 'Counter: %s' % self.counter, 'Time: %s' % self.time,
80 'Reason: %s' % self.reason, 'Auto: %s' % self.auto])
asharifb237bca2013-02-15 20:54:57 +000081
82
asharif455157b2013-02-15 21:15:05 +000083class FileLock(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000084 """File lock operation class."""
yunlian8c9419b2013-02-19 21:11:29 +000085 FILE_OPS = []
asharif455157b2013-02-15 21:15:05 +000086
87 def __init__(self, lock_filename):
yunlian52a6c572013-02-19 20:42:07 +000088 self._filepath = lock_filename
89 lock_dir = os.path.dirname(lock_filename)
Luis Lozanof2a3ef42015-12-15 13:49:30 -080090 assert os.path.isdir(lock_dir), ("Locks dir: %s doesn't exist!" % lock_dir)
asharif455157b2013-02-15 21:15:05 +000091 self._file = None
92
93 @classmethod
94 def AsString(cls, file_locks):
Luis Lozanof2a3ef42015-12-15 13:49:30 -080095 stringify_fmt = '%-30s %-15s %-4s %-4s %-15s %-40s %-4s'
96 header = stringify_fmt % ('machine', 'owner', 'excl', 'ctr', 'elapsed',
97 'reason', 'auto')
asharif455157b2013-02-15 21:15:05 +000098 lock_strings = []
99 for file_lock in file_locks:
100
101 elapsed_time = datetime.timedelta(
102 seconds=int(time.time() - file_lock._description.time))
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800103 elapsed_time = '%s ago' % elapsed_time
104 lock_strings.append(
105 stringify_fmt %
106 (os.path.basename(file_lock._filepath), file_lock._description.owner,
107 file_lock._description.exclusive, file_lock._description.counter,
108 elapsed_time, file_lock._description.reason,
109 file_lock._description.auto))
110 table = '\n'.join(lock_strings)
111 return '\n'.join([header, table])
asharif455157b2013-02-15 21:15:05 +0000112
113 @classmethod
yunlian52a6c572013-02-19 20:42:07 +0000114 def ListLock(cls, pattern, locks_dir):
115 if not locks_dir:
116 locks_dir = Machine.LOCKS_DIR
117 full_pattern = os.path.join(locks_dir, pattern)
asharif455157b2013-02-15 21:15:05 +0000118 file_locks = []
119 for lock_filename in glob.glob(full_pattern):
yunlian8c9419b2013-02-19 21:11:29 +0000120 if LOCK_SUFFIX in lock_filename:
121 continue
asharif455157b2013-02-15 21:15:05 +0000122 file_lock = FileLock(lock_filename)
123 with file_lock as lock:
124 if lock.IsLocked():
125 file_locks.append(file_lock)
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800126 logger.GetLogger().LogOutput('\n%s' % cls.AsString(file_locks))
asharif455157b2013-02-15 21:15:05 +0000127
128 def __enter__(self):
Caroline Tice6a00af92015-10-14 11:24:09 -0700129 with FileCreationMask(LOCK_MASK):
asharif455157b2013-02-15 21:15:05 +0000130 try:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800131 self._file = open(self._filepath, 'a+')
asharif455157b2013-02-15 21:15:05 +0000132 self._file.seek(0, os.SEEK_SET)
133
134 if fcntl.flock(self._file.fileno(), fcntl.LOCK_EX) == -1:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800135 raise IOError('flock(%s, LOCK_EX) failed!' % self._filepath)
asharif455157b2013-02-15 21:15:05 +0000136
137 try:
yunlian8a3bdcb2013-02-19 20:42:48 +0000138 desc = json.load(self._file)
139 except (EOFError, ValueError):
140 desc = None
141 self._description = LockDescription(desc)
142
yunlian8c9419b2013-02-19 21:11:29 +0000143 if self._description.exclusive and self._description.auto:
144 locked_byself = False
145 for fd in self.FILE_OPS:
146 if fd.name == FileCheckName(self._filepath):
147 locked_byself = True
148 break
149 if not locked_byself:
150 try:
151 fp = OpenLiveCheck(FileCheckName(self._filepath))
152 except IOError:
153 pass
154 else:
155 self._description = LockDescription()
156 fcntl.lockf(fp, fcntl.LOCK_UN)
157 fp.close()
asharif455157b2013-02-15 21:15:05 +0000158 return self._description
159 # Check this differently?
160 except IOError as ex:
161 logger.GetLogger().LogError(ex)
162 return None
163
164 def __exit__(self, type, value, traceback):
165 self._file.truncate(0)
yunlian8a3bdcb2013-02-19 20:42:48 +0000166 self._file.write(json.dumps(self._description.__dict__, skipkeys=True))
asharif455157b2013-02-15 21:15:05 +0000167 self._file.close()
168
169 def __str__(self):
170 return self.AsString([self])
171
172
173class Lock(object):
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800174
yunlian77dc5712013-02-19 21:13:33 +0000175 def __init__(self, lock_file, auto=True):
yunlian52a6c572013-02-19 20:42:07 +0000176 self._to_lock = os.path.basename(lock_file)
177 self._lock_file = lock_file
asharif455157b2013-02-15 21:15:05 +0000178 self._logger = logger.GetLogger()
yunlian8c9419b2013-02-19 21:11:29 +0000179 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000180
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800181 def NonBlockingLock(self, exclusive, reason=''):
yunlian52a6c572013-02-19 20:42:07 +0000182 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000183 if lock.exclusive:
184 self._logger.LogError(
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800185 'Exclusive lock already acquired by %s. Reason: %s' %
asharif455157b2013-02-15 21:15:05 +0000186 (lock.owner, lock.reason))
187 return False
188
189 if exclusive:
190 if lock.counter:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800191 self._logger.LogError('Shared lock already acquired')
asharif455157b2013-02-15 21:15:05 +0000192 return False
yunlian8c9419b2013-02-19 21:11:29 +0000193 lock_file_check = FileCheckName(self._lock_file)
194 fd = OpenLiveCheck(lock_file_check)
195 FileLock.FILE_OPS.append(fd)
196
asharif455157b2013-02-15 21:15:05 +0000197 lock.exclusive = True
198 lock.reason = reason
asharifda8e93b2013-02-15 22:49:27 +0000199 lock.owner = getpass.getuser()
asharif455157b2013-02-15 21:15:05 +0000200 lock.time = time.time()
yunlian8c9419b2013-02-19 21:11:29 +0000201 lock.auto = self._auto
asharif455157b2013-02-15 21:15:05 +0000202 else:
203 lock.counter += 1
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800204 self._logger.LogOutput('Successfully locked: %s' % self._to_lock)
asharif455157b2013-02-15 21:15:05 +0000205 return True
206
207 def Unlock(self, exclusive, force=False):
yunlian52a6c572013-02-19 20:42:07 +0000208 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000209 if not lock.IsLocked():
Caroline Tice570b2ce2013-08-07 12:46:30 -0700210 self._logger.LogWarning("Can't unlock unlocked machine!")
211 return True
asharif455157b2013-02-15 21:15:05 +0000212
213 if lock.exclusive != exclusive:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800214 self._logger.LogError('shared locks must be unlocked with --shared')
asharif455157b2013-02-15 21:15:05 +0000215 return False
216
217 if lock.exclusive:
asharifda8e93b2013-02-15 22:49:27 +0000218 if lock.owner != getpass.getuser() and not force:
asharif455157b2013-02-15 21:15:05 +0000219 self._logger.LogError("%s can't unlock lock owned by: %s" %
asharifda8e93b2013-02-15 22:49:27 +0000220 (getpass.getuser(), lock.owner))
asharif455157b2013-02-15 21:15:05 +0000221 return False
yunlian8c9419b2013-02-19 21:11:29 +0000222 if lock.auto != self._auto:
223 self._logger.LogError("Can't unlock lock with different -a"
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800224 ' parameter.')
yunlian8c9419b2013-02-19 21:11:29 +0000225 return False
asharif455157b2013-02-15 21:15:05 +0000226 lock.exclusive = False
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800227 lock.reason = ''
228 lock.owner = ''
yunlianb0eaee82013-02-19 21:13:28 +0000229
230 if self._auto:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800231 del_list = [i
232 for i in FileLock.FILE_OPS
yunlianb0eaee82013-02-19 21:13:28 +0000233 if i.name == FileCheckName(self._lock_file)]
234 for i in del_list:
235 FileLock.FILE_OPS.remove(i)
236 for f in del_list:
yunlian8c9419b2013-02-19 21:11:29 +0000237 fcntl.lockf(f, fcntl.LOCK_UN)
238 f.close()
yunlianb0eaee82013-02-19 21:13:28 +0000239 del del_list
240 os.remove(FileCheckName(self._lock_file))
yunlian8c9419b2013-02-19 21:11:29 +0000241
asharif455157b2013-02-15 21:15:05 +0000242 else:
243 lock.counter -= 1
244 return True
245
246
247class Machine(object):
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800248 LOCKS_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/locks'
yunlian8a3bdcb2013-02-19 20:42:48 +0000249
yunlian77dc5712013-02-19 21:13:33 +0000250 def __init__(self, name, locks_dir=LOCKS_DIR, auto=True):
asharif455157b2013-02-15 21:15:05 +0000251 self._name = name
yunlian8c9419b2013-02-19 21:11:29 +0000252 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000253 try:
254 self._full_name = socket.gethostbyaddr(name)[0]
255 except socket.error:
256 self._full_name = self._name
yunlian52a6c572013-02-19 20:42:07 +0000257 self._full_name = os.path.join(locks_dir, self._full_name)
asharif455157b2013-02-15 21:15:05 +0000258
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800259 def Lock(self, exclusive=False, reason=''):
yunlian8c9419b2013-02-19 21:11:29 +0000260 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000261 return lock.NonBlockingLock(exclusive, reason)
262
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800263 def TryLock(self, timeout=300, exclusive=False, reason=''):
shenhane0a6d8a2013-02-19 20:42:29 +0000264 locked = False
yunlian8a3bdcb2013-02-19 20:42:48 +0000265 sleep = timeout / 10
shenhane0a6d8a2013-02-19 20:42:29 +0000266 while True:
267 locked = self.Lock(exclusive, reason)
268 if locked or not timeout >= 0:
269 break
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800270 print 'Lock not acquired for {0}, wait {1} seconds ...'.format(self._name,
271 sleep)
shenhane0a6d8a2013-02-19 20:42:29 +0000272 time.sleep(sleep)
273 timeout -= sleep
274 return locked
275
asharif455157b2013-02-15 21:15:05 +0000276 def Unlock(self, exclusive=False, ignore_ownership=False):
yunlian8c9419b2013-02-19 21:11:29 +0000277 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000278 return lock.Unlock(exclusive, ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000279
280
281def Main(argv):
282 """The main function."""
asharifb237bca2013-02-15 20:54:57 +0000283 parser = optparse.OptionParser()
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800284 parser.add_option('-r',
285 '--reason',
286 dest='reason',
287 default='',
288 help='The lock reason.')
289 parser.add_option('-u',
290 '--unlock',
291 dest='unlock',
292 action='store_true',
asharifb237bca2013-02-15 20:54:57 +0000293 default=False,
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800294 help='Use this to unlock.')
295 parser.add_option('-l',
296 '--list_locks',
297 dest='list_locks',
298 action='store_true',
asharifb237bca2013-02-15 20:54:57 +0000299 default=False,
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800300 help='Use this to list locks.')
301 parser.add_option('-f',
302 '--ignore_ownership',
303 dest='ignore_ownership',
304 action='store_true',
asharif455157b2013-02-15 21:15:05 +0000305 default=False,
306 help="Use this to force unlock on a lock you don't own.")
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800307 parser.add_option('-s',
308 '--shared',
309 dest='shared',
310 action='store_true',
asharif455157b2013-02-15 21:15:05 +0000311 default=False,
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800312 help='Use this for a shared (non-exclusive) lock.')
313 parser.add_option('-d',
314 '--dir',
315 dest='locks_dir',
316 action='store',
yunlian52a6c572013-02-19 20:42:07 +0000317 default=Machine.LOCKS_DIR,
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800318 help='Use this to set different locks_dir')
asharifb237bca2013-02-15 20:54:57 +0000319
asharif455157b2013-02-15 21:15:05 +0000320 options, args = parser.parse_args(argv)
asharifb237bca2013-02-15 20:54:57 +0000321
yunlian52a6c572013-02-19 20:42:07 +0000322 options.locks_dir = os.path.abspath(options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000323 exclusive = not options.shared
324
325 if not options.list_locks and len(args) != 2:
326 logger.GetLogger().LogError(
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800327 'Either --list_locks or a machine arg is needed.')
asharifb237bca2013-02-15 20:54:57 +0000328 return 1
329
asharif455157b2013-02-15 21:15:05 +0000330 if len(args) > 1:
yunlian77dc5712013-02-19 21:13:33 +0000331 machine = Machine(args[1], options.locks_dir, auto=False)
asharif455157b2013-02-15 21:15:05 +0000332 else:
333 machine = None
asharifb237bca2013-02-15 20:54:57 +0000334
335 if options.list_locks:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800336 FileLock.ListLock('*', options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000337 retval = True
338 elif options.unlock:
339 retval = machine.Unlock(exclusive, options.ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000340 else:
asharif455157b2013-02-15 21:15:05 +0000341 retval = machine.Lock(exclusive, options.reason)
342
343 if retval:
344 return 0
345 else:
346 return 1
asharifb237bca2013-02-15 20:54:57 +0000347
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800348
349if __name__ == '__main__':
asharif455157b2013-02-15 21:15:05 +0000350 sys.exit(Main(sys.argv))