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.
104 (quota_total, quota_single) = Owner.getDiskQuotas(machine.owner if machine else owner)
106 if machine is not None:
107 machine_id = machine.machine_id
110 disk_usage_query = Disk.query.filter(Disk.machine_id != machine_id).\
111 join('machine').filter_by(owner=owner)
113 disk_usage = sum([m.size for m in disk_usage_query]) or 0
114 return min(quota_single, quota_total-disk_usage/1024.)
116 def cantAddVm(owner, g):
117 machines = getMachinesByOwner(owner)
118 active_machines = [m for m in machines if m.name in g.xmlist_raw]
119 (quota_total, quota_active) = Owner.getVMQuotas(owner)
120 if machines.count() >= quota_total:
121 return 'You have too many VMs to create a new one.'
122 if len(active_machines) >= quota_active:
123 return ('You already have the maximum number of VMs turned on. '
124 'To create more, turn one off.')
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))
132 def owns(user, machine):
133 """Return whether a user owns a machine"""
134 return user in authz.expandOwner(machine.owner)
136 def validMachineName(name):
137 """Check that name is valid for a machine name"""
140 charset = string.lowercase + string.digits + '-'
141 if '-' in (name[0], name[-1]) or len(name) > 63:
148 def validMemory(owner, g, memory, machine=None, on=True):
149 """Parse and validate limits for memory for a given owner and machine.
151 on is whether the memory must be valid after the machine is
156 if memory < MIN_MEMORY_SINGLE:
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))
167 def validDisk(owner, g, disk, machine=None):
168 """Parse and validate limits for disk for a given owner and machine."""
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:
178 raise InvalidInput('disk', disk,
179 "Minimum %s GiB" % MIN_DISK_SINGLE)
182 def validVmType(vm_type):
185 t = Type.query.get(vm_type)
187 raise CodeError("Invalid vm type '%s'" % vm_type)
190 def testMachineId(user, state, machine_id, exists=True):
191 """Parse, validate and check authorization for a given user and machine.
193 If exists is False, don't check that it exists.
195 if machine_id is None:
196 raise InvalidInput('machine_id', machine_id,
197 "Must specify a machine ID.")
199 machine_id = int(machine_id)
201 raise InvalidInput('machine_id', machine_id, "Must be an integer.")
202 machine = Machine.query.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.")
210 def testAdmin(user, admin, machine):
211 """Determine whether a user can set the admin of a machine to this value.
213 Return the value to set the admin field to (possibly 'system:' + admin).
217 if machine is not None and admin == machine.administrator:
221 # we do not require that the user be in the admin group;
222 # just that it is a non-empty set
223 if authz.expandAdmin(admin):
226 if authz.expandAdmin('system:' + admin):
227 return 'system:' + admin
228 errmsg = 'No user "%s" or non-empty group "system:%s" found.' % (admin, admin)
230 errmsg = 'No non-empty group "%s" found.' % (admin,)
231 raise InvalidInput('administrator', admin, errmsg)
233 def testOwner(user, owner, machine=None):
234 """Determine whether a user can set the owner of a machine to this value.
236 If machine is None, this is the owner of a new machine.
238 if machine is not None and owner in (machine.owner, None):
241 raise InvalidInput('owner', owner, "Owner must be specified")
243 raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed")
245 if user not in authz.expandOwner(owner):
246 raise InvalidInput('owner', owner, 'You do not have access to the '
247 + owner + ' locker (Is system:anyuser missing '
248 + 'the l permission?)')
249 except getafsgroups.AfsProcessError, e:
250 raise InvalidInput('owner', owner, str(e))
253 def testContact(user, contact, machine=None):
254 if contact is None or (machine is not None and contact == machine.contact):
256 if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
257 raise InvalidInput('contact', contact, "Not a valid email.")
260 def testName(user, name, machine=None):
264 if machine is not None and name == machine.name:
267 hostname = '%s.%s.' % (name, config.dns.domains[0])
268 resolver = dns.resolver.Resolver()
269 resolver.nameservers = ['127.0.0.1']
271 resolver.query(hostname, 'A')
272 except dns.resolver.NoAnswer, e:
273 # If we can get the TXT record, then we can verify it's
274 # reserved. If this lookup fails, let it bubble up and be
276 answer = resolver.query(hostname, 'TXT')
277 txt = answer[0].strings[0]
278 if txt.startswith('reserved'):
279 raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
281 # If the hostname didn't exist, it would have thrown an
282 # exception by now - error out
283 raise InvalidInput('name', name, 'Name is already taken.')
284 except dns.resolver.NXDOMAIN, e:
285 if not validMachineName(name):
286 raise InvalidInput('name', name, 'You must provide a machine name. Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
291 # Any other error is a validation failure
292 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)
294 def testDescription(user, description, machine=None):
295 if description is None or description.strip() == '':
297 return description.strip()
299 def testHostname(user, hostname, machine):
300 for nic in machine.nics:
301 if hostname == nic.hostname:
303 # check if doesn't already exist
304 if NIC.select_by(hostname=hostname):
305 raise InvalidInput('hostname', hostname,
307 if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
308 raise InvalidInput('hostname', hostname, "Not a valid hostname; "
309 "must only use number, letters, and dashes.")