blob: 8493b082c842540b58c6aece3771cdf962c5245d [file] [log] [blame]
Caroline Ticef6ef4392017-04-06 17:16:05 -07001#!/usr/bin/env 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):
Caroline Ticef6ef4392017-04-06 17:16:05 -070082 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 ])
asharifb237bca2013-02-15 20:54:57 +000087
88
asharif455157b2013-02-15 21:15:05 +000089class FileLock(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000090 """File lock operation class."""
yunlian8c9419b2013-02-19 21:11:29 +000091 FILE_OPS = []
asharif455157b2013-02-15 21:15:05 +000092
93 def __init__(self, lock_filename):
yunlian52a6c572013-02-19 20:42:07 +000094 self._filepath = lock_filename
95 lock_dir = os.path.dirname(lock_filename)
Luis Lozanof2a3ef42015-12-15 13:49:30 -080096 assert os.path.isdir(lock_dir), ("Locks dir: %s doesn't exist!" % lock_dir)
asharif455157b2013-02-15 21:15:05 +000097 self._file = None
Caroline Tice88272d42016-01-13 09:48:29 -080098 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
asharif455157b2013-02-15 21:15:05 +0000108
109 @classmethod
110 def AsString(cls, file_locks):
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800111 stringify_fmt = '%-30s %-15s %-4s %-4s %-15s %-40s %-4s'
112 header = stringify_fmt % ('machine', 'owner', 'excl', 'ctr', 'elapsed',
113 'reason', 'auto')
asharif455157b2013-02-15 21:15:05 +0000114 lock_strings = []
115 for file_lock in file_locks:
116
117 elapsed_time = datetime.timedelta(
Caroline Tice88272d42016-01-13 09:48:29 -0800118 seconds=int(time.time() - file_lock.getDescription().time))
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800119 elapsed_time = '%s ago' % elapsed_time
120 lock_strings.append(
121 stringify_fmt %
Caroline Tice88272d42016-01-13 09:48:29 -0800122 (os.path.basename(file_lock.getFilePath),
123 file_lock.getDescription().owner,
124 file_lock.getDescription().exclusive,
Caroline Ticef6ef4392017-04-06 17:16:05 -0700125 file_lock.getDescription().counter, elapsed_time,
126 file_lock.getDescription().reason, file_lock.getDescription().auto))
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800127 table = '\n'.join(lock_strings)
128 return '\n'.join([header, table])
asharif455157b2013-02-15 21:15:05 +0000129
130 @classmethod
yunlian52a6c572013-02-19 20:42:07 +0000131 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)
asharif455157b2013-02-15 21:15:05 +0000135 file_locks = []
136 for lock_filename in glob.glob(full_pattern):
yunlian8c9419b2013-02-19 21:11:29 +0000137 if LOCK_SUFFIX in lock_filename:
138 continue
asharif455157b2013-02-15 21:15:05 +0000139 file_lock = FileLock(lock_filename)
140 with file_lock as lock:
141 if lock.IsLocked():
142 file_locks.append(file_lock)
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800143 logger.GetLogger().LogOutput('\n%s' % cls.AsString(file_locks))
asharif455157b2013-02-15 21:15:05 +0000144
145 def __enter__(self):
Caroline Tice6a00af92015-10-14 11:24:09 -0700146 with FileCreationMask(LOCK_MASK):
asharif455157b2013-02-15 21:15:05 +0000147 try:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800148 self._file = open(self._filepath, 'a+')
asharif455157b2013-02-15 21:15:05 +0000149 self._file.seek(0, os.SEEK_SET)
150
151 if fcntl.flock(self._file.fileno(), fcntl.LOCK_EX) == -1:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800152 raise IOError('flock(%s, LOCK_EX) failed!' % self._filepath)
asharif455157b2013-02-15 21:15:05 +0000153
154 try:
yunlian8a3bdcb2013-02-19 20:42:48 +0000155 desc = json.load(self._file)
156 except (EOFError, ValueError):
157 desc = None
158 self._description = LockDescription(desc)
159
yunlian8c9419b2013-02-19 21:11:29 +0000160 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()
asharif455157b2013-02-15 21:15:05 +0000175 return self._description
176 # Check this differently?
177 except IOError as ex:
178 logger.GetLogger().LogError(ex)
179 return None
180
Caroline Tice88272d42016-01-13 09:48:29 -0800181 def __exit__(self, typ, value, traceback):
asharif455157b2013-02-15 21:15:05 +0000182 self._file.truncate(0)
yunlian8a3bdcb2013-02-19 20:42:48 +0000183 self._file.write(json.dumps(self._description.__dict__, skipkeys=True))
asharif455157b2013-02-15 21:15:05 +0000184 self._file.close()
185
186 def __str__(self):
187 return self.AsString([self])
188
189
190class Lock(object):
Caroline Tice88272d42016-01-13 09:48:29 -0800191 """Lock class"""
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800192
yunlian77dc5712013-02-19 21:13:33 +0000193 def __init__(self, lock_file, auto=True):
yunlian52a6c572013-02-19 20:42:07 +0000194 self._to_lock = os.path.basename(lock_file)
195 self._lock_file = lock_file
asharif455157b2013-02-15 21:15:05 +0000196 self._logger = logger.GetLogger()
yunlian8c9419b2013-02-19 21:11:29 +0000197 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000198
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800199 def NonBlockingLock(self, exclusive, reason=''):
yunlian52a6c572013-02-19 20:42:07 +0000200 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000201 if lock.exclusive:
202 self._logger.LogError(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700203 'Exclusive lock already acquired by %s. Reason: %s' % (lock.owner,
204 lock.reason))
asharif455157b2013-02-15 21:15:05 +0000205 return False
206
207 if exclusive:
208 if lock.counter:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800209 self._logger.LogError('Shared lock already acquired')
asharif455157b2013-02-15 21:15:05 +0000210 return False
yunlian8c9419b2013-02-19 21:11:29 +0000211 lock_file_check = FileCheckName(self._lock_file)
212 fd = OpenLiveCheck(lock_file_check)
213 FileLock.FILE_OPS.append(fd)
214
asharif455157b2013-02-15 21:15:05 +0000215 lock.exclusive = True
216 lock.reason = reason
asharifda8e93b2013-02-15 22:49:27 +0000217 lock.owner = getpass.getuser()
asharif455157b2013-02-15 21:15:05 +0000218 lock.time = time.time()
yunlian8c9419b2013-02-19 21:11:29 +0000219 lock.auto = self._auto
asharif455157b2013-02-15 21:15:05 +0000220 else:
221 lock.counter += 1
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800222 self._logger.LogOutput('Successfully locked: %s' % self._to_lock)
asharif455157b2013-02-15 21:15:05 +0000223 return True
224
225 def Unlock(self, exclusive, force=False):
yunlian52a6c572013-02-19 20:42:07 +0000226 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000227 if not lock.IsLocked():
Caroline Tice570b2ce2013-08-07 12:46:30 -0700228 self._logger.LogWarning("Can't unlock unlocked machine!")
229 return True
asharif455157b2013-02-15 21:15:05 +0000230
231 if lock.exclusive != exclusive:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800232 self._logger.LogError('shared locks must be unlocked with --shared')
asharif455157b2013-02-15 21:15:05 +0000233 return False
234
235 if lock.exclusive:
asharifda8e93b2013-02-15 22:49:27 +0000236 if lock.owner != getpass.getuser() and not force:
asharif455157b2013-02-15 21:15:05 +0000237 self._logger.LogError("%s can't unlock lock owned by: %s" %
asharifda8e93b2013-02-15 22:49:27 +0000238 (getpass.getuser(), lock.owner))
asharif455157b2013-02-15 21:15:05 +0000239 return False
yunlian8c9419b2013-02-19 21:11:29 +0000240 if lock.auto != self._auto:
241 self._logger.LogError("Can't unlock lock with different -a"
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800242 ' parameter.')
yunlian8c9419b2013-02-19 21:11:29 +0000243 return False
asharif455157b2013-02-15 21:15:05 +0000244 lock.exclusive = False
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800245 lock.reason = ''
246 lock.owner = ''
yunlianb0eaee82013-02-19 21:13:28 +0000247
248 if self._auto:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700249 del_list = [
250 i for i in FileLock.FILE_OPS
251 if i.name == FileCheckName(self._lock_file)
252 ]
yunlianb0eaee82013-02-19 21:13:28 +0000253 for i in del_list:
254 FileLock.FILE_OPS.remove(i)
255 for f in del_list:
yunlian8c9419b2013-02-19 21:11:29 +0000256 fcntl.lockf(f, fcntl.LOCK_UN)
257 f.close()
yunlianb0eaee82013-02-19 21:13:28 +0000258 del del_list
259 os.remove(FileCheckName(self._lock_file))
yunlian8c9419b2013-02-19 21:11:29 +0000260
asharif455157b2013-02-15 21:15:05 +0000261 else:
262 lock.counter -= 1
263 return True
264
265
266class Machine(object):
Caroline Tice88272d42016-01-13 09:48:29 -0800267 """Machine class"""
268
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800269 LOCKS_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/locks'
yunlian8a3bdcb2013-02-19 20:42:48 +0000270
yunlian77dc5712013-02-19 21:13:33 +0000271 def __init__(self, name, locks_dir=LOCKS_DIR, auto=True):
asharif455157b2013-02-15 21:15:05 +0000272 self._name = name
yunlian8c9419b2013-02-19 21:11:29 +0000273 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000274 try:
275 self._full_name = socket.gethostbyaddr(name)[0]
276 except socket.error:
277 self._full_name = self._name
yunlian52a6c572013-02-19 20:42:07 +0000278 self._full_name = os.path.join(locks_dir, self._full_name)
asharif455157b2013-02-15 21:15:05 +0000279
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800280 def Lock(self, exclusive=False, reason=''):
yunlian8c9419b2013-02-19 21:11:29 +0000281 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000282 return lock.NonBlockingLock(exclusive, reason)
283
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800284 def TryLock(self, timeout=300, exclusive=False, reason=''):
shenhane0a6d8a2013-02-19 20:42:29 +0000285 locked = False
yunlian8a3bdcb2013-02-19 20:42:48 +0000286 sleep = timeout / 10
shenhane0a6d8a2013-02-19 20:42:29 +0000287 while True:
288 locked = self.Lock(exclusive, reason)
289 if locked or not timeout >= 0:
290 break
Caroline Tice88272d42016-01-13 09:48:29 -0800291 print('Lock not acquired for {0}, wait {1} seconds ...'.format(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700292 self._name, sleep))
shenhane0a6d8a2013-02-19 20:42:29 +0000293 time.sleep(sleep)
294 timeout -= sleep
295 return locked
296
asharif455157b2013-02-15 21:15:05 +0000297 def Unlock(self, exclusive=False, ignore_ownership=False):
yunlian8c9419b2013-02-19 21:11:29 +0000298 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000299 return lock.Unlock(exclusive, ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000300
301
302def Main(argv):
303 """The main function."""
asharifb237bca2013-02-15 20:54:57 +0000304
Caroline Tice88272d42016-01-13 09:48:29 -0800305 parser = argparse.ArgumentParser()
Caroline Ticef6ef4392017-04-06 17:16:05 -0700306 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')
Caroline Tice88272d42016-01-13 09:48:29 -0800343 parser.add_argument('args', nargs='*', help='Machine arg.')
344
345 options = parser.parse_args(argv)
asharifb237bca2013-02-15 20:54:57 +0000346
yunlian52a6c572013-02-19 20:42:07 +0000347 options.locks_dir = os.path.abspath(options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000348 exclusive = not options.shared
349
Caroline Tice88272d42016-01-13 09:48:29 -0800350 if not options.list_locks and len(options.args) != 2:
asharif455157b2013-02-15 21:15:05 +0000351 logger.GetLogger().LogError(
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800352 'Either --list_locks or a machine arg is needed.')
asharifb237bca2013-02-15 20:54:57 +0000353 return 1
354
Caroline Tice88272d42016-01-13 09:48:29 -0800355 if len(options.args) > 1:
356 machine = Machine(options.args[1], options.locks_dir, auto=False)
asharif455157b2013-02-15 21:15:05 +0000357 else:
358 machine = None
asharifb237bca2013-02-15 20:54:57 +0000359
360 if options.list_locks:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800361 FileLock.ListLock('*', options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000362 retval = True
363 elif options.unlock:
364 retval = machine.Unlock(exclusive, options.ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000365 else:
asharif455157b2013-02-15 21:15:05 +0000366 retval = machine.Lock(exclusive, options.reason)
367
368 if retval:
369 return 0
370 else:
371 return 1
asharifb237bca2013-02-15 20:54:57 +0000372
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800373
374if __name__ == '__main__':
Caroline Tice88272d42016-01-13 09:48:29 -0800375 sys.exit(Main(sys.argv[1:]))