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

Last change on this file since 326 was 309, checked in by price, 17 years ago

move framebuffer tip to /help page

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