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 super(InvalidInput, self).__init__(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):
588 def testHostname(user, hostname, machine):
589 for nic in machine.nics:
590 if hostname == nic.hostname:
592 # check if doesn't already exist
593 if NIC.select_by(hostname=hostname) == []:
595 raise InvalidInput('hostname', hostname,
596 "Different from before")
599 def modify(user, fields):
600 """Handler for modifying attributes of a machine."""
603 transaction = ctx.current.create_transaction()
605 machine = testMachineId(user, fields.getfirst('machine_id'))
606 owner = testOwner(user, fields.getfirst('owner'), machine)
607 contact = testContact(user, fields.getfirst('contact'))
608 hostname = testHostname(owner, fields.getfirst('hostname'),
610 name = testName(user, fields.getfirst('name'))
611 oldname = machine.name
614 memory = fields.getfirst('memory')
615 if memory is not None:
616 memory = validMemory(user, memory, machine)
617 if memory != machine.memory:
618 machine.memory = memory
620 disksize = testDisk(user, fields.getfirst('disk'))
621 if disksize is not None:
622 disksize = validDisk(user, disksize, machine)
624 for disk in machine.disks:
626 olddisk[disk.guest_device_name] = disk.size
627 ctx.current.save(disk)
629 # XXX all NICs get same hostname on change? Interface doesn't support more.
630 for nic in machine.nics:
631 nic.hostname = hostname
632 ctx.current.save(nic)
634 if owner != machine.owner:
635 machine.owner = owner
636 if name != machine.name:
639 ctx.current.save(machine)
642 transaction.rollback()
643 remctl("web", "moveregister", oldname, name)
644 for disk in machine.disks:
645 # XXX all disks get the same size on change? Interface doesn't support more.
646 if disk.size != olddisk[disk.guest_device_name]:
647 remctl("web", "lvresize", oldname, disk.guest_device_name, str(disk.size))
649 remctl("web", "lvrename", oldname, disk.guest_device_name, name)
653 return Template(file="command.tmpl", searchList=[d, global_dict])
656 def help(user, fields):
657 """Handler for help messages."""
658 simple = fields.getfirst('simple')
659 subjects = fields.getlist('subject')
661 mapping = dict(paravm_console="""
662 ParaVM machines do not support console access over VNC. To access
663 these machines, you either need to boot with a liveCD and ssh in or
664 hope that the sipb-xen maintainers add support for serial consoles.""",
666 HVM machines use the virtualization features of the processor, while
667 ParaVM machines use Xen's emulation of virtualization features. You
668 want an HVM virtualized machine.""",
669 cpu_weight="""Don't ask us! We're as mystified as you are.""")
676 return Template(file="help.tmpl", searchList=[d, global_dict])
679 def info(user, fields):
680 """Handler for info on a single VM."""
681 machine = testMachineId(user, fields.getfirst('machine_id'))
682 status = statusInfo(machine)
683 has_vnc = hasVnc(status)
685 main_status = dict(name=machine.name,
686 memory=str(machine.memory))
688 main_status = dict(status[1:])
689 start_time = float(main_status.get('start_time', 0))
690 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
691 cpu_time_float = float(main_status.get('cpu_time', 0))
692 cputime = datetime.timedelta(seconds=int(cpu_time_float))
693 display_fields = """name uptime memory state cpu_weight on_reboot
694 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
695 display_fields = [('name', 'Name'),
697 ('contact', 'Contact'),
700 ('uptime', 'uptime'),
701 ('cputime', 'CPU usage'),
704 ('state', 'state (xen format)'),
705 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
706 ('on_reboot', 'Action on VM reboot'),
707 ('on_poweroff', 'Action on VM poweroff'),
708 ('on_crash', 'Action on VM crash'),
709 ('on_xend_start', 'Action on Xen start'),
710 ('on_xend_stop', 'Action on Xen stop'),
711 ('bootloader', 'Bootloader options'),
715 machine_info['name'] = machine.name
716 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
717 machine_info['owner'] = machine.owner
718 machine_info['contact'] = machine.contact
720 nic_fields = getNicInfo(machine_info, machine)
721 nic_point = display_fields.index('NIC_INFO')
722 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
724 disk_fields = getDiskInfo(machine_info, machine)
725 disk_point = display_fields.index('DISK_INFO')
726 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
728 main_status['memory'] += ' MB'
729 for field, disp in display_fields:
730 if field in ('uptime', 'cputime'):
731 fields.append((disp, locals()[field]))
732 elif field in machine_info:
733 fields.append((disp, machine_info[field]))
734 elif field in main_status:
735 fields.append((disp, main_status[field]))
738 #fields.append((disp, None))
739 max_mem = maxMemory(user, machine)
740 max_disk = maxDisk(user, machine)
742 cdroms=CDROM.select(),
743 on=status is not None,
751 return Template(file='info.tmpl',
752 searchList=[d, global_dict])
754 mapping = dict(list=listVms,
762 if __name__ == '__main__':
763 start_time = time.time()
764 fields = cgi.FieldStorage()
767 email = 'moo@cow.com'
770 if 'SSL_CLIENT_S_DN_Email' in os.environ:
771 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
772 u.username = username
773 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
777 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
778 operation = os.environ.get('PATH_INFO', '')
779 # print 'Content-Type: text/plain\n'
782 print "Status: 301 Moved Permanently"
783 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
786 if operation.startswith('/'):
787 operation = operation[1:]
791 def badOperation(u, e):
792 raise CodeError("Unknown operation")
794 fun = mapping.get(operation, badOperation)
795 if fun not in (help, ):
796 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
798 output = fun(u, fields)
799 print 'Content-Type: text/html\n'
801 e = sys.stderr.read()
804 output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
806 except CodeError, err:
807 print 'Content-Type: text/html\n'
809 e = sys.stderr.read()
810 sys.stderr=sys.stdout
811 print error(operation, u, fields, err, e)
812 except InvalidInput, err:
813 print 'Content-Type: text/html\n'
815 e = sys.stderr.read()
816 sys.stderr=sys.stdout
817 print invalidInput(operation, u, fields, err, e)
819 print 'Content-Type: text/plain\n'
821 e = sys.stderr.read()
824 sys.stderr = sys.stdout