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