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

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

Help!

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