17 sys.stderr = StringIO.StringIO()
18 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
20 from Cheetah.Template import Template
21 from sipb_xen_database import *
24 class MyException(Exception):
25 """Base class for my exceptions"""
28 class InvalidInput(MyException):
29 """Exception for user-provided input is invalid but maybe in good faith.
31 This would include setting memory to negative (which might be a
32 typo) but not setting an invalid boot CD (which requires bypassing
35 def __init__(self, err_field, err_value, expl=None):
36 super(InvalidInput, self).__init__(expl)
37 self.err_field = err_field
38 self.err_value = err_value
40 class CodeError(MyException):
41 """Exception for internal errors or bad faith input."""
45 def __init__(self, user):
48 def __get_uptimes(self):
49 if not hasattr(self, '_uptimes'):
50 self._uptimes = getUptimes(Machine.select())
52 uptimes = property(__get_uptimes)
57 """Return HTML code for a (?) link to a specified help topic"""
58 return '<span class="helplink"><a href="help?subject='+subj+'&simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
62 global_dict['helppopup'] = helppopup
65 # ... and stolen from xend/uuid.py
67 """Generate a random UUID."""
69 return [ random.randint(0, 255) for _ in range(0, 16) ]
72 """Turn a numeric UUID to a hyphen-seperated one."""
73 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
74 "%02x" * 6]) % tuple(u)
76 MAX_MEMORY_TOTAL = 512
77 MAX_MEMORY_SINGLE = 256
78 MIN_MEMORY_SINGLE = 16
85 def getMachinesByOwner(owner):
86 """Return the machines owned by a given owner."""
87 return Machine.select_by(owner=owner)
89 def maxMemory(user, machine=None):
90 """Return the maximum memory for a machine or a user.
92 If machine is None, return the memory available for a new
93 machine. Else, return the maximum that machine can have.
95 on is a dictionary from machines to booleans, whether a machine is
96 on. If None, it is recomputed. XXX make this global?
99 machines = getMachinesByOwner(user.username)
100 active_machines = [x for x in machines if g.uptimes[x]]
101 mem_usage = sum([x.memory for x in active_machines if x != machine])
102 return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
104 def maxDisk(user, machine=None):
105 machines = getMachinesByOwner(user.username)
106 disk_usage = sum([sum([y.size for y in x.disks])
107 for x in machines if x != machine])
108 return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
111 machines = getMachinesByOwner(user.username)
112 active_machines = [x for x in machines if g.uptimes[x]]
113 return (len(machines) < MAX_VMS_TOTAL and
114 len(active_machines) < MAX_VMS_ACTIVE)
116 def haveAccess(user, machine):
117 """Return whether a user has access to a machine"""
118 if user.username == 'moo':
120 return machine.owner == user.username
122 def error(op, user, fields, err, emsg):
123 """Print an error page when a CodeError occurs"""
124 d = dict(op=op, user=user, errorMessage=str(err),
126 return Template(file='error.tmpl', searchList=[d, global_dict]);
128 def invalidInput(op, user, fields, err, emsg):
129 """Print an error page when an InvalidInput exception occurs"""
130 d = dict(op=op, user=user, err_field=err.err_field,
131 err_value=str(err.err_value), stderr=emsg,
132 errorMessage=str(err))
133 return Template(file='invalid.tmpl', searchList=[d, global_dict]);
135 def validMachineName(name):
136 """Check that name is valid for a machine name"""
139 charset = string.ascii_letters + string.digits + '-_'
140 if name[0] in '-_' or len(name) > 22:
147 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
148 """Kinit with a given username and keytab"""
150 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
151 stderr=subprocess.PIPE)
154 raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
157 """If we lack tickets, kinit."""
158 p = subprocess.Popen(['klist', '-s'])
162 def remctl(*args, **kws):
163 """Perform a remctl and return the output.
165 kinits if necessary, and outputs errors to stderr.
168 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
170 stdout=subprocess.PIPE,
171 stderr=subprocess.PIPE)
174 return p.stdout.read(), p.stderr.read()
176 raise CodeError('ERROR on remctl %s: %s' %
177 (args, p.stderr.read()))
178 return p.stdout.read()
180 def lvcreate(machine, disk):
181 """Create a single disk for a machine"""
182 remctl('web', 'lvcreate', machine.name,
183 disk.guest_device_name, str(disk.size))
185 def makeDisks(machine):
186 """Update the lvm partitions to add a disk."""
187 for disk in machine.disks:
188 lvcreate(machine, disk)
190 def bootMachine(machine, cdtype):
191 """Boot a machine with a given boot CD.
193 If cdtype is None, give no boot cd. Otherwise, it is the string
194 id of the CD (e.g. 'gutsy_i386')
196 if cdtype is not None:
197 remctl('web', 'vmboot', machine.name,
200 remctl('web', 'vmboot', machine.name)
202 def registerMachine(machine):
203 """Register a machine to be controlled by the web interface"""
204 remctl('web', 'register', machine.name)
206 def unregisterMachine(machine):
207 """Unregister a machine to not be controlled by the web interface"""
208 remctl('web', 'unregister', machine.name)
211 """Parse a status string into nested tuples of strings.
213 s = output of xm list --long <machine_name>
215 values = re.split('([()])', s)
217 for v in values[2:-2]: #remove initial and final '()'
224 if len(stack[-1]) == 1:
226 stack[-2].append(stack[-1])
231 stack[-1].extend(v.split())
234 def getUptimes(machines=None):
235 """Return a dictionary mapping machine names to uptime strings"""
236 value_string = remctl('web', 'listvms')
237 lines = value_string.splitlines()
242 uptime = ' '.join(lst[2:])
246 ans[m] = d.get(m.name)
249 def statusInfo(machine):
250 """Return the status list for a given machine.
252 Gets and parses xm list --long
254 value_string, err_string = remctl('list-long', machine.name, err=True)
255 if 'Unknown command' in err_string:
256 raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
257 elif 'does not exist' in err_string:
260 raise CodeError("ERROR in remctl list-long %s: %s" % (machine.name, err_string))
261 status = parseStatus(value_string)
265 """Does the machine with a given status list support VNC?"""
269 if l[0] == 'device' and l[1][0] == 'vfb':
271 return 'location' in d
274 def createVm(user, name, memory, disk, is_hvm, cdrom):
275 """Create a VM and put it in the database"""
276 # put stuff in the table
277 transaction = ctx.current.create_transaction()
279 if memory > maxMemory(user):
280 raise InvalidInput('memory', memory,
281 "Max %s" % maxMemory(user))
282 if disk > maxDisk(user) * 1024:
283 raise InvalidInput('disk', disk,
284 "Max %s" % maxDisk(user))
285 if not canAddVm(user):
286 raise InvalidInput('create', True, 'Unable to create more VMs')
287 res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
288 id = res.fetchone()[0]
290 machine.machine_id = id
292 machine.memory = memory
293 machine.owner = user.username
294 machine.contact = user.email
295 machine.uuid = uuidToString(randomUUID())
296 machine.boot_off_cd = True
297 machine_type = Type.get_by(hvm=is_hvm)
298 machine.type_id = machine_type.type_id
299 ctx.current.save(machine)
300 disk = Disk(machine.machine_id,
302 open = NIC.select_by(machine_id=None)
303 if not open: #No IPs left!
304 raise CodeError("No IP addresses left! Contact sipb-xen-dev@mit.edu")
306 nic.machine_id = machine.machine_id
308 ctx.current.save(nic)
309 ctx.current.save(disk)
312 transaction.rollback()
314 registerMachine(machine)
316 # tell it to boot with cdrom
317 bootMachine(machine, cdrom)
321 def validMemory(user, memory, machine=None):
322 """Parse and validate limits for memory for a given user and machine."""
325 if memory < MIN_MEMORY_SINGLE:
328 raise InvalidInput('memory', memory,
329 "Minimum %s MB" % MIN_MEMORY_SINGLE)
330 if memory > maxMemory(user, machine):
331 raise InvalidInput('memory', memory,
332 'Maximum %s MB' % maxMemory(user, machine))
335 def validDisk(user, disk, machine=None):
336 """Parse and validate limits for disk for a given user and machine."""
339 if disk > maxDisk(user, machine):
340 raise InvalidInput('disk', disk,
341 "Maximum %s G" % maxDisk(user, machine))
342 disk = int(disk * 1024)
343 if disk < MIN_DISK_SINGLE * 1024:
346 raise InvalidInput('disk', disk,
347 "Minimum %s GB" % MIN_DISK_SINGLE)
350 def create(user, fields):
351 """Handler for create requests."""
352 name = fields.getfirst('name')
353 if not validMachineName(name):
354 raise InvalidInput('name', name)
355 name = user.username + '_' + name.lower()
357 if Machine.get_by(name=name):
358 raise InvalidInput('name', name,
361 memory = fields.getfirst('memory')
362 memory = validMemory(user, memory)
364 disk = fields.getfirst('disk')
365 disk = validDisk(user, disk)
367 vm_type = fields.getfirst('vmtype')
368 if vm_type not in ('hvm', 'paravm'):
369 raise CodeError("Invalid vm type '%s'" % vm_type)
370 is_hvm = (vm_type == 'hvm')
372 cdrom = fields.getfirst('cdrom')
373 if cdrom is not None and not CDROM.get(cdrom):
374 raise CodeError("Invalid cdrom type '%s'" % cdrom)
376 machine = createVm(user, name, memory, disk, is_hvm, cdrom)
379 return Template(file='create.tmpl',
380 searchList=[d, global_dict]);
382 def listVms(user, fields):
383 """Handler for list requests."""
384 machines = [m for m in Machine.select() if haveAccess(user, m)]
394 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
396 # status = statusInfo(m)
397 # on[m.name] = status is not None
398 # has_vnc[m.name] = hasVnc(status)
399 max_mem=maxMemory(user)
400 max_disk=maxDisk(user)
402 can_add_vm=canAddVm(user),
406 default_disk=min(4.0, max_disk),
410 cdroms=CDROM.select())
411 return Template(file='list.tmpl', searchList=[d, global_dict])
413 def testMachineId(user, machineId, exists=True):
414 """Parse, validate and check authorization for a given machineId.
416 If exists is False, don't check that it exists.
418 if machineId is None:
419 raise CodeError("No machine ID specified")
421 machineId = int(machineId)
423 raise CodeError("Invalid machine ID '%s'" % machineId)
424 machine = Machine.get(machineId)
425 if exists and machine is None:
426 raise CodeError("No such machine ID '%s'" % machineId)
427 if machine is not None and not haveAccess(user, machine):
428 raise CodeError("No access to machine ID '%s'" % machineId)
431 def vnc(user, fields):
434 Note that due to same-domain restrictions, the applet connects to
435 the webserver, which needs to forward those requests to the xen
436 server. The Xen server runs another proxy that (1) authenticates
437 and (2) finds the correct port for the VM.
439 You might want iptables like:
441 -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
442 -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
443 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
445 Remember to enable iptables!
446 echo 1 > /proc/sys/net/ipv4/ip_forward
448 machine = testMachineId(user, fields.getfirst('machine_id'))
450 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
453 data["user"] = user.username
454 data["machine"]=machine.name
455 data["expires"]=time.time()+(5*60)
456 pickledData = cPickle.dumps(data)
457 m = hmac.new(TOKEN_KEY, digestmod=sha)
458 m.update(pickledData)
459 token = {'data': pickledData, 'digest': m.digest()}
460 token = cPickle.dumps(token)
461 token = base64.urlsafe_b64encode(token)
463 status = statusInfo(machine)
464 has_vnc = hasVnc(status)
470 hostname=os.environ.get('SERVER_NAME', 'localhost'),
472 return Template(file='vnc.tmpl',
473 searchList=[d, global_dict])
475 def getNicInfo(data_dict, machine):
476 """Helper function for info, get data on nics for a machine.
478 Modifies data_dict to include the relevant data, and returns a list
479 of (key, name) pairs to display "name: data_dict[key]" to the user.
481 data_dict['num_nics'] = len(machine.nics)
482 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
483 ('nic%s_mac', 'NIC %s MAC Addr'),
484 ('nic%s_ip', 'NIC %s IP'),
487 for i in range(len(machine.nics)):
488 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
489 data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
490 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
491 data_dict['nic%s_ip' % i] = machine.nics[i].ip
492 if len(machine.nics) == 1:
493 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
496 def getDiskInfo(data_dict, machine):
497 """Helper function for info, get data on disks for a machine.
499 Modifies data_dict to include the relevant data, and returns a list
500 of (key, name) pairs to display "name: data_dict[key]" to the user.
502 data_dict['num_disks'] = len(machine.disks)
503 disk_fields_template = [('%s_size', '%s size')]
505 for disk in machine.disks:
506 name = disk.guest_device_name
507 disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
508 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
511 def deleteVM(machine):
513 remctl('destroy', machine.name)
514 transaction = ctx.current.create_transaction()
515 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
517 for nic in machine.nics:
518 nic.machine_id = None
520 ctx.current.save(nic)
521 for disk in machine.disks:
522 ctx.current.delete(disk)
523 ctx.current.delete(machine)
526 transaction.rollback()
528 for mname, dname in delete_disk_pairs:
529 remctl('web', 'lvremove', mname, dname)
530 unregisterMachine(machine)
532 def command(user, fields):
533 """Handler for running commands like boot and delete on a VM."""
534 print >> sys.stderr, time.time()-start_time
535 machine = testMachineId(user, fields.getfirst('machine_id'))
536 action = fields.getfirst('action')
537 cdrom = fields.getfirst('cdrom')
538 print >> sys.stderr, time.time()-start_time
539 if cdrom is not None and not CDROM.get(cdrom):
540 raise CodeError("Invalid cdrom type '%s'" % cdrom)
541 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
542 raise CodeError("Invalid action '%s'" % action)
543 if action == 'Reboot':
544 if cdrom is not None:
545 remctl('reboot', machine.name, cdrom)
547 remctl('reboot', machine.name)
548 elif action == 'Power on':
549 if maxMemory(user) < machine.memory:
550 raise InvalidInput('action', 'Power on',
551 "You don't have enough free RAM quota to turn on this machine")
552 bootMachine(machine, cdrom)
553 elif action == 'Power off':
554 remctl('destroy', machine.name)
555 elif action == 'Shutdown':
556 remctl('shutdown', machine.name)
557 elif action == 'Delete VM':
559 print >> sys.stderr, time.time()-start_time
564 return Template(file="command.tmpl", searchList=[d, global_dict])
566 def testOwner(user, owner, machine=None):
567 if owner != user.username:
568 raise InvalidInput('owner', owner,
572 def testContact(user, contact, machine=None):
573 if contact != user.email:
574 raise InvalidInput('contact', contact,
578 def testHostname(user, hostname, machine):
579 for nic in machine.nics:
580 if hostname == nic.hostname:
582 raise InvalidInput('hostname', hostname,
583 "Different from before")
586 def modify(user, fields):
587 """Handler for modifying attributes of a machine."""
589 machine = testMachineId(user, fields.getfirst('machine_id'))
590 owner = testOwner(user, fields.getfirst('owner'), machine)
591 contact = testContact(user, fields.getfirst('contact'))
592 hostname = testHostname(user, fields.getfirst('hostname'),
594 ram = fields.getfirst('memory')
596 ram = validMemory(user, ram, machine)
597 disk = testDisk(user, fields.getfirst('disk'))
599 disk = validDisk(user, disk, machine)
603 def help(user, fields):
604 """Handler for help messages."""
605 simple = fields.getfirst('simple')
606 subjects = fields.getlist('subject')
608 mapping = dict(paravm_console="""
609 ParaVM machines do not support console access over VNC. To access
610 these machines, you either need to boot with a liveCD and ssh in or
611 hope that the sipb-xen maintainers add support for serial consoles.""",
613 HVM machines use the virtualization features of the processor, while
614 ParaVM machines use Xen's emulation of virtualization features. You
615 want an HVM virtualized machine.""",
616 cpu_weight="""Don't ask us! We're as mystified as you are.""")
623 return Template(file="help.tmpl", searchList=[d, global_dict])
626 def info(user, fields):
627 """Handler for info on a single VM."""
628 machine = testMachineId(user, fields.getfirst('machine_id'))
629 status = statusInfo(machine)
630 has_vnc = hasVnc(status)
632 main_status = dict(name=machine.name,
633 memory=str(machine.memory))
635 main_status = dict(status[1:])
636 start_time = float(main_status.get('start_time', 0))
637 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
638 cpu_time_float = float(main_status.get('cpu_time', 0))
639 cputime = datetime.timedelta(seconds=int(cpu_time_float))
640 display_fields = """name uptime memory state cpu_weight on_reboot
641 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
642 display_fields = [('name', 'Name'),
644 ('contact', 'Contact'),
647 ('uptime', 'uptime'),
648 ('cputime', 'CPU usage'),
651 ('state', 'state (xen format)'),
652 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
653 ('on_reboot', 'Action on VM reboot'),
654 ('on_poweroff', 'Action on VM poweroff'),
655 ('on_crash', 'Action on VM crash'),
656 ('on_xend_start', 'Action on Xen start'),
657 ('on_xend_stop', 'Action on Xen stop'),
658 ('bootloader', 'Bootloader options'),
662 machine_info['name'] = machine.name
663 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
664 machine_info['owner'] = machine.owner
665 machine_info['contact'] = machine.contact
667 nic_fields = getNicInfo(machine_info, machine)
668 nic_point = display_fields.index('NIC_INFO')
669 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
671 disk_fields = getDiskInfo(machine_info, machine)
672 disk_point = display_fields.index('DISK_INFO')
673 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
675 main_status['memory'] += ' MB'
676 for field, disp in display_fields:
677 if field in ('uptime', 'cputime'):
678 fields.append((disp, locals()[field]))
679 elif field in machine_info:
680 fields.append((disp, machine_info[field]))
681 elif field in main_status:
682 fields.append((disp, main_status[field]))
685 #fields.append((disp, None))
686 max_mem = maxMemory(user, machine)
687 max_disk = maxDisk(user, machine)
689 cdroms=CDROM.select(),
690 on=status is not None,
698 return Template(file='info.tmpl',
699 searchList=[d, global_dict])
701 mapping = dict(list=listVms,
709 if __name__ == '__main__':
710 start_time = time.time()
711 fields = cgi.FieldStorage()
714 email = 'moo@cow.com'
717 if 'SSL_CLIENT_S_DN_Email' in os.environ:
718 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
719 u.username = username
720 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
724 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
725 operation = os.environ.get('PATH_INFO', '')
726 # print 'Content-Type: text/plain\n'
729 print "Status: 301 Moved Permanently"
730 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
733 if operation.startswith('/'):
734 operation = operation[1:]
738 def badOperation(u, e):
739 raise CodeError("Unknown operation")
741 fun = mapping.get(operation, badOperation)
742 if fun not in (help, ):
743 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
745 output = fun(u, fields)
746 print 'Content-Type: text/html\n'
748 e = sys.stderr.read()
751 output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
753 except CodeError, err:
754 print 'Content-Type: text/html\n'
756 e = sys.stderr.read()
757 sys.stderr=sys.stdout
758 print error(operation, u, fields, err, e)
759 except InvalidInput, err:
760 print 'Content-Type: text/html\n'
762 e = sys.stderr.read()
763 sys.stderr=sys.stdout
764 print invalidInput(operation, u, fields, err, e)
766 print 'Content-Type: text/plain\n'
768 e = sys.stderr.read()
771 sys.stderr = sys.stdout