Removed outage notice
[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:' + admin).
213     """
214     if admin is None:
215         return None
216     if machine is not None and admin == machine.administrator:
217         return admin
218     if admin == user:
219         return admin
220     # we do not require that the user be in the admin group;
221     # just that it is a non-empty set
222     if authz.expandAdmin(admin):
223         return admin
224     if ':' not in admin:
225         if authz.expandAdmin('system:' + admin):
226             return 'system:' + admin
227         errmsg = 'No user "%s" or non-empty group "system:%s" found.' % (admin, admin)
228     else:
229         errmsg = 'No non-empty group "%s" found.' % (admin,)
230     raise InvalidInput('administrator', admin, errmsg)
231
232 def testOwner(user, owner, machine=None):
233     """Determine whether a user can set the owner of a machine to this value.
234
235     If machine is None, this is the owner of a new machine.
236     """
237     if machine is not None and owner in (machine.owner, None):
238         return machine.owner
239     if owner is None:
240         raise InvalidInput('owner', owner, "Owner must be specified")
241     if '@' in owner:
242         raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed")
243     try:
244         if user not in authz.expandOwner(owner):
245             raise InvalidInput('owner', owner, 'You do not have access to the '
246                                + owner + ' locker')
247     except getafsgroups.AfsProcessError, e:
248         raise InvalidInput('owner', owner, str(e))
249     return owner
250
251 def testContact(user, contact, machine=None):
252     if contact is None or (machine is not None and contact == machine.contact):
253         return None
254     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
255         raise InvalidInput('contact', contact, "Not a valid email.")
256     return contact
257
258 def testName(user, name, machine=None):
259     if name is None:
260         return None
261     name = name.lower()
262     if machine is not None and name == machine.name:
263         return None
264     try:
265         hostname = '%s.%s.' % (name, config.dns.domains[0])
266         resolver = dns.resolver.Resolver()
267         resolver.nameservers = ['127.0.0.1']
268         try:
269             resolver.query(hostname, 'A')
270         except dns.resolver.NoAnswer, e:
271             # If we can get the TXT record, then we can verify it's
272             # reserved. If this lookup fails, let it bubble up and be
273             # dealt with
274             answer = resolver.query(hostname, 'TXT')
275             txt = answer[0].strings[0]
276             if txt.startswith('reserved'):
277                 raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
278
279         # If the hostname didn't exist, it would have thrown an
280         # exception by now - error out
281         raise InvalidInput('name', name, 'Name is already taken.')
282     except dns.resolver.NXDOMAIN, e:
283         if not validMachineName(name):
284             raise InvalidInput('name', name, 'You must provide a machine name.  Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
285         return name
286     except InvalidInput:
287         raise
288     except:
289         # Any other error is a validation failure
290         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)
291
292 def testDescription(user, description, machine=None):
293     if description is None or description.strip() == '':
294         return None
295     return description.strip()
296
297 def testHostname(user, hostname, machine):
298     for nic in machine.nics:
299         if hostname == nic.hostname:
300             return hostname
301     # check if doesn't already exist
302     if NIC.select_by(hostname=hostname):
303         raise InvalidInput('hostname', hostname,
304                            "Already exists")
305     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
306         raise InvalidInput('hostname', hostname, "Not a valid hostname; "
307                            "must only use number, letters, and dashes.")
308     return hostname