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

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

HVM/ParaVM

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