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

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

More updates.

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