#!/usr/bin/python
"""Main CGI script for web interface"""

import base64
import cPickle
import cgi
import datetime
import getafsgroups
import hmac
import os
import random
import re
import sha
import simplejson
import string
import subprocess
import sys
import time
from StringIO import StringIO


def revertStandardError():
    """Move stderr to stdout, and return the contents of the old stderr."""
    errio = sys.stderr
    if not isinstance(errio, StringIO):
        return None
    sys.stderr = sys.stdout
    errio.seek(0)
    return errio.read()

def printError():
    """Revert stderr to stdout, and print the contents of stderr"""
    if isinstance(sys.stderr, StringIO):
        print revertStandardError()

if __name__ == '__main__':
    import atexit
    atexit.register(printError)
    sys.stderr = StringIO()

sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')

from Cheetah.Template import Template
from sipb_xen_database import *

class MyException(Exception):
    """Base class for my exceptions"""
    pass

class InvalidInput(MyException):
    """Exception for user-provided input is invalid but maybe in good faith.

    This would include setting memory to negative (which might be a
    typo) but not setting an invalid boot CD (which requires bypassing
    the select box).
    """
    def __init__(self, err_field, err_value, expl=None):
        MyException.__init__(self, expl)
        self.err_field = err_field
        self.err_value = err_value

class CodeError(MyException):
    """Exception for internal errors or bad faith input."""
    pass

def helppopup(subj):
    """Return HTML code for a (?) link to a specified help topic"""
    return ('<span class="helplink"><a href="help?subject=' + subj + 
            '&amp;simple=true" target="_blank" ' + 
            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')

class Global(object):
    """Global state of the system, to avoid duplicate remctls to get state"""
    def __init__(self, user):
        self.user = user

    def __get_uptimes(self):
        if not hasattr(self, '_uptimes'):
            self._uptimes = getUptimes(Machine.select())
        return self._uptimes
    uptimes = property(__get_uptimes)

    def clear(self):
        """Clear the state so future accesses reload it."""
        for attr in ('_uptimes', ):
            if hasattr(self, attr):
                delattr(self, attr)

g = None

class User:
    """User class (sort of useless, I admit)"""
    def __init__(self, username, email):
        self.username = username
        self.email = email

def makeErrorPre(old, addition):
    if addition is None:
        return
    if old:
        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
    else:
        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'

Template.helppopup = staticmethod(helppopup)
Template.err = None

class JsonDict:
    """Class to store a dictionary that will be converted to JSON"""
    def __init__(self, **kws):
        self.data = kws
        if 'err' in kws:
            err = kws['err']
            del kws['err']
            self.addError(err)

    def __str__(self):
        return simplejson.dumps(self.data)

    def addError(self, text):
        """Add stderr text to be displayed on the website."""
        self.data['err'] = \
            makeErrorPre(self.data.get('err'), text)

class Defaults:
    """Class to store default values for fields."""
    memory = 256
    disk = 4.0
    cdrom = ''
    name = ''
    vmtype = 'hvm'
    def __init__(self, max_memory=None, max_disk=None, **kws):
        if max_memory is not None:
            self.memory = min(self.memory, max_memory)
        if max_disk is not None:
            self.max_disk = min(self.disk, max_disk)
        for key in kws:
            setattr(self, key, kws[key])



default_headers = {'Content-Type': 'text/html'}

# ... and stolen from xend/uuid.py
def randomUUID():
    """Generate a random UUID."""

    return [ random.randint(0, 255) for _ in range(0, 16) ]

def uuidToString(u):
    """Turn a numeric UUID to a hyphen-seperated one."""
    return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
                     "%02x" * 6]) % tuple(u)

MAX_MEMORY_TOTAL = 512
MAX_MEMORY_SINGLE = 256
MIN_MEMORY_SINGLE = 16
MAX_DISK_TOTAL = 50
MAX_DISK_SINGLE = 50
MIN_DISK_SINGLE = 0.1
MAX_VMS_TOTAL = 10
MAX_VMS_ACTIVE = 4

def getMachinesByOwner(user, machine=None):
    """Return the machines owned by the same as a machine.
    
    If the machine is None, return the machines owned by the same
    user.
    """
    if machine:
        owner = machine.owner
    else:
        owner = user.username
    return Machine.select_by(owner=owner)

