2 Miscellaneous utility functions and classes.
6 from wizard.util import *
25 class ChangeDirectory(object):
27 Context for temporarily changing the working directory.
29 >>> with ChangeDirectory("/tmp"):
33 def __init__(self, dir):
37 self.olddir = os.getcwd()
39 def __exit__(self, *args):
42 class Counter(object):
44 Object for counting different values when you don't know what
45 they are a priori. Supports index access and iteration.
47 >>> counter = Counter()
48 >>> counter.count("foo")
49 >>> print counter["foo"]
54 def count(self, value):
55 """Increments count for ``value``."""
56 self.dict.setdefault(value, 0)
58 def __getitem__(self, key):
61 return self.dict.__iter__()
63 """Returns the max counter value seen."""
64 return max(self.dict.values())
66 """Returns the sum of all counter values."""
67 return sum(self.dict.values())
69 """Returns the keys of counters."""
70 return self.dict.keys()
72 class PipeToLess(object):
74 Context for printing output to a pager. Use this if output
75 is expected to be long.
78 self.proc = subprocess.Popen("less", stdin=subprocess.PIPE)
79 self.old_stdout = sys.stdout
80 sys.stdout = self.proc.stdin
81 def __exit__(self, *args):
83 self.proc.stdin.close()
85 sys.stdout = self.old_stdout
87 class IgnoreKeyboardInterrupts(object):
89 Context for temporarily ignoring keyboard interrupts. Use this
90 if aborting would cause more harm than finishing the job.
93 signal.signal(signal.SIGINT,signal.SIG_IGN)
94 def __exit__(self, *args):
95 signal.signal(signal.SIGINT, signal.default_int_handler)
97 class LockDirectory(object):
99 Context for locking a directory.
101 def __init__(self, lockfile, expiry = 3600):
102 self.lockfile = lockfile
103 self.expiry = expiry # by default an hour
106 for i in range(0, 3):
108 os.open(self.lockfile, os.O_CREAT | os.O_EXCL)
109 open(self.lockfile, "w").write("%d" % os.getpid())
111 if e.errno == errno.EEXIST:
112 # There is a possibility of infinite recursion, but we
113 # expect it to be unlikely, and not harmful if it does happen
114 with LockDirectory(self.lockfile + "_"):
115 # See if we can break the lock
117 pid = open(self.lockfile, "r").read().strip()
118 if not os.path.exists("/proc/%s" % pid):
119 # break the lock, try again
120 logging.warning("Breaking orphaned lock at %s", self.lockfile)
121 os.unlink(self.lockfile)
124 # check if the file is expiry old, if so, break the lock, try again
125 if time.time() - os.stat(self.lockfile).st_mtime > self.expiry:
126 logging.warning("Breaking stale lock at %s", self.lockfile)
127 os.unlink(self.lockfile)
130 if e.errno == errno.ENOENT:
134 # oh hey, it went away; try again
136 raise DirectoryLockedError(os.getcwd())
137 elif e.errno == errno.EACCES:
138 raise PermissionsError(os.getcwd())
141 raise DirectoryLockedError(os.getcwd())
142 def __exit__(self, *args):
144 os.unlink(self.lockfile)
150 Changes a directory, but has special exceptions for certain
156 if e.errno == errno.EACCES:
157 raise PermissionsError()
158 elif e.errno == errno.ENOENT:
159 raise NoSuchDirectoryError()
164 A map function for dictionaries. Only changes values.
166 >>> dictmap(lambda x: x + 2, {'a': 1, 'b': 2})
169 return dict((k,f(v)) for k,v in d.items())
173 A map function for dictionaries that passes key and value.
175 >>> dictkmap(lambda x, y: x + y, {1: 4, 3: 4})
178 return dict((k,f(k,v)) for k,v in d.items())
180 def get_exception_name(output):
182 Reads the traceback from a Python program and grabs the
183 fully qualified exception name.
185 lines = output.split("\n")
188 for line in lines[1:]:
190 if not line: continue
196 return line.partition(':')[0]
199 def get_dir_uid(dir):
200 """Finds the uid of the person who owns this directory."""
201 return os.stat(dir).st_uid
203 def get_dir_owner(dir = "."):
205 Finds the name of the locker this directory is in.
209 This function uses the passwd database and thus
210 only works on scripts servers when querying directories
213 uid = get_dir_uid(dir)
215 pwentry = pwd.getpwuid(uid)
216 return pwentry.pw_name
218 # do an pts query to get the name
219 return subprocess.Popen(['pts', 'examine', str(uid)], stdout=subprocess.PIPE).communicate()[0].partition(",")[0].partition(": ")[2]
222 """Returns the commit ID of the current Wizard install."""
223 # If you decide to convert this to use wizard.shell, be warned
224 # that there is a circular dependency, so this function would
225 # probably have to live somewhere else, probably wizard.git
226 wizard_git = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".git")
227 return subprocess.Popen(["git", "--git-dir=" + wizard_git, "rev-parse", "HEAD"], stdout=subprocess.PIPE).communicate()[0].rstrip()
229 def get_operator_info():
231 Returns tuple of ``(realname, email)`` about the person running
232 the script. If run from a scripts server, get info from Hesiod.
233 Otherwise, use the passwd database (email generated probably won't
234 actually accept mail). Useful when generating commit messages.
236 username = get_operator_name_from_gssapi()
239 hesinfo = subprocess.Popen(["hesinfo", username, "passwd"],stdout=subprocess.PIPE).communicate()[0]
240 fields = hesinfo.partition(",")[0]
241 realname = fields.rpartition(":")[2]
242 return realname, username + "@mit.edu"
244 # more traditional approach, but the email probably doesn't work
247 # since root isn't actually a useful designation, but maybe
248 # SUDO_USER contains something helpful
249 sudo_user = os.getenv("SUDO_USER")
252 pwdentry = pwd.getpwnam(sudo_user)
254 pwdentry = pwd.getpwuid(uid)
255 # XXX: error checking might be nice
256 # We follow the Ubuntu convention of gecos being a comma split field
257 # with the person's realname being the first entry.
258 return pwdentry.pw_gecos.split(",")[0], pwdentry.pw_name + "@" + socket.gethostname()
260 def get_operator_git():
262 Returns ``Real Name <username@mit.edu>`` suitable for use in
263 Git ``Something-by:`` string.
265 return "%s <%s>" % get_operator_info()
267 def get_operator_name_from_gssapi():
269 Returns username of the person operating this script based
270 off of the :envvar:`SSH_GSSAPI_NAME` environment variable.
274 :envvar:`SSH_GSSAPI_NAME` is not set by a vanilla OpenSSH
275 distributions. Scripts servers are patched to support this
276 environment variable.
278 principal = os.getenv("SSH_GSSAPI_NAME")
281 instance, _, _ = principal.partition("@")
282 if instance.endswith("/root"):
283 username, _, _ = principal.partition("/")
288 def set_operator_env():
290 Sets :envvar:`GIT_COMMITTER_NAME` and :envvar:`GIT_COMMITTER_EMAIL`
291 environment variables if applicable. Does nothing if
292 :func:`get_operator_info` throws :exc:`NoOperatorInfo`.
295 op_realname, op_email = get_operator_info()
296 os.putenv("GIT_COMMITTER_NAME", op_realname)
297 os.putenv("GIT_COMMITTER_EMAIL", op_email)
298 except NoOperatorInfo:
301 def set_author_env():
303 Sets :envvar:`GIT_AUTHOR_NAME` and :envvar:`GIT_AUTHOR_EMAIL` environment
304 variables if applicable. Does nothing if :func:`get_dir_owner` fails.
307 # XXX: should check if the directory is in AFS, and if not, use
308 # a more traditional metric
309 lockername = get_dir_owner()
310 os.putenv("GIT_AUTHOR_NAME", "%s locker" % lockername)
311 os.putenv("GIT_AUTHOR_EMAIL", "%s@scripts.mit.edu" % lockername)
312 except KeyError: # XXX: This doesn't actually make sense
316 """Sets all appropriate environment variables for Git commits."""
320 def get_git_footer():
321 """Returns strings for placing in Git log info about Wizard."""
322 return "\n".join(["Wizard-revision: %s" % get_revision()
323 ,"Wizard-args: %s" % " ".join(sys.argv)
326 def safe_unlink(file):
327 """Moves a file/dir to a backup location."""
328 if not os.path.exists(file):
330 prefix = "%s.bak" % file
332 for i in itertools.count():
333 name = "%s.%d" % (prefix, i)
334 if not os.path.exists(name):
336 os.rename(file, name)
339 def soft_unlink(file):
340 """Unlink a file, but don't complain if it doesn't exist."""
346 def fetch(host, path, subpath, post=None):
347 h = httplib.HTTPConnection(host)
348 fullpath = path.rstrip("/") + "/" + subpath.lstrip("/") # to be lenient about input we accept
350 headers = {"Content-type": "application/x-www-form-urlencoded"}
351 h.request("POST", fullpath, urllib.urlencode(post), headers)
353 h.request("GET", fullpath)
359 def mixed_newlines(filename):
360 """Returns ``True`` if ``filename`` has mixed newlines."""
361 f = open(filename, "U") # requires universal newline support
363 ret = isinstance(f.newlines, tuple)
364 f.close() # just to be safe
367 class NoOperatorInfo(wizard.Error):
368 """No information could be found about the operator from Kerberos."""
371 class PermissionsError(IOError):
374 class NoSuchDirectoryError(IOError):
377 class DirectoryLockedError(wizard.Error):
378 def __init__(self, dir):
383 ERROR: Could not acquire lock on directory. Maybe there is
384 another migration process running?