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

Last change on this file since 1490 was 1473, checked in by broder, 16 years ago

Validate hostnames by querying to see if the name exists in DNS, and
add an appropriate dependency

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