source: trunk/packages/invirt-web/code/validation.py @ 2168

Last change on this file since 2168 was 2141, checked in by iannucci, 16 years ago

fixed a minor bug in validation.py

File size: 12.0 KB
RevLine 
[209]1#!/usr/bin/python
2
[411]3import cache_acls
[209]4import getafsgroups
5import re
6import string
[1473]7import dns.resolver
[2132]8from invirt.database import Machine, NIC, Type, Disk, CDROM, Autoinstall, Owner
[879]9from invirt.config import structs as config
[2064]10from invirt.common import InvalidInput, CodeError
[209]11
12MIN_MEMORY_SINGLE = 16
13MIN_DISK_SINGLE = 0.1
14
[572]15class Validate:
[609]16    def __init__(self, username, state, machine_id=None, name=None, description=None, owner=None,
[572]17                 admin=None, contact=None, memory=None, disksize=None,
[629]18                 vmtype=None, cdrom=None, autoinstall=None, strict=False):
[572]19        # XXX Successive quota checks aren't a good idea, since you
20        # can't necessarily change the locker and disk size at the
21        # same time.
22        created_new = (machine_id is None)
23
[577]24        if strict:
25            if name is None:
26                raise InvalidInput('name', name, "You must provide a machine name.")
[609]27            if description is None:
28                raise InvalidInput('description', description, "You must provide a description.")
[577]29            if memory is None:
30                raise InvalidInput('memory', memory, "You must provide a memory size.")
31            if disksize is None:
32                raise InvalidInput('disk', disksize, "You must provide a disk size.")
33
[572]34        if machine_id is not None:
[632]35            self.machine = testMachineId(username, state, machine_id)
[572]36        machine = getattr(self, 'machine', None)
37
38        owner = testOwner(username, owner, machine)
39        if owner is not None:
40            self.owner = owner
[1709]41        self.admin = testAdmin(username, admin, machine)
[572]42        contact = testContact(username, contact, machine)
43        if contact is not None:
44            self.contact = contact
45        name = testName(username, name, machine)
46        if name is not None:
47            self.name = name
[609]48        description = testDescription(username, description, machine)
49        if description is not None:
50            self.description = description
[572]51        if memory is not None:
52            self.memory = validMemory(self.owner, state, memory, machine,
53                                      on=not created_new)
54        if disksize is not None:
[632]55            self.disksize = validDisk(self.owner, state, disksize, machine)
[572]56        if vmtype is not None:
57            self.vmtype = validVmType(vmtype)
58        if cdrom is not None:
[1013]59            if not CDROM.query().get(cdrom):
[572]60                raise CodeError("Invalid cdrom type '%s'" % cdrom)
61            self.cdrom = cdrom
[629]62        if autoinstall is not None:
[1721]63            #raise InvalidInput('autoinstall', 'install',
64            #                   "The autoinstaller has been temporarily disabled")
[1013]65            self.autoinstall = Autoinstall.query().get(autoinstall)
[572]66
67
68def getMachinesByOwner(owner, machine=None):
[209]69    """Return the machines owned by the same as a machine.
[440]70
[209]71    If the machine is None, return the machines owned by the same
72    user.
73    """
74    if machine:
75        owner = machine.owner
[1001]76    return Machine.query().filter_by(owner=owner)
[209]77
[572]78def maxMemory(owner, g, machine=None, on=True):
[209]79    """Return the maximum memory for a machine or a user.
80
[440]81    If machine is None, return the memory available for a new
[209]82    machine.  Else, return the maximum that machine can have.
83
84    on is whether the machine should be turned on.  If false, the max
85    memory for the machine to change to, if it is left off, is
86    returned.
87    """
[2134]88    (quota_total, quota_single) = Owner.getMemoryQuotas(machine.owner if machine else owner)
[2132]89
[253]90    if not on:
[2132]91        return quota_single
[572]92    machines = getMachinesByOwner(owner, machine)
[575]93    active_machines = [m for m in machines if m.name in g.xmlist_raw]
[209]94    mem_usage = sum([x.memory for x in active_machines if x != machine])
[2132]95    return min(quota_single, quota_total-mem_usage)
[209]96
[572]97def maxDisk(owner, machine=None):
[440]98    """Return the maximum disk that a machine can reach.
99
100    If machine is None, the maximum disk for a new machine. Otherwise,
101    return the maximum that a given machine can be changed to.
102    """
[2134]103    (quota_total, quota_single) = Owner.getDiskQuotas(machine.owner if machine else owner)
104
[535]105    if machine is not None:
106        machine_id = machine.machine_id
107    else:
108        machine_id = None
[1001]109    disk_usage = Disk.query().filter(Disk.c.machine_id != machine_id).\
110                     join('machine').\
111                     filter_by(owner=owner).sum(Disk.c.size) or 0
[2134]112    return min(quota_single, quota_total-disk_usage/1024.)
[209]113
[572]114def cantAddVm(owner, g):
115    machines = getMachinesByOwner(owner)
[575]116    active_machines = [m for m in machines if m.name in g.xmlist_raw]
[2141]117    (quota_total, quota_active) = Owner.getVMQuotas(owner)
[2134]118    if machines.count() >= quota_total:
[209]119        return 'You have too many VMs to create a new one.'
[2134]120    if len(active_machines) >= quota_active:
[209]121        return ('You already have the maximum number of VMs turned on.  '
122                'To create more, turn one off.')
123    return False
124
[632]125def haveAccess(user, state, machine):
[235]126    """Return whether a user has administrative access to a machine"""
[874]127    return (user in cache_acls.accessList(machine)
128            or (machine.adminable and state.isadmin))
[209]129
130def owns(user, machine):
131    """Return whether a user owns a machine"""
[411]132    return user in expandLocker(machine.owner)
[209]133
134def validMachineName(name):
135    """Check that name is valid for a machine name"""
136    if not name:
137        return False
[648]138    charset = string.lowercase + string.digits + '-'
139    if '-' in (name[0], name[-1]) or len(name) > 63:
[209]140        return False
141    for x in name:
142        if x not in charset:
143            return False
144    return True
145
[572]146def validMemory(owner, g, memory, machine=None, on=True):
147    """Parse and validate limits for memory for a given owner and machine.
[209]148
149    on is whether the memory must be valid after the machine is
150    switched on.
151    """
152    try:
153        memory = int(memory)
154        if memory < MIN_MEMORY_SINGLE:
155            raise ValueError
156    except ValueError:
[440]157        raise InvalidInput('memory', memory,
[211]158                           "Minimum %s MiB" % MIN_MEMORY_SINGLE)
[572]159    max_val = maxMemory(owner, g, machine, on)
[867]160    if not g.isadmin and memory > max_val:
[209]161        raise InvalidInput('memory', memory,
[572]162                           'Maximum %s MiB for %s' % (max_val, owner))
[209]163    return memory
164
[632]165def validDisk(owner, g, disk, machine=None):
[572]166    """Parse and validate limits for disk for a given owner and machine."""
[209]167    try:
168        disk = float(disk)
[867]169        if not g.isadmin and disk > maxDisk(owner, machine):
[209]170            raise InvalidInput('disk', disk,
[572]171                               "Maximum %s G" % maxDisk(owner, machine))
[209]172        disk = int(disk * 1024)
173        if disk < MIN_DISK_SINGLE * 1024:
174            raise ValueError
175    except ValueError:
176        raise InvalidInput('disk', disk,
[211]177                           "Minimum %s GiB" % MIN_DISK_SINGLE)
[209]178    return disk
[437]179
180def validVmType(vm_type):
[440]181    if vm_type is None:
182        return None
[1013]183    t = Type.query().get(vm_type)
[440]184    if t is None:
[437]185        raise CodeError("Invalid vm type '%s'"  % vm_type)
[440]186    return t
[437]187
[632]188def testMachineId(user, state, machine_id, exists=True):
[209]189    """Parse, validate and check authorization for a given user and machine.
190
191    If exists is False, don't check that it exists.
192    """
193    if machine_id is None:
[440]194        raise InvalidInput('machine_id', machine_id,
[209]195                           "Must specify a machine ID.")
196    try:
197        machine_id = int(machine_id)
198    except ValueError:
199        raise InvalidInput('machine_id', machine_id, "Must be an integer.")
[1013]200    machine = Machine.query().get(machine_id)
[209]201    if exists and machine is None:
202        raise InvalidInput('machine_id', machine_id, "Does not exist.")
[632]203    if machine is not None and not haveAccess(user, state, machine):
[209]204        raise InvalidInput('machine_id', machine_id,
205                           "You do not have access to this machine.")
206    return machine
207
208def testAdmin(user, admin, machine):
[411]209    """Determine whether a user can set the admin of a machine to this value.
210
211    Return the value to set the admin field to (possibly 'system:' +
212    admin).  XXX is modifying this a good idea?
213    """
[575]214    if admin is None:
[209]215        return None
[575]216    if machine is not None and admin == machine.administrator:
[1709]217        return admin
[228]218    if admin == user:
[209]219        return admin
[411]220    if ':' not in admin:
221        if cache_acls.isUser(admin):
222            return admin
223        admin = 'system:' + admin
[413]224    try:
[879]225        if user in getafsgroups.getAfsGroupMembers(admin, config.authz[0].cell):
[413]226            return admin
227    except getafsgroups.AfsProcessError, e:
[431]228        errmsg = str(e)
229        if errmsg.startswith("pts: User or group doesn't exist"):
230            errmsg = 'The group "%s" does not exist.' % admin
231        raise InvalidInput('administrator', admin, errmsg)
[413]232    #XXX Should we require that user is in the admin group?
[209]233    return admin
[440]234
[228]235def testOwner(user, owner, machine=None):
[411]236    """Determine whether a user can set the owner of a machine to this value.
237
238    If machine is None, this is the owner of a new machine.
239    """
[572]240    if machine is not None and owner in (machine.owner, None):
[573]241        return machine.owner
[228]242    if owner is None:
243        raise InvalidInput('owner', owner, "Owner must be specified")
[411]244    try:
245        if user not in cache_acls.expandLocker(owner):
246            raise InvalidInput('owner', owner, 'You do not have access to the '
247                               + owner + ' locker')
248    except getafsgroups.AfsProcessError, e:
249        raise InvalidInput('owner', owner, str(e))
250    return owner
[209]251
252def testContact(user, contact, machine=None):
[576]253    if contact is None or (machine is not None and contact == machine.contact):
[209]254        return None
255    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
256        raise InvalidInput('contact', contact, "Not a valid email.")
257    return contact
258
259def testDisk(user, disksize, machine=None):
260    return disksize
261
262def testName(user, name, machine=None):
[572]263    if name is None:
[209]264        return None
[647]265    name = name.lower()
[572]266    if machine is not None and name == machine.name:
267        return None
[1473]268    try:
[1492]269        hostname = '%s.%s.' % (name, config.dns.domains[0])
[1542]270        resolver = dns.resolver.Resolver()
271        resolver.nameservers = ['127.0.0.1']
[1492]272        try:
[1542]273            resolver.query(hostname, 'A')
[1492]274        except dns.resolver.NoAnswer, e:
275            # If we can get the TXT record, then we can verify it's
276            # reserved. If this lookup fails, let it bubble up and be
277            # dealt with
[1542]278            answer = resolver.query(hostname, 'TXT')
[1492]279            txt = answer[0].strings[0]
[1495]280            if txt.startswith('reserved'):
[1492]281                raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
282
[1473]283        # If the hostname didn't exist, it would have thrown an
284        # exception by now - error out
285        raise InvalidInput('name', name, 'Name is already taken.')
286    except dns.resolver.NXDOMAIN, e:
[572]287        if not validMachineName(name):
[649]288            raise InvalidInput('name', name, 'You must provide a machine name.  Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
[209]289        return name
[1473]290    except InvalidInput:
291        raise
292    except:
293        # Any other error is a validation failure
294        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)
[209]295
[609]296def testDescription(user, description, machine=None):
297    if description is None or description.strip() == '':
298        return None
299    return description.strip()
300
[209]301def testHostname(user, hostname, machine):
302    for nic in machine.nics:
303        if hostname == nic.hostname:
304            return hostname
305    # check if doesn't already exist
306    if NIC.select_by(hostname=hostname):
307        raise InvalidInput('hostname', hostname,
308                           "Already exists")
309    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
310        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
311                           "must only use number, letters, and dashes.")
312    return hostname
Note: See TracBrowser for help on using the repository browser.