def maxMemory(user, machine=None, on=True):
    """Return the maximum memory for a machine or a user.

    If machine is None, return the memory available for a new 
    machine.  Else, return the maximum that machine can have.

    on is whether the machine should be turned on.  If false, the max
    memory for the machine to change to, if it is left off, is
    returned.
    """
    if not on:
        return MAX_MEMORY_SINGLE
    machines = getMachinesByOwner(user, machine)
    active_machines = [x for x in machines if g.uptimes[x]]
    mem_usage = sum([x.memory for x in active_machines if x != machine])
    return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)

def maxDisk(user, machine=None):
    machines = getMachinesByOwner(user, machine)
    disk_usage = sum([sum([y.size for y in x.disks])
                      for x in machines if x != machine])
    return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)

def cantAddVm(user):
    machines = getMachinesByOwner(user)
    active_machines = [x for x in machines if g.uptimes[x]]
    if len(machines) >= MAX_VMS_TOTAL:
        return 'You have too many VMs to create a new one.'
    if len(active_machines) >= MAX_VMS_ACTIVE:
        return ('You already have the maximum number of VMs turned on.  '
                'To create more, turn one off.')
    return False

def haveAccess(user, machine):
    """Return whether a user has adminstrative access to a machine"""
    if user.username == 'moo':
        return True
    if user.username in (machine.administrator, machine.owner):
        return True
    if getafsgroups.checkAfsGroup(user.username, machine.administrator, 
                                  'athena.mit.edu'): #XXX Cell?
        return True
    if getafsgroups.checkLockerOwner(user.username, machine.owner):
        return True
    return owns(user, machine)

def owns(user, machine):
    """Return whether a user owns a machine"""
    if user.username == 'moo':
        return True
    return getafsgroups.checkLockerOwner(user.username, machine.owner)

def error(op, user, fields, err, emsg):
    """Print an error page when a CodeError occurs"""
    d = dict(op=op, user=user, errorMessage=str(err),
             stderr=emsg)
    return Template(file='error.tmpl', searchList=[d]);

def invalidInput(op, user, fields, err, emsg):
    """Print an error page when an InvalidInput exception occurs"""
    d = dict(op=op, user=user, err_field=err.err_field,
             err_value=str(err.err_value), stderr=emsg,
             errorMessage=str(err))
    return Template(file='invalid.tmpl', searchList=[d]);

def validMachineName(name):
    """Check that name is valid for a machine name"""
    if not name:
        return False
    charset = string.ascii_letters + string.digits + '-_'
    if name[0] in '-_' or len(name) > 22:
        return False
    for x in name:
        if x not in charset:
            return False
    return True

def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
    """Kinit with a given username and keytab"""

    p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
                         stderr=subprocess.PIPE)
    e = p.wait()
    if e:
        raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))

def checkKinit():
    """If we lack tickets, kinit."""
    p = subprocess.Popen(['klist', '-s'])
    if p.wait():
        kinit()

def remctl(*args, **kws):
    """Perform a remctl and return the output.

    kinits if necessary, and outputs errors to stderr.
    """
    checkKinit()
    p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
                         + list(args),
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    v = p.wait()
    if kws.get('err'):
        return p.stdout.read(), p.stderr.read()
    if v:
        print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
        print >> sys.stderr, p.stderr.read()
        raise CodeError('ERROR on remctl')
    return p.stdout.read()

def lvcreate(machine, disk):
    """Create a single disk for a machine"""
    remctl('web', 'lvcreate', machine.name,
           disk.guest_device_name, str(disk.size))
    
def makeDisks(machine):
    """Update the lvm partitions to add a disk."""
    for disk in machine.disks:
        lvcreate(machine, disk)

def bootMachine(machine, cdtype):
    """Boot a machine with a given boot CD.

    If cdtype is None, give no boot cd.  Otherwise, it is the string
    id of the CD (e.g. 'gutsy_i386')
    """
    if cdtype is not None:
        remctl('control', machine.name, 'create', 
               cdtype)
    else:
        remctl('control', machine.name, 'create')

