blob: f67a9bb18e6883c17967a5b8dc75ccef2e8b5c1f [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.
4
yunlian52a6c572013-02-19 20:42:07 +00005"""Script to lock/unlock machines."""
asharifb237bca2013-02-15 20:54:57 +00006
7__author__ = "asharif@google.com (Ahmad Sharif)"
8
9import datetime
asharif455157b2013-02-15 21:15:05 +000010import fcntl
asharifda8e93b2013-02-15 22:49:27 +000011import getpass
asharifb237bca2013-02-15 20:54:57 +000012import glob
yunlian8a3bdcb2013-02-19 20:42:48 +000013import json
asharifb237bca2013-02-15 20:54:57 +000014import optparse
15import os
asharifb237bca2013-02-15 20:54:57 +000016import socket
asharif455157b2013-02-15 21:15:05 +000017import sys
18import time
yunlian52a6c572013-02-19 20:42:07 +000019
asharifb237bca2013-02-15 20:54:57 +000020from utils import logger
21
yunlian8c9419b2013-02-19 21:11:29 +000022LOCK_SUFFIX = "_check_lock_liveness"
23
Caroline Tice6a00af92015-10-14 11:24:09 -070024# The locks file directory REQUIRES that 'group' only has read/write
25# privileges and 'world' has no privileges. So the mask must be
26# '0027': 0777 - 0027 = 0750.
27LOCK_MASK = 0027
yunlian8c9419b2013-02-19 21:11:29 +000028
29def 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):
yunlian8c9419b2013-02-19 21:11:29 +000035 fd = open(file_name, "a+w")
36 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):
44 def __init__(self, mask):
45 self._mask = mask
46
47 def __enter__(self):
48 self._old_mask = os.umask(self._mask)
49
50 def __exit__(self, type, value, traceback):
51 os.umask(self._old_mask)
asharifb237bca2013-02-15 20:54:57 +000052
53
asharif455157b2013-02-15 21:15:05 +000054class LockDescription(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000055 """The description of the lock."""
56
yunlian8c9419b2013-02-19 21:11:29 +000057 def __init__(self, desc=None):
yunlian8a3bdcb2013-02-19 20:42:48 +000058 try:
59 self.owner = desc["owner"]
60 self.exclusive = desc["exclusive"]
61 self.counter = desc["counter"]
62 self.time = desc["time"]
63 self.reason = desc["reason"]
yunlian8c9419b2013-02-19 21:11:29 +000064 self.auto = desc["auto"]
yunlian8a3bdcb2013-02-19 20:42:48 +000065 except (KeyError, TypeError):
66 self.owner = ""
67 self.exclusive = False
68 self.counter = 0
69 self.time = 0
70 self.reason = ""
yunlian8c9419b2013-02-19 21:11:29 +000071 self.auto = False
asharifb237bca2013-02-15 20:54:57 +000072
asharif455157b2013-02-15 21:15:05 +000073 def IsLocked(self):
74 return self.counter or self.exclusive
asharifb237bca2013-02-15 20:54:57 +000075
asharif455157b2013-02-15 21:15:05 +000076 def __str__(self):
77 return " ".join(["Owner: %s" % self.owner,
78 "Exclusive: %s" % self.exclusive,
79 "Counter: %s" % self.counter,
80 "Time: %s" % self.time,
yunlian8c9419b2013-02-19 21:11:29 +000081 "Reason: %s" % self.reason,
82 "Auto: %s" % self.auto])
asharifb237bca2013-02-15 20:54:57 +000083
84
asharif455157b2013-02-15 21:15:05 +000085class FileLock(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000086 """File lock operation class."""
yunlian8c9419b2013-02-19 21:11:29 +000087 FILE_OPS = []
asharif455157b2013-02-15 21:15:05 +000088
89 def __init__(self, lock_filename):
yunlian52a6c572013-02-19 20:42:07 +000090 self._filepath = lock_filename
91 lock_dir = os.path.dirname(lock_filename)
92 assert os.path.isdir(lock_dir), (
93 "Locks dir: %s doesn't exist!" % lock_dir)
asharif455157b2013-02-15 21:15:05 +000094 self._file = None
95
96 @classmethod
97 def AsString(cls, file_locks):
yunlian8c9419b2013-02-19 21:11:29 +000098 stringify_fmt = "%-30s %-15s %-4s %-4s %-15s %-40s %-4s"
asharif455157b2013-02-15 21:15:05 +000099 header = stringify_fmt % ("machine", "owner", "excl", "ctr",
yunlian8c9419b2013-02-19 21:11:29 +0000100 "elapsed", "reason", "auto")
asharif455157b2013-02-15 21:15:05 +0000101 lock_strings = []
102 for file_lock in file_locks:
103
104 elapsed_time = datetime.timedelta(
105 seconds=int(time.time() - file_lock._description.time))
106 elapsed_time = "%s ago" % elapsed_time
107 lock_strings.append(stringify_fmt %
108 (os.path.basename(file_lock._filepath),
109 file_lock._description.owner,
110 file_lock._description.exclusive,
111 file_lock._description.counter,
112 elapsed_time,
yunlian8c9419b2013-02-19 21:11:29 +0000113 file_lock._description.reason,
114 file_lock._description.auto))
asharif455157b2013-02-15 21:15:05 +0000115 table = "\n".join(lock_strings)
116 return "\n".join([header, table])
117
118 @classmethod
yunlian52a6c572013-02-19 20:42:07 +0000119 def ListLock(cls, pattern, locks_dir):
120 if not locks_dir:
121 locks_dir = Machine.LOCKS_DIR
122 full_pattern = os.path.join(locks_dir, pattern)
asharif455157b2013-02-15 21:15:05 +0000123 file_locks = []
124 for lock_filename in glob.glob(full_pattern):
yunlian8c9419b2013-02-19 21:11:29 +0000125 if LOCK_SUFFIX in lock_filename:
126 continue
asharif455157b2013-02-15 21:15:05 +0000127 file_lock = FileLock(lock_filename)
128 with file_lock as lock:
129 if lock.IsLocked():
130 file_locks.append(file_lock)
131 logger.GetLogger().LogOutput("\n%s" % cls.AsString(file_locks))
132
133 def __enter__(self):
Caroline Tice6a00af92015-10-14 11:24:09 -0700134 with FileCreationMask(LOCK_MASK):
asharif455157b2013-02-15 21:15:05 +0000135 try:
136 self._file = open(self._filepath, "a+")
137 self._file.seek(0, os.SEEK_SET)
138
139 if fcntl.flock(self._file.fileno(), fcntl.LOCK_EX) == -1:
140 raise IOError("flock(%s, LOCK_EX) failed!" % self._filepath)
141
142 try:
yunlian8a3bdcb2013-02-19 20:42:48 +0000143 desc = json.load(self._file)
144 except (EOFError, ValueError):
145 desc = None
146 self._description = LockDescription(desc)
147
yunlian8c9419b2013-02-19 21:11:29 +0000148 if self._description.exclusive and self._description.auto:
149 locked_byself = False
150 for fd in self.FILE_OPS:
151 if fd.name == FileCheckName(self._filepath):
152 locked_byself = True
153 break
154 if not locked_byself:
155 try:
156 fp = OpenLiveCheck(FileCheckName(self._filepath))
157 except IOError:
158 pass
159 else:
160 self._description = LockDescription()
161 fcntl.lockf(fp, fcntl.LOCK_UN)
162 fp.close()
asharif455157b2013-02-15 21:15:05 +0000163 return self._description
164 # Check this differently?
165 except IOError as ex:
166 logger.GetLogger().LogError(ex)
167 return None
168
169 def __exit__(self, type, value, traceback):
170 self._file.truncate(0)
yunlian8a3bdcb2013-02-19 20:42:48 +0000171 self._file.write(json.dumps(self._description.__dict__, skipkeys=True))
asharif455157b2013-02-15 21:15:05 +0000172 self._file.close()
173
174 def __str__(self):
175 return self.AsString([self])
176
177
178class Lock(object):
yunlian77dc5712013-02-19 21:13:33 +0000179 def __init__(self, lock_file, auto=True):
yunlian52a6c572013-02-19 20:42:07 +0000180 self._to_lock = os.path.basename(lock_file)
181 self._lock_file = lock_file
asharif455157b2013-02-15 21:15:05 +0000182 self._logger = logger.GetLogger()
yunlian8c9419b2013-02-19 21:11:29 +0000183 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000184
185 def NonBlockingLock(self, exclusive, reason=""):
yunlian52a6c572013-02-19 20:42:07 +0000186 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000187 if lock.exclusive:
188 self._logger.LogError(
189 "Exclusive lock already acquired by %s. Reason: %s" %
190 (lock.owner, lock.reason))
191 return False
192
193 if exclusive:
194 if lock.counter:
195 self._logger.LogError("Shared lock already acquired")
196 return False
yunlian8c9419b2013-02-19 21:11:29 +0000197 lock_file_check = FileCheckName(self._lock_file)
198 fd = OpenLiveCheck(lock_file_check)
199 FileLock.FILE_OPS.append(fd)
200
asharif455157b2013-02-15 21:15:05 +0000201 lock.exclusive = True
202 lock.reason = reason
asharifda8e93b2013-02-15 22:49:27 +0000203 lock.owner = getpass.getuser()
asharif455157b2013-02-15 21:15:05 +0000204 lock.time = time.time()
yunlian8c9419b2013-02-19 21:11:29 +0000205 lock.auto = self._auto
asharif455157b2013-02-15 21:15:05 +0000206 else:
207 lock.counter += 1
208 self._logger.LogOutput("Successfully locked: %s" % self._to_lock)
209 return True
210
211 def Unlock(self, exclusive, force=False):
yunlian52a6c572013-02-19 20:42:07 +0000212 with FileLock(self._lock_file) as lock:
asharif455157b2013-02-15 21:15:05 +0000213 if not lock.IsLocked():
Caroline Tice570b2ce2013-08-07 12:46:30 -0700214 self._logger.LogWarning("Can't unlock unlocked machine!")
215 return True
asharif455157b2013-02-15 21:15:05 +0000216
217 if lock.exclusive != exclusive:
218 self._logger.LogError("shared locks must be unlocked with --shared")
219 return False
220
221 if lock.exclusive:
asharifda8e93b2013-02-15 22:49:27 +0000222 if lock.owner != getpass.getuser() and not force:
asharif455157b2013-02-15 21:15:05 +0000223 self._logger.LogError("%s can't unlock lock owned by: %s" %
asharifda8e93b2013-02-15 22:49:27 +0000224 (getpass.getuser(), lock.owner))
asharif455157b2013-02-15 21:15:05 +0000225 return False
yunlian8c9419b2013-02-19 21:11:29 +0000226 if lock.auto != self._auto:
227 self._logger.LogError("Can't unlock lock with different -a"
228 " parameter.")
229 return False
asharif455157b2013-02-15 21:15:05 +0000230 lock.exclusive = False
231 lock.reason = ""
232 lock.owner = ""
yunlianb0eaee82013-02-19 21:13:28 +0000233
234 if self._auto:
235 del_list = [i for i in FileLock.FILE_OPS
236 if i.name == FileCheckName(self._lock_file)]
237 for i in del_list:
238 FileLock.FILE_OPS.remove(i)
239 for f in del_list:
yunlian8c9419b2013-02-19 21:11:29 +0000240 fcntl.lockf(f, fcntl.LOCK_UN)
241 f.close()
yunlianb0eaee82013-02-19 21:13:28 +0000242 del del_list
243 os.remove(FileCheckName(self._lock_file))
yunlian8c9419b2013-02-19 21:11:29 +0000244
asharif455157b2013-02-15 21:15:05 +0000245 else:
246 lock.counter -= 1
247 return True
248
249
250class Machine(object):
Caroline Tice6a00af92015-10-14 11:24:09 -0700251 LOCKS_DIR = "/google/data/rw/users/mo/mobiletc-prebuild/locks"
yunlian8a3bdcb2013-02-19 20:42:48 +0000252
yunlian77dc5712013-02-19 21:13:33 +0000253 def __init__(self, name, locks_dir=LOCKS_DIR, auto=True):
asharif455157b2013-02-15 21:15:05 +0000254 self._name = name
yunlian8c9419b2013-02-19 21:11:29 +0000255 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000256 try:
257 self._full_name = socket.gethostbyaddr(name)[0]
258 except socket.error:
259 self._full_name = self._name
yunlian52a6c572013-02-19 20:42:07 +0000260 self._full_name = os.path.join(locks_dir, self._full_name)
asharif455157b2013-02-15 21:15:05 +0000261
262 def Lock(self, exclusive=False, reason=""):
yunlian8c9419b2013-02-19 21:11:29 +0000263 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000264 return lock.NonBlockingLock(exclusive, reason)
265
shenhane0a6d8a2013-02-19 20:42:29 +0000266 def TryLock(self, timeout=300, exclusive=False, reason=""):
267 locked = False
yunlian8a3bdcb2013-02-19 20:42:48 +0000268 sleep = timeout / 10
shenhane0a6d8a2013-02-19 20:42:29 +0000269 while True:
270 locked = self.Lock(exclusive, reason)
271 if locked or not timeout >= 0:
272 break
273 print "Lock not acquired for {0}, wait {1} seconds ...".format(
yunlian8a3bdcb2013-02-19 20:42:48 +0000274 self._name, sleep)
shenhane0a6d8a2013-02-19 20:42:29 +0000275 time.sleep(sleep)
276 timeout -= sleep
277 return locked
278
asharif455157b2013-02-15 21:15:05 +0000279 def Unlock(self, exclusive=False, ignore_ownership=False):
yunlian8c9419b2013-02-19 21:11:29 +0000280 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000281 return lock.Unlock(exclusive, ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000282
283
284def Main(argv):
285 """The main function."""
asharifb237bca2013-02-15 20:54:57 +0000286 parser = optparse.OptionParser()
asharifb237bca2013-02-15 20:54:57 +0000287 parser.add_option("-r",
288 "--reason",
289 dest="reason",
asharif455157b2013-02-15 21:15:05 +0000290 default="",
asharifb237bca2013-02-15 20:54:57 +0000291 help="The lock reason.")
292 parser.add_option("-u",
293 "--unlock",
294 dest="unlock",
295 action="store_true",
296 default=False,
297 help="Use this to unlock.")
298 parser.add_option("-l",
299 "--list_locks",
300 dest="list_locks",
301 action="store_true",
302 default=False,
303 help="Use this to list locks.")
asharif455157b2013-02-15 21:15:05 +0000304 parser.add_option("-f",
305 "--ignore_ownership",
306 dest="ignore_ownership",
307 action="store_true",
308 default=False,
309 help="Use this to force unlock on a lock you don't own.")
310 parser.add_option("-s",
311 "--shared",
312 dest="shared",
313 action="store_true",
314 default=False,
315 help="Use this for a shared (non-exclusive) lock.")
yunlian52a6c572013-02-19 20:42:07 +0000316 parser.add_option("-d",
317 "--dir",
318 dest="locks_dir",
319 action="store",
320 default=Machine.LOCKS_DIR,
321 help="Use this to set different locks_dir")
asharifb237bca2013-02-15 20:54:57 +0000322
asharif455157b2013-02-15 21:15:05 +0000323 options, args = parser.parse_args(argv)
asharifb237bca2013-02-15 20:54:57 +0000324
yunlian52a6c572013-02-19 20:42:07 +0000325 options.locks_dir = os.path.abspath(options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000326 exclusive = not options.shared
327
328 if not options.list_locks and len(args) != 2:
329 logger.GetLogger().LogError(
330 "Either --list_locks or a machine arg is needed.")
asharifb237bca2013-02-15 20:54:57 +0000331 return 1
332
asharif455157b2013-02-15 21:15:05 +0000333 if len(args) > 1:
yunlian77dc5712013-02-19 21:13:33 +0000334 machine = Machine(args[1], options.locks_dir, auto=False)
asharif455157b2013-02-15 21:15:05 +0000335 else:
336 machine = None
asharifb237bca2013-02-15 20:54:57 +0000337
338 if options.list_locks:
yunlian52a6c572013-02-19 20:42:07 +0000339 FileLock.ListLock("*", options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000340 retval = True
341 elif options.unlock:
342 retval = machine.Unlock(exclusive, options.ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000343 else:
asharif455157b2013-02-15 21:15:05 +0000344 retval = machine.Lock(exclusive, options.reason)
345
346 if retval:
347 return 0
348 else:
349 return 1
asharifb237bca2013-02-15 20:54:57 +0000350
351if __name__ == "__main__":
asharif455157b2013-02-15 21:15:05 +0000352 sys.exit(Main(sys.argv))