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