]> scripts.mit.edu Git - wizard.git/blob - plugins/scripts/wizard_scripts.py
No AFS means OSError, not CallError.
[wizard.git] / plugins / scripts / wizard_scripts.py
1 """
2 This is ostensibly the place where Scripts specific code should live.
3 """
4
5 import os
6 import shlex
7 import errno
8 import logging
9 import urlparse
10 import time
11 import errno
12 import sqlalchemy
13
14 import wizard
15 from wizard import deploy, shell, install, util, user
16
17 def dummy_apps():
18     return [
19         "joomla", "e107", "gallery2", "advancedbook", "phpical",
20         "trac", "turbogears", "django", "rails",
21         # these are technically deprecated
22         "advancedpoll", "gallery",
23     ]
24
25 def deploy_web(dir):
26     # try the directory
27     homedir, _, web_path = dir.partition("/web_scripts")
28     if web_path:
29         # XXX: To be truly correct, we should emulate the logic of suexec; but
30         # looking at the home directory is a pretty good stopgap for now
31         name = homedir.rpartition("/")[2]
32         yield urlparse.ParseResult(
33                 "http",
34                 name + ".scripts.mit.edu",
35                 web_path.rstrip('/'),
36                 "", "", "")
37         yield urlparse.ParseResult(
38                 "http",
39                 "scripts.mit.edu",
40                 "/~" + name + web_path.rstrip('/'),
41                 "", "", "")
42     else:
43         logging.info("Directory location did not contain web_scripts: %s", dir)
44
45 def user_quota(dir=None):
46     """
47     Returns a tuple (quota usage, quota limit).  Works only for scripts
48     servers.  Values are in bytes.  Returns ``(0, None)`` if we couldn't figure it out.
49     """
50     end = 2
51     # sometimes the volume is busy; so we try several times
52     for i in range(0, end + 1):
53         try:
54             return _user_quota(dir)
55         except QuotaParseError as e:
56             if i == end:
57                 raise e
58             time.sleep(3) # give it a chance to unbusy
59     assert False # should not get here
60
61 def _user_quota(dir=None):
62     # XXX: The correct way is to implement Python modules implementing
63     # bindings for all the appropriate interfaces
64     unknown = (0, None)
65     def parse_last_quote(ret):
66         return ret.rstrip('\'').rpartition('\'')[2]
67     if dir is None:
68         dir = os.getcwd()
69     sh = shell.Shell()
70     try:
71         cell = parse_last_quote(sh.eval("fs", "whichcell", "-path", dir))
72     except shell.CallError:
73         return unknown
74     except OSError as e:
75         if e.errno == errno.ENOENT:
76             return unknown
77         raise
78     mount = None
79     while dir:
80         try:
81             volume = parse_last_quote(sh.eval("fs", "lsmount", dir))[1:]
82             break
83         except shell.CallError:
84             dir = os.path.dirname(dir)
85         except OSError as e:
86             if e.errno == errno.ENOENT:
87                 return unknown
88             raise
89     if not volume: return unknown
90     try:
91         result = sh.eval("vos", "examine", "-id", volume, "-cell", cell).splitlines()
92     except OSError:
93         try:
94             result = sh.eval("/usr/sbin/vos", "examine", "-id", volume, "-cell", cell).splitlines()
95         except OSError:
96             return unknown
97     except shell.CallError:
98         return unknown
99     logging.debug("vos examine output was:\n\n" + "\n".join(result))
100     try:
101         usage = int(result[0].split()[3]) * 1024
102         limit = int(result[3].split()[1]) * 1024 # XXX: FRAGILE
103     except ValueError:
104         raise QuotaParseError("vos examine output was:\n\n" + "\n".join(result))
105     return (usage, limit)
106
107 class QuotaParseError(wizard.Error):
108     """Could not parse quota information."""
109     def __init__(self, msg):
110         self.msg = msg
111     def __str__(self):
112         return """
113
114 ERROR: Could not parse quota. %s
115 """ % self.msg
116
117 def sql_auth(url):
118     if url.driver == "mysql":
119         try:
120             url.host, url.username, url.password = shell.Shell().eval("/mit/scripts/sql/bin/get-password").split()
121             return url
122         except shell.CallError:
123             pass
124         except OSError:
125             pass
126     return None
127
128 def sql_drop(url):
129     if url.host == "sql.mit.edu":
130         try:
131             shell.call("/mit/scripts/sql/bin/drop-database", url.database)
132             return True
133         except shell.CallError:
134             pass
135     return None
136
137 def user_email(name):
138     # XXX: simplistic install which doesn't work most of the time
139     return "%s@scripts.mit.edu" % name
140
141 def user_operator():
142     """
143     Returns username of the person operating this script based
144     off of the :envvar:`SSH_GSSAPI_NAME` environment variable.
145
146     .. note::
147
148         :envvar:`SSH_GSSAPI_NAME` is not set by a vanilla OpenSSH
149         distributions.  Scripts servers are patched to support this
150         environment variable.
151     """
152     principal = os.getenv("SSH_GSSAPI_NAME")
153     if not principal:
154         return None
155     instance, _, _ = principal.partition("@")
156     if instance.endswith("/root"):
157         username, _, _ = principal.partition("/")
158     else:
159         username = instance
160     return username
161
162 def user_passwd(dir, uid):
163     # XXX: simplistic heuristic for detecting AFS.  The correct thing to
164     # is either to statfs and match magic number, use one of the
165     # vos tools or check mounted directories.
166     if not dir.startswith("/afs/"):
167         return None
168     try:
169         result = shell.eval("hesinfo", str(uid), "uid")
170     except shell.CallError:
171         return None
172     name, password, uid, gid, gecos, homedir, console = result.split(":")
173     realname = gecos.split(",")[0]
174     return user.Info(name, uid, gid, realname, homedir, console)
175
176 class MysqlStrategy(install.Strategy):
177     """
178     Performs scripts specific guesses for MySQL variables.  This
179     may create an appropriate database for the user.
180     """
181     side_effects = True
182     provides = frozenset(["dsn"])
183     def prepare(self):
184         """Uses the SQL programs in the scripts locker"""
185         logging.info("Attempting wizard_scripts MySQL strategy")
186         if self.application.database != "mysql":
187             raise install.StrategyFailed
188         try:
189             self._triplet = shell.eval("/mit/scripts/sql/bin/get-password").split()
190         except OSError:
191             raise install.StrategyFailed
192         except shell.CallError:
193             raise install.StrategyFailed
194         if len(self._triplet) != 3:
195             raise install.StrategyFailed
196         self._username = os.getenv('USER')
197         if self._username is None:
198             raise install.StrategyFailed
199     def execute(self, options):
200         """Creates a new database for the user using ``get-next-database`` and ``create-database``."""
201         host, username, password = self._triplet
202         # race condition
203         name = shell.eval("/mit/scripts/sql/bin/get-next-database", os.path.basename(self.dir))
204         database = shell.eval("/mit/scripts/sql/bin/create-database", name)
205         if not database:
206             raise CreateDatabaseError
207         options.dsn = sqlalchemy.engine.url.URL("mysql", username=username, password=password, host=host, database=database)
208
209 class EmailStrategy(install.Strategy):
210     """Performs script specific guess for email."""
211     provides = frozenset(["email"])
212     def prepare(self):
213         """Uses :envvar:`USER` and assumes you are an MIT affiliate."""
214         # XXX: This might be buggy, because locker might be set to USER
215         self._user = os.getenv("USER")
216         if self._user is None:
217             raise install.StrategyFailed
218     def execute(self, options):
219         """No-op."""
220         options.email = self._user + "@mit.edu"
221
222 class CreateDatabaseError(wizard.Error):
223     """Could not create a database with the create-database script."""
224     def __str__(self):
225         return """
226
227 ERROR: We were unable to create a database for you: this may indicate
228 that you have exceeded your quota of databases or indicate a problem
229 with the sql.mit.edu service.  Please mail scripts@mit.edu with this
230 error message."""