blob: 9b1d3367a190b2c885720cef99728b306f56edf7 [file] [log] [blame]
Caroline Tice88272d42016-01-13 09:48:29 -08001#!/usr/bin/python2
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
Caroline Tice88272d42016-01-13 09:48:29 -08006from __future__ import print_function
7
Luis Lozanof2a3ef42015-12-15 13:49:30 -08008__author__ = 'asharif@google.com (Ahmad Sharif)'
asharifb237bca2013-02-15 20:54:57 +00009
Caroline Tice88272d42016-01-13 09:48:29 -080010import argparse
asharifb237bca2013-02-15 20:54:57 +000011import datetime
asharif455157b2013-02-15 21:15:05 +000012import fcntl
asharifda8e93b2013-02-15 22:49:27 +000013import getpass
asharifb237bca2013-02-15 20:54:57 +000014import glob
yunlian8a3bdcb2013-02-19 20:42:48 +000015import json
asharifb237bca2013-02-15 20:54:57 +000016import os
asharifb237bca2013-02-15 20:54:57 +000017import socket
asharif455157b2013-02-15 21:15:05 +000018import sys
19import time
yunlian52a6c572013-02-19 20:42:07 +000020
Caroline Tice88272d42016-01-13 09:48:29 -080021from cros_utils import logger
asharifb237bca2013-02-15 20:54:57 +000022
Luis Lozanof2a3ef42015-12-15 13:49:30 -080023LOCK_SUFFIX = '_check_lock_liveness'
yunlian8c9419b2013-02-19 21:11:29 +000024
Caroline Tice6a00af92015-10-14 11:24:09 -070025# 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.
28LOCK_MASK = 0027
yunlian8c9419b2013-02-19 21:11:29 +000029
Luis Lozanof2a3ef42015-12-15 13:49:30 -080030
yunlian8c9419b2013-02-19 21:11:29 +000031def FileCheckName(name):
32 return name + LOCK_SUFFIX
33
34
35def OpenLiveCheck(file_name):
Caroline Tice6a00af92015-10-14 11:24:09 -070036 with FileCreationMask(LOCK_MASK):
Caroline Tice88272d42016-01-13 09:48:29 -080037 fd = open(file_name, 'a')
yunlian8c9419b2013-02-19 21:11:29 +000038 try:
39 fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
40 except IOError:
41 raise
42 return fd
43
asharifb237bca2013-02-15 20:54:57 +000044
asharif455157b2013-02-15 21:15:05 +000045class FileCreationMask(object):
Caroline Tice88272d42016-01-13 09:48:29 -080046 """Class for the file creation mask."""
Luis Lozanof2a3ef42015-12-15 13:49:30 -080047
asharif455157b2013-02-15 21:15:05 +000048 def __init__(self, mask):
49 self._mask = mask
Caroline Tice88272d42016-01-13 09:48:29 -080050 self._old_mask = None
asharif455157b2013-02-15 21:15:05 +000051
52 def __enter__(self):
53 self._old_mask = os.umask(self._mask)
54
Caroline Tice88272d42016-01-13 09:48:29 -080055 def __exit__(self, typ, value, traceback):
asharif455157b2013-02-15 21:15:05 +000056 os.umask(self._old_mask)
asharifb237bca2013-02-15 20:54:57 +000057
58
asharif455157b2013-02-15 21:15:05 +000059class LockDescription(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000060 """The description of the lock."""
61
yunlian8c9419b2013-02-19 21:11:29 +000062 def __init__(self, desc=None):
yunlian8a3bdcb2013-02-19 20:42:48 +000063 try:
Luis Lozanof2a3ef42015-12-15 13:49:30 -080064 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']
yunlian8a3bdcb2013-02-19 20:42:48 +000070 except (KeyError, TypeError):
Luis Lozanof2a3ef42015-12-15 13:49:30 -080071 self.owner = ''
yunlian8a3bdcb2013-02-19 20:42:48 +000072 self.exclusive = False
73 self.counter = 0
74 self.time = 0
Luis Lozanof2a3ef42015-12-15 13:49:30 -080075 self.reason = ''
yunlian8c9419b2013-02-19 21:11:29 +000076 self.auto = False
asharifb237bca2013-02-15 20:54:57 +000077
asharif455157b2013-02-15 21:15:05 +000078 def IsLocked(self):
79 return self.counter or self.exclusive
asharifb237bca2013-02-15 20:54:57 +000080
asharif455157b2013-02-15 21:15:05 +000081 def __str__(self):
Luis Lozanof2a3ef42015-12-15 13:49:30 -080082 return ' '.join(['Owner: %s' % self.owner, 'Exclusive: %s' % self.exclusive,
83 'Counter: %s' % self.counter, 'Time: %s' % self.time,
84 'Reason: %s' % self.reason, 'Auto: %s' % self.auto])
asharifb237bca2013-02-15 20:54:57 +000085
86
asharif455157b2013-02-15 21:15:05 +000087class FileLock(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000088 """File lock operation class."""
yunlian8c9419b2013-02-19 21:11:29 +000089 FILE_OPS = []
asharif455157b2013-02-15 21:15:05 +000090
91 def __init__(self, lock_filename):
yunlian52a6c572013-02-19 20:42:07 +000092 self._filepath = lock_filename
93 lock_dir = os.path.dirname(lock_filename)
Luis Lozanof2a3ef42015-12-15 13:49:30 -080094 assert os.path.isdir(lock_dir), ("Locks dir: %s doesn't exist!" % lock_dir)
asharif455157b2013-02-15 21:15:05 +000095 self._file = None
Caroline Tice88272d42016-01-13 09:48:29 -080096 self._description = None
97
98 def getDescription(self):
99 return self._description
100
101 def getFilePath(self):
102 return self._filepath
103
104 def setDescription(self, desc):
105 self._description = desc
asharif455157b2013-02-15 21:15:05 +0000106
107 @classmethod
108 def AsString(cls, file_locks):
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800109 stringify_fmt = '%-30s %-15s %-4s %-4s %-15s %-40s %-4s'
110 header = stringify_fmt % ('machine', 'owner', 'excl', 'ctr', 'elapsed',
111 'reason', 'auto')
asharif455157b2013-02-15 21:15:05 +0000112 lock_strings = []
113 for file_lock in file_locks:
114
115 elapsed_time = datetime.timedelta(
Caroline Tice88272d42016-01-13 09:48:29 -0800116 seconds=int(time.time() - file_lock.getDescription().time))
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800117 elapsed_time = '%s ago' % elapsed_time
118 lock_strings.append(
119 stringify_fmt %
Caroline Tice88272d42016-01-13 09:48:29 -0800120 (os.path.basename(file_lock.getFilePath),
121 file_lock.getDescription().owner,
122 file_lock.getDescription().exclusive,
123 file_lock.getDescription().counter,
124 elapsed_time, file_lock.getDescription().reason,
125 file_lock.getDescription().auto))
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800126 table = '\n'.join(lock_strings)
127 return '\n'.join([header, table])
asharif455157b2013-02-15 21:15:05 +0000128
129 @classmethod
yunlian52a6c572013-02-19 20:42:07 +0000130 def ListLock(cls, pattern, locks_dir):
131 if not locks_dir:
132 locks_dir = Machine.LOCKS_DIR
133 full_pattern = os.path.join(locks_dir, pattern)
asharif455157b2013-02-15 21:15:05 +0000134 file_locks = []
135 for lock_filename in glob.glob(full_pattern):
yunlian8c9419b2013-02-19 21:11:29 +0000136 if LOCK_SUFFIX in lock_filename:
137 continue
asharif455157b2013-02-15 21:15:05 +0000138 file_lock = FileLock(lock_filename)
139 with file_lock as lock:
140 if lock.IsLocked():
141 file_locks.append(file_lock)
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800142 logger.GetLogger().LogOutput('\n%s' % cls.AsString(file_locks))
asharif455157b2013-02-15 21:15:05 +0000143
144 def __enter__(self):
Caroline Tice6a00af92015-10-14 11:24:09 -0700145 with FileCreationMask(LOCK_MASK):
asharif455157b2013-02-15 21:15:05 +0000146 try:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800147 self._file = open(self._filepath, 'a+')
asharif455157b2013-02-15 21:15:05 +0000148 self._file.seek(0, os.SEEK_SET)
149
150 if fcntl.flock(self._file.fileno(), fcntl.LOCK_EX) == -1:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800151 raise IOError('flock(%s, LOCK_EX) failed!' % self._filepath)
asharif455157b2013-02-15 21:15:05 +0000152
153 try:
yunlian8a3bdcb2013-02-19 20:42:48 +0000154 desc = json.load(self._file)
155 except (EOFError, ValueError):
156 desc = None
157 self._description = LockDescription(desc)
158
yunlian8c9419b2013-02-19 21:11:29 +0000159 if self._description.exclusive and self._description.auto:
160 locked_byself = False
161 for fd in self.FILE_OPS:
162 if fd.name == FileCheckName(self._filepath):
163 locked_byself = True
164 break
165 if not locked_byself:
166 try:
167 fp = OpenLiveCheck(FileCheckName(self._filepath))
168 except IOError:
169 pass
170 else:
171 self._description = LockDescription()
172 fcntl.lockf(fp, fcntl.LOCK_UN)
173 fp.close()
asharif455157b2013-02-15 21:15:05 +0000174 return self._description
175 # Check this differently?
176 except IOError as ex:
177 logger.GetLogger().LogError(ex)
178 return None
179
Caroline Tice88272d42016-01-13 09:48:29 -0800180 def __exit__(self, typ, value, traceback):
asharif455157b2013-02-15 21:15:05 +0000181 self._file.truncate(0)
yunlian8a3bdcb2013-02-19 20:42:48 +0000182 self._file.write(json.dumps(self._description.__dict__, skipkeys=True))
asharif455157b2013-02-15 21:15:05 +0000183 self._file.close()
184
185 def __str__(self):
186 return self.AsString([self])
187
188
189class Lock(object):
Caroline Tice88272d42016-01-13 09:48:29 -0800190 """Lock class"""
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800191
yunlian77dc5712013-02-19 21:13:33 +0000192 def __init__(self, lock_file, auto=True):
yunlian52a6c572013-02-19 20:42:07 +0000193 self._to_lock = os.path.basename(lock_file)
194 self._lock_file = lock_file
asharif455157b2013-02-15 21:15:05 +0000195 self._logger = logger.GetLogger()
yunlian8c9419b2013-02-19 21:11:29 +0000196 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000197
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800198 def NonBlockingLock(self, exclusive, reason=''):
yunlian52a6c572013-02-19 20:42:07 +0000199 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000200 if lock.exclusive:
201 self._logger.LogError(
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800202 'Exclusive lock already acquired by %s. Reason: %s' %
asharif455157b2013-02-15 21:15:05 +0000203 (lock.owner, lock.reason))
204 return False
205
206 if exclusive:
207 if lock.counter:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800208 self._logger.LogError('Shared lock already acquired')
asharif455157b2013-02-15 21:15:05 +0000209 return False
yunlian8c9419b2013-02-19 21:11:29 +0000210 lock_file_check = FileCheckName(self._lock_file)
211 fd = OpenLiveCheck(lock_file_check)
212 FileLock.FILE_OPS.append(fd)
213
asharif455157b2013-02-15 21:15:05 +0000214 lock.exclusive = True
215 lock.reason = reason
asharifda8e93b2013-02-15 22:49:27 +0000216 lock.owner = getpass.getuser()
asharif455157b2013-02-15 21:15:05 +0000217 lock.time = time.time()
yunlian8c9419b2013-02-19 21:11:29 +0000218 lock.auto = self._auto
asharif455157b2013-02-15 21:15:05 +0000219 else:
220 lock.counter += 1
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800221 self._logger.LogOutput('Successfully locked: %s' % self._to_lock)
asharif455157b2013-02-15 21:15:05 +0000222 return True
223
224 def Unlock(self, exclusive, force=False):
yunlian52a6c572013-02-19 20:42:07 +0000225 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000226 if not lock.IsLocked():
Caroline Tice570b2ce2013-08-07 12:46:30 -0700227 self._logger.LogWarning("Can't unlock unlocked machine!")
228 return True
asharif455157b2013-02-15 21:15:05 +0000229
230 if lock.exclusive != exclusive:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800231 self._logger.LogError('shared locks must be unlocked with --shared')
asharif455157b2013-02-15 21:15:05 +0000232 return False
233
234 if lock.exclusive:
asharifda8e93b2013-02-15 22:49:27 +0000235 if lock.owner != getpass.getuser() and not force:
asharif455157b2013-02-15 21:15:05 +0000236 self._logger.LogError("%s can't unlock lock owned by: %s" %
asharifda8e93b2013-02-15 22:49:27 +0000237 (getpass.getuser(), lock.owner))
asharif455157b2013-02-15 21:15:05 +0000238 return False
yunlian8c9419b2013-02-19 21:11:29 +0000239 if lock.auto != self._auto:
240 self._logger.LogError("Can't unlock lock with different -a"
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800241 ' parameter.')
yunlian8c9419b2013-02-19 21:11:29 +0000242 return False
asharif455157b2013-02-15 21:15:05 +0000243 lock.exclusive = False
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800244 lock.reason = ''
245 lock.owner = ''
yunlianb0eaee82013-02-19 21:13:28 +0000246
247 if self._auto:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800248 del_list = [i
249 for i in FileLock.FILE_OPS
yunlianb0eaee82013-02-19 21:13:28 +0000250 if i.name == FileCheckName(self._lock_file)]
251 for i in del_list:
252 FileLock.FILE_OPS.remove(i)
253 for f in del_list:
yunlian8c9419b2013-02-19 21:11:29 +0000254 fcntl.lockf(f, fcntl.LOCK_UN)
255 f.close()
yunlianb0eaee82013-02-19 21:13:28 +0000256 del del_list
257 os.remove(FileCheckName(self._lock_file))
yunlian8c9419b2013-02-19 21:11:29 +0000258
asharif455157b2013-02-15 21:15:05 +0000259 else:
260 lock.counter -= 1
261 return True
262
263
264class Machine(object):
Caroline Tice88272d42016-01-13 09:48:29 -0800265 """Machine class"""
266
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800267 LOCKS_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/locks'
yunlian8a3bdcb2013-02-19 20:42:48 +0000268
yunlian77dc5712013-02-19 21:13:33 +0000269 def __init__(self, name, locks_dir=LOCKS_DIR, auto=True):
asharif455157b2013-02-15 21:15:05 +0000270 self._name = name
yunlian8c9419b2013-02-19 21:11:29 +0000271 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000272 try:
273 self._full_name = socket.gethostbyaddr(name)[0]
274 except socket.error:
275 self._full_name = self._name
yunlian52a6c572013-02-19 20:42:07 +0000276 self._full_name = os.path.join(locks_dir, self._full_name)
asharif455157b2013-02-15 21:15:05 +0000277
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800278 def Lock(self, exclusive=False, reason=''):
yunlian8c9419b2013-02-19 21:11:29 +0000279 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000280 return lock.NonBlockingLock(exclusive, reason)
281
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800282 def TryLock(self, timeout=300, exclusive=False, reason=''):
shenhane0a6d8a2013-02-19 20:42:29 +0000283 locked = False
yunlian8a3bdcb2013-02-19 20:42:48 +0000284 sleep = timeout / 10
shenhane0a6d8a2013-02-19 20:42:29 +0000285 while True:
286 locked = self.Lock(exclusive, reason)
287 if locked or not timeout >= 0:
288 break
Caroline Tice88272d42016-01-13 09:48:29 -0800289 print('Lock not acquired for {0}, wait {1} seconds ...'.format(
290 self._name,
291 sleep))
shenhane0a6d8a2013-02-19 20:42:29 +0000292 time.sleep(sleep)
293 timeout -= sleep
294 return locked
295
asharif455157b2013-02-15 21:15:05 +0000296 def Unlock(self, exclusive=False, ignore_ownership=False):
yunlian8c9419b2013-02-19 21:11:29 +0000297 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000298 return lock.Unlock(exclusive, ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000299
300
301def Main(argv):
302 """The main function."""
asharifb237bca2013-02-15 20:54:57 +0000303
Caroline Tice88272d42016-01-13 09:48:29 -0800304 parser = argparse.ArgumentParser()
305 parser.add_argument('-r',
306 '--reason',
307 dest='reason',
308 default='',
309 help='The lock reason.')
310 parser.add_argument('-u',
311 '--unlock',
312 dest='unlock',
313 action='store_true',
314 default=False,
315 help='Use this to unlock.')
316 parser.add_argument('-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('-f',
323 '--ignore_ownership',
324 dest='ignore_ownership',
325 action='store_true',
326 default=False,
327 help="Use this to force unlock on a lock you don't own.")
328 parser.add_argument('-s',
329 '--shared',
330 dest='shared',
331 action='store_true',
332 default=False,
333 help='Use this for a shared (non-exclusive) lock.')
334 parser.add_argument('-d',
335 '--dir',
336 dest='locks_dir',
337 action='store',
338 default=Machine.LOCKS_DIR,
339 help='Use this to set different locks_dir')
340 parser.add_argument('args', nargs='*', help='Machine arg.')
341
342 options = parser.parse_args(argv)
asharifb237bca2013-02-15 20:54:57 +0000343
yunlian52a6c572013-02-19 20:42:07 +0000344 options.locks_dir = os.path.abspath(options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000345 exclusive = not options.shared
346
Caroline Tice88272d42016-01-13 09:48:29 -0800347 if not options.list_locks and len(options.args) != 2:
asharif455157b2013-02-15 21:15:05 +0000348 logger.GetLogger().LogError(
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800349 'Either --list_locks or a machine arg is needed.')
asharifb237bca2013-02-15 20:54:57 +0000350 return 1
351
Caroline Tice88272d42016-01-13 09:48:29 -0800352 if len(options.args) > 1:
353 machine = Machine(options.args[1], options.locks_dir, auto=False)
asharif455157b2013-02-15 21:15:05 +0000354 else:
355 machine = None
asharifb237bca2013-02-15 20:54:57 +0000356
357 if options.list_locks:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800358 FileLock.ListLock('*', options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000359 retval = True
360 elif options.unlock:
361 retval = machine.Unlock(exclusive, options.ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000362 else:
asharif455157b2013-02-15 21:15:05 +0000363 retval = machine.Lock(exclusive, options.reason)
364
365 if retval:
366 return 0
367 else:
368 return 1
asharifb237bca2013-02-15 20:54:57 +0000369
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800370
371if __name__ == '__main__':
Caroline Tice88272d42016-01-13 09:48:29 -0800372 sys.exit(Main(sys.argv[1:]))