a1529460b4f508d9cdb3746d1a1bfb44b861a3c0
[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):
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 machine_id is not None:
29             self.machine = testMachineId(username, machine_id)
30         machine = getattr(self, 'machine', None)
31
32         owner = testOwner(username, owner, machine)
33         if owner is not None:
34             self.owner = owner
35         admin = testAdmin(username, admin, machine)
36         if admin is not None:
37             self.admin = admin
38         contact = testContact(username, contact, machine)
39         if contact is not None:
40             self.contact = contact
41         name = testName(username, name, machine)
42         if name is not None:
43             self.name = name
44         if memory is not None:
45             self.memory = validMemory(self.owner, state, memory, machine,
46                                       on=not created_new)
47         if disksize is not None:
48             self.disksize = validDisk(self.owner, disksize, machine)
49         if vmtype is not None:
50             self.vmtype = validVmType(vmtype)
51         if cdrom is not None:
52             if not CDROM.get(cdrom):
53                 raise CodeError("Invalid cdrom type '%s'" % cdrom)
54             self.cdrom = cdrom
55         if clone_from is not None:
56             if clone_from not in ('ice3', ):
57                 raise CodeError("Invalid clone image '%s'" % clone_from)
58             self.clone_from = clone_from
59
60
61 def getMachinesByOwner(owner, machine=None):
62     """Return the machines owned by the same as a machine.
63
64     If the machine is None, return the machines owned by the same
65     user.
66     """
67     if machine:
68         owner = machine.owner
69     return Machine.select_by(owner=owner)
70
71 def maxMemory(owner, g, machine=None, on=True):
72     """Return the maximum memory for a machine or a user.
73
74     If machine is None, return the memory available for a new
75     machine.  Else, return the maximum that machine can have.
76
77     on is whether the machine should be turned on.  If false, the max
78     memory for the machine to change to, if it is left off, is
79     returned.
80     """
81     if machine is not None and machine.memory > MAX_MEMORY_SINGLE:
82         # If they've been blessed, let them have it
83         return machine.memory
84     if not on:
85         return MAX_MEMORY_SINGLE
86     machines = getMachinesByOwner(owner, machine)
87     active_machines = [x for x in machines if g.xmlist.get(x)]
88     mem_usage = sum([x.memory for x in active_machines if x != machine])
89     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
90
91 def maxDisk(owner, machine=None):
92     """Return the maximum disk that a machine can reach.
93
94     If machine is None, the maximum disk for a new machine. Otherwise,
95     return the maximum that a given machine can be changed to.
96     """
97     if machine is not None:
98         machine_id = machine.machine_id
99     else:
100         machine_id = None
101     disk_usage = Disk.query().filter_by(Disk.c.machine_id != machine_id,
102                                         owner=owner).sum(Disk.c.size) or 0
103     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
104
105 def cantAddVm(owner, g):
106     machines = getMachinesByOwner(owner)
107     active_machines = [x for x in machines if g.xmlist.get(x)]
108     if len(machines) >= MAX_VMS_TOTAL:
109         return 'You have too many VMs to create a new one.'
110     if len(active_machines) >= MAX_VMS_ACTIVE:
111         return ('You already have the maximum number of VMs turned on.  '
112                 'To create more, turn one off.')
113     return False
114
115 def haveAccess(user, machine):
116     """Return whether a user has administrative access to a machine"""
117     return user in cache_acls.accessList(machine)
118
119 def owns(user, machine):
120     """Return whether a user owns a machine"""
121     return user in expandLocker(machine.owner)
122
123 def validMachineName(name):
124     """Check that name is valid for a machine name"""
125     if not name:
126         return False
127     charset = string.ascii_letters + string.digits + '-_'
128     if name[0] in '-_' or len(name) > 22:
129         return False
130     for x in name:
131         if x not in charset:
132             return False
133     return True
134
135 def validMemory(owner, g, memory, machine=None, on=True):
136     """Parse and validate limits for memory for a given owner and machine.
137
138     on is whether the memory must be valid after the machine is
139     switched on.
140     """
141     try:
142         memory = int(memory)
143         if memory < MIN_MEMORY_SINGLE:
144             raise ValueError
145     except ValueError:
146         raise InvalidInput('memory', memory,
147                            "Minimum %s MiB" % MIN_MEMORY_SINGLE)
148     max_val = maxMemory(owner, g, machine, on)
149     if memory > max_val:
150         raise InvalidInput('memory', memory,
151                            'Maximum %s MiB for %s' % (max_val, owner))
152     return memory
153
154 def validDisk(owner, disk, machine=None):
155     """Parse and validate limits for disk for a given owner and machine."""
156     try:
157         disk = float(disk)
158         if disk > maxDisk(owner, machine):
159             raise InvalidInput('disk', disk,
160                                "Maximum %s G" % maxDisk(owner, machine))
161         disk = int(disk * 1024)
162         if disk < MIN_DISK_SINGLE * 1024:
163             raise ValueError
164     except ValueError:
165         raise InvalidInput('disk', disk,
166                            "Minimum %s GiB" % MIN_DISK_SINGLE)
167     return disk
168
169 def validVmType(vm_type):
170     if vm_type is None:
171         return None
172     t = Type.get(vm_type)
173     if t is None:
174         raise CodeError("Invalid vm type '%s'"  % vm_type)
175     return t
176
177 def testMachineId(user, machine_id, exists=True):
178     """Parse, validate and check authorization for a given user and machine.
179
180     If exists is False, don't check that it exists.
181     """
182     if machine_id is None:
183         raise InvalidInput('machine_id', machine_id,
184                            "Must specify a machine ID.")
185     try:
186         machine_id = int(machine_id)
187     except ValueError:
188         raise InvalidInput('machine_id', machine_id, "Must be an integer.")
189     machine = Machine.get(machine_id)
190     if exists and machine is None:
191         raise InvalidInput('machine_id', machine_id, "Does not exist.")
192     if machine is not None and not haveAccess(user, machine):
193         raise InvalidInput('machine_id', machine_id,
194                            "You do not have access to this machine.")
195     return machine
196
197 def testAdmin(user, admin, machine):
198     """Determine whether a user can set the admin of a machine to this value.
199
200     Return the value to set the admin field to (possibly 'system:' +
201     admin).  XXX is modifying this a good idea?
202     """
203     if admin in (None, machine.administrator):
204         return None
205     if admin == user:
206         return admin
207     if ':' not in admin:
208         if cache_acls.isUser(admin):
209             return admin
210         admin = 'system:' + admin
211     try:
212         if user in getafsgroups.getAfsGroupMembers(admin, 'athena.mit.edu'):
213             return admin
214     except getafsgroups.AfsProcessError, e:
215         errmsg = str(e)
216         if errmsg.startswith("pts: User or group doesn't exist"):
217             errmsg = 'The group "%s" does not exist.' % admin
218         raise InvalidInput('administrator', admin, errmsg)
219     #XXX Should we require that user is in the admin group?
220     return admin
221
222 def testOwner(user, owner, machine=None):
223     """Determine whether a user can set the owner of a machine to this value.
224
225     If machine is None, this is the owner of a new machine.
226     """
227     if owner == user:
228         return owner
229     if machine is not None and owner in (machine.owner, None):
230         return machine.owner
231     if owner is None:
232         raise InvalidInput('owner', owner, "Owner must be specified")
233     try:
234         if user not in cache_acls.expandLocker(owner):
235             raise InvalidInput('owner', owner, 'You do not have access to the '
236                                + owner + ' locker')
237     except getafsgroups.AfsProcessError, e:
238         raise InvalidInput('owner', owner, str(e))
239     return owner
240
241 def testContact(user, contact, machine=None):
242     if contact in (None, machine.contact):
243         return None
244     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
245         raise InvalidInput('contact', contact, "Not a valid email.")
246     return contact
247
248 def testDisk(user, disksize, machine=None):
249     return disksize
250
251 def testName(user, name, machine=None):
252     if name is None:
253         return None
254     if machine is not None and name == machine.name:
255         return None
256     if not Machine.select_by(name=name):
257         if not validMachineName(name):
258             raise InvalidInput('name', name, 'You must provide a machine name.  Max 22 chars, alnum plus \'-\' and \'_\'.')
259         return name
260     raise InvalidInput('name', name, "Name is already taken.")
261
262 def testHostname(user, hostname, machine):
263     for nic in machine.nics:
264         if hostname == nic.hostname:
265             return hostname
266     # check if doesn't already exist
267     if NIC.select_by(hostname=hostname):
268         raise InvalidInput('hostname', hostname,
269                            "Already exists")
270     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
271         raise InvalidInput('hostname', hostname, "Not a valid hostname; "
272                            "must only use number, letters, and dashes.")
273     return hostname