source: trunk/packages/sipb-xen-www/code/main.py @ 536

Last change on this file since 536 was 536, checked in by ecprice, 16 years ago

Prettier help titles

  • Property svn:executable set to *
File size: 24.0 KB
Line 
1#!/usr/bin/python
2"""Main CGI script for web interface"""
3
4import base64
5import cPickle
6import cgi
7import datetime
8import hmac
9import os
10import sha
11import simplejson
12import sys
13import time
14import urllib
15from StringIO import StringIO
16
17def 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 None
22    sys.stderr = sys.stdout
23    errio.seek(0)
24    return errio.read()
25
26def printError():
27    """Revert stderr to stdout, and print the contents of stderr"""
28    if isinstance(sys.stderr, StringIO):
29        print revertStandardError()
30
31if __name__ == '__main__':
32    import atexit
33    atexit.register(printError)
34    sys.stderr = StringIO()
35
36sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
37
38import templates
39from Cheetah.Template import Template
40import sipb_xen_database
41from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
42import validation
43import cache_acls
44from webcommon import InvalidInput, CodeError, g
45import controls
46
47class Checkpoint:
48    def __init__(self):
49        self.start_time = time.time()
50        self.checkpoints = []
51
52    def checkpoint(self, s):
53        self.checkpoints.append((s, time.time()))
54
55    def __str__(self):
56        return ('Timing info:\n%s\n' %
57                '\n'.join(['%s: %s' % (d, t - self.start_time) for
58                           (d, t) in self.checkpoints]))
59
60checkpoint = Checkpoint()
61
62def jquote(string):
63    return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
64
65def helppopup(subj):
66    """Return HTML code for a (?) link to a specified help topic"""
67    return ('<span class="helplink"><a href="help?' +
68            cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
69            +'" target="_blank" ' +
70            'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
71
72def makeErrorPre(old, addition):
73    if addition is None:
74        return
75    if old:
76        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
77    else:
78        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
79
80Template.sipb_xen_database = sipb_xen_database
81Template.helppopup = staticmethod(helppopup)
82Template.err = None
83
84class JsonDict:
85    """Class to store a dictionary that will be converted to JSON"""
86    def __init__(self, **kws):
87        self.data = kws
88        if 'err' in kws:
89            err = kws['err']
90            del kws['err']
91            self.addError(err)
92
93    def __str__(self):
94        return simplejson.dumps(self.data)
95
96    def addError(self, text):
97        """Add stderr text to be displayed on the website."""
98        self.data['err'] = \
99            makeErrorPre(self.data.get('err'), text)
100
101class Defaults:
102    """Class to store default values for fields."""
103    memory = 256
104    disk = 4.0
105    cdrom = ''
106    autoinstall = ''
107    name = ''
108    type = 'linux-hvm'
109
110    def __init__(self, max_memory=None, max_disk=None, **kws):
111        if max_memory is not None:
112            self.memory = min(self.memory, max_memory)
113        if max_disk is not None:
114            self.max_disk = min(self.disk, max_disk)
115        for key in kws:
116            setattr(self, key, kws[key])
117
118
119
120DEFAULT_HEADERS = {'Content-Type': 'text/html'}
121
122def error(op, user, fields, err, emsg):
123    """Print an error page when a CodeError occurs"""
124    d = dict(op=op, user=user, errorMessage=str(err),
125             stderr=emsg)
126    return templates.error(searchList=[d])
127
128def invalidInput(op, user, fields, err, emsg):
129    """Print an error page when an InvalidInput exception occurs"""
130    d = dict(op=op, user=user, err_field=err.err_field,
131             err_value=str(err.err_value), stderr=emsg,
132             errorMessage=str(err))
133    return templates.invalid(searchList=[d])
134
135def hasVnc(status):
136    """Does the machine with a given status list support VNC?"""
137    if status is None:
138        return False
139    for l in status:
140        if l[0] == 'device' and l[1][0] == 'vfb':
141            d = dict(l[1][1:])
142            return 'location' in d
143    return False
144
145def parseCreate(user, fields):
146    name = fields.getfirst('name')
147    if not validation.validMachineName(name):
148        raise InvalidInput('name', name, 'You must provide a machine name.  Max 22 chars, alnum plus \'-\' and \'_\'.')
149    name = name.lower()
150
151    if Machine.get_by(name=name):
152        raise InvalidInput('name', name,
153                           "Name already exists.")
154
155    owner = validation.testOwner(user, fields.getfirst('owner'))
156
157    memory = fields.getfirst('memory')
158    memory = validation.validMemory(owner, memory, on=True)
159
160    disk_size = fields.getfirst('disk')
161    disk_size = validation.validDisk(owner, disk_size)
162
163    vm_type = fields.getfirst('vmtype')
164    vm_type = validation.validVmType(vm_type)
165
166    cdrom = fields.getfirst('cdrom')
167    if cdrom is not None and not CDROM.get(cdrom):
168        raise CodeError("Invalid cdrom type '%s'" % cdrom)
169
170    clone_from = fields.getfirst('clone_from')
171    if clone_from and clone_from != 'ice3':
172        raise CodeError("Invalid clone image '%s'" % clone_from)
173
174    return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
175                owner=owner, machine_type=vm_type, cdrom=cdrom, clone_from=clone_from)
176
177def create(user, fields):
178    """Handler for create requests."""
179    try:
180        parsed_fields = parseCreate(user, fields)
181        machine = controls.createVm(**parsed_fields)
182    except InvalidInput, err:
183        pass
184    else:
185        err = None
186    g.clear() #Changed global state
187    d = getListDict(user)
188    d['err'] = err
189    if err:
190        for field in fields.keys():
191            setattr(d['defaults'], field, fields.getfirst(field))
192    else:
193        d['new_machine'] = parsed_fields['name']
194    return templates.list(searchList=[d])
195
196
197def getListDict(user):
198    """Gets the list of local variables used by list.tmpl."""
199    checkpoint.checkpoint('Starting')
200    machines = g.machines
201    checkpoint.checkpoint('Got my machines')
202    on = {}
203    has_vnc = {}
204    xmlist = g.xmlist
205    checkpoint.checkpoint('Got uptimes')
206    for m in machines:
207        if m not in xmlist:
208            has_vnc[m] = 'Off'
209            m.uptime = None
210        else:
211            m.uptime = xmlist[m]['uptime']
212            if xmlist[m]['console']:
213                has_vnc[m] = True
214            elif m.type.hvm:
215                has_vnc[m] = "WTF?"
216            else:
217                has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
218    max_memory = validation.maxMemory(user)
219    max_disk = validation.maxDisk(user)
220    checkpoint.checkpoint('Got max mem/disk')
221    defaults = Defaults(max_memory=max_memory,
222                        max_disk=max_disk,
223                        owner=user,
224                        cdrom='gutsy-i386')
225    checkpoint.checkpoint('Got defaults')
226    def sortkey(machine):
227        return (machine.owner != user, machine.owner, machine.name)
228    machines = sorted(machines, key=sortkey)
229    d = dict(user=user,
230             cant_add_vm=validation.cantAddVm(user),
231             max_memory=max_memory,
232             max_disk=max_disk,
233             defaults=defaults,
234             machines=machines,
235             has_vnc=has_vnc)
236    return d
237
238def listVms(user, fields):
239    """Handler for list requests."""
240    checkpoint.checkpoint('Getting list dict')
241    d = getListDict(user)
242    checkpoint.checkpoint('Got list dict')
243    return templates.list(searchList=[d])
244
245def vnc(user, fields):
246    """VNC applet page.
247
248    Note that due to same-domain restrictions, the applet connects to
249    the webserver, which needs to forward those requests to the xen
250    server.  The Xen server runs another proxy that (1) authenticates
251    and (2) finds the correct port for the VM.
252
253    You might want iptables like:
254
255    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
256      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
257    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
258      --dport 10003 -j SNAT --to-source 18.187.7.142
259    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
260      --dport 10003 -j ACCEPT
261
262    Remember to enable iptables!
263    echo 1 > /proc/sys/net/ipv4/ip_forward
264    """
265    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
266
267    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
268
269    data = {}
270    data["user"] = user
271    data["machine"] = machine.name
272    data["expires"] = time.time()+(5*60)
273    pickled_data = cPickle.dumps(data)
274    m = hmac.new(TOKEN_KEY, digestmod=sha)
275    m.update(pickled_data)
276    token = {'data': pickled_data, 'digest': m.digest()}
277    token = cPickle.dumps(token)
278    token = base64.urlsafe_b64encode(token)
279
280    status = controls.statusInfo(machine)
281    has_vnc = hasVnc(status)
282
283    d = dict(user=user,
284             on=status,
285             has_vnc=has_vnc,
286             machine=machine,
287             hostname=os.environ.get('SERVER_NAME', 'localhost'),
288             authtoken=token)
289    return templates.vnc(searchList=[d])
290
291def getHostname(nic):
292    """Find the hostname associated with a NIC.
293
294    XXX this should be merged with the similar logic in DNS and DHCP.
295    """
296    if nic.hostname and '.' in nic.hostname:
297        return nic.hostname
298    elif nic.machine:
299        return nic.machine.name + '.xvm.mit.edu'
300    else:
301        return None
302
303
304def getNicInfo(data_dict, machine):
305    """Helper function for info, get data on nics for a machine.
306
307    Modifies data_dict to include the relevant data, and returns a list
308    of (key, name) pairs to display "name: data_dict[key]" to the user.
309    """
310    data_dict['num_nics'] = len(machine.nics)
311    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
312                           ('nic%s_mac', 'NIC %s MAC Addr'),
313                           ('nic%s_ip', 'NIC %s IP'),
314                           ]
315    nic_fields = []
316    for i in range(len(machine.nics)):
317        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
318        if not i:
319            data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
320        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
321        data_dict['nic%s_ip' % i] = machine.nics[i].ip
322    if len(machine.nics) == 1:
323        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
324    return nic_fields
325
326def getDiskInfo(data_dict, machine):
327    """Helper function for info, get data on disks for a machine.
328
329    Modifies data_dict to include the relevant data, and returns a list
330    of (key, name) pairs to display "name: data_dict[key]" to the user.
331    """
332    data_dict['num_disks'] = len(machine.disks)
333    disk_fields_template = [('%s_size', '%s size')]
334    disk_fields = []
335    for disk in machine.disks:
336        name = disk.guest_device_name
337        disk_fields.extend([(x % name, y % name) for x, y in
338                            disk_fields_template])
339        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
340    return disk_fields
341
342def command(user, fields):
343    """Handler for running commands like boot and delete on a VM."""
344    back = fields.getfirst('back')
345    try:
346        d = controls.commandResult(user, fields)
347        if d['command'] == 'Delete VM':
348            back = 'list'
349    except InvalidInput, err:
350        if not back:
351            raise
352        #print >> sys.stderr, err
353        result = err
354    else:
355        result = 'Success!'
356        if not back:
357            return templates.command(searchList=[d])
358    if back == 'list':
359        g.clear() #Changed global state
360        d = getListDict(user)
361        d['result'] = result
362        return templates.list(searchList=[d])
363    elif back == 'info':
364        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
365        return ({'Status': '302',
366                 'Location': '/info?machine_id=%d' % machine.machine_id},
367                "You shouldn't see this message.")
368    else:
369        raise InvalidInput('back', back, 'Not a known back page.')
370
371def modifyDict(user, fields):
372    """Modify a machine as specified by CGI arguments.
373
374    Return a list of local variables for modify.tmpl.
375    """
376    olddisk = {}
377    transaction = ctx.current.create_transaction()
378    try:
379        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
380        owner = validation.testOwner(user, fields.getfirst('owner'), machine)
381        admin = validation.testAdmin(user, fields.getfirst('administrator'),
382                                     machine)
383        contact = validation.testContact(user, fields.getfirst('contact'),
384                                         machine)
385        name = validation.testName(user, fields.getfirst('name'), machine)
386        oldname = machine.name
387        command = "modify"
388
389        memory = fields.getfirst('memory')
390        if memory is not None:
391            memory = validation.validMemory(user, memory, machine, on=False)
392            machine.memory = memory
393
394        vm_type = validation.validVmType(fields.getfirst('vmtype'))
395        if vm_type is not None:
396            machine.type = vm_type
397
398        disksize = validation.testDisk(user, fields.getfirst('disk'))
399        if disksize is not None:
400            disksize = validation.validDisk(user, disksize, machine)
401            disk = machine.disks[0]
402            if disk.size != disksize:
403                olddisk[disk.guest_device_name] = disksize
404                disk.size = disksize
405                ctx.current.save(disk)
406
407        update_acl = False
408        if owner is not None and owner != machine.owner:
409            machine.owner = owner
410            update_acl = True
411        if name is not None:
412            machine.name = name
413        if admin is not None and admin != machine.administrator:
414            machine.administrator = admin
415            update_acl = True
416        if contact is not None:
417            machine.contact = contact
418
419        ctx.current.save(machine)
420        if update_acl:
421            cache_acls.refreshMachine(machine)
422        transaction.commit()
423    except:
424        transaction.rollback()
425        raise
426    for diskname in olddisk:
427        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
428    if name is not None:
429        controls.renameMachine(machine, oldname, name)
430    return dict(user=user,
431                command=command,
432                machine=machine)
433
434def modify(user, fields):
435    """Handler for modifying attributes of a machine."""
436    try:
437        modify_dict = modifyDict(user, fields)
438    except InvalidInput, err:
439        result = None
440        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
441    else:
442        machine = modify_dict['machine']
443        result = 'Success!'
444        err = None
445    info_dict = infoDict(user, machine)
446    info_dict['err'] = err
447    if err:
448        for field in fields.keys():
449            setattr(info_dict['defaults'], field, fields.getfirst(field))
450    info_dict['result'] = result
451    return templates.info(searchList=[info_dict])
452
453
454def helpHandler(user, fields):
455    """Handler for help messages."""
456    simple = fields.getfirst('simple')
457    subjects = fields.getlist('subject')
458
459    help_mapping = {'ParaVM Console': """
460ParaVM machines do not support local console access over VNC.  To
461access the serial console of these machines, you can SSH with Kerberos
462to console.xvm.mit.edu, using the name of the machine as your
463username.""",
464                    'HVM/ParaVM': """
465HVM machines use the virtualization features of the processor, while
466ParaVM machines use Xen's emulation of virtualization features.  You
467want an HVM virtualized machine.""",
468                    'CPU Weight': """
469Don't ask us!  We're as mystified as you are.""",
470                    'Owner': """
471The owner field is used to determine <a
472href="help?subject=Quotas">quotas</a>.  It must be the name of a
473locker that you are an AFS administrator of.  In particular, you or an
474AFS group you are a member of must have AFS rlidwka bits on the
475locker.  You can check who administers the LOCKER locker using the
476commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
477href="help?subject=Administrator">administrator</a>.""",
478                    'Administrator': """
479The administrator field determines who can access the console and
480power on and off the machine.  This can be either a user or a moira
481group.""",
482                    'Quotas': """
483Quotas are determined on a per-locker basis.  Each locker may have a
484maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
485active machines.""",
486                    'Console': """
487<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
488setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
489your machine will run just fine, but the applet's display of the
490console will suffer artifacts.
491"""
492                    }
493
494    if not subjects:
495        subjects = sorted(help_mapping.keys())
496
497    d = dict(user=user,
498             simple=simple,
499             subjects=subjects,
500             mapping=help_mapping)
501
502    return templates.help(searchList=[d])
503
504
505def badOperation(u, e):
506    """Function called when accessing an unknown URI."""
507    raise CodeError("Unknown operation")
508
509def infoDict(user, machine):
510    """Get the variables used by info.tmpl."""
511    status = controls.statusInfo(machine)
512    checkpoint.checkpoint('Getting status info')
513    has_vnc = hasVnc(status)
514    if status is None:
515        main_status = dict(name=machine.name,
516                           memory=str(machine.memory))
517        uptime = None
518        cputime = None
519    else:
520        main_status = dict(status[1:])
521        start_time = float(main_status.get('start_time', 0))
522        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
523        cpu_time_float = float(main_status.get('cpu_time', 0))
524        cputime = datetime.timedelta(seconds=int(cpu_time_float))
525    checkpoint.checkpoint('Status')
526    display_fields = """name uptime memory state cpu_weight on_reboot
527     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
528    display_fields = [('name', 'Name'),
529                      ('owner', 'Owner'),
530                      ('administrator', 'Administrator'),
531                      ('contact', 'Contact'),
532                      ('type', 'Type'),
533                      'NIC_INFO',
534                      ('uptime', 'uptime'),
535                      ('cputime', 'CPU usage'),
536                      ('memory', 'RAM'),
537                      'DISK_INFO',
538                      ('state', 'state (xen format)'),
539                      ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
540                      ('on_reboot', 'Action on VM reboot'),
541                      ('on_poweroff', 'Action on VM poweroff'),
542                      ('on_crash', 'Action on VM crash'),
543                      ('on_xend_start', 'Action on Xen start'),
544                      ('on_xend_stop', 'Action on Xen stop'),
545                      ('bootloader', 'Bootloader options'),
546                      ]
547    fields = []
548    machine_info = {}
549    machine_info['name'] = machine.name
550    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
551    machine_info['owner'] = machine.owner
552    machine_info['administrator'] = machine.administrator
553    machine_info['contact'] = machine.contact
554
555    nic_fields = getNicInfo(machine_info, machine)
556    nic_point = display_fields.index('NIC_INFO')
557    display_fields = (display_fields[:nic_point] + nic_fields +
558                      display_fields[nic_point+1:])
559
560    disk_fields = getDiskInfo(machine_info, machine)
561    disk_point = display_fields.index('DISK_INFO')
562    display_fields = (display_fields[:disk_point] + disk_fields +
563                      display_fields[disk_point+1:])
564
565    main_status['memory'] += ' MiB'
566    for field, disp in display_fields:
567        if field in ('uptime', 'cputime') and locals()[field] is not None:
568            fields.append((disp, locals()[field]))
569        elif field in machine_info:
570            fields.append((disp, machine_info[field]))
571        elif field in main_status:
572            fields.append((disp, main_status[field]))
573        else:
574            pass
575            #fields.append((disp, None))
576
577    checkpoint.checkpoint('Got fields')
578
579
580    max_mem = validation.maxMemory(user, machine, False)
581    checkpoint.checkpoint('Got mem')
582    max_disk = validation.maxDisk(user, machine)
583    defaults = Defaults()
584    for name in 'machine_id name administrator owner memory contact'.split():
585        setattr(defaults, name, getattr(machine, name))
586    defaults.type = machine.type.type_id
587    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
588    checkpoint.checkpoint('Got defaults')
589    d = dict(user=user,
590             on=status is not None,
591             machine=machine,
592             defaults=defaults,
593             has_vnc=has_vnc,
594             uptime=str(uptime),
595             ram=machine.memory,
596             max_mem=max_mem,
597             max_disk=max_disk,
598             owner_help=helppopup("Owner"),
599             fields = fields)
600    return d
601
602def info(user, fields):
603    """Handler for info on a single VM."""
604    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
605    d = infoDict(user, machine)
606    checkpoint.checkpoint('Got infodict')
607    return templates.info(searchList=[d])
608
609def unauthFront(_, fields):
610    """Information for unauth'd users."""
611    return templates.unauth(searchList=[{'simple' : True}])
612
613mapping = dict(list=listVms,
614               vnc=vnc,
615               command=command,
616               modify=modify,
617               info=info,
618               create=create,
619               help=helpHandler,
620               unauth=unauthFront)
621
622def printHeaders(headers):
623    """Print a dictionary as HTTP headers."""
624    for key, value in headers.iteritems():
625        print '%s: %s' % (key, value)
626    print
627
628
629def getUser():
630    """Return the current user based on the SSL environment variables"""
631    email = os.environ.get('SSL_CLIENT_S_DN_Email', None)
632    if email is None:
633        return None
634    return email.split("@")[0]
635
636def main(operation, user, fields):
637    start_time = time.time()
638    fun = mapping.get(operation, badOperation)
639
640    if fun not in (helpHandler, ):
641        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
642    try:
643        checkpoint.checkpoint('Before')
644        output = fun(u, fields)
645        checkpoint.checkpoint('After')
646
647        headers = dict(DEFAULT_HEADERS)
648        if isinstance(output, tuple):
649            new_headers, output = output
650            headers.update(new_headers)
651        e = revertStandardError()
652        if e:
653            output.addError(e)
654        printHeaders(headers)
655        output_string =  str(output)
656        checkpoint.checkpoint('output as a string')
657        print output_string
658        if fields.has_key('timedebug'):
659            print '<pre>%s</pre>' % checkpoint
660    except Exception, err:
661        if not fields.has_key('js'):
662            if isinstance(err, CodeError):
663                print 'Content-Type: text/html\n'
664                e = revertStandardError()
665                print error(operation, u, fields, err, e)
666                sys.exit(1)
667            if isinstance(err, InvalidInput):
668                print 'Content-Type: text/html\n'
669                e = revertStandardError()
670                print invalidInput(operation, u, fields, err, e)
671                sys.exit(1)
672        print 'Content-Type: text/plain\n'
673        print 'Uh-oh!  We experienced an error.'
674        print 'Please email xvm-dev@mit.edu with the contents of this page.'
675        print '----'
676        e = revertStandardError()
677        print e
678        print '----'
679        raise
680
681if __name__ == '__main__':
682    fields = cgi.FieldStorage()
683
684    if fields.has_key('sqldebug'):
685        import logging
686        logging.basicConfig()
687        logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
688        logging.getLogger('sqlalchemy.orm.unitofwork').setLevel(logging.INFO)
689
690    u = getUser()
691    g.user = u
692    operation = os.environ.get('PATH_INFO', '')
693    if not operation:
694        print "Status: 301 Moved Permanently"
695        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
696        sys.exit(0)
697
698    if u is None:
699        operation = 'unauth'
700
701    if operation.startswith('/'):
702        operation = operation[1:]
703    if not operation:
704        operation = 'list'
705
706    if os.getenv("SIPB_XEN_PROFILE"):
707        import profile
708        profile.run('main(operation, u, fields)', 'log-'+operation)
709    else:
710        main(operation, u, fields)
Note: See TracBrowser for help on using the repository browser.