def registerMachine(machine):
    """Register a machine to be controlled by the web interface"""
    remctl('web', 'register', machine.name)

def unregisterMachine(machine):
    """Unregister a machine to not be controlled by the web interface"""
    remctl('web', 'unregister', machine.name)

def parseStatus(s):
    """Parse a status string into nested tuples of strings.

    s = output of xm list --long <machine_name>
    """
    values = re.split('([()])', s)
    stack = [[]]
    for v in values[2:-2]: #remove initial and final '()'
        if not v:
            continue
        v = v.strip()
        if v == '(':
            stack.append([])
        elif v == ')':
            if len(stack[-1]) == 1:
                stack[-1].append('')
            stack[-2].append(stack[-1])
            stack.pop()
        else:
            if not v:
                continue
            stack[-1].extend(v.split())
    return stack[-1]

def getUptimes(machines=None):
    """Return a dictionary mapping machine names to uptime strings"""
    value_string = remctl('web', 'listvms')
    lines = value_string.splitlines()
    d = {}
    for line in lines:
        lst = line.split()
        name, id = lst[:2]
        uptime = ' '.join(lst[2:])
        d[name] = uptime
    ans = {}
    for m in machines:
        ans[m] = d.get(m.name)
    return ans

def statusInfo(machine):
    """Return the status list for a given machine.

    Gets and parses xm list --long
    """
    value_string, err_string = remctl('control', machine.name, 'list-long', 
                                      err=True)
    if 'Unknown command' in err_string:
        raise CodeError("ERROR in remctl list-long %s is not registered" % 
                        (machine.name,))
    elif 'does not exist' in err_string:
        return None
    elif err_string:
        raise CodeError("ERROR in remctl list-long %s:  %s" % 
                        (machine.name, err_string))
    status = parseStatus(value_string)
    return status

def hasVnc(status):
    """Does the machine with a given status list support VNC?"""
    if status is None:
        return False
    for l in status:
        if l[0] == 'device' and l[1][0] == 'vfb':
            d = dict(l[1][1:])
            return 'location' in d
    return False

def createVm(user, name, memory, disk, is_hvm, cdrom):
    """Create a VM and put it in the database"""
    # put stuff in the table
    transaction = ctx.current.create_transaction()
    try:
        if memory > maxMemory(user):
            raise InvalidInput('memory', memory,
                               "Max %s" % maxMemory(user))
        if disk > maxDisk(user) * 1024:
            raise InvalidInput('disk', disk,
                               "Max %s" % maxDisk(user))
        reason = cantAddVm(user)
        if reason:
            raise InvalidInput('create', True, reason)
        res = meta.engine.execute('select nextval('
                                  '\'"machines_machine_id_seq"\')')
        id = res.fetchone()[0]
        machine = Machine()
        machine.machine_id = id
        machine.name = name
        machine.memory = memory
        machine.owner = user.username
        machine.administrator = user.username
        machine.contact = user.email
        machine.uuid = uuidToString(randomUUID())
        machine.boot_off_cd = True
        machine_type = Type.get_by(hvm=is_hvm)
        machine.type_id = machine_type.type_id
        ctx.current.save(machine)
        disk = Disk(machine.machine_id, 
                    'hda', disk)
        open_nics = NIC.select_by(machine_id=None)
        if not open_nics: #No IPs left!
            raise CodeError("No IP addresses left!  "
                            "Contact sipb-xen-dev@mit.edu")
        nic = open_nics[0]
        nic.machine_id = machine.machine_id
        nic.hostname = name
        ctx.current.save(nic)    
        ctx.current.save(disk)
        transaction.commit()
    except:
        transaction.rollback()
        raise
    registerMachine(machine)
    makeDisks(machine)
    # tell it to boot with cdrom
    bootMachine(machine, cdrom)

    return machine

def validMemory(user, memory, machine=None, on=True):
    """Parse and validate limits for memory for a given user and machine.

    on is whether the memory must be valid after the machine is
    switched on.
    """
    try:
        memory = int(memory)
        if memory < MIN_MEMORY_SINGLE:
            raise ValueError
    except ValueError:
        raise InvalidInput('memory', memory, 
                           "Minimum %s MB" % MIN_MEMORY_SINGLE)
    if memory > maxMemory(user, machine, on):
        raise InvalidInput('memory', memory,
                           'Maximum %s MB' % maxMemory(user, machine))
    return memory

