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