source: branches/wsgi/packages/sipb-xen-www/code/main.py @ 837

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

Use owner's quota on info page, not user's quota.

  • Property svn:executable set to *
File size: 24.1 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    can_clone = 'ice3' not in g.xmlist_raw
207    for m in machines:
208        if m not in xmlist:
209            has_vnc[m] = 'Off'
210            m.uptime = None
211        else:
212            m.uptime = xmlist[m]['uptime']
213            if xmlist[m]['console']:
214                has_vnc[m] = True
215            elif m.type.hvm:
216                has_vnc[m] = "WTF?"
217            else:
218                has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
219    max_memory = validation.maxMemory(user)
220    max_disk = validation.maxDisk(user)
221    checkpoint.checkpoint('Got max mem/disk')
222    defaults = Defaults(max_memory=max_memory,
223                        max_disk=max_disk,
224                        owner=user,
225                        cdrom='gutsy-i386')
226    checkpoint.checkpoint('Got defaults')
227    def sortkey(machine):
228        return (machine.owner != user, machine.owner, machine.name)
229    machines = sorted(machines, key=sortkey)
230    d = dict(user=user,
231             cant_add_vm=validation.cantAddVm(user),
232             max_memory=max_memory,
233             max_disk=max_disk,
234             defaults=defaults,
235             machines=machines,
236             has_vnc=has_vnc,
237             can_clone=can_clone)
238    return d
239
240def listVms(user, fields):
241    """Handler for list requests."""
242    checkpoint.checkpoint('Getting list dict')
243    d = getListDict(user)
244    checkpoint.checkpoint('Got list dict')
245    return templates.list(searchList=[d])
246
247def vnc(user, fields):
248    """VNC applet page.
249
250    Note that due to same-domain restrictions, the applet connects to
251    the webserver, which needs to forward those requests to the xen
252    server.  The Xen server runs another proxy that (1) authenticates
253    and (2) finds the correct port for the VM.
254
255    You might want iptables like:
256
257    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
258      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
259    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
260      --dport 10003 -j SNAT --to-source 18.187.7.142
261    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
262      --dport 10003 -j ACCEPT
263
264    Remember to enable iptables!
265    echo 1 > /proc/sys/net/ipv4/ip_forward
266    """
267    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
268
269    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
270
271    data = {}
272    data["user"] = user
273    data["machine"] = machine.name
274    data["expires"] = time.time()+(5*60)
275    pickled_data = cPickle.dumps(data)
276    m = hmac.new(TOKEN_KEY, digestmod=sha)
277    m.update(pickled_data)
278    token = {'data': pickled_data, 'digest': m.digest()}
279    token = cPickle.dumps(token)
280    token = base64.urlsafe_b64encode(token)
281
282    status = controls.statusInfo(machine)
283    has_vnc = hasVnc(status)
284
285    d = dict(user=user,
286             on=status,
287             has_vnc=has_vnc,
288             machine=machine,
289             hostname=os.environ.get('SERVER_NAME', 'localhost'),
290             authtoken=token)
291    return templates.vnc(searchList=[d])
292
293def getHostname(nic):
294    """Find the hostname associated with a NIC.
295
296    XXX this should be merged with the similar logic in DNS and DHCP.
297    """
298    if nic.hostname and '.' in nic.hostname:
299        return nic.hostname
300    elif nic.machine:
301        return nic.machine.name + '.xvm.mit.edu'
302    else:
303        return None
304
305
306def getNicInfo(data_dict, machine):
307    """Helper function for info, get data on nics for a machine.
308
309    Modifies data_dict to include the relevant data, and returns a list
310    of (key, name) pairs to display "name: data_dict[key]" to the user.
311    """
312    data_dict['num_nics'] = len(machine.nics)
313    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
314                           ('nic%s_mac', 'NIC %s MAC Addr'),
315                           ('nic%s_ip', 'NIC %s IP'),
316                           ]
317    nic_fields = []
318    for i in range(len(machine.nics)):
319        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
320        if not i:
321            data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
322        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
323        data_dict['nic%s_ip' % i] = machine.nics[i].ip
324    if len(machine.nics) == 1:
325        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
326    return nic_fields
327
328def getDiskInfo(data_dict, machine):
329    """Helper function for info, get data on disks for a machine.
330
331    Modifies data_dict to include the relevant data, and returns a list
332    of (key, name) pairs to display "name: data_dict[key]" to the user.
333    """
334    data_dict['num_disks'] = len(machine.disks)
335    disk_fields_template = [('%s_size', '%s size')]
336    disk_fields = []
337    for disk in machine.disks:
338        name = disk.guest_device_name
339        disk_fields.extend([(x % name, y % name) for x, y in
340                            disk_fields_template])
341        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
342    return disk_fields
343
344def command(user, fields):
345    """Handler for running commands like boot and delete on a VM."""
346    back = fields.getfirst('back')
347    try:
348        d = controls.commandResult(user, fields)
349        if d['command'] == 'Delete VM':
350            back = 'list'
351    except InvalidInput, err:
352        if not back:
353            raise
354        #print >> sys.stderr, err
355        result = err
356    else:
357        result = 'Success!'
358        if not back:
359            return templates.command(searchList=[d])
360    if back == 'list':
361        g.clear() #Changed global state
362        d = getListDict(user)
363        d['result'] = result
364        return templates.list(searchList=[d])
365    elif back == 'info':
366        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
367        return ({'Status': '302',
368                 'Location': '/info?machine_id=%d' % machine.machine_id},
369                "You shouldn't see this message.")
370    else:
371        raise InvalidInput('back', back, 'Not a known back page.')
372
373def modifyDict(user, fields):
374    """Modify a machine as specified by CGI arguments.
375
376    Return a list of local variables for modify.tmpl.
377    """
378    olddisk = {}
379    transaction = ctx.current.create_transaction()
380    try:
381        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
382        owner = validation.testOwner(user, fields.getfirst('owner'), machine)
383        admin = validation.testAdmin(user, fields.getfirst('administrator'),
384                                     machine)
385        contact = validation.testContact(user, fields.getfirst('contact'),
386                                         machine)
387        name = validation.testName(user, fields.getfirst('name'), machine)
388        oldname = machine.name
389        command = "modify"
390
391        memory = fields.getfirst('memory')
392        if memory is not None:
393            memory = validation.validMemory(owner, memory, machine, on=False)
394            machine.memory = memory
395
396        vm_type = validation.validVmType(fields.getfirst('vmtype'))
397        if vm_type is not None:
398            machine.type = vm_type
399
400        disksize = validation.testDisk(owner, fields.getfirst('disk'))
401        if disksize is not None:
402            disksize = validation.validDisk(owner, disksize, machine)
403            disk = machine.disks[0]
404            if disk.size != disksize:
405                olddisk[disk.guest_device_name] = disksize
406                disk.size = disksize
407                ctx.current.save(disk)
408
409        update_acl = False
410        if owner is not None and owner != machine.owner:
411            machine.owner = owner
412            update_acl = True
413        if name is not None:
414            machine.name = name
415        if admin is not None and admin != machine.administrator:
416            machine.administrator = admin
417            update_acl = True
418        if contact is not None:
419            machine.contact = contact
420
421        ctx.current.save(machine)
422        if update_acl:
423            cache_acls.refreshMachine(machine)
424        transaction.commit()
425    except:
426        transaction.rollback()
427        raise
428    for diskname in olddisk:
429        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
430    if name is not None:
431        controls.renameMachine(machine, oldname, name)
432    return dict(user=user,
433                command=command,
434                machine=machine)
435
436def modify(user, fields):
437    """Handler for modifying attributes of a machine."""
438    try:
439        modify_dict = modifyDict(user, fields)
440    except InvalidInput, err:
441        result = None
442        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
443    else:
444        machine = modify_dict['machine']
445        result = 'Success!'
446        err = None
447    info_dict = infoDict(user, machine)
448    info_dict['err'] = err
449    if err:
450        for field in fields.keys():
451            setattr(info_dict['defaults'], field, fields.getfirst(field))
452    info_dict['result'] = result
453    return templates.info(searchList=[info_dict])
454
455
456def helpHandler(user, fields):
457    """Handler for help messages."""
458    simple = fields.getfirst('simple')
459    subjects = fields.getlist('subject')
460
461    help_mapping = {'ParaVM Console': """
462ParaVM machines do not support local console access over VNC.  To
463access the serial console of these machines, you can SSH with Kerberos
464to console.xvm.mit.edu, using the name of the machine as your
465username.""",
466                    'HVM/ParaVM': """
467HVM machines use the virtualization features of the processor, while
468ParaVM machines use Xen's emulation of virtualization features.  You
469want an HVM virtualized machine.""",
470                    'CPU Weight': """
471Don't ask us!  We're as mystified as you are.""",
472                    'Owner': """
473The owner field is used to determine <a
474href="help?subject=Quotas">quotas</a>.  It must be the name of a
475locker that you are an AFS administrator of.  In particular, you or an
476AFS group you are a member of must have AFS rlidwka bits on the
477locker.  You can check who administers the LOCKER locker using the
478commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
479href="help?subject=Administrator">administrator</a>.""",
480                    'Administrator': """
481The administrator field determines who can access the console and
482power on and off the machine.  This can be either a user or a moira
483group.""",
484                    'Quotas': """
485Quotas are determined on a per-locker basis.  Each locker may have a
486maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
487active machines.""",
488                    'Console': """
489<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
490setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
491your machine will run just fine, but the applet's display of the
492console will suffer artifacts.
493"""
494                    }
495
496    if not subjects:
497        subjects = sorted(help_mapping.keys())
498
499    d = dict(user=user,
500             simple=simple,
501             subjects=subjects,
502             mapping=help_mapping)
503
504    return templates.help(searchList=[d])
505
506
507def badOperation(u, e):
508    """Function called when accessing an unknown URI."""
509    raise CodeError("Unknown operation")
510
511def infoDict(user, machine):
512    """Get the variables used by info.tmpl."""
513    status = controls.statusInfo(machine)
514    checkpoint.checkpoint('Getting status info')
515    has_vnc = hasVnc(status)
516    if status is None:
517        main_status = dict(name=machine.name,
518                           memory=str(machine.memory))
519        uptime = None
520        cputime = None
521    else:
522        main_status = dict(status[1:])
523        start_time = float(main_status.get('start_time', 0))
524        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
525        cpu_time_float = float(main_status.get('cpu_time', 0))
526        cputime = datetime.timedelta(seconds=int(cpu_time_float))
527    checkpoint.checkpoint('Status')
528    display_fields = """name uptime memory state cpu_weight on_reboot
529     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
530    display_fields = [('name', 'Name'),
531                      ('owner', 'Owner'),
532                      ('administrator', 'Administrator'),
533                      ('contact', 'Contact'),
534                      ('type', 'Type'),
535                      'NIC_INFO',
536                      ('uptime', 'uptime'),
537                      ('cputime', 'CPU usage'),
538                      ('memory', 'RAM'),
539                      'DISK_INFO',
540                      ('state', 'state (xen format)'),
541                      ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
542                      ('on_reboot', 'Action on VM reboot'),
543                      ('on_poweroff', 'Action on VM poweroff'),
544                      ('on_crash', 'Action on VM crash'),
545                      ('on_xend_start', 'Action on Xen start'),
546                      ('on_xend_stop', 'Action on Xen stop'),
547                      ('bootloader', 'Bootloader options'),
548                      ]
549    fields = []
550    machine_info = {}
551    machine_info['name'] = machine.name
552    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
553    machine_info['owner'] = machine.owner
554    machine_info['administrator'] = machine.administrator
555    machine_info['contact'] = machine.contact
556
557    nic_fields = getNicInfo(machine_info, machine)
558    nic_point = display_fields.index('NIC_INFO')
559    display_fields = (display_fields[:nic_point] + nic_fields +
560                      display_fields[nic_point+1:])
561
562    disk_fields = getDiskInfo(machine_info, machine)
563    disk_point = display_fields.index('DISK_INFO')
564    display_fields = (display_fields[:disk_point] + disk_fields +
565                      display_fields[disk_point+1:])
566
567    main_status['memory'] += ' MiB'
568    for field, disp in display_fields:
569        if field in ('uptime', 'cputime') and locals()[field] is not None:
570            fields.append((disp, locals()[field]))
571        elif field in machine_info:
572            fields.append((disp, machine_info[field]))
573        elif field in main_status:
574            fields.append((disp, main_status[field]))
575        else:
576            pass
577            #fields.append((disp, None))
578
579    checkpoint.checkpoint('Got fields')
580
581
582    max_mem = validation.maxMemory(machine.owner, machine, False)
583    checkpoint.checkpoint('Got mem')
584    max_disk = validation.maxDisk(machine.owner, machine)
585    defaults = Defaults()
586    for name in 'machine_id name administrator owner memory contact'.split():
587        setattr(defaults, name, getattr(machine, name))
588    defaults.type = machine.type.type_id
589    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
590    checkpoint.checkpoint('Got defaults')
591    d = dict(user=user,
592             on=status is not None,
593             machine=machine,
594             defaults=defaults,
595             has_vnc=has_vnc,
596             uptime=str(uptime),
597             ram=machine.memory,
598             max_mem=max_mem,
599             max_disk=max_disk,
600             owner_help=helppopup("Owner"),
601             fields = fields)
602    return d
603
604def info(user, fields):
605    """Handler for info on a single VM."""
606    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
607    d = infoDict(user, machine)
608    checkpoint.checkpoint('Got infodict')
609    return templates.info(searchList=[d])
610
611def unauthFront(_, fields):
612    """Information for unauth'd users."""
613    return templates.unauth(searchList=[{'simple' : True}])
614
615mapping = dict(list=listVms,
616               vnc=vnc,
617               command=command,
618               modify=modify,
619               info=info,
620               create=create,
621               help=helpHandler,
622               unauth=unauthFront)
623
624def printHeaders(headers):
625    """Print a dictionary as HTTP headers."""
626    for key, value in headers.iteritems():
627        print '%s: %s' % (key, value)
628    print
629
630
631def getUser():
632    """Return the current user based on the SSL environment variables"""
633    email = os.environ.get('SSL_CLIENT_S_DN_Email', None)
634    if email is None:
635        return None
636    return email.split("@")[0]
637
638def main(operation, user, fields):
639    start_time = time.time()
640    fun = mapping.get(operation, badOperation)
641
642    if fun not in (helpHandler, ):
643        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
644    try:
645        checkpoint.checkpoint('Before')
646        output = fun(u, fields)
647        checkpoint.checkpoint('After')
648
649        headers = dict(DEFAULT_HEADERS)
650        if isinstance(output, tuple):
651            new_headers, output = output
652            headers.update(new_headers)
653        e = revertStandardError()
654        if e:
655            output.addError(e)
656        printHeaders(headers)
657        output_string =  str(output)
658        checkpoint.checkpoint('output as a string')
659        print output_string
660        if fields.has_key('timedebug'):
661            print '<pre>%s</pre>' % checkpoint
662    except Exception, err:
663        if not fields.has_key('js'):
664            if isinstance(err, CodeError):
665                print 'Content-Type: text/html\n'
666                e = revertStandardError()
667                print error(operation, u, fields, err, e)
668                sys.exit(1)
669            if isinstance(err, InvalidInput):
670                print 'Content-Type: text/html\n'
671                e = revertStandardError()
672                print invalidInput(operation, u, fields, err, e)
673                sys.exit(1)
674        print 'Content-Type: text/plain\n'
675        print 'Uh-oh!  We experienced an error.'
676        print 'Please email xvm-dev@mit.edu with the contents of this page.'
677        print '----'
678        e = revertStandardError()
679        print e
680        print '----'
681        raise
682
683if __name__ == '__main__':
684    fields = cgi.FieldStorage()
685
686    if fields.has_key('sqldebug'):
687        import logging
688        logging.basicConfig()
689        logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
690        logging.getLogger('sqlalchemy.orm.unitofwork').setLevel(logging.INFO)
691
692    u = getUser()
693    g.user = u
694    operation = os.environ.get('PATH_INFO', '')
695    if not operation:
696        print "Status: 301 Moved Permanently"
697        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
698        sys.exit(0)
699
700    if u is None:
701        operation = 'unauth'
702
703    if operation.startswith('/'):
704        operation = operation[1:]
705    if not operation:
706        operation = 'list'
707
708    if os.getenv("SIPB_XEN_PROFILE"):
709        import profile
710        profile.run('main(operation, u, fields)', 'log-'+operation)
711    else:
712        main(operation, u, fields)
Note: See TracBrowser for help on using the repository browser.