18 sys.stderr = StringIO.StringIO()
19 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
21 from Cheetah.Template import Template
22 from sipb_xen_database import *
25 class MyException(Exception):
26 """Base class for my exceptions"""
29 class InvalidInput(MyException):
30 """Exception for user-provided input is invalid but maybe in good faith.
32 This would include setting memory to negative (which might be a
33 typo) but not setting an invalid boot CD (which requires bypassing
36 def __init__(self, err_field, err_value, expl=None):
37 MyException.__init__(self, expl)
38 self.err_field = err_field
39 self.err_value = err_value
41 class CodeError(MyException):
42 """Exception for internal errors or bad faith input."""
46 def __init__(self, user):
49 def __get_uptimes(self):
50 if not hasattr(self, '_uptimes'):
51 self._uptimes = getUptimes(Machine.select())
53 uptimes = property(__get_uptimes)
58 """Return HTML code for a (?) link to a specified help topic"""
59 return '<span class="helplink"><a href="help?subject='+subj+'&simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
63 global_dict['helppopup'] = helppopup
66 # ... and stolen from xend/uuid.py
68 """Generate a random UUID."""
70 return [ random.randint(0, 255) for _ in range(0, 16) ]
73 """Turn a numeric UUID to a hyphen-seperated one."""
74 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
75 "%02x" * 6]) % tuple(u)
77 MAX_MEMORY_TOTAL = 512
78 MAX_MEMORY_SINGLE = 256
79 MIN_MEMORY_SINGLE = 16
86 def getMachinesByOwner(owner):
87 """Return the machines owned by a given owner."""
88 return Machine.select_by(owner=owner)
90 def maxMemory(user, machine=None):
91 """Return the maximum memory for a machine or a user.
93 If machine is None, return the memory available for a new
94 machine. Else, return the maximum that machine can have.
96 on is a dictionary from machines to booleans, whether a machine is
97 on. If None, it is recomputed. XXX make this global?
100 machines = getMachinesByOwner(user.username)
101 active_machines = [x for x in machines if g.uptimes[x]]
102 mem_usage = sum([x.memory for x in active_machines if x != machine])
103 return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
105 def maxDisk(user, machine=None):
106 machines = getMachinesByOwner(user.username)
107 disk_usage = sum([sum([y.size for y in x.disks])
108 for x in machines if x != machine])
109 return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
112 machines = getMachinesByOwner(user.username)
113 active_machines = [x for x in machines if g.uptimes[x]]
114 return (len(machines) < MAX_VMS_TOTAL and
115 len(active_machines) < MAX_VMS_ACTIVE)
117 def haveAccess(user, machine):
118 """Return whether a user has access to a machine"""
119 if user.username == 'moo':
121 return getafsgroups.checkLockerOwner(user.username,machine.owner)
123 def error(op, user, fields, err, emsg):
124 """Print an error page when a CodeError occurs"""
125 d = dict(op=op, user=user, errorMessage=str(err),
127 return Template(file='error.tmpl', searchList=[d, global_dict]);
129 def invalidInput(op, user, fields, err, emsg):
130 """Print an error page when an InvalidInput exception occurs"""
131 d = dict(op=op, user=user, err_field=err.err_field,
132 err_value=str(err.err_value), stderr=emsg,
133 errorMessage=str(err))
134 return Template(file='invalid.tmpl', searchList=[d, global_dict]);
136 def validMachineName(name):
137 """Check that name is valid for a machine name"""
140 charset = string.ascii_letters + string.digits + '-_'
141 if name[0] in '-_' or len(name) > 22:
148 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
149 """Kinit with a given username and keytab"""
151 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
152 stderr=subprocess.PIPE)
155 raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
158 """If we lack tickets, kinit."""
159 p = subprocess.Popen(['klist', '-s'])
163 def remctl(*args, **kws):
164 """Perform a remctl and return the output.
166 kinits if necessary, and outputs errors to stderr.
169 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
171 stdout=subprocess.PIPE,
172 stderr=subprocess.PIPE)
175 return p.stdout.read(), p.stderr.read()
177 raise CodeError('ERROR on remctl %s: %s' %
178 (args, p.stderr.read()))
179 return p.stdout.read()
181 def lvcreate(machine, disk):
182 """Create a single disk for a machine"""
183 remctl('web', 'lvcreate', machine.name,
184 disk.guest_device_name, str(disk.size))
186 def makeDisks(machine):
187 """Update the lvm partitions to add a disk."""
188 for disk in machine.disks:
189 lvcreate(machine, disk)
191 def bootMachine(machine, cdtype):
192 """Boot a machine with a given boot CD.
194 If cdtype is None, give no boot cd. Otherwise, it is the string
195 id of the CD (e.g. 'gutsy_i386')
197 if cdtype is not None:
198 remctl('web', 'vmboot', machine.name,
201 remctl('web', 'vmboot', machine.name)
203 def registerMachine(machine):
204 """Register a machine to be controlled by the web interface"""
205 remctl('web', 'register', machine.name)
207 def unregisterMachine(machine):
208 """Unregister a machine to not be controlled by the web interface"""
209 remctl('web', 'unregister', machine.name)
212 """Parse a status string into nested tuples of strings.
214 s = output of xm list --long <machine_name>
216 values = re.split('([()])', s)
218 for v in values[2:-2]: #remove initial and final '()'
225 if len(stack[-1]) == 1:
227 stack[-2].append(stack[-1])
232 stack[-1].extend(v.split())
235 def getUptimes(machines=None):
236 """Return a dictionary mapping machine names to uptime strings"""
237 value_string = remctl('web', 'listvms')
238 lines = value_string.splitlines()
243 uptime = ' '.join(lst[2:])
247 ans[m] = d.get(m.name)
250 def statusInfo(machine):
251 """Return the status list for a given machine.
253 Gets and parses xm list --long
255 value_string, err_string = remctl('list-long', machine.name, err=True)
256 if 'Unknown command' in err_string:
257 raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
258 elif 'does not exist' in err_string:
261 raise CodeError("ERROR in remctl list-long %s: %s" % (machine.name, err_string))
262 status = parseStatus(value_string)
266 """Does the machine with a given status list support VNC?"""
270 if l[0] == 'device' and l[1][0] == 'vfb':
272 return 'location' in d
275 def createVm(user, name, memory, disk, is_hvm, cdrom):
276 """Create a VM and put it in the database"""
277 # put stuff in the table
278 transaction = ctx.current.create_transaction()
280 if memory > maxMemory(user):
281 raise InvalidInput('memory', memory,
282 "Max %s" % maxMemory(user))
283 if disk > maxDisk(user) * 1024:
284 raise InvalidInput('disk', disk,
285 "Max %s" % maxDisk(user))
286 if not canAddVm(user):
287 raise InvalidInput('create', True, 'Unable to create more VMs')
288 res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
289 id = res.fetchone()[0]
291 machine.machine_id = id
293 machine.memory = memory
294 machine.owner = user.username
295 machine.contact = user.email
296 machine.uuid = uuidToString(randomUUID())
297 machine.boot_off_cd = True
298 machine_type = Type.get_by(hvm=is_hvm)
299 machine.type_id = machine_type.type_id
300 ctx.current.save(machine)
301 disk = Disk(machine.machine_id,
303 open = NIC.select_by(machine_id=None)
304 if not open: #No IPs left!
305 raise CodeError("No IP addresses left! Contact sipb-xen-dev@mit.edu")
307 nic.machine_id = machine.machine_id
309 ctx.current.save(nic)
310 ctx.current.save(disk)
313 transaction.rollback()
315 registerMachine(machine)
317 # tell it to boot with cdrom
318 bootMachine(machine, cdrom)
322 def validMemory(user, memory, machine=None):
323 """Parse and validate limits for memory for a given user and machine."""
326 if memory < MIN_MEMORY_SINGLE:
329 raise InvalidInput('memory', memory,
330 "Minimum %s MB" % MIN_MEMORY_SINGLE)
331 if memory > maxMemory(user, machine):
332 raise InvalidInput('memory', memory,
333 'Maximum %s MB' % maxMemory(user, machine))
336 def validDisk(user, disk, machine=None):
337 """Parse and validate limits for disk for a given user and machine."""
340 if disk > maxDisk(user, machine):
341 raise InvalidInput('disk', disk,
342 "Maximum %s G" % maxDisk(user, machine))
343 disk = int(disk * 1024)
344 if disk < MIN_DISK_SINGLE * 1024:
347 raise InvalidInput('disk', disk,
348 "Minimum %s GB" % MIN_DISK_SINGLE)
351 def create(user, fields):
352 """Handler for create requests."""
353 name = fields.getfirst('name')
354 if not validMachineName(name):
355 raise InvalidInput('name', name)
358 if Machine.get_by(name=name):
359 raise InvalidInput('name', name,
362 memory = fields.getfirst('memory')
363 memory = validMemory(user, memory)
365 disk = fields.getfirst('disk')
366 disk = validDisk(user, disk)
368 vm_type = fields.getfirst('vmtype')
369 if vm_type not in ('hvm', 'paravm'):
370 raise CodeError("Invalid vm type '%s'" % vm_type)
371 is_hvm = (vm_type == 'hvm')
373 cdrom = fields.getfirst('cdrom')
374 if cdrom is not None and not CDROM.get(cdrom):
375 raise CodeError("Invalid cdrom type '%s'" % cdrom)
377 machine = createVm(user, name, memory, disk, is_hvm, cdrom)
380 return Template(file='create.tmpl',
381 searchList=[d, global_dict]);
383 def listVms(user, fields):
384 """Handler for list requests."""
385 machines = [m for m in Machine.select() if haveAccess(user, m)]
395 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
397 # status = statusInfo(m)
398 # on[m.name] = status is not None
399 # has_vnc[m.name] = hasVnc(status)
400 max_mem=maxMemory(user)
401 max_disk=maxDisk(user)
403 can_add_vm=canAddVm(user),
407 default_disk=min(4.0, max_disk),
411 cdroms=CDROM.select())
412 return Template(file='list.tmpl', searchList=[d, global_dict])
414 def testMachineId(user, machineId, exists=True):
415 """Parse, validate and check authorization for a given machineId.
417 If exists is False, don't check that it exists.
419 if machineId is None:
420 raise CodeError("No machine ID specified")
422 machineId = int(machineId)
424 raise CodeError("Invalid machine ID '%s'" % machineId)
425 machine = Machine.get(machineId)
426 if exists and machine is None:
427 raise CodeError("No such machine ID '%s'" % machineId)
428 if machine is not None and not haveAccess(user, machine):
429 raise CodeError("No access to machine ID '%s'" % machineId)
432 def vnc(user, fields):
435 Note that due to same-domain restrictions, the applet connects to
436 the webserver, which needs to forward those requests to the xen
437 server. The Xen server runs another proxy that (1) authenticates
438 and (2) finds the correct port for the VM.
440 You might want iptables like:
442 -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
443 -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
444 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
446 Remember to enable iptables!
447 echo 1 > /proc/sys/net/ipv4/ip_forward
449 machine = testMachineId(user, fields.getfirst('machine_id'))
451 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
454 data["user"] = user.username
455 data["machine"]=machine.name
456 data["expires"]=time.time()+(5*60)
457 pickledData = cPickle.dumps(data)
458 m = hmac.new(TOKEN_KEY, digestmod=sha)
459 m.update(pickledData)
460 token = {'data': pickledData, 'digest': m.digest()}
461 token = cPickle.dumps(token)
462 token = base64.urlsafe_b64encode(token)
464 status = statusInfo(machine)
465 has_vnc = hasVnc(status)
471 hostname=os.environ.get('SERVER_NAME', 'localhost'),
473 return Template(file='vnc.tmpl',
474 searchList=[d, global_dict])
476 def getNicInfo(data_dict, machine):
477 """Helper function for info, get data on nics for a machine.
479 Modifies data_dict to include the relevant data, and returns a list
480 of (key, name) pairs to display "name: data_dict[key]" to the user.
482 data_dict['num_nics'] = len(machine.nics)
483 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
484 ('nic%s_mac', 'NIC %s MAC Addr'),
485 ('nic%s_ip', 'NIC %s IP'),
488 for i in range(len(machine.nics)):
489 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
490 data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
491 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
492 data_dict['nic%s_ip' % i] = machine.nics[i].ip
493 if len(machine.nics) == 1:
494 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
497 def getDiskInfo(data_dict, machine):
498 """Helper function for info, get data on disks for a machine.
500 Modifies data_dict to include the relevant data, and returns a list
501 of (key, name) pairs to display "name: data_dict[key]" to the user.
503 data_dict['num_disks'] = len(machine.disks)
504 disk_fields_template = [('%s_size', '%s size')]
506 for disk in machine.disks:
507 name = disk.guest_device_name
508 disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
509 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
512 def deleteVM(machine):
515 remctl('destroy', machine.name)
518 transaction = ctx.current.create_transaction()
519 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
521 for nic in machine.nics:
522 nic.machine_id = None
524 ctx.current.save(nic)
525 for disk in machine.disks:
526 ctx.current.delete(disk)
527 ctx.current.delete(machine)
530 transaction.rollback()
532 for mname, dname in delete_disk_pairs:
533 remctl('web', 'lvremove', mname, dname)
534 unregisterMachine(machine)
536 def command(user, fields):
537 """Handler for running commands like boot and delete on a VM."""
538 print >> sys.stderr, time.time()-start_time
539 machine = testMachineId(user, fields.getfirst('machine_id'))
540 action = fields.getfirst('action')
541 cdrom = fields.getfirst('cdrom')
542 print >> sys.stderr, time.time()-start_time
543 if cdrom is not None and not CDROM.get(cdrom):
544 raise CodeError("Invalid cdrom type '%s'" % cdrom)
545 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
546 raise CodeError("Invalid action '%s'" % action)
547 if action == 'Reboot':
548 if cdrom is not None:
549 remctl('reboot', machine.name, cdrom)
551 remctl('reboot', machine.name)
552 elif action == 'Power on':
553 if maxMemory(user) < machine.memory:
554 raise InvalidInput('action', 'Power on',
555 "You don't have enough free RAM quota to turn on this machine")
556 bootMachine(machine, cdrom)
557 elif action == 'Power off':
558 remctl('destroy', machine.name)
559 elif action == 'Shutdown':
560 remctl('shutdown', machine.name)
561 elif action == 'Delete VM':
563 print >> sys.stderr, time.time()-start_time
568 return Template(file="command.tmpl", searchList=[d, global_dict])
570 def testOwner(user, owner, machine=None):
571 if not getafsgroups.checkLockerOwner(user.username, owner):
572 raise InvalidInput('owner', owner,
576 def testContact(user, contact, machine=None):
577 if contact != user.email:
578 raise InvalidInput('contact', contact,
582 def testDisk(user, disksize, machine=None):
585 def testName(user, name, machine=None):
586 if Machine.select_by(name=name) == []:
588 if name == machine.name:
590 raise InvalidInput('name', name,
593 def testHostname(user, hostname, machine):
594 for nic in machine.nics:
595 if hostname == nic.hostname:
597 # check if doesn't already exist
598 if NIC.select_by(hostname=hostname) == []:
600 raise InvalidInput('hostname', hostname,
601 "Different from before")
604 def modify(user, fields):
605 """Handler for modifying attributes of a machine."""
608 transaction = ctx.current.create_transaction()
610 machine = testMachineId(user, fields.getfirst('machine_id'))
611 owner = testOwner(user, fields.getfirst('owner'), machine)
612 contact = testContact(user, fields.getfirst('contact'))
613 hostname = testHostname(owner, fields.getfirst('hostname'),
615 name = testName(user, fields.getfirst('name'), machine)
616 oldname = machine.name
619 memory = fields.getfirst('memory')
620 if memory is not None:
621 memory = validMemory(user, memory, machine)
623 memory = machine.memory
624 if memory != machine.memory:
625 machine.memory = memory
627 disksize = testDisk(user, fields.getfirst('disk'))
628 if disksize is not None:
629 disksize = validDisk(user, disksize, machine)
630 for disk in machine.disks:
632 olddisk[disk.guest_device_name] = disk.size
633 ctx.current.save(disk)
635 # XXX all NICs get same hostname on change? Interface doesn't support more.
636 for nic in machine.nics:
637 nic.hostname = hostname
638 ctx.current.save(nic)
640 if owner != machine.owner:
641 machine.owner = owner
642 if name != machine.name:
645 ctx.current.save(machine)
648 transaction.rollback()
650 remctl("web", "moveregister", oldname, name)
651 for disk in machine.disks:
652 # XXX all disks get the same size on change? Interface doesn't support more.
653 if disk.size != olddisk[disk.guest_device_name]:
654 remctl("web", "lvresize", oldname, disk.guest_device_name, str(disk.size))
656 remctl("web", "lvrename", oldname, disk.guest_device_name, name)
660 return Template(file="command.tmpl", searchList=[d, global_dict])
663 def help(user, fields):
664 """Handler for help messages."""
665 simple = fields.getfirst('simple')
666 subjects = fields.getlist('subject')
668 mapping = dict(paravm_console="""
669 ParaVM machines do not support console access over VNC. To access
670 these machines, you either need to boot with a liveCD and ssh in or
671 hope that the sipb-xen maintainers add support for serial consoles.""",
673 HVM machines use the virtualization features of the processor, while
674 ParaVM machines use Xen's emulation of virtualization features. You
675 want an HVM virtualized machine.""",
676 cpu_weight="""Don't ask us! We're as mystified as you are.""")
683 return Template(file="help.tmpl", searchList=[d, global_dict])
686 def info(user, fields):
687 """Handler for info on a single VM."""
688 machine = testMachineId(user, fields.getfirst('machine_id'))
689 status = statusInfo(machine)
690 has_vnc = hasVnc(status)
692 main_status = dict(name=machine.name,
693 memory=str(machine.memory))
695 main_status = dict(status[1:])
696 start_time = float(main_status.get('start_time', 0))
697 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
698 cpu_time_float = float(main_status.get('cpu_time', 0))
699 cputime = datetime.timedelta(seconds=int(cpu_time_float))
700 display_fields = """name uptime memory state cpu_weight on_reboot
701 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
702 display_fields = [('name', 'Name'),
704 ('contact', 'Contact'),
707 ('uptime', 'uptime'),
708 ('cputime', 'CPU usage'),
711 ('state', 'state (xen format)'),
712 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
713 ('on_reboot', 'Action on VM reboot'),
714 ('on_poweroff', 'Action on VM poweroff'),
715 ('on_crash', 'Action on VM crash'),
716 ('on_xend_start', 'Action on Xen start'),
717 ('on_xend_stop', 'Action on Xen stop'),
718 ('bootloader', 'Bootloader options'),
722 machine_info['name'] = machine.name
723 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
724 machine_info['owner'] = machine.owner
725 machine_info['contact'] = machine.contact
727 nic_fields = getNicInfo(machine_info, machine)
728 nic_point = display_fields.index('NIC_INFO')
729 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
731 disk_fields = getDiskInfo(machine_info, machine)
732 disk_point = display_fields.index('DISK_INFO')
733 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
735 main_status['memory'] += ' MB'
736 for field, disp in display_fields:
737 if field in ('uptime', 'cputime'):
738 fields.append((disp, locals()[field]))
739 elif field in machine_info:
740 fields.append((disp, machine_info[field]))
741 elif field in main_status:
742 fields.append((disp, main_status[field]))
745 #fields.append((disp, None))
746 max_mem = maxMemory(user, machine)
747 max_disk = maxDisk(user, machine)
749 cdroms=CDROM.select(),
750 on=status is not None,
758 return Template(file='info.tmpl',
759 searchList=[d, global_dict])
761 mapping = dict(list=listVms,
769 if __name__ == '__main__':
770 start_time = time.time()
771 fields = cgi.FieldStorage()
774 email = 'moo@cow.com'
777 if 'SSL_CLIENT_S_DN_Email' in os.environ:
778 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
779 u.username = username
780 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
784 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
785 operation = os.environ.get('PATH_INFO', '')
786 # print 'Content-Type: text/plain\n'
789 print "Status: 301 Moved Permanently"
790 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
793 if operation.startswith('/'):
794 operation = operation[1:]
798 def badOperation(u, e):
799 raise CodeError("Unknown operation")
801 fun = mapping.get(operation, badOperation)
802 if fun not in (help, ):
803 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
805 output = fun(u, fields)
806 print 'Content-Type: text/html\n'
808 e = sys.stderr.read()
811 output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
813 except CodeError, err:
814 print 'Content-Type: text/html\n'
816 e = sys.stderr.read()
817 sys.stderr=sys.stdout
818 print error(operation, u, fields, err, e)
819 except InvalidInput, err:
820 print 'Content-Type: text/html\n'
822 e = sys.stderr.read()
823 sys.stderr=sys.stdout
824 print invalidInput(operation, u, fields, err, e)
826 print 'Content-Type: text/plain\n'
828 e = sys.stderr.read()
831 sys.stderr = sys.stdout