validate.py is the new validation code for invirt-web and invirt-remote
[invirt/packages/invirt-database.git] / python / database / validate.py
1 #!/usr/bin/python
2
3 #import cache_acls
4 #import getafsgroups
5 import re
6 import 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
12 MIN_MEMORY_SINGLE = 16
13 MIN_DISK_SINGLE = 0.1
14
15 """
16 validate takes a Machine with values for every
17 property you want to change and None for everything else
18
19 username is the user makeing the changes for authorization
20
21 Returns: list of changes to make
22 Errors: when something is invalid it throws an error
23
24 ToDo: create EmptyMachine Class
25 ToDo: redo all the other methods. They were stolen from web code
26
27 """
28
29 def 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
86 def 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
96 def 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
115 def 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
132 def 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
143 def 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
148 def owns(user, machine):
149     """Return whether a user owns a machine"""
150     return user in expandLocker(machine.owner)
151
152 def 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
164 def 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
183 def 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
198 def 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
206 def 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
226 def 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[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
253 def 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
272 def 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
279 def 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
313 def testDescription(user, description, machine=None):
314     if description is None or description.strip() == '':
315         return None
316     return description.strip()
317
318 def 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