blob: 5bba44304d3299ec7be2f6984850d3c4ef941323 [file] [log] [blame]
Zhizhou Yang5534af82020-01-15 16:25:04 -08001#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
yunlian52a6c572013-02-19 20:42:07 +00007"""Script to lock/unlock machines."""
asharifb237bca2013-02-15 20:54:57 +00008
Zhizhou Yang5534af82020-01-15 16:25:04 -08009from __future__ import division
Caroline Tice88272d42016-01-13 09:48:29 -080010from __future__ import print_function
11
Luis Lozanof2a3ef42015-12-15 13:49:30 -080012__author__ = 'asharif@google.com (Ahmad Sharif)'
asharifb237bca2013-02-15 20:54:57 +000013
Caroline Tice88272d42016-01-13 09:48:29 -080014import argparse
asharifb237bca2013-02-15 20:54:57 +000015import datetime
asharif455157b2013-02-15 21:15:05 +000016import fcntl
asharifda8e93b2013-02-15 22:49:27 +000017import getpass
asharifb237bca2013-02-15 20:54:57 +000018import glob
yunlian8a3bdcb2013-02-19 20:42:48 +000019import json
asharifb237bca2013-02-15 20:54:57 +000020import os
asharifb237bca2013-02-15 20:54:57 +000021import socket
asharif455157b2013-02-15 21:15:05 +000022import sys
23import time
yunlian52a6c572013-02-19 20:42:07 +000024
Caroline Tice88272d42016-01-13 09:48:29 -080025from cros_utils import logger
asharifb237bca2013-02-15 20:54:57 +000026
Luis Lozanof2a3ef42015-12-15 13:49:30 -080027LOCK_SUFFIX = '_check_lock_liveness'
yunlian8c9419b2013-02-19 21:11:29 +000028
Caroline Tice6a00af92015-10-14 11:24:09 -070029# The locks file directory REQUIRES that 'group' only has read/write
30# privileges and 'world' has no privileges. So the mask must be
Zhizhou Yang5534af82020-01-15 16:25:04 -080031# '0o27': 0o777 - 0o27 = 0o750.
32LOCK_MASK = 0o27
yunlian8c9419b2013-02-19 21:11:29 +000033
Luis Lozanof2a3ef42015-12-15 13:49:30 -080034
yunlian8c9419b2013-02-19 21:11:29 +000035def FileCheckName(name):
36 return name + LOCK_SUFFIX
37
38
39def OpenLiveCheck(file_name):
Caroline Tice6a00af92015-10-14 11:24:09 -070040 with FileCreationMask(LOCK_MASK):
Caroline Tice88272d42016-01-13 09:48:29 -080041 fd = open(file_name, 'a')
yunlian8c9419b2013-02-19 21:11:29 +000042 try:
43 fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
Zhizhou Yang5534af82020-01-15 16:25:04 -080044 except IOError as e:
45 logger.GetLogger().LogError(e)
yunlian8c9419b2013-02-19 21:11:29 +000046 raise
47 return fd
48
asharifb237bca2013-02-15 20:54:57 +000049
asharif455157b2013-02-15 21:15:05 +000050class FileCreationMask(object):
Caroline Tice88272d42016-01-13 09:48:29 -080051 """Class for the file creation mask."""
Luis Lozanof2a3ef42015-12-15 13:49:30 -080052
asharif455157b2013-02-15 21:15:05 +000053 def __init__(self, mask):
54 self._mask = mask
Caroline Tice88272d42016-01-13 09:48:29 -080055 self._old_mask = None
asharif455157b2013-02-15 21:15:05 +000056
57 def __enter__(self):
58 self._old_mask = os.umask(self._mask)
59
Caroline Tice88272d42016-01-13 09:48:29 -080060 def __exit__(self, typ, value, traceback):
asharif455157b2013-02-15 21:15:05 +000061 os.umask(self._old_mask)
asharifb237bca2013-02-15 20:54:57 +000062
63
asharif455157b2013-02-15 21:15:05 +000064class LockDescription(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000065 """The description of the lock."""
66
yunlian8c9419b2013-02-19 21:11:29 +000067 def __init__(self, desc=None):
yunlian8a3bdcb2013-02-19 20:42:48 +000068 try:
Luis Lozanof2a3ef42015-12-15 13:49:30 -080069 self.owner = desc['owner']
70 self.exclusive = desc['exclusive']
71 self.counter = desc['counter']
72 self.time = desc['time']
73 self.reason = desc['reason']
74 self.auto = desc['auto']
yunlian8a3bdcb2013-02-19 20:42:48 +000075 except (KeyError, TypeError):
Luis Lozanof2a3ef42015-12-15 13:49:30 -080076 self.owner = ''
yunlian8a3bdcb2013-02-19 20:42:48 +000077 self.exclusive = False
78 self.counter = 0
79 self.time = 0
Luis Lozanof2a3ef42015-12-15 13:49:30 -080080 self.reason = ''
yunlian8c9419b2013-02-19 21:11:29 +000081 self.auto = False
asharifb237bca2013-02-15 20:54:57 +000082
asharif455157b2013-02-15 21:15:05 +000083 def IsLocked(self):
84 return self.counter or self.exclusive
asharifb237bca2013-02-15 20:54:57 +000085
asharif455157b2013-02-15 21:15:05 +000086 def __str__(self):
Caroline Ticef6ef4392017-04-06 17:16:05 -070087 return ' '.join([
Zhizhou Yang5534af82020-01-15 16:25:04 -080088 'Owner: %s' % self.owner,
89 'Exclusive: %s' % self.exclusive,
90 'Counter: %s' % self.counter,
91 'Time: %s' % self.time,
92 'Reason: %s' % self.reason,
93 'Auto: %s' % self.auto,
Caroline Ticef6ef4392017-04-06 17:16:05 -070094 ])
asharifb237bca2013-02-15 20:54:57 +000095
96
asharif455157b2013-02-15 21:15:05 +000097class FileLock(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000098 """File lock operation class."""
yunlian8c9419b2013-02-19 21:11:29 +000099 FILE_OPS = []
asharif455157b2013-02-15 21:15:05 +0000100
101 def __init__(self, lock_filename):
yunlian52a6c572013-02-19 20:42:07 +0000102 self._filepath = lock_filename
103 lock_dir = os.path.dirname(lock_filename)
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800104 assert os.path.isdir(lock_dir), ("Locks dir: %s doesn't exist!" % lock_dir)
asharif455157b2013-02-15 21:15:05 +0000105 self._file = None
Caroline Tice88272d42016-01-13 09:48:29 -0800106 self._description = None
107
Zhizhou Yang5534af82020-01-15 16:25:04 -0800108 self.exclusive = None
109 self.auto = None
110 self.reason = None
111 self.time = None
112 self.owner = None
113
Caroline Tice88272d42016-01-13 09:48:29 -0800114 def getDescription(self):
115 return self._description
116
117 def getFilePath(self):
118 return self._filepath
119
120 def setDescription(self, desc):
121 self._description = desc
asharif455157b2013-02-15 21:15:05 +0000122
123 @classmethod
124 def AsString(cls, file_locks):
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800125 stringify_fmt = '%-30s %-15s %-4s %-4s %-15s %-40s %-4s'
126 header = stringify_fmt % ('machine', 'owner', 'excl', 'ctr', 'elapsed',
127 'reason', 'auto')
asharif455157b2013-02-15 21:15:05 +0000128 lock_strings = []
129 for file_lock in file_locks:
130
131 elapsed_time = datetime.timedelta(
Caroline Tice88272d42016-01-13 09:48:29 -0800132 seconds=int(time.time() - file_lock.getDescription().time))
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800133 elapsed_time = '%s ago' % elapsed_time
134 lock_strings.append(
Zhizhou Yang5534af82020-01-15 16:25:04 -0800135 stringify_fmt % (os.path.basename(file_lock.getFilePath),
136 file_lock.getDescription().owner,
137 file_lock.getDescription().exclusive,
138 file_lock.getDescription().counter, elapsed_time,
139 file_lock.getDescription().reason,
140 file_lock.getDescription().auto))
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800141 table = '\n'.join(lock_strings)
142 return '\n'.join([header, table])
asharif455157b2013-02-15 21:15:05 +0000143
144 @classmethod
yunlian52a6c572013-02-19 20:42:07 +0000145 def ListLock(cls, pattern, locks_dir):
146 if not locks_dir:
147 locks_dir = Machine.LOCKS_DIR
148 full_pattern = os.path.join(locks_dir, pattern)
asharif455157b2013-02-15 21:15:05 +0000149 file_locks = []
150 for lock_filename in glob.glob(full_pattern):
yunlian8c9419b2013-02-19 21:11:29 +0000151 if LOCK_SUFFIX in lock_filename:
152 continue
asharif455157b2013-02-15 21:15:05 +0000153 file_lock = FileLock(lock_filename)
154 with file_lock as lock:
155 if lock.IsLocked():
156 file_locks.append(file_lock)
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800157 logger.GetLogger().LogOutput('\n%s' % cls.AsString(file_locks))
asharif455157b2013-02-15 21:15:05 +0000158
159 def __enter__(self):
Caroline Tice6a00af92015-10-14 11:24:09 -0700160 with FileCreationMask(LOCK_MASK):
asharif455157b2013-02-15 21:15:05 +0000161 try:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800162 self._file = open(self._filepath, 'a+')
asharif455157b2013-02-15 21:15:05 +0000163 self._file.seek(0, os.SEEK_SET)
164
165 if fcntl.flock(self._file.fileno(), fcntl.LOCK_EX) == -1:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800166 raise IOError('flock(%s, LOCK_EX) failed!' % self._filepath)
asharif455157b2013-02-15 21:15:05 +0000167
168 try:
yunlian8a3bdcb2013-02-19 20:42:48 +0000169 desc = json.load(self._file)
170 except (EOFError, ValueError):
171 desc = None
172 self._description = LockDescription(desc)
173
yunlian8c9419b2013-02-19 21:11:29 +0000174 if self._description.exclusive and self._description.auto:
175 locked_byself = False
176 for fd in self.FILE_OPS:
177 if fd.name == FileCheckName(self._filepath):
178 locked_byself = True
179 break
180 if not locked_byself:
181 try:
182 fp = OpenLiveCheck(FileCheckName(self._filepath))
183 except IOError:
184 pass
185 else:
186 self._description = LockDescription()
187 fcntl.lockf(fp, fcntl.LOCK_UN)
188 fp.close()
asharif455157b2013-02-15 21:15:05 +0000189 return self._description
190 # Check this differently?
191 except IOError as ex:
192 logger.GetLogger().LogError(ex)
193 return None
194
Caroline Tice88272d42016-01-13 09:48:29 -0800195 def __exit__(self, typ, value, traceback):
asharif455157b2013-02-15 21:15:05 +0000196 self._file.truncate(0)
yunlian8a3bdcb2013-02-19 20:42:48 +0000197 self._file.write(json.dumps(self._description.__dict__, skipkeys=True))
asharif455157b2013-02-15 21:15:05 +0000198 self._file.close()
199
200 def __str__(self):
201 return self.AsString([self])
202
203
204class Lock(object):
Caroline Tice88272d42016-01-13 09:48:29 -0800205 """Lock class"""
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800206
yunlian77dc5712013-02-19 21:13:33 +0000207 def __init__(self, lock_file, auto=True):
yunlian52a6c572013-02-19 20:42:07 +0000208 self._to_lock = os.path.basename(lock_file)
209 self._lock_file = lock_file
asharif455157b2013-02-15 21:15:05 +0000210 self._logger = logger.GetLogger()
yunlian8c9419b2013-02-19 21:11:29 +0000211 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000212
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800213 def NonBlockingLock(self, exclusive, reason=''):
yunlian52a6c572013-02-19 20:42:07 +0000214 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000215 if lock.exclusive:
216 self._logger.LogError(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700217 'Exclusive lock already acquired by %s. Reason: %s' % (lock.owner,
218 lock.reason))
asharif455157b2013-02-15 21:15:05 +0000219 return False
220
221 if exclusive:
222 if lock.counter:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800223 self._logger.LogError('Shared lock already acquired')
asharif455157b2013-02-15 21:15:05 +0000224 return False
yunlian8c9419b2013-02-19 21:11:29 +0000225 lock_file_check = FileCheckName(self._lock_file)
226 fd = OpenLiveCheck(lock_file_check)
227 FileLock.FILE_OPS.append(fd)
228
asharif455157b2013-02-15 21:15:05 +0000229 lock.exclusive = True
230 lock.reason = reason
asharifda8e93b2013-02-15 22:49:27 +0000231 lock.owner = getpass.getuser()
asharif455157b2013-02-15 21:15:05 +0000232 lock.time = time.time()
yunlian8c9419b2013-02-19 21:11:29 +0000233 lock.auto = self._auto
asharif455157b2013-02-15 21:15:05 +0000234 else:
235 lock.counter += 1
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800236 self._logger.LogOutput('Successfully locked: %s' % self._to_lock)
asharif455157b2013-02-15 21:15:05 +0000237 return True
238
239 def Unlock(self, exclusive, force=False):
yunlian52a6c572013-02-19 20:42:07 +0000240 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000241 if not lock.IsLocked():
Caroline Tice570b2ce2013-08-07 12:46:30 -0700242 self._logger.LogWarning("Can't unlock unlocked machine!")
243 return True
asharif455157b2013-02-15 21:15:05 +0000244
245 if lock.exclusive != exclusive:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800246 self._logger.LogError('shared locks must be unlocked with --shared')
asharif455157b2013-02-15 21:15:05 +0000247 return False
248
249 if lock.exclusive:
asharifda8e93b2013-02-15 22:49:27 +0000250 if lock.owner != getpass.getuser() and not force:
asharif455157b2013-02-15 21:15:05 +0000251 self._logger.LogError("%s can't unlock lock owned by: %s" %
asharifda8e93b2013-02-15 22:49:27 +0000252 (getpass.getuser(), lock.owner))
asharif455157b2013-02-15 21:15:05 +0000253 return False
yunlian8c9419b2013-02-19 21:11:29 +0000254 if lock.auto != self._auto:
255 self._logger.LogError("Can't unlock lock with different -a"
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800256 ' parameter.')
yunlian8c9419b2013-02-19 21:11:29 +0000257 return False
asharif455157b2013-02-15 21:15:05 +0000258 lock.exclusive = False
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800259 lock.reason = ''
260 lock.owner = ''
yunlianb0eaee82013-02-19 21:13:28 +0000261
262 if self._auto:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700263 del_list = [
264 i for i in FileLock.FILE_OPS
265 if i.name == FileCheckName(self._lock_file)
266 ]
yunlianb0eaee82013-02-19 21:13:28 +0000267 for i in del_list:
268 FileLock.FILE_OPS.remove(i)
269 for f in del_list:
yunlian8c9419b2013-02-19 21:11:29 +0000270 fcntl.lockf(f, fcntl.LOCK_UN)
271 f.close()
yunlianb0eaee82013-02-19 21:13:28 +0000272 del del_list
273 os.remove(FileCheckName(self._lock_file))
yunlian8c9419b2013-02-19 21:11:29 +0000274
asharif455157b2013-02-15 21:15:05 +0000275 else:
276 lock.counter -= 1
277 return True
278
279
280class Machine(object):
Caroline Tice88272d42016-01-13 09:48:29 -0800281 """Machine class"""
282
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800283 LOCKS_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/locks'
yunlian8a3bdcb2013-02-19 20:42:48 +0000284
yunlian77dc5712013-02-19 21:13:33 +0000285 def __init__(self, name, locks_dir=LOCKS_DIR, auto=True):
asharif455157b2013-02-15 21:15:05 +0000286 self._name = name
yunlian8c9419b2013-02-19 21:11:29 +0000287 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000288 try:
289 self._full_name = socket.gethostbyaddr(name)[0]
290 except socket.error:
291 self._full_name = self._name
yunlian52a6c572013-02-19 20:42:07 +0000292 self._full_name = os.path.join(locks_dir, self._full_name)
asharif455157b2013-02-15 21:15:05 +0000293
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800294 def Lock(self, exclusive=False, reason=''):
yunlian8c9419b2013-02-19 21:11:29 +0000295 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000296 return lock.NonBlockingLock(exclusive, reason)
297
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800298 def TryLock(self, timeout=300, exclusive=False, reason=''):
shenhane0a6d8a2013-02-19 20:42:29 +0000299 locked = False
yunlian8a3bdcb2013-02-19 20:42:48 +0000300 sleep = timeout / 10
shenhane0a6d8a2013-02-19 20:42:29 +0000301 while True:
302 locked = self.Lock(exclusive, reason)
Zhizhou Yang5534af82020-01-15 16:25:04 -0800303 if locked or timeout < 0:
shenhane0a6d8a2013-02-19 20:42:29 +0000304 break
Caroline Tice88272d42016-01-13 09:48:29 -0800305 print('Lock not acquired for {0}, wait {1} seconds ...'.format(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700306 self._name, sleep))
shenhane0a6d8a2013-02-19 20:42:29 +0000307 time.sleep(sleep)
308 timeout -= sleep
309 return locked
310
asharif455157b2013-02-15 21:15:05 +0000311 def Unlock(self, exclusive=False, ignore_ownership=False):
yunlian8c9419b2013-02-19 21:11:29 +0000312 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000313 return lock.Unlock(exclusive, ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000314
315
316def Main(argv):
317 """The main function."""
asharifb237bca2013-02-15 20:54:57 +0000318
Caroline Tice88272d42016-01-13 09:48:29 -0800319 parser = argparse.ArgumentParser()
Caroline Ticef6ef4392017-04-06 17:16:05 -0700320 parser.add_argument(
321 '-r', '--reason', dest='reason', default='', help='The lock reason.')
322 parser.add_argument(
323 '-u',
324 '--unlock',
325 dest='unlock',
326 action='store_true',
327 default=False,
328 help='Use this to unlock.')
329 parser.add_argument(
330 '-l',
331 '--list_locks',
332 dest='list_locks',
333 action='store_true',
334 default=False,
335 help='Use this to list locks.')
336 parser.add_argument(
337 '-f',
338 '--ignore_ownership',
339 dest='ignore_ownership',
340 action='store_true',
341 default=False,
342 help="Use this to force unlock on a lock you don't own.")
343 parser.add_argument(
344 '-s',
345 '--shared',
346 dest='shared',
347 action='store_true',
348 default=False,
349 help='Use this for a shared (non-exclusive) lock.')
350 parser.add_argument(
351 '-d',
352 '--dir',
353 dest='locks_dir',
354 action='store',
355 default=Machine.LOCKS_DIR,
356 help='Use this to set different locks_dir')
Caroline Tice88272d42016-01-13 09:48:29 -0800357 parser.add_argument('args', nargs='*', help='Machine arg.')
358
359 options = parser.parse_args(argv)
asharifb237bca2013-02-15 20:54:57 +0000360
yunlian52a6c572013-02-19 20:42:07 +0000361 options.locks_dir = os.path.abspath(options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000362 exclusive = not options.shared
363
Caroline Tice88272d42016-01-13 09:48:29 -0800364 if not options.list_locks and len(options.args) != 2:
asharif455157b2013-02-15 21:15:05 +0000365 logger.GetLogger().LogError(
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800366 'Either --list_locks or a machine arg is needed.')
asharifb237bca2013-02-15 20:54:57 +0000367 return 1
368
Caroline Tice88272d42016-01-13 09:48:29 -0800369 if len(options.args) > 1:
370 machine = Machine(options.args[1], options.locks_dir, auto=False)
asharif455157b2013-02-15 21:15:05 +0000371 else:
372 machine = None
asharifb237bca2013-02-15 20:54:57 +0000373
374 if options.list_locks:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800375 FileLock.ListLock('*', options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000376 retval = True
377 elif options.unlock:
378 retval = machine.Unlock(exclusive, options.ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000379 else:
asharif455157b2013-02-15 21:15:05 +0000380 retval = machine.Lock(exclusive, options.reason)
381
382 if retval:
383 return 0
384 else:
385 return 1
asharifb237bca2013-02-15 20:54:57 +0000386
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800387
388if __name__ == '__main__':
Caroline Tice88272d42016-01-13 09:48:29 -0800389 sys.exit(Main(sys.argv[1:]))