8de8a371f24bae34919bade85b9fe1ab0571c3cb
[invirt/packages/invirt-web.git] / templates / main.py
1 #!/usr/bin/python
2
3 import sys
4 import cgi
5 import os
6 import string
7 import subprocess
8 import re
9 import time
10 import cPickle
11 import base64
12 import sha
13 import hmac
14 import datetime
15
16 sys.stderr = sys.stdout
17 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
18
19 from Cheetah.Template import Template
20 from sipb_xen_database import *
21 import random
22
23 class MyException(Exception):
24     """Base class for my exceptions"""
25     pass
26
27 class InvalidInput(MyException):
28     """Exception for user-provided input is invalid but maybe in good faith.
29
30     This would include setting memory to negative (which might be a
31     typo) but not setting an invalid boot CD (which requires bypassing
32     the select box).
33     """
34     pass
35
36 class CodeError(MyException):
37     """Exception for internal errors or bad faith input."""
38     pass
39
40 class Global(object):
41     def __init__(self, user):
42         self.user = user
43
44     def __get_uptimes(self):
45         if not hasattr(self, '_uptimes'):
46             self._uptimes = getUptimes(self.machines)
47         return self._uptimes
48     uptimes = property(__get_uptimes)
49
50 g = None
51
52 def helppopup(subj):
53     """Return HTML code for a (?) link to a specified help topic"""
54     return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
55
56
57 global_dict = {}
58 global_dict['helppopup'] = helppopup
59
60
61 # ... and stolen from xend/uuid.py
62 def randomUUID():
63     """Generate a random UUID."""
64
65     return [ random.randint(0, 255) for _ in range(0, 16) ]
66
67 def uuidToString(u):
68     """Turn a numeric UUID to a hyphen-seperated one."""
69     return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
70                      "%02x" * 6]) % tuple(u)
71
72 MAX_MEMORY_TOTAL = 512
73 MAX_MEMORY_SINGLE = 256
74 MIN_MEMORY_SINGLE = 16
75 MAX_DISK_TOTAL = 50
76 MAX_DISK_SINGLE = 50
77 MIN_DISK_SINGLE = 0.1
78 MAX_VMS_TOTAL = 10
79 MAX_VMS_ACTIVE = 4
80
81 def getMachinesByOwner(owner):
82     """Return the machines owned by a given owner."""
83     return Machine.select_by(owner=owner)
84
85 def maxMemory(user, machine=None):
86     """Return the maximum memory for a machine or a user.
87
88     If machine is None, return the memory available for a new 
89     machine.  Else, return the maximum that machine can have.
90
91     on is a dictionary from machines to booleans, whether a machine is
92     on.  If None, it is recomputed. XXX make this global?
93     """
94
95     machines = getMachinesByOwner(user.username)
96     active_machines = [x for x in machines if g.uptimes[x]]
97     mem_usage = sum([x.memory for x in active_machines if x != machine])
98     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
99
100 def maxDisk(user, machine=None):
101     machines = getMachinesByOwner(user.username)
102     disk_usage = sum([sum([y.size for y in x.disks])
103                       for x in machines if x != machine])
104     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
105
106 def canAddVm(user):
107     machines = getMachinesByOwner(user.username)
108     active_machines = [x for x in machines if g.uptimes[x]]
109     return (len(machines) < MAX_VMS_TOTAL and
110             len(active_machines) < MAX_VMS_ACTIVE)
111
112 def haveAccess(user, machine):
113     """Return whether a user has access to a machine"""
114     if user.username == 'moo':
115         return True
116     return machine.owner == user.username
117
118 def error(op, user, fields, err):
119     """Print an error page when a CodeError occurs"""
120     d = dict(op=op, user=user, errorMessage=str(err))
121     print Template(file='error.tmpl', searchList=[d, global_dict]);
122
123 def validMachineName(name):
124     """Check that name is valid for a machine name"""
125     if not name:
126         return False
127     charset = string.ascii_letters + string.digits + '-_'
128     if name[0] in '-_' or len(name) > 22:
129         return False
130     for x in name:
131         if x not in charset:
132             return False
133     return True
134
135 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
136     """Kinit with a given username and keytab"""
137
138     p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
139                          stderr=subprocess.PIPE)
140     e = p.wait()
141     if e:
142         raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
143
144 def checkKinit():
145     """If we lack tickets, kinit."""
146     p = subprocess.Popen(['klist', '-s'])
147     if p.wait():
148         kinit()
149
150 def remctl(*args, **kws):
151     """Perform a remctl and return the output.
152
153     kinits if necessary, and outputs errors to stderr.
154     """
155     checkKinit()
156     p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
157                          + list(args),
158                          stdout=subprocess.PIPE,
159                          stderr=subprocess.PIPE)
160     if kws.get('err'):
161         p.wait()
162         return p.stdout.read(), p.stderr.read()
163     if p.wait():
164         raise CodeError('ERROR on remctl %s: %s' %
165                           (args, p.stderr.read()))
166     return p.stdout.read()
167
168 def lvcreate(machine, disk):
169     """Create a single disk for a machine"""
170     remctl('web', 'lvcreate', machine.name,
171            disk.guest_device_name, str(disk.size))
172     
173 def makeDisks(machine):
174     """Update the lvm partitions to add a disk."""
175     for disk in machine.disks:
176         lvcreate(machine, disk)
177
178 def bootMachine(machine, cdtype):
179     """Boot a machine with a given boot CD.
180
181     If cdtype is None, give no boot cd.  Otherwise, it is the string
182     id of the CD (e.g. 'gutsy_i386')
183     """
184     if cdtype is not None:
185         remctl('web', 'vmboot', machine.name,
186                cdtype)
187     else:
188         remctl('web', 'vmboot', machine.name)
189
190 def registerMachine(machine):
191     """Register a machine to be controlled by the web interface"""
192     remctl('web', 'register', machine.name)
193
194 def unregisterMachine(machine):
195     """Unregister a machine to not be controlled by the web interface"""
196     remctl('web', 'unregister', machine.name)
197
198 def parseStatus(s):
199     """Parse a status string into nested tuples of strings.
200
201     s = output of xm list --long <machine_name>
202     """
203     values = re.split('([()])', s)
204     stack = [[]]
205     for v in values[2:-2]: #remove initial and final '()'
206         if not v:
207             continue
208         v = v.strip()
209         if v == '(':
210             stack.append([])
211         elif v == ')':
212             if len(stack[-1]) == 1:
213                 stack[-1].append('')
214             stack[-2].append(stack[-1])
215             stack.pop()
216         else:
217             if not v:
218                 continue
219             stack[-1].extend(v.split())
220     return stack[-1]
221
222 def getUptimes(machines):
223     """Return a dictionary mapping machine names to uptime strings"""
224     value_string = remctl('web', 'listvms')
225     lines = value_string.splitlines()
226     d = {}
227     for line in lines:
228         lst = line.split()
229         name, id = lst[:2]
230         uptime = ' '.join(lst[2:])
231         d[name] = uptime
232     ans = {}
233     for m in machines:
234         ans[m] = d.get(m.name)
235     return ans
236
237 def statusInfo(machine):
238     """Return the status list for a given machine.
239
240     Gets and parses xm list --long
241     """
242     value_string, err_string = remctl('list-long', machine.name, err=True)
243     if 'Unknown command' in err_string:
244         raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
245     elif 'does not exist' in err_string:
246         return None
247     elif err_string:
248         raise CodeError("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
249     status = parseStatus(value_string)
250     return status
251
252 def hasVnc(status):
253     """Does the machine with a given status list support VNC?"""
254     if status is None:
255         return False
256     for l in status:
257         if l[0] == 'device' and l[1][0] == 'vfb':
258             d = dict(l[1][1:])
259             return 'location' in d
260     return False
261
262 def createVm(user, name, memory, disk, is_hvm, cdrom):
263     """Create a VM and put it in the database"""
264     # put stuff in the table
265     transaction = ctx.current.create_transaction()
266     try:
267         if memory > maxMemory(user):
268             raise InvalidInput("Too much memory requested")
269         if disk > maxDisk(user) * 1024:
270             raise InvalidInput("Too much disk requested")
271         if not canAddVm(user):
272             raise InvalidInput("Too many VMs requested")
273         res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
274         id = res.fetchone()[0]
275         machine = Machine()
276         machine.machine_id = id
277         machine.name = name
278         machine.memory = memory
279         machine.owner = user.username
280         machine.contact = user.email
281         machine.uuid = uuidToString(randomUUID())
282         machine.boot_off_cd = True
283         machine_type = Type.get_by(hvm=is_hvm)
284         machine.type_id = machine_type.type_id
285         ctx.current.save(machine)
286         disk = Disk(machine.machine_id, 
287                     'hda', disk)
288         open = NIC.select_by(machine_id=None)
289         if not open: #No IPs left!
290             raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
291         nic = open[0]
292         nic.machine_id = machine.machine_id
293         nic.hostname = name
294         ctx.current.save(nic)    
295         ctx.current.save(disk)
296         transaction.commit()
297     except:
298         transaction.rollback()
299         raise
300     registerMachine(machine)
301     makeDisks(machine)
302     # tell it to boot with cdrom
303     bootMachine(machine, cdrom)
304
305     return machine
306
307 def validMemory(user, memory, machine=None):
308     """Parse and validate limits for memory for a given user and machine."""
309     try:
310         memory = int(memory)
311         if memory < MIN_MEMORY_SINGLE:
312             raise ValueError
313     except ValueError:
314         raise InvalidInput("Invalid memory amount; must be at least %s MB" %
315                           MIN_MEMORY_SINGLE)
316     if memory > maxMemory(user, machine):
317         raise InvalidInput("Too much memory requested")
318     return memory
319
320 def validDisk(user, disk, machine=None):
321     """Parse and validate limits for disk for a given user and machine."""
322     try:
323         disk = float(disk)
324         if disk > maxDisk(user, machine):
325             raise InvalidInput("Too much disk requested")
326         disk = int(disk * 1024)
327         if disk < MIN_DISK_SINGLE * 1024:
328             raise ValueError
329     except ValueError:
330         raise InvalidInput("Invalid disk amount; minimum is %s GB" %
331                           MIN_DISK_SINGLE)
332     return disk
333
334 def create(user, fields):
335     """Handler for create requests."""
336     name = fields.getfirst('name')
337     if not validMachineName(name):
338         raise InvalidInput("Invalid name '%s'" % name)
339     name = user.username + '_' + name.lower()
340
341     if Machine.get_by(name=name):
342         raise InvalidInput("A machine named '%s' already exists" % name)
343     
344     memory = fields.getfirst('memory')
345     memory = validMemory(user, memory)
346     
347     disk = fields.getfirst('disk')
348     disk = validDisk(user, disk)
349
350     vm_type = fields.getfirst('vmtype')
351     if vm_type not in ('hvm', 'paravm'):
352         raise CodeError("Invalid vm type '%s'"  % vm_type)    
353     is_hvm = (vm_type == 'hvm')
354
355     cdrom = fields.getfirst('cdrom')
356     if cdrom is not None and not CDROM.get(cdrom):
357         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
358     
359     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
360     d = dict(user=user,
361              machine=machine)
362     print Template(file='create.tmpl',
363                    searchList=[d, global_dict]);
364
365 def listVms(user, fields):
366     """Handler for list requests."""
367     machines = [m for m in Machine.select() if haveAccess(user, m)]    
368     on = {}
369     has_vnc = {}
370     on = g.uptimes
371     for m in machines:
372         if not on[m]:
373             has_vnc[m] = 'Off'
374         elif m.type.hvm:
375             has_vnc[m] = True
376         else:
377             has_vnc[m] = "ParaVM"+helppopup("paravm_console")
378     #     for m in machines:
379     #         status = statusInfo(m)
380     #         on[m.name] = status is not None
381     #         has_vnc[m.name] = hasVnc(status)
382     max_mem=maxMemory(user)
383     max_disk=maxDisk(user)
384     d = dict(user=user,
385              can_add_vm=canAddVm(user),
386              max_mem=max_mem,
387              max_disk=max_disk,
388              default_mem=max_mem,
389              default_disk=min(4.0, max_disk),
390              machines=machines,
391              has_vnc=has_vnc,
392              uptimes=uptimes,
393              cdroms=CDROM.select())
394     print Template(file='list.tmpl', searchList=[d, global_dict])
395
396 def testMachineId(user, machineId, exists=True):
397     """Parse, validate and check authorization for a given machineId.
398
399     If exists is False, don't check that it exists.
400     """
401     if machineId is None:
402         raise CodeError("No machine ID specified")
403     try:
404         machineId = int(machineId)
405     except ValueError:
406         raise CodeError("Invalid machine ID '%s'" % machineId)
407     machine = Machine.get(machineId)
408     if exists and machine is None:
409         raise CodeError("No such machine ID '%s'" % machineId)
410     if machine is not None and not haveAccess(user, machine):
411         raise CodeError("No access to machine ID '%s'" % machineId)
412     return machine
413
414 def vnc(user, fields):
415     """VNC applet page.
416
417     Note that due to same-domain restrictions, the applet connects to
418     the webserver, which needs to forward those requests to the xen
419     server.  The Xen server runs another proxy that (1) authenticates
420     and (2) finds the correct port for the VM.
421
422     You might want iptables like:
423
424     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
425     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142 
426     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
427
428     Remember to enable iptables!
429     echo 1 > /proc/sys/net/ipv4/ip_forward
430     """
431     machine = testMachineId(user, fields.getfirst('machine_id'))
432     
433     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
434
435     data = {}
436     data["user"] = user.username
437     data["machine"]=machine.name
438     data["expires"]=time.time()+(5*60)
439     pickledData = cPickle.dumps(data)
440     m = hmac.new(TOKEN_KEY, digestmod=sha)
441     m.update(pickledData)
442     token = {'data': pickledData, 'digest': m.digest()}
443     token = cPickle.dumps(token)
444     token = base64.urlsafe_b64encode(token)
445     
446     status = statusInfo(machine)
447     has_vnc = hasVnc(status)
448     
449     d = dict(user=user,
450              on=status,
451              has_vnc=has_vnc,
452              machine=machine,
453              hostname=os.environ.get('SERVER_NAME', 'localhost'),
454              authtoken=token)
455     print Template(file='vnc.tmpl',
456                    searchList=[d, global_dict])
457
458 def getNicInfo(data_dict, machine):
459     """Helper function for info, get data on nics for a machine.
460
461     Modifies data_dict to include the relevant data, and returns a list
462     of (key, name) pairs to display "name: data_dict[key]" to the user.
463     """
464     data_dict['num_nics'] = len(machine.nics)
465     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
466                            ('nic%s_mac', 'NIC %s MAC Addr'),
467                            ('nic%s_ip', 'NIC %s IP'),
468                            ]
469     nic_fields = []
470     for i in range(len(machine.nics)):
471         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
472         data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
473         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
474         data_dict['nic%s_ip' % i] = machine.nics[i].ip
475     if len(machine.nics) == 1:
476         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
477     return nic_fields
478
479 def getDiskInfo(data_dict, machine):
480     """Helper function for info, get data on disks for a machine.
481
482     Modifies data_dict to include the relevant data, and returns a list
483     of (key, name) pairs to display "name: data_dict[key]" to the user.
484     """
485     data_dict['num_disks'] = len(machine.disks)
486     disk_fields_template = [('%s_size', '%s size')]
487     disk_fields = []
488     for disk in machine.disks:
489         name = disk.guest_device_name
490         disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
491         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
492     return disk_fields
493
494 def deleteVM(machine):
495     """Delete a VM."""
496     transaction = ctx.current.create_transaction()
497     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
498     try:
499         for nic in machine.nics:
500             nic.machine_id = None
501             nic.hostname = None
502             ctx.current.save(nic)
503         for disk in machine.disks:
504             ctx.current.delete(disk)
505         ctx.current.delete(machine)
506         transaction.commit()
507     except:
508         transaction.rollback()
509         raise
510     for mname, dname in delete_disk_pairs:
511         remctl('web', 'lvremove', mname, dname)
512     unregisterMachine(machine)
513
514 def command(user, fields):
515     """Handler for running commands like boot and delete on a VM."""
516     print time.time()-start_time
517     machine = testMachineId(user, fields.getfirst('machine_id'))
518     action = fields.getfirst('action')
519     cdrom = fields.getfirst('cdrom')
520     print time.time()-start_time
521     if cdrom is not None and not CDROM.get(cdrom):
522         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
523     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
524         raise CodeError("Invalid action '%s'" % action)
525     if action == 'Reboot':
526         if cdrom is not None:
527             remctl('reboot', machine.name, cdrom)
528         else:
529             remctl('reboot', machine.name)
530     elif action == 'Power on':
531         if maxMemory(user) < machine.memory:
532             raise InvalidInput("You don't have enough free RAM quota")
533         bootMachine(machine, cdrom)
534     elif action == 'Power off':
535         remctl('destroy', machine.name)
536     elif action == 'Shutdown':
537         remctl('shutdown', machine.name)
538     elif action == 'Delete VM':
539         deleteVM(machine)
540     print time.time()-start_time
541
542     d = dict(user=user,
543              command=action,
544              machine=machine)
545     print Template(file="command.tmpl", searchList=[d, global_dict])
546         
547 def modify(user, fields):
548     """Handler for modifying attributes of a machine."""
549     #XXX not written yet
550     machine = testMachineId(user, fields.getfirst('machine_id'))
551     
552 def help(user, fields):
553     """Handler for help messages."""
554     simple = fields.getfirst('simple')
555     subjects = fields.getlist('subject')
556     
557     mapping = dict(paravm_console="""
558 ParaVM machines do not support console access over VNC.  To access
559 these machines, you either need to boot with a liveCD and ssh in or
560 hope that the sipb-xen maintainers add support for serial consoles.""",
561                    hvm_paravm="""
562 HVM machines use the virtualization features of the processor, while
563 ParaVM machines use Xen's emulation of virtualization features.  You
564 want an HVM virtualized machine.""",
565                    cpu_weight="""Don't ask us!  We're as mystified as you are.""")
566     
567     d = dict(user=user,
568              simple=simple,
569              subjects=subjects,
570              mapping=mapping)
571     
572     print Template(file="help.tmpl", searchList=[d, global_dict])
573     
574
575 def info(user, fields):
576     """Handler for info on a single VM."""
577     machine = testMachineId(user, fields.getfirst('machine_id'))
578     status = statusInfo(machine)
579     has_vnc = hasVnc(status)
580     if status is None:
581         main_status = dict(name=machine.name,
582                            memory=str(machine.memory))
583     else:
584         main_status = dict(status[1:])
585     start_time = float(main_status.get('start_time', 0))
586     uptime = datetime.timedelta(seconds=int(time.time()-start_time))
587     cpu_time_float = float(main_status.get('cpu_time', 0))
588     cputime = datetime.timedelta(seconds=int(cpu_time_float))
589     display_fields = """name uptime memory state cpu_weight on_reboot 
590      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
591     display_fields = [('name', 'Name'),
592                       ('owner', 'Owner'),
593                       ('contact', 'Contact'),
594                       ('type', 'Type'),
595                       'NIC_INFO',
596                       ('uptime', 'uptime'),
597                       ('cputime', 'CPU usage'),
598                       ('memory', 'RAM'),
599                       'DISK_INFO',
600                       ('state', 'state (xen format)'),
601                       ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
602                       ('on_reboot', 'Action on VM reboot'),
603                       ('on_poweroff', 'Action on VM poweroff'),
604                       ('on_crash', 'Action on VM crash'),
605                       ('on_xend_start', 'Action on Xen start'),
606                       ('on_xend_stop', 'Action on Xen stop'),
607                       ('bootloader', 'Bootloader options'),
608                       ]
609     fields = []
610     machine_info = {}
611     machine_info['name'] = machine.name
612     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
613     machine_info['owner'] = machine.owner
614     machine_info['contact'] = machine.contact
615
616     nic_fields = getNicInfo(machine_info, machine)
617     nic_point = display_fields.index('NIC_INFO')
618     display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
619
620     disk_fields = getDiskInfo(machine_info, machine)
621     disk_point = display_fields.index('DISK_INFO')
622     display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
623     
624     main_status['memory'] += ' MB'
625     for field, disp in display_fields:
626         if field in ('uptime', 'cputime'):
627             fields.append((disp, locals()[field]))
628         elif field in machine_info:
629             fields.append((disp, machine_info[field]))
630         elif field in main_status:
631             fields.append((disp, main_status[field]))
632         else:
633             pass
634             #fields.append((disp, None))
635     max_mem = maxMemory(user, machine)
636     max_disk = maxDisk(user, machine)
637     d = dict(user=user,
638              cdroms=CDROM.select(),
639              on=status is not None,
640              machine=machine,
641              has_vnc=has_vnc,
642              uptime=str(uptime),
643              ram=machine.memory,
644              max_mem=max_mem,
645              max_disk=max_disk,
646              fields = fields)
647     print Template(file='info.tmpl',
648                    searchList=[d, global_dict])
649
650 mapping = dict(list=listVms,
651                vnc=vnc,
652                command=command,
653                modify=modify,
654                info=info,
655                create=create,
656                help=help)
657
658 if __name__ == '__main__':
659     start_time = time.time()
660     fields = cgi.FieldStorage()
661     class User:
662         username = "moo"
663         email = 'moo@cow.com'
664     u = User()
665     g = Global(u)
666     if 'SSL_CLIENT_S_DN_Email' in os.environ:
667         username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
668         u.username = username
669         u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
670     else:
671         u.username = 'moo'
672         u.email = 'nobody'
673     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
674     operation = os.environ.get('PATH_INFO', '')
675     #print 'Content-Type: text/plain\n'
676     #print operation
677     if not operation:
678         print "Status: 301 Moved Permanently"
679         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
680         sys.exit(0)
681     print 'Content-Type: text/html\n'
682
683     if operation.startswith('/'):
684         operation = operation[1:]
685     if not operation:
686         operation = 'list'
687     
688     fun = mapping.get(operation, 
689                       lambda u, e:
690                           error(operation, u, e,
691                                 "Invalid operation '%s'" % operation))
692     if fun not in (help, ):
693         connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
694     try:
695         fun(u, fields)
696     except CodeError, err:
697         error(operation, u, fields, err)
698     except InvalidInput, err:
699         error(operation, u, fields, err)