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