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, 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
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)
59 if nictype is not None:
60 self.nictype = validNICType(nictype)
62 if not CDROM.query.get(cdrom):
63 raise CodeError("Invalid cdrom type '%s'" % 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)
71 def getMachinesByOwner(owner, machine=None):
72 """Return the machines owned by the same as a machine.
74 If the machine is None, return the machines owned by the same
79 return Machine.query.filter_by(owner=owner)
81 def maxMemory(owner, g, machine=None, on=True):
82 """Return the maximum memory for a machine or a user.
84 If machine is None, return the memory available for a new
85 machine. Else, return the maximum that machine can have.
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
91 (quota_total, quota_single) = Owner.getMemoryQuotas(machine.owner if machine else owner)
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)
100 def maxDisk(owner, machine=None):
101 """Return the maximum disk that a machine can reach.
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.
106 (quota_total, quota_single) = Owner.getDiskQuotas(machine.owner if machine else owner)
108 if machine is not None:
109 machine_id = machine.machine_id
112 disk_usage_query = Disk.query.filter(Disk.machine_id != machine_id).\
113 join('machine').filter_by(owner=owner)
115 disk_usage = sum([m.size for m in disk_usage_query]) or 0
116 return min(quota_single, quota_total-disk_usage/1024.)
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.')
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))
134 def owns(user, machine):
135 """Return whether a user owns a machine"""
136 return user in authz.expandOwner(machine.owner)
138 def validMachineName(name):
139 """Check that name is valid for a machine name"""
142 charset = string.lowercase + string.digits + '-'
143 if '-' in (name[0], name[-1]) or len(name) > 63:
150 def validMemory(owner, g, memory, machine=None, on=True):
151 """Parse and validate limits for memory for a given owner and machine.
153 on is whether the memory must be valid after the machine is
158 if memory < MIN_MEMORY_SINGLE:
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))
169 def validDisk(owner, g, disk, machine=None):
170 """Parse and validate limits for disk for a given owner and machine."""
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:
180 raise InvalidInput('disk', disk,
181 "Minimum %s GiB" % MIN_DISK_SINGLE)
184 def validVmType(vm_type):
187 t = Type.query.get(vm_type)
189 raise CodeError("Invalid vm type '%s'" % vm_type)
192 def validNICType(nic_type):
196 if not (t is "e1000" or t is "pcnet"):
197 raise CodeError("Invalid nic type '%s'" % nic_type)
200 def testMachineId(user, state, machine_id, exists=True):
201 """Parse, validate and check authorization for a given user and machine.
203 If exists is False, don't check that it exists.
205 if machine_id is None:
206 raise InvalidInput('machine_id', machine_id,
207 "Must specify a machine ID.")
209 machine_id = int(machine_id)
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.")
220 def testAdmin(user, admin, machine):
221 """Determine whether a user can set the admin of a machine to this value.
223 Return the value to set the admin field to (possibly 'system:' + admin).
227 if machine is not None and admin == machine.administrator:
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):
236 if authz.expandAdmin('system:' + admin):
237 return 'system:' + admin
238 errmsg = 'No user "%s" or non-empty group "system:%s" found.' % (admin, admin)
240 errmsg = 'No non-empty group "%s" found.' % (admin,)
241 raise InvalidInput('administrator', admin, errmsg)
243 def testOwner(user, owner, machine=None):
244 """Determine whether a user can set the owner of a machine to this value.
246 If machine is None, this is the owner of a new machine.
248 if machine is not None and owner in (machine.owner, None):
251 raise InvalidInput('owner', owner, "Owner must be specified")
253 raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed")
255 if user not in authz.expandOwner(owner):
256 raise InvalidInput('owner', owner, 'You do not have access to the '
258 except getafsgroups.AfsProcessError, e:
259 raise InvalidInput('owner', owner, str(e))
262 def testContact(user, contact, machine=None):
263 if contact is None or (machine is not None and contact == machine.contact):
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.")
269 def testName(user, name, machine=None):
273 if machine is not None and name == machine.name:
276 hostname = '%s.%s.' % (name, config.dns.domains[0])
277 resolver = dns.resolver.Resolver()
278 resolver.nameservers = ['127.0.0.1']
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
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))
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 \'-\'.')
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)
303 def testDescription(user, description, machine=None):
304 if description is None or description.strip() == '':
306 return description.strip()
308 def testHostname(user, hostname, machine):
309 for nic in machine.nics:
310 if hostname == nic.hostname:
312 # check if doesn't already exist
313 if NIC.select_by(hostname=hostname):
314 raise InvalidInput('hostname', hostname,
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.")