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

Last change on this file since 197 was 197, checked in by tabbott, 17 years ago

use the new remctl interface

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