source: trunk/packages/invirt-database/python/database/validate.py @ 2574

Last change on this file since 2574 was 2557, checked in by broder, 15 years ago

Re-arrange the authz configuration.

In particular, even if we allow for mixing of multiple authz
mechanisms at some point, you won't have multiple instances of the
locker authz type, so the "type" shouldn't be a property of each of
the cells we specify how to authenticate against.

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