From: Paul Weaver Date: Tue, 17 Nov 2009 03:10:46 +0000 (-0500) Subject: validate.py is the new validation code for invirt-web and invirt-remote X-Git-Tag: 0.2.2~3 X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-database.git/commitdiff_plain/59294cdc7be3c7df5eadcec430c13dfcf6054d96?ds=sidebyside validate.py is the new validation code for invirt-web and invirt-remote it is still incomplete svn path=/trunk/packages/invirt-database/; revision=2540 --- diff --git a/python/database/validate.py b/python/database/validate.py new file mode 100644 index 0000000..c072367 --- /dev/null +++ b/python/database/validate.py @@ -0,0 +1,329 @@ +#!/usr/bin/python + +#import cache_acls +#import getafsgroups +import re +import string +#import dns.resolver +#from invirt.database import Machine, NIC, Type, Disk, CDROM, Autoinstall, Owner +#from invirt.config import structs as config +#from invirt.common import InvalidInput, CodeError + +MIN_MEMORY_SINGLE = 16 +MIN_DISK_SINGLE = 0.1 + +""" +validate takes a Machine with values for every +property you want to change and None for everything else + +username is the user makeing the changes for authorization + +Returns: list of changes to make +Errors: when something is invalid it throws an error + +ToDo: create EmptyMachine Class +ToDo: redo all the other methods. They were stolen from web code + +""" + +def validate(username,machineValid): + + if machineValid.name is None: + raise InvalidInput('name', name, "You must provide a machine name.") + + machine = Machine.query() + + authorize(username,machine) + + testQuota = False + + if machineValid.machine_id is not None: + machineValid.machine = testMachineId(state, machineValid.machine_id) + + if machineValid.owner is not None: + machineValid.owner = testOwner(username, machineValid.owner, machine) + testQuote = True + + if machineValid.admin is not None: + machineValid.admin = testAdmin(username, machineValid.admin, machine) + testQuota = True + + if contact is not None: + machineValid.contact = testContact(machineValid.contact, machine) + + if name is not None: + machineValid.name = testName(machineValid.name, machine) + + if description is not None: + machineValid.description = testDescription(machineValid.description, machine) + + if machineValid.memory is not None: + machineValid.memory = validMemory(machineValid.memory, machine) + testQuote = True + + if machineValid.disksize is not None: + machineValid.disksize = validDisk(machineValid.disksize, machine) + testQuote = True + + if machineValid.vmtype is not None: + machineValid.vmtype = validVmType(machineValid.vmtype) + + if machineValid.cdrom is not None: + if not CDROM.query().get(machineValid.cdrom): + raise CodeError("Invalid cdrom type '%s'" % machineValid.cdrom) + + if machineValid.autoinstall is not None: + #raise InvalidInput('autoinstall', 'install', + # "The autoinstaller has been temporarily disabled") + machineValid.autoinstall = Autoinstall.query().get(machineValid.autoinstall) + + if testQuota: + validQuota(username, machineValid.owner, machine) + + return machineValid + + +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 + return Machine.query().filter_by(owner=owner) + +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 + 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. + """ + (quota_total, quota_single) = Owner.getMemoryQuotas(machine.owner if machine else owner) + + if not on: + 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(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 = Disk.query().filter(Disk.c.machine_id != machine_id).\ + join('machine').\ + filter_by(owner=owner).sum(Disk.c.size) 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) >= quota_active: + return ('You already have the maximum number of VMs turned on. ' + 'To create more, turn one off.') + return False + +def haveAccess(user, state, machine): + """Return whether a user has administrative access to a 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 user in expandLocker(machine.owner) + +def validMachineName(name): + """Check that name is valid for a machine name""" + if not name: + return False + 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(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. + """ + try: + memory = int(memory) + if memory < MIN_MEMORY_SINGLE: + raise ValueError + except ValueError: + raise InvalidInput('memory', memory, + "Minimum %s MiB" % MIN_MEMORY_SINGLE) + max_val = maxMemory(owner, g, machine, on) + if not g.isadmin and memory > max_val: + raise InvalidInput('memory', memory, + 'Maximum %s MiB for %s' % (max_val, owner)) + return memory + +def validDisk(owner, g, disk, machine=None): + """Parse and validate limits for disk for a given owner and machine.""" + try: + disk = float(disk) + if not g.isadmin and disk > maxDisk(owner, machine): + raise InvalidInput('disk', disk, + "Maximum %s G" % maxDisk(owner, machine)) + disk = int(disk * 1024) + if disk < MIN_DISK_SINGLE * 1024: + raise ValueError + except ValueError: + raise InvalidInput('disk', disk, + "Minimum %s GiB" % MIN_DISK_SINGLE) + return disk + +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, + "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.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, state, machine): + raise InvalidInput('machine_id', machine_id, + "You do not have access to this machine.") + return machine + +def testAdmin(user, admin, machine): + """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). XXX is modifying this a good idea? + """ + if admin is None: + return None + if machine is not None and admin == machine.administrator: + return admin + if admin == user: + return admin + if ':' not in admin: + if cache_acls.isUser(admin): + return admin + admin = 'system:' + admin + try: + if user in getafsgroups.getAfsGroupMembers(admin, config.authz[0].cell): + return admin + except getafsgroups.AfsProcessError, e: + errmsg = str(e) + if errmsg.startswith("pts: User or group doesn't exist"): + errmsg = 'The group "%s" does not exist.' % admin + raise InvalidInput('administrator', admin, errmsg) + #XXX Should we require that user is in the admin group? + return admin + +def testOwner(user, owner, machine=None): + """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") + if '@' in owner: + raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed") + try: + if user not in cache_acls.expandLocker(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 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 testName(user, name, machine=None): + if name is None: + return None + name = name.lower() + if machine is not None and name == machine.name: + return None + 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 + 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: + 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