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

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

Moo!

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