def validDisk(user, disk, machine=None):
    """Parse and validate limits for disk for a given user and machine."""
    try:
        disk = float(disk)
        if disk > maxDisk(user, machine):
            raise InvalidInput('disk', disk,
                               "Maximum %s G" % maxDisk(user, machine))
        disk = int(disk * 1024)
        if disk < MIN_DISK_SINGLE * 1024:
            raise ValueError
    except ValueError:
        raise InvalidInput('disk', disk,
                           "Minimum %s GB" % MIN_DISK_SINGLE)
    return disk

def parseCreate(user, fields):
    name = fields.getfirst('name')
    if not validMachineName(name):
        raise InvalidInput('name', name, 'You must provide a machine name.')
    name = name.lower()

    if Machine.get_by(name=name):
        raise InvalidInput('name', name,
                           "Name already exists.")
    
    memory = fields.getfirst('memory')
    memory = validMemory(user, memory, on=True)
    
    disk = fields.getfirst('disk')
    disk = validDisk(user, disk)

    vm_type = fields.getfirst('vmtype')
    if vm_type not in ('hvm', 'paravm'):
        raise CodeError("Invalid vm type '%s'"  % vm_type)    
    is_hvm = (vm_type == 'hvm')

    cdrom = fields.getfirst('cdrom')
    if cdrom is not None and not CDROM.get(cdrom):
        raise CodeError("Invalid cdrom type '%s'" % cdrom)
    return dict(user=user, name=name, memory=memory, disk=disk,
                is_hvm=is_hvm, cdrom=cdrom)

def create(user, fields):
    """Handler for create requests."""
    try:
        parsed_fields = parseCreate(user, fields)
        machine = createVm(**parsed_fields)
    except InvalidInput, err:
        pass
    else:
        err = None
    g.clear() #Changed global state
    d = getListDict(user)
    d['err'] = err
    if err:
        for field in fields.keys():
            setattr(d['defaults'], field, fields.getfirst(field))
    else:
        d['new_machine'] = parsed_fields['name']
    return Template(file='list.tmpl', searchList=[d])


def getListDict(user):
    machines = [m for m in Machine.select() if haveAccess(user, m)]    
    on = {}
    has_vnc = {}
    on = g.uptimes
    for m in machines:
        m.uptime = g.uptimes.get(m)
        if not on[m]:
            has_vnc[m] = 'Off'
        elif m.type.hvm:
            has_vnc[m] = True
        else:
            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
    #     for m in machines:
    #         status = statusInfo(m)
    #         on[m.name] = status is not None
    #         has_vnc[m.name] = hasVnc(status)
    max_memory = maxMemory(user)
    max_disk = maxDisk(user)
    defaults = Defaults(max_memory=max_memory,
                        max_disk=max_disk,
                        cdrom='gutsy-i386')
    d = dict(user=user,
             cant_add_vm=cantAddVm(user),
             max_memory=max_memory,
             max_disk=max_disk,
             defaults=defaults,
             machines=machines,
             has_vnc=has_vnc,
             uptimes=g.uptimes,
             cdroms=CDROM.select())
    return d

def listVms(user, fields):
    """Handler for list requests."""
    d = getListDict(user)
    return Template(file='list.tmpl', searchList=[d])
            
def testMachineId(user, machineId, exists=True):
    """Parse, validate and check authorization for a given machineId.

    If exists is False, don't check that it exists.
    """
    if machineId is None:
        raise CodeError("No machine ID specified")
    try:
        machineId = int(machineId)
    except ValueError:
        raise CodeError("Invalid machine ID '%s'" % machineId)
    machine = Machine.get(machineId)
    if exists and machine is None:
        raise CodeError("No such machine ID '%s'" % machineId)
    if machine is not None and not haveAccess(user, machine):
        raise CodeError("No access to machine ID '%s'" % machineId)
    return machine

