Prettier help titles
[invirt/packages/invirt-web.git] / code / main.py
1 #!/usr/bin/python
2 """Main CGI script for web interface"""
3
4 import base64
5 import cPickle
6 import cgi
7 import datetime
8 import hmac
9 import os
10 import sha
11 import simplejson
12 import sys
13 import time
14 import urllib
15 from StringIO import StringIO
16
17 def 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
26 def printError():
27     """Revert stderr to stdout, and print the contents of stderr"""
28     if isinstance(sys.stderr, StringIO):
29         print revertStandardError()
30
31 if __name__ == '__main__':
32     import atexit
33     atexit.register(printError)
34     sys.stderr = StringIO()
35
36 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
37
38 import templates
39 from Cheetah.Template import Template
40 import sipb_xen_database
41 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
42 import validation
43 import cache_acls
44 from webcommon import InvalidInput, CodeError, g
45 import controls
46
47 class 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
60 checkpoint = Checkpoint()
61
62 def jquote(string):
63     return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
64
65 def 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
72 def 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
80 Template.sipb_xen_database = sipb_xen_database
81 Template.helppopup = staticmethod(helppopup)
82 Template.err = None
83
84 class 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
101 class 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
120 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
121
122 def 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
128 def 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
135 def 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
145 def 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
177 def 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
197 def 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
238 def 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
245 def 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
291 def 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
304 def 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
326 def 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
342 def 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
371 def 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
434 def 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
454 def helpHandler(user, fields):
455     """Handler for help messages."""
456     simple = fields.getfirst('simple')
457     subjects = fields.getlist('subject')
458
459     help_mapping = {'ParaVM Console': """
460 ParaVM machines do not support local console access over VNC.  To
461 access the serial console of these machines, you can SSH with Kerberos
462 to console.xvm.mit.edu, using the name of the machine as your
463 username.""",
464                     'HVM/ParaVM': """
465 HVM machines use the virtualization features of the processor, while
466 ParaVM machines use Xen's emulation of virtualization features.  You
467 want an HVM virtualized machine.""",
468                     'CPU Weight': """
469 Don't ask us!  We're as mystified as you are.""",
470                     'Owner': """
471 The owner field is used to determine <a
472 href="help?subject=Quotas">quotas</a>.  It must be the name of a
473 locker that you are an AFS administrator of.  In particular, you or an
474 AFS group you are a member of must have AFS rlidwka bits on the
475 locker.  You can check who administers the LOCKER locker using the
476 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
477 href="help?subject=Administrator">administrator</a>.""",
478                     'Administrator': """
479 The administrator field determines who can access the console and
480 power on and off the machine.  This can be either a user or a moira
481 group.""",
482                     'Quotas': """
483 Quotas are determined on a per-locker basis.  Each locker may have a
484 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
485 active machines.""",
486                     'Console': """
487 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
488 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
489 your machine will run just fine, but the applet's display of the
490 console 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
505 def badOperation(u, e):
506     """Function called when accessing an unknown URI."""
507     raise CodeError("Unknown operation")
508
509 def 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
602 def 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
609 def unauthFront(_, fields):
610     """Information for unauth'd users."""
611     return templates.unauth(searchList=[{'simple' : True}])
612
613 mapping = 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
622 def 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
629 def 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
636 def 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
681 if __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)