5018ca34c1e1910abdbcbc15fdf30e30a9c6759b
[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 import dns.resolver
8 from invirt import authz
9 from invirt.database import Machine, NIC, Type, Disk, CDROM, Autoinstall, Owner
10 from invirt.config import structs as config
11 from invirt.common import InvalidInput, CodeError
12
13 MIN_MEMORY_SINGLE = 16
14 MIN_DISK_SINGLE = 0.1
15
16 class Validate:
17     def __init__(self, username, state, machine_id=None, name=None, description=None, owner=None,
18                  admin=None, contact=None, memory=None, disksize=None,
19                  vmtype=None, cdrom=None, autoinstall=None, strict=False):
20         # XXX Successive quota checks aren't a good idea, since you
21         # can't necessarily change the locker and disk size at the
22         # same time.
23         created_new = (machine_id is None)
24
25         if strict:
26             if name is None:
27                 raise InvalidInput('name', name, "You must provide a machine name.")
28             if description is None or description.strip() == '':
29                 raise InvalidInput('description', description, "You must provide a description.")
30             if memory is None:
31                 raise InvalidInput('memory', memory, "You must provide a memory size.")
32             if disksize is None:
33                 raise InvalidInput('disk', disksize, "You must provide a disk size.")
34
35         if machine_id is not None:
36             self.machine = testMachineId(username, state, machine_id)
37         machine = getattr(self, 'machine', None)
38
39         owner = testOwner(username, owner, machine)
40         if owner is not None:
41             self.owner = owner
42         self.admin = testAdmin(username, admin, machine)
43         contact = testContact(username, contact, machine)
44         if contact is not None:
45             self.contact = contact
46         name = testName(username, name, machine)
47         if name is not None:
48             self.name = name
49         description = testDescription(username, description, machine)
50         if description is not None:
51             self.description = description
52         if memory is not None:
53             self.memory = validMemory(self.owner, state, memory, machine,
54                                       on=not created_new)
55         if disksize is not None:
56             self.disksize = validDisk(self.owner, state, disksize, machine)
57         if vmtype is not None:
58             self.vmtype = validVmType(vmtype)
59         if cdrom is not None:
60             if not CDROM.query().get(cdrom):
61                 raise CodeError("Invalid cdrom type '%s'" % cdrom)
62             self.cdrom = cdrom
63         if autoinstall is not None:
64             #raise InvalidInput('autoinstall', 'install',
65             #                   "The autoinstaller has been temporarily disabled")
66             self.autoinstall = Autoinstall.query().get(autoinstall)
67
68
69 def getMachinesByOwner(owner, machine=None):
70     """Return the machines owned by the same as a machine.
71
72     If the machine is None, return the machines owned by the same
73     user.
74     """
75     if machine:
76         owner = machine.owner
77     return Machine.query().filter_by(owner=owner)
78
79 def maxMemory(owner, g, machine=None, on=True):
80     """Return the maximum memory for a machine or a user.
81
82     If machine is None, return the memory available for a new
83     machine.  Else, return the maximum that machine can have.
84
85     on is whether the machine should be turned on.  If false, the max
86     memory for the machine to change to, if it is left off, is
87     returned.
88     """
89     (quota_total, quota_single) = Owner.getMemoryQuotas(machine.owner if machine else owner)
90
91     if not on:
92         return quota_single
93     machines = getMachinesByOwner(owner, machine)
94     active_machines = [m for m in machines if m.name in g.xmlist_raw]
95     mem_usage = sum([x.memory for x in active_machines if x != machine])
96     return min(quota_single, quota_total-mem_usage)
97
98 def maxDisk(owner, machine=None):
99     """Return the maximum disk that a machine can reach.
100
101     If machine is None, the maximum disk for a new machine. Otherwise,
102     return the maximum that a given machine can be changed to.
103     """
104     (quota_total, quota_single) = Owner.getDiskQuotas(machine.owner if machine else owner)
105
106     if machine is not None:
107         machine_id = machine.machine_id
108     else:
109         machine_id = None
110     disk_usage = Disk.query().filter(Disk.c.machine_id != machine_id).\
111                      join('machine').\
112                      filter_by(owner=owner).sum(Disk.c.size) or 0
113     return min(quota_single, quota_total-disk_usage/1024.)
114
115 def cantAddVm(owner, g):
116     machines = getMachinesByOwner(owner)
117     active_machines = [m for m in machines if m.name in g.xmlist_raw]
118     (quota_total, quota_active) = Owner.getVMQuotas(owner)
119     if machines.count() >= quota_total:
120         return 'You have too many VMs to create a new one.'
121     if len(active_machines) >= quota_active:
122         return ('You already have the maximum number of VMs turned on.  '
123                 'To create more, turn one off.')
124     return False
125
126 def 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
131 def owns(user, machine):
132     """Return whether a user owns a machine"""
133     return user in authz.expandOwner(machine.owner)
134
135 def 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
147 def 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
166 def 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
181 def 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
189 def 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
209 def 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.afs.cells[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
236 def 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     if '@' in owner:
246         raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed")
247     try:
248         if user not in authz.expandOwner(owner):
249             raise InvalidInput('owner', owner, 'You do not have access to the '
250                                + owner + ' locker')
251     except getafsgroups.AfsProcessError, e:
252         raise InvalidInput('owner', owner, str(e))
253     return owner
254
255 def testContact(user, contact, machine=None):
256     if contact is None or (machine is not None and contact == machine.contact):
257         return None
258     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
259         raise InvalidInput('contact', contact, "Not a valid email.")
260     return contact
261
262 def testName(user, name, machine=None):
263     if name is None:
264         return None
265     name = name.lower()
266     if machine is not None and name == machine.name:
267         return None
268     try:
269         hostname = '%s.%s.' % (name, config.dns.domains[0])
270         resolver = dns.resolver.Resolver()
271         resolver.nameservers = ['127.0.0.1']
272         try:
273             resolver.query(hostname, 'A')
274         except dns.resolver.NoAnswer, e:
275             # If we can get the TXT record, then we can verify it's
276             # reserved. If this lookup fails, let it bubble up and be
277             # dealt with
278             answer = resolver.query(hostname, 'TXT')
279             txt = answer[0].strings[0]
280             if txt.startswith('reserved'):
281                 raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
282
283         # If the hostname didn't exist, it would have thrown an
284         # exception by now - error out
285         raise InvalidInput('name', name, 'Name is already taken.')
286     except dns.resolver.NXDOMAIN, e:
287         if not validMachineName(name):
288             raise InvalidInput('name', name, 'You must provide a machine name.  Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
289         return name
290     except InvalidInput:
291         raise
292     except:
293         # Any other error is a validation failure
294         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)
295
296 def testDescription(user, description, machine=None):
297     if description is None or description.strip() == '':
298         return None
299     return description.strip()
300
301 def testHostname(user, hostname, machine):
302     for nic in machine.nics:
303         if hostname == nic.hostname:
304             return hostname
305     # check if doesn't already exist
306     if NIC.select_by(hostname=hostname):
307         raise InvalidInput('hostname', hostname,
308                            "Already exists")
309     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
310         raise InvalidInput('hostname', hostname, "Not a valid hostname; "
311                            "must only use number, letters, and dashes.")
312     return hostname