source: trunk/web/templates/main.py @ 232

Last change on this file since 232 was 228, checked in by ecprice, 17 years ago

Remove the useless User class (now user is a string)
Allow creation under another owner.

  • Property svn:executable set to *
File size: 20.6 KB
RevLine 
[113]1#!/usr/bin/python
[205]2"""Main CGI script for web interface"""
[113]3
[205]4import base64
5import cPickle
[113]6import cgi
[205]7import datetime
8import hmac
[113]9import os
[205]10import sha
11import simplejson
12import sys
[118]13import time
[205]14from StringIO import StringIO
[113]15
[205]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
[113]36sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
37
38from Cheetah.Template import Template
[209]39from sipb_xen_database import Machine, CDROM, ctx, connect
40import validation
41from webcommon import InvalidInput, CodeError, g
42import controls
[113]43
[205]44def helppopup(subj):
45    """Return HTML code for a (?) link to a specified help topic"""
46    return ('<span class="helplink"><a href="help?subject=' + subj + 
47            '&amp;simple=true" target="_blank" ' + 
48            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
49
50def makeErrorPre(old, addition):
51    if addition is None:
52        return
53    if old:
54        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
55    else:
56        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
[139]57
[205]58Template.helppopup = staticmethod(helppopup)
59Template.err = None
[139]60
[205]61class JsonDict:
62    """Class to store a dictionary that will be converted to JSON"""
63    def __init__(self, **kws):
64        self.data = kws
65        if 'err' in kws:
66            err = kws['err']
67            del kws['err']
68            self.addError(err)
[139]69
[205]70    def __str__(self):
71        return simplejson.dumps(self.data)
72
73    def addError(self, text):
74        """Add stderr text to be displayed on the website."""
75        self.data['err'] = \
76            makeErrorPre(self.data.get('err'), text)
77
78class Defaults:
79    """Class to store default values for fields."""
80    memory = 256
81    disk = 4.0
82    cdrom = ''
83    name = ''
84    vmtype = 'hvm'
85    def __init__(self, max_memory=None, max_disk=None, **kws):
86        if max_memory is not None:
87            self.memory = min(self.memory, max_memory)
88        if max_disk is not None:
89            self.max_disk = min(self.disk, max_disk)
90        for key in kws:
91            setattr(self, key, kws[key])
92
93
94
[209]95DEFAULT_HEADERS = {'Content-Type': 'text/html'}
[205]96
[153]97def error(op, user, fields, err, emsg):
[145]98    """Print an error page when a CodeError occurs"""
[153]99    d = dict(op=op, user=user, errorMessage=str(err),
100             stderr=emsg)
[209]101    return Template(file='error.tmpl', searchList=[d])
[113]102
[153]103def invalidInput(op, user, fields, err, emsg):
104    """Print an error page when an InvalidInput exception occurs"""
105    d = dict(op=op, user=user, err_field=err.err_field,
106             err_value=str(err.err_value), stderr=emsg,
107             errorMessage=str(err))
[209]108    return Template(file='invalid.tmpl', searchList=[d])
[153]109
[119]110def hasVnc(status):
[133]111    """Does the machine with a given status list support VNC?"""
[119]112    if status is None:
113        return False
114    for l in status:
115        if l[0] == 'device' and l[1][0] == 'vfb':
116            d = dict(l[1][1:])
117            return 'location' in d
118    return False
119
[205]120def parseCreate(user, fields):
[134]121    name = fields.getfirst('name')
[209]122    if not validation.validMachineName(name):
[205]123        raise InvalidInput('name', name, 'You must provide a machine name.')
[162]124    name = name.lower()
[134]125
126    if Machine.get_by(name=name):
[153]127        raise InvalidInput('name', name,
[205]128                           "Name already exists.")
[113]129   
[228]130    owner = validation.testOwner(user, fields.getfirst('owner'))
131
[134]132    memory = fields.getfirst('memory')
[209]133    memory = validation.validMemory(user, memory, on=True)
[134]134   
135    disk = fields.getfirst('disk')
[209]136    disk = validation.validDisk(user, disk)
[134]137
[113]138    vm_type = fields.getfirst('vmtype')
139    if vm_type not in ('hvm', 'paravm'):
[145]140        raise CodeError("Invalid vm type '%s'"  % vm_type)   
[113]141    is_hvm = (vm_type == 'hvm')
142
143    cdrom = fields.getfirst('cdrom')
144    if cdrom is not None and not CDROM.get(cdrom):
[205]145        raise CodeError("Invalid cdrom type '%s'" % cdrom)
[228]146    return dict(contact=user, name=name, memory=memory, disk=disk,
147                owner=owner, is_hvm=is_hvm, cdrom=cdrom)
[113]148
[205]149def create(user, fields):
150    """Handler for create requests."""
151    try:
152        parsed_fields = parseCreate(user, fields)
[209]153        machine = controls.createVm(**parsed_fields)
[205]154    except InvalidInput, err:
[207]155        pass
[205]156    else:
157        err = None
158    g.clear() #Changed global state
159    d = getListDict(user)
160    d['err'] = err
161    if err:
162        for field in fields.keys():
163            setattr(d['defaults'], field, fields.getfirst(field))
164    else:
165        d['new_machine'] = parsed_fields['name']
[207]166    return Template(file='list.tmpl', searchList=[d])
[205]167
168
169def getListDict(user):
[209]170    machines = [m for m in Machine.select() 
171                if validation.haveAccess(user, m)]   
[133]172    on = {}
[119]173    has_vnc = {}
[152]174    on = g.uptimes
[136]175    for m in machines:
[205]176        m.uptime = g.uptimes.get(m)
[144]177        if not on[m]:
178            has_vnc[m] = 'Off'
[138]179        elif m.type.hvm:
[144]180            has_vnc[m] = True
[136]181        else:
[144]182            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
[209]183    max_memory = validation.maxMemory(user)
184    max_disk = validation.maxDisk(user)
[205]185    defaults = Defaults(max_memory=max_memory,
186                        max_disk=max_disk,
[228]187                        owner=user,
[205]188                        cdrom='gutsy-i386')
[113]189    d = dict(user=user,
[209]190             cant_add_vm=validation.cantAddVm(user),
[205]191             max_memory=max_memory,
[144]192             max_disk=max_disk,
[205]193             defaults=defaults,
[113]194             machines=machines,
[119]195             has_vnc=has_vnc,
[157]196             uptimes=g.uptimes,
[113]197             cdroms=CDROM.select())
[205]198    return d
[113]199
[205]200def listVms(user, fields):
201    """Handler for list requests."""
202    d = getListDict(user)
[207]203    return Template(file='list.tmpl', searchList=[d])
[205]204           
[113]205def vnc(user, fields):
[119]206    """VNC applet page.
207
208    Note that due to same-domain restrictions, the applet connects to
209    the webserver, which needs to forward those requests to the xen
210    server.  The Xen server runs another proxy that (1) authenticates
211    and (2) finds the correct port for the VM.
212
213    You might want iptables like:
214
[205]215    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
216      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
217    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
218      --dport 10003 -j SNAT --to-source 18.187.7.142
219    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
220      --dport 10003 -j ACCEPT
[145]221
222    Remember to enable iptables!
223    echo 1 > /proc/sys/net/ipv4/ip_forward
[119]224    """
[209]225    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
[118]226   
227    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
228
229    data = {}
[228]230    data["user"] = user
[205]231    data["machine"] = machine.name
232    data["expires"] = time.time()+(5*60)
233    pickled_data = cPickle.dumps(data)
[118]234    m = hmac.new(TOKEN_KEY, digestmod=sha)
[205]235    m.update(pickled_data)
236    token = {'data': pickled_data, 'digest': m.digest()}
[118]237    token = cPickle.dumps(token)
238    token = base64.urlsafe_b64encode(token)
239   
[209]240    status = controls.statusInfo(machine)
[152]241    has_vnc = hasVnc(status)
242   
[113]243    d = dict(user=user,
[152]244             on=status,
245             has_vnc=has_vnc,
[113]246             machine=machine,
[119]247             hostname=os.environ.get('SERVER_NAME', 'localhost'),
[113]248             authtoken=token)
[205]249    return Template(file='vnc.tmpl', searchList=[d])
[113]250
[133]251def getNicInfo(data_dict, machine):
[145]252    """Helper function for info, get data on nics for a machine.
253
254    Modifies data_dict to include the relevant data, and returns a list
255    of (key, name) pairs to display "name: data_dict[key]" to the user.
256    """
[133]257    data_dict['num_nics'] = len(machine.nics)
[227]258    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
[133]259                           ('nic%s_mac', 'NIC %s MAC Addr'),
260                           ('nic%s_ip', 'NIC %s IP'),
261                           ]
262    nic_fields = []
263    for i in range(len(machine.nics)):
264        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
[227]265        if not i:
266            data_dict['nic%s_hostname' % i] = (machine.name + 
267                                               '.servers.csail.mit.edu')
[133]268        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
269        data_dict['nic%s_ip' % i] = machine.nics[i].ip
270    if len(machine.nics) == 1:
271        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
272    return nic_fields
273
274def getDiskInfo(data_dict, machine):
[145]275    """Helper function for info, get data on disks for a machine.
276
277    Modifies data_dict to include the relevant data, and returns a list
278    of (key, name) pairs to display "name: data_dict[key]" to the user.
279    """
[133]280    data_dict['num_disks'] = len(machine.disks)
281    disk_fields_template = [('%s_size', '%s size')]
282    disk_fields = []
283    for disk in machine.disks:
284        name = disk.guest_device_name
[205]285        disk_fields.extend([(x % name, y % name) for x, y in 
286                            disk_fields_template])
[211]287        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
[133]288    return disk_fields
289
[205]290def command(user, fields):
291    """Handler for running commands like boot and delete on a VM."""
[207]292    back = fields.getfirst('back')
[205]293    try:
[209]294        d = controls.commandResult(user, fields)
[207]295        if d['command'] == 'Delete VM':
296            back = 'list'
[205]297    except InvalidInput, err:
[207]298        if not back:
[205]299            raise
[207]300        print >> sys.stderr, err
[205]301        result = None
302    else:
303        result = 'Success!'
[207]304        if not back:
[205]305            return Template(file='command.tmpl', searchList=[d])
[207]306    if back == 'list':
[205]307        g.clear() #Changed global state
308        d = getListDict(user)
[207]309        d['result'] = result
310        return Template(file='list.tmpl', searchList=[d])
311    elif back == 'info':
[209]312        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
[205]313        d = infoDict(user, machine)
[207]314        d['result'] = result
315        return Template(file='info.tmpl', searchList=[d])
[205]316    else:
[207]317        raise InvalidInput('back', back, 'Not a known back page.')
[205]318
319def modifyDict(user, fields):
[177]320    olddisk = {}
[161]321    transaction = ctx.current.create_transaction()
322    try:
[209]323        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
324        owner = validation.testOwner(user, fields.getfirst('owner'), machine)
325        admin = validation.testAdmin(user, fields.getfirst('administrator'),
326                                     machine)
327        contact = validation.testContact(user, fields.getfirst('contact'),
328                                         machine)
329        name = validation.testName(user, fields.getfirst('name'), machine)
[161]330        oldname = machine.name
[205]331        command = "modify"
[153]332
[161]333        memory = fields.getfirst('memory')
334        if memory is not None:
[209]335            memory = validation.validMemory(user, memory, machine, on=False)
[161]336            machine.memory = memory
[177]337 
[209]338        disksize = validation.testDisk(user, fields.getfirst('disk'))
[161]339        if disksize is not None:
[209]340            disksize = validation.validDisk(user, disksize, machine)
[177]341            disk = machine.disks[0]
342            if disk.size != disksize:
343                olddisk[disk.guest_device_name] = disksize
344                disk.size = disksize
345                ctx.current.save(disk)
[161]346       
[187]347        if owner is not None:
[161]348            machine.owner = owner
[187]349        if name is not None:
[161]350            machine.name = name
[187]351        if admin is not None:
352            machine.administrator = admin
353        if contact is not None:
354            machine.contact = contact
[161]355           
356        ctx.current.save(machine)
357        transaction.commit()
358    except:
359        transaction.rollback()
[163]360        raise
[177]361    for diskname in olddisk:
[209]362        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
[187]363    if name is not None:
[209]364        controls.renameMachine(machine, oldname, name)
[205]365    return dict(user=user,
366                command=command,
367                machine=machine)
368   
369def modify(user, fields):
370    """Handler for modifying attributes of a machine."""
371    try:
372        modify_dict = modifyDict(user, fields)
373    except InvalidInput, err:
[207]374        result = None
[209]375        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
[205]376    else:
377        machine = modify_dict['machine']
[209]378        result = 'Success!'
[205]379        err = None
380    info_dict = infoDict(user, machine)
381    info_dict['err'] = err
382    if err:
383        for field in fields.keys():
384            setattr(info_dict['defaults'], field, fields.getfirst(field))
[207]385    info_dict['result'] = result
386    return Template(file='info.tmpl', searchList=[info_dict])
[205]387   
[161]388
[205]389def helpHandler(user, fields):
[145]390    """Handler for help messages."""
[139]391    simple = fields.getfirst('simple')
392    subjects = fields.getlist('subject')
393   
[205]394    help_mapping = dict(paravm_console="""
[139]395ParaVM machines do not support console access over VNC.  To access
396these machines, you either need to boot with a liveCD and ssh in or
397hope that the sipb-xen maintainers add support for serial consoles.""",
[205]398                        hvm_paravm="""
[139]399HVM machines use the virtualization features of the processor, while
400ParaVM machines use Xen's emulation of virtualization features.  You
401want an HVM virtualized machine.""",
[205]402                        cpu_weight="""
403Don't ask us!  We're as mystified as you are.""",
404                        owner="""
405The owner field is used to determine <a
406href="help?subject=quotas">quotas</a>.  It must be the name of a
407locker that you are an AFS administrator of.  In particular, you or an
408AFS group you are a member of must have AFS rlidwka bits on the
[187]409locker.  You can check see who administers the LOCKER locker using the
[205]410command 'fs la /mit/LOCKER' on Athena.)  See also <a
411href="help?subject=administrator">administrator</a>.""",
412                        administrator="""
413The administrator field determines who can access the console and
414power on and off the machine.  This can be either a user or a moira
415group.""",
416                        quotas="""
417Quotas are determined on a per-locker basis.  Each quota may have a
418maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
419active machines."""
[187]420                   )
[139]421   
[187]422    if not subjects:
[205]423        subjects = sorted(help_mapping.keys())
[187]424       
[139]425    d = dict(user=user,
426             simple=simple,
427             subjects=subjects,
[205]428             mapping=help_mapping)
[139]429   
[205]430    return Template(file="help.tmpl", searchList=[d])
[139]431   
[133]432
[205]433def badOperation(u, e):
434    raise CodeError("Unknown operation")
435
436def infoDict(user, machine):
[209]437    status = controls.statusInfo(machine)
[133]438    has_vnc = hasVnc(status)
439    if status is None:
440        main_status = dict(name=machine.name,
441                           memory=str(machine.memory))
[205]442        uptime = None
443        cputime = None
[133]444    else:
445        main_status = dict(status[1:])
[167]446        start_time = float(main_status.get('start_time', 0))
447        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
448        cpu_time_float = float(main_status.get('cpu_time', 0))
449        cputime = datetime.timedelta(seconds=int(cpu_time_float))
[133]450    display_fields = """name uptime memory state cpu_weight on_reboot
451     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
452    display_fields = [('name', 'Name'),
453                      ('owner', 'Owner'),
[187]454                      ('administrator', 'Administrator'),
[133]455                      ('contact', 'Contact'),
[136]456                      ('type', 'Type'),
[133]457                      'NIC_INFO',
458                      ('uptime', 'uptime'),
459                      ('cputime', 'CPU usage'),
460                      ('memory', 'RAM'),
461                      'DISK_INFO',
462                      ('state', 'state (xen format)'),
[139]463                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
[133]464                      ('on_reboot', 'Action on VM reboot'),
465                      ('on_poweroff', 'Action on VM poweroff'),
466                      ('on_crash', 'Action on VM crash'),
467                      ('on_xend_start', 'Action on Xen start'),
468                      ('on_xend_stop', 'Action on Xen stop'),
469                      ('bootloader', 'Bootloader options'),
470                      ]
471    fields = []
472    machine_info = {}
[147]473    machine_info['name'] = machine.name
[136]474    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
[133]475    machine_info['owner'] = machine.owner
[187]476    machine_info['administrator'] = machine.administrator
[133]477    machine_info['contact'] = machine.contact
478
479    nic_fields = getNicInfo(machine_info, machine)
480    nic_point = display_fields.index('NIC_INFO')
[205]481    display_fields = (display_fields[:nic_point] + nic_fields + 
482                      display_fields[nic_point+1:])
[133]483
484    disk_fields = getDiskInfo(machine_info, machine)
485    disk_point = display_fields.index('DISK_INFO')
[205]486    display_fields = (display_fields[:disk_point] + disk_fields + 
487                      display_fields[disk_point+1:])
[133]488   
[211]489    main_status['memory'] += ' MiB'
[133]490    for field, disp in display_fields:
[167]491        if field in ('uptime', 'cputime') and locals()[field] is not None:
[133]492            fields.append((disp, locals()[field]))
[147]493        elif field in machine_info:
494            fields.append((disp, machine_info[field]))
[133]495        elif field in main_status:
496            fields.append((disp, main_status[field]))
497        else:
498            pass
499            #fields.append((disp, None))
[209]500    max_mem = validation.maxMemory(user, machine)
501    max_disk = validation.maxDisk(user, machine)
502    defaults = Defaults()
[205]503    for name in 'machine_id name administrator owner memory contact'.split():
504        setattr(defaults, name, getattr(machine, name))
505    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
[113]506    d = dict(user=user,
[133]507             cdroms=CDROM.select(),
508             on=status is not None,
509             machine=machine,
[205]510             defaults=defaults,
[133]511             has_vnc=has_vnc,
512             uptime=str(uptime),
513             ram=machine.memory,
[144]514             max_mem=max_mem,
515             max_disk=max_disk,
[166]516             owner_help=helppopup("owner"),
[133]517             fields = fields)
[205]518    return d
[113]519
[205]520def info(user, fields):
521    """Handler for info on a single VM."""
[209]522    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
[205]523    d = infoDict(user, machine)
524    return Template(file='info.tmpl', searchList=[d])
525
[113]526mapping = dict(list=listVms,
527               vnc=vnc,
[133]528               command=command,
529               modify=modify,
[113]530               info=info,
[139]531               create=create,
[205]532               help=helpHandler)
[113]533
[205]534def printHeaders(headers):
535    for key, value in headers.iteritems():
536        print '%s: %s' % (key, value)
537    print
538
539
540def getUser():
541    """Return the current user based on the SSL environment variables"""
542    if 'SSL_CLIENT_S_DN_Email' in os.environ:
543        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
[228]544        return username
[205]545    else:
[228]546        return 'moo'
[205]547
[209]548def main(operation, user, fields):   
[153]549    fun = mapping.get(operation, badOperation)
[205]550
551    if fun not in (helpHandler, ):
552        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
[119]553    try:
[153]554        output = fun(u, fields)
[205]555
[209]556        headers = dict(DEFAULT_HEADERS)
[205]557        if isinstance(output, tuple):
558            new_headers, output = output
559            headers.update(new_headers)
560
561        e = revertStandardError()
[153]562        if e:
[205]563            output.addError(e)
564        printHeaders(headers)
[153]565        print output
[205]566    except Exception, err:
567        if not fields.has_key('js'):
568            if isinstance(err, CodeError):
569                print 'Content-Type: text/html\n'
570                e = revertStandardError()
571                print error(operation, u, fields, err, e)
572                sys.exit(1)
573            if isinstance(err, InvalidInput):
574                print 'Content-Type: text/html\n'
575                e = revertStandardError()
576                print invalidInput(operation, u, fields, err, e)
577                sys.exit(1)
[153]578        print 'Content-Type: text/plain\n'
[205]579        print 'Uh-oh!  We experienced an error.'
580        print 'Please email sipb-xen@mit.edu with the contents of this page.'
581        print '----'
582        e = revertStandardError()
[153]583        print e
584        print '----'
585        raise
[209]586
587if __name__ == '__main__':
588    start_time = time.time()
589    fields = cgi.FieldStorage()
590    u = getUser()
591    g.user = u
592    operation = os.environ.get('PATH_INFO', '')
593    if not operation:
594        print "Status: 301 Moved Permanently"
595        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
596        sys.exit(0)
597
598    if operation.startswith('/'):
599        operation = operation[1:]
600    if not operation:
601        operation = 'list'
602
603    main(operation, u, fields)
604
Note: See TracBrowser for help on using the repository browser.