Added nc
[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 import authz
9 from invirt.database import Machine, NIC, Type, Disk, CDROM, Autoinstall, Owner
10 from invirt.config import structs as config
11 from invirt.common import InvalidInput, CodeError
12
13 MIN_MEMORY_SINGLE = 16
14 MIN_DISK_SINGLE = 0.1
15
16 class Validate:
17     def __init__(self, username, state, machine_id=None, name=None, description=None, owner=None,
18                  admin=None, contact=None, memory=None, disksize=None,
19                  vmtype=None, nictype=None, cdrom=None, autoinstall=None, strict=False):
20         # XXX Successive quota checks aren't a good idea, since you
21         # can't necessarily change the locker and disk size at the
22         # same time.
23         created_new = (machine_id is None)
24
25         if strict:
26             if name is None:
27                 raise InvalidInput('name', name, "You must provide a machine name.")
28             if description is None or description.strip() == '':
29                 raise InvalidInput('description', description, "You must provide a description.")
30             if memory is None:
31                 raise InvalidInput('memory', memory, "You must provide a memory size.")
32             if disksize is None:
33                 raise InvalidInput('disk', disksize, "You must provide a disk size.")
34
35         if machine_id is not None:
36             self.machine = testMachineId(username, state, machine_id)
37         machine = getattr(self, 'machine', None)
38
39         owner = testOwner(username, owner, machine)
40         if owner is not None:
41             self.owner = owner
42         self.admin = testAdmin(username, admin, machine)
43         contact = testContact(username, contact, machine)
44         if contact is not None:
45             self.contact = contact
46         name = testName(username, name, machine)
47         if name is not None:
48             self.name = name
49         description = testDescription(username, description, machine)
50         if description is not None:
51             self.description = description
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, state, disksize, machine)
57         if vmtype is not None:
58             self.vmtype = validVmType(vmtype)
59         if nictype is not None:
60             self.nictype = validNICType(nictype)
61         if cdrom is not None:
62             if not CDROM.query.get(cdrom):
63                 raise CodeError("Invalid cdrom type '%s'" % cdrom)
64             self.cdrom = cdrom
65         if autoinstall is not None:
66             #raise InvalidInput('autoinstall', 'install',
67             #                   "The autoinstaller has been temporarily disabled")
68             self.autoinstall = Autoinstall.query.get(autoinstall)
69
70
71 def getMachinesByOwner(owner, machine=None):
72     """Return the machines owned by the same as a machine.
73
74     If the machine is None, return the machines owned by the same
75     user.
76     """
77     if machine:
78         owner = machine.owner
79     return Machine.query.filter_by(owner=owner)
80
81 def maxMemory(owner, g, machine=None, on=True):
82     """Return the maximum memory for a machine or a user.
83
84     If machine is None, return the memory available for a new
85     machine.  Else, return the maximum that machine can have.
86
87     on is whether the machine should be turned on.  If false, the max
88     memory for the machine to change to, if it is left off, is
89     returned.
90     """
91     (quota_total, quota_single) = Owner.getMemoryQuotas(machine.owner if machine else owner)
92
93     if not on:
94         return quota_single
95     machines = getMachinesByOwner(owner, machine)
96     active_machines = [m for m in machines if m.name in g.xmlist_raw]
97     mem_usage = sum([x.memory for x in active_machines if x != machine])
98     return min(quota_single, quota_total-mem_usage)
99
100 def maxDisk(owner, machine=None):
101     """Return the maximum disk that a machine can reach.
102
103     If machine is None, the maximum disk for a new machine. Otherwise,
104     return the maximum that a given machine can be changed to.
105     """
106     (quota_total, quota_single) = Owner.getDiskQuotas(machine.owner if machine else owner)
107
108     if machine is not None:
109         machine_id = machine.machine_id
110     else:
111         machine_id = None
112     disk_usage_query = Disk.query.filter(Disk.machine_id != machine_id).\
113         join('machine').filter_by(owner=owner)
114
115     disk_usage = sum([m.size for m in disk_usage_query]) or 0
116     return min(quota_single, quota_total-disk_usage/1024.)
117
118 def cantAddVm(owner, g):
119     machines = getMachinesByOwner(owner)
120     active_machines = [m for m in machines if m.name in g.xmlist_raw]
121     (quota_total, quota_active) = Owner.getVMQuotas(owner)
122     if machines.count() >= quota_total:
123         return 'You have too many VMs to create a new one.'
124     if len(active_machines) >= quota_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 authz.expandOwner(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 validNICType(nic_type):
193     if nic_type is None:
194         return None
195     t = nic_type
196     if not (t is "e1000" or t is "pcnet"):
197         raise CodeError("Invalid nic type '%s'"  % nic_type)
198     return t
199
200 def testMachineId(user, state, machine_id, exists=True):
201     """Parse, validate and check authorization for a given user and machine.
202
203     If exists is False, don't check that it exists.
204     """
205     if machine_id is None:
206         raise InvalidInput('machine_id', machine_id,
207                            "Must specify a machine ID.")
208     try:
209         machine_id = int(machine_id)
210     except ValueError:
211         raise InvalidInput('machine_id', machine_id, "Must be an integer.")
212     machine = Machine.query.get(machine_id)
213     if exists and machine is None:
214         raise InvalidInput('machine_id', machine_id, "Does not exist.")
215     if machine is not None and not haveAccess(user, state, machine):
216         raise InvalidInput('machine_id', machine_id,
217                            "You do not have access to this machine.")
218     return machine
219
220 def testAdmin(user, admin, machine):
221     """Determine whether a user can set the admin of a machine to this value.
222
223     Return the value to set the admin field to (possibly 'system:' + admin).
224     """
225     if admin is None:
226         return None
227     if machine is not None and admin == machine.administrator:
228         return admin
229     if admin == user:
230         return admin
231     # we do not require that the user be in the admin group;
232     # just that it is a non-empty set
233     if authz.expandAdmin(admin):
234         return admin
235     if ':' not in admin:
236         if authz.expandAdmin('system:' + admin):
237             return 'system:' + admin
238         errmsg = 'No user "%s" or non-empty group "system:%s" found.' % (admin, admin)
239     else:
240         errmsg = 'No non-empty group "%s" found.' % (admin,)
241     raise InvalidInput('administrator', admin, errmsg)
242
243 def testOwner(user, owner, machine=None):
244     """Determine whether a user can set the owner of a machine to this value.
245
246     If machine is None, this is the owner of a new machine.
247     """
248     if machine is not None and owner in (machine.owner, None):
249         return machine.owner
250     if owner is None:
251         raise InvalidInput('owner', owner, "Owner must be specified")
252     if '@' in owner:
253         raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed")
254     try:
255         if user not in authz.expandOwner(owner):
256             raise InvalidInput('owner', owner, 'You do not have access to the '
257                                + owner + ' locker')
258     except getafsgroups.AfsProcessError, e:
259         raise InvalidInput('owner', owner, str(e))
260     return owner
261
262 def testContact(user, contact, machine=None):
263     if contact is None or (machine is not None and contact == machine.contact):
264         return None
265     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
266         raise InvalidInput('contact', contact, "Not a valid email.")
267     return contact
268
269 def testName(user, name, machine=None):
270     if name is None:
271         return None
272     name = name.lower()
273     if machine is not None and name == machine.name:
274         return None
275     try:
276         hostname = '%s.%s.' % (name, config.dns.domains[0])
277         resolver = dns.resolver.Resolver()
278         resolver.nameservers = ['127.0.0.1']
279         try:
280             resolver.query(hostname, 'A')
281         except dns.resolver.NoAnswer, e:
282             # If we can get the TXT record, then we can verify it's
283             # reserved. If this lookup fails, let it bubble up and be
284             # dealt with
285             answer = resolver.query(hostname, 'TXT')
286             txt = answer[0].strings[0]
287             if txt.startswith('reserved'):
288                 raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
289
290         # If the hostname didn't exist, it would have thrown an
291         # exception by now - error out
292         raise InvalidInput('name', name, 'Name is already taken.')
293     except dns.resolver.NXDOMAIN, e:
294         if not validMachineName(name):
295             raise InvalidInput('name', name, 'You must provide a machine name.  Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
296         return name
297     except InvalidInput:
298         raise
299     except:
300         # Any other error is a validation failure
301         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)
302
303 def testDescription(user, description, machine=None):
304     if description is None or description.strip() == '':
305         return None
306     return description.strip()
307
308 def testHostname(user, hostname, machine):
309     for nic in machine.nics:
310         if hostname == nic.hostname:
311             return hostname
312     # check if doesn't already exist
313     if NIC.select_by(hostname=hostname):
314         raise InvalidInput('hostname', hostname,
315                            "Already exists")
316     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
317         raise InvalidInput('hostname', hostname, "Not a valid hostname; "
318                            "must only use number, letters, and dashes.")
319     return hostname