source: trunk/web/main.py @ 245

Last change on this file since 245 was 243, checked in by quentin, 17 years ago

Remove unnecessary constructors

Rename disk to disk_size in some (but not all!) places

Added MachineAccess? to all

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