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