Change VM creation auth failure message
[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_query = Disk.query.filter(Disk.machine_id != machine_id).\
111         join('machine').filter_by(owner=owner)
112
113     disk_usage = sum([m.size for m in disk_usage_query]) or 0
114     return min(quota_single, quota_total-disk_usage/1024.)
115
116 def cantAddVm(owner, g):
117     machines = getMachinesByOwner(owner)
118     active_machines = [m for m in machines if m.name in g.xmlist_raw]
119     (quota_total, quota_active) = Owner.getVMQuotas(owner)
120     if machines.count() >= quota_total:
121         return 'You have too many VMs to create a new one.'
122     if len(active_machines) >= quota_active:
123         return ('You already have the maximum number of VMs turned on.  '
124                 'To create more, turn one off.')
125     return False
126
127 def haveAccess(user, state, machine):
128     """Return whether a user has administrative access to a machine"""
129     return (user in cache_acls.accessList(machine)
130             or (machine.adminable and state.isadmin))
131
132 def owns(user, machine):
133     """Return whether a user owns a machine"""
134     return user in authz.expandOwner(machine.owner)
135
136 def validMachineName(name):
137     """Check that name is valid for a machine name"""
138     if not name:
139         return False
140     charset = string.lowercase + string.digits + '-'
141     if '-' in (name[0], name[-1]) or len(name) > 63:
142         return False
143     for x in name:
144         if x not in charset:
145             return False
146     return True
147
148 def validMemory(owner, g, memory, machine=None, on=True):
149     """Parse and validate limits for memory for a given owner and machine.
150
151     on is whether the memory must be valid after the machine is
152     switched on.
153     """
154     try:
155         memory = int(memory)
156         if memory < MIN_MEMORY_SINGLE:
157             raise ValueError
158     except ValueError:
159         raise InvalidInput('memory', memory,
160                            "Minimum %s MiB" % MIN_MEMORY_SINGLE)
161     max_val = maxMemory(owner, g, machine, on)
162     if not g.isadmin and memory > max_val:
163         raise InvalidInput('memory', memory,
164                            'Maximum %s MiB for %s' % (max_val, owner))
165     return memory
166
167 def validDisk(owner, g, disk, machine=None):
168     """Parse and validate limits for disk for a given owner and machine."""
169     try:
170         disk = float(disk)
171         if not g.isadmin and disk > maxDisk(owner, machine):
172             raise InvalidInput('disk', disk,
173                                "Maximum %s G" % maxDisk(owner, machine))
174         disk = int(disk * 1024)
175         if disk < MIN_DISK_SINGLE * 1024:
176             raise ValueError
177     except ValueError:
178         raise InvalidInput('disk', disk,
179                            "Minimum %s GiB" % MIN_DISK_SINGLE)
180     return disk
181
182 def validVmType(vm_type):
183     if vm_type is None:
184         return None
185     t = Type.query.get(vm_type)
186     if t is None:
187         raise CodeError("Invalid vm type '%s'"  % vm_type)
188     return t
189
190 def testMachineId(user, state, machine_id, exists=True):
191     """Parse, validate and check authorization for a given user and machine.
192
193     If exists is False, don't check that it exists.
194     """
195     if machine_id is None:
196         raise InvalidInput('machine_id', machine_id,
197                            "Must specify a machine ID.")
198     try:
199         machine_id = int(machine_id)
200     except ValueError:
201         raise InvalidInput('machine_id', machine_id, "Must be an integer.")
202     machine = Machine.query.get(machine_id)
203     if exists and machine is None:
204         raise InvalidInput('machine_id', machine_id, "Does not exist.")
205     if machine is not None and not haveAccess(user, state, machine):
206         raise InvalidInput('machine_id', machine_id,
207                            "You do not have access to this machine.")
208     return machine
209
210 def testAdmin(user, admin, machine):
211     """Determine whether a user can set the admin of a machine to this value.
212
213     Return the value to set the admin field to (possibly 'system:' + admin).
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     # we do not require that the user be in the admin group;
222     # just that it is a non-empty set
223     if authz.expandAdmin(admin):
224         return admin
225     if ':' not in admin:
226         if authz.expandAdmin('system:' + admin):
227             return 'system:' + admin
228         errmsg = 'No user "%s" or non-empty group "system:%s" found.' % (admin, admin)
229     else:
230         errmsg = 'No non-empty group "%s" found.' % (admin,)
231     raise InvalidInput('administrator', admin, errmsg)
232
233 def testOwner(user, owner, machine=None):
234     """Determine whether a user can set the owner of a machine to this value.
235
236     If machine is None, this is the owner of a new machine.
237     """
238     if machine is not None and owner in (machine.owner, None):
239         return machine.owner
240     if owner is None:
241         raise InvalidInput('owner', owner, "Owner must be specified")
242     if '@' in owner:
243         raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed")
244     try:
245         if user not in authz.expandOwner(owner):
246             raise InvalidInput('owner', owner, 'You do not have access to the '
247                                + owner + ' locker (Is system:anyuser missing '
248                                + 'the l permission?)')
249     except getafsgroups.AfsProcessError, e:
250         raise InvalidInput('owner', owner, str(e))
251     return owner
252
253 def testContact(user, contact, machine=None):
254     if contact is None or (machine is not None and contact == machine.contact):
255         return None
256     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
257         raise InvalidInput('contact', contact, "Not a valid email.")
258     return contact
259
260 def testName(user, name, machine=None):
261     if name is None:
262         return None
263     name = name.lower()
264     if machine is not None and name == machine.name:
265         return None
266     try:
267         hostname = '%s.%s.' % (name, config.dns.domains[0])
268         resolver = dns.resolver.Resolver()
269         resolver.nameservers = ['127.0.0.1']
270         try:
271             resolver.query(hostname, 'A')
272         except dns.resolver.NoAnswer, e:
273             # If we can get the TXT record, then we can verify it's
274             # reserved. If this lookup fails, let it bubble up and be
275             # dealt with
276             answer = resolver.query(hostname, 'TXT')
277             txt = answer[0].strings[0]
278             if txt.startswith('reserved'):
279                 raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
280
281         # If the hostname didn't exist, it would have thrown an
282         # exception by now - error out
283         raise InvalidInput('name', name, 'Name is already taken.')
284     except dns.resolver.NXDOMAIN, e:
285         if not validMachineName(name):
286             raise InvalidInput('name', name, 'You must provide a machine name.  Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
287         return name
288     except InvalidInput:
289         raise
290     except:
291         # Any other error is a validation failure
292         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)
293
294 def testDescription(user, description, machine=None):
295     if description is None or description.strip() == '':
296         return None
297     return description.strip()
298
299 def testHostname(user, hostname, machine):
300     for nic in machine.nics:
301         if hostname == nic.hostname:
302             return hostname
303     # check if doesn't already exist
304     if NIC.select_by(hostname=hostname):
305         raise InvalidInput('hostname', hostname,
306                            "Already exists")
307     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
308         raise InvalidInput('hostname', hostname, "Not a valid hostname; "
309                            "must only use number, letters, and dashes.")
310     return hostname