blob: 0095ffc161292e42819254d4e10e4bca3ab37644 [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
24
25def FileCheckName(name):
26 return name + LOCK_SUFFIX
27
28
29def OpenLiveCheck(file_name):
30 with FileCreationMask(0000):
31 fd = open(file_name, "a+w")
32 try:
33 fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
34 except IOError:
35 raise
36 return fd
37
asharifb237bca2013-02-15 20:54:57 +000038
asharif455157b2013-02-15 21:15:05 +000039class FileCreationMask(object):
40 def __init__(self, mask):
41 self._mask = mask
42
43 def __enter__(self):
44 self._old_mask = os.umask(self._mask)
45
46 def __exit__(self, type, value, traceback):
47 os.umask(self._old_mask)
asharifb237bca2013-02-15 20:54:57 +000048
49
asharif455157b2013-02-15 21:15:05 +000050class LockDescription(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000051 """The description of the lock."""
52
yunlian8c9419b2013-02-19 21:11:29 +000053 def __init__(self, desc=None):
yunlian8a3bdcb2013-02-19 20:42:48 +000054 try:
55 self.owner = desc["owner"]
56 self.exclusive = desc["exclusive"]
57 self.counter = desc["counter"]
58 self.time = desc["time"]
59 self.reason = desc["reason"]
yunlian8c9419b2013-02-19 21:11:29 +000060 self.auto = desc["auto"]
yunlian8a3bdcb2013-02-19 20:42:48 +000061 except (KeyError, TypeError):
62 self.owner = ""
63 self.exclusive = False
64 self.counter = 0
65 self.time = 0
66 self.reason = ""
yunlian8c9419b2013-02-19 21:11:29 +000067 self.auto = False
asharifb237bca2013-02-15 20:54:57 +000068
asharif455157b2013-02-15 21:15:05 +000069 def IsLocked(self):
70 return self.counter or self.exclusive
asharifb237bca2013-02-15 20:54:57 +000071
asharif455157b2013-02-15 21:15:05 +000072 def __str__(self):
73 return " ".join(["Owner: %s" % self.owner,
74 "Exclusive: %s" % self.exclusive,
75 "Counter: %s" % self.counter,
76 "Time: %s" % self.time,
yunlian8c9419b2013-02-19 21:11:29 +000077 "Reason: %s" % self.reason,
78 "Auto: %s" % self.auto])
asharifb237bca2013-02-15 20:54:57 +000079
80
asharif455157b2013-02-15 21:15:05 +000081class FileLock(object):
yunlian8a3bdcb2013-02-19 20:42:48 +000082 """File lock operation class."""
yunlian8c9419b2013-02-19 21:11:29 +000083 FILE_OPS = []
asharif455157b2013-02-15 21:15:05 +000084
85 def __init__(self, lock_filename):
yunlian52a6c572013-02-19 20:42:07 +000086 self._filepath = lock_filename
87 lock_dir = os.path.dirname(lock_filename)
88 assert os.path.isdir(lock_dir), (
89 "Locks dir: %s doesn't exist!" % lock_dir)
asharif455157b2013-02-15 21:15:05 +000090 self._file = None
91
92 @classmethod
93 def AsString(cls, file_locks):
yunlian8c9419b2013-02-19 21:11:29 +000094 stringify_fmt = "%-30s %-15s %-4s %-4s %-15s %-40s %-4s"
asharif455157b2013-02-15 21:15:05 +000095 header = stringify_fmt % ("machine", "owner", "excl", "ctr",
yunlian8c9419b2013-02-19 21:11:29 +000096 "elapsed", "reason", "auto")
asharif455157b2013-02-15 21:15:05 +000097 lock_strings = []
98 for file_lock in file_locks:
99
100 elapsed_time = datetime.timedelta(
101 seconds=int(time.time() - file_lock._description.time))
102 elapsed_time = "%s ago" % elapsed_time
103 lock_strings.append(stringify_fmt %
104 (os.path.basename(file_lock._filepath),
105 file_lock._description.owner,
106 file_lock._description.exclusive,
107 file_lock._description.counter,
108 elapsed_time,
yunlian8c9419b2013-02-19 21:11:29 +0000109 file_lock._description.reason,
110 file_lock._description.auto))
asharif455157b2013-02-15 21:15:05 +0000111 table = "\n".join(lock_strings)
112 return "\n".join([header, table])
113
114 @classmethod
yunlian52a6c572013-02-19 20:42:07 +0000115 def ListLock(cls, pattern, locks_dir):
116 if not locks_dir:
117 locks_dir = Machine.LOCKS_DIR
118 full_pattern = os.path.join(locks_dir, pattern)
asharif455157b2013-02-15 21:15:05 +0000119 file_locks = []
120 for lock_filename in glob.glob(full_pattern):
yunlian8c9419b2013-02-19 21:11:29 +0000121 if LOCK_SUFFIX in lock_filename:
122 continue
asharif455157b2013-02-15 21:15:05 +0000123 file_lock = FileLock(lock_filename)
124 with file_lock as lock:
125 if lock.IsLocked():
126 file_locks.append(file_lock)
127 logger.GetLogger().LogOutput("\n%s" % cls.AsString(file_locks))
128
129 def __enter__(self):
130 with FileCreationMask(0000):
131 try:
132 self._file = open(self._filepath, "a+")
133 self._file.seek(0, os.SEEK_SET)
134
135 if fcntl.flock(self._file.fileno(), fcntl.LOCK_EX) == -1:
136 raise IOError("flock(%s, LOCK_EX) failed!" % self._filepath)
137
138 try:
yunlian8a3bdcb2013-02-19 20:42:48 +0000139 desc = json.load(self._file)
140 except (EOFError, ValueError):
141 desc = None
142 self._description = LockDescription(desc)
143
yunlian8c9419b2013-02-19 21:11:29 +0000144 if self._description.exclusive and self._description.auto:
145 locked_byself = False
146 for fd in self.FILE_OPS:
147 if fd.name == FileCheckName(self._filepath):
148 locked_byself = True
149 break
150 if not locked_byself:
151 try:
152 fp = OpenLiveCheck(FileCheckName(self._filepath))
153 except IOError:
154 pass
155 else:
156 self._description = LockDescription()
157 fcntl.lockf(fp, fcntl.LOCK_UN)
158 fp.close()
asharif455157b2013-02-15 21:15:05 +0000159 return self._description
160 # Check this differently?
161 except IOError as ex:
162 logger.GetLogger().LogError(ex)
163 return None
164
165 def __exit__(self, type, value, traceback):
166 self._file.truncate(0)
yunlian8a3bdcb2013-02-19 20:42:48 +0000167 self._file.write(json.dumps(self._description.__dict__, skipkeys=True))
asharif455157b2013-02-15 21:15:05 +0000168 self._file.close()
169
170 def __str__(self):
171 return self.AsString([self])
172
173
174class Lock(object):
yunlian8c9419b2013-02-19 21:11:29 +0000175 def __init__(self, lock_file, auto=False):
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
181 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(
185 "Exclusive lock already acquired by %s. Reason: %s" %
186 (lock.owner, lock.reason))
187 return False
188
189 if exclusive:
190 if lock.counter:
191 self._logger.LogError("Shared lock already acquired")
192 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
204 self._logger.LogOutput("Successfully locked: %s" % self._to_lock)
205 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():
210 self._logger.LogError("Can't unlock unlocked machine!")
211 return False
212
213 if lock.exclusive != exclusive:
214 self._logger.LogError("shared locks must be unlocked with --shared")
215 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"
224 " parameter.")
225 return False
asharif455157b2013-02-15 21:15:05 +0000226 lock.exclusive = False
227 lock.reason = ""
228 lock.owner = ""
yunlianb0eaee82013-02-19 21:13:28 +0000229
230 if self._auto:
231 del_list = [i for i in FileLock.FILE_OPS
232 if i.name == FileCheckName(self._lock_file)]
233 for i in del_list:
234 FileLock.FILE_OPS.remove(i)
235 for f in del_list:
yunlian8c9419b2013-02-19 21:11:29 +0000236 fcntl.lockf(f, fcntl.LOCK_UN)
237 f.close()
yunlianb0eaee82013-02-19 21:13:28 +0000238 del del_list
239 os.remove(FileCheckName(self._lock_file))
yunlian8c9419b2013-02-19 21:11:29 +0000240
asharif455157b2013-02-15 21:15:05 +0000241 else:
242 lock.counter -= 1
243 return True
244
245
246class Machine(object):
yunlian52a6c572013-02-19 20:42:07 +0000247 LOCKS_DIR = "/home/mobiletc-prebuild/locks"
yunlian8a3bdcb2013-02-19 20:42:48 +0000248
yunlian8c9419b2013-02-19 21:11:29 +0000249 def __init__(self, name, locks_dir=LOCKS_DIR, auto=False):
asharif455157b2013-02-15 21:15:05 +0000250 self._name = name
yunlian8c9419b2013-02-19 21:11:29 +0000251 self._auto = auto
asharif455157b2013-02-15 21:15:05 +0000252 try:
253 self._full_name = socket.gethostbyaddr(name)[0]
254 except socket.error:
255 self._full_name = self._name
yunlian52a6c572013-02-19 20:42:07 +0000256 self._full_name = os.path.join(locks_dir, self._full_name)
asharif455157b2013-02-15 21:15:05 +0000257
258 def Lock(self, exclusive=False, reason=""):
yunlian8c9419b2013-02-19 21:11:29 +0000259 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000260 return lock.NonBlockingLock(exclusive, reason)
261
shenhane0a6d8a2013-02-19 20:42:29 +0000262 def TryLock(self, timeout=300, exclusive=False, reason=""):
263 locked = False
yunlian8a3bdcb2013-02-19 20:42:48 +0000264 sleep = timeout / 10
shenhane0a6d8a2013-02-19 20:42:29 +0000265 while True:
266 locked = self.Lock(exclusive, reason)
267 if locked or not timeout >= 0:
268 break
269 print "Lock not acquired for {0}, wait {1} seconds ...".format(
yunlian8a3bdcb2013-02-19 20:42:48 +0000270 self._name, sleep)
shenhane0a6d8a2013-02-19 20:42:29 +0000271 time.sleep(sleep)
272 timeout -= sleep
273 return locked
274
asharif455157b2013-02-15 21:15:05 +0000275 def Unlock(self, exclusive=False, ignore_ownership=False):
yunlian8c9419b2013-02-19 21:11:29 +0000276 lock = Lock(self._full_name, self._auto)
asharif455157b2013-02-15 21:15:05 +0000277 return lock.Unlock(exclusive, ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000278
279
280def Main(argv):
281 """The main function."""
asharifb237bca2013-02-15 20:54:57 +0000282 parser = optparse.OptionParser()
asharifb237bca2013-02-15 20:54:57 +0000283 parser.add_option("-r",
284 "--reason",
285 dest="reason",
asharif455157b2013-02-15 21:15:05 +0000286 default="",
asharifb237bca2013-02-15 20:54:57 +0000287 help="The lock reason.")
288 parser.add_option("-u",
289 "--unlock",
290 dest="unlock",
291 action="store_true",
292 default=False,
293 help="Use this to unlock.")
294 parser.add_option("-l",
295 "--list_locks",
296 dest="list_locks",
297 action="store_true",
298 default=False,
299 help="Use this to list locks.")
asharif455157b2013-02-15 21:15:05 +0000300 parser.add_option("-f",
301 "--ignore_ownership",
302 dest="ignore_ownership",
303 action="store_true",
304 default=False,
305 help="Use this to force unlock on a lock you don't own.")
306 parser.add_option("-s",
307 "--shared",
308 dest="shared",
309 action="store_true",
310 default=False,
311 help="Use this for a shared (non-exclusive) lock.")
yunlian52a6c572013-02-19 20:42:07 +0000312 parser.add_option("-d",
313 "--dir",
314 dest="locks_dir",
315 action="store",
316 default=Machine.LOCKS_DIR,
317 help="Use this to set different locks_dir")
yunlian8c9419b2013-02-19 21:11:29 +0000318 parser.add_option("-a",
319 "--auto",
320 dest="auto",
321 action="store_true",
322 default=False,
323 help=("Use this to automatic unlock when"
324 " the process is gone."))
asharifb237bca2013-02-15 20:54:57 +0000325
asharif455157b2013-02-15 21:15:05 +0000326 options, args = parser.parse_args(argv)
asharifb237bca2013-02-15 20:54:57 +0000327
yunlian52a6c572013-02-19 20:42:07 +0000328 options.locks_dir = os.path.abspath(options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000329 exclusive = not options.shared
330
331 if not options.list_locks and len(args) != 2:
332 logger.GetLogger().LogError(
333 "Either --list_locks or a machine arg is needed.")
asharifb237bca2013-02-15 20:54:57 +0000334 return 1
335
asharif455157b2013-02-15 21:15:05 +0000336 if len(args) > 1:
yunlian8c9419b2013-02-19 21:11:29 +0000337 machine = Machine(args[1], options.locks_dir, options.auto)
asharif455157b2013-02-15 21:15:05 +0000338 else:
339 machine = None
asharifb237bca2013-02-15 20:54:57 +0000340
341 if options.list_locks:
yunlian52a6c572013-02-19 20:42:07 +0000342 FileLock.ListLock("*", options.locks_dir)
asharif455157b2013-02-15 21:15:05 +0000343 retval = True
344 elif options.unlock:
345 retval = machine.Unlock(exclusive, options.ignore_ownership)
asharifb237bca2013-02-15 20:54:57 +0000346 else:
asharif455157b2013-02-15 21:15:05 +0000347 retval = machine.Lock(exclusive, options.reason)
348
349 if retval:
350 return 0
351 else:
352 return 1
asharifb237bca2013-02-15 20:54:57 +0000353
354if __name__ == "__main__":
asharif455157b2013-02-15 21:15:05 +0000355 sys.exit(Main(sys.argv))