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 class PipeToLess(object):
63 Context for printing output to a pager. Use this if output
64 is expected to be long.
67 self.proc = subprocess.Popen("less", stdin=subprocess.PIPE)
68 self.old_stdout = sys.stdout
69 sys.stdout = self.proc.stdin
70 def __exit__(self, *args):
72 self.proc.stdin.close()
74 sys.stdout = self.old_stdout
76 class IgnoreKeyboardInterrupts(object):
78 Context for temporarily ignoring keyboard interrupts. Use this
79 if aborting would cause more harm than finishing the job.
82 signal.signal(signal.SIGINT,signal.SIG_IGN)
83 def __exit__(self, *args):
84 signal.signal(signal.SIGINT, signal.default_int_handler)
86 class LockDirectory(object):
88 Context for locking a directory.
90 def __init__(self, lockfile):
91 self.lockfile = lockfile
94 os.open(self.lockfile, os.O_CREAT | os.O_EXCL)
96 if e.errno == errno.EEXIST:
97 raise DirectoryLockedError(os.getcwd())
98 elif e.errno == errno.EACCES:
99 raise command.PermissionsError(os.getcwd())
101 def __exit__(self, *args):
103 os.unlink(self.lockfile)
109 Changes a directory, but has special exceptions for certain
115 if e.errno == errno.EACCES:
116 raise PermissionsError()
117 elif e.errno == errno.ENOENT:
118 raise NoSuchDirectoryError()
123 A map function for dictionaries. Only changes values.
125 >>> dictmap(lambda x: x + 2, {'a': 1, 'b': 2})
128 return dict((k,f(v)) for k,v in d.items())
132 A map function for dictionaries that passes key and value.
134 >>> dictkmap(lambda x, y: x + y, {1: 4, 3: 4})
137 return dict((k,f(k,v)) for k,v in d.items())
139 def get_exception_name(output):
141 Reads the traceback from a Python program and grabs the
142 fully qualified exception name.
144 lines = output.split("\n")
147 for line in lines[1:]:
149 if not line: continue
161 def get_dir_uid(dir):
162 """Finds the uid of the person who owns this directory."""
163 return os.stat(dir).st_uid
165 def get_dir_owner(dir = "."):
167 Finds the name of the locker this directory is in.
171 This function uses the passwd database and thus
172 only works on scripts servers when querying directories
175 pwentry = pwd.getpwuid(get_dir_uid(dir))
176 # XXX: Error handling!
177 return pwentry.pw_name
180 """Returns the commit ID of the current Wizard install."""
181 # If you decide to convert this to use wizard.shell, be warned
182 # that there is a circular dependency, so this function would
183 # probably have to live somewhere else, probably wizard.git
184 wizard_git = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".git")
185 return subprocess.Popen(["git", "--git-dir=" + wizard_git, "rev-parse", "HEAD"], stdout=subprocess.PIPE).communicate()[0].rstrip()
187 def get_operator_info():
189 Returns tuple of ``(realname, email)`` about the person running
190 the script. If run from a scripts server, get info from Hesiod.
191 Otherwise, use the passwd database (email generated probably won't
192 actually accept mail). Useful when generating commit messages.
194 username = get_operator_name_from_gssapi()
197 hesinfo = subprocess.Popen(["hesinfo", username, "passwd"],stdout=subprocess.PIPE).communicate()[0]
198 fields = hesinfo.partition(",")[0]
199 realname = fields.rpartition(":")[2]
200 return realname, username + "@mit.edu"
202 # more traditional approach, but the email probably doesn't work
205 # since root isn't actually a useful designation, but maybe
206 # SUDO_USER contains something helpful
207 sudo_user = os.getenv("SUDO_USER")
210 pwdentry = pwd.getpwnam(sudo_user)
212 pwdentry = pwd.getpwuid(uid)
213 # XXX: error checking might be nice
214 # We follow the Ubuntu convention of gecos being a comma split field
215 # with the person's realname being the first entry.
216 return pwdentry.pw_gecos.split(",")[0], pwdentry.pw_name + "@" + socket.gethostname()
218 def get_operator_git():
220 Returns ``Real Name <username@mit.edu>`` suitable for use in
221 Git ``Something-by:`` string.
223 return "%s <%s>" % get_operator_info()
225 def get_operator_name_from_gssapi():
227 Returns username of the person operating this script based
228 off of the :envvar:`SSH_GSSAPI_NAME` environment variable.
232 :envvar:`SSH_GSSAPI_NAME` is not set by a vanilla OpenSSH
233 distributions. Scripts servers are patched to support this
234 environment variable.
236 principal = os.getenv("SSH_GSSAPI_NAME")
239 instance, _, _ = principal.partition("@")
240 if instance.endswith("/root"):
241 username, _, _ = principal.partition("/")
246 def set_operator_env():
248 Sets :envvar:`GIT_COMMITTER_NAME` and :envvar:`GIT_COMMITTER_EMAIL`
249 environment variables if applicable. Does nothing if
250 :func:`get_operator_info` throws :exc:`NoOperatorInfo`.
253 op_realname, op_email = get_operator_info()
254 os.putenv("GIT_COMMITTER_NAME", op_realname)
255 os.putenv("GIT_COMMITTER_EMAIL", op_email)
256 except NoOperatorInfo:
259 def set_author_env():
261 Sets :envvar:`GIT_AUTHOR_NAME` and :envvar:`GIT_AUTHOR_EMAIL` environment
262 variables if applicable. Does nothing if :func:`get_dir_owner` fails.
265 # XXX: should check if the directory is in AFS, and if not, use
266 # a more traditional metric
267 lockername = get_dir_owner()
268 os.putenv("GIT_AUTHOR_NAME", "%s locker" % lockername)
269 os.putenv("GIT_AUTHOR_EMAIL", "%s@scripts.mit.edu" % lockername)
270 except KeyError: # XXX: This doesn't actually make sense
274 """Sets all appropriate environment variables for Git commits."""
278 def get_git_footer():
279 """Returns strings for placing in Git log info about Wizard."""
280 return "\n".join(["Wizard-revision: %s" % get_revision()
281 ,"Wizard-args: %s" % " ".join(sys.argv)
284 def safe_unlink(file):
285 """Moves a file/dir to a backup location."""
286 prefix = "%s.bak" % file
288 for i in itertools.count():
289 name = "%s.%d" % (prefix, i)
290 if not os.path.exists(name):
292 os.rename(file, name)
295 def fetch(host, path, subpath, post=None):
296 h = httplib.HTTPConnection(host)
297 fullpath = path + "/" + subpath
299 headers = {"Content-type": "application/x-www-form-urlencoded"}
300 h.request("POST", fullpath, urllib.urlencode(post), headers)
302 h.request("GET", fullpath)
308 class NoOperatorInfo(wizard.Error):
309 """No information could be found about the operator from Kerberos."""
312 class PermissionsError(IOError):
315 class NoSuchDirectoryError(IOError):
318 class DirectoryLockedError(wizard.Error):
319 def __init__(self, dir):
324 ERROR: Could not acquire lock on directory. Maybe there is
325 another migration process running?