def vnc(user, fields):
    """VNC applet page.

    Note that due to same-domain restrictions, the applet connects to
    the webserver, which needs to forward those requests to the xen
    server.  The Xen server runs another proxy that (1) authenticates
    and (2) finds the correct port for the VM.

    You might want iptables like:

    -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 
    -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 
    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
      --dport 10003 -j ACCEPT

    Remember to enable iptables!
    echo 1 > /proc/sys/net/ipv4/ip_forward
    """
    machine = testMachineId(user, fields.getfirst('machine_id'))
    
    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"

    data = {}
    data["user"] = user.username
    data["machine"] = machine.name
    data["expires"] = time.time()+(5*60)
    pickled_data = cPickle.dumps(data)
    m = hmac.new(TOKEN_KEY, digestmod=sha)
    m.update(pickled_data)
    token = {'data': pickled_data, 'digest': m.digest()}
    token = cPickle.dumps(token)
    token = base64.urlsafe_b64encode(token)
    
    status = statusInfo(machine)
    has_vnc = hasVnc(status)
    
    d = dict(user=user,
             on=status,
             has_vnc=has_vnc,
             machine=machine,
             hostname=os.environ.get('SERVER_NAME', 'localhost'),
             authtoken=token)
    return Template(file='vnc.tmpl', searchList=[d])

def getNicInfo(data_dict, machine):
    """Helper function for info, get data on nics for a machine.

    Modifies data_dict to include the relevant data, and returns a list
    of (key, name) pairs to display "name: data_dict[key]" to the user.
    """
    data_dict['num_nics'] = len(machine.nics)
    nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
                           ('nic%s_mac', 'NIC %s MAC Addr'),
                           ('nic%s_ip', 'NIC %s IP'),
                           ]
    nic_fields = []
    for i in range(len(machine.nics)):
        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
        data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname + 
                                           '.servers.csail.mit.edu')
        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
        data_dict['nic%s_ip' % i] = machine.nics[i].ip
    if len(machine.nics) == 1:
        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
    return nic_fields

def getDiskInfo(data_dict, machine):
    """Helper function for info, get data on disks for a machine.

    Modifies data_dict to include the relevant data, and returns a list
    of (key, name) pairs to display "name: data_dict[key]" to the user.
    """
    data_dict['num_disks'] = len(machine.disks)
    disk_fields_template = [('%s_size', '%s size')]
    disk_fields = []
    for disk in machine.disks:
        name = disk.guest_device_name
        disk_fields.extend([(x % name, y % name) for x, y in 
                            disk_fields_template])
        data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
    return disk_fields

def deleteVM(machine):
    """Delete a VM."""
    remctl('control', machine.name, 'destroy', err=True)
    transaction = ctx.current.create_transaction()
    delete_disk_pairs = [(machine.name, d.guest_device_name) 
                         for d in machine.disks]
    try:
        for nic in machine.nics:
            nic.machine_id = None
            nic.hostname = None
            ctx.current.save(nic)
        for disk in machine.disks:
            ctx.current.delete(disk)
        ctx.current.delete(machine)
        transaction.commit()
    except:
        transaction.rollback()
        raise
    for mname, dname in delete_disk_pairs:
        remctl('web', 'lvremove', mname, dname)
    unregisterMachine(machine)

