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(self.machines)
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):
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 transaction = ctx.current.create_transaction()
514 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
516 for nic in machine.nics:
517 nic.machine_id = None
519 ctx.current.save(nic)
520 for disk in machine.disks:
521 ctx.current.delete(disk)
522 ctx.current.delete(machine)
525 transaction.rollback()
527 for mname, dname in delete_disk_pairs:
528 remctl('web', 'lvremove', mname, dname)
529 unregisterMachine(machine)
531 def command(user, fields):
532 """Handler for running commands like boot and delete on a VM."""
533 print time.time()-start_time
534 machine = testMachineId(user, fields.getfirst('machine_id'))
535 action = fields.getfirst('action')
536 cdrom = fields.getfirst('cdrom')
537 print time.time()-start_time
538 if cdrom is not None and not CDROM.get(cdrom):
539 raise CodeError("Invalid cdrom type '%s'" % cdrom)
540 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
541 raise CodeError("Invalid action '%s'" % action)
542 if action == 'Reboot':
543 if cdrom is not None:
544 remctl('reboot', machine.name, cdrom)
546 remctl('reboot', machine.name)
547 elif action == 'Power on':
548 if maxMemory(user) < machine.memory:
549 raise InvalidInput('action', 'Power on',
550 "You don't have enough free RAM quota to turn on this machine")
551 bootMachine(machine, cdrom)
552 elif action == 'Power off':
553 remctl('destroy', machine.name)
554 elif action == 'Shutdown':
555 remctl('shutdown', machine.name)
556 elif action == 'Delete VM':
558 print time.time()-start_time
563 return Template(file="command.tmpl", searchList=[d, global_dict])
565 def testOwner(user, owner, machine=None):
566 if owner != user.username:
567 raise InvalidInput('owner', owner,
571 def testContact(user, contact, machine=None):
572 if contact != user.email:
573 raise InvalidInput('contact', contact,
577 def testHostname(user, hostname, machine):
578 for nic in machine.nics:
579 if hostname == nic.hostname:
581 raise InvalidInput('hostname', hostname,
582 "Different from before")
585 def modify(user, fields):
586 """Handler for modifying attributes of a machine."""
588 machine = testMachineId(user, fields.getfirst('machine_id'))
589 owner = testOwner(user, fields.getfirst('owner'), machine)
590 contact = testContact(user, fields.getfirst('contact'))
591 hostname = testHostname(user, fields.getfirst('hostname'),
593 ram = fields.getfirst('memory')
595 ram = validMemory(user, ram, machine)
596 disk = testDisk(user, fields.getfirst('disk'))
598 disk = validDisk(user, disk, machine)
602 def help(user, fields):
603 """Handler for help messages."""
604 simple = fields.getfirst('simple')
605 subjects = fields.getlist('subject')
607 mapping = dict(paravm_console="""
608 ParaVM machines do not support console access over VNC. To access
609 these machines, you either need to boot with a liveCD and ssh in or
610 hope that the sipb-xen maintainers add support for serial consoles.""",
612 HVM machines use the virtualization features of the processor, while
613 ParaVM machines use Xen's emulation of virtualization features. You
614 want an HVM virtualized machine.""",
615 cpu_weight="""Don't ask us! We're as mystified as you are.""")
622 return Template(file="help.tmpl", searchList=[d, global_dict])
625 def info(user, fields):
626 """Handler for info on a single VM."""
627 machine = testMachineId(user, fields.getfirst('machine_id'))
628 status = statusInfo(machine)
629 has_vnc = hasVnc(status)
631 main_status = dict(name=machine.name,
632 memory=str(machine.memory))
634 main_status = dict(status[1:])
635 start_time = float(main_status.get('start_time', 0))
636 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
637 cpu_time_float = float(main_status.get('cpu_time', 0))
638 cputime = datetime.timedelta(seconds=int(cpu_time_float))
639 display_fields = """name uptime memory state cpu_weight on_reboot
640 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
641 display_fields = [('name', 'Name'),
643 ('contact', 'Contact'),
646 ('uptime', 'uptime'),
647 ('cputime', 'CPU usage'),
650 ('state', 'state (xen format)'),
651 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
652 ('on_reboot', 'Action on VM reboot'),
653 ('on_poweroff', 'Action on VM poweroff'),
654 ('on_crash', 'Action on VM crash'),
655 ('on_xend_start', 'Action on Xen start'),
656 ('on_xend_stop', 'Action on Xen stop'),
657 ('bootloader', 'Bootloader options'),
661 machine_info['name'] = machine.name
662 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
663 machine_info['owner'] = machine.owner
664 machine_info['contact'] = machine.contact
666 nic_fields = getNicInfo(machine_info, machine)
667 nic_point = display_fields.index('NIC_INFO')
668 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
670 disk_fields = getDiskInfo(machine_info, machine)
671 disk_point = display_fields.index('DISK_INFO')
672 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
674 main_status['memory'] += ' MB'
675 for field, disp in display_fields:
676 if field in ('uptime', 'cputime'):
677 fields.append((disp, locals()[field]))
678 elif field in machine_info:
679 fields.append((disp, machine_info[field]))
680 elif field in main_status:
681 fields.append((disp, main_status[field]))
684 #fields.append((disp, None))
685 max_mem = maxMemory(user, machine)
686 max_disk = maxDisk(user, machine)
688 cdroms=CDROM.select(),
689 on=status is not None,
697 return Template(file='info.tmpl',
698 searchList=[d, global_dict])
700 mapping = dict(list=listVms,
708 if __name__ == '__main__':
709 start_time = time.time()
710 fields = cgi.FieldStorage()
713 email = 'moo@cow.com'
716 if 'SSL_CLIENT_S_DN_Email' in os.environ:
717 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
718 u.username = username
719 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
723 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
724 operation = os.environ.get('PATH_INFO', '')
725 #print 'Content-Type: text/plain\n'
728 print "Status: 301 Moved Permanently"
729 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
732 if operation.startswith('/'):
733 operation = operation[1:]
737 def badOperation(u, e):
738 raise CodeError("Unknown operation")
740 fun = mapping.get(operation, badOperation)
741 if fun not in (help, ):
742 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
744 output = fun(u, fields)
745 print 'Content-Type: text/html\n'
747 e = sys.stderr.read()
749 output = output.replace('<body>', '<body><pre>'+e+'</pre>')
751 except CodeError, err:
752 print 'Content-Type: text/html\n'
754 e = sys.stderr.read()
755 print error(operation, u, fields, err, e)
756 except InvalidInput, err:
757 print 'Content-Type: text/html\n'
759 e = sys.stderr.read()
760 print invalidInput(operation, u, fields, err, e)
762 print 'Content-Type: text/plain\n'
764 e = sys.stderr.read()