* further integration of invirt.config
[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 invirt.database import Machine, NIC, Type, Disk, CDROM, Autoinstall
8 from invirt.config import structs as config
9 from webcommon import InvalidInput
10
11 MAX_MEMORY_TOTAL = 512
12 MAX_MEMORY_SINGLE = 256
13 MIN_MEMORY_SINGLE = 16
14 MAX_DISK_TOTAL = 50
15 MAX_DISK_SINGLE = 50
16 MIN_DISK_SINGLE = 0.1
17 MAX_VMS_TOTAL = 10
18 MAX_VMS_ACTIVE = 4
19
20 class Validate:
21     def __init__(self, username, state, machine_id=None, name=None, description=None, owner=None,
22                  admin=None, contact=None, memory=None, disksize=None,
23                  vmtype=None, cdrom=None, autoinstall=None, strict=False):
24         # XXX Successive quota checks aren't a good idea, since you
25         # can't necessarily change the locker and disk size at the
26         # same time.
27         created_new = (machine_id is None)
28
29         if strict:
30             if name is None:
31                 raise InvalidInput('name', name, "You must provide a machine name.")
32             if description is None:
33                 raise InvalidInput('description', description, "You must provide a description.")
34             if memory is None:
35                 raise InvalidInput('memory', memory, "You must provide a memory size.")
36             if disksize is None:
37                 raise InvalidInput('disk', disksize, "You must provide a disk size.")
38
39         if machine_id is not None:
40             self.machine = testMachineId(username, state, machine_id)
41         machine = getattr(self, 'machine', None)
42
43         owner = testOwner(username, owner, machine)
44         if owner is not None:
45             self.owner = owner
46         admin = testAdmin(username, admin, machine)
47         if admin is not None:
48             self.admin = admin
49         contact = testContact(username, contact, machine)
50         if contact is not None:
51             self.contact = contact
52         name = testName(username, name, machine)
53         if name is not None:
54             self.name = name
55         description = testDescription(username, description, machine)
56         if description is not None:
57             self.description = description
58         if memory is not None:
59             self.memory = validMemory(self.owner, state, memory, machine,
60                                       on=not created_new)
61         if disksize is not None:
62             self.disksize = validDisk(self.owner, state, disksize, machine)
63         if vmtype is not None:
64             self.vmtype = validVmType(vmtype)
65         if cdrom is not None:
66             if not CDROM.get(cdrom):
67                 raise CodeError("Invalid cdrom type '%s'" % cdrom)
68             self.cdrom = cdrom
69         if autoinstall is not None:
70             self.autoinstall = Autoinstall.get(autoinstall)
71
72
73 def getMachinesByOwner(owner, machine=None):
74     """Return the machines owned by the same as a machine.
75
76     If the machine is None, return the machines owned by the same
77     user.
78     """
79     if machine:
80         owner = machine.owner
81     return Machine.select_by(owner=owner)
82
83 def maxMemory(owner, g, machine=None, on=True):
84     """Return the maximum memory for a machine or a user.
85
86     If machine is None, return the memory available for a new
87     machine.  Else, return the maximum that machine can have.
88
89     on is whether the machine should be turned on.  If false, the max
90     memory for the machine to change to, if it is left off, is
91     returned.
92     """
93     if machine is not None and machine.memory > MAX_MEMORY_SINGLE:
94         # If they've been blessed, let them have it
95         return machine.memory
96     if not on:
97         return MAX_MEMORY_SINGLE
98     machines = getMachinesByOwner(owner, machine)
99     active_machines = [m for m in machines if m.name in g.xmlist_raw]
100     mem_usage = sum([x.memory for x in active_machines if x != machine])
101     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
102
103 def maxDisk(owner, machine=None):
104     """Return the maximum disk that a machine can reach.
105
106     If machine is None, the maximum disk for a new machine. Otherwise,
107     return the maximum that a given machine can be changed to.
108     """
109     if machine is not None:
110         machine_id = machine.machine_id
111     else:
112         machine_id = None
113     disk_usage = Disk.query().filter_by(Disk.c.machine_id != machine_id,
114                                         owner=owner).sum(Disk.c.size) or 0
115     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
116
117 def cantAddVm(owner, g):
118     machines = getMachinesByOwner(owner)
119     active_machines = [m for m in machines if m.name in g.xmlist_raw]
120     if len(machines) >= MAX_VMS_TOTAL:
121         return 'You have too many VMs to create a new one.'
122     if len(active_machines) >= MAX_VMS_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 expandLocker(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.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.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:' +
214     admin).  XXX is modifying this a good idea?
215     """
216     if admin is None:
217         return None
218     if machine is not None and admin == machine.administrator:
219         return None
220     if admin == user:
221         return admin
222     if ':' not in admin:
223         if cache_acls.isUser(admin):
224             return admin
225         admin = 'system:' + admin
226     try:
227         if user in getafsgroups.getAfsGroupMembers(admin, config.authz[0].cell):
228             return admin
229     except getafsgroups.AfsProcessError, e:
230         errmsg = str(e)
231         if errmsg.startswith("pts: User or group doesn't exist"):
232             errmsg = 'The group "%s" does not exist.' % admin
233         raise InvalidInput('administrator', admin, errmsg)
234     #XXX Should we require that user is in the admin group?
235     return admin
236
237 def testOwner(user, owner, machine=None):
238     """Determine whether a user can set the owner of a machine to this value.
239
240     If machine is None, this is the owner of a new machine.
241     """
242     if owner == user:
243         return owner
244     if machine is not None and owner in (machine.owner, None):
245         return machine.owner
246     if owner is None:
247         raise InvalidInput('owner', owner, "Owner must be specified")
248     try:
249         if user not in cache_acls.expandLocker(owner):
250             raise InvalidInput('owner', owner, 'You do not have access to the '
251                                + owner + ' locker')
252     except getafsgroups.AfsProcessError, e:
253         raise InvalidInput('owner', owner, str(e))
254     return owner
255
256 def testContact(user, contact, machine=None):
257     if contact is None or (machine is not None and contact == machine.contact):
258         return None
259     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
260         raise InvalidInput('contact', contact, "Not a valid email.")
261     return contact
262
263 def testDisk(user, disksize, machine=None):
264     return disksize
265
266 def testName(user, name, machine=None):
267     if name is None:
268         return None
269     name = name.lower()
270     if machine is not None and name == machine.name:
271         return None
272     if not Machine.select_by(name=name):
273         if not validMachineName(name):
274             raise InvalidInput('name', name, 'You must provide a machine name.  Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
275         return name
276     raise InvalidInput('name', name, "Name is already taken.")
277
278 def testDescription(user, description, machine=None):
279     if description is None or description.strip() == '':
280         return None
281     return description.strip()
282
283 def testHostname(user, hostname, machine):
284     for nic in machine.nics:
285         if hostname == nic.hostname:
286             return hostname
287     # check if doesn't already exist
288     if NIC.select_by(hostname=hostname):
289         raise InvalidInput('hostname', hostname,
290                            "Already exists")
291     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
292         raise InvalidInput('hostname', hostname, "Not a valid hostname; "
293                            "must only use number, letters, and dashes.")
294     return hostname