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

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

Default type in info page

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