X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-web.git/blobdiff_plain/bc2c23e470e232b2a87cad3c826fcf4c07dc9d62..7268d3236dc1ea69c8ff786e43ea77939c6d1a97:/templates/main.py?ds=inline diff --git a/templates/main.py b/templates/main.py index 984f3b8..36ea239 100755 --- a/templates/main.py +++ b/templates/main.py @@ -12,8 +12,10 @@ import base64 import sha import hmac import datetime +import StringIO +import getafsgroups -sys.stderr = sys.stdout +sys.stderr = StringIO.StringIO() sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages') from Cheetah.Template import Template @@ -31,13 +33,26 @@ class InvalidInput(MyException): typo) but not setting an invalid boot CD (which requires bypassing the select box). """ - pass + def __init__(self, err_field, err_value, expl=None): + super(InvalidInput, self).__init__(expl) + self.err_field = err_field + self.err_value = err_value class CodeError(MyException): """Exception for internal errors or bad faith input.""" pass +class Global(object): + 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) +g = None def helppopup(subj): """Return HTML code for a (?) link to a specified help topic""" @@ -72,7 +87,7 @@ def getMachinesByOwner(owner): """Return the machines owned by a given owner.""" return Machine.select_by(owner=owner) -def maxMemory(user, machine=None, on=None): +def maxMemory(user, machine=None): """Return the maximum memory for a machine or a user. If machine is None, return the memory available for a new @@ -83,9 +98,7 @@ def maxMemory(user, machine=None, on=None): """ machines = getMachinesByOwner(user.username) - if on is None: - on = getUptimes(machines) - active_machines = [x for x in machines if on[x]] + 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) @@ -95,11 +108,9 @@ def maxDisk(user, machine=None): for x in machines if x != machine]) return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.) -def canAddVm(user, on=None): +def canAddVm(user): machines = getMachinesByOwner(user.username) - if on is None: - on = getUptimes(machines) - active_machines = [x for x in machines if on[x]] + active_machines = [x for x in machines if g.uptimes[x]] return (len(machines) < MAX_VMS_TOTAL and len(active_machines) < MAX_VMS_ACTIVE) @@ -107,12 +118,20 @@ def haveAccess(user, machine): """Return whether a user has access to a machine""" if user.username == 'moo': return True - return machine.owner == user.username + return getafsgroups.checkLockerOwner(user.username,machine.owner) -def error(op, user, fields, err): +def error(op, user, fields, err, emsg): """Print an error page when a CodeError occurs""" - d = dict(op=op, user=user, errorMessage=str(err)) - print Template(file='error.tmpl', searchList=[d, global_dict]); + d = dict(op=op, user=user, errorMessage=str(err), + stderr=emsg) + return Template(file='error.tmpl', searchList=[d, global_dict]); + +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, global_dict]); def validMachineName(name): """Check that name is valid for a machine name""" @@ -159,9 +178,15 @@ def remctl(*args, **kws): (args, p.stderr.read())) return p.stdout.read() -def makeDisks(): - """Update the lvm partitions to include all disks in the database.""" - remctl('web', 'lvcreate') +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. @@ -207,7 +232,7 @@ def parseStatus(s): stack[-1].extend(v.split()) return stack[-1] -def getUptimes(machines): +def getUptimes(machines=None): """Return a dictionary mapping machine names to uptime strings""" value_string = remctl('web', 'listvms') lines = value_string.splitlines() @@ -253,11 +278,13 @@ def createVm(user, name, memory, disk, is_hvm, cdrom): transaction = ctx.current.create_transaction() try: if memory > maxMemory(user): - raise InvalidInput("Too much memory requested") + raise InvalidInput('memory', memory, + "Max %s" % maxMemory(user)) if disk > maxDisk(user) * 1024: - raise InvalidInput("Too much disk requested") + raise InvalidInput('disk', disk, + "Max %s" % maxDisk(user)) if not canAddVm(user): - raise InvalidInput("Too many VMs requested") + raise InvalidInput('create', True, 'Unable to create more VMs') res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')') id = res.fetchone()[0] machine = Machine() @@ -286,7 +313,7 @@ def createVm(user, name, memory, disk, is_hvm, cdrom): transaction.rollback() raise registerMachine(machine) - makeDisks() + makeDisks(machine) # tell it to boot with cdrom bootMachine(machine, cdrom) @@ -299,10 +326,11 @@ def validMemory(user, memory, machine=None): if memory < MIN_MEMORY_SINGLE: raise ValueError except ValueError: - raise InvalidInput("Invalid memory amount; must be at least %s MB" % - MIN_MEMORY_SINGLE) + raise InvalidInput('memory', memory, + "Minimum %s MB" % MIN_MEMORY_SINGLE) if memory > maxMemory(user, machine): - raise InvalidInput("Too much memory requested") + raise InvalidInput('memory', memory, + 'Maximum %s MB' % maxMemory(user, machine)) return memory def validDisk(user, disk, machine=None): @@ -310,24 +338,26 @@ def validDisk(user, disk, machine=None): try: disk = float(disk) if disk > maxDisk(user, machine): - raise InvalidInput("Too much disk requested") + 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("Invalid disk amount; minimum is %s GB" % - MIN_DISK_SINGLE) + raise InvalidInput('disk', disk, + "Minimum %s GB" % MIN_DISK_SINGLE) return disk def create(user, fields): """Handler for create requests.""" name = fields.getfirst('name') if not validMachineName(name): - raise InvalidInput("Invalid name '%s'" % name) - name = user.username + '_' + name.lower() + raise InvalidInput('name', name) + name = name.lower() if Machine.get_by(name=name): - raise InvalidInput("A machine named '%s' already exists" % name) + raise InvalidInput('name', name, + "Already exists") memory = fields.getfirst('memory') memory = validMemory(user, memory) @@ -347,7 +377,7 @@ def create(user, fields): machine = createVm(user, name, memory, disk, is_hvm, cdrom) d = dict(user=user, machine=machine) - print Template(file='create.tmpl', + return Template(file='create.tmpl', searchList=[d, global_dict]); def listVms(user, fields): @@ -355,8 +385,7 @@ def listVms(user, fields): machines = [m for m in Machine.select() if haveAccess(user, m)] on = {} has_vnc = {} - uptimes = getUptimes(machines) - on = uptimes + on = g.uptimes for m in machines: if not on[m]: has_vnc[m] = 'Off' @@ -368,19 +397,19 @@ def listVms(user, fields): # status = statusInfo(m) # on[m.name] = status is not None # has_vnc[m.name] = hasVnc(status) - max_mem=maxMemory(user, on=on) + max_mem=maxMemory(user) max_disk=maxDisk(user) d = dict(user=user, - can_add_vm=canAddVm(user, on=on), + can_add_vm=canAddVm(user), max_mem=max_mem, max_disk=max_disk, default_mem=max_mem, default_disk=min(4.0, max_disk), machines=machines, has_vnc=has_vnc, - uptimes=uptimes, + uptimes=g.uptimes, cdroms=CDROM.select()) - print Template(file='list.tmpl', searchList=[d, global_dict]) + return Template(file='list.tmpl', searchList=[d, global_dict]) def testMachineId(user, machineId, exists=True): """Parse, validate and check authorization for a given machineId. @@ -432,11 +461,16 @@ def vnc(user, fields): 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) - print Template(file='vnc.tmpl', + return Template(file='vnc.tmpl', searchList=[d, global_dict]) def getNicInfo(data_dict, machine): @@ -477,6 +511,10 @@ def getDiskInfo(data_dict, machine): def deleteVM(machine): """Delete a VM.""" + try: + remctl('destroy', machine.name) + except: + pass transaction = ctx.current.create_transaction() delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks] try: @@ -497,11 +535,11 @@ def deleteVM(machine): def command(user, fields): """Handler for running commands like boot and delete on a VM.""" - print time.time()-start_time + print >> sys.stderr, time.time()-start_time machine = testMachineId(user, fields.getfirst('machine_id')) action = fields.getfirst('action') cdrom = fields.getfirst('cdrom') - print time.time()-start_time + 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'): @@ -513,7 +551,8 @@ def command(user, fields): remctl('reboot', machine.name) elif action == 'Power on': if maxMemory(user) < machine.memory: - raise InvalidInput("You don't have enough free RAM quota") + 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': remctl('destroy', machine.name) @@ -521,18 +560,99 @@ def command(user, fields): remctl('shutdown', machine.name) elif action == 'Delete VM': deleteVM(machine) - print time.time()-start_time + print >> sys.stderr, time.time()-start_time d = dict(user=user, command=action, machine=machine) - print Template(file="command.tmpl", searchList=[d, global_dict]) - + return Template(file="command.tmpl", searchList=[d, global_dict]) + +def testOwner(user, owner, machine=None): + if not getafsgroups.checkLockerOwner(user.username, owner): + raise InvalidInput('owner', owner, + "Invalid") + return owner + +def testContact(user, contact, machine=None): + if contact != user.email: + raise InvalidInput('contact', contact, + "Invalid") + return contact + +def testDisk(user, disksize, machine=None): + return disksize + +def testName(user, name, machine=None): + return name + +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) == []: + return hostname + raise InvalidInput('hostname', hostname, + "Different from before") + + def modify(user, fields): """Handler for modifying attributes of a machine.""" #XXX not written yet - machine = testMachineId(user, fields.getfirst('machine_id')) - + + transaction = ctx.current.create_transaction() + try: + machine = testMachineId(user, fields.getfirst('machine_id')) + owner = testOwner(user, fields.getfirst('owner'), machine) + contact = testContact(user, fields.getfirst('contact')) + hostname = testHostname(owner, fields.getfirst('hostname'), + machine) + name = testName(user, fields.getfirst('name')) + oldname = machine.name + olddisk = {} + + memory = fields.getfirst('memory') + if memory is not None: + memory = validMemory(user, memory, machine) + if memory != machine.memory: + machine.memory = memory + + disksize = testDisk(user, fields.getfirst('disk')) + if disksize is not None: + disksize = validDisk(user, disksize, machine) + + for disk in machine.disks: + disk.size = disksize + olddisk[disk.guest_device_name] = disk.size + ctx.current.save(disk) + + # XXX all NICs get same hostname on change? Interface doesn't support more. + for nic in machine.nics: + nic.hostname = hostname + ctx.current.save(nic) + + if owner != machine.owner: + machine.owner = owner + if name != machine.name: + machine.name = name + + ctx.current.save(machine) + transaction.commit() + except: + transaction.rollback() + remctl("web", "moveregister", oldname, name) + for disk in machine.disks: + # XXX all disks get the same size on change? Interface doesn't support more. + if disk.size != olddisk[disk.guest_device_name]: + remctl("web", "lvresize", oldname, disk.guest_device_name, str(disk.size)) + if oldname != name: + remctl("web", "lvrename", oldname, disk.guest_device_name, name) + d = dict(user=user, + command="modify", + machine=machine) + return Template(file="command.tmpl", searchList=[d, global_dict]) + + def help(user, fields): """Handler for help messages.""" simple = fields.getfirst('simple') @@ -553,7 +673,7 @@ want an HVM virtualized machine.""", subjects=subjects, mapping=mapping) - print Template(file="help.tmpl", searchList=[d, global_dict]) + return Template(file="help.tmpl", searchList=[d, global_dict]) def info(user, fields): @@ -628,7 +748,7 @@ def info(user, fields): max_mem=max_mem, max_disk=max_disk, fields = fields) - print Template(file='info.tmpl', + return Template(file='info.tmpl', searchList=[d, global_dict]) mapping = dict(list=listVms, @@ -646,6 +766,7 @@ if __name__ == '__main__': username = "moo" email = 'moo@cow.com' u = User() + g = Global(u) if 'SSL_CLIENT_S_DN_Email' in os.environ: username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0] u.username = username @@ -655,28 +776,50 @@ if __name__ == '__main__': u.email = 'nobody' connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen') operation = os.environ.get('PATH_INFO', '') - #print 'Content-Type: text/plain\n' - #print operation +# print 'Content-Type: text/plain\n' +# print operation if not operation: print "Status: 301 Moved Permanently" print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n' sys.exit(0) - print 'Content-Type: text/html\n' if operation.startswith('/'): operation = operation[1:] if not operation: operation = 'list' - - fun = mapping.get(operation, - lambda u, e: - error(operation, u, e, - "Invalid operation '%s'" % operation)) + + def badOperation(u, e): + raise CodeError("Unknown operation") + + fun = mapping.get(operation, badOperation) if fun not in (help, ): connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen') try: - fun(u, fields) + output = fun(u, fields) + print 'Content-Type: text/html\n' + sys.stderr.seek(0) + e = sys.stderr.read() + if e: + output = str(output) + output = output.replace('
', 'STDERR:
'+e+'') + print output except CodeError, err: - error(operation, u, fields, err) + print 'Content-Type: text/html\n' + sys.stderr.seek(0) + e = sys.stderr.read() + sys.stderr=sys.stdout + print error(operation, u, fields, err, e) except InvalidInput, err: - error(operation, u, fields, err) + print 'Content-Type: text/html\n' + sys.stderr.seek(0) + e = sys.stderr.read() + sys.stderr=sys.stdout + print invalidInput(operation, u, fields, err, e) + except: + print 'Content-Type: text/plain\n' + sys.stderr.seek(0) + e = sys.stderr.read() + print e + print '----' + sys.stderr = sys.stdout + raise