add wiki and console-server links to ParaVM/HVM help text
[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.max_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                         cdrom='gutsy-i386')
206     checkpoint.checkpoint('Got defaults')
207     def sortkey(machine):
208         return (machine.owner != username, machine.owner, machine.name)
209     machines = sorted(machines, key=sortkey)
210     d = dict(user=username,
211              cant_add_vm=validation.cantAddVm(username, state),
212              max_memory=max_memory,
213              max_disk=max_disk,
214              defaults=defaults,
215              machines=machines,
216              has_vnc=has_vnc,
217              can_clone=can_clone)
218     return d
219
220 def listVms(username, state, path, fields):
221     """Handler for list requests."""
222     checkpoint.checkpoint('Getting list dict')
223     d = getListDict(username, state)
224     checkpoint.checkpoint('Got list dict')
225     return templates.list(searchList=[d])
226
227 def vnc(username, state, path, fields):
228     """VNC applet page.
229
230     Note that due to same-domain restrictions, the applet connects to
231     the webserver, which needs to forward those requests to the xen
232     server.  The Xen server runs another proxy that (1) authenticates
233     and (2) finds the correct port for the VM.
234
235     You might want iptables like:
236
237     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
238       --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
239     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
240       --dport 10003 -j SNAT --to-source 18.187.7.142
241     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
242       --dport 10003 -j ACCEPT
243
244     Remember to enable iptables!
245     echo 1 > /proc/sys/net/ipv4/ip_forward
246     """
247     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
248
249     token = controls.vnctoken(machine)
250     host = controls.listHost(machine)
251     if host:
252         port = 10003 + [h.hostname for h in config.hosts].index(host)
253     else:
254         port = 5900 # dummy
255
256     status = controls.statusInfo(machine)
257     has_vnc = hasVnc(status)
258
259     d = dict(user=username,
260              on=status,
261              has_vnc=has_vnc,
262              machine=machine,
263              hostname=state.environ.get('SERVER_NAME', 'localhost'),
264              port=port,
265              authtoken=token)
266     return templates.vnc(searchList=[d])
267
268 def getHostname(nic):
269     """Find the hostname associated with a NIC.
270
271     XXX this should be merged with the similar logic in DNS and DHCP.
272     """
273     if nic.hostname and '.' in nic.hostname:
274         return nic.hostname
275     elif nic.machine:
276         return nic.machine.name + '.' + config.dns.domains[0]
277     else:
278         return None
279
280
281 def getNicInfo(data_dict, machine):
282     """Helper function for info, get data on nics for a machine.
283
284     Modifies data_dict to include the relevant data, and returns a list
285     of (key, name) pairs to display "name: data_dict[key]" to the user.
286     """
287     data_dict['num_nics'] = len(machine.nics)
288     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
289                            ('nic%s_mac', 'NIC %s MAC Addr'),
290                            ('nic%s_ip', 'NIC %s IP'),
291                            ]
292     nic_fields = []
293     for i in range(len(machine.nics)):
294         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
295         if not i:
296             data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
297         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
298         data_dict['nic%s_ip' % i] = machine.nics[i].ip
299     if len(machine.nics) == 1:
300         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
301     return nic_fields
302
303 def getDiskInfo(data_dict, machine):
304     """Helper function for info, get data on disks for a machine.
305
306     Modifies data_dict to include the relevant data, and returns a list
307     of (key, name) pairs to display "name: data_dict[key]" to the user.
308     """
309     data_dict['num_disks'] = len(machine.disks)
310     disk_fields_template = [('%s_size', '%s size')]
311     disk_fields = []
312     for disk in machine.disks:
313         name = disk.guest_device_name
314         disk_fields.extend([(x % name, y % name) for x, y in
315                             disk_fields_template])
316         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
317     return disk_fields
318
319 def command(username, state, path, fields):
320     """Handler for running commands like boot and delete on a VM."""
321     back = fields.getfirst('back')
322     try:
323         d = controls.commandResult(username, state, fields)
324         if d['command'] == 'Delete VM':
325             back = 'list'
326     except InvalidInput, err:
327         if not back:
328             raise
329         print >> sys.stderr, err
330         result = err
331     else:
332         result = 'Success!'
333         if not back:
334             return templates.command(searchList=[d])
335     if back == 'list':
336         state.clear() #Changed global state
337         d = getListDict(username, state)
338         d['result'] = result
339         return templates.list(searchList=[d])
340     elif back == 'info':
341         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
342         return ({'Status': '303 See Other',
343                  'Location': 'info?machine_id=%d' % machine.machine_id},
344                 "You shouldn't see this message.")
345     else:
346         raise InvalidInput('back', back, 'Not a known back page.')
347
348 def modifyDict(username, state, fields):
349     """Modify a machine as specified by CGI arguments.
350
351     Return a list of local variables for modify.tmpl.
352     """
353     olddisk = {}
354     session.begin()
355     try:
356         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
357         validate = validation.Validate(username, state, **kws)
358         machine = validate.machine
359         oldname = machine.name
360
361         if hasattr(validate, 'memory'):
362             machine.memory = validate.memory
363
364         if hasattr(validate, 'vmtype'):
365             machine.type = validate.vmtype
366
367         if hasattr(validate, 'disksize'):
368             disksize = validate.disksize
369             disk = machine.disks[0]
370             if disk.size != disksize:
371                 olddisk[disk.guest_device_name] = disksize
372                 disk.size = disksize
373                 session.save_or_update(disk)
374
375         update_acl = False
376         if hasattr(validate, 'owner') and validate.owner != machine.owner:
377             machine.owner = validate.owner
378             update_acl = True
379         if hasattr(validate, 'name'):
380             machine.name = validate.name
381         if hasattr(validate, 'description'):
382             machine.description = validate.description
383         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
384             machine.administrator = validate.admin
385             update_acl = True
386         if hasattr(validate, 'contact'):
387             machine.contact = validate.contact
388
389         session.save_or_update(machine)
390         if update_acl:
391             cache_acls.refreshMachine(machine)
392         session.commit()
393     except:
394         session.rollback()
395         raise
396     for diskname in olddisk:
397         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
398     if hasattr(validate, 'name'):
399         controls.renameMachine(machine, oldname, validate.name)
400     return dict(user=username,
401                 command="modify",
402                 machine=machine)
403
404 def modify(username, state, path, fields):
405     """Handler for modifying attributes of a machine."""
406     try:
407         modify_dict = modifyDict(username, state, fields)
408     except InvalidInput, err:
409         result = None
410         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
411     else:
412         machine = modify_dict['machine']
413         result = 'Success!'
414         err = None
415     info_dict = infoDict(username, state, machine)
416     info_dict['err'] = err
417     if err:
418         for field in fields.keys():
419             setattr(info_dict['defaults'], field, fields.getfirst(field))
420     info_dict['result'] = result
421     return templates.info(searchList=[info_dict])
422
423
424 def helpHandler(username, state, path, fields):
425     """Handler for help messages."""
426     simple = fields.getfirst('simple')
427     subjects = fields.getlist('subject')
428
429     help_mapping = {
430                     'Autoinstalls': """
431 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
432 ParaVM.  You can access the resulting system by logging into the <a
433 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
434 with your Kerberos tickets; there is no root password so sshd will
435 refuse login.</p>
436
437 <p>Under the covers, the autoinstaller uses our own patched version of
438 xen-create-image, which is a tool based on debootstrap.  If you log
439 into the serial console while the install is running, you can watch
440 it.
441 """,
442                     'ParaVM Console': """
443 ParaVM machines do not support local console access over VNC.  To
444 access the serial console of these machines, you can SSH with Kerberos
445 to %s, using the name of the machine as your
446 username.""" % config.console.hostname,
447                     'HVM/ParaVM': """
448 HVM machines use the virtualization features of the processor, while
449 ParaVM machines rely on a modified kernel to communicate directly with
450 the hypervisor.  HVMs support boot CDs of any operating system, and
451 the VNC console applet.  The three-minute autoinstaller produces
452 ParaVMs.  ParaVMs typically are more efficient, and always support the
453 <a href="help?subject=ParaVM+Console">console server</a>.</p>
454
455 <p>More details are <a
456 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
457 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
458 (which you can skip by using the autoinstaller to begin with.)</p>
459
460 <p>We recommend using a ParaVM when possible and an HVM when necessary.
461 """,
462                     'CPU Weight': """
463 Don't ask us!  We're as mystified as you are.""",
464                     'Owner': """
465 The owner field is used to determine <a
466 href="help?subject=Quotas">quotas</a>.  It must be the name of a
467 locker that you are an AFS administrator of.  In particular, you or an
468 AFS group you are a member of must have AFS rlidwka bits on the
469 locker.  You can check who administers the LOCKER locker using the
470 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
471 href="help?subject=Administrator">administrator</a>.""",
472                     'Administrator': """
473 The administrator field determines who can access the console and
474 power on and off the machine.  This can be either a user or a moira
475 group.""",
476                     'Quotas': """
477 Quotas are determined on a per-locker basis.  Each locker may have a
478 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
479 active machines.""",
480                     'Console': """
481 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
482 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
483 your machine will run just fine, but the applet's display of the
484 console will suffer artifacts.
485 """,
486                     'Windows': """
487 <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>
488 <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.
489 """
490                     }
491
492     if not subjects:
493         subjects = sorted(help_mapping.keys())
494
495     d = dict(user=username,
496              simple=simple,
497              subjects=subjects,
498              mapping=help_mapping)
499
500     return templates.help(searchList=[d])
501
502
503 def badOperation(u, s, p, e):
504     """Function called when accessing an unknown URI."""
505     return ({'Status': '404 Not Found'}, 'Invalid operation.')
506
507 def infoDict(username, state, machine):
508     """Get the variables used by info.tmpl."""
509     status = controls.statusInfo(machine)
510     checkpoint.checkpoint('Getting status info')
511     has_vnc = hasVnc(status)
512     if status is None:
513         main_status = dict(name=machine.name,
514                            memory=str(machine.memory))
515         uptime = None
516         cputime = None
517     else:
518         main_status = dict(status[1:])
519         main_status['host'] = controls.listHost(machine)
520         start_time = float(main_status.get('start_time', 0))
521         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
522         cpu_time_float = float(main_status.get('cpu_time', 0))
523         cputime = datetime.timedelta(seconds=int(cpu_time_float))
524     checkpoint.checkpoint('Status')
525     display_fields = """name uptime memory state cpu_weight on_reboot 
526      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
527     display_fields = [('name', 'Name'),
528                       ('description', 'Description'),
529                       ('owner', 'Owner'),
530                       ('administrator', 'Administrator'),
531                       ('contact', 'Contact'),
532                       ('type', 'Type'),
533                       'NIC_INFO',
534                       ('uptime', 'uptime'),
535                       ('cputime', 'CPU usage'),
536                       ('host', 'Hosted on'),
537                       ('memory', 'RAM'),
538                       'DISK_INFO',
539                       ('state', 'state (xen format)'),
540                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
541                       ('on_reboot', 'Action on VM reboot'),
542                       ('on_poweroff', 'Action on VM poweroff'),
543                       ('on_crash', 'Action on VM crash'),
544                       ('on_xend_start', 'Action on Xen start'),
545                       ('on_xend_stop', 'Action on Xen stop'),
546                       ('bootloader', 'Bootloader options'),
547                       ]
548     fields = []
549     machine_info = {}
550     machine_info['name'] = machine.name
551     machine_info['description'] = machine.description
552     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
553     machine_info['owner'] = machine.owner
554     machine_info['administrator'] = machine.administrator
555     machine_info['contact'] = machine.contact
556
557     nic_fields = getNicInfo(machine_info, machine)
558     nic_point = display_fields.index('NIC_INFO')
559     display_fields = (display_fields[:nic_point] + nic_fields +
560                       display_fields[nic_point+1:])
561
562     disk_fields = getDiskInfo(machine_info, machine)
563     disk_point = display_fields.index('DISK_INFO')
564     display_fields = (display_fields[:disk_point] + disk_fields +
565                       display_fields[disk_point+1:])
566
567     main_status['memory'] += ' MiB'
568     for field, disp in display_fields:
569         if field in ('uptime', 'cputime') and locals()[field] is not None:
570             fields.append((disp, locals()[field]))
571         elif field in machine_info:
572             fields.append((disp, machine_info[field]))
573         elif field in main_status:
574             fields.append((disp, main_status[field]))
575         else:
576             pass
577             #fields.append((disp, None))
578
579     checkpoint.checkpoint('Got fields')
580
581
582     max_mem = validation.maxMemory(machine.owner, state, machine, False)
583     checkpoint.checkpoint('Got mem')
584     max_disk = validation.maxDisk(machine.owner, machine)
585     defaults = Defaults()
586     for name in 'machine_id name description administrator owner memory contact'.split():
587         setattr(defaults, name, getattr(machine, name))
588     defaults.type = machine.type.type_id
589     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
590     checkpoint.checkpoint('Got defaults')
591     d = dict(user=username,
592              on=status is not None,
593              machine=machine,
594              defaults=defaults,
595              has_vnc=has_vnc,
596              uptime=str(uptime),
597              ram=machine.memory,
598              max_mem=max_mem,
599              max_disk=max_disk,
600              owner_help=helppopup("Owner"),
601              fields = fields)
602     return d
603
604 def info(username, state, path, fields):
605     """Handler for info on a single VM."""
606     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
607     d = infoDict(username, state, machine)
608     checkpoint.checkpoint('Got infodict')
609     return templates.info(searchList=[d])
610
611 def unauthFront(_, _2, _3, fields):
612     """Information for unauth'd users."""
613     return templates.unauth(searchList=[{'simple' : True}])
614
615 def admin(username, state, path, fields):
616     if path == '':
617         return ({'Status': '303 See Other',
618                  'Location': 'admin/'},
619                 "You shouldn't see this message.")
620     if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
621         raise InvalidInput('username', username,
622                            'Not in admin group %s.' % config.web.adminacl)
623     newstate = State(username, isadmin=True)
624     newstate.environ = state.environ
625     return handler(username, newstate, path, fields)
626
627 def throwError(_, __, ___, ____):
628     """Throw an error, to test the error-tracing mechanisms."""
629     raise RuntimeError("test of the emergency broadcast system")
630
631 mapping = dict(list=listVms,
632                vnc=vnc,
633                command=command,
634                modify=modify,
635                info=info,
636                create=create,
637                help=helpHandler,
638                unauth=unauthFront,
639                admin=admin,
640                overlord=admin,
641                errortest=throwError)
642
643 def printHeaders(headers):
644     """Print a dictionary as HTTP headers."""
645     for key, value in headers.iteritems():
646         print '%s: %s' % (key, value)
647     print
648
649 def send_error_mail(subject, body):
650     import subprocess
651
652     to = config.web.errormail
653     mail = """To: %s
654 From: root@%s
655 Subject: %s
656
657 %s
658 """ % (to, config.web.hostname, subject, body)
659     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
660                          stdin=subprocess.PIPE)
661     p.stdin.write(mail)
662     p.stdin.close()
663     p.wait()
664
665 def show_error(op, username, fields, err, emsg, traceback):
666     """Print an error page when an exception occurs"""
667     d = dict(op=op, user=username, fields=fields,
668              errorMessage=str(err), stderr=emsg, traceback=traceback)
669     details = templates.error_raw(searchList=[d])
670     exclude = config.web.errormail_exclude
671     if username not in exclude and '*' not in exclude:
672         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
673                         details)
674     d['details'] = details
675     return templates.error(searchList=[d])
676
677 def getUser(environ):
678     """Return the current user based on the SSL environment variables"""
679     user = environ.get('REMOTE_USER')
680     if user is None:
681         return
682     
683     if environ.get('AUTH_TYPE') == 'Negotiate':
684         # Convert the krb5 principal into a krb4 username
685         if not user.endswith('@%s' % config.authn[0].realm):
686             return
687         else:
688             return user.split('@')[0].replace('/', '.')
689     else:
690         return user
691
692 def handler(username, state, path, fields):
693     operation, path = pathSplit(path)
694     if not operation:
695         operation = 'list'
696     print 'Starting', operation
697     fun = mapping.get(operation, badOperation)
698     return fun(username, state, path, fields)
699
700 class App:
701     def __init__(self, environ, start_response):
702         self.environ = environ
703         self.start = start_response
704
705         self.username = getUser(environ)
706         self.state = State(self.username)
707         self.state.environ = environ
708
709         random.seed() #sigh
710
711     def __iter__(self):
712         start_time = time.time()
713         database.clear_cache()
714         sys.stderr = StringIO()
715         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
716         operation = self.environ.get('PATH_INFO', '')
717         if not operation:
718             self.start("301 Moved Permanently", [('Location', './')])
719             return
720         if self.username is None:
721             operation = 'unauth'
722
723         try:
724             checkpoint.checkpoint('Before')
725             output = handler(self.username, self.state, operation, fields)
726             checkpoint.checkpoint('After')
727
728             headers = dict(DEFAULT_HEADERS)
729             if isinstance(output, tuple):
730                 new_headers, output = output
731                 headers.update(new_headers)
732             e = revertStandardError()
733             if e:
734                 if hasattr(output, 'addError'):
735                     output.addError(e)
736                 else:
737                     # This only happens on redirects, so it'd be a pain to get
738                     # the message to the user.  Maybe in the response is useful.
739                     output = output + '\n\nstderr:\n' + e
740             output_string =  str(output)
741             checkpoint.checkpoint('output as a string')
742         except Exception, err:
743             if not fields.has_key('js'):
744                 if isinstance(err, InvalidInput):
745                     self.start('200 OK', [('Content-Type', 'text/html')])
746                     e = revertStandardError()
747                     yield str(invalidInput(operation, self.username, fields,
748                                            err, e))
749                     return
750             import traceback
751             self.start('500 Internal Server Error',
752                        [('Content-Type', 'text/html')])
753             e = revertStandardError()
754             s = show_error(operation, self.username, fields,
755                            err, e, traceback.format_exc())
756             yield str(s)
757             return
758         status = headers.setdefault('Status', '200 OK')
759         del headers['Status']
760         self.start(status, headers.items())
761         yield output_string
762         if fields.has_key('timedebug'):
763             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
764
765 def constructor():
766     connect()
767     return App
768
769 def main():
770     from flup.server.fcgi_fork import WSGIServer
771     WSGIServer(constructor()).run()
772
773 if __name__ == '__main__':
774     main()