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):
514 remctl('destroy', machine.name)
517 transaction = ctx.current.create_transaction()
518 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
520 for nic in machine.nics:
521 nic.machine_id = None
523 ctx.current.save(nic)
524 for disk in machine.disks:
525 ctx.current.delete(disk)
526 ctx.current.delete(machine)
529 transaction.rollback()
531 for mname, dname in delete_disk_pairs:
532 remctl('web', 'lvremove', mname, dname)
533 unregisterMachine(machine)
535 def command(user, fields):
536 """Handler for running commands like boot and delete on a VM."""
537 print >> sys.stderr, time.time()-start_time
538 machine = testMachineId(user, fields.getfirst('machine_id'))
539 action = fields.getfirst('action')
540 cdrom = fields.getfirst('cdrom')
541 print >> sys.stderr, time.time()-start_time
542 if cdrom is not None and not CDROM.get(cdrom):
543 raise CodeError("Invalid cdrom type '%s'" % cdrom)
544 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
545 raise CodeError("Invalid action '%s'" % action)
546 if action == 'Reboot':
547 if cdrom is not None:
548 remctl('reboot', machine.name, cdrom)
550 remctl('reboot', machine.name)
551 elif action == 'Power on':
552 if maxMemory(user) < machine.memory:
553 raise InvalidInput('action', 'Power on',
554 "You don't have enough free RAM quota to turn on this machine")
555 bootMachine(machine, cdrom)
556 elif action == 'Power off':
557 remctl('destroy', machine.name)
558 elif action == 'Shutdown':
559 remctl('shutdown', machine.name)
560 elif action == 'Delete VM':
562 print >> sys.stderr, time.time()-start_time
567 return Template(file="command.tmpl", searchList=[d, global_dict])
569 def testOwner(user, owner, machine=None):
570 if owner != user.username:
571 raise InvalidInput('owner', owner,
575 def testContact(user, contact, machine=None):
576 if contact != user.email:
577 raise InvalidInput('contact', contact,
581 def testHostname(user, hostname, machine):
582 for nic in machine.nics:
583 if hostname == nic.hostname:
585 raise InvalidInput('hostname', hostname,
586 "Different from before")
589 def modify(user, fields):
590 """Handler for modifying attributes of a machine."""
592 machine = testMachineId(user, fields.getfirst('machine_id'))
593 owner = testOwner(user, fields.getfirst('owner'), machine)
594 contact = testContact(user, fields.getfirst('contact'))
595 hostname = testHostname(user, fields.getfirst('hostname'),
597 ram = fields.getfirst('memory')
599 ram = validMemory(user, ram, machine)
600 disk = testDisk(user, fields.getfirst('disk'))
602 disk = validDisk(user, disk, machine)
606 def help(user, fields):
607 """Handler for help messages."""
608 simple = fields.getfirst('simple')
609 subjects = fields.getlist('subject')
611 mapping = dict(paravm_console="""
612 ParaVM machines do not support console access over VNC. To access
613 these machines, you either need to boot with a liveCD and ssh in or
614 hope that the sipb-xen maintainers add support for serial consoles.""",
616 HVM machines use the virtualization features of the processor, while
617 ParaVM machines use Xen's emulation of virtualization features. You
618 want an HVM virtualized machine.""",
619 cpu_weight="""Don't ask us! We're as mystified as you are.""")
626 return Template(file="help.tmpl", searchList=[d, global_dict])
629 def info(user, fields):
630 """Handler for info on a single VM."""
631 machine = testMachineId(user, fields.getfirst('machine_id'))
632 status = statusInfo(machine)
633 has_vnc = hasVnc(status)
635 main_status = dict(name=machine.name,
636 memory=str(machine.memory))
638 main_status = dict(status[1:])
639 start_time = float(main_status.get('start_time', 0))
640 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
641 cpu_time_float = float(main_status.get('cpu_time', 0))
642 cputime = datetime.timedelta(seconds=int(cpu_time_float))
643 display_fields = """name uptime memory state cpu_weight on_reboot
644 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
645 display_fields = [('name', 'Name'),
647 ('contact', 'Contact'),
650 ('uptime', 'uptime'),
651 ('cputime', 'CPU usage'),
654 ('state', 'state (xen format)'),
655 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
656 ('on_reboot', 'Action on VM reboot'),
657 ('on_poweroff', 'Action on VM poweroff'),
658 ('on_crash', 'Action on VM crash'),
659 ('on_xend_start', 'Action on Xen start'),
660 ('on_xend_stop', 'Action on Xen stop'),
661 ('bootloader', 'Bootloader options'),
665 machine_info['name'] = machine.name
666 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
667 machine_info['owner'] = machine.owner
668 machine_info['contact'] = machine.contact
670 nic_fields = getNicInfo(machine_info, machine)
671 nic_point = display_fields.index('NIC_INFO')
672 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
674 disk_fields = getDiskInfo(machine_info, machine)
675 disk_point = display_fields.index('DISK_INFO')
676 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
678 main_status['memory'] += ' MB'
679 for field, disp in display_fields:
680 if field in ('uptime', 'cputime'):
681 fields.append((disp, locals()[field]))
682 elif field in machine_info:
683 fields.append((disp, machine_info[field]))
684 elif field in main_status:
685 fields.append((disp, main_status[field]))
688 #fields.append((disp, None))
689 max_mem = maxMemory(user, machine)
690 max_disk = maxDisk(user, machine)
692 cdroms=CDROM.select(),
693 on=status is not None,
701 return Template(file='info.tmpl',
702 searchList=[d, global_dict])
704 mapping = dict(list=listVms,
712 if __name__ == '__main__':
713 start_time = time.time()
714 fields = cgi.FieldStorage()
717 email = 'moo@cow.com'
720 if 'SSL_CLIENT_S_DN_Email' in os.environ:
721 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
722 u.username = username
723 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
727 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
728 operation = os.environ.get('PATH_INFO', '')
729 # print 'Content-Type: text/plain\n'
732 print "Status: 301 Moved Permanently"
733 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
736 if operation.startswith('/'):
737 operation = operation[1:]
741 def badOperation(u, e):
742 raise CodeError("Unknown operation")
744 fun = mapping.get(operation, badOperation)
745 if fun not in (help, ):
746 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
748 output = fun(u, fields)
749 print 'Content-Type: text/html\n'
751 e = sys.stderr.read()
754 output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
756 except CodeError, err:
757 print 'Content-Type: text/html\n'
759 e = sys.stderr.read()
760 sys.stderr=sys.stdout
761 print error(operation, u, fields, err, e)
762 except InvalidInput, err:
763 print 'Content-Type: text/html\n'
765 e = sys.stderr.read()
766 sys.stderr=sys.stdout
767 print invalidInput(operation, u, fields, err, e)
769 print 'Content-Type: text/plain\n'
771 e = sys.stderr.read()
774 sys.stderr = sys.stdout