--- /dev/null
+[_DEFAULT]
+db_host=sql.mit.edu
+db_user=root
+db_pass=
+db_name=mitsql
+import config
+import os as _os
+if False and config._ENV_BASE in _os.environ.keys():
+ _base = _os.environ[config._ENV_BASE]
+else:
+ _base = _os.path.abspath(_os.path.dirname(_os.path.realpath(__file__)) + '/../../../')
+config = config.Config('mitsql.ini')
+
+import logging
+logging.disable(logging.DEBUG)
\ No newline at end of file
--- /dev/null
+"""
+Global configuration objects.
+
+Joe Presbrey <presbrey@mit.edu>
+"""
+
+from ConfigParser import ConfigParser, NoSectionError, NoOptionError
+import os
+
+_ENV_BASE='_SQL_MIT_EDU'
+
+class Section(object):
+ """Configuration section namespace."""
+ def __init__(self, d):
+ self.items = d
+
+ def __getattr__(self, k):
+ v = self.items.get(k, None)
+ try:
+ if str(int(v)) == str(v): v = int(v)
+ except TypeError: pass
+ except ValueError: pass
+ try:
+ if str(v).lower() == 'true': v = True
+ if str(v).lower() == 'false': v = False
+ except TypeError: pass
+ except ValueError: pass
+ return v
+
+ def __iter__(self):
+ return iter(self.items)
+
+ def __str__(self):
+ return str(self.items)
+
+class Config(object):
+ """Base configuration namespace."""
+ def __init__(self, filename, *args, **kwargs):
+ self._cp = ConfigParser(*args, **kwargs)
+ self.read(filename)
+ self._none = Section({})
+
+ def _samefile(self, f1, f2):
+ try:
+ return os.path.samefile(f1, f2)
+ except OSError, e:
+ return False
+
+ def read(self, filename):
+ config_path = [os.path.join(x, filename) for x in [
+ os.path.dirname(__file__),
+ '/etc']]
+ if _ENV_BASE in os.environ:
+ config_path.insert(1, os.path.join(os.environ[_ENV_BASE], 'etc', filename))
+ def append(path):
+ r = os.path.join(path, filename)
+ if not r in config_path:
+ config_path.append(r)
+ if 'HOME' in os.environ:
+ append(os.environ['HOME'])
+ if 'PWD' in os.environ:
+ append(os.environ['PWD'])
+ self._cp.read(config_path)
+ self._sections = dict(map(lambda x: (x, Section(dict(self._cp.items(x)))),
+ self._cp.sections()))
+
+ def get(self, *av, **kw):
+ try: return self._cp.get(*av, **kw)
+ except NoSectionError: return None
+ except NoOptionError: return None
+
+ def __getattr__(self, k):
+ return self._sections.get(k, self._none)
+
+ def __str__(self):
+ d = {}
+ for x in self._cp.sections():
+ d[x] = dict(self._cp.items(x))
+ return str(d)
+
+if __name__ == '__main__':
+ import mitsql
+ print mitsql.config
-import cherrypy
+import mitsql
+config = mitsql.config.interface
+
+from MySQLdb import Connection as _connect
+from MySQLdb.cursors import DictCursor as _cursor
+
+def _connect(*argv, **kw):
+ return _connect(config.db_host, config.db_user, config.db_pass, config.db_name, cursorclass=_Cursor)
-from main import Root as root
-root = root()
+from handler import Root as _root
+root = _root()
+
+from cheetah import load
+load(mitsql._base + '/www/templates')
+
+import cherrypy
+cherrypy.tree.mount(root, '/')
+cherrypy.config.update({'error_page.404': root.error._404,
+ 'error_page.500': root.error._500,
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': mitsql._base + '/www/root'})
def map_uri():
print root.default()
# if authorization:
# cherrypy.request.login = parseAuthorization(authorization)
-cherrypy.tools.map_uri = cherrypy.Tool('on_start_resource', map_uri)
+#cherrypy.tools.map_uri = cherrypy.Tool('on_start_resource', map_uri)
#cherrypy.tools.map_uri = cherrypy.Tool('before_request_body', map_uri)
-# Joe Presbrey <presbrey@mit.edu>
-
+import qwobl
import os, sys, types, time
import threading
from pyinotify import WatchManager, Notifier, EventsCodes
from Cheetah.Template import Template as _Template
-
-__all__ = ['Templates']
+import logging
class Template(object):
- def __init__(self, name, ns={}, path=['templates']):
- self._name, self._ns, self._path = name, ns, path+[name]
- assert self._template()
+ def __init__(self, name='templates', ns={}, path=[]):
+ self._name, self._ns, self._path = name, ns, len(path) and path+[name] or [name]
+ if len(self._path) > 1:
+ assert self._template()
def _template(self):
return sys.modules['.'.join(self._path)]
return getattr(self._template(),
self._name)(namespaces=namespaces).respond()
-class Templates(object):
- _base = ''
+class Loader(object):
_mask = EventsCodes.ALL_FLAGS['IN_CREATE'] \
| EventsCodes.ALL_FLAGS['IN_MODIFY'] \
| EventsCodes.ALL_FLAGS['IN_DELETE'] \
_files = {}
_modules = {}
- def __init__(self, template_path, global_namespace={}):
- self._base = os.path.abspath(template_path)
+ def __init__(self, global_namespace={}):
self._event_init()
- self._assert_module('templates', [template_path])
+ self._assert_module('templates')
self._globals = global_namespace
- self.load()
self._thread = threading.Thread(target=self.loop)
self._thread.setDaemon(True)
self._thread.start()
self._modules[name].__path__ = path
sys.modules[name] = self._modules[name]
- def _get_template_path(self, template_file):
- r = template_file.startswith(self._base) \
- and template_file[len(self._base)+1:].split(os.path.sep) or []
+ def _get_template_path(self, path_name, template_file):
+ r = template_file.startswith(path_name) \
+ and template_file[len(path_name)+1:].split(os.path.sep) or []
if r[-1].endswith('.tmpl'):
r[-1] = r[-1][:-5]
return tuple(r)
compilerSettings={'useStackFrames':False})
return template
- def add(self, template_file):
- template_path = self._get_template_path(template_file)
+ def add(self, path_name, template_file):
+ template_path = self._get_template_path(path_name, template_file)
template_name = '.'.join(['templates']+list(template_path))
template_pkg = '.'.join(['templates']+list(template_path[:-1]))
template_py = os.path.join(os.path.dirname(template_file), '%s.py' % template_path[-1])
f_code = file(template_py, 'w')
f_code.write(code)
f_code.close()
- if self._loaded and \
- template_name in sys.modules:
+ if template_name in sys.modules:
reload(sys.modules[template_name])
def remove(self, template_file):
#print 'removing', template_file
pass
- def _walk_load(self, args, dname, fnames):
- for fname in fnames:
- if fname.endswith('.tmpl'):
- template_file = os.path.join(dname, fname)
- self.add(template_file)
-
- def load(self):
- if not self._loaded:
- os.path.walk(self._base, self._walk_load, None)
- for path, filename in self._files.items():
- __import__('.'.join(['templates']+list(path)))
- self._loaded = True
-
- def _event_handler(self, event):
- if event.name.endswith('.tmpl') and not event.name.startswith('.'):
- if event.maskname == 'IN_DELETE':
- self.remove(event.pathname)
- else:
- time.sleep(0.5)
- self.add(event.pathname)
-
+ def _walk_path(self, path_name):
+ def _walk_load(args, dname, fnames):
+ for fname in fnames:
+ if fname.endswith('.tmpl'):
+ template_file = os.path.join(dname, fname)
+ self.add(path_name, template_file)
+ return _walk_load
+
+ def load(self, path_name):
+ os.path.walk(path_name, self._walk_path(path_name), None)
+ for path, filename in self._files.items():
+ logging.debug('TEMPLATE LOAD [%s] %s', path, filename)
+ __import__('.'.join(['templates']+list(path)))
+ self._event_add_watch(path_name)
+
+ def _path_event_handler(self, path_name):
+ def _event_handler(event):
+ logging.debug('TEMPLATE EVENT: %s', repr(event.__dict__.items()))
+ if event.name.endswith('.tmpl') and not event.name.startswith('.'):
+ if event.maskname == 'IN_DELETE':
+ self.remove(event.pathname)
+ else:
+ time.sleep(0.5)
+ self.add(path_name, event.pathname)
+ return _event_handler
def _event_init(self):
self._watch_manager = WatchManager()
- self._watch_manager.add_watch(self._base,
- self._mask,
- self._event_handler,
- rec=True,
- auto_add=True)
self._notifier = Notifier(self._watch_manager)
+ def _event_add_watch(self, path_name):
+ r = self._watch_manager.add_watch(path_name,
+ self._mask,
+ self._path_event_handler(path_name),
+ rec=True,
+ auto_add=True)
+
def loop(self):
- self.load()
while True:
try:
self._notifier.process_events()
except KeyboardInterrupt:
self._notifier.stop()
break
-
- def __getattr__(self, key):
- return Template(key, self._globals)
+
+_loader = Loader()
+load = _loader.load
+templates = Template()
+
+__all__ = ['load', 'templates']
if __name__ == '__main__':
- t = Templates('/home/joe/templates')
+ t = Loader('/home/joe/templates')
--- /dev/null
+def db_list_entry(name, size_tot, size_max):
+ return '%s: %s/%s' % (name, size_tot, size_max)
\ No newline at end of file
--- /dev/null
+import mitsql
+from cheetah import templates
+import cherrypy
+
+from pprint import pformat
+
+def _500(*argv, **kw):
+ s_exc = cherrypy._cperror.format_exc()
+ mitsql.logging.error(s_exc)
+ return templates.common.shell(title='%s (CherryPy %s)' % (kw.get('status'), kw.get('version')),
+ content=('<p>%s</p>' + (2*'<div><pre>%s</pre></div>'))
+ % (kw.get('message'), kw.get('traceback'),
+ _phtml(dict(filter(lambda x: not x[0][:2] == '__',
+ cherrypy.request.__dict__.items())))))
+
+class Root(object):
+ class Error(object):
+ @cherrypy.expose
+ def _404(*argv, **kw):
+ return templates.common._404()
+ def _500(*argv, **kw):
+ return _500(*argv, **kw)
+ error = Error()
+
+ class DB(object):
+ def list(*argv, **kw):
+ r = {'db_list': [],
+ 'size_tot': '1M',
+ 'size_max': '100M',
+ 'user': {
+ 'db_prefix': 'presbrey+'
+ }}
+ return templates.main.db_list(**r)
+ list.exposed = True
+ db = DB()
+
+ def test(self, *argv, **kw):
+ cherrypy.response.headers['Content-type'] = 'text/plain'
+ return pformat(dict(filter(lambda x: not x[0][:2] == '__', cherrypy.request.__dict__.items())))
+ test.exposed = True
+
+ def index(self, *argv, **kw):
+ return templates.main.index()
+ index.exposed = True
+
+ def passwd(self, *argv, **kw):
+ return templates.main.passwd()
+ passwd.exposed = True
\ No newline at end of file
+++ /dev/null
-import cherrypy
-from pprint import pformat
-
-class Root(object):
- def default(self, *args, **kw):
- cherrypy.response.headers['Content-type'] = 'text/plain'
- return pformat(dict(filter(lambda x: not x[0][:2] == '__', cherrypy.request.__dict__.items())))
- default.exposed = True
-
- def test(self):
- return 'test'
- test.exposed = True
-
#!/usr/bin/python
import os, sys, time
-import sql.db
+from sql import db
from sql.util import new_cursor, get_dbs, db_backup_pre, db_backup_mkdir
from Queue import Queue, Empty
import threading
try:
next = queue.get(timeout=3)
print next[0] + ':',
- log = sql.db.Backup.get_by(db=next[0])
+ log = db.Backup.get_by(db=next[0])
if not log:
- log = sql.db.Backup(db=next[0])
+ log = db.Backup(db=next[0])
log.dump_path = next[1]
- log.dump_date = sql.db.func.now()
+ log.dump_date = db.func.now()
db_backup_mkdir(next[1])
args = ['mysqldump', next[0]]
args.extend(MYSQLDUMP_ARGS)
log.dump_errnum = None
log.dump_errstr = None
log.save_or_update()
- sql.db.session.flush()
+ db.session.flush()
print 'Done'
except (KeyboardInterrupt, SystemExit):
break
def producer():
c = new_cursor('mysqldump')
for db in get_dbs(c):
- log = sql.db.Backup.get_by(db=db)
+ log = db.Backup.get_by(db=db)
if not log:
- log = sql.db.Backup(db=db)
+ log = db.Backup(db=db)
elif log.skip_date:
if time.mktime(log.skip_date.timetuple()) + 3600 > time.time():
# never recheck a db skipped in the past hour
log.skip_date = None
else:
log.skip_reason = d[1]
- log.skip_date = sql.db.func.now()
+ log.skip_date = db.func.now()
log.save_or_update()
- #sql.db.session.flush()
+ #db.session.flush()
try:
producer()
--- /dev/null
+#!/bin/bash
+
+exec cherryd -c $_SQL_MIT_EDU/www/etc/devel.conf -i mitsql.interface
--- /dev/null
+[global]
+server.socket_host: "0.0.0.0"
+server.socket_port: 8336
+tools.encode.on: True
+tools.encode.encoding: 'utf-8'
+#tools.map_uri.on: True
+#tools.header_auth.on: True
+#request.show_tracebacks: True
--- /dev/null
+[global]
+tree.root: mitsql.interface.root
+server.socket_file: '/tmp/mitsql.sock'
+
+tools.encode.on: True
+tools.encode.encoding: 'utf-8'
+#tools.map_uri.on: True
+#autoreload.on: False
+#checker.on: False
+#engine.SIGHUP: None
+#engine.SIGTERM: None
--- /dev/null
+html,body,div,span,form,h1,h2,h3,h4,p,blockquote{margin:0;padding:0;border:0;outline:0;}
+
+/* ------- Layout CSS (Centered Fixed Width) ------ */
+
+body {
+ font-family: Verdana, sans-serif;
+ font-size: 12px;
+ margin: 0;
+ padding: 0;
+ text-align: center; /* IE 5.5 hack */
+}
+#farouter {
+ background: #ffffff;
+ width: 80%;
+ border: 1px solid #3A291F;
+ margin: 10px auto 10px auto;
+ text-align: left; /* IE 5.5 hack part II */
+}
+#outer div#masthead { margin: 1em; }
+#outer div#content_wide { margin: 1em; }
+#content { float: left; width: 490px; }
+#content_wide { padding: 0 35px 0 35px; }
+#content_home { float: left; width: 490px; padding-top: 0px; }
+#menu { float: right; width: 175px; margin-right: 10px; border-left: 1px dashed #2050A0; }
+#nav { padding-left: 10px; }
+#logo { padding-top: 25px; padding-left: 12px; }
+#hmenu { height: 30px; }
+#hnav { margin: 0; padding: 0; }
+#clearer { clear: both; margin: 0; padding: 0; }
+#footer { margin: 8px 8px 8px 8px; }
+
+/* ----------------- Color CSS ------------------------ */
+
+body { background: #2050A0; }
+#footer { background-color: #eee; border-top: 1px solid #ddd; }
+#hnav { background: #2050A0; }
+a { color: #2050A0; text-decoration: none; }
+a:visited { color: #2050A0; text-decoration: none; }
+a:hover { color: #2050A0; text-decoration: underline; }
+h2 { /*color: #17397A; */ font-size: 0.9em; font-weight: normal; }
+h3 { /* color: #17397A; */ }
+h3 a { color: #17397A; }
+h3 a:hover { color: #17397A; }
+h3 a:visited { color: #17397A; }
+h4 { font-size: 16px; font-weight: normal; }
+
+/* -------- Core CSS --------- */
+
+
+a { text-decoration: none; font-weight:bold; }
+a img { border: none; }
+
+h1 { margin: 0; padding: 0; }
+h1#title { float: left; }
+h1#sitename { text-align: right; }
+h1#sitename a:hover { text-decoration: underline; }
+h2#tagline { text-align: right; letter-spacing: 0.2em; }
+#hidden {display:none;}
+
+img { margin: 10px; padding: 10px; border: 0px solid #2F700F; }
+
+p { line-height: 175%; }
+
+.textarea { width:200px; margin:0; }
+
+/*------------- hnav------------*/
+
+#hnav ul {
+ text-align: center;
+ padding-bottom: 5px;
+ padding-top: 5px;
+ padding-left: 0;
+ margin-top: 0;
+ /* cancels gap caused by top padding in Opera 7.54 */
+ margin-left: 0;
+ background-color: #2050A0;
+ color: #F2EEEC;
+ width: 100%;
+ line-height: 18px;
+ /* fixes Firefox 0.9.3 */
+}
+
+#hnav ul li {
+ display: inline;
+ padding-left: 0;
+ padding-right: 0;
+ padding-bottom: 5px;
+ /* matches link padding except for left and right */
+ padding-top: 5px;
+}
+
+#hnav ul li a {
+ padding-left: 10px;
+ padding-right: 10px;
+ padding-bottom: 5px;
+ padding-top: 5px;
+ color: #F2EEEC;
+ text-decoration: none;
+ border-right: 1px solid #F2EEEC;
+}
+
+#hnav ul li.menu_last a {
+ border-right: 0;
+}
+
+#hnav ul li a:hover {
+ background: #F2EEEC;
+ color: #3A291F;
+}
+
+#hnav #active { border-left: 1px solid #F2EEEC; }
+
--- /dev/null
+#extends templates.common.shell
+
+#def title: Page Not Found
+
+#block content
+<p>We're sorry! The page you requested does not exist. Please return to our <a href="/">homepage</a> or <a href="/contact">contact us</a>.</p>
+#end block content
--- /dev/null
+#extends templates.common.shell
+
+#def title: Page Error
+
+#block content
+<p>We're sorry! The requested you made caused an internal error we weren't expecting. The support team has been notified.</p>
+<p>Please return to our <a href="/">homepage</a>, go back and try again, or <a href="/contact">contact us</a>.</p>
+#end block content
--- /dev/null
+#extends skeleton
--- /dev/null
+#from mitsql import config
+#import cherrypy, time
+#set global x_agent = cherrypy.request.headers.get('X-Agent', 'Mozilla')
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ #set page_title = $title.split('|')[0]
+ <title>$page_title</title>
+ <link rel="stylesheet" type="text/css" href="/assets/css/style.css?$time.time" />
+ <link rel="icon" type="image/png" href="/assets/images/favicon.ico" />
+ <script type="text/javascript" src="/assets/js/prototype.js"></script>
+ #block head_append
+ #end block head_append
+</head>
+<body>
+<div id="farouter">
+ <div id="outer">
+ <div id="masthead">
+ <h1 id="title">$title</h1>
+ <h1 id="sitename"><a href="http://sql.mit.edu/" target="_top">sql.mit.edu</a></h1>
+ <h2 id="tagline">MIT SIPB MySQL Service for Athena<br />
+ email: sql@mit.edu</h2>
+ </div>
+ <div id="hmenu">
+ <div id="hnav">
+ <ul id="navlist">
+<div style="float:left;">
+<li><a href="/db/list">Manage DBs</a></li>
+<li><a href="do/setup">Account</a></li>
+<li><a href="do/admin/main">Admin</a></li>
+<li><a href="do/logout">Logout</a></li>
+<li><a href="do/signup">Sign up</a></li>
+<li><a href="https://scripts.mit.edu/~sql/phpMyAdmin/" target="_blank">phpMyAdmin</a></li>
+</div>
+<div style="text-align: right;">
+Login via:
+ <li><a href="do/login?ssl=0">SQL Password</a></li>
+ <li class="menu_last"><a href="do/login?ssl=1">MIT Certificate</a></li>
+</div>
+ </ul>
+ </div>
+ </div>
+ <div id="main">
+ <div id="content_wide">
+ #block body
+ #end block body
+ </div><!-- end#content -->
+ </div><!-- end#main -->
+
+ <!-- footer -->
+ <div id="foot"> </div><!-- end#foot -->
+ <div id="clearer"> </div><!-- end#clearer -->
+ <div id="footer">
+ <p id="credit" style="text-align: right;">
+ <strong><em>sql.mit.edu v2.0</em></strong>
+ </p>
+ </div><!-- end#footer -->
+ </div><!-- end#outer -->
+</div><!-- end#farouter -->
+</body>
+</html>
--- /dev/null
+#extends templates.common.shell
+
+#def title: Contact
+
+#block body
+<h3>Contact/Help</h3>
+
+<p>We welcome all questions, comments, and suggestions.</p>
+<p>Please direct inquiries to <strong>sql@mit.edu</strong></p>
+
+#end block body
\ No newline at end of file
--- /dev/null
+#extends templates.common.shell
+#from mitsql.interface import display
+
+#def title: Manage Databases
+
+#block body
+
+<table width="100%">
+#if len($db_list) == 0:
+<tr><td width="100%"><em>You have no databases. Add one below.</em></td></tr>
+#else:
+#for name, data in $db_list:
+<tr><td width="100%">
+#echo display.db_list_entry($name, $data.size_tot, $data.size_max)
+</td><td>
+<p>drop</p>
+</td></tr>
+#end for
+#end if
+<tr><td colspan=2>
+<hr />
+</td></tr><tr><td colspan=2>
+#echo display.db_list_entry('TOTAL', $size_tot, $size_max)
+</td></tr>
+</table>
+
+<form method="post" action="create">
+<p align="right">
+ <span style="width: 150px; font-style: italic;">
+ <label for="p1">new database:</label>
+ </span> $user.db_prefix<input type="text" name="db_name" />
+ <input type=submit value="create">
+</p>
+</form>
+
+<h3>Manage Data</h3>
+<p>One interface we recommend for managing SQL data is <a href="https://scripts.mit.edu/~sql/phpMyAdmin/" target="_blank">phpMyAdmin</a>. Feel free to use it after you've created your databases.</p>
+
+#end block body
\ No newline at end of file
--- /dev/null
+#extends templates.common.shell
+
+#def title: MySQL Services
+
+#block body
+<p>
+This service provides <a target="_blank" href="http://dev.mysql.com/doc">MySQL</a> databases to <a target="_blank" href="http://ca.mit.edu/">MIT certificate</a> holders.
+You must choose a MySQL password (which should be different from your Athena account password) when you <a href="do/signup">sign up</a>, and
+then use this interface to create and drop databases. All subsequent SQL commands can be issued from any host, client, and/or script of your choice;
+simply connect to the MySQL server at <b>sql.mit.edu</b> using your username and your new MySQL password.
+You may find it convenient to run scripts using the <a target="_blank" href="http://scripts.mit.edu/web">web script service</a> or
+<a target="_blank" href="http://scripts.mit.edu/cron">shortjobs service</a>.
+</p>
+
+<p>
+All uses of this service must comply with the <a target="_blank" href="http://web.mit.edu/olh/Welcome/rules.html">MITnet rules of use</a>.
+</p>
+
+<p>
+This service has been designed with reliability in mind; we utilize RAID, live server mirroring and periodic offline backups to ensure data reliability.
+However, the SIPB MySQL service should not be used to host critical applications that cannot tolerate downtime.
+One nightly backup is available for your locker at <code>/mit/sql/backup/LOCKER_NAME</code>. You should perform additional backups of your data using the <a href="http://scripts.mit.edu/cron">shortjobs service</a> or <a href="http://scripts.mit.edu/faq/10/how-can-i-back-up-or-restore-my-sql-data">phpMyAdmin</a>.
+</p>
+
+<div style="text-align:center; font-style: italic;">
+<!-- /afs/athena.mit.edu/contrib/sql/stat/questions -->
+</div>
+#end block body
\ No newline at end of file
--- /dev/null
+#extends templates.common.shell
+
+#def title: Change Password
+
+#block body
+<p>Your MySQL password should be different from your Athena account password.</p>
+
+<form method="post">
+<table>
+<tr><td align="right">new password:</td><td><input type="password" name="p[1]"></td></tr>
+<tr><td align="right">repeat new password:</td><td><input type="password" name="p[2]"></td></tr>
+<tr><td align="right" colspan=2><br /><input name="chgpw" type="submit" value="change password"></td
+></tr>
+</table>
+</form>
+<hr />
+<p>Note: you need to also <a href="http://scripts.mit.edu/faq/59">update your .my.cnf file on Athena</a> if you want to use SIPB <a href="http://scripts.mit.edu/start/">scripts auto-installers</a> or access the MySQL service from the command-line.</p>
+
+<br />
+<div style="width: 50%">
+<hr />
+This website provides signups for individual user locker
+MySQL accounts. Groups locker administrators can
+<a href="http://scripts.mit.edu/faq/66">signup for group locker MySQL accounts</a>
+from any Athena prompt.
+</div>
+
+#end block body
\ No newline at end of file
--- /dev/null
+#extends templates.common.shell
+
+#def title: Change Password
+
+#block body
+<p>Your MySQL password should be different from your Athena account password.</p>
+
+<form method="post">
+<table>
+<tr><td align="right">new password:</td><td><input type="password" name="p[1]"></td></tr>
+<tr><td align="right">repeat new password:</td><td><input type="password" name="p[2]"></td></tr>
+<tr><td align="right" colspan=2><br /><input name="chgpw" type="submit" value="change password"></td
+></tr>
+</table>
+</form>
+<hr />
+<p>Note: you need to also <a href="http://scripts.mit.edu/faq/59">update your .my.cnf file on Athena</a> if you want to use SIPB <a href="http://scripts.mit.edu/start/">scripts auto-installers</a> or access the MySQL service from the command-line.</p>
+#end block body
\ No newline at end of file