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
20 def __init__(self, username, state, machine_id=None, name=None, description=None, owner=None,
21 admin=None, contact=None, memory=None, disksize=None,
22 vmtype=None, cdrom=None, autoinstall=None, strict=False):
23 # XXX Successive quota checks aren't a good idea, since you
24 # can't necessarily change the locker and disk size at the
26 created_new = (machine_id is None)
30 raise InvalidInput('name', name, "You must provide a machine name.")
31 if description is None:
32 raise InvalidInput('description', description, "You must provide a description.")
34 raise InvalidInput('memory', memory, "You must provide a memory size.")
36 raise InvalidInput('disk', disksize, "You must provide a disk size.")
38 if machine_id is not None:
39 self.machine = testMachineId(username, state, machine_id)
40 machine = getattr(self, 'machine', None)
42 owner = testOwner(username, owner, machine)
45 self.admin = testAdmin(username, admin, machine)
46 contact = testContact(username, contact, machine)
47 if contact is not None:
48 self.contact = contact
49 name = testName(username, name, machine)
52 description = testDescription(username, description, machine)
53 if description is not None:
54 self.description = description
55 if memory is not None:
56 self.memory = validMemory(self.owner, state, memory, machine,
58 if disksize is not None:
59 self.disksize = validDisk(self.owner, state, disksize, machine)
60 if vmtype is not None:
61 self.vmtype = validVmType(vmtype)
63 if not CDROM.query().get(cdrom):
64 raise CodeError("Invalid cdrom type '%s'" % cdrom)
66 if autoinstall is not None:
67 #raise InvalidInput('autoinstall', 'install',
68 # "The autoinstaller has been temporarily disabled")
69 self.autoinstall = Autoinstall.query().get(autoinstall)
72 def getMachinesByOwner(owner, machine=None):
73 """Return the machines owned by the same as a machine.
75 If the machine is None, return the machines owned by the same
80 return Machine.query().filter_by(owner=owner)
82 def maxMemory(owner, g, machine=None, on=True):
83 """Return the maximum memory for a machine or a user.
85 If machine is None, return the memory available for a new
86 machine. Else, return the maximum that machine can have.
88 on is whether the machine should be turned on. If false, the max
89 memory for the machine to change to, if it is left off, is
92 (quota_total, quota_single) = Owner.getQuotas(machine.owner if machine else owner)
96 machines = getMachinesByOwner(owner, machine)
97 active_machines = [m for m in machines if m.name in g.xmlist_raw]
98 mem_usage = sum([x.memory for x in active_machines if x != machine])
99 return min(quota_single, quota_total-mem_usage)
101 def maxDisk(owner, machine=None):
102 """Return the maximum disk that a machine can reach.
104 If machine is None, the maximum disk for a new machine. Otherwise,
105 return the maximum that a given machine can be changed to.
107 if machine is not None:
108 machine_id = machine.machine_id
111 disk_usage = Disk.query().filter(Disk.c.machine_id != machine_id).\
113 filter_by(owner=owner).sum(Disk.c.size) or 0
114 return min(MAX_DISK_SINGLE, MAX_DISK_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 if machines.count() >= MAX_VMS_TOTAL:
120 return 'You have too many VMs to create a new one.'
121 if len(active_machines) >= MAX_VMS_ACTIVE:
122 return ('You already have the maximum number of VMs turned on. '
123 'To create more, turn one off.')
126 def haveAccess(user, state, machine):
127 """Return whether a user has administrative access to a machine"""
128 return (user in cache_acls.accessList(machine)
129 or (machine.adminable and state.isadmin))
131 def owns(user, machine):
132 """Return whether a user owns a machine"""
133 return user in expandLocker(machine.owner)
135 def validMachineName(name):
136 """Check that name is valid for a machine name"""
139 charset = string.lowercase + string.digits + '-'
140 if '-' in (name[0], name[-1]) or len(name) > 63:
147 def validMemory(owner, g, memory, machine=None, on=True):
148 """Parse and validate limits for memory for a given owner and machine.
150 on is whether the memory must be valid after the machine is
155 if memory < MIN_MEMORY_SINGLE:
158 raise InvalidInput('memory', memory,
159 "Minimum %s MiB" % MIN_MEMORY_SINGLE)
160 max_val = maxMemory(owner, g, machine, on)
161 if not g.isadmin and memory > max_val:
162 raise InvalidInput('memory', memory,
163 'Maximum %s MiB for %s' % (max_val, owner))
166 def validDisk(owner, g, disk, machine=None):
167 """Parse and validate limits for disk for a given owner and machine."""
170 if not g.isadmin and disk > maxDisk(owner, machine):
171 raise InvalidInput('disk', disk,
172 "Maximum %s G" % maxDisk(owner, machine))
173 disk = int(disk * 1024)
174 if disk < MIN_DISK_SINGLE * 1024:
177 raise InvalidInput('disk', disk,
178 "Minimum %s GiB" % MIN_DISK_SINGLE)
181 def validVmType(vm_type):
184 t = Type.query().get(vm_type)
186 raise CodeError("Invalid vm type '%s'" % vm_type)
189 def testMachineId(user, state, machine_id, exists=True):
190 """Parse, validate and check authorization for a given user and machine.
192 If exists is False, don't check that it exists.
194 if machine_id is None:
195 raise InvalidInput('machine_id', machine_id,
196 "Must specify a machine ID.")
198 machine_id = int(machine_id)
200 raise InvalidInput('machine_id', machine_id, "Must be an integer.")
201 machine = Machine.query().get(machine_id)
202 if exists and machine is None:
203 raise InvalidInput('machine_id', machine_id, "Does not exist.")
204 if machine is not None and not haveAccess(user, state, machine):
205 raise InvalidInput('machine_id', machine_id,
206 "You do not have access to this machine.")
209 def testAdmin(user, admin, machine):
210 """Determine whether a user can set the admin of a machine to this value.
212 Return the value to set the admin field to (possibly 'system:' +
213 admin). XXX is modifying this a good idea?
217 if machine is not None and admin == machine.administrator:
222 if cache_acls.isUser(admin):
224 admin = 'system:' + admin
226 if user in getafsgroups.getAfsGroupMembers(admin, config.authz[0].cell):
228 except getafsgroups.AfsProcessError, e:
230 if errmsg.startswith("pts: User or group doesn't exist"):
231 errmsg = 'The group "%s" does not exist.' % admin
232 raise InvalidInput('administrator', admin, errmsg)
233 #XXX Should we require that user is in the admin group?
236 def testOwner(user, owner, machine=None):
237 """Determine whether a user can set the owner of a machine to this value.
239 If machine is None, this is the owner of a new machine.
241 if machine is not None and owner in (machine.owner, None):
244 raise InvalidInput('owner', owner, "Owner must be specified")
246 if user not in cache_acls.expandLocker(owner):
247 raise InvalidInput('owner', owner, 'You do not have access to the '
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 testDisk(user, disksize, machine=None):
263 def testName(user, name, machine=None):
267 if machine is not None and name == machine.name:
270 hostname = '%s.%s.' % (name, config.dns.domains[0])
271 resolver = dns.resolver.Resolver()
272 resolver.nameservers = ['127.0.0.1']
274 resolver.query(hostname, 'A')
275 except dns.resolver.NoAnswer, e:
276 # If we can get the TXT record, then we can verify it's
277 # reserved. If this lookup fails, let it bubble up and be
279 answer = resolver.query(hostname, 'TXT')
280 txt = answer[0].strings[0]
281 if txt.startswith('reserved'):
282 raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))
284 # If the hostname didn't exist, it would have thrown an
285 # exception by now - error out
286 raise InvalidInput('name', name, 'Name is already taken.')
287 except dns.resolver.NXDOMAIN, e:
288 if not validMachineName(name):
289 raise InvalidInput('name', name, 'You must provide a machine name. Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
294 # Any other error is a validation failure
295 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)
297 def testDescription(user, description, machine=None):
298 if description is None or description.strip() == '':
300 return description.strip()
302 def testHostname(user, hostname, machine):
303 for nic in machine.nics:
304 if hostname == nic.hostname:
306 # check if doesn't already exist
307 if NIC.select_by(hostname=hostname):
308 raise InvalidInput('hostname', hostname,
310 if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
311 raise InvalidInput('hostname', hostname, "Not a valid hostname; "
312 "must only use number, letters, and dashes.")