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

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

More stuff.

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