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