""" This is ostensibly the place where Scripts specific code should live. """ import os import shlex import errno import logging import urlparse import time import errno import sqlalchemy import wizard from wizard import deploy, shell, install, util, user def dummy_apps(): return [ "joomla", "e107", "gallery2", "advancedbook", "phpical", "trac", "turbogears", "django", "rails", # these are technically deprecated "advancedpoll", "gallery", ] def deploy_web(dir): # try the directory homedir, _, web_path = dir.partition("/web_scripts") if web_path: # XXX: To be truly correct, we should emulate the logic of suexec; but # looking at the home directory is a pretty good stopgap for now name = homedir.rpartition("/")[2] yield urlparse.ParseResult( "http", name + ".scripts.mit.edu", web_path.rstrip('/'), "", "", "") yield urlparse.ParseResult( "http", "scripts.mit.edu", "/~" + name + web_path.rstrip('/'), "", "", "") else: logging.info("Directory location did not contain web_scripts: %s", dir) def user_quota(dir=None): """ Returns a tuple (quota usage, quota limit). Works only for scripts servers. Values are in bytes. Returns ``(0, None)`` if we couldn't figure it out. """ end = 2 # sometimes the volume is busy; so we try several times for i in range(0, end + 1): try: return _user_quota(dir) except QuotaParseError as e: if i == end: raise e time.sleep(3) # give it a chance to unbusy assert False # should not get here def _user_quota(dir=None): # XXX: The correct way is to implement Python modules implementing # bindings for all the appropriate interfaces unknown = (0, None) def parse_last_quote(ret): return ret.rstrip('\'').rpartition('\'')[2] if dir is None: dir = os.getcwd() sh = shell.Shell() try: cell = parse_last_quote(sh.eval("fs", "whichcell", "-path", dir)) except shell.CallError: return unknown except OSError as e: if e.errno == errno.ENOENT: return unknown raise mount = None while dir: try: volume = parse_last_quote(sh.eval("fs", "lsmount", dir))[1:] break except shell.CallError: dir = os.path.dirname(dir) except OSError as e: if e.errno == errno.ENOENT: return unknown raise if not volume: return unknown try: result = sh.eval("vos", "examine", "-id", volume, "-cell", cell).splitlines() except OSError: try: result = sh.eval("/usr/sbin/vos", "examine", "-id", volume, "-cell", cell).splitlines() except OSError: return unknown except shell.CallError: return unknown logging.debug("vos examine output was:\n\n" + "\n".join(result)) try: usage = int(result[0].split()[3]) * 1024 limit = int(result[3].split()[1]) * 1024 # XXX: FRAGILE except ValueError: raise QuotaParseError("vos examine output was:\n\n" + "\n".join(result)) return (usage, limit) class QuotaParseError(wizard.Error): """Could not parse quota information.""" def __init__(self, msg): self.msg = msg def __str__(self): return """ ERROR: Could not parse quota. %s """ % self.msg def sql_auth(url): if url.driver == "mysql": try: url.host, url.username, url.password = shell.Shell().eval("/mit/scripts/sql/bin/get-password").split() return url except shell.CallError: pass except OSError: pass return None def sql_drop(url): if url.host == "sql.mit.edu": try: shell.call("/mit/scripts/sql/bin/drop-database", url.database) return True except shell.CallError: pass return None def user_email(name): # XXX: simplistic heuristic which doesn't work most of the time # See Scripts #224 (this issue) and Scripts #193 (tracking a contact # address) return "%s@scripts.mit.edu" % name def user_operator(): """ Returns username of the person operating this script based off of the :envvar:`SSH_GSSAPI_NAME` environment variable. .. note:: :envvar:`SSH_GSSAPI_NAME` is not set by a vanilla OpenSSH distributions. Scripts servers are patched to support this environment variable. """ principal = os.getenv("SSH_GSSAPI_NAME") if not principal: return None instance, _, _ = principal.partition("@") if instance.endswith("/root"): username, _, _ = principal.partition("/") else: username = instance return username def user_passwd(dir, uid): # XXX: simplistic heuristic for detecting AFS. The correct thing to # is either to statfs and match magic number, use one of the # vos tools or check mounted directories. if not dir.startswith("/afs/"): return None try: result = shell.eval("hesinfo", str(uid), "uid") except shell.CallError: return None name, password, uid, gid, gecos, homedir, console = result.split(":") realname = gecos.split(",")[0] return user.Info(name, uid, gid, realname, homedir, console) class MysqlStrategy(install.Strategy): """ Performs scripts specific guesses for MySQL variables. This may create an appropriate database for the user. """ side_effects = True provides = frozenset(["dsn"]) def prepare(self): """Uses the SQL programs in the scripts locker""" logging.info("Attempting wizard_scripts MySQL strategy") if self.application.database != "mysql": raise install.StrategyFailed try: self._triplet = shell.eval("/mit/scripts/sql/bin/get-password").split() except OSError: raise install.StrategyFailed except shell.CallError: raise install.StrategyFailed if len(self._triplet) != 3: raise install.StrategyFailed self._username = os.getenv('USER') if self._username is None: raise install.StrategyFailed def execute(self, options): """Creates a new database for the user using ``get-next-database`` and ``create-database``.""" host, username, password = self._triplet # race condition name = shell.eval("/mit/scripts/sql/bin/get-next-database", os.path.basename(self.dir)) database = shell.eval("/mit/scripts/sql/bin/create-database", name) if not database: raise CreateDatabaseError options.dsn = sqlalchemy.engine.url.URL("mysql", username=username, password=password, host=host, database=database) class EmailStrategy(install.Strategy): """Performs script specific guess for email.""" provides = frozenset(["email"]) def prepare(self): """Uses :envvar:`USER` and assumes you are an MIT affiliate.""" # XXX: This might be buggy, because locker might be set to USER self._user = os.getenv("USER") if self._user is None: raise install.StrategyFailed def execute(self, options): """No-op.""" options.email = self._user + "@mit.edu" class CreateDatabaseError(wizard.Error): """Could not create a database with the create-database script.""" def __str__(self): return """ ERROR: We were unable to create a database for you: this may indicate that you have exceeded your quota of databases or indicate a problem with the sql.mit.edu service. Please mail scripts@mit.edu with this error message."""