use wildcard for config.web.errormail_exclude
[invirt/packages/invirt-web.git] / code / main.py
1 #!/usr/bin/python
2 """Main CGI script for web interface"""
3
4 import base64
5 import cPickle
6 import cgi
7 import datetime
8 import hmac
9 import random
10 import sha
11 import simplejson
12 import sys
13 import time
14 import urllib
15 from StringIO import StringIO
16
17 def revertStandardError():
18     """Move stderr to stdout, and return the contents of the old stderr."""
19     errio = sys.stderr
20     if not isinstance(errio, StringIO):
21         return ''
22     sys.stderr = sys.stdout
23     errio.seek(0)
24     return errio.read()
25
26 def printError():
27     """Revert stderr to stdout, and print the contents of stderr"""
28     if isinstance(sys.stderr, StringIO):
29         print revertStandardError()
30
31 if __name__ == '__main__':
32     import atexit
33     atexit.register(printError)
34
35 import templates
36 from Cheetah.Template import Template
37 import validation
38 import cache_acls
39 from webcommon import InvalidInput, CodeError, State
40 import controls
41 from getafsgroups import getAfsGroupMembers
42 from invirt import database
43 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
44 from invirt.config import structs as config
45
46 def pathSplit(path):
47     if path.startswith('/'):
48         path = path[1:]
49     i = path.find('/')
50     if i == -1:
51         i = len(path)
52     return path[:i], path[i:]
53
54 class Checkpoint:
55     def __init__(self):
56         self.start_time = time.time()
57         self.checkpoints = []
58
59     def checkpoint(self, s):
60         self.checkpoints.append((s, time.time()))
61
62     def __str__(self):
63         return ('Timing info:\n%s\n' %
64                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
65                            (d, t) in self.checkpoints]))
66
67 checkpoint = Checkpoint()
68
69 def jquote(string):
70     return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
71
72 def helppopup(subj):
73     """Return HTML code for a (?) link to a specified help topic"""
74     return ('<span class="helplink"><a href="help?' +
75             cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
76             +'" target="_blank" ' +
77             'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
78
79 def makeErrorPre(old, addition):
80     if addition is None:
81         return
82     if old:
83         return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
84     else:
85         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
86
87 Template.database = database
88 Template.config = config
89 Template.helppopup = staticmethod(helppopup)
90 Template.err = None
91
92 class JsonDict:
93     """Class to store a dictionary that will be converted to JSON"""
94     def __init__(self, **kws):
95         self.data = kws
96         if 'err' in kws:
97             err = kws['err']
98             del kws['err']
99             self.addError(err)
100
101     def __str__(self):
102         return simplejson.dumps(self.data)
103
104     def addError(self, text):
105         """Add stderr text to be displayed on the website."""
106         self.data['err'] = \
107             makeErrorPre(self.data.get('err'), text)
108
109 class Defaults:
110     """Class to store default values for fields."""
111     memory = 256
112     disk = 4.0
113     cdrom = ''
114     autoinstall = ''
115     name = ''
116     description = ''
117     type = 'linux-hvm'
118
119     def __init__(self, max_memory=None, max_disk=None, **kws):
120         if max_memory is not None:
121             self.memory = min(self.memory, max_memory)
122         if max_disk is not None:
123             self.max_disk = min(self.disk, max_disk)
124         for key in kws:
125             setattr(self, key, kws[key])
126
127
128
129 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
130
131 def invalidInput(op, username, fields, err, emsg):
132     """Print an error page when an InvalidInput exception occurs"""
133     d = dict(op=op, user=username, err_field=err.err_field,
134              err_value=str(err.err_value), stderr=emsg,
135              errorMessage=str(err))
136     return templates.invalid(searchList=[d])
137
138 def hasVnc(status):
139     """Does the machine with a given status list support VNC?"""
140     if status is None:
141         return False
142     for l in status:
143         if l[0] == 'device' and l[1][0] == 'vfb':
144             d = dict(l[1][1:])
145             return 'location' in d
146     return False
147
148 def parseCreate(username, state, fields):
149     kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
150     validate = validation.Validate(username, state, strict=True, **kws)
151     return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
152                 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
153                 cdrom=getattr(validate, 'cdrom', None),
154                 autoinstall=getattr(validate, 'autoinstall', None))
155
156 def create(username, state, path, fields):
157     """Handler for create requests."""
158     try:
159         parsed_fields = parseCreate(username, state, fields)
160         machine = controls.createVm(username, state, **parsed_fields)
161     except InvalidInput, err:
162         pass
163     else:
164         err = None
165     state.clear() #Changed global state
166     d = getListDict(username, state)
167     d['err'] = err
168     if err:
169         for field in fields.keys():
170             setattr(d['defaults'], field, fields.getfirst(field))
171     else:
172         d['new_machine'] = parsed_fields['name']
173     return templates.list(searchList=[d])
174
175
176 def getListDict(username, state):
177     """Gets the list of local variables used by list.tmpl."""
178     checkpoint.checkpoint('Starting')
179     machines = state.machines
180     checkpoint.checkpoint('Got my machines')
181     on = {}
182     has_vnc = {}
183     xmlist = state.xmlist
184     checkpoint.checkpoint('Got uptimes')
185     can_clone = 'ice3' not in state.xmlist_raw
186     for m in machines:
187         if m not in xmlist:
188             has_vnc[m] = 'Off'
189             m.uptime = None
190         else:
191             m.uptime = xmlist[m]['uptime']
192             if xmlist[m]['console']:
193                 has_vnc[m] = True
194             elif m.type.hvm:
195                 has_vnc[m] = "WTF?"
196             else:
197                 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
198     max_memory = validation.maxMemory(username, state)
199     max_disk = validation.maxDisk(username)
200     checkpoint.checkpoint('Got max mem/disk')
201     defaults = Defaults(max_memory=max_memory,
202                         max_disk=max_disk,
203                         owner=username,
204                         cdrom='gutsy-i386')
205     checkpoint.checkpoint('Got defaults')
206     def sortkey(machine):
207         return (machine.owner != username, machine.owner, machine.name)
208     machines = sorted(machines, key=sortkey)
209     d = dict(user=username,
210              cant_add_vm=validation.cantAddVm(username, state),
211              max_memory=max_memory,
212              max_disk=max_disk,
213              defaults=defaults,
214              machines=machines,
215              has_vnc=has_vnc,
216              can_clone=can_clone)
217     return d
218
219 def listVms(username, state, path, fields):
220     """Handler for list requests."""
221     checkpoint.checkpoint('Getting list dict')
222     d = getListDict(username, state)
223     checkpoint.checkpoint('Got list dict')
224     return templates.list(searchList=[d])
225
226 def vnc(username, state, path, fields):
227     """VNC applet page.
228
229     Note that due to same-domain restrictions, the applet connects to
230     the webserver, which needs to forward those requests to the xen
231     server.  The Xen server runs another proxy that (1) authenticates
232     and (2) finds the correct port for the VM.
233
234     You might want iptables like:
235
236     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
237       --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
238     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
239       --dport 10003 -j SNAT --to-source 18.187.7.142
240     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
241       --dport 10003 -j ACCEPT
242
243     Remember to enable iptables!
244     echo 1 > /proc/sys/net/ipv4/ip_forward
245     """
246     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
247
248     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
249
250     data = {}
251     data["user"] = username
252     data["machine"] = machine.name
253     data["expires"] = time.time()+(5*60)
254     pickled_data = cPickle.dumps(data)
255     m = hmac.new(TOKEN_KEY, digestmod=sha)
256     m.update(pickled_data)
257     token = {'data': pickled_data, 'digest': m.digest()}
258     token = cPickle.dumps(token)
259     token = base64.urlsafe_b64encode(token)
260     host = controls.listHost(machine)
261     if host:
262         port = 10003 + [h.hostname for h in config.hosts].index(host)
263     else:
264         port = 5900 # dummy
265
266     status = controls.statusInfo(machine)
267     has_vnc = hasVnc(status)
268
269     d = dict(user=username,
270              on=status,
271              has_vnc=has_vnc,
272              machine=machine,
273              hostname=state.environ.get('SERVER_NAME', 'localhost'),
274              port=port,
275              authtoken=token)
276     return templates.vnc(searchList=[d])
277
278 def getHostname(nic):
279     """Find the hostname associated with a NIC.
280
281     XXX this should be merged with the similar logic in DNS and DHCP.
282     """
283     if nic.hostname and '.' in nic.hostname:
284         return nic.hostname
285     elif nic.machine:
286         return nic.machine.name + '.' + config.dns.domains[0]
287     else:
288         return None
289
290
291 def getNicInfo(data_dict, machine):
292     """Helper function for info, get data on nics for a machine.
293
294     Modifies data_dict to include the relevant data, and returns a list
295     of (key, name) pairs to display "name: data_dict[key]" to the user.
296     """
297     data_dict['num_nics'] = len(machine.nics)
298     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
299                            ('nic%s_mac', 'NIC %s MAC Addr'),
300                            ('nic%s_ip', 'NIC %s IP'),
301                            ]
302     nic_fields = []
303     for i in range(len(machine.nics)):
304         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
305         if not i:
306             data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
307         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
308         data_dict['nic%s_ip' % i] = machine.nics[i].ip
309     if len(machine.nics) == 1:
310         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
311     return nic_fields
312
313 def getDiskInfo(data_dict, machine):
314     """Helper function for info, get data on disks for a machine.
315
316     Modifies data_dict to include the relevant data, and returns a list
317     of (key, name) pairs to display "name: data_dict[key]" to the user.
318     """
319     data_dict['num_disks'] = len(machine.disks)
320     disk_fields_template = [('%s_size', '%s size')]
321     disk_fields = []
322     for disk in machine.disks:
323         name = disk.guest_device_name
324         disk_fields.extend([(x % name, y % name) for x, y in
325                             disk_fields_template])
326         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
327     return disk_fields
328
329 def command(username, state, path, fields):
330     """Handler for running commands like boot and delete on a VM."""
331     back = fields.getfirst('back')
332     try:
333         d = controls.commandResult(username, state, fields)
334         if d['command'] == 'Delete VM':
335             back = 'list'
336     except InvalidInput, err:
337         if not back:
338             raise
339         print >> sys.stderr, err
340         result = err
341     else:
342         result = 'Success!'
343         if not back:
344             return templates.command(searchList=[d])
345     if back == 'list':
346         state.clear() #Changed global state
347         d = getListDict(username, state)
348         d['result'] = result
349         return templates.list(searchList=[d])
350     elif back == 'info':
351         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
352         return ({'Status': '303 See Other',
353                  'Location': 'info?machine_id=%d' % machine.machine_id},
354                 "You shouldn't see this message.")
355     else:
356         raise InvalidInput('back', back, 'Not a known back page.')
357
358 def modifyDict(username, state, fields):
359     """Modify a machine as specified by CGI arguments.
360
361     Return a list of local variables for modify.tmpl.
362     """
363     olddisk = {}
364     session.begin()
365     try:
366         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
367         validate = validation.Validate(username, state, **kws)
368         machine = validate.machine
369         oldname = machine.name
370
371         if hasattr(validate, 'memory'):
372             machine.memory = validate.memory
373
374         if hasattr(validate, 'vmtype'):
375             machine.type = validate.vmtype
376
377         if hasattr(validate, 'disksize'):
378             disksize = validate.disksize
379             disk = machine.disks[0]
380             if disk.size != disksize:
381                 olddisk[disk.guest_device_name] = disksize
382                 disk.size = disksize
383                 session.save_or_update(disk)
384
385         update_acl = False
386         if hasattr(validate, 'owner') and validate.owner != machine.owner:
387             machine.owner = validate.owner
388             update_acl = True
389         if hasattr(validate, 'name'):
390             machine.name = validate.name
391         if hasattr(validate, 'description'):
392             machine.description = validate.description
393         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
394             machine.administrator = validate.admin
395             update_acl = True
396         if hasattr(validate, 'contact'):
397             machine.contact = validate.contact
398
399         session.save_or_update(machine)
400         if update_acl:
401             print >> sys.stderr, machine, machine.administrator
402             cache_acls.refreshMachine(machine)
403         session.commit()
404     except:
405         session.rollback()
406         raise
407     for diskname in olddisk:
408         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
409     if hasattr(validate, 'name'):
410         controls.renameMachine(machine, oldname, validate.name)
411     return dict(user=username,
412                 command="modify",
413                 machine=machine)
414
415 def modify(username, state, path, fields):
416     """Handler for modifying attributes of a machine."""
417     try:
418         modify_dict = modifyDict(username, state, fields)
419     except InvalidInput, err:
420         result = None
421         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
422     else:
423         machine = modify_dict['machine']
424         result = 'Success!'
425         err = None
426     info_dict = infoDict(username, state, machine)
427     info_dict['err'] = err
428     if err:
429         for field in fields.keys():
430             setattr(info_dict['defaults'], field, fields.getfirst(field))
431     info_dict['result'] = result
432     return templates.info(searchList=[info_dict])
433
434
435 def helpHandler(username, state, path, fields):
436     """Handler for help messages."""
437     simple = fields.getfirst('simple')
438     subjects = fields.getlist('subject')
439
440     help_mapping = {'ParaVM Console': """
441 ParaVM machines do not support local console access over VNC.  To
442 access the serial console of these machines, you can SSH with Kerberos
443 to console.%s, using the name of the machine as your
444 username.""" % config.dns.domains[0],
445                     'HVM/ParaVM': """
446 HVM machines use the virtualization features of the processor, while
447 ParaVM machines use Xen's emulation of virtualization features.  You
448 want an HVM virtualized machine.""",
449                     'CPU Weight': """
450 Don't ask us!  We're as mystified as you are.""",
451                     'Owner': """
452 The owner field is used to determine <a
453 href="help?subject=Quotas">quotas</a>.  It must be the name of a
454 locker that you are an AFS administrator of.  In particular, you or an
455 AFS group you are a member of must have AFS rlidwka bits on the
456 locker.  You can check who administers the LOCKER locker using the
457 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
458 href="help?subject=Administrator">administrator</a>.""",
459                     'Administrator': """
460 The administrator field determines who can access the console and
461 power on and off the machine.  This can be either a user or a moira
462 group.""",
463                     'Quotas': """
464 Quotas are determined on a per-locker basis.  Each locker may have a
465 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
466 active machines.""",
467                     'Console': """
468 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
469 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
470 your machine will run just fine, but the applet's display of the
471 console will suffer artifacts.
472 """,
473                     'Windows': """
474 <strong>Windows Vista:</strong> The Vista image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-email.txt">the licensing confirmation e-mail</a> for details. The installer req     uires 512 MB RAM and at least 7.5 GB disk space (15 GB or more recommended).<br>
475 <strong>Windows XP:</strong> 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.
476 """
477                     }
478
479     if not subjects:
480         subjects = sorted(help_mapping.keys())
481
482     d = dict(user=username,
483              simple=simple,
484              subjects=subjects,
485              mapping=help_mapping)
486
487     return templates.help(searchList=[d])
488
489
490 def badOperation(u, s, p, e):
491     """Function called when accessing an unknown URI."""
492     return ({'Status': '404 Not Found'}, 'Invalid operation.')
493
494 def infoDict(username, state, machine):
495     """Get the variables used by info.tmpl."""
496     status = controls.statusInfo(machine)
497     checkpoint.checkpoint('Getting status info')
498     has_vnc = hasVnc(status)
499     if status is None:
500         main_status = dict(name=machine.name,
501                            memory=str(machine.memory))
502         uptime = None
503         cputime = None
504     else:
505         main_status = dict(status[1:])
506         main_status['host'] = controls.listHost(machine)
507         start_time = float(main_status.get('start_time', 0))
508         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
509         cpu_time_float = float(main_status.get('cpu_time', 0))
510         cputime = datetime.timedelta(seconds=int(cpu_time_float))
511     checkpoint.checkpoint('Status')
512     display_fields = """name uptime memory state cpu_weight on_reboot 
513      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
514     display_fields = [('name', 'Name'),
515                       ('description', 'Description'),
516                       ('owner', 'Owner'),
517                       ('administrator', 'Administrator'),
518                       ('contact', 'Contact'),
519                       ('type', 'Type'),
520                       'NIC_INFO',
521                       ('uptime', 'uptime'),
522                       ('cputime', 'CPU usage'),
523                       ('host', 'Hosted on'),
524                       ('memory', 'RAM'),
525                       'DISK_INFO',
526                       ('state', 'state (xen format)'),
527                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
528                       ('on_reboot', 'Action on VM reboot'),
529                       ('on_poweroff', 'Action on VM poweroff'),
530                       ('on_crash', 'Action on VM crash'),
531                       ('on_xend_start', 'Action on Xen start'),
532                       ('on_xend_stop', 'Action on Xen stop'),
533                       ('bootloader', 'Bootloader options'),
534                       ]
535     fields = []
536     machine_info = {}
537     machine_info['name'] = machine.name
538     machine_info['description'] = machine.description
539     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
540     machine_info['owner'] = machine.owner
541     machine_info['administrator'] = machine.administrator
542     machine_info['contact'] = machine.contact
543
544     nic_fields = getNicInfo(machine_info, machine)
545     nic_point = display_fields.index('NIC_INFO')
546     display_fields = (display_fields[:nic_point] + nic_fields +
547                       display_fields[nic_point+1:])
548
549     disk_fields = getDiskInfo(machine_info, machine)
550     disk_point = display_fields.index('DISK_INFO')
551     display_fields = (display_fields[:disk_point] + disk_fields +
552                       display_fields[disk_point+1:])
553
554     main_status['memory'] += ' MiB'
555     for field, disp in display_fields:
556         if field in ('uptime', 'cputime') and locals()[field] is not None:
557             fields.append((disp, locals()[field]))
558         elif field in machine_info:
559             fields.append((disp, machine_info[field]))
560         elif field in main_status:
561             fields.append((disp, main_status[field]))
562         else:
563             pass
564             #fields.append((disp, None))
565
566     checkpoint.checkpoint('Got fields')
567
568
569     max_mem = validation.maxMemory(machine.owner, state, machine, False)
570     checkpoint.checkpoint('Got mem')
571     max_disk = validation.maxDisk(machine.owner, machine)
572     defaults = Defaults()
573     for name in 'machine_id name description administrator owner memory contact'.split():
574         setattr(defaults, name, getattr(machine, name))
575     defaults.type = machine.type.type_id
576     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
577     checkpoint.checkpoint('Got defaults')
578     d = dict(user=username,
579              on=status is not None,
580              machine=machine,
581              defaults=defaults,
582              has_vnc=has_vnc,
583              uptime=str(uptime),
584              ram=machine.memory,
585              max_mem=max_mem,
586              max_disk=max_disk,
587              owner_help=helppopup("Owner"),
588              fields = fields)
589     return d
590
591 def info(username, state, path, fields):
592     """Handler for info on a single VM."""
593     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
594     d = infoDict(username, state, machine)
595     checkpoint.checkpoint('Got infodict')
596     return templates.info(searchList=[d])
597
598 def unauthFront(_, _2, _3, fields):
599     """Information for unauth'd users."""
600     return templates.unauth(searchList=[{'simple' : True}])
601
602 def admin(username, state, path, fields):
603     if path == '':
604         return ({'Status': '303 See Other',
605                  'Location': 'admin/'},
606                 "You shouldn't see this message.")
607     if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
608         raise InvalidInput('username', username,
609                            'Not in admin group %s.' % config.web.adminacl)
610     newstate = State(username, isadmin=True)
611     newstate.environ = state.environ
612     return handler(username, newstate, path, fields)
613
614 def throwError(_, __, ___, ____):
615     """Throw an error, to test the error-tracing mechanisms."""
616     raise RuntimeError("test of the emergency broadcast system")
617
618 mapping = dict(list=listVms,
619                vnc=vnc,
620                command=command,
621                modify=modify,
622                info=info,
623                create=create,
624                help=helpHandler,
625                unauth=unauthFront,
626                admin=admin,
627                overlord=admin,
628                errortest=throwError)
629
630 def printHeaders(headers):
631     """Print a dictionary as HTTP headers."""
632     for key, value in headers.iteritems():
633         print '%s: %s' % (key, value)
634     print
635
636 def send_error_mail(subject, body):
637     import subprocess
638
639     to = config.web.errormail
640     mail = """To: %s
641 From: root@%s
642 Subject: %s
643
644 %s
645 """ % (to, config.web.hostname, subject, body)
646     p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
647     p.stdin.write(mail)
648     p.stdin.close()
649     p.wait()
650
651 def show_error(op, username, fields, err, emsg, traceback):
652     """Print an error page when an exception occurs"""
653     d = dict(op=op, user=username, fields=fields,
654              errorMessage=str(err), stderr=emsg, traceback=traceback)
655     details = templates.error_raw(searchList=[d])
656     exclude = config.web.errormail_exclude
657     if username not in exclude and '*' not in exclude:
658         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
659                         details)
660     d['details'] = details
661     return templates.error(searchList=[d])
662
663 def getUser(environ):
664     """Return the current user based on the SSL environment variables"""
665     return environ.get('REMOTE_USER', None)
666
667 def handler(username, state, path, fields):
668     operation, path = pathSplit(path)
669     if not operation:
670         operation = 'list'
671     print 'Starting', operation
672     fun = mapping.get(operation, badOperation)
673     return fun(username, state, path, fields)
674
675 class App:
676     def __init__(self, environ, start_response):
677         self.environ = environ
678         self.start = start_response
679
680         self.username = getUser(environ)
681         self.state = State(self.username)
682         self.state.environ = environ
683
684         random.seed() #sigh
685
686     def __iter__(self):
687         start_time = time.time()
688         database.clear_cache()
689         sys.stderr = StringIO()
690         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
691         operation = self.environ.get('PATH_INFO', '')
692         if not operation:
693             self.start("301 Moved Permanently", [('Location', './')])
694             return
695         if self.username is None:
696             operation = 'unauth'
697
698         try:
699             checkpoint.checkpoint('Before')
700             output = handler(self.username, self.state, operation, fields)
701             checkpoint.checkpoint('After')
702
703             headers = dict(DEFAULT_HEADERS)
704             if isinstance(output, tuple):
705                 new_headers, output = output
706                 headers.update(new_headers)
707             e = revertStandardError()
708             if e:
709                 if hasattr(output, 'addError'):
710                     output.addError(e)
711                 else:
712                     # This only happens on redirects, so it'd be a pain to get
713                     # the message to the user.  Maybe in the response is useful.
714                     output = output + '\n\nstderr:\n' + e
715             output_string =  str(output)
716             checkpoint.checkpoint('output as a string')
717         except Exception, err:
718             if not fields.has_key('js'):
719                 if isinstance(err, InvalidInput):
720                     self.start('200 OK', [('Content-Type', 'text/html')])
721                     e = revertStandardError()
722                     yield str(invalidInput(operation, self.username, fields,
723                                            err, e))
724                     return
725             import traceback
726             self.start('500 Internal Server Error',
727                        [('Content-Type', 'text/html')])
728             e = revertStandardError()
729             s = show_error(operation, self.username, fields,
730                            err, e, traceback.format_exc())
731             yield str(s)
732             return
733         status = headers.setdefault('Status', '200 OK')
734         del headers['Status']
735         self.start(status, headers.items())
736         yield output_string
737         if fields.has_key('timedebug'):
738             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
739
740 def constructor():
741     connect()
742     return App
743
744 def main():
745     from flup.server.fcgi_fork import WSGIServer
746     WSGIServer(constructor()).run()
747
748 if __name__ == '__main__':
749     main()