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

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

Leave open the option of administrator acls.

  • Property svn:executable set to *
File size: 29.0 KB
RevLine 
[113]1#!/usr/bin/python
2
3import sys
4import cgi
5import os
6import string
7import subprocess
[119]8import re
[118]9import time
10import cPickle
11import base64
[119]12import sha
13import hmac
[133]14import datetime
[153]15import StringIO
[161]16import getafsgroups
[113]17
[153]18sys.stderr = StringIO.StringIO()
[113]19sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
20
21from Cheetah.Template import Template
22from sipb_xen_database import *
23import random
24
[119]25class MyException(Exception):
[145]26    """Base class for my exceptions"""
[119]27    pass
28
[145]29class InvalidInput(MyException):
30    """Exception for user-provided input is invalid but maybe in good faith.
31
32    This would include setting memory to negative (which might be a
33    typo) but not setting an invalid boot CD (which requires bypassing
34    the select box).
35    """
[153]36    def __init__(self, err_field, err_value, expl=None):
[164]37        MyException.__init__(self, expl)
[153]38        self.err_field = err_field
39        self.err_value = err_value
[145]40
41class CodeError(MyException):
42    """Exception for internal errors or bad faith input."""
43    pass
44
[152]45class Global(object):
46    def __init__(self, user):
47        self.user = user
[145]48
[152]49    def __get_uptimes(self):
50        if not hasattr(self, '_uptimes'):
[157]51            self._uptimes = getUptimes(Machine.select())
[152]52        return self._uptimes
53    uptimes = property(__get_uptimes)
[145]54
[152]55g = None
56
[139]57def helppopup(subj):
[145]58    """Return HTML code for a (?) link to a specified help topic"""
[139]59    return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
60
61
62global_dict = {}
63global_dict['helppopup'] = helppopup
64
65
[113]66# ... and stolen from xend/uuid.py
67def randomUUID():
68    """Generate a random UUID."""
69
70    return [ random.randint(0, 255) for _ in range(0, 16) ]
71
72def uuidToString(u):
[145]73    """Turn a numeric UUID to a hyphen-seperated one."""
[113]74    return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
75                     "%02x" * 6]) % tuple(u)
76
[144]77MAX_MEMORY_TOTAL = 512
78MAX_MEMORY_SINGLE = 256
79MIN_MEMORY_SINGLE = 16
80MAX_DISK_TOTAL = 50
81MAX_DISK_SINGLE = 50
82MIN_DISK_SINGLE = 0.1
83MAX_VMS_TOTAL = 10
84MAX_VMS_ACTIVE = 4
[113]85
[145]86def getMachinesByOwner(owner):
87    """Return the machines owned by a given owner."""
[144]88    return Machine.select_by(owner=owner)
89
[177]90def maxMemory(user, machine=None, on=True):
[145]91    """Return the maximum memory for a machine or a user.
92
93    If machine is None, return the memory available for a new
94    machine.  Else, return the maximum that machine can have.
95
[177]96    on is whether the machine should be turned on.  If false, the max
97    memory for the machine to change to, if it is left off, is
98    returned.
[145]99    """
[177]100    if not on:
101        return MAX_MEMORY_SINGLE
[145]102    machines = getMachinesByOwner(user.username)
[152]103    active_machines = [x for x in machines if g.uptimes[x]]
[144]104    mem_usage = sum([x.memory for x in active_machines if x != machine])
105    return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
106
[133]107def maxDisk(user, machine=None):
[145]108    machines = getMachinesByOwner(user.username)
[144]109    disk_usage = sum([sum([y.size for y in x.disks])
110                      for x in machines if x != machine])
111    return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
[119]112
[152]113def canAddVm(user):
[145]114    machines = getMachinesByOwner(user.username)
[152]115    active_machines = [x for x in machines if g.uptimes[x]]
[144]116    return (len(machines) < MAX_VMS_TOTAL and
117            len(active_machines) < MAX_VMS_ACTIVE)
118
[113]119def haveAccess(user, machine):
[145]120    """Return whether a user has access to a machine"""
[138]121    if user.username == 'moo':
[135]122        return True
[177]123    return getafsgroups.checkLockerOwner(user.username, machine.owner)
[113]124
[153]125def error(op, user, fields, err, emsg):
[145]126    """Print an error page when a CodeError occurs"""
[153]127    d = dict(op=op, user=user, errorMessage=str(err),
128             stderr=emsg)
129    return Template(file='error.tmpl', searchList=[d, global_dict]);
[113]130
[153]131def invalidInput(op, user, fields, err, emsg):
132    """Print an error page when an InvalidInput exception occurs"""
133    d = dict(op=op, user=user, err_field=err.err_field,
134             err_value=str(err.err_value), stderr=emsg,
135             errorMessage=str(err))
136    return Template(file='invalid.tmpl', searchList=[d, global_dict]);
137
[113]138def validMachineName(name):
[119]139    """Check that name is valid for a machine name"""
[113]140    if not name:
141        return False
[119]142    charset = string.ascii_letters + string.digits + '-_'
143    if name[0] in '-_' or len(name) > 22:
[113]144        return False
[140]145    for x in name:
146        if x not in charset:
147            return False
148    return True
[113]149
[119]150def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
151    """Kinit with a given username and keytab"""
[113]152
[133]153    p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
154                         stderr=subprocess.PIPE)
[119]155    e = p.wait()
156    if e:
[145]157        raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
[119]158
[113]159def checkKinit():
[119]160    """If we lack tickets, kinit."""
[113]161    p = subprocess.Popen(['klist', '-s'])
162    if p.wait():
163        kinit()
164
[119]165def remctl(*args, **kws):
166    """Perform a remctl and return the output.
167
168    kinits if necessary, and outputs errors to stderr.
169    """
[113]170    checkKinit()
171    p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
172                         + list(args),
173                         stdout=subprocess.PIPE,
174                         stderr=subprocess.PIPE)
[119]175    if kws.get('err'):
[144]176        p.wait()
[119]177        return p.stdout.read(), p.stderr.read()
[113]178    if p.wait():
[177]179        print >> sys.stderr, 'Error on remctl %s:' % args
180        print >> sys.stderr, p.stderr.read()
181        raise CodeError('ERROR on remctl')
[119]182    return p.stdout.read()
[113]183
[152]184def lvcreate(machine, disk):
185    """Create a single disk for a machine"""
186    remctl('web', 'lvcreate', machine.name,
187           disk.guest_device_name, str(disk.size))
188   
189def makeDisks(machine):
190    """Update the lvm partitions to add a disk."""
191    for disk in machine.disks:
192        lvcreate(machine, disk)
[113]193
194def bootMachine(machine, cdtype):
[119]195    """Boot a machine with a given boot CD.
196
197    If cdtype is None, give no boot cd.  Otherwise, it is the string
198    id of the CD (e.g. 'gutsy_i386')
199    """
[113]200    if cdtype is not None:
[119]201        remctl('web', 'vmboot', machine.name,
[113]202               cdtype)
203    else:
[119]204        remctl('web', 'vmboot', machine.name)
[113]205
[119]206def registerMachine(machine):
207    """Register a machine to be controlled by the web interface"""
208    remctl('web', 'register', machine.name)
209
[133]210def unregisterMachine(machine):
211    """Unregister a machine to not be controlled by the web interface"""
212    remctl('web', 'unregister', machine.name)
213
[119]214def parseStatus(s):
215    """Parse a status string into nested tuples of strings.
216
217    s = output of xm list --long <machine_name>
218    """
219    values = re.split('([()])', s)
220    stack = [[]]
221    for v in values[2:-2]: #remove initial and final '()'
222        if not v:
223            continue
224        v = v.strip()
225        if v == '(':
226            stack.append([])
227        elif v == ')':
[133]228            if len(stack[-1]) == 1:
229                stack[-1].append('')
[119]230            stack[-2].append(stack[-1])
231            stack.pop()
232        else:
233            if not v:
234                continue
235            stack[-1].extend(v.split())
236    return stack[-1]
237
[157]238def getUptimes(machines=None):
[133]239    """Return a dictionary mapping machine names to uptime strings"""
240    value_string = remctl('web', 'listvms')
241    lines = value_string.splitlines()
242    d = {}
[147]243    for line in lines:
[133]244        lst = line.split()
245        name, id = lst[:2]
246        uptime = ' '.join(lst[2:])
247        d[name] = uptime
[144]248    ans = {}
249    for m in machines:
250        ans[m] = d.get(m.name)
251    return ans
[133]252
[119]253def statusInfo(machine):
[133]254    """Return the status list for a given machine.
255
256    Gets and parses xm list --long
257    """
[119]258    value_string, err_string = remctl('list-long', machine.name, err=True)
259    if 'Unknown command' in err_string:
[145]260        raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
[119]261    elif 'does not exist' in err_string:
262        return None
263    elif err_string:
[145]264        raise CodeError("ERROR in remctl list-long %s%s" % (machine.name, err_string))
[119]265    status = parseStatus(value_string)
266    return status
267
268def hasVnc(status):
[133]269    """Does the machine with a given status list support VNC?"""
[119]270    if status is None:
271        return False
272    for l in status:
273        if l[0] == 'device' and l[1][0] == 'vfb':
274            d = dict(l[1][1:])
275            return 'location' in d
276    return False
277
[113]278def createVm(user, name, memory, disk, is_hvm, cdrom):
[133]279    """Create a VM and put it in the database"""
[113]280    # put stuff in the table
281    transaction = ctx.current.create_transaction()
282    try:
[144]283        if memory > maxMemory(user):
[153]284            raise InvalidInput('memory', memory,
285                               "Max %s" % maxMemory(user))
[144]286        if disk > maxDisk(user) * 1024:
[153]287            raise InvalidInput('disk', disk,
288                               "Max %s" % maxDisk(user))
[144]289        if not canAddVm(user):
[153]290            raise InvalidInput('create', True, 'Unable to create more VMs')
[113]291        res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
292        id = res.fetchone()[0]
293        machine = Machine()
294        machine.machine_id = id
295        machine.name = name
296        machine.memory = memory
297        machine.owner = user.username
[179]298        machine.administrator = user.username
[113]299        machine.contact = user.email
300        machine.uuid = uuidToString(randomUUID())
301        machine.boot_off_cd = True
302        machine_type = Type.get_by(hvm=is_hvm)
303        machine.type_id = machine_type.type_id
304        ctx.current.save(machine)
305        disk = Disk(machine.machine_id, 
306                    'hda', disk)
307        open = NIC.select_by(machine_id=None)
308        if not open: #No IPs left!
[145]309            raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
[113]310        nic = open[0]
311        nic.machine_id = machine.machine_id
312        nic.hostname = name
313        ctx.current.save(nic)   
314        ctx.current.save(disk)
315        transaction.commit()
316    except:
317        transaction.rollback()
318        raise
[144]319    registerMachine(machine)
[152]320    makeDisks(machine)
[113]321    # tell it to boot with cdrom
322    bootMachine(machine, cdrom)
323
324    return machine
325
[177]326def validMemory(user, memory, machine=None, on=True):
327    """Parse and validate limits for memory for a given user and machine.
328
329    on is whether the memory must be valid after the machine is
330    switched on.
331    """
[113]332    try:
333        memory = int(memory)
[144]334        if memory < MIN_MEMORY_SINGLE:
[113]335            raise ValueError
336    except ValueError:
[153]337        raise InvalidInput('memory', memory, 
338                           "Minimum %s MB" % MIN_MEMORY_SINGLE)
[177]339    if memory > maxMemory(user, machine, on):
[153]340        raise InvalidInput('memory', memory,
341                           'Maximum %s MB' % maxMemory(user, machine))
[134]342    return memory
343
344def validDisk(user, disk, machine=None):
[145]345    """Parse and validate limits for disk for a given user and machine."""
[113]346    try:
347        disk = float(disk)
[134]348        if disk > maxDisk(user, machine):
[153]349            raise InvalidInput('disk', disk,
350                               "Maximum %s G" % maxDisk(user, machine))
[113]351        disk = int(disk * 1024)
[144]352        if disk < MIN_DISK_SINGLE * 1024:
[113]353            raise ValueError
354    except ValueError:
[153]355        raise InvalidInput('disk', disk,
356                           "Minimum %s GB" % MIN_DISK_SINGLE)
[134]357    return disk
358
359def create(user, fields):
[145]360    """Handler for create requests."""
[134]361    name = fields.getfirst('name')
362    if not validMachineName(name):
[153]363        raise InvalidInput('name', name)
[162]364    name = name.lower()
[134]365
366    if Machine.get_by(name=name):
[153]367        raise InvalidInput('name', name,
368                           "Already exists")
[113]369   
[134]370    memory = fields.getfirst('memory')
[177]371    memory = validMemory(user, memory, on=True)
[134]372   
373    disk = fields.getfirst('disk')
374    disk = validDisk(user, disk)
375
[113]376    vm_type = fields.getfirst('vmtype')
377    if vm_type not in ('hvm', 'paravm'):
[145]378        raise CodeError("Invalid vm type '%s'"  % vm_type)   
[113]379    is_hvm = (vm_type == 'hvm')
380
381    cdrom = fields.getfirst('cdrom')
382    if cdrom is not None and not CDROM.get(cdrom):
[145]383        raise CodeError("Invalid cdrom type '%s'" % cdrom)   
[113]384   
385    machine = createVm(user, name, memory, disk, is_hvm, cdrom)
386    d = dict(user=user,
387             machine=machine)
[153]388    return Template(file='create.tmpl',
[139]389                   searchList=[d, global_dict]);
[113]390
391def listVms(user, fields):
[145]392    """Handler for list requests."""
[135]393    machines = [m for m in Machine.select() if haveAccess(user, m)]   
[133]394    on = {}
[119]395    has_vnc = {}
[152]396    on = g.uptimes
[136]397    for m in machines:
[144]398        if not on[m]:
399            has_vnc[m] = 'Off'
[138]400        elif m.type.hvm:
[144]401            has_vnc[m] = True
[136]402        else:
[144]403            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
[133]404    #     for m in machines:
405    #         status = statusInfo(m)
406    #         on[m.name] = status is not None
407    #         has_vnc[m.name] = hasVnc(status)
[152]408    max_mem=maxMemory(user)
[144]409    max_disk=maxDisk(user)
[113]410    d = dict(user=user,
[152]411             can_add_vm=canAddVm(user),
[144]412             max_mem=max_mem,
413             max_disk=max_disk,
414             default_mem=max_mem,
415             default_disk=min(4.0, max_disk),
[113]416             machines=machines,
[119]417             has_vnc=has_vnc,
[157]418             uptimes=g.uptimes,
[113]419             cdroms=CDROM.select())
[153]420    return Template(file='list.tmpl', searchList=[d, global_dict])
[113]421
422def testMachineId(user, machineId, exists=True):
[145]423    """Parse, validate and check authorization for a given machineId.
424
425    If exists is False, don't check that it exists.
426    """
[113]427    if machineId is None:
[145]428        raise CodeError("No machine ID specified")
[113]429    try:
430        machineId = int(machineId)
431    except ValueError:
[145]432        raise CodeError("Invalid machine ID '%s'" % machineId)
[113]433    machine = Machine.get(machineId)
434    if exists and machine is None:
[145]435        raise CodeError("No such machine ID '%s'" % machineId)
436    if machine is not None and not haveAccess(user, machine):
437        raise CodeError("No access to machine ID '%s'" % machineId)
[113]438    return machine
439
440def vnc(user, fields):
[119]441    """VNC applet page.
442
443    Note that due to same-domain restrictions, the applet connects to
444    the webserver, which needs to forward those requests to the xen
445    server.  The Xen server runs another proxy that (1) authenticates
446    and (2) finds the correct port for the VM.
447
448    You might want iptables like:
449
450    -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
451    -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
452    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
[145]453
454    Remember to enable iptables!
455    echo 1 > /proc/sys/net/ipv4/ip_forward
[119]456    """
[113]457    machine = testMachineId(user, fields.getfirst('machine_id'))
[118]458   
459    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
460
461    data = {}
[133]462    data["user"] = user.username
[120]463    data["machine"]=machine.name
[118]464    data["expires"]=time.time()+(5*60)
465    pickledData = cPickle.dumps(data)
466    m = hmac.new(TOKEN_KEY, digestmod=sha)
467    m.update(pickledData)
468    token = {'data': pickledData, 'digest': m.digest()}
469    token = cPickle.dumps(token)
470    token = base64.urlsafe_b64encode(token)
471   
[152]472    status = statusInfo(machine)
473    has_vnc = hasVnc(status)
474   
[113]475    d = dict(user=user,
[152]476             on=status,
477             has_vnc=has_vnc,
[113]478             machine=machine,
[119]479             hostname=os.environ.get('SERVER_NAME', 'localhost'),
[113]480             authtoken=token)
[153]481    return Template(file='vnc.tmpl',
[139]482                   searchList=[d, global_dict])
[113]483
[133]484def getNicInfo(data_dict, machine):
[145]485    """Helper function for info, get data on nics for a machine.
486
487    Modifies data_dict to include the relevant data, and returns a list
488    of (key, name) pairs to display "name: data_dict[key]" to the user.
489    """
[133]490    data_dict['num_nics'] = len(machine.nics)
491    nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
492                           ('nic%s_mac', 'NIC %s MAC Addr'),
493                           ('nic%s_ip', 'NIC %s IP'),
494                           ]
495    nic_fields = []
496    for i in range(len(machine.nics)):
497        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
498        data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
499        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
500        data_dict['nic%s_ip' % i] = machine.nics[i].ip
501    if len(machine.nics) == 1:
502        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
503    return nic_fields
504
505def getDiskInfo(data_dict, machine):
[145]506    """Helper function for info, get data on disks for a machine.
507
508    Modifies data_dict to include the relevant data, and returns a list
509    of (key, name) pairs to display "name: data_dict[key]" to the user.
510    """
[133]511    data_dict['num_disks'] = len(machine.disks)
512    disk_fields_template = [('%s_size', '%s size')]
513    disk_fields = []
514    for disk in machine.disks:
515        name = disk.guest_device_name
516        disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
517        data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
518    return disk_fields
519
520def deleteVM(machine):
[145]521    """Delete a VM."""
[176]522    remctl('destroy', machine.name, err=True)
[133]523    transaction = ctx.current.create_transaction()
524    delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
525    try:
526        for nic in machine.nics:
527            nic.machine_id = None
528            nic.hostname = None
529            ctx.current.save(nic)
530        for disk in machine.disks:
531            ctx.current.delete(disk)
532        ctx.current.delete(machine)
533        transaction.commit()
534    except:
535        transaction.rollback()
536        raise
537    for mname, dname in delete_disk_pairs:
538        remctl('web', 'lvremove', mname, dname)
539    unregisterMachine(machine)
540
541def command(user, fields):
[145]542    """Handler for running commands like boot and delete on a VM."""
[157]543    print >> sys.stderr, time.time()-start_time
[133]544    machine = testMachineId(user, fields.getfirst('machine_id'))
545    action = fields.getfirst('action')
546    cdrom = fields.getfirst('cdrom')
[157]547    print >> sys.stderr, time.time()-start_time
[133]548    if cdrom is not None and not CDROM.get(cdrom):
[145]549        raise CodeError("Invalid cdrom type '%s'" % cdrom)   
[133]550    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
[145]551        raise CodeError("Invalid action '%s'" % action)
[133]552    if action == 'Reboot':
553        if cdrom is not None:
554            remctl('reboot', machine.name, cdrom)
555        else:
556            remctl('reboot', machine.name)
557    elif action == 'Power on':
[144]558        if maxMemory(user) < machine.memory:
[153]559            raise InvalidInput('action', 'Power on',
560                               "You don't have enough free RAM quota to turn on this machine")
[133]561        bootMachine(machine, cdrom)
562    elif action == 'Power off':
563        remctl('destroy', machine.name)
564    elif action == 'Shutdown':
565        remctl('shutdown', machine.name)
566    elif action == 'Delete VM':
567        deleteVM(machine)
[157]568    print >> sys.stderr, time.time()-start_time
[133]569
570    d = dict(user=user,
571             command=action,
572             machine=machine)
[153]573    return Template(file="command.tmpl", searchList=[d, global_dict])
574
575def testOwner(user, owner, machine=None):
[177]576    if owner == machine.owner:   #XXX What do we do when you lose access to the locker?
577        return owner
578    value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
579    if value == True:
580        return owner
581    raise InvalidInput('owner', owner, value)
[153]582
583def testContact(user, contact, machine=None):
[177]584    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
585        raise InvalidInput('contact', contact, "Not a valid email")
[153]586    return contact
587
[161]588def testDisk(user, disksize, machine=None):
589    return disksize
590
591def testName(user, name, machine=None):
[177]592    if name is None:
593        return None
594    if not Machine.select_by(name=name):
[163]595        return name
[164]596    if name == machine.name:
597        return name
[177]598    raise InvalidInput('name', name, "Already taken")
[161]599
[153]600def testHostname(user, hostname, machine):
601    for nic in machine.nics:
602        if hostname == nic.hostname:
603            return hostname
[161]604    # check if doesn't already exist
[177]605    if NIC.select_by(hostname=hostname):
606        raise InvalidInput('hostname', hostname,
607                           "Already exists")
608    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
609        raise InvalidInput('hostname', hostname, "Not a valid hostname; must only use number, letters, and dashes.")
610    return hostname
[153]611
[133]612def modify(user, fields):
[145]613    """Handler for modifying attributes of a machine."""
[161]614
[177]615    olddisk = {}
[161]616    transaction = ctx.current.create_transaction()
617    try:
618        machine = testMachineId(user, fields.getfirst('machine_id'))
619        owner = testOwner(user, fields.getfirst('owner'), machine)
620        contact = testContact(user, fields.getfirst('contact'))
621        hostname = testHostname(owner, fields.getfirst('hostname'),
[177]622                                machine)
[164]623        name = testName(user, fields.getfirst('name'), machine)
[161]624        oldname = machine.name
[165]625        command="modify"
[153]626
[161]627        memory = fields.getfirst('memory')
628        if memory is not None:
[177]629            memory = validMemory(user, memory, machine, on=False)
[161]630            machine.memory = memory
[177]631 
[161]632        disksize = testDisk(user, fields.getfirst('disk'))
633        if disksize is not None:
634            disksize = validDisk(user, disksize, machine)
[177]635            disk = machine.disks[0]
636            if disk.size != disksize:
637                olddisk[disk.guest_device_name] = disksize
638                disk.size = disksize
639                ctx.current.save(disk)
[161]640       
[177]641        # XXX first NIC gets hostname on change?  Interface doesn't support more.
642        for nic in machine.nics[:1]:
[161]643            nic.hostname = hostname
644            ctx.current.save(nic)
645
[177]646        if owner is not None and owner != machine.owner:
[161]647            machine.owner = owner
[177]648        if name is not None and name != machine.name:
[161]649            machine.name = name
650           
651        ctx.current.save(machine)
652        transaction.commit()
653    except:
654        transaction.rollback()
[163]655        raise
[177]656    for diskname in olddisk:
657        remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
658    if name is not None and name != oldname:
659        for disk in machine.disks:
660            if oldname != name:
661                remctl("web", "lvrename", oldname, disk.guest_device_name, name)
662        remctl("web", "moveregister", oldname, name)
[161]663    d = dict(user=user,
[165]664             command=command,
[161]665             machine=machine)
666    return Template(file="command.tmpl", searchList=[d, global_dict])   
667
668
[139]669def help(user, fields):
[145]670    """Handler for help messages."""
[139]671    simple = fields.getfirst('simple')
672    subjects = fields.getlist('subject')
673   
674    mapping = dict(paravm_console="""
675ParaVM machines do not support console access over VNC.  To access
676these machines, you either need to boot with a liveCD and ssh in or
677hope that the sipb-xen maintainers add support for serial consoles.""",
678                   hvm_paravm="""
679HVM machines use the virtualization features of the processor, while
680ParaVM machines use Xen's emulation of virtualization features.  You
681want an HVM virtualized machine.""",
[166]682                   cpu_weight="""Don't ask us!  We're as mystified as you are.""",
683                   owner="""The Owner must be the name of a locker that you are an AFS
684administrator of.  In particular, you or an AFS group you are a member
685of must have AFS rlidwka bits on the locker.  You can check see who
686administers the LOCKER locker using the command 'fs la /mit/LOCKER' on
687Athena.)""")
[139]688   
689    d = dict(user=user,
690             simple=simple,
691             subjects=subjects,
692             mapping=mapping)
693   
[153]694    return Template(file="help.tmpl", searchList=[d, global_dict])
[139]695   
[133]696
[113]697def info(user, fields):
[145]698    """Handler for info on a single VM."""
[113]699    machine = testMachineId(user, fields.getfirst('machine_id'))
[133]700    status = statusInfo(machine)
701    has_vnc = hasVnc(status)
702    if status is None:
703        main_status = dict(name=machine.name,
704                           memory=str(machine.memory))
[167]705        uptime=None
706        cputime=None
[133]707    else:
708        main_status = dict(status[1:])
[167]709        start_time = float(main_status.get('start_time', 0))
710        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
711        cpu_time_float = float(main_status.get('cpu_time', 0))
712        cputime = datetime.timedelta(seconds=int(cpu_time_float))
[133]713    display_fields = """name uptime memory state cpu_weight on_reboot
714     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
715    display_fields = [('name', 'Name'),
716                      ('owner', 'Owner'),
717                      ('contact', 'Contact'),
[136]718                      ('type', 'Type'),
[133]719                      'NIC_INFO',
720                      ('uptime', 'uptime'),
721                      ('cputime', 'CPU usage'),
722                      ('memory', 'RAM'),
723                      'DISK_INFO',
724                      ('state', 'state (xen format)'),
[139]725                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
[133]726                      ('on_reboot', 'Action on VM reboot'),
727                      ('on_poweroff', 'Action on VM poweroff'),
728                      ('on_crash', 'Action on VM crash'),
729                      ('on_xend_start', 'Action on Xen start'),
730                      ('on_xend_stop', 'Action on Xen stop'),
731                      ('bootloader', 'Bootloader options'),
732                      ]
733    fields = []
734    machine_info = {}
[147]735    machine_info['name'] = machine.name
[136]736    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
[133]737    machine_info['owner'] = machine.owner
738    machine_info['contact'] = machine.contact
739
740    nic_fields = getNicInfo(machine_info, machine)
741    nic_point = display_fields.index('NIC_INFO')
742    display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
743
744    disk_fields = getDiskInfo(machine_info, machine)
745    disk_point = display_fields.index('DISK_INFO')
746    display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
747   
748    main_status['memory'] += ' MB'
749    for field, disp in display_fields:
[167]750        if field in ('uptime', 'cputime') and locals()[field] is not None:
[133]751            fields.append((disp, locals()[field]))
[147]752        elif field in machine_info:
753            fields.append((disp, machine_info[field]))
[133]754        elif field in main_status:
755            fields.append((disp, main_status[field]))
756        else:
757            pass
758            #fields.append((disp, None))
[144]759    max_mem = maxMemory(user, machine)
760    max_disk = maxDisk(user, machine)
[113]761    d = dict(user=user,
[133]762             cdroms=CDROM.select(),
763             on=status is not None,
764             machine=machine,
765             has_vnc=has_vnc,
766             uptime=str(uptime),
767             ram=machine.memory,
[144]768             max_mem=max_mem,
769             max_disk=max_disk,
[166]770             owner_help=helppopup("owner"),
[133]771             fields = fields)
[153]772    return Template(file='info.tmpl',
[139]773                   searchList=[d, global_dict])
[113]774
775mapping = dict(list=listVms,
776               vnc=vnc,
[133]777               command=command,
778               modify=modify,
[113]779               info=info,
[139]780               create=create,
781               help=help)
[113]782
783if __name__ == '__main__':
[133]784    start_time = time.time()
[113]785    fields = cgi.FieldStorage()
[133]786    class User:
[113]787        username = "moo"
788        email = 'moo@cow.com'
[133]789    u = User()
[152]790    g = Global(u)
[140]791    if 'SSL_CLIENT_S_DN_Email' in os.environ:
792        username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
793        u.username = username
794        u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
795    else:
[144]796        u.username = 'moo'
797        u.email = 'nobody'
[140]798    connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
[113]799    operation = os.environ.get('PATH_INFO', '')
[157]800#    print 'Content-Type: text/plain\n'
801#    print operation
[119]802    if not operation:
[140]803        print "Status: 301 Moved Permanently"
804        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
805        sys.exit(0)
[119]806
[113]807    if operation.startswith('/'):
808        operation = operation[1:]
809    if not operation:
810        operation = 'list'
[157]811
[153]812    def badOperation(u, e):
813        raise CodeError("Unknown operation")
814
815    fun = mapping.get(operation, badOperation)
[139]816    if fun not in (help, ):
817        connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
[119]818    try:
[153]819        output = fun(u, fields)
820        print 'Content-Type: text/html\n'
821        sys.stderr.seek(0)
822        e = sys.stderr.read()
[177]823        sys.stderr=sys.stdout
[153]824        if e:
[157]825            output = str(output)
826            output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
[153]827        print output
[145]828    except CodeError, err:
[153]829        print 'Content-Type: text/html\n'
830        sys.stderr.seek(0)
831        e = sys.stderr.read()
[157]832        sys.stderr=sys.stdout
[153]833        print error(operation, u, fields, err, e)
[145]834    except InvalidInput, err:
[153]835        print 'Content-Type: text/html\n'
836        sys.stderr.seek(0)
837        e = sys.stderr.read()
[157]838        sys.stderr=sys.stdout
[153]839        print invalidInput(operation, u, fields, err, e)
840    except:
841        print 'Content-Type: text/plain\n'
842        sys.stderr.seek(0)
843        e = sys.stderr.read()
844        print e
845        print '----'
[157]846        sys.stderr = sys.stdout
[153]847        raise
Note: See TracBrowser for help on using the repository browser.