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