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

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

Update

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