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

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

Python 2.4 support

  • Property svn:executable set to *
File size: 19.1 KB
Line 
1#!/usr/bin/python
2
3import sys
4import cgi
5import os
6import string
7import subprocess
8import re
9import time
10import cPickle
11import base64
12import sha
13import hmac
14import datetime
15
16sys.stderr = sys.stdout
17sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
18
19from Cheetah.Template import Template
20from sipb_xen_database import *
21import random
22
23class MyException(Exception):
24    pass
25
26def helppopup(subj):
27    return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
28
29
30global_dict = {}
31global_dict['helppopup'] = helppopup
32
33
34# ... and stolen from xend/uuid.py
35def randomUUID():
36    """Generate a random UUID."""
37
38    return [ random.randint(0, 255) for _ in range(0, 16) ]
39
40def uuidToString(u):
41    return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
42                     "%02x" * 6]) % tuple(u)
43
44def maxMemory(user, machine=None):
45    return 256
46
47def maxDisk(user, machine=None):
48    return 10.0
49
50def haveAccess(user, machine):
51    if user.username == 'moo':
52        return True
53    return machine.owner == user.username
54
55def error(op, user, fields, err):
56    d = dict(op=op, user=user, errorMessage=str(err))
57    print Template(file='error.tmpl', searchList=[d, global_dict]);
58
59def validMachineName(name):
60    """Check that name is valid for a machine name"""
61    if not name:
62        return False
63    charset = string.ascii_letters + string.digits + '-_'
64    if name[0] in '-_' or len(name) > 22:
65        return False
66    for x in name:
67        if x not in charset:
68            return False
69    return True
70
71def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
72    """Kinit with a given username and keytab"""
73
74    p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
75                         stderr=subprocess.PIPE)
76    e = p.wait()
77    if e:
78        raise MyException("Error %s in kinit: %s" % (e, p.stderr.read()))
79
80def checkKinit():
81    """If we lack tickets, kinit."""
82    p = subprocess.Popen(['klist', '-s'])
83    if p.wait():
84        kinit()
85
86def remctl(*args, **kws):
87    """Perform a remctl and return the output.
88
89    kinits if necessary, and outputs errors to stderr.
90    """
91    checkKinit()
92    p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
93                         + list(args),
94                         stdout=subprocess.PIPE,
95                         stderr=subprocess.PIPE)
96    if kws.get('err'):
97        return p.stdout.read(), p.stderr.read()
98    if p.wait():
99        print >> sys.stderr, 'ERROR on remctl ', args
100        print >> sys.stderr, p.stderr.read()
101    return p.stdout.read()
102
103def makeDisks():
104    """Update the lvm partitions to include all disks in the database."""
105    remctl('web', 'lvcreate')
106
107def bootMachine(machine, cdtype):
108    """Boot a machine with a given boot CD.
109
110    If cdtype is None, give no boot cd.  Otherwise, it is the string
111    id of the CD (e.g. 'gutsy_i386')
112    """
113    if cdtype is not None:
114        remctl('web', 'vmboot', machine.name,
115               cdtype)
116    else:
117        remctl('web', 'vmboot', machine.name)
118
119def registerMachine(machine):
120    """Register a machine to be controlled by the web interface"""
121    remctl('web', 'register', machine.name)
122
123def unregisterMachine(machine):
124    """Unregister a machine to not be controlled by the web interface"""
125    remctl('web', 'unregister', machine.name)
126
127def parseStatus(s):
128    """Parse a status string into nested tuples of strings.
129
130    s = output of xm list --long <machine_name>
131    """
132    values = re.split('([()])', s)
133    stack = [[]]
134    for v in values[2:-2]: #remove initial and final '()'
135        if not v:
136            continue
137        v = v.strip()
138        if v == '(':
139            stack.append([])
140        elif v == ')':
141            if len(stack[-1]) == 1:
142                stack[-1].append('')
143            stack[-2].append(stack[-1])
144            stack.pop()
145        else:
146            if not v:
147                continue
148            stack[-1].extend(v.split())
149    return stack[-1]
150
151def getUptimes(machines):
152    """Return a dictionary mapping machine names to uptime strings"""
153    value_string = remctl('web', 'listvms')
154    lines = value_string.splitlines()
155    d = {}
156    for line in lines[1:]:
157        lst = line.split()
158        name, id = lst[:2]
159        uptime = ' '.join(lst[2:])
160        d[name] = uptime
161    return d
162
163def statusInfo(machine):
164    """Return the status list for a given machine.
165
166    Gets and parses xm list --long
167    """
168    value_string, err_string = remctl('list-long', machine.name, err=True)
169    if 'Unknown command' in err_string:
170        raise MyException("ERROR in remctl list-long %s is not registered" % (machine.name,))
171    elif 'does not exist' in err_string:
172        return None
173    elif err_string:
174        raise MyException("ERROR in remctl list-long %s%s" % (machine.name, err_string))
175    status = parseStatus(value_string)
176    return status
177
178def hasVnc(status):
179    """Does the machine with a given status list support VNC?"""
180    if status is None:
181        return False
182    for l in status:
183        if l[0] == 'device' and l[1][0] == 'vfb':
184            d = dict(l[1][1:])
185            return 'location' in d
186    return False
187
188def createVm(user, name, memory, disk, is_hvm, cdrom):
189    """Create a VM and put it in the database"""
190    # put stuff in the table
191    transaction = ctx.current.create_transaction()
192    try:
193        res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
194        id = res.fetchone()[0]
195        machine = Machine()
196        machine.machine_id = id
197        machine.name = name
198        machine.memory = memory
199        machine.owner = user.username
200        machine.contact = user.email
201        machine.uuid = uuidToString(randomUUID())
202        machine.boot_off_cd = True
203        machine_type = Type.get_by(hvm=is_hvm)
204        machine.type_id = machine_type.type_id
205        ctx.current.save(machine)
206        disk = Disk(machine.machine_id, 
207                    'hda', disk)
208        open = NIC.select_by(machine_id=None)
209        if not open: #No IPs left!
210            return "No IP addresses left!  Contact sipb-xen-dev@mit.edu"
211        nic = open[0]
212        nic.machine_id = machine.machine_id
213        nic.hostname = name
214        ctx.current.save(nic)   
215        ctx.current.save(disk)
216        transaction.commit()
217    except:
218        transaction.rollback()
219        raise
220    makeDisks()
221    registerMachine(machine)
222    # tell it to boot with cdrom
223    bootMachine(machine, cdrom)
224
225    return machine
226
227def validMemory(user, memory, machine=None):
228    try:
229        memory = int(memory)
230        if memory <= 0:
231            raise ValueError
232    except ValueError:
233        raise MyException("Invalid memory amount")
234    if memory > maxMemory(user, machine):
235        raise MyException("Too much memory requested")
236    return memory
237
238def validDisk(user, disk, machine=None):
239    try:
240        disk = float(disk)
241        if disk > maxDisk(user, machine):
242            raise MyException("Too much disk requested")
243        disk = int(disk * 1024)
244        if disk <= 0:
245            raise ValueError
246    except ValueError:
247        raise MyException("Invalid disk amount")
248    return disk
249
250def create(user, fields):
251    name = fields.getfirst('name')
252    if not validMachineName(name):
253        raise MyException("Invalid name '%s'" % name)
254    name = user.username + '_' + name.lower()
255
256    if Machine.get_by(name=name):
257        raise MyException("A machine named '%s' already exists" % name)
258   
259    memory = fields.getfirst('memory')
260    memory = validMemory(user, memory)
261   
262    disk = fields.getfirst('disk')
263    disk = validDisk(user, disk)
264
265    vm_type = fields.getfirst('vmtype')
266    if vm_type not in ('hvm', 'paravm'):
267        raise MyException("Invalid vm type '%s'"  % vm_type)   
268    is_hvm = (vm_type == 'hvm')
269
270    cdrom = fields.getfirst('cdrom')
271    if cdrom is not None and not CDROM.get(cdrom):
272        raise MyException("Invalid cdrom type '%s'" % cdrom)   
273   
274    machine = createVm(user, name, memory, disk, is_hvm, cdrom)
275    if isinstance(machine, basestring):
276        raise MyException(machine)
277    d = dict(user=user,
278             machine=machine)
279    print Template(file='create.tmpl',
280                   searchList=[d, global_dict]);
281
282def listVms(user, fields):
283    machines = [m for m in Machine.select() if haveAccess(user, m)]   
284    on = {}
285    has_vnc = {}
286    uptimes = getUptimes(machines)
287    on = uptimes
288    for m in machines:
289        if not on.get(m.name):
290            has_vnc[m.name] = 'Off'
291        elif m.type.hvm:
292            has_vnc[m.name] = True
293        else:
294            has_vnc[m.name] = "ParaVM"+helppopup("paravm_console")
295    #     for m in machines:
296    #         status = statusInfo(m)
297    #         on[m.name] = status is not None
298    #         has_vnc[m.name] = hasVnc(status)
299    d = dict(user=user,
300             maxmem=maxMemory(user),
301             maxdisk=maxDisk(user),
302             machines=machines,
303             has_vnc=has_vnc,
304             uptimes=uptimes,
305             cdroms=CDROM.select())
306    print Template(file='list.tmpl', searchList=[d, global_dict])
307
308def testMachineId(user, machineId, exists=True):
309    if machineId is None:
310        raise MyException("No machine ID specified")
311    try:
312        machineId = int(machineId)
313    except ValueError:
314        raise MyException("Invalid machine ID '%s'" % machineId)
315    machine = Machine.get(machineId)
316    if exists and machine is None:
317        raise MyException("No such machine ID '%s'" % machineId)
318    if not haveAccess(user, machine):
319        raise MyException("No access to machine ID '%s'" % machineId)
320    return machine
321
322def vnc(user, fields):
323    """VNC applet page.
324
325    Note that due to same-domain restrictions, the applet connects to
326    the webserver, which needs to forward those requests to the xen
327    server.  The Xen server runs another proxy that (1) authenticates
328    and (2) finds the correct port for the VM.
329
330    You might want iptables like:
331
332    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
333    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142
334    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
335    """
336    machine = testMachineId(user, fields.getfirst('machine_id'))
337    #XXX fix
338   
339    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
340
341    data = {}
342    data["user"] = user.username
343    data["machine"]=machine.name
344    data["expires"]=time.time()+(5*60)
345    pickledData = cPickle.dumps(data)
346    m = hmac.new(TOKEN_KEY, digestmod=sha)
347    m.update(pickledData)
348    token = {'data': pickledData, 'digest': m.digest()}
349    token = cPickle.dumps(token)
350    token = base64.urlsafe_b64encode(token)
351   
352    d = dict(user=user,
353             machine=machine,
354             hostname=os.environ.get('SERVER_NAME', 'localhost'),
355             authtoken=token)
356    print Template(file='vnc.tmpl',
357                   searchList=[d, global_dict])
358
359def getNicInfo(data_dict, machine):
360    data_dict['num_nics'] = len(machine.nics)
361    nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
362                           ('nic%s_mac', 'NIC %s MAC Addr'),
363                           ('nic%s_ip', 'NIC %s IP'),
364                           ]
365    nic_fields = []
366    for i in range(len(machine.nics)):
367        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
368        data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
369        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
370        data_dict['nic%s_ip' % i] = machine.nics[i].ip
371    if len(machine.nics) == 1:
372        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
373    return nic_fields
374
375def getDiskInfo(data_dict, machine):
376    data_dict['num_disks'] = len(machine.disks)
377    disk_fields_template = [('%s_size', '%s size')]
378    disk_fields = []
379    for disk in machine.disks:
380        name = disk.guest_device_name
381        disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
382        data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
383    return disk_fields
384
385def deleteVM(machine):
386    transaction = ctx.current.create_transaction()
387    delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
388    try:
389        for nic in machine.nics:
390            nic.machine_id = None
391            nic.hostname = None
392            ctx.current.save(nic)
393        for disk in machine.disks:
394            ctx.current.delete(disk)
395        ctx.current.delete(machine)
396        transaction.commit()
397    except:
398        transaction.rollback()
399        raise
400    for mname, dname in delete_disk_pairs:
401        remctl('web', 'lvremove', mname, dname)
402    unregisterMachine(machine)
403
404def command(user, fields):
405    print time.time()-start_time
406    machine = testMachineId(user, fields.getfirst('machine_id'))
407    action = fields.getfirst('action')
408    cdrom = fields.getfirst('cdrom')
409    print time.time()-start_time
410    if cdrom is not None and not CDROM.get(cdrom):
411        raise MyException("Invalid cdrom type '%s'" % cdrom)   
412    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
413        raise MyException("Invalid action '%s'" % action)
414    if action == 'Reboot':
415        if cdrom is not None:
416            remctl('reboot', machine.name, cdrom)
417        else:
418            remctl('reboot', machine.name)
419    elif action == 'Power on':
420        bootMachine(machine, cdrom)
421    elif action == 'Power off':
422        remctl('destroy', machine.name)
423    elif action == 'Shutdown':
424        remctl('shutdown', machine.name)
425    elif action == 'Delete VM':
426        deleteVM(machine)
427    print time.time()-start_time
428
429    d = dict(user=user,
430             command=action,
431             machine=machine)
432    print Template(file="command.tmpl", searchList=[d, global_dict])
433       
434def modify(user, fields):
435    machine = testMachineId(user, fields.getfirst('machine_id'))
436   
437def help(user, fields):
438    simple = fields.getfirst('simple')
439    subjects = fields.getlist('subject')
440   
441    mapping = dict(paravm_console="""
442ParaVM machines do not support console access over VNC.  To access
443these machines, you either need to boot with a liveCD and ssh in or
444hope that the sipb-xen maintainers add support for serial consoles.""",
445                   hvm_paravm="""
446HVM machines use the virtualization features of the processor, while
447ParaVM machines use Xen's emulation of virtualization features.  You
448want an HVM virtualized machine.""",
449                   cpu_weight="""Don't ask us!  We're as mystified as you are.""")
450   
451    d = dict(user=user,
452             simple=simple,
453             subjects=subjects,
454             mapping=mapping)
455   
456    print Template(file="help.tmpl", searchList=[d, global_dict])
457   
458
459def info(user, fields):
460    machine = testMachineId(user, fields.getfirst('machine_id'))
461    status = statusInfo(machine)
462    has_vnc = hasVnc(status)
463    if status is None:
464        main_status = dict(name=machine.name,
465                           memory=str(machine.memory))
466    else:
467        main_status = dict(status[1:])
468    start_time = float(main_status.get('start_time', 0))
469    uptime = datetime.timedelta(seconds=int(time.time()-start_time))
470    cpu_time_float = float(main_status.get('cpu_time', 0))
471    cputime = datetime.timedelta(seconds=int(cpu_time_float))
472    display_fields = """name uptime memory state cpu_weight on_reboot
473     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
474    display_fields = [('name', 'Name'),
475                      ('owner', 'Owner'),
476                      ('contact', 'Contact'),
477                      ('type', 'Type'),
478                      'NIC_INFO',
479                      ('uptime', 'uptime'),
480                      ('cputime', 'CPU usage'),
481                      ('memory', 'RAM'),
482                      'DISK_INFO',
483                      ('state', 'state (xen format)'),
484                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
485                      ('on_reboot', 'Action on VM reboot'),
486                      ('on_poweroff', 'Action on VM poweroff'),
487                      ('on_crash', 'Action on VM crash'),
488                      ('on_xend_start', 'Action on Xen start'),
489                      ('on_xend_stop', 'Action on Xen stop'),
490                      ('bootloader', 'Bootloader options'),
491                      ]
492    fields = []
493    machine_info = {}
494    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
495    machine_info['owner'] = machine.owner
496    machine_info['contact'] = machine.contact
497
498    nic_fields = getNicInfo(machine_info, machine)
499    nic_point = display_fields.index('NIC_INFO')
500    display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
501
502    disk_fields = getDiskInfo(machine_info, machine)
503    disk_point = display_fields.index('DISK_INFO')
504    display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
505   
506    main_status['memory'] += ' MB'
507    for field, disp in display_fields:
508        if field in ('uptime', 'cputime'):
509            fields.append((disp, locals()[field]))
510        elif field in main_status:
511            fields.append((disp, main_status[field]))
512        elif field in machine_info:
513            fields.append((disp, machine_info[field]))
514        else:
515            pass
516            #fields.append((disp, None))
517
518    d = dict(user=user,
519             cdroms=CDROM.select(),
520             on=status is not None,
521             machine=machine,
522             has_vnc=has_vnc,
523             uptime=str(uptime),
524             ram=machine.memory,
525             maxmem=maxMemory(user, machine),
526             maxdisk=maxDisk(user, machine),
527             fields = fields)
528    print Template(file='info.tmpl',
529                   searchList=[d, global_dict])
530
531mapping = dict(list=listVms,
532               vnc=vnc,
533               command=command,
534               modify=modify,
535               info=info,
536               create=create,
537               help=help)
538
539if __name__ == '__main__':
540    start_time = time.time()
541    fields = cgi.FieldStorage()
542    class User:
543        username = "moo"
544        email = 'moo@cow.com'
545    u = User()
546    if 'SSL_CLIENT_S_DN_Email' in os.environ:
547        username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
548        u.username = username
549        u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
550    else:
551        u.username = 'nobody'
552        u.email = None
553    connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
554    operation = os.environ.get('PATH_INFO', '')
555    #print 'Content-Type: text/plain\n'
556    #print operation
557    if not operation:
558        print "Status: 301 Moved Permanently"
559        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
560        sys.exit(0)
561    print 'Content-Type: text/html\n'
562
563    if operation.startswith('/'):
564        operation = operation[1:]
565    if not operation:
566        operation = 'list'
567   
568    fun = mapping.get(operation, 
569                      lambda u, e:
570                          error(operation, u, e,
571                                "Invalid operation '%s'" % operation))
572    if fun not in (help, ):
573        connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
574    try:
575        fun(u, fields)
576    except MyException, err:
577        error(operation, u, fields, err)
Note: See TracBrowser for help on using the repository browser.