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):
514 remctl('destroy', machine.name, err=True)
515 transaction = ctx.current.create_transaction()
516 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
518 for nic in machine.nics:
519 nic.machine_id = None
521 ctx.current.save(nic)
522 for disk in machine.disks:
523 ctx.current.delete(disk)
524 ctx.current.delete(machine)
527 transaction.rollback()
529 for mname, dname in delete_disk_pairs:
530 remctl('web', 'lvremove', mname, dname)
531 unregisterMachine(machine)
533 def command(user, fields):
534 """Handler for running commands like boot and delete on a VM."""
535 print >> sys.stderr, time.time()-start_time
536 machine = testMachineId(user, fields.getfirst('machine_id'))
537 action = fields.getfirst('action')
538 cdrom = fields.getfirst('cdrom')
539 print >> sys.stderr, time.time()-start_time
540 if cdrom is not None and not CDROM.get(cdrom):
541 raise CodeError("Invalid cdrom type '%s'" % cdrom)
542 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
543 raise CodeError("Invalid action '%s'" % action)
544 if action == 'Reboot':
545 if cdrom is not None:
546 remctl('reboot', machine.name, cdrom)
548 remctl('reboot', machine.name)
549 elif action == 'Power on':
550 if maxMemory(user) < machine.memory:
551 raise InvalidInput('action', 'Power on',
552 "You don't have enough free RAM quota to turn on this machine")
553 bootMachine(machine, cdrom)
554 elif action == 'Power off':
555 remctl('destroy', machine.name)
556 elif action == 'Shutdown':
557 remctl('shutdown', machine.name)
558 elif action == 'Delete VM':
560 print >> sys.stderr, time.time()-start_time
565 return Template(file="command.tmpl", searchList=[d, global_dict])
567 def testOwner(user, owner, machine=None):
568 if not getafsgroups.checkLockerOwner(user.username, owner):
569 raise InvalidInput('owner', owner,
573 def testContact(user, contact, machine=None):
574 if contact != user.email:
575 raise InvalidInput('contact', contact,
579 def testDisk(user, disksize, machine=None):
582 def testName(user, name, machine=None):
583 if Machine.select_by(name=name) == []:
585 if name == machine.name:
587 raise InvalidInput('name', name,
590 def testHostname(user, hostname, machine):
591 for nic in machine.nics:
592 if hostname == nic.hostname:
594 # check if doesn't already exist
595 if NIC.select_by(hostname=hostname) == []:
597 raise InvalidInput('hostname', hostname,
598 "Different from before")
601 def modify(user, fields):
602 """Handler for modifying attributes of a machine."""
605 transaction = ctx.current.create_transaction()
607 machine = testMachineId(user, fields.getfirst('machine_id'))
608 owner = testOwner(user, fields.getfirst('owner'), machine)
609 contact = testContact(user, fields.getfirst('contact'))
610 hostname = testHostname(owner, fields.getfirst('hostname'),
612 name = testName(user, fields.getfirst('name'), machine)
613 oldname = machine.name
617 memory = fields.getfirst('memory')
618 if memory is not None:
619 memory = validMemory(user, memory, machine)
621 memory = machine.memory
622 if memory != machine.memory:
623 machine.memory = memory
625 disksize = testDisk(user, fields.getfirst('disk'))
626 if disksize is not None:
627 disksize = validDisk(user, disksize, machine)
629 disksize = machine.disks[0].size
630 for disk in machine.disks:
631 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.""",
677 owner="""The Owner must be the name of a locker that you are an AFS
678 administrator of. In particular, you or an AFS group you are a member
679 of must have AFS rlidwka bits on the locker. You can check see who
680 administers the LOCKER locker using the command 'fs la /mit/LOCKER' on
688 return Template(file="help.tmpl", searchList=[d, global_dict])
691 def info(user, fields):
692 """Handler for info on a single VM."""
693 machine = testMachineId(user, fields.getfirst('machine_id'))
694 status = statusInfo(machine)
695 has_vnc = hasVnc(status)
697 main_status = dict(name=machine.name,
698 memory=str(machine.memory))
702 main_status = dict(status[1:])
703 start_time = float(main_status.get('start_time', 0))
704 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
705 cpu_time_float = float(main_status.get('cpu_time', 0))
706 cputime = datetime.timedelta(seconds=int(cpu_time_float))
707 display_fields = """name uptime memory state cpu_weight on_reboot
708 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
709 display_fields = [('name', 'Name'),
711 ('contact', 'Contact'),
714 ('uptime', 'uptime'),
715 ('cputime', 'CPU usage'),
718 ('state', 'state (xen format)'),
719 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
720 ('on_reboot', 'Action on VM reboot'),
721 ('on_poweroff', 'Action on VM poweroff'),
722 ('on_crash', 'Action on VM crash'),
723 ('on_xend_start', 'Action on Xen start'),
724 ('on_xend_stop', 'Action on Xen stop'),
725 ('bootloader', 'Bootloader options'),
729 machine_info['name'] = machine.name
730 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
731 machine_info['owner'] = machine.owner
732 machine_info['contact'] = machine.contact
734 nic_fields = getNicInfo(machine_info, machine)
735 nic_point = display_fields.index('NIC_INFO')
736 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
738 disk_fields = getDiskInfo(machine_info, machine)
739 disk_point = display_fields.index('DISK_INFO')
740 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
742 main_status['memory'] += ' MB'
743 for field, disp in display_fields:
744 if field in ('uptime', 'cputime') and locals()[field] is not None:
745 fields.append((disp, locals()[field]))
746 elif field in machine_info:
747 fields.append((disp, machine_info[field]))
748 elif field in main_status:
749 fields.append((disp, main_status[field]))
752 #fields.append((disp, None))
753 max_mem = maxMemory(user, machine)
754 max_disk = maxDisk(user, machine)
756 cdroms=CDROM.select(),
757 on=status is not None,
764 owner_help=helppopup("owner"),
766 return Template(file='info.tmpl',
767 searchList=[d, global_dict])
769 mapping = dict(list=listVms,
777 if __name__ == '__main__':
778 start_time = time.time()
779 fields = cgi.FieldStorage()
782 email = 'moo@cow.com'
785 if 'SSL_CLIENT_S_DN_Email' in os.environ:
786 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
787 u.username = username
788 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
792 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
793 operation = os.environ.get('PATH_INFO', '')
794 # print 'Content-Type: text/plain\n'
797 print "Status: 301 Moved Permanently"
798 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
801 if operation.startswith('/'):
802 operation = operation[1:]
806 def badOperation(u, e):
807 raise CodeError("Unknown operation")
809 fun = mapping.get(operation, badOperation)
810 if fun not in (help, ):
811 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
813 output = fun(u, fields)
814 print 'Content-Type: text/html\n'
816 e = sys.stderr.read()
819 output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
821 except CodeError, err:
822 print 'Content-Type: text/html\n'
824 e = sys.stderr.read()
825 sys.stderr=sys.stdout
826 print error(operation, u, fields, err, e)
827 except InvalidInput, err:
828 print 'Content-Type: text/html\n'
830 e = sys.stderr.read()
831 sys.stderr=sys.stdout
832 print invalidInput(operation, u, fields, err, e)
834 print 'Content-Type: text/plain\n'
836 e = sys.stderr.read()
839 sys.stderr = sys.stdout