def commandResult(user, fields):
    print >> sys.stderr, time.time()-start_time
    machine = testMachineId(user, fields.getfirst('machine_id'))
    action = fields.getfirst('action')
    cdrom = fields.getfirst('cdrom')
    print >> sys.stderr, time.time()-start_time
    if cdrom is not None and not CDROM.get(cdrom):
        raise CodeError("Invalid cdrom type '%s'" % cdrom)    
    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
                      'Delete VM'):
        raise CodeError("Invalid action '%s'" % action)
    if action == 'Reboot':
        if cdrom is not None:
            out, err = remctl('control', machine.name, 'reboot', cdrom,
                              err=True)
        else:
            out, err = remctl('control', machine.name, 'reboot',
                              err=True)
        if err:
            if re.match("Error: Domain '.*' does not exist.", err):
                raise InvalidInput("action", "reboot", 
                                   "Machine is not on")
            else:
                print >> sys.stderr, 'Error on reboot:'
                print >> sys.stderr, err
                raise CodeError('ERROR on remctl')
                
    elif action == 'Power on':
        if maxMemory(user) < machine.memory:
            raise InvalidInput('action', 'Power on',
                               "You don't have enough free RAM quota "
                               "to turn on this machine.")
        bootMachine(machine, cdrom)
    elif action == 'Power off':
        out, err = remctl('control', machine.name, 'destroy', err=True)
        if err:
            if re.match("Error: Domain '.*' does not exist.", err):
                raise InvalidInput("action", "Power off", 
                                   "Machine is not on.")
            else:
                print >> sys.stderr, 'Error on power off:'
                print >> sys.stderr, err
                raise CodeError('ERROR on remctl')
    elif action == 'Shutdown':
        out, err = remctl('control', machine.name, 'shutdown', err=True)
        if err:
            if re.match("Error: Domain '.*' does not exist.", err):
                raise InvalidInput("action", "Shutdown", 
                                   "Machine is not on.")
            else:
                print >> sys.stderr, 'Error on Shutdown:'
                print >> sys.stderr, err
                raise CodeError('ERROR on remctl')
    elif action == 'Delete VM':
        deleteVM(machine)
    print >> sys.stderr, time.time()-start_time

    d = dict(user=user,
             command=action,
             machine=machine)
    return d

def command(user, fields):
    """Handler for running commands like boot and delete on a VM."""
    back = fields.getfirst('back')
    try:
        d = commandResult(user, fields)
        if d['command'] == 'Delete VM':
            back = 'list'
    except InvalidInput, err:
        if not back:
            raise
        print >> sys.stderr, err
        result = None
    else:
        result = 'Success!'
        if not back:
            return Template(file='command.tmpl', searchList=[d])
    if back == 'list':
        g.clear() #Changed global state
        d = getListDict(user)
        d['result'] = result
        return Template(file='list.tmpl', searchList=[d])
    elif back == 'info':
        machine = testMachineId(user, fields.getfirst('machine_id'))
        d = infoDict(user, machine)
        d['result'] = result
        return Template(file='info.tmpl', searchList=[d])
    else:
        raise InvalidInput('back', back, 'Not a known back page.')

def testAdmin(user, admin, machine):
    if admin in (None, machine.administrator):
        return None
    if admin == user.username:
        return admin
    if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
        return admin
    if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
                                  'athena.mit.edu'):
        return 'system:'+admin
    return admin
    #raise InvalidInput('administrator', admin, 
    #                   'You must control the group you move it to.')
    
def testOwner(user, owner, machine):
    if owner in (None, machine.owner):
        return None
    value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
    if value == True:
        return owner
    raise InvalidInput('owner', owner, value)

def testContact(user, contact, machine=None):
    if contact in (None, machine.contact):
        return None
    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
        raise InvalidInput('contact', contact, "Not a valid email.")
    return contact

def testDisk(user, disksize, machine=None):
    return disksize

def testName(user, name, machine=None):
    if name in (None, machine.name):
        return None
    if not Machine.select_by(name=name):
        return name
    raise InvalidInput('name', name, "Name is already taken.")

def testHostname(user, hostname, machine):
    for nic in machine.nics:
        if hostname == nic.hostname:
            return hostname
    # check if doesn't already exist
    if NIC.select_by(hostname=hostname):
        raise InvalidInput('hostname', hostname,
                           "Already exists")
    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
                           "must only use number, letters, and dashes.")
    return hostname

