2 Miscellaneous utility functions and classes.
6 from wizard.util import *
23 class ChangeDirectory(object):
25 Context for temporarily changing the working directory.
27 >>> with ChangeDirectory("/tmp"):
31 def __init__(self, dir):
35 self.olddir = os.getcwd()
37 def __exit__(self, *args):
40 class Counter(object):
42 Object for counting different values when you don't know what
43 they are a priori. Supports index access and iteration.
45 >>> counter = Counter()
46 >>> counter.count("foo")
47 >>> print counter["foo"]
52 def count(self, value):
53 """Increments count for ``value``."""
54 self.dict.setdefault(value, 0)
56 def __getitem__(self, key):
59 return self.dict.__iter__()
61 """Returns the max counter value seen."""
62 return max(self.dict.values())
64 """Returns the sum of all counter values."""
65 return sum(self.dict.values())
67 """Returns the keys of counters."""
68 return self.dict.keys()
70 class PipeToLess(object):
72 Context for printing output to a pager. Use this if output
73 is expected to be long.
76 self.proc = subprocess.Popen("less", stdin=subprocess.PIPE)
77 self.old_stdout = sys.stdout
78 sys.stdout = self.proc.stdin
79 def __exit__(self, *args):
81 self.proc.stdin.close()
83 sys.stdout = self.old_stdout
85 class IgnoreKeyboardInterrupts(object):
87 Context for temporarily ignoring keyboard interrupts. Use this
88 if aborting would cause more harm than finishing the job.
91 signal.signal(signal.SIGINT,signal.SIG_IGN)
92 def __exit__(self, *args):
93 signal.signal(signal.SIGINT, signal.default_int_handler)
95 class LockDirectory(object):
97 Context for locking a directory.
99 def __init__(self, lockfile):
100 self.lockfile = lockfile
103 os.open(self.lockfile, os.O_CREAT | os.O_EXCL)
105 if e.errno == errno.EEXIST:
106 raise DirectoryLockedError(os.getcwd())
107 elif e.errno == errno.EACCES:
108 raise PermissionsError(os.getcwd())
110 def __exit__(self, *args):
112 os.unlink(self.lockfile)
118 Changes a directory, but has special exceptions for certain
124 if e.errno == errno.EACCES:
125 raise PermissionsError()
126 elif e.errno == errno.ENOENT:
127 raise NoSuchDirectoryError()
132 A map function for dictionaries. Only changes values.
134 >>> dictmap(lambda x: x + 2, {'a': 1, 'b': 2})
137 return dict((k,f(v)) for k,v in d.items())
141 A map function for dictionaries that passes key and value.
143 >>> dictkmap(lambda x, y: x + y, {1: 4, 3: 4})
146 return dict((k,f(k,v)) for k,v in d.items())
148 def get_exception_name(output):
150 Reads the traceback from a Python program and grabs the
151 fully qualified exception name.
153 lines = output.split("\n")
156 for line in lines[1:]:
158 if not line: continue
164 return line.partition(':')[0]
167 def get_dir_uid(dir):
168 """Finds the uid of the person who owns this directory."""
169 return os.stat(dir).st_uid
171 def get_dir_owner(dir = "."):
173 Finds the name of the locker this directory is in.
177 This function uses the passwd database and thus
178 only works on scripts servers when querying directories
181 uid = get_dir_uid(dir)
183 pwentry = pwd.getpwuid(uid)
184 return pwentry.pw_name
186 # do an pts query to get the name
187 return subprocess.Popen(['pts', 'examine', str(uid)], stdout=subprocess.PIPE).communicate()[0].partition(",")[0].partition(": ")[2]
190 """Returns the commit ID of the current Wizard install."""
191 # If you decide to convert this to use wizard.shell, be warned
192 # that there is a circular dependency, so this function would
193 # probably have to live somewhere else, probably wizard.git
194 wizard_git = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".git")
195 return subprocess.Popen(["git", "--git-dir=" + wizard_git, "rev-parse", "HEAD"], stdout=subprocess.PIPE).communicate()[0].rstrip()
197 def get_operator_info():
199 Returns tuple of ``(realname, email)`` about the person running
200 the script. If run from a scripts server, get info from Hesiod.
201 Otherwise, use the passwd database (email generated probably won't
202 actually accept mail). Useful when generating commit messages.
204 username = get_operator_name_from_gssapi()
207 hesinfo = subprocess.Popen(["hesinfo", username, "passwd"],stdout=subprocess.PIPE).communicate()[0]
208 fields = hesinfo.partition(",")[0]
209 realname = fields.rpartition(":")[2]
210 return realname, username + "@mit.edu"
212 # more traditional approach, but the email probably doesn't work
215 # since root isn't actually a useful designation, but maybe
216 # SUDO_USER contains something helpful
217 sudo_user = os.getenv("SUDO_USER")
220 pwdentry = pwd.getpwnam(sudo_user)
222 pwdentry = pwd.getpwuid(uid)
223 # XXX: error checking might be nice
224 # We follow the Ubuntu convention of gecos being a comma split field
225 # with the person's realname being the first entry.
226 return pwdentry.pw_gecos.split(",")[0], pwdentry.pw_name + "@" + socket.gethostname()
228 def get_operator_git():
230 Returns ``Real Name <username@mit.edu>`` suitable for use in
231 Git ``Something-by:`` string.
233 return "%s <%s>" % get_operator_info()
235 def get_operator_name_from_gssapi():
237 Returns username of the person operating this script based
238 off of the :envvar:`SSH_GSSAPI_NAME` environment variable.
242 :envvar:`SSH_GSSAPI_NAME` is not set by a vanilla OpenSSH
243 distributions. Scripts servers are patched to support this
244 environment variable.
246 principal = os.getenv("SSH_GSSAPI_NAME")
249 instance, _, _ = principal.partition("@")
250 if instance.endswith("/root"):
251 username, _, _ = principal.partition("/")
256 def set_operator_env():
258 Sets :envvar:`GIT_COMMITTER_NAME` and :envvar:`GIT_COMMITTER_EMAIL`
259 environment variables if applicable. Does nothing if
260 :func:`get_operator_info` throws :exc:`NoOperatorInfo`.
263 op_realname, op_email = get_operator_info()
264 os.putenv("GIT_COMMITTER_NAME", op_realname)
265 os.putenv("GIT_COMMITTER_EMAIL", op_email)
266 except NoOperatorInfo:
269 def set_author_env():
271 Sets :envvar:`GIT_AUTHOR_NAME` and :envvar:`GIT_AUTHOR_EMAIL` environment
272 variables if applicable. Does nothing if :func:`get_dir_owner` fails.
275 # XXX: should check if the directory is in AFS, and if not, use
276 # a more traditional metric
277 lockername = get_dir_owner()
278 os.putenv("GIT_AUTHOR_NAME", "%s locker" % lockername)
279 os.putenv("GIT_AUTHOR_EMAIL", "%s@scripts.mit.edu" % lockername)
280 except KeyError: # XXX: This doesn't actually make sense
284 """Sets all appropriate environment variables for Git commits."""
288 def get_git_footer():
289 """Returns strings for placing in Git log info about Wizard."""
290 return "\n".join(["Wizard-revision: %s" % get_revision()
291 ,"Wizard-args: %s" % " ".join(sys.argv)
294 def safe_unlink(file):
295 """Moves a file/dir to a backup location."""
296 if not os.path.exists(file):
298 prefix = "%s.bak" % file
300 for i in itertools.count():
301 name = "%s.%d" % (prefix, i)
302 if not os.path.exists(name):
304 os.rename(file, name)
307 def soft_unlink(file):
308 """Unlink a file, but don't complain if it doesn't exist."""
314 def fetch(host, path, subpath, post=None):
315 h = httplib.HTTPConnection(host)
316 fullpath = path.rstrip("/") + "/" + subpath.lstrip("/") # to be lenient about input we accept
318 headers = {"Content-type": "application/x-www-form-urlencoded"}
319 h.request("POST", fullpath, urllib.urlencode(post), headers)
321 h.request("GET", fullpath)
327 class NoOperatorInfo(wizard.Error):
328 """No information could be found about the operator from Kerberos."""
331 class PermissionsError(IOError):
334 class NoSuchDirectoryError(IOError):
337 class DirectoryLockedError(wizard.Error):
338 def __init__(self, dir):
343 ERROR: Could not acquire lock on directory. Maybe there is
344 another migration process running?