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

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

RAM quotas at remctl; RAM quota exception script, table, and usage in -web and -remote-create; /etc/nocreate support

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