def modifyDict(user, fields):
    olddisk = {}
    transaction = ctx.current.create_transaction()
    try:
        machine = testMachineId(user, fields.getfirst('machine_id'))
        owner = testOwner(user, fields.getfirst('owner'), machine)
        admin = testAdmin(user, fields.getfirst('administrator'), machine)
        contact = testContact(user, fields.getfirst('contact'), machine)
        hostname = testHostname(owner, fields.getfirst('hostname'), machine)
        name = testName(user, fields.getfirst('name'), machine)
        oldname = machine.name
        command = "modify"

        memory = fields.getfirst('memory')
        if memory is not None:
            memory = validMemory(user, memory, machine, on=False)
            machine.memory = memory
 
        disksize = testDisk(user, fields.getfirst('disk'))
        if disksize is not None:
            disksize = validDisk(user, disksize, machine)
            disk = machine.disks[0]
            if disk.size != disksize:
                olddisk[disk.guest_device_name] = disksize
                disk.size = disksize
                ctx.current.save(disk)
        
        # XXX first NIC gets hostname on change?  
        # Interface doesn't support more.
        for nic in machine.nics[:1]:
            nic.hostname = hostname
            ctx.current.save(nic)

        if owner is not None:
            machine.owner = owner
        if name is not None:
            machine.name = name
        if admin is not None:
            machine.administrator = admin
        if contact is not None:
            machine.contact = contact
            
        ctx.current.save(machine)
        transaction.commit()
    except:
        transaction.rollback()
        raise
    for diskname in olddisk:
        remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
    if name is not None:
        for disk in machine.disks:
            remctl("web", "lvrename", oldname, disk.guest_device_name, name)
        remctl("web", "moveregister", oldname, name)
    return dict(user=user,
                command=command,
                machine=machine)
    
def modify(user, fields):
    """Handler for modifying attributes of a machine."""
    try:
        modify_dict = modifyDict(user, fields)
    except InvalidInput, err:
        result = None
        machine = testMachineId(user, fields.getfirst('machine_id'))
    else:
        machine = modify_dict['machine']
        result='Success!'
        err = None
    info_dict = infoDict(user, machine)
    info_dict['err'] = err
    if err:
        for field in fields.keys():
            setattr(info_dict['defaults'], field, fields.getfirst(field))
    info_dict['result'] = result
    return Template(file='info.tmpl', searchList=[info_dict])
    

