source: trunk/web/templates/main.py @ 152

Last change on this file since 152 was 152, checked in by ecprice, 17 years ago

Store a little global state to avoid an extra remctls.

  • Property svn:executable set to *
File size: 23.5 KB
Line 
1#!/usr/bin/python
2
3import sys
4import cgi
5import os
6import string
7import subprocess
8import re
9import time
10import cPickle
11import base64
12import sha
13import hmac
14import datetime
15
16sys.stderr = sys.stdout
17sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
18
19from Cheetah.Template import Template
20from sipb_xen_database import *
21import random
22
23class MyException(Exception):
24    """Base class for my exceptions"""
25    pass
26
27class 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
36class CodeError(MyException):
37    """Exception for internal errors or bad faith input."""
38    pass
39
40class 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
50g = None
51
52def 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
57global_dict = {}
58global_dict['helppopup'] = helppopup
59
60
61# ... and stolen from xend/uuid.py
62def randomUUID():
63    """Generate a random UUID."""
64
65    return [ random.randint(0, 255) for _ in range(0, 16) ]
66
67def 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
72MAX_MEMORY_TOTAL = 512
73MAX_MEMORY_SINGLE = 256
74MIN_MEMORY_SINGLE = 16
75MAX_DISK_TOTAL = 50
76MAX_DISK_SINGLE = 50
77MIN_DISK_SINGLE = 0.1
78MAX_VMS_TOTAL = 10
79MAX_VMS_ACTIVE = 4
80
81def getMachinesByOwner(owner):
82    """Return the machines owned by a given owner."""
83    return Machine.select_by(owner=owner)
84
85def 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
100def 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
106def 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
112def 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
118def 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
123def 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
135def 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
144def checkKinit():
145    """If we lack tickets, kinit."""
146    p = subprocess.Popen(['klist', '-s'])
147    if p.wait():
148        kinit()
149
150def 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
168def 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   
173def makeDisks(machine):
174    """Update the lvm partitions to add a disk."""
175    for disk in machine.disks:
176        lvcreate(machine, disk)
177
178def 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
190def registerMachine(machine):
191    """Register a machine to be controlled by the web interface"""
192    remctl('web', 'register', machine.name)
193
194def unregisterMachine(machine):
195    """Unregister a machine to not be controlled by the web interface"""
196    remctl('web', 'unregister', machine.name)
197
198def 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
222def 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
237def 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
252def 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
262def 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
307def 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
320def 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
334def 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
365def 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
396def 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
414def 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
458def 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
479def 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
494def 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
514def 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       
547def 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   
552def help(user, fields):
553    """Handler for help messages."""
554    simple = fields.getfirst('simple')
555    subjects = fields.getlist('subject')
556   
557    mapping = dict(paravm_console="""
558ParaVM machines do not support console access over VNC.  To access
559these machines, you either need to boot with a liveCD and ssh in or
560hope that the sipb-xen maintainers add support for serial consoles.""",
561                   hvm_paravm="""
562HVM machines use the virtualization features of the processor, while
563ParaVM machines use Xen's emulation of virtualization features.  You
564want 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
575def 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
650mapping = dict(list=listVms,
651               vnc=vnc,
652               command=command,
653               modify=modify,
654               info=info,
655               create=create,
656               help=help)
657
658if __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)
Note: See TracBrowser for help on using the repository browser.