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
13 MIN_MEMORY_SINGLE = 16
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, 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
23 created_new = (machine_id 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.")
31 raise InvalidInput('memory', memory, "You must provide a memory size.")
33 raise InvalidInput('disk', disksize, "You must provide a disk size.")
35 if machine_id is not None:
36 self.machine = testMachineId(username, state, machine_id)
37 machine = getattr(self, 'machine', None)
39 owner = testOwner(username, owner, machine)
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)
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,
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)
60 if not CDROM.query.get(cdrom):
61 raise CodeError("Invalid cdrom type '%s'" % cdrom)
63 if autoinstall is not None:
64 #raise InvalidInput('autoinstall', 'install',
65 # "The autoinstaller has been temporarily disabled")
66 self.autoinstall = Autoinstall.query.get(autoinstall)
69 def getMachinesByOwner(owner, machine=None):
70 """Return the machines owned by the same as a machine.
72 If the machine is None, return the machines owned by the same
77 return Machine.query.filter_by(owner=owner)
79 def maxMemory(owner, g, machine=None, on=True):
80 """Return the maximum memory for a machine or a user.
82 If machine is None, return the memory available for a new
83 machine. Else, return the maximum that machine can have.
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
89 (quota_total, quota_single) = Owner.getMemoryQuotas(machine.owner if machine else owner)
93 machines = getMachinesByOwner(owner, machine)
94 active_machines = [m for m in machines if m.name in g.xmlist_raw]
95 mem_usage = sum([x.memory for x in active_machines if x != machine])
96 return min(quota_single, quota_total-mem_usage)
98 def maxDisk(owner, machine=None):
99 """Return the maximum disk that a machine can reach.
101 If machine is None, the maximum disk for a new machine. Otherwise,
102 return the maximum that a given machine can be changed to. If the
103 disk currently exceeds the quotas, it can be changed to anything up
106 (quota_total, quota_single) = Owner.getDiskQuotas(machine.owner if machine else owner)
108 machine_id, current_size = None, 0
109 if machine is not None and machine.disks:
110 machine_id = machine.machine_id
111 # XXX The machine modification form doesn't currently handle the
112 # case of machines with multiple disks. It simply addresses the "first"
113 # disk (which is possibly nondeterministic and wrong). Since we're
114 # doing validation for that form, we have to use the same logic it
116 current_size = machine.disks[0].size / 1024.
117 disk_usage_query = Disk.query.filter(Disk.machine_id != machine_id).\
118 join('machine').filter_by(owner=owner)
120 disk_usage = sum([m.size for m in disk_usage_query]) or 0
121 return max(current_size, min(quota_single, quota_total-disk_usage/1024.))
123 def cantAddVm(owner, g):
124 machines = getMachinesByOwner(owner)
125 active_machines = [m for m in machines if m.name in g.xmlist_raw]
126 (quota_total, quota_active) = Owner.getVMQuotas(owner)
127 if machines.count() >= quota_total:
128 return 'You have too many VMs to create a new one.'
129 if len(active_machines) >= quota_active:
130 return ('You already have the maximum number of VMs turned on. '
131 'To create more, turn one off.')
134 def haveAccess(user, state, machine):
135 """Return whether a user has administrative access to a machine"""
136 return (user in cache_acls.accessList(machine)
137 or (machine.adminable and state.isadmin))
139 def owns(user, machine):
140 """Return whether a user owns a machine"""
141 return user in authz.expandOwner(machine.owner)
143 def validMachineName(name):
144 """Check that name is valid for a machine name"""
147 charset = string.lowercase + string.digits + '-'
148 if '-' in (name[0], name[-1]) or len(name) > 63:
155 def validMemory(owner, g, memory, machine=None, on=True):
156 """Parse and validate limits for memory for a given owner and machine.
158 on is whether the memory must be valid after the machine is
163 if memory < MIN_MEMORY_SINGLE:
166 raise InvalidInput('memory', memory,
167 "Minimum %s MiB" % MIN_MEMORY_SINGLE)
168 max_val = maxMemory(owner, g, machine, on)
169 if not g.isadmin and memory > max_val:
170 raise InvalidInput('memory', memory,
171 'Maximum %s MiB for %s' % (max_val, owner))
174 def validDisk(owner, g, disk, machine=None):
175 """Parse and validate limits for disk for a given owner and machine."""
178 if not g.isadmin and disk > maxDisk(owner, machine):
179 raise InvalidInput('disk', disk,
180 "Maximum %s G" % maxDisk(owner, machine))
181 disk = int(disk * 1024)
182 if disk < MIN_DISK_SINGLE * 1024:
185 raise InvalidInput('disk', disk,
186 "Minimum %s GiB" % MIN_DISK_SINGLE)
189 def validVmType(vm_type):
192 t = Type.query.get(vm_type)
194 raise CodeError("Invalid vm type '%s'" % vm_type)
197 def testMachineId(user, state, machine_id, exists=True):
198 """Parse, validate and check authorization for a given user and machine.
200 If exists is False, don't check that it exists.
202 if machine_id is None:
203 raise InvalidInput('machine_id', machine_id,
204 "Must specify a machine ID.")
206 machine_id = int(machine_id)
208 raise InvalidInput('machine_id', machine_id, "Must be an integer.")
209 machine = Machine.query.get(machine_id)
210 if exists and machine is None:
211 raise InvalidInput('machine_id', machine_id, "Does not exist.")
212 if machine is not None and not haveAccess(user, state, machine):
213 raise InvalidInput('machine_id', machine_id,
214 "You do not have access to this machine.")
217 def testAdmin(user, admin, machine):
218 """Determine whether a user can set the admin of a machine to this value.
220 Return the value to set the admin field to (possibly 'system:' + admin).
224 if machine is not None and admin == machine.administrator:
228 # we do not require that the user be in the admin group;
229 # just that it is a non-empty set
230 if authz.expandAdmin(admin):
233 if authz.expandAdmin('system:' + admin):
234 return 'system:' + admin
235 errmsg = 'No user "%s" or non-empty group "system:%s" found.' % (admin, admin)
237 errmsg = 'No non-empty group "%s" found.' % (admin,)
238 raise InvalidInput('administrator', admin, errmsg)
240 def testOwner(user, owner, machine=None):
241 """Determine whether a user can set the owner of a machine to this value.
243 If machine is None, this is the owner of a new machine.
245 if machine is not None and owner in (machine.owner, None):
248 raise InvalidInput('owner', owner, "Owner must be specified")
250 raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed")
252 if user not in authz.expandOwner(owner):
253 raise InvalidInput('owner', owner, 'You do not have access to the '
254 + owner + ' locker (Is system:anyuser missing '
255 + 'the l permission?)')
256 except getafsgroups.AfsProcessError, e:
257 raise InvalidInput('owner', owner, str(e))
260 def testContact(user, contact, machine=None):
261 if contact is None or (machine is not None and contact == machine.contact):
263 if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
264 raise InvalidInput('contact', contact, "Not a valid email.")
267 def testName(user, name, machine=None):
271 if machine is not None and name == machine.name:
274 hostname = '%s.%s.' % (name, config.dns.domains[0])
275 resolver = dns.resolver.Resolver()
276 resolver.nameservers = ['127.0.0.1']
278 resolver.query(hostname, 'A')
279 except dns.resolver.NoAnswer, e:
280 # If we can get the TXT record, then we can verify it's
281 # reserved. If this lookup fails, let it bubble up and be
283 answer = resolver.query(hostname, 'TXT')
284 txt = answer[0].strings[0]
285 if txt.startswith('reserved'):
286 raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
288 # If the hostname didn't exist, it would have thrown an
289 # exception by now - error out
290 raise InvalidInput('name', name, 'Name is already taken.')
291 except dns.resolver.NXDOMAIN, e:
292 if not validMachineName(name):
293 raise InvalidInput('name', name, 'You must provide a machine name. Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
298 # Any other error is a validation failure
299 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)
301 def testDescription(user, description, machine=None):
302 if description is None or description.strip() == '':
304 return description.strip()
306 def testHostname(user, hostname, machine):
307 for nic in machine.nics:
308 if hostname == nic.hostname:
310 # check if doesn't already exist
311 if NIC.select_by(hostname=hostname):
312 raise InvalidInput('hostname', hostname,
314 if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
315 raise InvalidInput('hostname', hostname, "Not a valid hostname; "
316 "must only use number, letters, and dashes.")