"Cherry-pick" r2557 (Re-arrange the authz configuration.) to the
[invirt/packages/invirt-web.git] / code / validation.py
old mode 100644 (file)
new mode 100755 (executable)
index c0e3aeb..fc9b969
@@ -4,19 +4,68 @@ import cache_acls
 import getafsgroups
 import re
 import string
 import getafsgroups
 import re
 import string
-from sipb_xen_database import Machine, NIC, Type, Disk
-from webcommon import InvalidInput, g
+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
 
 
-MAX_MEMORY_TOTAL = 512
-MAX_MEMORY_SINGLE = 256
 MIN_MEMORY_SINGLE = 16
 MIN_MEMORY_SINGLE = 16
-MAX_DISK_TOTAL = 50
-MAX_DISK_SINGLE = 50
 MIN_DISK_SINGLE = 0.1
 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:
+                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
     """Return the machines owned by the same as a machine.
 
     If the machine is None, return the machines owned by the same
@@ -24,11 +73,9 @@ def getMachinesByOwner(user, machine=None):
     """
     if machine:
         owner = machine.owner
     """
     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
     """Return the maximum memory for a machine or a user.
 
     If machine is None, return the memory available for a new
@@ -38,49 +85,47 @@ def maxMemory(user, machine=None, on=True):
     memory for the machine to change to, if it is left off, is
     returned.
     """
     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:
     if not on:
-        return MAX_MEMORY_SINGLE
-    machines = getMachinesByOwner(user, machine)
-    active_machines = [x for x in machines if g.xmlist.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])
     mem_usage = sum([x.memory for x in active_machines if x != machine])
-    return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
+    return min(quota_single, quota_total-mem_usage)
 
 
-def maxDisk(user, machine=None):
+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.
     """
     """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
     if machine is not None:
         machine_id = machine.machine_id
     else:
         machine_id = None
-    disk_usage = Disk.query().filter_by(Disk.c.machine_id != machine_id,
-                                        owner=user).sum(Disk.c.size) or 0
-    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.xmlist.get(x)]
-    if len(machines) >= MAX_VMS_TOTAL:
+    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.'
         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
 
         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"""
     """Return whether a user has administrative access to a machine"""
-    return user in cache_acls.accessList(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"""
 
 def owns(user, machine):
     """Return whether a user owns a machine"""
@@ -90,16 +135,16 @@ def validMachineName(name):
     """Check that name is valid for a machine name"""
     if not name:
         return False
     """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
 
         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.
 
     on is whether the memory must be valid after the machine is
     switched on.
@@ -111,19 +156,19 @@ def validMemory(user, memory, machine=None, on=True):
     except ValueError:
         raise InvalidInput('memory', memory,
                            "Minimum %s MiB" % MIN_MEMORY_SINGLE)
     except ValueError:
         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,
         raise InvalidInput('memory', memory,
-                           'Maximum %s MiB for %s' % (maxMemory(user, machine),
-                                                      user))
+                           'Maximum %s MiB for %s' % (max_val, owner))
     return memory
 
     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)
     try:
         disk = float(disk)
-        if disk > maxDisk(user, machine):
+        if not g.isadmin and disk > maxDisk(owner, machine):
             raise InvalidInput('disk', disk,
             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
         disk = int(disk * 1024)
         if disk < MIN_DISK_SINGLE * 1024:
             raise ValueError
@@ -135,12 +180,12 @@ def validDisk(user, disk, machine=None):
 def validVmType(vm_type):
     if vm_type is None:
         return None
 def validVmType(vm_type):
     if vm_type is None:
         return None
-    t = Type.get(vm_type)
+    t = Type.query().get(vm_type)
     if t is None:
         raise CodeError("Invalid vm type '%s'"  % vm_type)
     return t
 
     if t is None:
         raise CodeError("Invalid vm type '%s'"  % vm_type)
     return t
 
-def testMachineId(user, machine_id, exists=True):
+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.
     """Parse, validate and check authorization for a given user and machine.
 
     If exists is False, don't check that it exists.
@@ -152,10 +197,10 @@ def testMachineId(user, machine_id, exists=True):
         machine_id = int(machine_id)
     except ValueError:
         raise InvalidInput('machine_id', machine_id, "Must be an integer.")
         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 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
         raise InvalidInput('machine_id', machine_id,
                            "You do not have access to this machine.")
     return machine
@@ -166,8 +211,10 @@ def testAdmin(user, admin, machine):
     Return the value to set the admin field to (possibly 'system:' +
     admin).  XXX is modifying this a good idea?
     """
     Return the value to set the admin field to (possibly 'system:' +
     admin).  XXX is modifying this a good idea?
     """
-    if admin in (None, machine.administrator):
+    if admin is None:
         return None
         return None
+    if machine is not None and admin == machine.administrator:
+        return admin
     if admin == user:
         return admin
     if ':' not in admin:
     if admin == user:
         return admin
     if ':' not in admin:
@@ -175,7 +222,7 @@ def testAdmin(user, admin, machine):
             return admin
         admin = 'system:' + admin
     try:
             return admin
         admin = 'system:' + admin
     try:
-        if user in getafsgroups.getAfsGroupMembers(admin, 'athena.mit.edu'):
+        if user in getafsgroups.getAfsGroupMembers(admin, config.authz.cells[0].cell):
             return admin
     except getafsgroups.AfsProcessError, e:
         errmsg = str(e)
             return admin
     except getafsgroups.AfsProcessError, e:
         errmsg = str(e)
@@ -190,10 +237,12 @@ def testOwner(user, owner, machine=None):
 
     If machine is None, this is the owner of a new machine.
     """
 
     If machine is None, this is the owner of a new machine.
     """
-    if owner == user or machine is not None and owner == machine.owner:
-        return owner
+    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 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 '
     try:
         if user not in cache_acls.expandLocker(owner):
             raise InvalidInput('owner', owner, 'You do not have access to the '
@@ -203,21 +252,50 @@ def testOwner(user, owner, machine=None):
     return owner
 
 def testContact(user, contact, machine=None):
     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
 
         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):
 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
         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
         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:
 
 def testHostname(user, hostname, machine):
     for nic in machine.nics: