X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-web.git/blobdiff_plain/cdcd6b7a886dbc7f3908e70ce646ed7ad7c49546..2be0969e153ace507ec07b27d156b50e54e00f94:/code/main.py diff --git a/code/main.py b/code/main.py index e068e48..86da50e 100755 --- a/code/main.py +++ b/code/main.py @@ -6,23 +6,16 @@ import cPickle import cgi import datetime import hmac +import os import random import sha -import simplejson import sys import time import urllib import socket import cherrypy +from cherrypy import _cperror from StringIO import StringIO -def revertStandardError(): - """Move stderr to stdout, and return the contents of the old stderr.""" - errio = sys.stderr - if not isinstance(errio, StringIO): - return '' - sys.stderr = sys.stdout - errio.seek(0) - return errio.read() def printError(): """Revert stderr to stdout, and print the contents of stderr""" @@ -33,8 +26,6 @@ if __name__ == '__main__': import atexit atexit.register(printError) -import templates -from Cheetah.Template import Template import validation import cache_acls from webcommon import State @@ -45,26 +36,100 @@ from invirt.database import Machine, CDROM, session, connect, MachineAccess, Typ from invirt.config import structs as config from invirt.common import InvalidInput, CodeError -from view import View +from view import View, revertStandardError + + +static_dir = os.path.join(os.path.dirname(__file__), 'static') +InvirtStatic = cherrypy.tools.staticdir.handler( + root=static_dir, + dir=static_dir, + section='/static') + +class InvirtUnauthWeb(View): + static = InvirtStatic + + @cherrypy.expose + @cherrypy.tools.mako(filename="/unauth.mako") + def index(self): + return dict(simple=True) class InvirtWeb(View): def __init__(self): super(self.__class__,self).__init__() connect() self._cp_config['tools.require_login.on'] = True + self._cp_config['tools.catch_stderr.on'] = True self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config', 'from invirt import database'] + self._cp_config['request.error_response'] = self.handle_error + + static = InvirtStatic + + @cherrypy.expose + @cherrypy.tools.mako(filename="/invalid.mako") + def invalidInput(self): + """Print an error page when an InvalidInput exception occurs""" + err = cherrypy.request.prev.params["err"] + emsg = cherrypy.request.prev.params["emsg"] + d = dict(err_field=err.err_field, + err_value=str(err.err_value), stderr=emsg, + errorMessage=str(err)) + return d + + @cherrypy.expose + @cherrypy.tools.mako(filename="/error.mako") + def error(self): + """Print an error page when an exception occurs""" + op = cherrypy.request.prev.path_info + username = cherrypy.request.login + err = cherrypy.request.prev.params["err"] + emsg = cherrypy.request.prev.params["emsg"] + traceback = cherrypy.request.prev.params["traceback"] + d = dict(op=op, user=username, fields=cherrypy.request.prev.params, + errorMessage=str(err), stderr=emsg, traceback=traceback) + error_raw = cherrypy.request.lookup.get_template("/error_raw.mako") + details = error_raw.render(**d) + exclude = config.web.errormail_exclude + if username not in exclude and '*' not in exclude: + send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err), + details) + d['details'] = details + return d + def __getattr__(self, name): + # At the point __getattr__ is called, tools haven't been run. Make sure the user is logged in. + cherrypy.tools.remote_user_login.callable() + + if name in ("admin", "overlord"): + if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell): + raise InvalidInput('username', cherrypy.request.login, + 'Not in admin group %s.' % config.adminacl) + cherrypy.request.state = State(cherrypy.request.login, isadmin=True) + return self + else: + return super(InvirtWeb, self).__getattr__(name) + + def handle_error(self): + err = sys.exc_info()[1] + if isinstance(err, InvalidInput): + cherrypy.request.params['err'] = err + cherrypy.request.params['emsg'] = revertStandardError() + raise cherrypy.InternalRedirect('/invalidInput') + if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params: + cherrypy.request.params['err'] = err + cherrypy.request.params['emsg'] = revertStandardError() + cherrypy.request.params['traceback'] = _cperror.format_exc() + raise cherrypy.InternalRedirect('/error') + # fall back to cherrypy default error page + cherrypy.HTTPError(500).set_response() @cherrypy.expose @cherrypy.tools.mako(filename="/list.mako") def list(self, result=None): """Handler for list requests.""" - checkpoint.checkpoint('Getting list dict') d = getListDict(cherrypy.request.login, cherrypy.request.state) if result is not None: d['result'] = result - checkpoint.checkpoint('Got list dict') return d index=list @@ -106,8 +171,6 @@ wiki, including steps to prepare an HVM guest to boot as a ParaVM

