865aa12f9627e2abecffebcde7146d48d1aecd95
[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     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
240 def 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
247 def 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
293 def 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
306 def 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
328 def 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
344 def 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
373 def 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(user, 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(user, fields.getfirst('disk'))
401         if disksize is not None:
402             disksize = validation.validDisk(user, 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
436 def 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
456 def helpHandler(user, fields):
457     """Handler for help messages."""
458     simple = fields.getfirst('simple')
459     subjects = fields.getlist('subject')
460
461     help_mapping = {'ParaVM Console': """
462 ParaVM machines do not support local console access over VNC.  To
463 access the serial console of these machines, you can SSH with Kerberos
464 to console.xvm.mit.edu, using the name of the machine as your
465 username.""",
466                     'HVM/ParaVM': """
467 HVM machines use the virtualization features of the processor, while
468 ParaVM machines use Xen's emulation of virtualization features.  You
469 want an HVM virtualized machine.""",
470                     'CPU Weight': """
471 Don't ask us!  We're as mystified as you are.""",
472                     'Owner': """
473 The owner field is used to determine <a
474 href="help?subject=Quotas">quotas</a>.  It must be the name of a
475 locker that you are an AFS administrator of.  In particular, you or an
476 AFS group you are a member of must have AFS rlidwka bits on the
477 locker.  You can check who administers the LOCKER locker using the
478 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
479 href="help?subject=Administrator">administrator</a>.""",
480                     'Administrator': """
481 The administrator field determines who can access the console and
482 power on and off the machine.  This can be either a user or a moira
483 group.""",
484                     'Quotas': """
485 Quotas are determined on a per-locker basis.  Each locker may have a
486 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
487 active machines.""",
488                     'Console': """
489 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
490 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
491 your machine will run just fine, but the applet's display of the
492 console 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
507 def badOperation(u, e):
508     """Function called when accessing an unknown URI."""
509     raise CodeError("Unknown operation")
510
511 def 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(user, machine, False)
583     checkpoint.checkpoint('Got mem')
584     max_disk = validation.maxDisk(user, 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
604 def 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
611 def unauthFront(_, fields):
612     """Information for unauth'd users."""
613     return templates.unauth(searchList=[{'simple' : True}])
614
615 mapping = 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
624 def 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
631 def 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
638 def 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
683 if __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)