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
620 memory = fields.getfirst('memory')
621 if memory is not None:
622 memory = validMemory(user, memory, machine)
624 memory = machine.memory
625 if memory != machine.memory:
626 machine.memory = memory
628 disksize = testDisk(user, fields.getfirst('disk'))
629 if disksize is not None:
630 disksize = validDisk(user, disksize, machine)
632 disksize = machine.disks[0].size
633 for disk in machine.disks:
634 olddisk[disk.guest_device_name] = disk.size
636 ctx.current.save(disk)
638 # XXX all NICs get same hostname on change? Interface doesn't support more.
639 for nic in machine.nics:
640 nic.hostname = hostname
641 ctx.current.save(nic)
643 if owner != machine.owner:
644 machine.owner = owner
645 if name != machine.name:
648 ctx.current.save(machine)
651 transaction.rollback()
653 remctl("web", "moveregister", oldname, name)
654 for disk in machine.disks:
655 # XXX all disks get the same size on change? Interface doesn't support more.
656 if disk.size != olddisk[disk.guest_device_name]:
657 remctl("web", "lvresize", oldname, disk.guest_device_name, str(disk.size))
659 remctl("web", "lvrename", oldname, disk.guest_device_name, name)
663 return Template(file="command.tmpl", searchList=[d, global_dict])
666 def help(user, fields):
667 """Handler for help messages."""
668 simple = fields.getfirst('simple')
669 subjects = fields.getlist('subject')
671 mapping = dict(paravm_console="""
672 ParaVM machines do not support console access over VNC. To access
673 these machines, you either need to boot with a liveCD and ssh in or
674 hope that the sipb-xen maintainers add support for serial consoles.""",
676 HVM machines use the virtualization features of the processor, while
677 ParaVM machines use Xen's emulation of virtualization features. You
678 want an HVM virtualized machine.""",
679 cpu_weight="""Don't ask us! We're as mystified as you are.""",
680 owner="""The Owner must be the name of a locker that you are an AFS
681 administrator of. In particular, you or an AFS group you are a member
682 of must have AFS rlidwka bits on the locker. You can check see who
683 administers the LOCKER locker using the command 'fs la /mit/LOCKER' on
691 return Template(file="help.tmpl", searchList=[d, global_dict])
694 def info(user, fields):
695 """Handler for info on a single VM."""
696 machine = testMachineId(user, fields.getfirst('machine_id'))
697 status = statusInfo(machine)
698 has_vnc = hasVnc(status)
700 main_status = dict(name=machine.name,
701 memory=str(machine.memory))
703 main_status = dict(status[1:])
704 start_time = float(main_status.get('start_time', 0))
705 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
706 cpu_time_float = float(main_status.get('cpu_time', 0))
707 cputime = datetime.timedelta(seconds=int(cpu_time_float))
708 display_fields = """name uptime memory state cpu_weight on_reboot
709 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
710 display_fields = [('name', 'Name'),
712 ('contact', 'Contact'),
715 ('uptime', 'uptime'),
716 ('cputime', 'CPU usage'),
719 ('state', 'state (xen format)'),
720 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
721 ('on_reboot', 'Action on VM reboot'),
722 ('on_poweroff', 'Action on VM poweroff'),
723 ('on_crash', 'Action on VM crash'),
724 ('on_xend_start', 'Action on Xen start'),
725 ('on_xend_stop', 'Action on Xen stop'),
726 ('bootloader', 'Bootloader options'),
730 machine_info['name'] = machine.name
731 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
732 machine_info['owner'] = machine.owner
733 machine_info['contact'] = machine.contact
735 nic_fields = getNicInfo(machine_info, machine)
736 nic_point = display_fields.index('NIC_INFO')
737 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
739 disk_fields = getDiskInfo(machine_info, machine)
740 disk_point = display_fields.index('DISK_INFO')
741 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
743 main_status['memory'] += ' MB'
744 for field, disp in display_fields:
745 if field in ('uptime', 'cputime'):
746 fields.append((disp, locals()[field]))
747 elif field in machine_info:
748 fields.append((disp, machine_info[field]))
749 elif field in main_status:
750 fields.append((disp, main_status[field]))
753 #fields.append((disp, None))
754 max_mem = maxMemory(user, machine)
755 max_disk = maxDisk(user, machine)
757 cdroms=CDROM.select(),
758 on=status is not None,
765 owner_help=helppopup("owner"),
767 return Template(file='info.tmpl',
768 searchList=[d, global_dict])
770 mapping = dict(list=listVms,
778 if __name__ == '__main__':
779 start_time = time.time()
780 fields = cgi.FieldStorage()
783 email = 'moo@cow.com'
786 if 'SSL_CLIENT_S_DN_Email' in os.environ:
787 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
788 u.username = username
789 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
793 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
794 operation = os.environ.get('PATH_INFO', '')
795 # print 'Content-Type: text/plain\n'
798 print "Status: 301 Moved Permanently"
799 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
802 if operation.startswith('/'):
803 operation = operation[1:]
807 def badOperation(u, e):
808 raise CodeError("Unknown operation")
810 fun = mapping.get(operation, badOperation)
811 if fun not in (help, ):
812 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
814 output = fun(u, fields)
815 print 'Content-Type: text/html\n'
817 e = sys.stderr.read()
820 output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
822 except CodeError, err:
823 print 'Content-Type: text/html\n'
825 e = sys.stderr.read()
826 sys.stderr=sys.stdout
827 print error(operation, u, fields, err, e)
828 except InvalidInput, err:
829 print 'Content-Type: text/html\n'
831 e = sys.stderr.read()
832 sys.stderr=sys.stdout
833 print invalidInput(operation, u, fields, err, e)
835 print 'Content-Type: text/plain\n'
837 e = sys.stderr.read()
840 sys.stderr = sys.stdout