source: trunk/web/main.py @ 252

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

Allow fully qualified domains in the NICs table to override the
name.servers.csail.mit.edu domains.

  • Property svn:executable set to *
File size: 22.1 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
[209]39from sipb_xen_database import Machine, CDROM, ctx, connect
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')
[209]149    memory = validation.validMemory(user, memory, on=True)
[134]150   
[243]151    disk_size = fields.getfirst('disk')
152    disk_size = validation.validDisk(user, 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):
[246]186    machines = [m for m in Machine.select()
187                if validation.haveAccess(user, m)]
188    #if user == 'moo':
189    #    machines = Machine.select()
190    #else:
191    #    machines = Machine.query().join('users').filter_by(user=user).all()
[235]192    checkpoint.checkpoint('Got my machines')
[133]193    on = {}
[119]194    has_vnc = {}
[152]195    on = g.uptimes
[235]196    checkpoint.checkpoint('Got uptimes')
[136]197    for m in machines:
[205]198        m.uptime = g.uptimes.get(m)
[144]199        if not on[m]:
200            has_vnc[m] = 'Off'
[138]201        elif m.type.hvm:
[144]202            has_vnc[m] = True
[136]203        else:
[144]204            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
[209]205    max_memory = validation.maxMemory(user)
206    max_disk = validation.maxDisk(user)
[235]207    checkpoint.checkpoint('Got max mem/disk')
[205]208    defaults = Defaults(max_memory=max_memory,
209                        max_disk=max_disk,
[228]210                        owner=user,
[205]211                        cdrom='gutsy-i386')
[235]212    checkpoint.checkpoint('Got defaults')
[113]213    d = dict(user=user,
[209]214             cant_add_vm=validation.cantAddVm(user),
[205]215             max_memory=max_memory,
[144]216             max_disk=max_disk,
[205]217             defaults=defaults,
[113]218             machines=machines,
[119]219             has_vnc=has_vnc,
[157]220             uptimes=g.uptimes,
[113]221             cdroms=CDROM.select())
[205]222    return d
[113]223
[205]224def listVms(user, fields):
225    """Handler for list requests."""
[235]226    checkpoint.checkpoint('Getting list dict')
[205]227    d = getListDict(user)
[235]228    checkpoint.checkpoint('Got list dict')
229    return templates.list(searchList=[d])
[205]230           
[113]231def vnc(user, fields):
[119]232    """VNC applet page.
233
234    Note that due to same-domain restrictions, the applet connects to
235    the webserver, which needs to forward those requests to the xen
236    server.  The Xen server runs another proxy that (1) authenticates
237    and (2) finds the correct port for the VM.
238
239    You might want iptables like:
240
[205]241    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
242      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
243    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
244      --dport 10003 -j SNAT --to-source 18.187.7.142
245    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
246      --dport 10003 -j ACCEPT
[145]247
248    Remember to enable iptables!
249    echo 1 > /proc/sys/net/ipv4/ip_forward
[119]250    """
[209]251    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
[118]252   
253    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
254
255    data = {}
[228]256    data["user"] = user
[205]257    data["machine"] = machine.name
258    data["expires"] = time.time()+(5*60)
259    pickled_data = cPickle.dumps(data)
[118]260    m = hmac.new(TOKEN_KEY, digestmod=sha)
[205]261    m.update(pickled_data)
262    token = {'data': pickled_data, 'digest': m.digest()}
[118]263    token = cPickle.dumps(token)
264    token = base64.urlsafe_b64encode(token)
265   
[209]266    status = controls.statusInfo(machine)
[152]267    has_vnc = hasVnc(status)
268   
[113]269    d = dict(user=user,
[152]270             on=status,
271             has_vnc=has_vnc,
[113]272             machine=machine,
[119]273             hostname=os.environ.get('SERVER_NAME', 'localhost'),
[113]274             authtoken=token)
[235]275    return templates.vnc(searchList=[d])
[113]276
[252]277def getHostname(nic):
278    if nic.hostname and '.' in nic.hostname:
279        return nic.hostname
280    elif nic.machine:
281        return nic.machine.name + '.servers.csail.mit.edu'
282    else:
283        return None
284
285
[133]286def getNicInfo(data_dict, machine):
[145]287    """Helper function for info, get data on nics for a machine.
288
289    Modifies data_dict to include the relevant data, and returns a list
290    of (key, name) pairs to display "name: data_dict[key]" to the user.
291    """
[133]292    data_dict['num_nics'] = len(machine.nics)
[227]293    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
[133]294                           ('nic%s_mac', 'NIC %s MAC Addr'),
295                           ('nic%s_ip', 'NIC %s IP'),
296                           ]
297    nic_fields = []
298    for i in range(len(machine.nics)):
299        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
[227]300        if not i:
[252]301            data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
[133]302        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
303        data_dict['nic%s_ip' % i] = machine.nics[i].ip
304    if len(machine.nics) == 1:
305        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
306    return nic_fields
307
308def getDiskInfo(data_dict, machine):
[145]309    """Helper function for info, get data on disks for a machine.
310
311    Modifies data_dict to include the relevant data, and returns a list
312    of (key, name) pairs to display "name: data_dict[key]" to the user.
313    """
[133]314    data_dict['num_disks'] = len(machine.disks)
315    disk_fields_template = [('%s_size', '%s size')]
316    disk_fields = []
317    for disk in machine.disks:
318        name = disk.guest_device_name
[205]319        disk_fields.extend([(x % name, y % name) for x, y in 
320                            disk_fields_template])
[211]321        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
[133]322    return disk_fields
323
[205]324def command(user, fields):
325    """Handler for running commands like boot and delete on a VM."""
[207]326    back = fields.getfirst('back')
[205]327    try:
[209]328        d = controls.commandResult(user, fields)
[207]329        if d['command'] == 'Delete VM':
330            back = 'list'
[205]331    except InvalidInput, err:
[207]332        if not back:
[205]333            raise
[207]334        print >> sys.stderr, err
[205]335        result = None
336    else:
337        result = 'Success!'
[207]338        if not back:
[235]339            return templates.command(searchList=[d])
[207]340    if back == 'list':
[205]341        g.clear() #Changed global state
342        d = getListDict(user)
[207]343        d['result'] = result
[235]344        return templates.list(searchList=[d])
[207]345    elif back == 'info':
[209]346        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
[205]347        d = infoDict(user, machine)
[207]348        d['result'] = result
[235]349        return templates.info(searchList=[d])
[205]350    else:
[235]351        raise InvalidInput
352    ('back', back, 'Not a known back page.')
[205]353
354def modifyDict(user, fields):
[177]355    olddisk = {}
[161]356    transaction = ctx.current.create_transaction()
357    try:
[209]358        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
359        owner = validation.testOwner(user, fields.getfirst('owner'), machine)
360        admin = validation.testAdmin(user, fields.getfirst('administrator'),
361                                     machine)
362        contact = validation.testContact(user, fields.getfirst('contact'),
363                                         machine)
364        name = validation.testName(user, fields.getfirst('name'), machine)
[161]365        oldname = machine.name
[205]366        command = "modify"
[153]367
[161]368        memory = fields.getfirst('memory')
369        if memory is not None:
[209]370            memory = validation.validMemory(user, memory, machine, on=False)
[161]371            machine.memory = memory
[177]372 
[209]373        disksize = validation.testDisk(user, fields.getfirst('disk'))
[161]374        if disksize is not None:
[209]375            disksize = validation.validDisk(user, disksize, machine)
[177]376            disk = machine.disks[0]
377            if disk.size != disksize:
378                olddisk[disk.guest_device_name] = disksize
379                disk.size = disksize
380                ctx.current.save(disk)
[161]381       
[187]382        if owner is not None:
[161]383            machine.owner = owner
[187]384        if name is not None:
[161]385            machine.name = name
[187]386        if admin is not None:
387            machine.administrator = admin
388        if contact is not None:
389            machine.contact = contact
[161]390           
391        ctx.current.save(machine)
392        transaction.commit()
393    except:
394        transaction.rollback()
[163]395        raise
[177]396    for diskname in olddisk:
[209]397        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
[187]398    if name is not None:
[209]399        controls.renameMachine(machine, oldname, name)
[205]400    return dict(user=user,
401                command=command,
402                machine=machine)
403   
404def modify(user, fields):
405    """Handler for modifying attributes of a machine."""
406    try:
407        modify_dict = modifyDict(user, fields)
408    except InvalidInput, err:
[207]409        result = None
[209]410        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
[205]411    else:
412        machine = modify_dict['machine']
[209]413        result = 'Success!'
[205]414        err = None
415    info_dict = infoDict(user, machine)
416    info_dict['err'] = err
417    if err:
418        for field in fields.keys():
419            setattr(info_dict['defaults'], field, fields.getfirst(field))
[207]420    info_dict['result'] = result
[235]421    return templates.info(searchList=[info_dict])
[205]422   
[161]423
[205]424def helpHandler(user, fields):
[145]425    """Handler for help messages."""
[139]426    simple = fields.getfirst('simple')
427    subjects = fields.getlist('subject')
428   
[205]429    help_mapping = dict(paravm_console="""
[139]430ParaVM machines do not support console access over VNC.  To access
431these machines, you either need to boot with a liveCD and ssh in or
432hope that the sipb-xen maintainers add support for serial consoles.""",
[205]433                        hvm_paravm="""
[139]434HVM machines use the virtualization features of the processor, while
435ParaVM machines use Xen's emulation of virtualization features.  You
436want an HVM virtualized machine.""",
[205]437                        cpu_weight="""
438Don't ask us!  We're as mystified as you are.""",
439                        owner="""
440The owner field is used to determine <a
441href="help?subject=quotas">quotas</a>.  It must be the name of a
442locker that you are an AFS administrator of.  In particular, you or an
443AFS group you are a member of must have AFS rlidwka bits on the
[187]444locker.  You can check see who administers the LOCKER locker using the
[205]445command 'fs la /mit/LOCKER' on Athena.)  See also <a
446href="help?subject=administrator">administrator</a>.""",
447                        administrator="""
448The administrator field determines who can access the console and
449power on and off the machine.  This can be either a user or a moira
450group.""",
451                        quotas="""
452Quotas are determined on a per-locker basis.  Each quota may have a
453maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
454active machines."""
[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"""
586    if 'SSL_CLIENT_S_DN_Email' in os.environ:
587        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
[228]588        return username
[205]589    else:
[228]590        return 'moo'
[205]591
[209]592def main(operation, user, fields):   
[235]593    start_time = time.time()
[153]594    fun = mapping.get(operation, badOperation)
[205]595
596    if fun not in (helpHandler, ):
597        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
[119]598    try:
[235]599        checkpoint.checkpoint('Before')
[153]600        output = fun(u, fields)
[235]601        checkpoint.checkpoint('After')
[205]602
[209]603        headers = dict(DEFAULT_HEADERS)
[205]604        if isinstance(output, tuple):
605            new_headers, output = output
606            headers.update(new_headers)
607        e = revertStandardError()
[153]608        if e:
[205]609            output.addError(e)
610        printHeaders(headers)
[235]611        output_string =  str(output)
612        checkpoint.checkpoint('output as a string')
613        print output_string
614        print '<pre>%s</pre>' % checkpoint
[205]615    except Exception, err:
616        if not fields.has_key('js'):
617            if isinstance(err, CodeError):
618                print 'Content-Type: text/html\n'
619                e = revertStandardError()
620                print error(operation, u, fields, err, e)
621                sys.exit(1)
622            if isinstance(err, InvalidInput):
623                print 'Content-Type: text/html\n'
624                e = revertStandardError()
625                print invalidInput(operation, u, fields, err, e)
626                sys.exit(1)
[153]627        print 'Content-Type: text/plain\n'
[205]628        print 'Uh-oh!  We experienced an error.'
629        print 'Please email sipb-xen@mit.edu with the contents of this page.'
630        print '----'
631        e = revertStandardError()
[153]632        print e
633        print '----'
634        raise
[209]635
636if __name__ == '__main__':
637    fields = cgi.FieldStorage()
638    u = getUser()
639    g.user = u
640    operation = os.environ.get('PATH_INFO', '')
641    if not operation:
642        print "Status: 301 Moved Permanently"
643        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
644        sys.exit(0)
645
646    if operation.startswith('/'):
647        operation = operation[1:]
648    if not operation:
649        operation = 'list'
650
[248]651    if os.getenv("SIPB_XEN_PROFILE"):
652        import profile
653        profile.run('main(operation, u, fields)', 'log-'+operation)
654    else:
655        main(operation, u, fields)
Note: See TracBrowser for help on using the repository browser.