X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-web.git/blobdiff_plain/bc2c23e470e232b2a87cad3c826fcf4c07dc9d62..b1f4849b34267d7c937600e777b79d9455266df1:/templates/main.py diff --git a/templates/main.py b/templates/main.py index 984f3b8..f64fe3d 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): + 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 +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,7 @@ def getDiskInfo(data_dict, machine): def deleteVM(machine): """Delete a VM.""" + remctl('destroy', machine.name, err=True) transaction = ctx.current.create_transaction() delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks] try: @@ -497,11 +532,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 +548,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 +557,109 @@ 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): + if Machine.select_by(name=name) == []: + return name + if name == machine.name: + return name + raise InvalidInput('name', name, + "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) == []: + 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'), machine) + oldname = machine.name + command="modify" + olddisk = {} + + memory = fields.getfirst('memory') + if memory is not None: + memory = validMemory(user, memory, machine) + else: + memory = machine.memory + if memory != machine.memory: + machine.memory = memory + + disksize = testDisk(user, fields.getfirst('disk')) + if disksize is not None: + disksize = validDisk(user, disksize, machine) + else: + disksize = machine.disks[0].size + for disk in machine.disks: + olddisk[disk.guest_device_name] = disk.size + disk.size = disksize + 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() + raise + 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=command, + machine=machine) + return Template(file="command.tmpl", searchList=[d, global_dict]) + + def help(user, fields): """Handler for help messages.""" simple = fields.getfirst('simple') @@ -546,14 +673,19 @@ hope that the sipb-xen maintainers add support for serial consoles.""", 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.""") + cpu_weight="""Don't ask us! We're as mystified as you are.""", + owner="""The Owner 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.)""") d = dict(user=user, simple=simple, 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): @@ -564,12 +696,14 @@ def info(user, fields): 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)) + 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'), @@ -607,7 +741,7 @@ def info(user, fields): main_status['memory'] += ' MB' for field, disp in display_fields: - if field in ('uptime', 'cputime'): + 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])) @@ -627,8 +761,9 @@ def info(user, fields): ram=machine.memory, max_mem=max_mem, max_disk=max_disk, + owner_help=helppopup("owner"), fields = fields) - print Template(file='info.tmpl', + return Template(file='info.tmpl', searchList=[d, global_dict]) mapping = dict(list=listVms, @@ -646,6 +781,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 +791,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