We recommend using a ParaVM when possible and an HVM when necessary. """, - 'CPU Weight': """ -Don't ask us! We're as mystified as you are.""", 'Owner': """ The owner field is used to determine quotas. It must be the name of a @@ -131,8 +194,9 @@ your machine will run just fine, but the applet's display of the console will suffer artifacts. """, 'Windows': """ +Windows 7: The Windows 7 image is licensed for all MIT students and will automatically activate off the network; see the licensing agreement for details. The installer requires 512 MiB RAM and at least 15 GiB disk space (20 GiB or more recommended).
Windows Vista: The Vista image is licensed for all MIT students and will automatically activate off the network; see the licensing confirmation e-mail for details. The installer requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).
-Windows XP: This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one. +Windows XP: This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one, or visit http://msca.mit.edu/ if you are staff/faculty to request one. """ } @@ -147,10 +211,16 @@ console will suffer artifacts. help._cp_config['tools.require_login.on'] = False def parseCreate(self, fields): - kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()]) - validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws) - return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory, - disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type), + kws = dict([(kw, fields[kw]) for kw in + 'name description owner memory disksize vmtype cdrom autoinstall'.split() + if fields[kw]]) + validate = validation.Validate(cherrypy.request.login, + cherrypy.request.state, + strict=True, **kws) + return dict(contact=cherrypy.request.login, name=validate.name, + description=validate.description, memory=validate.memory, + disksize=validate.disksize, owner=validate.owner, + machine_type=getattr(validate, 'vmtype', Defaults.type), cdrom=getattr(validate, 'cdrom', None), autoinstall=getattr(validate, 'autoinstall', None)) @@ -161,7 +231,8 @@ console will suffer artifacts. """Handler for create requests.""" try: parsed_fields = self.parseCreate(fields) - machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields) + machine = controls.createVm(cherrypy.request.login, + cherrypy.request.state, **parsed_fields) except InvalidInput, err: pass else: @@ -170,8 +241,8 @@ console will suffer artifacts. d = getListDict(cherrypy.request.login, cherrypy.request.state) d['err'] = err if err: - for field in fields.keys(): - setattr(d['defaults'], field, fields.get(field)) + for field, value in fields.items(): + setattr(d['defaults'], field, value) else: d['new_machine'] = parsed_fields['name'] return d @@ -182,15 +253,26 @@ console will suffer artifacts. return {'request': cherrypy.request, 'kwargs': kwargs} helloworld._cp_config['tools.require_login.on'] = False - class MachineView(View): - # This is hairy. Fix when CherryPy 3.2 is out. (rename to - # _cp_dispatch, and parse the argument as a list instead of - # string + @cherrypy.expose + def errortest(self): + """Throw an error, to test the error-tracing mechanisms.""" + print >>sys.stderr, "look ma, it's a stderr" + raise RuntimeError("test of the emergency broadcast system") + class MachineView(View): def __getattr__(self, name): + """Synthesize attributes to allow RESTful URLs like + /machine/13/info. This is hairy. CherryPy 3.2 adds a + method called _cp_dispatch that allows you to explicitly + handle URLs that can't be mapped, and it allows you to + rewrite the path components and continue processing. + + This function gets the next path component being resolved + as a string. _cp_dispatch will get an array of strings + representing any subsequent path components as well.""" + try: - machine_id = int(name) - cherrypy.request.params['machine_id'] = machine_id + cherrypy.request.params['machine_id'] = int(name) return self except ValueError: return None @@ -199,13 +281,41 @@ console will suffer artifacts. @cherrypy.tools.mako(filename="/info.mako") def info(self, machine_id): """Handler for info on a single VM.""" - machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine + machine = validation.Validate(cherrypy.request.login, + cherrypy.request.state, + machine_id=machine_id).machine d = infoDict(cherrypy.request.login, cherrypy.request.state, machine) - checkpoint.checkpoint('Got infodict') return d index = info @cherrypy.expose + @cherrypy.tools.mako(filename="/info.mako") + @cherrypy.tools.require_POST() + def modify(self, machine_id, **fields): + """Handler for modifying attributes of a machine.""" + try: + modify_dict = modifyDict(cherrypy.request.login, + cherrypy.request.state, + machine_id, fields) + except InvalidInput, err: + result = None + machine = validation.Validate(cherrypy.request.login, + cherrypy.request.state, + machine_id=machine_id).machine + else: + machine = modify_dict['machine'] + result = 'Success!' + err = None + info_dict = infoDict(cherrypy.request.login, + cherrypy.request.state, machine) + info_dict['err'] = err + if err: + for field, value in fields.items(): + setattr(info_dict['defaults'], field, value) + info_dict['result'] = result + return info_dict + + @cherrypy.expose @cherrypy.tools.mako(filename="/vnc.mako") def vnc(self, machine_id): """VNC applet page. @@ -227,8 +337,9 @@ console will suffer artifacts. Remember to enable iptables! echo 1 > /proc/sys/net/ipv4/ip_forward """ - machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine - + machine = validation.Validate(cherrypy.request.login, + cherrypy.request.state, + machine_id=machine_id).machine token = controls.vnctoken(machine) host = controls.listHost(machine) if host: @@ -246,86 +357,41 @@ console will suffer artifacts. port=port, authtoken=token) return d + @cherrypy.expose @cherrypy.tools.mako(filename="/command.mako") @cherrypy.tools.require_POST() def command(self, command_name, machine_id, **kwargs): """Handler for running commands like boot and delete on a VM.""" - back = kwargs.get('back', None) + back = kwargs.get('back') + if command_name == 'delete': + back = 'list' try: - d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs) - if d['command'] == 'Delete VM': - back = 'list' + d = controls.commandResult(cherrypy.request.login, + cherrypy.request.state, + command_name, machine_id, kwargs) except InvalidInput, err: if not back: raise print >> sys.stderr, err - result = err + result = str(err) else: result = 'Success!' if not back: return d if back == 'list': cherrypy.request.state.clear() #Changed global state - raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result)) + raise cherrypy.InternalRedirect('/list?result=%s' + % urllib.quote(result)) elif back == 'info': - raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303) + raise cherrypy.HTTPRedirect(cherrypy.request.base + + '/machine/%d/' % machine_id, + status=303) else: raise InvalidInput('back', back, 'Not a known back page.') machine = MachineView() -def pathSplit(path): - if path.startswith('/'): - path = path[1:] - i = path.find('/') - if i == -1: - i = len(path) - return path[:i], path[i:] - -class Checkpoint: - def __init__(self): - self.start_time = time.time() - self.checkpoints = [] - - def checkpoint(self, s): - self.checkpoints.append((s, time.time())) - - def __str__(self): - return ('Timing info:\n%s\n' % - '\n'.join(['%s: %s' % (d, t - self.start_time) for - (d, t) in self.checkpoints])) - -checkpoint = Checkpoint() - -def makeErrorPre(old, addition): - if addition is None: - return - if old: - return old[:-6] + '\n----\n' + str(addition) + '' - else: - return '

