source: trunk/packages/sipb-xen-www/code/validation.py @ 835

Last change on this file since 835 was 649, checked in by ecprice, 16 years ago

More accurate error message for new machine naming guidelines.

File size: 10.7 KB
Line 
1#!/usr/bin/python
2
3import cache_acls
4import getafsgroups
5import re
6import string
7from sipb_xen_database import Machine, NIC, Type, Disk, CDROM, Autoinstall
8from webcommon import InvalidInput
9
10MAX_MEMORY_TOTAL = 512
11MAX_MEMORY_SINGLE = 256
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        admin = testAdmin(username, admin, machine)
46        if admin is not None:
47            self.admin = admin
48        contact = testContact(username, contact, machine)
49        if contact is not None:
50            self.contact = contact
51        name = testName(username, name, machine)
52        if name is not None:
53            self.name = name
54        description = testDescription(username, description, machine)
55        if description is not None:
56            self.description = description
57        if memory is not None:
58            self.memory = validMemory(self.owner, state, memory, machine,
59                                      on=not created_new)
60        if disksize is not None:
61            self.disksize = validDisk(self.owner, state, disksize, machine)
62        if vmtype is not None:
63            self.vmtype = validVmType(vmtype)
64        if cdrom is not None:
65            if not CDROM.get(cdrom):
66                raise CodeError("Invalid cdrom type '%s'" % cdrom)
67            self.cdrom = cdrom
68        if autoinstall is not None:
69            self.autoinstall = Autoinstall.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.select_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    if machine is not None and machine.memory > MAX_MEMORY_SINGLE:
93        # If they've been blessed, let them have it
94        return machine.memory
95    if not on:
96        return MAX_MEMORY_SINGLE
97    machines = getMachinesByOwner(owner, machine)
98    active_machines = [m for m in machines if m.name in g.xmlist_raw]
99    mem_usage = sum([x.memory for x in active_machines if x != machine])
100    return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
101
102def maxDisk(owner, machine=None):
103    """Return the maximum disk that a machine can reach.
104
105    If machine is None, the maximum disk for a new machine. Otherwise,
106    return the maximum that a given machine can be changed to.
107    """
108    if machine is not None:
109        machine_id = machine.machine_id
110    else:
111        machine_id = None
112    disk_usage = Disk.query().filter_by(Disk.c.machine_id != machine_id,
113                                        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 len(machines) >= 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 state.overlord or user in cache_acls.accessList(machine)
129
130def owns(user, machine):
131    """Return whether a user owns a machine"""
132    return user in expandLocker(machine.owner)
133
134def validMachineName(name):
135    """Check that name is valid for a machine name"""
136    if not name:
137        return False
138    charset = string.lowercase + string.digits + '-'
139    if '-' in (name[0], name[-1]) or len(name) > 63:
140        return False
141    for x in name:
142        if x not in charset:
143            return False
144    return True
145
146def validMemory(owner, g, memory, machine=None, on=True):
147    """Parse and validate limits for memory for a given owner and machine.
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:
157        raise InvalidInput('memory', memory,
158                           "Minimum %s MiB" % MIN_MEMORY_SINGLE)
159    max_val = maxMemory(owner, g, machine, on)
160    if not g.overlord and memory > max_val:
161        raise InvalidInput('memory', memory,
162                           'Maximum %s MiB for %s' % (max_val, owner))
163    return memory
164
165def validDisk(owner, g, disk, machine=None):
166    """Parse and validate limits for disk for a given owner and machine."""
167    try:
168        disk = float(disk)
169        if not g.overlord and disk > maxDisk(owner, machine):
170            raise InvalidInput('disk', disk,
171                               "Maximum %s G" % maxDisk(owner, machine))
172        disk = int(disk * 1024)
173        if disk < MIN_DISK_SINGLE * 1024:
174            raise ValueError
175    except ValueError:
176        raise InvalidInput('disk', disk,
177                           "Minimum %s GiB" % MIN_DISK_SINGLE)
178    return disk
179
180def validVmType(vm_type):
181    if vm_type is None:
182        return None
183    t = Type.get(vm_type)
184    if t is None:
185        raise CodeError("Invalid vm type '%s'"  % vm_type)
186    return t
187
188def testMachineId(user, state, machine_id, exists=True):
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:
194        raise InvalidInput('machine_id', machine_id,
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.")
200    machine = Machine.get(machine_id)
201    if exists and machine is None:
202        raise InvalidInput('machine_id', machine_id, "Does not exist.")
203    if machine is not None and not haveAccess(user, state, machine):
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):
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    """
214    if admin is None:
215        return None
216    if machine is not None and admin == machine.administrator:
217        return None
218    if admin == user:
219        return admin
220    if ':' not in admin:
221        if cache_acls.isUser(admin):
222            return admin
223        admin = 'system:' + admin
224    try:
225        if user in getafsgroups.getAfsGroupMembers(admin, 'athena.mit.edu'):
226            return admin
227    except getafsgroups.AfsProcessError, e:
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)
232    #XXX Should we require that user is in the admin group?
233    return admin
234
235def testOwner(user, owner, machine=None):
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    """
240    if owner == user:
241        return owner
242    if machine is not None and owner in (machine.owner, None):
243        return machine.owner
244    if owner is None:
245        raise InvalidInput('owner', owner, "Owner must be specified")
246    try:
247        if user not in cache_acls.expandLocker(owner):
248            raise InvalidInput('owner', owner, 'You do not have access to the '
249                               + owner + ' locker')
250    except getafsgroups.AfsProcessError, e:
251        raise InvalidInput('owner', owner, str(e))
252    return owner
253
254def testContact(user, contact, machine=None):
255    if contact is None or (machine is not None and contact == machine.contact):
256        return None
257    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
258        raise InvalidInput('contact', contact, "Not a valid email.")
259    return contact
260
261def testDisk(user, disksize, machine=None):
262    return disksize
263
264def testName(user, name, machine=None):
265    if name is None:
266        return None
267    name = name.lower()
268    if machine is not None and name == machine.name:
269        return None
270    if not Machine.select_by(name=name):
271        if not validMachineName(name):
272            raise InvalidInput('name', name, 'You must provide a machine name.  Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
273        return name
274    raise InvalidInput('name', name, "Name is already taken.")
275
276def testDescription(user, description, machine=None):
277    if description is None or description.strip() == '':
278        return None
279    return description.strip()
280
281def testHostname(user, hostname, machine):
282    for nic in machine.nics:
283        if hostname == nic.hostname:
284            return hostname
285    # check if doesn't already exist
286    if NIC.select_by(hostname=hostname):
287        raise InvalidInput('hostname', hostname,
288                           "Already exists")
289    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
290        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
291                           "must only use number, letters, and dashes.")
292    return hostname
Note: See TracBrowser for help on using the repository browser.