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