Make it easy to kill fcgi processes.
[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 from sipb_xen_database import Machine, NIC, Type, Disk
8 from webcommon import InvalidInput
9
10 MAX_MEMORY_TOTAL = 512
11 MAX_MEMORY_SINGLE = 256
12 MIN_MEMORY_SINGLE = 16
13 MAX_DISK_TOTAL = 50
14 MAX_DISK_SINGLE = 50
15 MIN_DISK_SINGLE = 0.1
16 MAX_VMS_TOTAL = 10
17 MAX_VMS_ACTIVE = 4
18
19 class Validate:
20     def __init__(self, username, state, machine_id=None, name=None, owner=None,
21                  admin=None, contact=None, memory=None, disksize=None,
22                  vmtype=None, cdrom=None, clone_from=None, strict=False):
23         # XXX Successive quota checks aren't a good idea, since you
24         # can't necessarily change the locker and disk size at the
25         # same time.
26         created_new = (machine_id is None)
27
28         if strict:
29             if name is None:
30                 raise InvalidInput('name', name, "You must provide a machine name.")
31             if memory is None:
32                 raise InvalidInput('memory', memory, "You must provide a memory size.")
33             if disksize is None:
34                 raise InvalidInput('disk', disksize, "You must provide a disk size.")
35
36         if machine_id is not None:
37             self.machine = testMachineId(username, machine_id)
38         machine = getattr(self, 'machine', None)
39
40         owner = testOwner(username, owner, machine)
41         if owner is not None:
42             self.owner = owner
43         admin = testAdmin(username, admin, machine)
44         if admin is not None:
45             self.admin = admin
46         contact = testContact(username, contact, machine)
47         if contact is not None:
48             self.contact = contact
49         name = testName(username, name, machine)
50         if name is not None:
51             self.name = name
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, disksize, machine)
57         if vmtype is not None:
58             self.vmtype = validVmType(vmtype)
59         if cdrom is not None:
60             if not CDROM.get(cdrom):
61                 raise CodeError("Invalid cdrom type '%s'" % cdrom)
62             self.cdrom = cdrom
63         if clone_from is not None:
64             if clone_from not in ('ice3', ):
65                 raise CodeError("Invalid clone image '%s'" % clone_from)
66             self.clone_from = clone_from
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.select_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     if machine is not None and machine.memory > MAX_MEMORY_SINGLE:
90         # If they've been blessed, let them have it
91         return machine.memory
92     if not on:
93         return MAX_MEMORY_SINGLE
94     machines = getMachinesByOwner(owner, machine)
95     active_machines = [m for m in machines if m.name in g.xmlist_raw]
96     mem_usage = sum([x.memory for x in active_machines if x != machine])
97     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
98
99 def maxDisk(owner, machine=None):
100     """Return the maximum disk that a machine can reach.
101
102     If machine is None, the maximum disk for a new machine. Otherwise,
103     return the maximum that a given machine can be changed to.
104     """
105     if machine is not None:
106         machine_id = machine.machine_id
107     else:
108         machine_id = None
109     disk_usage = Disk.query().filter_by(Disk.c.machine_id != machine_id,
110                                         owner=owner).sum(Disk.c.size) or 0
111     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
112
113 def cantAddVm(owner, g):
114     machines = getMachinesByOwner(owner)
115     active_machines = [m for m in machines if m.name in g.xmlist_raw]
116     if len(machines) >= MAX_VMS_TOTAL:
117         return 'You have too many VMs to create a new one.'
118     if len(active_machines) >= MAX_VMS_ACTIVE:
119         return ('You already have the maximum number of VMs turned on.  '
120                 'To create more, turn one off.')
121     return False
122
123 def haveAccess(user, machine):
124     """Return whether a user has administrative access to a machine"""
125     return user in cache_acls.accessList(machine)
126
127 def owns(user, machine):
128     """Return whether a user owns a machine"""
129     return user in expandLocker(machine.owner)
130
131 def validMachineName(name):
132     """Check that name is valid for a machine name"""
133     if not name:
134         return False
135     charset = string.ascii_letters + string.digits + '-_'
136     if name[0] in '-_' or len(name) > 22:
137         return False
138     for x in name:
139         if x not in charset:
140             return False
141     return True
142
143 def validMemory(owner, g, memory, machine=None, on=True):
144     """Parse and validate limits for memory for a given owner and machine.
145
146     on is whether the memory must be valid after the machine is
147     switched on.
148     """
149     try:
150         memory = int(memory)
151         if memory < MIN_MEMORY_SINGLE:
152             raise ValueError
153     except ValueError:
154         raise InvalidInput('memory', memory,
155                            "Minimum %s MiB" % MIN_MEMORY_SINGLE)
156     max_val = maxMemory(owner, g, machine, on)
157     if memory > max_val:
158         raise InvalidInput('memory', memory,
159                            'Maximum %s MiB for %s' % (max_val, owner))
160     return memory
161
162 def validDisk(owner, disk, machine=None):
163     """Parse and validate limits for disk for a given owner and machine."""
164     try:
165         disk = float(disk)
166         if disk > maxDisk(owner, machine):
167             raise InvalidInput('disk', disk,
168                                "Maximum %s G" % maxDisk(owner, machine))
169         disk = int(disk * 1024)
170         if disk < MIN_DISK_SINGLE * 1024:
171             raise ValueError
172     except ValueError:
173         raise InvalidInput('disk', disk,
174                            "Minimum %s GiB" % MIN_DISK_SINGLE)
175     return disk
176
177 def validVmType(vm_type):
178     if vm_type is None:
179         return None
180     t = Type.get(vm_type)
181     if t is None:
182         raise CodeError("Invalid vm type '%s'"  % vm_type)
183     return t
184
185 def testMachineId(user, machine_id, exists=True):
186     """Parse, validate and check authorization for a given user and machine.
187
188     If exists is False, don't check that it exists.
189     """
190     if machine_id is None:
191         raise InvalidInput('machine_id', machine_id,
192                            "Must specify a machine ID.")
193     try:
194         machine_id = int(machine_id)
195     except ValueError:
196         raise InvalidInput('machine_id', machine_id, "Must be an integer.")
197     machine = Machine.get(machine_id)
198     if exists and machine is None:
199         raise InvalidInput('machine_id', machine_id, "Does not exist.")
200     if machine is not None and not haveAccess(user, machine):
201         raise InvalidInput('machine_id', machine_id,
202                            "You do not have access to this machine.")
203     return machine
204
205 def testAdmin(user, admin, machine):
206     """Determine whether a user can set the admin of a machine to this value.
207
208     Return the value to set the admin field to (possibly 'system:' +
209     admin).  XXX is modifying this a good idea?
210     """
211     if admin is None:
212         return None
213     if machine is not None and admin == machine.administrator:
214         return None
215     if admin == user:
216         return admin
217     if ':' not in admin:
218         if cache_acls.isUser(admin):
219             return admin
220         admin = 'system:' + admin
221     try:
222         if user in getafsgroups.getAfsGroupMembers(admin, 'athena.mit.edu'):
223             return admin
224     except getafsgroups.AfsProcessError, e:
225         errmsg = str(e)
226         if errmsg.startswith("pts: User or group doesn't exist"):
227             errmsg = 'The group "%s" does not exist.' % admin
228         raise InvalidInput('administrator', admin, errmsg)
229     #XXX Should we require that user is in the admin group?
230     return admin
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 owner == user:
238         return owner
239     if machine is not None and owner in (machine.owner, None):
240         return machine.owner
241     if owner is None:
242         raise InvalidInput('owner', owner, "Owner must be specified")
243     try:
244         if user not in cache_acls.expandLocker(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 testDisk(user, disksize, machine=None):
259     return disksize
260
261 def testName(user, name, machine=None):
262     if name is None:
263         return None
264     if machine is not None and name == machine.name:
265         return None
266     if not Machine.select_by(name=name):
267         if not validMachineName(name):
268             raise InvalidInput('name', name, 'You must provide a machine name.  Max 22 chars, alnum plus \'-\' and \'_\'.')
269         return name
270     raise InvalidInput('name', name, "Name is already taken.")
271
272 def testHostname(user, hostname, machine):
273     for nic in machine.nics:
274         if hostname == nic.hostname:
275             return hostname
276     # check if doesn't already exist
277     if NIC.select_by(hostname=hostname):
278         raise InvalidInput('hostname', hostname,
279                            "Already exists")
280     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
281         raise InvalidInput('hostname', hostname, "Not a valid hostname; "
282                            "must only use number, letters, and dashes.")
283     return hostname