def helpHandler(user, fields):
    """Handler for help messages."""
    simple = fields.getfirst('simple')
    subjects = fields.getlist('subject')
    
    help_mapping = dict(paravm_console="""
ParaVM machines do not support console access over VNC.  To access
these machines, you either need to boot with a liveCD and ssh in or
hope that the sipb-xen maintainers add support for serial consoles.""",
                        hvm_paravm="""
HVM machines use the virtualization features of the processor, while
ParaVM machines use Xen's emulation of virtualization features.  You
want an HVM virtualized machine.""",
                        cpu_weight="""
Don't ask us!  We're as mystified as you are.""",
                        owner="""
The owner field is used to determine <a
href="help?subject=quotas">quotas</a>.  It must be the name of a
locker that you are an AFS administrator of.  In particular, you or an
AFS group you are a member of must have AFS rlidwka bits on the
locker.  You can check see who administers the LOCKER locker using the
command 'fs la /mit/LOCKER' on Athena.)  See also <a
href="help?subject=administrator">administrator</a>.""",
                        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.""",
                        quotas="""
Quotas are determined on a per-locker basis.  Each quota may have a
maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
active machines."""
                   )
    
    if not subjects:
        subjects = sorted(help_mapping.keys())
        
    d = dict(user=user,
             simple=simple,
             subjects=subjects,
             mapping=help_mapping)
    
    return Template(file="help.tmpl", searchList=[d])
    

def badOperation(u, e):
    raise CodeError("Unknown operation")

def infoDict(user, machine):
    status = statusInfo(machine)
    has_vnc = hasVnc(status)
    if status is None:
        main_status = dict(name=machine.name,
                           memory=str(machine.memory))
        uptime = None
        cputime = None
    else:
        main_status = dict(status[1:])
        start_time = float(main_status.get('start_time', 0))
        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
        cpu_time_float = float(main_status.get('cpu_time', 0))
        cputime = datetime.timedelta(seconds=int(cpu_time_float))
    display_fields = """name uptime memory state cpu_weight on_reboot 
     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
    display_fields = [('name', 'Name'),
                      ('owner', 'Owner'),
                      ('administrator', 'Administrator'),
                      ('contact', 'Contact'),
                      ('type', 'Type'),
                      'NIC_INFO',
                      ('uptime', 'uptime'),
                      ('cputime', 'CPU usage'),
                      ('memory', 'RAM'),
                      'DISK_INFO',
                      ('state', 'state (xen format)'),
                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
                      ('on_reboot', 'Action on VM reboot'),
                      ('on_poweroff', 'Action on VM poweroff'),
                      ('on_crash', 'Action on VM crash'),
                      ('on_xend_start', 'Action on Xen start'),
                      ('on_xend_stop', 'Action on Xen stop'),
                      ('bootloader', 'Bootloader options'),
                      ]
    fields = []
    machine_info = {}
    machine_info['name'] = machine.name
    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
    machine_info['owner'] = machine.owner
    machine_info['administrator'] = machine.administrator
    machine_info['contact'] = machine.contact

    nic_fields = getNicInfo(machine_info, machine)
    nic_point = display_fields.index('NIC_INFO')
    display_fields = (display_fields[:nic_point] + nic_fields + 
                      display_fields[nic_point+1:])

    disk_fields = getDiskInfo(machine_info, machine)
    disk_point = display_fields.index('DISK_INFO')
    display_fields = (display_fields[:disk_point] + disk_fields + 
                      display_fields[disk_point+1:])
    
    main_status['memory'] += ' MB'
    for field, disp in display_fields:
        if field in ('uptime', 'cputime') and locals()[field] is not None:
            fields.append((disp, locals()[field]))
        elif field in machine_info:
            fields.append((disp, machine_info[field]))
        elif field in main_status:
            fields.append((disp, main_status[field]))
        else:
            pass
            #fields.append((disp, None))
    max_mem = maxMemory(user, machine)
    max_disk = maxDisk(user, machine)
    defaults=Defaults()
    for name in 'machine_id name administrator owner memory contact'.split():
        setattr(defaults, name, getattr(machine, name))
    if machine.nics:
        defaults.hostname = machine.nics[0].hostname
    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
    d = dict(user=user,
             cdroms=CDROM.select(),
             on=status is not None,
             machine=machine,
             defaults=defaults,
             has_vnc=has_vnc,
             uptime=str(uptime),
             ram=machine.memory,
             max_mem=max_mem,
             max_disk=max_disk,
             owner_help=helppopup("owner"),
             fields = fields)
    return d

def info(user, fields):
    """Handler for info on a single VM."""
    machine = testMachineId(user, fields.getfirst('machine_id'))
    d = infoDict(user, machine)
    return Template(file='info.tmpl', searchList=[d])

mapping = dict(list=listVms,
               vnc=vnc,
               command=command,
               modify=modify,
               info=info,
               create=create,
               help=helpHandler)

def printHeaders(headers):
    for key, value in headers.iteritems():
        print '%s: %s' % (key, value)
    print


def getUser():
    """Return the current user based on the SSL environment variables"""
    if 'SSL_CLIENT_S_DN_Email' in os.environ:
        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
        return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
    else:
        return User('moo', 'nobody')

if __name__ == '__main__':
    start_time = time.time()
    fields = cgi.FieldStorage()
    u = getUser()
    g = Global(u)
    operation = os.environ.get('PATH_INFO', '')
    if not operation:
        print "Status: 301 Moved Permanently"
        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
        sys.exit(0)

    if operation.startswith('/'):
        operation = operation[1:]
    if not operation:
        operation = 'list'



    fun = mapping.get(operation, badOperation)

    if fun not in (helpHandler, ):
        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
    try:
        output = fun(u, fields)

        headers = dict(default_headers)
        if isinstance(output, tuple):
            new_headers, output = output
            headers.update(new_headers)

        e = revertStandardError()
        if e:
            output.addError(e)
        printHeaders(headers)
        print output
    except Exception, err:
        if not fields.has_key('js'):
            if isinstance(err, CodeError):
                print 'Content-Type: text/html\n'
                e = revertStandardError()
                print error(operation, u, fields, err, e)
                sys.exit(1)
            if isinstance(err, InvalidInput):
                print 'Content-Type: text/html\n'
                e = revertStandardError()
                print invalidInput(operation, u, fields, err, e)
                sys.exit(1)
        print 'Content-Type: text/plain\n'
        print 'Uh-oh!  We experienced an error.'
        print 'Please email sipb-xen@mit.edu with the contents of this page.'
        print '----'
        e = revertStandardError()
        print e
        print '----'
        raise
