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

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

ParaVM on list

  • Property svn:executable set to *
File size: 17.5 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 == 'moo':
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 not on.get(m.name):
280            has_vnc[m.name] = 'Off'
281        elif m.type.hvm:
282            has_vnc[m.name] = True
283        else:
284            help_name = 'paravm_console'
285            has_vnc[m.name] = 'ParaVM <span class="helplink"><a href="help?subject=%s&amp;simple=true" target="_blank" onclick="return helppopup(\'%s\')">(?)</a></span>' % (help_name, help_name)
286    #     for m in machines:
287    #         status = statusInfo(m)
288    #         on[m.name] = status is not None
289    #         has_vnc[m.name] = hasVnc(status)
290    d = dict(user=user,
291             maxmem=maxMemory(user),
292             maxdisk=maxDisk(user),
293             machines=machines,
294             has_vnc=has_vnc,
295             uptimes=uptimes,
296             cdroms=CDROM.select())
297    print Template(file='list.tmpl', searchList=d)
298
299def testMachineId(user, machineId, exists=True):
300    if machineId is None:
301        raise MyException("No machine ID specified")
302    try:
303        machineId = int(machineId)
304    except ValueError:
305        raise MyException("Invalid machine ID '%s'" % machineId)
306    machine = Machine.get(machineId)
307    if exists and machine is None:
308        raise MyException("No such machine ID '%s'" % machineId)
309    if not haveAccess(user, machine):
310        raise MyException("No access to machine ID '%s'" % machineId)
311    return machine
312
313def vnc(user, fields):
314    """VNC applet page.
315
316    Note that due to same-domain restrictions, the applet connects to
317    the webserver, which needs to forward those requests to the xen
318    server.  The Xen server runs another proxy that (1) authenticates
319    and (2) finds the correct port for the VM.
320
321    You might want iptables like:
322
323    -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
324    -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
325    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
326    """
327    machine = testMachineId(user, fields.getfirst('machine_id'))
328    #XXX fix
329   
330    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
331
332    data = {}
333    data["user"] = user.username
334    data["machine"]=machine.name
335    data["expires"]=time.time()+(5*60)
336    pickledData = cPickle.dumps(data)
337    m = hmac.new(TOKEN_KEY, digestmod=sha)
338    m.update(pickledData)
339    token = {'data': pickledData, 'digest': m.digest()}
340    token = cPickle.dumps(token)
341    token = base64.urlsafe_b64encode(token)
342   
343    d = dict(user=user,
344             machine=machine,
345             hostname=os.environ.get('SERVER_NAME', 'localhost'),
346             authtoken=token)
347    print Template(file='vnc.tmpl',
348                   searchList=d)
349
350def getNicInfo(data_dict, machine):
351    data_dict['num_nics'] = len(machine.nics)
352    nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
353                           ('nic%s_mac', 'NIC %s MAC Addr'),
354                           ('nic%s_ip', 'NIC %s IP'),
355                           ]
356    nic_fields = []
357    for i in range(len(machine.nics)):
358        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
359        data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
360        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
361        data_dict['nic%s_ip' % i] = machine.nics[i].ip
362    if len(machine.nics) == 1:
363        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
364    return nic_fields
365
366def getDiskInfo(data_dict, machine):
367    data_dict['num_disks'] = len(machine.disks)
368    disk_fields_template = [('%s_size', '%s size')]
369    disk_fields = []
370    for disk in machine.disks:
371        name = disk.guest_device_name
372        disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
373        data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
374    return disk_fields
375
376def deleteVM(machine):
377    transaction = ctx.current.create_transaction()
378    delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
379    try:
380        for nic in machine.nics:
381            nic.machine_id = None
382            nic.hostname = None
383            ctx.current.save(nic)
384        for disk in machine.disks:
385            ctx.current.delete(disk)
386        ctx.current.delete(machine)
387        transaction.commit()
388    except:
389        transaction.rollback()
390        raise
391    for mname, dname in delete_disk_pairs:
392        remctl('web', 'lvremove', mname, dname)
393    unregisterMachine(machine)
394
395def command(user, fields):
396    print time.time()-start_time
397    machine = testMachineId(user, fields.getfirst('machine_id'))
398    action = fields.getfirst('action')
399    cdrom = fields.getfirst('cdrom')
400    print time.time()-start_time
401    if cdrom is not None and not CDROM.get(cdrom):
402        raise MyException("Invalid cdrom type '%s'" % cdrom)   
403    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
404        raise MyException("Invalid action '%s'" % action)
405    if action == 'Reboot':
406        if cdrom is not None:
407            remctl('reboot', machine.name, cdrom)
408        else:
409            remctl('reboot', machine.name)
410    elif action == 'Power on':
411        bootMachine(machine, cdrom)
412    elif action == 'Power off':
413        remctl('destroy', machine.name)
414    elif action == 'Shutdown':
415        remctl('shutdown', machine.name)
416    elif action == 'Delete VM':
417        deleteVM(machine)
418    print time.time()-start_time
419
420    d = dict(user=user,
421             command=action,
422             machine=machine)
423    print Template(file="command.tmpl", searchList=d)
424       
425def modify(user, fields):
426    machine = testMachineId(user, fields.getfirst('machine_id'))
427   
428
429def info(user, fields):
430    machine = testMachineId(user, fields.getfirst('machine_id'))
431    status = statusInfo(machine)
432    has_vnc = hasVnc(status)
433    if status is None:
434        main_status = dict(name=machine.name,
435                           memory=str(machine.memory))
436    else:
437        main_status = dict(status[1:])
438    start_time = float(main_status.get('start_time', 0))
439    uptime = datetime.timedelta(seconds=int(time.time()-start_time))
440    cpu_time_float = float(main_status.get('cpu_time', 0))
441    cputime = datetime.timedelta(seconds=int(cpu_time_float))
442    display_fields = """name uptime memory state cpu_weight on_reboot
443     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
444    display_fields = [('name', 'Name'),
445                      ('owner', 'Owner'),
446                      ('contact', 'Contact'),
447                      ('type', 'Type'),
448                      'NIC_INFO',
449                      ('uptime', 'uptime'),
450                      ('cputime', 'CPU usage'),
451                      ('memory', 'RAM'),
452                      'DISK_INFO',
453                      ('state', 'state (xen format)'),
454                      ('cpu_weight', 'CPU weight'),
455                      ('on_reboot', 'Action on VM reboot'),
456                      ('on_poweroff', 'Action on VM poweroff'),
457                      ('on_crash', 'Action on VM crash'),
458                      ('on_xend_start', 'Action on Xen start'),
459                      ('on_xend_stop', 'Action on Xen stop'),
460                      ('bootloader', 'Bootloader options'),
461                      ]
462    fields = []
463    machine_info = {}
464    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
465    machine_info['owner'] = machine.owner
466    machine_info['contact'] = machine.contact
467
468    nic_fields = getNicInfo(machine_info, machine)
469    nic_point = display_fields.index('NIC_INFO')
470    display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
471
472    disk_fields = getDiskInfo(machine_info, machine)
473    disk_point = display_fields.index('DISK_INFO')
474    display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
475   
476    main_status['memory'] += ' MB'
477    for field, disp in display_fields:
478        if field in ('uptime', 'cputime'):
479            fields.append((disp, locals()[field]))
480        elif field in main_status:
481            fields.append((disp, main_status[field]))
482        elif field in machine_info:
483            fields.append((disp, machine_info[field]))
484        else:
485            pass
486            #fields.append((disp, None))
487
488    d = dict(user=user,
489             cdroms=CDROM.select(),
490             on=status is not None,
491             machine=machine,
492             has_vnc=has_vnc,
493             uptime=str(uptime),
494             ram=machine.memory,
495             maxmem=maxMemory(user, machine),
496             maxdisk=maxDisk(user, machine),
497             fields = fields)
498    print Template(file='info.tmpl',
499                   searchList=d)
500
501mapping = dict(list=listVms,
502               vnc=vnc,
503               command=command,
504               modify=modify,
505               info=info,
506               create=create)
507
508if __name__ == '__main__':
509    start_time = time.time()
510    fields = cgi.FieldStorage()
511    class User:
512        username = "moo"
513        email = 'moo@cow.com'
514    u = User()
515    connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
516    operation = os.environ.get('PATH_INFO', '')
517    if not operation:
518        pass
519        #XXX do redirect
520
521    if operation.startswith('/'):
522        operation = operation[1:]
523    if not operation:
524        operation = 'list'
525   
526    fun = mapping.get(operation, 
527                      lambda u, e:
528                          error(operation, u, e,
529                                "Invalid operation '%s'" % operation))
530    try:
531        fun(u, fields)
532    except MyException, err:
533        error(operation, u, fields, err)
Note: See TracBrowser for help on using the repository browser.