STDERR:

' + str(addition) + '
' - -Template.database = database -Template.config = config -Template.err = None - -class JsonDict: - """Class to store a dictionary that will be converted to JSON""" - def __init__(self, **kws): - self.data = kws - if 'err' in kws: - err = kws['err'] - del kws['err'] - self.addError(err) - - def __str__(self): - return simplejson.dumps(self.data) - - def addError(self, text): - """Add stderr text to be displayed on the website.""" - self.data['err'] = \ - makeErrorPre(self.data.get('err'), text) class Defaults: """Class to store default values for fields.""" @@ -335,6 +401,7 @@ class Defaults: autoinstall = '' name = '' description = '' + administrator = '' type = 'linux-hvm' def __init__(self, max_memory=None, max_disk=None, **kws): @@ -345,17 +412,6 @@ class Defaults: for key in kws: setattr(self, key, kws[key]) - - -DEFAULT_HEADERS = {'Content-Type': 'text/html'} - -def invalidInput(op, username, fields, err, emsg): - """Print an error page when an InvalidInput exception occurs""" - d = dict(op=op, user=username, err_field=err.err_field, - err_value=str(err.err_value), stderr=emsg, - errorMessage=str(err)) - return templates.invalid(searchList=[d]) - def hasVnc(status): """Does the machine with a given status list support VNC?""" if status is None: @@ -369,20 +425,18 @@ def hasVnc(status): def getListDict(username, state): """Gets the list of local variables used by list.tmpl.""" - checkpoint.checkpoint('Starting') machines = state.machines - checkpoint.checkpoint('Got my machines') on = {} has_vnc = {} + installing = {} xmlist = state.xmlist - checkpoint.checkpoint('Got uptimes') - can_clone = 'ice3' not in state.xmlist_raw for m in machines: if m not in xmlist: has_vnc[m] = 'Off' m.uptime = None else: m.uptime = xmlist[m]['uptime'] + installing[m] = bool(xmlist[m].get('autoinstall')) if xmlist[m]['console']: has_vnc[m] = True elif m.type.hvm: @@ -391,11 +445,9 @@ def getListDict(username, state): has_vnc[m] = "ParaVM" max_memory = validation.maxMemory(username, state) max_disk = validation.maxDisk(username) - checkpoint.checkpoint('Got max mem/disk') defaults = Defaults(max_memory=max_memory, max_disk=max_disk, owner=username) - checkpoint.checkpoint('Got defaults') def sortkey(machine): return (machine.owner != username, machine.owner, machine.name) machines = sorted(machines, key=sortkey) @@ -406,7 +458,8 @@ def getListDict(username, state): defaults=defaults, machines=machines, has_vnc=has_vnc, - can_clone=can_clone) + installing=installing, + disable_creation=False) return d def getHostname(nic): @@ -462,15 +515,18 @@ def getDiskInfo(data_dict, machine): data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.) return disk_fields -def modifyDict(username, state, fields): +def modifyDict(username, state, machine_id, fields): """Modify a machine as specified by CGI arguments. - Return a list of local variables for modify.tmpl. + Return a dict containing the machine that was modified. """ olddisk = {} session.begin() try: - kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()]) + kws = dict((kw, fields[kw]) for kw in + 'owner admin contact name description memory vmtype disksize'.split() + if fields.get(kw)) + kws['machine_id'] = machine_id validate = validation.Validate(username, state, **kws) machine = validate.machine oldname = machine.name @@ -481,23 +537,10 @@ def modifyDict(username, state, fields): if hasattr(validate, 'vmtype'): machine.type = validate.vmtype - if hasattr(validate, 'disksize'): - disksize = validate.disksize - disk = machine.disks[0] - if disk.size != disksize: - olddisk[disk.guest_device_name] = disksize - disk.size = disksize - session.save_or_update(disk) - update_acl = False if hasattr(validate, 'owner') and validate.owner != machine.owner: machine.owner = validate.owner update_acl = True - if hasattr(validate, 'name'): - machine.name = validate.name - for n in machine.nics: - if n.hostname == oldname: - n.hostname = validate.name if hasattr(validate, 'description'): machine.description = validate.description if hasattr(validate, 'admin') and validate.admin != machine.administrator: @@ -506,48 +549,56 @@ def modifyDict(username, state, fields): if hasattr(validate, 'contact'): machine.contact = validate.contact - session.save_or_update(machine) - if update_acl: - cache_acls.refreshMachine(machine) + session.add(machine) session.commit() except: session.rollback() raise - for diskname in olddisk: - controls.resizeDisk(oldname, diskname, str(olddisk[diskname])) - if hasattr(validate, 'name'): - controls.renameMachine(machine, oldname, validate.name) - return dict(user=username, - command="modify", - machine=machine) - -def modify(username, state, path, fields): - """Handler for modifying attributes of a machine.""" + + session.begin() try: - modify_dict = modifyDict(username, state, fields) - except InvalidInput, err: - result = None - machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine - else: - machine = modify_dict['machine'] - result = 'Success!' - err = None - info_dict = infoDict(username, state, machine) - info_dict['err'] = err - if err: - for field in fields.keys(): - setattr(info_dict['defaults'], field, fields.getfirst(field)) - info_dict['result'] = result - return templates.info(searchList=[info_dict]) - -def badOperation(u, s, p, e): - """Function called when accessing an unknown URI.""" - return ({'Status': '404 Not Found'}, 'Invalid operation.') + if hasattr(validate, 'disksize'): + disksize = validate.disksize + disk = machine.disks[0] + if disk.size != disksize: + olddisk[disk.guest_device_name] = disksize + disk.size = disksize + session.add(disk) + for diskname in olddisk: + controls.resizeDisk(oldname, diskname, str(olddisk[diskname])) + session.add(machine) + session.commit() + except: + session.rollback() + raise + + session.begin() + try: + if hasattr(validate, 'name'): + machine.name = validate.name + for n in machine.nics: + if n.hostname == oldname: + n.hostname = validate.name + if hasattr(validate, 'name'): + controls.renameMachine(machine, oldname, validate.name) + session.add(machine) + session.commit() + except: + session.rollback() + raise + + if update_acl: + cache_acls.refreshMachine(machine) + + return dict(machine=machine) def infoDict(username, state, machine): """Get the variables used by info.tmpl.""" - status = controls.statusInfo(machine) - checkpoint.checkpoint('Getting status info') + try: + status = controls.statusInfo(machine) + except CodeError, e: + # machine was shut down in between the call to listInfoDict and this + status = None has_vnc = hasVnc(status) if status is None: main_status = dict(name=machine.name, @@ -557,11 +608,14 @@ def infoDict(username, state, machine): else: main_status = dict(status[1:]) main_status['host'] = controls.listHost(machine) - start_time = float(main_status.get('start_time', 0)) - uptime = datetime.timedelta(seconds=int(time.time()-start_time)) + start_time = main_status.get('start_time') + if start_time is None: + uptime = "Still booting?" + else: + start_time = float(start_time) + uptime = datetime.timedelta(seconds=int(time.time()-start_time)) cpu_time_float = float(main_status.get('cpu_time', 0)) cputime = datetime.timedelta(seconds=int(cpu_time_float)) - checkpoint.checkpoint('Status') display_fields = [('name', 'Name'), ('description', 'Description'), ('owner', 'Owner'), @@ -607,18 +661,14 @@ def infoDict(username, state, machine): pass #fields.append((disp, None)) - checkpoint.checkpoint('Got fields') - - max_mem = validation.maxMemory(machine.owner, state, machine, False) - checkpoint.checkpoint('Got mem') max_disk = validation.maxDisk(machine.owner, machine) defaults = Defaults() for name in 'machine_id name description administrator owner memory contact'.split(): - setattr(defaults, name, getattr(machine, name)) + if getattr(machine, name): + setattr(defaults, name, getattr(machine, name)) defaults.type = machine.type.type_id defaults.disk = "%0.2f" % (machine.disks[0].size/1024.) - checkpoint.checkpoint('Got defaults') d = dict(user=username, on=status is not None, machine=machine, @@ -631,40 +681,6 @@ def infoDict(username, state, machine): fields = fields) return d -def unauthFront(_, _2, _3, fields): - """Information for unauth'd users.""" - return templates.unauth(searchList=[{'simple' : True, - 'hostname' : socket.getfqdn()}]) - -def admin(username, state, path, fields): - if path == '': - return ({'Status': '303 See Other', - 'Location': 'admin/'}, - "You shouldn't see this message.") - if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'): - raise InvalidInput('username', username, - 'Not in admin group %s.' % config.adminacl) - newstate = State(username, isadmin=True) - newstate.environ = state.environ - return handler(username, newstate, path, fields) - -def throwError(_, __, ___, ____): - """Throw an error, to test the error-tracing mechanisms.""" - raise RuntimeError("test of the emergency broadcast system") - -mapping = dict( - modify=modify, - unauth=unauthFront, - admin=admin, - overlord=admin, - errortest=throwError) - -def printHeaders(headers): - """Print a dictionary as HTTP headers.""" - for key, value in headers.iteritems(): - print '%s: %s' % (key, value) - print - def send_error_mail(subject, body): import subprocess @@ -681,98 +697,4 @@ Subject: %s p.stdin.close() p.wait() -def show_error(op, username, fields, err, emsg, traceback): - """Print an error page when an exception occurs""" - d = dict(op=op, user=username, fields=fields, - errorMessage=str(err), stderr=emsg, traceback=traceback) - details = templates.error_raw(searchList=[d]) - exclude = config.web.errormail_exclude - if username not in exclude and '*' not in exclude: - send_error_mail('xvm error on %s for %s: %s' % (op, username, err), - details) - d['details'] = details - return templates.error(searchList=[d]) - -def handler(username, state, path, fields): - operation, path = pathSplit(path) - if not operation: - operation = 'list' - print 'Starting', operation - fun = mapping.get(operation, badOperation) - return fun(username, state, path, fields) - -class App: - def __init__(self, environ, start_response): - self.environ = environ - self.start = start_response - - self.username = getUser(environ) - self.state = State(self.username) - self.state.environ = environ - - random.seed() #sigh - - def __iter__(self): - start_time = time.time() - database.clear_cache() - sys.stderr = StringIO() - fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ) - operation = self.environ.get('PATH_INFO', '') - if not operation: - self.start("301 Moved Permanently", [('Location', './')]) - return - if self.username is None: - operation = 'unauth' - - try: - checkpoint.checkpoint('Before') - output = handler(self.username, self.state, operation, fields) - checkpoint.checkpoint('After') - - headers = dict(DEFAULT_HEADERS) - if isinstance(output, tuple): - new_headers, output = output - headers.update(new_headers) - e = revertStandardError() - if e: - if hasattr(output, 'addError'): - output.addError(e) - else: - # This only happens on redirects, so it'd be a pain to get - # the message to the user. Maybe in the response is useful. - output = output + '\n\nstderr:\n' + e - output_string = str(output) - checkpoint.checkpoint('output as a string') - except Exception, err: - if not fields.has_key('js'): - if isinstance(err, InvalidInput): - self.start('200 OK', [('Content-Type', 'text/html')]) - e = revertStandardError() - yield str(invalidInput(operation, self.username, fields, - err, e)) - return - import traceback - self.start('500 Internal Server Error', - [('Content-Type', 'text/html')]) - e = revertStandardError() - s = show_error(operation, self.username, fields, - err, e, traceback.format_exc()) - yield str(s) - return - status = headers.setdefault('Status', '200 OK') - del headers['Status'] - self.start(status, headers.items()) - yield output_string - if fields.has_key('timedebug'): - yield '
%s
' % cgi.escape(str(checkpoint)) - -def constructor(): - connect() - return App - -def main(): - from flup.server.fcgi_fork import WSGIServer - WSGIServer(constructor()).run() - -if __name__ == '__main__': - main() +random.seed() #sigh