16 sys.stderr = sys.stdout
17 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
19 from Cheetah.Template import Template
20 from sipb_xen_database import *
23 class MyException(Exception):
24 """Base class for my exceptions"""
27 class InvalidInput(MyException):
28 """Exception for user-provided input is invalid but maybe in good faith.
30 This would include setting memory to negative (which might be a
31 typo) but not setting an invalid boot CD (which requires bypassing
36 class CodeError(MyException):
37 """Exception for internal errors or bad faith input."""
41 def __init__(self, user):
44 def __get_uptimes(self):
45 if not hasattr(self, '_uptimes'):
46 self._uptimes = getUptimes(self.machines)
48 uptimes = property(__get_uptimes)
53 """Return HTML code for a (?) link to a specified help topic"""
54 return '<span class="helplink"><a href="help?subject='+subj+'&simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
58 global_dict['helppopup'] = helppopup
61 # ... and stolen from xend/uuid.py
63 """Generate a random UUID."""
65 return [ random.randint(0, 255) for _ in range(0, 16) ]
68 """Turn a numeric UUID to a hyphen-seperated one."""
69 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
70 "%02x" * 6]) % tuple(u)
72 MAX_MEMORY_TOTAL = 512
73 MAX_MEMORY_SINGLE = 256
74 MIN_MEMORY_SINGLE = 16
81 def getMachinesByOwner(owner):
82 """Return the machines owned by a given owner."""
83 return Machine.select_by(owner=owner)
85 def maxMemory(user, machine=None):
86 """Return the maximum memory for a machine or a user.
88 If machine is None, return the memory available for a new
89 machine. Else, return the maximum that machine can have.
91 on is a dictionary from machines to booleans, whether a machine is
92 on. If None, it is recomputed. XXX make this global?
95 machines = getMachinesByOwner(user.username)
96 active_machines = [x for x in machines if g.uptimes[x]]
97 mem_usage = sum([x.memory for x in active_machines if x != machine])
98 return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
100 def maxDisk(user, machine=None):
101 machines = getMachinesByOwner(user.username)
102 disk_usage = sum([sum([y.size for y in x.disks])
103 for x in machines if x != machine])
104 return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
107 machines = getMachinesByOwner(user.username)
108 active_machines = [x for x in machines if g.uptimes[x]]
109 return (len(machines) < MAX_VMS_TOTAL and
110 len(active_machines) < MAX_VMS_ACTIVE)
112 def haveAccess(user, machine):
113 """Return whether a user has access to a machine"""
114 if user.username == 'moo':
116 return machine.owner == user.username
118 def error(op, user, fields, err):
119 """Print an error page when a CodeError occurs"""
120 d = dict(op=op, user=user, errorMessage=str(err))
121 print Template(file='error.tmpl', searchList=[d, global_dict]);
123 def validMachineName(name):
124 """Check that name is valid for a machine name"""
127 charset = string.ascii_letters + string.digits + '-_'
128 if name[0] in '-_' or len(name) > 22:
135 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
136 """Kinit with a given username and keytab"""
138 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
139 stderr=subprocess.PIPE)
142 raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
145 """If we lack tickets, kinit."""
146 p = subprocess.Popen(['klist', '-s'])
150 def remctl(*args, **kws):
151 """Perform a remctl and return the output.
153 kinits if necessary, and outputs errors to stderr.
156 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
158 stdout=subprocess.PIPE,
159 stderr=subprocess.PIPE)
162 return p.stdout.read(), p.stderr.read()
164 raise CodeError('ERROR on remctl %s: %s' %
165 (args, p.stderr.read()))
166 return p.stdout.read()
168 def lvcreate(machine, disk):
169 """Create a single disk for a machine"""
170 remctl('web', 'lvcreate', machine.name,
171 disk.guest_device_name, str(disk.size))
173 def makeDisks(machine):
174 """Update the lvm partitions to add a disk."""
175 for disk in machine.disks:
176 lvcreate(machine, disk)
178 def bootMachine(machine, cdtype):
179 """Boot a machine with a given boot CD.
181 If cdtype is None, give no boot cd. Otherwise, it is the string
182 id of the CD (e.g. 'gutsy_i386')
184 if cdtype is not None:
185 remctl('web', 'vmboot', machine.name,
188 remctl('web', 'vmboot', machine.name)
190 def registerMachine(machine):
191 """Register a machine to be controlled by the web interface"""
192 remctl('web', 'register', machine.name)
194 def unregisterMachine(machine):
195 """Unregister a machine to not be controlled by the web interface"""
196 remctl('web', 'unregister', machine.name)
199 """Parse a status string into nested tuples of strings.
201 s = output of xm list --long <machine_name>
203 values = re.split('([()])', s)
205 for v in values[2:-2]: #remove initial and final '()'
212 if len(stack[-1]) == 1:
214 stack[-2].append(stack[-1])
219 stack[-1].extend(v.split())
222 def getUptimes(machines):
223 """Return a dictionary mapping machine names to uptime strings"""
224 value_string = remctl('web', 'listvms')
225 lines = value_string.splitlines()
230 uptime = ' '.join(lst[2:])
234 ans[m] = d.get(m.name)
237 def statusInfo(machine):
238 """Return the status list for a given machine.
240 Gets and parses xm list --long
242 value_string, err_string = remctl('list-long', machine.name, err=True)
243 if 'Unknown command' in err_string:
244 raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
245 elif 'does not exist' in err_string:
248 raise CodeError("ERROR in remctl list-long %s: %s" % (machine.name, err_string))
249 status = parseStatus(value_string)
253 """Does the machine with a given status list support VNC?"""
257 if l[0] == 'device' and l[1][0] == 'vfb':
259 return 'location' in d
262 def createVm(user, name, memory, disk, is_hvm, cdrom):
263 """Create a VM and put it in the database"""
264 # put stuff in the table
265 transaction = ctx.current.create_transaction()
267 if memory > maxMemory(user):
268 raise InvalidInput("Too much memory requested")
269 if disk > maxDisk(user) * 1024:
270 raise InvalidInput("Too much disk requested")
271 if not canAddVm(user):
272 raise InvalidInput("Too many VMs requested")
273 res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
274 id = res.fetchone()[0]
276 machine.machine_id = id
278 machine.memory = memory
279 machine.owner = user.username
280 machine.contact = user.email
281 machine.uuid = uuidToString(randomUUID())
282 machine.boot_off_cd = True
283 machine_type = Type.get_by(hvm=is_hvm)
284 machine.type_id = machine_type.type_id
285 ctx.current.save(machine)
286 disk = Disk(machine.machine_id,
288 open = NIC.select_by(machine_id=None)
289 if not open: #No IPs left!
290 raise CodeError("No IP addresses left! Contact sipb-xen-dev@mit.edu")
292 nic.machine_id = machine.machine_id
294 ctx.current.save(nic)
295 ctx.current.save(disk)
298 transaction.rollback()
300 registerMachine(machine)
302 # tell it to boot with cdrom
303 bootMachine(machine, cdrom)
307 def validMemory(user, memory, machine=None):
308 """Parse and validate limits for memory for a given user and machine."""
311 if memory < MIN_MEMORY_SINGLE:
314 raise InvalidInput("Invalid memory amount; must be at least %s MB" %
316 if memory > maxMemory(user, machine):
317 raise InvalidInput("Too much memory requested")
320 def validDisk(user, disk, machine=None):
321 """Parse and validate limits for disk for a given user and machine."""
324 if disk > maxDisk(user, machine):
325 raise InvalidInput("Too much disk requested")
326 disk = int(disk * 1024)
327 if disk < MIN_DISK_SINGLE * 1024:
330 raise InvalidInput("Invalid disk amount; minimum is %s GB" %
334 def create(user, fields):
335 """Handler for create requests."""
336 name = fields.getfirst('name')
337 if not validMachineName(name):
338 raise InvalidInput("Invalid name '%s'" % name)
339 name = user.username + '_' + name.lower()
341 if Machine.get_by(name=name):
342 raise InvalidInput("A machine named '%s' already exists" % name)
344 memory = fields.getfirst('memory')
345 memory = validMemory(user, memory)
347 disk = fields.getfirst('disk')
348 disk = validDisk(user, disk)
350 vm_type = fields.getfirst('vmtype')
351 if vm_type not in ('hvm', 'paravm'):
352 raise CodeError("Invalid vm type '%s'" % vm_type)
353 is_hvm = (vm_type == 'hvm')
355 cdrom = fields.getfirst('cdrom')
356 if cdrom is not None and not CDROM.get(cdrom):
357 raise CodeError("Invalid cdrom type '%s'" % cdrom)
359 machine = createVm(user, name, memory, disk, is_hvm, cdrom)
362 print Template(file='create.tmpl',
363 searchList=[d, global_dict]);
365 def listVms(user, fields):
366 """Handler for list requests."""
367 machines = [m for m in Machine.select() if haveAccess(user, m)]
377 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
379 # status = statusInfo(m)
380 # on[m.name] = status is not None
381 # has_vnc[m.name] = hasVnc(status)
382 max_mem=maxMemory(user)
383 max_disk=maxDisk(user)
385 can_add_vm=canAddVm(user),
389 default_disk=min(4.0, max_disk),
393 cdroms=CDROM.select())
394 print Template(file='list.tmpl', searchList=[d, global_dict])
396 def testMachineId(user, machineId, exists=True):
397 """Parse, validate and check authorization for a given machineId.
399 If exists is False, don't check that it exists.
401 if machineId is None:
402 raise CodeError("No machine ID specified")
404 machineId = int(machineId)
406 raise CodeError("Invalid machine ID '%s'" % machineId)
407 machine = Machine.get(machineId)
408 if exists and machine is None:
409 raise CodeError("No such machine ID '%s'" % machineId)
410 if machine is not None and not haveAccess(user, machine):
411 raise CodeError("No access to machine ID '%s'" % machineId)
414 def vnc(user, fields):
417 Note that due to same-domain restrictions, the applet connects to
418 the webserver, which needs to forward those requests to the xen
419 server. The Xen server runs another proxy that (1) authenticates
420 and (2) finds the correct port for the VM.
422 You might want iptables like:
424 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
425 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142
426 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
428 Remember to enable iptables!
429 echo 1 > /proc/sys/net/ipv4/ip_forward
431 machine = testMachineId(user, fields.getfirst('machine_id'))
433 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
436 data["user"] = user.username
437 data["machine"]=machine.name
438 data["expires"]=time.time()+(5*60)
439 pickledData = cPickle.dumps(data)
440 m = hmac.new(TOKEN_KEY, digestmod=sha)
441 m.update(pickledData)
442 token = {'data': pickledData, 'digest': m.digest()}
443 token = cPickle.dumps(token)
444 token = base64.urlsafe_b64encode(token)
446 status = statusInfo(machine)
447 has_vnc = hasVnc(status)
453 hostname=os.environ.get('SERVER_NAME', 'localhost'),
455 print Template(file='vnc.tmpl',
456 searchList=[d, global_dict])
458 def getNicInfo(data_dict, machine):
459 """Helper function for info, get data on nics for a machine.
461 Modifies data_dict to include the relevant data, and returns a list
462 of (key, name) pairs to display "name: data_dict[key]" to the user.
464 data_dict['num_nics'] = len(machine.nics)
465 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
466 ('nic%s_mac', 'NIC %s MAC Addr'),
467 ('nic%s_ip', 'NIC %s IP'),
470 for i in range(len(machine.nics)):
471 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
472 data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
473 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
474 data_dict['nic%s_ip' % i] = machine.nics[i].ip
475 if len(machine.nics) == 1:
476 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
479 def getDiskInfo(data_dict, machine):
480 """Helper function for info, get data on disks for a machine.
482 Modifies data_dict to include the relevant data, and returns a list
483 of (key, name) pairs to display "name: data_dict[key]" to the user.
485 data_dict['num_disks'] = len(machine.disks)
486 disk_fields_template = [('%s_size', '%s size')]
488 for disk in machine.disks:
489 name = disk.guest_device_name
490 disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
491 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
494 def deleteVM(machine):
496 transaction = ctx.current.create_transaction()
497 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
499 for nic in machine.nics:
500 nic.machine_id = None
502 ctx.current.save(nic)
503 for disk in machine.disks:
504 ctx.current.delete(disk)
505 ctx.current.delete(machine)
508 transaction.rollback()
510 for mname, dname in delete_disk_pairs:
511 remctl('web', 'lvremove', mname, dname)
512 unregisterMachine(machine)
514 def command(user, fields):
515 """Handler for running commands like boot and delete on a VM."""
516 print time.time()-start_time
517 machine = testMachineId(user, fields.getfirst('machine_id'))
518 action = fields.getfirst('action')
519 cdrom = fields.getfirst('cdrom')
520 print time.time()-start_time
521 if cdrom is not None and not CDROM.get(cdrom):
522 raise CodeError("Invalid cdrom type '%s'" % cdrom)
523 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
524 raise CodeError("Invalid action '%s'" % action)
525 if action == 'Reboot':
526 if cdrom is not None:
527 remctl('reboot', machine.name, cdrom)
529 remctl('reboot', machine.name)
530 elif action == 'Power on':
531 if maxMemory(user) < machine.memory:
532 raise InvalidInput("You don't have enough free RAM quota")
533 bootMachine(machine, cdrom)
534 elif action == 'Power off':
535 remctl('destroy', machine.name)
536 elif action == 'Shutdown':
537 remctl('shutdown', machine.name)
538 elif action == 'Delete VM':
540 print time.time()-start_time
545 print Template(file="command.tmpl", searchList=[d, global_dict])
547 def modify(user, fields):
548 """Handler for modifying attributes of a machine."""
550 machine = testMachineId(user, fields.getfirst('machine_id'))
552 def help(user, fields):
553 """Handler for help messages."""
554 simple = fields.getfirst('simple')
555 subjects = fields.getlist('subject')
557 mapping = dict(paravm_console="""
558 ParaVM machines do not support console access over VNC. To access
559 these machines, you either need to boot with a liveCD and ssh in or
560 hope that the sipb-xen maintainers add support for serial consoles.""",
562 HVM machines use the virtualization features of the processor, while
563 ParaVM machines use Xen's emulation of virtualization features. You
564 want an HVM virtualized machine.""",
565 cpu_weight="""Don't ask us! We're as mystified as you are.""")
572 print Template(file="help.tmpl", searchList=[d, global_dict])
575 def info(user, fields):
576 """Handler for info on a single VM."""
577 machine = testMachineId(user, fields.getfirst('machine_id'))
578 status = statusInfo(machine)
579 has_vnc = hasVnc(status)
581 main_status = dict(name=machine.name,
582 memory=str(machine.memory))
584 main_status = dict(status[1:])
585 start_time = float(main_status.get('start_time', 0))
586 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
587 cpu_time_float = float(main_status.get('cpu_time', 0))
588 cputime = datetime.timedelta(seconds=int(cpu_time_float))
589 display_fields = """name uptime memory state cpu_weight on_reboot
590 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
591 display_fields = [('name', 'Name'),
593 ('contact', 'Contact'),
596 ('uptime', 'uptime'),
597 ('cputime', 'CPU usage'),
600 ('state', 'state (xen format)'),
601 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
602 ('on_reboot', 'Action on VM reboot'),
603 ('on_poweroff', 'Action on VM poweroff'),
604 ('on_crash', 'Action on VM crash'),
605 ('on_xend_start', 'Action on Xen start'),
606 ('on_xend_stop', 'Action on Xen stop'),
607 ('bootloader', 'Bootloader options'),
611 machine_info['name'] = machine.name
612 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
613 machine_info['owner'] = machine.owner
614 machine_info['contact'] = machine.contact
616 nic_fields = getNicInfo(machine_info, machine)
617 nic_point = display_fields.index('NIC_INFO')
618 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
620 disk_fields = getDiskInfo(machine_info, machine)
621 disk_point = display_fields.index('DISK_INFO')
622 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
624 main_status['memory'] += ' MB'
625 for field, disp in display_fields:
626 if field in ('uptime', 'cputime'):
627 fields.append((disp, locals()[field]))
628 elif field in machine_info:
629 fields.append((disp, machine_info[field]))
630 elif field in main_status:
631 fields.append((disp, main_status[field]))
634 #fields.append((disp, None))
635 max_mem = maxMemory(user, machine)
636 max_disk = maxDisk(user, machine)
638 cdroms=CDROM.select(),
639 on=status is not None,
647 print Template(file='info.tmpl',
648 searchList=[d, global_dict])
650 mapping = dict(list=listVms,
658 if __name__ == '__main__':
659 start_time = time.time()
660 fields = cgi.FieldStorage()
663 email = 'moo@cow.com'
666 if 'SSL_CLIENT_S_DN_Email' in os.environ:
667 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
668 u.username = username
669 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
673 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
674 operation = os.environ.get('PATH_INFO', '')
675 #print 'Content-Type: text/plain\n'
678 print "Status: 301 Moved Permanently"
679 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
681 print 'Content-Type: text/html\n'
683 if operation.startswith('/'):
684 operation = operation[1:]
688 fun = mapping.get(operation,
690 error(operation, u, e,
691 "Invalid operation '%s'" % operation))
692 if fun not in (help, ):
693 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
696 except CodeError, err:
697 error(operation, u, fields, err)
698 except InvalidInput, err:
699 error(operation, u, fields, err)