source: trunk/web/main.py @ 246

Last change on this file since 246 was 246, checked in by ecprice, 16 years ago

Revert to old list method until we start updating the cache.
Fix a bug in getafsgroups.

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