source: trunk/packages/sipb-xen-www/code/main.py @ 309

Last change on this file since 309 was 309, checked in by price, 16 years ago

move framebuffer tip to /help page

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