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

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

Fix to admin

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