X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-web.git/blobdiff_plain/e37dd15bbf8683bab8454ea0eec8e9968ce982b2..refs/heads/andersk:/code/validation.py?ds=sidebyside diff --git a/code/validation.py b/code/validation.py old mode 100644 new mode 100755 index fd2b979..26a49a3 --- a/code/validation.py +++ b/code/validation.py @@ -1,103 +1,152 @@ #!/usr/bin/python +import cache_acls import getafsgroups import re import string -from sipb_xen_database import Machine, NIC -from webcommon import InvalidInput, g +import dns.resolver +from invirt import authz +from invirt.database import Machine, NIC, Type, Disk, CDROM, Autoinstall, Owner +from invirt.config import structs as config +from invirt.common import InvalidInput, CodeError -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): +class Validate: + def __init__(self, username, state, machine_id=None, name=None, description=None, owner=None, + admin=None, contact=None, memory=None, disksize=None, + vmtype=None, cdrom=None, autoinstall=None, strict=False): + # XXX Successive quota checks aren't a good idea, since you + # can't necessarily change the locker and disk size at the + # same time. + created_new = (machine_id is None) + + if strict: + if name is None: + raise InvalidInput('name', name, "You must provide a machine name.") + if description is None or description.strip() == '': + raise InvalidInput('description', description, "You must provide a description.") + if memory is None: + raise InvalidInput('memory', memory, "You must provide a memory size.") + if disksize is None: + raise InvalidInput('disk', disksize, "You must provide a disk size.") + + if machine_id is not None: + self.machine = testMachineId(username, state, machine_id) + machine = getattr(self, 'machine', None) + + owner = testOwner(username, owner, machine) + if owner is not None: + self.owner = owner + self.admin = testAdmin(username, admin, machine) + contact = testContact(username, contact, machine) + if contact is not None: + self.contact = contact + name = testName(username, name, machine) + if name is not None: + self.name = name + description = testDescription(username, description, machine) + if description is not None: + self.description = description + if memory is not None: + self.memory = validMemory(self.owner, state, memory, machine, + on=not created_new) + if disksize is not None: + self.disksize = validDisk(self.owner, state, disksize, machine) + if vmtype is not None: + self.vmtype = validVmType(vmtype) + if cdrom is not None: + if not CDROM.query.get(cdrom): + raise CodeError("Invalid cdrom type '%s'" % cdrom) + self.cdrom = cdrom + if autoinstall is not None: + #raise InvalidInput('autoinstall', 'install', + # "The autoinstaller has been temporarily disabled") + self.autoinstall = Autoinstall.query.get(autoinstall) + + +def getMachinesByOwner(owner, 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 - return Machine.select_by(owner=owner) + return Machine.query.filter_by(owner=owner) -def maxMemory(user, machine=None, on=True): +def maxMemory(owner, g, 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 + 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 machine is not None and machine.memory > MAX_MEMORY_SINGLE: - # If they've been blessed, let them have it - return machine.memory + (quota_total, quota_single) = Owner.getMemoryQuotas(machine.owner if machine else owner) + if not on: - return MAX_MEMORY_SINGLE - machines = getMachinesByOwner(user, machine) - active_machines = [x for x in machines if g.uptimes.get(x)] + return quota_single + machines = getMachinesByOwner(owner, machine) + active_machines = [m for m in machines if m.name in g.xmlist_raw] 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.get(x)] - if len(machines) >= MAX_VMS_TOTAL: + return min(quota_single, quota_total-mem_usage) + +def maxDisk(owner, machine=None): + """Return the maximum disk that a machine can reach. + + If machine is None, the maximum disk for a new machine. Otherwise, + return the maximum that a given machine can be changed to. + """ + (quota_total, quota_single) = Owner.getDiskQuotas(machine.owner if machine else owner) + + if machine is not None: + machine_id = machine.machine_id + else: + machine_id = None + disk_usage_query = Disk.query.filter(Disk.machine_id != machine_id).\ + join('machine').filter_by(owner=owner) + + disk_usage = sum([m.size for m in disk_usage_query]) or 0 + return min(quota_single, quota_total-disk_usage/1024.) + +def cantAddVm(owner, g): + machines = getMachinesByOwner(owner) + active_machines = [m for m in machines if m.name in g.xmlist_raw] + (quota_total, quota_active) = Owner.getVMQuotas(owner) + if machines.count() >= quota_total: return 'You have too many VMs to create a new one.' - if len(active_machines) >= MAX_VMS_ACTIVE: + if len(active_machines) >= quota_active: return ('You already have the maximum number of VMs turned on. ' 'To create more, turn one off.') return False -def validAddVm(user): - reason = cantAddVm(user) - if reason: - raise InvalidInput('create', True, reason) - return True - -def haveAccess(user, machine): +def haveAccess(user, state, machine): """Return whether a user has administrative access to a machine""" - if user in (machine.administrator, machine.owner): - return True - if getafsgroups.checkAfsGroup(user, machine.administrator, - 'athena.mit.edu'): #XXX Cell? - return True - if not getafsgroups.notLockerOwner(user, machine.owner): - return True - return owns(user, machine) + return (user in cache_acls.accessList(machine) + or (machine.adminable and state.isadmin)) def owns(user, machine): """Return whether a user owns a machine""" - return not getafsgroups.notLockerOwner(user, machine.owner) + return user in authz.expandOwner(machine.owner) 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: + charset = string.lowercase + string.digits + '-' + if '-' in (name[0], name[-1]) or len(name) > 63: return False for x in name: if x not in charset: return False return True -def validMemory(user, memory, machine=None, on=True): - """Parse and validate limits for memory for a given user and machine. +def validMemory(owner, g, memory, machine=None, on=True): + """Parse and validate limits for memory for a given owner and machine. on is whether the memory must be valid after the machine is switched on. @@ -107,21 +156,21 @@ def validMemory(user, memory, machine=None, on=True): if memory < MIN_MEMORY_SINGLE: raise ValueError except ValueError: - raise InvalidInput('memory', memory, + raise InvalidInput('memory', memory, "Minimum %s MiB" % MIN_MEMORY_SINGLE) - if memory > maxMemory(user, machine, on): + max_val = maxMemory(owner, g, machine, on) + if not g.isadmin and memory > max_val: raise InvalidInput('memory', memory, - 'Maximum %s MiB for %s' % (maxMemory(user, machine), - user)) + 'Maximum %s MiB for %s' % (max_val, owner)) return memory -def validDisk(user, disk, machine=None): - """Parse and validate limits for disk for a given user and machine.""" +def validDisk(owner, g, disk, machine=None): + """Parse and validate limits for disk for a given owner and machine.""" try: disk = float(disk) - if disk > maxDisk(user, machine): + if not g.isadmin and disk > maxDisk(owner, machine): raise InvalidInput('disk', disk, - "Maximum %s G" % maxDisk(user, machine)) + "Maximum %s G" % maxDisk(owner, machine)) disk = int(disk * 1024) if disk < MIN_DISK_SINGLE * 1024: raise ValueError @@ -129,65 +178,122 @@ def validDisk(user, disk, machine=None): raise InvalidInput('disk', disk, "Minimum %s GiB" % MIN_DISK_SINGLE) return disk - -def testMachineId(user, machine_id, exists=True): + +def validVmType(vm_type): + if vm_type is None: + return None + t = Type.query.get(vm_type) + if t is None: + raise CodeError("Invalid vm type '%s'" % vm_type) + return t + +def testMachineId(user, state, machine_id, exists=True): """Parse, validate and check authorization for a given user and machine. If exists is False, don't check that it exists. """ if machine_id is None: - raise InvalidInput('machine_id', machine_id, + raise InvalidInput('machine_id', machine_id, "Must specify a machine ID.") try: machine_id = int(machine_id) except ValueError: raise InvalidInput('machine_id', machine_id, "Must be an integer.") - machine = Machine.get(machine_id) + machine = Machine.query.get(machine_id) if exists and machine is None: raise InvalidInput('machine_id', machine_id, "Does not exist.") - if machine is not None and not haveAccess(user, machine): + if machine is not None and not haveAccess(user, state, machine): raise InvalidInput('machine_id', machine_id, "You do not have access to this machine.") return machine def testAdmin(user, admin, machine): - if admin in (None, machine.administrator): + """Determine whether a user can set the admin of a machine to this value. + + Return the value to set the admin field to (possibly 'system:' + admin). + """ + if admin is None: return None + if machine is not None and admin == machine.administrator: + return admin if admin == user: return admin - if getafsgroups.checkAfsGroup(user, admin, 'athena.mit.edu'): + # we do not require that the user be in the admin group; + # just that it is a non-empty set + if authz.expandAdmin(admin): return admin - if getafsgroups.checkAfsGroup(user, 'system:'+admin, - 'athena.mit.edu'): - return 'system:'+admin - return admin - + if ':' not in admin: + if authz.expandAdmin('system:' + admin): + return 'system:' + admin + errmsg = 'No user "%s" or non-empty group "system:%s" found.' % (admin, admin) + else: + errmsg = 'No non-empty group "%s" found.' % (admin,) + raise InvalidInput('administrator', admin, errmsg) + def testOwner(user, owner, machine=None): - if owner == user or machine is not None and owner == machine.owner: - return owner + """Determine whether a user can set the owner of a machine to this value. + + If machine is None, this is the owner of a new machine. + """ + if machine is not None and owner in (machine.owner, None): + return machine.owner if owner is None: raise InvalidInput('owner', owner, "Owner must be specified") - value = getafsgroups.notLockerOwner(user, owner) - if not value: - return owner - raise InvalidInput('owner', owner, value) + if '@' in owner: + raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed") + try: + if user not in authz.expandOwner(owner): + raise InvalidInput('owner', owner, 'You do not have access to the ' + + owner + ' locker') + except getafsgroups.AfsProcessError, e: + raise InvalidInput('owner', owner, str(e)) + return owner def testContact(user, contact, machine=None): - if contact in (None, machine.contact): + if contact is None or (machine is not None and contact == 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): + if name is None: + return None + name = name.lower() + if machine is not None and name == machine.name: return None - if not Machine.select_by(name=name): + try: + hostname = '%s.%s.' % (name, config.dns.domains[0]) + resolver = dns.resolver.Resolver() + resolver.nameservers = ['127.0.0.1'] + try: + resolver.query(hostname, 'A') + except dns.resolver.NoAnswer, e: + # If we can get the TXT record, then we can verify it's + # reserved. If this lookup fails, let it bubble up and be + # dealt with + answer = resolver.query(hostname, 'TXT') + txt = answer[0].strings[0] + if txt.startswith('reserved'): + raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact)) + + # If the hostname didn't exist, it would have thrown an + # exception by now - error out + raise InvalidInput('name', name, 'Name is already taken.') + except dns.resolver.NXDOMAIN, e: + if not validMachineName(name): + raise InvalidInput('name', name, 'You must provide a machine name. Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.') return name - raise InvalidInput('name', name, "Name is already taken.") + except InvalidInput: + raise + except: + # Any other error is a validation failure + raise InvalidInput('name', name, 'We were unable to verify that this name is available. If you believe this is in error, please contact us at %s' % config.dns.contact) + +def testDescription(user, description, machine=None): + if description is None or description.strip() == '': + return None + return description.strip() def testHostname(user, hostname, machine): for nic in machine.nics: