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

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

Remove the hostname as separate from machine name.

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