source: trunk/web/main.py @ 248

Last change on this file since 248 was 248, checked in by quentin, 16 years ago

Make the profiling depend on an environment variable "SIPB_XEN_PROFILE" being set, else don't profile.

  • Property svn:executable set to *
File size: 21.9 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
14from StringIO import StringIO
15
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
35sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
36
37import templates
38from Cheetah.Template import Template
39from sipb_xen_database import Machine, CDROM, ctx, connect
40import validation
41from webcommon import InvalidInput, CodeError, g
42import controls
43
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
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>'
73
74Template.helppopup = staticmethod(helppopup)
75Template.err = None
76
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)
85
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
111DEFAULT_HEADERS = {'Content-Type': 'text/html'}
112
113def error(op, user, fields, err, emsg):
114    """Print an error page when a CodeError occurs"""
115    d = dict(op=op, user=user, errorMessage=str(err),
116             stderr=emsg)
117    return templates.error(searchList=[d])
118
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))
124    return templates.invalid(searchList=[d])
125
126def hasVnc(status):
127    """Does the machine with a given status list support VNC?"""
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
136def parseCreate(user, fields):
137    name = fields.getfirst('name')
138    if not validation.validMachineName(name):
139        raise InvalidInput('name', name, 'You must provide a machine name.')
140    name = name.lower()
141
142    if Machine.get_by(name=name):
143        raise InvalidInput('name', name,
144                           "Name already exists.")
145   
146    owner = validation.testOwner(user, fields.getfirst('owner'))
147
148    memory = fields.getfirst('memory')
149    memory = validation.validMemory(user, memory, on=True)
150   
151    disk_size = fields.getfirst('disk')
152    disk_size = validation.validDisk(user, disk_size)
153
154    vm_type = fields.getfirst('vmtype')
155    if vm_type not in ('hvm', 'paravm'):
156        raise CodeError("Invalid vm type '%s'"  % vm_type)   
157    is_hvm = (vm_type == 'hvm')
158
159    cdrom = fields.getfirst('cdrom')
160    if cdrom is not None and not CDROM.get(cdrom):
161        raise CodeError("Invalid cdrom type '%s'" % cdrom)
162    return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
163                owner=owner, is_hvm=is_hvm, cdrom=cdrom)
164
165def create(user, fields):
166    """Handler for create requests."""
167    try:
168        parsed_fields = parseCreate(user, fields)
169        machine = controls.createVm(**parsed_fields)
170    except InvalidInput, err:
171        pass
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']
182    return templates.list(searchList=[d])
183
184
185def getListDict(user):
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()
192    checkpoint.checkpoint('Got my machines')
193    on = {}
194    has_vnc = {}
195    on = g.uptimes
196    checkpoint.checkpoint('Got uptimes')
197    for m in machines:
198        m.uptime = g.uptimes.get(m)
199        if not on[m]:
200            has_vnc[m] = 'Off'
201        elif m.type.hvm:
202            has_vnc[m] = True
203        else:
204            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
205    max_memory = validation.maxMemory(user)
206    max_disk = validation.maxDisk(user)
207    checkpoint.checkpoint('Got max mem/disk')
208    defaults = Defaults(max_memory=max_memory,
209                        max_disk=max_disk,
210                        owner=user,
211                        cdrom='gutsy-i386')
212    checkpoint.checkpoint('Got defaults')
213    d = dict(user=user,
214             cant_add_vm=validation.cantAddVm(user),
215             max_memory=max_memory,
216             max_disk=max_disk,
217             defaults=defaults,
218             machines=machines,
219             has_vnc=has_vnc,
220             uptimes=g.uptimes,
221             cdroms=CDROM.select())
222    return d
223
224def listVms(user, fields):
225    """Handler for list requests."""
226    checkpoint.checkpoint('Getting list dict')
227    d = getListDict(user)
228    checkpoint.checkpoint('Got list dict')
229    return templates.list(searchList=[d])
230           
231def vnc(user, fields):
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
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
247
248    Remember to enable iptables!
249    echo 1 > /proc/sys/net/ipv4/ip_forward
250    """
251    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
252   
253    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
254
255    data = {}
256    data["user"] = user
257    data["machine"] = machine.name
258    data["expires"] = time.time()+(5*60)
259    pickled_data = cPickle.dumps(data)
260    m = hmac.new(TOKEN_KEY, digestmod=sha)
261    m.update(pickled_data)
262    token = {'data': pickled_data, 'digest': m.digest()}
263    token = cPickle.dumps(token)
264    token = base64.urlsafe_b64encode(token)
265   
266    status = controls.statusInfo(machine)
267    has_vnc = hasVnc(status)
268   
269    d = dict(user=user,
270             on=status,
271             has_vnc=has_vnc,
272             machine=machine,
273             hostname=os.environ.get('SERVER_NAME', 'localhost'),
274             authtoken=token)
275    return templates.vnc(searchList=[d])
276
277def getNicInfo(data_dict, machine):
278    """Helper function for info, get data on nics for a machine.
279
280    Modifies data_dict to include the relevant data, and returns a list
281    of (key, name) pairs to display "name: data_dict[key]" to the user.
282    """
283    data_dict['num_nics'] = len(machine.nics)
284    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
285                           ('nic%s_mac', 'NIC %s MAC Addr'),
286                           ('nic%s_ip', 'NIC %s IP'),
287                           ]
288    nic_fields = []
289    for i in range(len(machine.nics)):
290        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
291        if not i:
292            data_dict['nic%s_hostname' % i] = (machine.name + 
293                                               '.servers.csail.mit.edu')
294        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
295        data_dict['nic%s_ip' % i] = machine.nics[i].ip
296    if len(machine.nics) == 1:
297        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
298    return nic_fields
299
300def getDiskInfo(data_dict, machine):
301    """Helper function for info, get data on disks for a machine.
302
303    Modifies data_dict to include the relevant data, and returns a list
304    of (key, name) pairs to display "name: data_dict[key]" to the user.
305    """
306    data_dict['num_disks'] = len(machine.disks)
307    disk_fields_template = [('%s_size', '%s size')]
308    disk_fields = []
309    for disk in machine.disks:
310        name = disk.guest_device_name
311        disk_fields.extend([(x % name, y % name) for x, y in 
312                            disk_fields_template])
313        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
314    return disk_fields
315
316def command(user, fields):
317    """Handler for running commands like boot and delete on a VM."""
318    back = fields.getfirst('back')
319    try:
320        d = controls.commandResult(user, fields)
321        if d['command'] == 'Delete VM':
322            back = 'list'
323    except InvalidInput, err:
324        if not back:
325            raise
326        print >> sys.stderr, err
327        result = None
328    else:
329        result = 'Success!'
330        if not back:
331            return templates.command(searchList=[d])
332    if back == 'list':
333        g.clear() #Changed global state
334        d = getListDict(user)
335        d['result'] = result
336        return templates.list(searchList=[d])
337    elif back == 'info':
338        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
339        d = infoDict(user, machine)
340        d['result'] = result
341        return templates.info(searchList=[d])
342    else:
343        raise InvalidInput
344    ('back', back, 'Not a known back page.')
345
346def modifyDict(user, fields):
347    olddisk = {}
348    transaction = ctx.current.create_transaction()
349    try:
350        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
351        owner = validation.testOwner(user, fields.getfirst('owner'), machine)
352        admin = validation.testAdmin(user, fields.getfirst('administrator'),
353                                     machine)
354        contact = validation.testContact(user, fields.getfirst('contact'),
355                                         machine)
356        name = validation.testName(user, fields.getfirst('name'), machine)
357        oldname = machine.name
358        command = "modify"
359
360        memory = fields.getfirst('memory')
361        if memory is not None:
362            memory = validation.validMemory(user, memory, machine, on=False)
363            machine.memory = memory
364 
365        disksize = validation.testDisk(user, fields.getfirst('disk'))
366        if disksize is not None:
367            disksize = validation.validDisk(user, disksize, machine)
368            disk = machine.disks[0]
369            if disk.size != disksize:
370                olddisk[disk.guest_device_name] = disksize
371                disk.size = disksize
372                ctx.current.save(disk)
373       
374        if owner is not None:
375            machine.owner = owner
376        if name is not None:
377            machine.name = name
378        if admin is not None:
379            machine.administrator = admin
380        if contact is not None:
381            machine.contact = contact
382           
383        ctx.current.save(machine)
384        transaction.commit()
385    except:
386        transaction.rollback()
387        raise
388    for diskname in olddisk:
389        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
390    if name is not None:
391        controls.renameMachine(machine, oldname, name)
392    return dict(user=user,
393                command=command,
394                machine=machine)
395   
396def modify(user, fields):
397    """Handler for modifying attributes of a machine."""
398    try:
399        modify_dict = modifyDict(user, fields)
400    except InvalidInput, err:
401        result = None
402        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
403    else:
404        machine = modify_dict['machine']
405        result = 'Success!'
406        err = None
407    info_dict = infoDict(user, machine)
408    info_dict['err'] = err
409    if err:
410        for field in fields.keys():
411            setattr(info_dict['defaults'], field, fields.getfirst(field))
412    info_dict['result'] = result
413    return templates.info(searchList=[info_dict])
414   
415
416def helpHandler(user, fields):
417    """Handler for help messages."""
418    simple = fields.getfirst('simple')
419    subjects = fields.getlist('subject')
420   
421    help_mapping = dict(paravm_console="""
422ParaVM machines do not support console access over VNC.  To access
423these machines, you either need to boot with a liveCD and ssh in or
424hope that the sipb-xen maintainers add support for serial consoles.""",
425                        hvm_paravm="""
426HVM machines use the virtualization features of the processor, while
427ParaVM machines use Xen's emulation of virtualization features.  You
428want an HVM virtualized machine.""",
429                        cpu_weight="""
430Don't ask us!  We're as mystified as you are.""",
431                        owner="""
432The owner field is used to determine <a
433href="help?subject=quotas">quotas</a>.  It must be the name of a
434locker that you are an AFS administrator of.  In particular, you or an
435AFS group you are a member of must have AFS rlidwka bits on the
436locker.  You can check see who administers the LOCKER locker using the
437command 'fs la /mit/LOCKER' on Athena.)  See also <a
438href="help?subject=administrator">administrator</a>.""",
439                        administrator="""
440The administrator field determines who can access the console and
441power on and off the machine.  This can be either a user or a moira
442group.""",
443                        quotas="""
444Quotas are determined on a per-locker basis.  Each quota may have a
445maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
446active machines."""
447                   )
448   
449    if not subjects:
450        subjects = sorted(help_mapping.keys())
451       
452    d = dict(user=user,
453             simple=simple,
454             subjects=subjects,
455             mapping=help_mapping)
456   
457    return templates.help(searchList=[d])
458   
459
460def badOperation(u, e):
461    raise CodeError("Unknown operation")
462
463def infoDict(user, machine):
464    status = controls.statusInfo(machine)
465    checkpoint.checkpoint('Getting status info')
466    has_vnc = hasVnc(status)
467    if status is None:
468        main_status = dict(name=machine.name,
469                           memory=str(machine.memory))
470        uptime = None
471        cputime = None
472    else:
473        main_status = dict(status[1:])
474        start_time = float(main_status.get('start_time', 0))
475        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
476        cpu_time_float = float(main_status.get('cpu_time', 0))
477        cputime = datetime.timedelta(seconds=int(cpu_time_float))
478    checkpoint.checkpoint('Status')
479    display_fields = """name uptime memory state cpu_weight on_reboot
480     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
481    display_fields = [('name', 'Name'),
482                      ('owner', 'Owner'),
483                      ('administrator', 'Administrator'),
484                      ('contact', 'Contact'),
485                      ('type', 'Type'),
486                      'NIC_INFO',
487                      ('uptime', 'uptime'),
488                      ('cputime', 'CPU usage'),
489                      ('memory', 'RAM'),
490                      'DISK_INFO',
491                      ('state', 'state (xen format)'),
492                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
493                      ('on_reboot', 'Action on VM reboot'),
494                      ('on_poweroff', 'Action on VM poweroff'),
495                      ('on_crash', 'Action on VM crash'),
496                      ('on_xend_start', 'Action on Xen start'),
497                      ('on_xend_stop', 'Action on Xen stop'),
498                      ('bootloader', 'Bootloader options'),
499                      ]
500    fields = []
501    machine_info = {}
502    machine_info['name'] = machine.name
503    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
504    machine_info['owner'] = machine.owner
505    machine_info['administrator'] = machine.administrator
506    machine_info['contact'] = machine.contact
507
508    nic_fields = getNicInfo(machine_info, machine)
509    nic_point = display_fields.index('NIC_INFO')
510    display_fields = (display_fields[:nic_point] + nic_fields + 
511                      display_fields[nic_point+1:])
512
513    disk_fields = getDiskInfo(machine_info, machine)
514    disk_point = display_fields.index('DISK_INFO')
515    display_fields = (display_fields[:disk_point] + disk_fields + 
516                      display_fields[disk_point+1:])
517   
518    main_status['memory'] += ' MiB'
519    for field, disp in display_fields:
520        if field in ('uptime', 'cputime') and locals()[field] is not None:
521            fields.append((disp, locals()[field]))
522        elif field in machine_info:
523            fields.append((disp, machine_info[field]))
524        elif field in main_status:
525            fields.append((disp, main_status[field]))
526        else:
527            pass
528            #fields.append((disp, None))
529
530    checkpoint.checkpoint('Got fields')
531
532
533    max_mem = validation.maxMemory(user, machine, False)
534    checkpoint.checkpoint('Got mem')
535    max_disk = validation.maxDisk(user, machine)
536    defaults = Defaults()
537    for name in 'machine_id name administrator owner memory contact'.split():
538        setattr(defaults, name, getattr(machine, name))
539    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
540    checkpoint.checkpoint('Got defaults')
541    d = dict(user=user,
542             cdroms=CDROM.select(),
543             on=status is not None,
544             machine=machine,
545             defaults=defaults,
546             has_vnc=has_vnc,
547             uptime=str(uptime),
548             ram=machine.memory,
549             max_mem=max_mem,
550             max_disk=max_disk,
551             owner_help=helppopup("owner"),
552             fields = fields)
553    return d
554
555def info(user, fields):
556    """Handler for info on a single VM."""
557    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
558    d = infoDict(user, machine)
559    checkpoint.checkpoint('Got infodict')
560    return templates.info(searchList=[d])
561
562mapping = dict(list=listVms,
563               vnc=vnc,
564               command=command,
565               modify=modify,
566               info=info,
567               create=create,
568               help=helpHandler)
569
570def printHeaders(headers):
571    for key, value in headers.iteritems():
572        print '%s: %s' % (key, value)
573    print
574
575
576def getUser():
577    """Return the current user based on the SSL environment variables"""
578    if 'SSL_CLIENT_S_DN_Email' in os.environ:
579        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
580        return username
581    else:
582        return 'moo'
583
584def main(operation, user, fields):   
585    start_time = time.time()
586    fun = mapping.get(operation, badOperation)
587
588    if fun not in (helpHandler, ):
589        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
590    try:
591        checkpoint.checkpoint('Before')
592        output = fun(u, fields)
593        checkpoint.checkpoint('After')
594
595        headers = dict(DEFAULT_HEADERS)
596        if isinstance(output, tuple):
597            new_headers, output = output
598            headers.update(new_headers)
599        e = revertStandardError()
600        if e:
601            output.addError(e)
602        printHeaders(headers)
603        output_string =  str(output)
604        checkpoint.checkpoint('output as a string')
605        print output_string
606        print '<pre>%s</pre>' % checkpoint
607    except Exception, err:
608        if not fields.has_key('js'):
609            if isinstance(err, CodeError):
610                print 'Content-Type: text/html\n'
611                e = revertStandardError()
612                print error(operation, u, fields, err, e)
613                sys.exit(1)
614            if isinstance(err, InvalidInput):
615                print 'Content-Type: text/html\n'
616                e = revertStandardError()
617                print invalidInput(operation, u, fields, err, e)
618                sys.exit(1)
619        print 'Content-Type: text/plain\n'
620        print 'Uh-oh!  We experienced an error.'
621        print 'Please email sipb-xen@mit.edu with the contents of this page.'
622        print '----'
623        e = revertStandardError()
624        print e
625        print '----'
626        raise
627
628if __name__ == '__main__':
629    fields = cgi.FieldStorage()
630    u = getUser()
631    g.user = u
632    operation = os.environ.get('PATH_INFO', '')
633    if not operation:
634        print "Status: 301 Moved Permanently"
635        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
636        sys.exit(0)
637
638    if operation.startswith('/'):
639        operation = operation[1:]
640    if not operation:
641        operation = 'list'
642
643    if os.getenv("SIPB_XEN_PROFILE"):
644        import profile
645        profile.run('main(operation, u, fields)', 'log-'+operation)
646    else:
647        main(operation, u, fields)
Note: See TracBrowser for help on using the repository browser.