8 from invirt.database import Machine, NIC, Type, Disk, CDROM, Autoinstall, Owner
9 from invirt.config import structs as config
10 from invirt.common import InvalidInput, CodeError
12 MIN_MEMORY_SINGLE = 16
16 def __init__(self, username, state, machine_id=None, name=None, description=None, owner=None,
17 admin=None, contact=None, memory=None, disksize=None,
18 vmtype=None, cdrom=None, autoinstall=None, strict=False):
19 # XXX Successive quota checks aren't a good idea, since you
20 # can't necessarily change the locker and disk size at the
22 created_new = (machine_id is None)
26 raise InvalidInput('name', name, "You must provide a machine name.")
27 if description is None:
28 raise InvalidInput('description', description, "You must provide a description.")
30 raise InvalidInput('memory', memory, "You must provide a memory size.")
32 raise InvalidInput('disk', disksize, "You must provide a disk size.")
34 if machine_id is not None:
35 self.machine = testMachineId(username, state, machine_id)
36 machine = getattr(self, 'machine', None)
38 owner = testOwner(username, owner, machine)
41 self.admin = testAdmin(username, admin, machine)
42 contact = testContact(username, contact, machine)
43 if contact is not None:
44 self.contact = contact
45 name = testName(username, name, machine)
48 description = testDescription(username, description, machine)
49 if description is not None:
50 self.description = description
51 if memory is not None:
52 self.memory = validMemory(self.owner, state, memory, machine,
54 if disksize is not None:
55 self.disksize = validDisk(self.owner, state, disksize, machine)
56 if vmtype is not None:
57 self.vmtype = validVmType(vmtype)
59 if not CDROM.query().get(cdrom):
60 raise CodeError("Invalid cdrom type '%s'" % cdrom)
62 if autoinstall is not None:
63 #raise InvalidInput('autoinstall', 'install',
64 # "The autoinstaller has been temporarily disabled")
65 self.autoinstall = Autoinstall.query().get(autoinstall)
68 def getMachinesByOwner(owner, machine=None):
69 """Return the machines owned by the same as a machine.
71 If the machine is None, return the machines owned by the same
76 return Machine.query().filter_by(owner=owner)
78 def maxMemory(owner, g, machine=None, on=True):
79 """Return the maximum memory for a machine or a user.
81 If machine is None, return the memory available for a new
82 machine. Else, return the maximum that machine can have.
84 on is whether the machine should be turned on. If false, the max
85 memory for the machine to change to, if it is left off, is
88 (quota_total, quota_single) = Owner.getMemoryQuotas(machine.owner if machine else owner)
92 machines = getMachinesByOwner(owner, machine)
93 active_machines = [m for m in machines if m.name in g.xmlist_raw]
94 mem_usage = sum([x.memory for x in active_machines if x != machine])
95 return min(quota_single, quota_total-mem_usage)
97 def maxDisk(owner, machine=None):
98 """Return the maximum disk that a machine can reach.
100 If machine is None, the maximum disk for a new machine. Otherwise,
101 return the maximum that a given machine can be changed to.
103 (quota_total, quota_single) = Owner.getDiskQuotas(machine.owner if machine else owner)
105 if machine is not None:
106 machine_id = machine.machine_id
109 disk_usage = Disk.query().filter(Disk.c.machine_id != machine_id).\
111 filter_by(owner=owner).sum(Disk.c.size) or 0
112 return min(quota_single, quota_total-disk_usage/1024.)
114 def cantAddVm(owner, g):
115 machines = getMachinesByOwner(owner)
116 active_machines = [m for m in machines if m.name in g.xmlist_raw]
117 (quota_total, quota_active) = Owner.getVMQuotas(owner)
118 if machines.count() >= quota_total:
119 return 'You have too many VMs to create a new one.'
120 if len(active_machines) >= quota_active:
121 return ('You already have the maximum number of VMs turned on. '
122 'To create more, turn one off.')
125 def haveAccess(user, state, machine):
126 """Return whether a user has administrative access to a machine"""
127 return (user in cache_acls.accessList(machine)
128 or (machine.adminable and state.isadmin))
130 def owns(user, machine):
131 """Return whether a user owns a machine"""
132 return user in expandLocker(machine.owner)
134 def validMachineName(name):
135 """Check that name is valid for a machine name"""
138 charset = string.lowercase + string.digits + '-'
139 if '-' in (name[0], name[-1]) or len(name) > 63:
146 def validMemory(owner, g, memory, machine=None, on=True):
147 """Parse and validate limits for memory for a given owner and machine.
149 on is whether the memory must be valid after the machine is
154 if memory < MIN_MEMORY_SINGLE:
157 raise InvalidInput('memory', memory,
158 "Minimum %s MiB" % MIN_MEMORY_SINGLE)
159 max_val = maxMemory(owner, g, machine, on)
160 if not g.isadmin and memory > max_val:
161 raise InvalidInput('memory', memory,
162 'Maximum %s MiB for %s' % (max_val, owner))
165 def validDisk(owner, g, disk, machine=None):
166 """Parse and validate limits for disk for a given owner and machine."""
169 if not g.isadmin and disk > maxDisk(owner, machine):
170 raise InvalidInput('disk', disk,
171 "Maximum %s G" % maxDisk(owner, machine))
172 disk = int(disk * 1024)
173 if disk < MIN_DISK_SINGLE * 1024:
176 raise InvalidInput('disk', disk,
177 "Minimum %s GiB" % MIN_DISK_SINGLE)
180 def validVmType(vm_type):
183 t = Type.query().get(vm_type)
185 raise CodeError("Invalid vm type '%s'" % vm_type)
188 def testMachineId(user, state, machine_id, exists=True):
189 """Parse, validate and check authorization for a given user and machine.
191 If exists is False, don't check that it exists.
193 if machine_id is None:
194 raise InvalidInput('machine_id', machine_id,
195 "Must specify a machine ID.")
197 machine_id = int(machine_id)
199 raise InvalidInput('machine_id', machine_id, "Must be an integer.")
200 machine = Machine.query().get(machine_id)
201 if exists and machine is None:
202 raise InvalidInput('machine_id', machine_id, "Does not exist.")
203 if machine is not None and not haveAccess(user, state, machine):
204 raise InvalidInput('machine_id', machine_id,
205 "You do not have access to this machine.")
208 def testAdmin(user, admin, machine):
209 """Determine whether a user can set the admin of a machine to this value.
211 Return the value to set the admin field to (possibly 'system:' +
212 admin). XXX is modifying this a good idea?
216 if machine is not None and admin == machine.administrator:
221 if cache_acls.isUser(admin):
223 admin = 'system:' + admin
225 if user in getafsgroups.getAfsGroupMembers(admin, config.authz.afs.cells[0].cell):
227 except getafsgroups.AfsProcessError, e:
229 if errmsg.startswith("pts: User or group doesn't exist"):
230 errmsg = 'The group "%s" does not exist.' % admin
231 raise InvalidInput('administrator', admin, errmsg)
232 #XXX Should we require that user is in the admin group?
235 def testOwner(user, owner, machine=None):
236 """Determine whether a user can set the owner of a machine to this value.
238 If machine is None, this is the owner of a new machine.
240 if machine is not None and owner in (machine.owner, None):
243 raise InvalidInput('owner', owner, "Owner must be specified")
245 raise InvalidInput('owner', owner, "No cross-realm Hesiod lockers allowed")
247 if user not in cache_acls.expandLocker(owner):
248 raise InvalidInput('owner', owner, 'You do not have access to the '
250 except getafsgroups.AfsProcessError, e:
251 raise InvalidInput('owner', owner, str(e))
254 def testContact(user, contact, machine=None):
255 if contact is None or (machine is not None and contact == machine.contact):
257 if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
258 raise InvalidInput('contact', contact, "Not a valid email.")
261 def testName(user, name, machine=None):
265 if machine is not None and name == machine.name:
268 hostname = '%s.%s.' % (name, config.dns.domains[0])
269 resolver = dns.resolver.Resolver()
270 resolver.nameservers = ['127.0.0.1']
272 resolver.query(hostname, 'A')
273 except dns.resolver.NoAnswer, e:
274 # If we can get the TXT record, then we can verify it's
275 # reserved. If this lookup fails, let it bubble up and be
277 answer = resolver.query(hostname, 'TXT')
278 txt = answer[0].strings[0]
279 if txt.startswith('reserved'):
280 raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
282 # If the hostname didn't exist, it would have thrown an
283 # exception by now - error out
284 raise InvalidInput('name', name, 'Name is already taken.')
285 except dns.resolver.NXDOMAIN, e:
286 if not validMachineName(name):
287 raise InvalidInput('name', name, 'You must provide a machine name. Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
292 # Any other error is a validation failure
293 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)
295 def testDescription(user, description, machine=None):
296 if description is None or description.strip() == '':
298 return description.strip()
300 def testHostname(user, hostname, machine):
301 for nic in machine.nics:
302 if hostname == nic.hostname:
304 # check if doesn't already exist
305 if NIC.select_by(hostname=hostname):
306 raise InvalidInput('hostname', hostname,
308 if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
309 raise InvalidInput('hostname', hostname, "Not a valid hostname; "
310 "must only use number, letters, and dashes.")